diff options
Diffstat (limited to '')
| -rw-r--r-- | web/ui/src/routes/dashboard/account/+page.server.ts | 107 |
1 files changed, 107 insertions, 0 deletions
diff --git a/web/ui/src/routes/dashboard/account/+page.server.ts b/web/ui/src/routes/dashboard/account/+page.server.ts new file mode 100644 index 0000000..5581b52 --- /dev/null +++ b/web/ui/src/routes/dashboard/account/+page.server.ts @@ -0,0 +1,107 @@ +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, '/'); + } +}; |
