feat: add administration domain core
This commit is contained in:
1 parent
065dcd955a
commit
cea2751dc5
9 files changed
+1135
No files matched your search
@@ -0,0 +1,218 @@
|
||||
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
|
||||
}
|
||||
Reference in new issue
Block a user