| package adminauth |
|
|
| import ( |
| "errors" |
| "io" |
| "net" |
| "net/http" |
| "strings" |
| "time" |
|
|
| adminapp "github.com/chenyme/grok2api/backend/internal/application/adminauth" |
| admindomain "github.com/chenyme/grok2api/backend/internal/domain/admin" |
| "github.com/chenyme/grok2api/backend/internal/shared/response" |
| "github.com/chenyme/grok2api/backend/internal/transport/http/middleware" |
| "github.com/gin-gonic/gin" |
| ) |
|
|
| const refreshCookieName = "grok2api_admin_refresh" |
|
|
| type Handler struct { |
| service *adminapp.Service |
| secureCookies bool |
| } |
|
|
| func NewHandler(service *adminapp.Service, secureCookies bool) *Handler { |
| return &Handler{service: service, secureCookies: secureCookies} |
| } |
|
|
| func (h *Handler) RegisterPublic(router *gin.RouterGroup) { |
| router.POST("/auth/login", h.login) |
| router.POST("/auth/refresh", h.refresh) |
| router.POST("/auth/logout", h.logout) |
| } |
|
|
| func (h *Handler) RegisterAuthenticated(router *gin.RouterGroup) { |
| router.GET("/me", h.me) |
| router.PUT("/me/password", h.changePassword) |
| } |
|
|
| type loginRequest struct { |
| Username string `json:"username" binding:"required"` |
| Password string `json:"password" binding:"required"` |
| } |
|
|
| type refreshRequest struct { |
| RefreshToken string `json:"refreshToken"` |
| } |
|
|
| type changePasswordRequest struct { |
| CurrentPassword string `json:"currentPassword" binding:"required"` |
| NewPassword string `json:"newPassword" binding:"required"` |
| } |
|
|
| type tokenResponse struct { |
| AccessToken string `json:"accessToken"` |
| AccessTokenExpiresAt string `json:"accessTokenExpiresAt"` |
| RefreshTokenExpiresAt string `json:"refreshTokenExpiresAt"` |
| } |
|
|
| type adminResponse struct { |
| ID uint64 `json:"id,string"` |
| Username string `json:"username"` |
| } |
|
|
| func (h *Handler) login(c *gin.Context) { |
| var request loginRequest |
| if err := c.ShouldBindJSON(&request); err != nil { |
| response.Error(c, http.StatusBadRequest, "invalidRequest", "请求参数无效") |
| return |
| } |
| adminValue, tokens, err := h.service.Login(c.Request.Context(), request.Username, request.Password, remoteAddress(c.Request)) |
| if err != nil { |
| if errors.Is(err, adminapp.ErrLoginRateLimited) { |
| response.Error(c, http.StatusTooManyRequests, "loginRateLimited", "登录尝试过于频繁,请稍后重试") |
| return |
| } |
| if errors.Is(err, adminapp.ErrRuntimeUnavailable) { |
| response.Error(c, http.StatusServiceUnavailable, "authRuntimeUnavailable", "管理员认证服务暂不可用") |
| return |
| } |
| response.Error(c, http.StatusUnauthorized, "invalidCredentials", "管理员账号或密码错误") |
| return |
| } |
| h.setRefreshCookie(c, tokens) |
| response.Success(c, http.StatusOK, gin.H{"admin": newAdminResponse(adminValue), "tokens": newTokenResponse(tokens)}) |
| } |
|
|
| func remoteAddress(request *http.Request) string { |
| value := strings.TrimSpace(request.RemoteAddr) |
| host, _, err := net.SplitHostPort(value) |
| if err == nil && host != "" { |
| return host |
| } |
| return value |
| } |
|
|
| func (h *Handler) refresh(c *gin.Context) { |
| var request refreshRequest |
| if err := c.ShouldBindJSON(&request); err != nil && !errors.Is(err, io.EOF) { |
| response.Error(c, http.StatusBadRequest, "invalidRequest", "请求参数无效") |
| return |
| } |
| if request.RefreshToken == "" { |
| request.RefreshToken, _ = c.Cookie(refreshCookieName) |
| } |
| if request.RefreshToken == "" { |
| response.Error(c, http.StatusUnauthorized, "invalidRefreshToken", "刷新会话无效") |
| return |
| } |
| tokens, err := h.service.Refresh(c.Request.Context(), request.RefreshToken) |
| if err != nil { |
| if errors.Is(err, adminapp.ErrRuntimeUnavailable) { |
| response.Error(c, http.StatusServiceUnavailable, "authRuntimeUnavailable", "管理员认证服务暂不可用") |
| return |
| } |
| response.Error(c, http.StatusUnauthorized, "invalidRefreshToken", "刷新会话无效") |
| return |
| } |
| h.setRefreshCookie(c, tokens) |
| response.Success(c, http.StatusOK, newTokenResponse(tokens)) |
| } |
|
|
| func (h *Handler) logout(c *gin.Context) { |
| var request refreshRequest |
| if err := c.ShouldBindJSON(&request); err != nil && !errors.Is(err, io.EOF) { |
| response.Error(c, http.StatusBadRequest, "invalidRequest", "请求参数无效") |
| return |
| } |
| if request.RefreshToken == "" { |
| request.RefreshToken, _ = c.Cookie(refreshCookieName) |
| } |
| if err := h.service.Logout(c.Request.Context(), request.RefreshToken); err != nil { |
| response.Error(c, http.StatusServiceUnavailable, "authRuntimeUnavailable", "管理员认证服务暂不可用") |
| return |
| } |
| c.SetSameSite(http.SameSiteStrictMode) |
| c.SetCookie(refreshCookieName, "", -1, "/api/admin/v1/auth", "", h.secureCookies || c.Request.TLS != nil, true) |
| response.Success(c, http.StatusOK, gin.H{"loggedOut": true}) |
| } |
|
|
| func (h *Handler) me(c *gin.Context) { |
| value, ok := c.Get(middleware.AdminKey) |
| adminValue, valid := value.(admindomain.Admin) |
| if !ok || !valid { |
| response.Error(c, http.StatusUnauthorized, "adminUnauthorized", "管理员登录已失效") |
| return |
| } |
| response.Success(c, http.StatusOK, newAdminResponse(adminValue)) |
| } |
|
|
| func (h *Handler) changePassword(c *gin.Context) { |
| var request changePasswordRequest |
| if err := c.ShouldBindJSON(&request); err != nil { |
| response.Error(c, http.StatusBadRequest, "invalidRequest", "请求参数无效") |
| return |
| } |
| value, ok := c.Get(middleware.AdminKey) |
| adminValue, valid := value.(admindomain.Admin) |
| if !ok || !valid { |
| response.Error(c, http.StatusUnauthorized, "adminUnauthorized", "管理员登录已失效") |
| return |
| } |
| if err := h.service.ChangePassword(c.Request.Context(), adminValue.ID, request.CurrentPassword, request.NewPassword); err != nil { |
| if errors.Is(err, adminapp.ErrInvalidCredentials) || errors.Is(err, adminapp.ErrInvalidPassword) { |
| response.Error(c, http.StatusBadRequest, "passwordChangeFailed", err.Error()) |
| return |
| } |
| response.Error(c, http.StatusInternalServerError, "passwordChangeFailed", "修改管理员密码失败") |
| return |
| } |
| response.Success(c, http.StatusOK, gin.H{"passwordChanged": true}) |
| } |
|
|
| func newAdminResponse(value admindomain.Admin) adminResponse { |
| return adminResponse{ID: value.ID, Username: value.Username} |
| } |
|
|
| func newTokenResponse(value adminapp.Tokens) tokenResponse { |
| return tokenResponse{AccessToken: value.AccessToken, AccessTokenExpiresAt: value.AccessTokenExpiresAt.Format(time.RFC3339), RefreshTokenExpiresAt: value.RefreshTokenExpiresAt.Format(time.RFC3339)} |
| } |
|
|
| func (h *Handler) setRefreshCookie(c *gin.Context, value adminapp.Tokens) { |
| maxAge := int(time.Until(value.RefreshTokenExpiresAt).Seconds()) |
| if maxAge < 0 { |
| maxAge = 0 |
| } |
| c.SetSameSite(http.SameSiteStrictMode) |
| c.SetCookie(refreshCookieName, value.RefreshToken, maxAge, "/api/admin/v1/auth", "", h.secureCookies || c.Request.TLS != nil, true) |
| } |
|
|