From d60aae26e6ff3c24a6b8564aaeb5a30a26036aae Mon Sep 17 00:00:00 2001 From: Sergey Krylov Date: Thu, 28 Aug 2025 05:05:15 +0300 Subject: [PATCH] feat(backend): add expenses --- server/eslint.config.ts | 57 ++++++ server/prisma/schema.prisma | 40 ++++ server/src/main.ts | 5 +- server/src/modules/app.module.ts | 5 +- server/src/modules/auth/auth.controller.ts | 92 +++------ server/src/modules/auth/auth.module.ts | 4 +- server/src/modules/auth/auth.service.ts | 51 +++-- server/src/modules/auth/decorators/index.ts | 5 + .../auth/decorators/login.decorator.ts | 22 +++ .../auth/decorators/logout.decorator.ts | 21 ++ .../modules/auth/decorators/me.decorator.ts | 23 +++ .../auth/decorators/refresh.decorator.ts | 18 ++ .../auth/decorators/register.decorator.ts | 27 +++ server/src/modules/auth/dto/index.ts | 10 + server/src/modules/auth/dto/login/index.ts | 4 - server/src/modules/auth/dto/profile/index.ts | 1 - server/src/modules/auth/dto/register/index.ts | 3 - .../modules/auth/strategies/jwt.strategies.ts | 1 + .../create-expense-list-item.decorator.ts | 30 +++ .../delete-expense-list-item.decorator.ts | 33 ++++ .../edit-expense-list-item.decorator.ts | 33 ++++ .../find-expense-list-item.decorator.ts | 34 ++++ .../create-expense-list.decorator.ts | 24 +++ .../delete-expense-list.decorator.ts | 32 ++++ .../edit-expense-list.decorator.ts | 32 ++++ .../find-all-expense-list.decorator.ts | 28 +++ .../find-expense-list.decorator.ts | 32 ++++ .../src/modules/expenses/decorators/index.ts | 12 ++ .../create-expense-list.dto.ts | 70 +++++++ .../edit-expense-list.dto.ts | 69 +++++++ .../expense-list-item.dto.ts | 75 ++++++++ .../expense-list/create-expense-list.dto.ts | 13 ++ .../expense-list/delete-expense-list.dto.ts | 12 ++ .../expense-list/deleted-expense-list.dto.ts | 9 + .../dto/expense-list/edit-expense-list.dto.ts | 13 ++ .../dto/expense-list/expense-list.dto.ts | 37 ++++ server/src/modules/expenses/dto/index.ts | 11 ++ .../modules/expenses/expenses.controller.ts | 120 ++++++++++++ .../src/modules/expenses/expenses.module.ts | 10 + .../src/modules/expenses/expenses.service.ts | 179 ++++++++++++++++++ server/src/modules/prisma/prisma.service.ts | 4 +- .../Auth}/authorization.decorator.ts | 3 +- .../decorators/Auth}/authorized.decorator.ts | 5 +- server/src/shared/decorators/index.ts | 2 + .../guards/JwtGuard}/jwt.guard.ts | 0 server/src/shared/guards/index.ts | 1 + server/src/shared/types/index.ts | 2 + .../auth => shared}/types/jwt-payload.ts | 0 .../{modules/auth => shared}/types/request.ts | 0 .../IsISODateTimeString.validator.ts | 28 +++ server/src/shared/validators/index.ts | 1 + server/tsconfig.json | 5 +- 52 files changed, 1242 insertions(+), 106 deletions(-) create mode 100644 server/src/modules/auth/decorators/index.ts create mode 100644 server/src/modules/auth/decorators/login.decorator.ts create mode 100644 server/src/modules/auth/decorators/logout.decorator.ts create mode 100644 server/src/modules/auth/decorators/me.decorator.ts create mode 100644 server/src/modules/auth/decorators/refresh.decorator.ts create mode 100644 server/src/modules/auth/decorators/register.decorator.ts create mode 100644 server/src/modules/auth/dto/index.ts delete mode 100644 server/src/modules/auth/dto/login/index.ts delete mode 100644 server/src/modules/auth/dto/profile/index.ts delete mode 100644 server/src/modules/auth/dto/register/index.ts create mode 100644 server/src/modules/expenses/decorators/expense-list-item/create-expense-list-item.decorator.ts create mode 100644 server/src/modules/expenses/decorators/expense-list-item/delete-expense-list-item.decorator.ts create mode 100644 server/src/modules/expenses/decorators/expense-list-item/edit-expense-list-item.decorator.ts create mode 100644 server/src/modules/expenses/decorators/expense-list-item/find-expense-list-item.decorator.ts create mode 100644 server/src/modules/expenses/decorators/expense-list/create-expense-list.decorator.ts create mode 100644 server/src/modules/expenses/decorators/expense-list/delete-expense-list.decorator.ts create mode 100644 server/src/modules/expenses/decorators/expense-list/edit-expense-list.decorator.ts create mode 100644 server/src/modules/expenses/decorators/expense-list/find-all-expense-list.decorator.ts create mode 100644 server/src/modules/expenses/decorators/expense-list/find-expense-list.decorator.ts create mode 100644 server/src/modules/expenses/decorators/index.ts create mode 100644 server/src/modules/expenses/dto/expense-list-item/create-expense-list.dto.ts create mode 100644 server/src/modules/expenses/dto/expense-list-item/edit-expense-list.dto.ts create mode 100644 server/src/modules/expenses/dto/expense-list-item/expense-list-item.dto.ts create mode 100644 server/src/modules/expenses/dto/expense-list/create-expense-list.dto.ts create mode 100644 server/src/modules/expenses/dto/expense-list/delete-expense-list.dto.ts create mode 100644 server/src/modules/expenses/dto/expense-list/deleted-expense-list.dto.ts create mode 100644 server/src/modules/expenses/dto/expense-list/edit-expense-list.dto.ts create mode 100644 server/src/modules/expenses/dto/expense-list/expense-list.dto.ts create mode 100644 server/src/modules/expenses/dto/index.ts create mode 100644 server/src/modules/expenses/expenses.controller.ts create mode 100644 server/src/modules/expenses/expenses.module.ts create mode 100644 server/src/modules/expenses/expenses.service.ts rename server/src/{modules/auth/decorators => shared/decorators/Auth}/authorization.decorator.ts (75%) rename server/src/{modules/auth/decorators => shared/decorators/Auth}/authorized.decorator.ts (89%) create mode 100644 server/src/shared/decorators/index.ts rename server/src/{modules/auth/guards => shared/guards/JwtGuard}/jwt.guard.ts (100%) create mode 100644 server/src/shared/guards/index.ts create mode 100644 server/src/shared/types/index.ts rename server/src/{modules/auth => shared}/types/jwt-payload.ts (100%) rename server/src/{modules/auth => shared}/types/request.ts (100%) create mode 100644 server/src/shared/validators/IsISODateTimeString/IsISODateTimeString.validator.ts create mode 100644 server/src/shared/validators/index.ts diff --git a/server/eslint.config.ts b/server/eslint.config.ts index 0073422..a1d23ee 100644 --- a/server/eslint.config.ts +++ b/server/eslint.config.ts @@ -14,6 +14,63 @@ export default [ '@typescript-eslint/parameter-properties': 'off', '@typescript-eslint/class-methods-use-this': '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, + }, + }, + ], }, }, ]; diff --git a/server/prisma/schema.prisma b/server/prisma/schema.prisma index 7dcd692..8ffb950 100644 --- a/server/prisma/schema.prisma +++ b/server/prisma/schema.prisma @@ -14,6 +14,46 @@ model Account { password String createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") + userId String @unique + user User @relation(fields: [userId], references: [id]) @@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") +} diff --git a/server/src/main.ts b/server/src/main.ts index bd1ba50..dc2d54d 100644 --- a/server/src/main.ts +++ b/server/src/main.ts @@ -1,14 +1,17 @@ +import * as process from 'node:process'; + import { ValidationPipe } from '@nestjs/common'; import { NestFactory } from '@nestjs/core'; 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() { const app = await NestFactory.create(AppModule); const config = new DocumentBuilder() + .addBearerAuth() .setTitle('Investments Control API') .setDescription('Documentation for **Investments Control API**') .setVersion(process.env.npm_package_version ?? '0.0.0') diff --git a/server/src/modules/app.module.ts b/server/src/modules/app.module.ts index d9f2b42..1aa348c 100644 --- a/server/src/modules/app.module.ts +++ b/server/src/modules/app.module.ts @@ -1,7 +1,9 @@ import { Module } from '@nestjs/common'; import { ConfigModule } from '@nestjs/config'; -import { PrismaModule } from './prisma/prisma.module'; + import { AuthModule } from './auth/auth.module'; +import { ExpensesModule } from './expenses/expenses.module'; +import { PrismaModule } from './prisma/prisma.module'; @Module({ imports: [ @@ -10,6 +12,7 @@ import { AuthModule } from './auth/auth.module'; }), PrismaModule, AuthModule, + ExpensesModule, ], }) export class AppModule {} diff --git a/server/src/modules/auth/auth.controller.ts b/server/src/modules/auth/auth.controller.ts index 80e4a8a..b197a49 100644 --- a/server/src/modules/auth/auth.controller.ts +++ b/server/src/modules/auth/auth.controller.ts @@ -1,69 +1,40 @@ import { Body, - Controller, Get, HttpCode, HttpStatus, - Post, Req, Res, + Controller, + Get, + 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 { Authorized } from '@/shared/decorators'; + 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'; + Login, + Logout, + Me, + Refresh, + Register, +} from './decorators'; +import { LoginDto, RegisterDto } from './dto'; + +import type { Request } from '@/shared/types'; +import type { Response } from 'express'; @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') + @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') + @Login() public async login( @Res({ passthrough: true }) response: Response, @Body() body: LoginDto, @@ -71,35 +42,20 @@ export class AuthController { return this.authService.login(response, body); } - @ApiOperation({ - summary: 'Логаут', - description: 'Выход из аккаунта', - }) - @ApiOkResponse({ description: 'Успешный выход из аккаунт', type: Boolean }) - @Authorization() @Post('logout') + @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']) { + @Me() + public profile(@Authorized('id') user: Account['id']) { return { id: user }; } - @ApiOperation({ summary: 'Обновление access и refresh токенов' }) - @ApiOkResponse({ description: 'Успешное обновление токенов', type: SuccessLoginDto }) - @ApiUnauthorizedResponse({ description: 'Пользователь не авторизован' }) - @ApiNotFoundResponse({ description: '' }) @Post('refresh') + @Refresh() public async refresh( @Req() request: Request, @Res({ passthrough: true }) response: Response, diff --git a/server/src/modules/auth/auth.module.ts b/server/src/modules/auth/auth.module.ts index c031dae..f81438a 100644 --- a/server/src/modules/auth/auth.module.ts +++ b/server/src/modules/auth/auth.module.ts @@ -2,9 +2,11 @@ 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 { AuthService } from './auth.service'; import { JwtStrategy } from './strategies/jwt.strategies'; @Module({ diff --git a/server/src/modules/auth/auth.service.ts b/server/src/modules/auth/auth.service.ts index 442bb1c..d81696d 100644 --- a/server/src/modules/auth/auth.service.ts +++ b/server/src/modules/auth/auth.service.ts @@ -1,16 +1,22 @@ import { - BadRequestException, ConflictException, Injectable, NotFoundException, UnauthorizedException, + 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 { JwtPayload, Request } from '@/shared/types'; + 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'; + +import { LoginDto, RegisterDto, SuccessLoginDto } from './dto'; + +import type { Response } from 'express'; @Injectable() export class AuthService { @@ -36,43 +42,46 @@ export class AuthService { const salt = await bcrypt.genSalt(); const hashedPassword = await bcrypt.hash(password, salt); - const existingUser = await this.prismaService.account.findUnique({ + const existingAccount = await this.prismaService.account.findUnique({ where: { email }, }); - if (existingUser) { + if (existingAccount) { throw new ConflictException('Пользователь с такой почтой уже зарегистрирован'); } - const user = await this.prismaService.account.create({ + const account = await this.prismaService.account.create({ data: { email, password: hashedPassword, + user: { + create: {}, + }, }, }); return { - id: user.id, - email: user.email, + id: account.id, + email: account.email, }; } public async login(response: Response, dto: LoginDto): Promise { const { email, password } = dto; - const existingUser = await this.prismaService.account.findUnique({ + const existingAccount = await this.prismaService.account.findUnique({ where: { email }, }); - if (!existingUser) { + if (!existingAccount) { throw new BadRequestException('Неверный логин или пароль'); } - const isMatch = await bcrypt.compare(password, existingUser.password); + const isMatch = await bcrypt.compare(password, existingAccount.password); if (!isMatch) { throw new BadRequestException('Неверный логин или пароль'); } - const { accessToken } = this.auth(response, existingUser.id); + const { accessToken } = this.auth(response, existingAccount.id); return { accessToken, @@ -86,17 +95,17 @@ export class AuthService { } public async validate(id: string) { - const existingUser = await this.prismaService.account.findUnique({ + const existingAccount = await this.prismaService.account.findUnique({ where: { id, }, }); - if (!existingUser) { + if (!existingAccount) { throw new NotFoundException(); } - return existingUser; + return existingAccount; } public async refresh(req: Request, res: Response): Promise { @@ -107,15 +116,15 @@ export class AuthService { } 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 }, }); - if (!existingUser) { + if (!existingAccount) { throw new NotFoundException('Пользователь не нейден'); } - return this.auth(res, existingUser.id); + return this.auth(res, existingAccount.id); } private auth(response: Response, id: Account['id']) { diff --git a/server/src/modules/auth/decorators/index.ts b/server/src/modules/auth/decorators/index.ts new file mode 100644 index 0000000..510b64b --- /dev/null +++ b/server/src/modules/auth/decorators/index.ts @@ -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'; diff --git a/server/src/modules/auth/decorators/login.decorator.ts b/server/src/modules/auth/decorators/login.decorator.ts new file mode 100644 index 0000000..1b4edeb --- /dev/null +++ b/server/src/modules/auth/decorators/login.decorator.ts @@ -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, + }), + ); +} diff --git a/server/src/modules/auth/decorators/logout.decorator.ts b/server/src/modules/auth/decorators/logout.decorator.ts new file mode 100644 index 0000000..1acbe33 --- /dev/null +++ b/server/src/modules/auth/decorators/logout.decorator.ts @@ -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(), + ); +} diff --git a/server/src/modules/auth/decorators/me.decorator.ts b/server/src/modules/auth/decorators/me.decorator.ts new file mode 100644 index 0000000..34cf450 --- /dev/null +++ b/server/src/modules/auth/decorators/me.decorator.ts @@ -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: 'Пользователь не авторизован' }), + ); +} diff --git a/server/src/modules/auth/decorators/refresh.decorator.ts b/server/src/modules/auth/decorators/refresh.decorator.ts new file mode 100644 index 0000000..2741745 --- /dev/null +++ b/server/src/modules/auth/decorators/refresh.decorator.ts @@ -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: '' }), + ); +} diff --git a/server/src/modules/auth/decorators/register.decorator.ts b/server/src/modules/auth/decorators/register.decorator.ts new file mode 100644 index 0000000..e8e7de5 --- /dev/null +++ b/server/src/modules/auth/decorators/register.decorator.ts @@ -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, + }), + ); +} diff --git a/server/src/modules/auth/dto/index.ts b/server/src/modules/auth/dto/index.ts new file mode 100644 index 0000000..108e9ba --- /dev/null +++ b/server/src/modules/auth/dto/index.ts @@ -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'; diff --git a/server/src/modules/auth/dto/login/index.ts b/server/src/modules/auth/dto/login/index.ts deleted file mode 100644 index e19ad3d..0000000 --- a/server/src/modules/auth/dto/login/index.ts +++ /dev/null @@ -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'; diff --git a/server/src/modules/auth/dto/profile/index.ts b/server/src/modules/auth/dto/profile/index.ts deleted file mode 100644 index 68bedb2..0000000 --- a/server/src/modules/auth/dto/profile/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { CurrentProfileDto } from './current-profile.dto'; diff --git a/server/src/modules/auth/dto/register/index.ts b/server/src/modules/auth/dto/register/index.ts deleted file mode 100644 index 0e0fd1d..0000000 --- a/server/src/modules/auth/dto/register/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export { ConflictRegisterDto } from './conflict-register.dto'; -export { RegisterValidationDto } from './register-validation.dto'; -export { RegisterDto } from './register.dto'; diff --git a/server/src/modules/auth/strategies/jwt.strategies.ts b/server/src/modules/auth/strategies/jwt.strategies.ts index 2066a42..608f316 100644 --- a/server/src/modules/auth/strategies/jwt.strategies.ts +++ b/server/src/modules/auth/strategies/jwt.strategies.ts @@ -2,6 +2,7 @@ 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 = { diff --git a/server/src/modules/expenses/decorators/expense-list-item/create-expense-list-item.decorator.ts b/server/src/modules/expenses/decorators/expense-list-item/create-expense-list-item.decorator.ts new file mode 100644 index 0000000..e579bfd --- /dev/null +++ b/server/src/modules/expenses/decorators/expense-list-item/create-expense-list-item.decorator.ts @@ -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(), + ); +} diff --git a/server/src/modules/expenses/decorators/expense-list-item/delete-expense-list-item.decorator.ts b/server/src/modules/expenses/decorators/expense-list-item/delete-expense-list-item.decorator.ts new file mode 100644 index 0000000..0b02c5e --- /dev/null +++ b/server/src/modules/expenses/decorators/expense-list-item/delete-expense-list-item.decorator.ts @@ -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(), + ); +} diff --git a/server/src/modules/expenses/decorators/expense-list-item/edit-expense-list-item.decorator.ts b/server/src/modules/expenses/decorators/expense-list-item/edit-expense-list-item.decorator.ts new file mode 100644 index 0000000..b802383 --- /dev/null +++ b/server/src/modules/expenses/decorators/expense-list-item/edit-expense-list-item.decorator.ts @@ -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(), + ); +} diff --git a/server/src/modules/expenses/decorators/expense-list-item/find-expense-list-item.decorator.ts b/server/src/modules/expenses/decorators/expense-list-item/find-expense-list-item.decorator.ts new file mode 100644 index 0000000..ad58132 --- /dev/null +++ b/server/src/modules/expenses/decorators/expense-list-item/find-expense-list-item.decorator.ts @@ -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(), + ); +} diff --git a/server/src/modules/expenses/decorators/expense-list/create-expense-list.decorator.ts b/server/src/modules/expenses/decorators/expense-list/create-expense-list.decorator.ts new file mode 100644 index 0000000..5a78590 --- /dev/null +++ b/server/src/modules/expenses/decorators/expense-list/create-expense-list.decorator.ts @@ -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(), + ); +} diff --git a/server/src/modules/expenses/decorators/expense-list/delete-expense-list.decorator.ts b/server/src/modules/expenses/decorators/expense-list/delete-expense-list.decorator.ts new file mode 100644 index 0000000..08511be --- /dev/null +++ b/server/src/modules/expenses/decorators/expense-list/delete-expense-list.decorator.ts @@ -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(), + ); +} diff --git a/server/src/modules/expenses/decorators/expense-list/edit-expense-list.decorator.ts b/server/src/modules/expenses/decorators/expense-list/edit-expense-list.decorator.ts new file mode 100644 index 0000000..fa86b07 --- /dev/null +++ b/server/src/modules/expenses/decorators/expense-list/edit-expense-list.decorator.ts @@ -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(), + ); +} diff --git a/server/src/modules/expenses/decorators/expense-list/find-all-expense-list.decorator.ts b/server/src/modules/expenses/decorators/expense-list/find-all-expense-list.decorator.ts new file mode 100644 index 0000000..00dad2c --- /dev/null +++ b/server/src/modules/expenses/decorators/expense-list/find-all-expense-list.decorator.ts @@ -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(), + ); +} diff --git a/server/src/modules/expenses/decorators/expense-list/find-expense-list.decorator.ts b/server/src/modules/expenses/decorators/expense-list/find-expense-list.decorator.ts new file mode 100644 index 0000000..7752881 --- /dev/null +++ b/server/src/modules/expenses/decorators/expense-list/find-expense-list.decorator.ts @@ -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(), + ); +} diff --git a/server/src/modules/expenses/decorators/index.ts b/server/src/modules/expenses/decorators/index.ts new file mode 100644 index 0000000..ecc964f --- /dev/null +++ b/server/src/modules/expenses/decorators/index.ts @@ -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'; diff --git a/server/src/modules/expenses/dto/expense-list-item/create-expense-list.dto.ts b/server/src/modules/expenses/dto/expense-list-item/create-expense-list.dto.ts new file mode 100644 index 0000000..34fe39c --- /dev/null +++ b/server/src/modules/expenses/dto/expense-list-item/create-expense-list.dto.ts @@ -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; +} diff --git a/server/src/modules/expenses/dto/expense-list-item/edit-expense-list.dto.ts b/server/src/modules/expenses/dto/expense-list-item/edit-expense-list.dto.ts new file mode 100644 index 0000000..888c12f --- /dev/null +++ b/server/src/modules/expenses/dto/expense-list-item/edit-expense-list.dto.ts @@ -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; +} diff --git a/server/src/modules/expenses/dto/expense-list-item/expense-list-item.dto.ts b/server/src/modules/expenses/dto/expense-list-item/expense-list-item.dto.ts new file mode 100644 index 0000000..d589780 --- /dev/null +++ b/server/src/modules/expenses/dto/expense-list-item/expense-list-item.dto.ts @@ -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; +} diff --git a/server/src/modules/expenses/dto/expense-list/create-expense-list.dto.ts b/server/src/modules/expenses/dto/expense-list/create-expense-list.dto.ts new file mode 100644 index 0000000..6797d08 --- /dev/null +++ b/server/src/modules/expenses/dto/expense-list/create-expense-list.dto.ts @@ -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; +} diff --git a/server/src/modules/expenses/dto/expense-list/delete-expense-list.dto.ts b/server/src/modules/expenses/dto/expense-list/delete-expense-list.dto.ts new file mode 100644 index 0000000..0c71363 --- /dev/null +++ b/server/src/modules/expenses/dto/expense-list/delete-expense-list.dto.ts @@ -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; +} diff --git a/server/src/modules/expenses/dto/expense-list/deleted-expense-list.dto.ts b/server/src/modules/expenses/dto/expense-list/deleted-expense-list.dto.ts new file mode 100644 index 0000000..e897478 --- /dev/null +++ b/server/src/modules/expenses/dto/expense-list/deleted-expense-list.dto.ts @@ -0,0 +1,9 @@ +import { ApiProperty } from '@nestjs/swagger'; + +export class DeletedExpenseListDto { + @ApiProperty({ + description: 'ID', + type: String, + }) + id: string; +} diff --git a/server/src/modules/expenses/dto/expense-list/edit-expense-list.dto.ts b/server/src/modules/expenses/dto/expense-list/edit-expense-list.dto.ts new file mode 100644 index 0000000..cd96661 --- /dev/null +++ b/server/src/modules/expenses/dto/expense-list/edit-expense-list.dto.ts @@ -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; +} diff --git a/server/src/modules/expenses/dto/expense-list/expense-list.dto.ts b/server/src/modules/expenses/dto/expense-list/expense-list.dto.ts new file mode 100644 index 0000000..6e7b4e7 --- /dev/null +++ b/server/src/modules/expenses/dto/expense-list/expense-list.dto.ts @@ -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; +} diff --git a/server/src/modules/expenses/dto/index.ts b/server/src/modules/expenses/dto/index.ts new file mode 100644 index 0000000..3a05a76 --- /dev/null +++ b/server/src/modules/expenses/dto/index.ts @@ -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'; diff --git a/server/src/modules/expenses/expenses.controller.ts b/server/src/modules/expenses/expenses.controller.ts new file mode 100644 index 0000000..3008c20 --- /dev/null +++ b/server/src/modules/expenses/expenses.controller.ts @@ -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); + } +} diff --git a/server/src/modules/expenses/expenses.module.ts b/server/src/modules/expenses/expenses.module.ts new file mode 100644 index 0000000..b3d71fa --- /dev/null +++ b/server/src/modules/expenses/expenses.module.ts @@ -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 {} diff --git a/server/src/modules/expenses/expenses.service.ts b/server/src/modules/expenses/expenses.service.ts new file mode 100644 index 0000000..5178ce2 --- /dev/null +++ b/server/src/modules/expenses/expenses.service.ts @@ -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 { + 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 { + 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 { + return this.prismaService.expenseList.findMany({ + where: { userId }, + include: { items: true }, + }); + } + + async editList(id: ExpenseList['id'], dto: EditExpenseListDto, userId: User['id']): Promise { + 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 { + 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 { + 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 { + 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 { + 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; + } +} diff --git a/server/src/modules/prisma/prisma.service.ts b/server/src/modules/prisma/prisma.service.ts index 39f208e..74ed3b1 100644 --- a/server/src/modules/prisma/prisma.service.ts +++ b/server/src/modules/prisma/prisma.service.ts @@ -8,7 +8,7 @@ export class PrismaService extends PrismaClient implements OnModuleInit, OnModul await this.$connect(); } - public async onModuleDestroy() { - await this.$disconnect; + public onModuleDestroy() { + void this.$disconnect; } } diff --git a/server/src/modules/auth/decorators/authorization.decorator.ts b/server/src/shared/decorators/Auth/authorization.decorator.ts similarity index 75% rename from server/src/modules/auth/decorators/authorization.decorator.ts rename to server/src/shared/decorators/Auth/authorization.decorator.ts index 2c15073..b2cfdf2 100644 --- a/server/src/modules/auth/decorators/authorization.decorator.ts +++ b/server/src/shared/decorators/Auth/authorization.decorator.ts @@ -1,5 +1,6 @@ import { applyDecorators, UseGuards } from '@nestjs/common'; -import { JwtGuard } from '../guards/jwt.guard'; + +import { JwtGuard } from '../../guards'; export function Authorization() { return applyDecorators(UseGuards(JwtGuard)); diff --git a/server/src/modules/auth/decorators/authorized.decorator.ts b/server/src/shared/decorators/Auth/authorized.decorator.ts similarity index 89% rename from server/src/modules/auth/decorators/authorized.decorator.ts rename to server/src/shared/decorators/Auth/authorized.decorator.ts index 99dbb3d..9d72b10 100644 --- a/server/src/modules/auth/decorators/authorized.decorator.ts +++ b/server/src/shared/decorators/Auth/authorized.decorator.ts @@ -1,7 +1,8 @@ -import type { ExecutionContext } 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 { Request } from '../types/request'; export const Authorized = createParamDecorator( (data: keyof Account | undefined, ctx: ExecutionContext) => { diff --git a/server/src/shared/decorators/index.ts b/server/src/shared/decorators/index.ts new file mode 100644 index 0000000..e976975 --- /dev/null +++ b/server/src/shared/decorators/index.ts @@ -0,0 +1,2 @@ +export { Authorization } from './Auth/authorization.decorator'; +export { Authorized } from './Auth/authorized.decorator'; diff --git a/server/src/modules/auth/guards/jwt.guard.ts b/server/src/shared/guards/JwtGuard/jwt.guard.ts similarity index 100% rename from server/src/modules/auth/guards/jwt.guard.ts rename to server/src/shared/guards/JwtGuard/jwt.guard.ts diff --git a/server/src/shared/guards/index.ts b/server/src/shared/guards/index.ts new file mode 100644 index 0000000..671db55 --- /dev/null +++ b/server/src/shared/guards/index.ts @@ -0,0 +1 @@ +export { JwtGuard } from './JwtGuard/jwt.guard'; diff --git a/server/src/shared/types/index.ts b/server/src/shared/types/index.ts new file mode 100644 index 0000000..b9e3ba6 --- /dev/null +++ b/server/src/shared/types/index.ts @@ -0,0 +1,2 @@ +export type { Request } from './request'; +export type { JwtPayload } from './jwt-payload'; diff --git a/server/src/modules/auth/types/jwt-payload.ts b/server/src/shared/types/jwt-payload.ts similarity index 100% rename from server/src/modules/auth/types/jwt-payload.ts rename to server/src/shared/types/jwt-payload.ts diff --git a/server/src/modules/auth/types/request.ts b/server/src/shared/types/request.ts similarity index 100% rename from server/src/modules/auth/types/request.ts rename to server/src/shared/types/request.ts diff --git a/server/src/shared/validators/IsISODateTimeString/IsISODateTimeString.validator.ts b/server/src/shared/validators/IsISODateTimeString/IsISODateTimeString.validator.ts new file mode 100644 index 0000000..f013976 --- /dev/null +++ b/server/src/shared/validators/IsISODateTimeString/IsISODateTimeString.validator.ts @@ -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'; + }, + }, + }); + }; +} diff --git a/server/src/shared/validators/index.ts b/server/src/shared/validators/index.ts new file mode 100644 index 0000000..e22bf5a --- /dev/null +++ b/server/src/shared/validators/index.ts @@ -0,0 +1 @@ +export { IsISODateTimeString } from './IsISODateTimeString/IsISODateTimeString.validator'; diff --git a/server/tsconfig.json b/server/tsconfig.json index a4f2c6e..cdc0014 100644 --- a/server/tsconfig.json +++ b/server/tsconfig.json @@ -16,7 +16,10 @@ "forceConsistentCasingInFileNames": true, "noImplicitAny": false, "strictBindCallApply": false, - "noFallthroughCasesInSwitch": false + "noFallthroughCasesInSwitch": false, + "paths": { + "@/*": ["./src/*"] + } }, "include": [ "src",