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 uipkge-ng@latest add secrets-rotation-scheduler$npx uipkge-ng@latest add secrets-rotation-scheduler$yarn dlx uipkge-ng@latest add secrets-rotation-scheduler$bunx uipkge-ng@latest add secrets-rotation-schedulerManifest:
https://uipkge.dev/r/angular/secrets-rotation-scheduler.jsonInstalls to:src/app/components/blocks/secrets-rotation-scheduler/Variants
Loading interactive previews…
Loading interactive previews…
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 (2)
src/app/components/blocks/secrets-rotation-scheduler/secrets-rotation-scheduler.component.ts46.5 kB
// AWS Secrets Manager style automated credential rotation scheduler: lifecycle // telemetry cards, searchable/filterable secrets table, zero-downtime instant-rotate // dialog, schedule create/edit dialog, and rotation lambda log viewer. Port of the // React block 1:1. import { ChangeDetectionStrategy, Component, Input, computed, signal } from '@angular/core' import { AlertTriangle, Check, CheckCircle2, Clock, Copy, Database, Globe, Info, KeyRound, Lock, LucideAngularModule, MoreHorizontal, Pause, Pencil, Play, Plus, RefreshCw, Search, Server, ShieldCheck, Terminal, X, } from 'lucide-angular' import { cn } from '@/lib/utils' import { UiBadgeComponent } from '@/ui/badge/badge.component' import { UiButtonComponent } from '@/ui/button/button.component' import { UiCardComponent, UiCardContentComponent, UiCardDescriptionComponent, UiCardHeaderComponent, UiCardTitleComponent, } from '@/ui/card/card.component' import { UiDialogComponent, UiDialogContentComponent, UiDialogDescriptionComponent, UiDialogFooterComponent, UiDialogHeaderComponent, UiDialogTitleComponent, } from '@/ui/dialog/dialog.component' import { UiDropdownMenuComponent, UiDropdownMenuContentComponent, UiDropdownMenuItemComponent, UiDropdownMenuLabelComponent, UiDropdownMenuSeparatorComponent, UiDropdownMenuTriggerComponent, } from '@/ui/dropdown-menu/dropdown-menu.component' import { UiInputComponent } from '@/ui/input/input.component' import { UiSelectComponent, UiSelectContentComponent, UiSelectItemComponent, UiSelectTriggerComponent, UiSelectValueComponent, } from '@/ui/select/select.component' import { UiTableBodyComponent, UiTableCellComponent, UiTableComponent, UiTableHeadComponent, UiTableHeaderComponent, UiTableRowComponent, } from '@/ui/table/table.component' 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 } 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, } const SECRET_TYPES: SecretType[] = ['Postgres DB', 'MySQL DB', 'API Key', 'RSA Keypair', 'KMS Key', 'Redis Auth'] const INTERVALS = ['Every 7 Days', 'Every 30 Days', 'Every 60 Days', 'Every 90 Days', 'Every 365 Days', 'Manual Only'] function typeBadgeVariant(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' } } @Component({ changeDetection: ChangeDetectionStrategy.Eager, selector: 'ui-secrets-rotation-scheduler, [ui-secrets-rotation-scheduler]', standalone: true, // React renders the root <div> itself: the host stays out of layout and `class` goes to the root. host: { '[attr.class]': '"contents"' }, imports: [ LucideAngularModule, UiBadgeComponent, UiButtonComponent, UiCardComponent, UiCardContentComponent, UiCardDescriptionComponent, UiCardHeaderComponent, UiCardTitleComponent, UiDialogComponent, UiDialogContentComponent, UiDialogDescriptionComponent, UiDialogFooterComponent, UiDialogHeaderComponent, UiDialogTitleComponent, UiDropdownMenuComponent, UiDropdownMenuContentComponent, UiDropdownMenuItemComponent, UiDropdownMenuLabelComponent, UiDropdownMenuSeparatorComponent, UiDropdownMenuTriggerComponent, UiInputComponent, UiSelectComponent, UiSelectContentComponent, UiSelectItemComponent, UiSelectTriggerComponent, UiSelectValueComponent, UiTableBodyComponent, UiTableCellComponent, UiTableComponent, UiTableHeadComponent, UiTableHeaderComponent, UiTableRowComponent, ], template: ` <div data-slot="secrets-rotation-scheduler" [class]="rootClass"> <!-- Header --> <div class="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between"> <div class="space-y-1"> <div class="flex items-center gap-2"> <div class="bg-primary/10 text-primary flex size-8 items-center justify-center rounded-lg"> <lucide-icon [img]="Lock" class="size-4.5" /> </div> <h1 class="text-foreground text-2xl font-bold tracking-tight"> Automated Secrets Rotation & Key Lifecycle </h1> </div> <p class="text-muted-foreground text-sm"> Configure zero-downtime rotation schedules for database credentials, API keys, and certificates. </p> </div> <div class="flex items-center gap-2"> <button ui-button (click)="openScheduleModal()"> <lucide-icon [img]="Plus" class="mr-1.5 size-4" /> <span>Schedule New Rotation</span> </button> </div> </div> <!-- Notification / Action banner --> @if (bannerMessage(); as banner) { <div class="border-border bg-card flex items-center justify-between gap-3 rounded-lg border p-3 shadow-xs"> <div class="flex items-center gap-2.5 text-sm"> @if (banner.type === 'success') { <lucide-icon [img]="CheckCircle2" class="text-success size-4 shrink-0" /> } @else { <lucide-icon [img]="Info" class="text-primary size-4 shrink-0" /> } <span class="text-foreground font-medium">{{ banner.text }}</span> </div> <button ui-button variant="ghost" size="icon" class="size-7" aria-label="Dismiss" (click)="bannerMessage.set(null)" > <lucide-icon [img]="X" class="size-3.5" /> <span class="sr-only">Dismiss</span> </button> </div> } <!-- 4 Key Lifecycle Telemetry Cards --> <div class="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4"> <div ui-card class="shadow-xs"> <div ui-card-header class="flex flex-row items-center justify-between pb-2"> <h3 ui-card-title class="text-muted-foreground text-sm font-medium">Total Managed Secrets</h3> <div class="bg-primary/10 text-primary rounded-lg p-2"> <lucide-icon [img]="KeyRound" class="size-4" /> </div> </div> <div ui-card-content class="space-y-1"> <div class="text-foreground text-2xl font-bold tracking-tight">{{ stats().totalSecrets }} credentials</div> <p class="text-muted-foreground text-xs">Across 4 cloud regions</p> </div> </div> <div ui-card class="shadow-xs"> <div ui-card-header class="flex flex-row items-center justify-between pb-2"> <h3 ui-card-title class="text-muted-foreground text-sm font-medium">Auto-Rotation Active</h3> <div class="bg-success/10 text-success rounded-lg p-2"> <lucide-icon [img]="RefreshCw" class="size-4" /> </div> </div> <div ui-card-content class="space-y-1"> <div class="flex items-center gap-2"> <span class="text-foreground text-2xl font-bold tracking-tight"> {{ stats().autoRotationActive }} / {{ stats().totalSecrets }} secrets </span> <span ui-badge variant="success" class="text-xs">{{ stats().autoRotationPercentage }}%</span> </div> <p class="text-muted-foreground text-xs">Zero-downtime dual versioning enabled</p> </div> </div> <div ui-card class="border-warning/30 bg-warning/5 bg-warning/10 shadow-xs"> <div ui-card-header class="flex flex-row items-center justify-between pb-2"> <h3 ui-card-title class="text-warning text-sm font-medium">Rotation Due Soon</h3> <div class="bg-warning/20 text-warning rounded-lg p-2"> <lucide-icon [img]="Clock" class="size-4" /> </div> </div> <div ui-card-content class="space-y-1"> <div class="flex items-center gap-2"> <span class="text-warning text-2xl font-bold tracking-tight">{{ stats().dueSoonCount }} secrets</span> <span ui-badge variant="warning" class="text-xs">Next 48h</span> </div> <p class="text-warning/90 text-xs">Automated queue pre-scheduled</p> </div> </div> <div ui-card class="shadow-xs"> <div ui-card-header class="flex flex-row items-center justify-between pb-2"> <h3 ui-card-title class="text-muted-foreground text-sm font-medium">Expired / Overdue</h3> <div class="bg-success/10 text-success rounded-lg p-2"> <lucide-icon [img]="ShieldCheck" class="size-4" /> </div> </div> <div ui-card-content class="space-y-1"> <div class="flex items-center gap-2"> <span class="text-foreground text-2xl font-bold tracking-tight"> {{ stats().overdueCount }} critical overdue </span> <span ui-badge variant="outline" class="text-xs">Compliant</span> </div> <p class="text-muted-foreground text-xs">Zero security compliance violations</p> </div> </div> </div> <!-- Secrets Rotation Table Card --> <div ui-card class="shadow-xs"> <div ui-card-header class="pb-3"> <div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between"> <div> <h3 ui-card-title class="text-lg font-semibold">Managed Secrets & Automated Rotation</h3> <p ui-card-description class="text-xs"> {{ filteredSecrets().length }} of {{ secrets().length }} cryptographic secrets configured for automated lifecycle management. </p> </div> <div class="flex flex-wrap items-center gap-2"> <div class="relative w-full min-w-0 sm:w-64"> <lucide-icon [img]="Search" class="text-muted-foreground pointer-events-none absolute top-1/2 left-3 size-4 -translate-y-1/2" /> <ui-input [value]="search()" (valueChange)="search.set($event)" placeholder="Search secrets or ARN" class="h-8 pl-9 text-xs" /> </div> <ui-select [value]="statusFilter()" (valueChange)="statusFilter.set($event)"> <button ui-select-trigger class="h-8 w-36 text-xs"> <ui-select-value placeholder="All Statuses" /> </button> <ui-select-content> <ui-select-item value="all">All Statuses</ui-select-item> <ui-select-item value="healthy">Automated · Healthy</ui-select-item> <ui-select-item value="due-soon">Due Soon (<48h)</ui-select-item> <ui-select-item value="manual-needed">Manual Needed</ui-select-item> <ui-select-item value="paused">Paused</ui-select-item> </ui-select-content> </ui-select> <span ui-badge variant="outline" class="hidden h-8 items-center gap-1 font-mono text-xs md:inline-flex"> <lucide-icon [img]="Globe" class="size-3" /> <span>AWS us-east-1</span> </span> </div> </div> </div> <div ui-card-content class="p-0"> <div class="overflow-x-auto"> <table ui-table> <thead ui-table-header> <tr ui-table-row class="hover:bg-transparent"> <th ui-table-head class="min-w-[260px]">Secret Name & ARN Identifier</th> <th ui-table-head class="min-w-[130px]">Secret Type</th> <th ui-table-head class="min-w-[170px]">Rotation Interval</th> <th ui-table-head class="min-w-[200px]">Last Rotated & Next Due</th> <th ui-table-head class="min-w-[170px]">Rotation Status</th> <th ui-table-head class="w-16 text-right">Actions</th> </tr> </thead> <tbody ui-table-body> @for (secret of filteredSecrets(); track secret.id) { <tr ui-table-row class="group transition-colors"> <td ui-table-cell class="py-3.5"> <div class="space-y-1"> <div class="flex items-center gap-1.5"> <span class="text-foreground text-sm font-medium">{{ secret.name }}</span> @if (rotatingSecretId() === secret.id) { <lucide-icon [img]="RefreshCw" class="text-primary size-3.5 animate-spin" aria-label="Rotating now" /> } </div> <div class="flex items-center gap-1.5"> <code class="text-muted-foreground max-w-[260px] truncate font-mono text-xs sm:max-w-[320px]" [attr.title]="secret.arn" > {{ secret.arn }} </code> <button type="button" class="text-muted-foreground hover:text-foreground inline-flex size-4 shrink-0 items-center justify-center transition-colors" [attr.aria-label]="'Copy ARN for ' + secret.name" (click)="copyToClipboard(secret.arn, secret.id)" > @if (copiedArn() === secret.id) { <lucide-icon [img]="Check" class="text-success size-3" /> } @else { <lucide-icon [img]="Copy" class="size-3" /> } </button> </div> </div> </td> <td ui-table-cell class="py-3.5"> <span ui-badge [variant]="typeVariant(secret.type)" class="gap-1 text-xs"> @if (secret.type === 'Postgres DB' || secret.type === 'MySQL DB') { <lucide-icon [img]="Database" class="size-3" /> } @else if (secret.type === 'API Key') { <lucide-icon [img]="KeyRound" class="size-3" /> } @else if (secret.type === 'RSA Keypair') { <lucide-icon [img]="ShieldCheck" class="size-3" /> } @else if (secret.type === 'KMS Key') { <lucide-icon [img]="Lock" class="size-3" /> } @else if (secret.type === 'Redis Auth') { <lucide-icon [img]="Server" class="size-3" /> } <span>{{ secret.type }}</span> </span> </td> <td ui-table-cell class="py-3.5"> <div class="space-y-0.5"> <div class="flex items-center gap-1.5 text-xs font-medium"> <lucide-icon [img]="Clock" class="text-muted-foreground size-3.5 shrink-0" /> <span class="text-foreground">{{ secret.schedule.interval }}</span> </div> @if (secret.schedule.lambdaArn) { <p class="text-muted-foreground max-w-[160px] truncate font-mono text-xs" [attr.title]="secret.schedule.lambdaArn" > λ {{ lambdaShort(secret.schedule.lambdaArn) }} </p> } @else { <p class="text-muted-foreground font-mono text-xs">Manual trigger</p> } </div> </td> <td ui-table-cell class="py-3.5"> <div class="space-y-0.5 text-xs"> <div class="text-foreground font-medium"> {{ secret.nextRotationDue }} @if (secret.status === 'due-soon') { <span class="text-warning font-semibold"> ({{ secret.dueRelative }})</span> } @else { <span class="text-muted-foreground"> ({{ secret.dueRelative }})</span> } </div> <p class="text-muted-foreground text-xs"> Last: <span class="font-mono">{{ secret.lastRotated }}</span> </p> </div> </td> <td ui-table-cell class="py-3.5"> <div class="space-y-1"> @if (secret.status === 'healthy') { <span ui-badge variant="success" class="gap-1 text-xs"> <span class="bg-success size-1.5 animate-pulse rounded-full"></span> <span>Automated · Healthy</span> </span> } @else if (secret.status === 'due-soon') { <span ui-badge variant="warning" class="gap-1 text-xs"> <lucide-icon [img]="Clock" class="size-3" /> <span>Rotation Due Soon</span> </span> } @else if (secret.status === 'manual-needed') { <span ui-badge variant="warning" class="gap-1 text-xs"> <lucide-icon [img]="AlertTriangle" class="size-3" /> <span>Manual Rotation Needed</span> </span> } @else if (secret.status === 'paused') { <span ui-badge variant="secondary" class="gap-1 text-xs"> <lucide-icon [img]="Pause" class="size-3" /> <span>Paused</span> </span> } @else { <span ui-badge variant="outline" class="gap-1 text-xs"> <lucide-icon [img]="RefreshCw" class="size-3 animate-spin" /> <span>Rotating</span> </span> } <div class="text-muted-foreground font-mono text-xs">{{ secret.currentVersion }}</div> </div> </td> <td ui-table-cell class="py-3.5 text-right"> <ui-dropdown-menu> <button ui-button ui-dropdown-menu-trigger variant="ghost" size="icon" class="size-8"> <lucide-icon [img]="MoreHorizontal" class="size-4" /> <span class="sr-only">Open actions menu</span> </button> <ui-dropdown-menu-content align="end" class="w-56"> <ui-dropdown-menu-label class="text-xs">Rotation Controls</ui-dropdown-menu-label> <ui-dropdown-menu-separator /> <ui-dropdown-menu-item (select)="openInstantRotate(secret)"> <lucide-icon [img]="Play" class="text-success mr-2 size-4" /> <span>Rotate Now (Zero Downtime)</span> </ui-dropdown-menu-item> <ui-dropdown-menu-item (select)="openScheduleModal(secret)"> <lucide-icon [img]="Pencil" class="mr-2 size-4" /> <span>Edit Schedule</span> </ui-dropdown-menu-item> <ui-dropdown-menu-item (select)="openLogsModal(secret)"> <lucide-icon [img]="Terminal" class="mr-2 size-4" /> <span>View Rotation Lambda Log</span> </ui-dropdown-menu-item> <ui-dropdown-menu-separator /> <ui-dropdown-menu-item (select)="togglePause(secret)"> @if (secret.status !== 'paused') { <lucide-icon [img]="Pause" class="mr-2 size-4" /> <span>Pause Auto-Rotation</span> } @else { <lucide-icon [img]="Play" class="text-success mr-2 size-4" /> <span>Resume Auto-Rotation</span> } </ui-dropdown-menu-item> </ui-dropdown-menu-content> </ui-dropdown-menu> </td> </tr> } @if (filteredSecrets().length === 0) { <tr ui-table-row> <td ui-table-cell [attr.colspan]="6" class="text-muted-foreground h-32 text-center text-sm"> No secrets found matching the selected filter criteria. </td> </tr> } </tbody> </table> </div> </div> </div> <!-- Instant Rotate Dialog (Zero-Downtime Confirmation) --> <ui-dialog [open]="isRotateModalOpen()" (openChange)="isRotateModalOpen.set($event)"> <ui-dialog-content class="sm:max-w-xl"> <div ui-dialog-header> <div class="flex items-center gap-2"> <div class="bg-success/10 text-success rounded-lg p-2"> <lucide-icon [img]="RefreshCw" class="size-5" /> </div> <div> <h2 ui-dialog-title>Trigger Zero-Downtime Rotation</h2> <p ui-dialog-description class="text-xs"> AWS Secrets Manager 4-step staging promotion for credential lifecycle safety. </p> </div> </div> </div> <div class="space-y-4 py-2"> <div class="border-border bg-muted/50 space-y-2 rounded-lg border p-3"> <div class="flex items-center justify-between text-xs"> <span class="text-muted-foreground">Target Secret:</span> <span class="text-foreground font-mono font-semibold">{{ selectedSecretForRotate()?.name }}</span> </div> <div class="flex items-center justify-between text-xs"> <span class="text-muted-foreground">Current Active:</span> <span ui-badge variant="outline" class="font-mono text-xs"> {{ selectedSecretForRotate()?.currentVersion }} </span> </div> <div class="flex items-center justify-between text-xs"> <span class="text-muted-foreground">Staging Target:</span> <span ui-badge variant="info" class="font-mono text-xs"> {{ selectedSecretForRotate()?.stagingVersion || 'v2.5 (AWSPENDING)' }} </span> </div> </div> <div class="space-y-2"> <span class="text-foreground text-xs font-semibold tracking-wider uppercase"> Dual-Version Execution Workflow </span> <div class="border-border bg-card space-y-2.5 rounded-lg border p-3.5 text-xs"> <div class="flex items-start gap-2.5"> <div class="bg-primary text-primary-foreground flex size-5 shrink-0 items-center justify-center rounded-full text-xs font-bold" > 1 </div> <div class="space-y-0.5"> <p class="text-foreground font-semibold">createSecret (Staging)</p> <p class="text-muted-foreground text-xs"> Generates 32-byte cryptographic entropy and provisions version <code class="font-mono font-semibold">AWSPENDING</code>. </p> </div> </div> <div class="flex items-start gap-2.5"> <div class="bg-primary text-primary-foreground flex size-5 shrink-0 items-center justify-center rounded-full text-xs font-bold" > 2 </div> <div class="space-y-0.5"> <p class="text-foreground font-semibold">setSecret (Dual-Authentication)</p> <p class="text-muted-foreground text-xs"> Updates target resource (Postgres / API / KMS) so both Version A and Version B remain valid. </p> </div> </div> <div class="flex items-start gap-2.5"> <div class="bg-primary text-primary-foreground flex size-5 shrink-0 items-center justify-center rounded-full text-xs font-bold" > 3 </div> <div class="space-y-0.5"> <p class="text-foreground font-semibold">testSecret (Handshake Verification)</p> <p class="text-muted-foreground text-xs"> Lambda executes synthetic database query using new credentials to verify validity. </p> </div> </div> <div class="flex items-start gap-2.5"> <div class="bg-primary text-primary-foreground flex size-5 shrink-0 items-center justify-center rounded-full text-xs font-bold" > 4 </div> <div class="space-y-0.5"> <p class="text-foreground font-semibold">finishSecret (Label Promotion)</p> <p class="text-muted-foreground text-xs"> Moves <code class="font-mono font-semibold">AWSCURRENT</code> to Version B and demotes old version to <code class="font-mono font-semibold">AWSPREVIOUS</code>. </p> </div> </div> </div> </div> <div class="border-success/20 bg-success/5 text-success flex items-center gap-2.5 rounded-lg border p-3 text-xs" > <lucide-icon [img]="ShieldCheck" class="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> <div ui-dialog-footer> <button ui-button variant="outline" [disabled]="isRotatingInProgress()" (click)="isRotateModalOpen.set(false)" > Cancel </button> <button ui-button [disabled]="isRotatingInProgress()" class="gap-1.5" (click)="executeRotation()"> @if (isRotatingInProgress()) { <lucide-icon [img]="RefreshCw" class="size-4 animate-spin" /> <span>Executing Dual-Stage Rotation...</span> } @else { <lucide-icon [img]="Play" class="size-4" /> <span>Confirm & Rotate Immediately</span> } </button> </div> </ui-dialog-content> </ui-dialog> <!-- Schedule / Edit Schedule Dialog --> <ui-dialog [open]="isScheduleModalOpen()" (openChange)="isScheduleModalOpen.set($event)"> <ui-dialog-content class="sm:max-w-lg"> <div ui-dialog-header> <h2 ui-dialog-title>{{ editingSecret() ? 'Edit Rotation Schedule' : 'Schedule Secret Rotation' }}</h2> <p ui-dialog-description class="text-xs"> Configure automated periodic rotation interval and AWS Lambda rotation handler. </p> </div> <div class="space-y-4 py-2"> <div class="space-y-1.5"> <label for="angular-secret-name" class="text-foreground text-xs font-medium">Secret Name / Path</label> <ui-input id="angular-secret-name" [value]="formName()" (valueChange)="formName.set($event)" placeholder="e.g. prod/postgres/replica-credentials" class="text-xs" /> </div> <div class="grid grid-cols-2 gap-3"> <div class="space-y-1.5"> <label class="text-foreground text-xs font-medium">Secret Type</label> <ui-select [value]="formType()" (valueChange)="setFormType($event)"> <button ui-select-trigger class="text-xs"> <ui-select-value placeholder="Secret Type" /> </button> <ui-select-content> @for (t of secretTypes; track t) { <ui-select-item [value]="t">{{ t }}</ui-select-item> } </ui-select-content> </ui-select> </div> <div class="space-y-1.5"> <label class="text-foreground text-xs font-medium">Rotation Interval</label> <ui-select [value]="formInterval()" (valueChange)="formInterval.set($event)"> <button ui-select-trigger class="text-xs"> <ui-select-value placeholder="Interval" /> </button> <ui-select-content> @for (iv of intervals; track iv) { <ui-select-item [value]="iv">{{ iv }}</ui-select-item> } </ui-select-content> </ui-select> </div> </div> <div class="space-y-1.5"> <label for="angular-secret-lambda" class="text-foreground text-xs font-medium"> Rotation Lambda Function ARN </label> <ui-input id="angular-secret-lambda" [value]="formLambdaArn()" (valueChange)="formLambdaArn.set($event)" placeholder="arn:aws:lambda:us-east-1:182938491029:function:RotateSecret" class="font-mono text-xs" /> </div> <div class="border-border bg-muted/40 flex items-center justify-between rounded-lg border p-3"> <div class="space-y-0.5"> <p class="text-foreground text-xs font-medium">Zero-Downtime Dual Versioning</p> <p class="text-muted-foreground text-xs">Verify staging version before promoting AWSCURRENT</p> </div> <span ui-badge variant="success" class="text-xs">Active</span> </div> </div> <div ui-dialog-footer> <button ui-button variant="outline" (click)="isScheduleModalOpen.set(false)">Cancel</button> <button ui-button [disabled]="!formName().trim()" (click)="saveSchedule()"> {{ editingSecret() ? 'Save Changes' : 'Create Rotation Schedule' }} </button> </div> </ui-dialog-content> </ui-dialog> <!-- View Rotation Lambda Log Dialog --> <ui-dialog [open]="isLogsModalOpen()" (openChange)="isLogsModalOpen.set($event)"> <ui-dialog-content class="sm:max-w-2xl"> <div ui-dialog-header> <div class="flex items-center gap-2"> <lucide-icon [img]="Terminal" class="text-primary size-5" /> <h2 ui-dialog-title class="font-mono text-base">{{ selectedSecretForLogs()?.name }}</h2> </div> <p ui-dialog-description class="font-mono text-xs"> Lambda Handler: {{ selectedSecretForLogs()?.schedule?.lambdaArn || 'arn:aws:lambda:...:RotateDefault' }} </p> </div> <div class="space-y-3 py-1"> <div class="border-border bg-muted/60 flex items-center justify-between rounded-md border px-3 py-2 text-xs" > <div class="flex items-center gap-2"> <span class="text-muted-foreground">Last Invocation:</span> <span class="text-foreground font-mono font-medium">{{ selectedSecretForLogs()?.lastRotated }}</span> </div> <div class="flex items-center gap-2"> <span class="text-muted-foreground">Status:</span> <span ui-badge variant="success" class="font-mono text-xs">200 OK - SUCCESS</span> </div> </div> <div class="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 class="text-muted-foreground">START RequestId: 4f8b91a2-631c-4b89-9801-ec8492019a Version: $LATEST</p> <p class="text-success"> [INFO] Step 1/4 createSecret: Generating 32-byte cryptographic entropy for AWSPENDING... </p> <p class="text-success"> [INFO] Step 2/4 setSecret: Applying shadow credentials to destination cluster. Dual-auth enabled. </p> <p class="text-success"> [INFO] Step 3/4 testSecret: Executing test query from staging connection pool -> Verified (6.4ms) </p> <p class="text-success"> [INFO] Step 4/4 finishSecret: Swapped AWSCURRENT label to staging version. Previous marked AWSPREVIOUS. </p> <p class="text-muted-foreground">END RequestId: 4f8b91a2-631c-4b89-9801-ec8492019a</p> <p class="text-muted-foreground"> REPORT RequestId: 4f8b91a2-631c-4b89-9801-ec8492019a Duration: 342.15 ms Billed Duration: 343 ms </p> </div> </div> <div ui-dialog-footer> <button ui-button variant="outline" (click)="isLogsModalOpen.set(false)">Close Logs</button> </div> </ui-dialog-content> </ui-dialog> </div> `, }) export class UiSecretsRotationSchedulerComponent { protected readonly AlertTriangle = AlertTriangle protected readonly Check = Check protected readonly CheckCircle2 = CheckCircle2 protected readonly Clock = Clock protected readonly Copy = Copy protected readonly Database = Database protected readonly Globe = Globe protected readonly Info = Info protected readonly KeyRound = KeyRound protected readonly Lock = Lock protected readonly MoreHorizontal = MoreHorizontal protected readonly Pause = Pause protected readonly Pencil = Pencil protected readonly Play = Play protected readonly Plus = Plus protected readonly RefreshCw = RefreshCw protected readonly Search = Search protected readonly Server = Server protected readonly ShieldCheck = ShieldCheck protected readonly Terminal = Terminal protected readonly X = X protected readonly secretTypes = SECRET_TYPES protected readonly intervals = INTERVALS @Input() set initialSecrets(v: ManagedSecret[] | undefined) { this.secrets.set((v ?? defaultSecrets).map((s) => ({ ...s, schedule: { ...s.schedule } }))) } @Input() set initialStats(v: SecretsRotationStats | undefined) { this.stats.set({ ...(v ?? defaultStats) }) } @Input('class') className?: string readonly secrets = signal<ManagedSecret[]>(defaultSecrets.map((s) => ({ ...s, schedule: { ...s.schedule } }))) readonly stats = signal<SecretsRotationStats>({ ...defaultStats }) readonly search = signal('') readonly statusFilter = signal('all') readonly bannerMessage = signal<{ type: 'success' | 'info'; text: string } | null>(null) readonly rotatingSecretId = signal<string | null>(null) readonly copiedArn = signal<string | null>(null) readonly isRotateModalOpen = signal(false) readonly selectedSecretForRotate = signal<ManagedSecret | null>(null) readonly isRotatingInProgress = signal(false) readonly isScheduleModalOpen = signal(false) readonly editingSecret = signal<ManagedSecret | null>(null) readonly formName = signal('') readonly formArn = signal('') readonly formType = signal<SecretType>('Postgres DB') readonly formInterval = signal('Every 30 Days') readonly formLambdaArn = signal('') readonly formZeroDowntime = signal(true) readonly isLogsModalOpen = signal(false) readonly selectedSecretForLogs = signal<ManagedSecret | null>(null) readonly filteredSecrets = computed(() => { const q = this.search().trim().toLowerCase() const status = this.statusFilter() return this.secrets().filter((sec) => { const matchesStatus = status === 'all' || sec.status === status const matchesSearch = !q || sec.name.toLowerCase().includes(q) || sec.arn.toLowerCase().includes(q) || sec.type.toLowerCase().includes(q) return matchesStatus && matchesSearch }) }) get rootClass(): string { return cn('w-full space-y-6', this.className) } typeVariant(type: SecretType): 'default' | 'secondary' | 'outline' | 'info' | 'warning' { return typeBadgeVariant(type) } lambdaShort(arn: string): string { return arn.split(':function:')[1] || 'Lambda' } setFormType(v: string): void { this.formType.set(v as SecretType) } copyToClipboard(text: string, id: string): void { try { navigator.clipboard.writeText(text) } catch { // Clipboard unavailable in this context. } this.copiedArn.set(id) setTimeout(() => { if (this.copiedArn() === id) this.copiedArn.set(null) }, 2000) } openInstantRotate(secret: ManagedSecret): void { this.selectedSecretForRotate.set(secret) this.isRotateModalOpen.set(true) } executeRotation(): void { const target = this.selectedSecretForRotate() if (!target) return this.isRotatingInProgress.set(true) const targetId = target.id const targetName = target.name this.rotatingSecretId.set(targetId) setTimeout(() => { this.isRotatingInProgress.set(false) this.isRotateModalOpen.set(false) this.rotatingSecretId.set(null) this.secrets.update((prev) => prev.map((s) => s.id === targetId ? { ...s, lastRotated: 'Just now', nextRotationDue: 'Sep 20, 2026', dueRelative: 'in 30 days', status: 'healthy' as RotationStatus, currentVersion: 'v2.5 (AWSCURRENT)', stagingVersion: undefined, lastLogSnippet: 'Zero-downtime rotation completed successfully. Version B verified, promoted to AWSCURRENT, and staged safely.', } : s, ), ) this.bannerMessage.set({ type: 'success', text: `Zero-downtime rotation successfully completed for "${targetName}". Credentials validated with 0 dropped sockets.`, }) }, 1400) } openScheduleModal(secret?: ManagedSecret): void { if (secret) { this.editingSecret.set(secret) this.formName.set(secret.name) this.formArn.set(secret.arn) this.formType.set(secret.type) this.formInterval.set(secret.schedule.interval) this.formLambdaArn.set(secret.schedule.lambdaArn || '') this.formZeroDowntime.set(secret.schedule.zeroDowntimeVerified) } else { this.editingSecret.set(null) this.formName.set('') this.formArn.set('') this.formType.set('Postgres DB') this.formInterval.set('Every 30 Days') this.formLambdaArn.set('arn:aws:lambda:us-east-1:182938491029:function:RotateSecretHandler') this.formZeroDowntime.set(true) } this.isScheduleModalOpen.set(true) } saveSchedule(): void { if (!this.formName().trim()) return const editing = this.editingSecret() if (editing) { const name = this.formName().trim() const arn = this.formArn().trim() this.secrets.update((prev) => prev.map((s) => s.id === editing.id ? { ...s, name, arn: arn || s.arn, type: this.formType(), schedule: { ...s.schedule, interval: this.formInterval(), lambdaArn: this.formLambdaArn().trim() || undefined, zeroDowntimeVerified: this.formZeroDowntime(), }, } : s, ), ) this.bannerMessage.set({ type: 'info', text: `Rotation schedule for "${name}" updated successfully.` }) } else { const name = this.formName().trim() const newSecret: ManagedSecret = { id: `sec-${Date.now()}`, name, arn: this.formArn().trim() || `arn:aws:secretsmanager:us-east-1:182938491029:secret:${name.toLowerCase()}-pending`, type: this.formType(), schedule: { interval: this.formInterval(), daysInterval: this.formInterval().includes('7') ? 7 : this.formInterval().includes('90') ? 90 : 30, lambdaArn: this.formLambdaArn().trim() || undefined, zeroDowntimeVerified: this.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.', } this.secrets.update((prev) => [newSecret, ...prev]) this.bannerMessage.set({ type: 'success', text: `New secret rotation schedule registered for "${name}".` }) } this.isScheduleModalOpen.set(false) } togglePause(secret: ManagedSecret): void { const nextStatus: RotationStatus = secret.status === 'paused' ? 'healthy' : 'paused' this.secrets.update((prev) => prev.map((s) => (s.id === secret.id ? { ...s, status: nextStatus } : s))) this.bannerMessage.set({ type: 'info', text: `Rotation schedule for "${secret.name}" ${nextStatus === 'paused' ? 'paused' : 'resumed'}.`, }) } openLogsModal(secret: ManagedSecret): void { this.selectedSecretForLogs.set(secret) this.isLogsModalOpen.set(true) } }src/app/components/blocks/secrets-rotation-scheduler/index.ts0.2 kB
export { UiSecretsRotationSchedulerComponent, type SecretType, type RotationStatus, type RotationSchedule, type ManagedSecret, type SecretsRotationStats, } from './secrets-rotation-scheduler.component'
Raw manifest:https://uipkge.dev/r/angular/secrets-rotation-scheduler.json