Registration always writes role-guest; a deleted standard role became a masked INTERNAL. Username unique violations are now a 400 instead of an infrastructure 500.
334 lines
9.2 KiB
Go
334 lines
9.2 KiB
Go
package auth
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"chat-agent-system/internal/config"
|
|
)
|
|
|
|
// ErrUsernameExists 用户名或 account_id 唯一约束冲突。调用方应返回 400,而不是基础设施 500。
|
|
var ErrUsernameExists = errors.New("用户名已存在")
|
|
|
|
// AuthStore 用户与认证配置存储接口
|
|
type AuthStore interface {
|
|
GetUserByUsername(ctx context.Context, username string) (*UserAccount, error)
|
|
GetUserByID(ctx context.Context, id int64) (*UserAccount, error)
|
|
CreateUser(ctx context.Context, user *UserAccount) (*UserAccount, error)
|
|
UpdateUser(ctx context.Context, user *UserAccount, actorID int64) (*UserAccount, error)
|
|
UpdateUserRole(ctx context.Context, id int64, roleID, position string) (*UserAccount, error)
|
|
UpdateUserStatus(ctx context.Context, id int64, status AccountStatus) (*UserAccount, error)
|
|
UpdateUserPassword(ctx context.Context, id int64, passwordHash string, mustChangePassword bool) (*UserAccount, error)
|
|
RecordPasswordResetAudit(ctx context.Context, targetUserID, actorUserID int64, requestID string) error
|
|
ListUsers(ctx context.Context) ([]*UserAccount, error)
|
|
ListActiveUserDirectory(ctx context.Context) ([]UserDirectoryEntry, error)
|
|
GetConfig(ctx context.Context) (SystemAuthConfig, error)
|
|
UpdateConfig(ctx context.Context, cfg SystemAuthConfig) error
|
|
}
|
|
|
|
// MemoryStore 内存与并发安全存储实现
|
|
type MemoryStore struct {
|
|
mu sync.RWMutex
|
|
users map[int64]*UserAccount
|
|
nextID int64
|
|
cfg SystemAuthConfig
|
|
}
|
|
|
|
// NewMemoryStore 初始化并载入初始系统管理员与基线用户
|
|
func NewMemoryStore(cfg *config.Config) *MemoryStore {
|
|
store := &MemoryStore{
|
|
users: make(map[int64]*UserAccount),
|
|
nextID: 1,
|
|
cfg: SystemAuthConfig{
|
|
AllowRegistration: true,
|
|
RequireApproval: false,
|
|
},
|
|
}
|
|
|
|
if cfg != nil {
|
|
store.cfg.AllowRegistration = cfg.AuthRegistrationEnabled
|
|
store.cfg.RequireApproval = cfg.AuthRequireApproval
|
|
}
|
|
|
|
now := time.Now().Format("2006-01-02 15:04:05")
|
|
|
|
// 1. 初始化 .env 系统超级管理员
|
|
adminUser := "admin"
|
|
adminPass := "admin-secret-password-123"
|
|
if cfg != nil && cfg.AdminUsername != "" {
|
|
adminUser = cfg.AdminUsername
|
|
}
|
|
if cfg != nil && cfg.AdminPassword != "" {
|
|
adminPass = cfg.AdminPassword
|
|
}
|
|
adminHash, _ := HashPassword(adminPass)
|
|
store.users[1] = &UserAccount{
|
|
ID: 1,
|
|
Username: adminUser,
|
|
Name: "系统管理员",
|
|
PasswordHash: adminHash,
|
|
Status: StatusActive,
|
|
MustChangePassword: false,
|
|
RoleID: "role-admin",
|
|
Position: "系统管理员",
|
|
Department: "系统运维部",
|
|
EmployeeID: "SYS-000",
|
|
CreatedAt: now,
|
|
UpdatedAt: now,
|
|
}
|
|
store.nextID = 2
|
|
|
|
// 2. 标准测试用户仅 dev/test 环境种子化;生产不得携带固定密码演示账号
|
|
env := ""
|
|
if cfg != nil {
|
|
env = cfg.Env
|
|
}
|
|
if env == "dev" || env == "test" || env == "" {
|
|
defaultPassHash, _ := HashPassword("123456")
|
|
seedUsers := []struct {
|
|
id int64
|
|
username string
|
|
name string
|
|
roleID string
|
|
position string
|
|
dept string
|
|
employeeID string
|
|
}{
|
|
{2, "zhanggong", "张工", "role-tech-lead", "技术主管", "骨科耗材研发组", "RD-001"},
|
|
{3, "lizhuguan", "李主管", "role-tech-lead", "技术主管", "骨科耗材研发组", "RD-002"},
|
|
{4, "wangzong", "王总监", "role-director", "研发总监", "研发中心", "RD-003"},
|
|
{5, "guest", "访客", "role-guest", "待指派", "外协中心", "EXT-999"},
|
|
}
|
|
|
|
for _, su := range seedUsers {
|
|
store.users[su.id] = &UserAccount{
|
|
ID: su.id,
|
|
Username: su.username,
|
|
Name: su.name,
|
|
PasswordHash: defaultPassHash,
|
|
Status: StatusActive,
|
|
MustChangePassword: false,
|
|
RoleID: su.roleID,
|
|
Position: su.position,
|
|
Department: su.dept,
|
|
EmployeeID: su.employeeID,
|
|
CreatedAt: now,
|
|
UpdatedAt: now,
|
|
}
|
|
if su.id >= store.nextID {
|
|
store.nextID = su.id + 1
|
|
}
|
|
}
|
|
}
|
|
|
|
return store
|
|
}
|
|
|
|
// GetUserByUsername 根据用户名查询
|
|
func (s *MemoryStore) GetUserByUsername(ctx context.Context, username string) (*UserAccount, error) {
|
|
s.mu.RLock()
|
|
defer s.mu.RUnlock()
|
|
|
|
for _, u := range s.users {
|
|
if strings.EqualFold(u.Username, username) {
|
|
copy := *u
|
|
return ©, nil
|
|
}
|
|
}
|
|
return nil, fmt.Errorf("user not found: %s", username)
|
|
}
|
|
|
|
// GetUserByID 根据用户ID查询
|
|
func (s *MemoryStore) GetUserByID(ctx context.Context, id int64) (*UserAccount, error) {
|
|
s.mu.RLock()
|
|
defer s.mu.RUnlock()
|
|
|
|
u, exists := s.users[id]
|
|
if !exists {
|
|
return nil, fmt.Errorf("user not found: %d", id)
|
|
}
|
|
copy := *u
|
|
return ©, nil
|
|
}
|
|
|
|
// CreateUser 创建新用户账号
|
|
func (s *MemoryStore) CreateUser(ctx context.Context, user *UserAccount) (*UserAccount, error) {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
|
|
if user == nil || strings.TrimSpace(user.PasswordHash) == "" {
|
|
return nil, fmt.Errorf("password hash required")
|
|
}
|
|
|
|
for _, existing := range s.users {
|
|
if strings.EqualFold(existing.Username, user.Username) {
|
|
return nil, ErrUsernameExists
|
|
}
|
|
}
|
|
|
|
user.ID = s.nextID
|
|
s.nextID++
|
|
now := time.Now().Format("2006-01-02 15:04:05")
|
|
if user.CreatedAt == "" {
|
|
user.CreatedAt = now
|
|
}
|
|
user.UpdatedAt = now
|
|
|
|
copied := *user
|
|
s.users[user.ID] = &copied
|
|
|
|
result := *user
|
|
return &result, nil
|
|
}
|
|
|
|
// RecordPasswordResetAudit records an admin password reset in memory (no persistence).
|
|
func (s *MemoryStore) RecordPasswordResetAudit(ctx context.Context, targetUserID, actorUserID int64, requestID string) error {
|
|
return nil
|
|
}
|
|
|
|
// UpdateUser 更新用户基础资料;actorID==user.ID 时清除管理员代改标记。
|
|
func (s *MemoryStore) UpdateUser(ctx context.Context, user *UserAccount, actorID int64) (*UserAccount, error) {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
|
|
existing, ok := s.users[user.ID]
|
|
if !ok {
|
|
return nil, fmt.Errorf("user not found: %d", user.ID)
|
|
}
|
|
|
|
existing.Name = user.Name
|
|
existing.Department = user.Department
|
|
existing.EmployeeID = user.EmployeeID
|
|
existing.Email = user.Email
|
|
existing.Avatar = user.Avatar
|
|
existing.Locale = user.Locale
|
|
existing.Timezone = user.Timezone
|
|
now := time.Now().Format("2006-01-02 15:04:05")
|
|
existing.UpdatedAt = now
|
|
if actorID > 0 && actorID != user.ID {
|
|
existing.ProfileUpdatedBy = actorID
|
|
existing.ProfileUpdatedAt = now
|
|
} else {
|
|
existing.ProfileUpdatedBy = 0
|
|
existing.ProfileUpdatedAt = ""
|
|
}
|
|
|
|
copy := *existing
|
|
return ©, nil
|
|
}
|
|
|
|
// UpdateUserRole 更新用户职位与全局角色
|
|
func (s *MemoryStore) UpdateUserRole(ctx context.Context, id int64, roleID, position string) (*UserAccount, error) {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
|
|
existing, ok := s.users[id]
|
|
if !ok {
|
|
return nil, fmt.Errorf("user not found: %d", id)
|
|
}
|
|
|
|
existing.RoleID = roleID
|
|
existing.Position = position
|
|
existing.UpdatedAt = time.Now().Format("2006-01-02 15:04:05")
|
|
|
|
copy := *existing
|
|
return ©, nil
|
|
}
|
|
|
|
// UpdateUserStatus 更新用户账号状态
|
|
func (s *MemoryStore) UpdateUserStatus(ctx context.Context, id int64, status AccountStatus) (*UserAccount, error) {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
|
|
existing, ok := s.users[id]
|
|
if !ok {
|
|
return nil, fmt.Errorf("user not found: %d", id)
|
|
}
|
|
|
|
existing.Status = status
|
|
existing.UpdatedAt = time.Now().Format("2006-01-02 15:04:05")
|
|
|
|
copy := *existing
|
|
return ©, nil
|
|
}
|
|
|
|
// UpdateUserPassword 更新用户密码并置位/复位首次强制改密
|
|
func (s *MemoryStore) UpdateUserPassword(ctx context.Context, id int64, passwordHash string, mustChangePassword bool) (*UserAccount, error) {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
|
|
existing, ok := s.users[id]
|
|
if !ok {
|
|
return nil, fmt.Errorf("user not found: %d", id)
|
|
}
|
|
|
|
existing.PasswordHash = passwordHash
|
|
existing.MustChangePassword = mustChangePassword
|
|
existing.UpdatedAt = time.Now().Format("2006-01-02 15:04:05")
|
|
|
|
copy := *existing
|
|
return ©, nil
|
|
}
|
|
|
|
// ListActiveUserDirectory 返回 active 用户的轻量目录。
|
|
func (s *MemoryStore) ListActiveUserDirectory(ctx context.Context) ([]UserDirectoryEntry, error) {
|
|
s.mu.RLock()
|
|
defer s.mu.RUnlock()
|
|
out := make([]UserDirectoryEntry, 0, len(s.users))
|
|
for _, u := range s.users {
|
|
if u.Status != StatusActive {
|
|
continue
|
|
}
|
|
out = append(out, UserDirectoryEntry{
|
|
ID: u.ID,
|
|
Username: u.Username,
|
|
Name: u.Name,
|
|
Department: u.Department,
|
|
Position: u.Position,
|
|
})
|
|
}
|
|
// 稳定排序:按姓名再按 id
|
|
for i := 0; i < len(out); i++ {
|
|
for j := i + 1; j < len(out); j++ {
|
|
if out[j].Name < out[i].Name || (out[j].Name == out[i].Name && out[j].ID < out[i].ID) {
|
|
out[i], out[j] = out[j], out[i]
|
|
}
|
|
}
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
// ListUsers 列出所有用户
|
|
func (s *MemoryStore) ListUsers(ctx context.Context) ([]*UserAccount, error) {
|
|
s.mu.RLock()
|
|
defer s.mu.RUnlock()
|
|
|
|
list := make([]*UserAccount, 0, len(s.users))
|
|
for _, u := range s.users {
|
|
copy := *u
|
|
// 脱敏密码哈希
|
|
copy.PasswordHash = ""
|
|
list = append(list, ©)
|
|
}
|
|
return list, nil
|
|
}
|
|
|
|
// GetConfig 获取准入配置
|
|
func (s *MemoryStore) GetConfig(ctx context.Context) (SystemAuthConfig, error) {
|
|
s.mu.RLock()
|
|
defer s.mu.RUnlock()
|
|
return s.cfg, nil
|
|
}
|
|
|
|
// UpdateConfig 更新准入配置
|
|
func (s *MemoryStore) UpdateConfig(ctx context.Context, cfg SystemAuthConfig) error {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
s.cfg = cfg
|
|
return nil
|
|
}
|