aboutsummaryrefslogtreecommitdiff
path: root/web/ui/src/routes/dashboard/account/+page.server.ts
blob: 5581b524b83b07f98823c8f75060e9b52be5fe76 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
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, '/');
  }
};