All checks were successful
CD / Build and push images (push) Successful in 3m3s
CI / Lint, typecheck, test (push) Successful in 2m29s
CI / Auth e2e pack (push) Successful in 3m8s
CI / Build container images (push) Has been skipped
CD / Deploy to Test (push) Successful in 9s
CD / Smoke tests against Test (push) Successful in 1m12s
CD / Promote to Int (push) Successful in 12s
Pond Admins manage who participates in a pond, by role, with editor/reader seat quotas — the member-facing layer over the grant model (#51/#52). - shared: `MemberView`/`PondMembersView` + add/change-role schemas (`members.ts`), a `members` i18n namespace (de+en), and member error codes. - api `members/`: a member-centric API over pond-scope user grants — `GET /ponds/:id/members` (any member, for transparency: list grouped by effective role + seat usage + `canManage`), `POST` (add by exact username or e-mail — no directory browsing), `PATCH :userId` (change role), `DELETE :userId` (remove), all Pond-Admin-gated by the guard. Editor/reader seats are enforced against `editors_per_pond`/`readers_per_pond` (#22) inside a per-pond advisory-locked transaction so counts cannot race; the owner's membership is protected, personal ponds refuse a second admin (shared grant rule), and the last Pond Admin cannot be dropped. Every change invalidates the pond permission cache and fires the access NOTIFY (#39/#53). - web `members/`: `MemberManager` in Pond Settings — list grouped by role with a search filter and seat usage, add-by-identifier form (disabled with a localized explanation when the chosen role's seats are full), per-member role change and remove; read-only for non-admins; the personal-pond rule is surfaced. There is no invitation flow (v1): adding is immediate, and the copy says so. - tests: `members.e2e.db.test.ts` (add/change/remove, seat exhaustion, personal-pond and owner rules, read-only transparency, last-admin) and a `members` browser pack (immediate second-browser access, quota disables the add action, non-admin read-only) with its own CI step. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EwZ4jR4KFAPvpjWevfUGX1
66 lines
2.1 KiB
TypeScript
66 lines
2.1 KiB
TypeScript
import { Body, Controller, Delete, Get, HttpCode, Param, Patch, Post, Req } from '@nestjs/common';
|
|
import {
|
|
AddMemberInput,
|
|
ChangeMemberRoleInput,
|
|
MemberView,
|
|
PondMembersView,
|
|
addMemberInputSchema,
|
|
changeMemberRoleInputSchema,
|
|
} from '@dorfteich/shared';
|
|
|
|
import { AuthedRequest } from '../auth/auth.guard';
|
|
import { ZodValidationPipe } from '../common/zod-validation.pipe';
|
|
import { RequiresPondRole } from '../permissions/permission.decorators';
|
|
import { MembersService } from './members.service';
|
|
|
|
/**
|
|
* Pond member management (issue #54). Listing is open to any member so the
|
|
* membership is transparent (read-only for non-admins); adding, changing a
|
|
* role, and removing are Pond-Admin-gated by the permission guard.
|
|
*/
|
|
@Controller('ponds/:pondId/members')
|
|
export class MembersController {
|
|
constructor(private readonly members: MembersService) {}
|
|
|
|
@Get()
|
|
@RequiresPondRole('reader', { idParam: 'pondId' })
|
|
async list(
|
|
@Param('pondId') pondId: string,
|
|
@Req() request: AuthedRequest,
|
|
): Promise<PondMembersView> {
|
|
return this.members.list(request.user ?? null, pondId);
|
|
}
|
|
|
|
@Post()
|
|
@RequiresPondRole('pond_admin', { idParam: 'pondId' })
|
|
async add(
|
|
@Param('pondId') pondId: string,
|
|
@Body(new ZodValidationPipe(addMemberInputSchema)) input: AddMemberInput,
|
|
@Req() request: AuthedRequest,
|
|
): Promise<MemberView> {
|
|
return this.members.add(request.user!, pondId, input);
|
|
}
|
|
|
|
@Patch(':userId')
|
|
@RequiresPondRole('pond_admin', { idParam: 'pondId' })
|
|
async changeRole(
|
|
@Param('pondId') pondId: string,
|
|
@Param('userId') userId: string,
|
|
@Body(new ZodValidationPipe(changeMemberRoleInputSchema)) input: ChangeMemberRoleInput,
|
|
@Req() request: AuthedRequest,
|
|
): Promise<MemberView> {
|
|
return this.members.changeRole(request.user!, pondId, userId, input);
|
|
}
|
|
|
|
@Delete(':userId')
|
|
@HttpCode(204)
|
|
@RequiresPondRole('pond_admin', { idParam: 'pondId' })
|
|
async remove(
|
|
@Param('pondId') pondId: string,
|
|
@Param('userId') userId: string,
|
|
@Req() request: AuthedRequest,
|
|
): Promise<void> {
|
|
await this.members.remove(request.user!, pondId, userId);
|
|
}
|
|
}
|