193 lines
15 KiB
Markdown
193 lines
15 KiB
Markdown
---
|
|
phase: 09-cert-manager-module
|
|
plan: 02
|
|
type: execute
|
|
wave: 1
|
|
depends_on: []
|
|
files_modified:
|
|
- apps/web/src/messages/de.json
|
|
- apps/web/src/messages/en.json
|
|
- apps/web/src/app/(portal)/modules/cert-manager/page.tsx
|
|
- apps/web/src/app/(portal)/modules/cert-manager/actions.ts
|
|
- apps/web/src/app/(portal)/modules/cert-manager/components/DropZone.tsx
|
|
- apps/web/src/app/(portal)/modules/cert-manager/components/PasswordField.tsx
|
|
- apps/web/src/app/(portal)/modules/cert-manager/components/InspectTab.tsx
|
|
- apps/web/src/app/(portal)/modules/cert-manager/components/SplitTab.tsx
|
|
- apps/web/src/app/(portal)/modules/cert-manager/components/MergeTab.tsx
|
|
- apps/web/src/app/(portal)/modules/cert-manager/components/ConvertTab.tsx
|
|
- apps/web/src/app/(portal)/modules/cert-manager/cert-manager.test.tsx
|
|
autonomous: true
|
|
requirements: [CERT-06]
|
|
|
|
must_haves:
|
|
truths:
|
|
- "The /modules/cert-manager page renders the title, description and four tabs (Analysieren, Aufteilen, Zusammenfuehren, Konvertieren)"
|
|
- "The shared input card shows a drag-and-drop DropZone, an OR divider, a PEM textarea, and a conditionally-shown password field"
|
|
- "Selecting a .pfx/.p12 file OR choosing PFX output reveals the password field; otherwise it is hidden"
|
|
- "The certManager i18n namespace resolves in both de.json and en.json with no missing keys"
|
|
artifacts:
|
|
- "apps/web/src/app/(portal)/modules/cert-manager/page.tsx (tab shell + shared input state)"
|
|
- "apps/web/src/app/(portal)/modules/cert-manager/actions.ts (API_URL, downloadBase64, postForm helpers)"
|
|
- "apps/web/src/app/(portal)/modules/cert-manager/components/DropZone.tsx + PasswordField.tsx + 4 tab stubs"
|
|
- "certManager namespace in de.json and en.json"
|
|
- "apps/web/src/app/(portal)/modules/cert-manager/cert-manager.test.tsx (shell tests)"
|
|
key_links:
|
|
- "page.tsx passes { file, pemText, password } down to the active tab component"
|
|
- "useTranslations('certManager') resolves keys defined in messages/de.json + en.json"
|
|
---
|
|
|
|
<objective>
|
|
Build the frontend shell for the cert-manager module: the tab-based page, shared input card (DropZone + PEM textarea + conditional password field), reusable download/fetch helpers, empty tab-component stubs, and the full `certManager` i18n namespace in German and English.
|
|
|
|
MVP framing — this delivers the visible half of the first vertical slice: after this plan a user who activates the module can open `/modules/cert-manager`, see all four tabs and the input card, and read localized copy, even though no operation is wired yet.
|
|
|
|
Purpose: Every feature slice (Inspect, Split, Convert, Merge) fills in one tab component and one action against this shell.
|
|
Output: Rendering cert-manager page + localized strings + shared client helpers.
|
|
</objective>
|
|
|
|
<execution_context>
|
|
@$HOME/.claude/gsd-core/workflows/execute-plan.md
|
|
@$HOME/.claude/gsd-core/templates/summary.md
|
|
</execution_context>
|
|
|
|
<context>
|
|
@.planning/PROJECT.md
|
|
@.planning/ROADMAP.md
|
|
@.planning/STATE.md
|
|
@.planning/phases/09-cert-manager-module/09-CONTEXT.md
|
|
@.planning/phases/09-cert-manager-module/09-UI-SPEC.md
|
|
@.planning/phases/09-cert-manager-module/09-PATTERNS.md
|
|
@apps/web/src/app/(portal)/modules/domaincheck/page.tsx
|
|
@apps/web/src/app/(portal)/modules/domaincheck/actions.ts
|
|
@apps/web/src/app/(portal)/modules/dkv-fleet/settings/components/CsvImportButton.tsx
|
|
@apps/web/src/messages/de.json
|
|
</context>
|
|
|
|
<artifacts>
|
|
## Artifacts this plan produces
|
|
|
|
- New route page: `CertManagerPage` (default export, 'use client') at `apps/web/src/app/(portal)/modules/cert-manager/page.tsx`
|
|
- New symbols in `actions.ts`: `API_URL` const, `downloadBase64(filename, content, mimeType)`, `postForm(endpoint, form)` (fetch wrapper, credentials:'include', throws on !ok)
|
|
- New components: `DropZone` (props: onFile, accept), `PasswordField` (props: value, onChange, show), `InspectTab`, `SplitTab`, `MergeTab`, `ConvertTab` (each props: file, pemText, password — render empty state for now)
|
|
- New i18n namespace `certManager` added to `apps/web/src/messages/de.json` and `en.json`
|
|
- New test file: `apps/web/src/app/(portal)/modules/cert-manager/cert-manager.test.tsx`
|
|
</artifacts>
|
|
|
|
<tasks>
|
|
|
|
<task type="auto">
|
|
<name>Task 1: Add certManager i18n namespace (de + en)</name>
|
|
<files>apps/web/src/messages/de.json, apps/web/src/messages/en.json</files>
|
|
<read_first>
|
|
- apps/web/src/messages/de.json (locate the existing domaincheck namespace; append certManager as a sibling — do not restructure)
|
|
- apps/web/src/messages/en.json (same)
|
|
- .planning/phases/09-cert-manager-module/09-RESEARCH.md (i18n Namespace Structure de.json — canonical key set)
|
|
- .planning/phases/09-cert-manager-module/09-UI-SPEC.md (Copywriting Contract — exact German strings)
|
|
</read_first>
|
|
<action>
|
|
Add a certManager namespace to de.json using the exact keys and German strings from the RESEARCH i18n Namespace Structure and the UI-SPEC Copywriting Contract: title 'Zertifikat-Manager', description 'Zertifikate analysieren, aufteilen, zusammenfuehren und konvertieren.', tabs.{inspect,split,merge,convert} = Analysieren/Aufteilen/Zusammenfuehren/Konvertieren, dropZone.{placeholder,formats}, paste.placeholder, password.label 'Passwort (PFX/P12)', or 'oder', actions.{inspect,split,merge,convert,download,processing}, emptyState.{inspect,inspectBody,split,splitBody,merge,mergeBody,convert,convertBody}, error.{generic,wrongPassword,unknownFormat}. Use the exact umlaut spellings from UI-SPEC. Add the same key structure to en.json with English equivalents. Preserve existing JSON ordering/formatting; append the namespace only.
|
|
</action>
|
|
<verify>
|
|
<automated>node -e "const de=require('./apps/web/src/messages/de.json'); const en=require('./apps/web/src/messages/en.json'); const k=Object.keys(de.certManager.tabs).sort().join(','); if(k!=='convert,inspect,merge,split') throw new Error('de tabs '+k); if(!en.certManager.actions.download) throw new Error('en missing download'); console.log('I18N_OK');"</automated>
|
|
</verify>
|
|
<acceptance_criteria>
|
|
- `de.certManager.title` equals 'Zertifikat-Manager' and `de.certManager.tabs.merge` equals 'Zusammenfuehren'
|
|
- de.json and en.json share identical key paths under certManager (same tabs, actions, emptyState, error keys)
|
|
- `pnpm --filter @tessera/web type-check` still passes (valid JSON)
|
|
</acceptance_criteria>
|
|
<done>certManager namespace exists in both de.json and en.json with the full key set; German copy matches the UI-SPEC Copywriting Contract.</done>
|
|
</task>
|
|
|
|
<task type="auto">
|
|
<name>Task 2: Build page shell, shared input card, DropZone, PasswordField, tab stubs, and client helpers</name>
|
|
<files>apps/web/src/app/(portal)/modules/cert-manager/page.tsx, apps/web/src/app/(portal)/modules/cert-manager/actions.ts, apps/web/src/app/(portal)/modules/cert-manager/components/DropZone.tsx, apps/web/src/app/(portal)/modules/cert-manager/components/PasswordField.tsx, apps/web/src/app/(portal)/modules/cert-manager/components/InspectTab.tsx, apps/web/src/app/(portal)/modules/cert-manager/components/SplitTab.tsx, apps/web/src/app/(portal)/modules/cert-manager/components/MergeTab.tsx, apps/web/src/app/(portal)/modules/cert-manager/components/ConvertTab.tsx</files>
|
|
<read_first>
|
|
- apps/web/src/app/(portal)/modules/domaincheck/page.tsx (client component + useTranslations + Card layout + loading/error state pattern)
|
|
- apps/web/src/app/(portal)/modules/domaincheck/actions.ts (fetch wrapper + credentials:'include' pattern)
|
|
- apps/web/src/app/(portal)/modules/dkv-fleet/settings/components/CsvImportButton.tsx (hidden file input + drag-over DropZone pattern)
|
|
- .planning/phases/09-cert-manager-module/09-UI-SPEC.md (Layout Contract, Conditional Elements, Interaction Contract, Color/Spacing/Typography)
|
|
- .planning/phases/09-cert-manager-module/09-PATTERNS.md (page.tsx header/layout pattern, DropZone pattern, actions.ts pattern, downloadBase64 helper)
|
|
</read_first>
|
|
<action>
|
|
Create page.tsx as a 'use client' component using useTranslations('certManager'). State: activeTab ('inspect'|'split'|'merge'|'convert'), file (File|null), pemText (string), password (string). Layout per UI-SPEC: max-w-4xl mx-auto p-6 space-y-6; header (h1 text-2xl font-bold tracking-tight + p text-sm text-muted-foreground); shared input Card (rounded-lg border border-border bg-card p-6 shadow-sm space-y-4) containing DropZone, an 'oder' divider (t('or')), a PEM textarea (t('paste.placeholder')), and PasswordField shown only when the selected file extension is .pfx/.p12 OR activeTab is 'merge' with PFX output (pass a `show` prop). Tab nav (border-b border-border flex gap-6; active tab border-b-2 border-primary text-foreground, inactive text-muted-foreground). Tab content Card renders the active tab component, passing { file, pemText, password }. Selecting a file clears pemText and vice versa (single active source per Interaction Contract). Changing the active tab clears the previous tab's result but keeps the shared input.
|
|
Create components/DropZone.tsx per PATTERNS DropZone pattern: hidden file input, click-to-browse, drag-over highlight (border-primary bg-primary/5), accept prop, calls onFile; reset e.target.value to allow re-selecting the same file. Use i18n for placeholder text.
|
|
Create components/PasswordField.tsx: input[type=password] with a show/hide toggle rendered as an inline SVG eye icon (no external icon lib per UI-SPEC); props value, onChange, show (render null when show is false — no reflow). Label from t('password.label').
|
|
Create components/InspectTab.tsx, SplitTab.tsx, MergeTab.tsx, ConvertTab.tsx as stubs: each accepts { file, pemText, password } and renders the corresponding empty state from t('emptyState.*'). No API calls yet — later slices fill these in.
|
|
Create actions.ts with: API_URL const (process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3001'); downloadBase64(filename, content, mimeType) per PATTERNS (atob -> Uint8Array -> Blob -> object URL -> anchor click -> revoke); a postForm(endpoint, form) helper that fetches `${API_URL}/modules/cert-manager/${endpoint}` with method POST, body form, credentials 'include', no manual Content-Type, and throws Error(`${status} ${body}`) on !response.ok, else returns response.json().
|
|
All strings via t(); no hardcoded UI copy. No shadcn, no Radix, Tailwind utilities only.
|
|
</action>
|
|
<verify>
|
|
<automated>pnpm --filter @tessera/web test cert-manager --run</automated>
|
|
</verify>
|
|
<acceptance_criteria>
|
|
- `apps/web/src/app/(portal)/modules/cert-manager/page.tsx` starts with `'use client'` and calls `useTranslations('certManager')`
|
|
- `grep -q "max-w-4xl" apps/web/src/app/(portal)/modules/cert-manager/page.tsx`
|
|
- `grep -q "credentials: 'include'" apps/web/src/app/(portal)/modules/cert-manager/actions.ts`
|
|
- `grep -q "URL.createObjectURL" apps/web/src/app/(portal)/modules/cert-manager/actions.ts`
|
|
- PasswordField renders nothing when `show` is false and renders an input[type=password] with a toggle when true
|
|
- `pnpm --filter @tessera/web type-check` exits 0
|
|
</acceptance_criteria>
|
|
<done>The page renders the header, four tabs, shared input card with DropZone + textarea + conditional PasswordField, and delegates to tab stubs; actions.ts exposes downloadBase64 + postForm helpers.</done>
|
|
</task>
|
|
|
|
<task type="auto" tdd="true">
|
|
<name>Task 3: Shell render tests</name>
|
|
<files>apps/web/src/app/(portal)/modules/cert-manager/cert-manager.test.tsx</files>
|
|
<read_first>
|
|
- apps/web/vitest.config.ts (jsdom env, globals, setupFiles ./src/test/setup.ts)
|
|
- apps/web/src/test/setup.ts (existing test setup — how providers/i18n are wired for tests)
|
|
- Any existing *.test.tsx under apps/web/src/app/(portal)/modules (reference NextIntlClientProvider wiring in component tests)
|
|
</read_first>
|
|
<behavior>
|
|
- Test: page renders the title 'Zertifikat-Manager' and all four tab labels (Analysieren, Aufteilen, Zusammenfuehren, Konvertieren).
|
|
- Test: the password field is NOT in the document on initial render (no PFX file, inspect tab).
|
|
- Test: each tab, when active, shows its empty-state text from certManager.emptyState.
|
|
</behavior>
|
|
<action>
|
|
Create cert-manager.test.tsx rendering CertManagerPage wrapped in NextIntlClientProvider with the de messages (follow the existing module component-test wiring found in read_first). Implement the Behavior assertions using @testing-library/react queries (getByText / queryByLabelText). Write the tests to describe the shell contract; they should pass against the Task 2 implementation (GREEN). If the shell is missing anything they assert, fix the shell.
|
|
</action>
|
|
<verify>
|
|
<automated>pnpm --filter @tessera/web test cert-manager --run</automated>
|
|
</verify>
|
|
<acceptance_criteria>
|
|
- `pnpm --filter @tessera/web test cert-manager --run` exits 0 with the title, tab-label, hidden-password, and empty-state assertions passing
|
|
- The test file imports NextIntlClientProvider and renders with de messages
|
|
</acceptance_criteria>
|
|
<done>Shell render tests are green: title, four tabs, hidden password field, and per-tab empty states are asserted.</done>
|
|
</task>
|
|
|
|
</tasks>
|
|
|
|
<threat_model>
|
|
## Trust Boundaries
|
|
|
|
| Boundary | Description |
|
|
|----------|-------------|
|
|
| browser -> API | Client sends uploaded cert bytes + optional password to the API via fetch |
|
|
|
|
## STRIDE Threat Register
|
|
|
|
| Threat ID | Category | Component | Severity | Disposition | Mitigation Plan |
|
|
|-----------|----------|-----------|----------|-------------|-----------------|
|
|
| T-09-02 | Information Disclosure | PasswordField / actions.ts | high | mitigate | Password is held in local React state and sent only in the FormData body over the authenticated fetch; never placed in URL query, console.log, or download filename |
|
|
| T-09-04 | Elevation of Privilege | client fetch to /modules/cert-manager/* | high | mitigate | All requests use `credentials: 'include'`; the API enforces JwtAuthGuard + ModuleGuard, so an unauthenticated/unactivated client cannot process certs |
|
|
| T-09-05 | Tampering | client-side accept filter | low | accept | `accept=".pem,.crt,..."` is a UX guard only; real validation happens server-side in the API (Plan 01/03+) — client filter is not a security boundary |
|
|
</threat_model>
|
|
|
|
<verification>
|
|
- `pnpm --filter @tessera/web test cert-manager --run` — shell tests green
|
|
- `pnpm --filter @tessera/web type-check` — web compiles
|
|
- Manual (deferred to phase gate): open /modules/cert-manager after activation, confirm tabs, DropZone drag highlight, and password field toggling on .pfx selection
|
|
</verification>
|
|
|
|
<success_criteria>
|
|
- certManager i18n namespace complete in de + en
|
|
- Page shell renders title, four tabs, shared input card, conditional password field
|
|
- actions.ts exposes downloadBase64 + postForm; no shadcn/Radix used
|
|
- Shell tests green; web type-checks clean
|
|
</success_criteria>
|
|
|
|
<output>
|
|
Create `.planning/phases/09-cert-manager-module/09-02-SUMMARY.md` when done
|
|
</output>
|