diff --git a/frontend/src/components/admin/AdminDashboardPage.tsx b/frontend/src/components/admin/AdminDashboardPage.tsx
new file mode 100644
index 0000000..33d95c4
--- /dev/null
+++ b/frontend/src/components/admin/AdminDashboardPage.tsx
@@ -0,0 +1,241 @@
+import {
+ Users,
+ UserCheck,
+ UserX,
+ Activity,
+ Smartphone,
+ LogIn,
+ ShieldAlert,
+} from 'lucide-react';
+import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
+import { Skeleton } from '@/components/ui/skeleton';
+import { useAdminDashboard, useAuditLog } from '@/hooks/useAdmin';
+import { getRelativeTime } from '@/lib/date-utils';
+import { cn } from '@/lib/utils';
+
+interface StatCardProps {
+ icon: React.ReactNode;
+ label: string;
+ value: string | number;
+ iconBg?: string;
+}
+
+function StatCard({ icon, label, value, iconBg = 'bg-accent/10' }: StatCardProps) {
+ return (
+
+
+
+
+
+ );
+}
+
+function actionColor(action: string): string {
+ if (action.includes('failed') || action.includes('locked') || action.includes('disabled')) {
+ return 'bg-red-500/15 text-red-400';
+ }
+ if (action.includes('login') || action.includes('create') || action.includes('enabled')) {
+ return 'bg-green-500/15 text-green-400';
+ }
+ if (action.includes('config') || action.includes('role') || action.includes('password')) {
+ return 'bg-orange-500/15 text-orange-400';
+ }
+ return 'bg-blue-500/15 text-blue-400';
+}
+
+export default function AdminDashboardPage() {
+ const { data: dashboard, isLoading } = useAdminDashboard();
+ const { data: auditData } = useAuditLog(1, 10);
+
+ const mfaPct = dashboard ? Math.round(dashboard.mfa_adoption_rate * 100) : null;
+ const disabledUsers =
+ dashboard ? dashboard.total_users - dashboard.active_users : null;
+
+ return (
+
+ {/* Stats grid */}
+
+ {isLoading ? (
+ Array.from({ length: 5 }).map((_, i) => (
+
+
+
+
+
+ ))
+ ) : (
+ <>
+ }
+ label="Total Users"
+ value={dashboard?.total_users ?? '—'}
+ />
+ }
+ label="Active Users"
+ value={dashboard?.active_users ?? '—'}
+ iconBg="bg-green-500/10"
+ />
+ }
+ label="Disabled Users"
+ value={disabledUsers ?? '—'}
+ iconBg="bg-red-500/10"
+ />
+ }
+ label="Active Sessions"
+ value={dashboard?.active_sessions ?? '—'}
+ iconBg="bg-blue-500/10"
+ />
+ }
+ label="MFA Adoption"
+ value={mfaPct !== null ? `${mfaPct}%` : '—'}
+ iconBg="bg-purple-500/10"
+ />
+ >
+ )}
+
+
+
+ {/* Recent logins */}
+
+
+
+
+
+ {isLoading ? (
+
+ {Array.from({ length: 5 }).map((_, i) => (
+
+ ))}
+
+ ) : !dashboard?.recent_logins?.length ? (
+ No recent logins.
+ ) : (
+
+
+
+
+ |
+ Username
+ |
+
+ When
+ |
+
+ IP
+ |
+
+
+
+ {dashboard.recent_logins.map((entry, idx) => (
+
+ | {entry.username} |
+
+ {getRelativeTime(entry.last_login_at)}
+ |
+
+ {entry.ip_address ?? '—'}
+ |
+
+ ))}
+
+
+
+ )}
+
+
+
+ {/* Recent admin actions */}
+
+
+
+
+
+
+
Recent Admin Actions
+
+
+
+ {!auditData?.entries?.length ? (
+ No recent actions.
+ ) : (
+
+
+
+
+ |
+ Action
+ |
+
+ Actor
+ |
+
+ Target
+ |
+
+ When
+ |
+
+
+
+ {auditData.entries.slice(0, 10).map((entry, idx) => (
+
+ |
+
+ {entry.action}
+
+ |
+
+ {entry.actor_username ?? (
+ system
+ )}
+ |
+
+ {entry.target_username ?? '—'}
+ |
+
+ {getRelativeTime(entry.created_at)}
+ |
+
+ ))}
+
+
+
+ )}
+
+
+
+
+ );
+}
diff --git a/frontend/src/components/admin/AdminPortal.tsx b/frontend/src/components/admin/AdminPortal.tsx
new file mode 100644
index 0000000..b519f6f
--- /dev/null
+++ b/frontend/src/components/admin/AdminPortal.tsx
@@ -0,0 +1,64 @@
+import { NavLink, Routes, Route, Navigate, useLocation } from 'react-router-dom';
+import { Users, Settings2, LayoutDashboard, ShieldCheck } from 'lucide-react';
+import { cn } from '@/lib/utils';
+import IAMPage from './IAMPage';
+import ConfigPage from './ConfigPage';
+import AdminDashboardPage from './AdminDashboardPage';
+
+const tabs = [
+ { label: 'IAM Management', path: '/admin/iam', icon: Users },
+ { label: 'Configuration', path: '/admin/config', icon: Settings2 },
+ { label: 'Management Dashboard', path: '/admin/dashboard', icon: LayoutDashboard },
+];
+
+export default function AdminPortal() {
+ const location = useLocation();
+
+ return (
+
+ {/* Portal header with tab navigation */}
+
+
+
+
+ {/* Horizontal tab navigation */}
+
+
+
+
+ {/* Page content */}
+
+
+ } />
+ } />
+ } />
+ } />
+
+
+
+ );
+}
diff --git a/frontend/src/components/admin/ConfigPage.tsx b/frontend/src/components/admin/ConfigPage.tsx
new file mode 100644
index 0000000..8a9ce15
--- /dev/null
+++ b/frontend/src/components/admin/ConfigPage.tsx
@@ -0,0 +1,231 @@
+import { useState } from 'react';
+import {
+ FileText,
+ ChevronLeft,
+ ChevronRight,
+ Filter,
+ X,
+} from 'lucide-react';
+import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
+import { Button } from '@/components/ui/button';
+import { Select } from '@/components/ui/select';
+import { Skeleton } from '@/components/ui/skeleton';
+import { useAuditLog } from '@/hooks/useAdmin';
+import { getRelativeTime } from '@/lib/date-utils';
+import { cn } from '@/lib/utils';
+
+const ACTION_TYPES = [
+ 'user.create',
+ 'user.login',
+ 'user.logout',
+ 'user.login_failed',
+ 'user.locked',
+ 'user.unlocked',
+ 'user.role_changed',
+ 'user.disabled',
+ 'user.enabled',
+ 'user.password_reset',
+ 'user.totp_disabled',
+ 'user.mfa_enforced',
+ 'user.mfa_enforcement_removed',
+ 'user.sessions_revoked',
+ 'config.updated',
+];
+
+function actionLabel(action: string): string {
+ return action
+ .split('.')
+ .map((p) => p.replace(/_/g, ' '))
+ .join(' — ');
+}
+
+function actionColor(action: string): string {
+ if (action.includes('failed') || action.includes('locked') || action.includes('disabled')) {
+ return 'bg-red-500/15 text-red-400';
+ }
+ if (action.includes('login') || action.includes('create') || action.includes('enabled')) {
+ return 'bg-green-500/15 text-green-400';
+ }
+ if (action.includes('config') || action.includes('role') || action.includes('password')) {
+ return 'bg-orange-500/15 text-orange-400';
+ }
+ return 'bg-blue-500/15 text-blue-400';
+}
+
+export default function ConfigPage() {
+ const [page, setPage] = useState(1);
+ const [filterAction, setFilterAction] = useState('');
+ const PER_PAGE = 25;
+
+ const { data, isLoading } = useAuditLog(page, PER_PAGE, filterAction || undefined);
+
+ const totalPages = data ? Math.ceil(data.total / PER_PAGE) : 1;
+
+ return (
+
+
+
+
+
+
+
+
Audit Log
+ {data && (
+
+ {data.total} entries
+
+ )}
+
+
+ {/* Filter controls */}
+
+
+
+ Filter:
+
+
+
+
+ {filterAction && (
+
+ )}
+
+
+
+
+ {isLoading ? (
+
+ {Array.from({ length: 8 }).map((_, i) => (
+
+ ))}
+
+ ) : !data?.entries?.length ? (
+ No audit entries found.
+ ) : (
+ <>
+
+
+
+
+ |
+ Time
+ |
+
+ Actor
+ |
+
+ Action
+ |
+
+ Target
+ |
+
+ IP
+ |
+
+ Detail
+ |
+
+
+
+ {data.entries.map((entry, idx) => (
+
+ |
+ {getRelativeTime(entry.created_at)}
+ |
+
+ {entry.actor_username ?? (
+ system
+ )}
+ |
+
+
+ {entry.action}
+
+ |
+
+ {entry.target_username ?? '—'}
+ |
+
+ {entry.ip_address ?? '—'}
+ |
+
+ {entry.detail ?? '—'}
+ |
+
+ ))}
+
+
+
+
+ {/* Pagination */}
+ {totalPages > 1 && (
+
+
+ Page {page} of {totalPages}
+
+
+
+
+
+
+ )}
+ >
+ )}
+
+
+
+ );
+}
diff --git a/frontend/src/components/admin/CreateUserDialog.tsx b/frontend/src/components/admin/CreateUserDialog.tsx
new file mode 100644
index 0000000..a44164e
--- /dev/null
+++ b/frontend/src/components/admin/CreateUserDialog.tsx
@@ -0,0 +1,121 @@
+import { useState } from 'react';
+import { toast } from 'sonner';
+import { UserPlus, Loader2 } from 'lucide-react';
+import {
+ Dialog,
+ DialogContent,
+ DialogHeader,
+ DialogTitle,
+ DialogFooter,
+ DialogClose,
+} from '@/components/ui/dialog';
+import { Input } from '@/components/ui/input';
+import { Label } from '@/components/ui/label';
+import { Select } from '@/components/ui/select';
+import { Button } from '@/components/ui/button';
+import { useCreateUser, getErrorMessage } from '@/hooks/useAdmin';
+import type { UserRole } from '@/types';
+
+interface CreateUserDialogProps {
+ open: boolean;
+ onOpenChange: (open: boolean) => void;
+}
+
+export default function CreateUserDialog({ open, onOpenChange }: CreateUserDialogProps) {
+ const [username, setUsername] = useState('');
+ const [password, setPassword] = useState('');
+ const [role, setRole] = useState('standard');
+
+ const createUser = useCreateUser();
+
+ const handleSubmit = async (e: React.FormEvent) => {
+ e.preventDefault();
+ if (!username.trim() || !password.trim()) return;
+
+ try {
+ await createUser.mutateAsync({ username: username.trim(), password, role });
+ toast.success(`User "${username.trim()}" created successfully`);
+ setUsername('');
+ setPassword('');
+ setRole('standard');
+ onOpenChange(false);
+ } catch (err) {
+ toast.error(getErrorMessage(err, 'Failed to create user'));
+ }
+ };
+
+ return (
+
+ );
+}
diff --git a/frontend/src/components/admin/IAMPage.tsx b/frontend/src/components/admin/IAMPage.tsx
new file mode 100644
index 0000000..64329b9
--- /dev/null
+++ b/frontend/src/components/admin/IAMPage.tsx
@@ -0,0 +1,298 @@
+import { useState } from 'react';
+import { toast } from 'sonner';
+import {
+ Users,
+ UserCheck,
+ ShieldCheck,
+ Smartphone,
+ Plus,
+ Loader2,
+ Activity,
+} from 'lucide-react';
+import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
+import { Button } from '@/components/ui/button';
+import { Switch } from '@/components/ui/switch';
+import { Label } from '@/components/ui/label';
+import { Skeleton } from '@/components/ui/skeleton';
+import {
+ useAdminUsers,
+ useAdminDashboard,
+ useAdminConfig,
+ useUpdateConfig,
+ getErrorMessage,
+} from '@/hooks/useAdmin';
+import { getRelativeTime } from '@/lib/date-utils';
+import type { AdminUserDetail, UserRole } from '@/types';
+import { cn } from '@/lib/utils';
+import UserActionsMenu from './UserActionsMenu';
+import CreateUserDialog from './CreateUserDialog';
+
+// ── Role badge ────────────────────────────────────────────────────────────────
+
+function RoleBadge({ role }: { role: UserRole }) {
+ const styles: Record = {
+ admin: 'bg-red-500/15 text-red-400',
+ standard: 'bg-blue-500/15 text-blue-400',
+ public_event_manager: 'bg-purple-500/15 text-purple-400',
+ };
+ const labels: Record = {
+ admin: 'Admin',
+ standard: 'Standard',
+ public_event_manager: 'Pub. Events',
+ };
+ return (
+
+ {labels[role]}
+
+ );
+}
+
+// ── Stat card ─────────────────────────────────────────────────────────────────
+
+interface StatCardProps {
+ icon: React.ReactNode;
+ label: string;
+ value: string | number;
+ iconBg?: string;
+}
+
+function StatCard({ icon, label, value, iconBg = 'bg-accent/10' }: StatCardProps) {
+ return (
+
+
+
+
+
+ );
+}
+
+// ── Main page ─────────────────────────────────────────────────────────────────
+
+export default function IAMPage() {
+ const [createOpen, setCreateOpen] = useState(false);
+
+ const { data: users, isLoading: usersLoading } = useAdminUsers();
+ const { data: dashboard } = useAdminDashboard();
+ const { data: config, isLoading: configLoading } = useAdminConfig();
+ const updateConfig = useUpdateConfig();
+
+ const handleConfigToggle = async (key: 'allow_registration' | 'enforce_mfa_new_users', value: boolean) => {
+ try {
+ await updateConfig.mutateAsync({ [key]: value });
+ toast.success('System settings updated');
+ } catch (err) {
+ toast.error(getErrorMessage(err, 'Failed to update settings'));
+ }
+ };
+
+ const mfaPct = dashboard
+ ? Math.round(dashboard.mfa_adoption_rate * 100)
+ : null;
+
+ return (
+
+ {/* Stats row */}
+
+ }
+ label="Total Users"
+ value={dashboard?.total_users ?? '—'}
+ />
+ }
+ label="Active Sessions"
+ value={dashboard?.active_sessions ?? '—'}
+ iconBg="bg-green-500/10"
+ />
+ }
+ label="Admins"
+ value={dashboard?.admin_count ?? '—'}
+ iconBg="bg-red-500/10"
+ />
+ }
+ label="MFA Adoption"
+ value={mfaPct !== null ? `${mfaPct}%` : '—'}
+ iconBg="bg-purple-500/10"
+ />
+
+
+ {/* User table */}
+
+
+
+
+
+
+ {usersLoading ? (
+
+ {Array.from({ length: 4 }).map((_, i) => (
+
+ ))}
+
+ ) : !users?.length ? (
+ No users found.
+ ) : (
+
+
+
+
+ |
+ Username
+ |
+
+ Role
+ |
+
+ Status
+ |
+
+ Last Login
+ |
+
+ MFA
+ |
+
+ Sessions
+ |
+
+ Created
+ |
+
+ Actions
+ |
+
+
+
+ {users.map((user: AdminUserDetail, idx) => (
+
+ | {user.username} |
+
+
+ |
+
+
+ {user.is_active ? 'Active' : 'Disabled'}
+
+ |
+
+ {user.last_login_at ? getRelativeTime(user.last_login_at) : '—'}
+ |
+
+ {user.totp_enabled ? (
+
+ On
+
+ ) : user.mfa_enforce_pending ? (
+
+ Pending
+
+ ) : (
+ —
+ )}
+ |
+
+ {user.active_sessions}
+ |
+
+ {getRelativeTime(user.created_at)}
+ |
+
+
+ |
+
+ ))}
+
+
+
+ )}
+
+
+
+ {/* System settings */}
+
+
+
+
+
+
+
System Settings
+
+
+
+ {configLoading ? (
+
+
+
+
+ ) : (
+ <>
+
+
+
+
+ When enabled, the /register page accepts new sign-ups.
+
+
+
handleConfigToggle('allow_registration', v)}
+ disabled={updateConfig.isPending}
+ />
+
+
+
+
+
+
+ Newly registered users will be required to set up TOTP before accessing the app.
+
+
+
handleConfigToggle('enforce_mfa_new_users', v)}
+ disabled={updateConfig.isPending}
+ />
+
+ >
+ )}
+
+
+
+
+
+ );
+}
diff --git a/frontend/src/components/admin/UserActionsMenu.tsx b/frontend/src/components/admin/UserActionsMenu.tsx
new file mode 100644
index 0000000..3163b2f
--- /dev/null
+++ b/frontend/src/components/admin/UserActionsMenu.tsx
@@ -0,0 +1,296 @@
+import { useState, useRef, useEffect } from 'react';
+import { toast } from 'sonner';
+import {
+ MoreHorizontal,
+ ShieldCheck,
+ ShieldOff,
+ KeyRound,
+ UserX,
+ UserCheck,
+ LogOut,
+ Smartphone,
+ SmartphoneOff,
+ ChevronRight,
+ Loader2,
+} from 'lucide-react';
+import { Button } from '@/components/ui/button';
+import { useConfirmAction } from '@/hooks/useConfirmAction';
+import {
+ useUpdateRole,
+ useResetPassword,
+ useDisableMfa,
+ useEnforceMfa,
+ useRemoveMfaEnforcement,
+ useToggleUserActive,
+ useRevokeSessions,
+ getErrorMessage,
+} from '@/hooks/useAdmin';
+import type { AdminUserDetail, UserRole } from '@/types';
+import { cn } from '@/lib/utils';
+
+interface UserActionsMenuProps {
+ user: AdminUserDetail;
+}
+
+const ROLES: { value: UserRole; label: string }[] = [
+ { value: 'admin', label: 'Admin' },
+ { value: 'standard', label: 'Standard' },
+ { value: 'public_event_manager', label: 'Public Event Manager' },
+];
+
+export default function UserActionsMenu({ user }: UserActionsMenuProps) {
+ const [open, setOpen] = useState(false);
+ const [roleSubmenuOpen, setRoleSubmenuOpen] = useState(false);
+ const [showResetPassword, setShowResetPassword] = useState(false);
+ const [newPassword, setNewPassword] = useState('');
+ const menuRef = useRef(null);
+
+ const updateRole = useUpdateRole();
+ const resetPassword = useResetPassword();
+ const disableMfa = useDisableMfa();
+ const enforceMfa = useEnforceMfa();
+ const removeMfaEnforcement = useRemoveMfaEnforcement();
+ const toggleActive = useToggleUserActive();
+ const revokeSessions = useRevokeSessions();
+
+ // Close on outside click
+ useEffect(() => {
+ const handleOutside = (e: MouseEvent) => {
+ if (menuRef.current && !menuRef.current.contains(e.target as Node)) {
+ setOpen(false);
+ setRoleSubmenuOpen(false);
+ }
+ };
+ if (open) document.addEventListener('mousedown', handleOutside);
+ return () => document.removeEventListener('mousedown', handleOutside);
+ }, [open]);
+
+ const handleAction = async (fn: () => Promise, successMsg: string) => {
+ try {
+ await fn();
+ toast.success(successMsg);
+ setOpen(false);
+ } catch (err) {
+ toast.error(getErrorMessage(err, 'Action failed'));
+ }
+ };
+
+ // Two-click confirms
+ const disableMfaConfirm = useConfirmAction(() => {
+ handleAction(() => disableMfa.mutateAsync(user.id), 'MFA disabled');
+ });
+
+ const toggleActiveConfirm = useConfirmAction(() => {
+ handleAction(
+ () => toggleActive.mutateAsync({ userId: user.id, active: !user.is_active }),
+ user.is_active ? 'Account disabled' : 'Account enabled'
+ );
+ });
+
+ const revokeSessionsConfirm = useConfirmAction(() => {
+ handleAction(() => revokeSessions.mutateAsync(user.id), 'Sessions revoked');
+ });
+
+ const isLoading =
+ updateRole.isPending ||
+ resetPassword.isPending ||
+ disableMfa.isPending ||
+ enforceMfa.isPending ||
+ removeMfaEnforcement.isPending ||
+ toggleActive.isPending ||
+ revokeSessions.isPending;
+
+ return (
+
+
+
+ {open && (
+
+ {/* Edit Role */}
+
+
+
+ {roleSubmenuOpen && (
+
setRoleSubmenuOpen(true)}
+ onMouseLeave={() => setRoleSubmenuOpen(false)}
+ >
+ {ROLES.map(({ value, label }) => (
+
+ ))}
+
+ )}
+
+
+ {/* Reset Password */}
+ {!showResetPassword ? (
+
+ ) : (
+
+
setNewPassword(e.target.value)}
+ autoFocus
+ />
+
+
+
+
+
+ )}
+
+
+
+ {/* MFA actions */}
+ {user.mfa_enforce_pending ? (
+
+ ) : (
+
+ )}
+
+ {user.totp_enabled && (
+
+ )}
+
+
+
+ {/* Disable / Enable Account */}
+
+
+ {/* Revoke Sessions */}
+
+
+ )}
+
+ );
+}
diff --git a/frontend/src/hooks/useAdmin.ts b/frontend/src/hooks/useAdmin.ts
new file mode 100644
index 0000000..3d9d484
--- /dev/null
+++ b/frontend/src/hooks/useAdmin.ts
@@ -0,0 +1,165 @@
+import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
+import api, { getErrorMessage } from '@/lib/api';
+import type {
+ AdminUser,
+ AdminUserDetail,
+ AdminDashboardData,
+ SystemConfig,
+ AuditLogEntry,
+ UserRole,
+} from '@/types';
+
+interface AuditLogResponse {
+ entries: AuditLogEntry[];
+ total: number;
+ page: number;
+ per_page: number;
+}
+
+interface CreateUserPayload {
+ username: string;
+ password: string;
+ role: UserRole;
+}
+
+interface UpdateRolePayload {
+ userId: number;
+ role: UserRole;
+}
+
+interface ResetPasswordPayload {
+ userId: number;
+ new_password: string;
+}
+
+// ── Queries ──────────────────────────────────────────────────────────────────
+
+export function useAdminUsers() {
+ return useQuery({
+ queryKey: ['admin', 'users'],
+ queryFn: async () => {
+ const { data } = await api.get('/admin/users');
+ return data;
+ },
+ });
+}
+
+export function useAdminDashboard() {
+ return useQuery({
+ queryKey: ['admin', 'dashboard'],
+ queryFn: async () => {
+ const { data } = await api.get('/admin/dashboard');
+ return data;
+ },
+ });
+}
+
+export function useAdminConfig() {
+ return useQuery({
+ queryKey: ['admin', 'config'],
+ queryFn: async () => {
+ const { data } = await api.get('/admin/config');
+ return data;
+ },
+ });
+}
+
+export function useAuditLog(
+ page: number,
+ perPage: number,
+ action?: string,
+ targetUserId?: number
+) {
+ return useQuery({
+ queryKey: ['admin', 'audit-log', page, perPage, action, targetUserId],
+ queryFn: async () => {
+ const params: Record = { page, per_page: perPage };
+ if (action) params.action = action;
+ if (targetUserId) params.target_user_id = targetUserId;
+ const { data } = await api.get('/admin/audit-log', { params });
+ return data;
+ },
+ });
+}
+
+// ── Mutations ─────────────────────────────────────────────────────────────────
+
+function useAdminMutation(
+ mutationFn: (vars: TVariables) => Promise,
+ onSuccess?: () => void
+) {
+ const queryClient = useQueryClient();
+ return useMutation({
+ mutationFn,
+ onSuccess: () => {
+ queryClient.invalidateQueries({ queryKey: ['admin'] });
+ onSuccess?.();
+ },
+ });
+}
+
+export function useCreateUser() {
+ return useAdminMutation(async (payload: CreateUserPayload) => {
+ const { data } = await api.post('/admin/users', payload);
+ return data;
+ });
+}
+
+export function useUpdateRole() {
+ return useAdminMutation(async ({ userId, role }: UpdateRolePayload) => {
+ const { data } = await api.patch(`/admin/users/${userId}/role`, { role });
+ return data;
+ });
+}
+
+export function useResetPassword() {
+ return useAdminMutation(async ({ userId, new_password }: ResetPasswordPayload) => {
+ const { data } = await api.post(`/admin/users/${userId}/reset-password`, { new_password });
+ return data;
+ });
+}
+
+export function useDisableMfa() {
+ return useAdminMutation(async (userId: number) => {
+ const { data } = await api.delete(`/admin/users/${userId}/totp`);
+ return data;
+ });
+}
+
+export function useEnforceMfa() {
+ return useAdminMutation(async (userId: number) => {
+ const { data } = await api.post(`/admin/users/${userId}/enforce-mfa`);
+ return data;
+ });
+}
+
+export function useRemoveMfaEnforcement() {
+ return useAdminMutation(async (userId: number) => {
+ const { data } = await api.delete(`/admin/users/${userId}/enforce-mfa`);
+ return data;
+ });
+}
+
+export function useToggleUserActive() {
+ return useAdminMutation(async ({ userId, active }: { userId: number; active: boolean }) => {
+ const { data } = await api.patch(`/admin/users/${userId}/active`, { is_active: active });
+ return data;
+ });
+}
+
+export function useRevokeSessions() {
+ return useAdminMutation(async (userId: number) => {
+ const { data } = await api.delete(`/admin/users/${userId}/sessions`);
+ return data;
+ });
+}
+
+export function useUpdateConfig() {
+ return useAdminMutation(async (config: Partial) => {
+ const { data } = await api.patch('/admin/config', config);
+ return data;
+ });
+}
+
+// Re-export getErrorMessage for convenience in admin components
+export { getErrorMessage };