Skip to content

feat: let moderators require a login before attendees can post messages - #245

Open
junkerderprovinz wants to merge 3 commits into
ClaperCo:devfrom
junkerderprovinz:feature/require-login-for-messages
Open

feat: let moderators require a login before attendees can post messages#245
junkerderprovinz wants to merge 3 commits into
ClaperCo:devfrom
junkerderprovinz:feature/require-login-for-messages

Conversation

@junkerderprovinz

Copy link
Copy Markdown

Summary

Issue #107 asks for a way to hold an audience accountable when nobody is free to moderate: instead of deleting troll messages after the fact, let the moderator require that attendees are authenticated (e.g. through their university SSO) before they can post at all. This adds a per-event Require login to post toggle that does exactly that, leaving polls, forms and quizzes untouched.

What this adds

  • Migration + schema: authenticated_chat_only (boolean, default false) on presentation_states, cast in PresentationState.changeset/2. Every existing event keeps behaving exactly as before until a moderator turns it on.
  • Moderator UI: a new toggle row in the Attendee Settings panel (ManageAttendeesOptionsComponent), sitting right under "Allow anonymous messages", with the same disabled-when-messages-are-off behaviour and keyboard shortcut handling (F; A/S/D were taken). Label flips between "Require login to post" and "Allow messages without login" like the neighbouring toggles do. Handled by a new "checked" clause in EventLive.Manage, matching the existing ones one-for-one.
  • Attendee gate (LiveView): a new handle_event("save", ...) clause in EventLive.Show, placed in front of the two existing save clauses, next to where chat_enabled and anonymous_chat_enabled are already enforced. It refuses the post and flashes "You must be logged in to post a message". Because it matches on the live @state, it also covers a save pushed straight over the websocket rather than through the form.
  • Attendee UI: while the gate applies and the visitor is not logged in, the composer is replaced by a short notice plus a Log in button, in the same slot the "Messages deactivated" notice already uses. The if around the composer became a cond with three branches (messages off / login required / normal composer).
  • Raw endpoint: PostController.create/2 builds a post with no user at all, so it answers 403 while the flag is on, via a new Presentations.authenticated_chat_only?/1 that always reads the current value from the database rather than trusting a state struct loaded earlier.
  • i18n: mix gettext.extract + mix gettext.merge priv/gettext as AGENTS.md asks; 4 new msgids across the 10 locales ("Log in" already existed), the rest of that diff is reference-line churn.
  • Tests: 9 new ones (see below).

Deliberately a per-event presentation state flag rather than an env var: whether an audience must be identified is a per-lecture decision of the same kind as "allow anonymous messages", so it belongs in the same panel, changeable mid-event without a redeploy.

Verification

Run locally against a real PostgreSQL with Elixir 1.18.4 / OTP 28 (the versions in .tool-versions) and the same environment the CI workflow sets:

  • mix format --check-formatted — clean.
  • mix credo diff --from-git-merge-base origin/main — added no issues.
  • mix test — 345 tests, 3 failures. Those 3 are pre-existing: I ran the suite on untouched dev before writing a line (336 tests, the identical 3 failures) and they are ConverterTest's two ImageMagick-availability tests plus PresentationsTest's missing_slide_thumbnails?/1 case, all local-thumbnail path tests that misbehave on my Windows host. Nothing they touch is in this PR.

New tests, all passing:

  • attendee room, anonymous visitor: composer is replaced by the login prompt (#post-form gone, #login-required-composer present).
  • attendee room, logged-in visitor: composer stays.
  • a save pushed by an anonymous attendee is refused, the flash is rendered, and nothing is written (Posts.list_posts/1 stays empty).
  • the same push from a logged-in attendee is stored with their user_id.
  • mid-session flip: after {:state_updated, ...} the anonymous attendee's composer swaps to the login prompt without a reload.
  • manage page: clicking the new toggle persists the state and flips the label.
  • three context tests for Presentations.authenticated_chat_only?/1 (default, enabled, unknown event).

Not verified, to be explicit about it:

  • No manual click-through in a browser against a running server — I had no dev server or Docker available in this environment, so the LiveView assertions above are the whole of the evidence. The PostForm JS hook path and how the new notice looks on a narrow phone viewport were not checked visually.
  • The 403 in PostController is defence in depth and has no test, because PostController currently has no route in router.ex — nothing reaches it today. I added the check so the endpoint cannot become a hole if it is ever wired up.

Behavioural edge cases worth knowing:

  • Attendee who was anonymous when the moderator flips it on: their session is not kicked. The composer swaps to the login prompt on the next render, and any save they still push is refused, because the clause reads the live @state. Messages they already posted stay — the moderator's existing pin/delete/ban tools handle those, as before.
  • Flipping it back off restores the composer immediately, same mechanism.
  • Independent of "Allow anonymous messages": this gate is about who may post, that one is about whether a nickname is required. A logged-in attendee can still post under an empty nickname if the moderator allows anonymous messages. A moderator who wants a visible clear name on every message turns this on and "Allow anonymous messages" off.
  • Disabled in the panel while messages are off, mirroring the anonymous toggle.
  • Login method agnostic: any authenticated identity satisfies it (local password, OIDC, LTI), so it composes with, and does not overlap, Add DISABLE_PASSWORD_LOGIN to hide local login when OIDC is configured #240.
  • Poll/form/quiz answering is untouched, per the issue.

Fixes #107

Adds a "Require login to post" toggle to the moderator's Attendee Settings,
next to "Allow anonymous messages" and "Enable messages". While it is on,
only attendees with a Claper account (local password, OIDC, LTI, whichever
the deployment allows) can post; everyone else sees the composer replaced by
a short notice and a link to the login page.

This is the missing option for a lecturer moderating alone: instead of
deleting troll messages after the fact, every message carries an account
behind it, so an SSO-authenticated audience is accountable by construction.

Deliberately a per-event presentation state flag, not a deployment-wide env
var: whether an audience needs to be identified is a per-lecture decision,
the same kind of decision as "allow anonymous messages", and it belongs next
to it in the same panel. New migration adds `authenticated_chat_only`
(boolean, default false) to presentation_states, so every existing event
keeps behaving exactly as before until a moderator turns it on.

The gate itself sits in the "save" handle_event clause of the attendee
LiveView, in front of the two existing save clauses, which is where
chat_enabled and anonymous_chat_enabled are already enforced. That clause
reads the live @State, so it also covers a message pushed straight over the
websocket rather than through the form, and it follows a mid-session flip
immediately (the moderator's update broadcasts :state_updated).

PostController.create/2 has no authenticated user at all, so it answers 403
while the flag is on, via a new Presentations.authenticated_chat_only?/1 that
always reads the current value from the database.

Poll, form and quiz answering are untouched.

Fixes ClaperCo#107
Five LiveView tests around the attendee room (anonymous attendee sees the
login prompt instead of the composer, logged in attendee keeps it, a "save"
pushed by an anonymous attendee is refused and stores nothing, the same push
from a logged in attendee is stored, and the composer swaps over when the
moderator turns the setting on mid-session) plus three context tests for
Presentations.authenticated_chat_only?/1.
Clicks the new Attendee Settings toggle on the manage page and asserts the
presentation state was persisted and the label flipped.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Disable anonymous messages and enforce login though e.g. OAuth2/OIDC

1 participant