admin area

This commit is contained in:
2026-01-01 12:10:35 +01:00
parent 02c0cc44e1
commit f001df3744
68 changed files with 10324 additions and 32 deletions

View File

@@ -0,0 +1,485 @@
import type { IEntity } from '@core/shared/domain';
import { UserId } from '../value-objects/UserId';
import { Email } from '../value-objects/Email';
import { UserRole } from '../value-objects/UserRole';
import { UserStatus } from '../value-objects/UserStatus';
import { AdminDomainValidationError, AdminDomainInvariantError } from '../errors/AdminDomainError';
export interface AdminUserProps {
id: UserId;
email: Email;
roles: UserRole[];
status: UserStatus;
displayName: string;
createdAt: Date;
updatedAt: Date;
lastLoginAt: Date | undefined;
primaryDriverId: string | undefined;
}
export class AdminUser implements IEntity<UserId> {
readonly id: UserId;
private _email: Email;
private _roles: UserRole[];
private _status: UserStatus;
private _displayName: string;
private _createdAt: Date;
private _updatedAt: Date;
private _lastLoginAt: Date | undefined;
private _primaryDriverId: string | undefined;
private constructor(props: AdminUserProps) {
this.id = props.id;
this._email = props.email;
this._roles = props.roles;
this._status = props.status;
this._displayName = props.displayName;
this._createdAt = props.createdAt;
this._updatedAt = props.updatedAt;
this._lastLoginAt = props.lastLoginAt;
this._primaryDriverId = props.primaryDriverId;
}
/**
* Factory method to create a new AdminUser
* Validates all business rules and invariants
*/
static create(props: {
id: string;
email: string;
roles: string[];
status: string;
displayName: string;
createdAt?: Date;
updatedAt?: Date;
lastLoginAt?: Date;
primaryDriverId?: string;
}): AdminUser {
// Validate required fields
if (!props.id || props.id.trim().length === 0) {
throw new AdminDomainValidationError('User ID is required');
}
if (!props.email || props.email.trim().length === 0) {
throw new AdminDomainValidationError('Email is required');
}
if (!props.roles || props.roles.length === 0) {
throw new AdminDomainValidationError('At least one role is required');
}
if (!props.status || props.status.trim().length === 0) {
throw new AdminDomainValidationError('Status is required');
}
if (!props.displayName || props.displayName.trim().length === 0) {
throw new AdminDomainValidationError('Display name is required');
}
// Validate display name length
const trimmedName = props.displayName.trim();
if (trimmedName.length < 2 || trimmedName.length > 100) {
throw new AdminDomainValidationError('Display name must be between 2 and 100 characters');
}
// Create value objects
const id = UserId.fromString(props.id);
const email = Email.fromString(props.email);
const roles = props.roles.map(role => UserRole.fromString(role));
const status = UserStatus.fromString(props.status);
// Validate role hierarchy - ensure no duplicate roles
const uniqueRoles = new Set(roles.map(r => r.toString()));
if (uniqueRoles.size !== roles.length) {
throw new AdminDomainValidationError('Duplicate roles are not allowed');
}
const now = props.createdAt ?? new Date();
return new AdminUser({
id,
email,
roles,
status,
displayName: trimmedName,
createdAt: now,
updatedAt: props.updatedAt ?? now,
lastLoginAt: props.lastLoginAt ?? undefined,
primaryDriverId: props.primaryDriverId ?? undefined,
});
}
/**
* Rehydrate from storage
*/
static rehydrate(props: {
id: string;
email: string;
roles: string[];
status: string;
displayName: string;
createdAt: Date;
updatedAt: Date;
lastLoginAt?: Date;
primaryDriverId?: string;
}): AdminUser {
return this.create(props);
}
// Getters
get email(): Email {
return this._email;
}
get roles(): UserRole[] {
return [...this._roles];
}
get status(): UserStatus {
return this._status;
}
get displayName(): string {
return this._displayName;
}
get createdAt(): Date {
return new Date(this._createdAt.getTime());
}
get updatedAt(): Date {
return new Date(this._updatedAt.getTime());
}
get lastLoginAt(): Date | undefined {
return this._lastLoginAt ? new Date(this._lastLoginAt.getTime()) : undefined;
}
get primaryDriverId(): string | undefined {
return this._primaryDriverId;
}
// Domain methods
/**
* Add a role to the user
* Cannot add duplicate roles
* Cannot add owner role if user already has other roles
*/
addRole(role: UserRole): void {
if (this._roles.some(r => r.equals(role))) {
throw new AdminDomainInvariantError(`Role ${role.value} is already assigned`);
}
// If adding owner role, user must have no other roles
if (role.value === 'owner' && this._roles.length > 0) {
throw new AdminDomainInvariantError('Cannot add owner role to user with existing roles');
}
// If user has owner role, cannot add other roles
if (this._roles.some(r => r.value === 'owner')) {
throw new AdminDomainInvariantError('Owner cannot have additional roles');
}
this._roles.push(role);
this._updatedAt = new Date();
}
/**
* Remove a role from the user
* Cannot remove the last role
* Cannot remove owner role (must be transferred first)
*/
removeRole(role: UserRole): void {
const roleIndex = this._roles.findIndex(r => r.equals(role));
if (roleIndex === -1) {
throw new AdminDomainInvariantError(`Role ${role.value} not found`);
}
if (this._roles.length === 1) {
throw new AdminDomainInvariantError('Cannot remove the last role from user');
}
if (role.value === 'owner') {
throw new AdminDomainInvariantError('Cannot remove owner role. Transfer ownership first.');
}
this._roles.splice(roleIndex, 1);
this._updatedAt = new Date();
}
/**
* Update user status
*/
updateStatus(newStatus: UserStatus): void {
if (this._status.equals(newStatus)) {
throw new AdminDomainInvariantError(`User already has status ${newStatus.value}`);
}
this._status = newStatus;
this._updatedAt = new Date();
}
/**
* Check if user has a specific role
*/
hasRole(roleValue: string): boolean {
return this._roles.some(r => r.value === roleValue);
}
/**
* Check if user is a system administrator
*/
isSystemAdmin(): boolean {
return this._roles.some(r => r.isSystemAdmin());
}
/**
* Check if user has higher authority than another user
*/
hasHigherAuthorityThan(other: AdminUser): boolean {
// Get highest role for each user
const hierarchy: Record<string, number> = {
user: 0,
admin: 1,
owner: 2,
};
const myHighest = Math.max(...this._roles.map(r => hierarchy[r.value] ?? 0));
const otherHighest = Math.max(...other._roles.map(r => hierarchy[r.value] ?? 0));
return myHighest > otherHighest;
}
/**
* Update last login timestamp
*/
recordLogin(): void {
this._lastLoginAt = new Date();
this._updatedAt = new Date();
}
/**
* Update display name (only for admin operations)
*/
updateDisplayName(newName: string): void {
const trimmed = newName.trim();
if (trimmed.length < 2 || trimmed.length > 100) {
throw new AdminDomainValidationError('Display name must be between 2 and 100 characters');
}
this._displayName = trimmed;
this._updatedAt = new Date();
}
/**
* Update email
*/
updateEmail(newEmail: Email): void {
if (this._email.equals(newEmail)) {
throw new AdminDomainInvariantError('Email is already the same');
}
this._email = newEmail;
this._updatedAt = new Date();
}
/**
* Check if user is active
*/
isActive(): boolean {
return this._status.isActive();
}
/**
* Suspend user
*/
suspend(): void {
if (this._status.isSuspended()) {
throw new AdminDomainInvariantError('User is already suspended');
}
if (this._status.isDeleted()) {
throw new AdminDomainInvariantError('Cannot suspend a deleted user');
}
this._status = UserStatus.create('suspended');
this._updatedAt = new Date();
}
/**
* Activate user
*/
activate(): void {
if (this._status.isActive()) {
throw new AdminDomainInvariantError('User is already active');
}
if (this._status.isDeleted()) {
throw new AdminDomainInvariantError('Cannot activate a deleted user');
}
this._status = UserStatus.create('active');
this._updatedAt = new Date();
}
/**
* Soft delete user
*/
delete(): void {
if (this._status.isDeleted()) {
throw new AdminDomainInvariantError('User is already deleted');
}
this._status = UserStatus.create('deleted');
this._updatedAt = new Date();
}
/**
* Get role display names
*/
getRoleDisplayNames(): string[] {
return this._roles.map(r => {
switch (r.value) {
case 'owner': return 'Owner';
case 'admin': return 'Admin';
case 'user': return 'User';
default: return r.value;
}
});
}
/**
* Check if this user can manage another user
* Owner can manage everyone (including self)
* Admin can manage users but not admins/owners (including self)
* User can manage self only
*/
canManage(target: AdminUser): boolean {
// Owner can manage everyone
if (this.hasRole('owner')) {
return true;
}
// Admin can manage non-admin users
if (this.hasRole('admin')) {
// Cannot manage admins/owners (including self)
if (target.isSystemAdmin()) {
return false;
}
// Can manage non-admin users
return true;
}
// User can only manage self
return this.id.equals(target.id);
}
/**
* Check if this user can modify roles of target user
* Only owner can modify roles
*/
canModifyRoles(target: AdminUser): boolean {
// Only owner can modify roles
if (!this.hasRole('owner')) {
return false;
}
// Cannot modify own roles (prevents accidental lockout)
if (this.id.equals(target.id)) {
return false;
}
return true;
}
/**
* Check if this user can change status of target user
* Owner can change anyone's status
* Admin can change user status but not other admins/owners
*/
canChangeStatus(target: AdminUser): boolean {
if (this.id.equals(target.id)) {
return false; // Cannot change own status
}
if (this.hasRole('owner')) {
return true;
}
if (this.hasRole('admin')) {
return !target.isSystemAdmin();
}
return false;
}
/**
* Check if this user can delete target user
* Owner can delete anyone except self
* Admin can delete users but not admins/owners
*/
canDelete(target: AdminUser): boolean {
if (this.id.equals(target.id)) {
return false; // Cannot delete self
}
if (this.hasRole('owner')) {
return true;
}
if (this.hasRole('admin')) {
return !target.isSystemAdmin();
}
return false;
}
/**
* Get summary for display
*/
toSummary(): {
id: string;
email: string;
displayName: string;
roles: string[];
status: string;
isSystemAdmin: boolean;
lastLoginAt?: Date;
} {
const summary: {
id: string;
email: string;
displayName: string;
roles: string[];
status: string;
isSystemAdmin: boolean;
lastLoginAt?: Date;
} = {
id: this.id.value,
email: this._email.value,
displayName: this._displayName,
roles: this._roles.map(r => r.value),
status: this._status.value,
isSystemAdmin: this.isSystemAdmin(),
};
if (this._lastLoginAt) {
summary.lastLoginAt = this._lastLoginAt;
}
return summary;
}
/**
* Equals comparison
*/
equals(other?: AdminUser): boolean {
if (!other) {
return false;
}
return this.id.equals(other.id);
}
}