Add site-wide announcements, account migration, and profile self-service

Announcements: a Super Admin (or an Admin individually flagged via
profiles.can_post_announcements) can post/retire a site-wide banner.
Account migration: a Super Admin can move a locked-out user's diagrams,
private catalog entries, and submissions to another account, with a
migration-history log; ProfileModal adds the self-service half (link a new
Google identity via Supabase manual linking, then unlink the old one,
while signed in as the account being migrated). Also reworks the top bar's
flat button row into grouped dropdown menus (Diagram / Admin / Account) now
that there are enough entries to need it.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017DUU6CnxECCDeqDNYJgr5x
This commit is contained in:
2026-09-28 10:05:11 -05:00
co-authored by Claude Sonnet 5
parent 143a96aed1
commit 6cdfde822d
18 changed files with 1857 additions and 56 deletions
+48
View File
@@ -9,9 +9,40 @@ export interface AdminUserSummary {
username: string
email: string
role: UserRole
/** Lets this specific Admin post/retire site-wide announcements — a
* narrower grant than the role itself (a Super Admin can always post
* regardless of this). Meaningless for a regular/super_admin row, but
* present either way since it mirrors the underlying profiles column. */
canPostAnnouncements: boolean
/** Set (a future timestamp) while banned; undefined otherwise. */
bannedUntil?: string
createdAt: string
/** Set once this account's data has been moved to another account via
* migrateAccount below (organized-ideas.md §2) — never cleared, and
* never set back to undefined by anything in this app. */
migratedToUserId?: string
migratedToUsername?: string
}
export interface AccountMigrationImpact {
diagramCount: number
privateEntityCount: number
submissionCount: number
}
/** One row of the permanent account-migration audit log. */
export interface AccountMigrationRecord extends AccountMigrationImpact {
id: string
/** Null if that account has since been deleted — fromUsername/toUsername
* (snapshotted at migration time) stay populated regardless, so the log
* stays legible either way. */
fromUserId: string | null
toUserId: string | null
fromUsername: string
toUsername: string
performedBy: string | null
verificationNotes: string
createdAt: string
}
/** Storage abstraction for Super-Admin user management (organized-ideas.md
@@ -23,10 +54,27 @@ export interface AdminUserSummary {
export interface AdminUserRepository {
listUsers(): Promise<AdminUserSummary[]>
updateRole(userId: string, role: UserRole): Promise<void>
/** Super-Admin-only in practice (RLS), same as updateRole — grants or
* revokes one Admin's ability to post announcements. */
updateCanPostAnnouncements(userId: string, canPostAnnouncements: boolean): Promise<void>
/** Reversible — blocks login without touching the account's data. */
banUser(userId: string): Promise<void>
unbanUser(userId: string): Promise<void>
/** Irreversible — cascades to the user's profile, diagrams, and owned
* private catalog entries via their existing foreign keys. */
deleteUser(userId: string): Promise<void>
/** Read-only "what would move" check before committing a migration below —
* same idea as the catalog usage-impact check before an unpublish. */
previewAccountMigration(fromUserId: string, toUserId: string): Promise<AccountMigrationImpact>
/** Moves diagrams, private catalog entries, and submissions from one
* account to another (organized-ideas.md §2's tool for someone who's
* lost access to their old account) — never touches credentials or
* deletes the old account, just reassigns what it owns. Throws (rather
* than swallowing, unlike most methods here) so the caller can surface
* *why* it failed — most commonly: already migrated, missing
* verification notes, or migrating an account into itself. */
migrateAccount(fromUserId: string, toUserId: string, verificationNotes: string): Promise<AccountMigrationImpact>
/** The permanent audit trail, most recent first. */
listAccountMigrations(): Promise<AccountMigrationRecord[]>
}