Files
NianAIGC/backend/internal/httpapi/auth_password_change.go

131 lines
4.1 KiB
Go

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})
}