import { useState } from 'react'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { useNavigate, Link } from 'react-router-dom'; import { getIssuers, getAgents, getProfiles, getOwners, getTeams, getPolicies, createIssuer, testIssuerConnection, createCertificate, triggerRenewal, createTeam, createOwner, getApiKey, } from '../api/client'; import { issuerTypes, type IssuerTypeConfig } from '../config/issuerTypes'; import ConfigForm from '../components/issuer/ConfigForm'; import type { Issuer, Agent } from '../api/types'; // ─── Types ─────────────────────────────────────────── type WizardStep = 'issuer' | 'agent' | 'certificate' | 'complete'; const STEPS: { key: WizardStep; label: string }[] = [ { key: 'issuer', label: 'Connect a CA' }, { key: 'agent', label: 'Deploy Agent' }, { key: 'certificate', label: 'Add Certificate' }, { key: 'complete', label: 'Done' }, ]; // ─── Helpers ───────────────────────────────────────── function CodeBlock({ code, label }: { code: string; label?: string }) { const [copied, setCopied] = useState(false); return (
{label &&
{label}
}
        {code}
      
); } function StepIndicator({ steps, current }: { steps: typeof STEPS; current: WizardStep }) { const currentIdx = steps.findIndex(s => s.key === current); return (
{steps.map((s, i) => { const isCompleted = i < currentIdx; const isCurrent = s.key === current; return (
{isCompleted ? ( ) : i + 1}
{i < steps.length - 1 && (
)}
); })}
); } function WizardFooter({ onSkip, onNext, nextLabel, nextDisabled, showSkip = true }: { onSkip?: () => void; onNext?: () => void; nextLabel?: string; nextDisabled?: boolean; showSkip?: boolean; }) { return (
{showSkip && onSkip && ( )}
{onNext && ( )}
); } // ─── Step 1: Connect a CA ──────────────────────────── function IssuerStep({ onNext, onSkip, onIssuerCreated }: { onNext: () => void; onSkip: () => void; onIssuerCreated: (issuer: Issuer) => void; }) { const queryClient = useQueryClient(); const [selectedType, setSelectedType] = useState(null); const [configValues, setConfigValues] = useState>({}); const [issuerName, setIssuerName] = useState(''); // Pre-populate default values when a type is selected (matches IssuersPage behavior) function handleTypeSelect(typeId: string) { setSelectedType(typeId); const tc = issuerTypes.find(t => t.id === typeId); const defaults: Record = {}; tc?.configFields.forEach(f => { if (f.defaultValue !== undefined) defaults[f.key] = f.defaultValue; }); setConfigValues(defaults); } const [error, setError] = useState(''); const [testResult, setTestResult] = useState<{ ok: boolean; msg: string } | null>(null); const [createdIssuer, setCreatedIssuer] = useState(null); const typeConfig = selectedType ? issuerTypes.find(t => t.id === selectedType) : null; const createMutation = useMutation({ mutationFn: () => createIssuer({ name: issuerName || `${typeConfig?.name || selectedType} Issuer`, type: selectedType!, config: configValues as Record, }), onSuccess: (issuer) => { setCreatedIssuer(issuer); onIssuerCreated(issuer); queryClient.invalidateQueries({ queryKey: ['issuers'] }); setError(''); }, onError: (err: Error) => setError(err.message), }); const testMutation = useMutation({ mutationFn: () => testIssuerConnection(createdIssuer!.id), onSuccess: () => setTestResult({ ok: true, msg: 'Connection successful' }), onError: (err: Error) => setTestResult({ ok: false, msg: err.message }), }); // After issuer is created successfully if (createdIssuer) { return (

CA Connected

{createdIssuer.name} ({typeConfig?.name}) created successfully
{!testResult && ( )} {testResult?.ok && (
Connection test passed.
)} {testResult && !testResult.ok && (
Connection test failed: {testResult.msg}
)}
); } // Type selection if (!selectedType) { return (

Connect a Certificate Authority

Choose a CA to issue and manage certificates. You can add more later from the Issuers page.

{issuerTypes.filter(t => !t.comingSoon).map((type: IssuerTypeConfig) => ( ))}
); } // Config form for selected type const requiredFields = typeConfig?.configFields.filter(f => f.required) || []; const allRequiredFilled = requiredFields.every(f => configValues[f.key]); return (

Configure {typeConfig?.name}

{typeConfig?.description}

setIssuerName(e.target.value)} placeholder={`${typeConfig?.name || ''} Issuer`} className="w-full px-3 py-2 bg-surface border border-surface-border rounded text-ink placeholder-ink-faint focus:outline-none focus:border-brand-500 transition-colors" />
setConfigValues(prev => ({ ...prev, [key]: val }))} /> {error && (
{error}
)} createMutation.mutate()} nextLabel={createMutation.isPending ? 'Creating...' : 'Create Issuer'} nextDisabled={!allRequiredFilled || createMutation.isPending} />
); } // ─── Step 2: Deploy an Agent ───────────────────────── function AgentStep({ onNext, onSkip }: { onNext: () => void; onSkip: () => void }) { const [activeTab, setActiveTab] = useState<'linux' | 'macos' | 'docker'>('linux'); const apiKey = getApiKey() || ''; const serverUrl = typeof window !== 'undefined' ? `${window.location.protocol}//${window.location.hostname}:8443` : 'http://localhost:8443'; // Poll for agents every 5s const { data: agents } = useQuery({ queryKey: ['agents'], queryFn: () => getAgents(), refetchInterval: 5000, }); const agentList = agents?.data || []; const hasAgents = agentList.length > 0; const tabs = [ { key: 'linux' as const, label: 'Linux' }, { key: 'macos' as const, label: 'macOS' }, { key: 'docker' as const, label: 'Docker' }, ]; const commands: Record = { linux: { label: 'Install via shell script (systemd service)', code: `# Non-interactive install (recommended for curl | bash): curl -sSL https://raw.githubusercontent.com/shankar0123/certctl/master/install-agent.sh \\ | sudo bash -s -- \\ --server-url ${serverUrl} \\ --api-key ${apiKey} # The script downloads the agent binary, writes /etc/certctl/agent.env, # installs /etc/systemd/system/certctl-agent.service, and starts it. # Check status with: sudo systemctl status certctl-agent`, }, macos: { label: 'Install via shell script (launchd service)', code: `# Non-interactive install (recommended for curl | bash): curl -sSL https://raw.githubusercontent.com/shankar0123/certctl/master/install-agent.sh \\ | bash -s -- \\ --server-url ${serverUrl} \\ --api-key ${apiKey} # The script writes ~/.certctl/agent.env and loads # ~/Library/LaunchAgents/com.certctl.agent.plist. # Check status with: launchctl list | grep certctl`, }, docker: { label: 'Run as Docker container', code: `docker run -d --name certctl-agent \\ -e CERTCTL_SERVER_URL=${serverUrl} \\ -e CERTCTL_API_KEY=${apiKey} \\ ghcr.io/shankar0123/certctl-agent:latest`, }, }; return (

Deploy a certctl Agent

Agents run on your infrastructure to manage certificates, generate keys, and deploy to targets. Install one now or skip to do it later.

{/* OS Tabs */}
{tabs.map(t => ( ))}
{/* Agent detection */}
{hasAgents ? ( <>
{agentList.length} agent{agentList.length !== 1 ? 's' : ''} detected
{agentList.slice(0, 3).map(a => a.name || a.id).join(', ')} {agentList.length > 3 && ` and ${agentList.length - 3} more`}
) : ( <>
Waiting for an agent to connect... (polling every 5s)
)}
); } // ─── Step 3 helpers: inline team + owner creation ─── // Inline CreateTeamModal — mirrors TeamsPage.tsx CreateTeamModal pattern. // Used inside CertificateStep so users can create a team without leaving the wizard. function CreateTeamModalInline({ isOpen, onClose, onCreated }: { isOpen: boolean; onClose: () => void; onCreated: (teamId: string) => void; }) { const queryClient = useQueryClient(); const [name, setName] = useState(''); const [description, setDescription] = useState(''); const [error, setError] = useState(''); const mutation = useMutation({ mutationFn: () => createTeam({ name: name.trim(), description: description.trim() }), onSuccess: (team) => { queryClient.invalidateQueries({ queryKey: ['teams'] }); setName(''); setDescription(''); setError(''); onCreated(team.id); onClose(); }, onError: (err: Error) => setError(err.message), }); if (!isOpen) return null; return (
e.stopPropagation()}>

Create Team

{error &&
{error}
}
{ e.preventDefault(); if (!name.trim()) return; mutation.mutate(); }} className="space-y-4">
setName(e.target.value)} placeholder="Platform Engineering" autoFocus className="w-full px-3 py-2 bg-surface border border-surface-border rounded text-ink placeholder-ink-faint focus:outline-none focus:border-brand-500 transition-colors" />