Files
zpan/shared/schemas
Jasper VanandClaude Opus 4.7 78f741c3d7 refactor: replace presign+commit image flow with RESTful PUT/DELETE (#336)
Avatar upload (T7 #327) was a 3-endpoint presigned-URL flow:
  POST   /api/profile/avatar        (presign)
  POST   /api/profile/avatar/commit (verify + write)
  DELETE /api/profile/avatar

This mixed two anti-patterns: (1) an action verb `/commit` in the URL
and (2) two-phase client orchestration per upload. Closed PR #335 was
extending the same pattern to org logo — 6 endpoints for what's
conceptually one operation ("replace this image").

Collapse to two clean REST resources:

  PUT    /api/me/avatar             (multipart/form-data, file field)
  DELETE /api/me/avatar
  PUT    /api/teams/:teamId/logo    (multipart/form-data, file field)
  DELETE /api/teams/:teamId/logo

PUT is idempotent — re-uploading produces the same resource state,
matching "set the avatar" semantics. Stream-proxy through Worker
(read bytes → putObject → headObject no longer needed since we just
wrote it → DB update → return public URL). Zero client orchestration:
one fetch per user action.

### Backend

- NEW `server/services/image-upload.ts` — shared `uploadPublicImage` +
  `deletePublicImageVariants` helpers. Both routes use them, zero
  duplication. Constants `PUBLIC_IMAGE_MIMES` (png/jpg/webp) and
  `MAX_PUBLIC_IMAGE_SIZE` (2 MiB) live in shared/schemas for client +
  server reuse.
- NEW `server/routes/me.ts` — `/api/me/*` namespace for session-scoped
  resources. Separate from `/api/profiles/:username` (public read-only).
- EXTENDED `server/routes/teams.ts` with `:teamId/logo` PUT/DELETE.
  Owner/admin only via `getMemberRole`.
- REMOVED avatar endpoints from `server/routes/profile.ts` and the
  `profileMe` mount from `server/app.ts`.
- REMOVED `AVATAR_MIMES` / `requestAvatarUploadSchema` /
  `commitAvatarSchema` from shared/schemas; superseded by the simpler
  constants above.

### Frontend

- Hono RPC client: `profileMeApi` → `meApi` rename; new DELETE wrappers
  go through RPC for type safety. PUT goes through raw fetch
  (multipart/form-data — Hono RPC doesn't express it cleanly).
- NEW wrappers: `uploadAvatar(file)`, `deleteAvatar()`,
  `uploadTeamLogo(teamId, file)`, `deleteTeamLogo(teamId)`.
- REMOVED wrappers: `requestAvatarUpload`, `commitAvatar`, the old
  `deleteAvatar` (3 calls → 2).
- Settings Profile AvatarCard: one mutation (upload) instead of three
  (presign → uploadToS3 → commit). Same UX, fewer round trips + less
  code.
- Teams settings page: redesigned to the Vercel-style card layout that
  #334 established for other settings tabs (LogoCard / TeamNameCard /
  SlugCard / DangerZoneCard). Logo uses hover-to-upload (Cal.com
  pattern) — click avatar → camera overlay → file picker.

### Tests

- NEW `server/routes/me.integration.test.ts` — 10 cases covering auth
  (401), Content-Type validation (415), missing file (400), mime
  rejection (400), size > 2 MiB (413), no public storage (503), happy
  path, PUT idempotency, DELETE authoritative DB clear, graceful
  fallback when no public storage.
- EXTENDED `server/routes/teams.integration.test.ts` with 11 logo cases
  mirroring the above + owner-vs-admin permission matrix.
- REMOVED avatar tests from `server/routes/profile.integration.test.ts`
  (those endpoints no longer exist).
- Frontend `src/lib/api.test.ts`: 4 new test blocks for the 4 new
  wrappers — path/method/form-body/error assertions, plus URL-encoding
  check for teamId in the team logo wrapper.

Total: 83 test files, 2587 tests all green (+~15 new cases; the rest
was replacing avatar tests 1:1 with new PUT-based equivalents).

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 22:56:27 -04:00
..