import { createClient } from '@supabase/supabase-js'; import { SUPABASE_URL, SUPABASE_ANON_KEY } from './config.js'; import { isPostgresStorageMode } from './dataPaths.js'; import { decryptJsonPayload, encryptJsonPayload, makeLookupToken, makeOwnerLookup, pgQuery, } from './postgres.js'; const supabaseAnon = createClient(SUPABASE_URL, SUPABASE_ANON_KEY); function userClient(accessToken) { return createClient(SUPABASE_URL, SUPABASE_ANON_KEY, { global: { headers: { Authorization: `Bearer ${accessToken}` } }, auth: { persistSession: false }, }); } export async function verifySupabaseToken(accessToken) { if (!accessToken) return null; try { const { data, error } = await supabaseAnon.auth.getUser(accessToken); if (error || !data?.user) return null; return data.user; } catch { return null; } } export async function getUserSettings(userId, accessToken) { if (isPostgresStorageMode()) { const { rows } = await pgQuery( 'SELECT payload FROM user_settings WHERE owner_lookup = $1', [makeOwnerLookup({ type: 'user', id: userId })] ); const payload = rows[0] ? decryptJsonPayload(rows[0].payload, `user-settings:${userId}`) : null; return { ...defaultSettings(), ...(payload?.settings || {}) }; } try { const uc = userClient(accessToken); const { data } = await uc.from('user_settings').select('settings') .eq('user_id', userId).single(); return { ...defaultSettings(), ...(data?.settings || {}) }; } catch { return defaultSettings(); } } export async function saveUserSettings(userId, accessToken, settings) { if (isPostgresStorageMode()) { try { const payload = { userId, settings, updatedAt: new Date().toISOString(), }; await pgQuery( `INSERT INTO user_settings (owner_lookup, updated_at, payload) VALUES ($1, $2, $3::jsonb) ON CONFLICT (owner_lookup) DO UPDATE SET updated_at = EXCLUDED.updated_at, payload = EXCLUDED.payload`, [ makeOwnerLookup({ type: 'user', id: userId }), payload.updatedAt, JSON.stringify(encryptJsonPayload(payload, `user-settings:${userId}`)), ] ); return; } catch (e) { console.error('saveUserSettings', e.message); return; } } try { const uc = userClient(accessToken); await uc.from('user_settings').upsert({ user_id: userId, settings, updated_at: new Date().toISOString(), }); } catch (e) { console.error('saveUserSettings', e.message); } } export async function getUserProfile(userId, accessToken) { if (isPostgresStorageMode()) { try { const { rows } = await pgQuery( 'SELECT payload FROM user_profiles WHERE owner_lookup = $1', [makeOwnerLookup({ type: 'user', id: userId })] ); const payload = rows[0] ? decryptJsonPayload(rows[0].payload, `user-profile:${userId}`) : null; return payload?.username ? { username: payload.username } : null; } catch { return null; } } try { const uc = userClient(accessToken); const { data } = await uc.from('profiles').select('username') .eq('id', userId).maybeSingle(); return data || null; } catch { return null; } } export async function setUsername(userId, accessToken, username) { if (!username?.trim()) return { error: 'Invalid username' }; const trimmed = username.trim().toLowerCase().replace(/[^a-z0-9_]/g, ''); if (trimmed.length < 3) return { error: 'Username must be at least 3 characters' }; if (isPostgresStorageMode()) { try { const usernameLookup = makeLookupToken('username', trimmed); const ownerLookup = makeOwnerLookup({ type: 'user', id: userId }); const { rows: existingRows } = await pgQuery( 'SELECT owner_lookup FROM user_profiles WHERE username_lookup = $1', [usernameLookup] ); if (existingRows[0] && existingRows[0].owner_lookup !== ownerLookup) { return { error: 'Username already taken' }; } const payload = { userId, username: trimmed, updatedAt: new Date().toISOString(), }; await pgQuery( `INSERT INTO user_profiles (owner_lookup, username_lookup, updated_at, payload) VALUES ($1, $2, $3, $4::jsonb) ON CONFLICT (owner_lookup) DO UPDATE SET username_lookup = EXCLUDED.username_lookup, updated_at = EXCLUDED.updated_at, payload = EXCLUDED.payload`, [ ownerLookup, usernameLookup, payload.updatedAt, JSON.stringify(encryptJsonPayload(payload, `user-profile:${userId}`)), ] ); return { success: true, username: trimmed }; } catch (e) { return { error: e.message }; } } try { const uc = userClient(accessToken); const { data: existing } = await uc.from('profiles') .select('id').eq('username', trimmed).maybeSingle(); if (existing) return { error: 'Username already taken' }; await uc.from('profiles').upsert({ id: userId, username: trimmed }, { onConflict: 'id' }); return { success: true, username: trimmed }; } catch (e) { return { error: e.message }; } } export async function getSubscriptionInfo(accessToken) { try { const r = await fetch('https://sharktide-lightning.hf.space/subscription', { headers: { Authorization: `Bearer ${accessToken}`, Accept: 'application/json' }, }); if (!r.ok) { console.warn(`[Subscription] Failed: HTTP ${r.status}`); return null; } const data = await r.json(); // Normalize snake_case keys from HF Space to camelCase const normalized = { planKey: data.plan_key || null, planName: data.plan_name || null, email: data.email, signedUp: data.signed_up, subscription: data.subscription, }; return normalized; } catch (err) { console.error('[Subscription] Error fetching subscription info:', err.message); return null; } } export async function getTierConfig() { try { const r = await fetch('https://sharktide-lightning.hf.space/tier-config', { headers: { Accept: 'application/json' } }); return r.ok ? r.json() : null; } catch { return null; } } export async function getUsageInfo(accessToken, clientId = '') { try { const h = { Accept: 'application/json' }; if (accessToken) h.Authorization = `Bearer ${accessToken}`; if (clientId) h['X-Client-ID'] = clientId; const r = await fetch('https://sharktide-lightning.hf.space/usage', { headers: h }); const payload = r.ok ? await r.json() : null; return payload; } catch (err) { console.error('[Usage API] request failed:', err.message); return null; } } function defaultSettings() { return { theme: 'dark', webSearch: true, imageGen: true, videoGen: true, audioGen: true }; }