174 lines
4.8 KiB
Go
174 lines
4.8 KiB
Go
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
|
|
}
|
|
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}
|
|
}
|