package httpapi import ( "context" "encoding/json" "errors" "fmt" "net/http" "strings" "time" "git.nianxx.cn/wangxuming/NianAIGC/backend/internal/identity" ) type PasswordChangeSessionIssuer interface { Change(context.Context, identity.PasswordChangeCommand) (identity.Session, error) } type PasswordChangeConfig struct { SessionSecret, CookieSecure, PublicBaseURL string } type authPasswordChangeHandler struct { config PasswordChangeConfig authorizer *PlatformAuthorizer changer PasswordChangeSessionIssuer } func NewAuthPasswordChangeHandler(config PasswordChangeConfig, authorizer *PlatformAuthorizer, changer PasswordChangeSessionIssuer) (http.Handler, error) { if authorizer == nil || changer == nil || strings.TrimSpace(config.SessionSecret) == "" { return nil, fmt.Errorf("auth/password/change: authorizer, password changer, and session secret are required") } return &authPasswordChangeHandler{config: config, authorizer: authorizer, changer: changer}, nil } func (handler *authPasswordChangeHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/api/auth/password/change" { http.NotFound(w, r) return } if r.Method != http.MethodPost { w.Header().Set("Allow", http.MethodPost) w.WriteHeader(http.StatusMethodNotAllowed) return } session, err := handler.authorizer.Authorize(r, PlatformApp) if err != nil { writePasswordChangeError(w, err) return } var body struct{ CurrentPassword, NewPassword, ConfirmPassword any } if json.NewDecoder(r.Body).Decode(&body) != nil { writePasswordChangeError(w, &identity.PasswordChangeError{Reason: identity.PasswordChangeInvalidInput}) return } current := passwordChangeString(body.CurrentPassword) next := passwordChangeString(body.NewPassword) confirm := passwordChangeString(body.ConfirmPassword) if current == "" || next == "" || confirm == "" { writePasswordChangeError(w, &identity.PasswordChangeError{Reason: identity.PasswordChangeInvalidInput}) return } if next != confirm { writePasswordJSON(w, http.StatusBadRequest, map[string]any{"error": "两次输入的新密码不一致。"}) return } nextSession, err := handler.changer.Change(r.Context(), identity.PasswordChangeCommand{AccountID: session.User.ID, CurrentPassword: current, NewPassword: next}) if err != nil { writePasswordChangeError(w, err) return } raw, err := json.Marshal(nextSession) if err != nil { writePasswordChangeError(w, err) return } signed, err := identity.Sign(raw, handler.config.SessionSecret) if err != nil { writePasswordChangeError(w, err) return } secure := identity.ResolveSecureCookie(handler.config.CookieSecure, handler.config.PublicBaseURL, absoluteRequestURL(r)) writes, err := identity.SetSessionCookies(signed, time.Unix(nextSession.ExpiresAt, 0).UTC(), secure) if err != nil { writePasswordChangeError(w, err) return } payload, err := json.Marshal(map[string]any{"ok": true, "user": passwordPublicUser(nextSession.User)}) if err != nil { writePasswordChangeError(w, err) return } for _, write := range writes { http.SetCookie(w, transportCookie(write)) } w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) _, _ = w.Write(payload) } func passwordChangeString(value any) string { text, ok := value.(string) if !ok { return "" } return strings.TrimSpace(text) } func writePasswordChangeError(w http.ResponseWriter, err error) { status, message := http.StatusInternalServerError, "服务器内部错误。" var authErr *PlatformAuthError if errors.As(err, &authErr) { status, message = authErr.Status, authErr.Message } var changeErr *identity.PasswordChangeError if errors.As(err, &changeErr) { status = http.StatusBadRequest switch changeErr.Reason { case identity.PasswordChangeNotFound: status, message = http.StatusNotFound, "账号不存在或已停用。" case identity.PasswordChangeCurrentIncorrect: message = "当前密码不正确。" case identity.PasswordChangeInvalidNewPassword: message = "新密码至少需要 8 位。" default: message = "当前密码、新密码和确认密码不能为空。" } } writePasswordJSON(w, status, map[string]any{"error": message}) }