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
11 changes: 3 additions & 8 deletions src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,11 @@ import { PrometheusModule } from '@willsoto/nestjs-prometheus';
import { HealthModule } from '@libs/health';
import { UserModule } from './modules/user';
import { GlobalExceptionFilter } from '@shared/error';
import { AuthModule } from './modules/auth';
import { AuthModule } from './auth/auth.module';
import { BullBoardModule } from '@bull-board/nestjs';
import { FastifyAdapter } from '@bull-board/fastify';
import { MailProcessor } from '@shared/workers';
import { BullModule } from '@nestjs/bullmq';
import { MailAdapter } from '@shared/adapters/mail';
import { MailModule } from '@shared/adapters/mail';
import { MigrationService } from '@shared/migration';
import { TeamsModule } from './modules/teams';
import { ProjectsModule } from './modules/projects';
Expand Down Expand Up @@ -63,11 +62,7 @@ import { ProjectsModule } from './modules/projects';
],
providers: [
MigrationService,
{
provide: 'IMailPort',
useClass: MailAdapter,
},
MailProcessor,
MailModule,
{
provide: APP_PIPE,
useClass: ZodValidationPipe,
Expand Down
66 changes: 66 additions & 0 deletions src/auth/application/auth.facade.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { Injectable } from '@nestjs/common';
import {
SignInUseCase,
SignUpUseCase,
SignOutUseCase,
SignUpVerifyUseCase,
RefreshTokensUseCase,
ResetPasswordUseCase,
VerifyResetPasswordUseCase,
ConfirmResetPasswordUseCase,
} from './use-cases';
import {
PasswordResetConfirmDto,
ResetPasswordDto,
SignInDto,
SignUpDto,
VerifyDto,
VerifyResetCodeDto,
} from './dtos';
import type { DeviceMetadata } from '../infrastructure/utils/get-device-meta';

@Injectable()
export class AuthFacade {
constructor(
private readonly signInUseCase: SignInUseCase,
private readonly signUpUseCase: SignUpUseCase,
private readonly signOutUseCase: SignOutUseCase,
private readonly signUpVerifyUseCase: SignUpVerifyUseCase,
private readonly refreshTokensUseCase: RefreshTokensUseCase,
private readonly resetPasswordUseCase: ResetPasswordUseCase,
private readonly verifyResetPasswordUseCase: VerifyResetPasswordUseCase,
private readonly confirmResetPasswordUseCase: ConfirmResetPasswordUseCase,
) {}

async signIn(dto: SignInDto, device: DeviceMetadata) {
return this.signInUseCase.execute(dto, device);
}

async signUp(dto: SignUpDto) {
return this.signUpUseCase.execute(dto);
}

async verifySignUp(dto: VerifyDto, device: DeviceMetadata) {
return this.signUpVerifyUseCase.execute(dto, device);
}

async signOut(userId: string) {
return this.signOutUseCase.execute(userId);
}

async refreshTokens(token: string, device: DeviceMetadata) {
return this.refreshTokensUseCase.execute(token, device);
}

async sendResetCode(dto: ResetPasswordDto) {
return this.resetPasswordUseCase.execute(dto);
}

async verifyResetCode(dto: VerifyResetCodeDto) {
return this.verifyResetPasswordUseCase.execute(dto);
}

async confirmNewPassword(dto: PasswordResetConfirmDto) {
return this.confirmResetPasswordUseCase.execute(dto);
}
}
Original file line number Diff line number Diff line change
@@ -1,21 +1,21 @@
import { ApiBaseController } from '../../../shared/decorators';
import { ApiBaseController } from '@shared/decorators';
import { Body, HttpCode, Post, Req, Res, UseGuards } from '@nestjs/common';
import { AuthService } from '../services';
import {
PostLoginSwagger,
PostLogoutSwagger,
PostRefreshSwagger,
PostRegisterSwagger,
PostSignUpConfirmSwagger,
} from './auth.swagger';
import { SignInDto, SignUpDto, VerifyDto } from '../dtos';
} from './swagger';
import { SignInDto, SignUpDto, VerifyDto } from '../../dtos';
import type { FastifyReply, FastifyRequest } from 'fastify';
import { getDeviceMeta } from '../helpers';
import { BearerAuthGuard, CookieAuthGuard } from '@shared/guards';
import { AuthFacade } from '../../auth.facade';
import { getDeviceMeta } from '@core/auth/infrastructure/utils/get-device-meta';

@ApiBaseController('auth', 'Auth')
export class AuthController {
constructor(private readonly facade: AuthService) {}
constructor(private readonly facade: AuthFacade) {}

@Post('sign-up')
@PostRegisterSwagger()
Expand All @@ -27,13 +27,13 @@ export class AuthController {
@Post('sign-up/confirm')
@PostSignUpConfirmSwagger()
@HttpCode(201)
async verify(
async verifySignUp(
@Res({ passthrough: true }) res: FastifyReply,
@Req() req: FastifyRequest,
@Body() dto: VerifyDto,
) {
const meta = getDeviceMeta(req);
const { tokens, ...response } = await this.facade.verify(dto, meta);
const { tokens, ...response } = await this.facade.verifySignUp(dto, meta);

res.setCookie('refresh', tokens.refresh, {
httpOnly: true,
Expand Down Expand Up @@ -84,7 +84,7 @@ export class AuthController {
async refresh(@Res({ passthrough: true }) res: FastifyReply, @Req() req: FastifyRequest) {
const meta = getDeviceMeta(req);
const session = req.cookies?.['refresh'];
const { tokens, ...response } = await this.facade.refresh(session, meta);
const { tokens, ...response } = await this.facade.refreshTokens(session, meta);

res.setCookie('refresh', tokens.refresh, {
httpOnly: true,
Expand Down
143 changes: 143 additions & 0 deletions src/auth/application/controller/auth/swagger.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
import { applyDecorators } from '@nestjs/common';
import { ApiBody, ApiOperation, ApiParam, ApiResponse } from '@nestjs/swagger';
import {
ApiBadRequest,
ApiConflict,
ApiForbidden,
ApiNotFound,
ApiUnauthorized,
ApiValidationError,
} from '@shared/error';
import { SignInDto, SignUpDto, VerifyDto } from '../../dtos';
import { ActionResponse } from '@shared/dtos';

export const PostRegisterSwagger = () =>
applyDecorators(
ApiOperation({
summary: 'Регистрация нового пользователя',
description: 'Создает пользователя, базовые настройки безопасности и уведомлений.',
}),
ApiBody({ type: SignUpDto.Output }),
ApiResponse({
status: 201,
description: 'Пользователь успешно зарегистрирован.',
type: ActionResponse.Output,
}),
ApiValidationError('Ошибка валидации данных (например, неверный формат email)'),
ApiConflict('Пользователь с таким email уже существует'),
);

export const PostLoginSwagger = () =>
applyDecorators(
ApiOperation({
summary: 'Вход в систему',
description:
'Возвращает Access/Refresh токены. Если у пользователя включена 2FA, вернет временный токен.',
}),
ApiBody({ type: SignInDto.Output }),
ApiResponse({
status: 200,
description: 'Успешный вход.',
schema: {
example: {
success: true,
message: false,
token: 'eyJhbGciOiJIUzI1NiIsInR5c...',
},
},
}),
ApiBadRequest('Неверный формат email'),
ApiUnauthorized('Неверный email или пароль'),
);

export const PostRefreshSwagger = () =>
applyDecorators(
ApiOperation({
summary: 'Обновление токенов',
description: 'Выдает новую пару Access и Refresh токенов.',
}),
ApiResponse({
status: 200,
description: 'Токены успешно обновлены.',
schema: {
example: {
success: true,
token: 'eyJhbGciOiJIUzI1NiIsInR5c...',
message: 'def50200508a1768c7e...',
},
},
}),
ApiBadRequest('Ошибка валидации (не передан refresh токен)'),
ApiUnauthorized('Refresh токен недействителен, истек или отозван'),
);

export const PostLogoutSwagger = () =>
applyDecorators(
ApiOperation({
summary: 'Выход из системы',
description: 'Удаляет текущую сессию пользователя из Redis.',
}),
ApiResponse({ status: 200, description: 'Успешный выход.', type: ActionResponse.Output }),
ApiUnauthorized(),
);

export const PostSignUpConfirmSwagger = () =>
applyDecorators(
ApiOperation({
summary: 'Подтверждение регистрации по коду',
description:
'Проверяет OTP из письма, создаёт аккаунт, выдаёт access-токен в теле ответа и устанавливает refresh в httpOnly cookie.',
}),
ApiBody({ type: VerifyDto.Output }),
ApiResponse({
status: 201,
description: 'Аккаунт подтверждён, сессия создана.',
schema: {
example: {
success: true,
message: 'Аккаунт успешно подтвержден',
token: 'eyJhbGciOiJIUzI1NiIsInR5c...',
},
},
}),
ApiValidationError('Ошибка валидации (неверный формат email или длина кода)'),
ApiBadRequest('Срок регистрации истёк или сессия не найдена'),
ApiBadRequest('Неверный или истёкший код подтверждения'),
);

export const GetSessionsSwagger = () =>
applyDecorators(
ApiOperation({
summary: 'Получить активные сессии',
description: 'Возвращает список всех активных устройств/сессий пользователя.',
}),
ApiResponse({
status: 200,
description: 'Список сессий успешно получен.',
schema: {
example: [
{
id: 'clj1xyz990000abc1',
device: 'Chrome on macOS',
ip: '192.168.1.1',
lastActive: '2026-04-11T14:30:00.000Z',
isCurrent: true,
},
],
},
}),
ApiUnauthorized(),
);

export const DeleteSessionSwagger = () =>
applyDecorators(
ApiOperation({
summary: 'Завершить чужую сессию',
description: 'Принудительно удаляет указанную сессию из Redis.',
}),
ApiParam({ name: 'cuid', description: 'ID сессии, которую нужно завершить' }),
ApiResponse({ status: 200, description: 'Сессия успешно завершена.' }),
ApiUnauthorized(),
ApiForbidden(),
ApiNotFound('Сессия не найдена или уже истекла'),
);
2 changes: 2 additions & 0 deletions src/auth/application/controller/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { AuthController } from './auth/controller';
export { AuthRecoveryController } from './recovery/controller';
Original file line number Diff line number Diff line change
@@ -1,32 +1,32 @@
import { ApiBaseController } from '../../../shared/decorators';
import { ApiBaseController } from '@shared/decorators';
import { Body, Post } from '@nestjs/common';
import { AuthRecoveryService } from '../services';
import {
PostPasswordResetConfirmSwagger,
PostPasswordResetSwagger,
PostPasswordResetVerifySwagger,
} from './auth.swagger';
import { PasswordResetConfirmDto, ResetPasswordDto, VerifyResetCodeDto } from '../dtos';
} from './swagger';
import { PasswordResetConfirmDto, ResetPasswordDto, VerifyResetCodeDto } from '../../dtos';
import { AuthFacade } from '../../auth.facade';

@ApiBaseController('auth', 'Auth Recovery')
export class AuthRecoveryController {
constructor(private readonly facade: AuthRecoveryService) {}
constructor(private readonly facade: AuthFacade) {}

@Post('password/reset')
@PostPasswordResetSwagger()
async resetPasswordRequest(@Body() dto: ResetPasswordDto) {
return this.facade.resetPass(dto);
async sendResetCode(@Body() dto: ResetPasswordDto) {
return this.facade.sendResetCode(dto);
}

@Post('password/reset/verify')
@PostPasswordResetVerifySwagger()
async verifyResetCode(@Body() dto: VerifyResetCodeDto) {
return this.facade.verifyResetPassword(dto);
return this.facade.verifyResetCode(dto);
}

@Post('password/reset/confirm')
@PostPasswordResetConfirmSwagger()
async confirmPasswordReset(@Body() dto: PasswordResetConfirmDto) {
return this.facade.confirmResetPass(dto);
async confirmNewPassword(@Body() dto: PasswordResetConfirmDto) {
return this.facade.confirmNewPassword(dto);
}
}
Loading
Loading