Skip to content
81 linesCodeBlameRaw
1/**
2 * The confirmation gate: a signed-in person whose account has not confirmed
3 * its email address is sent to /confirm-email from every page, except the
4 * pages that page needs (confirming by link, signing out and in again,
5 * resetting a password) and the public pages about g1t: its policies,
6 * security, support, status and prices. No Workers or React imports, so it
7 * can be tested under Node.
8 */
9
10/** Where an account confirms its address: the code, a new one, a new address. */
11export const CONFIRM_PATH = "/confirm-email";
12
13/** Pages a pending account can open as they are. */
14const OPEN = new Set([
15 CONFIRM_PATH,
16 // The link in the email, which confirms whoever follows it.
17 "/verify",
18 "/logout",
19 "/login",
20 "/login/two-factor",
21 "/register",
22 "/forgot",
23 "/reset",
24 // Who makes g1t and the promises it keeps.
25 "/policies",
26 "/security",
27 "/support",
28 "/status",
29 "/status.json",
30 "/pricing",
31]);
32
33/** Prefixes of the same: each policy, signing in with GitHub, well-known files. */
34const OPEN_UNDER = ["/policies/", "/auth/github", "/.well-known/"];
35
36type Pending = { kind?: string; verified?: boolean } | null | undefined;
37
38/** The page a data request (`/foo.data`, `/_root.data`) is for. */
39export function pageOf(pathname: string): string {
40 if (!pathname.endsWith(".data")) return pathname;
41 const page = pathname.slice(0, -".data".length);
42 return page === "/_root" || page === "" ? "/" : page;
43}
44
45/** Whether a pending account may open `pathname` as it is. */
46export function openWhilePending(pathname: string): boolean {
47 const page = pageOf(pathname);
48 const path = page.length > 1 ? page.replace(/\/+$/, "") : page;
49 return OPEN.has(path) || OPEN_UNDER.some((prefix) => path.startsWith(prefix));
50}
51
52/**
53 * Where to send `viewer` instead of `pathname` + `search`: the confirmation
54 * page, with where they were going as `next`; null when the page is theirs
55 * to open (confirmed, signed out, an agent or a workspace, or a page in the
56 * list above).
57 */
58export function confirmGate(pathname: string, search: string, viewer: Pending): string | null {
59 if (!viewer || (viewer.kind ?? "user") !== "user" || viewer.verified) return null;
60 if (openWhilePending(pathname)) return null;
61 const page = pageOf(pathname);
62 // A data request's own parameters are not where they were going.
63 const params = new URLSearchParams(search);
64 params.delete("_routes");
65 const query = params.toString();
66 const next = page === "/" && !query ? "" : `?next=${encodeURIComponent(page + (query ? `?${query}` : ""))}`;
67 return `${CONFIRM_PATH}${next}`;
68}
69
70/** What the confirmation page says once a code or link has worked. */
71export function confirmedLine(done: { joined?: string | null; inviteLapsed?: string | null }): string {
72 if (done.inviteLapsed) return done.inviteLapsed;
73 if (done.joined) return `Your email address is confirmed, and you have joined ${done.joined}.`;
74 return "Your email address is confirmed.";
75}
76
77/** Where to go once confirmed: back where they were going, else the workspace joined, else home. */
78export function afterConfirming(next: string, joined?: string | null): string {
79 if (next && next !== "/") return next;
80 return joined ? `/${joined}` : "/";
81}