| 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 | */ |
| 7 | import type { AccountDeletion, AccountWent, DeletedWorkspace, SoleOwnedWorkspace, WorkspaceDeletedWith } from "@g1t/contracts"; |
| 8 | |
| 9 | const 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". */ |
| 12 | export 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 | */ |
| 26 | export 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. */ |
| 32 | export 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. */ |
| 39 | export 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 | */ |
| 49 | export 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. */ |
| 56 | export 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. */ |
| 62 | export 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. */ |
| 73 | export 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. */ |
| 81 | export 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. */ |
| 87 | export 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 | } |