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>
64 lines
3.7 KiB
Markdown
64 lines
3.7 KiB
Markdown
# Design — Profile picture upload
|
||
|
||
> Companion to [`./spec.md`](./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](./adr-001-avatars-reuse-archive-bucket.md). |
|
||
| 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.2–1.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`) |
|