feat(invitations): implement team invitation management #33
feat(invitations): implement team invitation management #33maksberegovoi wants to merge 0 commit into
Conversation
There was a problem hiding this comment.
Pull request overview
This PR adds server-side “team invitation management” capabilities to the Teams module: listing invitations for a team, fetching a single invitation, updating an invitation’s role, and deleting an invitation, alongside new DTOs and Swagger documentation.
Changes:
- Implemented invitation CRUD-like management operations backed by Redis (list/get/update/delete).
- Added DTOs/Zod schemas for invitation responses and role updates, and improved role validation for inviting members.
- Extended Swagger docs and replaced deprecated invitation controller stubs with working endpoints.
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 9 comments.
Show a summary per file
| File | Description |
|---|---|
| src/modules/teams/services/invitations.service.ts | Adds Redis key helpers + invitation management methods; refactors invite accept/remove flow. |
| src/modules/teams/entities/enums.ts | Exposes TeamRole type derived from roleEnum. |
| src/modules/teams/dtos/member.dto.ts | Validates invite role using roleEnum instead of free-form string. |
| src/modules/teams/dtos/invitation.dto.ts | Introduces invitation DTOs/schemas for update + response payload. |
| src/modules/teams/dtos/index.ts | Re-exports new invitation DTOs. |
| src/modules/teams/controller/teams.swagger.ts | Adds Swagger decorators for team invitation management endpoints. |
| src/modules/teams/controller/invitations.controller.ts | Implements invitation management routes (list/get/update/delete) replacing deprecated placeholders. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| multi.del(this.INVITES_KEY(code)); | ||
| multi.srem(this.TEAM_INVITES_KEY(teamId), code); | ||
| multi.srem(this.USER_INVITES_KEY(email.toLowerCase()), code); | ||
| await multi.exec(); |
There was a problem hiding this comment.
ioredis multi.exec() не гарантирует throw при ошибке отдельной команды — часто возвращает массив [err, result] или null. Сейчас removeInvitation() всегда возвращает success даже при частичном фейле, из-за чего индексы (team/user set) могут рассинхронизироваться с inv:code. Проверьте результат exec(): null и/или наличие err в элементах — и выбрасывайте исключение.
| await multi.exec(); | |
| const execResult = await multi.exec(); | |
| if (!execResult || execResult.some(([err]) => err != null)) { | |
| throw new Error('Redis transaction returned an invalid result'); | |
| } |
| multi.set(this.INVITES_KEY(code), JSON.stringify(inviteData), 'EX', this.INVITE_TTL); | ||
| multi.sadd(this.TEAM_INVITES_KEY(team.id), code); | ||
| multi.sadd(this.USER_INVITES_KEY(dto.email.toLowerCase()), code); | ||
| await multi.exec(); |
There was a problem hiding this comment.
Аналогично: в invite() результат multi.exec() не проверяется. При частичном фейле возможна ситуация, когда код добавился в team/user set, но сам inv:code: не записался (или наоборот), что сломает последующие списки/принятие/удаление. После exec() нужно валидировать, что транзакция не вернула null и что ни один элемент не содержит err.
Suggested change
await multi.exec();
const execResult = await multi.exec();
if (!execResult || execResult.some(([err]) => err)) {
throw new Error('Redis transaction returned an invalid result');
}
| return invitesRaw | ||
| .map((raw, idx) => { | ||
| if (!raw) return null; | ||
| return this.parseInvite(raw, codes[idx]); | ||
| }) | ||
| .filter((v): v is TeamInvite => Boolean(v)); | ||
| }; |
There was a problem hiding this comment.
Сейчас getInvitations() формирует объекты с полем code (через parseInvite(raw, code)), но затем сужает тип через filter((v): v is TeamInvite => ...). В итоге тип возвращаемого значения «теряет» code, хотя Swagger/контракт эндпоинта его ожидает. Лучше ввести отдельный тип для ответа (например TeamInvite & { code: string }) и использовать его в parseInvite/фильтре/сигнатурах методов.
| teamName: z.string().describe('Название команды'), | ||
| teamAvatar: z.string().nullable().describe('Аватар команды'), | ||
| email: z.string().email().describe('Email приглашённого пользователя'), | ||
| role: z.string().describe('Роль, которая будет назначена после принятия инвайта'), |
There was a problem hiding this comment.
TeamInvitationSchema: поле role объявлено как z.string(), хотя в остальных DTO (InviteMember/UpdateInvitation) role валидируется через roleEnum.enumValues. Из-за этого Swagger/валидация ответа будут показывать «любой string», что расходится с реальными поддерживаемыми ролями. Лучше сделать role: z.enum(roleEnum.enumValues) (или использовать TeamRole) и синхронизировать контракт.
| role: z.string().describe('Роль, которая будет назначена после принятия инвайта'), | |
| role: z | |
| .enum(roleEnum.enumValues) | |
| .describe('Роль, которая будет назначена после принятия инвайта'), |
| import { InviteMemberDto, UpdateInvitationDto } from '../dtos'; | ||
| import { ConfigService } from '@nestjs/config'; | ||
| import { BaseException } from '@shared/error'; | ||
| import { TeamInvite } from '@core/modules/teams/dtos/invitation.dto'; |
There was a problem hiding this comment.
Импорт TeamInvite идёт через абсолютный alias @core/modules/teams/... внутри самого модуля teams. В остальных сервисах модуля используются относительные импорты (../dtos, ../mappers и т.п.), поэтому лучше придерживаться того же подхода (например, импортировать из ../dtos/invitation.dto или экспортировать тип через ../dtos). Это уменьшит путаницу и риск циклических зависимостей.
| import { TeamInvite } from '@core/modules/teams/dtos/invitation.dto'; | |
| import { TeamInvite } from '../dtos/invitation.dto'; |
| const invitesRaw = await this.redis.mget(...keys); | ||
|
|
There was a problem hiding this comment.
getInvitations(): если код инвайта остался в set team:invites, но сам ключ inv:code: уже истёк, метод просто отфильтрует null и вернёт список без него. При этом «битые» коды останутся в Redis и будут создавать лишние mget на каждый запрос. Лучше при обнаружении raw === null сразу чистить TEAM_INVITES_KEY(teamId) (и при необходимости другие индексы).
Suggested change
const invitesRaw = await this.redis.mget(...keys);
const invitesRaw = await this.redis.mget(...keys);
const staleCodes = codes.filter((_, idx) => !invitesRaw[idx]);
if (staleCodes.length) {
await this.redis.srem(this.TEAM_INVITES_KEY(team.id), ...staleCodes);
}
| @Post(':code/accept') | ||
| @AcceptInviteSwagger() | ||
| async accept(@Param('code') code: string, @GetUser() user: JwtPayload) { | ||
| return this.facade.acceptInvite(code, user.sub, user.email); | ||
| } |
There was a problem hiding this comment.
Эндпоинт принятия инвайта находится под базовым путём teams/:slug/invitations, но метод accept() не использует slug и в данных инвайта/ответах users/me/invites slug тоже не передаётся (там есть только code и teamName). В результате клиент с ссылкой вида /invites/accept?code=... не сможет однозначно сформировать API URL с обязательным :slug. Стоит либо вынести accept в отдельный контроллер без slug в маршруте, либо добавлять teamSlug в payload инвайта/ответы и обновить Swagger (ApiParam slug) соответственно.
530905d to
e2da98f
Compare
No description provided.