Vollstaendige Next.js/Prisma-E-Commerce-Anwendung fuer eine Imkerei: Shop, Warenkorb, Stripe-Checkout, Kundenkonto mit 2FA, Admin-Bereich (Produkte, Ernten, Chargen, Bestellungen, Gutscheine, admin-editierbare Website-Inhalte), i18n (DE/EN), Rate-Limiting/CSP/Audit-Log-Haertung, PostgreSQL- und MySQL-Unterstuetzung ueber austauschbare Prisma- Treiber-Adapter, Unit-/Komponenten-/E2E-Tests.
68 lines
2.4 KiB
TypeScript
68 lines
2.4 KiB
TypeScript
import * as OTPAuth from "otpauth";
|
|
import QRCode from "qrcode";
|
|
import { randomBytes, createHash } from "node:crypto";
|
|
|
|
const ISSUER = "Wabenhain";
|
|
|
|
/** Erzeugt ein neues, zufaelliges Base32-TOTP-Secret. */
|
|
export function generateTotpSecret(): string {
|
|
return new OTPAuth.Secret({ size: 20 }).base32;
|
|
}
|
|
|
|
function buildTotp(secret: string, accountLabel: string): OTPAuth.TOTP {
|
|
return new OTPAuth.TOTP({
|
|
issuer: ISSUER,
|
|
label: accountLabel,
|
|
algorithm: "SHA1",
|
|
digits: 6,
|
|
period: 30,
|
|
secret,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Prueft einen 6-stelligen TOTP-Code gegen das Secret. `window: 1` erlaubt
|
|
* eine Toleranz von einem 30-Sekunden-Schritt vor/zurueck fuer leichte
|
|
* Uhrenabweichungen zwischen Server und Authenticator-App.
|
|
*/
|
|
export function verifyTotpCode(secret: string, code: string, accountLabel: string): boolean {
|
|
const totp = buildTotp(secret, accountLabel);
|
|
return totp.validate({ token: code.trim(), window: 1 }) !== null;
|
|
}
|
|
|
|
/**
|
|
* Wie verifyTotpCode, gibt bei einem gueltigen Code zusaetzlich den
|
|
* absoluten Zeitschritt zurueck (Unix-Zeit / Periode). Fuer Replay-Schutz
|
|
* beim Login: der Aufrufer vergleicht/speichert diesen Wert gegen
|
|
* Customer.lastTotpStep, damit ein mitgelesener Code nicht ein zweites Mal
|
|
* innerhalb desselben oder eines frueheren Zeitfensters funktioniert.
|
|
*/
|
|
export function verifyTotpCodeAndGetStep(secret: string, code: string, accountLabel: string): number | null {
|
|
const totp = buildTotp(secret, accountLabel);
|
|
const delta = totp.validate({ token: code.trim(), window: 1 });
|
|
if (delta === null) return null;
|
|
return Math.floor(Date.now() / 1000 / totp.period) + delta;
|
|
}
|
|
|
|
/** Data-URI eines QR-Codes fuer die otpauth://-URI, direkt in <img src> einsetzbar. */
|
|
export async function generateTotpQrCodeDataUrl(secret: string, accountLabel: string): Promise<string> {
|
|
const totp = buildTotp(secret, accountLabel);
|
|
return QRCode.toDataURL(totp.toString(), { margin: 1, width: 240 });
|
|
}
|
|
|
|
const BACKUP_CODE_COUNT = 8;
|
|
|
|
function hashBackupCode(code: string): string {
|
|
return createHash("sha256").update(code).digest("hex");
|
|
}
|
|
|
|
/** Erzeugt N lesbare Einmal-Codes (Klartext) plus deren Hashes zum Speichern. */
|
|
export function generateBackupCodes(): { plain: string[]; hashes: string[] } {
|
|
const plain = Array.from({ length: BACKUP_CODE_COUNT }, () =>
|
|
randomBytes(5).toString("hex").toUpperCase().match(/.{1,5}/g)!.join("-")
|
|
);
|
|
return { plain, hashes: plain.map(hashBackupCode) };
|
|
}
|
|
|
|
export { hashBackupCode };
|