Compare commits

..

10 Commits

Author SHA1 Message Date
Иван 4112645f5e make my shit working 2025-12-19 20:37:33 +03:00
Sergey Bolshakov 81a5a829a9 Доработка методов, userId в токене, инфа в сваггере 2025-12-15 17:02:27 +03:00
Иван 7895352e76 fix reservation time 2025-12-15 00:54:48 +03:00
Иван 58d605e090 Expand filters logic 2025-12-14 23:17:12 +03:00
Иван 755f07af92 Fix build errors 2025-12-14 22:09:20 +03:00
Иван 11d59be696 Add long yacht + reservations + reviews 2025-12-14 21:58:27 +03:00
Иван 4b7c100b3d add best offer flag 2025-12-14 20:09:32 +03:00
Иван 56c6560a05 fix img urls 2025-12-14 19:56:58 +03:00
Иван bbca08b8e9 fix img urls 2025-12-14 19:47:10 +03:00
Иван 9733b1fca4 remove uploads 2025-12-14 19:40:38 +03:00
73 changed files with 13793 additions and 12808 deletions

112
.gitignore vendored
View File

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

View File

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

View File

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

196
README.md
View File

@ -1,98 +1,98 @@
<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>
</p>
[circleci-image]: https://img.shields.io/circleci/build/github/nestjs/nest/master?token=abc123def456
[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 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/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://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#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://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>
</p>
<!--[![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)-->
## Description
[Nest](https://github.com/nestjs/nest) framework TypeScript starter repository.
## Project setup
```bash
$ npm install
```
## Compile and run the project
```bash
# development
$ npm run start
# watch mode
$ npm run start:dev
# production mode
$ npm run start:prod
```
## Run tests
```bash
# unit tests
$ npm run test
# e2e tests
$ npm run test:e2e
# test coverage
$ npm run test:cov
```
## 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.
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
$ npm install -g @nestjs/mau
$ 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.
## Resources
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.
- 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/).
- 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).
- 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).
- Looking for a job, or have a job to offer? Check out our official [Jobs board](https://jobs.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
- Author - [Kamil Myśliwiec](https://twitter.com/kammysliwiec)
- Website - [https://nestjs.com](https://nestjs.com/)
- Twitter - [@nestframework](https://twitter.com/nestframework)
## License
Nest is [MIT licensed](https://github.com/nestjs/nest/blob/master/LICENSE).
<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>
</p>
[circleci-image]: https://img.shields.io/circleci/build/github/nestjs/nest/master?token=abc123def456
[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 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/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://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#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://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>
</p>
<!--[![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)-->
## Description
[Nest](https://github.com/nestjs/nest) framework TypeScript starter repository.
## Project setup
```bash
$ npm install
```
## Compile and run the project
```bash
# development
$ npm run start
# watch mode
$ npm run start:dev
# production mode
$ npm run start:prod
```
## Run tests
```bash
# unit tests
$ npm run test
# e2e tests
$ npm run test:e2e
# test coverage
$ npm run test:cov
```
## 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.
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
$ npm install -g @nestjs/mau
$ 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.
## Resources
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.
- 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/).
- 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).
- 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).
- Looking for a job, or have a job to offer? Check out our official [Jobs board](https://jobs.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
- Author - [Kamil Myśliwiec](https://twitter.com/kammysliwiec)
- Website - [https://nestjs.com](https://nestjs.com/)
- Twitter - [@nestframework](https://twitter.com/nestframework)
## License
Nest is [MIT licensed](https://github.com/nestjs/nest/blob/master/LICENSE).

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 206 KiB

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

View File

Before

Width:  |  Height:  |  Size: 206 KiB

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