package auth import ( "context" "crypto/rand" "encoding/hex" "errors" "fmt" "log/slog" "strings" "sync" "chat-agent-system/internal/config" ) // RegisterRequest 注册入参 (仅用户名、密码、姓名为必填) type RegisterRequest struct { Username string `json:"username"` Password string `json:"password"` Name string `json:"name"` EmployeeID string `json:"employee_id,omitempty"` Department string `json:"department,omitempty"` Email string `json:"email,omitempty"` } // RegisterResponse 注册响应 type RegisterResponse struct { Status AccountStatus `json:"status"` Message string `json:"message"` UserID int64 `json:"user_id"` } // CreateUserRequest 管理员手动创建账号入参 type CreateUserRequest struct { Username string `json:"username"` Name string `json:"name"` InitialPassword string `json:"initial_password"` RoleID string `json:"role_id"` Position string `json:"position"` Department string `json:"department,omitempty"` EmployeeID string `json:"employee_id,omitempty"` } // Service 认证与账号治理业务逻辑 type Service struct { store AuthStore sessions SessionStore cfg *config.Config prefMu sync.RWMutex prefs map[int64]map[string]any } // NewService 实例化认证服务;sessionStore 为空时使用进程内 MemorySessionStore。 func NewService(store AuthStore, sessionStore SessionStore, cfg *config.Config) *Service { if store == nil { store = NewMemoryStore(cfg) } if sessionStore == nil { sessionStore = NewMemorySessionStore() } return &Service{ store: store, sessions: sessionStore, cfg: cfg, } } // generateToken 生成随机安全会话 Token func generateToken() string { b := make([]byte, 24) _, _ = rand.Read(b) return hex.EncodeToString(b) } // Login 登录鉴权 func (s *Service) Login(ctx context.Context, username, password string) (*SessionInfo, error) { username = strings.TrimSpace(username) if username == "" || password == "" { return nil, errors.New("用户名或密码不能为空") } user, err := s.store.GetUserByUsername(ctx, username) if err != nil { return nil, errors.New("用户名或密码错误") } if strings.TrimSpace(user.PasswordHash) == "" { slog.Warn("auth_login_missing_password_hash", "user_id", user.ID) return nil, errors.New("用户名或密码错误") } if !CheckPassword(user.PasswordHash, password) { return nil, errors.New("用户名或密码错误") } if user.Status == StatusPendingApproval { return nil, errors.New("账号正在等待管理员审核中,暂无法登录") } if user.Status == StatusDisabled { return nil, errors.New("账号已被停用,禁止登录") } token := generateToken() tokenHash := HashToken(token) if err := s.sessions.CreateSession(ctx, user.ID, tokenHash, SessionMeta{}); err != nil { return nil, fmt.Errorf("创建会话失败: %w", err) } // 敏感信息脱敏 user.PasswordHash = "" env := "dev" if s.cfg != nil && s.cfg.Env != "" { env = s.cfg.Env } return &SessionInfo{ Token: token, User: user, MustChangePassword: user.MustChangePassword, Env: env, }, nil } // Register 用户自主注册 (职位严格由系统固定为访客待分配) func (s *Service) Register(ctx context.Context, req RegisterRequest) (*RegisterResponse, error) { req.Username = strings.TrimSpace(req.Username) req.Name = strings.TrimSpace(req.Name) if req.Username == "" || req.Password == "" || req.Name == "" { return nil, errors.New("用户名、密码与真实姓名均为必填项") } sysCfg, err := s.store.GetConfig(ctx) if err != nil { return nil, fmt.Errorf("读取系统准入策略失败: %w", err) } if !sysCfg.AllowRegistration { return nil, errors.New("系统当前已暂停自主注册,请联系系统管理员分配账号") } passHash, err := HashPassword(req.Password) if err != nil { return nil, fmt.Errorf("密码哈希处理失败: %w", err) } status := StatusActive msg := "注册成功,已激活,请返回登录" if sysCfg.RequireApproval { status = StatusPendingApproval msg = "注册成功!您的账号正在等待管理员审核与职位指派,审核通过后方可登录。" } account := &UserAccount{ Username: req.Username, Name: req.Name, PasswordHash: passHash, Status: status, MustChangePassword: false, RoleID: "role-guest", Position: "访客/待指派", EmployeeID: req.EmployeeID, Department: req.Department, Email: req.Email, } created, err := s.store.CreateUser(ctx, account) if err != nil { return nil, err } return &RegisterResponse{ Status: status, Message: msg, UserID: created.ID, }, nil } // ChangePassword 修改密码 (处理普通改密与首次登录强制改密) func (s *Service) ChangePassword(ctx context.Context, userID int64, oldPass, newPass string) error { if strings.TrimSpace(newPass) == "" { return errors.New("新密码不能为空") } user, err := s.store.GetUserByID(ctx, userID) if err != nil { return fmt.Errorf("用户不存在: %w", err) } if !CheckPassword(user.PasswordHash, oldPass) { return errors.New("原密码不正确") } newHash, err := HashPassword(newPass) if err != nil { return fmt.Errorf("新密码加密失败: %w", err) } _, err = s.store.UpdateUserPassword(ctx, userID, newHash, false) return err } // AdminResetPasswordResult is returned once to the calling admin; plaintext must not be logged. type AdminResetPasswordResult struct { TempPassword string `json:"temp_password"` MustChangePassword bool `json:"must_change_password"` UserID int64 `json:"user_id"` } // AdminResetPassword generates a temporary password, persists its hash with // must_change_password=true, revokes existing sessions, and records an audit row. func (s *Service) AdminResetPassword(ctx context.Context, targetID, actorID int64, requestID string) (*AdminResetPasswordResult, error) { if targetID <= 0 { return nil, errors.New("无效的用户ID") } if actorID <= 0 { return nil, errors.New("无效的操作者") } if _, err := s.store.GetUserByID(ctx, targetID); err != nil { return nil, errors.New("用户不存在") } temp, err := GenerateTempPassword(16) if err != nil { return nil, fmt.Errorf("生成临时口令失败: %w", err) } hash, err := HashPassword(temp) if err != nil { return nil, fmt.Errorf("临时口令加密失败: %w", err) } if _, err := s.store.UpdateUserPassword(ctx, targetID, hash, true); err != nil { return nil, err } _ = s.sessions.RevokeAllForUser(ctx, targetID) if err := s.store.RecordPasswordResetAudit(ctx, targetID, actorID, requestID); err != nil { return nil, fmt.Errorf("记录重置审计失败: %w", err) } return &AdminResetPasswordResult{ TempPassword: temp, MustChangePassword: true, UserID: targetID, }, nil } // GetUserByToken 根据会话 Token 查找登录用户 func (s *Service) GetUserByToken(ctx context.Context, token string) (*UserAccount, error) { userID, err := s.sessions.ResolveUserID(ctx, HashToken(token)) if err != nil { return nil, errors.New("会话已过期或未登录") } user, err := s.store.GetUserByID(ctx, userID) if err != nil { return nil, err } if user.Status == StatusDisabled || user.Status == StatusPendingApproval { _ = s.sessions.RevokeByTokenHash(ctx, HashToken(token)) return nil, errors.New("会话已过期或未登录") } user.PasswordHash = "" return user, nil } // GetUserByID 根据用户 ID 直接查询用户信息 (安全过滤密码哈希) func (s *Service) GetUserByID(ctx context.Context, userID int64) (*UserAccount, error) { user, err := s.store.GetUserByID(ctx, userID) if err != nil { return nil, err } cpy := *user cpy.PasswordHash = "" return &cpy, nil } // Logout 注销会话 func (s *Service) Logout(token string) { _ = s.sessions.RevokeByTokenHash(context.Background(), HashToken(token)) } // ListUsers 列出所有用户 func (s *Service) ListUsers(ctx context.Context) ([]*UserAccount, error) { return s.store.ListUsers(ctx) } // CreateUserByAdmin 管理员手动开通账号 (自动标记 mustChangePassword = true) func (s *Service) CreateUserByAdmin(ctx context.Context, req CreateUserRequest) (*UserAccount, error) { if req.Username == "" || req.Name == "" || req.InitialPassword == "" { return nil, errors.New("用户名、姓名与初始密码为必填") } hash, err := HashPassword(req.InitialPassword) if err != nil { return nil, err } roleID := req.RoleID if roleID == "" { roleID = "role-engineer" } position := req.Position if position == "" { position = "方案工程师" } account := &UserAccount{ Username: req.Username, Name: req.Name, PasswordHash: hash, Status: StatusActive, MustChangePassword: true, // 核心约束:管理员创建的账号首次登录必须修改密码 RoleID: roleID, Position: position, Department: req.Department, EmployeeID: req.EmployeeID, } created, err := s.store.CreateUser(ctx, account) if err != nil { return nil, err } created.PasswordHash = "" return created, nil } // UpdateUserRole 管理员为用户指定职位与角色 func (s *Service) UpdateUserRole(ctx context.Context, userID int64, roleID, position string) (*UserAccount, error) { u, err := s.store.UpdateUserRole(ctx, userID, roleID, position) if err != nil { return nil, err } u.PasswordHash = "" return u, nil } // UpdateUserStatus 管理员审批通过/停用账号 func (s *Service) UpdateUserStatus(ctx context.Context, userID int64, status AccountStatus) (*UserAccount, error) { u, err := s.store.UpdateUserStatus(ctx, userID, status) if err != nil { return nil, err } if status == StatusDisabled { _ = s.sessions.RevokeAllForUser(ctx, userID) } u.PasswordHash = "" return u, nil } // ListUserSessions 列出当前用户活跃会话。 func (s *Service) ListUserSessions(ctx context.Context, userID int64, currentToken string) ([]SessionRecord, error) { currentHash := "" if currentToken != "" { currentHash = HashToken(currentToken) } return s.sessions.ListActiveSessions(ctx, userID, currentHash) } // RevokeUserSession 撤销指定会话(不可跨用户)。 func (s *Service) RevokeUserSession(ctx context.Context, userID int64, sessionID string) error { return s.sessions.RevokeSessionByID(ctx, userID, sessionID) } // GetConfig 获取准入策略 func (s *Service) GetConfig(ctx context.Context) (SystemAuthConfig, error) { return s.store.GetConfig(ctx) } // UpdateConfig 更新准入策略 func (s *Service) UpdateConfig(ctx context.Context, cfg SystemAuthConfig) error { return s.store.UpdateConfig(ctx, cfg) } // UpdateUserProfile 当前用户自助更新展示资料;清除管理员代改标记。 func (s *Service) UpdateUserProfile(ctx context.Context, userID int64, patch ProfilePatch) (*UserAccount, error) { if userID <= 0 { return nil, errors.New("invalid user") } existing, err := s.store.GetUserByID(ctx, userID) if err != nil { return nil, err } if err := applyProfilePatch(existing, patch, false); err != nil { return nil, err } updated, err := s.store.UpdateUser(ctx, existing, userID) if err != nil { return nil, err } updated.PasswordHash = "" return updated, nil } // AdminUpdateUserProfile 管理员更新他人展示资料;记录代改人。 func (s *Service) AdminUpdateUserProfile(ctx context.Context, targetID int64, patch ProfilePatch, actorID int64) (*UserAccount, error) { if targetID <= 0 || actorID <= 0 { return nil, errors.New("invalid user") } if patch.Name == nil && patch.Department == nil && patch.Email == nil && patch.EmployeeID == nil { return nil, errors.New("至少提供一个可更新字段") } existing, err := s.store.GetUserByID(ctx, targetID) if err != nil { return nil, err } if err := applyProfilePatch(existing, patch, true); err != nil { return nil, err } updated, err := s.store.UpdateUser(ctx, existing, actorID) if err != nil { return nil, err } updated.PasswordHash = "" return updated, nil } func applyProfilePatch(user *UserAccount, patch ProfilePatch, adminMode bool) error { if patch.Name != nil { name := strings.TrimSpace(*patch.Name) if name == "" { return errors.New("姓名不能为空") } user.Name = name } if patch.Email != nil { user.Email = strings.TrimSpace(*patch.Email) } if patch.Department != nil { user.Department = strings.TrimSpace(*patch.Department) } if adminMode && patch.EmployeeID != nil { user.EmployeeID = strings.TrimSpace(*patch.EmployeeID) } if !adminMode { if patch.Locale != nil { user.Locale = strings.TrimSpace(*patch.Locale) } if patch.Timezone != nil { user.Timezone = strings.TrimSpace(*patch.Timezone) } } return nil } // ListUserDirectory 返回 active 用户轻量目录。 func (s *Service) ListUserDirectory(ctx context.Context) ([]UserDirectoryEntry, error) { return s.store.ListActiveUserDirectory(ctx) } // GetUserPreferences 读取用户偏好(内存/后续 PG user_preferences)。 func (s *Service) GetUserPreferences(ctx context.Context, userID int64) (map[string]any, error) { s.prefMu.RLock() defer s.prefMu.RUnlock() if s.prefs == nil { return map[string]any{}, nil } if p, ok := s.prefs[userID]; ok { return p, nil } return map[string]any{}, nil } // SaveUserPreferences 保存用户偏好。 func (s *Service) SaveUserPreferences(ctx context.Context, userID int64, body map[string]any) (map[string]any, error) { if userID <= 0 { return nil, errors.New("invalid user id") } s.prefMu.Lock() defer s.prefMu.Unlock() if s.prefs == nil { s.prefs = make(map[int64]map[string]any) } s.prefs[userID] = body return body, nil }