// Package api 管理 API(/api/v1):认证、用户、密钥、用量、管理后台。 package api import ( "net/http" "strings" "time" "github.com/gin-gonic/gin" "github.com/openteam/server/internal/app" "github.com/openteam/server/internal/pkg/resp" "github.com/openteam/server/internal/store" "gorm.io/gorm" ) // Handler 聚合所有管理 API。 type Handler struct { a *app.App } func NewHandler(a *app.App) *Handler { return &Handler{a: a} } // --------------------------------------------------------------------------- // 认证 type registerReq struct { Username string `json:"username" binding:"required,min=3,max=32"` Email string `json:"email" binding:"required,email"` Password string `json:"password" binding:"required,min=8,max=72"` InviteCode string `json:"invite_code"` } // Register POST /api/v1/auth/register func (h *Handler) Register(c *gin.Context) { var req registerReq if err := c.ShouldBindJSON(&req); err != nil { resp.Fail(c, http.StatusBadRequest, "invalid input: "+err.Error()) return } if h.a.Cfg.Auth.RegistrationMode == "invite" { var ic string h.a.DB.Model(&store.SystemConfig{}).Where("key = ?", "invite_codes").Pluck("value", &ic) if !strings.Contains(ic, req.InviteCode) { resp.Fail(c, http.StatusForbidden, "valid invite code required") return } } var count int64 h.a.DB.Model(&store.User{}).Where("username = ? OR email = ?", req.Username, req.Email).Count(&count) if count > 0 { resp.Fail(c, http.StatusConflict, "username or email already exists") return } hash, err := h.a.Hasher.HashPassword(req.Password) if err != nil { resp.Fail(c, http.StatusInternalServerError, "failed to hash password") return } u := store.User{ Username: req.Username, Email: strings.ToLower(req.Email), PasswordHash: hash, Role: store.RoleUser, Balance: 5.0, // 新用户赠送体验余额(可通过 admin 调整) Status: store.UserStatusActive, } if req.InviteCode != "" { ic := req.InviteCode u.InviteCode = &ic } if err := h.a.DB.Create(&u).Error; err != nil { resp.Fail(c, http.StatusInternalServerError, "failed to create user") return } resp.Created(c, gin.H{"id": u.ID, "username": u.Username}) } type loginReq struct { Username string `json:"username" binding:"required"` Password string `json:"password" binding:"required"` } // Login POST /api/v1/auth/login — 返回 access token,refresh token 写入 HttpOnly Cookie。 func (h *Handler) Login(c *gin.Context) { var req loginReq if err := c.ShouldBindJSON(&req); err != nil { resp.Fail(c, http.StatusBadRequest, "invalid input") return } var u store.User err := h.a.DB.Where("username = ? OR email = ?", req.Username, strings.ToLower(req.Username)).First(&u).Error if err != nil || u.Status != store.UserStatusActive { resp.Fail(c, http.StatusUnauthorized, "invalid credentials") return } ok, err := h.a.Hasher.VerifyPassword(u.PasswordHash, req.Password) if err != nil || !ok { resp.Fail(c, http.StatusUnauthorized, "invalid credentials") return } now := time.Now() h.a.DB.Model(&u).Update("last_login_at", now) access, _, err := h.a.JWT.Sign(u.ID, u.Username, u.Role, "access") if err != nil { resp.Fail(c, http.StatusInternalServerError, "failed to issue token") return } refresh, _, err := h.a.JWT.Sign(u.ID, u.Username, u.Role, "refresh") if err != nil { resp.Fail(c, http.StatusInternalServerError, "failed to issue token") return } h.setRefreshCookie(c, refresh) resp.OK(c, gin.H{"access_token": access, "expires_in": int(h.a.JWT.AccessTTL().Seconds()), "user": h.publicUser(&u)}) } // Refresh POST /api/v1/auth/refresh — 用 refresh cookie 换新 access token。 func (h *Handler) Refresh(c *gin.Context) { tok, err := c.Cookie(h.a.Cfg.JWT.CookieName) if err != nil || tok == "" { resp.Fail(c, http.StatusUnauthorized, "refresh token missing") return } claims, err := h.a.JWT.Parse(tok) if err != nil || claims.Subject != "refresh" { resp.Fail(c, http.StatusUnauthorized, "invalid refresh token") return } var u store.User if err := h.a.DB.First(&u, claims.UserID).Error; err != nil || u.Status != store.UserStatusActive { resp.Fail(c, http.StatusUnauthorized, "user not found or disabled") return } access, _, err := h.a.JWT.Sign(u.ID, u.Username, u.Role, "access") if err != nil { resp.Fail(c, http.StatusInternalServerError, "failed to issue token") return } resp.OK(c, gin.H{"access_token": access, "expires_in": int(h.a.JWT.AccessTTL().Seconds())}) } // Logout POST /api/v1/auth/logout — 清除 refresh cookie。 func (h *Handler) Logout(c *gin.Context) { h.setRefreshCookie(c, "") resp.OK(c, gin.H{"ok": true}) } // Me GET /api/v1/auth/me func (h *Handler) Me(c *gin.Context) { u := sessionUser(c) resp.OK(c, gin.H{"user": h.publicUser(u)}) } func (h *Handler) setRefreshCookie(c *gin.Context, value string) { maxAge := int(h.a.JWT.RefreshTTL().Seconds()) if value == "" { maxAge = -1 } http.SetCookie(c.Writer, &http.Cookie{ Name: h.a.Cfg.JWT.CookieName, Value: value, Path: "/", MaxAge: maxAge, HttpOnly: true, Secure: h.a.Cfg.JWT.CookieSecure, SameSite: http.SameSiteLaxMode, Domain: h.a.Cfg.JWT.CookieDomain, }) } func (h *Handler) publicUser(u *store.User) gin.H { return gin.H{ "id": u.ID, "username": u.Username, "email": u.Email, "role": u.Role, "balance": u.Balance, "status": u.Status, "created_at": u.CreatedAt, } } func sessionUser(c *gin.Context) *store.User { u, _ := c.Get("session_user") return u.(*store.User) } func userFromContext(c *gin.Context) (*store.User, bool) { u, ok := c.Get("session_user") if !ok { return nil, false } return u.(*store.User), true } var _ = gorm.ErrRecordNotFound