# Profile Protocol (Frontend <-> Backend) This document defines the canonical backend contract for user profile read/write and avatar upload signing. Protocol verification status: - Backend model source: `backend/src/models/profile.py` - Storage config source: `backend/src/core/config/settings.py` - Current status: planned ## Compatibility strategy - Current strategy: breaking changes allowed during implementation phase (no production compatibility burden). - Once production compatibility is required, switch to additive-only evolution. ## Route overview - Get profile: `GET /api/v1/users/me/profile` - Update profile: `PATCH /api/v1/users/me/profile` - Create avatar upload url: `POST /api/v1/users/me/avatar/upload-url` - Upload avatar directly: `POST /api/v1/users/me/avatar` (multipart) ## Auth and trust boundary - All routes require authenticated user context. - `user_id` is derived from verified JWT `sub`; never accepted from client payload. ## Profile read contract ### `GET /api/v1/users/me/profile` Response: ```json { "user_id": "uuid", "display_name": "string", "bio": "string|null", "avatar_path": "avatars/{user_id}/{file}", "avatar_url": "https://...signed-or-public...", "settings": { "version": 1, "preferences": { "interface_language": "zh-CN", "ai_language": "zh-CN", "timezone": "Asia/Shanghai", "country": "CN" }, "privacy": {}, "notification": {} }, "updated_at": "2026-04-05T12:34:56+00:00" } ``` Mapping note: - `display_name` maps to `profiles.username`. - `avatar_path` is stored in profile layer. - `avatar_url` is render-ready URL generated from storage strategy. ## Profile update contract ### `PATCH /api/v1/users/me/profile` Request: ```json { "display_name": "string(1..30)", "bio": "string(0..200)", "avatar_path": "avatars/{user_id}/{file}" } ``` Rules: - At least one field must be provided. - `display_name` must be non-empty after trim. - `bio` can be empty string and should be normalized to `null` only if agreed by API implementation. - `avatar_path` must stay in current user prefix: `avatars/{current_user.id}/`. Response: - Returns the same shape as `GET /users/me/profile`. ## Avatar upload signing contract ### `POST /api/v1/users/me/avatar/upload-url` Request: ```json { "mime_type": "image/png|image/jpeg|image/webp", "file_size": 123456, "ext": "png|jpg|jpeg|webp" } ``` Response: ```json { "bucket": "avatars", "path": "avatars/{user_id}/{uuid}.png", "upload_url": "https://...signed...", "expires_in": 600 } ``` Validation rules: - `bucket` must equal `config.storage.avatar.bucket`. - `file_size` must be `>0` and `<= config.storage.avatar.max_size_mb`. - Only image mime types are allowed. - Path must be server-generated and never trusted from client. ## Direct avatar upload contract ### `POST /api/v1/users/me/avatar` Request: - `multipart/form-data` - field name: `file` Validation rules: - extension must be one of `png|jpg|jpeg|webp` - mime must map to image type (`image/png|image/jpeg|image/webp`) - payload size must be `<= config.storage.avatar.max_size_mb` Behavior: - backend writes avatar bytes to `bucket=config.storage.avatar.bucket` - backend stores canonical path in profile - response returns latest profile payload (`ProfileResponse`) ## Error contract linkage - All errors must follow RFC7807 `application/problem+json`. - `code` values must be registered in `docs/protocols/common/http-error-codes.md`.