Auth (hand-built on the auth core): - 2FA: User model gains two_factor_secret/recovery_codes/confirmed_at (+ idempotent MariaDB migration). authorize() requires a valid TOTP code when 2FA is confirmed (secret decrypted via Laravel APP_KEY, fail-closed). Two-step login (precheckLogin reveals the code field). /settings/2fa enable/confirm/disable flow. - Password reset: nodemailer email service; PasswordReset model + migration; /forgot (request, generic response) + /reset (token sha256 + 1h TTL, sets argon2id hash). Login links to forgot. Batch 7 (parallel agents): /admin/commandocentrum (RCON controls + emulator_errors), social write actions (friend request + guild forum new thread), /help/[category], /badges (public). env: APP_KEY, APP_URL, SMTP_*. Nav extended. Verified: tsc exit 0, vitest 48/48, next build exit 0 (64 page routes).
39 lines
1.0 KiB
TypeScript
39 lines
1.0 KiB
TypeScript
import nodemailer, { type Transporter } from "nodemailer";
|
|
import { env } from "@/env";
|
|
|
|
let transporter: Transporter | null = null;
|
|
|
|
function getTransport(): Transporter | null {
|
|
if (!env.SMTP_HOST) return null;
|
|
if (!transporter) {
|
|
transporter = nodemailer.createTransport({
|
|
host: env.SMTP_HOST,
|
|
port: env.SMTP_PORT ?? 587,
|
|
secure: env.SMTP_SECURE,
|
|
auth: env.SMTP_USER ? { user: env.SMTP_USER, pass: env.SMTP_PASSWORD } : undefined,
|
|
});
|
|
}
|
|
return transporter;
|
|
}
|
|
|
|
/** Send an HTML email. No-ops (returns false) when SMTP isn't configured. */
|
|
export async function sendMail(to: string, subject: string, html: string): Promise<boolean> {
|
|
const t = getTransport();
|
|
if (!t) {
|
|
console.warn("[email] SMTP not configured — skipping mail to", to);
|
|
return false;
|
|
}
|
|
try {
|
|
await t.sendMail({
|
|
from: env.SMTP_FROM ?? `no-reply@${env.HOTEL_NAME}`,
|
|
to,
|
|
subject,
|
|
html,
|
|
});
|
|
return true;
|
|
} catch (e) {
|
|
console.error("[email] send failed:", (e as Error).message);
|
|
return false;
|
|
}
|
|
}
|