feat: Wabenhain Honig-Shop Anwendung
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.
This commit is contained in:
1 parent
5c76fcc266
commit
c7927f2883
240 files changed
+26096
-137
No files matched your search
@@ -0,0 +1,67 @@
|
||||
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 };
|
||||
Reference in new issue
Block a user