Files
tessera-ctl/apps/web/src/app/(portal)/modules/cert-manager/actions.ts
T
schalli 64a8e725e7 feat(09-03): InspectTab UI + inspectCertAction + render tests
- Added inspectCertAction to actions.ts (JSON path for pemText, multipart path for file)
- Added CertDetails interface to actions.ts (mirrors API response shape)
- Implemented InspectTab: Analysieren button, loading state, grid-cols-2 result grid
- InspectTab handles wrong-password error (t('error.wrongPassword')) and generic error
- Added 2 new InspectTab tests: success grid (subject CN + SHA-256) and error (text-destructive)
- Fixed setup.ts: explicit expect.extend(matchers) for vitest@4.x compatibility
  (Rule 1: @testing-library/jest-dom/vitest not extending global expect in vitest 4)
- Fixed existing test: getByText -> getAllByText for 'Analysieren' (now appears in tab nav + button)
- 9/9 cert-manager tests pass
2026-07-01 23:55:41 +02:00

110 lines
3.5 KiB
TypeScript

export const API_URL =
process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3001';
// ---------------------------------------------------------------------------
// CertDetails — mirrors the API response shape from CertManagerService
// ---------------------------------------------------------------------------
export interface CertDetails {
subject: { cn: string; o: string; ou: string; c: string };
issuer: { cn: string; o: string; c: string };
validity: { notBefore: string; notAfter: string; isExpired: boolean; daysLeft: number };
san: string[];
keyType: string;
keyBits: number;
serialNumber: string;
signatureAlgorithm: string;
fingerprint: { sha1: string; sha256: string };
pemPreview: string;
}
// ---------------------------------------------------------------------------
// inspectCertAction — calls POST /modules/cert-manager/parse
// ---------------------------------------------------------------------------
/**
* Call POST /modules/cert-manager/parse.
* - If pemText is present → JSON body { pemText, password }
* - Otherwise → multipart FormData with file + optional password
*
* T-09-02: password is never placed in URL, logged, or echoed.
* T-09-04: credentials:'include' ensures JWT cookie is sent.
*/
export async function inspectCertAction(input: {
file?: File | null;
pemText?: string;
password?: string;
}): Promise<CertDetails> {
const { file, pemText, password } = input;
if (pemText) {
// JSON path — content-type must be application/json (not multipart)
const response = await fetch(`${API_URL}/modules/cert-manager/parse`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ pemText, password }),
credentials: 'include',
});
if (!response.ok) {
const body = await response.text().catch(() => '');
throw new Error(`${response.status} ${body}`.trim());
}
return response.json() as Promise<CertDetails>;
} else {
// Multipart path — let browser set Content-Type with boundary
const form = new FormData();
if (file) form.append('file', file);
if (password) form.append('password', password);
return postForm('parse', form) as Promise<CertDetails>;
}
}
/**
* 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();
}