Skip to content

Commit b21a631

Browse files
authored
refactor(auth): implement DDD architecture and layer separation (#49)
1 parent a360c20 commit b21a631

55 files changed

Lines changed: 942 additions & 647 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

src/app.module.ts

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,11 @@ import { PrometheusModule } from '@willsoto/nestjs-prometheus';
99
import { HealthModule } from '@libs/health';
1010
import { UserModule } from './modules/user';
1111
import { GlobalExceptionFilter } from '@shared/error';
12-
import { AuthModule } from './modules/auth';
12+
import { AuthModule } from './auth/auth.module';
1313
import { BullBoardModule } from '@bull-board/nestjs';
1414
import { FastifyAdapter } from '@bull-board/fastify';
15-
import { MailProcessor } from '@shared/workers';
1615
import { BullModule } from '@nestjs/bullmq';
17-
import { MailAdapter } from '@shared/adapters/mail';
16+
import { MailModule } from '@shared/adapters/mail';
1817
import { MigrationService } from '@shared/migration';
1918
import { TeamsModule } from './modules/teams';
2019
import { ProjectsModule } from './modules/projects';
@@ -63,11 +62,7 @@ import { ProjectsModule } from './modules/projects';
6362
],
6463
providers: [
6564
MigrationService,
66-
{
67-
provide: 'IMailPort',
68-
useClass: MailAdapter,
69-
},
70-
MailProcessor,
65+
MailModule,
7166
{
7267
provide: APP_PIPE,
7368
useClass: ZodValidationPipe,
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import { Injectable } from '@nestjs/common';
2+
import {
3+
SignInUseCase,
4+
SignUpUseCase,
5+
SignOutUseCase,
6+
SignUpVerifyUseCase,
7+
RefreshTokensUseCase,
8+
ResetPasswordUseCase,
9+
VerifyResetPasswordUseCase,
10+
ConfirmResetPasswordUseCase,
11+
} from './use-cases';
12+
import {
13+
PasswordResetConfirmDto,
14+
ResetPasswordDto,
15+
SignInDto,
16+
SignUpDto,
17+
VerifyDto,
18+
VerifyResetCodeDto,
19+
} from './dtos';
20+
import type { DeviceMetadata } from '../infrastructure/utils/get-device-meta';
21+
22+
@Injectable()
23+
export class AuthFacade {
24+
constructor(
25+
private readonly signInUseCase: SignInUseCase,
26+
private readonly signUpUseCase: SignUpUseCase,
27+
private readonly signOutUseCase: SignOutUseCase,
28+
private readonly signUpVerifyUseCase: SignUpVerifyUseCase,
29+
private readonly refreshTokensUseCase: RefreshTokensUseCase,
30+
private readonly resetPasswordUseCase: ResetPasswordUseCase,
31+
private readonly verifyResetPasswordUseCase: VerifyResetPasswordUseCase,
32+
private readonly confirmResetPasswordUseCase: ConfirmResetPasswordUseCase,
33+
) {}
34+
35+
async signIn(dto: SignInDto, device: DeviceMetadata) {
36+
return this.signInUseCase.execute(dto, device);
37+
}
38+
39+
async signUp(dto: SignUpDto) {
40+
return this.signUpUseCase.execute(dto);
41+
}
42+
43+
async verifySignUp(dto: VerifyDto, device: DeviceMetadata) {
44+
return this.signUpVerifyUseCase.execute(dto, device);
45+
}
46+
47+
async signOut(userId: string) {
48+
return this.signOutUseCase.execute(userId);
49+
}
50+
51+
async refreshTokens(token: string, device: DeviceMetadata) {
52+
return this.refreshTokensUseCase.execute(token, device);
53+
}
54+
55+
async sendResetCode(dto: ResetPasswordDto) {
56+
return this.resetPasswordUseCase.execute(dto);
57+
}
58+
59+
async verifyResetCode(dto: VerifyResetCodeDto) {
60+
return this.verifyResetPasswordUseCase.execute(dto);
61+
}
62+
63+
async confirmNewPassword(dto: PasswordResetConfirmDto) {
64+
return this.confirmResetPasswordUseCase.execute(dto);
65+
}
66+
}

src/modules/auth/controller/auth.controller.ts renamed to src/auth/application/controller/auth/controller.ts

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,21 @@
1-
import { ApiBaseController } from '../../../shared/decorators';
1+
import { ApiBaseController } from '@shared/decorators';
22
import { Body, HttpCode, Post, Req, Res, UseGuards } from '@nestjs/common';
3-
import { AuthService } from '../services';
43
import {
54
PostLoginSwagger,
65
PostLogoutSwagger,
76
PostRefreshSwagger,
87
PostRegisterSwagger,
98
PostSignUpConfirmSwagger,
10-
} from './auth.swagger';
11-
import { SignInDto, SignUpDto, VerifyDto } from '../dtos';
9+
} from './swagger';
10+
import { SignInDto, SignUpDto, VerifyDto } from '../../dtos';
1211
import type { FastifyReply, FastifyRequest } from 'fastify';
13-
import { getDeviceMeta } from '../helpers';
1412
import { BearerAuthGuard, CookieAuthGuard } from '@shared/guards';
13+
import { AuthFacade } from '../../auth.facade';
14+
import { getDeviceMeta } from '@core/auth/infrastructure/utils/get-device-meta';
1515

1616
@ApiBaseController('auth', 'Auth')
1717
export class AuthController {
18-
constructor(private readonly facade: AuthService) {}
18+
constructor(private readonly facade: AuthFacade) {}
1919

2020
@Post('sign-up')
2121
@PostRegisterSwagger()
@@ -27,13 +27,13 @@ export class AuthController {
2727
@Post('sign-up/confirm')
2828
@PostSignUpConfirmSwagger()
2929
@HttpCode(201)
30-
async verify(
30+
async verifySignUp(
3131
@Res({ passthrough: true }) res: FastifyReply,
3232
@Req() req: FastifyRequest,
3333
@Body() dto: VerifyDto,
3434
) {
3535
const meta = getDeviceMeta(req);
36-
const { tokens, ...response } = await this.facade.verify(dto, meta);
36+
const { tokens, ...response } = await this.facade.verifySignUp(dto, meta);
3737

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

8989
res.setCookie('refresh', tokens.refresh, {
9090
httpOnly: true,
Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
import { applyDecorators } from '@nestjs/common';
2+
import { ApiBody, ApiOperation, ApiParam, ApiResponse } from '@nestjs/swagger';
3+
import {
4+
ApiBadRequest,
5+
ApiConflict,
6+
ApiForbidden,
7+
ApiNotFound,
8+
ApiUnauthorized,
9+
ApiValidationError,
10+
} from '@shared/error';
11+
import { SignInDto, SignUpDto, VerifyDto } from '../../dtos';
12+
import { ActionResponse } from '@shared/dtos';
13+
14+
export const PostRegisterSwagger = () =>
15+
applyDecorators(
16+
ApiOperation({
17+
summary: 'Регистрация нового пользователя',
18+
description: 'Создает пользователя, базовые настройки безопасности и уведомлений.',
19+
}),
20+
ApiBody({ type: SignUpDto.Output }),
21+
ApiResponse({
22+
status: 201,
23+
description: 'Пользователь успешно зарегистрирован.',
24+
type: ActionResponse.Output,
25+
}),
26+
ApiValidationError('Ошибка валидации данных (например, неверный формат email)'),
27+
ApiConflict('Пользователь с таким email уже существует'),
28+
);
29+
30+
export const PostLoginSwagger = () =>
31+
applyDecorators(
32+
ApiOperation({
33+
summary: 'Вход в систему',
34+
description:
35+
'Возвращает Access/Refresh токены. Если у пользователя включена 2FA, вернет временный токен.',
36+
}),
37+
ApiBody({ type: SignInDto.Output }),
38+
ApiResponse({
39+
status: 200,
40+
description: 'Успешный вход.',
41+
schema: {
42+
example: {
43+
success: true,
44+
message: false,
45+
token: 'eyJhbGciOiJIUzI1NiIsInR5c...',
46+
},
47+
},
48+
}),
49+
ApiBadRequest('Неверный формат email'),
50+
ApiUnauthorized('Неверный email или пароль'),
51+
);
52+
53+
export const PostRefreshSwagger = () =>
54+
applyDecorators(
55+
ApiOperation({
56+
summary: 'Обновление токенов',
57+
description: 'Выдает новую пару Access и Refresh токенов.',
58+
}),
59+
ApiResponse({
60+
status: 200,
61+
description: 'Токены успешно обновлены.',
62+
schema: {
63+
example: {
64+
success: true,
65+
token: 'eyJhbGciOiJIUzI1NiIsInR5c...',
66+
message: 'def50200508a1768c7e...',
67+
},
68+
},
69+
}),
70+
ApiBadRequest('Ошибка валидации (не передан refresh токен)'),
71+
ApiUnauthorized('Refresh токен недействителен, истек или отозван'),
72+
);
73+
74+
export const PostLogoutSwagger = () =>
75+
applyDecorators(
76+
ApiOperation({
77+
summary: 'Выход из системы',
78+
description: 'Удаляет текущую сессию пользователя из Redis.',
79+
}),
80+
ApiResponse({ status: 200, description: 'Успешный выход.', type: ActionResponse.Output }),
81+
ApiUnauthorized(),
82+
);
83+
84+
export const PostSignUpConfirmSwagger = () =>
85+
applyDecorators(
86+
ApiOperation({
87+
summary: 'Подтверждение регистрации по коду',
88+
description:
89+
'Проверяет OTP из письма, создаёт аккаунт, выдаёт access-токен в теле ответа и устанавливает refresh в httpOnly cookie.',
90+
}),
91+
ApiBody({ type: VerifyDto.Output }),
92+
ApiResponse({
93+
status: 201,
94+
description: 'Аккаунт подтверждён, сессия создана.',
95+
schema: {
96+
example: {
97+
success: true,
98+
message: 'Аккаунт успешно подтвержден',
99+
token: 'eyJhbGciOiJIUzI1NiIsInR5c...',
100+
},
101+
},
102+
}),
103+
ApiValidationError('Ошибка валидации (неверный формат email или длина кода)'),
104+
ApiBadRequest('Срок регистрации истёк или сессия не найдена'),
105+
ApiBadRequest('Неверный или истёкший код подтверждения'),
106+
);
107+
108+
export const GetSessionsSwagger = () =>
109+
applyDecorators(
110+
ApiOperation({
111+
summary: 'Получить активные сессии',
112+
description: 'Возвращает список всех активных устройств/сессий пользователя.',
113+
}),
114+
ApiResponse({
115+
status: 200,
116+
description: 'Список сессий успешно получен.',
117+
schema: {
118+
example: [
119+
{
120+
id: 'clj1xyz990000abc1',
121+
device: 'Chrome on macOS',
122+
ip: '192.168.1.1',
123+
lastActive: '2026-04-11T14:30:00.000Z',
124+
isCurrent: true,
125+
},
126+
],
127+
},
128+
}),
129+
ApiUnauthorized(),
130+
);
131+
132+
export const DeleteSessionSwagger = () =>
133+
applyDecorators(
134+
ApiOperation({
135+
summary: 'Завершить чужую сессию',
136+
description: 'Принудительно удаляет указанную сессию из Redis.',
137+
}),
138+
ApiParam({ name: 'cuid', description: 'ID сессии, которую нужно завершить' }),
139+
ApiResponse({ status: 200, description: 'Сессия успешно завершена.' }),
140+
ApiUnauthorized(),
141+
ApiForbidden(),
142+
ApiNotFound('Сессия не найдена или уже истекла'),
143+
);
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export { AuthController } from './auth/controller';
2+
export { AuthRecoveryController } from './recovery/controller';
Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,32 @@
1-
import { ApiBaseController } from '../../../shared/decorators';
1+
import { ApiBaseController } from '@shared/decorators';
22
import { Body, Post } from '@nestjs/common';
3-
import { AuthRecoveryService } from '../services';
43
import {
54
PostPasswordResetConfirmSwagger,
65
PostPasswordResetSwagger,
76
PostPasswordResetVerifySwagger,
8-
} from './auth.swagger';
9-
import { PasswordResetConfirmDto, ResetPasswordDto, VerifyResetCodeDto } from '../dtos';
7+
} from './swagger';
8+
import { PasswordResetConfirmDto, ResetPasswordDto, VerifyResetCodeDto } from '../../dtos';
9+
import { AuthFacade } from '../../auth.facade';
1010

1111
@ApiBaseController('auth', 'Auth Recovery')
1212
export class AuthRecoveryController {
13-
constructor(private readonly facade: AuthRecoveryService) {}
13+
constructor(private readonly facade: AuthFacade) {}
1414

1515
@Post('password/reset')
1616
@PostPasswordResetSwagger()
17-
async resetPasswordRequest(@Body() dto: ResetPasswordDto) {
18-
return this.facade.resetPass(dto);
17+
async sendResetCode(@Body() dto: ResetPasswordDto) {
18+
return this.facade.sendResetCode(dto);
1919
}
2020

2121
@Post('password/reset/verify')
2222
@PostPasswordResetVerifySwagger()
2323
async verifyResetCode(@Body() dto: VerifyResetCodeDto) {
24-
return this.facade.verifyResetPassword(dto);
24+
return this.facade.verifyResetCode(dto);
2525
}
2626

2727
@Post('password/reset/confirm')
2828
@PostPasswordResetConfirmSwagger()
29-
async confirmPasswordReset(@Body() dto: PasswordResetConfirmDto) {
30-
return this.facade.confirmResetPass(dto);
29+
async confirmNewPassword(@Body() dto: PasswordResetConfirmDto) {
30+
return this.facade.confirmNewPassword(dto);
3131
}
3232
}

0 commit comments

Comments
 (0)