Files
familienarchiv/.specify/features/_example/spec.md
Marcel 01f51854f6 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 11:56:31 +02:00

8.5 KiB

As a user I want to upload a profile picture so other family members recognise me

This is the canonical worked example for SDD in this repo. It is fictional but realistic, chosen because no real avatar feature exists in the codebase. Use it as the reference shape for a real spec.md. Every section is filled — no placeholders.

Context & Why

Readers and transcribers collaborate in threads and on document comments, but every user is currently represented by initials only. Letting a user upload a small profile picture makes the activity feed, comments, and the public user profile page (/users/[id]) more personal and easier to scan — directly serving the family-archive product goal of feeling like a shared family space, not a database.

Constitution principles this feature depends on:

Related: builds on the existing FileService (MinIO) used by Document uploads.

User Journey

A logged-in user opens their profile settings (/profile), clicks "Profilbild ändern", selects a PNG or JPEG from their device, sees an instant preview, and confirms. The picture replaces their initials everywhere their name appears. They can later remove it and fall back to initials. An admin (with ADMIN_USER) can remove an inappropriate picture from another user's account from the admin user view.

Requirements

  • REQ-001 (Ubiquitous) — The user service shall store each profile picture as a single object in the existing archive bucket under the key avatars/{userId}, overwriting any previous object for that user.
  • REQ-002 (Event-driven) — When an authenticated user sends POST /api/users/me/avatar with a valid image, the user service shall store the image, set the user's avatarObjectKey, and return the updated profile view including a non-null avatarUrl.
  • REQ-003 (Event-driven) — When an authenticated user sends DELETE /api/users/me/avatar, the user service shall delete the stored object, clear avatarObjectKey, and return the profile view with avatarUrl = null.
  • REQ-004 (State-driven) — While a user has no stored avatar, the profile view for that user shall return avatarUrl = null and the frontend shall render the initials placeholder.
  • REQ-005 (Optional-feature) — Where the caller holds Permission.ADMIN_USER, the user service shall allow DELETE /api/users/{id}/avatar to remove another user's avatar.
  • REQ-006 (Unwanted-behavior) — If the request to any avatar endpoint is unauthenticated, then the system shall return 401 with ErrorCode.UNAUTHORIZED and store or delete nothing.
  • REQ-007 (Unwanted-behavior) — If the uploaded file's content type is not image/png or image/jpeg, then the user service shall return 400 ErrorCode.UNSUPPORTED_FILE_TYPE and store nothing.
  • REQ-008 (Unwanted-behavior) — If the uploaded file exceeds 2 MB, then the user service shall return 400 ErrorCode.AVATAR_TOO_LARGE and store nothing.
  • REQ-009 (Unwanted-behavior) — If a caller without Permission.ADMIN_USER targets another user's avatar via /api/users/{id}/avatar, then the system shall return 403 ErrorCode.FORBIDDEN and modify nothing.

Acceptance Criteria

  • REQ-001 — After a successful upload, exactly one object exists at avatars/{userId}; a second upload leaves exactly one object (no orphan), verified by a FileService interaction test.
  • REQ-002POST /api/users/me/avatar with a 100 KB PNG returns 200 and a body whose avatarUrl is a non-null string; the persisted app_users.avatar_object_key equals avatars/{userId}.
  • REQ-003DELETE /api/users/me/avatar returns 200, the object is gone, and the response avatarUrl is null.
  • REQ-004GET profile view for a user with avatar_object_key IS NULL returns avatarUrl: null; the rendered component shows a 2-letter initials placeholder (Playwright).
  • REQ-005 — An ADMIN_USER caller deleting another user's avatar returns 200; the target's avatar_object_key becomes NULL.
  • REQ-006 — An unauthenticated POST/DELETE returns 401; bucket object count is unchanged.
  • REQ-007 — A text/plain or application/pdf upload returns 400 UNSUPPORTED_FILE_TYPE; bucket object count is unchanged.
  • REQ-008 — A 2.1 MB PNG returns 400 AVATAR_TOO_LARGE; bucket object count is unchanged.
  • REQ-009 — A non-admin caller targeting another user's id returns 403 FORBIDDEN; the target's avatar_object_key is unchanged.

Out of Scope

  • Image cropping, resizing, or transformation — the client sends a final image; the server stores it verbatim within the size limit.
  • Avatars for historical Person entities — this feature is for AppUser accounts only (Person ≠ AppUser).
  • Gravatar / external avatar providers.
  • Animated formats (GIF/WebP) — PNG and JPEG only in v1.

API / Contract Stub

See ./api-contract.yaml. Endpoints: POST /api/users/me/avatar (multipart), DELETE /api/users/me/avatar, DELETE /api/users/{id}/avatar (ADMIN_USER). The profile view gains an optional avatarUrl: string | null. All mutating endpoints carry @RequirePermissionme endpoints require an authenticated session; the {id} delete requires ADMIN_USER.

Data Model Changes

  • Add nullable avatar_object_key VARCHAR(512) to app_users.
  • Flyway V78__add_app_user_avatar_object_key.sql (next free number — verify against backend/src/main/resources/db/migration/ on disk before committing).
  • Rollback: forward-only. Reverse manually with ALTER TABLE app_users DROP COLUMN avatar_object_key;. The MinIO avatars/ objects are orphaned but harmless on rollback and can be pruned with mc rm --recursive.

Security Considerations

STRIDE categories touched: Tampering (mass-assignment of avatarObjectKey if bound from body), Elevation of privilege (a non-admin modifying another user's avatar — REQ-009), Denial of service (oversized upload — REQ-008), Information disclosure (avatar URL must not expose a signed key that bypasses auth). No AI agent involved, so ASTRIDE does not apply. Full analysis: ./threat-model.md.

Open Questions

All resolved before implementation.

  • Public or signed avatar URL? — Resolved: served through an authenticated GET /api/users/{id}/avatar proxy (same auth as the rest of the API), not a public S3 URL.
  • New bucket or reuse archive bucket? — Resolved: reuse the archive bucket under an avatars/ prefix; see ./adr-001-avatars-reuse-archive-bucket.md.

Traceability

REQ-ID Task ID(s) Test ID(s) Status
REQ-001 T-3 UserServiceAvatarTest#storesUnderUserKey, …#replaceLeavesNoOrphan Planned
REQ-002 T-4 UserAvatarControllerTest#uploadReturnsAvatarUrl Planned
REQ-003 T-5 UserAvatarControllerTest#deleteClearsAvatar Planned
REQ-004 T-7 avatar-placeholder.svelte.spec.ts Planned
REQ-005 T-6 UserAvatarControllerTest#adminDeletesOthersAvatar Planned
REQ-006 T-2 UserAvatarControllerTest#unauthenticatedReturns401 Planned
REQ-007 T-2 UserAvatarControllerTest#rejectsNonImage Planned
REQ-008 T-2 UserAvatarControllerTest#rejectsOversize Planned
REQ-009 T-6 UserAvatarControllerTest#nonAdminForbiddenOnOthers Planned

Mirrored in .specify/rtm.md.

Persona Review Results

Persona Status Key Findings Resolved
Requirements Engineer APPROVE All 9 REQ ids EARS-formed; every limit has an If clause.
Developer APPROVE Reuses FileService/UserService; AVATAR_TOO_LARGE four-site update listed (T-1).
Security APPROVE REQ-006/008/009 cover authn/DoS/EoP; avatarObjectKey server-set only (see threat model T-1). Yes
DevOps APPROVE V78 forward-only with rollback note; no new bucket/env var.
UI/UX APPROVE Placeholder + loading/error states specified; strings via i18n (T-8).
Architect APPROVE Bucket-reuse decision captured in ADR-001; no new domain, no boundary crossing. Yes