import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { useState } from 'react'; import { Link } from 'react-router-dom'; import { CheckCircle2, Clock, Copy, FolderKanban, Megaphone, Pencil, ShieldCheck, Trash2, UserPlus, Users, XCircle, } from 'lucide-react'; import { createBanner, createInvite, deleteBanner, deleteProject, getAdminOverview, impersonateUser, type ImpersonationResult, listAdminBanners, listAdminUsers, listInvites, listProjects, revokeInvite, setProjectApproval, setUserAdmin, updateBanner, updateProject, type AdminUser, type Banner, type ProjectEntity, } from '@/lib/api-client'; import { useAuthSession } from '@/lib/use-auth-session'; import { useDemoMode } from '@/lib/demo-mode'; import { DemoRestricted, LoadingState, useToast } from '@/shared/components'; const statLabels: Record = { users: 'Users', projects: 'Projects', pending_projects: 'Pending approval', services: 'Services', running_services: 'Running', databases: 'Databases', agents: 'Agents', templates: 'Templates', user_templates: 'User templates', cron_jobs: 'Cron jobs', deployments_total: 'Deployments', }; export function AdminPage() { const queryClient = useQueryClient(); const { showToast } = useToast(); const isDemoMode = useDemoMode(); const sessionQuery = useAuthSession({ enabled: !isDemoMode }); const myId = sessionQuery.data?.user.id; const overviewQuery = useQuery({ queryKey: ['admin-overview'], queryFn: getAdminOverview, enabled: !isDemoMode }); const usersQuery = useQuery({ queryKey: ['admin-users'], queryFn: listAdminUsers, enabled: !isDemoMode }); const projectsQuery = useQuery({ queryKey: ['admin-projects'], queryFn: () => listProjects({ limit: 100 }), enabled: !isDemoMode }); const bannersQuery = useQuery({ queryKey: ['admin-banners'], queryFn: listAdminBanners, enabled: !isDemoMode }); const invitesQuery = useQuery({ queryKey: ['admin-invites'], queryFn: listInvites, enabled: !isDemoMode, select: (list) => list.map((invite) => ({ ...invite, expired: !invite.used_at && new Date(invite.expires_at).getTime() < Date.now(), })), }); const [editingProject, setEditingProject] = useState(null); const [editForm, setEditForm] = useState({ name: '', description: '' }); const [bannerForm, setBannerForm] = useState({ title: '', body: '', level: 'info' as Banner['level'] }); const [inviteEmail, setInviteEmail] = useState(''); const [inviteLink, setInviteLink] = useState(null); const [impersonation, setImpersonation] = useState(null); const invalidate = () => { queryClient.invalidateQueries({ queryKey: ['admin-overview'] }); queryClient.invalidateQueries({ queryKey: ['admin-users'] }); queryClient.invalidateQueries({ queryKey: ['admin-projects'] }); queryClient.invalidateQueries({ queryKey: ['admin-banners'] }); queryClient.invalidateQueries({ queryKey: ['active-banners'] }); queryClient.invalidateQueries({ queryKey: ['admin-invites'] }); queryClient.invalidateQueries({ queryKey: ['projects'] }); }; const inviteCreateMutation = useMutation({ mutationFn: () => createInvite({ email: inviteEmail.trim() || undefined }), onSuccess: (result) => { setInviteLink(`${window.location.origin}${result.url}`); setInviteEmail(''); invalidate(); }, onError: (error) => showToast('error', 'Invite failed', error instanceof Error ? error.message : undefined), }); const inviteRevokeMutation = useMutation({ mutationFn: (id: string) => revokeInvite(id), onSuccess: () => { showToast('success', 'Invite revoked'); invalidate(); }, onError: (error) => showToast('error', 'Revoke failed', error instanceof Error ? error.message : undefined), }); const impersonateMutation = useMutation({ mutationFn: (id: string) => impersonateUser(id), onSuccess: (result) => setImpersonation(result), onError: (error) => showToast('error', 'Impersonation failed', error instanceof Error ? error.message : undefined), }); const bannerCreateMutation = useMutation({ mutationFn: () => createBanner({ title: bannerForm.title.trim(), body: bannerForm.body.trim(), level: bannerForm.level, }), onSuccess: () => { showToast('success', 'Banner published'); setBannerForm({ title: '', body: '', level: 'info' }); invalidate(); }, onError: (error) => showToast('error', 'Create failed', error instanceof Error ? error.message : undefined), }); const bannerToggleMutation = useMutation({ mutationFn: ({ id, active }: { id: string; active: boolean }) => updateBanner(id, { active }), onSuccess: () => invalidate(), onError: (error) => showToast('error', 'Update failed', error instanceof Error ? error.message : undefined), }); const bannerDeleteMutation = useMutation({ mutationFn: (id: string) => deleteBanner(id), onSuccess: () => { showToast('success', 'Banner deleted'); invalidate(); }, onError: (error) => showToast('error', 'Delete failed', error instanceof Error ? error.message : undefined), }); const approvalMutation = useMutation({ mutationFn: ({ id, approved }: { id: string; approved: boolean }) => setProjectApproval(id, approved), onSuccess: (_, vars) => { showToast('success', vars.approved ? 'Project approved' : 'Project unapproved'); invalidate(); }, onError: (error) => showToast('error', 'Update failed', error instanceof Error ? error.message : undefined), }); const adminMutation = useMutation({ mutationFn: ({ id, isAdmin }: { id: string; isAdmin: boolean }) => setUserAdmin(id, isAdmin), onSuccess: (_, vars) => { showToast('success', vars.isAdmin ? 'Admin granted' : 'Admin revoked'); invalidate(); }, onError: (error) => showToast('error', 'Update failed', error instanceof Error ? error.message : undefined), }); const editMutation = useMutation({ mutationFn: () => updateProject(editingProject!.id, { name: editForm.name.trim(), description: editForm.description.trim(), }), onSuccess: () => { showToast('success', 'Project updated'); setEditingProject(null); invalidate(); }, onError: (error) => showToast('error', 'Update failed', error instanceof Error ? error.message : undefined), }); const deleteMutation = useMutation({ mutationFn: (id: string) => deleteProject(id), onSuccess: () => { showToast('success', 'Project deleted'); invalidate(); }, onError: (error) => showToast('error', 'Delete failed', error instanceof Error ? error.message : undefined), }); if (isDemoMode) { return ; } if (overviewQuery.isPending) { return ; } if (overviewQuery.isError) { return (

Admin access required

This area is restricted to platform administrators.

); } const overview = overviewQuery.data; const stats = overview?.stats ?? {}; const pending = overview?.pending_projects ?? []; const users = usersQuery.data ?? []; const allProjects = projectsQuery.data ?? []; const banners = bannersQuery.data ?? []; const invites = invitesQuery.data ?? []; return (

Admin console

Platform overview, project approvals, and user management.

{Object.entries(statLabels).map(([key, label]) => (

{stats[key] ?? 0}

{label}

))}

Pending approvals

{pending.length > 0 ? ( {pending.length} ) : null}
{pending.length > 0 ? (
{pending.map((project) => (
{project.name}

{project.description || 'No description'}

))}
) : (
Nothing waiting for approval.
)}

All projects

{allProjects.map((project) => (
{project.name}

{project.stats?.service_count ?? 0} services · {project.stats?.deployment_count ?? 0} deploys

{project.isApproved ? 'public' : 'pending'}
))} {allProjects.length === 0 ? (

No projects yet.

) : null}

Announcements

setBannerForm((f) => ({ ...f, title: e.target.value }))} placeholder="Title" className="h-9 min-w-48 flex-1 rounded-[var(--radius-md)] border border-[var(--border-subtle)] bg-[var(--surface-muted)] px-3 text-sm focus:border-[var(--accent-primary)]" /> setBannerForm((f) => ({ ...f, body: e.target.value }))} placeholder="Body (optional)" className="h-9 min-w-48 flex-[2] rounded-[var(--radius-md)] border border-[var(--border-subtle)] bg-[var(--surface-muted)] px-3 text-sm focus:border-[var(--accent-primary)]" />
{banners.map((banner) => (

{banner.title}

{banner.level} {banner.body ? ` · ${banner.body}` : ''}

))} {banners.length === 0 ? (

No announcements.

) : null}

Team invites

setInviteEmail(e.target.value)} placeholder="Bind to email (optional)" type="email" className="h-9 min-w-64 flex-1 rounded-[var(--radius-md)] border border-[var(--border-subtle)] bg-[var(--surface-muted)] px-3 text-sm focus:border-[var(--accent-primary)]" />
{inviteLink ? (
{inviteLink}
) : null} {invites.map((invite) => { const used = Boolean(invite.used_at); const expired = !used && invite.expired; return (

{invite.email || 'Open invite'}

expires {new Date(invite.expires_at).toLocaleString()}

{used ? 'used' : expired ? 'expired' : 'open'} {!used ? ( ) : null}
); })} {invites.length === 0 ? (

No invites yet.

) : null}

Users

{users.map((user) => ( adminMutation.mutate({ id: user.id, isAdmin })} onImpersonate={user.is_admin || user.id === myId ? undefined : () => impersonateMutation.mutate(user.id)} /> ))} {users.length === 0 ? (

No users.

) : null}
{impersonation ? (

Impersonating {impersonation.user.email}

Token valid until {new Date(impersonation.expires_at).toLocaleString()} — audit-logged.

{impersonation.token}

Use it as a bearer token — e.g. CONTAINR_TOKEN=<token> containr <command> or Authorization: Bearer <token>. Requests run with that user's permissions for 15 minutes.

) : null} {editingProject ? (

Edit project

Admin edit — changes apply immediately.

setEditForm((f) => ({ ...f, name: e.target.value }))} className="h-10 w-full rounded-[var(--radius-md)] border border-[var(--border-subtle)] bg-[var(--surface-muted)] px-3 text-sm focus:border-[var(--accent-primary)]" />