Files
NianAIGC/backend/internal/administration/service.go

219 lines
7.5 KiB
Go

package administration
import (
"context"
"crypto/rand"
"encoding/hex"
"fmt"
"strings"
"time"
)
type Service struct {
store Store
id func(string) string
now func() time.Time
hash func(string) (PasswordHash, error)
}
type Option func(*Service)
func WithIDGenerator(fn func(string) string) Option { return func(s *Service) { s.id = fn } }
func WithClock(fn func() time.Time) Option { return func(s *Service) { s.now = fn } }
func WithPasswordHasher(fn func(string) (PasswordHash, error)) Option {
return func(s *Service) { s.hash = fn }
}
func NewService(store Store, options ...Option) *Service {
s := &Service{store: store, id: randomID, now: time.Now, hash: HashPassword}
for _, option := range options {
option(s)
}
return s
}
func randomID(prefix string) string {
raw := make([]byte, 12)
if _, err := rand.Read(raw); err != nil {
return fmt.Sprintf("%s-%d", prefix, time.Now().UnixNano())
}
return prefix + "-" + hex.EncodeToString(raw)
}
func (s *Service) ListAccounts(ctx context.Context, actor Actor, filters AccountFilters) ([]AccountProjection, error) {
if actor.Role == RoleOrganizationAdmin {
if actor.OrganizationID == "" {
return nil, problem(ErrorForbidden, "当前账号没有组织归属。")
}
filters.OrganizationID, filters.Role = actor.OrganizationID, RoleUser
} else if actor.Role != RoleSuperAdmin {
return nil, problem(ErrorForbidden, "需要管理员权限。")
}
accounts, err := s.store.ListAccounts(ctx, filters)
if err != nil {
return nil, infrastructure("list accounts", err)
}
out := make([]AccountProjection, len(accounts))
for i, a := range accounts {
out[i] = ProjectAccount(a)
}
return out, nil
}
func (s *Service) CreateAccount(ctx context.Context, actor Actor, input CreateAccountInput) (Account, error) {
input.Phone = NormalizePhone(input.Phone)
input.DisplayName = strings.TrimSpace(input.DisplayName)
if !ValidPhone(input.Phone) {
return Account{}, problem(ErrorValidation, "手机号格式不正确。")
}
if input.DisplayName == "" {
return Account{}, problem(ErrorValidation, "显示名称不能为空。")
}
if len(input.Password) < 8 {
return Account{}, problem(ErrorValidation, "初始密码至少需要 8 位。")
}
if !validRole(input.Role) {
return Account{}, problem(ErrorValidation, "账号角色不正确。")
}
if actor.Role == RoleOrganizationAdmin {
if input.Role != RoleUser {
return Account{}, problem(ErrorForbidden, "组织管理员只能创建普通用户。")
}
if actor.OrganizationID == "" {
return Account{}, problem(ErrorForbidden, "当前账号没有组织归属。")
}
input.OrganizationID = actor.OrganizationID
} else if actor.Role != RoleSuperAdmin {
return Account{}, problem(ErrorForbidden, "需要管理员权限。")
}
if err := s.validateMembership(ctx, input.Role, input.OrganizationID); err != nil {
return Account{}, err
}
hashed, err := s.hash(input.Password)
if err != nil {
return Account{}, infrastructure("hash password", err)
}
now := s.now()
a := Account{ID: s.id("user"), Phone: input.Phone, DisplayName: input.DisplayName, Role: input.Role, OrganizationID: input.OrganizationID, Status: StatusActive, PasswordHash: hashed.Hash, PasswordSalt: hashed.Salt, SessionVersion: 1, LegacySubject: strings.TrimSpace(input.LegacySubject), CreatedAt: now, UpdatedAt: now}
created, err := s.store.CreateAccount(ctx, a)
return created, infrastructure("create account", err)
}
func (s *Service) UpdateAccount(ctx context.Context, actor Actor, id string, patch UpdateAccountInput) (Account, error) {
current, found, err := s.store.GetAccount(ctx, id)
if err != nil {
return Account{}, infrastructure("get account", err)
}
if !found {
return Account{}, problem(ErrorNotFound, "账号不存在。")
}
if err := AuthorizeAccountTarget(actor, current); err != nil {
return Account{}, err
}
if actor.Role != RoleSuperAdmin && ((patch.Role != nil && *patch.Role != current.Role) || patch.OrganizationID != nil) {
return Account{}, problem(ErrorForbidden, "组织管理员不能修改账号角色或归属。")
}
next := current
mutates := false
if patch.DisplayName != nil {
name := strings.TrimSpace(*patch.DisplayName)
if name == "" {
return Account{}, problem(ErrorValidation, "显示名称不能为空。")
}
next.DisplayName = name
}
if patch.Role != nil {
if !validRole(*patch.Role) {
return Account{}, problem(ErrorValidation, "账号角色不正确。")
}
next.Role = *patch.Role
mutates = true
}
if patch.OrganizationID != nil {
next.OrganizationID = strings.TrimSpace(*patch.OrganizationID)
mutates = true
}
if patch.Status != nil {
if !validStatus(*patch.Status) {
return Account{}, problem(ErrorValidation, "账号状态不正确。")
}
next.Status = *patch.Status
mutates = true
}
if err := s.validateMembership(ctx, next.Role, next.OrganizationID); err != nil {
return Account{}, err
}
if patch.Password != nil {
if len(*patch.Password) < 8 {
return Account{}, problem(ErrorValidation, "新密码至少需要 8 位。")
}
hashed, err := s.hash(*patch.Password)
if err != nil {
return Account{}, infrastructure("hash password", err)
}
next.PasswordHash, next.PasswordSalt = hashed.Hash, hashed.Salt
mutates = true
}
if patch.ClearLoginLock {
next.FailedLoginCount, next.LockedUntil = 0, nil
}
if mutates {
next.SessionVersion++
}
next.UpdatedAt = s.now()
updated, err := s.store.UpdateAccount(ctx, next)
return updated, infrastructure("update account", err)
}
func (s *Service) SetAccountStatus(ctx context.Context, actor Actor, id string, status Status) (Account, error) {
if actor.ID == id && status == StatusDisabled {
return Account{}, problem(ErrorValidation, "不能停用当前登录账号。")
}
return s.UpdateAccount(ctx, actor, id, UpdateAccountInput{Status: &status, ClearLoginLock: status == StatusActive})
}
func (s *Service) ResetPassword(ctx context.Context, actor Actor, id, password string) (Account, error) {
return s.UpdateAccount(ctx, actor, id, UpdateAccountInput{Password: &password, ClearLoginLock: true})
}
func (s *Service) DeleteAccount(ctx context.Context, actor Actor, id string) (string, error) {
current, found, err := s.store.GetAccount(ctx, id)
if err != nil {
return "", infrastructure("get account", err)
}
if !found {
return "", problem(ErrorNotFound, "账号不存在。")
}
if err := AuthorizeAccountTarget(actor, current); err != nil {
return "", err
}
if actor.ID == id {
return "", problem(ErrorValidation, "不能删除当前登录账号。")
}
if current.Role == RoleSuperAdmin {
return "", problem(ErrorValidation, "不能直接删除超级管理员账号。")
}
archive := "archive:global"
if current.OrganizationID != "" {
org, found, err := s.store.GetOrganization(ctx, current.OrganizationID)
if err != nil {
return "", infrastructure("get organization", err)
}
if found {
archive = org.ArchiveOwnerID
}
}
if err := s.store.DeleteAccount(ctx, id, archive); err != nil {
return "", infrastructure("delete account", err)
}
return archive, nil
}
func (s *Service) validateMembership(ctx context.Context, role Role, organizationID string) error {
if role == RoleSuperAdmin && organizationID == "" {
return nil
}
if organizationID == "" {
return problem(ErrorValidation, "普通账号必须归属有效组织。")
}
org, found, err := s.store.GetOrganization(ctx, organizationID)
if err != nil {
return infrastructure("get organization", err)
}
if !found || org.Status != StatusActive {
return problem(ErrorValidation, "账号归属的组织不存在或已停用。")
}
return nil
}