You've already forked agentic-coding-workflow
修改
This commit is contained in:
+303
-1
@@ -18,8 +18,10 @@ import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/google/uuid"
|
||||
|
||||
"github.com/yan1h/agent-coding-workflow/internal/infra/errs"
|
||||
httpx "github.com/yan1h/agent-coding-workflow/internal/transport/http"
|
||||
@@ -42,15 +44,47 @@ func NewHandler(svc Service) *Handler { return &Handler{svc: svc} }
|
||||
// Mount 把 user 模块的所有 HTTP 端点挂到给定 router 的 /api/v1/auth 子树。
|
||||
// 调用方负责在挂载之前装好 RequestID/Recover/Logger 等通用中间件。
|
||||
func (h *Handler) Mount(r chi.Router) {
|
||||
auth := middleware.Auth(h.svc, middleware.AuthOptions{})
|
||||
|
||||
r.Route("/api/v1/auth", func(r chi.Router) {
|
||||
r.Post("/login", h.login)
|
||||
|
||||
// /me 与 /logout 必须经过 Auth 中间件:未带 token 直接 401,由中间件
|
||||
// 落地响应;带了 token 才会走到下面的处理函数。
|
||||
auth := middleware.Auth(h.svc, middleware.AuthOptions{})
|
||||
r.With(auth).Get("/me", h.me)
|
||||
r.With(auth).Post("/logout", h.logout)
|
||||
// 自助改密:需认证,校验旧密码后撤销全部会话。
|
||||
r.With(auth).Post("/change-password", h.changePassword)
|
||||
})
|
||||
|
||||
// 管理员用户管理子树:全部经 Auth,handler 内部再用 requireAdmin 做 admin 鉴权。
|
||||
r.Route("/api/v1/admin/users", func(r chi.Router) {
|
||||
r.Use(auth)
|
||||
r.Get("/", h.listUsers)
|
||||
r.Post("/", h.createUser)
|
||||
r.Get("/{id}", h.getUser)
|
||||
r.Patch("/{id}", h.updateUser)
|
||||
r.Delete("/{id}", h.deleteUser)
|
||||
r.Patch("/{id}/role", h.setRole)
|
||||
r.Patch("/{id}/enabled", h.setEnabled)
|
||||
r.Post("/{id}/reset-password", h.resetPassword)
|
||||
})
|
||||
}
|
||||
|
||||
// requireAdmin 解析当前认证用户并校验其为管理员。返回 actor 的 UserID。
|
||||
func (h *Handler) requireAdmin(r *http.Request) (uuid.UUID, error) {
|
||||
uid, ok := middleware.UserIDFromContext(r.Context())
|
||||
if !ok {
|
||||
return uuid.Nil, errs.New(errs.CodeUnauthorized, "not authenticated")
|
||||
}
|
||||
u, err := h.svc.Get(r.Context(), uid)
|
||||
if err != nil {
|
||||
return uuid.Nil, err
|
||||
}
|
||||
if !u.IsAdmin {
|
||||
return uuid.Nil, errs.New(errs.CodeForbidden, "需要管理员权限")
|
||||
}
|
||||
return uid, nil
|
||||
}
|
||||
|
||||
// loginReq 是 POST /api/v1/auth/login 的请求体形态。
|
||||
@@ -161,3 +195,271 @@ func clientIP(r *http.Request) string {
|
||||
}
|
||||
return r.RemoteAddr
|
||||
}
|
||||
|
||||
// ===== 管理员用户管理 =====
|
||||
|
||||
// adminUserDTO 是管理后台的用户视图,比 userDTO 多 enabled 与时间戳,但同样
|
||||
// 不暴露 password_hash。
|
||||
type adminUserDTO struct {
|
||||
ID string `json:"id"`
|
||||
Email string `json:"email"`
|
||||
DisplayName string `json:"display_name"`
|
||||
IsAdmin bool `json:"is_admin"`
|
||||
Enabled bool `json:"enabled"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
func toAdminUserDTO(u *User) adminUserDTO {
|
||||
return adminUserDTO{
|
||||
ID: u.ID.String(),
|
||||
Email: u.Email,
|
||||
DisplayName: u.DisplayName,
|
||||
IsAdmin: u.IsAdmin,
|
||||
Enabled: u.Enabled,
|
||||
CreatedAt: u.CreatedAt,
|
||||
UpdatedAt: u.UpdatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
// userIDParam 解析路由中的 {id} 为 uuid。
|
||||
func userIDParam(r *http.Request) (uuid.UUID, error) {
|
||||
id, err := uuid.Parse(chi.URLParam(r, "id"))
|
||||
if err != nil {
|
||||
return uuid.Nil, errs.New(errs.CodeInvalidInput, "无效的用户 id")
|
||||
}
|
||||
return id, nil
|
||||
}
|
||||
|
||||
func (h *Handler) listUsers(w http.ResponseWriter, r *http.Request) {
|
||||
rid := middleware.RequestIDFromContext(r.Context())
|
||||
if _, err := h.requireAdmin(r); err != nil {
|
||||
httpx.WriteError(w, rid, err)
|
||||
return
|
||||
}
|
||||
users, err := h.svc.ListUsers(r.Context())
|
||||
if err != nil {
|
||||
httpx.WriteError(w, rid, err)
|
||||
return
|
||||
}
|
||||
out := make([]adminUserDTO, 0, len(users))
|
||||
for _, u := range users {
|
||||
out = append(out, toAdminUserDTO(u))
|
||||
}
|
||||
httpx.WriteJSON(w, http.StatusOK, out)
|
||||
}
|
||||
|
||||
type createUserReq struct {
|
||||
Email string `json:"email"`
|
||||
DisplayName string `json:"display_name"`
|
||||
Password string `json:"password"`
|
||||
IsAdmin bool `json:"is_admin"`
|
||||
}
|
||||
|
||||
func (h *Handler) createUser(w http.ResponseWriter, r *http.Request) {
|
||||
rid := middleware.RequestIDFromContext(r.Context())
|
||||
if _, err := h.requireAdmin(r); err != nil {
|
||||
httpx.WriteError(w, rid, err)
|
||||
return
|
||||
}
|
||||
var req createUserReq
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
httpx.WriteError(w, rid, errs.New(errs.CodeInvalidInput, "请求体无效"))
|
||||
return
|
||||
}
|
||||
email := strings.TrimSpace(req.Email)
|
||||
display := strings.TrimSpace(req.DisplayName)
|
||||
if email == "" || display == "" {
|
||||
httpx.WriteError(w, rid, errs.New(errs.CodeInvalidInput, "缺少邮箱或显示名"))
|
||||
return
|
||||
}
|
||||
u, err := h.svc.CreateUser(r.Context(), CreateUserInput{
|
||||
Email: email,
|
||||
DisplayName: display,
|
||||
Password: req.Password,
|
||||
IsAdmin: req.IsAdmin,
|
||||
})
|
||||
if err != nil {
|
||||
httpx.WriteError(w, rid, err)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(w, http.StatusCreated, toAdminUserDTO(u))
|
||||
}
|
||||
|
||||
func (h *Handler) getUser(w http.ResponseWriter, r *http.Request) {
|
||||
rid := middleware.RequestIDFromContext(r.Context())
|
||||
if _, err := h.requireAdmin(r); err != nil {
|
||||
httpx.WriteError(w, rid, err)
|
||||
return
|
||||
}
|
||||
id, err := userIDParam(r)
|
||||
if err != nil {
|
||||
httpx.WriteError(w, rid, err)
|
||||
return
|
||||
}
|
||||
u, err := h.svc.Get(r.Context(), id)
|
||||
if err != nil {
|
||||
httpx.WriteError(w, rid, err)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(w, http.StatusOK, toAdminUserDTO(u))
|
||||
}
|
||||
|
||||
type updateUserReq struct {
|
||||
DisplayName string `json:"display_name"`
|
||||
}
|
||||
|
||||
func (h *Handler) updateUser(w http.ResponseWriter, r *http.Request) {
|
||||
rid := middleware.RequestIDFromContext(r.Context())
|
||||
if _, err := h.requireAdmin(r); err != nil {
|
||||
httpx.WriteError(w, rid, err)
|
||||
return
|
||||
}
|
||||
id, err := userIDParam(r)
|
||||
if err != nil {
|
||||
httpx.WriteError(w, rid, err)
|
||||
return
|
||||
}
|
||||
var req updateUserReq
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
httpx.WriteError(w, rid, errs.New(errs.CodeInvalidInput, "请求体无效"))
|
||||
return
|
||||
}
|
||||
display := strings.TrimSpace(req.DisplayName)
|
||||
if display == "" {
|
||||
httpx.WriteError(w, rid, errs.New(errs.CodeInvalidInput, "显示名不能为空"))
|
||||
return
|
||||
}
|
||||
u, err := h.svc.UpdateProfile(r.Context(), id, display)
|
||||
if err != nil {
|
||||
httpx.WriteError(w, rid, err)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(w, http.StatusOK, toAdminUserDTO(u))
|
||||
}
|
||||
|
||||
func (h *Handler) deleteUser(w http.ResponseWriter, r *http.Request) {
|
||||
rid := middleware.RequestIDFromContext(r.Context())
|
||||
actor, err := h.requireAdmin(r)
|
||||
if err != nil {
|
||||
httpx.WriteError(w, rid, err)
|
||||
return
|
||||
}
|
||||
id, err := userIDParam(r)
|
||||
if err != nil {
|
||||
httpx.WriteError(w, rid, err)
|
||||
return
|
||||
}
|
||||
if err := h.svc.DeleteUser(r.Context(), actor, id); err != nil {
|
||||
httpx.WriteError(w, rid, err)
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
||||
type setRoleReq struct {
|
||||
IsAdmin bool `json:"is_admin"`
|
||||
}
|
||||
|
||||
func (h *Handler) setRole(w http.ResponseWriter, r *http.Request) {
|
||||
rid := middleware.RequestIDFromContext(r.Context())
|
||||
actor, err := h.requireAdmin(r)
|
||||
if err != nil {
|
||||
httpx.WriteError(w, rid, err)
|
||||
return
|
||||
}
|
||||
id, err := userIDParam(r)
|
||||
if err != nil {
|
||||
httpx.WriteError(w, rid, err)
|
||||
return
|
||||
}
|
||||
var req setRoleReq
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
httpx.WriteError(w, rid, errs.New(errs.CodeInvalidInput, "请求体无效"))
|
||||
return
|
||||
}
|
||||
u, err := h.svc.SetAdmin(r.Context(), actor, id, req.IsAdmin)
|
||||
if err != nil {
|
||||
httpx.WriteError(w, rid, err)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(w, http.StatusOK, toAdminUserDTO(u))
|
||||
}
|
||||
|
||||
type setEnabledReq struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
}
|
||||
|
||||
func (h *Handler) setEnabled(w http.ResponseWriter, r *http.Request) {
|
||||
rid := middleware.RequestIDFromContext(r.Context())
|
||||
actor, err := h.requireAdmin(r)
|
||||
if err != nil {
|
||||
httpx.WriteError(w, rid, err)
|
||||
return
|
||||
}
|
||||
id, err := userIDParam(r)
|
||||
if err != nil {
|
||||
httpx.WriteError(w, rid, err)
|
||||
return
|
||||
}
|
||||
var req setEnabledReq
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
httpx.WriteError(w, rid, errs.New(errs.CodeInvalidInput, "请求体无效"))
|
||||
return
|
||||
}
|
||||
u, err := h.svc.SetEnabled(r.Context(), actor, id, req.Enabled)
|
||||
if err != nil {
|
||||
httpx.WriteError(w, rid, err)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(w, http.StatusOK, toAdminUserDTO(u))
|
||||
}
|
||||
|
||||
type resetPasswordResp struct {
|
||||
TempPassword string `json:"temp_password"`
|
||||
}
|
||||
|
||||
func (h *Handler) resetPassword(w http.ResponseWriter, r *http.Request) {
|
||||
rid := middleware.RequestIDFromContext(r.Context())
|
||||
if _, err := h.requireAdmin(r); err != nil {
|
||||
httpx.WriteError(w, rid, err)
|
||||
return
|
||||
}
|
||||
id, err := userIDParam(r)
|
||||
if err != nil {
|
||||
httpx.WriteError(w, rid, err)
|
||||
return
|
||||
}
|
||||
temp, err := h.svc.AdminResetPassword(r.Context(), id)
|
||||
if err != nil {
|
||||
httpx.WriteError(w, rid, err)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(w, http.StatusOK, resetPasswordResp{TempPassword: temp})
|
||||
}
|
||||
|
||||
// ===== 自助改密 =====
|
||||
|
||||
type changePasswordReq struct {
|
||||
OldPassword string `json:"old_password"`
|
||||
NewPassword string `json:"new_password"`
|
||||
}
|
||||
|
||||
func (h *Handler) changePassword(w http.ResponseWriter, r *http.Request) {
|
||||
rid := middleware.RequestIDFromContext(r.Context())
|
||||
uid, ok := middleware.UserIDFromContext(r.Context())
|
||||
if !ok {
|
||||
httpx.WriteError(w, rid, errs.New(errs.CodeInternal, "missing auth context"))
|
||||
return
|
||||
}
|
||||
var req changePasswordReq
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
httpx.WriteError(w, rid, errs.New(errs.CodeInvalidInput, "请求体无效"))
|
||||
return
|
||||
}
|
||||
if err := h.svc.ChangePassword(r.Context(), uid, req.OldPassword, req.NewPassword); err != nil {
|
||||
httpx.WriteError(w, rid, err)
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user