Phase 2: private rooms, room roles, and invites (backend only)

Adds room_invites table + migration, owner/admin/member role enforcement
(require_room_role), and endpoints for private room creation, room
management (update/delete/leave/transfer-ownership/change-role/remove-member),
and the invite lifecycle (create/list/accept/decline/revoke). Registration
stays invite-only via the CLI from Phase 1 — this is a separate, room-level
invite system for adding existing users to private rooms.

Frontend is untouched: the UI redesign is happening separately, so this phase
is backend + tests only (35 passing). Verified no regressions in the Phase 1
open-room/WebSocket flow via manual smoke test.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-13 20:22:15 -06:00
co-authored by Claude Sonnet 5
parent 99aa029c0d
commit c79e96dd48
16 changed files with 1172 additions and 32 deletions
+32 -12
View File
@@ -1,7 +1,8 @@
# KeepItTalking backend (Phase 1)
# KeepItTalking backend (Phase 1 + 2)
FastAPI + SQLAlchemy 2.0 (async) + PostgreSQL. Implements auth, open-room CRUD,
and a single-instance WebSocket chat endpoint. See `../ARCHITECTURE.md` for the
FastAPI + SQLAlchemy 2.0 (async) + PostgreSQL. Implements auth, room CRUD
(open and private), room roles (owner/admin/member) and invites, and a
single-instance WebSocket chat endpoint. 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.
@@ -79,29 +80,48 @@ app/
main.py create_app(), session middleware, router/WS mounting
config.py environment-driven settings (pydantic-settings)
database.py async engine/session, get_db() dependency
dependencies.py get_current_user, require_room_member
dependencies.py get_current_user, require_room_member, require_room_role
security.py argon2 password hashing
cli.py `python -m app.cli create-user` (account provisioning)
models/ SQLAlchemy models (users, rooms, room_memberships, messages)
models/ SQLAlchemy models (users, rooms, room_memberships,
messages, room_invites)
schemas/ Pydantic request/response models
routers/ auth, rooms, health
routers/ auth, rooms, invites, health
services/ business logic called by routers
ws/ WebSocket connection manager + /ws/chat handler
alembic/ migrations
tests/ pytest + httpx/TestClient tests
```
## Room roles and invites (Phase 2)
Rooms can be `open` (anyone can join via `POST /api/rooms/{id}/join`) or
`private` (`is_private: true` at creation — joinable only via invite). Room
roles are `owner` > `admin` > `member`:
- **member**: post messages, leave the room
- **admin**: edit room settings, create/list/revoke invites, remove plain members
- **owner**: everything admin can, plus delete the room, remove admins, change
member roles, and transfer ownership
Invite flow: an admin+ calls `POST /api/rooms/{id}/invites` with an existing
`target_username`; the invited user sees it via `GET /api/invites/mine` and
calls `POST /api/invites/{id}/accept` (or `/decline`). `GET /api/rooms/mine`
lists every room (open + private) the current user belongs to, alongside
their role.
## Notes / scope decisions
- Invite-only: no `POST /api/auth/register`. Accounts are provisioned with
`python -m app.cli create-user` (see step 4 above). A more self-service
invite flow (per-user tokens, or an admin-portal "generate invite" button)
is a natural phase-2/6 follow-up, not built now.
- Invite-only site registration: no `POST /api/auth/register`. Accounts are
provisioned with `python -m app.cli create-user` (see step 4 above). This is
separate from *room* invites above — site accounts vs. room membership.
- Room invites are by **username only**`room_invites.target_email` exists
in the schema (per `ARCHITECTURE.md`) but is unused, since there's no
email-delivery mechanism anywhere in the stack yet.
- Sessions are signed cookies (Starlette `SessionMiddleware`), not a server-side
session table — see `ARCHITECTURE.md`'s rationale (simplest way to carry auth
through a WebSocket handshake). This means there's currently no way to force-
revoke a session server-side; that needs a real session table later.
- No CSRF token yet — `SameSite=Lax` cookies plus a same-origin frontend dev
proxy (see `../frontend/vite.config.ts`) is the accepted phase-1 mitigation.
- `rooms.is_private` exists in the schema but the API never sets it `True` yet;
private rooms/invites are phase 2 (tracked as a Gitea issue).
- Deleting a room explicitly deletes its messages/memberships/invites first
(`room_service.delete_room`) rather than relying on DB-level cascades.