feat(api/v2): ✨ Implement member routes - #138
Open
kyanvde wants to merge 2 commits into
Open
Conversation
Replaces the earlier draft of this section with the full set: the member list, adding a member by any account a team knows them by, reading one, putting a membership by ID, removing one, and the three permission routes. Every route is scoped to the team the token belongs to. A team can only see and revoke the permissions it granted itself, and cannot grant a global one at all. Removing a member drops the permissions that team gave them and re-syncs their Discord builder role, which only goes away if that was the last team they were in. Closes #63 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The worker learns both event types in #139, so adding and removing a member now reaches the team's own webhook as well as Discord and the website revalidation. The payload is listed field by field rather than spread: a member is selected with ssoId on it, which is the Keycloak account behind the person and has no business leaving this service, and a test now holds that line. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Nudelsuppe42
approved these changes
Aug 29, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #63. Supersedes #83 — that draft implemented
GET /membersonly; this is the whole section rewritten from scratch, in the repo's formatting and conventions. Close #83 when this lands (I left it alone rather than force-pushing over its branch).Routes
GET /membersPOST /membersGET /members/:userIdPUT /members/:userIdDELETE /members/:userIdGET /members/:userId/permissionsPUT /members/:userId/permissionsDELETE /members/:userId/permissions/:permissionIdEach is registered bare and behind a
:teamIdprefix. Nothing here is public — a member list is a list of people with their Discord and Minecraft accounts attached — so every route requires a token and is scoped to the team it belongs to.PUT /members/:userId— the one judgment callThe issue lists
put /members/[userId]next topost /members, but a user row is shared by every build team: there is nothing on it a single team may edit, and letting one team rename another team's builder would be wrong. So this route puts the membership rather than the user: it makes sure the user is a member and answers the member either way.That makes it idempotent and safe for a tool that syncs its roster repeatedly, which is the use I'd expect it to have. If you meant something else by it, say so and I'll change it — it is the only route here whose meaning wasn't obvious from the issue.
Permission safety
The permission routes are the part worth reviewing closely, because they are how a team could escalate its own reach:
buildTeamIdis set from the token, never from the payload.Permisision.globalflag is checked and a global key answers 403, or a team could hand itself rights over the whole site.buildTeamId, so a user's global permissions and the ones other teams gave them stay invisible.The bulk grant follows the same convention as socials and application questions: it only ever adds, and grants not named in the payload are left alone. Capped at 100 per request.
Worker jobs
Adding or removing a member queues three things, none of which the request waits on:
BUILDTEAM_WEBHOOKMEMBER_ADD/MEMBER_REMOVE, delivered to the team's own webhookSYNC_DISCORD_ROLESREVALIDATE_WEBSITETwo details worth a second pair of eyes:
ssoIdon it — the Keycloak account behind the person — and spreading the row would have sent it to every team's webhook. feat(worker/tasks): ✨ Handle MEMBER_ADD and MEMBER_REMOVE webhooks #139 also projects it out worker-side, so this is belt and braces; a test holds the line here either way.SYNC_DISCORD_ROLESand the two member event types are added tocommon/queue/jobs.ts, mirroring the worker's task schemas.Testing
yarn ws api-v2 test— 33 suites, 291 tests, all passing. 57 are new:members.service.spec.ts— team scoping on every read, user resolution by Minecraft name, the Discord role decision in both directions, the webhook payload not carryingssoId, and each permission guard.members.routes.spec.ts— end to end: all six member routes rejected without a token, the:teamIdprefix rejecting another team, 403 on a global permission, 404 on another team's grant, and unknown body fields refused.yarn ws api-v2 buildpasses.yarn ws api-v2 lintreports the 6 pre-existingunbound-methoderrors documented in CLAUDE.md and nothing new.🤖 Generated with Claude Code