Feature: Implement User Profile Endpoints — GET, PATCH & DELETE /api/v1/users/me
Problem
Authenticated users have no way to view their own profile, update their display name or language preferences, upload an avatar, or delete their account. Without these endpoints, the platform cannot support any personalisation or account management beyond initial registration. Account deletion is also a legal requirement (GDPR Right to Erasure) and must support both a grace-period soft delete and a permanent hard delete.
Proposed Solution
Implement three endpoints under /api/v1/users/me, all protected by the get_current_user dependency:
GET /me — returns the authenticated user's full profile.
PATCH /me — allows partial updates to display name, language preferences, and avatar.
DELETE /me — soft-deletes the account by default, with a ?hard=true query parameter for permanent deletion.
User Stories
- As a logged-in user, I want to retrieve my profile details, so I can verify my current settings and display them in the UI.
- As a user, I want to update my display name, speaking language, and listening language without re-authenticating, so I can personalise my experience at any time.
- As a user, I want to upload a profile picture that is visible to other meeting participants, so they can identify me during calls.
- As a user who wants to leave the platform, I want to delete my account and have my personal data removed, so my data is not retained longer than I consent to.
GET /api/v1/users/me — Get Current User Profile
Acceptance Criteria
- Requires a valid
Authorization: Bearer <access_token> header.
- Returns
200 OK with the authenticated user's public profile:
{
"id": 1,
"email": "user@example.com",
"full_name": "Ada Lovelace",
"avatar_url": "https://res.cloudinary.com/fluentmeet/...",
"speaking_language": "en",
"listening_language": "fr",
"is_active": true,
"is_verified": true,
"created_at": "2026-03-14T12:00:00Z"
}
hashed_password, deleted_at, and other internal fields are never returned.
PATCH /api/v1/users/me — Update Profile
Acceptance Criteria
- Requires a valid
Authorization: Bearer <access_token> header.
- Accepts
application/json for text fields or multipart/form-data when an avatar file is included. All fields are optional (partial update):
{
"full_name": "Ada K. Lovelace",
"speaking_language": "en",
"listening_language": "de"
}
- Avatar upload (when provided as
multipart/form-data):
- File must be JPEG, PNG, or WebP; max 5 MB.
- Uploaded via
StorageService (see External Services issue).
- The returned public URL is stored in
user.avatar_url.
- The old avatar is deleted from cloud storage before setting the new URL.
- If no new avatar is provided, the existing
avatar_url is unchanged.
speaking_language and listening_language must be valid BCP-47 language codes; invalid values return 400 Bad Request with code INVALID_LANGUAGE_CODE.
- Only the fields provided in the request body are updated (true partial update —
PATCH semantics).
- Returns
200 OK with the updated user profile (same schema as GET /me).
DELETE /api/v1/users/me — Account Deletion
Acceptance Criteria
- Requires a valid
Authorization: Bearer <access_token> header.
- Accepts an optional
?hard=true query parameter to distinguish between:
- Soft delete (default,
?hard=false or omitted): Sets user.deleted_at = now() and user.is_active = False. The account record is retained in the database.
- Hard delete (
?hard=true): Permanently deletes the User record and all associated data (verification tokens, reset tokens, room participations) from the database.
- In both cases, after the DB operation:
revoke_all_user_tokens(email) is called to invalidate all active refresh tokens.
- The
HttpOnly refresh token cookie is cleared (Max-Age=0).
- The current access token
jti is blacklisted in Redis.
- For soft delete, any subsequent
/login attempt returns 403 Forbidden with code ACCOUNT_DELETED (already enforced in the login endpoint).
- For hard delete, the user's avatar is deleted from cloud storage as part of the cleanup.
- Returns
200 OK on success:
{ "status": "ok", "message": "Account has been successfully deleted." }
- A soft-deleted account cannot be restored via the API — restoration requires a database admin operation.
Proposed Technical Details
- Router:
app/api/v1/endpoints/users.py [NEW] — all three routes registered under an APIRouter(prefix="/users", tags=["users"]).
- Schemas in
app/schemas/user.py:
UserResponse — already exists; add avatar_url: str | None.
UserUpdate(BaseModel) — full_name: str | None, speaking_language: str | None, listening_language: str | None (all optional).
- CRUD in
app/crud/user.py:
get_user_by_id(db, user_id) -> User | None
update_user(db, user, update_data: UserUpdate) -> User
soft_delete_user(db, user) -> None
hard_delete_user(db, user) -> None
- Avatar handling:
StorageService dependency injected into PATCH /me; old avatar deleted before uploading new one.
- Session teardown on DELETE: Reuses
blacklist_access_token, revoke_all_user_tokens, and cookie clearing (same pattern as /logout).
- New/Modified Files:
app/api/v1/endpoints/users.py [NEW]
app/schemas/user.py — add avatar_url to UserResponse, add UserUpdate [MODIFY]
app/crud/user.py — add update and delete CRUD functions [MODIFY]
app/api/v1/api.py — register users router [MODIFY]
Tasks
Open Questions/Considerations
- Should soft-deleted accounts have a grace period (e.g., 30 days) during which they can be reactivated by the user via a support request, or is soft-delete a permanent, admin-only reversal?
- For GDPR Right to Erasure, does hard-delete need to cascade to anonymise the user's data in meeting transcription logs and caption history, or is physical deletion of the
User row sufficient?
- Should
PATCH /me accept both application/json (no avatar) and multipart/form-data (with avatar) in a single endpoint, or should avatar upload be a separate POST /me/avatar endpoint for cleaner API design?
Feature: Implement User Profile Endpoints — GET, PATCH & DELETE /api/v1/users/me
Problem
Authenticated users have no way to view their own profile, update their display name or language preferences, upload an avatar, or delete their account. Without these endpoints, the platform cannot support any personalisation or account management beyond initial registration. Account deletion is also a legal requirement (GDPR Right to Erasure) and must support both a grace-period soft delete and a permanent hard delete.
Proposed Solution
Implement three endpoints under
/api/v1/users/me, all protected by theget_current_userdependency:GET /me— returns the authenticated user's full profile.PATCH /me— allows partial updates to display name, language preferences, and avatar.DELETE /me— soft-deletes the account by default, with a?hard=truequery parameter for permanent deletion.User Stories
GET /api/v1/users/me — Get Current User Profile
Acceptance Criteria
Authorization: Bearer <access_token>header.200 OKwith the authenticated user's public profile:{ "id": 1, "email": "user@example.com", "full_name": "Ada Lovelace", "avatar_url": "https://res.cloudinary.com/fluentmeet/...", "speaking_language": "en", "listening_language": "fr", "is_active": true, "is_verified": true, "created_at": "2026-03-14T12:00:00Z" }hashed_password,deleted_at, and other internal fields are never returned.PATCH /api/v1/users/me — Update Profile
Acceptance Criteria
Authorization: Bearer <access_token>header.application/jsonfor text fields ormultipart/form-datawhen an avatar file is included. All fields are optional (partial update):{ "full_name": "Ada K. Lovelace", "speaking_language": "en", "listening_language": "de" }multipart/form-data):StorageService(see External Services issue).user.avatar_url.avatar_urlis unchanged.speaking_languageandlistening_languagemust be valid BCP-47 language codes; invalid values return400 Bad Requestwith codeINVALID_LANGUAGE_CODE.PATCHsemantics).200 OKwith the updated user profile (same schema asGET /me).DELETE /api/v1/users/me — Account Deletion
Acceptance Criteria
Authorization: Bearer <access_token>header.?hard=truequery parameter to distinguish between:?hard=falseor omitted): Setsuser.deleted_at = now()anduser.is_active = False. The account record is retained in the database.?hard=true): Permanently deletes theUserrecord and all associated data (verification tokens, reset tokens, room participations) from the database.revoke_all_user_tokens(email)is called to invalidate all active refresh tokens.HttpOnlyrefresh token cookie is cleared (Max-Age=0).jtiis blacklisted in Redis./loginattempt returns403 Forbiddenwith codeACCOUNT_DELETED(already enforced in the login endpoint).200 OKon success:{ "status": "ok", "message": "Account has been successfully deleted." }Proposed Technical Details
app/api/v1/endpoints/users.py[NEW] — all three routes registered under anAPIRouter(prefix="/users", tags=["users"]).app/schemas/user.py:UserResponse— already exists; addavatar_url: str | None.UserUpdate(BaseModel)—full_name: str | None,speaking_language: str | None,listening_language: str | None(all optional).app/crud/user.py:get_user_by_id(db, user_id) -> User | Noneupdate_user(db, user, update_data: UserUpdate) -> Usersoft_delete_user(db, user) -> Nonehard_delete_user(db, user) -> NoneStorageServicedependency injected intoPATCH /me; old avatar deleted before uploading new one.blacklist_access_token,revoke_all_user_tokens, and cookie clearing (same pattern as/logout).app/api/v1/endpoints/users.py[NEW]app/schemas/user.py— addavatar_urltoUserResponse, addUserUpdate[MODIFY]app/crud/user.py— add update and delete CRUD functions [MODIFY]app/api/v1/api.py— register users router [MODIFY]Tasks
avatar_urltoUserResponseinapp/schemas/user.py.UserUpdateschema inapp/schemas/user.py.update_user,soft_delete_user, andhard_delete_userinapp/crud/user.py.GET /api/v1/users/meinapp/api/v1/endpoints/users.py.PATCH /api/v1/users/mewith JSON andmultipart/form-datasupport, avatar upload, and old avatar cleanup.DELETE /api/v1/users/mewith soft/hard delete logic, full session teardown, and avatar deletion on hard delete.app/api/v1/api.py.GET /me,PATCH /me(text update, avatar upload), andDELETE /me(soft and hard).Open Questions/Considerations
Userrow sufficient?PATCH /meaccept bothapplication/json(no avatar) andmultipart/form-data(with avatar) in a single endpoint, or should avatar upload be a separatePOST /me/avatarendpoint for cleaner API design?