import { pbAdmin, createPbClient } from '$lib/server/pocketbase'; import { redirect, fail } from '@sveltejs/kit'; import type { RequestEvent } from '@sveltejs/kit'; import { setPlatformSession, clearPlatformSession } from '$lib/server/session'; import { getPlatformFlags, setPlatformFlag } from '$lib/server/platform'; import type { Actions, PageServerLoad } from './$types'; function requirePlatform(event: RequestEvent) { if (!event.locals.platformAdmin) throw redirect(303, '/admin'); } // Random human-friendly code value: XXXX-XXXX (unambiguous charset). function genCodeValue(): string { const chars = 'ABCDEFGHJKMNPQRSTUVWXYZ23456789'; const pick = () => chars[Math.floor(Math.random() * chars.length)]; return `${Array.from({ length: 4 }, pick).join('')}-${Array.from({ length: 4 }, pick).join('')}`; } export const load: PageServerLoad = async (event) => { const { cookies } = event; if (!event.locals.platformAdmin) { return { authenticated: false, fams: [], codes: [], choreTemplates: [], bonusTemplates: [], platformFlags: {}, totalFams: 0, totalMembers: 0, totalRewards: 0, totalChores: 0, subCount: 0, gatedCount: 0, codeCount: 0, loadError: undefined }; } try { const [fams, codes, completions] = await Promise.all([ pbAdmin.getList('fams'), pbAdmin.getList('accesscodes'), pbAdmin.getList('completions') ]); const famsWithStats = await Promise.all( fams.map(async (fam: any) => { const [members, rewards, parents] = await Promise.all([ pbAdmin.getList('users', `famId = '${fam.id}' && role = 'child'`), pbAdmin.getList('rewards', `famId = '${fam.id}'`), pbAdmin.getList('users', `famId = '${fam.id}' && role = 'parent'`) ]); return { id: fam.id, name: fam.name, slug: fam.slug, memberCount: members.length, requestedRewards: (rewards as any[]).filter((r: any) => r.status === 'requested').length, totalRewards: rewards.length, parentEmail: (parents as any[])?.[0]?.email || '', paymentMode: fam.paymentMode || 'none', active: fam.active !== false }; }) ); // Usage map — which fams applied each code. const usage: Record = {}; for (const fam of fams as any[]) { if (fam.paymentMode === 'code' && fam.accessCodeId) { (usage[fam.accessCodeId] ||= []).push(fam.name); } } const codeList = (codes as any[]) .sort((a, b) => (b.createdAt || '').localeCompare(a.createdAt || '')) .map((c) => ({ id: c.id, value: c.value, name: c.name, duration: Number(c.duration) || 0, expiry: Number(c.expiry) || 0, trialDays: Number(c.trialDays) || 0, active: c.active !== false, usedBy: usage[c.id] || [] })); const [choreTemplates, bonusTemplates] = await Promise.all([ pbAdmin.getList('chore_templates', 'global = true'), pbAdmin.getList('bonus_templates', 'global = true') ]); return { authenticated: true, fams: famsWithStats, codes: codeList, choreTemplates: choreTemplates as any[], bonusTemplates: bonusTemplates as any[], totalFams: fams.length, totalMembers: famsWithStats.reduce((s, f) => s + f.memberCount, 0), totalRewards: famsWithStats.reduce((s, f) => s + f.totalRewards, 0), totalChores: completions.length, subCount: famsWithStats.filter((f) => f.paymentMode === 'sub').length, gatedCount: famsWithStats.filter( (f) => !f.active || f.paymentMode === 'none' || f.paymentMode === 'canceled' ).length, codeCount: codeList.filter((c) => c.active).length }; } catch (e) { // Never swallow silently — a failed load must not masquerade as logged-out. console.error('[admin] load failed:', e); return { authenticated: false, fams: [], codes: [], totalFams: 0, totalMembers: 0, totalRewards: 0, totalChores: 0, subCount: 0, gatedCount: 0, codeCount: 0, choreTemplates: [], bonusTemplates: [], loadError: e instanceof Error ? e.message : 'Failed to load platform data' }; } }; export const actions: Actions = { login: async ({ request, cookies }) => { const fd = await request.formData(); const email = fd.get('email') as string; const password = fd.get('password') as string; if (!email || !password) return fail(400, { error: 'Email and password required' }); // Real PB superuser auth — the minted JWT goes in the cookie and is // verified per-request in hooks (authRefresh). Forging the cookie // value gains nothing. try { const auth = await createPbClient() .collection('_superusers') .authWithPassword(email, password); setPlatformSession(cookies, auth.token); } catch { return fail(400, { error: 'Invalid credentials' }); } return { success: true }; }, logout: async ({ cookies }) => { clearPlatformSession(cookies); throw redirect(303, '/admin'); }, // Toggles a platform-level feature flag on the singleton platform record. togglePlatformFlag: async (event) => { requirePlatform(event); const fd = await event.request.formData(); const flag = fd.get('flag') as string; if (!flag) return fail(400, { error: 'Flag required' }); try { const flags = await getPlatformFlags(); await setPlatformFlag(flag, !flags[flag]); return { success: true }; } catch (e) { return fail(500, { error: e instanceof Error ? e.message : 'Failed to update' }); } }, // Issue a new access/trial code. Blank value → auto-generated. trialDays > 0 // makes it a trial code (maps to Stripe trial_period_days at checkout); // otherwise it's a platform-access code (duration months, 0 = continuous). createCode: async (event) => { requirePlatform(event); const fd = await event.request.formData(); const name = ((fd.get('name') as string) || '').trim(); const value = ((fd.get('value') as string) || '').trim().toUpperCase() || genCodeValue(); const duration = Math.max(0, parseInt(fd.get('duration') as string, 10) || 0); const expiry = Math.max(0, parseInt(fd.get('expiry') as string, 10) || 0); const trialDays = Math.max(0, parseInt(fd.get('trialDays') as string, 10) || 0); if (!name) return fail(400, { error: 'Name required' }); try { const existing = await pbAdmin.getList('accesscodes', `value = '${value}'`); if (existing.length) return fail(400, { error: `Code "${value}" already exists` }); await pbAdmin.create('accesscodes', { value, name, duration, expiry, trialDays, active: true, createdAt: new Date().toISOString() }); return { success: true, createdValue: value }; } catch (e) { return fail(500, { error: e instanceof Error ? e.message : 'Failed to create code' }); } }, toggleCode: async (event) => { requirePlatform(event); const fd = await event.request.formData(); const id = fd.get('id') as string; try { const rec = (await pbAdmin.getOne('accesscodes', id)) as any; await pbAdmin.update('accesscodes', id, { active: rec.active === false }); return { success: true }; } catch (e) { return fail(500, { error: e instanceof Error ? e.message : 'Failed to update code' }); } }, deleteCode: async (event) => { requirePlatform(event); const fd = await event.request.formData(); const id = fd.get('id') as string; // Guard: a code still applied to a fam must be disabled, not deleted. const inUse = await pbAdmin.getList('fams', `accessCodeId = '${id}'`); if (inUse.length) { return fail(400, { error: `In use by ${inUse.length} fam${inUse.length > 1 ? 's' : ''} — disable it instead.` }); } try { await pbAdmin.remove('accesscodes', id); return { success: true }; } catch (e) { return fail(500, { error: e instanceof Error ? e.message : 'Failed to delete code' }); } }, // Platform-owned chore/reward templates. `type` selects the target // collection (chore_templates | bonus_templates); the droplet is always // global (visible to every family as an assignable source). createTemplate: async (event) => { requirePlatform(event); const fd = await event.request.formData(); const type = fd.get('type') as string; const name = (fd.get('name') as string || '').trim(); const description = (fd.get('description') as string) || ''; const icon = (fd.get('icon') as string) || ''; const color = (fd.get('color') as string) || '#6366f1'; if (!name) return fail(400, { error: 'Name required' }); try { if (type === 'chore') { await pbAdmin.create('chore_templates', { name, description, icon, color, global: true, defaultFrequency: (fd.get('defaultFrequency') as string) || 'daily', defaultType: (fd.get('defaultType') as string) || 'points', defaultValue: Number(fd.get('defaultValue') || 0) }); } else { await pbAdmin.create('bonus_templates', { name, description, icon, color, global: true, target: (fd.get('target') as string) || 'individual', type: (fd.get('bonusType') as string) || 'threshold', thresholdType: (fd.get('thresholdType') as string) || 'points', occurrence: (fd.get('occurrence') as string) || 'recurring', rewardType: (fd.get('rewardType') as string) || 'points', rewardValue: (fd.get('rewardValue') as string) || '', criteriaValue: Number(fd.get('criteriaValue') || 0), period: (fd.get('period') as string) || 'weekly', isPocketMoney: fd.get('isPocketMoney') === 'true' }); } return { success: true }; } catch (e) { return fail(500, { error: e instanceof Error ? e.message : 'Failed to create template' }); } }, updateTemplate: async (event) => { requirePlatform(event); const fd = await event.request.formData(); const kind = fd.get('kind') as string; // chore | reward const id = fd.get('id') as string; if (!id) return fail(400, { error: 'Missing id' }); const name = (fd.get('name') as string || '').trim(); const description = (fd.get('description') as string) || ''; const icon = (fd.get('icon') as string) || ''; const color = (fd.get('color') as string) || '#6366f1'; try { if (kind === 'chore') { await pbAdmin.update('chore_templates', id, { name, description, icon, color, defaultFrequency: (fd.get('defaultFrequency') as string) || 'daily', defaultType: (fd.get('defaultType') as string) || 'points', defaultValue: Number(fd.get('defaultValue') || 0) }); } else { await pbAdmin.update('bonus_templates', id, { name, description, icon, color, target: (fd.get('target') as string) || 'individual', type: (fd.get('bonusType') as string) || 'threshold', thresholdType: (fd.get('thresholdType') as string) || 'points', occurrence: (fd.get('occurrence') as string) || 'recurring', rewardType: (fd.get('rewardType') as string) || 'points', rewardValue: (fd.get('rewardValue') as string) || '', criteriaValue: Number(fd.get('criteriaValue') || 0), period: (fd.get('period') as string) || 'weekly', isPocketMoney: fd.get('isPocketMoney') === 'true' }); } return { success: true }; } catch (e) { return fail(500, { error: e instanceof Error ? e.message : 'Failed to update template' }); } }, deleteTemplate: async (event) => { requirePlatform(event); const fd = await event.request.formData(); const kind = fd.get('kind') as string; const id = fd.get('id') as string; if (!id) return fail(400, { error: 'Missing id' }); try { await pbAdmin.remove(kind === 'chore' ? 'chore_templates' : 'bonus_templates', id); return { success: true }; } catch (e) { return fail(500, { error: e instanceof Error ? e.message : 'Failed to delete template' }); } } };