Skip to content
90 linesCodeBlameRaw
1/**
2 * Deleting accounts, as a person's page and the Deleted accounts page show
3 * it: what went with an account, what stands in the way of deleting one,
4 * and what staff type to confirm. Identity checks all of it again. No
5 * Workers imports, so it can be tested under Node.
6 */
7import type { AccountDeletion, AccountWent, DeletedWorkspace, SoleOwnedWorkspace, WorkspaceDeletedWith } from "@g1t/contracts";
8
9const plural = (n: number, one: string, many: string) => `${n} ${n === 1 ? one : many}`;
10
11/** What went with an account, in a line: "2 workspaces, 1 team, 3 repositories, 4 tokens, 1 SSH key". */
12export function accountWentSummary(went: AccountWent): string {
13 return [
14 plural(went.workspaces, "workspace", "workspaces"),
15 plural(went.teams, "team", "teams"),
16 plural(went.repositories, "repository", "repositories"),
17 plural(went.tokens, "token", "tokens"),
18 plural(went.sshKeys, "SSH key", "SSH keys"),
19 ].join(", ");
20}
21
22/**
23 * Why staff cannot delete the account at all, in a sentence about it, or
24 * null. Owning workspaces alone is not one: staff delete them with it.
25 */
26export function staffDeletionRefusal(deletion: AccountDeletion): string | null {
27 if (deletion.protected) return `${deletion.username} is protected and can never be deleted.`;
28 return null;
29}
30
31/** What the workspaces the account owns alone mean for deleting it, in a sentence, or null when it owns none. */
32export function soleOwnerNote(deletion: AccountDeletion): string | null {
33 const count = deletion.sole_owner_of.length;
34 if (count === 0) return null;
35 return `${deletion.username} is the only owner of ${count === 1 ? "1 workspace" : `${count} workspaces`}. Deleting the account deletes ${count === 1 ? "it" : "them"} first, each as its owner would: billing closes it, and it is kept for a restore like any deleted workspace.`;
36}
37
38/** Why staff cannot delete this workspace with the account, or null. */
39export function soleWorkspaceRefusal(workspace: SoleOwnedWorkspace): string | null {
40 if (workspace.protected) return `${workspace.slug} is protected and can never be deleted.`;
41 return workspace.billing;
42}
43
44/**
45 * Why staff cannot delete the account together with the workspaces it owns
46 * alone, naming each that stands in the way, or null when every one can go.
47 * Identity says the same, and deletes nothing, when asked anyway.
48 */
49export function soleWorkspacesRefusal(deletion: AccountDeletion): string | null {
50 const reasons = deletion.sole_owner_of.map(soleWorkspaceRefusal).filter((reason): reason is string => reason !== null);
51 if (reasons.length === 0) return null;
52 return `${deletion.username} cannot be deleted with its workspaces yet. ${reasons.join(" ")}`;
53}
54
55/** Whether what staff typed is the username. Identity checks it again. */
56export function confirmsUsername(username: string, typed: string): boolean {
57 const value = typed.trim();
58 return value !== "" && value.toLowerCase() === username.toLowerCase();
59}
60
61/** What staff sent to delete an account. */
62export type StaffDeleteForm = {
63 username: string;
64 reason: string;
65 confirm: string;
66 /** The form that deletes the workspaces it owns alone too. */
67 withWorkspaces: boolean;
68 /** The box saying those workspaces go too, ticked. */
69 acknowledged: boolean;
70};
71
72/** What is wrong with the form to delete an account, or null. Identity checks it all again. */
73export function staffDeleteProblem(form: StaffDeleteForm): string | null {
74 if (!form.reason) return "Say why the account is being deleted.";
75 if (!confirmsUsername(form.username, form.confirm)) return `Type ${form.username} to confirm.`;
76 if (form.withWorkspaces && !form.acknowledged) return "Tick the box to say the workspaces it alone owns are deleted too.";
77 return null;
78}
79
80/** A workspace deleted with the account, as its page shows it: still waiting to be purged, or gone. */
81export type DeletedWithAccount = WorkspaceDeletedWith & {
82 /** Its deletion, while it waits to be purged; null once it is purged or restored. */
83 deleted: DeletedWorkspace | null;
84};
85
86/** The workspaces deleted with an account, each matched to its deletion by id. */
87export function deletedWithAccount(went: AccountWent, deleted: DeletedWorkspace[]): DeletedWithAccount[] {
88 const byId = new Map(deleted.map((workspace) => [workspace.workspaceId, workspace]));
89 return (went.deletedWorkspaces ?? []).map((workspace) => ({ ...workspace, deleted: byId.get(workspace.workspaceId) ?? null }));
90}