103 lines
3.7 KiB
TypeScript
103 lines
3.7 KiB
TypeScript
/**
|
|
* 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<T>(fn: (c: Client) => Promise<T>): Promise<T> {
|
|
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<number> {
|
|
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),
|
|
);
|
|
}
|