693a8f09e9
按 SAAS_DESIGN.md P1 第一刀(真·多租户 + 桌面端为主):先立租户身份,不动查询。 - store.Tenant / TenantMember 表 + AutoMigrate - store: CreateTenant / AddMember(幂等) / DefaultTenantForUser / EnsureDefaultTenant(幂等) / BackfillDefaultTenants / GetTenant / MemberRole - 注册即建单人默认租户(owner);启动回填给存量用户补建(幂等) - middleware.TenantContext(挂 Auth 后)解析当前租户→注入 tenant_id;handler.tenantID(c) 助手 - GET /api/v1/tenants/current 验证端点(租户上下文 + 角色) - live:存量用户(回填 7 租户)/tenants/current 返回默认租户+owner;新注册自动建租户 下一步(增量2):核心表加 tenant_id + 统一 gorm scope 强制隔离 + 存量行回填 + 重写查询。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
108 lines
3.3 KiB
Go
108 lines
3.3 KiB
Go
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))
|
|
if !strings.Contains(email, "@") || len(body.Password) < 6 {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "需合法邮箱且密码至少 6 位"})
|
|
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))
|
|
u, err := h.db.GetUserByEmail(c.Request.Context(), email)
|
|
// 用户不存在与密码错误返回同一文案,避免邮箱枚举。
|
|
if err != nil || u == nil || !auth.CheckPassword(u.PasswordHash, body.Password) {
|
|
c.JSON(http.StatusUnauthorized, gin.H{"error": "邮箱或密码错误"})
|
|
return
|
|
}
|
|
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)})
|
|
}
|