feat(threads): governed group settings — rename, list members, remove participant

- MessageService.renameThread / removeParticipant / listParticipants,
  each fail-closed via OPA (kernel stays generic — no dm/group branching)
- dev OPA: iios.thread.update + iios.thread.participant.remove rules
  (group requires ADMIN; ungoverned threads unchanged)
- REST: PATCH /v1/threads/:id, GET + DELETE /v1/threads/:id/participants
- tests: admin renames/removes, member is denied, member list carries roles
  (message.spec + dev-opa.port.spec)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-18 17:34:28 +05:30
parent b164ef945c
commit ce33834d56
5 changed files with 142 additions and 0 deletions
@@ -3,10 +3,12 @@ import {
BadRequestException,
Body,
Controller,
Delete,
Get,
Headers,
HttpCode,
Param,
Patch,
Post,
Query,
UseGuards,
@@ -56,6 +58,27 @@ export class ThreadsController {
return this.messages.addParticipant(id, this.principal(auth), body.userId, body.role);
}
/** Members of a thread with their role — drives the group settings member list. */
@Get(':id/participants')
async listParticipants(@Param('id') id: string, @Headers('authorization') auth?: string) {
return this.messages.listParticipants(id, this.principal(auth));
}
/** Governed removal: remove a user from a thread — policy enforces group-admin. */
@Delete(':id/participants/:userId')
@HttpCode(200)
async removeParticipant(@Param('id') id: string, @Param('userId') userId: string, @Headers('authorization') auth?: string) {
return this.messages.removeParticipant(id, this.principal(auth), userId);
}
/** Governed rename (group settings): change a thread subject — policy enforces group-admin. */
@Patch(':id')
@HttpCode(200)
async updateThread(@Param('id') id: string, @Body() body: { subject?: string }, @Headers('authorization') auth?: string) {
if (body?.subject == null) throw new BadRequestException('subject is required');
return this.messages.renameThread(id, this.principal(auth), body.subject);
}
@Get(':id/messages')
async listMessages(@Param('id') id: string) {
return this.threads.getMessages(id);