251 lines
5.5 KiB
TypeScript
251 lines
5.5 KiB
TypeScript
/**
|
|
* User Entity
|
|
*
|
|
* Represents a user account in the Xpeditis platform.
|
|
*
|
|
* Business Rules:
|
|
* - Email must be valid and unique
|
|
* - Password must meet complexity requirements (enforced at application layer)
|
|
* - Users belong to an organization
|
|
* - Role-based access control (Admin, Manager, User, Viewer)
|
|
*/
|
|
|
|
export enum UserRole {
|
|
ADMIN = 'admin', // Full system access
|
|
MANAGER = 'manager', // Manage bookings and users within organization
|
|
USER = 'user', // Create and view bookings
|
|
VIEWER = 'viewer', // Read-only access
|
|
}
|
|
|
|
export interface UserProps {
|
|
id: string;
|
|
organizationId: string;
|
|
email: string;
|
|
passwordHash: string;
|
|
role: UserRole;
|
|
firstName: string;
|
|
lastName: string;
|
|
phoneNumber?: string;
|
|
totpSecret?: string; // For 2FA
|
|
isEmailVerified: boolean;
|
|
isActive: boolean;
|
|
lastLoginAt?: Date;
|
|
createdAt: Date;
|
|
updatedAt: Date;
|
|
}
|
|
|
|
export class User {
|
|
private readonly props: UserProps;
|
|
|
|
private constructor(props: UserProps) {
|
|
this.props = props;
|
|
}
|
|
|
|
/**
|
|
* Factory method to create a new User
|
|
*/
|
|
static create(
|
|
props: Omit<UserProps, 'createdAt' | 'updatedAt' | 'isEmailVerified' | 'isActive' | 'lastLoginAt'>
|
|
): User {
|
|
const now = new Date();
|
|
|
|
// Validate email format (basic validation)
|
|
if (!User.isValidEmail(props.email)) {
|
|
throw new Error('Invalid email format.');
|
|
}
|
|
|
|
return new User({
|
|
...props,
|
|
isEmailVerified: false,
|
|
isActive: true,
|
|
createdAt: now,
|
|
updatedAt: now,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Factory method to reconstitute from persistence
|
|
*/
|
|
static fromPersistence(props: UserProps): User {
|
|
return new User(props);
|
|
}
|
|
|
|
/**
|
|
* Validate email format
|
|
*/
|
|
private static isValidEmail(email: string): boolean {
|
|
const emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
return emailPattern.test(email);
|
|
}
|
|
|
|
// Getters
|
|
get id(): string {
|
|
return this.props.id;
|
|
}
|
|
|
|
get organizationId(): string {
|
|
return this.props.organizationId;
|
|
}
|
|
|
|
get email(): string {
|
|
return this.props.email;
|
|
}
|
|
|
|
get passwordHash(): string {
|
|
return this.props.passwordHash;
|
|
}
|
|
|
|
get role(): UserRole {
|
|
return this.props.role;
|
|
}
|
|
|
|
get firstName(): string {
|
|
return this.props.firstName;
|
|
}
|
|
|
|
get lastName(): string {
|
|
return this.props.lastName;
|
|
}
|
|
|
|
get fullName(): string {
|
|
return `${this.props.firstName} ${this.props.lastName}`;
|
|
}
|
|
|
|
get phoneNumber(): string | undefined {
|
|
return this.props.phoneNumber;
|
|
}
|
|
|
|
get totpSecret(): string | undefined {
|
|
return this.props.totpSecret;
|
|
}
|
|
|
|
get isEmailVerified(): boolean {
|
|
return this.props.isEmailVerified;
|
|
}
|
|
|
|
get isActive(): boolean {
|
|
return this.props.isActive;
|
|
}
|
|
|
|
get lastLoginAt(): Date | undefined {
|
|
return this.props.lastLoginAt;
|
|
}
|
|
|
|
get createdAt(): Date {
|
|
return this.props.createdAt;
|
|
}
|
|
|
|
get updatedAt(): Date {
|
|
return this.props.updatedAt;
|
|
}
|
|
|
|
// Business methods
|
|
has2FAEnabled(): boolean {
|
|
return !!this.props.totpSecret;
|
|
}
|
|
|
|
isAdmin(): boolean {
|
|
return this.props.role === UserRole.ADMIN;
|
|
}
|
|
|
|
isManager(): boolean {
|
|
return this.props.role === UserRole.MANAGER;
|
|
}
|
|
|
|
isRegularUser(): boolean {
|
|
return this.props.role === UserRole.USER;
|
|
}
|
|
|
|
isViewer(): boolean {
|
|
return this.props.role === UserRole.VIEWER;
|
|
}
|
|
|
|
canManageUsers(): boolean {
|
|
return this.props.role === UserRole.ADMIN || this.props.role === UserRole.MANAGER;
|
|
}
|
|
|
|
canCreateBookings(): boolean {
|
|
return (
|
|
this.props.role === UserRole.ADMIN ||
|
|
this.props.role === UserRole.MANAGER ||
|
|
this.props.role === UserRole.USER
|
|
);
|
|
}
|
|
|
|
updatePassword(newPasswordHash: string): void {
|
|
this.props.passwordHash = newPasswordHash;
|
|
this.props.updatedAt = new Date();
|
|
}
|
|
|
|
updateRole(newRole: UserRole): void {
|
|
this.props.role = newRole;
|
|
this.props.updatedAt = new Date();
|
|
}
|
|
|
|
updateFirstName(firstName: string): void {
|
|
if (!firstName || firstName.trim().length === 0) {
|
|
throw new Error('First name cannot be empty.');
|
|
}
|
|
this.props.firstName = firstName.trim();
|
|
this.props.updatedAt = new Date();
|
|
}
|
|
|
|
updateLastName(lastName: string): void {
|
|
if (!lastName || lastName.trim().length === 0) {
|
|
throw new Error('Last name cannot be empty.');
|
|
}
|
|
this.props.lastName = lastName.trim();
|
|
this.props.updatedAt = new Date();
|
|
}
|
|
|
|
updateProfile(firstName: string, lastName: string, phoneNumber?: string): void {
|
|
if (!firstName || firstName.trim().length === 0) {
|
|
throw new Error('First name cannot be empty.');
|
|
}
|
|
if (!lastName || lastName.trim().length === 0) {
|
|
throw new Error('Last name cannot be empty.');
|
|
}
|
|
|
|
this.props.firstName = firstName.trim();
|
|
this.props.lastName = lastName.trim();
|
|
this.props.phoneNumber = phoneNumber;
|
|
this.props.updatedAt = new Date();
|
|
}
|
|
|
|
verifyEmail(): void {
|
|
this.props.isEmailVerified = true;
|
|
this.props.updatedAt = new Date();
|
|
}
|
|
|
|
enable2FA(totpSecret: string): void {
|
|
this.props.totpSecret = totpSecret;
|
|
this.props.updatedAt = new Date();
|
|
}
|
|
|
|
disable2FA(): void {
|
|
this.props.totpSecret = undefined;
|
|
this.props.updatedAt = new Date();
|
|
}
|
|
|
|
recordLogin(): void {
|
|
this.props.lastLoginAt = new Date();
|
|
}
|
|
|
|
deactivate(): void {
|
|
this.props.isActive = false;
|
|
this.props.updatedAt = new Date();
|
|
}
|
|
|
|
activate(): void {
|
|
this.props.isActive = true;
|
|
this.props.updatedAt = new Date();
|
|
}
|
|
|
|
/**
|
|
* Convert to plain object for persistence
|
|
*/
|
|
toObject(): UserProps {
|
|
return { ...this.props };
|
|
}
|
|
}
|