302 lines
8.0 KiB
Go
302 lines
8.0 KiB
Go
package auth
|
|
|
|
import (
|
|
"context"
|
|
"crypto/rand"
|
|
"encoding/hex"
|
|
"errors"
|
|
"fmt"
|
|
"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
|
|
cfg *config.Config
|
|
tokensMu sync.RWMutex
|
|
tokens map[string]int64 // token -> userID
|
|
}
|
|
|
|
// NewService 实例化认证服务
|
|
func NewService(store AuthStore, cfg *config.Config) *Service {
|
|
if store == nil {
|
|
store = NewMemoryStore(cfg)
|
|
}
|
|
return &Service{
|
|
store: store,
|
|
cfg: cfg,
|
|
tokens: make(map[string]int64),
|
|
}
|
|
}
|
|
|
|
// 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 !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()
|
|
s.tokensMu.Lock()
|
|
s.tokens[token] = user.ID
|
|
s.tokensMu.Unlock()
|
|
|
|
// 敏感信息脱敏
|
|
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,
|
|
AllowDevSwitch: env == "dev",
|
|
}, 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
|
|
}
|
|
|
|
// GetUserByToken 根据会话 Token 查找登录用户
|
|
func (s *Service) GetUserByToken(ctx context.Context, token string) (*UserAccount, error) {
|
|
s.tokensMu.RLock()
|
|
userID, ok := s.tokens[token]
|
|
s.tokensMu.RUnlock()
|
|
|
|
if !ok {
|
|
return nil, errors.New("会话已过期或未登录")
|
|
}
|
|
|
|
user, err := s.store.GetUserByID(ctx, userID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
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.tokensMu.Lock()
|
|
defer s.tokensMu.Unlock()
|
|
delete(s.tokens, 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
|
|
}
|
|
u.PasswordHash = ""
|
|
return u, nil
|
|
}
|
|
|
|
// 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)
|
|
}
|