feat: certificate management with stage/activate/restore lifecycle
All checks were successful
CI / build (push) Successful in 1m8s
CI / docker (push) Successful in 45s

Provider-based architecture (Docker now, K8s later):
- CertificateManager interface + DockerCertificateManager (file-based)
- Atomic swap via .wip files for safe cert replacement
- Stage -> Activate -> Archive lifecycle with one-deep rollback
- Bootstrap supports user-supplied certs via CERT_FILE/KEY_FILE/CA_FILE
- CA bundle aggregates platform + tenant CAs, distributed to containers
- Vendor UI: Certificates page with upload, activate, restore, discard
- Stale tenant tracking (ca_applied_at) with restart banner
- Conditional TLS skip removal when CA bundle exists

Includes design spec, migration V012, service + controller tests.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
hsiegeln
2026-04-10 18:29:02 +02:00
parent 51a1aef10e
commit 45bcc954ac
23 changed files with 2035 additions and 7 deletions

View File

@@ -0,0 +1,68 @@
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { api } from './client';
export interface CertificateResponse {
id: string;
status: string;
subject: string;
issuer: string;
notBefore: string;
notAfter: string;
fingerprint: string;
hasCa: boolean;
selfSigned: boolean;
activatedAt: string | null;
archivedAt: string | null;
}
export interface CertificateOverview {
active: CertificateResponse | null;
staged: CertificateResponse | null;
archived: CertificateResponse | null;
staleTenantCount: number;
}
export interface StageResponse {
valid: boolean;
errors: string[];
certificate: CertificateResponse | null;
}
export function useVendorCertificates() {
return useQuery<CertificateOverview>({
queryKey: ['vendor', 'certificates'],
queryFn: () => api.get('/vendor/certificates'),
});
}
export function useStageCertificate() {
const qc = useQueryClient();
return useMutation<StageResponse, Error, FormData>({
mutationFn: (formData) => api.post('/vendor/certificates/stage', formData),
onSuccess: () => qc.invalidateQueries({ queryKey: ['vendor', 'certificates'] }),
});
}
export function useActivateCertificate() {
const qc = useQueryClient();
return useMutation<void, Error, void>({
mutationFn: () => api.post('/vendor/certificates/activate'),
onSuccess: () => qc.invalidateQueries({ queryKey: ['vendor', 'certificates'] }),
});
}
export function useRestoreCertificate() {
const qc = useQueryClient();
return useMutation<void, Error, void>({
mutationFn: () => api.post('/vendor/certificates/restore'),
onSuccess: () => qc.invalidateQueries({ queryKey: ['vendor', 'certificates'] }),
});
}
export function useDiscardStaged() {
const qc = useQueryClient();
return useMutation<void, Error, void>({
mutationFn: () => api.delete('/vendor/certificates/staged'),
onSuccess: () => qc.invalidateQueries({ queryKey: ['vendor', 'certificates'] }),
});
}