Private
Public Access
Add user profile management: display name + avatar upload (Gitea issue #12)
Users can set a display name (shown instead of username in the message list, room member list, TopBar, and admin Users tab) and upload a real avatar, replacing the generated color-initial avatars everywhere a user appears. Avatars are square-cropped and downscaled to 512px, reusing app/storage.py's upload primitives from image uploads with a new square option. Two deliberate divergences from message-image handling, documented in backend/README.md: the previous avatar file is deleted on replace/remove (safe since it's strictly one file per user), and avatar serving is not room-gated and uses a short cache (identity-addressed and mutable, unlike a message image's permanent content-addressed URL). Frontend: new ProfileModal reachable from the TopBar account menu; AuthContext gains updateUser() so a profile change reflects instantly everywhere without a refetch.
This commit is contained in:
+46
-7
@@ -1,4 +1,4 @@
|
||||
# KeepItTalking backend (Phase 1 + 2 + 4 + 5 + 6 + 7 + 8, image uploads, emoji & reactions)
|
||||
# KeepItTalking backend (Phase 1 + 2 + 4 + 5 + 6 + 7 + 8, image uploads, emoji & reactions, user profiles)
|
||||
|
||||
FastAPI + SQLAlchemy 2.0 (async) + PostgreSQL + Redis. Implements auth, room
|
||||
CRUD (open and private), room roles (owner/admin/member) and invites, a
|
||||
@@ -6,9 +6,10 @@ WebSocket chat endpoint that fans out across multiple app-server instances
|
||||
via Redis pub/sub, Web Push notifications for offline room members, a
|
||||
site-admin portal (user/room/bot management + an audit log), a bot/
|
||||
extension layer (scoped API tokens, live bot WebSocket access, incoming and
|
||||
outgoing webhooks, message editing), image uploads in chat messages, and
|
||||
emoji reactions on messages. See `../ARCHITECTURE.md` for the full system
|
||||
design and the phased build plan.
|
||||
outgoing webhooks, message editing), image uploads in chat messages, emoji
|
||||
reactions on messages, and self-service user profiles (display name,
|
||||
avatar). See `../ARCHITECTURE.md` for the full system design and the
|
||||
phased build plan.
|
||||
|
||||
This is an **invite-only site**: there is no public registration endpoint.
|
||||
Accounts are created by an operator on the app server — see step 4 below.
|
||||
@@ -116,8 +117,9 @@ app/
|
||||
require_room_member, require_room_role,
|
||||
require_site_admin, require_scope
|
||||
security.py argon2 password hashing + token generate/hash (sha256)
|
||||
storage.py uploaded-image validation (Pillow), downscaling,
|
||||
and on-disk save/read -- see Image uploads below
|
||||
storage.py uploaded-image validation (Pillow), downscaling
|
||||
(optionally square-cropped, for avatars), and
|
||||
on-disk save/read -- see Image uploads below
|
||||
cli.py `python -m app.cli create-user` / `generate-vapid-keys`
|
||||
models/ SQLAlchemy models (users, rooms, room_memberships,
|
||||
messages, message_images, message_reactions,
|
||||
@@ -125,7 +127,8 @@ app/
|
||||
admin_audit_log, api_tokens, webhooks_incoming,
|
||||
event_subscriptions)
|
||||
schemas/ Pydantic request/response models
|
||||
routers/ auth, rooms, invites, push, admin, bots, webhooks, health
|
||||
routers/ auth, rooms, users, invites, push, admin, bots,
|
||||
webhooks, health
|
||||
services/ business logic called by routers
|
||||
ws/ connection_manager (local sockets), presence +
|
||||
broadcaster (Redis), /ws/chat handler
|
||||
@@ -376,6 +379,42 @@ in `webhook_service.py` is unchanged — same restraint as image uploads), no
|
||||
reaction-count limit or rate limiting, no custom/uploaded emoji (unicode
|
||||
only, curated client-side list in `frontend/src/lib/emoji.ts`).
|
||||
|
||||
## User profiles
|
||||
|
||||
Display name and avatar live directly on `User`
|
||||
(`display_name`, `avatar_filename`, `avatar_content_type`, all nullable) —
|
||||
no separate profile table, since it's a strict 1:1 with no room-scoping
|
||||
concern the way message images have. Bio was considered and explicitly
|
||||
scoped out.
|
||||
|
||||
- `PATCH /api/auth/me` — updates `display_name` (`app/routers/auth.py`).
|
||||
Empty/whitespace clears it back to `None`, falling back to the username
|
||||
everywhere it's displayed.
|
||||
- `POST /api/auth/me/avatar` / `DELETE /api/auth/me/avatar` — reuse
|
||||
`app/storage.py`'s upload primitives (`read_capped`, `process_image`,
|
||||
`save_image`) from image uploads, but call `process_image(..., square=True,
|
||||
max_dimension=512)` — a new option that center-crops before downscaling,
|
||||
since avatars need a fixed square shape at a much smaller size than a
|
||||
message image. Unlike message images (which never delete), the previous
|
||||
avatar file **is deleted** on replace/remove (`storage.delete_image`) —
|
||||
safe to do here because it's strictly one file per user, no accumulation
|
||||
risk to accept the way an orphaned message-image upload has.
|
||||
- `GET /api/users/{user_id}/avatar` (`app/routers/users.py`, new router) —
|
||||
serves the file. Two deliberate divergences from the message-image
|
||||
serving endpoint: **not** room-membership-gated (avatar visibility
|
||||
matches username visibility — any authenticated user can see anyone's),
|
||||
and `Cache-Control: private, max-age=300` rather than `immutable` (an
|
||||
avatar URL is identity-addressed and its content changes on re-upload,
|
||||
unlike a message image's permanent content-addressed URL).
|
||||
|
||||
`RoomMemberRead` and `AdminUserRead` both carry `display_name`/
|
||||
`avatar_filename` so the frontend can render an avatar and a preferred name
|
||||
anywhere a user appears (message list, room member list, admin Users tab,
|
||||
TopBar) — `MessageRead` deliberately does **not** carry them; the frontend
|
||||
resolves both live from the room's member list instead of freezing them
|
||||
per-message, which is the more correct behavior for a field the sender can
|
||||
change after the fact.
|
||||
|
||||
## Notes / scope decisions
|
||||
|
||||
- Invite-only site registration: no `POST /api/auth/register`. Accounts are
|
||||
|
||||
Reference in New Issue
Block a user