This commit is contained in:
2026-04-18 23:11:35 +02:00
commit f7cfd2a86e
196 changed files with 35538 additions and 0 deletions
+108
View File
@@ -0,0 +1,108 @@
import type { SupportedLocale } from '../i18n/types.js';
import type { AppSupabaseClient } from '../supabase/client.js';
export interface SignupParams {
email: string;
// Login handle. Lowercased server-side; must match ^[a-z0-9_]{3,32}$ once lowercased.
username: string;
// Optional human-readable name. Defaults to username server-side if omitted.
displayName?: string;
// Invite code is mandatory (enforced by the handle_new_user trigger).
inviteCode: string;
// Deep link the magic-link email should bounce back to.
// e.g. `chatapp://auth/callback` on mobile, `http://localhost:1420/auth/callback` on desktop dev.
redirectTo: string;
// Optional initial locale for the profile row. Defaults to 'en' server-side.
locale?: SupportedLocale;
}
// Trigger signup. User receives a magic link email. Completion happens in
// `completeSessionFromUrl` below once the callback fires.
export async function signUpWithMagicLink(
client: AppSupabaseClient,
params: SignupParams,
): Promise<void> {
const { error } = await client.auth.signInWithOtp({
email: params.email,
options: {
emailRedirectTo: params.redirectTo,
shouldCreateUser: true,
data: {
invite_code: params.inviteCode,
username: params.username.toLowerCase(),
display_name: params.displayName ?? params.username,
...(params.locale ? { locale: params.locale } : {}),
},
},
});
if (error) throw error;
}
// Trigger magic-link login for an existing user. No invite code needed.
export async function loginWithMagicLink(
client: AppSupabaseClient,
email: string,
redirectTo: string,
): Promise<void> {
const { error } = await client.auth.signInWithOtp({
email,
options: {
emailRedirectTo: redirectTo,
shouldCreateUser: false,
},
});
if (error) throw error;
}
// Parse a callback URL produced by the magic-link email and establish a session.
// Works for both PKCE (`?code=…`) and legacy token-hash (`#access_token=…`).
export async function completeSessionFromUrl(
client: AppSupabaseClient,
url: string,
): Promise<void> {
const u = new URL(url);
// PKCE flow: exchange the code.
const code = u.searchParams.get('code');
if (code) {
const { error } = await client.auth.exchangeCodeForSession(code);
if (error) throw error;
return;
}
// Hash fragment flow.
if (u.hash && u.hash.length > 1) {
const hash = new URLSearchParams(u.hash.slice(1));
const access_token = hash.get('access_token');
const refresh_token = hash.get('refresh_token');
if (access_token && refresh_token) {
const { error } = await client.auth.setSession({ access_token, refresh_token });
if (error) throw error;
return;
}
}
throw new Error('callback URL contained no auth params');
}
export async function signOut(client: AppSupabaseClient): Promise<void> {
const { error } = await client.auth.signOut();
if (error) throw error;
}
// Verify a 6-digit OTP that arrived via magic-link email. Establishes a
// session in the CURRENT webview — no browser redirect involved. Used by the
// desktop/mobile apps where cross-origin redirects don't carry the session
// back to the native webview.
export async function verifyMagicLinkOtp(
client: AppSupabaseClient,
email: string,
token: string,
): Promise<void> {
const { error } = await client.auth.verifyOtp({
email,
token: token.trim(),
type: 'email',
});
if (error) throw error;
}