import { useQuery } from '@tanstack/react-query'; import { useNavigate } from 'react-router-dom'; import { getCertificates, getProfiles } from '../api/client'; import PageHeader from '../components/PageHeader'; import DataTable from '../components/DataTable'; import type { Column } from '../components/DataTable'; import StatusBadge from '../components/StatusBadge'; import ErrorState from '../components/ErrorState'; import { formatDateTime, daysUntil } from '../api/utils'; import type { Certificate, CertificateProfile } from '../api/types'; function formatTTL(seconds: number): string { if (seconds < 60) return `${seconds}s`; if (seconds < 3600) return `${Math.round(seconds / 60)}m`; if (seconds < 86400) return `${Math.round(seconds / 3600)}h`; return `${Math.round(seconds / 86400)}d`; } function ttlRemaining(expiresAt: string): { text: string; color: string; seconds: number } { const diff = new Date(expiresAt).getTime() - Date.now(); const secs = Math.floor(diff / 1000); if (secs <= 0) return { text: 'Expired', color: 'text-red-600', seconds: 0 }; if (secs < 300) return { text: `${secs}s`, color: 'text-red-600', seconds: secs }; if (secs < 1800) return { text: `${Math.round(secs / 60)}m`, color: 'text-amber-600', seconds: secs }; return { text: formatTTL(secs), color: 'text-emerald-600', seconds: secs }; } export default function ShortLivedPage() { const navigate = useNavigate(); const { data: certsData, isLoading: certsLoading, error: certsError, refetch } = useQuery({ queryKey: ['certificates', {}], queryFn: () => getCertificates(), refetchInterval: 10000, // Refresh every 10s for short-lived certs }); const { data: profilesData } = useQuery({ queryKey: ['profiles'], queryFn: () => getProfiles(), }); // Build profile lookup const profileMap = new Map(); profilesData?.data?.forEach(p => profileMap.set(p.id, p)); // Filter to short-lived certificates (profile with allow_short_lived and max_ttl < 1 hour) const shortLivedProfileIds = new Set( (profilesData?.data || []) .filter(p => p.allow_short_lived && p.max_ttl_seconds > 0 && p.max_ttl_seconds < 3600) .map(p => p.id) ); // Include certs that match short-lived profiles OR certs that expire within 1 hour const allCerts = certsData?.data || []; const shortLivedCerts = allCerts.filter(c => { if (c.status === 'Archived') return false; if (shortLivedProfileIds.has(c.certificate_profile_id)) return true; // Also include any cert with < 1 hour of life remaining that is active const secsRemaining = (new Date(c.expires_at).getTime() - Date.now()) / 1000; if (secsRemaining > 0 && secsRemaining < 3600 && c.status === 'Active') return true; return false; }); // Sort by expiration (soonest first) shortLivedCerts.sort((a, b) => new Date(a.expires_at).getTime() - new Date(b.expires_at).getTime()); // Stats const active = shortLivedCerts.filter(c => c.status === 'Active' && daysUntil(c.expires_at) >= 0).length; const expired = shortLivedCerts.filter(c => c.status === 'Expired' || daysUntil(c.expires_at) < 0).length; const profiles = new Set(shortLivedCerts.map(c => c.certificate_profile_id).filter(Boolean)); const columns: Column[] = [ { key: 'name', label: 'Certificate', render: (c) => (
{c.common_name}
{c.id}
), }, { key: 'status', label: 'Status', render: (c) => }, { key: 'ttl', label: 'TTL Remaining', render: (c) => { const ttl = ttlRemaining(c.expires_at); return (
{ttl.text}
{ttl.seconds > 0 && ttl.seconds < 300 && (
)}
); }, }, { key: 'profile', label: 'Profile', render: (c) => { const profile = profileMap.get(c.certificate_profile_id); return (
{profile?.name || c.certificate_profile_id || '—'}
{profile &&
Max TTL: {formatTTL(profile.max_ttl_seconds)}
}
); }, }, { key: 'env', label: 'Environment', render: (c) => {c.environment || '—'} }, { key: 'issuer', label: 'Issuer', render: (c) => {c.issuer_id} }, { key: 'expires', label: 'Expires At', render: (c) => {formatDateTime(c.expires_at)} }, ]; return ( <> {/* Stats bar */}
Active: {active}
Expired: {expired}
Profiles: {profiles.size}
{certsError ? ( refetch()} /> ) : ( navigate(`/certificates/${c.id}`)} emptyMessage="No short-lived credentials found. Certificates with profiles that have TTL < 1 hour will appear here." /> )}
); }