import type { MagicLinkRepository, PasswordResetRequest } from '@core/identity/domain/repositories/MagicLinkRepository'; import type { Logger } from '@core/shared/domain/Logger'; import { Result } from '@core/shared/domain/Result'; export class InMemoryMagicLinkRepository implements MagicLinkRepository { private resetRequests: Map = new Map(); private rateLimitStore: Map = new Map(); // Rate limit: max 3 requests per 15 minutes private readonly RATE_LIMIT_MAX = 3; private readonly RATE_LIMIT_WINDOW = 15 * 60 * 1000; // 15 minutes constructor(private readonly logger: Logger) {} async createPasswordResetRequest(request: PasswordResetRequest): Promise { this.logger.debug('[InMemoryMagicLinkRepository] Creating password reset request', { email: request.email, token: request.token.substring(0, 10) + '...', expiresAt: request.expiresAt, }); this.resetRequests.set(request.token, request); // Update rate limit tracking const now = new Date(); const existing = this.rateLimitStore.get(request.email); if (existing) { // Reset count if window has passed if (now.getTime() - existing.lastRequest.getTime() > this.RATE_LIMIT_WINDOW) { this.rateLimitStore.set(request.email, { count: 1, lastRequest: now }); } else { this.rateLimitStore.set(request.email, { count: existing.count + 1, lastRequest: now }); } } else { this.rateLimitStore.set(request.email, { count: 1, lastRequest: now }); } } async findByToken(token: string): Promise { const request = this.resetRequests.get(token); if (!request) { return null; } // Check if expired if (request.expiresAt < new Date()) { this.resetRequests.delete(token); return null; } // Check if already used if (request.used) { return null; } return request; } async markAsUsed(token: string): Promise { const request = this.resetRequests.get(token); if (request) { request.used = true; this.logger.debug('[InMemoryMagicLinkRepository] Marked token as used', { token: token.substring(0, 10) + '...', }); } } async checkRateLimit(email: string): Promise> { const now = new Date(); const tracking = this.rateLimitStore.get(email); if (!tracking) { return Result.ok(undefined); } // Check if window has passed if (now.getTime() - tracking.lastRequest.getTime() > this.RATE_LIMIT_WINDOW) { return Result.ok(undefined); } // Check if exceeded limit if (tracking.count >= this.RATE_LIMIT_MAX) { const timeRemaining = Math.ceil( (this.RATE_LIMIT_WINDOW - (now.getTime() - tracking.lastRequest.getTime())) / 60000 ); return Result.err({ message: `Too many reset attempts. Please try again in ${timeRemaining} minutes.`, }); } return Result.ok(undefined); } async cleanupExpired(): Promise { const now = new Date(); const toDelete: string[] = []; for (const [token, request] of this.resetRequests.entries()) { if (request.expiresAt < now || request.used) { toDelete.push(token); } } toDelete.forEach(token => this.resetRequests.delete(token)); if (toDelete.length > 0) { this.logger.debug('[InMemoryMagicLinkRepository] Cleaned up expired tokens', { count: toDelete.length, }); } } }