feat(rooms): read-only / moderators-only posting policy (LC-85) #149
Loading…
Reference in a new issue
No description provided.
Delete branch "feat/lc-85-readonly-rooms"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Closes LC-85. Adds a per-room posting policy: admins can flip a room into
moderators_only(announcement-style) oradmins_onlymode. Defaultallpreserves existing behavior. Reactions, pins, and edits-of-own-old-posts are intentionally NOT gated (per the ticket's carve-out).Schema
rooms.posting_allowed_for TEXT NOT NULL DEFAULT 'all' CHECK (posting_allowed_for IN ('all','moderators_only','admins_only')). DEFAULT backfills implicitly.Server enforcement (acceptance criterion: forged POST returns 403)
routes::room::post_messagenow calls acan_post_with_policyhelper after the access check. The helper consultsdb::room_rbac::is_room_moderator(LC-84), so a per-room Moderator override unlocksmoderators_onlyfor a caller whose org role is plain User.admins_onlyrequires effective Admin.Compose-box rendering (acceptance criterion: disabled for users below threshold)
RoomPagegainscan_post: boolandposting_locked_reason: &str.room/page.htmlrenders the composer whencan_post, otherwise a centered notice ("Only moderators/admins can post in this room."). Server enforcement runs regardless of the client toggle.Admin UI (acceptance criterion: admins can flip from room settings)
/room/{id}/moderators(the LC-84 admin page) gains a "Posting policy" section above the override list with a 3-option dropdown (Everyone (default),Moderators only,Admins only). Submits toPOST /room/{id}/posting-policy, gated on the sameroom_can_manage_overridespredicate the override grant/revoke endpoints use.Audit log (acceptance criterion: a log entry records the policy change)
room_posting_policyrow tomod_actionscarrying the room id, actor, and new policy in the metadata JSON.Reversibility (acceptance criterion: switching out is reversible; messages unchanged)
messages. Switching back toallimmediately restores everyone's post rights; nothing on disk references the policy.Out of scope (open questions in the ticket)
/poll(LC-66) and/remind(LC-63): those features do not exist yet. When built, they should route throughcan_post_with_policy.Tests
routes_readonly_rooms.rs(6/6 passing): admin sets policy (303), plain member denied (403), invalid policy 400, member POST in moderators-only room → 403, admin POST → 200, moderator override unlocks posting for a member, policy flip writes the expectedmod_actionsrow.just checkclean.just testandjust test-saaspass (no failures introduced).Migration drift: 0029 added to the six hand-list tests (
db_uploads,uploads_sweep,routes_reconnect,routes_uploads,db_enclave,admin_uploads) and todb_private_rooms.rs(verbose form). Tests undercommon::poolpick it up viasqlx::migrate!.Adds a per-room policy that limits who can post messages. Default keeps existing behavior (every room member can post); admins flip a room to `moderators_only` or `admins_only` for announcement-style channels. Reactions, pins, and edits-of-own-old-posts are unaffected (the spec explicitly carves those out). Schema: - Migration 0029 adds `rooms.posting_allowed_for TEXT NOT NULL DEFAULT 'all' CHECK (posting_allowed_for IN ('all','moderators_only','admins_only'))`. The DEFAULT backfills existing rows in place, so no data migration is needed. Code: - `models::Room` gains the `posting_allowed_for` field; `db::chat::map_room` and every `SELECT * FROM rooms` site is updated. `db::chat::set_room_posting_policy` writes the column. - `routes::room::post_message` calls a new `can_post_with_policy` helper after the existing access check. The helper reuses `db::room_rbac::is_room_moderator` (LC-84) so a per-room Moderator override unlocks a `moderators_only` room even when the caller's org role is plain User. `admins_only` requires effective Admin. - `RoomPage` view gains `can_post: bool` and `posting_locked_reason: &str`; `room/page.html` renders the composer when `can_post`, otherwise a "Only moderators/admins can post in this room." notice. Server enforcement still runs even when the composer is hidden. - `routes::room_rbac::post_posting_policy` handles `POST /room/{id}/posting-policy`. Authorization gated on the same `room_can_manage_overrides` predicate the moderator-grant endpoints use, so site admin / enclave owner / enclave admin (or anyone with an Admin override - though the override-Admin path is not exposed in the UI yet) can flip the policy. The handler writes a `room_posting_policy` row to `mod_actions` so the change is in the audit log. - The room moderators page (`/room/{id}/moderators`) gains a "Posting policy" section above the override list with a 3-option dropdown. Out of scope (open questions in the ticket, deferred): - Whether `/poll` (LC-66) and `/remind` (LC-63) honor the policy: those features do not yet exist; when they do, they should route through the same `can_post_with_policy` helper. - Whether designated bots (LC-73) bypass: bots are not yet built. - "Broadcast announcement to all rooms" remains a separate feature. Tests: - New integration binary `routes_readonly_rooms.rs` (6 tests): admin can set policy (303 redirect), plain member cannot (403), invalid policy rejected (400), a moderators-only room rejects member POSTs (403) but accepts admin POSTs (200), a moderator override unlocks posting for a previously-member user, and the policy flip writes a `room_posting_policy` row to `mod_actions` carrying the right metadata. - `just check` clean; `just test` / `just test-saas` pass. Migration drift: 0029 added to the six hand-list tests (`db_uploads`, `uploads_sweep`, `routes_reconnect`, `routes_uploads`, `db_enclave`, `admin_uploads`) plus `db_private_rooms.rs` (verbose form). Tests under `common::pool` pick it up via `sqlx::migrate!`.