Database recreation required. No Alembic migration is provided — drop the old SQLite/Postgres schema before upgrading. Existing rooms, memberships, and ACLs are not preserved.
- Replaced
Room.is_publicandRoom.lockedwith:Room.visibility:private|group|public(three-value scope)Room.owner_user_idXORRoom.owner_group_id(polymorphic owner)- Two
CHECKconstraints enforce exactly-one-owner and visibility/owner consistency at the DB level.
- Removed
RoomMembershipandMemberRoleentirely. Per-room ACLs are replaced by per-group membership. - Added
Group(UUID id, globally-unique name, creator),GroupMembership(group ↔ user, roleviewer|member|admin), andRoomShareLink(bearer-token capability scoped to one room,vieworedit, optional expiry + revocation). - Added
User.is_guest: bool = False. Set byPOST /v1/auth/guest. The old@guest.useremail-suffix identification is gone.
- Single source of truth:
src/zndraw/access.pyexposes pure predicatescan_read,can_edit,can_manageover(user, room, share, group_role). - FastAPI composites:
AccessReadDep,AccessEditDep,AccessManageDepbundle room + share + group-role resolution and enforce the predicates. - Unauthorized reads of
PRIVATE/GROUProoms return404 Not Found(not403) to avoid leaking existence. - The per-room admin lock is deleted. The Redis per-geometry edit lock stays as pure serialization (no authorization).
- Every REST route that previously accepted
OptionalUserDepnow requires a JWT (CurrentUserDep). Guest tokens viaPOST /v1/auth/guestremain the canonical anonymous path. - One exception:
GET /v1/rooms/{id}/trajectoryretains a localOptionalUserDepalias to support the pre-existingzndraw-cli downloadtoken-only flow. get_local_token_or_adminno longer depends onOptionalUserDep; JWT decode is inline.
POST /v1/groups create group (creator → ADMIN)
GET /v1/groups list groups the caller belongs to
GET /v1/groups/{id} group details + my_role
PATCH /v1/groups/{id} rename / describe (admin only)
DELETE /v1/groups/{id} delete (admin only; rooms must be reassigned first)
POST /v1/groups/{id}/members add member (admin only)
DELETE /v1/groups/{id}/members/{user_id} remove member (admin only, or self)
PATCH /v1/groups/{id}/members/{user_id} change role (admin only)
POST /v1/rooms/{id}/share-links create share link (manager only)
GET /v1/rooms/{id}/share-links list active links (manager only)
DELETE /v1/rooms/{id}/share-links/{link_id} revoke link (manager only)
GET /v1/rooms— list filter is now union of public ∪ owner-user ∪ group-member (previouslyis_public=Trueonly).GET /v1/rooms/{id}/presence/sessions— gated oncan_read.PATCH /v1/rooms/{id}— gainedowner_user_id,owner_group_id,visibility,descriptionas updatable fields. Ownership transfer is a PATCH that sets exactly one owner field; server enforces invariants and the caller-must-be-in-target-group rule for group transfers. Thelockedfield is gone.- Content routes (geometries, figures, frames, trajectory, bookmarks,
selection groups, presets, step) gate on
can_readorcan_editvia the new composites.
on_connectstashes ashare_tokenfield from the auth payload onto the session (alongsideuser_idandcurrent_room_id).room_joinreplaces theRoomMembershiplookup withcan_read. Share tokens from the connect-time auth payload participate. Denial raisesRoomNotFound(404) to avoid existence leaks.- Broadcast routing (
broadcast_room_update) splits by visibility:PUBLIC→rooms:feedGROUP→ every group member'suser:{uid}channelPRIVATE→ the owner'suser:{owner_user_id}channel
Added (RFC 9457):
GroupNotFound(404) — group missing or not visible to callerGroupNameTaken(409) — duplicate group nameNotGroupMember(403) — caller must be any-role memberNotGroupAdmin(403) — admin role requiredLastGroupAdmin(409) — cannot demote/remove sole adminGroupHasRooms(409) — group owns rooms; delete or reassign firstTransferTargetInvalid(409) — caller not in the target groupShareLinkNotFound(404) — revoke/list target missing or revokedShareLinkInvalid(401) —X-Room-Share-Tokenunknown / revoked / expired / wrong room (documentation-only today; resolver returnsNoneinstead of raising)
Removed:
NotRoomMemberAlreadyRoomMember
- New types in
frontend/src/myapi/client.ts:Visibility,GroupRole,ShareAccess,Group,GroupMember,ShareLink. - Room interfaces drop
locked; addvisibility,owner_user_id,owner_group_id. - New API clients:
listGroups,createGroup,getGroup,updateGroup,deleteGroup,listGroupMembers,addGroupMember,removeGroupMember,updateGroupMemberRole,listShareLinks,createShareLink,revokeShareLink. - New components:
VisibilitySelector,ShareDialog. - New page:
/groups(GroupsPage— list my groups + create form). ?share={token}URL param is parsed on room-view mount; stored per-tab in memory (not localStorage); attached to REST asX-Room-Share-Tokenvia an axios interceptor, and to socket.io asshare_tokenin the connect auth payload.- The lock/unlock menu items in
RoomManagementMenu,RoomsPanel, androomRowMenuare gone. Visibility is now controlled via theVisibilitySelectorinsideRoomManagementMenu.
- Added
ZNDRAW_SERVER_DEFAULT_ROOM_VISIBILITY(defaultpublic) — controls the visibility assigned whenPOST /v1/roomsomits it.
- External auth (OAuth / OIDC / SSO)
- Automated email verification (superuser can still flip
is_verifiedmanually viaPATCH /v1/auth/users/{id}) - Per-room individual ACLs (
RoomCollaborator) — share links + groups cover current use cases - Cross-group room sharing (a room has exactly one owner)