Files
familienarchiv/.specify/features/_example/design.md
Marcel fdc3e4ffa9 feat(sdd): add .specify scaffold — constitution, AGENTS, personas, templates, example, RTM
Introduces the SDD root: a v1.0.0 constitution and machine-readable AGENTS.md
grounded in the project's real conventions; six EARS-aware persona spec-review
checklists that cross-reference .claude/personas/; feature-spec/ADR/threat-model/
api-contract templates; a fully worked _example feature; a living RTM; and an
adrs/ pointer that reuses the existing docs/adr/ archive.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 12:55:26 +02:00

3.7 KiB
Raw Blame History

Design — Profile picture upload

Companion to ./spec.md. The spec says what; this says how, and records the alternatives weighed for the non-obvious choices.

Component overview

ProfileSettings.svelte ──► +page.server.ts (form action)
        (preview, validate)        │ POST /api/users/me/avatar (multipart)
                                    ▼
                         UserAvatarController  ── @RequirePermission(authenticated)
                                    │            ownership/admin check for /{id}
                                    ▼
                            UserService.setAvatar(userId, MultipartFile)
                                    │ validate type+size → ErrorCode
                                    ├──► FileService.put("avatars/{userId}", bytes)   (MinIO)
                                    └──► userRepository.save(user.avatarObjectKey=key)
                                    ▼
                            UserProfileView { …, avatarUrl }

Reads: GET /api/users/{id}/avatar streams the object through the authenticated API (FileService.get), so no public S3 URL is ever exposed. avatarUrl in the view is simply /api/users/{id}/avatar when a key exists, else null.

Key decisions

Decision Choice Why
Where avatars live Existing archive bucket, avatars/{userId} prefix No new bucket/env var/Compose change — see ADR-001.
URL exposure Authenticated proxy endpoint, not a signed/public URL Same auth surface as the rest of the API; no key leakage (Information disclosure).
Object key Deterministic avatars/{userId} (no random suffix) A new upload overwrites the old object — no orphan-cleanup job needed (REQ-001).
avatarObjectKey binding Server-set in UserService only Never bound from request body — prevents pointing a user's avatar at an arbitrary object (Tampering / CWE-639).
Validation site UserService, boundary-only Type + size checked once, at the service boundary, mapped to ErrorCode (constitution §2.3).

Layering & conventions

  • Controller → UserService only; UserService owns userRepository and calls FileService (its public API), never another domain's repository. (constitution §1.21.3)
  • New ErrorCode.AVATAR_TOO_LARGE requires the four-site update (see tasks.md T-1).
  • UserProfileView.avatarUrl is String (nullable) with @Schema describing the proxy path; not marked requiredMode = REQUIRED because it is legitimately null (REQ-004).
  • After backend changes: npm run generate:api regenerates avatarUrl into the TS types.

Non-functional notes

  • Size cap (2 MB, REQ-008) is enforced before the object touches MinIO — the multipart is read into a bounded buffer; Spring's spring.servlet.multipart.max-file-size is set to a matching ceiling so an oversized body is rejected at the container edge too.
  • No N+1 risk: the profile view derives avatarUrl from the already-loaded avatarObjectKey column; no extra query, no S3 round-trip on list/read paths.
  • The proxy GET streams bytes (no full-buffer) and sets a short Cache-Control so an updated avatar propagates quickly.

Test strategy (maps to tasks.md)

Level What Tooling
Unit UserService.setAvatar validation + storage interactions JUnit + Mockito (mock FileService)
Slice controller auth, status codes, error codes @WebMvcTest
E2E upload → preview → confirm → avatar visible; remove → initials Playwright
Component initials placeholder when avatarUrl is null vitest-browser-svelte (*.svelte.spec.ts)