@swoft/party-manager
Version:
1,796 lines (1,778 loc) • 74.7 kB
JavaScript
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() {