134 lines
4.7 KiB
TypeScript
134 lines
4.7 KiB
TypeScript
import { randomBytes, randomUUID } from 'node:crypto';
|
|
import { authConfig, env, TERMS_VERSION } from './auth-config';
|
|
|
|
/** Zufaellige E-Mail-Adresse je Test (Spec 7: jeder Test nutzt eigene Adressen). */
|
|
export function randomEmail(prefix = 'e2e'): string {
|
|
return `${prefix}-${Date.now().toString(36)}-${randomBytes(4).toString('hex')}@example.test`;
|
|
}
|
|
|
|
/** Zufaelliges, ausreichend langes Passwort (AUTH-CFG-05: mind. 10 Zeichen). */
|
|
export function randomPassword(): string {
|
|
return `Pw-${randomBytes(8).toString('hex')}-x`;
|
|
}
|
|
|
|
/** Zufaellige IPv4 fuer `X-Forwarded-For` (nur Projekt `throttle`, nie gegen die CLI, R13). */
|
|
export function randomIp(): string {
|
|
const octet = () => 1 + Math.floor(Math.random() * 253);
|
|
return `${octet()}.${octet()}.${octet()}.${octet()}`;
|
|
}
|
|
|
|
export const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));
|
|
|
|
/** Wartet etwas mehr als AUTH-CFG-08 (1 s), damit Folgemails nicht an 429 scheitern. */
|
|
export const waitMailInterval = () => sleep(authConfig.maxFrequencySeconds * 1000 + 300);
|
|
|
|
export interface GoTrueResponse<T = any> {
|
|
status: number;
|
|
body: T;
|
|
headers: Headers;
|
|
/** Dauer in Millisekunden. */
|
|
ms: number;
|
|
}
|
|
|
|
export interface ClientOptions {
|
|
/** Basis-URL inkl. `/auth/v1` (Standard: Kong). */
|
|
baseUrl?: string;
|
|
/** Beim Zugriff ueber Kong noetig, direkt auf GoTrue nicht. */
|
|
apikey?: string | null;
|
|
/** Zusatz-Header, z. B. `X-Forwarded-For` (nur `throttle`). */
|
|
headers?: Record<string, string>;
|
|
}
|
|
|
|
/** Minimaler GoTrue-Client (REST, wie `supabase_flutter`). */
|
|
export class GoTrueClient {
|
|
readonly baseUrl: string;
|
|
private readonly apikey: string | null;
|
|
private readonly extraHeaders: Record<string, string>;
|
|
|
|
constructor(opts: ClientOptions = {}) {
|
|
this.baseUrl = opts.baseUrl ?? `${env.supabaseUrl}/auth/v1`;
|
|
this.apikey = opts.apikey === undefined ? env.anonKey : opts.apikey;
|
|
this.extraHeaders = opts.headers ?? {};
|
|
}
|
|
|
|
async request<T = any>(
|
|
method: string,
|
|
path: string,
|
|
body?: unknown,
|
|
token?: string,
|
|
extra: Record<string, string> = {},
|
|
): Promise<GoTrueResponse<T>> {
|
|
const headers: Record<string, string> = { 'content-type': 'application/json', ...this.extraHeaders, ...extra };
|
|
if (this.apikey) headers.apikey = this.apikey;
|
|
if (token) headers.authorization = `Bearer ${token}`;
|
|
const started = Date.now();
|
|
const res = await fetch(`${this.baseUrl}${path}`, {
|
|
method,
|
|
headers,
|
|
body: body === undefined ? undefined : JSON.stringify(body),
|
|
});
|
|
const text = await res.text();
|
|
let parsed: any = text;
|
|
try {
|
|
parsed = text ? JSON.parse(text) : null;
|
|
} catch {
|
|
/* Body ist kein JSON */
|
|
}
|
|
return { status: res.status, body: parsed, headers: res.headers, ms: Date.now() - started };
|
|
}
|
|
|
|
signUp(email: string, password: string, data: Record<string, unknown> | null = validMetadata()) {
|
|
return this.request('POST', '/signup', { email, password, ...(data ? { data } : {}) });
|
|
}
|
|
signIn(email: string, password: string) {
|
|
return this.request('POST', '/token?grant_type=password', { email, password });
|
|
}
|
|
refresh(refreshToken: string) {
|
|
return this.request('POST', '/token?grant_type=refresh_token', { refresh_token: refreshToken });
|
|
}
|
|
recover(email: string) {
|
|
return this.request('POST', '/recover', { email });
|
|
}
|
|
resend(email: string, type: 'signup' = 'signup') {
|
|
return this.request('POST', '/resend', { email, type });
|
|
}
|
|
/** `verifyOTP(tokenHash, type)`. */
|
|
verify(tokenHash: string, type: 'email' | 'signup' | 'recovery') {
|
|
return this.request('POST', '/verify', { token_hash: tokenHash, type });
|
|
}
|
|
getUser(accessToken: string) {
|
|
return this.request('GET', '/user', undefined, accessToken);
|
|
}
|
|
updateUser(accessToken: string, attributes: Record<string, unknown>) {
|
|
return this.request('PUT', '/user', attributes, accessToken);
|
|
}
|
|
/** Legt ein bestaetigtes Konto direkt an (Service-Role, nur lokal), fuer Tests, die die Anmeldung pruefen. */
|
|
async createConfirmedUser(
|
|
email: string,
|
|
password: string,
|
|
data: Record<string, unknown> = validMetadata(),
|
|
) {
|
|
return this.request(
|
|
'POST',
|
|
'/admin/users',
|
|
{ email, password, email_confirm: true, user_metadata: data },
|
|
env.serviceRoleKey,
|
|
);
|
|
}
|
|
}
|
|
|
|
/** Gueltige Registrierungs-Metadaten (Spec 4.2). */
|
|
export function validMetadata(overrides: Record<string, unknown> = {}): Record<string, unknown> {
|
|
const data: Record<string, unknown> = {
|
|
display_name: 'E2E Tester',
|
|
age_confirmed: true,
|
|
terms_accepted: true,
|
|
terms_version: TERMS_VERSION,
|
|
locale: 'en',
|
|
...overrides,
|
|
};
|
|
for (const key of Object.keys(data)) if (data[key] === undefined) delete data[key];
|
|
return data;
|
|
}
|
|
|
|
export const uniqueId = () => randomUUID();
|