Skip to content

feat(invitations): implement team invitation management #33

Closed
maksberegovoi wants to merge 0 commit into
devfrom
feat/team-invitations
Closed

feat(invitations): implement team invitation management #33
maksberegovoi wants to merge 0 commit into
devfrom
feat/team-invitations

Conversation

@maksberegovoi
Copy link
Copy Markdown
Collaborator

@maksberegovoi maksberegovoi commented Apr 23, 2026

No description provided.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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();
Copy link

Copilot AI Apr 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ioredis multi.exec() не гарантирует throw при ошибке отдельной команды — часто возвращает массив [err, result] или null. Сейчас removeInvitation() всегда возвращает success даже при частичном фейле, из-за чего индексы (team/user set) могут рассинхронизироваться с inv:code. Проверьте результат exec(): null и/или наличие err в элементах — и выбрасывайте исключение.

Suggested change
await multi.exec();
const execResult = await multi.exec();
if (!execResult || execResult.some(([err]) => err != null)) {
throw new Error('Redis transaction returned an invalid result');
}

Copilot uses AI. Check for mistakes.
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();
Copy link

Copilot AI Apr 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Аналогично: в 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');
}

Copilot uses AI. Check for mistakes.
Comment on lines +83 to +89
return invitesRaw
.map((raw, idx) => {
if (!raw) return null;
return this.parseInvite(raw, codes[idx]);
})
.filter((v): v is TeamInvite => Boolean(v));
};
Copy link

Copilot AI Apr 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Сейчас getInvitations() формирует объекты с полем code (через parseInvite(raw, code)), но затем сужает тип через filter((v): v is TeamInvite => ...). В итоге тип возвращаемого значения «теряет» code, хотя Swagger/контракт эндпоинта его ожидает. Лучше ввести отдельный тип для ответа (например TeamInvite & { code: string }) и использовать его в parseInvite/фильтре/сигнатурах методов.

Copilot uses AI. Check for mistakes.
teamName: z.string().describe('Название команды'),
teamAvatar: z.string().nullable().describe('Аватар команды'),
email: z.string().email().describe('Email приглашённого пользователя'),
role: z.string().describe('Роль, которая будет назначена после принятия инвайта'),
Copy link

Copilot AI Apr 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TeamInvitationSchema: поле role объявлено как z.string(), хотя в остальных DTO (InviteMember/UpdateInvitation) role валидируется через roleEnum.enumValues. Из-за этого Swagger/валидация ответа будут показывать «любой string», что расходится с реальными поддерживаемыми ролями. Лучше сделать role: z.enum(roleEnum.enumValues) (или использовать TeamRole) и синхронизировать контракт.

Suggested change
role: z.string().describe('Роль, которая будет назначена после принятия инвайта'),
role: z
.enum(roleEnum.enumValues)
.describe('Роль, которая будет назначена после принятия инвайта'),

Copilot uses AI. Check for mistakes.
import { InviteMemberDto, UpdateInvitationDto } from '../dtos';
import { ConfigService } from '@nestjs/config';
import { BaseException } from '@shared/error';
import { TeamInvite } from '@core/modules/teams/dtos/invitation.dto';
Copy link

Copilot AI Apr 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Импорт TeamInvite идёт через абсолютный alias @core/modules/teams/... внутри самого модуля teams. В остальных сервисах модуля используются относительные импорты (../dtos, ../mappers и т.п.), поэтому лучше придерживаться того же подхода (например, импортировать из ../dtos/invitation.dto или экспортировать тип через ../dtos). Это уменьшит путаницу и риск циклических зависимостей.

Suggested change
import { TeamInvite } from '@core/modules/teams/dtos/invitation.dto';
import { TeamInvite } from '../dtos/invitation.dto';

Copilot uses AI. Check for mistakes.
Comment on lines +81 to +82
const invitesRaw = await this.redis.mget(...keys);

Copy link

Copilot AI Apr 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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);
}

Copilot uses AI. Check for mistakes.
Comment thread src/modules/teams/services/invitations.service.ts Outdated
Comment thread src/modules/teams/services/invitations.service.ts Outdated
Comment on lines 45 to 49
@Post(':code/accept')
@AcceptInviteSwagger()
async accept(@Param('code') code: string, @GetUser() user: JwtPayload) {
return this.facade.acceptInvite(code, user.sub, user.email);
}
Copy link

Copilot AI Apr 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Эндпоинт принятия инвайта находится под базовым путём teams/:slug/invitations, но метод accept() не использует slug и в данных инвайта/ответах users/me/invites slug тоже не передаётся (там есть только code и teamName). В результате клиент с ссылкой вида /invites/accept?code=... не сможет однозначно сформировать API URL с обязательным :slug. Стоит либо вынести accept в отдельный контроллер без slug в маршруте, либо добавлять teamSlug в payload инвайта/ответы и обновить Swagger (ApiParam slug) соответственно.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants