feat(09-02): build cert-manager page shell, components, and client helpers

- CertManagerPage: 'use client', useTranslations('certManager'), max-w-4xl layout
- Shared input card with DropZone, OR divider, PEM textarea, conditional PasswordField
- PasswordField renders null when show=false (T-09-02 threat mitigation)
- Tab nav: Analysieren / Aufteilen / Zusammenfuehren / Konvertieren
- Tab stubs: InspectTab, SplitTab, MergeTab, ConvertTab (empty state only)
- actions.ts: API_URL const, downloadBase64(atob->Blob->URL), postForm(credentials:'include')
- File/paste mutual exclusion: selecting one clears the other
- No shadcn/Radix; Tailwind utilities only; inline SVG eye icon
This commit is contained in:
2026-07-01 23:20:10 +02:00
parent 82a80e7634
commit 42a41f77d0
8 changed files with 457 additions and 0 deletions
@@ -0,0 +1,51 @@
export const API_URL =
process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3001';
/**
* Download a base64-encoded file as a browser download.
* T-09-02: password is never placed in URL, console.log, or filename.
*/
export function downloadBase64(
filename: string,
content: string,
mimeType: string,
): void {
const bytes = atob(content);
const byteArray = new Uint8Array(bytes.length);
for (let i = 0; i < bytes.length; i++) {
byteArray[i] = bytes.charCodeAt(i);
}
const blob = new Blob([byteArray], { type: mimeType });
const url = URL.createObjectURL(blob);
const anchor = document.createElement('a');
anchor.href = url;
anchor.download = filename;
anchor.click();
URL.revokeObjectURL(url);
}
/**
* POST a FormData payload to a cert-manager endpoint.
* T-09-04: credentials:'include' ensures JWT cookie is sent for ModuleGuard.
* No manual Content-Type header — browser sets multipart boundary automatically.
*/
export async function postForm(
endpoint: string,
form: FormData,
): Promise<unknown> {
const response = await fetch(
`${API_URL}/modules/cert-manager/${endpoint}`,
{
method: 'POST',
body: form,
credentials: 'include',
},
);
if (!response.ok) {
const body = await response.text().catch(() => '');
throw new Error(`${response.status} ${body}`.trim());
}
return response.json();
}