Team leaders pick one mentor per track, gated on the booking link - #88
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Claude finished @draaujpeg's task in 4m 16s —— View job Review completeVerdict: needs changes — one authorization gap, otherwise solid. Checked: the migration (RLS-with-no-policies posture, revokes, the two SECURITY DEFINER RPCs, the partial unique index), every server action under
Everything else checked out: every admin mutation scopes on edition id + row id, member writes go through the RPC (not a service-role action) as CLAUDE.md prescribes, Branch: |
| v_user := auth.uid(); | ||
| if v_user is null then raise exception 'not_authenticated'; end if; | ||
|
|
||
| select m.id, m.hackathon_id, m.track | ||
| into v_mentor | ||
| from public.hackathon_mentors m | ||
| join public.hackathons h on h.id = m.hackathon_id and h.status <> 'draft' | ||
| where m.id = p_mentor_id | ||
| and m.deleted_at is null | ||
| and m.available; | ||
|
|
||
| if v_mentor.id is null then raise exception 'mentor_not_found'; end if; |
There was a problem hiding this comment.
book_mentorship never checks hackathon_registrations for the caller, unlike mentorship_board (line 120-125), whose own comment explains why that check exists: "The page checks these too, but the function is callable straight over PostgREST, where nothing checked anything." That reasoning applies at least as much to the write path.
create_team_with_leader (00002_rpcs.sql) has no registration check either — it only requires auth. So a user can sign up, create a team, and call book_mentorship directly over PostgREST (bypassing the /h/[slug]/mentorship page's isRegistrationComplete redirect entirely) to claim a mentor slot without ever registering for the edition. That's exactly the abuse case the PR description calls out ("people with no team fill the agenda of the teams actually competing") — a "team" here can be created without registering.
Suggest mirroring mentorship_board's check: if not exists (select 1 from hackathon_registrations where user_id = v_user and hackathon_id = v_mentor.hackathon_id) then raise exception 'not_registered'; end if; before resolving the team.
The mentorship day runs on the mentors' own scheduling pages, and those links are bearer tokens: whoever holds one takes a slot. Without a gate, people with no team fill the agenda of the teams actually competing. So the platform does two things and no more. It reveals a mentor's link only to the leader of a formed team, and only for the mentor that team already chose; and it records the choice so the organizers can reconcile against the external agendas on the day. booking_url therefore leaves the database in exactly one shape. The catalog mentorship_board returns carries name, specialty and track — never the link — so a leader cannot lift every URL out of the page payload before choosing anything. Both tables run RLS with no policies at all, the submission_ratings posture: a row policy protects rows, not columns, and a leader allowed to select a mentor row is a leader allowed to select booking_url. Every member-facing read and write goes through the two SECURITY DEFINER functions instead; 00009's default privileges hand new tables select to anon, so the grant that matters is a revoke. Mentors come in two tracks and a team may claim one of each, which makes the limit (team, track) rather than (team) — one partial unique index carries the whole product rule. book_mentorship resolves the team from the mentor's own edition, so a leader who leads teams in two editions cannot spend the wrong one's slot, and copies the track from the mentor row inside the same transaction. Deliberately absent: capacity, a publish flag and a booking window. The mentorship is a single day and the external agenda is the real allocator. `available` is the manual off switch for a mentor whose agenda filled, which is a different thing from counting. Undo stays with the organizers: releasing is a soft release, so the ledger keeps the history and the track reopens the moment it commits. The five mentors with links are seeded here so the admin screen is not on the critical path the night before. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…-in per edition, drop the seed main already has 00059. book_mentorship now requires the same complete registration as mentorship_board and the 00053 team RPCs. hackathons gains mentorship_enabled (default false, on for Passo Fundo 2026) and both RPCs raise mentorship_disabled while it is off. The five mentor rows leave the migration: booking links are bearer tokens and git history is forever, so production is seeded out of band.
Most editions will never run a mentorship day. editionUsesMentorship() gates the dashboard card and the participant page (404 when off); the admin page stays reachable with a ligar/desligar toggle, and the edition settings form gets the same flag as a checkbox. The RPCs already refuse while it is off (00060).
f7d93e9 to
6317cd4
Compare
|
Rebased on main and added two commits on top: the migration is now 00060 (main already had a 00059), book_mentorship gates on complete registration like mentorship_board, and the module is opt-in per edition via hackathons.mentorship_enabled (on for Cursor, off by default; dashboard card, member page and both RPCs respect it; toggle on the edition settings and the admin mentorship page). The five-mentor seed left the migration since booking links are bearer tokens; it will be run out of band on production at merge. 189 tests pass. |
|
Claude finished @thomgabriel's task in 3m 11s —— View job Review complete
Verdict: safe to merge — no findings. This is a re-review of the pushed changes since the last automated pass. The previously flagged gap is fixed: The new commits also add an opt-in
Branch: |
The mentorship day runs on the mentors' own scheduling pages, and those links
are bearer tokens: whoever holds one takes a slot. Without a gate, people with
no team fill the agenda of the teams actually competing.
So the platform does two things and no more. It reveals a mentor's link only to
the leader of a formed team, and only for the mentor that team already chose;
and it records the choice so the organizers can reconcile against the external
agendas on the day. It does not book anything, and it cannot see whether anyone
finished booking — the copy on the page says so plainly.
Mentors come in two tracks. A team may claim one of each: none, técnico only,
negócios only, or both — never two of the same.
The rule that shapes the design
booking_urlleaves the database in exactly one shape: the booking a teamalready made, and only to that team's leader. The catalog
mentorship_boardreturns carries name, specialty and track and never the link, so a leader
cannot lift every URL out of the page payload before choosing anything. An
earlier draft of this put the link in the catalog and would have handed the
whole list to anyone who opened devtools, with an empty ledger to show for it.
Both tables run RLS with no policies at all — the
submission_ratingsposture already documented in CLAUDE.md. A row policy protects rows, not
columns, and a leader allowed to
selecta mentor row is a leader allowed toselect=booking_url. Every member-facing read and write goes through the twoSECURITY DEFINERfunctions instead. Note00009's default privileges handevery new table
selecttoanon, so the grant that matters here is arevoke, not a grant.
One index carries the product rule
book_mentorshipresolves the team from the mentor's edition rather thananything the client sends, so a leader who leads teams in two editions cannot
spend the wrong one's slot, and copies
trackfrom the mentor row inside thesame transaction.
for update of tserialises a double click; the index is thereal backstop and turns a
unique_violationintoalready_booked.An RPC rather than a service-role server action, per CLAUDE.md's rule for
member-facing cross-table writes: the action shape would need a read-then-insert
that a double click passes twice, and would put
service_rolein a participantpath for a flow with a natural RPC shape.
Deliberately absent
No capacity counting, no publish flag, no booking window — the mentorship is a
single day and the external agenda is the real allocator.
availableis themanual off switch for a mentor whose agenda filled, which is a different thing
from counting. Undo stays with the organizers:
releaseBookingis a softrelease, so the ledger keeps the history and the track reopens on commit.
Accepted risk, raised and decided: without self-service switching, every team
that picks a mentor whose agenda is full becomes a manual request to an
organizer mid-event. The
availabletoggle is the mitigation — one tap stopsthe incident repeating.
Entry point
A card in the dashboard sidebar, not a sixth nav tab: the segmented control is
overflow-x-autowith a hidden scrollbar, and five labels already overflow a360px phone.
dashboard/page.tsxalready has the team snapshot in scope, so thecard costs no extra round trip.
What is in here
00059_mentorship.sql— two tables, the partial unique index, the revokes,RLS with no policies,
mentorship_boardandbook_mentorship, and a seed ofthe five mentors who have links, so the admin screen is not on the critical
path the night before.
/h/[slug]/mentorship— four states, all decided by the purementorshipView./admin/h/[slug]/mentorship— two panels with inline edit and bulk paste(
Nome | Especialidade | link, one per line), the availability toggle, and thebookings ledger with a release button.
house convention.
Verification
npm run buildandnpm testpass (159 tests). The check that matters beforethis ships is the leak test: as a leader, call
mentorship_boardand confirm nocatalog mentor carries a
booking_urlkey; as a non-leader member, confirm thelink is absent from the RSC payload.
Before the event
not exist yet. Add him from the admin screen when it arrives. Until then the
técnico track has two mentors against three on negócios.
kind='mentoria'content row from seed00017still reads "Horários ementores a confirmar" and shows in Conteúdos to every participant — two
surfaces named "Mentorias" on the day unless it is unpublished or rewritten.
page_md(00044:26) still describes mentorship as a WhatsApp thing.🤖 Generated with Claude Code