Secrets Rotation Scheduler
blocksecurityAWS Secrets Manager style automated credential rotation scheduler and security key audit with lifecycle telemetry cards, zero-downtime dual-version rotation workflow, rotation lambda log inspection, and schedule configuration dialogs.
Installation
$pnpm dlx shadcn@latest add https://uipkge.dev/r/react/secrets-rotation-scheduler.json$npx shadcn@latest add https://uipkge.dev/r/react/secrets-rotation-scheduler.json$yarn dlx shadcn@latest add https://uipkge.dev/r/react/secrets-rotation-scheduler.json$bunx shadcn@latest add https://uipkge.dev/r/react/secrets-rotation-scheduler.jsonNamed registry:
npx shadcn@latest add @uipkge-react/secrets-rotation-schedulerInstalls to:components/blocks/Variants
Loading interactive previews…
Loading interactive previews…
Props
| Name | Type / Values | Default | Required |
|---|---|---|---|
initialSecrets | ManagedSecret[] | — | optional |
initialStats | SecretsRotationStats | — | optional |
className | string | — | optional |
Schema
Type aliases exported from this item's source. Use these to shape the data you pass in.
RotationScheduleinterface RotationSchedule {
interval: string
daysInterval: number
lambdaArn?: string
zeroDowntimeVerified: boolean
}ManagedSecretinterface ManagedSecret {
id: string
name: string
arn: string
type: SecretType
schedule: RotationSchedule
lastRotated: string
nextRotationDue: string
dueRelative: string
status: RotationStatus
currentVersion: string
stagingVersion?: string
lastLogSnippet?: string
}SecretsRotationStatsinterface SecretsRotationStats {
totalSecrets: number
autoRotationActive: number
autoRotationPercentage: number
dueSoonCount: number
overdueCount: number
}npm dependencies
Files installed (1)
components/blocks/SecretsRotationScheduler.tsx44.8 kB
'use client' import * as React from 'react' import { AlertTriangle, Check, CheckCircle2, Clock, Copy, Database, Globe, Info, KeyRound, Lock, MoreHorizontal, Pause, Pencil, Play, Plus, RefreshCw, Search, Server, ShieldCheck, Terminal, X, } from 'lucide-react' import { cn } from '@/lib/utils' import { Badge } from '@/components/ui/badge' import { Button } from '@/components/ui/button' import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from '@/components/ui/dialog' import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger, } from '@/components/ui/dropdown-menu' import { Input } from '@/components/ui/input' import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select' import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table' export type SecretType = 'Postgres DB' | 'API Key' | 'RSA Keypair' | 'KMS Key' | 'Redis Auth' | 'MySQL DB' export type RotationStatus = 'healthy' | 'due-soon' | 'manual-needed' | 'paused' | 'rotating' export interface RotationSchedule { interval: string daysInterval: number lambdaArn?: string zeroDowntimeVerified: boolean } export interface ManagedSecret { id: string name: string arn: string type: SecretType schedule: RotationSchedule lastRotated: string nextRotationDue: string dueRelative: string status: RotationStatus currentVersion: string stagingVersion?: string lastLogSnippet?: string } export interface SecretsRotationStats { totalSecrets: number autoRotationActive: number autoRotationPercentage: number dueSoonCount: number overdueCount: number } export interface SecretsRotationSchedulerProps { initialSecrets?: ManagedSecret[] initialStats?: SecretsRotationStats className?: string } const defaultSecrets: ManagedSecret[] = [ { id: 'sec-1', name: 'prod/postgres/master-credentials', arn: 'arn:aws:secretsmanager:us-east-1:182938491029:secret:prod/postgres/master-cred-9aF1x', type: 'Postgres DB', schedule: { interval: 'Every 30 Days', daysInterval: 30, lambdaArn: 'arn:aws:lambda:us-east-1:182938491029:function:RotatePostgresMaster', zeroDowntimeVerified: true, }, lastRotated: 'Aug 10, 2026', nextRotationDue: 'Sep 09, 2026', dueRelative: 'in 19 days', status: 'healthy', currentVersion: 'v2.4 (AWSCURRENT)', stagingVersion: 'v2.5 (AWSPENDING)', lastLogSnippet: 'Postgres dual-user handshake verified. Master role privileges synchronized with 0 connection terminations.', }, { id: 'sec-2', name: 'prod/stripe/webhook-secret', arn: 'arn:aws:secretsmanager:us-east-1:182938491029:secret:prod/stripe/wh-sec-48k2p', type: 'API Key', schedule: { interval: 'Every 90 Days', daysInterval: 90, lambdaArn: 'arn:aws:lambda:us-east-1:182938491029:function:RotateStripeWebhook', zeroDowntimeVerified: true, }, lastRotated: 'Jul 14, 2026', nextRotationDue: 'Aug 22, 2026', dueRelative: 'in 24 hours', status: 'due-soon', currentVersion: 'v1.8 (AWSCURRENT)', stagingVersion: 'v1.9 (AWSPENDING)', lastLogSnippet: 'Dual webhook signature validation window active. Webhook endpoint test passed with 200 OK.', }, { id: 'sec-3', name: 'prod/jwt/signing-key-rsa', arn: 'arn:aws:secretsmanager:us-east-1:182938491029:secret:prod/jwt/rsa-pair-71b3e', type: 'RSA Keypair', schedule: { interval: 'Every 7 Days', daysInterval: 7, lambdaArn: 'arn:aws:lambda:us-east-1:182938491029:function:RotateJWTRSAKeys', zeroDowntimeVerified: true, }, lastRotated: 'Aug 18, 2026', nextRotationDue: 'Aug 25, 2026', dueRelative: 'in 4 days', status: 'healthy', currentVersion: 'v9.1 (AWSCURRENT)', stagingVersion: 'v9.2 (AWSPENDING)', lastLogSnippet: 'JWKS endpoint updated with secondary public key kid:rsa-2026-w34. Token signature verification succeeded.', }, { id: 'sec-4', name: 'prod/aws/kms-envelope-key', arn: 'arn:aws:kms:us-east-1:182938491029:key/mrk-8492019a-9e1b-4f21-99ad', type: 'KMS Key', schedule: { interval: 'Every 365 Days', daysInterval: 365, lambdaArn: 'arn:aws:lambda:us-east-1:182938491029:function:KmsEnvelopeRotationHandler', zeroDowntimeVerified: true, }, lastRotated: 'Oct 01, 2025', nextRotationDue: 'Oct 01, 2026', dueRelative: 'in 41 days', status: 'healthy', currentVersion: 'v3.0 (AWSCURRENT)', stagingVersion: 'v3.1 (AWSPENDING)', lastLogSnippet: 'Hardware Security Module (HSM) backing key refreshed. Transparent decryption verified for existing ciphertexts.', }, { id: 'sec-5', name: 'prod/redis/cache-auth', arn: 'arn:aws:secretsmanager:us-east-1:182938491029:secret:prod/redis/auth-token-6c90d', type: 'Redis Auth', schedule: { interval: 'Manual Only', daysInterval: 0, zeroDowntimeVerified: false, }, lastRotated: 'May 12, 2026', nextRotationDue: 'Aug 20, 2026', dueRelative: 'Overdue (1 day)', status: 'manual-needed', currentVersion: 'v1.0 (AWSCURRENT)', stagingVersion: undefined, lastLogSnippet: 'Automated lambda execution unconfigured. Dual-AUTH token rotation requires manual trigger or handler attachment.', }, ] const defaultStats: SecretsRotationStats = { totalSecrets: 18, autoRotationActive: 15, autoRotationPercentage: 83.3, dueSoonCount: 2, overdueCount: 0, } function getTypeBadgeVariant(type: SecretType): 'default' | 'secondary' | 'outline' | 'info' | 'warning' { switch (type) { case 'Postgres DB': case 'MySQL DB': return 'info' case 'API Key': return 'secondary' case 'RSA Keypair': return 'outline' case 'KMS Key': return 'default' case 'Redis Auth': return 'warning' default: return 'outline' } } export function SecretsRotationScheduler({ initialSecrets = defaultSecrets, initialStats = defaultStats, className, }: SecretsRotationSchedulerProps) { const [secrets, setSecrets] = React.useState<ManagedSecret[]>(initialSecrets) const [search, setSearch] = React.useState<string>('') const [statusFilter, setStatusFilter] = React.useState<string>('all') const [bannerMessage, setBannerMessage] = React.useState<{ type: 'success' | 'info'; text: string } | null>(null) const [rotatingSecretId, setRotatingSecretId] = React.useState<string | null>(null) const [copiedArn, setCopiedArn] = React.useState<string | null>(null) // Dialogs const [isRotateModalOpen, setIsRotateModalOpen] = React.useState<boolean>(false) const [selectedSecretForRotate, setSelectedSecretForRotate] = React.useState<ManagedSecret | null>(null) const [isRotatingInProgress, setIsRotatingInProgress] = React.useState<boolean>(false) const [isScheduleModalOpen, setIsScheduleModalOpen] = React.useState<boolean>(false) const [editingSecret, setEditingSecret] = React.useState<ManagedSecret | null>(null) const [formName, setFormName] = React.useState<string>('') const [formArn, setFormArn] = React.useState<string>('') const [formType, setFormType] = React.useState<SecretType>('Postgres DB') const [formInterval, setFormInterval] = React.useState<string>('Every 30 Days') const [formLambdaArn, setFormLambdaArn] = React.useState<string>('') const [formZeroDowntime, setFormZeroDowntime] = React.useState<boolean>(true) const [isLogsModalOpen, setIsLogsModalOpen] = React.useState<boolean>(false) const [selectedSecretForLogs, setSelectedSecretForLogs] = React.useState<ManagedSecret | null>(null) const filteredSecrets = React.useMemo(() => { const q = search.trim().toLowerCase() return secrets.filter((sec) => { const matchesStatus = statusFilter === 'all' || sec.status === statusFilter || (statusFilter === 'healthy' && sec.status === 'healthy') || (statusFilter === 'due-soon' && sec.status === 'due-soon') || (statusFilter === 'manual-needed' && sec.status === 'manual-needed') || (statusFilter === 'paused' && sec.status === 'paused') const matchesSearch = !q || sec.name.toLowerCase().includes(q) || sec.arn.toLowerCase().includes(q) || sec.type.toLowerCase().includes(q) return matchesStatus && matchesSearch }) }, [secrets, search, statusFilter]) const copyToClipboard = async (text: string, id: string) => { try { await navigator.clipboard.writeText(text) setCopiedArn(id) setTimeout(() => setCopiedArn(null), 2000) } catch { setCopiedArn(id) setTimeout(() => setCopiedArn(null), 2000) } } const openInstantRotate = (secret: ManagedSecret) => { setSelectedSecretForRotate(secret) setIsRotateModalOpen(true) } const executeRotation = () => { if (!selectedSecretForRotate) return setIsRotatingInProgress(true) const targetId = selectedSecretForRotate.id setRotatingSecretId(targetId) setTimeout(() => { setIsRotatingInProgress(false) setIsRotateModalOpen(false) setRotatingSecretId(null) setSecrets((prev) => prev.map((s) => s.id === targetId ? { ...s, lastRotated: 'Just now', nextRotationDue: 'Sep 20, 2026', dueRelative: 'in 30 days', status: 'healthy', currentVersion: 'v2.5 (AWSCURRENT)', stagingVersion: undefined, lastLogSnippet: 'Zero-downtime rotation completed successfully. Version B verified, promoted to AWSCURRENT, and staged safely.', } : s, ), ) setBannerMessage({ type: 'success', text: `Zero-downtime rotation successfully completed for "${selectedSecretForRotate.name}". Credentials validated with 0 dropped sockets.`, }) }, 1400) } const openScheduleModal = (secret?: ManagedSecret) => { if (secret) { setEditingSecret(secret) setFormName(secret.name) setFormArn(secret.arn) setFormType(secret.type) setFormInterval(secret.schedule.interval) setFormLambdaArn(secret.schedule.lambdaArn || '') setFormZeroDowntime(secret.schedule.zeroDowntimeVerified) } else { setEditingSecret(null) setFormName('') setFormArn('') setFormType('Postgres DB') setFormInterval('Every 30 Days') setFormLambdaArn('arn:aws:lambda:us-east-1:182938491029:function:RotateSecretHandler') setFormZeroDowntime(true) } setIsScheduleModalOpen(true) } const saveSchedule = () => { if (!formName.trim()) return if (editingSecret) { setSecrets((prev) => prev.map((s) => s.id === editingSecret.id ? { ...s, name: formName.trim(), arn: formArn.trim() || s.arn, type: formType, schedule: { ...s.schedule, interval: formInterval, lambdaArn: formLambdaArn.trim() || undefined, zeroDowntimeVerified: formZeroDowntime, }, } : s, ), ) setBannerMessage({ type: 'info', text: `Rotation schedule for "${formName}" updated successfully.`, }) } else { const newId = `sec-${Date.now()}` const generatedArn = formArn.trim() || `arn:aws:secretsmanager:us-east-1:182938491029:secret:${formName.trim().toLowerCase()}-${Math.random().toString(36).substring(2, 7)}` const newSecret: ManagedSecret = { id: newId, name: formName.trim(), arn: generatedArn, type: formType, schedule: { interval: formInterval, daysInterval: formInterval.includes('7') ? 7 : formInterval.includes('90') ? 90 : 30, lambdaArn: formLambdaArn.trim() || undefined, zeroDowntimeVerified: formZeroDowntime, }, lastRotated: 'Never (Initial)', nextRotationDue: 'Pending First Run', dueRelative: 'in 2 hours', status: 'healthy', currentVersion: 'v1.0 (AWSCURRENT)', stagingVersion: undefined, lastLogSnippet: 'Initial rotation schedule registered with automated zero-downtime dual-version handshake.', } setSecrets((prev) => [newSecret, ...prev]) setBannerMessage({ type: 'success', text: `New secret rotation schedule registered for "${newSecret.name}".`, }) } setIsScheduleModalOpen(false) } const togglePause = (secret: ManagedSecret) => { const nextStatus: RotationStatus = secret.status === 'paused' ? 'healthy' : 'paused' setSecrets((prev) => prev.map((s) => (s.id === secret.id ? { ...s, status: nextStatus } : s))) setBannerMessage({ type: 'info', text: `Rotation schedule for "${secret.name}" ${nextStatus === 'paused' ? 'paused' : 'resumed'}.`, }) } const openLogsModal = (secret: ManagedSecret) => { setSelectedSecretForLogs(secret) setIsLogsModalOpen(true) } return ( <div data-slot="secrets-rotation-scheduler" className={cn('w-full space-y-6', className)}> {/* Header */} <div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between"> <div className="space-y-1"> <div className="flex items-center gap-2"> <div className="bg-primary/10 text-primary flex size-8 items-center justify-center rounded-lg"> <Lock className="size-4.5" /> </div> <h1 className="text-foreground text-2xl font-bold tracking-tight"> Automated Secrets Rotation & Key Lifecycle </h1> </div> <p className="text-muted-foreground text-sm"> Configure zero-downtime rotation schedules for database credentials, API keys, and certificates. </p> </div> <div className="flex items-center gap-2"> <Button onClick={() => openScheduleModal()}> <Plus className="mr-1.5 size-4" /> Schedule New Rotation </Button> </div> </div> {/* Notification / Action banner */} {bannerMessage && ( <div className="border-border bg-card flex items-center justify-between gap-3 rounded-lg border p-3 shadow-xs"> <div className="flex items-center gap-2.5 text-sm"> {bannerMessage.type === 'success' ? ( <CheckCircle2 className="text-success size-4 shrink-0" /> ) : ( <Info className="text-primary size-4 shrink-0" /> )} <span className="text-foreground font-medium">{bannerMessage.text}</span> </div> <Button variant="ghost" size="icon" className="size-7" onClick={() => setBannerMessage(null)}> <X className="size-3.5" /> <span className="sr-only">Dismiss</span> </Button> </div> )} {/* 4 Key Lifecycle Telemetry Cards */} <div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4"> {/* Total Managed Secrets */} <Card className="shadow-xs"> <CardHeader className="flex flex-row items-center justify-between pb-2"> <CardTitle className="text-muted-foreground text-sm font-medium">Total Managed Secrets</CardTitle> <div className="bg-primary/10 text-primary rounded-lg p-2"> <KeyRound className="size-4" /> </div> </CardHeader> <CardContent className="space-y-1"> <div className="text-foreground text-2xl font-bold tracking-tight"> {initialStats.totalSecrets} credentials </div> <p className="text-muted-foreground text-xs">Across 4 cloud regions</p> </CardContent> </Card> {/* Auto-Rotation Active */} <Card className="shadow-xs"> <CardHeader className="flex flex-row items-center justify-between pb-2"> <CardTitle className="text-muted-foreground text-sm font-medium">Auto-Rotation Active</CardTitle> <div className="bg-success/10 text-success rounded-lg p-2"> <RefreshCw className="size-4" /> </div> </CardHeader> <CardContent className="space-y-1"> <div className="flex items-center gap-2"> <span className="text-foreground text-2xl font-bold tracking-tight"> {initialStats.autoRotationActive} / {initialStats.totalSecrets} secrets </span> <Badge variant="success" className="text-xs"> {initialStats.autoRotationPercentage}% </Badge> </div> <p className="text-muted-foreground text-xs">Zero-downtime dual versioning enabled</p> </CardContent> </Card> {/* Rotation Due Soon */} <Card className="border-warning/30 bg-warning/5 bg-warning/10 shadow-xs"> <CardHeader className="flex flex-row items-center justify-between pb-2"> <CardTitle className="text-warning text-sm font-medium">Rotation Due Soon</CardTitle> <div className="bg-warning/20 text-warning rounded-lg p-2"> <Clock className="size-4" /> </div> </CardHeader> <CardContent className="space-y-1"> <div className="flex items-center gap-2"> <span className="text-warning text-2xl font-bold tracking-tight"> {initialStats.dueSoonCount} secrets </span> <Badge variant="warning" className="text-xs"> Next 48h </Badge> </div> <p className="text-warning/90 text-xs">Automated queue pre-scheduled</p> </CardContent> </Card> {/* Expired / Overdue */} <Card className="shadow-xs"> <CardHeader className="flex flex-row items-center justify-between pb-2"> <CardTitle className="text-muted-foreground text-sm font-medium">Expired / Overdue</CardTitle> <div className="bg-success/10 text-success rounded-lg p-2"> <ShieldCheck className="size-4" /> </div> </CardHeader> <CardContent className="space-y-1"> <div className="flex items-center gap-2"> <span className="text-foreground text-2xl font-bold tracking-tight"> {initialStats.overdueCount} critical overdue </span> <Badge variant="outline" className="text-xs"> Compliant </Badge> </div> <p className="text-muted-foreground text-xs">Zero security compliance violations</p> </CardContent> </Card> </div> {/* Secrets Rotation Table Card */} <Card className="shadow-xs"> <CardHeader className="pb-3"> <div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between"> <div> <CardTitle className="text-lg font-semibold">Managed Secrets & Automated Rotation</CardTitle> <CardDescription className="text-xs"> {filteredSecrets.length} of {secrets.length} cryptographic secrets configured for automated lifecycle management. </CardDescription> </div> <div className="flex flex-wrap items-center gap-2"> <div className="relative w-full min-w-0 sm:w-64"> <Search className="text-muted-foreground pointer-events-none absolute top-1/2 left-3 size-4 -translate-y-1/2" /> <Input value={search} onChange={(e) => setSearch(e.target.value)} placeholder="Search secrets or ARN" className="h-8 pl-9 text-xs" /> </div> <Select value={statusFilter} onValueChange={setStatusFilter}> <SelectTrigger className="h-8 w-36 text-xs"> <SelectValue placeholder="All Statuses" /> </SelectTrigger> <SelectContent> <SelectItem value="all">All Statuses</SelectItem> <SelectItem value="healthy">Automated · Healthy</SelectItem> <SelectItem value="due-soon">Due Soon (<48h)</SelectItem> <SelectItem value="manual-needed">Manual Needed</SelectItem> <SelectItem value="paused">Paused</SelectItem> </SelectContent> </Select> <Badge variant="outline" className="hidden h-8 items-center gap-1 font-mono text-xs md:inline-flex"> <Globe className="size-3" /> AWS us-east-1 </Badge> </div> </div> </CardHeader> <CardContent className="p-0"> <div className="overflow-x-auto"> <Table> <TableHeader> <TableRow className="hover:bg-transparent"> <TableHead className="min-w-[260px]">Secret Name & ARN Identifier</TableHead> <TableHead className="min-w-[130px]">Secret Type</TableHead> <TableHead className="min-w-[170px]">Rotation Interval</TableHead> <TableHead className="min-w-[200px]">Last Rotated & Next Due</TableHead> <TableHead className="min-w-[170px]">Rotation Status</TableHead> <TableHead className="w-16 text-right">Actions</TableHead> </TableRow> </TableHeader> <TableBody> {filteredSecrets.map((secret) => ( <TableRow key={secret.id} className="group transition-colors"> {/* Secret Name & ARN Identifier */} <TableCell className="py-3.5"> <div className="space-y-1"> <div className="flex items-center gap-1.5"> <span className="text-foreground text-sm font-medium">{secret.name}</span> {rotatingSecretId === secret.id && ( <RefreshCw className="text-primary size-3.5 animate-spin" aria-label="Rotating now" /> )} </div> <div className="flex items-center gap-1.5"> <code className="text-muted-foreground max-w-[260px] truncate font-mono text-xs sm:max-w-[320px]" title={secret.arn} > {secret.arn} </code> <button type="button" className="text-muted-foreground hover:text-foreground inline-flex size-4 shrink-0 items-center justify-center transition-colors" aria-label={`Copy ARN for ${secret.name}`} onClick={() => copyToClipboard(secret.arn, secret.id)} > {copiedArn === secret.id ? ( <Check className="text-success size-3" /> ) : ( <Copy className="size-3" /> )} </button> </div> </div> </TableCell> {/* Secret Type Badge */} <TableCell className="py-3.5"> <Badge variant={getTypeBadgeVariant(secret.type)} className="gap-1 text-xs"> {(secret.type === 'Postgres DB' || secret.type === 'MySQL DB') && ( <Database className="size-3" /> )} {secret.type === 'API Key' && <KeyRound className="size-3" />} {secret.type === 'RSA Keypair' && <ShieldCheck className="size-3" />} {secret.type === 'KMS Key' && <Lock className="size-3" />} {secret.type === 'Redis Auth' && <Server className="size-3" />} {secret.type} </Badge> </TableCell> {/* Rotation Interval */} <TableCell className="py-3.5"> <div className="space-y-0.5"> <div className="flex items-center gap-1.5 text-xs font-medium"> <Clock className="text-muted-foreground size-3.5 shrink-0" /> <span className="text-foreground">{secret.schedule.interval}</span> </div> {secret.schedule.lambdaArn ? ( <p className="text-muted-foreground max-w-[160px] truncate font-mono text-xs" title={secret.schedule.lambdaArn} > λ {secret.schedule.lambdaArn.split(':function:')[1] || 'Lambda'} </p> ) : ( <p className="text-muted-foreground font-mono text-xs">Manual trigger</p> )} </div> </TableCell> {/* Last Rotated & Next Due */} <TableCell className="py-3.5"> <div className="space-y-0.5 text-xs"> <div className="text-foreground font-medium"> {secret.nextRotationDue} {secret.status === 'due-soon' ? ( <span className="text-warning font-semibold"> ({secret.dueRelative})</span> ) : ( <span className="text-muted-foreground"> ({secret.dueRelative})</span> )} </div> <p className="text-muted-foreground text-xs"> Last: <span className="font-mono">{secret.lastRotated}</span> </p> </div> </TableCell> {/* Rotation Status Badge */} <TableCell className="py-3.5"> <div className="space-y-1"> {secret.status === 'healthy' && ( <Badge variant="success" className="gap-1 text-xs"> <span className="bg-success size-1.5 animate-pulse rounded-full" /> Automated · Healthy </Badge> )} {secret.status === 'due-soon' && ( <Badge variant="warning" className="gap-1 text-xs"> <Clock className="size-3" /> Rotation Due Soon </Badge> )} {secret.status === 'manual-needed' && ( <Badge variant="warning" className="gap-1 text-xs"> <AlertTriangle className="size-3" /> Manual Rotation Needed </Badge> )} {secret.status === 'paused' && ( <Badge variant="secondary" className="gap-1 text-xs"> <Pause className="size-3" /> Paused </Badge> )} {secret.status === 'rotating' && ( <Badge variant="outline" className="gap-1 text-xs"> <RefreshCw className="size-3 animate-spin" /> Rotating </Badge> )} <div className="text-muted-foreground font-mono text-xs">{secret.currentVersion}</div> </div> </TableCell> {/* Actions Menu */} <TableCell className="py-3.5 text-right"> <DropdownMenu> <DropdownMenuTrigger asChild> <Button variant="ghost" size="icon" className="size-8"> <MoreHorizontal className="size-4" /> <span className="sr-only">Open actions menu</span> </Button> </DropdownMenuTrigger> <DropdownMenuContent align="end" className="w-56"> <DropdownMenuLabel className="text-xs">Rotation Controls</DropdownMenuLabel> <DropdownMenuSeparator /> <DropdownMenuItem className="cursor-pointer" onClick={() => openInstantRotate(secret)}> <Play className="text-success mr-2 size-4" /> <span>Rotate Now (Zero Downtime)</span> </DropdownMenuItem> <DropdownMenuItem className="cursor-pointer" onClick={() => openScheduleModal(secret)}> <Pencil className="mr-2 size-4" /> <span>Edit Schedule</span> </DropdownMenuItem> <DropdownMenuItem className="cursor-pointer" onClick={() => openLogsModal(secret)}> <Terminal className="mr-2 size-4" /> <span>View Rotation Lambda Log</span> </DropdownMenuItem> <DropdownMenuSeparator /> <DropdownMenuItem className="cursor-pointer" onClick={() => togglePause(secret)}> {secret.status !== 'paused' ? ( <> <Pause className="mr-2 size-4" /> <span>Pause Auto-Rotation</span> </> ) : ( <> <Play className="text-success mr-2 size-4" /> <span>Resume Auto-Rotation</span> </> )} </DropdownMenuItem> </DropdownMenuContent> </DropdownMenu> </TableCell> </TableRow> ))} {filteredSecrets.length === 0 && ( <TableRow> <TableCell colSpan={6} className="text-muted-foreground h-32 text-center text-sm"> No secrets found matching the selected filter criteria. </TableCell> </TableRow> )} </TableBody> </Table> </div> </CardContent> </Card> {/* Instant Rotate Modal / Dialog (Zero-Downtime Confirmation) */} <Dialog open={isRotateModalOpen} onOpenChange={setIsRotateModalOpen}> <DialogContent className="sm:max-w-xl"> <DialogHeader> <div className="flex items-center gap-2"> <div className="bg-success/10 text-success rounded-lg p-2"> <RefreshCw className="size-5" /> </div> <div> <DialogTitle>Trigger Zero-Downtime Rotation</DialogTitle> <DialogDescription className="text-xs"> AWS Secrets Manager 4-step staging promotion for credential lifecycle safety. </DialogDescription> </div> </div> </DialogHeader> <div className="space-y-4 py-2"> {/* Target Secret Summary */} <div className="border-border bg-muted/50 space-y-2 rounded-lg border p-3"> <div className="flex items-center justify-between text-xs"> <span className="text-muted-foreground">Target Secret:</span> <span className="text-foreground font-mono font-semibold">{selectedSecretForRotate?.name}</span> </div> <div className="flex items-center justify-between text-xs"> <span className="text-muted-foreground">Current Active:</span> <Badge variant="outline" className="font-mono text-xs"> {selectedSecretForRotate?.currentVersion} </Badge> </div> <div className="flex items-center justify-between text-xs"> <span className="text-muted-foreground">Staging Target:</span> <Badge variant="info" className="font-mono text-xs"> {selectedSecretForRotate?.stagingVersion || 'v2.5 (AWSPENDING)'} </Badge> </div> </div> {/* 4-Step Zero-Downtime Process Visualizer */} <div className="space-y-2"> <span className="text-foreground text-xs font-semibold tracking-wider uppercase"> Dual-Version Execution Workflow </span> <div className="border-border bg-card space-y-2.5 rounded-lg border p-3.5 text-xs"> <div className="flex items-start gap-2.5"> <div className="bg-primary text-primary-foreground flex size-5 shrink-0 items-center justify-center rounded-full text-xs font-bold"> 1 </div> <div className="space-y-0.5"> <p className="text-foreground font-semibold">createSecret (Staging)</p> <p className="text-muted-foreground text-xs"> Generates 32-byte cryptographic entropy and provisions version{' '} <code className="font-mono font-semibold">AWSPENDING</code>. </p> </div> </div> <div className="flex items-start gap-2.5"> <div className="bg-primary text-primary-foreground flex size-5 shrink-0 items-center justify-center rounded-full text-xs font-bold"> 2 </div> <div className="space-y-0.5"> <p className="text-foreground font-semibold">setSecret (Dual-Authentication)</p> <p className="text-muted-foreground text-xs"> Updates target resource (Postgres / API / KMS) so both Version A and Version B remain valid. </p> </div> </div> <div className="flex items-start gap-2.5"> <div className="bg-primary text-primary-foreground flex size-5 shrink-0 items-center justify-center rounded-full text-xs font-bold"> 3 </div> <div className="space-y-0.5"> <p className="text-foreground font-semibold">testSecret (Handshake Verification)</p> <p className="text-muted-foreground text-xs"> Lambda executes synthetic database query using new credentials to verify validity. </p> </div> </div> <div className="flex items-start gap-2.5"> <div className="bg-primary text-primary-foreground flex size-5 shrink-0 items-center justify-center rounded-full text-xs font-bold"> 4 </div> <div className="space-y-0.5"> <p className="text-foreground font-semibold">finishSecret (Label Promotion)</p> <p className="text-muted-foreground text-xs"> Moves <code className="font-mono font-semibold">AWSCURRENT</code> to Version B and demotes old version to <code className="font-mono font-semibold">AWSPREVIOUS</code>. </p> </div> </div> </div> </div> {/* Zero-Downtime Guarantee Banner */} <div className="border-success/20 bg-success/5 text-success flex items-center gap-2.5 rounded-lg border p-3 text-xs"> <ShieldCheck className="text-success size-5 shrink-0" /> <span> <strong>Zero-Downtime Guarantee:</strong> Existing connections continue using Version A until Version B is 100% verified. No dropped TCP sockets or authentication spikes. </span> </div> </div> <DialogFooter> <Button variant="outline" disabled={isRotatingInProgress} onClick={() => setIsRotateModalOpen(false)}> Cancel </Button> <Button disabled={isRotatingInProgress} className="gap-1.5" onClick={executeRotation}> {isRotatingInProgress ? ( <> <RefreshCw className="size-4 animate-spin" /> <span>Executing Dual-Stage Rotation...</span> </> ) : ( <> <Play className="size-4" /> <span>Confirm & Rotate Immediately</span> </> )} </Button> </DialogFooter> </DialogContent> </Dialog> {/* Schedule / Edit Schedule Dialog */} <Dialog open={isScheduleModalOpen} onOpenChange={setIsScheduleModalOpen}> <DialogContent className="sm:max-w-lg"> <DialogHeader> <DialogTitle>{editingSecret ? 'Edit Rotation Schedule' : 'Schedule Secret Rotation'}</DialogTitle> <DialogDescription className="text-xs"> Configure automated periodic rotation interval and AWS Lambda rotation handler. </DialogDescription> </DialogHeader> <div className="space-y-4 py-2"> <div className="space-y-1.5"> <label htmlFor="form-secret-name-react" className="text-foreground text-xs font-medium"> Secret Name / Path </label> <Input id="form-secret-name-react" value={formName} onChange={(e) => setFormName(e.target.value)} placeholder="e.g. prod/postgres/replica-credentials" className="text-xs" /> </div> <div className="grid grid-cols-2 gap-3"> <div className="space-y-1.5"> <label htmlFor="form-secret-type-react" className="text-foreground text-xs font-medium"> Secret Type </label> <Select value={formType} onValueChange={(val) => setFormType(val as SecretType)}> <SelectTrigger id="form-secret-type-react" className="text-xs"> <SelectValue placeholder="Secret Type" /> </SelectTrigger> <SelectContent> <SelectItem value="Postgres DB">Postgres DB</SelectItem> <SelectItem value="MySQL DB">MySQL DB</SelectItem> <SelectItem value="API Key">API Key</SelectItem> <SelectItem value="RSA Keypair">RSA Keypair</SelectItem> <SelectItem value="KMS Key">KMS Key</SelectItem> <SelectItem value="Redis Auth">Redis Auth</SelectItem> </SelectContent> </Select> </div> <div className="space-y-1.5"> <label htmlFor="form-secret-interval-react" className="text-foreground text-xs font-medium"> Rotation Interval </label> <Select value={formInterval} onValueChange={setFormInterval}> <SelectTrigger id="form-secret-interval-react" className="text-xs"> <SelectValue placeholder="Interval" /> </SelectTrigger> <SelectContent> <SelectItem value="Every 7 Days">Every 7 Days</SelectItem> <SelectItem value="Every 30 Days">Every 30 Days</SelectItem> <SelectItem value="Every 60 Days">Every 60 Days</SelectItem> <SelectItem value="Every 90 Days">Every 90 Days</SelectItem> <SelectItem value="Every 365 Days">Every 365 Days</SelectItem> <SelectItem value="Manual Only">Manual Only</SelectItem> </SelectContent> </Select> </div> </div> <div className="space-y-1.5"> <label htmlFor="form-secret-lambda-react" className="text-foreground text-xs font-medium"> Rotation Lambda Function ARN </label> <Input id="form-secret-lambda-react" value={formLambdaArn} onChange={(e) => setFormLambdaArn(e.target.value)} placeholder="arn:aws:lambda:us-east-1:182938491029:function:RotateSecret" className="font-mono text-xs" /> </div> <div className="border-border bg-muted/40 flex items-center justify-between rounded-lg border p-3"> <div className="space-y-0.5"> <p className="text-foreground text-xs font-medium">Zero-Downtime Dual Versioning</p> <p className="text-muted-foreground text-xs">Verify staging version before promoting AWSCURRENT</p> </div> <Badge variant="success" className="text-xs"> Active </Badge> </div> </div> <DialogFooter> <Button variant="outline" onClick={() => setIsScheduleModalOpen(false)}> Cancel </Button> <Button disabled={!formName.trim()} onClick={saveSchedule}> {editingSecret ? 'Save Changes' : 'Create Rotation Schedule'} </Button> </DialogFooter> </DialogContent> </Dialog> {/* View Rotation Lambda Log Dialog */} <Dialog open={isLogsModalOpen} onOpenChange={setIsLogsModalOpen}> <DialogContent className="sm:max-w-2xl"> <DialogHeader> <div className="flex items-center gap-2"> <Terminal className="text-primary size-5" /> <DialogTitle className="font-mono text-base">{selectedSecretForLogs?.name}</DialogTitle> </div> <DialogDescription className="font-mono text-xs"> Lambda Handler: {selectedSecretForLogs?.schedule.lambdaArn || 'arn:aws:lambda:...:RotateDefault'} </DialogDescription> </DialogHeader> <div className="space-y-3 py-1"> <div className="border-border bg-muted/60 flex items-center justify-between rounded-md border px-3 py-2 text-xs"> <div className="flex items-center gap-2"> <span className="text-muted-foreground">Last Invocation:</span> <span className="text-foreground font-mono font-medium">{selectedSecretForLogs?.lastRotated}</span> </div> <div className="flex items-center gap-2"> <span className="text-muted-foreground">Status:</span> <Badge variant="success" className="font-mono text-xs"> 200 OK - SUCCESS </Badge> </div> </div> {/* Terminal Output Simulation */} <div className="border-border text-success space-y-1.5 overflow-x-auto rounded-lg border bg-black/90 p-4 font-mono text-xs dark:bg-black"> <p className="text-muted-foreground"> START RequestId: 4f8b91a2-631c-4b89-9801-ec8492019a Version: $LATEST </p> <p className="text-success"> [INFO] Step 1/4 createSecret: Generating 32-byte cryptographic entropy for AWSPENDING... </p> <p className="text-success"> [INFO] Step 2/4 setSecret: Applying shadow credentials to destination cluster. Dual-auth enabled. </p> <p className="text-success"> [INFO] Step 3/4 testSecret: Executing test query `SELECT 1` from staging connection pool -> Verified (6.4ms) </p> <p className="text-success"> [INFO] Step 4/4 finishSecret: Swapped AWSCURRENT label to staging version. Previous marked AWSPREVIOUS. </p> <p className="text-muted-foreground">END RequestId: 4f8b91a2-631c-4b89-9801-ec8492019a</p> <p className="text-muted-foreground"> REPORT RequestId: 4f8b91a2-631c-4b89-9801-ec8492019a Duration: 342.15 ms Billed Duration: 343 ms Memory Size: 256 MB Max Memory Used: 68 MB </p> </div> </div> <DialogFooter> <Button variant="outline" onClick={() => setIsLogsModalOpen(false)}> Close Logs </Button> </DialogFooter> </DialogContent> </Dialog> </div> ) } export default SecretsRotationScheduler
Raw manifest:https://uipkge.dev/r/react/secrets-rotation-scheduler.json