Files
the-other-dude/frontend/src/components/certificates/CertConfirmDialog.tsx
Jason Staack b840047e19 feat: The Other Dude v9.0.1 — full-featured email system
ci: add GitHub Pages deployment workflow for docs site

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-08 19:30:44 -05:00

147 lines
4.7 KiB
TypeScript

/**
* CertConfirmDialog -- Confirmation dialog for certificate operations.
*
* - Rotate: Standard confirmation with consequence text.
* - Revoke: Type-to-confirm (must type hostname), destructive red styling.
*
* Uses the project's existing Dialog primitives (Radix react-dialog).
*/
import { useState, useEffect } from 'react'
import { AlertTriangle, RefreshCw, XCircle } from 'lucide-react'
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogDescription,
DialogFooter,
} from '@/components/ui/dialog'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
interface CertConfirmDialogProps {
open: boolean
onOpenChange: (open: boolean) => void
action: 'rotate' | 'revoke'
deviceHostname: string
onConfirm: () => void
}
export function CertConfirmDialog({
open,
onOpenChange,
action,
deviceHostname,
onConfirm,
}: CertConfirmDialogProps) {
const [confirmText, setConfirmText] = useState('')
const isRevoke = action === 'revoke'
const canConfirm = isRevoke ? confirmText === deviceHostname : true
// Reset confirm text when dialog opens/closes or action changes
useEffect(() => {
if (open) {
setConfirmText('')
}
}, [open, action])
const handleConfirm = () => {
if (!canConfirm) return
onConfirm()
}
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-md">
<DialogHeader>
<div className="flex items-center gap-3 mb-1">
<div
className={`flex h-10 w-10 items-center justify-center rounded-lg ${
isRevoke ? 'bg-error/10' : 'bg-amber-500/10'
}`}
>
{isRevoke ? (
<XCircle className="h-5 w-5 text-error" />
) : (
<RefreshCw className="h-5 w-5 text-amber-500" />
)}
</div>
<DialogTitle className="text-lg">
{isRevoke ? 'Revoke Certificate' : 'Rotate Certificate'}
</DialogTitle>
</div>
<DialogDescription>
{isRevoke
? `This will permanently revoke the certificate for ${deviceHostname}. The device will fall back to insecure TLS mode.`
: `This will generate a new certificate for ${deviceHostname}. The old certificate will be superseded.`}
</DialogDescription>
</DialogHeader>
<div className="space-y-4 my-2">
{/* Warning callout */}
<div
className={`flex items-start gap-3 rounded-md p-3 ${
isRevoke
? 'bg-error/10 border border-error/30'
: 'bg-amber-500/10 border border-amber-500/30'
}`}
>
<AlertTriangle
className={`h-4 w-4 mt-0.5 shrink-0 ${
isRevoke ? 'text-error' : 'text-amber-500'
}`}
/>
<p className="text-xs text-text-secondary leading-relaxed">
{isRevoke
? 'This action cannot be undone. The device will lose its verified TLS certificate and revert to self-signed mode until a new certificate is deployed.'
: 'The current certificate will be marked as superseded. A new certificate will be signed and deployed to the device.'}
</p>
</div>
{/* Type-to-confirm for revoke */}
{isRevoke && (
<div className="space-y-1.5">
<Label htmlFor="confirm-hostname" className="text-sm">
Type <span className="font-mono font-semibold text-text-primary">{deviceHostname}</span> to confirm
</Label>
<Input
id="confirm-hostname"
value={confirmText}
onChange={(e) => setConfirmText(e.target.value)}
placeholder={deviceHostname}
autoComplete="off"
autoFocus
/>
</div>
)}
</div>
<DialogFooter>
<Button variant="outline" onClick={() => onOpenChange(false)}>
Cancel
</Button>
<Button
variant={isRevoke ? 'destructive' : 'default'}
onClick={handleConfirm}
disabled={!canConfirm}
>
{isRevoke ? (
<>
<XCircle className="h-4 w-4 mr-1.5" />
Revoke Certificate
</>
) : (
<>
<RefreshCw className="h-4 w-4 mr-1.5" />
Rotate Certificate
</>
)}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
)
}