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
81 lines
3.8 KiB
TypeScript
81 lines
3.8 KiB
TypeScript
/** A user's role — mirrors `profiles.role`'s check constraint. Defined here
|
|
* (data layer) rather than in state/authStore.ts so both that store and
|
|
* this repository share one definition without state importing from data
|
|
* in the wrong direction. */
|
|
export type UserRole = 'regular' | 'admin' | 'super_admin'
|
|
|
|
export interface AdminUserSummary {
|
|
id: string
|
|
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
|
|
* §6's "CRUD user accounts"). Listing and role changes are plain
|
|
* RLS/privileged-function reads and writes; ban/unban/delete go through
|
|
* the admin-user-action Edge Function since those specifically need
|
|
* Supabase Auth's Admin API — see that function's own header comment for
|
|
* why this can't just be another SQL function like the rest. */
|
|
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[]>
|
|
}
|