add platform pages

This commit is contained in:
JCEEE
2026-08-23 15:12:19 +01:00
parent 762fbab4b6
commit bb2b1759dc
11 changed files with 1044 additions and 245 deletions
+190 -41
View File
@@ -1,39 +1,117 @@
import { pbAdmin } from '$lib/server/pocketbase';
import { pbAdmin, createPbClient } from '$lib/server/pocketbase';
import { redirect, fail } from '@sveltejs/kit';
import { PB_EMAIL, PB_PASSWORD } from '$app/env/private';
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';
export const load: PageServerLoad = async ({ cookies }) => {
const session = cookies.get('platform_session');
if (!session) {
return { authenticated: false, fams: [], totalFams: 0, totalMembers: 0, totalRewards: 0, platformFlags: {} };
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: [],
platformFlags: {},
totalFams: 0,
totalMembers: 0,
totalRewards: 0,
totalChores: 0,
subCount: 0,
gatedCount: 0,
codeCount: 0,
loadError: undefined
};
}
try {
const [fams, platformFlags] = await Promise.all([pbAdmin.getList('fams'), getPlatformFlags()]);
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 || '',
};
}));
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
};
})
);
const totalFams = fams.length;
const totalMembers = famsWithStats.reduce((s: number, f: any) => s + f.memberCount, 0);
const totalRewards = famsWithStats.reduce((s: number, f: any) => s + f.totalRewards, 0);
// Usage map — which fams applied each code.
const usage: Record<string, string[]> = {};
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] || []
}));
return { authenticated: true, fams: famsWithStats, totalFams, totalMembers, totalRewards, platformFlags };
} catch {
return { authenticated: false, fams: [], totalFams: 0, totalMembers: 0, totalRewards: 0, platformFlags: {} };
return {
authenticated: true,
fams: famsWithStats,
codes: codeList,
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,
loadError: e instanceof Error ? e.message : 'Failed to load platform data'
};
}
};
@@ -43,29 +121,32 @@ export const actions: Actions = {
const email = fd.get('email') as string;
const password = fd.get('password') as string;
if (email === PB_EMAIL && password === PB_PASSWORD) {
cookies.set('platform_session', 'authenticated', {
path: '/',
httpOnly: true,
sameSite: 'lax',
maxAge: 60 * 60 * 24, // 24 hours
});
return { success: true };
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 fail(400, { error: 'Invalid credentials' });
return { success: true };
},
logout: async ({ cookies }) => {
cookies.delete('platform_session', { path: '/' });
clearPlatformSession(cookies);
throw redirect(303, '/admin');
},
// Toggles a platform-level feature flag on the singleton platform record.
togglePlatformFlag: async ({ request, cookies }) => {
const session = cookies.get('platform_session');
if (!session) return fail(401, { error: 'Not authenticated' });
togglePlatformFlag: async (event) => {
requirePlatform(event);
const fd = await request.formData();
const fd = await event.request.formData();
const flag = fd.get('flag') as string;
if (!flag) return fail(400, { error: 'Flag required' });
@@ -77,4 +158,72 @@ export const actions: Actions = {
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' });
}
}
};