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
text/typescript
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;
}
},
};
}