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
28 changes: 16 additions & 12 deletions src/auth/application/controller/auth/controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ import { ConfigService } from '@nestjs/config';

@ApiBaseController('auth', 'Auth')
export class AuthController {
private readonly isProduction: boolean = false;
private readonly domain: string | null = null;

constructor(
private readonly facade: AuthFacade,
private cfg: ConfigService,
Expand All @@ -24,9 +27,6 @@ export class AuthController {
this.domain = this.cfg.get('DOMAIN');
}

private readonly isProduction: boolean;
private readonly domain: string;

@Post('sign-up')
@PostRegisterSwagger()
@HttpCode(202)
Expand All @@ -43,9 +43,9 @@ export class AuthController {
@Body() dto: VerifyDto,
) {
const meta = getDeviceMeta(req);
const { tokens, ...response } = await this.facade.verifySignUp(dto, meta);
const { tokens, expiresAt, ...response } = await this.facade.verifySignUp(dto, meta);

this.setRefreshCookie(res, tokens.refresh);
this.setRefreshCookie(res, tokens.refresh, expiresAt);

return { ...response, token: tokens.access };
}
Expand All @@ -58,9 +58,9 @@ export class AuthController {
@Body() dto: SignInDto,
) {
const meta = getDeviceMeta(req);
const { tokens, ...response } = await this.facade.signIn(dto, meta);
const { tokens, expiresAt, ...response } = await this.facade.signIn(dto, meta);

this.setRefreshCookie(res, tokens.refresh);
this.setRefreshCookie(res, tokens.refresh, expiresAt);

return { ...response, token: tokens.access };
}
Expand All @@ -73,7 +73,10 @@ export class AuthController {
const session = req.cookies?.['refresh'];
const response = await this.facade.signOut(session);

res.clearCookie('refresh', { path: '/', domain: `.${this.domain}` });
res.clearCookie('refresh', {
path: '/',
domain: this.domain ? `.${this.domain}` : undefined,
});

return response;
}
Expand All @@ -85,20 +88,21 @@ 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.refreshTokens(session, meta);
const { tokens, expiresAt, ...response } = await this.facade.refreshTokens(session, meta);

this.setRefreshCookie(res, tokens.refresh);
this.setRefreshCookie(res, tokens.refresh, expiresAt);

return { token: tokens.access, ...response };
}

private setRefreshCookie(res: FastifyReply, refreshToken: string) {
private setRefreshCookie(res: FastifyReply, refreshToken: string, expires: Date) {
res.setCookie('refresh', refreshToken, {
httpOnly: true,
secure: this.isProduction,
path: '/',
expires,
sameSite: 'lax',
domain: `.${this.domain}`,
domain: this.domain ? `.${this.domain}` : undefined,
});
}
}
26 changes: 4 additions & 22 deletions src/auth/application/controller/auth/swagger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {
ApiUnauthorized,
ApiValidationError,
} from '@shared/error';
import { SignInDto, SignUpDto, VerifyDto } from '../../dtos';
import { SignInDto, SignResponse, SignUpDto, VerifyDto } from '../../dtos';
import { ActionResponse } from '@shared/dtos';

export const PostRegisterSwagger = () =>
Expand Down Expand Up @@ -38,13 +38,7 @@ export const PostLoginSwagger = () =>
ApiResponse({
status: 200,
description: 'Успешный вход.',
schema: {
example: {
success: true,
message: false,
token: 'eyJhbGciOiJIUzI1NiIsInR5c...',
},
},
type: SignResponse.Output,
}),
ApiBadRequest('Неверный формат email'),
ApiUnauthorized('Неверный email или пароль'),
Expand All @@ -59,13 +53,7 @@ export const PostRefreshSwagger = () =>
ApiResponse({
status: 200,
description: 'Токены успешно обновлены.',
schema: {
example: {
success: true,
token: 'eyJhbGciOiJIUzI1NiIsInR5c...',
message: 'def50200508a1768c7e...',
},
},
type: SignResponse.Output,
}),
ApiBadRequest('Ошибка валидации (не передан refresh токен)'),
ApiUnauthorized('Refresh токен недействителен, истек или отозван'),
Expand All @@ -92,13 +80,7 @@ export const PostSignUpConfirmSwagger = () =>
ApiResponse({
status: 201,
description: 'Аккаунт подтверждён, сессия создана.',
schema: {
example: {
success: true,
message: 'Аккаунт успешно подтвержден',
token: 'eyJhbGciOiJIUzI1NiIsInR5c...',
},
},
type: SignResponse.Output,
}),
ApiValidationError('Ошибка валидации (неверный формат email или длина кода)'),
ApiBadRequest('Срок регистрации истёк или сессия не найдена'),
Expand Down
7 changes: 7 additions & 0 deletions src/auth/application/dtos/auth.dto.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { ActionResponseSchema } from '@shared/dtos';
import { createZodDto } from 'nestjs-zod';
import { z } from 'zod/v4';

Expand Down Expand Up @@ -56,3 +57,9 @@ export const VerifySchema = z
.describe('Схема верификации OTP кода');

export class VerifyDto extends createZodDto(VerifySchema) {}

export const SignResponseSchema = ActionResponseSchema.extend({
token: z.string().describe('JWT токен доступа пользователя'),
});

export class SignResponse extends createZodDto(SignResponseSchema) {}
18 changes: 11 additions & 7 deletions src/auth/application/use-cases/refresh-tokens.use-case.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { ISessionRepository } from '../../domain/repository';
import { TokenService } from '../../infrastructure/security';
import { DeviceMetadata } from '../../infrastructure/utils/get-device-meta';
import { FindUserQuery } from '@core/user';
import { createId } from '@paralleldrive/cuid2';

@Injectable()
export class RefreshTokensUseCase {
Expand Down Expand Up @@ -54,20 +55,23 @@ export class RefreshTokensUseCase {

await this.sessionRepo.revoke(session.id);

const newSession = await this.sessionRepo.create({
const sessionId = createId();
const { access, refresh, expiresAt } = await this.tokenService.generateTokens(
entity.user,
sessionId,
);

await this.sessionRepo.create({
id: sessionId,
userId: entity.user.id,
...metadata,
expiresAt: new Date(Date.now() + 3 * 24 * 60 * 60 * 1000),
expiresAt,
});

const { access, refresh } = await this.tokenService.generateTokens(
entity.user,
newSession.id,
);

return {
tokens: { access, refresh },
success: true,
expiresAt,
message: 'Токены успешно обновлены',
};
}
Expand Down
24 changes: 22 additions & 2 deletions src/auth/application/use-cases/reset-password.use-case.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,29 @@ export class ResetPasswordUseCase {
) {}

async execute(dto: ResetPasswordDto) {
const redisKey = `pass:reset:${dto.email}`;
const isExistsAttempt = await this.redis.get(redisKey);

if (isExistsAttempt) {
throw new BaseException(
{
code: 'PASS_RESET_ATTEMPT_ACTIVE',
message:
'Запрос на сброс пароля уже активен. Проверьте почту или попробуйте позже.',
details: [
{
target: 'email',
value: dto.email,
},
],
},
HttpStatus.CONFLICT,
);
}

const entity = await this.findUserQuery.execute({ email: dto.email });

if (!entity.user) {
if (!entity?.user) {
throw new BaseException(
{
code: 'USER_NOT_FOUND',
Expand All @@ -48,7 +68,7 @@ export class ResetPasswordUseCase {
isVerified: false,
};

await this.redis.set(`pass:reset:${dto.email}`, JSON.stringify(resetPayload), 'EX', 900);
await this.redis.set(redisKey, JSON.stringify(resetPayload), 'EX', 900);

const event = new ResetPasswordEvent(dto.email, token);
await this.mailQueue.add(AuthMailJobs.SEND_RESET_PASSWORD, event, {
Expand Down
14 changes: 10 additions & 4 deletions src/auth/application/use-cases/sign-in.use-case.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { TokenService } from '../../infrastructure/security';
import { DeviceMetadata } from '../../infrastructure/utils/get-device-meta';
import { SignInDto } from '../dtos';
import { FindUserQuery } from '@core/user';
import { createId } from '@paralleldrive/cuid2';

@Injectable()
export class SignInUseCase {
Expand Down Expand Up @@ -41,21 +42,26 @@ export class SignInUseCase {
HttpStatus.UNAUTHORIZED,
);
}
const sessionId = createId();
const { access, refresh, expiresAt } = await this.tokenService.generateTokens(
user,
sessionId,
);

const { id } = await this.sessionRepo.create({
await this.sessionRepo.create({
id: sessionId,
userId: user.id,
expiresAt: new Date(Date.now() + 3 * 24 * 60 * 60 * 1000),
expiresAt,
...meta,
});

const { access, refresh } = await this.tokenService.generateTokens(user, id);

return {
success: true,
tokens: {
access,
refresh,
},
expiresAt,
message: 'Вы успешно вошли в систему',
};
}
Expand Down
14 changes: 11 additions & 3 deletions src/auth/application/use-cases/sign-up-verify.use-case.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { ISessionRepository } from '../../domain/repository';
import { TokenService } from '../../infrastructure/security';
import { DeviceMetadata } from '../../infrastructure/utils/get-device-meta';
import { VerifyDto } from '../dtos';
import { createId } from '@paralleldrive/cuid2';

@Injectable()
export class SignUpVerifyUseCase {
Expand Down Expand Up @@ -72,18 +73,25 @@ export class SignUpVerifyUseCase {
password: userData.password,
});

const session = await this.sessionRepo.create({
const sessionId = createId();
const { access, refresh, expiresAt } = await this.tokenService.generateTokens(
user,
sessionId,
);

await this.sessionRepo.create({
id: sessionId,
userId: user.id,
expiresAt: new Date(Date.now() + 3 * 24 * 60 * 60 * 1000),
...meta,
expiresAt,
});
const { access, refresh } = await this.tokenService.generateTokens(user, session.id);

await this.redis.del(redisKey);

return {
success: true,
tokens: { access, refresh },
expiresAt,
message: 'Аккаунт успешно подтвержден',
};
}
Expand Down
11 changes: 8 additions & 3 deletions src/auth/infrastructure/security/token.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,18 +23,23 @@ export class TokenService {
aud: btoa(audConstraint),
};

const accessExp = this.cfg.get<any>('JWT_ACCESS_EXPIRES_IN');
const refreshExp = this.cfg.get<any>('JWT_REFRESH_EXPIRES_IN');

const [access, refresh] = await Promise.all([
this.jwtService.signAsync(payload, {
secret: this.cfg.get('JWT_ACCESS_SECRET'),
expiresIn: this.cfg.get<any>('JWT_ACCESS_EXPIRES_IN'),
expiresIn: accessExp,
}),
this.jwtService.signAsync(payload, {
secret: this.cfg.get('JWT_REFRESH_SECRET'),
expiresIn: this.cfg.get<any>('JWT_REFRESH_EXPIRES_IN'),
expiresIn: refreshExp,
}),
]);

return { access, refresh };
const refreshDecodedData = this.jwtService.decode(refresh);

return { access, refresh, expiresAt: new Date(refreshDecodedData?.exp * 1000) };
}

async validateToken(token: string, type: 'access' | 'refresh'): Promise<JwtPayload> {
Expand Down
2 changes: 1 addition & 1 deletion src/shared/error/filter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ export class GlobalExceptionFilter implements ExceptionFilter {
this.formatErrorResponse(request, status, {
code: errorCode,
message,
details: error?.constraint ? [{ target: error.constraint }] : [],
details: error?.detail ? [{ target: error.detail }] : [],
stack: exception.stack,
service: 'postgres',
}),
Expand Down
Loading