UNPKG

@swoft/party-manager

Version:
1,796 lines (1,778 loc) • 74.7 kB
var __defProp = Object.defineProperty; var __name = (target, value) => __defProp(target, "name", { value, configurable: true }); // src/infrastructure/factory/ServiceFactory.ts import { MongoClient as MongoClient6 } from "mongodb"; // src/config/environment.ts function getConfig() { return { mongodb: { url: process.env.PARTY_MANAGER_MONGO_URL || "mongodb://localhost:27017", database: process.env.PARTY_MANAGER_DB_NAME || "party_manager", options: { maxPoolSize: parseInt(process.env.MONGO_MAX_POOL_SIZE || "10"), minPoolSize: parseInt(process.env.MONGO_MIN_POOL_SIZE || "2"), maxIdleTimeMS: parseInt(process.env.MONGO_MAX_IDLE_TIME || "60000"), connectTimeoutMS: parseInt(process.env.MONGO_CONNECT_TIMEOUT || "10000"), serverSelectionTimeoutMS: parseInt(process.env.MONGO_SERVER_TIMEOUT || "5000") } }, logging: { level: process.env.LOG_LEVEL || "info", format: process.env.LOG_FORMAT || "json" }, health: { enabled: process.env.HEALTH_CHECK_ENABLED !== "false", path: process.env.HEALTH_CHECK_PATH || "/health" }, metrics: { enabled: process.env.METRICS_ENABLED === "true", path: process.env.METRICS_PATH || "/metrics" } }; } __name(getConfig, "getConfig"); // src/infrastructure/logging/Logger.ts var LogLevel = /* @__PURE__ */ function(LogLevel2) { LogLevel2[LogLevel2["ERROR"] = 0] = "ERROR"; LogLevel2[LogLevel2["WARN"] = 1] = "WARN"; LogLevel2[LogLevel2["INFO"] = 2] = "INFO"; LogLevel2[LogLevel2["DEBUG"] = 3] = "DEBUG"; return LogLevel2; }({}); var Logger = class _Logger { static { __name(this, "Logger"); } static instance; level; format; constructor(level = "info", format = "json") { this.level = this.parseLevel(level); this.format = format; } static getInstance() { if (!_Logger.instance) { _Logger.instance = new _Logger(process.env.LOG_LEVEL || "info", process.env.LOG_FORMAT || "json"); } return _Logger.instance; } parseLevel(level) { switch (level.toLowerCase()) { case "error": return 0; case "warn": return 1; case "info": return 2; case "debug": return 3; default: return 2; } } shouldLog(level) { return level <= this.level; } formatMessage(level, message, context, error) { const timestamp = (/* @__PURE__ */ new Date()).toISOString(); if (this.format === "json") { const log = { timestamp, level, message, ...context }; if (error) { log.error = { name: error.name, message: error.message, stack: error.stack }; } return JSON.stringify(log); } else { let log = `[${timestamp}] [${level}] ${message}`; if (context?.correlationId) { log += ` [${context.correlationId}]`; } if (error) { log += ` ${error.stack}`; } return log; } } error(message, error, context) { if (this.shouldLog(0)) { console.error(this.formatMessage("ERROR", message, context, error)); } } warn(message, context) { if (this.shouldLog(1)) { console.warn(this.formatMessage("WARN", message, context)); } } info(message, context) { if (this.shouldLog(2)) { console.log(this.formatMessage("INFO", message, context)); } } debug(message, context) { if (this.shouldLog(3)) { console.log(this.formatMessage("DEBUG", message, context)); } } /** * Create a child logger with fixed context */ child(context) { const parent = this; return { error(message, error, additionalContext) { parent.error(message, error, { ...context, ...additionalContext }); }, warn(message, additionalContext) { parent.warn(message, { ...context, ...additionalContext }); }, info(message, additionalContext) { parent.info(message, { ...context, ...additionalContext }); }, debug(message, additionalContext) { parent.debug(message, { ...context, ...additionalContext }); }, child(additionalContext) { return parent.child({ ...context, ...additionalContext }); } }; } }; // src/infrastructure/events/EventBus.ts var EventBus = class _EventBus { static { __name(this, "EventBus"); } static instance; subscribers = /* @__PURE__ */ new Map(); wildcardSubscribers = /* @__PURE__ */ new Set(); logger; eventHistory = []; maxHistorySize = 1e3; constructor() { this.logger = Logger.getInstance().child({ component: "EventBus" }); } static getInstance() { if (!_EventBus.instance) { _EventBus.instance = new _EventBus(); } return _EventBus.instance; } /** * Publish an event to all subscribers */ async publish(event) { const startTime = Date.now(); this.addToHistory(event); this.logger.debug("Publishing event", { eventType: event.eventType, aggregateId: event.aggregateId, timestamp: event.occurredAt }); const handlers = this.getHandlersForEvent(event.eventType); const results = await Promise.allSettled(handlers.map((handler) => this.executeHandler(handler, event))); const failures = results.filter((r) => r.status === "rejected"); if (failures.length > 0) { this.logger.error(`Event handler failures for ${event.eventType}`, new Error("Handler failures"), { eventType: event.eventType, failureCount: failures.length, failures: failures.map((f) => f.reason?.message || f.reason) }); } const duration = Date.now() - startTime; this.logger.debug("Event published", { eventType: event.eventType, handlerCount: handlers.length, failureCount: failures.length, duration }); } /** * Subscribe to a specific event type */ subscribe(eventType, handler) { if (!this.subscribers.has(eventType)) { this.subscribers.set(eventType, /* @__PURE__ */ new Set()); } this.subscribers.get(eventType).add(handler); this.logger.debug("Subscriber added", { eventType }); return { unsubscribe: /* @__PURE__ */ __name(() => { const handlers = this.subscribers.get(eventType); if (handlers) { handlers.delete(handler); if (handlers.size === 0) { this.subscribers.delete(eventType); } } this.logger.debug("Subscriber removed", { eventType }); }, "unsubscribe") }; } /** * Subscribe to all events (wildcard) */ subscribeToAll(handler) { this.wildcardSubscribers.add(handler); this.logger.debug("Wildcard subscriber added"); return { unsubscribe: /* @__PURE__ */ __name(() => { this.wildcardSubscribers.delete(handler); this.logger.debug("Wildcard subscriber removed"); }, "unsubscribe") }; } /** * Get all handlers for an event type */ getHandlersForEvent(eventType) { const handlers = []; const specificHandlers = this.subscribers.get(eventType); if (specificHandlers) { handlers.push(...specificHandlers); } handlers.push(...this.wildcardSubscribers); return handlers; } /** * Execute a handler with error isolation */ async executeHandler(handler, event) { try { await Promise.resolve(handler(event)); } catch (error) { this.logger.error("Event handler error", error, { eventType: event.eventType, aggregateId: event.aggregateId }); throw error; } } /** * Add event to history for debugging/replay */ addToHistory(event) { this.eventHistory.push(event); if (this.eventHistory.length > this.maxHistorySize) { this.eventHistory = this.eventHistory.slice(-this.maxHistorySize); } } /** * Get event history (for debugging) */ getHistory(limit) { if (limit) { return this.eventHistory.slice(-limit); } return [ ...this.eventHistory ]; } /** * Clear all subscribers (useful for testing) */ clearAllSubscribers() { this.subscribers.clear(); this.wildcardSubscribers.clear(); this.logger.info("All subscribers cleared"); } /** * Get subscriber statistics */ getStats() { let totalHandlers = 0; for (const handlers of this.subscribers.values()) { totalHandlers += handlers.size; } return { eventTypes: this.subscribers.size, totalHandlers, wildcardHandlers: this.wildcardSubscribers.size, historySize: this.eventHistory.length }; } }; var DomainEventBuilder = class { static { __name(this, "DomainEventBuilder"); } static create(eventType, aggregateId, data = {}) { return { eventType, aggregateId, occurredAt: /* @__PURE__ */ new Date(), version: 1, ...data }; } }; // src/shared/domain/AggregateRootBase.ts var AggregateRootBase = class { static { __name(this, "AggregateRootBase"); } id; version = 0; uncommittedEvents = []; constructor(id, version) { this.id = id; this.version = version || 0; } getId() { return this.id; } getVersion() { return this.version; } incrementVersion() { this.version++; } addDomainEvent(event) { this.uncommittedEvents.push(event); } getUncommittedEvents() { return this.uncommittedEvents; } markEventsAsCommitted() { this.uncommittedEvents = []; } }; // src/bounded-contexts/authentication/domain/aggregates/AuthSession.ts var AuthSession = class _AuthSession extends AggregateRootBase { static { __name(this, "AuthSession"); } personId; email; issuedAt; expiresAt; isActive; ipAddress; userAgent; lastAccessedAt; constructor(id, personId, email, issuedAt, expiresAt, isActive, ipAddress, userAgent, lastAccessedAt, version = 0) { super(id, version), this.personId = personId, this.email = email, this.issuedAt = issuedAt, this.expiresAt = expiresAt, this.isActive = isActive, this.ipAddress = ipAddress, this.userAgent = userAgent, this.lastAccessedAt = lastAccessedAt; } /** * Factory method to create new session */ static create(sessionId, personId, email, expirationHours, ipAddress, userAgent) { const now = /* @__PURE__ */ new Date(); const expiresAt = new Date(now.getTime() + expirationHours * 60 * 60 * 1e3); return new _AuthSession(sessionId, personId, email, now, expiresAt, true, ipAddress, userAgent, now); } /** * Reconstitute from persistence */ static fromSnapshot(snapshot) { return new _AuthSession(snapshot.id, snapshot.personId, snapshot.email, snapshot.issuedAt, snapshot.expiresAt, snapshot.isActive, snapshot.ipAddress, snapshot.userAgent, snapshot.lastAccessedAt, snapshot.version); } /** * Check if session is valid */ isValid() { if (!this.isActive) return false; if (this.expiresAt < /* @__PURE__ */ new Date()) return false; return true; } /** * Invalidate session */ invalidate(reason = "User logout") { if (!this.isActive) return; this.isActive = false; this.incrementVersion(); this.addDomainEvent({ aggregateId: this.id, personId: this.personId, reason, occurredAt: /* @__PURE__ */ new Date() }); } /** * Extend session expiration */ extend(additionalHours) { if (!this.isActive) { throw new Error("Cannot extend inactive session"); } const newExpiresAt = new Date(this.expiresAt.getTime() + additionalHours * 60 * 60 * 1e3); this.expiresAt = newExpiresAt; this.incrementVersion(); this.addDomainEvent({ aggregateId: this.id, personId: this.personId, extendedBy: additionalHours, newExpiresAt, occurredAt: /* @__PURE__ */ new Date() }); } /** * Update last accessed time */ updateLastAccessed() { this.lastAccessedAt = /* @__PURE__ */ new Date(); this.incrementVersion(); } /** * Get remaining time in minutes */ getRemainingMinutes() { if (!this.isValid()) return 0; const remaining = this.expiresAt.getTime() - Date.now(); return Math.max(0, Math.floor(remaining / (1e3 * 60))); } // Getters for read-only access getPersonId() { return this.personId; } getEmail() { return this.email; } getExpiresAt() { return this.expiresAt; } getIssuedAt() { return this.issuedAt; } getIpAddress() { return this.ipAddress; } getUserAgent() { return this.userAgent; } // Snapshot for persistence toSnapshot() { return { id: this.id, personId: this.personId, email: this.email, issuedAt: this.issuedAt, expiresAt: this.expiresAt, isActive: this.isActive, ipAddress: this.ipAddress, userAgent: this.userAgent, lastAccessedAt: this.lastAccessedAt, version: this.version }; } }; // src/bounded-contexts/authentication/application/services/AuthenticationService.ts var AuthenticationService = class { static { __name(this, "AuthenticationService"); } personAuthRepo; sessionRepo; eventBus; logger; constructor(personAuthRepo, sessionRepo, eventBus) { this.personAuthRepo = personAuthRepo; this.sessionRepo = sessionRepo; this.eventBus = eventBus; this.logger = Logger.getInstance().child({ service: "AuthenticationService" }); } /** * Handle login - thin orchestration, not business logic */ async login(command) { try { const personAuth = await this.personAuthRepo.findByEmail(command.email); if (!personAuth) { return { success: false, error: "Invalid credentials", attemptsRemaining: 5 }; } const authResult = await personAuth.authenticate(command.password); await this.personAuthRepo.save(personAuth); await this.publishEvents(personAuth); if (authResult.success && authResult.sessionId) { const session = await this.createSession(personAuth, authResult.sessionId, command.rememberMe, command.ipAddress, command.userAgent); return { success: true, sessionId: session.getId(), personId: personAuth.getPersonId(), email: personAuth.getEmail(), expiresAt: session.getExpiresAt() }; } return { success: false, error: authResult.failureReason || "Authentication failed", attemptsRemaining: authResult.attemptsRemaining }; } catch (error) { console.error("Login error:", error); return { success: false, error: "An error occurred during login" }; } } /** * Logout - simple and focused */ async logout(sessionId) { const session = await this.sessionRepo.findById(sessionId); if (session) { session.invalidate(); await this.sessionRepo.save(session); await this.publishEvents(session); } } async createSession(personAuth, sessionId, rememberMe, ipAddress, userAgent) { const expirationHours = rememberMe ? 24 * 30 : 24; const session = AuthSession.create(sessionId, personAuth.getPersonId(), personAuth.getEmail(), expirationHours, ipAddress, userAgent); await this.sessionRepo.save(session); await this.publishEvents(session); return session; } async publishEvents(aggregate) { const events = aggregate.getUncommittedEvents(); for (const event of events) { await this.eventBus.publish(event); } aggregate.markEventsAsCommitted(); } }; // src/bounded-contexts/authentication/domain/aggregates/PersonAuth.ts import * as bcrypt from "bcrypt"; import { v4 as uuidv4 } from "uuid"; var Email = class { static { __name(this, "Email"); } value; constructor(value) { this.value = value; if (!this.isValid(value)) { throw new Error(`Invalid email: ${value}`); } } isValid(email) { const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; return emailRegex.test(email); } equals(other) { return this.value === other.value; } }; var HashedPassword = class _HashedPassword { static { __name(this, "HashedPassword"); } value; constructor(value) { this.value = value; if (!value || value.length < 20) { throw new Error("Invalid hashed password"); } } async verify(plainPassword) { return bcrypt.compare(plainPassword, this.value); } static async create(plainPassword) { if (plainPassword.length < 8) { throw new Error("Password must be at least 8 characters"); } const hashed = await bcrypt.hash(plainPassword, 12); return new _HashedPassword(hashed); } }; var AuthenticationResult = class _AuthenticationResult { static { __name(this, "AuthenticationResult"); } success; sessionId; failureReason; attemptsRemaining; constructor(success, sessionId, failureReason, attemptsRemaining) { this.success = success; this.sessionId = sessionId; this.failureReason = failureReason; this.attemptsRemaining = attemptsRemaining; } static successful(sessionId) { return new _AuthenticationResult(true, sessionId); } static failed(reason, attemptsRemaining) { return new _AuthenticationResult(false, void 0, reason, attemptsRemaining); } static locked() { return new _AuthenticationResult(false, void 0, "Account is locked", 0); } }; var PersonAuth = class _PersonAuth extends AggregateRootBase { static { __name(this, "PersonAuth"); } personId; email; hashedPassword; isActive; isEmailVerified; failedLoginAttempts; lastLoginAt; emailVerifiedAt; passwordChangedAt; lockedUntil; static MAX_LOGIN_ATTEMPTS = 5; static LOCKOUT_DURATION_MINUTES = 30; constructor(id, personId, email, hashedPassword, isActive, isEmailVerified, failedLoginAttempts, lastLoginAt, emailVerifiedAt, passwordChangedAt, lockedUntil, version = 0) { super(id, version), this.personId = personId, this.email = email, this.hashedPassword = hashedPassword, this.isActive = isActive, this.isEmailVerified = isEmailVerified, this.failedLoginAttempts = failedLoginAttempts, this.lastLoginAt = lastLoginAt, this.emailVerifiedAt = emailVerifiedAt, this.passwordChangedAt = passwordChangedAt, this.lockedUntil = lockedUntil; } /** * Factory method to create a new PersonAuth */ static async create(personId, email, plainPassword) { const id = uuidv4(); const emailVO = new Email(email); const hashedPassword = await HashedPassword.create(plainPassword); const personAuth = new _PersonAuth( id, personId, emailVO, hashedPassword, true, false, 0, void 0, void 0, /* @__PURE__ */ new Date(), void 0 // lockedUntil ); personAuth.addDomainEvent({ aggregateId: id, personId, email, occurredAt: /* @__PURE__ */ new Date() }); return personAuth; } /** * Reconstitute from persistence */ static fromSnapshot(snapshot) { return new _PersonAuth(snapshot.id, snapshot.personId, new Email(snapshot.email), new HashedPassword(snapshot.hashedPassword), snapshot.isActive, snapshot.isEmailVerified, snapshot.failedLoginAttempts, snapshot.lastLoginAt, snapshot.emailVerifiedAt, snapshot.passwordChangedAt, snapshot.lockedUntil, snapshot.version); } /** * Authenticate with password * This is the core business logic - not in a handler! */ async authenticate(plainPassword) { if (this.isLocked()) { this.addDomainEvent({ aggregateId: this.id, email: this.email.value, reason: "Account is locked", attemptsRemaining: 0, occurredAt: /* @__PURE__ */ new Date() }); return AuthenticationResult.locked(); } if (!this.canLogin()) { const reason = !this.isActive ? "Account is inactive" : "Email not verified"; this.addDomainEvent({ aggregateId: this.id, email: this.email.value, reason, attemptsRemaining: _PersonAuth.MAX_LOGIN_ATTEMPTS - this.failedLoginAttempts, occurredAt: /* @__PURE__ */ new Date() }); return AuthenticationResult.failed(reason, _PersonAuth.MAX_LOGIN_ATTEMPTS - this.failedLoginAttempts); } const isValid = await this.hashedPassword.verify(plainPassword); if (!isValid) { this.recordFailedAttempt(); const attemptsRemaining = Math.max(0, _PersonAuth.MAX_LOGIN_ATTEMPTS - this.failedLoginAttempts); this.addDomainEvent({ aggregateId: this.id, email: this.email.value, reason: "Invalid password", attemptsRemaining, occurredAt: /* @__PURE__ */ new Date() }); if (this.failedLoginAttempts >= _PersonAuth.MAX_LOGIN_ATTEMPTS) { this.lock(); } return AuthenticationResult.failed("Invalid credentials", attemptsRemaining); } const sessionId = uuidv4(); this.recordSuccessfulLogin(); this.addDomainEvent({ aggregateId: this.id, personId: this.personId, email: this.email.value, sessionId, occurredAt: /* @__PURE__ */ new Date() }); return AuthenticationResult.successful(sessionId); } /** * Change password - domain logic, not in handler */ async changePassword(currentPassword, newPassword) { const isCurrentValid = await this.hashedPassword.verify(currentPassword); if (!isCurrentValid) { return false; } this.hashedPassword = await HashedPassword.create(newPassword); this.passwordChangedAt = /* @__PURE__ */ new Date(); this.failedLoginAttempts = 0; this.lockedUntil = void 0; this.incrementVersion(); this.addDomainEvent({ aggregateId: this.id, personId: this.personId, changedBy: "user", occurredAt: /* @__PURE__ */ new Date() }); return true; } /** * Admin password reset */ async resetPasswordByAdmin(newPassword) { this.hashedPassword = await HashedPassword.create(newPassword); this.passwordChangedAt = /* @__PURE__ */ new Date(); this.failedLoginAttempts = 0; this.lockedUntil = void 0; this.incrementVersion(); this.addDomainEvent({ aggregateId: this.id, personId: this.personId, changedBy: "admin", occurredAt: /* @__PURE__ */ new Date() }); } /** * Verify email */ verifyEmail() { if (this.isEmailVerified) return; this.isEmailVerified = true; this.emailVerifiedAt = /* @__PURE__ */ new Date(); this.incrementVersion(); } /** * Activate account */ activate() { if (this.isActive) return; this.isActive = true; this.incrementVersion(); } /** * Deactivate account */ deactivate() { if (!this.isActive) return; this.isActive = false; this.incrementVersion(); } // Private helper methods recordFailedAttempt() { this.failedLoginAttempts++; this.incrementVersion(); } recordSuccessfulLogin() { this.failedLoginAttempts = 0; this.lastLoginAt = /* @__PURE__ */ new Date(); this.incrementVersion(); } lock() { const lockUntil = /* @__PURE__ */ new Date(); lockUntil.setMinutes(lockUntil.getMinutes() + _PersonAuth.LOCKOUT_DURATION_MINUTES); this.lockedUntil = lockUntil; this.addDomainEvent({ aggregateId: this.id, personId: this.personId, reason: "Too many failed login attempts", lockedUntil: lockUntil, occurredAt: /* @__PURE__ */ new Date() }); } isLocked() { if (!this.lockedUntil) return false; return this.lockedUntil > /* @__PURE__ */ new Date(); } canLogin() { return this.isActive && this.isEmailVerified && !this.isLocked(); } // Getters for read-only access getPersonId() { return this.personId; } getEmail() { return this.email.value; } isAccountActive() { return this.isActive; } isAccountVerified() { return this.isEmailVerified; } getFailedAttempts() { return this.failedLoginAttempts; } getLastLoginAt() { return this.lastLoginAt; } // Snapshot for persistence toSnapshot() { return { id: this.id, personId: this.personId, email: this.email.value, hashedPassword: this.hashedPassword.value, isActive: this.isActive, isEmailVerified: this.isEmailVerified, failedLoginAttempts: this.failedLoginAttempts, lastLoginAt: this.lastLoginAt, emailVerifiedAt: this.emailVerifiedAt, passwordChangedAt: this.passwordChangedAt, lockedUntil: this.lockedUntil, version: this.version }; } }; // src/bounded-contexts/authentication/application/services/RegistrationService.ts var RegistrationService = class { static { __name(this, "RegistrationService"); } personAuthRepo; personRepo; eventBus; constructor(personAuthRepo, personRepo, eventBus) { this.personAuthRepo = personAuthRepo; this.personRepo = personRepo; this.eventBus = eventBus; } /** * Register new user authentication */ async register(command) { try { const person = await this.personRepo.findById(command.personId); if (!person) { return { success: false, error: "Person not found" }; } if (await this.personAuthRepo.emailExists(command.email)) { return { success: false, error: "Email already registered" }; } if (person.getEmail() !== command.email) { return { success: false, error: "Email does not match person record" }; } const personAuth = await PersonAuth.create(command.personId, command.email, command.password); await this.personAuthRepo.save(personAuth); await this.publishEvents(personAuth); return { success: true, personAuthId: personAuth.getId(), message: "Registration successful. Please verify your email." }; } catch (error) { console.error("Registration error:", error); return { success: false, error: error instanceof Error ? error.message : "Registration failed" }; } } /** * Verify email address */ async verifyEmail(token) { const personAuthId = await this.validateEmailToken(token); if (!personAuthId) return false; const personAuth = await this.personAuthRepo.findById(personAuthId); if (!personAuth) return false; personAuth.verifyEmail(); await this.personAuthRepo.save(personAuth); await this.publishEvents(personAuth); return true; } async validateEmailToken(token) { return null; } async publishEvents(aggregate) { const events = aggregate.getUncommittedEvents(); for (const event of events) { await this.eventBus.publish(event); } aggregate.markEventsAsCommitted(); } }; // src/bounded-contexts/authentication/application/services/PasswordService.ts import { v4 as uuidv42 } from "uuid"; var PasswordService = class { static { __name(this, "PasswordService"); } personAuthRepo; sessionRepo; eventBus; emailService; resetTokens = /* @__PURE__ */ new Map(); constructor(personAuthRepo, sessionRepo, eventBus, emailService) { this.personAuthRepo = personAuthRepo; this.sessionRepo = sessionRepo; this.eventBus = eventBus; this.emailService = emailService; } /** * User-initiated password change */ async changePassword(command) { const personAuth = await this.personAuthRepo.findById(command.personAuthId); if (!personAuth) return false; const success = await personAuth.changePassword(command.currentPassword, command.newPassword); if (success) { await this.personAuthRepo.save(personAuth); await this.publishEvents(personAuth); await this.terminateAllSessions(personAuth.getPersonId()); } return success; } /** * Request password reset via email */ async requestPasswordReset(command) { const personAuth = await this.personAuthRepo.findByEmail(command.email); if (!personAuth) { return true; } const token = uuidv42(); const expiresAt = /* @__PURE__ */ new Date(); expiresAt.setHours(expiresAt.getHours() + 1); this.resetTokens.set(token, { personAuthId: personAuth.getId(), expiresAt }); await this.emailService.send({ to: command.email, subject: "Password Reset Request", template: "password-reset", data: { token, expiresAt } }); return true; } /** * Complete password reset with token */ async completePasswordReset(command) { const tokenData = this.resetTokens.get(command.token); if (!tokenData || tokenData.expiresAt < /* @__PURE__ */ new Date()) { return false; } const personAuth = await this.personAuthRepo.findById(tokenData.personAuthId); if (!personAuth) return false; await personAuth.resetPasswordByAdmin(command.newPassword); await this.personAuthRepo.save(personAuth); await this.publishEvents(personAuth); this.resetTokens.delete(command.token); await this.terminateAllSessions(personAuth.getPersonId()); return true; } /** * Admin-initiated password reset */ async adminResetPassword(command) { const personAuth = await this.personAuthRepo.findById(command.personAuthId); if (!personAuth) return false; await personAuth.resetPasswordByAdmin(command.newPassword); await this.personAuthRepo.save(personAuth); await this.publishEvents(personAuth); await this.terminateAllSessions(personAuth.getPersonId()); return true; } async terminateAllSessions(personId) { const sessions = await this.sessionRepo.findByPersonId(personId); for (const session of sessions) { session.invalidate(); await this.sessionRepo.save(session); } } async publishEvents(aggregate) { const events = aggregate.getUncommittedEvents(); for (const event of events) { await this.eventBus.publish(event); } aggregate.markEventsAsCommitted(); } }; // src/infrastructure/repositories/MongoPersonAuthRepository.ts import { MongoClient } from "mongodb"; import { v4 as uuidv43 } from "uuid"; var MongoPersonAuthRepository = class { static { __name(this, "MongoPersonAuthRepository"); } mongoUrl; dbName; db; collection; client; constructor(mongoUrl, dbName) { this.mongoUrl = mongoUrl; this.dbName = dbName; } async ensureConnection() { if (!this.client) { this.client = new MongoClient(this.mongoUrl); await this.client.connect(); this.db = this.client.db(this.dbName); this.collection = this.db.collection("person_auth"); await this.collection.createIndex({ email: 1 }, { unique: true }); await this.collection.createIndex({ personId: 1 }); } } async save(personAuth) { await this.ensureConnection(); const snapshot = personAuth.toSnapshot(); await this.collection.replaceOne({ _id: snapshot.id }, { _id: snapshot.id, ...snapshot }, { upsert: true }); } async findById(id) { await this.ensureConnection(); const doc = await this.collection.findOne({ _id: id }); if (!doc) return null; return PersonAuth.fromSnapshot(doc); } async findByEmail(email) { await this.ensureConnection(); const doc = await this.collection.findOne({ email: email.toLowerCase() }); if (!doc) return null; return PersonAuth.fromSnapshot(doc); } async findByPersonId(personId) { await this.ensureConnection(); const doc = await this.collection.findOne({ personId }); if (!doc) return null; return PersonAuth.fromSnapshot(doc); } async emailExists(email) { await this.ensureConnection(); const count = await this.collection.countDocuments({ email: email.toLowerCase() }); return count > 0; } async remove(id) { await this.ensureConnection(); await this.collection.deleteOne({ _id: id }); } nextIdentity() { return uuidv43(); } async disconnect() { if (this.client) { await this.client.close(); } } }; // src/infrastructure/repositories/MongoAuthSessionRepository.ts import { MongoClient as MongoClient2 } from "mongodb"; var MongoAuthSessionRepository = class { static { __name(this, "MongoAuthSessionRepository"); } mongoUrl; dbName; db; collection; client; constructor(mongoUrl, dbName) { this.mongoUrl = mongoUrl; this.dbName = dbName; } async ensureConnection() { if (!this.client) { this.client = new MongoClient2(this.mongoUrl); await this.client.connect(); this.db = this.client.db(this.dbName); this.collection = this.db.collection("auth_sessions"); await this.collection.createIndex({ personId: 1 }); await this.collection.createIndex({ expiresAt: 1 }); await this.collection.createIndex({ isActive: 1 }); } } async save(session) { await this.ensureConnection(); const snapshot = session.toSnapshot(); await this.collection.replaceOne({ _id: snapshot.id }, { _id: snapshot.id, ...snapshot }, { upsert: true }); } async findById(sessionId) { await this.ensureConnection(); const doc = await this.collection.findOne({ _id: sessionId }); if (!doc) return null; return AuthSession.fromSnapshot(doc); } async findByPersonId(personId) { await this.ensureConnection(); const docs = await this.collection.find({ personId }).toArray(); return docs.map((doc) => AuthSession.fromSnapshot(doc)); } async findActiveByPersonId(personId) { await this.ensureConnection(); const now = /* @__PURE__ */ new Date(); const docs = await this.collection.find({ personId, isActive: true, expiresAt: { $gt: now } }).toArray(); return docs.map((doc) => AuthSession.fromSnapshot(doc)); } async remove(sessionId) { await this.ensureConnection(); await this.collection.deleteOne({ _id: sessionId }); } async removeByPersonId(personId) { await this.ensureConnection(); await this.collection.deleteMany({ personId }); } async removeExpired() { await this.ensureConnection(); const now = /* @__PURE__ */ new Date(); const result = await this.collection.deleteMany({ expiresAt: { $lt: now } }); return result.deletedCount; } async disconnect() { if (this.client) { await this.client.close(); } } }; // src/infrastructure/repositories/MongoPersonRepository.ts import { MongoClient as MongoClient3 } from "mongodb"; // src/bounded-contexts/parties/domain/aggregates/Person.ts import { v4 as uuidv44 } from "uuid"; var PersonName = class { static { __name(this, "PersonName"); } firstName; lastName; middleName; constructor(firstName, lastName, middleName) { this.firstName = firstName; this.lastName = lastName; this.middleName = middleName; if (!firstName || firstName.trim().length === 0) { throw new Error("First name is required"); } if (!lastName || lastName.trim().length === 0) { throw new Error("Last name is required"); } } getFullName() { return this.middleName ? `${this.firstName} ${this.middleName} ${this.lastName}` : `${this.firstName} ${this.lastName}`; } equals(other) { return this.firstName === other.firstName && this.lastName === other.lastName && this.middleName === other.middleName; } }; var EmailAddress = class { static { __name(this, "EmailAddress"); } value; constructor(value) { this.value = value; if (!this.isValid(value)) { throw new Error(`Invalid email: ${value}`); } } isValid(email) { const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; return emailRegex.test(email); } equals(other) { return this.value.toLowerCase() === other.value.toLowerCase(); } }; var Person = class _Person extends AggregateRootBase { static { __name(this, "Person"); } name; email; roleType; isActive; jobTitle; department; organizationId; createdAt; updatedAt; constructor(id, name, email, roleType, isActive, jobTitle, department, organizationId, createdAt, updatedAt, version = 0) { super(id, version), this.name = name, this.email = email, this.roleType = roleType, this.isActive = isActive, this.jobTitle = jobTitle, this.department = department, this.organizationId = organizationId, this.createdAt = createdAt, this.updatedAt = updatedAt; } /** * Factory method to create new Person */ static create(firstName, lastName, email, roleType = "Human", middleName) { const id = uuidv44(); const name = new PersonName(firstName, lastName, middleName); const emailVO = new EmailAddress(email); const now = /* @__PURE__ */ new Date(); const person = new _Person(id, name, emailVO, roleType, true, void 0, void 0, void 0, now, now); person.addDomainEvent({ aggregateId: id, email, firstName, lastName, roleType, occurredAt: now }); return person; } /** * Reconstitute from persistence */ static fromSnapshot(snapshot) { return new _Person(snapshot.id, new PersonName(snapshot.firstName, snapshot.lastName, snapshot.middleName), new EmailAddress(snapshot.email), snapshot.roleType, snapshot.isActive, snapshot.jobTitle, snapshot.department, snapshot.organizationId, snapshot.createdAt, snapshot.updatedAt, snapshot.version); } /** * Update personal information */ updatePersonalInfo(firstName, lastName, middleName) { const changedFields = []; if (firstName && firstName !== this.name.firstName) { changedFields.push("firstName"); } if (lastName && lastName !== this.name.lastName) { changedFields.push("lastName"); } if (middleName !== void 0 && middleName !== this.name.middleName) { changedFields.push("middleName"); } if (changedFields.length > 0) { this.name = new PersonName(firstName || this.name.firstName, lastName || this.name.lastName, middleName !== void 0 ? middleName : this.name.middleName); this.updatedAt = /* @__PURE__ */ new Date(); this.incrementVersion(); this.addDomainEvent({ aggregateId: this.id, changedFields, occurredAt: /* @__PURE__ */ new Date() }); } } /** * Update email address */ updateEmail(newEmail) { const newEmailVO = new EmailAddress(newEmail); if (!this.email.equals(newEmailVO)) { this.email = newEmailVO; this.updatedAt = /* @__PURE__ */ new Date(); this.incrementVersion(); this.addDomainEvent({ aggregateId: this.id, changedFields: [ "email" ], occurredAt: /* @__PURE__ */ new Date() }); } } /** * Update job information */ updateJobInfo(jobTitle, department) { const changedFields = []; if (jobTitle !== this.jobTitle) { this.jobTitle = jobTitle; changedFields.push("jobTitle"); } if (department !== this.department) { this.department = department; changedFields.push("department"); } if (changedFields.length > 0) { this.updatedAt = /* @__PURE__ */ new Date(); this.incrementVersion(); this.addDomainEvent({ aggregateId: this.id, changedFields, occurredAt: /* @__PURE__ */ new Date() }); } } /** * Assign to organization */ assignToOrganization(organizationId) { if (this.organizationId !== organizationId) { this.organizationId = organizationId; this.updatedAt = /* @__PURE__ */ new Date(); this.incrementVersion(); this.addDomainEvent({ aggregateId: this.id, changedFields: [ "organizationId" ], occurredAt: /* @__PURE__ */ new Date() }); } } /** * Deactivate person */ deactivate(reason) { if (!this.isActive) return; this.isActive = false; this.updatedAt = /* @__PURE__ */ new Date(); this.incrementVersion(); this.addDomainEvent({ aggregateId: this.id, reason, occurredAt: /* @__PURE__ */ new Date() }); } /** * Reactivate person */ reactivate() { if (this.isActive) return; this.isActive = true; this.updatedAt = /* @__PURE__ */ new Date(); this.incrementVersion(); this.addDomainEvent({ aggregateId: this.id, occurredAt: /* @__PURE__ */ new Date() }); } // Getters for read-only access getFullName() { return this.name.getFullName(); } getFirstName() { return this.name.firstName; } getLastName() { return this.name.lastName; } getMiddleName() { return this.name.middleName; } getEmail() { return this.email.value; } getRoleType() { return this.roleType; } getJobTitle() { return this.jobTitle; } getDepartment() { return this.department; } getOrganizationId() { return this.organizationId; } isPersonActive() { return this.isActive; } getCreatedAt() { return this.createdAt; } getUpdatedAt() { return this.updatedAt; } // Snapshot for persistence toSnapshot() { return { id: this.id, firstName: this.name.firstName, lastName: this.name.lastName, middleName: this.name.middleName, email: this.email.value, roleType: this.roleType, isActive: this.isActive, jobTitle: this.jobTitle, department: this.department, organizationId: this.organizationId, createdAt: this.createdAt, updatedAt: this.updatedAt, version: this.version }; } }; // src/infrastructure/repositories/MongoPersonRepository.ts import { v4 as uuidv45 } from "uuid"; var MongoPersonRepository = class { static { __name(this, "MongoPersonRepository"); } mongoUrl; dbName; db; collection; client; constructor(mongoUrl, dbName) { this.mongoUrl = mongoUrl; this.dbName = dbName; } async ensureConnection() { if (!this.client) { this.client = new MongoClient3(this.mongoUrl); await this.client.connect(); this.db = this.client.db(this.dbName); this.collection = this.db.collection("persons"); await this.collection.createIndex({ email: 1 }, { unique: true }); await this.collection.createIndex({ organizationId: 1 }); await this.collection.createIndex({ department: 1 }); await this.collection.createIndex({ isActive: 1 }); } } async save(person) { await this.ensureConnection(); const snapshot = person.toSnapshot(); await this.collection.replaceOne({ _id: snapshot.id }, { _id: snapshot.id, ...snapshot }, { upsert: true }); } async findById(id) { await this.ensureConnection(); const doc = await this.collection.findOne({ _id: id }); if (!doc) return null; return Person.fromSnapshot(doc); } async findByEmail(email) { await this.ensureConnection(); const doc = await this.collection.findOne({ email: email.toLowerCase() }); if (!doc) return null; return Person.fromSnapshot(doc); } async emailExists(email) { await this.ensureConnection(); const count = await this.collection.countDocuments({ email: email.toLowerCase() }); return count > 0; } async findByOrganization(organizationId) { await this.ensureConnection(); const docs = await this.collection.find({ organizationId, isActive: true }).toArray(); return docs.map((doc) => Person.fromSnapshot(doc)); } async findByDepartment(department) { await this.ensureConnection(); const docs = await this.collection.find({ department, isActive: true }).toArray(); return docs.map((doc) => Person.fromSnapshot(doc)); } async findAll(offset = 0, limit = 100) { await this.ensureConnection(); const docs = await this.collection.find({ isActive: true }).skip(offset).limit(limit).toArray(); return docs.map((doc) => Person.fromSnapshot(doc)); } async count() { await this.ensureConnection(); return await this.collection.countDocuments({}); } async countActive() { await this.ensureConnection(); return await this.collection.countDocuments({ isActive: true }); } async remove(id) { await this.ensureConnection(); await this.collection.updateOne({ _id: id }, { $set: { isActive: false, updatedAt: /* @__PURE__ */ new Date() } }); } nextIdentity() { return uuidv45(); } async disconnect() { if (this.client) { await this.client.close(); } } }; // src/infrastructure/repositories/MongoOrganisationRepository.ts import { MongoClient as MongoClient4 } from "mongodb"; // src/bounded-contexts/parties/domain/aggregates/Organisation.ts import { v4 as uuidv46 } from "uuid"; var OrganisationName = class { static { __name(this, "OrganisationName"); } legalName; tradingName; constructor(legalName, tradingName) { this.legalName = legalName; this.tradingName = tradingName; if (!legalName || legalName.trim().length === 0) { throw new Error("Legal name is required"); } } getDisplayName() { return this.tradingName || this.legalName; } equals(other) { return this.legalName === other.legalName && this.tradingName === other.tradingName; } }; var RegistrationNumber = class { static { __name(this, "RegistrationNumber"); } value; constructor(value) { this.value = value; if (!value || value.trim().length === 0) { throw new Error("Registration number is required"); } } equals(other) { return this.value === other.value; } }; var Organisation = class _Organisation extends AggregateRootBase { static { __name(this, "Organisation"); } name; registrationNumber; industry; website; isActive; parentOrganisationId; createdAt; updatedAt; constructor(id, name, registrationNumber, industry, website, isActive = true, parentOrganisationId, createdAt, updatedAt, version = 0) { super(id, version), this.name = name, this.registrationNumber = registrationNumber, this.industry = industry, this.website = website, this.isActive = isActive, this.parentOrganisationId = parentOrganisationId, this.createdAt = createdAt, this.updatedAt = updatedAt; } /** * Factory method to create new Organisation */ static create(legalName, tradingName, registrationNumber, industry) { const id = uuidv46(); const name = new OrganisationName(legalName, tradingName); const regNumber = registrationNumber ? new RegistrationNumber(registrationNumber) : void 0; const now = /* @__PURE__ */ new Date(); const org = new _Organisation(id, name, regNumber, industry, void 0, true, void 0, now, now); org.addDomainEvent({ aggregateId: id, legalName, industry, occurredAt: now }); return org; } /** * Reconstitute from persistence */ static fromSnapshot(snapshot) { return new _Organisation(snapshot.id, new OrganisationName(snapshot.legalName, snapshot.tradingName), snapshot.registrationNumber ? new RegistrationNumber(snapshot.registrationNumber) : void 0, snapshot.industry, snapshot.website, snapshot.isActive, snapshot.parentOrganisationId, snapshot.createdAt, snapshot.updatedAt, snapshot.version); } /** * Update organisation details */ updateDetails(tradingName, industry, website) { const changedFields = []; if (tradingName !== void 0 && tradingName !== this.name.tradingName) { this.name = new OrganisationName(this.name.legalName, tradingName); changedFields.push("tradingName"); } if (industry !== void 0 && industry !== this.industry) { this.industry = industry; changedFields.push("industry"); } if (website !== void 0 && website !== this.website) { this.website = website; changedFields.push("website"); } if (changedFields.length > 0) { this.updatedAt = /* @__PURE__ */ new Date(); this.incrementVersion(); this.addDomainEvent({ aggregateId: this.id, changedFields, occurredAt: /* @__PURE__ */ new Date() }); } } /** * Set parent organisation */ setParentOrganisation(parentId) { if (parentId === this.id) { throw new Error("Organisation cannot be its own parent"); } if (this.parentOrganisationId !== parentId) { this.parentOrganisationId = parentId; this.updatedAt = /* @__PURE__ */ new Date(); this.incrementVersion(); this.addDomainEvent({ aggregateId: this.id, changedFields: [ "parentOrganisationId" ], occurredAt: /* @__PURE__ */ new Date() }); } } /** * Deactivate organisation */ deactivate(reason) { if (!this.isActive) return; this.isActive = false; this.updatedAt = /* @__PURE__ */ new Date(); this.incrementVersion(); this.addDomainEvent({ aggregateId: this.id, reason, occurredAt: /* @__PURE__ */ new Date() }); } /** * Reactivate organisation */ reactivate() { if (this.isActive) return; this.isActive = true; this.updatedAt = /* @__PURE__ */ new Date(); this.incrementVersion(); } // Getters getLegalName() { return this.name.legalName; } getTradingName() { return this.name.tradingName; } getDisplayName() { return this.name.getDisplayName(); } getRegistrationNumber() {