Auth.js mit Zugangsdaten-Provider: E-Mail, Passwort und TOTP-Code werden in einem Formular abgefragt. Ein zweistufiger Ablauf muesste zwischen den Schritten einen Zwischenzustand halten - genau das ist die uebliche Schwachstelle, und am Handy ist ein Formular ohnehin schneller. Anmeldung: - Sperre wird VOR dem Passwortvergleich geprueft, damit ein gesperrter Zugang nicht einmal Argon2-Rechenzeit kostet. - Bei unbekannter Adresse wird gegen einen Dummy-Hash gerechnet und die Antwort auf eine Mindestdauer gestreckt. Sonst verraet die Antwortzeit, welche Adressen existieren. - Wiederherstellungscodes werden nach Gebrauch entfernt. - Veraltete Argon2-Parameter werden beim erfolgreichen Login nachgezogen. Rate-Limiting in der Datenbank statt Redis - ein Server, wenige Benutzer, kein Grund fuer eine weitere Komponente. Gezaehlt wird nach E-Mail und nach Anschluss, damit weder viele Anschluesse noch viele Adressen helfen. Zugriffsschutz (sitzung.ts): - Die Sitzung allein entscheidet nichts. Bei jedem Aufruf wird der Benutzer frisch geladen und die Sitzungsgeneration verglichen - so lassen sich alle Anmeldungen auf einen Schlag beenden. - erfordereZugang() prueft immer gegen die betreuteId, nie nur im UI. - Erneute Bestaetigung vor sensiblen Aktionen ueber ein signiertes, kurzlebiges Cookie statt ueber Serverzustand. Audit-Log: aenderungErmitteln() protokolliert nur die tatsaechlich geaenderten Felder; Passwoerter und TOTP-Geheimnisse werden ersetzt, nie gespeichert. Ein Fehler im Protokoll bricht die Fachaktion nicht ab - ausser die Aktion laeuft in einer Transaktion, dann faellt beides. CSP mit Nonce pro Anfrage in der Middleware: keine Fremdquellen, kein CDN, keine Fonts von aussen. Fachseiten werden nicht zwischengespeichert. Etappe: 1 Phase: Fundament
60 lines
1.9 KiB
TypeScript
60 lines
1.9 KiB
TypeScript
import 'server-only'
|
|
import { PrismaClient } from '@prisma/client'
|
|
|
|
/**
|
|
* Prisma-Client als Singleton.
|
|
*
|
|
* Im Entwicklungsmodus laedt Next.js Module bei jeder Aenderung neu. Ohne
|
|
* diesen Umweg ueber globalThis entstuende bei jedem Speichern eine neue
|
|
* Verbindung, bis Postgres keine mehr annimmt.
|
|
*/
|
|
|
|
const globalFuerPrisma = globalThis as unknown as {
|
|
prisma: PrismaClient | undefined
|
|
}
|
|
|
|
export const db =
|
|
globalFuerPrisma.prisma ??
|
|
new PrismaClient({
|
|
log:
|
|
process.env.NODE_ENV === 'development'
|
|
? [{ emit: 'stdout', level: 'warn' }, { emit: 'stdout', level: 'error' }]
|
|
: [{ emit: 'stdout', level: 'error' }],
|
|
})
|
|
|
|
if (process.env.NODE_ENV !== 'production') {
|
|
globalFuerPrisma.prisma = db
|
|
}
|
|
|
|
/**
|
|
* Standardfilter: Der Papierkorb wird ueberall ausgeblendet. Wer die
|
|
* geloeschten Datensaetze sehen will, fragt sie ausdruecklich ab.
|
|
*/
|
|
export const nichtGeloescht = { geloeschtAm: null } as const
|
|
|
|
/**
|
|
* Stichtag, ab dem ein Datensatz im Papierkorb endgueltig entfernt wird.
|
|
* 30 Tage - lang genug fuer ein "oh, das brauchte ich doch noch".
|
|
*/
|
|
export const PAPIERKORB_TAGE = 30
|
|
|
|
export function papierkorbGrenze(jetzt = new Date()): Date {
|
|
const grenze = new Date(jetzt)
|
|
grenze.setDate(grenze.getDate() - PAPIERKORB_TAGE)
|
|
return grenze
|
|
}
|
|
|
|
/**
|
|
* BigInt laesst sich nicht als JSON serialisieren. Server Components
|
|
* reichen Daten an Client Components weiter - deshalb hier ein Helfer,
|
|
* der Cent-Betraege auf dem Weg nach draussen in Number wandelt.
|
|
* Cent-Betraege bis 90 Billionen Euro bleiben dabei exakt.
|
|
*/
|
|
export function centZuZahl(wert: bigint | null | undefined): number | null {
|
|
if (wert === null || wert === undefined) return null
|
|
if (wert > BigInt(Number.MAX_SAFE_INTEGER) || wert < BigInt(-Number.MAX_SAFE_INTEGER)) {
|
|
throw new Error(`Betrag ${wert} liegt ausserhalb des sicher darstellbaren Bereichs.`)
|
|
}
|
|
return Number(wert)
|
|
}
|