package handlers import ( "encoding/json" "net/http" "strings" "trade/web/internal/auth" "trade/web/internal/middleware" "trade/web/internal/store" ) type loginReq struct { Username string `json:"username"` Password string `json:"password"` } type loginResp struct { Token string `json:"token"` User publicUserView `json:"user"` RequirePasswordChange bool `json:"require_password_change"` } type publicUserView struct { ID int64 `json:"id"` Username string `json:"username"` Role string `json:"role"` ForcePasswordChange bool `json:"force_password_change"` } type changePasswordReq struct { OldPassword string `json:"old_password"` NewPassword string `json:"new_password"` } func (d *Deps) Login(w http.ResponseWriter, r *http.Request) { var req loginReq if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeErr(w, http.StatusBadRequest, "invalid json") return } req.Username = strings.TrimSpace(req.Username) if req.Username == "" || req.Password == "" { writeErr(w, http.StatusBadRequest, "用户名和密码不能为空") return } u, err := d.Auth.GetByUsername(req.Username) if err != nil || u.Disabled { // 禁用账户与不存在账户返回同样的错误,避免账户枚举 writeErr(w, http.StatusUnauthorized, "用户名或密码错误") return } if !auth.CheckPassword(u.PasswordHash, req.Password) { writeErr(w, http.StatusUnauthorized, "用户名或密码错误") return } // 暂时不用 JWT,返回固定 token writeJSON(w, http.StatusOK, loginResp{ Token: "noop", User: publicUserView{ ID: u.ID, Username: u.Username, Role: u.Role, ForcePasswordChange: u.ForcePasswordChange, }, RequirePasswordChange: u.ForcePasswordChange, }) } func (d *Deps) ChangePassword(w http.ResponseWriter, r *http.Request) { me, ok := middleware.FromContext(r.Context()) if !ok { writeErr(w, http.StatusUnauthorized, "no user") return } var req changePasswordReq if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeErr(w, http.StatusBadRequest, "invalid json") return } if req.OldPassword == "" || req.NewPassword == "" { writeErr(w, http.StatusBadRequest, "旧密码和新密码都不能为空") return } if len(req.NewPassword) < 6 { writeErr(w, http.StatusBadRequest, "新密码至少 6 位") return } u, err := d.Auth.GetByID(me.ID) if err != nil { writeErr(w, http.StatusUnauthorized, "user not found") return } if !auth.CheckPassword(u.PasswordHash, req.OldPassword) { writeErr(w, http.StatusUnauthorized, "旧密码错误") return } hash, err := auth.HashPassword(req.NewPassword) if err != nil { writeErr(w, http.StatusInternalServerError, "hash failed") return } if err := d.Auth.UpdatePassword(me.ID, hash); err != nil { writeErr(w, http.StatusInternalServerError, err.Error()) return } // 改密码后清除强制改密标记 if err := d.Auth.SetForcePasswordChange(me.ID, false); err != nil { writeErr(w, http.StatusInternalServerError, err.Error()) return } writeJSON(w, http.StatusOK, map[string]string{"status": "ok"}) } func (d *Deps) Logout(w http.ResponseWriter, r *http.Request) { // JWT 是无状态的,服务端 logout 仅形式化;前端丢弃 token 即可。 writeJSON(w, http.StatusOK, map[string]string{"status": "ok"}) } func (d *Deps) Me(w http.ResponseWriter, r *http.Request) { u, ok := middleware.FromContext(r.Context()) if !ok { writeErr(w, http.StatusUnauthorized, "no user in context") return } full, err := d.Auth.GetByID(u.ID) if err != nil { writeErr(w, http.StatusUnauthorized, "user not found") return } writeJSON(w, http.StatusOK, sanitize(full)) } // sanitize 把内部 User 转成对外视图,剥掉 password_hash。 func sanitize(u *store.User) map[string]any { return map[string]any{ "id": u.ID, "username": u.Username, "role": u.Role, "disabled": u.Disabled, "force_password_change": u.ForcePasswordChange, "created_at": u.CreatedAt, "updated_at": u.UpdatedAt, } }