Skip to content

feat(api/v2): ✨ Implement member routes - #138

Open
kyanvde wants to merge 2 commits into
api-v2/claimsfrom
api-v2/members-routes
Open

feat(api/v2): ✨ Implement member routes#138
kyanvde wants to merge 2 commits into
api-v2/claimsfrom
api-v2/members-routes

Conversation

@kyanvde

@kyanvde kyanvde commented Aug 28, 2026

Copy link
Copy Markdown
Contributor

Closes #63. Supersedes #83 — that draft implemented GET /members only; 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).

Stacked on #136. Base is api-v2/claims, not api/v2, because member changes queue Discord and revalidation jobs and QueueService lands in that PR. GitHub retargets the base to api/v2 automatically once #136 merges.

Also needs #139 (against main) for the member webhook events — see below. Nothing here breaks without it; the events are just dropped by the worker until it ships.

Routes

Route Notes
GET /members Paginated, sortable, filterable
POST /members Adds a user named by BTE ID, Keycloak ID, Discord ID or Minecraft name
GET /members/:userId
PUT /members/:userId Idempotent membership put — see below
DELETE /members/:userId
GET /members/:userId/permissions This team's grants only
PUT /members/:userId/permissions Bulk grant
DELETE /members/:userId/permissions/:permissionId

Each is registered bare and behind a :teamId prefix. 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 call

The issue lists put /members/[userId] next to post /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:

  • Grants are always scoped to the authenticated team. buildTeamId is set from the token, never from the payload.
  • A team cannot grant a global permission. The Permisision.global flag is checked and a global key answers 403, or a team could hand itself rights over the whole site.
  • A team only sees its own grants. The listing filters on buildTeamId, so a user's global permissions and the ones other teams gave them stay invisible.
  • Revoking takes the grant ID, not the permission key — the ID the listing returns — and matches on the team too, so a team cannot revoke a grant someone else made.
  • Removing a member drops the grants that team gave them, in the same transaction as the disconnect, rather than leaving orphaned rows behind.

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:

Job Notes
BUILDTEAM_WEBHOOK MEMBER_ADD / MEMBER_REMOVE, delivered to the team's own webhook
SYNC_DISCORD_ROLES Skipped for a member with no linked Discord account
REVALIDATE_WEBSITE The pages that list members

Two details worth a second pair of eyes:

  • The builder role is decided by counting the teams they are still in. Leaving one team does not stop someone building in another, so the role only goes away if that was their last one.
  • The webhook payload is listed field by field, not spread. A member is selected with ssoId on 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_ROLES and the two member event types are added to common/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 carrying ssoId, and each permission guard.
  • members.routes.spec.ts — end to end: all six member routes rejected without a token, the :teamId prefix rejecting another team, 403 on a global permission, 404 on another team's grant, and unknown body fields refused.

yarn ws api-v2 build passes. yarn ws api-v2 lint reports the 6 pre-existing unbound-method errors documented in CLAUDE.md and nothing new.

🤖 Generated with Claude Code

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>
@github-project-automation github-project-automation Bot moved this from Backlog to In Progress in @BuildTheEarth/web Tracker Aug 29, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: In Progress

Development

Successfully merging this pull request may close these issues.

2 participants