feat(backend): add authorization
This commit is contained in:
parent
d02d42da83
commit
ee7be12428
20
compose.yml
Normal file
20
compose.yml
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
services:
|
||||||
|
db:
|
||||||
|
container_name: investments-db
|
||||||
|
image: postgres:15.2
|
||||||
|
restart: always
|
||||||
|
environment:
|
||||||
|
- POSTGRES_USER=${POSTGRES_USER}
|
||||||
|
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
|
||||||
|
ports:
|
||||||
|
- 5433:5432
|
||||||
|
volumes:
|
||||||
|
- postgres_data:/var/lib/posgresql/data
|
||||||
|
networks:
|
||||||
|
- investments
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
postgres_data:
|
||||||
|
|
||||||
|
networks:
|
||||||
|
investments:
|
||||||
2
server/.gitignore
vendored
2
server/.gitignore
vendored
@ -54,3 +54,5 @@ pids
|
|||||||
|
|
||||||
# Diagnostic reports (https://nodejs.org/api/report.html)
|
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||||
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||||
|
|
||||||
|
/generated/prisma
|
||||||
|
|||||||
@ -13,6 +13,7 @@ export default [
|
|||||||
'@typescript-eslint/no-extraneous-class': 'off',
|
'@typescript-eslint/no-extraneous-class': 'off',
|
||||||
'@typescript-eslint/parameter-properties': 'off',
|
'@typescript-eslint/parameter-properties': 'off',
|
||||||
'@typescript-eslint/class-methods-use-this': 'off',
|
'@typescript-eslint/class-methods-use-this': 'off',
|
||||||
|
'@typescript-eslint/strict-boolean-expressions': 'off',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
838
server/package-lock.json
generated
838
server/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -7,13 +7,28 @@
|
|||||||
"build": "nest build",
|
"build": "nest build",
|
||||||
"start": "nest start",
|
"start": "nest start",
|
||||||
"start:dev": "nest start --watch",
|
"start:dev": "nest start --watch",
|
||||||
|
"prisma:validate": "prisma validate",
|
||||||
|
"prisma:push": "prisma db push",
|
||||||
|
"prisma:generate": "prisma generate",
|
||||||
|
"prisma:studio": "prisma studio",
|
||||||
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\"",
|
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\"",
|
||||||
"typecheck": "tsc --noEmit"
|
"typecheck": "tsc --noEmit"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@nestjs/common": "11.1.6",
|
"@nestjs/common": "11.1.6",
|
||||||
|
"@nestjs/config": "4.0.2",
|
||||||
"@nestjs/core": "11.1.6",
|
"@nestjs/core": "11.1.6",
|
||||||
|
"@nestjs/jwt": "11.0.0",
|
||||||
|
"@nestjs/mapped-types": "*",
|
||||||
|
"@nestjs/passport": "11.0.5",
|
||||||
"@nestjs/platform-express": "11.1.6",
|
"@nestjs/platform-express": "11.1.6",
|
||||||
|
"@nestjs/swagger": "11.2.0",
|
||||||
|
"@prisma/client": "^6.14.0",
|
||||||
|
"bcrypt": "6.0.0",
|
||||||
|
"class-transformer": "0.5.1",
|
||||||
|
"class-validator": "0.14.2",
|
||||||
|
"cookie-parser": "1.4.7",
|
||||||
|
"passport-jwt": "4.0.1",
|
||||||
"reflect-metadata": "0.2.2",
|
"reflect-metadata": "0.2.2",
|
||||||
"rxjs": "7.8.2"
|
"rxjs": "7.8.2"
|
||||||
},
|
},
|
||||||
@ -23,12 +38,16 @@
|
|||||||
"@nestjs/testing": "11.1.6",
|
"@nestjs/testing": "11.1.6",
|
||||||
"@swc/cli": "0.7.8",
|
"@swc/cli": "0.7.8",
|
||||||
"@swc/core": "1.13.3",
|
"@swc/core": "1.13.3",
|
||||||
|
"@types/bcrypt": "6.0.0",
|
||||||
|
"@types/cookie-parser": "1.4.9",
|
||||||
"@types/express": "5.0.3",
|
"@types/express": "5.0.3",
|
||||||
"@types/node": "22.17.1",
|
"@types/node": "22.17.1",
|
||||||
|
"@types/passport-jwt": "4.0.1",
|
||||||
"@typescript-eslint/parser": "8.39.0",
|
"@typescript-eslint/parser": "8.39.0",
|
||||||
"eslint": "9.33.0",
|
"eslint": "9.33.0",
|
||||||
"eslint-config-ksv741": "0.2.0",
|
"eslint-config-ksv741": "0.2.0",
|
||||||
"jiti": "^2.5.1",
|
"jiti": "^2.5.1",
|
||||||
|
"prisma": "^6.14.0",
|
||||||
"source-map-support": "0.5.21",
|
"source-map-support": "0.5.21",
|
||||||
"ts-loader": "9.5.2",
|
"ts-loader": "9.5.2",
|
||||||
"ts-node": "10.9.2",
|
"ts-node": "10.9.2",
|
||||||
|
|||||||
19
server/prisma/schema.prisma
Normal file
19
server/prisma/schema.prisma
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
generator client {
|
||||||
|
provider = "prisma-client-js"
|
||||||
|
output = "../node_modules/.prisma/client"
|
||||||
|
}
|
||||||
|
|
||||||
|
datasource db {
|
||||||
|
provider = "postgresql"
|
||||||
|
url = env("DATABASE_URL")
|
||||||
|
}
|
||||||
|
|
||||||
|
model Account {
|
||||||
|
id String @id @default(uuid())
|
||||||
|
email String @unique
|
||||||
|
password String
|
||||||
|
createdAt DateTime @default(now()) @map("created_at")
|
||||||
|
updatedAt DateTime @updatedAt @map("updated_at")
|
||||||
|
|
||||||
|
@@map("accounts")
|
||||||
|
}
|
||||||
@ -1,12 +0,0 @@
|
|||||||
import { Controller, Get } from '@nestjs/common';
|
|
||||||
import { AppService } from './app.service';
|
|
||||||
|
|
||||||
@Controller()
|
|
||||||
export class AppController {
|
|
||||||
constructor(private readonly appService: AppService) {}
|
|
||||||
|
|
||||||
@Get()
|
|
||||||
getHello(): string {
|
|
||||||
return this.appService.getHello();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,10 +0,0 @@
|
|||||||
import { Module } from '@nestjs/common';
|
|
||||||
import { AppController } from './app.controller';
|
|
||||||
import { AppService } from './app.service';
|
|
||||||
|
|
||||||
@Module({
|
|
||||||
imports: [],
|
|
||||||
controllers: [AppController],
|
|
||||||
providers: [AppService],
|
|
||||||
})
|
|
||||||
export class AppModule {}
|
|
||||||
@ -1,8 +0,0 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class AppService {
|
|
||||||
getHello(): string {
|
|
||||||
return 'Hello World!';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
15
server/src/configs/jwt.config.ts
Normal file
15
server/src/configs/jwt.config.ts
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import type { ConfigService } from '@nestjs/config';
|
||||||
|
import type { JwtModuleOptions } from '@nestjs/jwt';
|
||||||
|
|
||||||
|
export function getJwtConfig(configService: ConfigService): JwtModuleOptions {
|
||||||
|
return {
|
||||||
|
secret: configService.getOrThrow<string>('JWT_SECRET'),
|
||||||
|
signOptions: {
|
||||||
|
algorithm: 'HS256',
|
||||||
|
},
|
||||||
|
verifyOptions: {
|
||||||
|
algorithms: ['HS256'],
|
||||||
|
ignoreExpiration: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
@ -1,8 +1,39 @@
|
|||||||
|
import { ValidationPipe } from '@nestjs/common';
|
||||||
import { NestFactory } from '@nestjs/core';
|
import { NestFactory } from '@nestjs/core';
|
||||||
import { AppModule } from './app.module';
|
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
|
||||||
|
import * as process from 'node:process';
|
||||||
|
import * as cookieParser from 'cookie-parser';
|
||||||
|
import { AppModule } from './modules/app.module';
|
||||||
|
|
||||||
async function bootstrap() {
|
async function bootstrap() {
|
||||||
const app = await NestFactory.create(AppModule);
|
const app = await NestFactory.create(AppModule);
|
||||||
|
|
||||||
|
const config = new DocumentBuilder()
|
||||||
|
.setTitle('Investments Control API')
|
||||||
|
.setDescription('Documentation for **Investments Control API**')
|
||||||
|
.setVersion(process.env.npm_package_version ?? '0.0.0')
|
||||||
|
.setContact('Krylov Sergey', 'https://ksv741.tech', 'ksv741@mail.ru')
|
||||||
|
.build();
|
||||||
|
const document = SwaggerModule.createDocument(app, config);
|
||||||
|
SwaggerModule.setup('/docs', app, document, {
|
||||||
|
jsonDocumentUrl: 'docs/swagger.json',
|
||||||
|
yamlDocumentUrl: 'docs/swagger.yaml',
|
||||||
|
customSiteTitle: 'Investments Control API',
|
||||||
|
});
|
||||||
|
|
||||||
|
app.use(cookieParser());
|
||||||
|
app.useGlobalPipes(
|
||||||
|
new ValidationPipe({
|
||||||
|
transform: true,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
app.enableCors({
|
||||||
|
origin: 'http://localhost:3001',
|
||||||
|
credentials: true,
|
||||||
|
exposedHeaders: ['set-cookie'],
|
||||||
|
});
|
||||||
|
|
||||||
await app.listen(process.env.PORT ?? 3000);
|
await app.listen(process.env.PORT ?? 3000);
|
||||||
}
|
}
|
||||||
void bootstrap();
|
void bootstrap();
|
||||||
|
|||||||
15
server/src/modules/app.module.ts
Normal file
15
server/src/modules/app.module.ts
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { ConfigModule } from '@nestjs/config';
|
||||||
|
import { PrismaModule } from './prisma/prisma.module';
|
||||||
|
import { AuthModule } from './auth/auth.module';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [
|
||||||
|
ConfigModule.forRoot({
|
||||||
|
isGlobal: true,
|
||||||
|
}),
|
||||||
|
PrismaModule,
|
||||||
|
AuthModule,
|
||||||
|
],
|
||||||
|
})
|
||||||
|
export class AppModule {}
|
||||||
109
server/src/modules/auth/auth.controller.ts
Normal file
109
server/src/modules/auth/auth.controller.ts
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
import {
|
||||||
|
Body,
|
||||||
|
Controller, Get, HttpCode, HttpStatus,
|
||||||
|
Post, Req, Res,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import {
|
||||||
|
ApiBadRequestResponse,
|
||||||
|
ApiConflictResponse, ApiNotFoundResponse,
|
||||||
|
ApiOkResponse,
|
||||||
|
ApiOperation, ApiUnauthorizedResponse,
|
||||||
|
} from '@nestjs/swagger';
|
||||||
|
import { Account } from '@prisma/client';
|
||||||
|
import type { Response } from 'express';
|
||||||
|
import { AuthService } from './auth.service';
|
||||||
|
import { Authorization } from './decorators/authorization.decorator';
|
||||||
|
import { Authorized } from './decorators/authorized.decorator';
|
||||||
|
import {
|
||||||
|
LoginDto,
|
||||||
|
LoginValidationDto,
|
||||||
|
IncorrectLoginDto,
|
||||||
|
SuccessLoginDto,
|
||||||
|
} from './dto/login';
|
||||||
|
import { CurrentProfileDto } from './dto/profile';
|
||||||
|
import {
|
||||||
|
RegisterValidationDto,
|
||||||
|
ConflictRegisterDto,
|
||||||
|
RegisterDto,
|
||||||
|
} from './dto/register';
|
||||||
|
import { Request } from './types/request';
|
||||||
|
|
||||||
|
@Controller('auth')
|
||||||
|
export class AuthController {
|
||||||
|
constructor(private readonly authService: AuthService) {}
|
||||||
|
|
||||||
|
@ApiOperation({
|
||||||
|
summary: 'Регистрация',
|
||||||
|
description: 'Регистрация аккаунта',
|
||||||
|
})
|
||||||
|
@ApiOkResponse({ description: 'Успешная регистрация', type: RegisterDto })
|
||||||
|
@ApiBadRequestResponse({
|
||||||
|
description: 'Ошибка валидации',
|
||||||
|
type: RegisterValidationDto,
|
||||||
|
})
|
||||||
|
@ApiConflictResponse({
|
||||||
|
description: 'Ошибка создания аккаунта',
|
||||||
|
type: ConflictRegisterDto,
|
||||||
|
})
|
||||||
|
@Post('register')
|
||||||
|
public async register(@Body() body: RegisterDto) {
|
||||||
|
return this.authService.register(body);
|
||||||
|
}
|
||||||
|
|
||||||
|
@ApiOperation({
|
||||||
|
summary: 'Логин',
|
||||||
|
description: 'Вход в аккаунт',
|
||||||
|
})
|
||||||
|
@ApiOkResponse({ description: 'Успешный вход в аккаунт', type: SuccessLoginDto })
|
||||||
|
@ApiBadRequestResponse({
|
||||||
|
description: 'Неверный логин или пароль',
|
||||||
|
type: IncorrectLoginDto,
|
||||||
|
})
|
||||||
|
@ApiBadRequestResponse({
|
||||||
|
description: 'Ошибка валидации',
|
||||||
|
type: LoginValidationDto,
|
||||||
|
})
|
||||||
|
@Post('login')
|
||||||
|
public async login(
|
||||||
|
@Res({ passthrough: true }) response: Response,
|
||||||
|
@Body() body: LoginDto,
|
||||||
|
) {
|
||||||
|
return this.authService.login(response, body);
|
||||||
|
}
|
||||||
|
|
||||||
|
@ApiOperation({
|
||||||
|
summary: 'Логаут',
|
||||||
|
description: 'Выход из аккаунта',
|
||||||
|
})
|
||||||
|
@ApiOkResponse({ description: 'Успешный выход из аккаунт', type: Boolean })
|
||||||
|
@Authorization()
|
||||||
|
@Post('logout')
|
||||||
|
public logout(@Res({ passthrough: true }) response: Response) {
|
||||||
|
return this.authService.logout(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
@ApiOperation({
|
||||||
|
summary: 'Получить профиль',
|
||||||
|
description: 'Получить данные о своем профиле',
|
||||||
|
})
|
||||||
|
@ApiOkResponse({ description: 'Успешное обновление токенов', type: CurrentProfileDto })
|
||||||
|
@ApiUnauthorizedResponse({ description: 'Пользователь не авторизован' })
|
||||||
|
@Authorization()
|
||||||
|
@Get('me')
|
||||||
|
@HttpCode(HttpStatus.OK)
|
||||||
|
public me(@Authorized('id') user: Account['id']) {
|
||||||
|
return { id: user };
|
||||||
|
}
|
||||||
|
|
||||||
|
@ApiOperation({ summary: 'Обновление access и refresh токенов' })
|
||||||
|
@ApiOkResponse({ description: 'Успешное обновление токенов', type: SuccessLoginDto })
|
||||||
|
@ApiUnauthorizedResponse({ description: 'Пользователь не авторизован' })
|
||||||
|
@ApiNotFoundResponse({ description: '' })
|
||||||
|
@Post('refresh')
|
||||||
|
public async refresh(
|
||||||
|
@Req() request: Request,
|
||||||
|
@Res({ passthrough: true }) response: Response,
|
||||||
|
) {
|
||||||
|
return this.authService.refresh(request, response);
|
||||||
|
}
|
||||||
|
}
|
||||||
22
server/src/modules/auth/auth.module.ts
Normal file
22
server/src/modules/auth/auth.module.ts
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { ConfigModule, ConfigService } from '@nestjs/config';
|
||||||
|
import { JwtModule } from '@nestjs/jwt';
|
||||||
|
import { PassportModule } from '@nestjs/passport';
|
||||||
|
import { getJwtConfig } from '../../configs/jwt.config';
|
||||||
|
import { AuthService } from './auth.service';
|
||||||
|
import { AuthController } from './auth.controller';
|
||||||
|
import { JwtStrategy } from './strategies/jwt.strategies';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [
|
||||||
|
PassportModule,
|
||||||
|
JwtModule.registerAsync({
|
||||||
|
imports: [ConfigModule],
|
||||||
|
useFactory: getJwtConfig,
|
||||||
|
inject: [ConfigService],
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
controllers: [AuthController],
|
||||||
|
providers: [AuthService, JwtStrategy],
|
||||||
|
})
|
||||||
|
export class AuthModule {}
|
||||||
154
server/src/modules/auth/auth.service.ts
Normal file
154
server/src/modules/auth/auth.service.ts
Normal file
@ -0,0 +1,154 @@
|
|||||||
|
import {
|
||||||
|
BadRequestException, ConflictException, Injectable, NotFoundException, UnauthorizedException,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import { ConfigService } from '@nestjs/config';
|
||||||
|
import { JwtService } from '@nestjs/jwt';
|
||||||
|
import { Account } from '@prisma/client';
|
||||||
|
import * as bcrypt from 'bcrypt';
|
||||||
|
import type { Response } from 'express';
|
||||||
|
import { PrismaService } from '../prisma/prisma.service';
|
||||||
|
import { LoginDto, SuccessLoginDto } from './dto/login';
|
||||||
|
import { RegisterDto } from './dto/register';
|
||||||
|
import { JwtPayload } from './types/jwt-payload';
|
||||||
|
import { Request } from './types/request';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class AuthService {
|
||||||
|
private readonly COOKIE_DOMAIN: string;
|
||||||
|
|
||||||
|
private readonly JWT_ACCESS_TOKEN_TTL: string;
|
||||||
|
|
||||||
|
private readonly JWT_REFRESH_TOKEN_TTL: string;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private readonly configService: ConfigService,
|
||||||
|
private readonly prismaService: PrismaService,
|
||||||
|
private readonly jwtService: JwtService,
|
||||||
|
) {
|
||||||
|
this.COOKIE_DOMAIN = this.configService.getOrThrow('COOKIE_DOMAIN');
|
||||||
|
this.JWT_ACCESS_TOKEN_TTL = this.configService.getOrThrow('JWT_ACCESS_TOKEN_TTL');
|
||||||
|
this.JWT_REFRESH_TOKEN_TTL = this.configService.getOrThrow('JWT_REFRESH_TOKEN_TTL');
|
||||||
|
}
|
||||||
|
|
||||||
|
public async register(dto: RegisterDto) {
|
||||||
|
const { password, email } = dto;
|
||||||
|
|
||||||
|
const salt = await bcrypt.genSalt();
|
||||||
|
const hashedPassword = await bcrypt.hash(password, salt);
|
||||||
|
|
||||||
|
const existingUser = await this.prismaService.account.findUnique({
|
||||||
|
where: { email },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (existingUser) {
|
||||||
|
throw new ConflictException('Пользователь с такой почтой уже зарегистрирован');
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = await this.prismaService.account.create({
|
||||||
|
data: {
|
||||||
|
email,
|
||||||
|
password: hashedPassword,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: user.id,
|
||||||
|
email: user.email,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public async login(response: Response, dto: LoginDto): Promise<SuccessLoginDto> {
|
||||||
|
const { email, password } = dto;
|
||||||
|
const existingUser = await this.prismaService.account.findUnique({
|
||||||
|
where: { email },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!existingUser) {
|
||||||
|
throw new BadRequestException('Неверный логин или пароль');
|
||||||
|
}
|
||||||
|
|
||||||
|
const isMatch = await bcrypt.compare(password, existingUser.password);
|
||||||
|
if (!isMatch) {
|
||||||
|
throw new BadRequestException('Неверный логин или пароль');
|
||||||
|
}
|
||||||
|
|
||||||
|
const { accessToken } = this.auth(response, existingUser.id);
|
||||||
|
|
||||||
|
return {
|
||||||
|
accessToken,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public logout(response: Response) {
|
||||||
|
this.setCookie(response, 'refreshToken', new Date(0));
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async validate(id: string) {
|
||||||
|
const existingUser = await this.prismaService.account.findUnique({
|
||||||
|
where: {
|
||||||
|
id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!existingUser) {
|
||||||
|
throw new NotFoundException();
|
||||||
|
}
|
||||||
|
|
||||||
|
return existingUser;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async refresh(req: Request, res: Response): Promise<SuccessLoginDto> {
|
||||||
|
const { refreshToken } = req.cookies;
|
||||||
|
|
||||||
|
if (!refreshToken) {
|
||||||
|
throw new UnauthorizedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
const payload: JwtPayload = await this.jwtService.verifyAsync(refreshToken);
|
||||||
|
const existingUser = await this.prismaService.account.findUnique({
|
||||||
|
where: { id: payload.id },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!existingUser) {
|
||||||
|
throw new NotFoundException('Пользователь не нейден');
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.auth(res, existingUser.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
private auth(response: Response, id: Account['id']) {
|
||||||
|
const { accessToken, refreshToken } = this.generateTokens(id);
|
||||||
|
|
||||||
|
this.setCookie(response, refreshToken, new Date(Date.now() + 60 * 60 * 24 * 7));
|
||||||
|
|
||||||
|
return { accessToken };
|
||||||
|
}
|
||||||
|
|
||||||
|
private generateTokens(id: Account['id']) {
|
||||||
|
const payload: JwtPayload = { id };
|
||||||
|
|
||||||
|
const accessToken = this.jwtService.sign(payload, {
|
||||||
|
expiresIn: this.JWT_ACCESS_TOKEN_TTL,
|
||||||
|
});
|
||||||
|
const refreshToken = this.jwtService.sign(payload, {
|
||||||
|
expiresIn: this.JWT_REFRESH_TOKEN_TTL,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
accessToken,
|
||||||
|
refreshToken,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private setCookie(res: Response, value: string, expires: Date) {
|
||||||
|
res.cookie('refreshToken', value, {
|
||||||
|
httpOnly: true,
|
||||||
|
domain: this.COOKIE_DOMAIN,
|
||||||
|
expires,
|
||||||
|
secure: false, // todo
|
||||||
|
sameSite: 'lax', // todo
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,6 @@
|
|||||||
|
import { applyDecorators, UseGuards } from '@nestjs/common';
|
||||||
|
import { JwtGuard } from '../guards/jwt.guard';
|
||||||
|
|
||||||
|
export function Authorization() {
|
||||||
|
return applyDecorators(UseGuards(JwtGuard));
|
||||||
|
}
|
||||||
14
server/src/modules/auth/decorators/authorized.decorator.ts
Normal file
14
server/src/modules/auth/decorators/authorized.decorator.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import type { ExecutionContext } from '@nestjs/common';
|
||||||
|
import { createParamDecorator } from '@nestjs/common';
|
||||||
|
import type { Account } from '@prisma/client';
|
||||||
|
import type { Request } from '../types/request';
|
||||||
|
|
||||||
|
export const Authorized = createParamDecorator(
|
||||||
|
(data: keyof Account | undefined, ctx: ExecutionContext) => {
|
||||||
|
const req: Request = ctx.switchToHttp().getRequest();
|
||||||
|
|
||||||
|
const { user } = req;
|
||||||
|
|
||||||
|
return data ? user[data] : user;
|
||||||
|
},
|
||||||
|
);
|
||||||
12
server/src/modules/auth/dto/login/incorrect-login.dto.ts
Normal file
12
server/src/modules/auth/dto/login/incorrect-login.dto.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
|
||||||
|
export class IncorrectLoginDto {
|
||||||
|
@ApiProperty({ example: 400 })
|
||||||
|
statusCode: number;
|
||||||
|
|
||||||
|
@ApiProperty({ example: 'Неверный логин или пароль' })
|
||||||
|
message: string[] | string;
|
||||||
|
|
||||||
|
@ApiProperty({ example: 'Bad Request' })
|
||||||
|
error: string;
|
||||||
|
}
|
||||||
4
server/src/modules/auth/dto/login/index.ts
Normal file
4
server/src/modules/auth/dto/login/index.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
export { IncorrectLoginDto } from './incorrect-login.dto';
|
||||||
|
export { LoginValidationDto } from './login-validation.dto';
|
||||||
|
export { LoginDto } from './login.dto';
|
||||||
|
export { SuccessLoginDto } from './success-login.dto';
|
||||||
22
server/src/modules/auth/dto/login/login-validation.dto.ts
Normal file
22
server/src/modules/auth/dto/login/login-validation.dto.ts
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
|
||||||
|
export class LoginValidationDto {
|
||||||
|
@ApiProperty({ example: 400 })
|
||||||
|
statusCode: number;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
example: [
|
||||||
|
'Email должен быть строкой.',
|
||||||
|
'Некорректный формат email.',
|
||||||
|
'Email обязателен для заполнения.',
|
||||||
|
'Пароль должен быть строкой.',
|
||||||
|
'Пароль обязателен для заполнения.',
|
||||||
|
'Пароль должен содержать минимум 6 символов.',
|
||||||
|
],
|
||||||
|
type: [String],
|
||||||
|
})
|
||||||
|
message: string[];
|
||||||
|
|
||||||
|
@ApiProperty({ example: 'Bad Request' })
|
||||||
|
error: string;
|
||||||
|
}
|
||||||
31
server/src/modules/auth/dto/login/login.dto.ts
Normal file
31
server/src/modules/auth/dto/login/login.dto.ts
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
import {
|
||||||
|
IsEmail,
|
||||||
|
IsNotEmpty,
|
||||||
|
IsString,
|
||||||
|
MinLength,
|
||||||
|
} from 'class-validator';
|
||||||
|
|
||||||
|
export class LoginDto {
|
||||||
|
@ApiProperty({
|
||||||
|
description: 'Email',
|
||||||
|
example: 'mail@example.com',
|
||||||
|
type: String,
|
||||||
|
})
|
||||||
|
@IsString({ message: 'Email должен быть строкой.' })
|
||||||
|
@IsEmail({}, { message: 'Некорректный формат email.' })
|
||||||
|
@IsNotEmpty({ message: 'Email обязателен для заполнения.' })
|
||||||
|
email: string;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
description: 'Пароль',
|
||||||
|
example: '123456',
|
||||||
|
type: String,
|
||||||
|
})
|
||||||
|
@IsString({ message: 'Пароль должен быть строкой.' })
|
||||||
|
@IsNotEmpty({ message: 'Пароль обязателен для заполнения.' })
|
||||||
|
@MinLength(6, {
|
||||||
|
message: 'Пароль должен содержать минимум 6 символов.',
|
||||||
|
})
|
||||||
|
password: string;
|
||||||
|
}
|
||||||
9
server/src/modules/auth/dto/login/success-login.dto.ts
Normal file
9
server/src/modules/auth/dto/login/success-login.dto.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
|
||||||
|
export class SuccessLoginDto {
|
||||||
|
@ApiProperty({
|
||||||
|
description: 'Access токен',
|
||||||
|
type: String,
|
||||||
|
})
|
||||||
|
accessToken: string;
|
||||||
|
}
|
||||||
10
server/src/modules/auth/dto/profile/current-profile.dto.ts
Normal file
10
server/src/modules/auth/dto/profile/current-profile.dto.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
import { Account } from '@prisma/client';
|
||||||
|
|
||||||
|
export class CurrentProfileDto {
|
||||||
|
@ApiProperty({
|
||||||
|
description: 'Индетификатор аккаунта',
|
||||||
|
type: String,
|
||||||
|
})
|
||||||
|
id: Account['id'];
|
||||||
|
}
|
||||||
1
server/src/modules/auth/dto/profile/index.ts
Normal file
1
server/src/modules/auth/dto/profile/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export { CurrentProfileDto } from './current-profile.dto';
|
||||||
@ -0,0 +1,12 @@
|
|||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
|
||||||
|
export class ConflictRegisterDto {
|
||||||
|
@ApiProperty({ example: 400 })
|
||||||
|
statusCode: number;
|
||||||
|
|
||||||
|
@ApiProperty({ example: 'Пользователь с такой почтой уже зарегистрирован' })
|
||||||
|
message: string[] | string;
|
||||||
|
|
||||||
|
@ApiProperty({ example: 'Conflict' })
|
||||||
|
error: string;
|
||||||
|
}
|
||||||
3
server/src/modules/auth/dto/register/index.ts
Normal file
3
server/src/modules/auth/dto/register/index.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export { ConflictRegisterDto } from './conflict-register.dto';
|
||||||
|
export { RegisterValidationDto } from './register-validation.dto';
|
||||||
|
export { RegisterDto } from './register.dto';
|
||||||
@ -0,0 +1,22 @@
|
|||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
|
||||||
|
export class RegisterValidationDto {
|
||||||
|
@ApiProperty({ example: 400 })
|
||||||
|
statusCode: number;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
example: [
|
||||||
|
'Email должен быть строкой.',
|
||||||
|
'Некорректный формат email.',
|
||||||
|
'Email обязателен для заполнения.',
|
||||||
|
'Пароль должен быть строкой.',
|
||||||
|
'Пароль обязателен для заполнения.',
|
||||||
|
'Пароль должен содержать минимум 6 символов.',
|
||||||
|
],
|
||||||
|
type: [String],
|
||||||
|
})
|
||||||
|
message: string[];
|
||||||
|
|
||||||
|
@ApiProperty({ example: 'Bad Request' })
|
||||||
|
error: string;
|
||||||
|
}
|
||||||
31
server/src/modules/auth/dto/register/register.dto.ts
Normal file
31
server/src/modules/auth/dto/register/register.dto.ts
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
import {
|
||||||
|
IsEmail,
|
||||||
|
IsNotEmpty,
|
||||||
|
IsString,
|
||||||
|
MinLength,
|
||||||
|
} from 'class-validator';
|
||||||
|
|
||||||
|
export class RegisterDto {
|
||||||
|
@ApiProperty({
|
||||||
|
description: 'Email',
|
||||||
|
example: 'mail@example.com',
|
||||||
|
type: String,
|
||||||
|
})
|
||||||
|
@IsString({ message: 'Email должен быть строкой.' })
|
||||||
|
@IsEmail({}, { message: 'Некорректный формат email.' })
|
||||||
|
@IsNotEmpty({ message: 'Email обязателен для заполнения.' })
|
||||||
|
email: string;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
description: 'Пароль',
|
||||||
|
example: '123456',
|
||||||
|
type: String,
|
||||||
|
})
|
||||||
|
@IsString({ message: 'Пароль должен быть строкой.' })
|
||||||
|
@IsNotEmpty({ message: 'Пароль обязателен для заполнения.' })
|
||||||
|
@MinLength(6, {
|
||||||
|
message: 'Пароль должен содержать минимум 6 символов.',
|
||||||
|
})
|
||||||
|
password: string;
|
||||||
|
}
|
||||||
3
server/src/modules/auth/guards/jwt.guard.ts
Normal file
3
server/src/modules/auth/guards/jwt.guard.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
import { AuthGuard } from '@nestjs/passport';
|
||||||
|
|
||||||
|
export class JwtGuard extends AuthGuard('jwt') {}
|
||||||
28
server/src/modules/auth/strategies/jwt.strategies.ts
Normal file
28
server/src/modules/auth/strategies/jwt.strategies.ts
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { ConfigService } from '@nestjs/config';
|
||||||
|
import { PassportStrategy } from '@nestjs/passport';
|
||||||
|
import { ExtractJwt, Strategy } from 'passport-jwt';
|
||||||
|
import { AuthService } from '../auth.service';
|
||||||
|
|
||||||
|
type JwtPayload = {
|
||||||
|
id: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class JwtStrategy extends PassportStrategy(Strategy) {
|
||||||
|
constructor(
|
||||||
|
private readonly authService: AuthService,
|
||||||
|
private readonly configService: ConfigService,
|
||||||
|
) {
|
||||||
|
super({
|
||||||
|
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
|
||||||
|
ignoreExpiration: false,
|
||||||
|
secretOrKey: configService.getOrThrow('JWT_SECRET'),
|
||||||
|
algorithms: ['HS256'],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async validate(payload: JwtPayload) {
|
||||||
|
return this.authService.validate(payload.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
5
server/src/modules/auth/types/jwt-payload.ts
Normal file
5
server/src/modules/auth/types/jwt-payload.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import type { Account } from '@prisma/client';
|
||||||
|
|
||||||
|
export type JwtPayload = {
|
||||||
|
id: Account['id'];
|
||||||
|
};
|
||||||
10
server/src/modules/auth/types/request.ts
Normal file
10
server/src/modules/auth/types/request.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import type { Account } from '@prisma/client';
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
|
||||||
|
export interface Request {
|
||||||
|
user: Account;
|
||||||
|
cookies: {
|
||||||
|
[key: string]: unknown;
|
||||||
|
refreshToken?: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
10
server/src/modules/prisma/prisma.module.ts
Normal file
10
server/src/modules/prisma/prisma.module.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import { Global, Module } from '@nestjs/common';
|
||||||
|
|
||||||
|
import { PrismaService } from './prisma.service';
|
||||||
|
|
||||||
|
@Global()
|
||||||
|
@Module({
|
||||||
|
providers: [PrismaService],
|
||||||
|
exports: [PrismaService],
|
||||||
|
})
|
||||||
|
export class PrismaModule {}
|
||||||
14
server/src/modules/prisma/prisma.service.ts
Normal file
14
server/src/modules/prisma/prisma.service.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import { Injectable, OnModuleDestroy, OnModuleInit } from '@nestjs/common';
|
||||||
|
import { PrismaClient } from '@prisma/client';
|
||||||
|
// import { PrismaClient } from '../../../prisma/generated/prisma/client';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class PrismaService extends PrismaClient implements OnModuleInit, OnModuleDestroy {
|
||||||
|
public async onModuleInit() {
|
||||||
|
await this.$connect();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async onModuleDestroy() {
|
||||||
|
await this.$disconnect;
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user