UNPKG

codalware-auth

Version:

Complete authentication system with enterprise security, attack protection, team workspaces, waitlist, billing, UI components, 2FA, and account recovery - production-ready in 5 minutes. Enhanced CLI with verification, rollback, and App Router scaffolding.

88 lines (79 loc) 5.2 kB
import { Adapter, User, Session, MagicToken } from './types'; import { createClient } from '@supabase/supabase-js'; export function createSupabaseAdapter(opts: { client?: unknown; url?: string; key?: string; usersTable?: string; tokensTable?: string; sessionsTable?: string } = {}): Adapter { const supabase = (opts.client as any) ?? createClient(opts.url ?? process.env.SUPABASE_URL ?? '', opts.key ?? process.env.SUPABASE_KEY ?? ''); const usersTable = opts.usersTable ?? process.env.SUPABASE_USERS_TABLE ?? 'auth_users'; const tokensTable = opts.tokensTable ?? process.env.SUPABASE_TOKENS_TABLE ?? 'magic_tokens'; const sessionsTable = opts.sessionsTable ?? process.env.SUPABASE_SESSIONS_TABLE ?? 'sessions'; function mapUser(row: any): User { return { id: String(row.id), email: row.email, name: row.name ?? null, metadata: row.metadata ?? null, createdAt: new Date(row.created_at), updatedAt: new Date(row.updated_at) }; } return { async createUser({ email, name, metadata }) { const { data, error } = await supabase.from(usersTable).insert([{ email, name, metadata }]).select().single(); if (error) throw error; return mapUser(data); }, async getUserById(id) { const { data, error } = await supabase.from(usersTable).select('*').eq('id', id).maybeSingle(); if (error) throw error; return data ? mapUser(data) : null; }, async getUserByEmail(email) { const { data, error } = await supabase.from(usersTable).select('*').eq('email', email).maybeSingle(); if (error) throw error; return data ? mapUser(data) : null; }, async updateUser(id, patch) { const payload: any = {}; if ('email' in patch) payload.email = patch.email; if ('name' in patch) payload.name = patch.name; if ('metadata' in patch) payload.metadata = patch.metadata; const { data, error } = await supabase.from(usersTable).update(payload).eq('id', id).select().single(); if (error) throw error; return mapUser(data); }, async createSession(session) { const { data, error } = await supabase.from(sessionsTable).insert([{ user_id: session.userId, expires_at: session.expiresAt.toISOString(), metadata: session.metadata }]).select().single(); if (error) throw error; return { id: String(data.id), userId: String(data.user_id), createdAt: new Date(data.created_at), expiresAt: new Date(data.expires_at), handle: data.handle ?? null, metadata: data.metadata ?? null } as Session; }, async getSessionById(id) { const { data, error } = await supabase.from(sessionsTable).select('*').eq('id', id).maybeSingle(); if (error) throw error; if (!data) return null; return { id: String(data.id), userId: String(data.user_id), createdAt: new Date(data.created_at), expiresAt: new Date(data.expires_at), handle: data.handle ?? null, metadata: data.metadata ?? null } as Session; }, async deleteSession(id) { const { error } = await supabase.from(sessionsTable).delete().eq('id', id); if (error) throw error; }, async deleteSessionsByUserId(userId) { const { error } = await supabase.from(sessionsTable).delete().eq('user_id', userId); if (error) throw error; }, async storeMagicToken({ tokenHash, userId = null, expiresAt, ip = null, userAgent = null }) { const payload: any = { token_hash: tokenHash, user_id: userId, expires_at: expiresAt.toISOString(), ip, user_agent: userAgent }; const { data, error } = await supabase.from(tokensTable).insert([payload]).select().single(); if (error) throw error; return { id: String(data.id), tokenHash: data.token_hash, userId: data.user_id ?? null, createdAt: new Date(data.created_at), expiresAt: new Date(data.expires_at), consumedAt: data.consumed_at ? new Date(data.consumed_at) : null, ip: data.ip ?? null, userAgent: data.user_agent ?? null } as MagicToken; }, async findValidMagicToken(tokenHash) { const { data, error } = await supabase.from(tokensTable).select('*').eq('token_hash', tokenHash).eq('consumed_at', null).maybeSingle(); if (error) throw error; if (!data) return null; const expires = new Date(data.expires_at); if (expires <= new Date()) return null; return { id: String(data.id), tokenHash: data.token_hash, userId: data.user_id ?? null, createdAt: new Date(data.created_at), expiresAt: new Date(data.expires_at), consumedAt: data.consumed_at ? new Date(data.consumed_at) : null, ip: data.ip ?? null, userAgent: data.user_agent ?? null } as MagicToken; }, async consumeMagicToken(id) { // Atomic update: only set consumed_at if it's currently null and return affected rows const { error } = await supabase.from(tokensTable).update({ consumed_at: new Date().toISOString() }).eq('id', id).eq('consumed_at', null).select().single(); if (error) { // If no rows were updated it's either already consumed or not found if (error.code === 'PGRST116') return; // supabase-specific no update - ignore throw error; } }, }; }