Files

148 lines
4.1 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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,
}
}