/** * Direkter Datenbankzugriff fuer Tests. NUR lokal/CI: der Helfer bricht ab, wenn der Host nicht * lokal ist (er veraendert `auth.users`). Spec 7.1 Krit. 8 (B0 (i)): Maßgeblich fuer den * Linkablauf sind `auth.users.recovery_sent_at` und `confirmation_sent_at`, nicht * `auth.one_time_tokens.created_at`. */ import { Client } from 'pg'; import { parse } from 'pg-connection-string'; import { authConfig, env, isLocalHost } from './auth-config'; /** * Parst die Verbindungszeichenfolge mit demselben Parser wie `pg` und prueft den Host des * GEPARSTEN Ergebnisses (auch `?host=...`, Unix-Sockets und mehrere Hosts werden so erfasst). * Der Client wird aus genau diesem Ergebnis gebaut, nicht erneut aus dem String. */ export function localDbConfig(connectionString: string) { const cfg = parse(connectionString); const host = (cfg.host ?? '').replace(/^\[|\]$/g, ''); // `parse` liefert IPv6 ohne Klammern; unsere Liste fuehrt sie mit Klammern. const candidate = host.includes(':') ? `[${host}]` : host; if (!candidate || !isLocalHost(candidate)) { throw new Error(`e2e/support/db.ts nur lokal/CI: Host "${cfg.host}" ist nicht erlaubt`); } return cfg; } export async function withDb(fn: (c: Client) => Promise): Promise { const cfg = localDbConfig(env.dbUrl); const client = new Client({ host: cfg.host ?? undefined, port: cfg.port ? Number(cfg.port) : undefined, user: cfg.user, password: cfg.password, database: cfg.database ?? undefined, }); await client.connect(); try { return await fn(client); } finally { await client.end(); } } /** Zeilen aus `auth.users` zu E-Mail oder ID (nie global zaehlen: Tests laufen parallel). */ export async function usersWhere(by: { email?: string; id?: string }) { return withDb(async (c) => by.email !== undefined ? (await c.query('select id from auth.users where email = $1', [by.email.toLowerCase()])).rows : (await c.query('select id from auth.users where id = $1', [by.id])).rows, ); } export async function countUsers(): Promise { return withDb(async (c) => Number((await c.query('select count(*) from auth.users')).rows[0].count)); } export async function userByEmail(email: string) { return withDb(async (c) => (await c.query('select * from auth.users where email = $1', [email.toLowerCase()])).rows[0]); } export async function profileByEmail(email: string) { return withDb( async (c) => ( await c.query( `select p.* from public.profiles p join auth.users u on u.id = p.id where u.email = $1`, [email.toLowerCase()], ) ).rows[0], ); } /** Setzt den Link als abgelaufen: `recovery_sent_at = now() - (otp_expiry + 1 s)` (AUTH-CFG-07). */ export async function expireRecoveryLink(email: string, secondsAgo = authConfig.otpExpirySeconds + 1) { await withDb((c) => c.query(`update auth.users set recovery_sent_at = now() - make_interval(secs => $2) where email = $1`, [ email.toLowerCase(), secondsAgo, ]), ); } /** Wie oben fuer den Bestaetigungslink (`confirmation_sent_at`). */ export async function expireConfirmationLink(email: string, secondsAgo = authConfig.otpExpirySeconds + 1) { await withDb((c) => c.query(`update auth.users set confirmation_sent_at = now() - make_interval(secs => $2) where email = $1`, [ email.toLowerCase(), secondsAgo, ]), ); } /** `not_after` aller Sitzungen des Nutzers (AUTH-CFG-16: nicht gesetzt). */ export async function sessionNotAfter(email: string): Promise<(string | null)[]> { return withDb(async (c) => ( await c.query( `select s.not_after from auth.sessions s join auth.users u on u.id = s.user_id where u.email = $1`, [email.toLowerCase()], ) ).rows.map((r) => r.not_after), ); }