feat(backend): add expenses
This commit is contained in:
parent
2c1a8ea127
commit
d60aae26e6
@ -14,6 +14,63 @@ export default [
|
|||||||
'@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',
|
'@typescript-eslint/strict-boolean-expressions': 'off',
|
||||||
|
'import/order': [
|
||||||
|
'error', {
|
||||||
|
pathGroups: [
|
||||||
|
{
|
||||||
|
pattern: '__spec__/**',
|
||||||
|
group: 'builtin',
|
||||||
|
position: 'before',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pattern: 'apps/**',
|
||||||
|
group: 'internal',
|
||||||
|
position: 'after',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pattern: 'pages/**',
|
||||||
|
group: 'internal',
|
||||||
|
position: 'after',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pattern: 'widgets/**',
|
||||||
|
group: 'internal',
|
||||||
|
position: 'after',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pattern: 'features/**',
|
||||||
|
group: 'internal',
|
||||||
|
position: 'after',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pattern: 'entities/**',
|
||||||
|
group: 'internal',
|
||||||
|
position: 'after',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pattern: 'shared/**',
|
||||||
|
group: 'internal',
|
||||||
|
position: 'after',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
distinctGroup: true,
|
||||||
|
groups: [
|
||||||
|
'builtin',
|
||||||
|
'external',
|
||||||
|
'internal',
|
||||||
|
'parent',
|
||||||
|
'sibling',
|
||||||
|
'object',
|
||||||
|
'index',
|
||||||
|
'type',
|
||||||
|
],
|
||||||
|
'newlines-between': 'always',
|
||||||
|
alphabetize: {
|
||||||
|
order: 'asc',
|
||||||
|
caseInsensitive: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
@ -14,6 +14,46 @@ model Account {
|
|||||||
password String
|
password String
|
||||||
createdAt DateTime @default(now()) @map("created_at")
|
createdAt DateTime @default(now()) @map("created_at")
|
||||||
updatedAt DateTime @updatedAt @map("updated_at")
|
updatedAt DateTime @updatedAt @map("updated_at")
|
||||||
|
userId String @unique
|
||||||
|
user User @relation(fields: [userId], references: [id])
|
||||||
|
|
||||||
@@map("accounts")
|
@@map("accounts")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
model User {
|
||||||
|
id String @id @default(uuid())
|
||||||
|
account Account?
|
||||||
|
createdAt DateTime @default(now()) @map("created_at")
|
||||||
|
updatedAt DateTime @updatedAt @map("updated_at")
|
||||||
|
expensesLists ExpenseList[]
|
||||||
|
|
||||||
|
@@map("users")
|
||||||
|
}
|
||||||
|
|
||||||
|
model ExpenseList {
|
||||||
|
id String @id @default(uuid())
|
||||||
|
items ExpenseItem[]
|
||||||
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||||
|
userId String
|
||||||
|
title String
|
||||||
|
createdAt DateTime @default(now()) @map("created_at")
|
||||||
|
updatedAt DateTime @updatedAt @map("updated_at")
|
||||||
|
|
||||||
|
@@map("expenses_lists")
|
||||||
|
}
|
||||||
|
|
||||||
|
model ExpenseItem {
|
||||||
|
id String @id @default(uuid())
|
||||||
|
expenseList ExpenseList @relation(fields: [expenseListId], references: [id], onDelete: Cascade)
|
||||||
|
expenseListId String
|
||||||
|
title String
|
||||||
|
description String?
|
||||||
|
date DateTime
|
||||||
|
amount Float
|
||||||
|
currency String @default("RUB")
|
||||||
|
count Float @default(1)
|
||||||
|
createdAt DateTime @default(now()) @map("created_at")
|
||||||
|
updatedAt DateTime @updatedAt @map("updated_at")
|
||||||
|
|
||||||
|
@@map("expenses_items")
|
||||||
|
}
|
||||||
|
|||||||
@ -1,14 +1,17 @@
|
|||||||
|
import * as process from 'node:process';
|
||||||
|
|
||||||
import { ValidationPipe } from '@nestjs/common';
|
import { ValidationPipe } from '@nestjs/common';
|
||||||
import { NestFactory } from '@nestjs/core';
|
import { NestFactory } from '@nestjs/core';
|
||||||
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
|
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
|
||||||
import * as process from 'node:process';
|
|
||||||
import * as cookieParser from 'cookie-parser';
|
import * as cookieParser from 'cookie-parser';
|
||||||
|
|
||||||
import { AppModule } from './modules/app.module';
|
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()
|
const config = new DocumentBuilder()
|
||||||
|
.addBearerAuth()
|
||||||
.setTitle('Investments Control API')
|
.setTitle('Investments Control API')
|
||||||
.setDescription('Documentation for **Investments Control API**')
|
.setDescription('Documentation for **Investments Control API**')
|
||||||
.setVersion(process.env.npm_package_version ?? '0.0.0')
|
.setVersion(process.env.npm_package_version ?? '0.0.0')
|
||||||
|
|||||||
@ -1,7 +1,9 @@
|
|||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
import { ConfigModule } from '@nestjs/config';
|
import { ConfigModule } from '@nestjs/config';
|
||||||
import { PrismaModule } from './prisma/prisma.module';
|
|
||||||
import { AuthModule } from './auth/auth.module';
|
import { AuthModule } from './auth/auth.module';
|
||||||
|
import { ExpensesModule } from './expenses/expenses.module';
|
||||||
|
import { PrismaModule } from './prisma/prisma.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
@ -10,6 +12,7 @@ import { AuthModule } from './auth/auth.module';
|
|||||||
}),
|
}),
|
||||||
PrismaModule,
|
PrismaModule,
|
||||||
AuthModule,
|
AuthModule,
|
||||||
|
ExpensesModule,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class AppModule {}
|
export class AppModule {}
|
||||||
|
|||||||
@ -1,69 +1,40 @@
|
|||||||
import {
|
import {
|
||||||
Body,
|
Body,
|
||||||
Controller, Get, HttpCode, HttpStatus,
|
Controller,
|
||||||
Post, Req, Res,
|
Get,
|
||||||
|
Post,
|
||||||
|
Req,
|
||||||
|
Res,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import {
|
|
||||||
ApiBadRequestResponse,
|
|
||||||
ApiConflictResponse, ApiNotFoundResponse,
|
|
||||||
ApiOkResponse,
|
|
||||||
ApiOperation, ApiUnauthorizedResponse,
|
|
||||||
} from '@nestjs/swagger';
|
|
||||||
import { Account } from '@prisma/client';
|
import { Account } from '@prisma/client';
|
||||||
import type { Response } from 'express';
|
|
||||||
|
import { Authorized } from '@/shared/decorators';
|
||||||
|
|
||||||
import { AuthService } from './auth.service';
|
import { AuthService } from './auth.service';
|
||||||
import { Authorization } from './decorators/authorization.decorator';
|
|
||||||
import { Authorized } from './decorators/authorized.decorator';
|
|
||||||
import {
|
import {
|
||||||
LoginDto,
|
Login,
|
||||||
LoginValidationDto,
|
Logout,
|
||||||
IncorrectLoginDto,
|
Me,
|
||||||
SuccessLoginDto,
|
Refresh,
|
||||||
} from './dto/login';
|
Register,
|
||||||
import { CurrentProfileDto } from './dto/profile';
|
} from './decorators';
|
||||||
import {
|
import { LoginDto, RegisterDto } from './dto';
|
||||||
RegisterValidationDto,
|
|
||||||
ConflictRegisterDto,
|
import type { Request } from '@/shared/types';
|
||||||
RegisterDto,
|
import type { Response } from 'express';
|
||||||
} from './dto/register';
|
|
||||||
import { Request } from './types/request';
|
|
||||||
|
|
||||||
@Controller('auth')
|
@Controller('auth')
|
||||||
export class AuthController {
|
export class AuthController {
|
||||||
constructor(private readonly authService: AuthService) {}
|
constructor(private readonly authService: AuthService) {}
|
||||||
|
|
||||||
@ApiOperation({
|
|
||||||
summary: 'Регистрация',
|
|
||||||
description: 'Регистрация аккаунта',
|
|
||||||
})
|
|
||||||
@ApiOkResponse({ description: 'Успешная регистрация', type: RegisterDto })
|
|
||||||
@ApiBadRequestResponse({
|
|
||||||
description: 'Ошибка валидации',
|
|
||||||
type: RegisterValidationDto,
|
|
||||||
})
|
|
||||||
@ApiConflictResponse({
|
|
||||||
description: 'Ошибка создания аккаунта',
|
|
||||||
type: ConflictRegisterDto,
|
|
||||||
})
|
|
||||||
@Post('register')
|
@Post('register')
|
||||||
|
@Register()
|
||||||
public async register(@Body() body: RegisterDto) {
|
public async register(@Body() body: RegisterDto) {
|
||||||
return this.authService.register(body);
|
return this.authService.register(body);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ApiOperation({
|
|
||||||
summary: 'Логин',
|
|
||||||
description: 'Вход в аккаунт',
|
|
||||||
})
|
|
||||||
@ApiOkResponse({ description: 'Успешный вход в аккаунт', type: SuccessLoginDto })
|
|
||||||
@ApiBadRequestResponse({
|
|
||||||
description: 'Неверный логин или пароль',
|
|
||||||
type: IncorrectLoginDto,
|
|
||||||
})
|
|
||||||
@ApiBadRequestResponse({
|
|
||||||
description: 'Ошибка валидации',
|
|
||||||
type: LoginValidationDto,
|
|
||||||
})
|
|
||||||
@Post('login')
|
@Post('login')
|
||||||
|
@Login()
|
||||||
public async login(
|
public async login(
|
||||||
@Res({ passthrough: true }) response: Response,
|
@Res({ passthrough: true }) response: Response,
|
||||||
@Body() body: LoginDto,
|
@Body() body: LoginDto,
|
||||||
@ -71,35 +42,20 @@ export class AuthController {
|
|||||||
return this.authService.login(response, body);
|
return this.authService.login(response, body);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ApiOperation({
|
|
||||||
summary: 'Логаут',
|
|
||||||
description: 'Выход из аккаунта',
|
|
||||||
})
|
|
||||||
@ApiOkResponse({ description: 'Успешный выход из аккаунт', type: Boolean })
|
|
||||||
@Authorization()
|
|
||||||
@Post('logout')
|
@Post('logout')
|
||||||
|
@Logout()
|
||||||
public logout(@Res({ passthrough: true }) response: Response) {
|
public logout(@Res({ passthrough: true }) response: Response) {
|
||||||
return this.authService.logout(response);
|
return this.authService.logout(response);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ApiOperation({
|
|
||||||
summary: 'Получить профиль',
|
|
||||||
description: 'Получить данные о своем профиле',
|
|
||||||
})
|
|
||||||
@ApiOkResponse({ description: 'Успешное обновление токенов', type: CurrentProfileDto })
|
|
||||||
@ApiUnauthorizedResponse({ description: 'Пользователь не авторизован' })
|
|
||||||
@Authorization()
|
|
||||||
@Get('me')
|
@Get('me')
|
||||||
@HttpCode(HttpStatus.OK)
|
@Me()
|
||||||
public me(@Authorized('id') user: Account['id']) {
|
public profile(@Authorized('id') user: Account['id']) {
|
||||||
return { id: user };
|
return { id: user };
|
||||||
}
|
}
|
||||||
|
|
||||||
@ApiOperation({ summary: 'Обновление access и refresh токенов' })
|
|
||||||
@ApiOkResponse({ description: 'Успешное обновление токенов', type: SuccessLoginDto })
|
|
||||||
@ApiUnauthorizedResponse({ description: 'Пользователь не авторизован' })
|
|
||||||
@ApiNotFoundResponse({ description: '' })
|
|
||||||
@Post('refresh')
|
@Post('refresh')
|
||||||
|
@Refresh()
|
||||||
public async refresh(
|
public async refresh(
|
||||||
@Req() request: Request,
|
@Req() request: Request,
|
||||||
@Res({ passthrough: true }) response: Response,
|
@Res({ passthrough: true }) response: Response,
|
||||||
|
|||||||
@ -2,9 +2,11 @@ import { Module } from '@nestjs/common';
|
|||||||
import { ConfigModule, ConfigService } from '@nestjs/config';
|
import { ConfigModule, ConfigService } from '@nestjs/config';
|
||||||
import { JwtModule } from '@nestjs/jwt';
|
import { JwtModule } from '@nestjs/jwt';
|
||||||
import { PassportModule } from '@nestjs/passport';
|
import { PassportModule } from '@nestjs/passport';
|
||||||
|
|
||||||
import { getJwtConfig } from '../../configs/jwt.config';
|
import { getJwtConfig } from '../../configs/jwt.config';
|
||||||
import { AuthService } from './auth.service';
|
|
||||||
import { AuthController } from './auth.controller';
|
import { AuthController } from './auth.controller';
|
||||||
|
import { AuthService } from './auth.service';
|
||||||
import { JwtStrategy } from './strategies/jwt.strategies';
|
import { JwtStrategy } from './strategies/jwt.strategies';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
|
|||||||
@ -1,16 +1,22 @@
|
|||||||
import {
|
import {
|
||||||
BadRequestException, ConflictException, Injectable, NotFoundException, UnauthorizedException,
|
BadRequestException,
|
||||||
|
ConflictException,
|
||||||
|
Injectable,
|
||||||
|
NotFoundException,
|
||||||
|
UnauthorizedException,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { ConfigService } from '@nestjs/config';
|
import { ConfigService } from '@nestjs/config';
|
||||||
import { JwtService } from '@nestjs/jwt';
|
import { JwtService } from '@nestjs/jwt';
|
||||||
import { Account } from '@prisma/client';
|
import { Account } from '@prisma/client';
|
||||||
import * as bcrypt from 'bcrypt';
|
import * as bcrypt from 'bcrypt';
|
||||||
import type { Response } from 'express';
|
|
||||||
|
import { JwtPayload, Request } from '@/shared/types';
|
||||||
|
|
||||||
import { PrismaService } from '../prisma/prisma.service';
|
import { PrismaService } from '../prisma/prisma.service';
|
||||||
import { LoginDto, SuccessLoginDto } from './dto/login';
|
|
||||||
import { RegisterDto } from './dto/register';
|
import { LoginDto, RegisterDto, SuccessLoginDto } from './dto';
|
||||||
import { JwtPayload } from './types/jwt-payload';
|
|
||||||
import { Request } from './types/request';
|
import type { Response } from 'express';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class AuthService {
|
export class AuthService {
|
||||||
@ -36,43 +42,46 @@ export class AuthService {
|
|||||||
const salt = await bcrypt.genSalt();
|
const salt = await bcrypt.genSalt();
|
||||||
const hashedPassword = await bcrypt.hash(password, salt);
|
const hashedPassword = await bcrypt.hash(password, salt);
|
||||||
|
|
||||||
const existingUser = await this.prismaService.account.findUnique({
|
const existingAccount = await this.prismaService.account.findUnique({
|
||||||
where: { email },
|
where: { email },
|
||||||
});
|
});
|
||||||
|
|
||||||
if (existingUser) {
|
if (existingAccount) {
|
||||||
throw new ConflictException('Пользователь с такой почтой уже зарегистрирован');
|
throw new ConflictException('Пользователь с такой почтой уже зарегистрирован');
|
||||||
}
|
}
|
||||||
|
|
||||||
const user = await this.prismaService.account.create({
|
const account = await this.prismaService.account.create({
|
||||||
data: {
|
data: {
|
||||||
email,
|
email,
|
||||||
password: hashedPassword,
|
password: hashedPassword,
|
||||||
|
user: {
|
||||||
|
create: {},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: user.id,
|
id: account.id,
|
||||||
email: user.email,
|
email: account.email,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public async login(response: Response, dto: LoginDto): Promise<SuccessLoginDto> {
|
public async login(response: Response, dto: LoginDto): Promise<SuccessLoginDto> {
|
||||||
const { email, password } = dto;
|
const { email, password } = dto;
|
||||||
const existingUser = await this.prismaService.account.findUnique({
|
const existingAccount = await this.prismaService.account.findUnique({
|
||||||
where: { email },
|
where: { email },
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!existingUser) {
|
if (!existingAccount) {
|
||||||
throw new BadRequestException('Неверный логин или пароль');
|
throw new BadRequestException('Неверный логин или пароль');
|
||||||
}
|
}
|
||||||
|
|
||||||
const isMatch = await bcrypt.compare(password, existingUser.password);
|
const isMatch = await bcrypt.compare(password, existingAccount.password);
|
||||||
if (!isMatch) {
|
if (!isMatch) {
|
||||||
throw new BadRequestException('Неверный логин или пароль');
|
throw new BadRequestException('Неверный логин или пароль');
|
||||||
}
|
}
|
||||||
|
|
||||||
const { accessToken } = this.auth(response, existingUser.id);
|
const { accessToken } = this.auth(response, existingAccount.id);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
accessToken,
|
accessToken,
|
||||||
@ -86,17 +95,17 @@ export class AuthService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async validate(id: string) {
|
public async validate(id: string) {
|
||||||
const existingUser = await this.prismaService.account.findUnique({
|
const existingAccount = await this.prismaService.account.findUnique({
|
||||||
where: {
|
where: {
|
||||||
id,
|
id,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!existingUser) {
|
if (!existingAccount) {
|
||||||
throw new NotFoundException();
|
throw new NotFoundException();
|
||||||
}
|
}
|
||||||
|
|
||||||
return existingUser;
|
return existingAccount;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async refresh(req: Request, res: Response): Promise<SuccessLoginDto> {
|
public async refresh(req: Request, res: Response): Promise<SuccessLoginDto> {
|
||||||
@ -107,15 +116,15 @@ export class AuthService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const payload: JwtPayload = await this.jwtService.verifyAsync(refreshToken);
|
const payload: JwtPayload = await this.jwtService.verifyAsync(refreshToken);
|
||||||
const existingUser = await this.prismaService.account.findUnique({
|
const existingAccount = await this.prismaService.account.findUnique({
|
||||||
where: { id: payload.id },
|
where: { id: payload.id },
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!existingUser) {
|
if (!existingAccount) {
|
||||||
throw new NotFoundException('Пользователь не нейден');
|
throw new NotFoundException('Пользователь не нейден');
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.auth(res, existingUser.id);
|
return this.auth(res, existingAccount.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
private auth(response: Response, id: Account['id']) {
|
private auth(response: Response, id: Account['id']) {
|
||||||
|
|||||||
5
server/src/modules/auth/decorators/index.ts
Normal file
5
server/src/modules/auth/decorators/index.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
export { Register } from './register.decorator';
|
||||||
|
export { Login } from './login.decorator';
|
||||||
|
export { Logout } from './logout.decorator';
|
||||||
|
export { Me } from './me.decorator';
|
||||||
|
export { Refresh } from './refresh.decorator';
|
||||||
22
server/src/modules/auth/decorators/login.decorator.ts
Normal file
22
server/src/modules/auth/decorators/login.decorator.ts
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import { applyDecorators } from '@nestjs/common';
|
||||||
|
import { ApiBadRequestResponse, ApiOkResponse, ApiOperation } from '@nestjs/swagger';
|
||||||
|
|
||||||
|
import { IncorrectLoginDto, LoginValidationDto, SuccessLoginDto } from '../dto';
|
||||||
|
|
||||||
|
export function Login() {
|
||||||
|
return applyDecorators(
|
||||||
|
ApiOperation({
|
||||||
|
summary: 'Логин',
|
||||||
|
description: 'Вход в аккаунт',
|
||||||
|
}),
|
||||||
|
ApiOkResponse({ description: 'Успешный вход в аккаунт', type: SuccessLoginDto }),
|
||||||
|
ApiBadRequestResponse({
|
||||||
|
description: 'Неверный логин или пароль',
|
||||||
|
type: IncorrectLoginDto,
|
||||||
|
}),
|
||||||
|
ApiBadRequestResponse({
|
||||||
|
description: 'Ошибка валидации',
|
||||||
|
type: LoginValidationDto,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
21
server/src/modules/auth/decorators/logout.decorator.ts
Normal file
21
server/src/modules/auth/decorators/logout.decorator.ts
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import { applyDecorators } from '@nestjs/common';
|
||||||
|
import { ApiOkResponse, ApiOperation } from '@nestjs/swagger';
|
||||||
|
|
||||||
|
import { Authorization } from '@/shared/decorators';
|
||||||
|
|
||||||
|
function Documentation() {
|
||||||
|
return applyDecorators(
|
||||||
|
ApiOperation({
|
||||||
|
summary: 'Логаут',
|
||||||
|
description: 'Выход из аккаунта',
|
||||||
|
}),
|
||||||
|
ApiOkResponse({ description: 'Успешный выход из аккаунт', type: Boolean }),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Logout() {
|
||||||
|
return applyDecorators(
|
||||||
|
Authorization(),
|
||||||
|
Documentation(),
|
||||||
|
);
|
||||||
|
}
|
||||||
23
server/src/modules/auth/decorators/me.decorator.ts
Normal file
23
server/src/modules/auth/decorators/me.decorator.ts
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import { applyDecorators } from '@nestjs/common';
|
||||||
|
import {
|
||||||
|
ApiBearerAuth,
|
||||||
|
ApiOkResponse,
|
||||||
|
ApiOperation,
|
||||||
|
ApiUnauthorizedResponse,
|
||||||
|
} from '@nestjs/swagger';
|
||||||
|
|
||||||
|
import { CurrentProfileDto } from '@/modules/auth/dto';
|
||||||
|
import { Authorization } from '@/shared/decorators';
|
||||||
|
|
||||||
|
export function Me() {
|
||||||
|
return applyDecorators(
|
||||||
|
Authorization(),
|
||||||
|
ApiOperation({
|
||||||
|
summary: 'Получить профиль',
|
||||||
|
description: 'Получить данные о своем профиле',
|
||||||
|
}),
|
||||||
|
ApiBearerAuth(),
|
||||||
|
ApiOkResponse({ description: 'Успешное обновление токенов', type: CurrentProfileDto }),
|
||||||
|
ApiUnauthorizedResponse({ description: 'Пользователь не авторизован' }),
|
||||||
|
);
|
||||||
|
}
|
||||||
18
server/src/modules/auth/decorators/refresh.decorator.ts
Normal file
18
server/src/modules/auth/decorators/refresh.decorator.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import { applyDecorators } from '@nestjs/common';
|
||||||
|
import {
|
||||||
|
ApiNotFoundResponse,
|
||||||
|
ApiOkResponse,
|
||||||
|
ApiOperation,
|
||||||
|
ApiUnauthorizedResponse,
|
||||||
|
} from '@nestjs/swagger';
|
||||||
|
|
||||||
|
import { SuccessLoginDto } from '../dto';
|
||||||
|
|
||||||
|
export function Refresh() {
|
||||||
|
return applyDecorators(
|
||||||
|
ApiOperation({ summary: 'Обновление access и refresh токенов' }),
|
||||||
|
ApiOkResponse({ description: 'Успешное обновление токенов', type: SuccessLoginDto }),
|
||||||
|
ApiUnauthorizedResponse({ description: 'Пользователь не авторизован' }),
|
||||||
|
ApiNotFoundResponse({ description: '' }),
|
||||||
|
);
|
||||||
|
}
|
||||||
27
server/src/modules/auth/decorators/register.decorator.ts
Normal file
27
server/src/modules/auth/decorators/register.decorator.ts
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import { applyDecorators } from '@nestjs/common';
|
||||||
|
import {
|
||||||
|
ApiBadRequestResponse,
|
||||||
|
ApiConflictResponse,
|
||||||
|
ApiOkResponse,
|
||||||
|
ApiOperation,
|
||||||
|
} from '@nestjs/swagger';
|
||||||
|
|
||||||
|
import { ConflictRegisterDto, RegisterDto, RegisterValidationDto } from '../dto';
|
||||||
|
|
||||||
|
export function Register() {
|
||||||
|
return applyDecorators(
|
||||||
|
ApiOperation({
|
||||||
|
summary: 'Регистрация',
|
||||||
|
description: 'Регистрация аккаунта',
|
||||||
|
}),
|
||||||
|
ApiOkResponse({ description: 'Успешная регистрация', type: RegisterDto }),
|
||||||
|
ApiBadRequestResponse({
|
||||||
|
description: 'Ошибка валидации',
|
||||||
|
type: RegisterValidationDto,
|
||||||
|
}),
|
||||||
|
ApiConflictResponse({
|
||||||
|
description: 'Ошибка создания аккаунта',
|
||||||
|
type: ConflictRegisterDto,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
10
server/src/modules/auth/dto/index.ts
Normal file
10
server/src/modules/auth/dto/index.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
export { IncorrectLoginDto } from './login/incorrect-login.dto';
|
||||||
|
export { LoginValidationDto } from './login/login-validation.dto';
|
||||||
|
export { LoginDto } from './login/login.dto';
|
||||||
|
export { SuccessLoginDto } from './login/success-login.dto';
|
||||||
|
|
||||||
|
export { CurrentProfileDto } from './profile/current-profile.dto';
|
||||||
|
|
||||||
|
export { ConflictRegisterDto } from './register/conflict-register.dto';
|
||||||
|
export { RegisterValidationDto } from './register/register-validation.dto';
|
||||||
|
export { RegisterDto } from './register/register.dto';
|
||||||
@ -1,4 +0,0 @@
|
|||||||
export { IncorrectLoginDto } from './incorrect-login.dto';
|
|
||||||
export { LoginValidationDto } from './login-validation.dto';
|
|
||||||
export { LoginDto } from './login.dto';
|
|
||||||
export { SuccessLoginDto } from './success-login.dto';
|
|
||||||
@ -1 +0,0 @@
|
|||||||
export { CurrentProfileDto } from './current-profile.dto';
|
|
||||||
@ -1,3 +0,0 @@
|
|||||||
export { ConflictRegisterDto } from './conflict-register.dto';
|
|
||||||
export { RegisterValidationDto } from './register-validation.dto';
|
|
||||||
export { RegisterDto } from './register.dto';
|
|
||||||
@ -2,6 +2,7 @@ import { Injectable } from '@nestjs/common';
|
|||||||
import { ConfigService } from '@nestjs/config';
|
import { ConfigService } from '@nestjs/config';
|
||||||
import { PassportStrategy } from '@nestjs/passport';
|
import { PassportStrategy } from '@nestjs/passport';
|
||||||
import { ExtractJwt, Strategy } from 'passport-jwt';
|
import { ExtractJwt, Strategy } from 'passport-jwt';
|
||||||
|
|
||||||
import { AuthService } from '../auth.service';
|
import { AuthService } from '../auth.service';
|
||||||
|
|
||||||
type JwtPayload = {
|
type JwtPayload = {
|
||||||
|
|||||||
@ -0,0 +1,30 @@
|
|||||||
|
import { applyDecorators } from '@nestjs/common';
|
||||||
|
import {
|
||||||
|
ApiBearerAuth,
|
||||||
|
ApiOkResponse,
|
||||||
|
ApiOperation,
|
||||||
|
ApiParam,
|
||||||
|
} from '@nestjs/swagger';
|
||||||
|
|
||||||
|
import { Authorization } from '@/shared/decorators';
|
||||||
|
|
||||||
|
import { ExpenseListItemDto } from '../../dto';
|
||||||
|
|
||||||
|
function Documentation() {
|
||||||
|
return applyDecorators(
|
||||||
|
ApiOperation({
|
||||||
|
summary: 'Создать элемент расхода',
|
||||||
|
description: 'Создать элемент расхода',
|
||||||
|
}),
|
||||||
|
ApiBearerAuth(),
|
||||||
|
ApiParam({ name: 'id', type: String, description: 'ID листа расходов' }),
|
||||||
|
ApiOkResponse({ description: 'Успешно создан элемент расхода', type: ExpenseListItemDto }),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function CreateExpenseListItem() {
|
||||||
|
return applyDecorators(
|
||||||
|
Authorization(),
|
||||||
|
Documentation(),
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -0,0 +1,33 @@
|
|||||||
|
import { applyDecorators } from '@nestjs/common';
|
||||||
|
import {
|
||||||
|
ApiBearerAuth,
|
||||||
|
ApiNotFoundResponse,
|
||||||
|
ApiOkResponse,
|
||||||
|
ApiOperation,
|
||||||
|
ApiParam,
|
||||||
|
} from '@nestjs/swagger';
|
||||||
|
|
||||||
|
import { Authorization } from '@/shared/decorators';
|
||||||
|
|
||||||
|
import { ExpenseListItemDto } from '../../dto';
|
||||||
|
|
||||||
|
function Documentation() {
|
||||||
|
return applyDecorators(
|
||||||
|
ApiOperation({
|
||||||
|
summary: 'Удалить элемент расхода',
|
||||||
|
description: 'Удалить элемент расхода',
|
||||||
|
}),
|
||||||
|
ApiBearerAuth(),
|
||||||
|
ApiParam({ name: 'listId', type: String, description: 'ID листа расходов' }),
|
||||||
|
ApiParam({ name: 'id', type: String, description: 'ID элемента' }),
|
||||||
|
ApiNotFoundResponse({ description: 'Лист расходов не найден' }),
|
||||||
|
ApiNotFoundResponse({ description: 'Позиция расходов не найдена' }),
|
||||||
|
ApiOkResponse({ description: 'Успешно удален элемент расхода', type: ExpenseListItemDto }),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
export function DeleteExpenseListItem() {
|
||||||
|
return applyDecorators(
|
||||||
|
Authorization(),
|
||||||
|
Documentation(),
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -0,0 +1,33 @@
|
|||||||
|
import { applyDecorators } from '@nestjs/common';
|
||||||
|
import {
|
||||||
|
ApiBearerAuth,
|
||||||
|
ApiNotFoundResponse,
|
||||||
|
ApiOkResponse,
|
||||||
|
ApiOperation,
|
||||||
|
ApiParam,
|
||||||
|
} from '@nestjs/swagger';
|
||||||
|
|
||||||
|
import { Authorization } from '@/shared/decorators';
|
||||||
|
|
||||||
|
import { ExpenseListItemDto } from '../../dto';
|
||||||
|
|
||||||
|
function Documentation() {
|
||||||
|
return applyDecorators(
|
||||||
|
ApiOperation({
|
||||||
|
summary: 'Редактировать элемент расхода',
|
||||||
|
description: 'Редактировать элемент расхода',
|
||||||
|
}),
|
||||||
|
ApiBearerAuth(),
|
||||||
|
ApiParam({ name: 'listId', type: String, description: 'ID листа расходов' }),
|
||||||
|
ApiParam({ name: 'id', type: String, description: 'ID элемента' }),
|
||||||
|
ApiOkResponse({ description: 'Успешно изменен элемент расхода', type: ExpenseListItemDto }),
|
||||||
|
ApiNotFoundResponse({ description: 'Лист расходов не найден' }),
|
||||||
|
ApiNotFoundResponse({ description: 'Позиция расходов не найдена' }),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
export function EditExpenseListItem() {
|
||||||
|
return applyDecorators(
|
||||||
|
Authorization(),
|
||||||
|
Documentation(),
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -0,0 +1,34 @@
|
|||||||
|
import { applyDecorators } from '@nestjs/common';
|
||||||
|
import {
|
||||||
|
ApiBearerAuth,
|
||||||
|
ApiNotFoundResponse,
|
||||||
|
ApiOkResponse,
|
||||||
|
ApiOperation,
|
||||||
|
ApiParam,
|
||||||
|
} from '@nestjs/swagger';
|
||||||
|
|
||||||
|
import { Authorization } from '@/shared/decorators';
|
||||||
|
|
||||||
|
import { ExpenseListItemDto } from '../../dto';
|
||||||
|
|
||||||
|
function Documentation() {
|
||||||
|
return applyDecorators(
|
||||||
|
ApiOperation({
|
||||||
|
summary: 'Найти элемент расхода',
|
||||||
|
description: 'Найти элемент расхода',
|
||||||
|
}),
|
||||||
|
ApiBearerAuth(),
|
||||||
|
ApiParam({ name: 'listId', type: String, description: 'ID листа расходов' }),
|
||||||
|
ApiParam({ name: 'id', type: String, description: 'ID элемента' }),
|
||||||
|
ApiNotFoundResponse({ description: 'Лист расходов не найден' }),
|
||||||
|
ApiNotFoundResponse({ description: 'Позиция расходов не найдена' }),
|
||||||
|
ApiOkResponse({ description: 'Успешно найден элемент расхода', type: ExpenseListItemDto }),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function FindExpenseListItem() {
|
||||||
|
return applyDecorators(
|
||||||
|
Authorization(),
|
||||||
|
Documentation(),
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -0,0 +1,24 @@
|
|||||||
|
import { applyDecorators } from '@nestjs/common';
|
||||||
|
import { ApiBearerAuth, ApiOkResponse, ApiOperation } from '@nestjs/swagger';
|
||||||
|
|
||||||
|
import { Authorization } from '@/shared/decorators';
|
||||||
|
|
||||||
|
import { ExpenseListDto } from '../../dto';
|
||||||
|
|
||||||
|
function Documentation() {
|
||||||
|
return applyDecorators(
|
||||||
|
ApiOperation({
|
||||||
|
summary: 'Создать лист расходов',
|
||||||
|
description: 'Создать лист расходов',
|
||||||
|
}),
|
||||||
|
ApiBearerAuth(),
|
||||||
|
ApiOkResponse({ description: 'Успешно создан лист расходов', type: ExpenseListDto }),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function CreateExpenseList() {
|
||||||
|
return applyDecorators(
|
||||||
|
Authorization(),
|
||||||
|
Documentation(),
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -0,0 +1,32 @@
|
|||||||
|
import { applyDecorators } from '@nestjs/common';
|
||||||
|
import {
|
||||||
|
ApiBearerAuth,
|
||||||
|
ApiNotFoundResponse,
|
||||||
|
ApiOkResponse,
|
||||||
|
ApiOperation,
|
||||||
|
ApiParam,
|
||||||
|
} from '@nestjs/swagger';
|
||||||
|
|
||||||
|
import { Authorization } from '@/shared/decorators';
|
||||||
|
|
||||||
|
import { DeletedExpenseListDto } from '../../dto';
|
||||||
|
|
||||||
|
function Documentation() {
|
||||||
|
return applyDecorators(
|
||||||
|
ApiOperation({
|
||||||
|
summary: 'Удалить лист расходов',
|
||||||
|
description: 'Удалить лист расходов',
|
||||||
|
}),
|
||||||
|
ApiBearerAuth(),
|
||||||
|
ApiParam({ name: 'id', type: String, description: 'ID листа расходов' }),
|
||||||
|
ApiNotFoundResponse({ description: 'Лист расходов не найден' }),
|
||||||
|
ApiOkResponse({ description: 'Успешно удален лист расходов', type: DeletedExpenseListDto }),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DeleteExpenseList() {
|
||||||
|
return applyDecorators(
|
||||||
|
Authorization(),
|
||||||
|
Documentation(),
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -0,0 +1,32 @@
|
|||||||
|
import { applyDecorators } from '@nestjs/common';
|
||||||
|
import {
|
||||||
|
ApiBearerAuth,
|
||||||
|
ApiNotFoundResponse,
|
||||||
|
ApiOkResponse,
|
||||||
|
ApiOperation,
|
||||||
|
ApiParam,
|
||||||
|
} from '@nestjs/swagger';
|
||||||
|
|
||||||
|
import { Authorization } from '@/shared/decorators';
|
||||||
|
|
||||||
|
import { ExpenseListDto } from '../../dto';
|
||||||
|
|
||||||
|
function Documentation() {
|
||||||
|
return applyDecorators(
|
||||||
|
ApiOperation({
|
||||||
|
summary: 'Редактировать лист расходов',
|
||||||
|
description: 'Редактировать лист расходов',
|
||||||
|
}),
|
||||||
|
ApiBearerAuth(),
|
||||||
|
ApiParam({ name: 'id', type: String, description: 'ID листа расходов' }),
|
||||||
|
ApiNotFoundResponse({ description: 'Лист расходов не найден' }),
|
||||||
|
ApiOkResponse({ description: 'Успешно изменен лист расходов', type: ExpenseListDto }),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function EditExpenseList() {
|
||||||
|
return applyDecorators(
|
||||||
|
Authorization(),
|
||||||
|
Documentation(),
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -0,0 +1,28 @@
|
|||||||
|
import { applyDecorators } from '@nestjs/common';
|
||||||
|
import {
|
||||||
|
ApiBearerAuth,
|
||||||
|
ApiOkResponse,
|
||||||
|
ApiOperation,
|
||||||
|
} from '@nestjs/swagger';
|
||||||
|
|
||||||
|
import { Authorization } from '@/shared/decorators';
|
||||||
|
|
||||||
|
import { ExpenseListDto } from '../../dto';
|
||||||
|
|
||||||
|
function Documentation() {
|
||||||
|
return applyDecorators(
|
||||||
|
ApiBearerAuth(),
|
||||||
|
ApiOperation({
|
||||||
|
summary: 'Найти все листы расходов',
|
||||||
|
description: 'Найти все листы расходов',
|
||||||
|
}),
|
||||||
|
ApiOkResponse({ description: 'Успешно создан лист расходов', type: [ExpenseListDto] }),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function FindAllExpenseLists() {
|
||||||
|
return applyDecorators(
|
||||||
|
Authorization(),
|
||||||
|
Documentation(),
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -0,0 +1,32 @@
|
|||||||
|
import { applyDecorators } from '@nestjs/common';
|
||||||
|
import {
|
||||||
|
ApiBearerAuth,
|
||||||
|
ApiNotFoundResponse,
|
||||||
|
ApiOkResponse,
|
||||||
|
ApiOperation,
|
||||||
|
ApiParam,
|
||||||
|
} from '@nestjs/swagger';
|
||||||
|
|
||||||
|
import { Authorization } from '@/shared/decorators';
|
||||||
|
|
||||||
|
import { ExpenseListDto } from '../../dto';
|
||||||
|
|
||||||
|
function Documentation() {
|
||||||
|
return applyDecorators(
|
||||||
|
ApiBearerAuth(),
|
||||||
|
ApiOperation({
|
||||||
|
summary: 'Найти лист расходов',
|
||||||
|
description: 'Найти лист расходов',
|
||||||
|
}),
|
||||||
|
ApiParam({ name: 'id', type: String, description: 'ID листа расходов' }),
|
||||||
|
ApiNotFoundResponse({ description: 'Лист расходов не найден' }),
|
||||||
|
ApiOkResponse({ description: 'Успешно создан лист расходов', type: ExpenseListDto }),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function FindExpenseList() {
|
||||||
|
return applyDecorators(
|
||||||
|
Authorization(),
|
||||||
|
Documentation(),
|
||||||
|
);
|
||||||
|
}
|
||||||
12
server/src/modules/expenses/decorators/index.ts
Normal file
12
server/src/modules/expenses/decorators/index.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
export { CreateExpenseList } from './expense-list/create-expense-list.decorator';
|
||||||
|
export { CreateExpenseListItem } from './expense-list-item/create-expense-list-item.decorator';
|
||||||
|
|
||||||
|
export { DeleteExpenseList } from './expense-list/delete-expense-list.decorator';
|
||||||
|
export { DeleteExpenseListItem } from './expense-list-item/delete-expense-list-item.decorator';
|
||||||
|
|
||||||
|
export { EditExpenseList } from './expense-list/edit-expense-list.decorator';
|
||||||
|
export { EditExpenseListItem } from './expense-list-item/edit-expense-list-item.decorator';
|
||||||
|
|
||||||
|
export { FindExpenseList } from './expense-list/find-expense-list.decorator';
|
||||||
|
export { FindAllExpenseLists } from './expense-list/find-all-expense-list.decorator';
|
||||||
|
export { FindExpenseListItem } from './expense-list-item/find-expense-list-item.decorator';
|
||||||
@ -0,0 +1,70 @@
|
|||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
import {
|
||||||
|
IsNotEmpty,
|
||||||
|
IsNumber,
|
||||||
|
IsOptional,
|
||||||
|
IsString,
|
||||||
|
} from 'class-validator';
|
||||||
|
|
||||||
|
import { IsISODateTimeString } from '../../../../shared/validators';
|
||||||
|
|
||||||
|
export class CreateExpenseListItemDto {
|
||||||
|
@ApiProperty({
|
||||||
|
description: 'Заголовок расхода',
|
||||||
|
example: 'Кофе',
|
||||||
|
type: String,
|
||||||
|
})
|
||||||
|
@IsString({ message: 'Заголовок расхода должен быть строкой.' })
|
||||||
|
@IsNotEmpty({ message: 'Заголовок расхода обязателен к заполнению.' })
|
||||||
|
title: string;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
description: 'Описание',
|
||||||
|
example: 'Перед работой',
|
||||||
|
type: String,
|
||||||
|
nullable: true,
|
||||||
|
})
|
||||||
|
@IsOptional()
|
||||||
|
@IsString({ message: 'Описание расхода должно быть строкой.' })
|
||||||
|
description?: string;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
description: 'Дата',
|
||||||
|
example: '2025-08-22T04:12:03.726Z',
|
||||||
|
type: Date,
|
||||||
|
})
|
||||||
|
@IsNotEmpty({ message: 'Дата расхода обязателен к заполнению.' })
|
||||||
|
@IsISODateTimeString({ message: 'Дата должна быть строкой в формате ISO 8601' })
|
||||||
|
date: Date;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
description: 'Сумма расхода',
|
||||||
|
example: 175.50,
|
||||||
|
type: Number,
|
||||||
|
})
|
||||||
|
@IsNotEmpty({ message: 'Сумма расхода обязательна к заполнению.' })
|
||||||
|
@IsNumber({ allowInfinity: false, allowNaN: false, maxDecimalPlaces: 2 }, { message: 'Сумма расхода должна быть числом.' })
|
||||||
|
amount: number;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
description: 'Валюта расхода',
|
||||||
|
example: 'RUB',
|
||||||
|
type: String,
|
||||||
|
default: 'RUB',
|
||||||
|
nullable: true,
|
||||||
|
})
|
||||||
|
@IsOptional()
|
||||||
|
@IsString({ message: 'Валюта расхода должна быть строкой.' })
|
||||||
|
currency?: string;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
description: 'Количество',
|
||||||
|
example: 2,
|
||||||
|
type: Number,
|
||||||
|
default: 1,
|
||||||
|
nullable: true,
|
||||||
|
})
|
||||||
|
@IsOptional()
|
||||||
|
@IsNumber({ allowInfinity: false, allowNaN: false, maxDecimalPlaces: 2 }, { message: 'Количество должно быть числом.' })
|
||||||
|
count?: number;
|
||||||
|
}
|
||||||
@ -0,0 +1,69 @@
|
|||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
import {
|
||||||
|
IsNumber,
|
||||||
|
IsOptional,
|
||||||
|
IsString,
|
||||||
|
} from 'class-validator';
|
||||||
|
|
||||||
|
import { IsISODateTimeString } from '../../../../shared/validators';
|
||||||
|
|
||||||
|
export class EditExpenseListItemDto {
|
||||||
|
@ApiProperty({
|
||||||
|
description: 'Заголовок расхода',
|
||||||
|
example: 'Кофе',
|
||||||
|
type: String,
|
||||||
|
})
|
||||||
|
@IsOptional()
|
||||||
|
@IsString({ message: 'Заголовок расхода должен быть строкой.' })
|
||||||
|
title?: string;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
description: 'Описание',
|
||||||
|
example: 'Перед работой',
|
||||||
|
type: String,
|
||||||
|
nullable: true,
|
||||||
|
})
|
||||||
|
@IsOptional()
|
||||||
|
@IsString({ message: 'Описание расхода должно быть строкой.' })
|
||||||
|
description?: string;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
description: 'Дата',
|
||||||
|
example: '2025-08-22T04:12:03.726Z',
|
||||||
|
type: Date,
|
||||||
|
})
|
||||||
|
@IsOptional()
|
||||||
|
@IsISODateTimeString({ message: 'Дата должна быть строкой в формате ISO 8601' })
|
||||||
|
date?: Date;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
description: 'Сумма расхода',
|
||||||
|
example: 175.50,
|
||||||
|
type: Number,
|
||||||
|
})
|
||||||
|
@IsOptional()
|
||||||
|
@IsNumber({ allowInfinity: false, allowNaN: false, maxDecimalPlaces: 2 }, { message: 'Сумма расхода должна быть числом.' })
|
||||||
|
amount?: number;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
description: 'Валюта расхода',
|
||||||
|
example: 'RUB',
|
||||||
|
type: String,
|
||||||
|
default: 'RUB',
|
||||||
|
nullable: true,
|
||||||
|
})
|
||||||
|
@IsOptional()
|
||||||
|
@IsString({ message: 'Валюта расхода должна быть строкой.' })
|
||||||
|
currency?: string;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
description: 'Количество',
|
||||||
|
example: 2,
|
||||||
|
type: Number,
|
||||||
|
default: 1,
|
||||||
|
nullable: true,
|
||||||
|
})
|
||||||
|
@IsOptional()
|
||||||
|
@IsNumber({ allowInfinity: false, allowNaN: false, maxDecimalPlaces: 2 }, { message: 'Количество должно быть числом.' })
|
||||||
|
count?: number;
|
||||||
|
}
|
||||||
@ -0,0 +1,75 @@
|
|||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
import { ExpenseItem, ExpenseList } from '@prisma/client';
|
||||||
|
|
||||||
|
export class ExpenseListItemDto {
|
||||||
|
@ApiProperty({
|
||||||
|
description: 'ID',
|
||||||
|
type: String,
|
||||||
|
})
|
||||||
|
id: ExpenseItem['id'];
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
description: 'ID списка',
|
||||||
|
type: String,
|
||||||
|
})
|
||||||
|
expenseListId: ExpenseList['id'];
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
description: 'Заголовок расхода',
|
||||||
|
example: 'Кофе',
|
||||||
|
type: String,
|
||||||
|
})
|
||||||
|
title: ExpenseItem['title'];
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
description: 'Описание',
|
||||||
|
example: 'Перед работой',
|
||||||
|
type: String,
|
||||||
|
nullable: true,
|
||||||
|
})
|
||||||
|
description: ExpenseItem['description'];
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
description: 'Дата',
|
||||||
|
example: '2025-08-22T04:12:03.726Z',
|
||||||
|
type: Date,
|
||||||
|
})
|
||||||
|
date: ExpenseItem['date'];
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
description: 'Сумма расхода',
|
||||||
|
example: 175.50,
|
||||||
|
type: Number,
|
||||||
|
})
|
||||||
|
amount: ExpenseItem['amount'];
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
description: 'Валюта расхода',
|
||||||
|
example: 'RUB',
|
||||||
|
type: String,
|
||||||
|
default: 'RUB',
|
||||||
|
nullable: true,
|
||||||
|
})
|
||||||
|
currency: ExpenseItem['currency'];
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
description: 'Количество',
|
||||||
|
example: 2,
|
||||||
|
type: Number,
|
||||||
|
default: 1,
|
||||||
|
nullable: true,
|
||||||
|
})
|
||||||
|
count: ExpenseItem['count'];
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
description: 'Дата создания',
|
||||||
|
type: Date,
|
||||||
|
})
|
||||||
|
createdAt: Date;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
description: 'Дата обновления',
|
||||||
|
type: Date,
|
||||||
|
})
|
||||||
|
updatedAt: Date;
|
||||||
|
}
|
||||||
@ -0,0 +1,13 @@
|
|||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
import { IsNotEmpty, IsString } from 'class-validator';
|
||||||
|
|
||||||
|
export class CreateExpenseListDto {
|
||||||
|
@ApiProperty({
|
||||||
|
description: 'Название листа',
|
||||||
|
example: 'Ежедневные покупки',
|
||||||
|
type: String,
|
||||||
|
})
|
||||||
|
@IsString({ message: 'Название листа должен быть строкой.' })
|
||||||
|
@IsNotEmpty({ message: 'Название листа обязателно к заполнению.' })
|
||||||
|
title: string;
|
||||||
|
}
|
||||||
@ -0,0 +1,12 @@
|
|||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
import { IsNotEmpty, IsString } from 'class-validator';
|
||||||
|
|
||||||
|
export class DeleteExpenseListDto {
|
||||||
|
@ApiProperty({
|
||||||
|
description: 'ID листа',
|
||||||
|
type: String,
|
||||||
|
})
|
||||||
|
@IsString({ message: 'ID должен быть строкой.' })
|
||||||
|
@IsNotEmpty({ message: 'ID обязателно к заполнению.' })
|
||||||
|
id: string;
|
||||||
|
}
|
||||||
@ -0,0 +1,9 @@
|
|||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
|
||||||
|
export class DeletedExpenseListDto {
|
||||||
|
@ApiProperty({
|
||||||
|
description: 'ID',
|
||||||
|
type: String,
|
||||||
|
})
|
||||||
|
id: string;
|
||||||
|
}
|
||||||
@ -0,0 +1,13 @@
|
|||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
import { IsNotEmpty, IsString } from 'class-validator';
|
||||||
|
|
||||||
|
export class EditExpenseListDto {
|
||||||
|
@ApiProperty({
|
||||||
|
description: 'Название листа',
|
||||||
|
example: 'Ежедневные покупки',
|
||||||
|
type: String,
|
||||||
|
})
|
||||||
|
@IsString({ message: 'Название листа должен быть строкой.' })
|
||||||
|
@IsNotEmpty({ message: 'Название листа обязателно к заполнению.' })
|
||||||
|
title?: string;
|
||||||
|
}
|
||||||
@ -0,0 +1,37 @@
|
|||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
import { ExpenseItem } from '@prisma/client';
|
||||||
|
|
||||||
|
import { ExpenseListItemDto } from '../expense-list-item/expense-list-item.dto';
|
||||||
|
|
||||||
|
export class ExpenseListDto {
|
||||||
|
@ApiProperty({
|
||||||
|
description: 'ID',
|
||||||
|
type: String,
|
||||||
|
})
|
||||||
|
id: string;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
description: 'Название листа',
|
||||||
|
type: String,
|
||||||
|
})
|
||||||
|
title: string;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
description: 'Список расходов',
|
||||||
|
type: [ExpenseListItemDto],
|
||||||
|
nullable: true,
|
||||||
|
})
|
||||||
|
items?: ExpenseItem[];
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
description: 'Дата создания',
|
||||||
|
type: Date,
|
||||||
|
})
|
||||||
|
createdAt: Date;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
description: 'Дата обновления',
|
||||||
|
type: Date,
|
||||||
|
})
|
||||||
|
updatedAt: Date;
|
||||||
|
}
|
||||||
11
server/src/modules/expenses/dto/index.ts
Normal file
11
server/src/modules/expenses/dto/index.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
export { CreateExpenseListDto } from './expense-list/create-expense-list.dto';
|
||||||
|
export { CreateExpenseListItemDto } from './expense-list-item/create-expense-list.dto';
|
||||||
|
|
||||||
|
export { DeleteExpenseListDto } from './expense-list/delete-expense-list.dto';
|
||||||
|
export { DeletedExpenseListDto } from './expense-list/deleted-expense-list.dto';
|
||||||
|
|
||||||
|
export { EditExpenseListDto } from './expense-list/edit-expense-list.dto';
|
||||||
|
export { EditExpenseListItemDto } from './expense-list-item/edit-expense-list.dto';
|
||||||
|
|
||||||
|
export { ExpenseListDto } from './expense-list/expense-list.dto';
|
||||||
|
export { ExpenseListItemDto } from './expense-list-item/expense-list-item.dto';
|
||||||
120
server/src/modules/expenses/expenses.controller.ts
Normal file
120
server/src/modules/expenses/expenses.controller.ts
Normal file
@ -0,0 +1,120 @@
|
|||||||
|
import {
|
||||||
|
Body,
|
||||||
|
Controller,
|
||||||
|
Delete,
|
||||||
|
Get,
|
||||||
|
Param,
|
||||||
|
Patch,
|
||||||
|
Post,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import { ExpenseItem, ExpenseList, User } from '@prisma/client';
|
||||||
|
|
||||||
|
import { Authorized } from '@/shared/decorators';
|
||||||
|
|
||||||
|
import {
|
||||||
|
CreateExpenseList,
|
||||||
|
CreateExpenseListItem,
|
||||||
|
DeleteExpenseList,
|
||||||
|
DeleteExpenseListItem,
|
||||||
|
EditExpenseList,
|
||||||
|
EditExpenseListItem,
|
||||||
|
FindAllExpenseLists,
|
||||||
|
FindExpenseList,
|
||||||
|
FindExpenseListItem,
|
||||||
|
} from './decorators';
|
||||||
|
import {
|
||||||
|
CreateExpenseListItemDto,
|
||||||
|
EditExpenseListItemDto,
|
||||||
|
CreateExpenseListDto,
|
||||||
|
EditExpenseListDto,
|
||||||
|
} from './dto';
|
||||||
|
import { ExpensesService } from './expenses.service';
|
||||||
|
|
||||||
|
@Controller('expenses')
|
||||||
|
export class ExpensesController {
|
||||||
|
constructor(private readonly expensesService: ExpensesService) {}
|
||||||
|
|
||||||
|
@Post('list')
|
||||||
|
@CreateExpenseList()
|
||||||
|
async createList(
|
||||||
|
@Body() body: CreateExpenseListDto,
|
||||||
|
@Authorized('userId') userId: User['id'],
|
||||||
|
) {
|
||||||
|
return this.expensesService.createList(body, userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get('list/:id')
|
||||||
|
@FindExpenseList()
|
||||||
|
async findList(
|
||||||
|
@Param('id') id: ExpenseList['id'],
|
||||||
|
@Authorized('userId') userId: User['id'],
|
||||||
|
) {
|
||||||
|
return this.expensesService.getListById(id, userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get('list')
|
||||||
|
@FindAllExpenseLists()
|
||||||
|
async findAllLists(@Authorized('userId') userId: User['id']) {
|
||||||
|
return this.expensesService.getAllLists(userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Patch('list/:id')
|
||||||
|
@EditExpenseList()
|
||||||
|
async editList(
|
||||||
|
@Param('id') id: ExpenseList['id'],
|
||||||
|
@Body() body: EditExpenseListDto,
|
||||||
|
@Authorized('userId') userId: User['id'],
|
||||||
|
) {
|
||||||
|
return this.expensesService.editList(id, body, userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Delete('list/:id')
|
||||||
|
@DeleteExpenseList()
|
||||||
|
async deleteList(
|
||||||
|
@Param('id') id: ExpenseList['id'],
|
||||||
|
@Authorized('userId') userId: User['id'],
|
||||||
|
) {
|
||||||
|
return this.expensesService.deleteList(id, userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post('list/:id/items')
|
||||||
|
@CreateExpenseListItem()
|
||||||
|
async createListItem(
|
||||||
|
@Body() body: CreateExpenseListItemDto,
|
||||||
|
@Param('id') listId: ExpenseList['id'],
|
||||||
|
@Authorized('userId') userId: User['id'],
|
||||||
|
) {
|
||||||
|
return this.expensesService.createListItem(body, listId, userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get('list/:listId/items/:id')
|
||||||
|
@FindExpenseListItem()
|
||||||
|
async findListItem(
|
||||||
|
@Param('listId') listId: ExpenseList['id'],
|
||||||
|
@Param('id') id: ExpenseItem['id'],
|
||||||
|
@Authorized('userId') userId: User['id'],
|
||||||
|
) {
|
||||||
|
return this.expensesService.findListItemById(id, listId, userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Patch('list/:listId/items/:id')
|
||||||
|
@EditExpenseListItem()
|
||||||
|
async editListItem(
|
||||||
|
@Body() body: EditExpenseListItemDto,
|
||||||
|
@Param('listId') listId: ExpenseList['id'],
|
||||||
|
@Param('id') id: ExpenseItem['id'],
|
||||||
|
@Authorized('userId') userId: User['id'],
|
||||||
|
) {
|
||||||
|
return this.expensesService.editListItem(body, id, listId, userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Delete('list/:listId/items/:id')
|
||||||
|
@DeleteExpenseListItem()
|
||||||
|
async deleteListItem(
|
||||||
|
@Param('listId') listId: ExpenseList['id'],
|
||||||
|
@Param('id') id: ExpenseItem['id'],
|
||||||
|
@Authorized('userId') userId: User['id'],
|
||||||
|
) {
|
||||||
|
return this.expensesService.deleteListItem(id, listId, userId);
|
||||||
|
}
|
||||||
|
}
|
||||||
10
server/src/modules/expenses/expenses.module.ts
Normal file
10
server/src/modules/expenses/expenses.module.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
|
||||||
|
import { ExpensesController } from './expenses.controller';
|
||||||
|
import { ExpensesService } from './expenses.service';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
controllers: [ExpensesController],
|
||||||
|
providers: [ExpensesService],
|
||||||
|
})
|
||||||
|
export class ExpensesModule {}
|
||||||
179
server/src/modules/expenses/expenses.service.ts
Normal file
179
server/src/modules/expenses/expenses.service.ts
Normal file
@ -0,0 +1,179 @@
|
|||||||
|
import { Injectable, NotFoundException } from '@nestjs/common';
|
||||||
|
import { ExpenseItem, ExpenseList, User } from '@prisma/client';
|
||||||
|
|
||||||
|
import { PrismaService } from '../prisma/prisma.service';
|
||||||
|
|
||||||
|
import {
|
||||||
|
CreateExpenseListDto,
|
||||||
|
DeletedExpenseListDto,
|
||||||
|
EditExpenseListDto,
|
||||||
|
ExpenseListDto,
|
||||||
|
CreateExpenseListItemDto,
|
||||||
|
EditExpenseListItemDto,
|
||||||
|
ExpenseListItemDto,
|
||||||
|
} from './dto';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class ExpensesService {
|
||||||
|
constructor(
|
||||||
|
private readonly prismaService: PrismaService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async createList(dto: CreateExpenseListDto, userId: User['id']): Promise<ExpenseListDto> {
|
||||||
|
const { title } = dto;
|
||||||
|
|
||||||
|
const expenseList = await this.prismaService.expenseList.create({
|
||||||
|
data: { title, user: { connect: { id: userId } } },
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: expenseList.id,
|
||||||
|
title: expenseList.title,
|
||||||
|
createdAt: expenseList.createdAt,
|
||||||
|
updatedAt: expenseList.updatedAt,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async getListById(id: ExpenseList['id'], userId: User['id']): Promise<ExpenseListDto> {
|
||||||
|
const list = await this.findList(id, userId);
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: list.id,
|
||||||
|
title: list.title,
|
||||||
|
items: list.items,
|
||||||
|
createdAt: list.createdAt,
|
||||||
|
updatedAt: list.updatedAt,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async getAllLists(userId: User['id']): Promise<ExpenseListDto[]> {
|
||||||
|
return this.prismaService.expenseList.findMany({
|
||||||
|
where: { userId },
|
||||||
|
include: { items: true },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async editList(id: ExpenseList['id'], dto: EditExpenseListDto, userId: User['id']): Promise<ExpenseListDto> {
|
||||||
|
const { title } = dto;
|
||||||
|
|
||||||
|
const expenseList = await this.findList(id, userId);
|
||||||
|
const updatedList = await this.prismaService.expenseList.update({
|
||||||
|
where: { id: expenseList.id },
|
||||||
|
data: { title },
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: updatedList.id,
|
||||||
|
title: updatedList.title,
|
||||||
|
createdAt: updatedList.createdAt,
|
||||||
|
updatedAt: updatedList.updatedAt,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteList(id: ExpenseList['id'], userId: User['id']): Promise<DeletedExpenseListDto> {
|
||||||
|
const { id: listId } = await this.findList(id, userId);
|
||||||
|
|
||||||
|
const deletedList = await this.prismaService.expenseList.delete({ where: { id: listId } });
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: deletedList.id,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async createListItem(dto: CreateExpenseListItemDto, listId: ExpenseList['id'], userId: User['id']): Promise<ExpenseListItemDto> {
|
||||||
|
const {
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
date,
|
||||||
|
amount,
|
||||||
|
currency,
|
||||||
|
count,
|
||||||
|
} = dto;
|
||||||
|
const list = await this.findList(listId, userId);
|
||||||
|
|
||||||
|
return this.prismaService.expenseItem.create({
|
||||||
|
data: {
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
date,
|
||||||
|
amount,
|
||||||
|
currency,
|
||||||
|
count,
|
||||||
|
expenseList: {
|
||||||
|
connect: { id: list.id },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async findListItemById(id: ExpenseItem['id'], listId: ExpenseList['id'], userId: User['id']) {
|
||||||
|
return this.findListItem(id, listId, userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/max-params
|
||||||
|
async editListItem(
|
||||||
|
dto: EditExpenseListItemDto,
|
||||||
|
itemId: ExpenseItem['id'],
|
||||||
|
listId: ExpenseList['id'],
|
||||||
|
userId: User['id'],
|
||||||
|
): Promise<ExpenseListItemDto> {
|
||||||
|
const item = await this.findListItem(itemId, listId, userId);
|
||||||
|
|
||||||
|
const {
|
||||||
|
title = item.title,
|
||||||
|
description = item.description,
|
||||||
|
date = item.date,
|
||||||
|
amount = item.amount,
|
||||||
|
currency = item.currency,
|
||||||
|
count = item.count,
|
||||||
|
} = dto;
|
||||||
|
|
||||||
|
return this.prismaService.expenseItem.update({
|
||||||
|
where: { id: item.id },
|
||||||
|
data: {
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
date,
|
||||||
|
amount,
|
||||||
|
currency,
|
||||||
|
count,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteListItem(
|
||||||
|
itemId: ExpenseItem['id'],
|
||||||
|
listId: ExpenseList['id'],
|
||||||
|
userId: User['id'],
|
||||||
|
): Promise<ExpenseListItemDto> {
|
||||||
|
const item = await this.findListItem(itemId, listId, userId);
|
||||||
|
|
||||||
|
return this.prismaService.expenseItem.delete({
|
||||||
|
where: { id: item.id },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async findList(id: ExpenseList['id'], userId: User['id']) {
|
||||||
|
const existingList = await this.prismaService.expenseList.findUnique({
|
||||||
|
where: { id, userId },
|
||||||
|
include: { items: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!existingList) {
|
||||||
|
throw new NotFoundException('Лист расходов не найден');
|
||||||
|
}
|
||||||
|
|
||||||
|
return existingList;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async findListItem(id: ExpenseItem['id'], listId: ExpenseList['id'], userId: User['id']) {
|
||||||
|
const list = await this.findList(listId, userId);
|
||||||
|
const existingItem = await this.prismaService.expenseItem.findUnique({ where: { id } });
|
||||||
|
|
||||||
|
if (!existingItem || existingItem.expenseListId !== list.id) {
|
||||||
|
throw new NotFoundException('Позиция расходов не найдена');
|
||||||
|
}
|
||||||
|
|
||||||
|
return existingItem;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -8,7 +8,7 @@ export class PrismaService extends PrismaClient implements OnModuleInit, OnModul
|
|||||||
await this.$connect();
|
await this.$connect();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async onModuleDestroy() {
|
public onModuleDestroy() {
|
||||||
await this.$disconnect;
|
void this.$disconnect;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import { applyDecorators, UseGuards } from '@nestjs/common';
|
import { applyDecorators, UseGuards } from '@nestjs/common';
|
||||||
import { JwtGuard } from '../guards/jwt.guard';
|
|
||||||
|
import { JwtGuard } from '../../guards';
|
||||||
|
|
||||||
export function Authorization() {
|
export function Authorization() {
|
||||||
return applyDecorators(UseGuards(JwtGuard));
|
return applyDecorators(UseGuards(JwtGuard));
|
||||||
@ -1,7 +1,8 @@
|
|||||||
import type { ExecutionContext } from '@nestjs/common';
|
|
||||||
import { createParamDecorator } from '@nestjs/common';
|
import { createParamDecorator } from '@nestjs/common';
|
||||||
|
|
||||||
|
import type { Request } from '../../types';
|
||||||
|
import type { ExecutionContext } from '@nestjs/common';
|
||||||
import type { Account } from '@prisma/client';
|
import type { Account } from '@prisma/client';
|
||||||
import type { Request } from '../types/request';
|
|
||||||
|
|
||||||
export const Authorized = createParamDecorator(
|
export const Authorized = createParamDecorator(
|
||||||
(data: keyof Account | undefined, ctx: ExecutionContext) => {
|
(data: keyof Account | undefined, ctx: ExecutionContext) => {
|
||||||
2
server/src/shared/decorators/index.ts
Normal file
2
server/src/shared/decorators/index.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export { Authorization } from './Auth/authorization.decorator';
|
||||||
|
export { Authorized } from './Auth/authorized.decorator';
|
||||||
1
server/src/shared/guards/index.ts
Normal file
1
server/src/shared/guards/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export { JwtGuard } from './JwtGuard/jwt.guard';
|
||||||
2
server/src/shared/types/index.ts
Normal file
2
server/src/shared/types/index.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export type { Request } from './request';
|
||||||
|
export type { JwtPayload } from './jwt-payload';
|
||||||
@ -0,0 +1,28 @@
|
|||||||
|
import { registerDecorator } from 'class-validator';
|
||||||
|
|
||||||
|
import type { ValidationOptions } from 'class-validator';
|
||||||
|
|
||||||
|
export function IsISODateTimeString(validationOptions?: ValidationOptions) {
|
||||||
|
// eslint-disable-next-line func-names
|
||||||
|
return function (object: object, propertyName: string) {
|
||||||
|
registerDecorator({
|
||||||
|
name: 'isISODateTimeString',
|
||||||
|
target: object.constructor,
|
||||||
|
propertyName,
|
||||||
|
options: validationOptions,
|
||||||
|
validator: {
|
||||||
|
validate(value: unknown) {
|
||||||
|
if (typeof value !== 'string') return false;
|
||||||
|
|
||||||
|
// Разрешаем только "2025-08-22T04:12:03.726Z" или без миллисекунд
|
||||||
|
const regex = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,3})?Z$/;
|
||||||
|
|
||||||
|
return regex.test(value);
|
||||||
|
},
|
||||||
|
defaultMessage() {
|
||||||
|
return 'Дата должна быть в формате YYYY-MM-DDTHH:mm:ss(.sss)Z';
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
1
server/src/shared/validators/index.ts
Normal file
1
server/src/shared/validators/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export { IsISODateTimeString } from './IsISODateTimeString/IsISODateTimeString.validator';
|
||||||
@ -16,7 +16,10 @@
|
|||||||
"forceConsistentCasingInFileNames": true,
|
"forceConsistentCasingInFileNames": true,
|
||||||
"noImplicitAny": false,
|
"noImplicitAny": false,
|
||||||
"strictBindCallApply": false,
|
"strictBindCallApply": false,
|
||||||
"noFallthroughCasesInSwitch": false
|
"noFallthroughCasesInSwitch": false,
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["./src/*"]
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"include": [
|
"include": [
|
||||||
"src",
|
"src",
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user