admin area
This commit is contained in:
531
core/admin/domain/entities/AdminUser.test.ts
Normal file
531
core/admin/domain/entities/AdminUser.test.ts
Normal file
@@ -0,0 +1,531 @@
|
||||
import { AdminUser } from './AdminUser';
|
||||
import { UserRole } from '../value-objects/UserRole';
|
||||
|
||||
describe('AdminUser', () => {
|
||||
describe('TDD - Test First', () => {
|
||||
it('should create a valid admin user', () => {
|
||||
// Arrange & Act
|
||||
const user = AdminUser.create({
|
||||
id: 'user-123',
|
||||
email: 'admin@example.com',
|
||||
displayName: 'Admin User',
|
||||
roles: ['owner'],
|
||||
status: 'active',
|
||||
});
|
||||
|
||||
// Assert
|
||||
expect(user.id.value).toBe('user-123');
|
||||
expect(user.email.value).toBe('admin@example.com');
|
||||
expect(user.displayName).toBe('Admin User');
|
||||
expect(user.roles).toHaveLength(1);
|
||||
expect(user.roles[0]!.value).toBe('owner');
|
||||
expect(user.status.value).toBe('active');
|
||||
});
|
||||
|
||||
it('should validate email format', () => {
|
||||
// Arrange & Act
|
||||
const user = AdminUser.create({
|
||||
id: 'user-123',
|
||||
email: 'invalid-email',
|
||||
displayName: 'Test User',
|
||||
roles: ['user'],
|
||||
status: 'active',
|
||||
});
|
||||
|
||||
// Assert - Email should be created but validation happens in value object
|
||||
expect(user.email.value).toBe('invalid-email');
|
||||
});
|
||||
|
||||
it('should detect system admin (owner)', () => {
|
||||
// Arrange
|
||||
const owner = AdminUser.create({
|
||||
id: 'owner-1',
|
||||
email: 'owner@example.com',
|
||||
displayName: 'Owner',
|
||||
roles: ['owner'],
|
||||
status: 'active',
|
||||
});
|
||||
|
||||
const admin = AdminUser.create({
|
||||
id: 'admin-1',
|
||||
email: 'admin@example.com',
|
||||
displayName: 'Admin',
|
||||
roles: ['admin'],
|
||||
status: 'active',
|
||||
});
|
||||
|
||||
const user = AdminUser.create({
|
||||
id: 'user-1',
|
||||
email: 'user@example.com',
|
||||
displayName: 'User',
|
||||
roles: ['user'],
|
||||
status: 'active',
|
||||
});
|
||||
|
||||
// Assert
|
||||
expect(owner.isSystemAdmin()).toBe(true);
|
||||
expect(admin.isSystemAdmin()).toBe(true);
|
||||
expect(user.isSystemAdmin()).toBe(false);
|
||||
});
|
||||
|
||||
it('should handle multiple roles', () => {
|
||||
// Arrange & Act
|
||||
const user = AdminUser.create({
|
||||
id: 'user-123',
|
||||
email: 'multi@example.com',
|
||||
displayName: 'Multi Role',
|
||||
roles: ['owner', 'admin'],
|
||||
status: 'active',
|
||||
});
|
||||
|
||||
// Assert
|
||||
expect(user.roles).toHaveLength(2);
|
||||
expect(user.roles.map(r => r.value)).toContain('owner');
|
||||
expect(user.roles.map(r => r.value)).toContain('admin');
|
||||
});
|
||||
|
||||
it('should handle suspended status', () => {
|
||||
// Arrange & Act
|
||||
const user = AdminUser.create({
|
||||
id: 'user-123',
|
||||
email: 'suspended@example.com',
|
||||
displayName: 'Suspended User',
|
||||
roles: ['user'],
|
||||
status: 'suspended',
|
||||
});
|
||||
|
||||
// Assert
|
||||
expect(user.status.value).toBe('suspended');
|
||||
expect(user.isActive()).toBe(false);
|
||||
});
|
||||
|
||||
it('should handle optional fields', () => {
|
||||
// Arrange & Act
|
||||
const user = AdminUser.create({
|
||||
id: 'user-123',
|
||||
email: 'minimal@example.com',
|
||||
displayName: 'Minimal User',
|
||||
roles: ['user'],
|
||||
status: 'active',
|
||||
});
|
||||
|
||||
// Assert
|
||||
expect(user.primaryDriverId).toBeUndefined();
|
||||
expect(user.lastLoginAt).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should handle all optional fields', () => {
|
||||
// Arrange
|
||||
const now = new Date();
|
||||
|
||||
// Act
|
||||
const user = AdminUser.create({
|
||||
id: 'user-123',
|
||||
email: 'full@example.com',
|
||||
displayName: 'Full User',
|
||||
roles: ['user'],
|
||||
status: 'active',
|
||||
primaryDriverId: 'driver-456',
|
||||
lastLoginAt: now,
|
||||
});
|
||||
|
||||
// Assert
|
||||
expect(user.primaryDriverId).toBe('driver-456');
|
||||
expect(user.lastLoginAt).toEqual(now);
|
||||
});
|
||||
|
||||
it('should handle createdAt and updatedAt', () => {
|
||||
// Arrange & Act
|
||||
const user = AdminUser.create({
|
||||
id: 'user-123',
|
||||
email: 'test@example.com',
|
||||
displayName: 'Test User',
|
||||
roles: ['user'],
|
||||
status: 'active',
|
||||
});
|
||||
|
||||
// Assert
|
||||
expect(user.createdAt).toBeInstanceOf(Date);
|
||||
expect(user.updatedAt).toBeInstanceOf(Date);
|
||||
});
|
||||
|
||||
it('should handle role assignment with validation', () => {
|
||||
// Arrange & Act
|
||||
const user = AdminUser.create({
|
||||
id: 'user-123',
|
||||
email: 'test@example.com',
|
||||
displayName: 'Test User',
|
||||
roles: ['user'],
|
||||
status: 'active',
|
||||
});
|
||||
|
||||
// Assert - Should accept any role string (validation happens in value object)
|
||||
expect(user.roles).toHaveLength(1);
|
||||
expect(user.roles[0]!.value).toBe('user');
|
||||
});
|
||||
|
||||
it('should handle status changes', () => {
|
||||
// Arrange
|
||||
const user = AdminUser.create({
|
||||
id: 'user-123',
|
||||
email: 'test@example.com',
|
||||
displayName: 'Test User',
|
||||
roles: ['user'],
|
||||
status: 'active',
|
||||
});
|
||||
|
||||
// Act - Use domain method to change status
|
||||
user.suspend();
|
||||
|
||||
// Assert
|
||||
expect(user.status.value).toBe('suspended');
|
||||
});
|
||||
|
||||
it('should check if user is active', () => {
|
||||
// Arrange
|
||||
const activeUser = AdminUser.create({
|
||||
id: 'user-123',
|
||||
email: 'active@example.com',
|
||||
displayName: 'Active User',
|
||||
roles: ['user'],
|
||||
status: 'active',
|
||||
});
|
||||
|
||||
const suspendedUser = AdminUser.create({
|
||||
id: 'user-456',
|
||||
email: 'suspended@example.com',
|
||||
displayName: 'Suspended User',
|
||||
roles: ['user'],
|
||||
status: 'suspended',
|
||||
});
|
||||
|
||||
// Assert
|
||||
expect(activeUser.isActive()).toBe(true);
|
||||
expect(suspendedUser.isActive()).toBe(false);
|
||||
});
|
||||
|
||||
it('should handle role management', () => {
|
||||
// Arrange
|
||||
const user = AdminUser.create({
|
||||
id: 'user-123',
|
||||
email: 'test@example.com',
|
||||
displayName: 'Test User',
|
||||
roles: ['user'],
|
||||
status: 'active',
|
||||
});
|
||||
|
||||
// Act
|
||||
user.addRole(UserRole.fromString('admin'));
|
||||
|
||||
// Assert
|
||||
expect(user.roles).toHaveLength(2);
|
||||
expect(user.hasRole('admin')).toBe(true);
|
||||
});
|
||||
|
||||
it('should handle display name updates', () => {
|
||||
// Arrange
|
||||
const user = AdminUser.create({
|
||||
id: 'user-123',
|
||||
email: 'test@example.com',
|
||||
displayName: 'Old Name',
|
||||
roles: ['user'],
|
||||
status: 'active',
|
||||
});
|
||||
|
||||
// Act
|
||||
user.updateDisplayName('New Name');
|
||||
|
||||
// Assert
|
||||
expect(user.displayName).toBe('New Name');
|
||||
});
|
||||
|
||||
it('should handle login recording', () => {
|
||||
// Arrange
|
||||
const user = AdminUser.create({
|
||||
id: 'user-123',
|
||||
email: 'test@example.com',
|
||||
displayName: 'Test User',
|
||||
roles: ['user'],
|
||||
status: 'active',
|
||||
});
|
||||
|
||||
const beforeLogin = user.lastLoginAt;
|
||||
|
||||
// Act
|
||||
user.recordLogin();
|
||||
|
||||
// Assert
|
||||
expect(user.lastLoginAt).toBeDefined();
|
||||
expect(user.lastLoginAt).not.toEqual(beforeLogin);
|
||||
});
|
||||
|
||||
it('should handle summary generation', () => {
|
||||
// Arrange
|
||||
const user = AdminUser.create({
|
||||
id: 'user-123',
|
||||
email: 'test@example.com',
|
||||
displayName: 'Test User',
|
||||
roles: ['owner'],
|
||||
status: 'active',
|
||||
lastLoginAt: new Date(),
|
||||
});
|
||||
|
||||
// Act
|
||||
const summary = user.toSummary();
|
||||
|
||||
// Assert
|
||||
expect(summary.id).toBe('user-123');
|
||||
expect(summary.email).toBe('test@example.com');
|
||||
expect(summary.displayName).toBe('Test User');
|
||||
expect(summary.roles).toEqual(['owner']);
|
||||
expect(summary.status).toBe('active');
|
||||
expect(summary.isSystemAdmin).toBe(true);
|
||||
expect(summary.lastLoginAt).toBeDefined();
|
||||
});
|
||||
|
||||
it('should handle equality comparison', () => {
|
||||
// Arrange
|
||||
const user1 = AdminUser.create({
|
||||
id: 'user-123',
|
||||
email: 'test@example.com',
|
||||
displayName: 'Test User',
|
||||
roles: ['user'],
|
||||
status: 'active',
|
||||
});
|
||||
|
||||
const user2 = AdminUser.create({
|
||||
id: 'user-123',
|
||||
email: 'test@example.com',
|
||||
displayName: 'Test User',
|
||||
roles: ['user'],
|
||||
status: 'active',
|
||||
});
|
||||
|
||||
const user3 = AdminUser.create({
|
||||
id: 'user-456',
|
||||
email: 'other@example.com',
|
||||
displayName: 'Other User',
|
||||
roles: ['user'],
|
||||
status: 'active',
|
||||
});
|
||||
|
||||
// Assert
|
||||
expect(user1.equals(user2)).toBe(true);
|
||||
expect(user1.equals(user3)).toBe(false);
|
||||
expect(user1.equals(undefined)).toBe(false);
|
||||
});
|
||||
|
||||
it('should handle management permissions', () => {
|
||||
// Arrange
|
||||
const owner = AdminUser.create({
|
||||
id: 'owner-1',
|
||||
email: 'owner@example.com',
|
||||
displayName: 'Owner',
|
||||
roles: ['owner'],
|
||||
status: 'active',
|
||||
});
|
||||
|
||||
const admin = AdminUser.create({
|
||||
id: 'admin-1',
|
||||
email: 'admin@example.com',
|
||||
displayName: 'Admin',
|
||||
roles: ['admin'],
|
||||
status: 'active',
|
||||
});
|
||||
|
||||
const user = AdminUser.create({
|
||||
id: 'user-1',
|
||||
email: 'user@example.com',
|
||||
displayName: 'User',
|
||||
roles: ['user'],
|
||||
status: 'active',
|
||||
});
|
||||
|
||||
// Assert - Owner can manage everyone
|
||||
expect(owner.canManage(admin)).toBe(true);
|
||||
expect(owner.canManage(user)).toBe(true);
|
||||
expect(owner.canManage(owner)).toBe(true); // Can manage self
|
||||
|
||||
// Admin can manage users but not admins/owners
|
||||
expect(admin.canManage(user)).toBe(true);
|
||||
expect(admin.canManage(admin)).toBe(false);
|
||||
expect(admin.canManage(owner)).toBe(false);
|
||||
|
||||
// User cannot manage anyone except self
|
||||
expect(user.canManage(user)).toBe(true);
|
||||
expect(user.canManage(admin)).toBe(false);
|
||||
expect(user.canManage(owner)).toBe(false);
|
||||
});
|
||||
|
||||
it('should handle role modification permissions', () => {
|
||||
// Arrange
|
||||
const owner = AdminUser.create({
|
||||
id: 'owner-1',
|
||||
email: 'owner@example.com',
|
||||
displayName: 'Owner',
|
||||
roles: ['owner'],
|
||||
status: 'active',
|
||||
});
|
||||
|
||||
const admin = AdminUser.create({
|
||||
id: 'admin-1',
|
||||
email: 'admin@example.com',
|
||||
displayName: 'Admin',
|
||||
roles: ['admin'],
|
||||
status: 'active',
|
||||
});
|
||||
|
||||
const user = AdminUser.create({
|
||||
id: 'user-1',
|
||||
email: 'user@example.com',
|
||||
displayName: 'User',
|
||||
roles: ['user'],
|
||||
status: 'active',
|
||||
});
|
||||
|
||||
// Assert - Only owner can modify roles
|
||||
expect(owner.canModifyRoles(user)).toBe(true);
|
||||
expect(owner.canModifyRoles(admin)).toBe(true);
|
||||
expect(owner.canModifyRoles(owner)).toBe(false); // Cannot modify own roles
|
||||
|
||||
expect(admin.canModifyRoles(user)).toBe(false);
|
||||
expect(user.canModifyRoles(user)).toBe(false);
|
||||
});
|
||||
|
||||
it('should handle status change permissions', () => {
|
||||
// Arrange
|
||||
const owner = AdminUser.create({
|
||||
id: 'owner-1',
|
||||
email: 'owner@example.com',
|
||||
displayName: 'Owner',
|
||||
roles: ['owner'],
|
||||
status: 'active',
|
||||
});
|
||||
|
||||
const admin = AdminUser.create({
|
||||
id: 'admin-1',
|
||||
email: 'admin@example.com',
|
||||
displayName: 'Admin',
|
||||
roles: ['admin'],
|
||||
status: 'active',
|
||||
});
|
||||
|
||||
const user = AdminUser.create({
|
||||
id: 'user-1',
|
||||
email: 'user@example.com',
|
||||
displayName: 'User',
|
||||
roles: ['user'],
|
||||
status: 'active',
|
||||
});
|
||||
|
||||
// Assert - Owner can change anyone's status
|
||||
expect(owner.canChangeStatus(user)).toBe(true);
|
||||
expect(owner.canChangeStatus(admin)).toBe(true);
|
||||
expect(owner.canChangeStatus(owner)).toBe(false); // Cannot change own status
|
||||
|
||||
// Admin can change user status but not admin/owner
|
||||
expect(admin.canChangeStatus(user)).toBe(true);
|
||||
expect(admin.canChangeStatus(admin)).toBe(false);
|
||||
expect(admin.canChangeStatus(owner)).toBe(false);
|
||||
|
||||
// User cannot change status
|
||||
expect(user.canChangeStatus(user)).toBe(false);
|
||||
expect(user.canChangeStatus(admin)).toBe(false);
|
||||
expect(user.canChangeStatus(owner)).toBe(false);
|
||||
});
|
||||
|
||||
it('should handle deletion permissions', () => {
|
||||
// Arrange
|
||||
const owner = AdminUser.create({
|
||||
id: 'owner-1',
|
||||
email: 'owner@example.com',
|
||||
displayName: 'Owner',
|
||||
roles: ['owner'],
|
||||
status: 'active',
|
||||
});
|
||||
|
||||
const admin = AdminUser.create({
|
||||
id: 'admin-1',
|
||||
email: 'admin@example.com',
|
||||
displayName: 'Admin',
|
||||
roles: ['admin'],
|
||||
status: 'active',
|
||||
});
|
||||
|
||||
const user = AdminUser.create({
|
||||
id: 'user-1',
|
||||
email: 'user@example.com',
|
||||
displayName: 'User',
|
||||
roles: ['user'],
|
||||
status: 'active',
|
||||
});
|
||||
|
||||
// Assert - Owner can delete anyone except self
|
||||
expect(owner.canDelete(user)).toBe(true);
|
||||
expect(owner.canDelete(admin)).toBe(true);
|
||||
expect(owner.canDelete(owner)).toBe(false); // Cannot delete self
|
||||
|
||||
// Admin can delete users but not admins/owners
|
||||
expect(admin.canDelete(user)).toBe(true);
|
||||
expect(admin.canDelete(admin)).toBe(false);
|
||||
expect(admin.canDelete(owner)).toBe(false);
|
||||
|
||||
// User cannot delete anyone
|
||||
expect(user.canDelete(user)).toBe(false);
|
||||
expect(user.canDelete(admin)).toBe(false);
|
||||
expect(user.canDelete(owner)).toBe(false);
|
||||
});
|
||||
|
||||
it('should handle authority comparison', () => {
|
||||
// Arrange
|
||||
const owner = AdminUser.create({
|
||||
id: 'owner-1',
|
||||
email: 'owner@example.com',
|
||||
displayName: 'Owner',
|
||||
roles: ['owner'],
|
||||
status: 'active',
|
||||
});
|
||||
|
||||
const admin = AdminUser.create({
|
||||
id: 'admin-1',
|
||||
email: 'admin@example.com',
|
||||
displayName: 'Admin',
|
||||
roles: ['admin'],
|
||||
status: 'active',
|
||||
});
|
||||
|
||||
const user = AdminUser.create({
|
||||
id: 'user-1',
|
||||
email: 'user@example.com',
|
||||
displayName: 'User',
|
||||
roles: ['user'],
|
||||
status: 'active',
|
||||
});
|
||||
|
||||
// Assert
|
||||
expect(owner.hasHigherAuthorityThan(admin)).toBe(true);
|
||||
expect(owner.hasHigherAuthorityThan(user)).toBe(true);
|
||||
expect(admin.hasHigherAuthorityThan(user)).toBe(true);
|
||||
expect(admin.hasHigherAuthorityThan(owner)).toBe(false);
|
||||
expect(user.hasHigherAuthorityThan(admin)).toBe(false);
|
||||
});
|
||||
|
||||
it('should handle role display names', () => {
|
||||
// Arrange
|
||||
const user = AdminUser.create({
|
||||
id: 'user-123',
|
||||
email: 'test@example.com',
|
||||
displayName: 'Test User',
|
||||
roles: ['owner', 'admin'],
|
||||
status: 'active',
|
||||
});
|
||||
|
||||
// Act
|
||||
const displayNames = user.getRoleDisplayNames();
|
||||
|
||||
// Assert
|
||||
expect(displayNames).toContain('Owner');
|
||||
expect(displayNames).toContain('Admin');
|
||||
});
|
||||
});
|
||||
});
|
||||
485
core/admin/domain/entities/AdminUser.ts
Normal file
485
core/admin/domain/entities/AdminUser.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user