mirror of
https://github.com/saltbo/zpan.git
synced 2026-09-24 23:22:31 +08:00
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>