Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.
| Merge account deletion: soft delete for 30 days, staff restore and purge, ghost for what remains (identity 0037) | 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 | */ | |
| Merge sudo: delete an account with the workspaces it alone owns, purge each | 7 | import type { AccountDeletion, AccountWent, DeletedWorkspace, SoleOwnedWorkspace, WorkspaceDeletedWith } from "@g1t/contracts"; |
| Merge account deletion: soft delete for 30 days, staff restore and purge, ghost for what remains (identity 0037) | 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 | ||
| Merge sudo: delete an account with the workspaces it alone owns, purge each | 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 | */ | |
| Merge account deletion: soft delete for 30 days, staff restore and purge, ghost for what remains (identity 0037) | 26 | export function staffDeletionRefusal(deletion: AccountDeletion): string | null { |
| 27 | if (deletion.protected) return `${deletion.username} is protected and can never be deleted.`; | |
| Merge sudo: delete an account with the workspaces it alone owns, purge each | 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(" ")}`; | |
| Merge account deletion: soft delete for 30 days, staff restore and purge, ghost for what remains (identity 0037) | 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 | } | |
| Merge sudo: delete an account with the workspaces it alone owns, purge each | 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 | } |
This file's history is long; its oldest lines are credited to the oldest commit read.