A super-admin can ban a member and remove any board by its record #216
No reviewers
Labels
No labels
No milestone
No project
No assignees
3 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
Grandiras/Ponente!216
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "issue-196-moderation"
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 #196. The super-admin half of the design posted there, on top of the soft delete (previous PR).
Since #190, a deployment's configured admin can't open a board they weren't given. They still have to be able to act on a report about one, and to stop a member who shouldn't be here. This PR adds those two verbs, with as little reach into boards as they need.
Ban a member
Member.BannedAt,BannedByDid, andBannedReason(encrypted). The row is what every card, comment and assignment they made points at. Deleting it fails for anybody who ever made anything, which is exactly who a ban is for, or it would destroy their work.DbAllowlistServiceleaves banned members out, and that one clause ends their circuits, closes every page and stops their calendar feed. Their grants stay, inert, so lifting the ban is one click.BoardSuccession, the rule removal already uses). A board with nobody else on it is left alone. It's unreachable while the ban holds, and back exactly as it was once lifted. Handing it to the super-admin would open a private board to somebody who was never given it. The Members page says which boards were left like that.CannotBanSuperAdminException), since the configuration would let them in anyway. A banned DID can't be re-added or approved from the requests panel (MemberBannedException). The assignee and access pickers leave banned members out, and "admins left on a board" counts only members who can sign in.EnsureBoardAdminsAsync) now runs succession first. A board open to everyone falls back to the super-admins, and a restricted one with nobody left is logged, never opened up.AddMemberBans: three nullable columns.Board record and moderation delete
/admin/board/{key}is for the configured admin only, reached from a link somebody reports. It shows one board's structure: name, who runs it, how many members, columns, cards and comments it has, when it was made and when a card last changed. Of everything members typed, only the name, because a delete made blind can't be undone and the name is in the report anyway. Each open is logged.DeletedAsModerationset. The board's own admins can't restore it (their "Recently deleted" doesn't list it, and the deleted-board page says it was removed by an admin). Only the super-admin brings it back, from the same page. Purge after 30 days as usual.Tests
tests/e2e/moderation.spec.js: a ban shuts a member out, hands on the board they ran, and lifts again; a board the super-admin isn't on is removed from its record, and only they bring it back.BoardRecordPage.Disposenow overriding the base's, so its locale subscription is released too (it was a CS0114 warning).auth.md(bans and the allowlist),boards-and-permissions.md(the record and where the line is),data.md(the new columns),testing.md(the spec).🤖 Generated with Claude Code
A ban is a flag on the member row, not a removal: the allowlist snapshot leaves them out, which ends their circuits and feeds, while their work and their grants stay put, so lifting it is one click. Their boards are handed on where they were the last admin (BoardSuccession); a board with nobody else on it is left alone rather than opened to the super-admin. /admin/board/{key} shows one board's structure — who runs it, how much is on it, never what anybody wrote besides its name — and removes it as moderation: the soft delete with DeletedAsModeration set, which only the super-admin can undo. It is reached from the link a report arrives as, never browsed. Closes #196. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>🤖 Reviewing this PR — the review will link the full session transcript.
💬 Verdict: comment — notes only, nothing blocking.
Solid implementation of both verbs, and the design's limits are enforced where they matter:
GetRecordAsyncis structure-only and reached by address,DeleteAsModerationAsync/RestoreAsSuperAdminAsyncre-check the configured admin at the write (RequireSuperAdmin), the succession rule counts only members who can sign in (HandOverAsync,SetAccessAsync,EnsureBoardAdminsAsyncall agree), and the boot repair no longer hands private boards to the super-admin. I checked the catch order inRequestsPanel.razorandProgram.cs(MemberBannedException vs DbUpdateException — both reachable), the locale keys including the.oneplural, the e2e helpers the new spec imports, and theBoardSuccession.Forsignature the initializer now passesleaving: string.Emptyto — all correct.Two minor notes, neither blocking:
Changelog.Unreleasedhas no entry for this. The repo convention (CLAUDE.md) is that a change a member would notice adds a line in the PR that makes it — bans (the access-denied message, the narrowed pickers) and the board record qualify. Worth adding before merge, in both locales.The board record's admin list includes banned admins.
GetRecordAsyncreturns every grant with the admin role, banned or not, while the empty-state label says "Nobody who can sign in" — so a board whose only admin is banned shows that banned admin as its admin rather than the label. Arguably correct (the grant row is real), but the label and the list disagree about what is being filtered; filteringAdminDidsto unbanned members, or rewording the empty label, would make them consistent.⏱ 3m 24s · 14 steps · $0.0600 · tokens: 283.2k in / 9.3k out (+428.9k cache-read) · Go: 5h 37% · wk 21% · mo 43%
📜 Full session transcript
🤖 On it — the reply will link the full session transcript.
Both notes addressed in
e0f3a3b:moderation.spec.jspins it with a board only the banned member was on.Also:
didOfin that spec now polls, since a single read could land between the prerender and the circuit's render (the one flaky run I saw). Unit 558/558, moderation spec 3/3.⚠️ The run failed: git -c http.extraheader=Authorization: Basic b3BlbmNvZGUtYm90OjMxNWQ2MDE5ZTg1M2I2NmQ5ZTcxN2ZiNWQzYTA2NmU3MmU2M2RjNDk= push origin HEAD:refs/heads/issue-196-moderation failed (1): To http://forgejo:3000/Grandiras/Ponente.git
! [rejected] HEAD -> issue-196-moderation (fetch first)
error: failed to push some refs to 'http://forgejo:3000/Grandiras/Ponente.git'
hint: Updates were rejected because the remote contains work that you do
hint: not have locally. This is usually
🤖 Reviewing this PR — the review will link the full session transcript.
✅ Verdict: approve — no blocking issues found.
Both notes from the previous pass are resolved: the Changelog
Unreleasedentry now names the new verbs in both locales, and the board record's admin list now shows the "Nobody who can sign in" label whenever every admin grant is banned, with banned admins still listed and marked — the label and the list agree, andmoderation.spec.jspins both visible together. The newDisposeoverride is correct againstLocalizedComponentBase(virtual,base.Dispose()called first), and theBoardSuccession.Forcall in the boot repair matches the optionaleligiblesignature. The core design holds:GetRecordAsyncis structure-only and reached by address, the moderation delete/restore re-check the configured admin at the write, and the succession rule counts only members who can sign in consistently acrossHandOverAsync,SetAccessAsync, andEnsureBoardAdminsAsync. Good to merge.⏱ 0m 34s · 5 steps · $0.0151 · tokens: 78.5k in / 1.2k out (+90.0k cache-read) · Go: 5h 55% · wk 29% · mo 47%
📜 Full session transcript