mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-08-21 07:36:40 +00:00
refactor(backend): extract bootstrap into startup/shutdown modules (phase 5) (#745)
Move the startup and shutdown lifecycles out of index.ts: - bootstrap/startup.ts exports startServer(server) - migration check, service initialization, background watchdogs, HTTP listen, pilot-agent loopback bind. - bootstrap/shutdown.ts exports installShutdownHandlers(server) - SIGTERM/SIGINT handlers, in-order service stop chain, 10s force-exit guard, SQLite close. Restructure MfaService to add an instance + lifecycle so the replay purge timer no longer lives as a module-scope setInterval in index.ts. MfaService keeps all existing static methods (generateSecret, verifyTotp, currentWindow, generateBackupCodes, hashBackupCodes, verifyBackupCode, formatBackupCodeForDisplay, normalizeBackupCode, buildOtpauthUri) so every existing caller stays unchanged. The new start() / stop() pair is idempotent and calls .unref() so test shutdown is not blocked. bootstrap/startup calls MfaService.getInstance().start(). bootstrap/shutdown calls MfaService.getInstance().stop(). index.ts drops from 305 to 147 lines and now contains only the Express app composition: createApp, route mounts, remote proxy, createServer, attachUpgrade, static/SPA fallback, errorHandler, installShutdownHandlers, and the require.main guard that boots the server when run directly. Behavior is byte-for-byte identical: shutdown service order, log strings, force-exit timer, pilot-agent loopback logic, and the MFA purge cadence and debug logging all preserved verbatim.
This commit is contained in:
@@ -2,6 +2,9 @@ import crypto from 'crypto';
|
||||
import bcrypt from 'bcrypt';
|
||||
import { authenticator } from 'otplib';
|
||||
import { HashAlgorithms } from '@otplib/core';
|
||||
import { DatabaseService } from './DatabaseService';
|
||||
import { MFA_REPLAY_TTL_MS, MFA_REPLAY_PURGE_INTERVAL_MS } from '../helpers/constants';
|
||||
import { isDebugEnabled } from '../utils/debug';
|
||||
|
||||
// Configure otplib for the default TOTP contract we present to users:
|
||||
// - 6 digits
|
||||
@@ -27,6 +30,41 @@ export interface BackupVerifyResult {
|
||||
}
|
||||
|
||||
export class MfaService {
|
||||
private static instance: MfaService;
|
||||
private purgeTimer: NodeJS.Timeout | null = null;
|
||||
|
||||
public static getInstance(): MfaService {
|
||||
if (!MfaService.instance) MfaService.instance = new MfaService();
|
||||
return MfaService.instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Start the periodic purge of used-MFA-code rows. The replay blacklist
|
||||
* holds (user, code, window) tuples for the last ~2 minutes; older rows
|
||||
* are safe to drop. Idempotent: calling start() twice is a no-op.
|
||||
*/
|
||||
public start(): void {
|
||||
if (this.purgeTimer) return;
|
||||
this.purgeTimer = setInterval(() => {
|
||||
try {
|
||||
const deleted = DatabaseService.getInstance().purgeOldMfaCodes(Date.now() - MFA_REPLAY_TTL_MS);
|
||||
if (isDebugEnabled() && deleted > 0) {
|
||||
console.log('[MFA:diag] replay purge deleted=', deleted);
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn('[MFA] Replay purge failed:', (err as Error).message);
|
||||
}
|
||||
}, MFA_REPLAY_PURGE_INTERVAL_MS);
|
||||
this.purgeTimer.unref();
|
||||
}
|
||||
|
||||
public stop(): void {
|
||||
if (this.purgeTimer) {
|
||||
clearInterval(this.purgeTimer);
|
||||
this.purgeTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a fresh base32 TOTP secret ready for `buildOtpauthUri` and
|
||||
* `verifyTotp`. Each user should receive a unique secret.
|
||||
|
||||
Reference in New Issue
Block a user