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
92 lines
3.3 KiB
TypeScript
92 lines
3.3 KiB
TypeScript
import { create } from 'zustand'
|
|
import type {
|
|
AccountMigrationImpact,
|
|
AccountMigrationRecord,
|
|
AdminUserRepository,
|
|
AdminUserSummary,
|
|
UserRole,
|
|
} from '../data/AdminUserRepository'
|
|
import { SupabaseAdminUserRepository } from '../data/SupabaseAdminUserRepository'
|
|
|
|
const repository: AdminUserRepository = new SupabaseAdminUserRepository()
|
|
|
|
interface AdminUserStoreState {
|
|
users: AdminUserSummary[]
|
|
isLoaded: boolean
|
|
/** The account-migration audit log — loaded on demand (opening the
|
|
* migration UI), not alongside users on every app load. */
|
|
migrations: AccountMigrationRecord[]
|
|
|
|
loadUsers: () => Promise<void>
|
|
updateRole: (userId: string, role: UserRole) => Promise<void>
|
|
updateCanPostAnnouncements: (userId: string, canPostAnnouncements: boolean) => Promise<void>
|
|
banUser: (userId: string) => Promise<void>
|
|
unbanUser: (userId: string) => Promise<void>
|
|
deleteUser: (userId: string) => Promise<void>
|
|
|
|
previewMigration: (fromUserId: string, toUserId: string) => Promise<AccountMigrationImpact>
|
|
/** Throws on failure (see AdminUserRepository.migrateAccount) — the
|
|
* caller is expected to catch and surface the reason. */
|
|
migrateAccount: (fromUserId: string, toUserId: string, verificationNotes: string) => Promise<AccountMigrationImpact>
|
|
loadMigrations: () => Promise<void>
|
|
}
|
|
|
|
export const useAdminUserStore = create<AdminUserStoreState>((set) => ({
|
|
users: [],
|
|
isLoaded: false,
|
|
migrations: [],
|
|
|
|
loadUsers: async () => {
|
|
const users = await repository.listUsers()
|
|
set({ users, isLoaded: true })
|
|
},
|
|
|
|
updateRole: async (userId, role) => {
|
|
await repository.updateRole(userId, role)
|
|
set((state) => ({ users: state.users.map((u) => (u.id === userId ? { ...u, role } : u)) }))
|
|
},
|
|
|
|
updateCanPostAnnouncements: async (userId, canPostAnnouncements) => {
|
|
await repository.updateCanPostAnnouncements(userId, canPostAnnouncements)
|
|
set((state) => ({
|
|
users: state.users.map((u) => (u.id === userId ? { ...u, canPostAnnouncements } : u)),
|
|
}))
|
|
},
|
|
|
|
banUser: async (userId) => {
|
|
await repository.banUser(userId)
|
|
// Re-fetch rather than guessing bannedUntil client-side — the Edge
|
|
// Function is the one place that actually knows the real value.
|
|
const users = await repository.listUsers()
|
|
set({ users })
|
|
},
|
|
|
|
unbanUser: async (userId) => {
|
|
await repository.unbanUser(userId)
|
|
const users = await repository.listUsers()
|
|
set({ users })
|
|
},
|
|
|
|
deleteUser: async (userId) => {
|
|
await repository.deleteUser(userId)
|
|
set((state) => ({ users: state.users.filter((u) => u.id !== userId) }))
|
|
},
|
|
|
|
previewMigration: (fromUserId, toUserId) => repository.previewAccountMigration(fromUserId, toUserId),
|
|
|
|
migrateAccount: async (fromUserId, toUserId, verificationNotes) => {
|
|
const impact = await repository.migrateAccount(fromUserId, toUserId, verificationNotes)
|
|
// Re-fetch rather than patch client-side — migratedToUserId/Username
|
|
// both need the fresh server-computed join, and the migration log needs
|
|
// its new row.
|
|
const [users, migrations] = await Promise.all([repository.listUsers(), repository.listAccountMigrations()])
|
|
set({ users, migrations })
|
|
return impact
|
|
},
|
|
|
|
loadMigrations: async () => {
|
|
const migrations = await repository.listAccountMigrations()
|
|
set({ migrations })
|
|
},
|
|
}))
|