b61f929cf7
packages/shared/src/index.ts and all sub-modules used .js extensions on relative imports (e.g. './admin/index.js') pointing at .ts source files. TypeScript with moduleResolution: "Bundler" doesn't need them, and Metro's eager exporter (used for preview / production builds) reads them literally and fails — only the dev-server Metro fell back to .ts. Workspace typecheck remains 8/8 green; Vite and TS Bundler resolution already accept both styles, so desktop is unaffected.
116 lines
3.9 KiB
TypeScript
116 lines
3.9 KiB
TypeScript
import type { SupportedLocale } from '../i18n/types';
|
|
import type { AppSupabaseClient } from '../supabase/client';
|
|
|
|
export interface SignupParams {
|
|
email: string;
|
|
// Login handle. Stored with original casing but uniqueness is case-insensitive
|
|
// (citext). Must match ^[A-Za-z0-9_]{3,32}$.
|
|
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,
|
|
// Preserve case. `profiles.username` is citext so uniqueness + lookups
|
|
// stay case-insensitive regardless of stored casing.
|
|
username: params.username.trim(),
|
|
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> {
|
|
// `scope: 'local'` only ends the session in THIS client. Without it Supabase
|
|
// defaults to 'global', which invalidates the user's refresh tokens
|
|
// everywhere — meaning a logout in the browser would also kick the desktop
|
|
// app (and vice versa) the next time it tries to refresh its token.
|
|
const { error } = await client.auth.signOut({ scope: 'local' });
|
|
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;
|
|
}
|