make my shit working

This commit is contained in:
Иван 2025-12-19 20:37:33 +03:00
parent 81a5a829a9
commit 4112645f5e
66 changed files with 13793 additions and 13781 deletions

112
.gitignore vendored
View File

@ -1,56 +1,56 @@
# compiled output # compiled output
/dist /dist
/node_modules /node_modules
/build /build
# Logs # Logs
logs logs
*.log *.log
npm-debug.log* npm-debug.log*
pnpm-debug.log* pnpm-debug.log*
yarn-debug.log* yarn-debug.log*
yarn-error.log* yarn-error.log*
lerna-debug.log* lerna-debug.log*
# OS # OS
.DS_Store .DS_Store
# Tests # Tests
/coverage /coverage
/.nyc_output /.nyc_output
# IDEs and editors # IDEs and editors
/.idea /.idea
.project .project
.classpath .classpath
.c9/ .c9/
*.launch *.launch
.settings/ .settings/
*.sublime-workspace *.sublime-workspace
# IDE - VSCode # IDE - VSCode
.vscode/* .vscode/*
!.vscode/settings.json !.vscode/settings.json
!.vscode/tasks.json !.vscode/tasks.json
!.vscode/launch.json !.vscode/launch.json
!.vscode/extensions.json !.vscode/extensions.json
# dotenv environment variable files # dotenv environment variable files
.env .env
.env.development.local .env.development.local
.env.test.local .env.test.local
.env.production.local .env.production.local
.env.local .env.local
# temp directory # temp directory
.temp .temp
.tmp .tmp
# Runtime data # Runtime data
pids pids
*.pid *.pid
*.seed *.seed
*.pid.lock *.pid.lock
# Diagnostic reports (https://nodejs.org/api/report.html) # Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json

View File

@ -1,16 +1,16 @@
stages: stages:
- deploy - deploy
variables: variables:
COMPOSE_PROJECT_NAME: travelmarine-backend COMPOSE_PROJECT_NAME: travelmarine-backend
DOCKER_HOST: unix:///var/run/docker.sock DOCKER_HOST: unix:///var/run/docker.sock
deploy: deploy:
stage: deploy stage: deploy
only: only:
- main - main
script: script:
- docker compose -p "$COMPOSE_PROJECT_NAME" up -d --build - docker compose -p "$COMPOSE_PROJECT_NAME" up -d --build
environment: environment:
name: production name: production
url: http://89.169.188.2 url: http://89.169.188.2

View File

@ -1,4 +1,4 @@
{ {
"singleQuote": true, "singleQuote": true,
"trailingComma": "all" "trailingComma": "all"
} }

196
README.md
View File

@ -1,98 +1,98 @@
<p align="center"> <p align="center">
<a href="http://nestjs.com/" target="blank"><img src="https://nestjs.com/img/logo-small.svg" width="120" alt="Nest Logo" /></a> <a href="http://nestjs.com/" target="blank"><img src="https://nestjs.com/img/logo-small.svg" width="120" alt="Nest Logo" /></a>
</p> </p>
[circleci-image]: https://img.shields.io/circleci/build/github/nestjs/nest/master?token=abc123def456 [circleci-image]: https://img.shields.io/circleci/build/github/nestjs/nest/master?token=abc123def456
[circleci-url]: https://circleci.com/gh/nestjs/nest [circleci-url]: https://circleci.com/gh/nestjs/nest
<p align="center">A progressive <a href="http://nodejs.org" target="_blank">Node.js</a> framework for building efficient and scalable server-side applications.</p> <p align="center">A progressive <a href="http://nodejs.org" target="_blank">Node.js</a> framework for building efficient and scalable server-side applications.</p>
<p align="center"> <p align="center">
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/v/@nestjs/core.svg" alt="NPM Version" /></a> <a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/v/@nestjs/core.svg" alt="NPM Version" /></a>
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/l/@nestjs/core.svg" alt="Package License" /></a> <a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/l/@nestjs/core.svg" alt="Package License" /></a>
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/dm/@nestjs/common.svg" alt="NPM Downloads" /></a> <a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/dm/@nestjs/common.svg" alt="NPM Downloads" /></a>
<a href="https://circleci.com/gh/nestjs/nest" target="_blank"><img src="https://img.shields.io/circleci/build/github/nestjs/nest/master" alt="CircleCI" /></a> <a href="https://circleci.com/gh/nestjs/nest" target="_blank"><img src="https://img.shields.io/circleci/build/github/nestjs/nest/master" alt="CircleCI" /></a>
<a href="https://discord.gg/G7Qnnhy" target="_blank"><img src="https://img.shields.io/badge/discord-online-brightgreen.svg" alt="Discord"/></a> <a href="https://discord.gg/G7Qnnhy" target="_blank"><img src="https://img.shields.io/badge/discord-online-brightgreen.svg" alt="Discord"/></a>
<a href="https://opencollective.com/nest#backer" target="_blank"><img src="https://opencollective.com/nest/backers/badge.svg" alt="Backers on Open Collective" /></a> <a href="https://opencollective.com/nest#backer" target="_blank"><img src="https://opencollective.com/nest/backers/badge.svg" alt="Backers on Open Collective" /></a>
<a href="https://opencollective.com/nest#sponsor" target="_blank"><img src="https://opencollective.com/nest/sponsors/badge.svg" alt="Sponsors on Open Collective" /></a> <a href="https://opencollective.com/nest#sponsor" target="_blank"><img src="https://opencollective.com/nest/sponsors/badge.svg" alt="Sponsors on Open Collective" /></a>
<a href="https://paypal.me/kamilmysliwiec" target="_blank"><img src="https://img.shields.io/badge/Donate-PayPal-ff3f59.svg" alt="Donate us"/></a> <a href="https://paypal.me/kamilmysliwiec" target="_blank"><img src="https://img.shields.io/badge/Donate-PayPal-ff3f59.svg" alt="Donate us"/></a>
<a href="https://opencollective.com/nest#sponsor" target="_blank"><img src="https://img.shields.io/badge/Support%20us-Open%20Collective-41B883.svg" alt="Support us"></a> <a href="https://opencollective.com/nest#sponsor" target="_blank"><img src="https://img.shields.io/badge/Support%20us-Open%20Collective-41B883.svg" alt="Support us"></a>
<a href="https://twitter.com/nestframework" target="_blank"><img src="https://img.shields.io/twitter/follow/nestframework.svg?style=social&label=Follow" alt="Follow us on Twitter"></a> <a href="https://twitter.com/nestframework" target="_blank"><img src="https://img.shields.io/twitter/follow/nestframework.svg?style=social&label=Follow" alt="Follow us on Twitter"></a>
</p> </p>
<!--[![Backers on Open Collective](https://opencollective.com/nest/backers/badge.svg)](https://opencollective.com/nest#backer) <!--[![Backers on Open Collective](https://opencollective.com/nest/backers/badge.svg)](https://opencollective.com/nest#backer)
[![Sponsors on Open Collective](https://opencollective.com/nest/sponsors/badge.svg)](https://opencollective.com/nest#sponsor)--> [![Sponsors on Open Collective](https://opencollective.com/nest/sponsors/badge.svg)](https://opencollective.com/nest#sponsor)-->
## Description ## Description
[Nest](https://github.com/nestjs/nest) framework TypeScript starter repository. [Nest](https://github.com/nestjs/nest) framework TypeScript starter repository.
## Project setup ## Project setup
```bash ```bash
$ npm install $ npm install
``` ```
## Compile and run the project ## Compile and run the project
```bash ```bash
# development # development
$ npm run start $ npm run start
# watch mode # watch mode
$ npm run start:dev $ npm run start:dev
# production mode # production mode
$ npm run start:prod $ npm run start:prod
``` ```
## Run tests ## Run tests
```bash ```bash
# unit tests # unit tests
$ npm run test $ npm run test
# e2e tests # e2e tests
$ npm run test:e2e $ npm run test:e2e
# test coverage # test coverage
$ npm run test:cov $ npm run test:cov
``` ```
## Deployment ## Deployment
When you're ready to deploy your NestJS application to production, there are some key steps you can take to ensure it runs as efficiently as possible. Check out the [deployment documentation](https://docs.nestjs.com/deployment) for more information. When you're ready to deploy your NestJS application to production, there are some key steps you can take to ensure it runs as efficiently as possible. Check out the [deployment documentation](https://docs.nestjs.com/deployment) for more information.
If you are looking for a cloud-based platform to deploy your NestJS application, check out [Mau](https://mau.nestjs.com), our official platform for deploying NestJS applications on AWS. Mau makes deployment straightforward and fast, requiring just a few simple steps: If you are looking for a cloud-based platform to deploy your NestJS application, check out [Mau](https://mau.nestjs.com), our official platform for deploying NestJS applications on AWS. Mau makes deployment straightforward and fast, requiring just a few simple steps:
```bash ```bash
$ npm install -g @nestjs/mau $ npm install -g @nestjs/mau
$ mau deploy $ mau deploy
``` ```
With Mau, you can deploy your application in just a few clicks, allowing you to focus on building features rather than managing infrastructure. With Mau, you can deploy your application in just a few clicks, allowing you to focus on building features rather than managing infrastructure.
## Resources ## Resources
Check out a few resources that may come in handy when working with NestJS: Check out a few resources that may come in handy when working with NestJS:
- Visit the [NestJS Documentation](https://docs.nestjs.com) to learn more about the framework. - Visit the [NestJS Documentation](https://docs.nestjs.com) to learn more about the framework.
- For questions and support, please visit our [Discord channel](https://discord.gg/G7Qnnhy). - For questions and support, please visit our [Discord channel](https://discord.gg/G7Qnnhy).
- To dive deeper and get more hands-on experience, check out our official video [courses](https://courses.nestjs.com/). - To dive deeper and get more hands-on experience, check out our official video [courses](https://courses.nestjs.com/).
- Deploy your application to AWS with the help of [NestJS Mau](https://mau.nestjs.com) in just a few clicks. - Deploy your application to AWS with the help of [NestJS Mau](https://mau.nestjs.com) in just a few clicks.
- Visualize your application graph and interact with the NestJS application in real-time using [NestJS Devtools](https://devtools.nestjs.com). - Visualize your application graph and interact with the NestJS application in real-time using [NestJS Devtools](https://devtools.nestjs.com).
- Need help with your project (part-time to full-time)? Check out our official [enterprise support](https://enterprise.nestjs.com). - Need help with your project (part-time to full-time)? Check out our official [enterprise support](https://enterprise.nestjs.com).
- To stay in the loop and get updates, follow us on [X](https://x.com/nestframework) and [LinkedIn](https://linkedin.com/company/nestjs). - To stay in the loop and get updates, follow us on [X](https://x.com/nestframework) and [LinkedIn](https://linkedin.com/company/nestjs).
- Looking for a job, or have a job to offer? Check out our official [Jobs board](https://jobs.nestjs.com). - Looking for a job, or have a job to offer? Check out our official [Jobs board](https://jobs.nestjs.com).
## Support ## Support
Nest is an MIT-licensed open source project. It can grow thanks to the sponsors and support by the amazing backers. If you'd like to join them, please [read more here](https://docs.nestjs.com/support). Nest is an MIT-licensed open source project. It can grow thanks to the sponsors and support by the amazing backers. If you'd like to join them, please [read more here](https://docs.nestjs.com/support).
## Stay in touch ## Stay in touch
- Author - [Kamil Myśliwiec](https://twitter.com/kammysliwiec) - Author - [Kamil Myśliwiec](https://twitter.com/kammysliwiec)
- Website - [https://nestjs.com](https://nestjs.com/) - Website - [https://nestjs.com](https://nestjs.com/)
- Twitter - [@nestframework](https://twitter.com/nestframework) - Twitter - [@nestframework](https://twitter.com/nestframework)
## License ## License
Nest is [MIT licensed](https://github.com/nestjs/nest/blob/master/LICENSE). Nest is [MIT licensed](https://github.com/nestjs/nest/blob/master/LICENSE).

View File

@ -1,35 +1,35 @@
// @ts-check // @ts-check
import eslint from '@eslint/js'; import eslint from '@eslint/js';
import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended'; import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended';
import globals from 'globals'; import globals from 'globals';
import tseslint from 'typescript-eslint'; import tseslint from 'typescript-eslint';
export default tseslint.config( export default tseslint.config(
{ {
ignores: ['eslint.config.mjs'], ignores: ['eslint.config.mjs'],
}, },
eslint.configs.recommended, eslint.configs.recommended,
...tseslint.configs.recommendedTypeChecked, ...tseslint.configs.recommendedTypeChecked,
eslintPluginPrettierRecommended, eslintPluginPrettierRecommended,
{ {
languageOptions: { languageOptions: {
globals: { globals: {
...globals.node, ...globals.node,
...globals.jest, ...globals.jest,
}, },
sourceType: 'commonjs', sourceType: 'commonjs',
parserOptions: { parserOptions: {
projectService: true, projectService: true,
tsconfigRootDir: import.meta.dirname, tsconfigRootDir: import.meta.dirname,
}, },
}, },
}, },
{ {
rules: { rules: {
'@typescript-eslint/no-explicit-any': 'off', '@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-floating-promises': 'warn', '@typescript-eslint/no-floating-promises': 'warn',
'@typescript-eslint/no-unsafe-argument': 'warn', '@typescript-eslint/no-unsafe-argument': 'warn',
"prettier/prettier": ["error", { endOfLine: "auto" }], "prettier/prettier": ["error", { endOfLine: "auto" }],
}, },
}, },
); );

View File

@ -1,8 +1,8 @@
{ {
"$schema": "https://json.schemastore.org/nest-cli", "$schema": "https://json.schemastore.org/nest-cli",
"collection": "@nestjs/schematics", "collection": "@nestjs/schematics",
"sourceRoot": "src", "sourceRoot": "src",
"compilerOptions": { "compilerOptions": {
"deleteOutDir": true "deleteOutDir": true
} }
} }

22592
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,86 +1,86 @@
{ {
"name": "nest-app", "name": "nest-app",
"version": "0.0.1", "version": "0.0.1",
"description": "", "description": "",
"author": "", "author": "",
"private": true, "private": true,
"license": "UNLICENSED", "license": "UNLICENSED",
"scripts": { "scripts": {
"build": "nest build", "build": "nest build",
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
"start": "nest start", "start": "nest start",
"start:dev": "nest start --watch", "start:dev": "nest start --watch",
"start:debug": "nest start --debug --watch", "start:debug": "nest start --debug --watch",
"start:prod": "node dist/main", "start:prod": "node dist/main",
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
"test": "jest", "test": "jest",
"test:watch": "jest --watch", "test:watch": "jest --watch",
"test:cov": "jest --coverage", "test:cov": "jest --coverage",
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
"test:e2e": "jest --config ./test/jest-e2e.json" "test:e2e": "jest --config ./test/jest-e2e.json"
}, },
"dependencies": { "dependencies": {
"@nestjs/common": "^11.0.1", "@nestjs/common": "^11.0.1",
"@nestjs/config": "^4.0.2", "@nestjs/config": "^4.0.2",
"@nestjs/core": "^11.0.1", "@nestjs/core": "^11.0.1",
"@nestjs/jwt": "^11.0.2", "@nestjs/jwt": "^11.0.2",
"@nestjs/mapped-types": "^2.1.0", "@nestjs/mapped-types": "^2.1.0",
"@nestjs/passport": "^11.0.5", "@nestjs/passport": "^11.0.5",
"@nestjs/platform-express": "^11.1.9", "@nestjs/platform-express": "^11.1.9",
"@nestjs/swagger": "^11.2.3", "@nestjs/swagger": "^11.2.3",
"@types/multer": "^2.0.0", "@types/multer": "^2.0.0",
"class-transformer": "^0.5.1", "class-transformer": "^0.5.1",
"class-validator": "^0.14.3", "class-validator": "^0.14.3",
"multer": "^2.0.2", "multer": "^2.0.2",
"passport": "^0.7.0", "passport": "^0.7.0",
"passport-jwt": "^4.0.1", "passport-jwt": "^4.0.1",
"passport-local": "^1.0.0", "passport-local": "^1.0.0",
"reflect-metadata": "^0.2.2", "reflect-metadata": "^0.2.2",
"rxjs": "^7.8.1", "rxjs": "^7.8.1",
"sharp": "^0.34.5" "sharp": "^0.34.5"
}, },
"devDependencies": { "devDependencies": {
"@eslint/eslintrc": "^3.2.0", "@eslint/eslintrc": "^3.2.0",
"@eslint/js": "^9.18.0", "@eslint/js": "^9.18.0",
"@nestjs/cli": "^11.0.0", "@nestjs/cli": "^11.0.0",
"@nestjs/schematics": "^11.0.0", "@nestjs/schematics": "^11.0.0",
"@nestjs/testing": "^11.0.1", "@nestjs/testing": "^11.0.1",
"@types/express": "^5.0.0", "@types/express": "^5.0.0",
"@types/jest": "^30.0.0", "@types/jest": "^30.0.0",
"@types/node": "^22.10.7", "@types/node": "^22.10.7",
"@types/passport-jwt": "^4.0.1", "@types/passport-jwt": "^4.0.1",
"@types/passport-local": "^1.0.38", "@types/passport-local": "^1.0.38",
"@types/supertest": "^6.0.2", "@types/supertest": "^6.0.2",
"eslint": "^9.18.0", "eslint": "^9.18.0",
"eslint-config-prettier": "^10.0.1", "eslint-config-prettier": "^10.0.1",
"eslint-plugin-prettier": "^5.2.2", "eslint-plugin-prettier": "^5.2.2",
"globals": "^16.0.0", "globals": "^16.0.0",
"jest": "^30.0.0", "jest": "^30.0.0",
"prettier": "^3.4.2", "prettier": "^3.4.2",
"source-map-support": "^0.5.21", "source-map-support": "^0.5.21",
"supertest": "^7.0.0", "supertest": "^7.0.0",
"ts-jest": "^29.2.5", "ts-jest": "^29.2.5",
"ts-loader": "^9.5.2", "ts-loader": "^9.5.2",
"ts-node": "^10.9.2", "ts-node": "^10.9.2",
"tsconfig-paths": "^4.2.0", "tsconfig-paths": "^4.2.0",
"typescript": "^5.7.3", "typescript": "^5.7.3",
"typescript-eslint": "^8.20.0" "typescript-eslint": "^8.20.0"
}, },
"jest": { "jest": {
"moduleFileExtensions": [ "moduleFileExtensions": [
"js", "js",
"json", "json",
"ts" "ts"
], ],
"rootDir": "src", "rootDir": "src",
"testRegex": ".*\\.spec\\.ts$", "testRegex": ".*\\.spec\\.ts$",
"transform": { "transform": {
"^.+\\.(t|j)s$": "ts-jest" "^.+\\.(t|j)s$": "ts-jest"
}, },
"collectCoverageFrom": [ "collectCoverageFrom": [
"**/*.(t|j)s" "**/*.(t|j)s"
], ],
"coverageDirectory": "../coverage", "coverageDirectory": "../coverage",
"testEnvironment": "node" "testEnvironment": "node"
} }
} }

View File

@ -1,30 +1,30 @@
import { Controller, Request, Post, UseGuards } from '@nestjs/common'; import { Controller, Request, Post, UseGuards } from '@nestjs/common';
import { ApiBody } from '@nestjs/swagger'; import { ApiBody } from '@nestjs/swagger';
import { LocalAuthGuard } from './auth/local-auth.guard'; import { LocalAuthGuard } from './auth/local-auth.guard';
import { AuthService } from './auth/auth.service'; import { AuthService } from './auth/auth.service';
@Controller() @Controller()
export class AppController { export class AppController {
constructor(private authService: AuthService) {} constructor(private authService: AuthService) {}
@UseGuards(LocalAuthGuard) @UseGuards(LocalAuthGuard)
@Post('auth/login') @Post('auth/login')
@ApiBody({ @ApiBody({
schema: { schema: {
type: 'object', type: 'object',
properties: { properties: {
email: { type: 'string', example: 'emai1@email.com' }, email: { type: 'string', example: 'emai1@email.com' },
password: { type: 'string', example: 'admin' }, password: { type: 'string', example: 'admin' },
}, },
}, },
}) })
async login(@Request() req) { async login(@Request() req) {
return this.authService.login(req.user); return this.authService.login(req.user);
} }
@UseGuards(LocalAuthGuard) @UseGuards(LocalAuthGuard)
@Post('auth/logout') @Post('auth/logout')
async logout(@Request() req) { async logout(@Request() req) {
return req.logout(); return req.logout();
} }
} }

View File

@ -1,17 +1,17 @@
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { AppController } from './app.controller'; import { AppController } from './app.controller';
import { AppService } from './app.service'; import { AppService } from './app.service';
import { YachtsModule } from './yachts/yachts.module'; import { YachtsModule } from './yachts/yachts.module';
import { CatalogModule } from './catalog/catalog.module'; import { CatalogModule } from './catalog/catalog.module';
import { AuthModule } from './auth/auth.module'; import { AuthModule } from './auth/auth.module';
import { UsersModule } from './users/users.module'; import { UsersModule } from './users/users.module';
import { FilesModule } from './files/files.module'; import { FilesModule } from './files/files.module';
import { ReviewsModule } from './reviews/reviews.module'; import { ReviewsModule } from './reviews/reviews.module';
import { ReservationsModule } from './reservations/reservations.module'; import { ReservationsModule } from './reservations/reservations.module';
@Module({ @Module({
imports: [YachtsModule, CatalogModule, AuthModule, UsersModule, FilesModule, ReviewsModule, ReservationsModule], imports: [YachtsModule, CatalogModule, AuthModule, UsersModule, FilesModule, ReviewsModule, ReservationsModule],
controllers: [AppController], controllers: [AppController],
providers: [AppService], providers: [AppService],
}) })
export class AppModule {} export class AppModule {}

View File

@ -1,8 +1,8 @@
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
@Injectable() @Injectable()
export class AppService { export class AppService {
getHello(): string { getHello(): string {
return 'Hello World!'; return 'Hello World!';
} }
} }

View File

@ -1,21 +1,21 @@
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { AuthService } from './auth.service'; import { AuthService } from './auth.service';
import { LocalStrategy } from './local.strategy'; import { LocalStrategy } from './local.strategy';
import { UsersModule } from '../users/users.module'; import { UsersModule } from '../users/users.module';
import { PassportModule } from '@nestjs/passport'; import { PassportModule } from '@nestjs/passport';
import { JwtModule } from '@nestjs/jwt'; import { JwtModule } from '@nestjs/jwt';
import { jwtConstants } from './constants'; import { jwtConstants } from './constants';
@Module({ @Module({
imports: [ imports: [
UsersModule, UsersModule,
PassportModule, PassportModule,
JwtModule.register({ JwtModule.register({
secret: jwtConstants.secret, secret: jwtConstants.secret,
signOptions: { expiresIn: '90d' }, signOptions: { expiresIn: '90d' },
}), }),
], ],
providers: [AuthService, LocalStrategy], providers: [AuthService, LocalStrategy],
exports: [AuthService], exports: [AuthService],
}) })
export class AuthModule {} export class AuthModule {}

View File

@ -1,38 +1,38 @@
import { Injectable, Logger } from '@nestjs/common'; import { Injectable, Logger } from '@nestjs/common';
import { UsersService } from '../users/users.service'; import { UsersService } from '../users/users.service';
import { User } from 'src/users/user.entity'; import { User } from 'src/users/user.entity';
import { JwtService } from '@nestjs/jwt'; import { JwtService } from '@nestjs/jwt';
@Injectable() @Injectable()
export class AuthService { export class AuthService {
private readonly logger = new Logger(AuthService.name); private readonly logger = new Logger(AuthService.name);
constructor( constructor(
private usersService: UsersService, private usersService: UsersService,
private jwtService: JwtService, private jwtService: JwtService,
) {} ) {}
async validateUser( async validateUser(
email: string, email: string,
pass: string, pass: string,
): Promise<Omit<User, 'password'> | null> { ): Promise<Omit<User, 'password'> | null> {
const user = await this.usersService.findOne(email); const user = await this.usersService.findOne(email);
if (user && user.password === pass) { if (user && user.password === pass) {
const { password, ...result } = user; const { password, ...result } = user;
return result; return result;
} }
return null; return null;
} }
login(user: Omit<User, 'password'>) { login(user: Omit<User, 'password'>) {
this.logger.log('LOG'); this.logger.log('LOG');
const payload = { const payload = {
username: user.email, username: user.email,
sub: user.userId, sub: user.userId,
userId: user.userId, userId: user.userId,
}; };
return { return {
access_token: this.jwtService.sign(payload), access_token: this.jwtService.sign(payload),
}; };
} }
} }

View File

@ -1,3 +1,3 @@
export const jwtConstants = { export const jwtConstants = {
secret: '6by876hiuGHiugiuG8t78t87tGUYUYg8u7g87', secret: '6by876hiuGHiugiuG8t78t87tGUYUYg8u7g87',
}; };

View File

@ -1,5 +1,5 @@
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport'; import { AuthGuard } from '@nestjs/passport';
@Injectable() @Injectable()
export class LocalAuthGuard extends AuthGuard('local') {} export class LocalAuthGuard extends AuthGuard('local') {}

View File

@ -1,26 +1,26 @@
import { Strategy } from 'passport-local'; import { Strategy } from 'passport-local';
import { PassportStrategy } from '@nestjs/passport'; import { PassportStrategy } from '@nestjs/passport';
import { Injectable, UnauthorizedException } from '@nestjs/common'; import { Injectable, UnauthorizedException } from '@nestjs/common';
import { AuthService } from './auth.service'; import { AuthService } from './auth.service';
import type { User } from 'src/users/user.entity'; import type { User } from 'src/users/user.entity';
@Injectable() @Injectable()
export class LocalStrategy extends PassportStrategy(Strategy) { export class LocalStrategy extends PassportStrategy(Strategy) {
constructor(private authService: AuthService) { constructor(private authService: AuthService) {
super({ super({
usernameField: 'email', usernameField: 'email',
}); });
} }
async validate( async validate(
email: string, email: string,
password: string, password: string,
): Promise<Omit<User, 'password'>> { ): Promise<Omit<User, 'password'>> {
const user = await this.authService.validateUser(email, password); const user = await this.authService.validateUser(email, password);
if (!user) { if (!user) {
throw new UnauthorizedException(); throw new UnauthorizedException();
} }
return user; return user;
} }
} }

View File

@ -1,113 +1,113 @@
import { Controller, Get, Query, Param, Post, Body, HttpCode, HttpStatus } from '@nestjs/common'; import { Controller, Get, Query, Param, Post, Body, HttpCode, HttpStatus } from '@nestjs/common';
import { CatalogService } from './catalog.service'; import { CatalogService } from './catalog.service';
import { CatalogResponseDto } from './dto/catalog-response.dto'; import { CatalogResponseDto } from './dto/catalog-response.dto';
import { CatalogFiltersDto } from './dto/catalog-filters.dto'; import { CatalogFiltersDto } from './dto/catalog-filters.dto';
import { import {
CatalogItemShortDto, CatalogItemShortDto,
CatalogItemLongDto, CatalogItemLongDto,
} from './dto/catalog-item.dto'; } from './dto/catalog-item.dto';
import { import {
ApiQuery, ApiQuery,
ApiResponse, ApiResponse,
ApiOperation, ApiOperation,
ApiProperty, ApiProperty,
ApiBody, ApiBody,
ApiTags, ApiTags,
} from '@nestjs/swagger'; } from '@nestjs/swagger';
import { MainPageCatalogResponseDto } from './dto/main-page-catalog-response.dto'; import { MainPageCatalogResponseDto } from './dto/main-page-catalog-response.dto';
import { CreateYachtDto } from './dto/create-yacht.dto'; import { CreateYachtDto } from './dto/create-yacht.dto';
@ApiTags('catalog') @ApiTags('catalog')
@Controller('catalog') @Controller('catalog')
export class CatalogController { export class CatalogController {
constructor(private readonly catalogService: CatalogService) {} constructor(private readonly catalogService: CatalogService) {}
@Get('filter') @Get('filter')
@ApiOperation({ summary: 'Filter catalog items with query parameters' }) @ApiOperation({ summary: 'Filter catalog items with query parameters' })
@ApiResponse({ @ApiResponse({
status: 200, status: 200,
description: 'Filtered catalog items', description: 'Filtered catalog items',
type: CatalogResponseDto, type: CatalogResponseDto,
}) })
@ApiQuery({ name: 'search', required: false, type: String }) @ApiQuery({ name: 'search', required: false, type: String })
@ApiQuery({ name: 'minLength', required: false, type: Number }) @ApiQuery({ name: 'minLength', required: false, type: Number })
@ApiQuery({ name: 'maxLength', required: false, type: Number }) @ApiQuery({ name: 'maxLength', required: false, type: Number })
@ApiQuery({ name: 'minPrice', required: false, type: Number }) @ApiQuery({ name: 'minPrice', required: false, type: Number })
@ApiQuery({ name: 'maxPrice', required: false, type: Number }) @ApiQuery({ name: 'maxPrice', required: false, type: Number })
@ApiQuery({ name: 'minYear', required: false, type: Number }) @ApiQuery({ name: 'minYear', required: false, type: Number })
@ApiQuery({ name: 'maxYear', required: false, type: Number }) @ApiQuery({ name: 'maxYear', required: false, type: Number })
@ApiQuery({ name: 'guests', required: false, type: Number }) @ApiQuery({ name: 'guests', required: false, type: Number })
@ApiQuery({ name: 'sortByPrice', required: false, type: String }) @ApiQuery({ name: 'sortByPrice', required: false, type: String })
@ApiQuery({ name: 'paymentType', required: false, type: String }) @ApiQuery({ name: 'paymentType', required: false, type: String })
@ApiQuery({ name: 'quickBooking', required: false, type: Boolean }) @ApiQuery({ name: 'quickBooking', required: false, type: Boolean })
@ApiQuery({ name: 'hasToilet', required: false, type: Boolean }) @ApiQuery({ name: 'hasToilet', required: false, type: Boolean })
@ApiQuery({ name: 'date', required: false, type: Date }) @ApiQuery({ name: 'date', required: false, type: Date })
@ApiQuery({ name: 'departureTime', required: false, type: String }) @ApiQuery({ name: 'departureTime', required: false, type: String })
@ApiQuery({ name: 'arrivalTime', required: false, type: String }) @ApiQuery({ name: 'arrivalTime', required: false, type: String })
async getFilteredCatalog( async getFilteredCatalog(
@Query() filters: CatalogFiltersDto, @Query() filters: CatalogFiltersDto,
): Promise<CatalogResponseDto> { ): Promise<CatalogResponseDto> {
return this.catalogService.getCatalog(filters); return this.catalogService.getCatalog(filters);
} }
@Get() @Get()
@ApiOperation({ summary: 'Get all catalog items (deprecated, use /filter)' }) @ApiOperation({ summary: 'Get all catalog items (deprecated, use /filter)' })
@ApiResponse({ @ApiResponse({
status: 200, status: 200,
description: 'All catalog items', description: 'All catalog items',
type: [CatalogItemShortDto], type: [CatalogItemShortDto],
}) })
async getAllCatalogItems(): Promise<CatalogItemShortDto[]> { async getAllCatalogItems(): Promise<CatalogItemShortDto[]> {
return this.catalogService.getAllCatalogItems(); return this.catalogService.getAllCatalogItems();
} }
@Get('main-page') @Get('main-page')
@ApiOperation({ summary: 'Get catalog for main page' }) @ApiOperation({ summary: 'Get catalog for main page' })
@ApiProperty({ type: MainPageCatalogResponseDto }) @ApiProperty({ type: MainPageCatalogResponseDto })
async getMainPageCatalog(): Promise<MainPageCatalogResponseDto | null> { async getMainPageCatalog(): Promise<MainPageCatalogResponseDto | null> {
return this.catalogService.getMainPageCatalog(); return this.catalogService.getMainPageCatalog();
} }
@Get('user/:userId') @Get('user/:userId')
@ApiOperation({ summary: 'Get catalog items by user ID' }) @ApiOperation({ summary: 'Get catalog items by user ID' })
@ApiResponse({ @ApiResponse({
status: 200, status: 200,
description: 'Catalog items for the specified user', description: 'Catalog items for the specified user',
type: [CatalogItemShortDto], type: [CatalogItemShortDto],
}) })
async getCatalogByUserId( async getCatalogByUserId(
@Param('userId') userId: string, @Param('userId') userId: string,
): Promise<CatalogItemShortDto[]> { ): Promise<CatalogItemShortDto[]> {
return this.catalogService.getCatalogByUserId(Number(userId)); return this.catalogService.getCatalogByUserId(Number(userId));
} }
@Get(':id') @Get(':id')
@ApiOperation({ summary: 'Get catalog item by ID with full details' }) @ApiOperation({ summary: 'Get catalog item by ID with full details' })
@ApiResponse({ @ApiResponse({
status: 200, status: 200,
description: 'Catalog item with reviews, reservations and owner', description: 'Catalog item with reviews, reservations and owner',
type: CatalogItemLongDto, type: CatalogItemLongDto,
}) })
async getCatalogItemById( async getCatalogItemById(
@Param('id') id: string, @Param('id') id: string,
): Promise<CatalogItemLongDto | null> { ): Promise<CatalogItemLongDto | null> {
return this.catalogService.getCatalogItemById(Number(id)); return this.catalogService.getCatalogItemById(Number(id));
} }
@Post() @Post()
@HttpCode(HttpStatus.CREATED) @HttpCode(HttpStatus.CREATED)
@ApiOperation({ summary: 'Create a new yacht in catalog' }) @ApiOperation({ summary: 'Create a new yacht in catalog' })
@ApiBody({ type: CreateYachtDto }) @ApiBody({ type: CreateYachtDto })
@ApiResponse({ @ApiResponse({
status: 201, status: 201,
description: 'Yacht successfully created', description: 'Yacht successfully created',
type: CatalogItemLongDto, type: CatalogItemLongDto,
}) })
@ApiResponse({ @ApiResponse({
status: 400, status: 400,
description: 'Bad request', description: 'Bad request',
}) })
async createYacht(@Body() createYachtDto: CreateYachtDto): Promise<CatalogItemLongDto> { async createYacht(@Body() createYachtDto: CreateYachtDto): Promise<CatalogItemLongDto> {
return this.catalogService.createYacht(createYachtDto); return this.catalogService.createYacht(createYachtDto);
} }
} }

View File

@ -1,18 +1,18 @@
import { Module, forwardRef } from '@nestjs/common'; import { Module, forwardRef } from '@nestjs/common';
import { CatalogService } from './catalog.service'; import { CatalogService } from './catalog.service';
import { CatalogController } from './catalog.controller'; import { CatalogController } from './catalog.controller';
import { UsersModule } from '../users/users.module'; import { UsersModule } from '../users/users.module';
import { ReservationsModule } from '../reservations/reservations.module'; import { ReservationsModule } from '../reservations/reservations.module';
import { ReviewsModule } from '../reviews/reviews.module'; import { ReviewsModule } from '../reviews/reviews.module';
@Module({ @Module({
imports: [ imports: [
UsersModule, // This provides UsersService UsersModule, // This provides UsersService
forwardRef(() => ReservationsModule), // This provides ReservationsService forwardRef(() => ReservationsModule), // This provides ReservationsService
ReviewsModule, // This provides ReviewsService ReviewsModule, // This provides ReviewsService
], ],
controllers: [CatalogController], controllers: [CatalogController],
providers: [CatalogService], providers: [CatalogService],
exports: [CatalogService], // Export for other modules to use exports: [CatalogService], // Export for other modules to use
}) })
export class CatalogModule {} export class CatalogModule {}

File diff suppressed because it is too large Load Diff

View File

@ -1,146 +1,146 @@
import { ApiProperty } from '@nestjs/swagger'; import { ApiProperty } from '@nestjs/swagger';
import { Type } from 'class-transformer'; import { Type } from 'class-transformer';
import { import {
IsOptional, IsOptional,
IsString, IsString,
IsNumber, IsNumber,
IsBoolean, IsBoolean,
IsDate, IsDate,
Min, Min,
} from 'class-validator'; } from 'class-validator';
export class CatalogFiltersDto { export class CatalogFiltersDto {
@ApiProperty({ required: false, description: 'Search by yacht name' }) @ApiProperty({ required: false, description: 'Search by yacht name' })
@IsOptional() @IsOptional()
@IsString() @IsString()
search?: string; search?: string;
@ApiProperty({ @ApiProperty({
required: false, required: false,
description: 'Minimum length in meters', description: 'Minimum length in meters',
example: 10, example: 10,
}) })
@IsOptional() @IsOptional()
@IsNumber() @IsNumber()
@Min(0) @Min(0)
@Type(() => Number) @Type(() => Number)
minLength?: number; minLength?: number;
@ApiProperty({ @ApiProperty({
required: false, required: false,
description: 'Maximum length in meters', description: 'Maximum length in meters',
example: 20, example: 20,
}) })
@IsOptional() @IsOptional()
@IsNumber() @IsNumber()
@Min(0) @Min(0)
@Type(() => Number) @Type(() => Number)
maxLength?: number; maxLength?: number;
@ApiProperty({ @ApiProperty({
required: false, required: false,
description: 'Minimum price', description: 'Minimum price',
example: 30000, example: 30000,
}) })
@IsOptional() @IsOptional()
@IsNumber() @IsNumber()
@Min(0) @Min(0)
@Type(() => Number) @Type(() => Number)
minPrice?: number; minPrice?: number;
@ApiProperty({ @ApiProperty({
required: false, required: false,
description: 'Maximum price', description: 'Maximum price',
example: 100000, example: 100000,
}) })
@IsOptional() @IsOptional()
@IsNumber() @IsNumber()
@Min(0) @Min(0)
@Type(() => Number) @Type(() => Number)
maxPrice?: number; maxPrice?: number;
@ApiProperty({ @ApiProperty({
required: false, required: false,
description: 'Sort by price', description: 'Sort by price',
example: 100000, example: 100000,
}) })
@IsOptional() @IsOptional()
@IsNumber() @IsNumber()
@Min(0) @Min(0)
@Type(() => Number) @Type(() => Number)
sortByPrice?: string; sortByPrice?: string;
@ApiProperty({ required: false, description: 'Minimum year', example: 2019 }) @ApiProperty({ required: false, description: 'Minimum year', example: 2019 })
@IsOptional() @IsOptional()
@IsNumber() @IsNumber()
@Min(1900) @Min(1900)
@Type(() => Number) @Type(() => Number)
minYear?: number; minYear?: number;
@ApiProperty({ required: false, description: 'Maximum year', example: 2023 }) @ApiProperty({ required: false, description: 'Maximum year', example: 2023 })
@IsOptional() @IsOptional()
@IsNumber() @IsNumber()
@Min(1900) @Min(1900)
@Type(() => Number) @Type(() => Number)
maxYear?: number; maxYear?: number;
@ApiProperty({ required: false, description: 'Number of guests', example: 4 }) @ApiProperty({ required: false, description: 'Number of guests', example: 4 })
@IsOptional() @IsOptional()
@IsNumber() @IsNumber()
@Min(1) @Min(1)
@Type(() => Number) @Type(() => Number)
guests?: number; guests?: number;
@ApiProperty({ @ApiProperty({
required: false, required: false,
description: 'Payment type', description: 'Payment type',
example: 'card', example: 'card',
}) })
@IsOptional() @IsOptional()
@IsString() @IsString()
paymentType?: string; paymentType?: string;
@ApiProperty({ @ApiProperty({
required: false, required: false,
description: 'Quick booking available', description: 'Quick booking available',
example: true, example: true,
}) })
@IsOptional() @IsOptional()
@IsBoolean() @IsBoolean()
@Type(() => Boolean) @Type(() => Boolean)
quickBooking?: boolean; quickBooking?: boolean;
@ApiProperty({ required: false, description: 'Has toilet', example: true }) @ApiProperty({ required: false, description: 'Has toilet', example: true })
@IsOptional() @IsOptional()
@IsBoolean() @IsBoolean()
@Type(() => Boolean) @Type(() => Boolean)
hasToilet?: boolean; hasToilet?: boolean;
@ApiProperty({ @ApiProperty({
required: false, required: false,
description: 'Departure date', description: 'Departure date',
example: '2025-12-20', example: '2025-12-20',
}) })
@IsOptional() @IsOptional()
@IsDate() @IsDate()
@Type(() => Date) @Type(() => Date)
date?: Date; date?: Date;
@ApiProperty({ @ApiProperty({
required: false, required: false,
description: 'Departure time', description: 'Departure time',
example: '08:00', example: '08:00',
}) })
@IsOptional() @IsOptional()
@IsString() @IsString()
departureTime?: string; departureTime?: string;
@ApiProperty({ @ApiProperty({
required: false, required: false,
description: 'Arrival time', description: 'Arrival time',
example: '20:00', example: '20:00',
}) })
@IsOptional() @IsOptional()
@IsString() @IsString()
arrivalTime?: string; arrivalTime?: string;
} }

View File

@ -1,133 +1,133 @@
import { ApiProperty } from '@nestjs/swagger'; import { ApiProperty } from '@nestjs/swagger';
export class UserDto { export class UserDto {
@ApiProperty({ example: 1 }) @ApiProperty({ example: 1 })
userId: number; userId: number;
@ApiProperty({ example: 'Иван' }) @ApiProperty({ example: 'Иван' })
firstName: string; firstName: string;
@ApiProperty({ example: 'Андреев' }) @ApiProperty({ example: 'Андреев' })
lastName: string; lastName: string;
@ApiProperty({ example: '+79009009090' }) @ApiProperty({ example: '+79009009090' })
phone: string; phone: string;
@ApiProperty({ example: 'ivan@yachting.ru' }) @ApiProperty({ example: 'ivan@yachting.ru' })
email: string; email: string;
@ApiProperty({ example: 'Северный Флот', required: false }) @ApiProperty({ example: 'Северный Флот', required: false })
companyName?: string; companyName?: string;
@ApiProperty({ example: 1234567890, required: false }) @ApiProperty({ example: 1234567890, required: false })
inn?: number; inn?: number;
@ApiProperty({ example: 1122334455667, required: false }) @ApiProperty({ example: 1122334455667, required: false })
ogrn?: number; ogrn?: number;
} }
export class ReviewDto { export class ReviewDto {
@ApiProperty({ example: 1 }) @ApiProperty({ example: 1 })
id: number; id: number;
@ApiProperty({ example: 1 }) @ApiProperty({ example: 1 })
reviewerId: number; reviewerId: number;
@ApiProperty({ example: 1 }) @ApiProperty({ example: 1 })
yachtId: number; yachtId: number;
@ApiProperty({ example: 5 }) @ApiProperty({ example: 5 })
starsCount: number; starsCount: number;
@ApiProperty({ example: 'Excellent yacht!' }) @ApiProperty({ example: 'Excellent yacht!' })
description: string; description: string;
} }
export class ReservationDto { export class ReservationDto {
@ApiProperty({ example: 1 }) @ApiProperty({ example: 1 })
id: number; id: number;
@ApiProperty({ example: 1 }) @ApiProperty({ example: 1 })
yachtId: number; yachtId: number;
@ApiProperty({ example: 1 }) @ApiProperty({ example: 1 })
reservatorId: number; reservatorId: number;
@ApiProperty({ example: 1733097600 }) @ApiProperty({ example: 1733097600 })
startUtc: number; startUtc: number;
@ApiProperty({ example: 1733133600 }) @ApiProperty({ example: 1733133600 })
endUtc: number; endUtc: number;
} }
export class CatalogItemShortDto { export class CatalogItemShortDto {
@ApiProperty({ example: 1 }) @ApiProperty({ example: 1 })
id?: number; id?: number;
@ApiProperty({ example: 'Азимут 55' }) @ApiProperty({ example: 'Азимут 55' })
name: string; name: string;
@ApiProperty({ example: 16.7 }) @ApiProperty({ example: 16.7 })
length: number; length: number;
@ApiProperty({ example: 32 }) @ApiProperty({ example: 32 })
speed: number; speed: number;
@ApiProperty({ example: 85000 }) @ApiProperty({ example: 85000 })
minCost: number; minCost: number;
@ApiProperty({ example: 'api/uploads/1765727362318-238005198.jpg' }) @ApiProperty({ example: 'api/uploads/1765727362318-238005198.jpg' })
mainImageUrl: string; mainImageUrl: string;
@ApiProperty({ @ApiProperty({
type: [String], type: [String],
example: ['api/uploads/1765727362318-238005198.jpg'], example: ['api/uploads/1765727362318-238005198.jpg'],
}) })
galleryUrls: string[]; galleryUrls: string[];
@ApiProperty({ example: true }) @ApiProperty({ example: true })
hasQuickRent: boolean; hasQuickRent: boolean;
@ApiProperty({ example: true }) @ApiProperty({ example: true })
isFeatured: boolean; isFeatured: boolean;
@ApiProperty({ required: false, example: '🔥 Лучшее предложение' }) @ApiProperty({ required: false, example: '🔥 Лучшее предложение' })
topText?: string; topText?: string;
@ApiProperty({ required: false, example: true }) @ApiProperty({ required: false, example: true })
isBestOffer?: boolean; isBestOffer?: boolean;
} }
export class CatalogItemLongDto extends CatalogItemShortDto { export class CatalogItemLongDto extends CatalogItemShortDto {
@ApiProperty({ example: 2022 }) @ApiProperty({ example: 2022 })
year: number; year: number;
@ApiProperty({ example: 8 }) @ApiProperty({ example: 8 })
comfortCapacity: number; comfortCapacity: number;
@ApiProperty({ example: 12 }) @ApiProperty({ example: 12 })
maxCapacity: number; maxCapacity: number;
@ApiProperty({ example: 4.8 }) @ApiProperty({ example: 4.8 })
width: number; width: number;
@ApiProperty({ example: 3 }) @ApiProperty({ example: 3 })
cabinsCount: number; cabinsCount: number;
@ApiProperty({ example: 'Стеклопластик' }) @ApiProperty({ example: 'Стеклопластик' })
matherial: string; matherial: string;
@ApiProperty({ example: 1200 }) @ApiProperty({ example: 1200 })
power: number; power: number;
@ApiProperty({ example: 'Роскошная моторная яхта...' }) @ApiProperty({ example: 'Роскошная моторная яхта...' })
description: string; description: string;
@ApiProperty({ type: UserDto }) @ApiProperty({ type: UserDto })
owner: UserDto; owner: UserDto;
@ApiProperty({ type: [ReviewDto] }) @ApiProperty({ type: [ReviewDto] })
reviews: ReviewDto[]; reviews: ReviewDto[];
@ApiProperty({ type: [ReservationDto] }) @ApiProperty({ type: [ReservationDto] })
reservations: ReservationDto[]; reservations: ReservationDto[];
} }

View File

@ -1,65 +1,65 @@
import { import {
IsOptional, IsOptional,
IsString, IsString,
IsNumber, IsNumber,
ValidateNested, ValidateNested,
IsEnum, IsEnum,
} from 'class-validator'; } from 'class-validator';
import { Type } from 'class-transformer'; import { Type } from 'class-transformer';
import { ApiProperty } from '@nestjs/swagger'; import { ApiProperty } from '@nestjs/swagger';
export enum SortDirection { export enum SortDirection {
ASC = 'asc', ASC = 'asc',
DESC = 'desc', DESC = 'desc',
} }
export class SortParams { export class SortParams {
@ApiProperty({ required: false, enum: SortDirection }) @ApiProperty({ required: false, enum: SortDirection })
@IsOptional() @IsOptional()
@IsEnum(SortDirection) @IsEnum(SortDirection)
direction?: SortDirection; direction?: SortDirection;
@ApiProperty({ required: false }) @ApiProperty({ required: false })
@IsOptional() @IsOptional()
@IsString() @IsString()
field?: string; field?: string;
} }
export class PriceFilter { export class PriceFilter {
@ApiProperty({ required: false }) @ApiProperty({ required: false })
@IsOptional() @IsOptional()
@IsNumber() @IsNumber()
minvalue?: number; minvalue?: number;
@ApiProperty({ required: false }) @ApiProperty({ required: false })
@IsOptional() @IsOptional()
@IsNumber() @IsNumber()
maxvalue?: number; maxvalue?: number;
} }
export class FilterParams { export class FilterParams {
@ApiProperty({ required: false, type: PriceFilter }) @ApiProperty({ required: false, type: PriceFilter })
@IsOptional() @IsOptional()
@ValidateNested() @ValidateNested()
@Type(() => PriceFilter) @Type(() => PriceFilter)
price?: PriceFilter; price?: PriceFilter;
} }
export class CatalogParamsDto { export class CatalogParamsDto {
@ApiProperty({ required: false }) @ApiProperty({ required: false })
@IsOptional() @IsOptional()
@IsString() @IsString()
search?: string; search?: string;
@ApiProperty({ required: false, type: SortParams }) @ApiProperty({ required: false, type: SortParams })
@IsOptional() @IsOptional()
@ValidateNested() @ValidateNested()
@Type(() => SortParams) @Type(() => SortParams)
sort?: SortParams; sort?: SortParams;
@ApiProperty({ required: false, type: FilterParams }) @ApiProperty({ required: false, type: FilterParams })
@IsOptional() @IsOptional()
@ValidateNested() @ValidateNested()
@Type(() => FilterParams) @Type(() => FilterParams)
filter?: FilterParams; filter?: FilterParams;
} }

View File

@ -1,29 +1,29 @@
import { ApiProperty } from '@nestjs/swagger'; import { ApiProperty } from '@nestjs/swagger';
import { CatalogItemShortDto } from './catalog-item.dto'; import { CatalogItemShortDto } from './catalog-item.dto';
import { CatalogFiltersDto } from './catalog-filters.dto'; import { CatalogFiltersDto } from './catalog-filters.dto';
export class CatalogResponseDto { export class CatalogResponseDto {
@ApiProperty({ type: [CatalogItemShortDto] }) @ApiProperty({ type: [CatalogItemShortDto] })
items: CatalogItemShortDto[]; items: CatalogItemShortDto[];
@ApiProperty({ example: 42 }) @ApiProperty({ example: 42 })
total: number; total: number;
@ApiProperty({ required: false, example: 1 }) @ApiProperty({ required: false, example: 1 })
page?: number; page?: number;
@ApiProperty({ required: false, example: 10 }) @ApiProperty({ required: false, example: 10 })
limit?: number; limit?: number;
@ApiProperty({ type: CatalogFiltersDto, required: false }) @ApiProperty({ type: CatalogFiltersDto, required: false })
filters?: CatalogFiltersDto; filters?: CatalogFiltersDto;
@ApiProperty({ @ApiProperty({
required: false, required: false,
example: { field: 'name', direction: 'asc' }, example: { field: 'name', direction: 'asc' },
}) })
sort?: any; sort?: any;
@ApiProperty({ required: false, example: 'yacht' }) @ApiProperty({ required: false, example: 'yacht' })
search?: string; search?: string;
} }

View File

@ -1,71 +1,71 @@
import { ApiProperty } from '@nestjs/swagger'; import { ApiProperty } from '@nestjs/swagger';
export class CreateYachtDto { export class CreateYachtDto {
@ApiProperty({ example: 'Азимут 55', description: 'Название яхты' }) @ApiProperty({ example: 'Азимут 55', description: 'Название яхты' })
name: string; name: string;
@ApiProperty({ example: 16.7, description: 'Длина яхты в метрах' }) @ApiProperty({ example: 16.7, description: 'Длина яхты в метрах' })
length: number; length: number;
@ApiProperty({ example: 32, description: 'Скорость в узлах' }) @ApiProperty({ example: 32, description: 'Скорость в узлах' })
speed: number; speed: number;
@ApiProperty({ example: 85000, description: 'Минимальная стоимость аренды' }) @ApiProperty({ example: 85000, description: 'Минимальная стоимость аренды' })
minCost: number; minCost: number;
@ApiProperty({ @ApiProperty({
example: 'api/uploads/1765727362318-238005198.jpg', example: 'api/uploads/1765727362318-238005198.jpg',
description: 'URL главного изображения', description: 'URL главного изображения',
}) })
mainImageUrl: string; mainImageUrl: string;
@ApiProperty({ @ApiProperty({
type: [String], type: [String],
example: ['api/uploads/1765727362318-238005198.jpg'], example: ['api/uploads/1765727362318-238005198.jpg'],
description: 'URL галереи изображений', description: 'URL галереи изображений',
}) })
galleryUrls: string[]; galleryUrls: string[];
@ApiProperty({ example: true, description: 'Доступна быстрая аренда' }) @ApiProperty({ example: true, description: 'Доступна быстрая аренда' })
hasQuickRent: boolean; hasQuickRent: boolean;
@ApiProperty({ example: false, description: 'Рекомендуемая яхта' }) @ApiProperty({ example: false, description: 'Рекомендуемая яхта' })
isFeatured: boolean; isFeatured: boolean;
@ApiProperty({ example: 2022, description: 'Год выпуска' }) @ApiProperty({ example: 2022, description: 'Год выпуска' })
year: number; year: number;
@ApiProperty({ example: 8, description: 'Комфортная вместимость' }) @ApiProperty({ example: 8, description: 'Комфортная вместимость' })
comfortCapacity: number; comfortCapacity: number;
@ApiProperty({ example: 12, description: 'Максимальная вместимость' }) @ApiProperty({ example: 12, description: 'Максимальная вместимость' })
maxCapacity: number; maxCapacity: number;
@ApiProperty({ example: 4.8, description: 'Ширина в метрах' }) @ApiProperty({ example: 4.8, description: 'Ширина в метрах' })
width: number; width: number;
@ApiProperty({ example: 3, description: 'Количество кают' }) @ApiProperty({ example: 3, description: 'Количество кают' })
cabinsCount: number; cabinsCount: number;
@ApiProperty({ example: 'Стеклопластик', description: 'Материал корпуса' }) @ApiProperty({ example: 'Стеклопластик', description: 'Материал корпуса' })
matherial: string; matherial: string;
@ApiProperty({ example: 1200, description: 'Мощность двигателя' }) @ApiProperty({ example: 1200, description: 'Мощность двигателя' })
power: number; power: number;
@ApiProperty({ @ApiProperty({
example: 'Роскошная моторная яхта...', example: 'Роскошная моторная яхта...',
description: 'Описание яхты', description: 'Описание яхты',
}) })
description: string; description: string;
@ApiProperty({ example: 1, description: 'ID владельца яхты' }) @ApiProperty({ example: 1, description: 'ID владельца яхты' })
userId: number; userId: number;
@ApiProperty({ @ApiProperty({
required: false, required: false,
example: '🔥 Лучшее предложение', example: '🔥 Лучшее предложение',
description: 'Текст для отображения сверху', description: 'Текст для отображения сверху',
}) })
topText?: string; topText?: string;
} }

View File

@ -1,6 +1,6 @@
import { CatalogItemShortDto } from './catalog-item.dto'; import { CatalogItemShortDto } from './catalog-item.dto';
export class MainPageCatalogResponseDto { export class MainPageCatalogResponseDto {
featuredYacht: CatalogItemShortDto; featuredYacht: CatalogItemShortDto;
restYachts: CatalogItemShortDto[]; restYachts: CatalogItemShortDto[];
} }

View File

@ -1,18 +1,18 @@
import { Test, TestingModule } from '@nestjs/testing'; import { Test, TestingModule } from '@nestjs/testing';
import { FilesController } from './files.controller'; import { FilesController } from './files.controller';
describe('FilesController', () => { describe('FilesController', () => {
let controller: FilesController; let controller: FilesController;
beforeEach(async () => { beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({ const module: TestingModule = await Test.createTestingModule({
controllers: [FilesController], controllers: [FilesController],
}).compile(); }).compile();
controller = module.get<FilesController>(FilesController); controller = module.get<FilesController>(FilesController);
}); });
it('should be defined', () => { it('should be defined', () => {
expect(controller).toBeDefined(); expect(controller).toBeDefined();
}); });
}); });

View File

@ -1,26 +1,26 @@
import { import {
Controller, Controller,
Post, Post,
UploadedFile, UploadedFile,
UploadedFiles, UploadedFiles,
UseInterceptors, UseInterceptors,
} from '@nestjs/common'; } from '@nestjs/common';
import { FileInterceptor, FilesInterceptor } from '@nestjs/platform-express'; import { FileInterceptor, FilesInterceptor } from '@nestjs/platform-express';
import { FilesService } from './files.service'; import { FilesService } from './files.service';
@Controller('files') @Controller('files')
export class FilesController { export class FilesController {
constructor(private readonly filesService: FilesService) {} constructor(private readonly filesService: FilesService) {}
@Post('upload') @Post('upload')
@UseInterceptors(FileInterceptor('file')) @UseInterceptors(FileInterceptor('file'))
uploadFile(@UploadedFile() file: Express.Multer.File) { uploadFile(@UploadedFile() file: Express.Multer.File) {
return this.filesService.saveFileInfo(file); return this.filesService.saveFileInfo(file);
} }
@Post('upload/multiple') @Post('upload/multiple')
@UseInterceptors(FilesInterceptor('files', 10)) @UseInterceptors(FilesInterceptor('files', 10))
uploadMultipleFiles(@UploadedFiles() files: Express.Multer.File[]) { uploadMultipleFiles(@UploadedFiles() files: Express.Multer.File[]) {
return this.filesService.saveMultipleFilesInfo(files); return this.filesService.saveMultipleFilesInfo(files);
} }
} }

View File

@ -1,32 +1,32 @@
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { FilesService } from './files.service'; import { FilesService } from './files.service';
import { FilesController } from './files.controller'; import { FilesController } from './files.controller';
import { MulterModule } from '@nestjs/platform-express'; import { MulterModule } from '@nestjs/platform-express';
import { diskStorage } from 'multer'; import { diskStorage } from 'multer';
import { extname } from 'path'; import { extname } from 'path';
import { ConfigModule } from '@nestjs/config'; import { ConfigModule } from '@nestjs/config';
@Module({ @Module({
imports: [ imports: [
MulterModule.register({ MulterModule.register({
storage: diskStorage({ storage: diskStorage({
destination: './uploads', destination: './uploads',
filename: (req, file, callback) => { filename: (req, file, callback) => {
const uniqueSuffix = const uniqueSuffix =
Date.now() + '-' + Math.round(Math.random() * 1e9); Date.now() + '-' + Math.round(Math.random() * 1e9);
const ext = extname(file.originalname); const ext = extname(file.originalname);
const filename = `${uniqueSuffix}${ext}`; const filename = `${uniqueSuffix}${ext}`;
callback(null, filename); callback(null, filename);
}, },
}), }),
limits: { limits: {
fileSize: 30 * 1024 * 1024, fileSize: 30 * 1024 * 1024,
}, },
}), }),
ConfigModule, ConfigModule,
], ],
controllers: [FilesController], controllers: [FilesController],
providers: [FilesService], providers: [FilesService],
exports: [FilesService], exports: [FilesService],
}) })
export class FilesModule {} export class FilesModule {}

View File

@ -1,18 +1,18 @@
import { Test, TestingModule } from '@nestjs/testing'; import { Test, TestingModule } from '@nestjs/testing';
import { FilesService } from './files.service'; import { FilesService } from './files.service';
describe('FilesService', () => { describe('FilesService', () => {
let service: FilesService; let service: FilesService;
beforeEach(async () => { beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({ const module: TestingModule = await Test.createTestingModule({
providers: [FilesService], providers: [FilesService],
}).compile(); }).compile();
service = module.get<FilesService>(FilesService); service = module.get<FilesService>(FilesService);
}); });
it('should be defined', () => { it('should be defined', () => {
expect(service).toBeDefined(); expect(service).toBeDefined();
}); });
}); });

View File

@ -1,21 +1,21 @@
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config'; import { ConfigService } from '@nestjs/config';
@Injectable() @Injectable()
export class FilesService { export class FilesService {
constructor(private configService: ConfigService) {} constructor(private configService: ConfigService) {}
async saveFileInfo(file: Express.Multer.File) { async saveFileInfo(file: Express.Multer.File) {
return { return {
filename: file.filename, filename: file.filename,
originalname: file.originalname, originalname: file.originalname,
size: file.size, size: file.size,
mimetype: file.mimetype, mimetype: file.mimetype,
url: `/api/uploads/${file.filename}`, url: `/api/uploads/${file.filename}`,
}; };
} }
async saveMultipleFilesInfo(files: Express.Multer.File[]) { async saveMultipleFilesInfo(files: Express.Multer.File[]) {
return Promise.all(files.map((file) => this.saveFileInfo(file))); return Promise.all(files.map((file) => this.saveFileInfo(file)));
} }
} }

View File

@ -1,36 +1,36 @@
import { NestFactory } from '@nestjs/core'; import { NestFactory } from '@nestjs/core';
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger'; import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';
import { AppModule } from './app.module'; import { AppModule } from './app.module';
import { join } from 'path'; import { join } from 'path';
import * as express from 'express'; import * as express from 'express';
async function bootstrap() { async function bootstrap() {
const app = await NestFactory.create(AppModule); const app = await NestFactory.create(AppModule);
app.use('/uploads', express.static(join(__dirname, '..', 'uploads'))); app.use('/uploads', express.static(join(__dirname, '..', 'uploads')));
app.setGlobalPrefix(''); app.setGlobalPrefix('');
app.enableCors({ app.enableCors({
origin: ['http://localhost:3000'], origin: ['http://localhost:3000', "http://travelmarine.ru", "https://travelmarine.ru"],
methods: 'GET,HEAD,PUT,PATCH,POST,DELETE,OPTIONS', methods: 'GET,HEAD,PUT,PATCH,POST,DELETE,OPTIONS',
credentials: true, credentials: true,
allowedHeaders: 'Content-Type, Authorization, X-Requested-With', allowedHeaders: 'Content-Type, Authorization, X-Requested-With',
exposedHeaders: 'Authorization', exposedHeaders: 'Authorization',
maxAge: 86400, maxAge: 86400,
}); });
const config = new DocumentBuilder() const config = new DocumentBuilder()
.setTitle('Travelmarine backend') .setTitle('Travelmarine backend')
.setDescription('Backend API for Travelmarine service') .setDescription('Backend API for Travelmarine service')
.setVersion('0.1') .setVersion('0.1')
.addServer('http://localhost:4000', 'Local server') .addServer('http://localhost:4000', 'Local server')
.addServer('http://89.169.188.2/api', 'Production server') .addServer('http://89.169.188.2/api', 'Production server')
.build(); .build();
const documentFactory = () => SwaggerModule.createDocument(app, config); const documentFactory = () => SwaggerModule.createDocument(app, config);
SwaggerModule.setup('/', app, documentFactory); SwaggerModule.setup('/', app, documentFactory);
await app.listen(process.env.PORT ?? 4000); await app.listen(process.env.PORT ?? 4000);
} }
bootstrap(); bootstrap();

View File

@ -1,28 +1,28 @@
import { ApiProperty } from '@nestjs/swagger'; import { ApiProperty } from '@nestjs/swagger';
import { CatalogItemLongDto } from '../catalog/dto/catalog-item.dto'; import { CatalogItemLongDto } from '../catalog/dto/catalog-item.dto';
export class ReservationItemDto { export class ReservationItemDto {
@ApiProperty({ example: 1, description: 'ID яхты' }) @ApiProperty({ example: 1, description: 'ID яхты' })
yachtId: number; yachtId: number;
@ApiProperty({ example: 1, description: 'ID резерватора' }) @ApiProperty({ example: 1, description: 'ID резерватора' })
reservatorId: number; reservatorId: number;
@ApiProperty({ example: 1733097600, description: 'Начало резервации (Unix timestamp в UTC)' }) @ApiProperty({ example: 1733097600, description: 'Начало резервации (Unix timestamp в UTC)' })
startUtc: number; startUtc: number;
@ApiProperty({ example: 1733133600, description: 'Конец резервации (Unix timestamp в UTC)' }) @ApiProperty({ example: 1733133600, description: 'Конец резервации (Unix timestamp в UTC)' })
endUtc: number; endUtc: number;
} }
export class ReservationWithYachtDto extends ReservationItemDto { export class ReservationWithYachtDto extends ReservationItemDto {
@ApiProperty({ example: 1, description: 'ID резервации' }) @ApiProperty({ example: 1, description: 'ID резервации' })
id: number; id: number;
@ApiProperty({ @ApiProperty({
type: CatalogItemLongDto, type: CatalogItemLongDto,
required: false, required: false,
description: 'Данные о яхте', description: 'Данные о яхте',
}) })
yacht?: CatalogItemLongDto; yacht?: CatalogItemLongDto;
} }

View File

@ -1,39 +1,39 @@
import { Controller, Get, Post, Body, Param } from '@nestjs/common'; import { Controller, Get, Post, Body, Param } from '@nestjs/common';
import { ApiOperation, ApiResponse, ApiBody, ApiParam } from '@nestjs/swagger'; import { ApiOperation, ApiResponse, ApiBody, ApiParam } from '@nestjs/swagger';
import { ReservationsService } from './reservations.service'; import { ReservationsService } from './reservations.service';
import { ReservationItemDto, ReservationWithYachtDto } from './reservation-item.dto'; import { ReservationItemDto, ReservationWithYachtDto } from './reservation-item.dto';
@Controller('reservations') @Controller('reservations')
export class ReservationsController { export class ReservationsController {
constructor(private readonly reservationsService: ReservationsService) {} constructor(private readonly reservationsService: ReservationsService) {}
@Post() @Post()
@ApiOperation({ summary: 'Создать новую резервацию' }) @ApiOperation({ summary: 'Создать новую резервацию' })
@ApiBody({ type: ReservationItemDto }) @ApiBody({ type: ReservationItemDto })
@ApiResponse({ status: 201, description: 'Резервация успешно создана' }) @ApiResponse({ status: 201, description: 'Резервация успешно создана' })
create(@Body() dto: ReservationItemDto) { create(@Body() dto: ReservationItemDto) {
return this.reservationsService.createReservation(dto); return this.reservationsService.createReservation(dto);
} }
@Get('user/:userId') @Get('user/:userId')
@ApiOperation({ summary: 'Получить резервации по ID пользователя' }) @ApiOperation({ summary: 'Получить резервации по ID пользователя' })
@ApiParam({ name: 'userId', description: 'ID пользователя', type: Number }) @ApiParam({ name: 'userId', description: 'ID пользователя', type: Number })
@ApiResponse({ @ApiResponse({
status: 200, status: 200,
description: 'Список резерваций пользователя с данными о яхтах', description: 'Список резерваций пользователя с данными о яхтах',
type: [ReservationWithYachtDto], type: [ReservationWithYachtDto],
}) })
async findByUserId(@Param('userId') userId: string) { async findByUserId(@Param('userId') userId: string) {
return this.reservationsService.getReservationsByUserId(Number(userId)); return this.reservationsService.getReservationsByUserId(Number(userId));
} }
@Get('yacht/:yachtId') @Get('yacht/:yachtId')
findByYachtId(@Param('yachtId') yachtId: string) { findByYachtId(@Param('yachtId') yachtId: string) {
return this.reservationsService.getReservationsByYachtId(Number(yachtId)); return this.reservationsService.getReservationsByYachtId(Number(yachtId));
} }
@Get() @Get()
findAll() { findAll() {
return this.reservationsService.getAllReservations(); return this.reservationsService.getAllReservations();
} }
} }

View File

@ -1,12 +1,12 @@
import { Module, forwardRef } from '@nestjs/common'; import { Module, forwardRef } from '@nestjs/common';
import { ReservationsService } from './reservations.service'; import { ReservationsService } from './reservations.service';
import { ReservationsController } from './reservations.controller'; import { ReservationsController } from './reservations.controller';
import { CatalogModule } from '../catalog/catalog.module'; import { CatalogModule } from '../catalog/catalog.module';
@Module({ @Module({
imports: [forwardRef(() => CatalogModule)], imports: [forwardRef(() => CatalogModule)],
controllers: [ReservationsController], controllers: [ReservationsController],
providers: [ReservationsService], providers: [ReservationsService],
exports: [ReservationsService], // Export for other modules to use exports: [ReservationsService], // Export for other modules to use
}) })
export class ReservationsModule {} export class ReservationsModule {}

View File

@ -1,108 +1,108 @@
import { Injectable, Inject, forwardRef } from '@nestjs/common'; import { Injectable, Inject, forwardRef } from '@nestjs/common';
import { ReservationItemDto } from './reservation-item.dto'; import { ReservationItemDto } from './reservation-item.dto';
import { CatalogService } from '../catalog/catalog.service'; import { CatalogService } from '../catalog/catalog.service';
import { CatalogItemLongDto } from '../catalog/dto/catalog-item.dto'; import { CatalogItemLongDto } from '../catalog/dto/catalog-item.dto';
export interface Reservation extends ReservationItemDto { export interface Reservation extends ReservationItemDto {
id: number; id: number;
} }
export interface ReservationWithYacht extends Reservation { export interface ReservationWithYacht extends Reservation {
yacht?: CatalogItemLongDto; yacht?: CatalogItemLongDto;
} }
@Injectable() @Injectable()
export class ReservationsService { export class ReservationsService {
constructor( constructor(
@Inject(forwardRef(() => CatalogService)) @Inject(forwardRef(() => CatalogService))
private readonly catalogService: CatalogService, private readonly catalogService: CatalogService,
) {} ) {}
private reservations: Reservation[] = [ private reservations: Reservation[] = [
{ {
id: 1, id: 1,
yachtId: 1, yachtId: 1,
reservatorId: 1, reservatorId: 1,
// Corrected: Jan 1, 2026 20:00 UTC to Jan 2, 2026 08:00 UTC // Corrected: Jan 1, 2026 20:00 UTC to Jan 2, 2026 08:00 UTC
startUtc: 1767369600, // Jan 1, 2026 20:00:00 UTC startUtc: 1767369600, // Jan 1, 2026 20:00:00 UTC
endUtc: 1767412800, // Jan 2, 2026 08:00:00 UTC endUtc: 1767412800, // Jan 2, 2026 08:00:00 UTC
}, },
{ {
id: 2, id: 2,
yachtId: 3, yachtId: 3,
reservatorId: 2, reservatorId: 2,
// Jan 3, 2026 08:00 UTC to Jan 5, 2026 20:00 UTC // Jan 3, 2026 08:00 UTC to Jan 5, 2026 20:00 UTC
startUtc: 1767484800, // Jan 3, 2026 08:00:00 UTC startUtc: 1767484800, // Jan 3, 2026 08:00:00 UTC
endUtc: 1767715200, // Jan 5, 2026 20:00:00 UTC endUtc: 1767715200, // Jan 5, 2026 20:00:00 UTC
}, },
{ {
id: 3, id: 3,
yachtId: 5, yachtId: 5,
reservatorId: 1, reservatorId: 1,
// Jan 10, 2026 20:00 UTC to Jan 12, 2026 08:00 UTC // Jan 10, 2026 20:00 UTC to Jan 12, 2026 08:00 UTC
startUtc: 1768070400, // Jan 10, 2026 20:00:00 UTC startUtc: 1768070400, // Jan 10, 2026 20:00:00 UTC
endUtc: 1768176000, // Jan 12, 2026 08:00:00 UTC endUtc: 1768176000, // Jan 12, 2026 08:00:00 UTC
}, },
{ {
id: 4, id: 4,
yachtId: 7, yachtId: 7,
reservatorId: 2, reservatorId: 2,
// Jan 15, 2026 08:00 UTC to Jan 17, 2026 20:00 UTC // Jan 15, 2026 08:00 UTC to Jan 17, 2026 20:00 UTC
startUtc: 1768435200, // Jan 15, 2026 08:00:00 UTC startUtc: 1768435200, // Jan 15, 2026 08:00:00 UTC
endUtc: 1768684800, // Jan 17, 2026 20:00:00 UTC endUtc: 1768684800, // Jan 17, 2026 20:00:00 UTC
}, },
{ {
id: 5, id: 5,
yachtId: 9, yachtId: 9,
reservatorId: 1, reservatorId: 1,
// Jan 20, 2026 20:00 UTC to Jan 22, 2026 08:00 UTC // Jan 20, 2026 20:00 UTC to Jan 22, 2026 08:00 UTC
startUtc: 1768944000, // Jan 20, 2026 20:00:00 UTC startUtc: 1768944000, // Jan 20, 2026 20:00:00 UTC
endUtc: 1769049600, // Jan 22, 2026 08:00:00 UTC endUtc: 1769049600, // Jan 22, 2026 08:00:00 UTC
}, },
{ {
id: 6, id: 6,
yachtId: 11, yachtId: 11,
reservatorId: 2, reservatorId: 2,
// Jan 25, 2026 08:00 UTC to Jan 27, 2026 20:00 UTC // Jan 25, 2026 08:00 UTC to Jan 27, 2026 20:00 UTC
startUtc: 1769385600, // Jan 25, 2026 08:00:00 UTC startUtc: 1769385600, // Jan 25, 2026 08:00:00 UTC
endUtc: 1769635200, // Jan 27, 2026 20:00:00 UTC endUtc: 1769635200, // Jan 27, 2026 20:00:00 UTC
}, },
]; ];
private idCounter = 7; private idCounter = 7;
createReservation(dto: ReservationItemDto): Reservation { createReservation(dto: ReservationItemDto): Reservation {
const reservation = { const reservation = {
id: this.idCounter++, id: this.idCounter++,
...dto, ...dto,
}; };
this.reservations.push(reservation); this.reservations.push(reservation);
return reservation; return reservation;
} }
async getReservationsByUserId(userId: number): Promise<ReservationWithYacht[]> { async getReservationsByUserId(userId: number): Promise<ReservationWithYacht[]> {
const reservations = this.reservations.filter((r) => r.reservatorId === userId); const reservations = this.reservations.filter((r) => r.reservatorId === userId);
// Populate данные по яхте для каждой резервации // Populate данные по яхте для каждой резервации
const reservationsWithYacht = await Promise.all( const reservationsWithYacht = await Promise.all(
reservations.map(async (reservation) => { reservations.map(async (reservation) => {
const yacht = await this.catalogService.getCatalogItemById(reservation.yachtId); const yacht = await this.catalogService.getCatalogItemById(reservation.yachtId);
return { return {
...reservation, ...reservation,
yacht: yacht || undefined, yacht: yacht || undefined,
}; };
}) })
); );
return reservationsWithYacht; return reservationsWithYacht;
} }
getReservationsByYachtId(yachtId: number): Reservation[] { getReservationsByYachtId(yachtId: number): Reservation[] {
return this.reservations.filter((r) => r.yachtId === yachtId); return this.reservations.filter((r) => r.yachtId === yachtId);
} }
getAllReservations(): Reservation[] { getAllReservations(): Reservation[] {
return this.reservations; return this.reservations;
} }
} }

View File

@ -1,6 +1,6 @@
export class ReviewItemDto { export class ReviewItemDto {
reviewerId: number; reviewerId: number;
yachtId: number; yachtId: number;
starsCount: number; starsCount: number;
description: string; description: string;
} }

View File

@ -1,28 +1,28 @@
import { Controller, Get, Post, Body, Param } from '@nestjs/common'; import { Controller, Get, Post, Body, Param } from '@nestjs/common';
import { ReviewsService } from './reviews.service'; import { ReviewsService } from './reviews.service';
import { ReviewItemDto } from './review-item.dto'; import { ReviewItemDto } from './review-item.dto';
@Controller('reviews') @Controller('reviews')
export class ReviewsController { export class ReviewsController {
constructor(private readonly reviewsService: ReviewsService) {} constructor(private readonly reviewsService: ReviewsService) {}
@Post() @Post()
create(@Body() dto: ReviewItemDto) { create(@Body() dto: ReviewItemDto) {
return this.reviewsService.createReview(dto); return this.reviewsService.createReview(dto);
} }
@Get('user/:userId') @Get('user/:userId')
findByUserId(@Param('userId') userId: string) { findByUserId(@Param('userId') userId: string) {
return this.reviewsService.getReviewsByUserId(Number(userId)); return this.reviewsService.getReviewsByUserId(Number(userId));
} }
@Get('yacht/:yachtId') @Get('yacht/:yachtId')
findByYachtId(@Param('yachtId') yachtId: string) { findByYachtId(@Param('yachtId') yachtId: string) {
return this.reviewsService.getReviewsByYachtId(Number(yachtId)); return this.reviewsService.getReviewsByYachtId(Number(yachtId));
} }
@Get() @Get()
findAll() { findAll() {
return this.reviewsService.getAllReviews(); return this.reviewsService.getAllReviews();
} }
} }

View File

@ -1,10 +1,10 @@
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { ReviewsService } from './reviews.service'; import { ReviewsService } from './reviews.service';
import { ReviewsController } from './reviews.controller'; import { ReviewsController } from './reviews.controller';
@Module({ @Module({
controllers: [ReviewsController], controllers: [ReviewsController],
providers: [ReviewsService], providers: [ReviewsService],
exports: [ReviewsService], // Export for other modules to use exports: [ReviewsService], // Export for other modules to use
}) })
export class ReviewsModule {} export class ReviewsModule {}

View File

@ -1,92 +1,92 @@
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { ReviewItemDto } from './review-item.dto'; import { ReviewItemDto } from './review-item.dto';
export interface Review extends ReviewItemDto { export interface Review extends ReviewItemDto {
id: number; id: number;
} }
@Injectable() @Injectable()
export class ReviewsService { export class ReviewsService {
private reviews: Review[] = [ private reviews: Review[] = [
{ {
id: 1, id: 1,
reviewerId: 1, reviewerId: 1,
yachtId: 1, yachtId: 1,
starsCount: 5, starsCount: 5,
description: 'Excellent yacht!', description: 'Excellent yacht!',
}, },
{ {
id: 2, id: 2,
reviewerId: 2, reviewerId: 2,
yachtId: 1, yachtId: 1,
starsCount: 4, starsCount: 4,
description: 'Very good experience', description: 'Very good experience',
}, },
{ {
id: 3, id: 3,
reviewerId: 1, reviewerId: 1,
yachtId: 3, yachtId: 3,
starsCount: 3, starsCount: 3,
description: 'Average condition', description: 'Average condition',
}, },
{ {
id: 4, id: 4,
reviewerId: 2, reviewerId: 2,
yachtId: 5, yachtId: 5,
starsCount: 5, starsCount: 5,
description: 'Perfect for sailing', description: 'Perfect for sailing',
}, },
{ {
id: 5, id: 5,
reviewerId: 1, reviewerId: 1,
yachtId: 7, yachtId: 7,
starsCount: 4, starsCount: 4,
description: 'Comfortable and fast', description: 'Comfortable and fast',
}, },
{ {
id: 6, id: 6,
reviewerId: 2, reviewerId: 2,
yachtId: 9, yachtId: 9,
starsCount: 2, starsCount: 2,
description: 'Needs maintenance', description: 'Needs maintenance',
}, },
{ {
id: 7, id: 7,
reviewerId: 1, reviewerId: 1,
yachtId: 11, yachtId: 11,
starsCount: 5, starsCount: 5,
description: 'Luxury experience', description: 'Luxury experience',
}, },
{ {
id: 8, id: 8,
reviewerId: 2, reviewerId: 2,
yachtId: 12, yachtId: 12,
starsCount: 4, starsCount: 4,
description: 'Great value for money', description: 'Great value for money',
}, },
]; ];
private idCounter = 9; private idCounter = 9;
createReview(dto: ReviewItemDto): Review { createReview(dto: ReviewItemDto): Review {
const review = { const review = {
id: this.idCounter++, id: this.idCounter++,
...dto, ...dto,
}; };
this.reviews.push(review); this.reviews.push(review);
return review; return review;
} }
getReviewsByUserId(userId: number): Review[] { getReviewsByUserId(userId: number): Review[] {
return this.reviews.filter((r) => r.reviewerId === userId); return this.reviews.filter((r) => r.reviewerId === userId);
} }
getReviewsByYachtId(yachtId: number): Review[] { getReviewsByYachtId(yachtId: number): Review[] {
return this.reviews.filter((r) => r.yachtId === yachtId); return this.reviews.filter((r) => r.yachtId === yachtId);
} }
getAllReviews(): Review[] { getAllReviews(): Review[] {
return this.reviews; return this.reviews;
} }
} }

View File

@ -1,14 +1,14 @@
import { Yacht } from '../yachts/yacht.entity'; import { Yacht } from '../yachts/yacht.entity';
export type User = { export type User = {
userId: number; userId: number;
firstName: string; firstName: string;
lastName: string; lastName: string;
phone: string; phone: string;
email: string; email: string;
password: string; password: string;
yachts?: Yacht[]; yachts?: Yacht[];
companyName?: string; companyName?: string;
inn?: number; inn?: number;
ogrn?: number; ogrn?: number;
}; };

View File

@ -1,8 +1,8 @@
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { UsersService } from './users.service'; import { UsersService } from './users.service';
@Module({ @Module({
providers: [UsersService], providers: [UsersService],
exports: [UsersService], // This is important! exports: [UsersService], // This is important!
}) })
export class UsersModule {} export class UsersModule {}

View File

@ -1,96 +1,96 @@
import { Injectable, Logger } from '@nestjs/common'; import { Injectable, Logger } from '@nestjs/common';
import { YachtsService } from '../yachts/yachts.service'; import { YachtsService } from '../yachts/yachts.service';
import { User } from './user.entity'; import { User } from './user.entity';
@Injectable() @Injectable()
export class UsersService { export class UsersService {
private readonly logger = new Logger(UsersService.name); private readonly logger = new Logger(UsersService.name);
private readonly users: User[] = [ private readonly users: User[] = [
{ {
userId: 1, userId: 1,
firstName: 'Иван', firstName: 'Иван',
lastName: 'Андреев', lastName: 'Андреев',
phone: '+79009009090', phone: '+79009009090',
email: 'ivan@yachting.ru', email: 'ivan@yachting.ru',
password: 'admin', password: 'admin',
companyName: 'Северный Флот', companyName: 'Северный Флот',
inn: 1234567890, inn: 1234567890,
ogrn: 1122334455667, ogrn: 1122334455667,
}, },
{ {
userId: 2, userId: 2,
firstName: 'Сергей', firstName: 'Сергей',
lastName: 'Большаков', lastName: 'Большаков',
phone: '+79119119191', phone: '+79119119191',
email: 'sergey@yachting.ru', email: 'sergey@yachting.ru',
password: 'admin', password: 'admin',
companyName: 'Балтийские Просторы', companyName: 'Балтийские Просторы',
inn: 9876543210, inn: 9876543210,
ogrn: 9988776655443, ogrn: 9988776655443,
}, },
{ {
userId: 3, userId: 3,
firstName: 'Анна', firstName: 'Анна',
lastName: 'Петрова', lastName: 'Петрова',
phone: '+79229229292', phone: '+79229229292',
email: 'anna@yachting.ru', email: 'anna@yachting.ru',
password: 'admin', password: 'admin',
companyName: 'Ладожские Ветры', companyName: 'Ладожские Ветры',
inn: 5555555555, inn: 5555555555,
ogrn: 3333444455556, ogrn: 3333444455556,
}, },
{ {
userId: 4, userId: 4,
firstName: 'Дмитрий', firstName: 'Дмитрий',
lastName: 'Соколов', lastName: 'Соколов',
phone: '+79339339393', phone: '+79339339393',
email: 'dmitry@yachting.ru', email: 'dmitry@yachting.ru',
password: 'admin', password: 'admin',
companyName: 'Финский Залив', companyName: 'Финский Залив',
inn: 1111222233, inn: 1111222233,
ogrn: 7777888899990, ogrn: 7777888899990,
}, },
]; ];
async findOne( async findOne(
email: string, email: string,
includeYachts: boolean = false, includeYachts: boolean = false,
): Promise<User | undefined> { ): Promise<User | undefined> {
const user = this.users.find((user) => user.email === email); const user = this.users.find((user) => user.email === email);
if (user && includeYachts) { if (user && includeYachts) {
user.yachts = []; user.yachts = [];
} }
return user; return user;
} }
async findById( async findById(
userId: number, userId: number,
includeYachts: boolean = false, includeYachts: boolean = false,
): Promise<User | undefined> { ): Promise<User | undefined> {
const user = this.users.find((user) => user.userId === userId); const user = this.users.find((user) => user.userId === userId);
if (user && includeYachts) { if (user && includeYachts) {
user.yachts = []; user.yachts = [];
} }
return user; return user;
} }
async findAll(includeYachts: boolean = false): Promise<User[]> { async findAll(includeYachts: boolean = false): Promise<User[]> {
if (!includeYachts) { if (!includeYachts) {
return this.users; return this.users;
} }
const usersWithYachts = await Promise.all( const usersWithYachts = await Promise.all(
this.users.map(async (user) => { this.users.map(async (user) => {
const yachts = []; const yachts = [];
return { ...user, yachts }; return { ...user, yachts };
}), }),
); );
return usersWithYachts; return usersWithYachts;
} }
} }

View File

@ -1,7 +1,7 @@
export class CreateYachtDto { export class CreateYachtDto {
name: string; name: string;
model: string; model: string;
year: number; year: number;
length: number; length: number;
userId: number; userId: number;
} }

View File

@ -1,4 +1,4 @@
import { PartialType } from '@nestjs/mapped-types'; import { PartialType } from '@nestjs/mapped-types';
import { CreateYachtDto } from './create-yacht.dto'; import { CreateYachtDto } from './create-yacht.dto';
export class UpdateYachtDto extends PartialType(CreateYachtDto) {} export class UpdateYachtDto extends PartialType(CreateYachtDto) {}

View File

@ -1,10 +1,10 @@
export type Yacht = { export type Yacht = {
yachtId: number; yachtId: number;
name: string; name: string;
model: string; model: string;
year: number; year: number;
length: number; length: number;
userId: number; userId: number;
createdAt: Date; createdAt: Date;
updatedAt: Date; updatedAt: Date;
}; };

View File

@ -1,54 +1,54 @@
import { import {
Controller, Controller,
Get, Get,
Post, Post,
Put, Put,
Delete, Delete,
Body, Body,
Param, Param,
Query, Query,
ParseIntPipe, ParseIntPipe,
HttpCode, HttpCode,
HttpStatus, HttpStatus,
} from '@nestjs/common'; } from '@nestjs/common';
import { YachtsService } from './yachts.service'; import { YachtsService } from './yachts.service';
import { CreateYachtDto } from './dto/create-yacht.dto'; import { CreateYachtDto } from './dto/create-yacht.dto';
import { UpdateYachtDto } from './dto/update-yacht.dto'; import { UpdateYachtDto } from './dto/update-yacht.dto';
@Controller('yachts') @Controller('yachts')
export class YachtsController { export class YachtsController {
constructor(private readonly yachtsService: YachtsService) {} constructor(private readonly yachtsService: YachtsService) {}
@Get() @Get()
async findAll(@Query('userId') userId?: string) { async findAll(@Query('userId') userId?: string) {
if (userId) { if (userId) {
return this.yachtsService.findByUserId(parseInt(userId)); return this.yachtsService.findByUserId(parseInt(userId));
} }
return this.yachtsService.findAll(); return this.yachtsService.findAll();
} }
@Get(':id') @Get(':id')
async findOne(@Param('id', ParseIntPipe) id: number) { async findOne(@Param('id', ParseIntPipe) id: number) {
return this.yachtsService.findById(id); return this.yachtsService.findById(id);
} }
@Post() @Post()
@HttpCode(HttpStatus.CREATED) @HttpCode(HttpStatus.CREATED)
async create(@Body() createYachtDto: CreateYachtDto) { async create(@Body() createYachtDto: CreateYachtDto) {
return this.yachtsService.create(createYachtDto); return this.yachtsService.create(createYachtDto);
} }
@Put(':id') @Put(':id')
async update( async update(
@Param('id', ParseIntPipe) id: number, @Param('id', ParseIntPipe) id: number,
@Body() updateYachtDto: UpdateYachtDto, @Body() updateYachtDto: UpdateYachtDto,
) { ) {
return this.yachtsService.update(id, updateYachtDto); return this.yachtsService.update(id, updateYachtDto);
} }
@Delete(':id') @Delete(':id')
@HttpCode(HttpStatus.NO_CONTENT) @HttpCode(HttpStatus.NO_CONTENT)
async delete(@Param('id', ParseIntPipe) id: number) { async delete(@Param('id', ParseIntPipe) id: number) {
return this.yachtsService.delete(id); return this.yachtsService.delete(id);
} }
} }

View File

@ -1,8 +1,8 @@
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { YachtsService } from './yachts.service'; import { YachtsService } from './yachts.service';
@Module({ @Module({
providers: [YachtsService], providers: [YachtsService],
exports: [YachtsService], exports: [YachtsService],
}) })
export class YachtsModule {} export class YachtsModule {}

View File

@ -1,95 +1,95 @@
import { Injectable, Logger, NotFoundException } from '@nestjs/common'; import { Injectable, Logger, NotFoundException } from '@nestjs/common';
import { Yacht } from './yacht.entity'; import { Yacht } from './yacht.entity';
import { CreateYachtDto } from './dto/create-yacht.dto'; import { CreateYachtDto } from './dto/create-yacht.dto';
import { UpdateYachtDto } from './dto/update-yacht.dto'; import { UpdateYachtDto } from './dto/update-yacht.dto';
@Injectable() @Injectable()
export class YachtsService { export class YachtsService {
private readonly logger = new Logger(YachtsService.name); private readonly logger = new Logger(YachtsService.name);
private yachts: Yacht[] = [ private yachts: Yacht[] = [
{ {
yachtId: 1, yachtId: 1,
name: 'Sea Dream', name: 'Sea Dream',
model: 'Sunseeker 76', model: 'Sunseeker 76',
year: 2020, year: 2020,
length: 76, length: 76,
userId: 1, userId: 1,
createdAt: new Date('2023-01-15'), createdAt: new Date('2023-01-15'),
updatedAt: new Date('2023-01-15'), updatedAt: new Date('2023-01-15'),
}, },
{ {
yachtId: 2, yachtId: 2,
name: 'Ocean Breeze', name: 'Ocean Breeze',
model: 'Princess 68', model: 'Princess 68',
year: 2021, year: 2021,
length: 68, length: 68,
userId: 1, userId: 1,
createdAt: new Date('2023-02-20'), createdAt: new Date('2023-02-20'),
updatedAt: new Date('2023-02-20'), updatedAt: new Date('2023-02-20'),
}, },
{ {
yachtId: 3, yachtId: 3,
name: 'Wave Rider', name: 'Wave Rider',
model: 'Ferretti 70', model: 'Ferretti 70',
year: 2019, year: 2019,
length: 70, length: 70,
userId: 2, userId: 2,
createdAt: new Date('2023-03-10'), createdAt: new Date('2023-03-10'),
updatedAt: new Date('2023-03-10'), updatedAt: new Date('2023-03-10'),
}, },
]; ];
async findAll(): Promise<Yacht[]> { async findAll(): Promise<Yacht[]> {
return this.yachts; return this.yachts;
} }
async findById(yachtId: number): Promise<Yacht> { async findById(yachtId: number): Promise<Yacht> {
const yacht = this.yachts.find((y) => y.yachtId === yachtId); const yacht = this.yachts.find((y) => y.yachtId === yachtId);
if (!yacht) { if (!yacht) {
throw new NotFoundException(`Yacht with ID ${yachtId} not found`); throw new NotFoundException(`Yacht with ID ${yachtId} not found`);
} }
return yacht; return yacht;
} }
async findByUserId(userId: number): Promise<Yacht[]> { async findByUserId(userId: number): Promise<Yacht[]> {
return this.yachts.filter((y) => y.userId === userId); return this.yachts.filter((y) => y.userId === userId);
} }
async create(createYachtDto: CreateYachtDto): Promise<Yacht> { async create(createYachtDto: CreateYachtDto): Promise<Yacht> {
const newYacht: Yacht = { const newYacht: Yacht = {
yachtId: this.yachts.length + 1, yachtId: this.yachts.length + 1,
...createYachtDto, ...createYachtDto,
createdAt: new Date(), createdAt: new Date(),
updatedAt: new Date(), updatedAt: new Date(),
}; };
this.yachts.push(newYacht); this.yachts.push(newYacht);
return newYacht; return newYacht;
} }
async update( async update(
yachtId: number, yachtId: number,
updateYachtDto: UpdateYachtDto, updateYachtDto: UpdateYachtDto,
): Promise<Yacht> { ): Promise<Yacht> {
const index = this.yachts.findIndex((y) => y.yachtId === yachtId); const index = this.yachts.findIndex((y) => y.yachtId === yachtId);
if (index === -1) { if (index === -1) {
throw new NotFoundException(`Yacht with ID ${yachtId} not found`); throw new NotFoundException(`Yacht with ID ${yachtId} not found`);
} }
this.yachts[index] = { this.yachts[index] = {
...this.yachts[index], ...this.yachts[index],
...updateYachtDto, ...updateYachtDto,
updatedAt: new Date(), updatedAt: new Date(),
}; };
return this.yachts[index]; return this.yachts[index];
} }
async delete(yachtId: number): Promise<void> { async delete(yachtId: number): Promise<void> {
const index = this.yachts.findIndex((y) => y.yachtId === yachtId); const index = this.yachts.findIndex((y) => y.yachtId === yachtId);
if (index === -1) { if (index === -1) {
throw new NotFoundException(`Yacht with ID ${yachtId} not found`); throw new NotFoundException(`Yacht with ID ${yachtId} not found`);
} }
this.yachts.splice(index, 1); this.yachts.splice(index, 1);
} }
} }

View File

@ -1,4 +1,4 @@
{ {
"extends": "./tsconfig.json", "extends": "./tsconfig.json",
"exclude": ["node_modules", "test", "dist", "**/*spec.ts"] "exclude": ["node_modules", "test", "dist", "**/*spec.ts"]
} }

View File

@ -1,25 +1,25 @@
{ {
"compilerOptions": { "compilerOptions": {
"module": "nodenext", "module": "nodenext",
"moduleResolution": "nodenext", "moduleResolution": "nodenext",
"resolvePackageJsonExports": true, "resolvePackageJsonExports": true,
"esModuleInterop": true, "esModuleInterop": true,
"isolatedModules": true, "isolatedModules": true,
"declaration": true, "declaration": true,
"removeComments": true, "removeComments": true,
"emitDecoratorMetadata": true, "emitDecoratorMetadata": true,
"experimentalDecorators": true, "experimentalDecorators": true,
"allowSyntheticDefaultImports": true, "allowSyntheticDefaultImports": true,
"target": "ES2023", "target": "ES2023",
"sourceMap": true, "sourceMap": true,
"outDir": "./dist", "outDir": "./dist",
"baseUrl": "./", "baseUrl": "./",
"incremental": true, "incremental": true,
"skipLibCheck": true, "skipLibCheck": true,
"strictNullChecks": true, "strictNullChecks": true,
"forceConsistentCasingInFileNames": true, "forceConsistentCasingInFileNames": true,
"noImplicitAny": false, "noImplicitAny": false,
"strictBindCallApply": false, "strictBindCallApply": false,
"noFallthroughCasesInSwitch": false "noFallthroughCasesInSwitch": false
} }
} }

BIN
uploads/1st.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

BIN
uploads/2nd.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 125 KiB

BIN
uploads/3rd.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 176 KiB

BIN
uploads/4th.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

BIN
uploads/5th.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 206 KiB

BIN
uploads/6th.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 177 KiB

BIN
uploads/gal1.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 129 KiB

BIN
uploads/gal10.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 134 KiB

BIN
uploads/gal2.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 133 KiB

BIN
uploads/gal3.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 167 KiB

BIN
uploads/gal4.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 176 KiB

BIN
uploads/gal5.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 166 KiB

BIN
uploads/gal6.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 134 KiB

BIN
uploads/gal7.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 167 KiB

BIN
uploads/gal8.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 176 KiB

BIN
uploads/gal9.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 166 KiB