--- phase: quick-261008-j9f plan: 01 subsystem: nextcloud-status tags: [ssrf, logo, validation, i18n, nextcloud-status] requires: [] provides: - "http logo address fetched once by Tessera and stored like an upload" - "shared SSRF guard (apps/api/src/common/public-url-guard.ts)" - "code-based form errors (API + web) for the cloud form" affects: [favorites (re-export only), nextcloud-status] tech-stack: added: [] patterns: ["{ code, message } errors mapped to de/en texts in the web (as in Domains)"] key-files: created: - apps/api/src/common/public-url-guard.ts - apps/api/src/nextcloud-status/nextcloud-form-errors.ts - apps/api/src/nextcloud-status/nextcloud-logo-fetch.ts - apps/api/src/nextcloud-status/nextcloud-logo-fetch.spec.ts - apps/api/src/nextcloud-status/dto/nextcloud-instance.dto.spec.ts - apps/web/src/components/nextcloud-status/cloud-form-errors.ts - apps/web/src/components/nextcloud-status/cloud-form-errors.test.ts modified: - apps/api/src/favorites/icon-discovery.service.ts - apps/api/src/nextcloud-status/dto/nextcloud-instance.dto.ts - apps/api/src/nextcloud-status/nextcloud-status.service.ts - apps/api/src/nextcloud-status/nextcloud-status.service.spec.ts - apps/web/src/lib/nextcloud-status-api.ts - apps/web/src/app/(portal)/modules/nextcloud-status/components/CloudForm.tsx - apps/web/src/app/(portal)/modules/nextcloud-status/components/CloudForm.test.tsx - apps/web/src/messages/de.json - apps/web/src/messages/en.json - apps/web/src/messages/umlaut-dictionary.ts - CHANGELOG.md - docs/anleitung-anwender.md - docs/anleitung-administration.md decisions: - "Only http addresses are downloaded; https stays a browser-loaded logoUrl (D-04)" - "DTO keeps only type guards whose messages are error codes; length and URL rules live in validateInstanceInput / classifyLogoUrl so they arrive as { code, message }" - "Web never renders server text; unknown errors fall back to the generic German save/delete text" metrics: tasks: 3 completed: 2026-10-08 status: complete commits: 3 plan_head_before: 4ff43c225241133c759d729b31cfc324467e00d2 plan_head_after: 166a6fc9d15ae4289f47a0314e102b37ff2380f9 actuals: tokens: 60000 tasks: 3 commits: 3 --- # Phase quick-261008-j9f Plan 01: Nextcloud-Status http logo address Summary An http:// logo address in the Nextcloud cloud form is now downloaded once by Tessera (SSRF-guarded per hop, 8 s total deadline, 1 MiB streaming cap, magic-byte check) and stored in logoData/logoMime like an upload; every error of the form now appears in German (English with the English UI) via stable error codes. ## Commits | Task | Commit | Description | | ---- | ------ | ----------- | | 1 (tracer, API) | 4432561 | shared SSRF guard, fetchLogoImage, error catalogue, DTO codes, service wiring + specs | | 2 (web) | c30a82e | client pre-check, code -> de/en mapping, label "Bildadresse" + new hint, umlaut allowlist | | 3 (docs) | 166a6fc | CHANGELOG (GeƤndert + Behoben), both manuals | ## Measurements - Task 1 gate: nextcloud-status + favorites + rls-coverage/inventory: 18 files, 365 tests green; api tsc clean; favorites spec/service/controller byte-identical to 4ff43c2; tenantPrisma call sites still 14. - New specs: nextcloud-logo-fetch.spec.ts 16 tests, dto spec 3 tests, service spec 32 tests, web cloud-form-errors.test.ts + extended CloudForm.test.tsx. - Full suites: api 137 files / 2525 tests green; web 133 files / 1472 tests green; both `tsc --noEmit` clean. - Biome: clean on all new/changed files (pre-existing findings in untouched favorites files left alone). - de/en parity, 12 error keys, label/hint, old keys removed, no Mandant/Lizenz words: "web form ok". - Umlaut allowlist addition: `Adressen` (correct German, in the new hint). ## Live probe (rebuilt local stack, admin) ``` POST instances logoUrl http://127.0.0.1/logo.png -> 400 {"code":"logoFetchInternal", ...German text} POST instances logoUrl ftp://x -> 400 {"code":"logoUrlInvalid", ...} POST instances customerName "" -> 400 {"code":"customerNameRequired", ...} POST instances customerName 5 -> 400 {"message":["customerNameRequired"]} (DTO message = code) POST instances logoUrl http://example.com/ -> 400 {"code":"logoFetchNotImage", ...} POST instances logoUrl http://upload.wikimedia.org/...png (follows http->https redirect) -> 201, hasUploadedLogo true, logoUrl null GET instances/:id/logo -> 200 image/png ``` The 201 probe row was deleted again; no "Probe j9f" row remains. ## Deviations from Plan None - plan executed as written. Notes: - The user-visible old/new `Cloud nicht gefunden` message changed to the code-based `notFound` text on all cloud routes as the plan specified (not a deviation). - A rejected file type in the upload picker clears the pending file and shows the error; a subsequent submit clears the error and saves without logo (same behaviour as the existing too-large case). ## Known Stubs None. ## Threat Flags None - the new outbound request surface (logo download) is exactly the one covered by T-j9f-01..05 in the plan's threat model. ## Browser steps for the orchestrator (dark mode) 1. Nextcloud-Status -> edit a cloud -> radio "Bildadresse" shows the new hint (https loaded by browser, http fetched once, internal via "Bild hochladen"). 2. Enter a public http:// image address of a real website, save -> tile shows the logo; reopening the form shows "Bild hochladen" selected (stored like an upload). A working example: http://upload.wikimedia.org/wikipedia/commons/4/47/PNG_transparency_demonstration_1.png 3. http://192.168.x.x/logo.png -> German hint pointing to "Bild hochladen", nothing saved. 4. http address of an HTML page (e.g. http://example.com/) -> "Unter dieser Adresse liegt kein Bild ...". 5. Empty name, address without http(s) and an ftp:// logo address -> German texts without any request. 6. https logo address still works as before (browser loads it, logoUrl kept). 7. Switch the UI to English once and repeat (5) -> English texts. 8. Favorites: an existing favorite still shows its icon. ## Self-Check: PASSED - Created files exist (public-url-guard.ts, nextcloud-form-errors.ts, nextcloud-logo-fetch.ts + spec, dto spec, cloud-form-errors.ts + test). - Commits 4432561, c30a82e, 166a6fc exist on main (3 commits measured from plan_head_before).