import { fail, redirect } from '@sveltejs/kit'; import type { RequestEvent } from '@sveltejs/kit'; import { pbAdmin } from '$lib/server/pocketbase'; import { createPbClient } from '$lib/server/pocketbase'; import { setSessionCookie } from '$lib/server/session'; import { issueAccess } from '$lib/server/member-otp'; import { slugify, handle, famUsername, handleOf } from '@shared/slugify'; import { applyAccessCode } from '$lib/server/access'; import { createEmbeddedCheckoutSession, PLAN_IDS } from '$lib/server/stripe'; import type { PlanId } from '$lib/server/stripe'; class SignupError extends Error {} export const actions = { // Step 1 — create the family + parent (admin) user, mint their session. // The parent's human-entered name is kept as the display `name`; their PB // `username` is `{famSlug}:{handle}` (globally unique, handle = no whitespace). signup: async (event) => { const fd = await event.request.formData(); const famName = fd.get('familyName') as string; const yourName = (fd.get('yourName') as string) || ''; const email = fd.get('email') as string; const password = fd.get('password') as string; if (!famName || !yourName || !email || !password) { return fail(400, { message: 'All fields required', famName, email }); } if (password.length < 8) { return fail(400, { message: 'Password must be at least 8 characters', famName, email }); } const parentName = yourName.trim(); const slug = slugify(famName); const handleName = handle(parentName) || 'admin'; const username = famUsername(slug, handleName); try { const famData: Record = { name: famName, slug, timezone: 'auto', paymentMode: 'none', active: false }; const fam = await pbAdmin.create('fams', famData); const user = await pbAdmin.create('users', { username, name: parentName, email, password, passwordConfirm: password, emailVisibility: false, famId: fam.id, role: 'parent' }); await pbAdmin.create('settings', { famId: fam.id }); } catch (e) { throw new SignupError( `Could not create account — ${e instanceof Error ? e.message : 'please try again'}` ); } // Auth as the new parent to mint their JWT, then move to the child step. const authResult = await createPbClient() .collection('users') .authWithPassword(email, password) .catch(() => null); if (authResult?.token) setSessionCookie(event.cookies, authResult.token); return { success: true, famSlug: slug, username: handleName }; }, // Step 2 — optionally add a child now; issues their OTP join code. child: async (event: RequestEvent) => { const user = requireUser(event); const fd = await event.request.formData(); const name = ((fd.get('member') as string) || '').trim(); // No [fam] URL param here (signup isn't fam-scoped) — resolve the slug // from DB. Everywhere else, the slug comes from event.params.fam / // page.data.famSlug ([fam] layout load) — never copy it into state. const fam = await pbAdmin.getOne('fams', user.famId); const famSlug = fam?.slug || user.famId; if (!name) { return { success: true, famSlug, username: handleOf(user.username || '') }; } const { otp, joinUrl } = await issueAccess({ famId: user.famId, famSlug, name }); return { success: true, code: otp, joinUrl, famSlug, username: handleOf(user.username || '') }; }, // Step 3 — apply an access code (or skip via client-side navigation). access: async (event: RequestEvent) => { const user = requireUser(event); const fd = await event.request.formData(); const code = ((fd.get('code') as string) || '').trim(); if (!code) { return { ok: true, skipped: true }; } const result = await applyAccessCode(user.famId, code); if (result.error) return fail(400, { error: result.error }); return { ok: true, ...result }; }, // Step 4 — choose a plan, start embedded checkout. choose: async (event: RequestEvent) => { const user = requireUser(event); const fd = await event.request.formData(); const plan = fd.get('plan') as PlanId; if (!['monthly', 'yearly'].includes(plan)) { return fail(400, { error: 'Unknown plan' }); } const fam = await pbAdmin.getOne('fams', user.famId); const parents = await pbAdmin.getList('users', `famId = '${user.famId}' && role = 'parent'`); const email = (parents[0] as { email?: string } | undefined)?.email || null; try { const { clientSecret, sessionId } = await createEmbeddedCheckoutSession({ plan, famId: user.famId, famSlug: fam.slug, email, customerId: fam.stripeCustomerId || null, origin: event.url.origin }); return { success: true, clientSecret, sessionId, plan }; } catch (e) { if (e instanceof redirect) throw e; return fail(500, { error: e instanceof Error ? e.message : 'Failed to start checkout' }); } } }; function requireUser(event: RequestEvent) { if (!event.locals.user) throw redirect(303, '/signup'); return event.locals.user; }