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 # 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,15 +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';
@Module({ import { ReservationsModule } from './reservations/reservations.module';
imports: [YachtsModule, CatalogModule, AuthModule, UsersModule, FilesModule],
controllers: [AppController], @Module({
providers: [AppService], imports: [YachtsModule, CatalogModule, AuthModule, UsersModule, FilesModule, ReviewsModule, ReservationsModule],
}) controllers: [AppController],
export class AppModule {} providers: [AppService],
})
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,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 { 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;
} }
async login(user: { email: string; password: string }) { login(user: Omit<User, 'password'>) {
this.logger.log('LOG'); this.logger.log('LOG');
const payload = { username: user.email, sub: user.password }; const payload = {
return { username: user.email,
access_token: this.jwtService.sign(payload), sub: user.userId,
}; userId: user.userId,
} };
} return {
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,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 { 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 { CatalogItemDto } from './dto/catalog-item.dto'; import { CatalogFiltersDto } from './dto/catalog-filters.dto';
import { CatalogParamsDto } from './dto/catalog-params.dto'; import {
import { CatalogItemShortDto,
ApiQuery, CatalogItemLongDto,
ApiResponse, } from './dto/catalog-item.dto';
ApiOperation, import {
ApiProperty, ApiQuery,
} from '@nestjs/swagger'; ApiResponse,
import { MainPageCatalogResponseDto } from './dto/main-page-catalog-response.dto'; ApiOperation,
ApiProperty,
@Controller('catalog') ApiBody,
export class CatalogController { ApiTags,
constructor(private readonly catalogService: CatalogService) {} } from '@nestjs/swagger';
import { MainPageCatalogResponseDto } from './dto/main-page-catalog-response.dto';
@Get() import { CreateYachtDto } from './dto/create-yacht.dto';
@ApiOperation({ summary: 'Get catalog with filters, search and sorting' })
@ApiResponse({ @ApiTags('catalog')
status: 200, @Controller('catalog')
description: 'Catalog items with filters applied', export class CatalogController {
type: CatalogResponseDto, constructor(private readonly catalogService: CatalogService) {}
})
@ApiQuery({ @Get('filter')
name: 'params', @ApiOperation({ summary: 'Filter catalog items with query parameters' })
required: false, @ApiResponse({
description: 'JSON string of filter parameters', status: 200,
type: String, description: 'Filtered catalog items',
}) type: CatalogResponseDto,
async getCatalog( })
@Query('params') paramsString?: string, @ApiQuery({ name: 'search', required: false, type: String })
): Promise<CatalogResponseDto> { @ApiQuery({ name: 'minLength', required: false, type: Number })
let params: CatalogParamsDto = {}; @ApiQuery({ name: 'maxLength', required: false, type: Number })
@ApiQuery({ name: 'minPrice', required: false, type: Number })
if (paramsString) { @ApiQuery({ name: 'maxPrice', required: false, type: Number })
try { @ApiQuery({ name: 'minYear', required: false, type: Number })
params = JSON.parse(decodeURIComponent(paramsString)); @ApiQuery({ name: 'maxYear', required: false, type: Number })
} catch (error) { @ApiQuery({ name: 'guests', required: false, type: Number })
params = {}; @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 })
return this.catalogService.getCatalog(params); @ApiQuery({ name: 'date', required: false, type: Date })
} @ApiQuery({ name: 'departureTime', required: false, type: String })
@ApiQuery({ name: 'arrivalTime', required: false, type: String })
@Get('main-page') async getFilteredCatalog(
@ApiOperation({ summary: 'Get catalog for main page' }) @Query() filters: CatalogFiltersDto,
@ApiProperty({ type: MainPageCatalogResponseDto }) ): Promise<CatalogResponseDto> {
async getMainPageCatalog(): Promise<MainPageCatalogResponseDto | null> { return this.catalogService.getCatalog(filters);
return this.catalogService.getMainPageCatalog(); }
}
@Get()
@Get('all') @ApiOperation({ summary: 'Get all catalog items (deprecated, use /filter)' })
@ApiOperation({ summary: 'Get all catalog items without filters' }) @ApiResponse({
@ApiResponse({ status: 200,
status: 200, description: 'All catalog items',
description: 'All catalog items', type: [CatalogItemShortDto],
type: [CatalogItemDto], })
}) async getAllCatalogItems(): Promise<CatalogItemShortDto[]> {
async getAllCatalogItems(): Promise<CatalogItemDto[]> { return this.catalogService.getAllCatalogItems();
return this.catalogService.getAllCatalogItems(); }
}
@Get('main-page')
@Get('filtered') @ApiOperation({ summary: 'Get catalog for main page' })
@ApiOperation({ summary: 'Get catalog with explicit query parameters' }) @ApiProperty({ type: MainPageCatalogResponseDto })
async getFilteredCatalog( async getMainPageCatalog(): Promise<MainPageCatalogResponseDto | null> {
@Query() params: CatalogParamsDto, return this.catalogService.getMainPageCatalog();
): Promise<CatalogResponseDto> { }
return this.catalogService.getCatalog(params);
} @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 { Module, forwardRef } from '@nestjs/common';
import { CatalogController } from './catalog.controller'; import { CatalogService } from './catalog.service';
import { CatalogService } from './catalog.service'; import { CatalogController } from './catalog.controller';
import { UsersModule } from '../users/users.module';
@Module({ import { ReservationsModule } from '../reservations/reservations.module';
controllers: [CatalogController], import { ReviewsModule } from '../reviews/reviews.module';
providers: [CatalogService],
exports: [CatalogService], @Module({
}) imports: [
export class CatalogModule {} 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 { import { ApiProperty } from '@nestjs/swagger';
id?: number; export class UserDto {
name: string; @ApiProperty({ example: 1 })
length: number; userId: number;
speed: number;
minCost: number; @ApiProperty({ example: 'Иван' })
mainImageUrl: string; firstName: string;
galleryUrls: string[];
hasQuickRent: boolean; @ApiProperty({ example: 'Андреев' })
isFeatured: boolean; lastName: string;
topText?: 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 { 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,11 +1,29 @@
import { CatalogItemDto } from './catalog-item.dto'; import { ApiProperty } from '@nestjs/swagger';
import { CatalogItemShortDto } from './catalog-item.dto';
export class CatalogResponseDto { import { CatalogFiltersDto } from './catalog-filters.dto';
items: CatalogItemDto[];
total: number; export class CatalogResponseDto {
page?: number; @ApiProperty({ type: [CatalogItemShortDto] })
limit?: number; items: CatalogItemShortDto[];
filters?: any;
sort?: any; @ApiProperty({ example: 42 })
search?: string; 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'; import { CatalogItemShortDto } from './catalog-item.dto';
export class MainPageCatalogResponseDto { export class MainPageCatalogResponseDto {
featuredYacht: CatalogItemDto; featuredYacht: CatalogItemShortDto;
restYachts: CatalogItemDto[]; 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

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

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