package auth import ( "net/http" "strings" "github.com/gin-gonic/gin" "hyapp-admin-server/internal/config" "hyapp-admin-server/internal/modules/shared" "hyapp-admin-server/internal/repository" "hyapp-admin-server/internal/response" authcore "hyapp-admin-server/internal/service" ) const ( refreshCookieName = "hyapp_admin_refresh" refreshCookiePath = "/" legacyRefreshCookiePath = "/api/v1/auth" ) type Handler struct { service *AuthFlowService audit shared.OperationLogger cfg config.Config } func New(store *repository.Store, auth *authcore.AuthService, audit shared.OperationLogger, cfg config.Config) *Handler { return &Handler{service: NewService(store, auth, cfg), audit: audit, cfg: cfg} } func (h *Handler) Login(c *gin.Context) { var req loginRequest if err := c.ShouldBindJSON(&req); err != nil { response.BadRequest(c, "用户名和密码不能为空") return } result, err := h.service.Login(LoginInput{ Username: req.Username, Password: req.Password, IP: c.ClientIP(), UserAgent: c.Request.UserAgent(), }) if err != nil { response.Unauthorized(c, "用户名或密码错误") return } h.setRefreshCookie(c, result.RefreshToken, int(h.cfg.RefreshTokenTTL.Seconds())) response.OK(c, gin.H{ "accessToken": result.AccessToken, "expiresAtMs": result.ExpiresAtMS, "user": shared.UserDTO(result.User), "permissions": result.Permissions, }) } func (h *Handler) Logout(c *gin.Context) { for _, token := range refreshCookieCandidates(c) { _ = h.service.Logout(token) } h.setRefreshCookie(c, "", -1) response.OK(c, gin.H{"loggedOut": true}) } func (h *Handler) Refresh(c *gin.Context) { tokens := refreshCookieCandidates(c) if len(tokens) == 0 { response.Unauthorized(c, "刷新会话不存在") return } var result LoginResult var err error for _, token := range tokens { result, err = h.service.Refresh(token, LoginInput{ IP: c.ClientIP(), UserAgent: c.Request.UserAgent(), }) if err == nil { h.setRefreshCookie(c, result.RefreshToken, int(h.cfg.RefreshTokenTTL.Seconds())) response.OK(c, gin.H{ "accessToken": result.AccessToken, "expiresAtMs": result.ExpiresAtMS, "user": shared.UserDTO(result.User), "permissions": result.Permissions, }) return } } response.Unauthorized(c, "刷新会话已失效") } func (h *Handler) Me(c *gin.Context) { user, permissions, err := h.service.Me(shared.ActorFromContext(c)) if err != nil { response.Unauthorized(c, "用户不存在") return } response.OK(c, gin.H{ "user": shared.UserDTO(*user), "permissions": permissions, }) } func (h *Handler) ChangePassword(c *gin.Context) { var req changePasswordRequest if err := c.ShouldBindJSON(&req); err != nil { response.BadRequest(c, "旧密码和新密码不能为空") return } if err := h.service.ChangePassword(shared.ActorFromContext(c), req.OldPassword, req.NewPassword); err != nil { response.BadRequest(c, err.Error()) return } shared.OperationLog(c, h.audit, "change-password", "admin_users", "success", "用户修改自己的密码") response.OK(c, gin.H{"changed": true}) } func (h *Handler) setRefreshCookie(c *gin.Context, token string, maxAge int) { c.SetSameSite(refreshCookieSameSite(h.cfg.RefreshCookieSameSite)) c.SetCookie(refreshCookieName, token, maxAge, refreshCookiePath, "", h.cfg.RefreshCookieSecure, true) c.SetCookie(refreshCookieName, "", -1, legacyRefreshCookiePath, "", h.cfg.RefreshCookieSecure, true) } func refreshCookieCandidates(c *gin.Context) []string { values := make([]string, 0, 2) seen := map[string]struct{}{} for _, cookie := range c.Request.Cookies() { if cookie.Name != refreshCookieName || cookie.Value == "" { continue } if _, ok := seen[cookie.Value]; ok { continue } seen[cookie.Value] = struct{}{} values = append(values, cookie.Value) } return values } func refreshCookieSameSite(value string) http.SameSite { switch strings.ToLower(strings.TrimSpace(value)) { case "strict": return http.SameSiteStrictMode case "none": return http.SameSiteNoneMode default: return http.SameSiteLaxMode } }