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, '/');
}
};
|