Skip to content

Judge and staff role-specific screens, gate sidebar by role - #29

Merged
matbrgz merged 2 commits into
masterfrom
fix/issue-19-role-specific-screens
Sep 10, 2026
Merged

matbrgz merged 2 commits into
masterfrom
fix/issue-19-role-specific-screens

Conversation

@matbrgz

@matbrgz matbrgz commented Sep 10, 2026

Copy link
Copy Markdown
Member

Closes #19 — adds the missing chief judge/staff screens and fixes role-based navigation visibility. Score-only visibility is already covered by the existing /scoreboard route, which every role can already reach read-only, so no separate screen was added for that.

Summary

Before this, the schema fully supported judge and staff roles (runs have judge_id/answer columns, a full tasks table exists) but there was zero web UI for either — manual judging was only reachable via the JSON API (PUT /api/runs/{run}/judge), and nothing at all read the tasks table. Separately, the sidebar's "Administração" section rendered for every authenticated user regardless of role (gated only by Auth::check()) — routes were protected server-side, but a team/judge/staff user would see admin links that 403'd the moment they were clicked.

  • app/Http/Controllers/JudgeController.php (new): /judge/runs lists pending and recently-judged runs for the current contest with a form to pick a verdict per pending run, reusing the same Score::updateScore() path Api\RunController::judge() uses. No per-judge run assignment/locking exists in the schema, so — like BOCA's judge/runchief.php vs judge/run.php — this is one shared queue any judge or admin can work from, rather than two separate screens.
  • app/Http/Controllers/StaffController.php (new): /staff/tasks lists the contest's Task rows (BOCA's staff/task.php — balloon delivery, printing, etc.) with a "mark complete" action.
  • Both gated by the role: middleware alias, which pointed at App\Http\Middleware\CheckRole in the old (dead, Laravel <11) app/Http/Kernel.php but was never actually registered in bootstrap/app.php — middleware('role:judge,admin') would have thrown "Target class [role] does not exist" if anyone had tried to use it. Registered it properly alongside the existing admin alias.
  • resources/views/layouts/app.blade.php: wrapped the Admin section in @if(isAdmin()) and added two new sections (Judge, Staff) gated the same way, so each role only sees the nav entries it can actually use.

Test plan

  • Full PHPUnit suite: 397 tests, 2673 assertions, all passing
  • New tests confirm: a team user gets 403 on both new screens; a judge can view and judge a pending run (a Score row is created correctly); a staff member can view and complete a task; the sidebar shows/hides each section correctly for team/judge/staff/admin users

Stacked on #24 (base branch chore/docker-deps-boca-review).

Co-Authored-By: Claude Sonnet 5 noreply@anthropic.com

🤖 Generated with Claude Code

https://claude.ai/code/session_011u3o4QCgpcqH51edYyRPMn

matbrgz and others added 2 commits September 9, 2026 22:19
Closes #19 (chief judge / staff task screens; role-based nav visibility).
Score-only visibility is already covered by the existing /scoreboard
route, which every role can already reach read-only.

Before this, the schema fully supported judge and staff roles (runs have
judge_id/answer columns, a full tasks table exists) but there was zero
web UI for either -- manual judging was only reachable via the JSON API
(PUT /api/runs/{run}/judge), and nothing at all read the tasks table.
Separately, the sidebar's "Administração" section rendered for every
authenticated user regardless of role (gated only by Auth::check()) --
routes were protected server-side, but a team/judge/staff user would see
admin links that 403'd the moment they were clicked.

- app/Http/Controllers/JudgeController.php (new): /judge/runs lists
  pending and recently-judged runs for the current contest with a form to
  pick a verdict per pending run, reusing the same Score::updateScore()
  path Api\RunController::judge() uses. No per-judge run
  assignment/locking exists in the schema, so -- like BOCA's
  judge/runchief.php vs judge/run.php -- this is one shared queue any
  judge or admin can work from, rather than two separate screens.
- app/Http/Controllers/StaffController.php (new): /staff/tasks lists the
  contest's Task rows (BOCA's staff/task.php -- balloon delivery,
  printing, etc.) with a "mark complete" action.
- Both gated by the `role:` middleware alias, which pointed at
  App\Http\Middleware\CheckRole in the old (dead, Laravel <11)
  app/Http/Kernel.php but was never actually registered in bootstrap/app.php
  -- `middleware('role:judge,admin')` would have thrown "Target class
  [role] does not exist" if anyone had tried to use it. Registered it
  properly alongside the existing `admin` alias.
- resources/views/layouts/app.blade.php: wrapped the Admin section in
  @if(isAdmin()) and added two new sections (Judge, Staff) gated the same
  way, so each role only sees the nav entries it can actually use.

Verified: full PHPUnit suite (397 tests, 2673 assertions) passes,
including new tests confirming a team user gets 403 on both new screens,
a judge can view and judge a pending run (Score row created correctly),
a staff member can view and complete a task, and the sidebar shows/hides
each section correctly for team/judge/staff/admin users.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011u3o4QCgpcqH51edYyRPMn
…Controller

The automated security review of the previous commit flagged the same
cross-contest IDOR pattern already fixed in SubmitController (#22) --
JudgeController::judge() and StaffController::complete() took a Run/Task
via route-model-binding but never checked it belonged to the acting
judge/staff member's own contest, so either could act on another
contest's data just by guessing/incrementing the id.

- Added authorizeRunAccess()/authorizeTaskAccess(): 403 unless the user
  is an admin or the run/task's contest_id matches the user's own.
- Also flagged: judging an already-'judged' run through this screen
  (rather than the dedicated rejudge flow, which resets state first)
  would double-count the attempt in Score::updateScore() -- a
  state-integrity issue, not just authorization. judge() now rejects
  that with a clear error pointing at the rejudge API.

Added tests for all three: cross-contest judging/task-completion is
blocked (403, no state mutated), and re-judging an already-judged run is
rejected.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011u3o4QCgpcqH51edYyRPMn
@matbrgz

matbrgz commented Sep 10, 2026

Copy link
Copy Markdown
Member Author

Pushed a follow-up fixing three issues the automated security review found: cross-contest IDOR in both JudgeController::judge() and StaffController::complete() (same pattern already fixed in #26's SubmitController), plus a state-integrity issue -- judging an already-judged run through this screen would double-count the attempt in Score::updateScore(). All three covered by new tests.

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.

Role-specific screens missing (chief judge, staff tasks, score-only, site)

1 participant