import { useEffect, useMemo, useState } from 'react'; import { useQuery } from '@tanstack/react-query'; import { useLocation, useSearchParams } from 'react-router-dom'; import { getAdminSCEPIntuneStats, getAdminSCEPProfiles, reloadAdminSCEPIntuneTrust, getAuditEvents, } from '../api/client'; import PageHeader from '../components/PageHeader'; import ErrorState from '../components/ErrorState'; import { useAuth } from '../components/AuthProvider'; import { useTrackedMutation } from '../hooks/useTrackedMutation'; import { formatDateTime } from '../api/utils'; import type { IntuneStatsSnapshot, IntuneTrustAnchorInfo, AuditEvent, SCEPProfileStatsSnapshot, } from '../api/types'; // SCEP RFC 8894 + Intune master bundle Phase 9 follow-up // (the project's SCEP GUI restructure spec): per-profile SCEP // administration page with three tabs. // // Profiles (default) — every configured SCEP profile, lean card per // profile with always-present fields (RA cert // expiry, mTLS sibling-route status, // challenge-password-set indicator). Cards on // Intune-enabled profiles get a "View Intune // details →" link that deep-links to the // Intune tab filtered to that profile. // Intune Monitoring — the existing Phase 9.4 deep-dive. Per-profile // counters (success / signature_invalid / // claim_mismatch / expired / wrong_audience / // replay / rate_limited / malformed / // compliance_failed / not_yet_valid / // unknown_version), trust anchor expiry // countdown, recent failures table, reload- // trust button + confirmation modal. Polled // every 30s via TanStack Query. // Recent Activity — full SCEP audit log filter covering all four // action codes (scep_pkcsreq, scep_renewalreq, // scep_pkcsreq_intune, scep_renewalreq_intune). // Merged + sorted descending by timestamp. // Filter chips for All / Initial / Renewal / // Intune / Static. Polled every 60s. // // Admin-gated: the page itself renders an "Admin access required" banner // for non-admin callers and never issues the underlying admin requests. // Server-side enforcement is the M-008 admin gate; this is a UX hint. const COUNTER_LABEL_ORDER = [ 'success', 'signature_invalid', 'expired', 'not_yet_valid', 'wrong_audience', 'replay', 'rate_limited', 'claim_mismatch', 'compliance_failed', 'malformed', 'unknown_version', ] as const; const COUNTER_PRESENTATION: Record = { success: { label: 'Success', tone: 'good' }, signature_invalid: { label: 'Signature invalid', tone: 'bad' }, expired: { label: 'Expired', tone: 'warn' }, not_yet_valid: { label: 'Not yet valid', tone: 'warn' }, wrong_audience: { label: 'Wrong audience', tone: 'bad' }, replay: { label: 'Replay', tone: 'bad' }, rate_limited: { label: 'Rate-limited', tone: 'warn' }, claim_mismatch: { label: 'Claim mismatch', tone: 'bad' }, compliance_failed: { label: 'Compliance failed', tone: 'warn' }, malformed: { label: 'Malformed', tone: 'bad' }, unknown_version: { label: 'Unknown version', tone: 'warn' }, }; const TONE_CLASS: Record<'good' | 'warn' | 'bad', string> = { good: 'text-emerald-600', warn: 'text-amber-600', bad: 'text-red-600', }; type TabId = 'profiles' | 'intune' | 'activity'; type ActivityFilter = 'all' | 'initial' | 'renewal' | 'intune' | 'static'; const TAB_LABELS: Record = { profiles: 'Profiles', intune: 'Intune Monitoring', activity: 'Recent Activity', }; const SCEP_AUDIT_ACTIONS = [ 'scep_pkcsreq', 'scep_renewalreq', 'scep_pkcsreq_intune', 'scep_renewalreq_intune', ] as const; // ============================================================================= // Tone + badge helpers (shared across tabs). // ============================================================================= function expiryBadge(days: number | null, expired: boolean): { text: string; tone: 'good' | 'warn' | 'bad' } { if (expired) return { text: 'EXPIRED', tone: 'bad' }; if (days === null) return { text: 'Not loaded', tone: 'warn' }; if (days < 7) return { text: `${days}d remaining`, tone: 'bad' }; if (days < 30) return { text: `${days}d remaining (rotate soon)`, tone: 'warn' }; return { text: `${days}d remaining`, tone: 'good' }; } function badgeClass(tone: 'good' | 'warn' | 'bad'): string { if (tone === 'good') return 'bg-emerald-100 text-emerald-800'; if (tone === 'warn') return 'bg-amber-100 text-amber-800'; return 'bg-red-100 text-red-800'; } function pillClass(active: boolean): string { return active ? 'bg-brand-100 text-brand-800 border-brand-300' : 'bg-surface-alt text-ink-muted border-surface-border'; } // soonestExpiryDays returns the smallest days_to_expiry across the // profile's Intune trust anchor pool. Returns null when the pool is // empty (the per-profile preflight should have refused this state at // boot, but defensive in case the holder is reloaded mid-flight to an // empty file). function soonestExpiryDays(anchors?: IntuneTrustAnchorInfo[]): number | null { if (!anchors || anchors.length === 0) return null; let min = Number.POSITIVE_INFINITY; for (const a of anchors) { if (a.expired) return -1; if (a.days_to_expiry < min) min = a.days_to_expiry; } return min === Number.POSITIVE_INFINITY ? null : min; } // ============================================================================= // Profiles tab — per-profile lean card with always-present fields. // ============================================================================= interface ProfilesTabProps { profiles: SCEPProfileStatsSnapshot[]; isLoading: boolean; onViewIntuneDetails: (pathID: string) => void; } function ProfilesTab({ profiles, isLoading, onViewIntuneDetails }: ProfilesTabProps) { if (isLoading) { return

Loading profiles…

; } if (profiles.length === 0) { return (
No SCEP profiles are configured. Set CERTCTL_SCEP_ENABLED=true and either the legacy single-profile env vars or CERTCTL_SCEP_PROFILES=... with the indexed per-profile family to register at least one endpoint.
); } return ( <> {profiles.map(p => ( ))} ); } interface ProfileSummaryCardProps { profile: SCEPProfileStatsSnapshot; onViewIntuneDetails: (pathID: string) => void; } function ProfileSummaryCard({ profile, onViewIntuneDetails }: ProfileSummaryCardProps) { const pathLabel = profile.path_id || '(legacy /scep root)'; const intuneEnabled = !!profile.intune; const raBadge = expiryBadge( profile.ra_cert_subject ? profile.ra_cert_days_to_expiry : null, profile.ra_cert_expired, ); return (

{pathLabel}

Issuer: {profile.issuer_id}

RA cert: {raBadge.text}
Challenge password{profile.challenge_password_set ? ' set' : ' MISSING'} mTLS {profile.mtls_enabled ? 'enabled' : 'disabled'} Intune {intuneEnabled ? 'enabled' : 'disabled'}
RA cert subject
{profile.ra_cert_subject || '(not loaded)'}
{profile.ra_cert_not_after && (
RA cert expires
{formatDateTime(profile.ra_cert_not_after)}
)} {profile.mtls_enabled && profile.mtls_trust_bundle_path && (
mTLS trust bundle
{profile.mtls_trust_bundle_path}
)}
{intuneEnabled && (
)}
); } // ============================================================================= // Intune Monitoring tab — the existing Phase 9.4 deep-dive surface. // ============================================================================= interface ConfirmReloadModalProps { profile: IntuneStatsSnapshot; onCancel: () => void; onConfirm: () => void; pending: boolean; errorMessage?: string; } function ConfirmReloadModal({ profile, onCancel, onConfirm, pending, errorMessage }: ConfirmReloadModalProps) { const pathLabel = profile.path_id || '(legacy /scep root)'; return (

Reload Intune trust anchor

This re-reads {profile.trust_anchor_path} from disk and atomically swaps the trust pool for SCEP profile {pathLabel}. Equivalent to sending SIGHUP to the server. If the new file fails to parse, the previous trust pool stays in place — enrollments keep working off the old trust anchor while you fix the file.

{errorMessage && (
{errorMessage}
)}
); } interface IntuneTabProps { profiles: IntuneStatsSnapshot[]; isLoading: boolean; onRequestReload: (profile: IntuneStatsSnapshot) => void; highlightPathID: string | null; events: AuditEvent[]; eventsLoading: boolean; } function IntuneTab({ profiles, isLoading, onRequestReload, highlightPathID, events, eventsLoading }: IntuneTabProps) { if (isLoading) { return

Loading Intune monitoring data…

; } const intuneProfiles = profiles.filter(p => p.enabled); return ( <> {intuneProfiles.length === 0 && (
No SCEP profile has Intune enabled. Set CERTCTL_SCEP_PROFILE_<NAME>_INTUNE_ENABLED=true plus the matching trust-anchor path env var, then restart the server.
)} {intuneProfiles.map(p => ( ))}

Recent Intune-dispatched enrollments (last 50)

Filtered to action=scep_pkcsreq_intune + action=scep_renewalreq_intune. Refreshes every 60s.

{eventsLoading ? (

Loading audit log…

) : ( )}
); } interface IntuneProfileCardProps { profile: IntuneStatsSnapshot; onRequestReload: (profile: IntuneStatsSnapshot) => void; highlighted: boolean; } function IntuneProfileCard({ profile, onRequestReload, highlighted }: IntuneProfileCardProps) { const pathLabel = profile.path_id || '(legacy /scep root)'; const days = soonestExpiryDays(profile.trust_anchors); const badge = expiryBadge(days, days !== null && days < 0); const cardClass = highlighted ? 'bg-surface border-2 border-brand-400 rounded-lg p-5 mb-4 shadow-sm' : 'bg-surface border border-surface-border rounded-lg p-5 mb-4'; return (

{pathLabel}

Issuer: {profile.issuer_id} {profile.audience && <> · Audience: {profile.audience}}

Trust anchor: {badge.text}
{COUNTER_LABEL_ORDER.map(label => { const value = profile.counters?.[label] ?? 0; const presentation = COUNTER_PRESENTATION[label]; return (
{value}
{presentation.label}
); })}
Replay cache size
{profile.replay_cache_size}
Per-device rate limit
{profile.rate_limit_disabled ? 'Disabled' : 'Active'}
Trust anchors
{profile.trust_anchors?.length ?? 0}
{profile.trust_anchors && profile.trust_anchors.length > 0 && (
Trust anchor details {profile.trust_anchors.map(a => ( ))}
Subject Not after Days to expiry
{a.subject || '(empty CN)'} {formatDateTime(a.not_after)} {a.expired ? 'EXPIRED' : a.days_to_expiry}
)}
); } // ============================================================================= // Recent Activity tab — full SCEP audit log filter. // ============================================================================= interface ActivityTabProps { events: AuditEvent[]; isLoading: boolean; filter: ActivityFilter; setFilter: (f: ActivityFilter) => void; } function activityFilterMatches(filter: ActivityFilter, action: string): boolean { switch (filter) { case 'all': return true; case 'initial': return action === 'scep_pkcsreq' || action === 'scep_pkcsreq_intune'; case 'renewal': return action === 'scep_renewalreq' || action === 'scep_renewalreq_intune'; case 'intune': return action === 'scep_pkcsreq_intune' || action === 'scep_renewalreq_intune'; case 'static': return action === 'scep_pkcsreq' || action === 'scep_renewalreq'; } } function ActivityTab({ events, isLoading, filter, setFilter }: ActivityTabProps) { const filtered = events.filter(e => activityFilterMatches(filter, e.action)); return (

SCEP enrollment audit log (last 100)

Merged across scep_pkcsreq + scep_renewalreq + scep_pkcsreq_intune + scep_renewalreq_intune. Refreshes every 60s.

{(['all', 'initial', 'renewal', 'intune', 'static'] as const).map(f => ( ))}
{isLoading ? (

Loading audit log…

) : ( )}
); } // ============================================================================= // Shared events table. // ============================================================================= interface RecentEventsTableProps { events: AuditEvent[]; testID: string; emptyMessage: string; } function RecentEventsTable({ events, testID, emptyMessage }: RecentEventsTableProps) { if (events.length === 0) { return

{emptyMessage}

; } return ( {events.map(e => ( ))}
Timestamp Action Resource Details
{formatDateTime(e.timestamp)} {e.action} {e.resource_type} · {e.resource_id} {e.details ? Object.entries(e.details).map(([k, v]) => `${k}=${typeof v === 'object' ? JSON.stringify(v) : String(v)}`).join(' · ') : '-'}
); } // ============================================================================= // Top-level page. // ============================================================================= // pickInitialTab honors three signals (precedence high → low): // 1. ?tab=intune|activity in the query string (deep link) // 2. Pathname ending in /scep/intune (legacy route alias from // Phase 9.4; preserved so external bookmarks land on Intune) // 3. Default to 'profiles' function pickInitialTab(searchParams: URLSearchParams, pathname: string): TabId { const fromQuery = searchParams.get('tab'); if (fromQuery === 'intune' || fromQuery === 'activity') return fromQuery; if (pathname.endsWith('/scep/intune')) return 'intune'; return 'profiles'; } export default function SCEPAdminPage() { const auth = useAuth(); const adminAccess = !auth.authRequired || auth.admin; const [searchParams, setSearchParams] = useSearchParams(); const location = useLocation(); const [activeTab, setActiveTab] = useState(() => pickInitialTab(searchParams, location.pathname)); const [highlightPathID, setHighlightPathID] = useState(searchParams.get('profile')); const [reloadTarget, setReloadTarget] = useState(null); const [reloadError, setReloadError] = useState(undefined); const [activityFilter, setActivityFilter] = useState('all'); // Keep URL in sync with tab + highlighted profile so deep links survive // page reloads + browser back/forward. useEffect(() => { const next = new URLSearchParams(searchParams); if (activeTab === 'profiles') { next.delete('tab'); } else { next.set('tab', activeTab); } if (highlightPathID && activeTab === 'intune') { next.set('profile', highlightPathID); } else { next.delete('profile'); } if (next.toString() !== searchParams.toString()) { setSearchParams(next, { replace: true }); } }, [activeTab, highlightPathID, searchParams, setSearchParams]); // Always-present per-profile data (Profiles tab). const profilesQuery = useQuery({ queryKey: ['admin', 'scep', 'profiles'], queryFn: getAdminSCEPProfiles, enabled: adminAccess, refetchInterval: 30_000, }); // Intune deep-dive data (Intune tab). const intuneStatsQuery = useQuery({ queryKey: ['admin', 'scep', 'intune', 'stats'], queryFn: getAdminSCEPIntuneStats, enabled: adminAccess && activeTab === 'intune', refetchInterval: 30_000, }); // Audit log queries — four parallel queries (one per SCEP action) so // both the Intune tab's recent-failures table and the Activity tab's // full SCEP audit feed can pull from the same React Query cache. const auditQueries = SCEP_AUDIT_ACTIONS.map(action => // eslint-disable-next-line react-hooks/rules-of-hooks useQuery({ queryKey: ['audit', { action }], queryFn: () => getAuditEvents({ action }), enabled: adminAccess && (activeTab === 'intune' || activeTab === 'activity'), refetchInterval: 60_000, }), ); const allAuditEvents: AuditEvent[] = useMemo(() => { const merged: AuditEvent[] = []; for (const q of auditQueries) { if (q.data?.data) merged.push(...q.data.data); } return merged.sort((a, b) => b.timestamp.localeCompare(a.timestamp)); // eslint-disable-next-line react-hooks/exhaustive-deps }, [auditQueries.map(q => q.dataUpdatedAt).join('|')]); const auditLoading = auditQueries.some(q => q.isLoading); const intuneOnlyEvents = useMemo( () => allAuditEvents.filter( e => e.action === 'scep_pkcsreq_intune' || e.action === 'scep_renewalreq_intune', ), [allAuditEvents], ); const reloadMutation = useTrackedMutation< Awaited>, Error, string >({ mutationFn: (pathID: string) => reloadAdminSCEPIntuneTrust(pathID), invalidates: [ ['admin', 'scep', 'intune', 'stats'], ['admin', 'scep', 'profiles'], ], onSuccess: () => { setReloadTarget(null); setReloadError(undefined); }, onError: (err: Error) => { setReloadError(err.message); }, }); if (auth.authRequired && !auth.admin) { return ( <>
); } const profiles = profilesQuery.data?.profiles ?? []; const intuneProfiles = intuneStatsQuery.data?.profiles ?? []; const handleViewIntuneDetails = (pathID: string) => { setHighlightPathID(pathID); setActiveTab('intune'); }; return ( <> { void profilesQuery.refetch(); if (activeTab === 'intune') void intuneStatsQuery.refetch(); }} className="text-xs px-3 py-1.5 rounded border border-surface-border bg-surface hover:bg-surface-alt" data-testid="refresh-stats-button" > Refresh now } />
{profilesQuery.error && activeTab === 'profiles' && ( profilesQuery.refetch()} /> )} {intuneStatsQuery.error && activeTab === 'intune' && ( intuneStatsQuery.refetch()} /> )} {activeTab === 'profiles' && !profilesQuery.error && ( )} {activeTab === 'intune' && !intuneStatsQuery.error && ( { setReloadError(undefined); setReloadTarget(profile); }} highlightPathID={highlightPathID} events={intuneOnlyEvents} eventsLoading={auditLoading} /> )} {activeTab === 'activity' && ( )}
{reloadTarget && ( { setReloadTarget(null); setReloadError(undefined); }} onConfirm={() => reloadMutation.mutate(reloadTarget.path_id)} pending={reloadMutation.isPending} errorMessage={reloadError} /> )} ); }