package handler import ( "errors" "log" "net/http" "strings" "github.com/gin-gonic/gin" "github.com/sundynix/sundynix-gateway/internal/auth" "github.com/sundynix/sundynix-gateway/internal/store" ) func firstNonEmpty(a, b string) string { if a != "" { return a } return b } // userJSON 是对外的用户视图(绝不含密码哈希)。 func userJSON(u *store.User) gin.H { return gin.H{"id": u.ID, "email": u.Email, "name": u.Name} } // Register: POST /api/v1/auth/register {email, password, name} —— 注册并签发 JWT。 func (h *Handler) Register(c *gin.Context) { var body struct { Email string `json:"email"` Password string `json:"password"` Name string `json:"name"` } if err := c.ShouldBindJSON(&body); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } email := strings.TrimSpace(strings.ToLower(body.Email)) // 密码 8–72 位:8 起步(此前 6 太弱);上限 72 字节——bcrypt 超 72 字节静默截断, // 不拦会让"超长密码"实际只用前 72 字节,用户以为更安全其实不然。 if !strings.Contains(email, "@") || len(body.Password) < 8 || len(body.Password) > 72 { c.JSON(http.StatusBadRequest, gin.H{"error": "需合法邮箱,密码 8–72 位"}) return } hash, err := auth.HashPassword(body.Password) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "密码处理失败"}) return } u, err := h.db.CreateUser(c.Request.Context(), email, strings.TrimSpace(body.Name), hash) if err != nil { if errors.Is(err, store.ErrUserExists) { c.JSON(http.StatusConflict, gin.H{"error": "该邮箱已注册"}) return } c.JSON(http.StatusBadGateway, gin.H{"error": err.Error()}) return } // 多租户:每个新用户建一个单人默认租户(owner)。失败不阻断注册(中间件会兜底补建)。 if _, e := h.db.EnsureDefaultTenant(c.Request.Context(), u.ID, firstNonEmpty(strings.TrimSpace(body.Name), email)); e != nil { log.Printf("[auth] 建默认租户失败 uid=%s: %v", u.ID, e) } issueToken(c, u) } // Login: POST /api/v1/auth/login {email, password} —— 校验并签发 JWT。 func (h *Handler) Login(c *gin.Context) { var body struct { Email string `json:"email"` Password string `json:"password"` } if err := c.ShouldBindJSON(&body); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } email := strings.TrimSpace(strings.ToLower(body.Email)) ctx := c.Request.Context() // 账户级锁定:连续失败达阈值即临时拒绝,不再校验密码(挡分布式慢速撞库;与 A5 IP 限流互补)。 if h.cache.LoginLocked(ctx, email) { c.JSON(http.StatusTooManyRequests, gin.H{"error": "登录失败次数过多,账户已临时锁定,请稍后再试"}) return } u, err := h.db.GetUserByEmail(ctx, email) // 用户不存在与密码错误返回同一文案,避免邮箱枚举。 if err != nil || u == nil || !auth.CheckPassword(u.PasswordHash, body.Password) { h.cache.NoteLoginFail(ctx, email) // 记一次失败,累计到阈值即锁定 c.JSON(http.StatusUnauthorized, gin.H{"error": "邮箱或密码错误"}) return } h.cache.ClearLoginFails(ctx, email) // 成功即清零 issueToken(c, u) } // Me: GET /api/v1/auth/me —— 返回当前登录用户(无有效令牌则 401)。 func (h *Handler) Me(c *gin.Context) { uid := userID(c) if uid == "" || uid == "anonymous" { c.JSON(http.StatusUnauthorized, gin.H{"error": "未登录"}) return } u, err := h.db.GetUserByID(c.Request.Context(), uid) if err != nil || u == nil { c.JSON(http.StatusUnauthorized, gin.H{"error": "未登录"}) return } c.JSON(http.StatusOK, gin.H{"user": userJSON(u)}) } // issueToken 为用户签发 JWT 并返回 {token, user}。 func issueToken(c *gin.Context, u *store.User) { token, err := auth.Issue(u.ID) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "签发令牌失败"}) return } c.JSON(http.StatusOK, gin.H{"token": token, "user": userJSON(u)}) }