import { fail, redirect, type Actions } from '@sveltejs/kit'; import { queries } from '$lib/server/db'; import { attachEmailToAccount, clearSession, createSession } from '$lib/server/auth'; import { PLANS, PLAN_ORDER, type PlanId } from '$lib/plans'; import type { PageServerLoad } from './$types'; export const load: PageServerLoad = async ({ locals }) => { const account = locals.account!; const keys = queries.keysForAccount.all(account.id); return { account, keyCount: keys.length, plans: PLAN_ORDER.map((id) => PLANS[id]), currentPlan: account.plan }; }; export const actions: Actions = { /** * Anonymous → pending. The user has typed an email; we save it as * pending_email and (eventually) send a magic link. Email sending is * not wired up yet — the UI just transitions to the pending state. */ requestSignInLink: async ({ request, locals }) => { const account = locals.account!; const form = await request.formData(); const email = ((form.get('email') ?? '') as string).trim().toLowerCase(); if (!email || !email.includes('@') || email.length > 254) { return fail(400, { error: 'Enter a valid email address.' }); } queries.setPendingEmail.run(email, account.id); return { linkSent: true, email }; }, /** Pending → anonymous. User abandons the verification. */ cancelPendingSignIn: async ({ locals }) => { const account = locals.account!; queries.clearPendingEmail.run(account.id); return { cancelled: true }; }, /** * Pending → signed in. Dev-only shortcut so the signed-in UI is * reachable without a working magic-link verification flow. Will be * removed once /auth/callback promotes pending_email itself. * * Goes through attachEmailToAccount so we get the same conflict * handling as real verification: if another account already owns the * email, current account merges into it (keys move over, current is * deleted, session cookie reissued). */ markVerified: async ({ locals, cookies }) => { const account = locals.account!; if (!account.pending_email) { return fail(400, { error: 'No pending email to verify.' }); } try { const result = attachEmailToAccount(account.id, account.pending_email); if (result.accountId === account.id) { // Email attached cleanly to this account — clear pending state. queries.clearPendingEmail.run(account.id); } else { // Merged into a pre-existing account that already owned this // email. The current account is gone; reissue the cookie so // subsequent requests load the surviving account. createSession(cookies, result.accountId); } return { verified: true, merged: result.accountId !== account.id }; } catch (e) { return fail(400, { error: e instanceof Error ? e.message : 'Could not verify.' }); } }, switchPlan: async ({ request, locals }) => { const account = locals.account!; const form = await request.formData(); const planId = ((form.get('plan') ?? '') as string) as PlanId; if (!PLAN_ORDER.includes(planId)) { return fail(400, { error: 'Unknown plan' }); } if (planId === 'enterprise') { return fail(400, { error: 'Enterprise is contact-only. Email contact@tidyindex.com.' }); } queries.updateAccountPlan.run(planId, account.id); return { switchedTo: planId }; }, deleteAccount: async ({ locals, cookies }) => { const account = locals.account!; queries.deleteAccount.run(account.id); clearSession(cookies); throw redirect(303, '/'); } };