feat/enclaves #32

Merged
nrupard merged 50 commits from feat/enclaves into main 2026-05-05 21:30:06 +02:00
Owner
No description provided.
Spec for a new top-level grouping (enclaves) that contains rooms. Covers data model with cascade rules, three-tier role model (owner/admin/member) plus site-admin god-mode, routes for CRUD/invite/discovery/transfer/leave, sidebar restructure into Discord-style switcher + per-enclave room list, last-visited cookie redirect, search scoping rules, WebSocket event additions, and migration plan that lifts existing rooms into a default General enclave while preserving private-room memberships.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The chat-DB SQL migration cannot reach auth-DB user rows, so the role-aware membership population has to live in a Rust startup step that can read both pools. Spec now specifies an idempotent backfill plus a hook from auth::register so the first user becomes General's owner without requiring a server restart.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase 1 covers schema migration, models, db::enclave module with all CRUD/role/invitation helpers, cross-DB backfill, is_room_accessible predicate, list_rooms_in_enclave, and permission helpers. Each task is TDD with frequent commits. Phases 2-4 will follow.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase 2 covers the full routes::enclave HTTP module (CRUD, invitations, discovery, member ops, room ops), the last_visited cookie redirect, and search scoping by enclave_id. Routes become reachable; sidebar wiring waits for phase 3.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase 3 swaps the single sidebar for a two-column ChromeView (switcher + per-enclave sidebar), fully expands the enclave/invitation/discover templates, and refreshes welcome and admin/rooms. Phase 4 wires WebSocket events for live membership/room/invitation updates, removes the legacy POST /admin/rooms, and ends with the PR.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds enclaves, enclave_members, enclave_invitations, and a rooms.enclave_id column. Inserts a default General enclave and moves every non-DM room into it. Membership backfill is intentionally deferred to a Rust startup step that can read auth.db.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Skips entirely when no site admin exists yet, so a fresh deploy waits for the first registration (which auto-promotes to admin and re-runs the backfill).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds enclave_id column binding to db::chat::create_room and a get_general_id helper that the admin room-create handler uses for backwards-compatible behavior. Existing test files updated to apply migrations 0007-0009 since several depended on partial migration sets.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds server/src/perms.rs with enclave_can_manage / delete / invite / add_room / manage_admins helpers, all of which short-circuit to true when the caller has the site-wide admin role. Includes unit tests covering the per-tier (owner/admin/member) capability matrix and the godmode short-circuit.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two correctness fixes from review:

- promote_first_user_to_admin now returns whether promotion actually happened. The post-register backfill is only invoked when the new user was promoted, matching the spec wording ("after the first user's promotion completes") and avoiding two cross-pool DB queries on every later registration.
- backfill_general_membership drops the early-return that fired whenever any member row existed for General. The function is still idempotent (per-row INSERT OR IGNORE, sentinel-gated UPDATE) but can now repair a partial state where membership rows are present but created_by is still 'system'. New test covers this scenario.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds EnclavePage, EnclaveSettingsPage, DiscoverPage, InvitationsPage view structs and stub templates that the Phase 2 route handlers will return. Phase 3 expands these into the full UI; Phase 2 needs them only well-formed enough to render so route integration tests can exercise the endpoints.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Mounts a new routes::enclave module and adds the create handler. The handler trims the form name, rejects empty submissions, and uses the existing AuthUser extractor so any logged-in user can create an enclave. Includes integration tests covering the success redirect and the unauthenticated-redirect path.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds the per-enclave landing handler. Site admins reach every enclave via godmode; everyone else needs an enclave_members row. Returns 404 for unknown ids, 403 for non-members, otherwise renders EnclavePage with the room list filtered through list_rooms_in_enclave (private rooms hidden from non-room-members unless the caller can manage the enclave).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds GET /enclaves/discover, POST /enclaves/discover/{id}/join, POST /enclaves/join (by code), POST /enclave/{id}/visibility, and POST/DELETE /enclave/{id}/invite-code. The require_manage helper centralizes the owner-or-admin-or-site-admin gate; rand::Alphanumeric generates 16-char invite codes. Tests cover the public/private discovery split, the bad-code rejection, and the discover-join refusal for private enclaves.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds POST /enclave/{id}/invite (manage-required, idempotent on UNIQUE collision), POST /invitations/{id}/accept and /decline (invitee-only), and GET /invitations rendering the pending list. Two-user test harness app_with_two_users covers the end-to-end invite -> list -> accept -> landing flow.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds GET /enclave/{id}/settings (manage-required) and the matching mutation handlers. Owner-self-leave is rejected with a clear message; the only-member case returns the delete-instead error so the user is told to use /delete. Kick refuses to remove the owner role even under site-admin god-mode (the spec mandates transfer-ownership first). The role endpoint accepts only "admin" or "member" form values.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds POST /enclave/{id}/rooms (create), .../rooms/{room_id}/edit, .../delete, .../members (add), and .../members/{user_id}/remove. assert_room_in_enclave guards every per-room handler so a manager of enclave A cannot delete or edit a room living in enclave B (404 instead of leaking that the room exists). Private rooms get a 10-char invite code and the creating manager is auto-added to room_members.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a small cookie helper module with read/set/is_safe_path. Path validation only accepts /room/<i64> and /dm/<alphanumeric-or-hyphen>; anything else (including /admin or /../etc/passwd attempts) falls through to the welcome page. routes/home.rs reads the cookie, verifies the target is accessible via is_room_accessible, then redirects; routes/room.rs and routes/dm.rs append a Set-Cookie header on every successful render. Also fixed a pre-existing godmode hole where is_room_accessible returned true for nonexistent rooms when called by a site admin (now requires the row to actually exist).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces the inline match in routes/room.rs::get_room with a single is_room_accessible call so that the enclave-membership rule (caller must be in the room's enclave to view non-DM rooms) is enforced at the request boundary, not just for the sidebar listing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
search_messages now takes enclave_id_filter and home_dm_only flags. When called with an enclave_id, results are restricted to non-DM rooms in that enclave (with private-room membership still required for non-admins). When neither flag is set, the function refuses to leak global results (returns nothing). The route handler enforces enclave membership before invoking the DB layer; site-admin god-mode bypasses the membership check but still respects the scope filter.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three review fixes:

- post_message in routes/room.rs no longer treats public rooms as world-writable. The handler now uses is_room_accessible (the same predicate as get_room) so a non-enclave-member - or a kicked member - cannot post to public rooms inside an enclave they do not belong to. New integration test covers this.
- home.rs target_accessible for /dm/{peer_id} now requires an existing DM room via find_dm_room, so the home redirect cannot lazily create a DM with a peer the user has never messaged.
- post_discover_join and post_join_by_code no-op when the caller is already a member, surfacing a redirect instead of a stray INSERT OR IGNORE write.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds SwitcherEntry to views::layout, a load_switcher helper that builds the Home + per-enclave list, and a load_chrome wrapper that returns sidebar lists plus switcher entries in one call. Every page handler now passes a switcher slice through to its template; the templates themselves still render only the legacy sidebar partial - the new switcher partial lands in the next task.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds partials/enclave_switcher.html and includes it before the existing sidebar partial in layout.html. Every enclave the caller is a member of plus a Home pseudo-enclave appears as a 64-pixel-wide vertical icon column on the left, with a "+" entry that links to /enclaves/discover. Per-icon badges include unread counts; the Home icon also shows pending invitation count. The legacy partials/sidebar.html stays in place for now so room/DM listings continue to render unchanged - Phase 4 expands the per-enclave sidebar mode.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two-column grid: rooms + add-room form on the left, members + invite-by-username form + settings link on the right. Public-flag badge in the header. The room row also surfaces a Remove button to managers, so the landing is self-sufficient for everyday admin actions.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The settings page now exposes promote, demote, kick, and transfer-ownership controls inline next to each member, gated on the owner-only can_delete flag (matches spec: only owner or site admin can manage admins). Visibility section explains the public/private trade-off in copy. Delete-enclave button has a confirm() wrapper.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Splits the discover page into Create / Join-by-code / Public-list sections in styled boxes. Empty-state copy explains how a private enclave becomes public.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Welcome page now points users at /enclaves/discover and /invitations so the new feature surfaces are reachable from the empty-state landing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
CSS rebuilt to pick up the new switcher / discover / settings classes; cargo fmt sweep over routes_enclave.rs after the test-harness refactor in earlier commits.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds EnclaveMemberAdded/Removed, EnclaveRoomAdded/Removed, EnclaveInvitationCreated/Resolved. The WS render-event matcher returns None for them; client-side OOB rendering of switcher/sidebar live updates is deferred (recipients pick up the change on the next page load). Also nudges the routes_enclave test parser to be markup-robust after the Phase 3 landing-template expansion.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Mutating handlers now fan out the new ChatEvent variants over the existing Hub: post_create_room and post_delete_room broadcast to every enclave member; post_invite, post_invitation_accept, and post_invitation_decline broadcast to the invitee; post_kick targets the kicked user; post_leave and post_discover_join / post_join_by_code target the caller; post_delete enumerates members before the cascade and broadcasts EnclaveMemberRemoved per former member. Receivers do nothing with these for now (render_event returns None) because OOB switcher / sidebar templates are deferred; they pick up the change on the next page load.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Drops the global create-room endpoint, the unused CreateRoomForm, and the create form on /admin/rooms. Site admins create rooms inside an enclave via the per-enclave landing page; god-mode lets them pick any enclave. The /admin/rooms page becomes a read-only moderation listing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
load_sidebar now takes a current_enclave parameter. Home (None) returns DM peers and an empty rooms list; an enclave (Some(eid)) returns that enclave's rooms (filtered through list_rooms_in_enclave) and an empty peers list. The room handler resolves the room's enclave_id via a new enclave_for_room helper so /room/{id} pages render with the enclave's rooms in the sidebar and the matching switcher icon highlighted. The sidebar partial drops the section header when its list is empty so neither column renders an empty "Rooms" or "Direct messages" placeholder.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The Home button in the enclave switcher now links to /?home=1 so it always renders the Home pseudo-enclave (DM list + welcome) instead of being intercepted by the last_visited cookie. Plain GET / keeps the resume-where-you-left-off behavior on first arrival.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Detects sqlx unique-violation in post_create, post_edit, post_create_room, and post_edit_room and returns AppError::BadRequest with a user-readable message naming the conflicting label. Previously the bare ? unwrap surfaced a sqlx error through the From impl, which mapped to AppError::Internal -> 500.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
On a unique-name conflict, post_create / post_edit / post_create_room / post_edit_room now redirect back to the source page (discover, settings, or landing) with the error message URL-encoded into a `?error=...` query parameter. The corresponding GET handlers read the param via FlashQuery and the templates render a small red bubble at the top of the section. Replaces the prior 400-text-page response with a clean re-render of the form.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces the freeform `?error=<message>` query parameter with a small set of opaque codes (`enclave_name_taken`, `room_name_taken`). The corresponding GET handlers map the code through flash_message() to a fixed string; unknown codes resolve to None and render nothing. This stops a hostile link such as `/enclaves/discover?error=Bad%20word` from injecting attacker-chosen text into the rendered banner.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
FlashQuery now accepts an optional `name` parameter alongside the opaque error code. flash_message() formats the message using the name when present, otherwise falls back to a generic "name is already taken" message. The name is clamped to 64 chars and the template renders it through Askama's default HTML-escaping, so attacker-controlled markup cannot reach the banner. The redirect URL percent-encodes the name on the way out.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Migration 0010 rebuilds the rooms table without the global UNIQUE on name and adds a partial unique index on (enclave_id, name) WHERE room_type != 'dm'. Two enclaves can now own a room with the same name; duplicates within a single enclave are still rejected. Foreign keys are temporarily disabled across the rebuild so dropping the old rooms table does not cascade-delete messages or room_members. Test setups for every chat-pool harness pick up the new migration; new regression test confirms the per-enclave uniqueness rule.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds an admin-only listing at /admin/enclaves that surfaces every enclave regardless of the caller's membership, alongside owner id, member count, public-flag, and direct Open / Manage links. The Manage link relies on existing site-admin god-mode in the per-enclave handlers, so admins can enter and modify any enclave without first being invited. Adds db::enclave::list_all_enclaves_with_counts and a new admin nav entry.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
nrupard changed title from WIP: feat/enclaves to feat/enclaves 2026-05-05 21:29:57 +02:00
nrupard deleted branch feat/enclaves 2026-05-05 21:30:06 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
psa-systems/lets-chat!32
No description provided.