aboutsummaryrefslogtreecommitdiff
path: root/web/ui/src/routes/dashboard/account/+page.server.ts
diff options
context:
space:
mode:
Diffstat (limited to 'web/ui/src/routes/dashboard/account/+page.server.ts')
-rw-r--r--web/ui/src/routes/dashboard/account/+page.server.ts107
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, '/');
+ }
+};