From 7348ba916d822373d3ab5cadf596293695919ada Mon Sep 17 00:00:00 2001 From: James Bhattarai Date: Tue, 3 Mar 2026 18:02:21 +0545 Subject: [PATCH 1/4] Chore(Feat): Refractor CA Code + CA host Sign via web --- src/lib/api.ts | 19 + src/pages/org/CAsPage.tsx | 776 ++++++------------------ src/pages/org/ca/CADetailCard.tsx | 236 +++++++ src/pages/org/ca/CADialogs.tsx | 468 ++++++++++++++ src/pages/org/ca/CASection.tsx | 161 +++++ src/pages/org/ca/CopyButton.tsx | 40 ++ src/pages/org/ca/IssueHostCertPanel.tsx | 344 +++++++++++ src/pages/org/ca/utils.ts | 32 + 8 files changed, 1481 insertions(+), 595 deletions(-) create mode 100644 src/pages/org/ca/CADetailCard.tsx create mode 100644 src/pages/org/ca/CADialogs.tsx create mode 100644 src/pages/org/ca/CASection.tsx create mode 100644 src/pages/org/ca/CopyButton.tsx create mode 100644 src/pages/org/ca/IssueHostCertPanel.tsx create mode 100644 src/pages/org/ca/utils.ts diff --git a/src/lib/api.ts b/src/lib/api.ts index 08a2c8d..18c6083 100644 --- a/src/lib/api.ts +++ b/src/lib/api.ts @@ -1098,6 +1098,25 @@ export const api = { body: JSON.stringify({ key_id, principals, cert_type, expiry_hours }), }, true, requestConfig), + // Issue a host certificate by submitting a raw server host public key + // (admin-only; does not require a pre-registered SSHKey record) + signHostCert: ( + hostPublicKey: string, + principals: string[], + validityHours: number, + caId: string, + requestConfig?: RequestConfig, + ) => + request('/ssh/sign/host', { + method: 'POST', + body: JSON.stringify({ + host_public_key: hostPublicKey, + principals, + validity_hours: validityHours, + ca_id: caId, + }), + }, true, requestConfig), + // Get the merged department certificate policy for the current user (used in sign dialog) getMyDeptCertPolicy: (requestConfig?: RequestConfig) => request<{ policy: DeptCertPolicy }>('/ssh/dept-cert-policy', {}, true, requestConfig), diff --git a/src/pages/org/CAsPage.tsx b/src/pages/org/CAsPage.tsx index ddfe44f..fbbfc89 100644 --- a/src/pages/org/CAsPage.tsx +++ b/src/pages/org/CAsPage.tsx @@ -1,298 +1,23 @@ +// ─── THIS FILE IS THE LEAN ORCHESTRATOR ────────────────────────────────────── +// Heavy sub-components live in ./ca/ — edit them there for isolated changes. +// ───────────────────────────────────────────────────────────────────────────── import { useState, useEffect } from "react"; -import { - Shield, - ShieldAlert, - Copy, - CheckCircle, - Loader2, - Terminal, - Plus, - User, - Server, - Settings, - AlertCircle, - ServerCog, - RefreshCw, - ShieldOff, -} from "lucide-react"; -import { Button } from "@/components/ui/button"; -import { Input } from "@/components/ui/input"; -import { Label } from "@/components/ui/label"; -import { Textarea } from "@/components/ui/textarea"; +import { Loader2, Server, Shield, User } from "lucide-react"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Badge } from "@/components/ui/badge"; -import { - Card, - CardContent, - CardDescription, - CardHeader, - CardTitle, -} from "@/components/ui/card"; -import { - Dialog, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogTitle, -} from "@/components/ui/dialog"; -import { - AlertDialog, - AlertDialogAction, - AlertDialogCancel, - AlertDialogContent, - AlertDialogDescription, - AlertDialogFooter, - AlertDialogHeader, - AlertDialogTitle, -} from "@/components/ui/alert-dialog"; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "@/components/ui/select"; import { useToast } from "@/hooks/use-toast"; import { useParams } from "react-router-dom"; import { useCurrentOrganizationId } from "@/hooks/useCurrentOrganization"; import { api, OrgCA, ApiError } from "@/lib/api"; - -function CopyButton({ text }: { text: string }) { - const [copied, setCopied] = useState(false); - const { toast } = useToast(); - const handleCopy = async () => { - try { - await navigator.clipboard.writeText(text); - setCopied(true); - toast({ title: "Copied to clipboard" }); - setTimeout(() => setCopied(false), 2000); - } catch { - toast({ variant: "destructive", title: "Copy failed" }); - } - }; - return ( - - ); -} - -function formatDate(d: string | null) { - if (!d) return "—"; - return new Date(d).toLocaleDateString(undefined, { year: "numeric", month: "short", day: "numeric" }); -} - -// ─── CA Detail Card ─────────────────────────────────────────────────────────── - -interface CADetailCardProps { - ca: OrgCA; - onEdit: (ca: OrgCA) => void; - onRotate: (ca: OrgCA) => void; - onDelete: (ca: OrgCA) => void; -} - -function CADetailCard({ ca, onEdit, onRotate, onDelete }: CADetailCardProps) { - const isUser = ca.ca_type === "user"; - const isSystem = !!ca.is_system; - const sshConfig = isUser - ? `# /etc/ssh/sshd_config:\nTrustedUserCAKeys /etc/ssh/trusted_user_ca_keys\n\n# Add public key:\necho '${ca.public_key.trim()}' \\\n >> /etc/ssh/trusted_user_ca_keys` - : `# /etc/ssh/sshd_config:\nHostCertificate /etc/ssh/ssh_host_ed25519_key-cert.pub\n\n# Add to known_hosts (clients):\n@cert-authority * ${ca.public_key.trim()}`; - - return ( -
- - -
-
- - {isSystem ? : isUser ? : } - {ca.name} - {isSystem ? ( - - - System - - ) : ca.is_active ? ( - Active - ) : ( - Inactive - )} - - {ca.description && ( - {ca.description} - )} -
- {ca.key_type} -
-
- - {/* Stats — hidden for system CAs (we have no cert records for them) */} - {!isSystem && ( -
-
-

{ca.active_certs}

-

Active certs

-
-
-

{ca.total_certs}

-

Total issued

-
-
-

{ca.default_cert_validity_hours}h

-

Default validity

-
-
-

{ca.next_serial_number ?? '—'}

-

Next serial

-
-
- )} - - {/* Fingerprint */} -
-

Fingerprint

- {ca.fingerprint} -
- - {/* Public key */} -
-
-

Public key

- -
-