package http import ( "net" stdhttp "net/http" "strings" "time" "backupx/server/internal/apperror" "backupx/server/internal/service" "backupx/server/pkg/response" "github.com/gin-gonic/gin" ) const ( trustedDeviceCookieName = "backupx_trusted_device" trustedDeviceCookiePath = "/api/auth" trustedDeviceCookieMaxAge = int((30 * 24 * time.Hour) / time.Second) ) type AuthHandler struct { authService *service.AuthService } func NewAuthHandler(authService *service.AuthService) *AuthHandler { return &AuthHandler{authService: authService} } func (h *AuthHandler) SetupStatus(c *gin.Context) { initialized, err := h.authService.SetupStatus(c.Request.Context()) if err != nil { response.Error(c, err) return } response.Success(c, gin.H{"initialized": initialized}) } func (h *AuthHandler) Setup(c *gin.Context) { var input service.SetupInput if err := c.ShouldBindJSON(&input); err != nil { response.Error(c, apperror.BadRequest("AUTH_SETUP_INVALID", "初始化参数不合法", err)) return } payload, err := h.authService.Setup(c.Request.Context(), input) if err != nil { response.Error(c, err) return } response.Success(c, payload) } func (h *AuthHandler) Login(c *gin.Context) { var input service.LoginInput if err := c.ShouldBindJSON(&input); err != nil { response.Error(c, apperror.BadRequest("AUTH_LOGIN_INVALID", "登录参数不合法", err)) return } if strings.TrimSpace(input.TrustedDeviceToken) == "" { input.TrustedDeviceToken = trustedDeviceCookieValue(c) } payload, err := h.authService.Login(c.Request.Context(), input, ClientKey(c)) if err != nil { response.Error(c, err) return } if payload.TrustedDeviceToken != "" { setTrustedDeviceCookie(c, payload.TrustedDeviceToken) payload.TrustedDeviceToken = "" } response.Success(c, payload) } func (h *AuthHandler) Profile(c *gin.Context) { subjectValue, _ := c.Get(contextUserSubjectKey) subject, err := service.SubjectFromContextValue(subjectValue) if err != nil { response.Error(c, apperror.Unauthorized("AUTH_INVALID_SUBJECT", "无效登录态", err)) return } user, err := h.authService.GetCurrentUser(c.Request.Context(), subject) if err != nil { response.Error(c, err) return } response.Success(c, user) } func (h *AuthHandler) ChangePassword(c *gin.Context) { subjectValue, _ := c.Get(contextUserSubjectKey) subject, err := service.SubjectFromContextValue(subjectValue) if err != nil { response.Error(c, apperror.Unauthorized("AUTH_INVALID_SUBJECT", "无效登录态", err)) return } var input service.ChangePasswordInput if err := c.ShouldBindJSON(&input); err != nil { response.Error(c, apperror.BadRequest("AUTH_PASSWORD_INVALID", "参数不合法", err)) return } if err := h.authService.ChangePassword(c.Request.Context(), subject, input); err != nil { response.Error(c, err) return } clearTrustedDeviceCookie(c) response.Success(c, gin.H{"changed": true}) } func (h *AuthHandler) PrepareTwoFactor(c *gin.Context) { subjectValue, _ := c.Get(contextUserSubjectKey) subject, err := service.SubjectFromContextValue(subjectValue) if err != nil { response.Error(c, apperror.Unauthorized("AUTH_INVALID_SUBJECT", "无效登录态", err)) return } var input service.TwoFactorSetupInput if err := c.ShouldBindJSON(&input); err != nil { response.Error(c, apperror.BadRequest("AUTH_2FA_INVALID", "参数不合法", err)) return } payload, err := h.authService.PrepareTwoFactor(c.Request.Context(), subject, input) if err != nil { response.Error(c, err) return } response.Success(c, payload) } func (h *AuthHandler) EnableTwoFactor(c *gin.Context) { subjectValue, _ := c.Get(contextUserSubjectKey) subject, err := service.SubjectFromContextValue(subjectValue) if err != nil { response.Error(c, apperror.Unauthorized("AUTH_INVALID_SUBJECT", "无效登录态", err)) return } var input service.EnableTwoFactorInput if err := c.ShouldBindJSON(&input); err != nil { response.Error(c, apperror.BadRequest("AUTH_2FA_INVALID", "参数不合法", err)) return } user, err := h.authService.EnableTwoFactor(c.Request.Context(), subject, input) if err != nil { response.Error(c, err) return } response.Success(c, user) } func (h *AuthHandler) DisableTwoFactor(c *gin.Context) { subjectValue, _ := c.Get(contextUserSubjectKey) subject, err := service.SubjectFromContextValue(subjectValue) if err != nil { response.Error(c, apperror.Unauthorized("AUTH_INVALID_SUBJECT", "无效登录态", err)) return } var input service.DisableTwoFactorInput if err := c.ShouldBindJSON(&input); err != nil { response.Error(c, apperror.BadRequest("AUTH_2FA_INVALID", "参数不合法", err)) return } user, err := h.authService.DisableTwoFactor(c.Request.Context(), subject, input) if err != nil { response.Error(c, err) return } if !user.MFAEnabled { clearTrustedDeviceCookie(c) } response.Success(c, user) } func (h *AuthHandler) RegenerateRecoveryCodes(c *gin.Context) { subjectValue, _ := c.Get(contextUserSubjectKey) subject, err := service.SubjectFromContextValue(subjectValue) if err != nil { response.Error(c, apperror.Unauthorized("AUTH_INVALID_SUBJECT", "无效登录态", err)) return } var input service.RegenerateRecoveryCodesInput if err := c.ShouldBindJSON(&input); err != nil { response.Error(c, apperror.BadRequest("AUTH_2FA_INVALID", "参数不合法", err)) return } payload, err := h.authService.RegenerateRecoveryCodes(c.Request.Context(), subject, input) if err != nil { response.Error(c, err) return } response.Success(c, payload) } func (h *AuthHandler) ConfigureOTP(c *gin.Context) { subjectValue, _ := c.Get(contextUserSubjectKey) subject, err := service.SubjectFromContextValue(subjectValue) if err != nil { response.Error(c, apperror.Unauthorized("AUTH_INVALID_SUBJECT", "无效登录态", err)) return } var input service.OTPConfigInput if err := c.ShouldBindJSON(&input); err != nil { response.Error(c, apperror.BadRequest("AUTH_OTP_INVALID", "参数不合法", err)) return } user, err := h.authService.ConfigureOutOfBandOTP(c.Request.Context(), subject, input) if err != nil { response.Error(c, err) return } if !user.MFAEnabled { clearTrustedDeviceCookie(c) } response.Success(c, user) } func (h *AuthHandler) SendLoginOTP(c *gin.Context) { var input service.LoginOTPInput if err := c.ShouldBindJSON(&input); err != nil { response.Error(c, apperror.BadRequest("AUTH_OTP_INVALID", "参数不合法", err)) return } if err := h.authService.SendLoginOTP(c.Request.Context(), input, ClientKey(c)); err != nil { response.Error(c, err) return } response.Success(c, gin.H{"sent": true}) } func (h *AuthHandler) BeginWebAuthnRegistration(c *gin.Context) { subjectValue, _ := c.Get(contextUserSubjectKey) subject, err := service.SubjectFromContextValue(subjectValue) if err != nil { response.Error(c, apperror.Unauthorized("AUTH_INVALID_SUBJECT", "无效登录态", err)) return } var input service.WebAuthnRegistrationOptionsInput if err := c.ShouldBindJSON(&input); err != nil { response.Error(c, apperror.BadRequest("AUTH_WEBAUTHN_INVALID", "参数不合法", err)) return } options, err := h.authService.BeginWebAuthnRegistration(c.Request.Context(), subject, input, webAuthnRequestContext(c)) if err != nil { response.Error(c, err) return } response.Success(c, options) } func (h *AuthHandler) FinishWebAuthnRegistration(c *gin.Context) { subjectValue, _ := c.Get(contextUserSubjectKey) subject, err := service.SubjectFromContextValue(subjectValue) if err != nil { response.Error(c, apperror.Unauthorized("AUTH_INVALID_SUBJECT", "无效登录态", err)) return } var input service.WebAuthnRegistrationFinishInput if err := c.ShouldBindJSON(&input); err != nil { response.Error(c, apperror.BadRequest("AUTH_WEBAUTHN_INVALID", "参数不合法", err)) return } user, err := h.authService.FinishWebAuthnRegistration(c.Request.Context(), subject, input) if err != nil { response.Error(c, err) return } response.Success(c, user) } func (h *AuthHandler) BeginWebAuthnLogin(c *gin.Context) { var input service.WebAuthnLoginOptionsInput if err := c.ShouldBindJSON(&input); err != nil { response.Error(c, apperror.BadRequest("AUTH_WEBAUTHN_INVALID", "参数不合法", err)) return } options, err := h.authService.BeginWebAuthnLogin(c.Request.Context(), input, webAuthnRequestContext(c), ClientKey(c)) if err != nil { response.Error(c, err) return } response.Success(c, options) } func (h *AuthHandler) ListWebAuthnCredentials(c *gin.Context) { subjectValue, _ := c.Get(contextUserSubjectKey) subject, err := service.SubjectFromContextValue(subjectValue) if err != nil { response.Error(c, apperror.Unauthorized("AUTH_INVALID_SUBJECT", "无效登录态", err)) return } items, err := h.authService.ListWebAuthnCredentials(c.Request.Context(), subject) if err != nil { response.Error(c, err) return } response.Success(c, items) } func (h *AuthHandler) DeleteWebAuthnCredential(c *gin.Context) { subjectValue, _ := c.Get(contextUserSubjectKey) subject, err := service.SubjectFromContextValue(subjectValue) if err != nil { response.Error(c, apperror.Unauthorized("AUTH_INVALID_SUBJECT", "无效登录态", err)) return } var input service.WebAuthnCredentialDeleteInput if err := c.ShouldBindJSON(&input); err != nil { response.Error(c, apperror.BadRequest("AUTH_WEBAUTHN_INVALID", "参数不合法", err)) return } user, err := h.authService.DeleteWebAuthnCredential(c.Request.Context(), subject, c.Param("id"), input) if err != nil { response.Error(c, err) return } if !user.MFAEnabled { clearTrustedDeviceCookie(c) } response.Success(c, user) } func (h *AuthHandler) ListTrustedDevices(c *gin.Context) { subjectValue, _ := c.Get(contextUserSubjectKey) subject, err := service.SubjectFromContextValue(subjectValue) if err != nil { response.Error(c, apperror.Unauthorized("AUTH_INVALID_SUBJECT", "无效登录态", err)) return } items, err := h.authService.ListTrustedDevices(c.Request.Context(), subject) if err != nil { response.Error(c, err) return } response.Success(c, items) } func (h *AuthHandler) RevokeTrustedDevice(c *gin.Context) { subjectValue, _ := c.Get(contextUserSubjectKey) subject, err := service.SubjectFromContextValue(subjectValue) if err != nil { response.Error(c, apperror.Unauthorized("AUTH_INVALID_SUBJECT", "无效登录态", err)) return } var input service.TrustedDeviceRevokeInput if err := c.ShouldBindJSON(&input); err != nil { response.Error(c, apperror.BadRequest("AUTH_TRUSTED_DEVICE_INVALID", "参数不合法", err)) return } if err := h.authService.RevokeTrustedDevice(c.Request.Context(), subject, c.Param("id"), input); err != nil { response.Error(c, err) return } clearTrustedDeviceCookie(c) response.Success(c, gin.H{"deleted": true}) } func (h *AuthHandler) Logout(c *gin.Context) { response.Success(c, gin.H{"loggedOut": true}) } func webAuthnRequestContext(c *gin.Context) service.WebAuthnRequestContext { host := firstForwardedValue(c.Request.Host) if forwardedHost := firstForwardedValue(c.GetHeader("X-Forwarded-Host")); forwardedHost != "" { host = forwardedHost } rpID := host if parsedHost, _, err := net.SplitHostPort(host); err == nil { rpID = parsedHost } scheme := "http" if c.Request.TLS != nil { scheme = "https" } if forwardedProto := firstForwardedValue(c.GetHeader("X-Forwarded-Proto")); forwardedProto != "" { scheme = forwardedProto } origin := strings.TrimSpace(c.GetHeader("Origin")) if origin == "" { origin = scheme + "://" + host } return service.WebAuthnRequestContext{RPID: rpID, Origin: origin} } func firstForwardedValue(value string) string { parts := strings.Split(value, ",") if len(parts) == 0 { return "" } return strings.TrimSpace(parts[0]) } func trustedDeviceCookieValue(c *gin.Context) string { token, err := c.Cookie(trustedDeviceCookieName) if err != nil { return "" } return strings.TrimSpace(token) } func setTrustedDeviceCookie(c *gin.Context, token string) { writeTrustedDeviceCookie(c, strings.TrimSpace(token), trustedDeviceCookieMaxAge) } func clearTrustedDeviceCookie(c *gin.Context) { writeTrustedDeviceCookie(c, "", -1) } func writeTrustedDeviceCookie(c *gin.Context, value string, maxAge int) { c.SetSameSite(stdhttp.SameSiteLaxMode) c.SetCookie(trustedDeviceCookieName, value, maxAge, trustedDeviceCookiePath, "", requestIsSecure(c), true) } func requestIsSecure(c *gin.Context) bool { if c.Request.TLS != nil { return true } return strings.EqualFold(firstForwardedValue(c.GetHeader("X-Forwarded-Proto")), "https") }