fix(workbench): complete profile and channel flows

This commit is contained in:
2026-07-24 14:46:38 +08:00
parent d7eb20a85f
commit f430566976
21 changed files with 563 additions and 151 deletions

View File

@@ -1,9 +1,12 @@
package auth
import (
"errors"
"net/http"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
"senlinai-agent/backend/internal/models"
)
type Handler struct {
@@ -16,6 +19,8 @@ func NewHandler(service *Service) *Handler {
func (h *Handler) Register(router gin.IRouter) {
router.POST("/auth/login", h.login)
router.GET("/auth/me", h.me)
router.PATCH("/auth/me", h.updateMe)
}
func (h *Handler) login(c *gin.Context) {
@@ -32,5 +37,72 @@ func (h *Handler) login(c *gin.Context) {
abortWithError(c, http.StatusUnauthorized, "invalid_credentials", "邮箱或密码错误")
return
}
c.JSON(http.StatusOK, gin.H{"token": token})
userID, err := h.service.VerifySession(token)
if err != nil {
abortWithError(c, http.StatusInternalServerError, "internal_error", "登录状态创建失败")
return
}
user, err := h.service.CurrentUser(userID)
if err != nil {
abortWithError(c, http.StatusInternalServerError, "internal_error", "用户资料加载失败")
return
}
c.JSON(http.StatusOK, gin.H{"token": token, "user": currentUserDTO(*user)})
}
type CurrentUserDTO struct {
Email string `json:"email"`
DisplayName string `json:"displayName"`
}
func (h *Handler) me(c *gin.Context) {
userID, ok := CurrentUserID(c)
if !ok {
abortWithError(c, http.StatusUnauthorized, "unauthorized", "请先登录后再操作")
return
}
user, err := h.service.CurrentUser(userID)
if err != nil {
writeProfileError(c, err)
return
}
c.JSON(http.StatusOK, currentUserDTO(*user))
}
func (h *Handler) updateMe(c *gin.Context) {
userID, ok := CurrentUserID(c)
if !ok {
abortWithError(c, http.StatusUnauthorized, "unauthorized", "请先登录后再操作")
return
}
var input struct {
DisplayName string `json:"displayName"`
CurrentPassword string `json:"currentPassword"`
NewPassword string `json:"newPassword"`
}
if err := c.ShouldBindJSON(&input); err != nil {
abortWithError(c, http.StatusBadRequest, "invalid_request", "个人资料参数无效")
return
}
user, err := h.service.UpdateCurrentUser(userID, input.DisplayName, input.CurrentPassword, input.NewPassword)
if err != nil {
writeProfileError(c, err)
return
}
c.JSON(http.StatusOK, currentUserDTO(*user))
}
func currentUserDTO(user models.SaUser) CurrentUserDTO {
return CurrentUserDTO{Email: user.Email, DisplayName: user.DisplayName}
}
func writeProfileError(c *gin.Context, err error) {
switch {
case errors.Is(err, gorm.ErrRecordNotFound):
abortWithError(c, http.StatusNotFound, "not_found", "用户不存在")
case errors.Is(err, ErrProfileNameRequired), errors.Is(err, ErrPasswordTooShort), errors.Is(err, ErrCurrentPassword):
abortWithError(c, http.StatusBadRequest, "invalid_request", err.Error())
default:
abortWithError(c, http.StatusInternalServerError, "internal_error", "个人资料更新失败")
}
}

View File

@@ -21,6 +21,12 @@ type Service struct {
sessionTTL time.Duration
}
var (
ErrProfileNameRequired = errors.New("display name is required")
ErrCurrentPassword = errors.New("current password is incorrect")
ErrPasswordTooShort = errors.New("new password must be at least 8 characters")
)
func NewService(secret string) *Service {
return &Service{secret: secret, inviteTokenTTL: 7 * 24 * time.Hour, sessionTTL: 24 * time.Hour}
}
@@ -86,6 +92,46 @@ func (s *Service) VerifySession(token string) (uint, error) {
return userID, nil
}
// CurrentUser returns the authenticated user's profile without exposing password hashes.
func (s *Service) CurrentUser(userID uint) (*models.SaUser, error) {
var user models.SaUser
if err := models.DBService.Where("id = ?", userID).First(&user).Error; err != nil {
return nil, err
}
return &user, nil
}
// UpdateCurrentUser updates the small MVP profile surface. A password change always
// requires the current password so a stolen unlocked session cannot silently rotate it.
func (s *Service) UpdateCurrentUser(userID uint, displayName, currentPassword, newPassword string) (*models.SaUser, error) {
user, err := s.CurrentUser(userID)
if err != nil {
return nil, err
}
name := strings.TrimSpace(displayName)
if name == "" {
return nil, ErrProfileNameRequired
}
updates := map[string]any{"display_name": name}
if strings.TrimSpace(newPassword) != "" {
if len(newPassword) < 8 {
return nil, ErrPasswordTooShort
}
if bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(currentPassword)) != nil {
return nil, ErrCurrentPassword
}
hash, err := bcrypt.GenerateFromPassword([]byte(newPassword), bcrypt.DefaultCost)
if err != nil {
return nil, err
}
updates["password_hash"] = string(hash)
}
if err := models.DBService.Model(user).Updates(updates).Error; err != nil {
return nil, err
}
return s.CurrentUser(userID)
}
type tokenPayload struct {
Type string `json:"type"`
Subject string `json:"subject"`