package administration import ( "context" "errors" "time" ) type Role string const ( RoleSuperAdmin Role = "super_admin" RoleOrganizationAdmin Role = "organization_admin" RoleUser Role = "user" ) type Status string const ( StatusActive Status = "active" StatusDisabled Status = "disabled" ) type Actor struct { ID string Role Role OrganizationID string } type Account struct { ID, Phone, DisplayName string Role Role OrganizationID string Status Status PasswordHash, PasswordSalt string FailedLoginCount int LockedUntil *time.Time SessionVersion int LastLoginAt *time.Time LegacySubject string CreatedAt, UpdatedAt time.Time } type Organization struct { ID, Name string Status Status ArchiveOwnerID string CreatedAt, UpdatedAt time.Time } type AccountProjection struct { ID, Phone, DisplayName string Role Role OrganizationID string Status Status CreatedAt time.Time LastLoginAt, LockedUntil *time.Time } type OrganizationProjection struct { ID, Name string Status Status } type AccountFilters struct { OrganizationID string Role Role IncludeDisabled bool } type PasswordHash struct{ Hash, Salt string } type Store interface { ListAccounts(context.Context, AccountFilters) ([]Account, error) GetAccount(context.Context, string) (Account, bool, error) CreateAccount(context.Context, Account) (Account, error) UpdateAccount(context.Context, Account) (Account, error) DeleteAccount(context.Context, string, string) error ListOrganizations(context.Context, bool) ([]Organization, error) GetOrganization(context.Context, string) (Organization, bool, error) CreateOrganization(context.Context, Organization) (Organization, error) UpdateOrganization(context.Context, Organization) (Organization, error) DeleteOrganization(context.Context, string) error CountOrganizationMembers(context.Context, string) (int, error) } type ErrorKind string const ( ErrorValidation ErrorKind = "validation" ErrorForbidden ErrorKind = "forbidden" ErrorNotFound ErrorKind = "not_found" ErrorConflict ErrorKind = "conflict" ErrorInfrastructure ErrorKind = "infrastructure" ) type Error struct { Kind ErrorKind Message string Err error } func (e *Error) Error() string { return e.Message } func (e *Error) Unwrap() error { return e.Err } func IsKind(err error, kind ErrorKind) bool { var target *Error return errors.As(err, &target) && target.Kind == kind } func StatusCode(err error) int { var target *Error if !errors.As(err, &target) { return 500 } switch target.Kind { case ErrorValidation: return 400 case ErrorForbidden: return 403 case ErrorNotFound: return 404 case ErrorConflict: return 409 default: return 500 } } func problem(kind ErrorKind, message string) error { return &Error{Kind: kind, Message: message} } func infrastructure(operation string, err error) error { if err == nil { return nil } var typed *Error if errors.As(err, &typed) { return err } return &Error{Kind: ErrorInfrastructure, Message: operation + ": " + err.Error(), Err: err} } type CreateAccountInput struct { Phone, DisplayName, Password string Role Role OrganizationID, LegacySubject string } type UpdateAccountInput struct { DisplayName *string Role *Role OrganizationID *string Status *Status Password *string ClearLoginLock bool } // AccountUpdate is the storage intent for an account PATCH. Implementations // must only mutate fields whose pointers/flags are present. In particular, // security state that is not part of this intent must remain database-owned so // a concurrent password change or login attempt cannot be overwritten by a // stale account snapshot. type AccountUpdate struct { Actor Actor DisplayName *string Role *Role OrganizationID *string Status *Status PasswordHash *PasswordHash ClearLoginLock bool IncrementSessionVersion bool UpdatedAt time.Time } // AtomicAccountUpdater is an optional deep storage seam for implementations // that can apply AccountUpdate atomically. Store remains backward compatible // for process-local/test adapters; durable stores should implement this seam. type AtomicAccountUpdater interface { ApplyAccountUpdate(context.Context, string, AccountUpdate) (Account, error) } type UpdateOrganizationInput struct { Name *string Status *Status } func AuthorizeAccountTarget(actor Actor, target Account) error { if actor.Role == RoleSuperAdmin { return nil } if actor.Role == RoleOrganizationAdmin && target.Role == RoleUser && actor.OrganizationID != "" && actor.OrganizationID == target.OrganizationID { return nil } return problem(ErrorForbidden, "组织管理员只能管理本组织普通用户。") } func requireSuper(actor Actor) error { if actor.Role != RoleSuperAdmin { return problem(ErrorForbidden, "需要超级管理员权限。") } return nil } func validRole(role Role) bool { return role == RoleSuperAdmin || role == RoleOrganizationAdmin || role == RoleUser } func validStatus(status Status) bool { return status == StatusActive || status == StatusDisabled } func ProjectAccount(a Account) AccountProjection { return AccountProjection{ID: a.ID, Phone: a.Phone, DisplayName: a.DisplayName, Role: a.Role, OrganizationID: a.OrganizationID, Status: a.Status, CreatedAt: a.CreatedAt, LastLoginAt: a.LastLoginAt, LockedUntil: a.LockedUntil} } func ProjectOrganization(o Organization) OrganizationProjection { return OrganizationProjection{ID: o.ID, Name: o.Name, Status: o.Status} }