@swoft/party-manager
Version:
1 lines • 153 kB
Source Map (JSON)
{"version":3,"sources":["../src/infrastructure/factory/ServiceFactory.ts","../src/config/environment.ts","../src/infrastructure/logging/Logger.ts","../src/infrastructure/events/EventBus.ts","../src/shared/domain/AggregateRootBase.ts","../src/bounded-contexts/authentication/domain/aggregates/AuthSession.ts","../src/bounded-contexts/authentication/application/services/AuthenticationService.ts","../src/bounded-contexts/authentication/domain/aggregates/PersonAuth.ts","../src/bounded-contexts/authentication/application/services/RegistrationService.ts","../src/bounded-contexts/authentication/application/services/PasswordService.ts","../src/infrastructure/repositories/MongoPersonAuthRepository.ts","../src/infrastructure/repositories/MongoAuthSessionRepository.ts","../src/infrastructure/repositories/MongoPersonRepository.ts","../src/bounded-contexts/parties/domain/aggregates/Person.ts","../src/infrastructure/repositories/MongoOrganisationRepository.ts","../src/bounded-contexts/parties/domain/aggregates/Organisation.ts","../src/infrastructure/repositories/MongoTeamRepository.ts","../src/bounded-contexts/parties/domain/aggregates/Team.ts","../src/infrastructure/errors/production-errors.ts"],"sourcesContent":["/**\n * Production-Ready Service Factory\n * \n * Simple, type-safe, no dependency injection complexity\n * As recommended by the expert panel\n */\n\nimport { MongoClient, Db } from 'mongodb';\nimport { getConfig, PartyManagerConfig } from '../../config/environment';\nimport { Logger } from '../logging/Logger';\nimport { EventBus } from '../events/EventBus';\n\n// Services\nimport { AuthenticationService } from '../../bounded-contexts/authentication/application/services/AuthenticationService';\nimport { RegistrationService } from '../../bounded-contexts/authentication/application/services/RegistrationService';\nimport { PasswordService } from '../../bounded-contexts/authentication/application/services/PasswordService';\n\n// Repositories\nimport { MongoPersonAuthRepository } from '../repositories/MongoPersonAuthRepository';\nimport { MongoAuthSessionRepository } from '../repositories/MongoAuthSessionRepository';\nimport { MongoPersonRepository } from '../repositories/MongoPersonRepository';\nimport { MongoOrganisationRepository } from '../repositories/MongoOrganisationRepository';\nimport { MongoTeamRepository } from '../repositories/MongoTeamRepository';\n\n/**\n * Service factory that manages all dependencies\n * No Inversify, no complexity, just simple factory pattern\n */\n\nexport class ServiceFactory {\n private static instance: ServiceFactory;\n \n private mongoClient?: MongoClient;\n private database?: Db;\n private config: PartyManagerConfig;\n private logger: Logger;\n private eventBus: EventBus;\n \n // Repository instances (lazy loaded)\n private personAuthRepo?: MongoPersonAuthRepository;\n private authSessionRepo?: MongoAuthSessionRepository;\n private personRepo?: MongoPersonRepository;\n private organisationRepo?: MongoOrganisationRepository;\n private teamRepo?: MongoTeamRepository;\n \n // Service instances (lazy loaded)\n private authService?: AuthenticationService;\n private registrationService?: RegistrationService;\n private passwordService?: PasswordService;\n\n private constructor() {\n this.config = getConfig();\n this.logger = Logger.getInstance();\n this.eventBus = EventBus.getInstance();\n }\n\n static getInstance(): ServiceFactory {\n if (!ServiceFactory.instance) {\n ServiceFactory.instance = new ServiceFactory();\n }\n return ServiceFactory.instance;\n }\n\n /**\n * Initialize database connection\n * Should be called once at application startup\n */\n async initialize(): Promise<void> {\n try {\n this.logger.info('Initializing Party Manager services');\n \n // Create MongoDB client with production settings\n this.mongoClient = new MongoClient(this.config.mongodb.url, {\n maxPoolSize: this.config.mongodb.options.maxPoolSize,\n minPoolSize: this.config.mongodb.options.minPoolSize,\n maxIdleTimeMS: this.config.mongodb.options.maxIdleTimeMS,\n connectTimeoutMS: this.config.mongodb.options.connectTimeoutMS,\n serverSelectionTimeoutMS: this.config.mongodb.options.serverSelectionTimeoutMS\n });\n\n await this.mongoClient.connect();\n this.database = this.mongoClient.db(this.config.mongodb.database);\n \n // Verify connection\n await this.database.admin().ping();\n \n this.logger.info('Party Manager services initialized successfully', {\n database: this.config.mongodb.database,\n poolSize: this.config.mongodb.options.maxPoolSize\n });\n } catch (error) {\n this.logger.error('Failed to initialize Party Manager services', error as Error);\n throw error;\n }\n }\n\n /**\n * Gracefully shutdown all connections\n */\n async shutdown(): Promise<void> {\n try {\n this.logger.info('Shutting down Party Manager services');\n \n if (this.mongoClient) {\n await this.mongoClient.close();\n }\n \n this.logger.info('Party Manager services shut down successfully');\n } catch (error) {\n this.logger.error('Error during shutdown', error as Error);\n throw error;\n }\n }\n\n /**\n * Get database instance\n */\n private getDatabase(): Db {\n if (!this.database) {\n throw new Error('Database not initialized. Call initialize() first.');\n }\n return this.database;\n }\n\n // ========================================================================\n // Repository Getters (Lazy Loading)\n // ========================================================================\n\n getPersonAuthRepository(): MongoPersonAuthRepository {\n if (!this.personAuthRepo) {\n this.personAuthRepo = new MongoPersonAuthRepository(\n this.config.mongodb.url,\n this.config.mongodb.database\n );\n }\n return this.personAuthRepo;\n }\n\n getAuthSessionRepository(): MongoAuthSessionRepository {\n if (!this.authSessionRepo) {\n this.authSessionRepo = new MongoAuthSessionRepository(\n this.config.mongodb.url,\n this.config.mongodb.database\n );\n }\n return this.authSessionRepo;\n }\n\n getPersonRepository(): MongoPersonRepository {\n if (!this.personRepo) {\n this.personRepo = new MongoPersonRepository(\n this.config.mongodb.url,\n this.config.mongodb.database\n );\n }\n return this.personRepo;\n }\n\n getOrganisationRepository(): MongoOrganisationRepository {\n if (!this.organisationRepo) {\n this.organisationRepo = new MongoOrganisationRepository(\n this.config.mongodb.url,\n this.config.mongodb.database\n );\n }\n return this.organisationRepo;\n }\n\n getTeamRepository(): MongoTeamRepository {\n if (!this.teamRepo) {\n this.teamRepo = new MongoTeamRepository(\n this.config.mongodb.url,\n this.config.mongodb.database\n );\n }\n return this.teamRepo;\n }\n\n // ========================================================================\n // Service Getters (Lazy Loading)\n // ========================================================================\n\n getAuthenticationService(): AuthenticationService {\n if (!this.authService) {\n this.authService = new AuthenticationService(\n this.getPersonAuthRepository(),\n this.getAuthSessionRepository(),\n this.eventBus // Now using real EventBus\n );\n }\n return this.authService;\n }\n\n getRegistrationService(): RegistrationService {\n if (!this.registrationService) {\n this.registrationService = new RegistrationService(\n this.getPersonAuthRepository(),\n this.getPersonRepository(),\n this.eventBus // Now using real EventBus\n );\n }\n return this.registrationService;\n }\n\n getPasswordService(): PasswordService {\n if (!this.passwordService) {\n const emailService = {\n send: async (email: any) => {\n this.logger.info('Email sent', { to: email.to, subject: email.subject });\n // In production, this would integrate with real email service\n }\n };\n\n this.passwordService = new PasswordService(\n this.getPersonAuthRepository(),\n this.getAuthSessionRepository(),\n this.eventBus, // Now using real EventBus\n emailService\n );\n }\n return this.passwordService;\n }\n\n /**\n * Get the event bus instance (for external subscriptions)\n */\n getEventBus(): EventBus {\n return this.eventBus;\n }\n\n /**\n * Get health status\n */\n async getHealthStatus(): Promise<any> {\n const status = {\n healthy: false,\n database: 'disconnected',\n uptime: process.uptime(),\n memory: process.memoryUsage(),\n timestamp: new Date().toISOString()\n };\n\n try {\n if (this.database) {\n await this.database.admin().ping();\n status.database = 'connected';\n status.healthy = true;\n }\n } catch (error) {\n this.logger.error('Health check failed', error as Error);\n }\n\n return status;\n }\n}","/**\n * Production-Ready Environment Configuration\n * \n * Simple, type-safe, no dependency injection complexity\n */\n\nexport interface PartyManagerConfig {\n mongodb: {\n url: string;\n database: string;\n options: {\n maxPoolSize: number;\n minPoolSize: number;\n maxIdleTimeMS: number;\n connectTimeoutMS: number;\n serverSelectionTimeoutMS: number;\n };\n };\n logging: {\n level: 'error' | 'warn' | 'info' | 'debug';\n format: 'json' | 'text';\n };\n health: {\n enabled: boolean;\n path: string;\n };\n metrics: {\n enabled: boolean;\n path: string;\n };\n}\n\n/**\n * Get configuration from environment variables with defaults\n */\nexport function getConfig(): PartyManagerConfig {\n return {\n mongodb: {\n url: process.env.PARTY_MANAGER_MONGO_URL || 'mongodb://localhost:27017',\n database: process.env.PARTY_MANAGER_DB_NAME || 'party_manager',\n options: {\n maxPoolSize: parseInt(process.env.MONGO_MAX_POOL_SIZE || '10'),\n minPoolSize: parseInt(process.env.MONGO_MIN_POOL_SIZE || '2'),\n maxIdleTimeMS: parseInt(process.env.MONGO_MAX_IDLE_TIME || '60000'),\n connectTimeoutMS: parseInt(process.env.MONGO_CONNECT_TIMEOUT || '10000'),\n serverSelectionTimeoutMS: parseInt(process.env.MONGO_SERVER_TIMEOUT || '5000')\n }\n },\n logging: {\n level: (process.env.LOG_LEVEL || 'info') as any,\n format: (process.env.LOG_FORMAT || 'json') as any\n },\n health: {\n enabled: process.env.HEALTH_CHECK_ENABLED !== 'false',\n path: process.env.HEALTH_CHECK_PATH || '/health'\n },\n metrics: {\n enabled: process.env.METRICS_ENABLED === 'true',\n path: process.env.METRICS_PATH || '/metrics'\n }\n };\n}\n\n/**\n * Validate configuration at startup\n */\nexport function validateConfig(config: PartyManagerConfig): void {\n if (!config.mongodb.url) {\n throw new Error('MongoDB URL is required');\n }\n \n if (!config.mongodb.database) {\n throw new Error('MongoDB database name is required');\n }\n \n if (config.mongodb.options.maxPoolSize < config.mongodb.options.minPoolSize) {\n throw new Error('MongoDB maxPoolSize must be >= minPoolSize');\n }\n}","/**\n * Production-Ready Logger\n * \n * Structured logging with correlation IDs and error tracking\n */\n\nexport interface LogContext {\n correlationId?: string;\n userId?: string;\n organizationId?: string;\n [key: string]: any;\n}\n\nexport enum LogLevel {\n ERROR = 0,\n WARN = 1,\n INFO = 2,\n DEBUG = 3\n}\n\nexport class Logger {\n private static instance: Logger;\n private level: LogLevel;\n private format: 'json' | 'text';\n\n constructor(level: string = 'info', format: 'json' | 'text' = 'json') {\n this.level = this.parseLevel(level);\n this.format = format;\n }\n\n static getInstance(): Logger {\n if (!Logger.instance) {\n Logger.instance = new Logger(\n process.env.LOG_LEVEL || 'info',\n (process.env.LOG_FORMAT || 'json') as any\n );\n }\n return Logger.instance;\n }\n\n private parseLevel(level: string): LogLevel {\n switch (level.toLowerCase()) {\n case 'error': return LogLevel.ERROR;\n case 'warn': return LogLevel.WARN;\n case 'info': return LogLevel.INFO;\n case 'debug': return LogLevel.DEBUG;\n default: return LogLevel.INFO;\n }\n }\n\n private shouldLog(level: LogLevel): boolean {\n return level <= this.level;\n }\n\n private formatMessage(\n level: string,\n message: string,\n context?: LogContext,\n error?: Error\n ): string {\n const timestamp = new Date().toISOString();\n \n if (this.format === 'json') {\n const log: any = {\n timestamp,\n level,\n message,\n ...context\n };\n\n if (error) {\n log.error = {\n name: error.name,\n message: error.message,\n stack: error.stack\n };\n }\n\n return JSON.stringify(log);\n } else {\n let log = `[${timestamp}] [${level}] ${message}`;\n \n if (context?.correlationId) {\n log += ` [${context.correlationId}]`;\n }\n \n if (error) {\n log += `\\n${error.stack}`;\n }\n \n return log;\n }\n }\n\n error(message: string, error?: Error, context?: LogContext): void {\n if (this.shouldLog(LogLevel.ERROR)) {\n console.error(this.formatMessage('ERROR', message, context, error));\n }\n }\n\n warn(message: string, context?: LogContext): void {\n if (this.shouldLog(LogLevel.WARN)) {\n console.warn(this.formatMessage('WARN', message, context));\n }\n }\n\n info(message: string, context?: LogContext): void {\n if (this.shouldLog(LogLevel.INFO)) {\n console.log(this.formatMessage('INFO', message, context));\n }\n }\n\n debug(message: string, context?: LogContext): void {\n if (this.shouldLog(LogLevel.DEBUG)) {\n console.log(this.formatMessage('DEBUG', message, context));\n }\n }\n\n /**\n * Create a child logger with fixed context\n */\n child(context: LogContext): Logger {\n const parent = this;\n return {\n error(message: string, error?: Error, additionalContext?: LogContext) {\n parent.error(message, error, { ...context, ...additionalContext });\n },\n warn(message: string, additionalContext?: LogContext) {\n parent.warn(message, { ...context, ...additionalContext });\n },\n info(message: string, additionalContext?: LogContext) {\n parent.info(message, { ...context, ...additionalContext });\n },\n debug(message: string, additionalContext?: LogContext) {\n parent.debug(message, { ...context, ...additionalContext });\n },\n child(additionalContext: LogContext) {\n return parent.child({ ...context, ...additionalContext });\n }\n } as Logger;\n }\n}","/**\n * Production-Ready Event Bus\n * \n * Real implementation with pub/sub, error handling, and async processing\n */\n\nimport { Logger } from '../logging/Logger';\n\nexport interface DomainEvent {\n aggregateId: string;\n eventType: string;\n occurredAt: Date;\n version?: number;\n metadata?: Record<string, any>;\n [key: string]: any;\n}\n\nexport type EventHandler = (event: DomainEvent) => Promise<void> | void;\n\nexport interface EventSubscription {\n unsubscribe(): void;\n}\n\n/**\n * Production Event Bus with proper pub/sub\n */\nexport class EventBus {\n private static instance: EventBus;\n private subscribers = new Map<string, Set<EventHandler>>();\n private wildcardSubscribers = new Set<EventHandler>();\n private logger: Logger;\n private eventHistory: DomainEvent[] = [];\n private maxHistorySize = 1000;\n\n private constructor() {\n this.logger = Logger.getInstance().child({ component: 'EventBus' });\n }\n\n static getInstance(): EventBus {\n if (!EventBus.instance) {\n EventBus.instance = new EventBus();\n }\n return EventBus.instance;\n }\n\n /**\n * Publish an event to all subscribers\n */\n async publish(event: DomainEvent): Promise<void> {\n const startTime = Date.now();\n \n // Add to history for debugging/replay\n this.addToHistory(event);\n \n // Log the event\n this.logger.debug('Publishing event', {\n eventType: event.eventType,\n aggregateId: event.aggregateId,\n timestamp: event.occurredAt\n });\n\n // Get all handlers for this event type\n const handlers = this.getHandlersForEvent(event.eventType);\n \n // Execute handlers in parallel with error isolation\n const results = await Promise.allSettled(\n handlers.map(handler => this.executeHandler(handler, event))\n );\n\n // Log any failures\n const failures = results.filter(r => r.status === 'rejected');\n if (failures.length > 0) {\n this.logger.error(\n `Event handler failures for ${event.eventType}`,\n new Error('Handler failures'),\n {\n eventType: event.eventType,\n failureCount: failures.length,\n failures: failures.map((f: any) => f.reason?.message || f.reason)\n }\n );\n }\n\n const duration = Date.now() - startTime;\n this.logger.debug('Event published', {\n eventType: event.eventType,\n handlerCount: handlers.length,\n failureCount: failures.length,\n duration\n });\n }\n\n /**\n * Subscribe to a specific event type\n */\n subscribe(eventType: string, handler: EventHandler): EventSubscription {\n if (!this.subscribers.has(eventType)) {\n this.subscribers.set(eventType, new Set());\n }\n \n this.subscribers.get(eventType)!.add(handler);\n \n this.logger.debug('Subscriber added', { eventType });\n\n // Return subscription handle for cleanup\n return {\n unsubscribe: () => {\n const handlers = this.subscribers.get(eventType);\n if (handlers) {\n handlers.delete(handler);\n if (handlers.size === 0) {\n this.subscribers.delete(eventType);\n }\n }\n this.logger.debug('Subscriber removed', { eventType });\n }\n };\n }\n\n /**\n * Subscribe to all events (wildcard)\n */\n subscribeToAll(handler: EventHandler): EventSubscription {\n this.wildcardSubscribers.add(handler);\n \n this.logger.debug('Wildcard subscriber added');\n\n return {\n unsubscribe: () => {\n this.wildcardSubscribers.delete(handler);\n this.logger.debug('Wildcard subscriber removed');\n }\n };\n }\n\n /**\n * Get all handlers for an event type\n */\n private getHandlersForEvent(eventType: string): EventHandler[] {\n const handlers: EventHandler[] = [];\n \n // Add specific handlers\n const specificHandlers = this.subscribers.get(eventType);\n if (specificHandlers) {\n handlers.push(...specificHandlers);\n }\n \n // Add wildcard handlers\n handlers.push(...this.wildcardSubscribers);\n \n return handlers;\n }\n\n /**\n * Execute a handler with error isolation\n */\n private async executeHandler(\n handler: EventHandler,\n event: DomainEvent\n ): Promise<void> {\n try {\n await Promise.resolve(handler(event));\n } catch (error) {\n // Log but don't throw - isolate handler failures\n this.logger.error(\n 'Event handler error',\n error as Error,\n {\n eventType: event.eventType,\n aggregateId: event.aggregateId\n }\n );\n throw error; // Re-throw for Promise.allSettled to catch\n }\n }\n\n /**\n * Add event to history for debugging/replay\n */\n private addToHistory(event: DomainEvent): void {\n this.eventHistory.push(event);\n \n // Trim history if too large\n if (this.eventHistory.length > this.maxHistorySize) {\n this.eventHistory = this.eventHistory.slice(-this.maxHistorySize);\n }\n }\n\n /**\n * Get event history (for debugging)\n */\n getHistory(limit?: number): DomainEvent[] {\n if (limit) {\n return this.eventHistory.slice(-limit);\n }\n return [...this.eventHistory];\n }\n\n /**\n * Clear all subscribers (useful for testing)\n */\n clearAllSubscribers(): void {\n this.subscribers.clear();\n this.wildcardSubscribers.clear();\n this.logger.info('All subscribers cleared');\n }\n\n /**\n * Get subscriber statistics\n */\n getStats(): {\n eventTypes: number;\n totalHandlers: number;\n wildcardHandlers: number;\n historySize: number;\n } {\n let totalHandlers = 0;\n for (const handlers of this.subscribers.values()) {\n totalHandlers += handlers.size;\n }\n\n return {\n eventTypes: this.subscribers.size,\n totalHandlers,\n wildcardHandlers: this.wildcardSubscribers.size,\n historySize: this.eventHistory.length\n };\n }\n}\n\n/**\n * Domain Event Builder Helper\n */\nexport class DomainEventBuilder {\n static create(\n eventType: string,\n aggregateId: string,\n data: Record<string, any> = {}\n ): DomainEvent {\n return {\n eventType,\n aggregateId,\n occurredAt: new Date(),\n version: 1,\n ...data\n };\n }\n}","/**\n * Base Aggregate Root\n * \n * Provides common functionality for all aggregates\n * Since @swoft/core AggregateRoot doesn't provide all methods we need\n */\n\nexport abstract class AggregateRootBase {\n protected version: number = 0;\n private uncommittedEvents: any[] = [];\n\n constructor(protected readonly id: string, version?: number) {\n this.version = version || 0;\n }\n\n getId(): string {\n return this.id;\n }\n\n getVersion(): number {\n return this.version;\n }\n\n protected incrementVersion(): void {\n this.version++;\n }\n\n protected addDomainEvent<T>(event: T): void {\n this.uncommittedEvents.push(event);\n }\n\n getUncommittedEvents(): any[] {\n return this.uncommittedEvents;\n }\n\n markEventsAsCommitted(): void {\n this.uncommittedEvents = [];\n }\n}","/**\n * AuthSession Aggregate Root\n * \n * Manages user sessions with proper DDD patterns\n * No duplicate entity files!\n */\nimport { AggregateRootBase } from '../../../../shared/domain/AggregateRootBase';\n\nexport interface SessionInvalidated {\n aggregateId: string;\n personId: string;\n reason: string;\n occurredAt: Date;\n}\n\nexport interface SessionExtended {\n aggregateId: string;\n personId: string;\n extendedBy: number;\n newExpiresAt: Date;\n occurredAt: Date;\n}\n\n/**\n * AuthSession Aggregate Root\n * Represents an authenticated user session\n */\nexport class AuthSession extends AggregateRootBase {\n private constructor(\n id: string,\n private readonly personId: string,\n private readonly email: string,\n private readonly issuedAt: Date,\n private expiresAt: Date,\n private isActive: boolean,\n private readonly ipAddress?: string,\n private readonly userAgent?: string,\n private lastAccessedAt?: Date,\n version: number = 0\n ) {\n super(id, version);\n }\n\n /**\n * Factory method to create new session\n */\n static create(\n sessionId: string,\n personId: string,\n email: string,\n expirationHours: number,\n ipAddress?: string,\n userAgent?: string\n ): AuthSession {\n const now = new Date();\n const expiresAt = new Date(now.getTime() + expirationHours * 60 * 60 * 1000);\n\n return new AuthSession(\n sessionId,\n personId,\n email,\n now,\n expiresAt,\n true,\n ipAddress,\n userAgent,\n now\n );\n }\n\n /**\n * Reconstitute from persistence\n */\n static fromSnapshot(snapshot: any): AuthSession {\n return new AuthSession(\n snapshot.id,\n snapshot.personId,\n snapshot.email,\n snapshot.issuedAt,\n snapshot.expiresAt,\n snapshot.isActive,\n snapshot.ipAddress,\n snapshot.userAgent,\n snapshot.lastAccessedAt,\n snapshot.version\n );\n }\n\n /**\n * Check if session is valid\n */\n isValid(): boolean {\n if (!this.isActive) return false;\n if (this.expiresAt < new Date()) return false;\n return true;\n }\n\n /**\n * Invalidate session\n */\n invalidate(reason: string = 'User logout'): void {\n if (!this.isActive) return;\n\n this.isActive = false;\n this.incrementVersion();\n\n this.addDomainEvent<SessionInvalidated>({\n aggregateId: this.id,\n personId: this.personId,\n reason,\n occurredAt: new Date()\n });\n }\n\n /**\n * Extend session expiration\n */\n extend(additionalHours: number): void {\n if (!this.isActive) {\n throw new Error('Cannot extend inactive session');\n }\n\n const newExpiresAt = new Date(this.expiresAt.getTime() + additionalHours * 60 * 60 * 1000);\n this.expiresAt = newExpiresAt;\n this.incrementVersion();\n\n this.addDomainEvent<SessionExtended>({\n aggregateId: this.id,\n personId: this.personId,\n extendedBy: additionalHours,\n newExpiresAt,\n occurredAt: new Date()\n });\n }\n\n /**\n * Update last accessed time\n */\n updateLastAccessed(): void {\n this.lastAccessedAt = new Date();\n this.incrementVersion();\n }\n\n /**\n * Get remaining time in minutes\n */\n getRemainingMinutes(): number {\n if (!this.isValid()) return 0;\n const remaining = this.expiresAt.getTime() - Date.now();\n return Math.max(0, Math.floor(remaining / (1000 * 60)));\n }\n\n // Getters for read-only access\n getPersonId(): string { return this.personId; }\n getEmail(): string { return this.email; }\n getExpiresAt(): Date { return this.expiresAt; }\n getIssuedAt(): Date { return this.issuedAt; }\n getIpAddress(): string | undefined { return this.ipAddress; }\n getUserAgent(): string | undefined { return this.userAgent; }\n\n // Snapshot for persistence\n toSnapshot(): any {\n return {\n id: this.id,\n personId: this.personId,\n email: this.email,\n issuedAt: this.issuedAt,\n expiresAt: this.expiresAt,\n isActive: this.isActive,\n ipAddress: this.ipAddress,\n userAgent: this.userAgent,\n lastAccessedAt: this.lastAccessedAt,\n version: this.version\n };\n }\n}","/**\n * Authentication Application Service\n * \n * Focused service for authentication use cases\n * Following DDD application layer principles:\n * - Thin orchestration layer\n * - Delegates business logic to domain\n * - Handles cross-cutting concerns\n */\nimport { PersonAuth, AuthenticationResult } from '../../domain/aggregates/PersonAuth';\nimport type { IPersonAuthRepository } from '../../domain/repositories/IPersonAuthRepository';\nimport { AuthSession } from '../../domain/aggregates/AuthSession';\nimport type { IAuthSessionRepository } from '../../domain/repositories/IAuthSessionRepository';\nimport { Logger } from '../../../../infrastructure/logging/Logger';\nimport { \n InvalidCredentialsError, \n AccountLockedError,\n DatabaseError \n} from '../../../../infrastructure/errors/production-errors';\n\nexport interface LoginCommand {\n email: string;\n password: string;\n rememberMe: boolean;\n ipAddress?: string;\n userAgent?: string;\n}\n\nexport interface LoginResult {\n success: boolean;\n sessionId?: string;\n personId?: string;\n email?: string;\n expiresAt?: Date;\n error?: string;\n attemptsRemaining?: number;\n}\n\nexport class AuthenticationService {\n private logger: Logger;\n\n constructor(\n private personAuthRepo: IPersonAuthRepository,\n private sessionRepo: IAuthSessionRepository,\n private eventBus: { publish: (event: any) => Promise<void> }\n ) {\n this.logger = Logger.getInstance().child({ service: 'AuthenticationService' });\n }\n\n /**\n * Handle login - thin orchestration, not business logic\n */\n async login(command: LoginCommand): Promise<LoginResult> {\n try {\n // 1. Find aggregate\n const personAuth = await this.personAuthRepo.findByEmail(command.email);\n if (!personAuth) {\n // Don't reveal account doesn't exist\n return {\n success: false,\n error: 'Invalid credentials',\n attemptsRemaining: 5\n };\n }\n\n // 2. Delegate authentication to domain\n const authResult = await personAuth.authenticate(command.password);\n\n // 3. Save state changes\n await this.personAuthRepo.save(personAuth);\n\n // 4. Publish domain events\n await this.publishEvents(personAuth);\n\n // 5. Create session if successful\n if (authResult.success && authResult.sessionId) {\n const session = await this.createSession(\n personAuth,\n authResult.sessionId!,\n command.rememberMe,\n command.ipAddress,\n command.userAgent\n );\n\n return {\n success: true,\n sessionId: session.getId(),\n personId: personAuth.getPersonId(),\n email: personAuth.getEmail(),\n expiresAt: session.getExpiresAt()\n };\n }\n\n // 6. Return failure\n return {\n success: false,\n error: authResult.failureReason || 'Authentication failed',\n attemptsRemaining: authResult.attemptsRemaining\n };\n\n } catch (error) {\n console.error('Login error:', error);\n return {\n success: false,\n error: 'An error occurred during login'\n };\n }\n }\n\n /**\n * Logout - simple and focused\n */\n async logout(sessionId: string): Promise<void> {\n const session = await this.sessionRepo.findById(sessionId);\n if (session) {\n session.invalidate();\n await this.sessionRepo.save(session);\n await this.publishEvents(session);\n }\n }\n\n private async createSession(\n personAuth: PersonAuth,\n sessionId: string,\n rememberMe: boolean,\n ipAddress?: string,\n userAgent?: string\n ): Promise<AuthSession> {\n const expirationHours = rememberMe ? 24 * 30 : 24; // 30 days or 1 day\n \n const session = AuthSession.create(\n sessionId,\n personAuth.getPersonId(),\n personAuth.getEmail(),\n expirationHours,\n ipAddress,\n userAgent\n );\n\n await this.sessionRepo.save(session);\n await this.publishEvents(session);\n \n return session;\n }\n\n private async publishEvents(aggregate: PersonAuth | AuthSession): Promise<void> {\n const events = aggregate.getUncommittedEvents();\n for (const event of events) {\n await this.eventBus.publish(event);\n }\n aggregate.markEventsAsCommitted();\n }\n}","/**\n * PersonAuth Aggregate Root\n * \n * Enterprise-grade DDD implementation following Eric Evans principles:\n * - Single Aggregate Root (no separate entity/aggregate files)\n * - Rich domain model with encapsulated business logic\n * - Immutable state transitions\n * - Domain events for important state changes\n */\nimport { AggregateRootBase } from '../../../../shared/domain/AggregateRootBase';\nimport * as bcrypt from 'bcrypt';\nimport { v4 as uuidv4 } from 'uuid';\n\n// Value Objects\nexport class Email {\n constructor(public readonly value: string) {\n if (!this.isValid(value)) {\n throw new Error(`Invalid email: ${value}`);\n }\n }\n\n private isValid(email: string): boolean {\n const emailRegex = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/;\n return emailRegex.test(email);\n }\n\n equals(other: Email): boolean {\n return this.value === other.value;\n }\n}\n\nexport class HashedPassword {\n constructor(public readonly value: string) {\n if (!value || value.length < 20) {\n throw new Error('Invalid hashed password');\n }\n }\n\n async verify(plainPassword: string): Promise<boolean> {\n return bcrypt.compare(plainPassword, this.value);\n }\n\n static async create(plainPassword: string): Promise<HashedPassword> {\n if (plainPassword.length < 8) {\n throw new Error('Password must be at least 8 characters');\n }\n const hashed = await bcrypt.hash(plainPassword, 12);\n return new HashedPassword(hashed);\n }\n}\n\n// Domain Events\nexport interface PersonAuthCreated {\n aggregateId: string;\n personId: string;\n email: string;\n occurredAt: Date;\n}\n\nexport interface LoginSucceeded {\n aggregateId: string;\n personId: string;\n email: string;\n sessionId: string;\n occurredAt: Date;\n}\n\nexport interface LoginFailed {\n aggregateId: string;\n email: string;\n reason: string;\n attemptsRemaining: number;\n occurredAt: Date;\n}\n\nexport interface PasswordChanged {\n aggregateId: string;\n personId: string;\n changedBy: 'user' | 'admin';\n occurredAt: Date;\n}\n\nexport interface AccountLocked {\n aggregateId: string;\n personId: string;\n reason: string;\n lockedUntil: Date;\n occurredAt: Date;\n}\n\n// Authentication Result Value Objects\nexport class AuthenticationResult {\n private constructor(\n public readonly success: boolean,\n public readonly sessionId?: string,\n public readonly failureReason?: string,\n public readonly attemptsRemaining?: number\n ) {}\n\n static successful(sessionId: string): AuthenticationResult {\n return new AuthenticationResult(true, sessionId);\n }\n\n static failed(reason: string, attemptsRemaining: number): AuthenticationResult {\n return new AuthenticationResult(false, undefined, reason, attemptsRemaining);\n }\n\n static locked(): AuthenticationResult {\n return new AuthenticationResult(false, undefined, 'Account is locked', 0);\n }\n}\n\n/**\n * PersonAuth Aggregate Root\n * Manages authentication and authorization for a person\n */\nexport class PersonAuth extends AggregateRootBase {\n private static readonly MAX_LOGIN_ATTEMPTS = 5;\n private static readonly LOCKOUT_DURATION_MINUTES = 30;\n\n private constructor(\n id: string,\n private readonly personId: string,\n private readonly email: Email,\n private hashedPassword: HashedPassword,\n private isActive: boolean,\n private isEmailVerified: boolean,\n private failedLoginAttempts: number,\n private lastLoginAt?: Date,\n private emailVerifiedAt?: Date,\n private passwordChangedAt?: Date,\n private lockedUntil?: Date,\n version: number = 0\n ) {\n super(id, version);\n }\n\n /**\n * Factory method to create a new PersonAuth\n */\n static async create(\n personId: string,\n email: string,\n plainPassword: string\n ): Promise<PersonAuth> {\n const id = uuidv4();\n const emailVO = new Email(email);\n const hashedPassword = await HashedPassword.create(plainPassword);\n \n const personAuth = new PersonAuth(\n id,\n personId,\n emailVO,\n hashedPassword,\n true, // isActive\n false, // isEmailVerified - require verification\n 0, // failedLoginAttempts\n undefined, // lastLoginAt\n undefined, // emailVerifiedAt\n new Date(), // passwordChangedAt\n undefined // lockedUntil\n );\n\n personAuth.addDomainEvent<PersonAuthCreated>({\n aggregateId: id,\n personId,\n email,\n occurredAt: new Date()\n });\n\n return personAuth;\n }\n\n /**\n * Reconstitute from persistence\n */\n static fromSnapshot(snapshot: any): PersonAuth {\n return new PersonAuth(\n snapshot.id,\n snapshot.personId,\n new Email(snapshot.email),\n new HashedPassword(snapshot.hashedPassword),\n snapshot.isActive,\n snapshot.isEmailVerified,\n snapshot.failedLoginAttempts,\n snapshot.lastLoginAt,\n snapshot.emailVerifiedAt,\n snapshot.passwordChangedAt,\n snapshot.lockedUntil,\n snapshot.version\n );\n }\n\n /**\n * Authenticate with password\n * This is the core business logic - not in a handler!\n */\n async authenticate(plainPassword: string): Promise<AuthenticationResult> {\n // Check if account is locked\n if (this.isLocked()) {\n this.addDomainEvent<LoginFailed>({\n aggregateId: this.id,\n email: this.email.value,\n reason: 'Account is locked',\n attemptsRemaining: 0,\n occurredAt: new Date()\n });\n return AuthenticationResult.locked();\n }\n\n // Check if account can login\n if (!this.canLogin()) {\n const reason = !this.isActive \n ? 'Account is inactive' \n : 'Email not verified';\n \n this.addDomainEvent<LoginFailed>({\n aggregateId: this.id,\n email: this.email.value,\n reason,\n attemptsRemaining: PersonAuth.MAX_LOGIN_ATTEMPTS - this.failedLoginAttempts,\n occurredAt: new Date()\n });\n \n return AuthenticationResult.failed(reason, PersonAuth.MAX_LOGIN_ATTEMPTS - this.failedLoginAttempts);\n }\n\n // Verify password\n const isValid = await this.hashedPassword.verify(plainPassword);\n \n if (!isValid) {\n this.recordFailedAttempt();\n const attemptsRemaining = Math.max(0, PersonAuth.MAX_LOGIN_ATTEMPTS - this.failedLoginAttempts);\n \n this.addDomainEvent<LoginFailed>({\n aggregateId: this.id,\n email: this.email.value,\n reason: 'Invalid password',\n attemptsRemaining,\n occurredAt: new Date()\n });\n\n if (this.failedLoginAttempts >= PersonAuth.MAX_LOGIN_ATTEMPTS) {\n this.lock();\n }\n\n return AuthenticationResult.failed('Invalid credentials', attemptsRemaining);\n }\n\n // Success\n const sessionId = uuidv4();\n this.recordSuccessfulLogin();\n \n this.addDomainEvent<LoginSucceeded>({\n aggregateId: this.id,\n personId: this.personId,\n email: this.email.value,\n sessionId,\n occurredAt: new Date()\n });\n\n return AuthenticationResult.successful(sessionId);\n }\n\n /**\n * Change password - domain logic, not in handler\n */\n async changePassword(\n currentPassword: string, \n newPassword: string\n ): Promise<boolean> {\n // Verify current password\n const isCurrentValid = await this.hashedPassword.verify(currentPassword);\n if (!isCurrentValid) {\n return false;\n }\n\n // Set new password\n this.hashedPassword = await HashedPassword.create(newPassword);\n this.passwordChangedAt = new Date();\n this.failedLoginAttempts = 0; // Reset on password change\n this.lockedUntil = undefined; // Unlock on password change\n this.incrementVersion();\n\n this.addDomainEvent<PasswordChanged>({\n aggregateId: this.id,\n personId: this.personId,\n changedBy: 'user',\n occurredAt: new Date()\n });\n\n return true;\n }\n\n /**\n * Admin password reset\n */\n async resetPasswordByAdmin(newPassword: string): Promise<void> {\n this.hashedPassword = await HashedPassword.create(newPassword);\n this.passwordChangedAt = new Date();\n this.failedLoginAttempts = 0;\n this.lockedUntil = undefined;\n this.incrementVersion();\n\n this.addDomainEvent<PasswordChanged>({\n aggregateId: this.id,\n personId: this.personId,\n changedBy: 'admin',\n occurredAt: new Date()\n });\n }\n\n /**\n * Verify email\n */\n verifyEmail(): void {\n if (this.isEmailVerified) return;\n \n this.isEmailVerified = true;\n this.emailVerifiedAt = new Date();\n this.incrementVersion();\n }\n\n /**\n * Activate account\n */\n activate(): void {\n if (this.isActive) return;\n \n this.isActive = true;\n this.incrementVersion();\n }\n\n /**\n * Deactivate account\n */\n deactivate(): void {\n if (!this.isActive) return;\n \n this.isActive = false;\n this.incrementVersion();\n }\n\n // Private helper methods\n private recordFailedAttempt(): void {\n this.failedLoginAttempts++;\n this.incrementVersion();\n }\n\n private recordSuccessfulLogin(): void {\n this.failedLoginAttempts = 0;\n this.lastLoginAt = new Date();\n this.incrementVersion();\n }\n\n private lock(): void {\n const lockUntil = new Date();\n lockUntil.setMinutes(lockUntil.getMinutes() + PersonAuth.LOCKOUT_DURATION_MINUTES);\n this.lockedUntil = lockUntil;\n \n this.addDomainEvent<AccountLocked>({\n aggregateId: this.id,\n personId: this.personId,\n reason: 'Too many failed login attempts',\n lockedUntil: lockUntil,\n occurredAt: new Date()\n });\n }\n\n private isLocked(): boolean {\n if (!this.lockedUntil) return false;\n return this.lockedUntil > new Date();\n }\n\n private canLogin(): boolean {\n return this.isActive && this.isEmailVerified && !this.isLocked();\n }\n\n // Getters for read-only access\n getPersonId(): string { return this.personId; }\n getEmail(): string { return this.email.value; }\n isAccountActive(): boolean { return this.isActive; }\n isAccountVerified(): boolean { return this.isEmailVerified; }\n getFailedAttempts(): number { return this.failedLoginAttempts; }\n getLastLoginAt(): Date | undefined { return this.lastLoginAt; }\n\n // Snapshot for persistence\n toSnapshot(): any {\n return {\n id: this.id,\n personId: this.personId,\n email: this.email.value,\n hashedPassword: this.hashedPassword.value,\n isActive: this.isActive,\n isEmailVerified: this.isEmailVerified,\n failedLoginAttempts: this.failedLoginAttempts,\n lastLoginAt: this.lastLoginAt,\n emailVerifiedAt: this.emailVerifiedAt,\n passwordChangedAt: this.passwordChangedAt,\n lockedUntil: this.lockedUntil,\n version: this.version\n };\n }\n}","/**\n * Registration Application Service\n * \n * Focused service for user registration\n * Separate from authentication to maintain single responsibility\n */\nimport { PersonAuth } from '../../domain/aggregates/PersonAuth';\nimport type { IPersonAuthRepository } from '../../domain/repositories/IPersonAuthRepository';\nimport type { IPersonRepository } from '../../../parties/domain/repositories/IPersonRepository';\n\nexport interface RegisterCommand {\n personId: string;\n email: string;\n password: string;\n}\n\nexport interface RegisterResult {\n success: boolean;\n personAuthId?: string;\n message?: string;\n error?: string;\n}\n\nexport class RegistrationService {\n constructor(\n private personAuthRepo: IPersonAuthRepository,\n private personRepo: IPersonRepository,\n private eventBus: { publish: (event: any) => Promise<void> }\n ) {}\n\n /**\n * Register new user authentication\n */\n async register(command: RegisterCommand): Promise<RegisterResult> {\n try {\n // 1. Verify person exists\n const person = await this.personRepo.findById(command.personId);\n if (!person) {\n return {\n success: false,\n error: 'Person not found'\n };\n }\n\n // 2. Check email uniqueness\n if (await this.personAuthRepo.emailExists(command.email)) {\n return {\n success: false,\n error: 'Email already registered'\n };\n }\n\n // 3. Verify email matches person record\n if (person.getEmail() !== command.email) {\n return {\n success: false,\n error: 'Email does not match person record'\n };\n }\n\n // 4. Create aggregate using factory method\n const personAuth = await PersonAuth.create(\n command.personId,\n command.email,\n command.password\n );\n\n // 5. Save aggregate\n await this.personAuthRepo.save(personAuth);\n\n // 6. Publish domain events\n await this.publishEvents(personAuth);\n\n // 7. Return success\n return {\n success: true,\n personAuthId: personAuth.getId(),\n message: 'Registration successful. Please verify your email.'\n };\n\n } catch (error) {\n console.error('Registration error:', error);\n return {\n success: false,\n error: error instanceof Error ? error.message : 'Registration failed'\n };\n }\n }\n\n /**\n * Verify email address\n */\n async verifyEmail(token: string): Promise<boolean> {\n // Token verification would be handled by a separate service\n // This is just the orchestration\n const personAuthId = await this.validateEmailToken(token);\n if (!personAuthId) return false;\n\n const personAuth = await this.personAuthRepo.findById(personAuthId);\n if (!personAuth) return false;\n\n personAuth.verifyEmail();\n await this.personAuthRepo.save(personAuth);\n await this.publishEvents(personAuth);\n\n return true;\n }\n\n private async validateEmailToken(token: string): Promise<string | null> {\n // Implementation would use a token service\n // Placeholder for now\n return null;\n }\n\n private async publishEvents(aggregate: PersonAuth): Promise<void> {\n const events = aggregate.getUncommittedEvents();\n for (const event of events) {\n await this.eventBus.publish(event);\n }\n aggregate.markEventsAsCommitted();\n }\n}","/**\n * Password Management Application Service\n * \n * Handles password changes, resets, and recovery\n * Separate service for password-related use cases\n */\nimport { PersonAuth } from '../../domain/aggregates/PersonAuth';\nimport type { IPersonAuthRepository } from '../../domain/repositories/IPersonAuthRepository';\nimport type { IAuthSessionRepository } from '../../domain/repositories/IAuthSessionRepository';\nimport { v4 as uuidv4 } from 'uuid';\n\nexport interface ChangePasswordCommand {\n personAuthId: string;\n currentPassword: string;\n newPassword: string;\n}\n\nexport interface ResetPasswordCommand {\n email: string;\n}\n\nexport interface CompleteResetCommand {\n token: string;\n newPassword: string;\n}\n\nexport interface AdminResetCommand {\n personAuthId: string;\n newPassword: string;\n adminId: string;\n reason: string;\n}\n\nexport class PasswordService {\n private resetTokens = new Map<string, { personAuthId: string; expiresAt: Date }>();\n\n constructor(\n private personAuthRepo: IPersonAuthRepository,\n private sessionRepo: IAuthSessionRepository,\n private eventBus: { publish: (event: any) => Promise<void> },\n private emailService: { send: (email: any) => Promise<void> }\n ) {}\n\n /**\n * User-initiated password change\n */\n async changePassword(command: ChangePasswordCommand): Promise<boolean> {\n const personAuth = await this.personAuthRepo.findById(command.personAuthId);\n if (!personAuth) return false;\n\n // Delegate to domain\n const success = await personAuth.changePassword(\n command.currentPassword,\n command.newPassword\n );\n\n if (success) {\n await this.personAuthRepo.save(personAuth);\n await this.publishEvents(personAuth);\n await this.terminateAllSessions(personAuth.getPersonId());\n }\n\n return success;\n }\n\n /**\n * Request password reset via email\n */\n async requestPasswordReset(command: ResetPasswordCommand): Promise<boolean> {\n const personAuth = await this.personAuthRepo.findByEmail(command.email);\n if (!personAuth) {\n // Don't reveal if email exists\n return true;\n }\n\n // Generate reset token\n const token = uuidv4();\n const expiresAt = new Date();\n expiresAt.setHours(expiresAt.getHours() + 1); // 1 hour expiry\n\n // Store token (in production, use persistent storage)\n this.resetTokens.set(token, {\n personAuthId: personAuth.getId(),\n expiresAt\n });\n\n // Send email\n await this.emailService.send({\n to: command.email,\n subject: 'Password Reset Request',\n template: 'password-reset',\n data: { token, expiresAt }\n });\n\n return true;\n }\n\n /**\n * Complete password reset with token\n */\n async completePasswordReset(command: CompleteResetCommand): Promise<boolean> {\n // Validate token\n const tokenData = this.resetTokens.get(command.token);\n if (!tokenData || tokenData.expiresAt < new Date()) {\n return false;\n }\n\n // Get aggregate\n const personAuth = await this.personAuthRepo.findById(tokenData.personAuthId);\n if (!personAuth) return false;\n\n // Reset password\n await personAuth.resetPasswordByAdmin(command.newPassword);\n await this.personAuthRepo.save(personAuth);\n await this.publishEvents(personAuth);\n\n // Clean up token\n this.resetTokens.delete(command.token);\n\n // Terminate all sessions\n await this.terminateAllSessions(personAuth.getPersonId());\n\n return true;\n }\n\n /**\n * Admin-initiated password reset\n */\n async adminResetPassword(command: AdminResetCommand): Promise<boolean> {\n const personAuth = await this.personAuthRepo.findById(command.personAuthId);\n if (!personAuth) return false;\n\n await personAuth.resetPasswordByAdmin(command.newPassword);\n await this.personAuthRepo.save(personAuth);\n await this.publishEvents(personAuth);\n await this.terminateAllSessions(personAuth.getPersonId());\n\n // Audit log would be handled via domain events\n return true;\n }\n\n private async terminateAllSessions(personId: string): Promise<void> {\n const sessions = await this.sessionRepo.findByPersonId(personId);\n for (const session of sessions) {\n session.invalidate();\n await this.sessionRepo.save(session);\n }\n }\n\n private async publishEvents(aggregate: PersonAuth): Promise<void> {\n const events = aggregate.getUncommittedEvents();\n for (const event of events) {\n await this.eventBus.publish(event);\n }\n aggregate.markEventsAsCom