Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 46 additions & 14 deletions src/modules/teams/controller/invitations.controller.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,44 @@
import { Body, Get, Param, Delete, Patch, Post } from '@nestjs/common';
import { ApiBaseController, GetUser, GetUserId } from '@shared/decorators';
import { TeamInvitationsService } from '../services';
import { AcceptInviteSwagger, InviteMemberSwagger } from './teams.swagger';
import {
AcceptInviteSwagger,
DeleteTeamInvitationSwagger,
GetTeamInvitationSwagger,
GetTeamInvitationsSwagger,
InviteMemberSwagger,
UpdateTeamInvitationSwagger,
} from './teams.swagger';
import type { JwtPayload } from '@shared/types';
import { ApiOperation } from '@nestjs/swagger';
import { InviteMemberDto, UpdateInvitationDto } from '../dtos';

@ApiBaseController('teams/:slug/invitations', 'Teams Invitations', true)
export class TeamsInvitationsController {
constructor(private readonly facade: TeamInvitationsService) {}

@Get()
@ApiOperation({ deprecated: true })
async getAll() {}
@GetTeamInvitationsSwagger()
async getAll(@Param('slug') slug: string, @GetUserId() userId: string) {
return this.facade.getInvitations(slug, userId);
}

@Get(':invitationId')
@ApiOperation({ deprecated: true })
async getOne() {}
@Get(':code')
@GetTeamInvitationSwagger()
async getOne(
@Param('slug') slug: string,
@Param('code') code: string,
@GetUserId() userId: string,
) {
return this.facade.getInvitation(slug, code, userId);
}

@Post()
@InviteMemberSwagger()
async invite(@Param('slug') slug: string, @GetUserId() inviterId: string, @Body() dto: any) {
async invite(
@Param('slug') slug: string,
@GetUserId() inviterId: string,
@Body() dto: InviteMemberDto,
) {
return this.facade.invite(slug, inviterId, dto);
}

Expand All @@ -29,11 +48,24 @@ export class TeamsInvitationsController {
return this.facade.acceptInvite(code, user.sub, user.email);
}

@Patch(':invitationId')
@ApiOperation({ deprecated: true })
async update() {}
@Patch(':code')
@UpdateTeamInvitationSwagger()
async update(
@Param('slug') slug: string,
@Param('code') code: string,
@GetUserId() userId: string,
@Body() dto: UpdateInvitationDto,
) {
return this.facade.updateInvitation(slug, code, userId, dto);
}

@Delete(':invitationId')
@ApiOperation({ deprecated: true })
async decline() {}
@Delete(':code')
@DeleteTeamInvitationSwagger()
async decline(
@Param('slug') slug: string,
@Param('code') code: string,
@GetUserId() userId: string,
) {
return this.facade.declineInvitation(slug, code, userId);
}
}
78 changes: 78 additions & 0 deletions src/modules/teams/controller/teams.swagger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,14 @@ import {
import {
CreateTeamDto,
InviteMemberDto,
TeamInvitationResponse,
SyncTagsDto,
UpdateTeamDto,
TagResponse,
TeamMemberResponse,
CheckSlugResponse,
UpdateMemberDto,
UpdateInvitationDto,
UserTeamResponse,
UserInviteResponse,
} from '../dtos';
Expand Down Expand Up @@ -306,3 +308,79 @@ export const AcceptInviteSwagger = () =>
ApiConflict('Пользователь уже является участником этой команды'),
ApiUnauthorized(),
);

export const GetTeamInvitationsSwagger = () =>
applyDecorators(
ApiOperation({
summary: 'Получить список всех приглашений в команду',
description: 'Возвращает все активные инвайты команды. Доступно только owner/admin.',
}),
ApiParam({ name: 'slug', description: 'Слаг команды' }),
ApiResponse({
status: 200,
description: 'Список приглашений команды',
type: [TeamInvitationResponse.Output],
}),
ApiNotFound('Команда не найдена'),
ApiForbidden('Недостаточно прав (только owner/admin)'),
ApiUnauthorized(),
);

export const GetTeamInvitationSwagger = () =>
applyDecorators(
ApiOperation({
summary: 'Получить приглашение по коду',
description:
'Возвращает данные инвайта по коду в рамках команды. Доступно только owner/admin.',
}),
ApiParam({ name: 'slug', description: 'Слаг команды' }),
ApiParam({ name: 'code', description: 'Код инвайта' }),
ApiResponse({
status: 200,
description: 'Инвайт найден',
type: TeamInvitationResponse.Output,
}),
ApiNotFound('Инвайт или команда не найдены'),
ApiForbidden('Недостаточно прав (только owner/admin)'),
ApiUnauthorized(),
);

export const UpdateTeamInvitationSwagger = () =>
applyDecorators(
ApiOperation({
summary: 'Обновить приглашение (только роль)',
description:
'Позволяет изменить только поле role у существующего инвайта. TTL сохраняется.',
}),
ApiParam({ name: 'slug', description: 'Слаг команды' }),
ApiParam({ name: 'code', description: 'Код инвайта' }),
ApiBody({ type: UpdateInvitationDto.Output }),
ApiResponse({
status: 200,
description: 'Инвайт обновлён',
type: TeamInvitationResponse.Output,
}),
ApiValidationError(),
ApiNotFound('Инвайт или команда не найдены'),
ApiForbidden('Недостаточно прав (только owner/admin)'),
ApiUnauthorized(),
);

export const DeleteTeamInvitationSwagger = () =>
applyDecorators(
ApiOperation({
summary: 'Удалить приглашение',
description:
'Удаляет инвайт и чистит индексы в Redis (team:invites и user:invites). Доступно только owner/admin.',
}),
ApiParam({ name: 'slug', description: 'Слаг команды' }),
ApiParam({ name: 'code', description: 'Код инвайта' }),
ApiResponse({
status: 200,
description: 'Инвайт удалён',
type: ActionResponse.Output,
}),
ApiNotFound('Инвайт или команда не найдены'),
ApiForbidden('Недостаточно прав (только owner/admin)'),
ApiUnauthorized(),
);
1 change: 1 addition & 0 deletions src/modules/teams/dtos/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ export {
TeamMemberResponse,
UserInviteResponse,
} from './member.dto';
export { UpdateInvitationDto, TeamInvitationResponse } from './invitation.dto';
export {
CreateTeamDto,
UpdateTeamDto,
Expand Down
38 changes: 38 additions & 0 deletions src/modules/teams/dtos/invitation.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { z } from 'zod/v4';
import { createZodDto } from 'nestjs-zod';
import { roleEnum, TeamRole } from '../entities/enums';

export const UpdateInvitationSchema = z.object({
role: z
.enum(roleEnum.enumValues)
.describe('Новая роль, которая будет назначена пользователю после принятия инвайта'),
});

export class UpdateInvitationDto extends createZodDto(UpdateInvitationSchema) {}

export const TeamInvitationSchema = z.object({
code: z.string().describe('Код инвайта'),
teamId: z.string().describe('ID команды'),
teamName: z.string().describe('Название команды'),
teamAvatar: z.string().nullable().describe('Аватар команды'),
email: z.string().email().describe('Email приглашённого пользователя'),
role: z.string().describe('Роль, которая будет назначена после принятия инвайта'),
inviterId: z.string().describe('ID пользователя, отправившего приглашение'),
inviterName: z.string().describe('Имя пригласившего'),
createdAt: z.string().datetime().describe('Дата создания инвайта (ISO 8601)'),
expiresAt: z.string().datetime().describe('Дата истечения инвайта (ISO 8601)'),
});

export class TeamInvitationResponse extends createZodDto(TeamInvitationSchema) {}

export interface TeamInvite {
teamId: string;
teamName: string;
teamAvatar: string | null;
email: string;
role: TeamRole;
inviterId: string;
inviterName: string;
createdAt: string;
expiresAt: string;
}
3 changes: 2 additions & 1 deletion src/modules/teams/dtos/member.dto.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { z } from 'zod/v4';
import { createZodDto } from 'nestjs-zod';
import { roleEnum } from '../entities';

export const InviteMemberSchema = z.object({
email: z.string().email().describe('Email пользователя, которого нужно пригласить'),
role: z
.string()
.enum(roleEnum.enumValues)
.default('member')
.describe('Роль, которая будет назначена пользователю после принятия инвайта'),
});
Expand Down
2 changes: 2 additions & 0 deletions src/modules/teams/entities/enums.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ export const roleEnum = baseSchema.enum('team_role', [
'member', // обычный работяга
'viewer', // просто смотрит
]);
export type TeamRole = (typeof roleEnum.enumValues)[number];

export const statusEnum = baseSchema.enum('member_status', [
'active', // Полноценный участник
'banned', // Заблокирован не может вернуться по инвайту
Expand Down
Loading
Loading