Files
Agent-System/internal/server/auth_handlers.go
T
weichenandCursor d5a9a29eec feat(api): add workflow catalog routes and public auth config endpoint
Expose governance APIs for workflow selection/termination and allow login pages to read registration policy without admin credentials.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-13 02:50:23 +00:00

359 lines
10 KiB
Go

package server
import (
"context"
"encoding/json"
"errors"
"net/http"
"strconv"
"strings"
"chat-agent-system/internal/auth"
"chat-agent-system/internal/authz"
)
// extractBearerToken 从 Authorization 请求头解析 Bearer Token
func extractBearerToken(r *http.Request) string {
authHeader := r.Header.Get("Authorization")
if strings.HasPrefix(authHeader, "Bearer ") {
return strings.TrimSpace(strings.TrimPrefix(authHeader, "Bearer "))
}
return ""
}
// authenticateUser 鉴权中间件辅助方法
func (s *Server) authenticateUser(r *http.Request) (*auth.UserAccount, error) {
token := extractBearerToken(r)
if token == "" {
token = r.Header.Get("X-Session-Token")
}
if token != "" {
user, err := s.authSvc.GetUserByToken(r.Context(), token)
if err == nil && user != nil {
return user, nil
}
}
return nil, errors.New("missing auth token")
}
// handleAuthLogin 用户名密码登录
func (s *Server) handleAuthLogin(w http.ResponseWriter, r *http.Request) {
var req struct {
Username string `json:"username"`
Password string `json:"password"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "无效的 JSON 请求体", http.StatusBadRequest)
return
}
session, err := s.authSvc.Login(r.Context(), req.Username, req.Password)
if err != nil {
errMsg := err.Error()
if strings.Contains(errMsg, "审核") {
http.Error(w, errMsg, http.StatusForbidden)
return
}
if strings.Contains(errMsg, "停用") {
http.Error(w, errMsg, http.StatusForbidden)
return
}
http.Error(w, errMsg, http.StatusUnauthorized)
return
}
w.Header().Set("Content-Type", "application/json; charset=utf-8")
_ = json.NewEncoder(w).Encode(session)
}
// handleAuthRegister 用户自主注册 (职位严格由系统固定为访客)
func (s *Server) handleAuthRegister(w http.ResponseWriter, r *http.Request) {
var req auth.RegisterRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "无效的 JSON 请求体", http.StatusBadRequest)
return
}
resp, err := s.authSvc.Register(r.Context(), req)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
w.Header().Set("Content-Type", "application/json; charset=utf-8")
_ = json.NewEncoder(w).Encode(resp)
}
// handleAuthLogout 注销登录
func (s *Server) handleAuthLogout(w http.ResponseWriter, r *http.Request) {
token := extractBearerToken(r)
if token != "" {
s.authSvc.Logout(token)
}
w.Header().Set("Content-Type", "application/json; charset=utf-8")
_ = json.NewEncoder(w).Encode(map[string]bool{"success": true})
}
// handleAuthChangePassword 修改密码 (支持普通用户与首次强制改密)
func (s *Server) handleAuthChangePassword(w http.ResponseWriter, r *http.Request) {
user, err := s.authenticateUser(r)
if err != nil {
http.Error(w, "未登录或登录已过期", http.StatusUnauthorized)
return
}
var req struct {
OldPassword string `json:"old_password"`
NewPassword string `json:"new_password"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "无效的 JSON 请求体", http.StatusBadRequest)
return
}
if err := s.authSvc.ChangePassword(r.Context(), user.ID, req.OldPassword, req.NewPassword); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
w.Header().Set("Content-Type", "application/json; charset=utf-8")
_ = json.NewEncoder(w).Encode(map[string]bool{"success": true})
}
// handleAuthMe 获取当前登录用户身份
func (s *Server) handleAuthMe(w http.ResponseWriter, r *http.Request) {
user, err := s.authenticateUser(r)
if err != nil {
http.Error(w, "未登录或登录已过期", http.StatusUnauthorized)
return
}
env := "dev"
if s.cfg != nil && s.cfg.Env != "" {
env = s.cfg.Env
}
caps := s.resolveUserCapabilities(r.Context(), user)
w.Header().Set("Content-Type", "application/json; charset=utf-8")
_ = json.NewEncoder(w).Encode(map[string]any{
"user": user,
"must_change_password": user.MustChangePassword,
"env": env,
"capabilities": caps,
})
}
func (s *Server) resolveUserCapabilities(ctx context.Context, user *auth.UserAccount) []string {
caps := []string{}
if s.authzSvc != nil {
if listed, capErr := s.authzSvc.ListCapabilities(ctx, user.ID); capErr == nil && len(listed) > 0 {
return listed
}
}
if user != nil && user.RoleID != "" {
if fallback := authz.FallbackCapabilitiesByRoleID(user.RoleID); len(fallback) > 0 {
caps = fallback
}
}
return caps
}
// handleAuthSessions 列出或撤销当前用户会话。
func (s *Server) handleAuthSessions(w http.ResponseWriter, r *http.Request) {
user, err := s.authenticateUser(r)
if err != nil {
http.Error(w, "未登录或登录已过期", http.StatusUnauthorized)
return
}
if r.Method == http.MethodGet {
token := extractBearerToken(r)
sessions, listErr := s.authSvc.ListUserSessions(r.Context(), user.ID, token)
if listErr != nil {
http.Error(w, listErr.Error(), http.StatusInternalServerError)
return
}
if sessions == nil {
sessions = []auth.SessionRecord{}
}
writeJSONResponse(w, http.StatusOK, map[string]any{"sessions": sessions})
return
}
http.Error(w, "不支持的请求方法", http.StatusMethodNotAllowed)
}
func (s *Server) handleAuthSessionByID(w http.ResponseWriter, r *http.Request) {
user, err := s.authenticateUser(r)
if err != nil {
http.Error(w, "未登录或登录已过期", http.StatusUnauthorized)
return
}
if r.Method != http.MethodDelete {
http.Error(w, "不支持的请求方法", http.StatusMethodNotAllowed)
return
}
sessionID := strings.TrimSpace(r.PathValue("id"))
if sessionID == "" {
http.Error(w, "无效的会话 ID", http.StatusBadRequest)
return
}
if err := s.authSvc.RevokeUserSession(r.Context(), user.ID, sessionID); err != nil {
http.Error(w, "会话不存在或已失效", http.StatusNotFound)
return
}
writeJSONResponse(w, http.StatusOK, map[string]bool{"success": true})
}
// handleAdminUsers 管理员查询或新建用户
func (s *Server) handleAdminUsers(w http.ResponseWriter, r *http.Request) {
if _, ok := s.requireSystemAdmin(w, r); !ok {
return
}
if r.Method == http.MethodGet {
users, err := s.authSvc.ListUsers(r.Context())
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json; charset=utf-8")
_ = json.NewEncoder(w).Encode(map[string]any{"users": users})
return
}
if r.Method == http.MethodPost {
var req auth.CreateUserRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "无效的 JSON 请求体", http.StatusBadRequest)
return
}
created, err := s.authSvc.CreateUserByAdmin(r.Context(), req)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
w.Header().Set("Content-Type", "application/json; charset=utf-8")
_ = json.NewEncoder(w).Encode(created)
return
}
http.Error(w, "不支持的请求方法", http.StatusMethodNotAllowed)
}
// handleAdminUpdateRole 管理员为用户指定职位与角色
func (s *Server) handleAdminUpdateRole(w http.ResponseWriter, r *http.Request) {
if _, ok := s.requireSystemAdmin(w, r); !ok {
return
}
targetID, err := strconv.ParseInt(r.PathValue("id"), 10, 64)
if err != nil || targetID <= 0 {
http.Error(w, "无效的用户ID", http.StatusBadRequest)
return
}
var req struct {
RoleID string `json:"role_id"`
Position string `json:"position"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "无效的请求体", http.StatusBadRequest)
return
}
updated, err := s.authSvc.UpdateUserRole(r.Context(), targetID, req.RoleID, req.Position)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
w.Header().Set("Content-Type", "application/json; charset=utf-8")
_ = json.NewEncoder(w).Encode(updated)
}
// handleAdminUpdateStatus 管理员审批通过/停用账号
func (s *Server) handleAdminUpdateStatus(w http.ResponseWriter, r *http.Request) {
if _, ok := s.requireSystemAdmin(w, r); !ok {
return
}
targetID, err := strconv.ParseInt(r.PathValue("id"), 10, 64)
if err != nil || targetID <= 0 {
http.Error(w, "无效的用户ID", http.StatusBadRequest)
return
}
var req struct {
Status auth.AccountStatus `json:"status"`
RoleID string `json:"role_id,omitempty"`
Position string `json:"position,omitempty"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "无效的请求体", http.StatusBadRequest)
return
}
if req.RoleID != "" {
_, _ = s.authSvc.UpdateUserRole(r.Context(), targetID, req.RoleID, req.Position)
}
updated, err := s.authSvc.UpdateUserStatus(r.Context(), targetID, req.Status)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
w.Header().Set("Content-Type", "application/json; charset=utf-8")
_ = json.NewEncoder(w).Encode(updated)
}
// handleAuthPublicConfig 返回登录/注册页所需的公开准入配置(无需鉴权)
func (s *Server) handleAuthPublicConfig(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
http.Error(w, "不支持的方法", http.StatusMethodNotAllowed)
return
}
cfg, err := s.authSvc.GetConfig(r.Context())
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json; charset=utf-8")
_ = json.NewEncoder(w).Encode(cfg)
}
// handleAdminConfig 获取或更新系统准入配置
func (s *Server) handleAdminConfig(w http.ResponseWriter, r *http.Request) {
if _, ok := s.requireSystemAdmin(w, r); !ok {
return
}
if r.Method == http.MethodGet {
cfg, err := s.authSvc.GetConfig(r.Context())
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json; charset=utf-8")
_ = json.NewEncoder(w).Encode(cfg)
return
}
if r.Method == http.MethodPut || r.Method == http.MethodPost {
var req auth.SystemAuthConfig
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "无效的请求体", http.StatusBadRequest)
return
}
if err := s.authSvc.UpdateConfig(r.Context(), req); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json; charset=utf-8")
_ = json.NewEncoder(w).Encode(req)
return
}
http.Error(w, "不支持的方法", http.StatusMethodNotAllowed)
}