From f797ea77d7076a0e7a35edbdcbb7692a480c9c80 Mon Sep 17 00:00:00 2001 From: erik1110 Date: Wed, 24 Jan 2024 09:58:39 +0800 Subject: [PATCH 1/5] [fix] file.originalname --- src/features/image/image.service.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/features/image/image.service.ts b/src/features/image/image.service.ts index b3c7598..797fe34 100644 --- a/src/features/image/image.service.ts +++ b/src/features/image/image.service.ts @@ -4,17 +4,21 @@ import { v4 as uuidv4 } from 'uuid'; import { GetSignedUrlConfig } from '@google-cloud/storage'; import { AppError } from "src/utils/appError"; +const maxSize = 3 * 1024 * 1024; // 3 MB in bytes +const allowedExtensions = ['png', 'jpg', 'jpeg', 'webp']; + @Injectable() export class ImageService { constructor(private readonly firebaseService: FirebaseService){} async uploadImage(file): Promise { - const maxSize = 3 * 1024 * 1024; // 3 MB in bytes - const allowedExtensions = ['png', 'jpg', 'jpeg', 'webp']; + if (!file || !file.originalname) { + throw new AppError(HttpStatus.BAD_REQUEST, 'UserError', '無效的檔案'); + } if (file.size > maxSize) { throw new AppError(HttpStatus.BAD_REQUEST, 'UserError', '超過 3 MB'); } - const fileExtension = file.name.split('.').pop()?.toLowerCase(); + const fileExtension = file.originalname.split('.').pop()?.toLowerCase(); if (!fileExtension || !allowedExtensions.includes(fileExtension)) { throw new AppError(HttpStatus.BAD_REQUEST, 'UserError', '不支援的檔案格式'); } From 5d5b2e54bb65da04643dc4b49854fec4a647c322 Mon Sep 17 00:00:00 2001 From: erik1110 Date: Wed, 24 Jan 2024 10:07:30 +0800 Subject: [PATCH 2/5] [refactor] use crypto instead of shortid --- .github/workflows/main.yaml | 2 +- package-lock.json | 15 ++------------- package.json | 1 - src/features/url/url.service.ts | 9 +++++++-- 4 files changed, 10 insertions(+), 17 deletions(-) diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index 6032a2e..69bf8d2 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -28,7 +28,7 @@ jobs: - name: NPM install, build and test run: | - npm install --force + npm install npm run build --if-present npm run test --if-present env: diff --git a/package-lock.json b/package-lock.json index 3b86c73..cdd4c6d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -25,7 +25,6 @@ "firebase-admin": "^12.0.0", "jsonwebtoken": "^9.0.2", "mongoose": "^8.1.0", - "mongoose-auto-increment": "^5.0.1", "nestjs-object-id": "^1.2.0", "nodemailer": "^6.9.7", "passport": "^0.7.0", @@ -5093,7 +5092,8 @@ "node_modules/extend": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", - "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "optional": true }, "node_modules/external-editor": { "version": "3.1.0", @@ -7781,17 +7781,6 @@ "url": "https://opencollective.com/mongoose" } }, - "node_modules/mongoose-auto-increment": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/mongoose-auto-increment/-/mongoose-auto-increment-5.0.1.tgz", - "integrity": "sha512-fuSw0np0ZZXYjNBBrD6Wg0y70N0i5NhBuH3AIETTXCchaq45FeHpISoQ3fFbbdSFhfVEDRtg+hVxEDpFjdI2lw==", - "dependencies": { - "extend": "^3.0.0" - }, - "peerDependencies": { - "mongoose": "^4.1.12" - } - }, "node_modules/mongoose/node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", diff --git a/package.json b/package.json index 50088f5..4224aaa 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,6 @@ "firebase-admin": "^12.0.0", "jsonwebtoken": "^9.0.2", "mongoose": "^8.1.0", - "mongoose-auto-increment": "^5.0.1", "nestjs-object-id": "^1.2.0", "nodemailer": "^6.9.7", "passport": "^0.7.0", diff --git a/src/features/url/url.service.ts b/src/features/url/url.service.ts index b6eadb7..205b68f 100644 --- a/src/features/url/url.service.ts +++ b/src/features/url/url.service.ts @@ -2,7 +2,7 @@ import { Injectable } from '@nestjs/common'; import { IUrl } from './interfaces/url.interface'; import { Model } from 'mongoose'; import { InjectModel } from '@nestjs/mongoose'; -import * as shortid from 'shortid'; +import * as crypto from 'crypto'; @Injectable() export class UrlService { @@ -15,7 +15,7 @@ export class UrlService { return existingUrl.shortUrl; } - const shortUrl = shortid.generate(); + const shortUrl = this.generateShortUrl(originalUrl); const newUrl = new this.urlModel({ originalUrl, shortUrl }); await newUrl.save(); @@ -27,4 +27,9 @@ export class UrlService { return url ? url.originalUrl : null; } + + private generateShortUrl(originalUrl: string): string { + const hash = crypto.createHash('sha256').update(originalUrl).digest('hex'); + return hash.slice(0, 8); + } } From 75e35bab6050947c10cfe01b2d49189f9dc14f03 Mon Sep 17 00:00:00 2001 From: erik1110 Date: Wed, 24 Jan 2024 10:15:13 +0800 Subject: [PATCH 3/5] [style] 200 -> 201 --- src/features/image/image.controller.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/features/image/image.controller.ts b/src/features/image/image.controller.ts index 4c2425a..8b5507a 100644 --- a/src/features/image/image.controller.ts +++ b/src/features/image/image.controller.ts @@ -1,7 +1,7 @@ -import { Controller, Get, HttpStatus, Param, Post, Res, UploadedFile, UseGuards, UseInterceptors } from '@nestjs/common'; +import { Controller, HttpStatus, Post, UploadedFile, UseGuards, UseInterceptors } from '@nestjs/common'; import { FileInterceptor } from '@nestjs/platform-express'; import { ImageService } from './image.service'; -import { ApiBearerAuth, ApiBody, ApiConsumes, ApiOkResponse, ApiOperation, ApiTags } from '@nestjs/swagger'; +import { ApiBearerAuth, ApiBody, ApiConsumes, ApiCreatedResponse, ApiOperation, ApiTags } from '@nestjs/swagger'; import { FileUploadDto, GetImageSuccessDto } from './dto/fileUpload.dto'; import { UrlService } from '../url/url.service'; import { AuthGuard } from '@nestjs/passport'; @@ -27,7 +27,7 @@ export class ImageController { @UseInterceptors(FileInterceptor('file')) @ApiConsumes('multipart/form-data') @ApiOperation({ summary: '上傳圖片 Upload an image' }) - @ApiOkResponse({ type: GetImageSuccessDto }) + @ApiCreatedResponse({ type: GetImageSuccessDto }) @ApiBody({ description: "網址會由該服務進行轉址 The URL will be redirected by the service.", type: FileUploadDto, From 2dc97557e5ccb800f8eba011ce3303fd42ef370d Mon Sep 17 00:00:00 2001 From: erik1110 Date: Wed, 24 Jan 2024 10:15:32 +0800 Subject: [PATCH 4/5] [style] add 400 description --- src/features/url/url.controller.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/features/url/url.controller.ts b/src/features/url/url.controller.ts index ac864c2..79cec35 100644 --- a/src/features/url/url.controller.ts +++ b/src/features/url/url.controller.ts @@ -1,4 +1,4 @@ -import { Controller, Get, HttpStatus, Param, Redirect, UseGuards } from '@nestjs/common'; +import { Controller, Get, HttpStatus, Param, Redirect } from '@nestjs/common'; import { UrlService } from './url.service'; import { ApiErrorDecorator } from 'src/common/decorators/error/error.decorator'; import { ApiOperation, ApiTags } from '@nestjs/swagger'; @@ -16,6 +16,7 @@ export class UrlController { @Get(':shortUrl') @ApiOperation({ summary: '轉址短網址 Redirect short URL' }) + @ApiErrorDecorator(HttpStatus.BAD_REQUEST, 'UserError', '無此網址或網址已失效') @Redirect() async redirectToOriginalUrl(@Param('shortUrl') shortUrl: string): Promise<{ url: string }> { const originalUrl = await this.urlService.getOriginalUrl(shortUrl); From 1cf43f7b7f515a23f3e037b36ccfc79ce12eb15f Mon Sep 17 00:00:00 2001 From: erik1110 Date: Wed, 24 Jan 2024 10:18:38 +0800 Subject: [PATCH 5/5] [style] prettier --- src/app.controller.spec.ts | 2 +- .../decorators/validation/dto.decorator.ts | 52 +-- src/features/culinary/culinary.controller.ts | 36 +- src/features/culinary/culinary.module.ts | 5 +- src/features/culinary/culinary.service.ts | 2 +- src/features/culinary/dto/culinary.dto.ts | 4 +- src/features/image/dto/fileUpload.dto.ts | 28 +- src/features/image/firebase.service.ts | 49 +-- src/features/image/image.controller.ts | 75 ++-- src/features/image/image.module.ts | 3 +- src/features/image/image.service.ts | 123 +++--- src/features/news/dto/news.dto.ts | 4 +- src/features/news/news.controller.ts | 37 +- src/features/news/news.service.ts | 2 +- src/features/order/dto/order.dto.ts | 19 +- src/features/order/dto/user.dto.ts | 77 ++-- .../order/interfaces/order.interface.ts | 8 +- src/features/order/order.controller.ts | 191 +++++---- src/features/order/order.module.ts | 18 +- src/features/order/order.service.ts | 365 ++++++++++-------- src/features/order/schemas/order.schema.ts | 2 +- src/features/room/dto/item.dto.ts | 1 - src/features/room/dto/room.dto.ts | 81 ++-- .../room/interfaces/item.interface.ts | 6 +- .../room/interfaces/room.interface.ts | 1 - src/features/room/room.controller.ts | 115 +++--- src/features/room/room.module.ts | 2 +- src/features/room/room.service.ts | 152 ++++---- src/features/room/schemas/room.schema.ts | 1 - src/features/url/interfaces/url.interface.ts | 4 +- src/features/url/schemas/url.schemas.ts | 1 - src/features/url/url.controller.ts | 16 +- src/features/url/url.module.ts | 12 +- src/features/url/url.service.ts | 48 +-- src/main.ts | 6 +- 35 files changed, 846 insertions(+), 702 deletions(-) diff --git a/src/app.controller.spec.ts b/src/app.controller.spec.ts index e1ffae6..c5f7314 100644 --- a/src/app.controller.spec.ts +++ b/src/app.controller.spec.ts @@ -18,4 +18,4 @@ describe('AppController', () => { expect(appController.root()).toBe('Health Check'); }); }); -}); \ No newline at end of file +}); diff --git a/src/common/decorators/validation/dto.decorator.ts b/src/common/decorators/validation/dto.decorator.ts index 7d96bf3..da72181 100644 --- a/src/common/decorators/validation/dto.decorator.ts +++ b/src/common/decorators/validation/dto.decorator.ts @@ -1,6 +1,12 @@ -import { ValidationOptions, registerDecorator, ValidationArguments } from "class-validator"; +import { + ValidationOptions, + registerDecorator, + ValidationArguments, +} from 'class-validator'; -export function IsNotBeforeToday(validationOptions?: ValidationOptions): PropertyDecorator { +export function IsNotBeforeToday( + validationOptions?: ValidationOptions, +): PropertyDecorator { return function (object: Record, propertyName: string) { registerDecorator({ name: 'isNotBeforeToday', @@ -20,24 +26,26 @@ export function IsNotBeforeToday(validationOptions?: ValidationOptions): Propert }; } - -export function IsBefore(property: string, validationOptions?: ValidationOptions) { - return function (object: Record, propertyName: string) { - registerDecorator({ - name: 'isBefore', - target: object.constructor, - propertyName: propertyName, - constraints: [property], - options: validationOptions, - validator: { - validate(value: any, args: ValidationArguments): boolean { - const relatedValue = args.object[property]; - if (value instanceof Date && relatedValue instanceof Date) { - return value > relatedValue; - } - return false; - }, +export function IsBefore( + property: string, + validationOptions?: ValidationOptions, +) { + return function (object: Record, propertyName: string) { + registerDecorator({ + name: 'isBefore', + target: object.constructor, + propertyName: propertyName, + constraints: [property], + options: validationOptions, + validator: { + validate(value: any, args: ValidationArguments): boolean { + const relatedValue = args.object[property]; + if (value instanceof Date && relatedValue instanceof Date) { + return value > relatedValue; + } + return false; }, - }); - }; - } \ No newline at end of file + }, + }); + }; +} diff --git a/src/features/culinary/culinary.controller.ts b/src/features/culinary/culinary.controller.ts index ded4ceb..420ac57 100644 --- a/src/features/culinary/culinary.controller.ts +++ b/src/features/culinary/culinary.controller.ts @@ -33,7 +33,6 @@ import { import { IsObjectIdPipe } from 'nestjs-object-id'; import { AuthGuard } from '@nestjs/passport'; - @ApiTags('Home/Culinary - 美味佳餚') @ApiErrorDecorator( HttpStatus.INTERNAL_SERVER_ERROR, @@ -44,25 +43,26 @@ import { AuthGuard } from '@nestjs/passport'; @ApiBearerAuth() @Controller('/api/v1/home/culinary') export class CulinaryController { - constructor(private readonly culinaryService: CulinaryService) {} + constructor(private readonly culinaryService: CulinaryService) {} - @Get('') - @HttpCode(HttpStatus.OK) - @ApiOperation({ summary: '取得所有美味佳餚 Get all delicious dishes' }) - @ApiOkResponse({ type: GetCulinarySuccessDto }) - async getallCulinary(@Req() req: Request) { - return await this.culinaryService.getallCulinary(req); - } + @Get('') + @HttpCode(HttpStatus.OK) + @ApiOperation({ summary: '取得所有美味佳餚 Get all delicious dishes' }) + @ApiOkResponse({ type: GetCulinarySuccessDto }) + async getallCulinary(@Req() req: Request) { + return await this.culinaryService.getallCulinary(req); + } - @Get(':id') - @HttpCode(HttpStatus.OK) - @ApiOperation({ summary: '取得單筆美味佳餚 Get one delicious dish' }) - @ApiOkResponse({ type: GetOneCulinarySuccessDto }) - async getOneCulinary( - @Param('id', IsObjectIdPipe) id: string, - @Req() req: Request) { - return await this.culinaryService.getOneCulinary(id, req); - } + @Get(':id') + @HttpCode(HttpStatus.OK) + @ApiOperation({ summary: '取得單筆美味佳餚 Get one delicious dish' }) + @ApiOkResponse({ type: GetOneCulinarySuccessDto }) + async getOneCulinary( + @Param('id', IsObjectIdPipe) id: string, + @Req() req: Request, + ) { + return await this.culinaryService.getOneCulinary(id, req); + } } @ApiTags('Admin/Culinary - 美味佳餚管理') diff --git a/src/features/culinary/culinary.module.ts b/src/features/culinary/culinary.module.ts index e60598d..b2e31ec 100644 --- a/src/features/culinary/culinary.module.ts +++ b/src/features/culinary/culinary.module.ts @@ -1,5 +1,8 @@ import { Module } from '@nestjs/common'; -import { CulinaryAdminController, CulinaryController } from './culinary.controller'; +import { + CulinaryAdminController, + CulinaryController, +} from './culinary.controller'; import { CulinaryService } from './culinary.service'; import { CulinarySchema } from './schemas/culinary.schema'; import { MongooseModule } from '@nestjs/mongoose'; diff --git a/src/features/culinary/culinary.service.ts b/src/features/culinary/culinary.service.ts index ee34b14..1ac364e 100644 --- a/src/features/culinary/culinary.service.ts +++ b/src/features/culinary/culinary.service.ts @@ -24,7 +24,7 @@ export class CulinaryService { async getallCulinary(req: Request) { const result = await this.culinaryModel.find(); - const ids = result.map(order => order._id.toString()); + const ids = result.map((order) => order._id.toString()); return getHttpResponse.successResponse({ message: '取得所有美味佳餚', data: ids, diff --git a/src/features/culinary/dto/culinary.dto.ts b/src/features/culinary/dto/culinary.dto.ts index c835d09..c123308 100644 --- a/src/features/culinary/dto/culinary.dto.ts +++ b/src/features/culinary/dto/culinary.dto.ts @@ -114,9 +114,7 @@ export class GetCulinarySuccessDto { message: string; @ApiProperty({ - example: [ - "658e985c1c91c1765e2972b5", - ], + example: ['658e985c1c91c1765e2972b5'], }) data: object; } diff --git a/src/features/image/dto/fileUpload.dto.ts b/src/features/image/dto/fileUpload.dto.ts index 9f5f280..1a725f9 100644 --- a/src/features/image/dto/fileUpload.dto.ts +++ b/src/features/image/dto/fileUpload.dto.ts @@ -1,19 +1,19 @@ -import { ApiProperty } from "@nestjs/swagger"; +import { ApiProperty } from '@nestjs/swagger'; export class FileUploadDto { - @ApiProperty({ type: 'string', format: 'binary' }) - file: any; + @ApiProperty({ type: 'string', format: 'binary' }) + file: any; } export class GetImageSuccessDto { - @ApiProperty({ example: true }) - status: boolean; - - @ApiProperty({ example: '取得圖片網址' }) - message: string; - - @ApiProperty({ - example: "http://localhost:3000/api/v1/url/HSIARDTXC" - }) - data: object; - } \ No newline at end of file + @ApiProperty({ example: true }) + status: boolean; + + @ApiProperty({ example: '取得圖片網址' }) + message: string; + + @ApiProperty({ + example: 'http://localhost:3000/api/v1/url/HSIARDTXC', + }) + data: object; +} diff --git a/src/features/image/firebase.service.ts b/src/features/image/firebase.service.ts index d7964df..1ca8d43 100644 --- a/src/features/image/firebase.service.ts +++ b/src/features/image/firebase.service.ts @@ -3,30 +3,33 @@ import * as admin from 'firebase-admin'; @Injectable() export class FirebaseService { - private readonly storage: admin.storage.Storage; + private readonly storage: admin.storage.Storage; - readonly firebase_params = { - type: process.env.FIREBASE_TYPE, - project_id: process.env.FIREBASE_PROJECT_ID, - private_key_id: process.env.FIREBASE_PRIVATE_KEY_ID, - private_key: process.env.FIREBASE_PRIVATE_KEY.replace(/\\n/g, '\n'), - client_email: process.env.FIREBASE_CLIENT_EMAIL, - client_id: process.env.FIREBASE_CLIENT_ID, - auth_uri: process.env.FIREBASE_AUTH_URI, - token_uri: process.env.FIREBASE_TOKEN_URI, - auth_provider_X509_cert_url: process.env.FIREBASE_AUTH_PROVIDER_X509_CERT_URL, - client_x509_cert_url: process.env.FIREBASE_CLIENT_X509_CERT_URL, - }; + readonly firebase_params = { + type: process.env.FIREBASE_TYPE, + project_id: process.env.FIREBASE_PROJECT_ID, + private_key_id: process.env.FIREBASE_PRIVATE_KEY_ID, + private_key: process.env.FIREBASE_PRIVATE_KEY.replace(/\\n/g, '\n'), + client_email: process.env.FIREBASE_CLIENT_EMAIL, + client_id: process.env.FIREBASE_CLIENT_ID, + auth_uri: process.env.FIREBASE_AUTH_URI, + token_uri: process.env.FIREBASE_TOKEN_URI, + auth_provider_X509_cert_url: + process.env.FIREBASE_AUTH_PROVIDER_X509_CERT_URL, + client_x509_cert_url: process.env.FIREBASE_CLIENT_X509_CERT_URL, + }; - constructor(){ - admin.initializeApp({ - credential: admin.credential.cert(this.firebase_params as admin.ServiceAccount), - storageBucket: `${process.env.FIREBASE_PROJECT_ID}.appspot.com`, - }); - this.storage = admin.storage(); - } + constructor() { + admin.initializeApp({ + credential: admin.credential.cert( + this.firebase_params as admin.ServiceAccount, + ), + storageBucket: `${process.env.FIREBASE_PROJECT_ID}.appspot.com`, + }); + this.storage = admin.storage(); + } - getStorageInstance(): admin.storage.Storage{ - return this.storage; - } + getStorageInstance(): admin.storage.Storage { + return this.storage; + } } diff --git a/src/features/image/image.controller.ts b/src/features/image/image.controller.ts index 8b5507a..57d88a0 100644 --- a/src/features/image/image.controller.ts +++ b/src/features/image/image.controller.ts @@ -1,7 +1,21 @@ -import { Controller, HttpStatus, Post, UploadedFile, UseGuards, UseInterceptors } from '@nestjs/common'; +import { + Controller, + HttpStatus, + Post, + UploadedFile, + UseGuards, + UseInterceptors, +} from '@nestjs/common'; import { FileInterceptor } from '@nestjs/platform-express'; import { ImageService } from './image.service'; -import { ApiBearerAuth, ApiBody, ApiConsumes, ApiCreatedResponse, ApiOperation, ApiTags } from '@nestjs/swagger'; +import { + ApiBearerAuth, + ApiBody, + ApiConsumes, + ApiCreatedResponse, + ApiOperation, + ApiTags, +} from '@nestjs/swagger'; import { FileUploadDto, GetImageSuccessDto } from './dto/fileUpload.dto'; import { UrlService } from '../url/url.service'; import { AuthGuard } from '@nestjs/passport'; @@ -18,34 +32,35 @@ import { getHttpResponse } from 'src/utils/successHandler'; @ApiBearerAuth() @Controller('/api/v1/image') export class ImageController { - constructor( - private readonly imageService: ImageService, - private readonly urlService: UrlService, - ) {} + constructor( + private readonly imageService: ImageService, + private readonly urlService: UrlService, + ) {} - @Post('upload') - @UseInterceptors(FileInterceptor('file')) - @ApiConsumes('multipart/form-data') - @ApiOperation({ summary: '上傳圖片 Upload an image' }) - @ApiCreatedResponse({ type: GetImageSuccessDto }) - @ApiBody({ - description: "網址會由該服務進行轉址 The URL will be redirected by the service.", - type: FileUploadDto, - }) - async uploadImage(@UploadedFile() file){ - const imageUrl = await this.imageService.uploadImage(file); - let shortenUrl = await this.urlService.shortenUrl(imageUrl) - if (process.env.NODE_ENV === 'dev') { - shortenUrl = `http://localhost:${process.env.PORT}/api/v1/url/`; - } else { - shortenUrl = `${process.env.PRODUCTION_URL}/api/v1/url/`; - } - - const shortenedUrl = await this.urlService.shortenUrl(imageUrl); - const redirectUrl = shortenUrl + shortenedUrl; - return getHttpResponse.successResponse({ - message: '取得圖片網址', - data: redirectUrl, - }); + @Post('upload') + @UseInterceptors(FileInterceptor('file')) + @ApiConsumes('multipart/form-data') + @ApiOperation({ summary: '上傳圖片 Upload an image' }) + @ApiCreatedResponse({ type: GetImageSuccessDto }) + @ApiBody({ + description: + '網址會由該服務進行轉址 The URL will be redirected by the service.', + type: FileUploadDto, + }) + async uploadImage(@UploadedFile() file) { + const imageUrl = await this.imageService.uploadImage(file); + let shortenUrl = await this.urlService.shortenUrl(imageUrl); + if (process.env.NODE_ENV === 'dev') { + shortenUrl = `http://localhost:${process.env.PORT}/api/v1/url/`; + } else { + shortenUrl = `${process.env.PRODUCTION_URL}/api/v1/url/`; } + + const shortenedUrl = await this.urlService.shortenUrl(imageUrl); + const redirectUrl = shortenUrl + shortenedUrl; + return getHttpResponse.successResponse({ + message: '取得圖片網址', + data: redirectUrl, + }); + } } diff --git a/src/features/image/image.module.ts b/src/features/image/image.module.ts index 114f57a..8c8e364 100644 --- a/src/features/image/image.module.ts +++ b/src/features/image/image.module.ts @@ -2,12 +2,11 @@ import { Module } from '@nestjs/common'; import { FirebaseService } from './firebase.service'; import { ImageService } from './image.service'; import { ImageController } from './image.controller'; -import { UrlService } from '../url/url.service'; import { UrlModule } from '../url/url.module'; @Module({ imports: [UrlModule], controllers: [ImageController], - providers: [FirebaseService, ImageService] + providers: [FirebaseService, ImageService], }) export class ImageModule {} diff --git a/src/features/image/image.service.ts b/src/features/image/image.service.ts index 797fe34..acf80ba 100644 --- a/src/features/image/image.service.ts +++ b/src/features/image/image.service.ts @@ -1,77 +1,82 @@ -import { HttpStatus, Injectable } from "@nestjs/common"; -import { FirebaseService } from "./firebase.service"; +import { HttpStatus, Injectable } from '@nestjs/common'; +import { FirebaseService } from './firebase.service'; import { v4 as uuidv4 } from 'uuid'; import { GetSignedUrlConfig } from '@google-cloud/storage'; -import { AppError } from "src/utils/appError"; +import { AppError } from 'src/utils/appError'; const maxSize = 3 * 1024 * 1024; // 3 MB in bytes const allowedExtensions = ['png', 'jpg', 'jpeg', 'webp']; @Injectable() export class ImageService { - constructor(private readonly firebaseService: FirebaseService){} + constructor(private readonly firebaseService: FirebaseService) {} - async uploadImage(file): Promise { - if (!file || !file.originalname) { - throw new AppError(HttpStatus.BAD_REQUEST, 'UserError', '無效的檔案'); - } - if (file.size > maxSize) { - throw new AppError(HttpStatus.BAD_REQUEST, 'UserError', '超過 3 MB'); - } - const fileExtension = file.originalname.split('.').pop()?.toLowerCase(); - if (!fileExtension || !allowedExtensions.includes(fileExtension)) { - throw new AppError(HttpStatus.BAD_REQUEST, 'UserError', '不支援的檔案格式'); - } - const storage = this.firebaseService.getStorageInstance(); - const bucket = storage.bucket(); - const encodedOriginalName = encodeURIComponent(file.originalname); - const fileName = `${uuidv4()}_${encodedOriginalName}`; - const fileUpload = bucket.file(fileName); - const stream = fileUpload.createWriteStream({ - metadata: { - contentType: file.mimetype, - } - }); + async uploadImage(file): Promise { + if (!file || !file.originalname) { + throw new AppError(HttpStatus.BAD_REQUEST, 'UserError', '無效的檔案'); + } + if (file.size > maxSize) { + throw new AppError(HttpStatus.BAD_REQUEST, 'UserError', '超過 3 MB'); + } + const fileExtension = file.originalname.split('.').pop()?.toLowerCase(); + if (!fileExtension || !allowedExtensions.includes(fileExtension)) { + throw new AppError( + HttpStatus.BAD_REQUEST, + 'UserError', + '不支援的檔案格式', + ); + } + const storage = this.firebaseService.getStorageInstance(); + const bucket = storage.bucket(); + const encodedOriginalName = encodeURIComponent(file.originalname); + const fileName = `${uuidv4()}_${encodedOriginalName}`; + const fileUpload = bucket.file(fileName); + const stream = fileUpload.createWriteStream({ + metadata: { + contentType: file.mimetype, + }, + }); - return new Promise((resolve, reject) => { - stream.on('error', (error) =>{ - reject(error); - }); + return new Promise((resolve, reject) => { + stream.on('error', (error) => { + reject(error); + }); - stream.on('finish', async () => { - const imageUrl = await this.getFirebaseUrl(fileUpload); - resolve(imageUrl); - }); + stream.on('finish', async () => { + const imageUrl = await this.getFirebaseUrl(fileUpload); + resolve(imageUrl); + }); - stream.end(file.buffer); - }); - } + stream.end(file.buffer); + }); + } - private async getFirebaseUrl(fileUpload): Promise { - return new Promise((resolve, reject) => { - const blobStream = fileUpload.createReadStream(); + private async getFirebaseUrl(fileUpload): Promise { + return new Promise((resolve, reject) => { + const blobStream = fileUpload.createReadStream(); - const bucket = this.firebaseService.getStorageInstance().bucket(); - const blob = bucket.file(fileUpload.name); + const bucket = this.firebaseService.getStorageInstance().bucket(); + const blob = bucket.file(fileUpload.name); - blobStream.pipe(blob.createWriteStream()) - .on('finish', () => { - const config: GetSignedUrlConfig = { - action: 'read', - expires: '12-31-2024', - }; + blobStream + .pipe(blob.createWriteStream()) + .on('finish', () => { + const config: GetSignedUrlConfig = { + action: 'read', + expires: '12-31-2024', + }; - blob.getSignedUrl(config, (err, imgUrl) => { - if (err) { - reject(err); - } else { - resolve(imgUrl); - } - }); - }) - .on('error', (err) => { - reject(err); - }); + blob.getSignedUrl(config, (err, imgUrl) => { + if (err) { + reject(err); + } else { + resolve(imgUrl); + } + }); + }) + .on('error', (err) => { + reject(err); }); - } + }); + } } diff --git a/src/features/news/dto/news.dto.ts b/src/features/news/dto/news.dto.ts index c71e177..a2110ad 100644 --- a/src/features/news/dto/news.dto.ts +++ b/src/features/news/dto/news.dto.ts @@ -54,9 +54,7 @@ export class GetNewsSuccessDto { message: string; @ApiProperty({ - example: [ - "658e985c1c91c1765e2972b5", - ], + example: ['658e985c1c91c1765e2972b5'], }) data: object; } diff --git a/src/features/news/news.controller.ts b/src/features/news/news.controller.ts index 06ad47c..e67f49a 100644 --- a/src/features/news/news.controller.ts +++ b/src/features/news/news.controller.ts @@ -32,7 +32,6 @@ import { RolesGuard } from 'src/auth/guards/roles.guard'; import { IsObjectIdPipe } from 'nestjs-object-id'; import { AuthGuard } from '@nestjs/passport'; - @ApiTags('Home/News - 最新消息') @ApiErrorDecorator( HttpStatus.INTERNAL_SERVER_ERROR, @@ -43,26 +42,26 @@ import { AuthGuard } from '@nestjs/passport'; @ApiBearerAuth() @Controller('/api/v1/home/news') export class NewsController { - constructor(private readonly newsService: NewsService) {} - - @Get('') - @HttpCode(HttpStatus.OK) - @ApiOperation({ summary: '取得所有最新消息 Get all latest news' }) - @ApiOkResponse({ type: GetNewsSuccessDto }) - async getallNews(@Req() req: Request) { - return await this.newsService.getallNews(req); - } + constructor(private readonly newsService: NewsService) {} - @Get(':id') - @HttpCode(HttpStatus.OK) - @ApiOperation({ summary: '取得單筆最新消息 Get one latest news' }) - @ApiOkResponse({ type: GetOneNewsSuccessDto }) - async getOneNews( - @Param('id', IsObjectIdPipe) id: string, - @Req() req: Request) { - return await this.newsService.getOneNews(id, req); - } + @Get('') + @HttpCode(HttpStatus.OK) + @ApiOperation({ summary: '取得所有最新消息 Get all latest news' }) + @ApiOkResponse({ type: GetNewsSuccessDto }) + async getallNews(@Req() req: Request) { + return await this.newsService.getallNews(req); + } + @Get(':id') + @HttpCode(HttpStatus.OK) + @ApiOperation({ summary: '取得單筆最新消息 Get one latest news' }) + @ApiOkResponse({ type: GetOneNewsSuccessDto }) + async getOneNews( + @Param('id', IsObjectIdPipe) id: string, + @Req() req: Request, + ) { + return await this.newsService.getOneNews(id, req); + } } @ApiTags('Admin/News - 最新消息管理') diff --git a/src/features/news/news.service.ts b/src/features/news/news.service.ts index 13b3eaa..e6ae5e7 100644 --- a/src/features/news/news.service.ts +++ b/src/features/news/news.service.ts @@ -22,7 +22,7 @@ export class NewsService { async getallNews(req: Request) { const result = await this.newsModel.find(); - const ids = result.map(order => order._id.toString()); + const ids = result.map((order) => order._id.toString()); return getHttpResponse.successResponse({ message: '取得所有最新資訊', data: ids, diff --git a/src/features/order/dto/order.dto.ts b/src/features/order/dto/order.dto.ts index 74c90e2..fe376ac 100644 --- a/src/features/order/dto/order.dto.ts +++ b/src/features/order/dto/order.dto.ts @@ -1,9 +1,19 @@ import { ApiProperty } from '@nestjs/swagger'; import { Transform, Type } from 'class-transformer'; -import { IsDate, IsNotEmpty, Matches, ValidateNested, ValidationOptions, registerDecorator } from 'class-validator'; +import { + IsDate, + IsNotEmpty, + Matches, + ValidateNested, + ValidationOptions, + registerDecorator, +} from 'class-validator'; import { Schema } from 'mongoose'; import { UserDto } from './user.dto'; -import { IsBefore, IsNotBeforeToday } from 'src/common/decorators/validation/dto.decorator'; +import { + IsBefore, + IsNotBeforeToday, +} from 'src/common/decorators/validation/dto.decorator'; export class CreateOrderDto { @ApiProperty({ @@ -47,7 +57,6 @@ export class CreateOrderDto { @ValidateNested({ each: true }) @Type(() => UserDto) userInfo: UserDto; - } export class CreateOrderSuccessDto { @@ -90,9 +99,7 @@ export class GetOrderSuccessDto { message: string; @ApiProperty({ - example: [ - "658e985c1c91c1765e2972b5" - ], + example: ['658e985c1c91c1765e2972b5'], }) data: object; } diff --git a/src/features/order/dto/user.dto.ts b/src/features/order/dto/user.dto.ts index 5310223..5d0b994 100644 --- a/src/features/order/dto/user.dto.ts +++ b/src/features/order/dto/user.dto.ts @@ -1,51 +1,58 @@ import { ApiProperty } from '@nestjs/swagger'; import { Type } from 'class-transformer'; -import { IsEmail, IsNotEmpty, IsString, Matches, MaxLength, MinLength, ValidateNested } from 'class-validator'; +import { + IsEmail, + IsNotEmpty, + IsString, + Matches, + MaxLength, + MinLength, + ValidateNested, +} from 'class-validator'; import { AddressDto } from 'src/features/user/dto/address'; export class UserDto { - - @ApiProperty({ + @ApiProperty({ type: AddressDto, description: 'Address', - }) - @ValidateNested({ each: true }) - @Type(() => AddressDto) - address: AddressDto; + }) + @ValidateNested({ each: true }) + @Type(() => AddressDto) + address: AddressDto; + + @ApiProperty({ + example: 'john', + description: 'Name', + format: 'string', + minLength: 2, + maxLength: 255, + }) + @IsNotEmpty() + @IsString() + @MinLength(2) + @MaxLength(255) + readonly name: string; - @ApiProperty({ - example: 'john', - description: 'Name', - format: 'string', - minLength: 2, - maxLength: 255, - }) - @IsNotEmpty() - @IsString() - @MinLength(2) - @MaxLength(255) - readonly name: string; - - @ApiProperty({ + @ApiProperty({ example: 'test@example.com', description: 'Email', format: 'email', uniqueItems: true, minLength: 5, maxLength: 255, - }) - @IsNotEmpty() - @IsString() - @MinLength(5) - @MaxLength(255) - @IsEmail() - readonly email: string; + }) + @IsNotEmpty() + @IsString() + @MinLength(5) + @MaxLength(255) + @IsEmail() + readonly email: string; - @ApiProperty({ - example: '0912345678', - description: 'Phone', - }) - @IsNotEmpty() - @Matches(/^09\d{8}$/, { message: 'Invalid Phone' }) - phone: string; + @ApiProperty({ + example: '0912345678', + description: 'Phone', + }) + @IsNotEmpty() + @Matches(/^09\d{8}$/, { message: 'Invalid Phone' }) + phone: string; } diff --git a/src/features/order/interfaces/order.interface.ts b/src/features/order/interfaces/order.interface.ts index 6854ce6..021bf64 100644 --- a/src/features/order/interfaces/order.interface.ts +++ b/src/features/order/interfaces/order.interface.ts @@ -2,7 +2,7 @@ import { Schema, Document } from 'mongoose'; export interface IOrder extends Document { roomId: Schema.Types.ObjectId; - checkInDate: Date, + checkInDate: Date; checkOutDate: Date; peopleNum: number; orderUserId: Schema.Types.ObjectId; @@ -11,9 +11,9 @@ export interface IOrder extends Document { phone: string; email: string; address: { - zipcode: number; - county: string; - city: string; + zipcode: number; + county: string; + city: string; }; }; // 可使用:1,已刪除:-1 diff --git a/src/features/order/order.controller.ts b/src/features/order/order.controller.ts index af6efc2..10a43ca 100644 --- a/src/features/order/order.controller.ts +++ b/src/features/order/order.controller.ts @@ -1,14 +1,37 @@ -import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Param, Post, Put, Req, UseGuards } from '@nestjs/common'; -import { ApiBearerAuth, ApiOkResponse, ApiOperation, ApiTags } from '@nestjs/swagger'; +import { + Body, + Controller, + Delete, + Get, + HttpCode, + HttpStatus, + Param, + Post, + Put, + Req, + UseGuards, +} from '@nestjs/common'; +import { + ApiBearerAuth, + ApiOkResponse, + ApiOperation, + ApiTags, +} from '@nestjs/swagger'; import { RolesGuard } from 'src/auth/guards/roles.guard'; import { ApiErrorDecorator } from 'src/common/decorators/error/error.decorator'; import { OrderService } from './order.service'; import { Roles } from 'src/auth/decorators/roles.decorator'; -import { CreateOrderDto, CreateOrderSuccessDto, DeleteOrderSuccessDto, GetOneOrderSuccessDto, GetOrderSuccessDto, UpdateOrderSuccessDto } from './dto/order.dto'; +import { + CreateOrderDto, + CreateOrderSuccessDto, + DeleteOrderSuccessDto, + GetOneOrderSuccessDto, + GetOrderSuccessDto, + UpdateOrderSuccessDto, +} from './dto/order.dto'; import { AuthGuard } from '@nestjs/passport'; import { IsObjectIdPipe } from 'nestjs-object-id'; - @ApiTags('Orders - 訂單') @ApiErrorDecorator( HttpStatus.INTERNAL_SERVER_ERROR, @@ -19,48 +42,49 @@ import { IsObjectIdPipe } from 'nestjs-object-id'; @ApiBearerAuth() @Controller('/api/v1/orders') export class OrderController { - constructor(private readonly orderService: OrderService) {} - + constructor(private readonly orderService: OrderService) {} - @Get('') - @HttpCode(HttpStatus.OK) - @ApiOperation({ summary: '取得自己的訂單列表 Get My Orders (刪除狀態訂單無法查詢)' }) - @ApiOkResponse({ type: GetOrderSuccessDto }) - async getMyOrders(@Req() req: Request) { - return await this.orderService.getMyOrders(req); - } + @Get('') + @HttpCode(HttpStatus.OK) + @ApiOperation({ + summary: '取得自己的訂單列表 Get My Orders (刪除狀態訂單無法查詢)', + }) + @ApiOkResponse({ type: GetOrderSuccessDto }) + async getMyOrders(@Req() req: Request) { + return await this.orderService.getMyOrders(req); + } - @Post('') - @HttpCode(HttpStatus.OK) - @ApiOperation({ summary: '新增訂單 Add an order' }) - @ApiOkResponse({ type: CreateOrderSuccessDto }) - async addOrder(@Req() req: Request, @Body() createOrderDto: CreateOrderDto) { - return await this.orderService.createOrder(req, createOrderDto); - } + @Post('') + @HttpCode(HttpStatus.OK) + @ApiOperation({ summary: '新增訂單 Add an order' }) + @ApiOkResponse({ type: CreateOrderSuccessDto }) + async addOrder(@Req() req: Request, @Body() createOrderDto: CreateOrderDto) { + return await this.orderService.createOrder(req, createOrderDto); + } - @Get(':id') - @HttpCode(HttpStatus.OK) - @ApiOperation({ summary: '取得自己訂單詳細資料 Get My Orders Detail ' }) - @ApiOkResponse({ type: GetOneOrderSuccessDto }) - async getMyOrderDetail( - @Param('id', IsObjectIdPipe) id: string, - @Req() req: Request) { - return await this.orderService.getMyOrderDetail(id, req); - } + @Get(':id') + @HttpCode(HttpStatus.OK) + @ApiOperation({ summary: '取得自己訂單詳細資料 Get My Orders Detail ' }) + @ApiOkResponse({ type: GetOneOrderSuccessDto }) + async getMyOrderDetail( + @Param('id', IsObjectIdPipe) id: string, + @Req() req: Request, + ) { + return await this.orderService.getMyOrderDetail(id, req); + } - @Delete(':id') - @HttpCode(HttpStatus.OK) - @ApiOperation({ summary: '刪除自己訂單 Delete My order' }) - @ApiOkResponse({ type: DeleteOrderSuccessDto }) - async deleteNews( - @Param('id', IsObjectIdPipe) id: string, - @Req() req: Request, - ) { - return await this.orderService.deleteMyOrder(id, req); - } + @Delete(':id') + @HttpCode(HttpStatus.OK) + @ApiOperation({ summary: '刪除自己訂單 Delete My order' }) + @ApiOkResponse({ type: DeleteOrderSuccessDto }) + async deleteNews( + @Param('id', IsObjectIdPipe) id: string, + @Req() req: Request, + ) { + return await this.orderService.deleteMyOrder(id, req); + } } - @ApiTags('Admin/Orders - 訂單管理') @UseGuards(RolesGuard) @ApiBearerAuth() @@ -72,50 +96,53 @@ export class OrderController { ) @Controller('/api/v1/admin/orders') export class OrderAdminController { - constructor(private readonly orderService: OrderService) {} + constructor(private readonly orderService: OrderService) {} - @Get('') - @Roles('admin') - @HttpCode(HttpStatus.OK) - @ApiOperation({ summary: '取得所有訂單 Get all orders (包含刪除狀態的訂單)' }) - @ApiOkResponse({ type: GetOrderSuccessDto }) - async getallOrders(@Req() req: Request) { - return await this.orderService.getallOrders(req); - } + @Get('') + @Roles('admin') + @HttpCode(HttpStatus.OK) + @ApiOperation({ summary: '取得所有訂單 Get all orders (包含刪除狀態的訂單)' }) + @ApiOkResponse({ type: GetOrderSuccessDto }) + async getallOrders(@Req() req: Request) { + return await this.orderService.getallOrders(req); + } - @Get(':id') - @Roles('admin') - @HttpCode(HttpStatus.OK) - @ApiOperation({ summary: '取得訂單詳細資料 Get My Orders Detail (包含刪除狀態的訂單)' }) - @ApiOkResponse({ type: GetOneOrderSuccessDto }) - async getMyOrderDetail( - @Param('id', IsObjectIdPipe) id: string, - @Req() req: Request) { - return await this.orderService.getMyOrderAdmin(id, req); - } + @Get(':id') + @Roles('admin') + @HttpCode(HttpStatus.OK) + @ApiOperation({ + summary: '取得訂單詳細資料 Get My Orders Detail (包含刪除狀態的訂單)', + }) + @ApiOkResponse({ type: GetOneOrderSuccessDto }) + async getMyOrderDetail( + @Param('id', IsObjectIdPipe) id: string, + @Req() req: Request, + ) { + return await this.orderService.getMyOrderAdmin(id, req); + } - @Put(':id') - @Roles('admin') - @HttpCode(HttpStatus.OK) - @ApiOperation({ summary: '修改訂單 Update the order' }) - @ApiOkResponse({ type: UpdateOrderSuccessDto }) - async updateOrderAdmin( - @Param('id', IsObjectIdPipe) id: string, - @Req() req: Request, - @Body() updateOrderDto: CreateOrderDto, - ) { - return await this.orderService.updateOrderAdmin(id, req, updateOrderDto); - } + @Put(':id') + @Roles('admin') + @HttpCode(HttpStatus.OK) + @ApiOperation({ summary: '修改訂單 Update the order' }) + @ApiOkResponse({ type: UpdateOrderSuccessDto }) + async updateOrderAdmin( + @Param('id', IsObjectIdPipe) id: string, + @Req() req: Request, + @Body() updateOrderDto: CreateOrderDto, + ) { + return await this.orderService.updateOrderAdmin(id, req, updateOrderDto); + } - @Delete(':id') - @Roles('admin') - @HttpCode(HttpStatus.OK) - @ApiOperation({ summary: '刪除訂單 Delete My order' }) - @ApiOkResponse({ type: DeleteOrderSuccessDto }) - async deleteOrderAdmin( - @Param('id', IsObjectIdPipe) id: string, - @Req() req: Request, - ) { - return await this.orderService.deleteOrderAdmin(id, req); - } + @Delete(':id') + @Roles('admin') + @HttpCode(HttpStatus.OK) + @ApiOperation({ summary: '刪除訂單 Delete My order' }) + @ApiOkResponse({ type: DeleteOrderSuccessDto }) + async deleteOrderAdmin( + @Param('id', IsObjectIdPipe) id: string, + @Req() req: Request, + ) { + return await this.orderService.deleteOrderAdmin(id, req); + } } diff --git a/src/features/order/order.module.ts b/src/features/order/order.module.ts index db79162..1940ff4 100644 --- a/src/features/order/order.module.ts +++ b/src/features/order/order.module.ts @@ -6,13 +6,13 @@ import { OrderSchema } from './schemas/order.schema'; import { RoomSchema } from '../room/schemas/room.schema'; @Module({ - imports: [ - MongooseModule.forFeature([ - { name: 'Order', schema: OrderSchema }, - { name: 'Room', schema: RoomSchema }, - ]), - ], - controllers: [OrderController, OrderAdminController], - providers: [OrderService] - }) + imports: [ + MongooseModule.forFeature([ + { name: 'Order', schema: OrderSchema }, + { name: 'Room', schema: RoomSchema }, + ]), + ], + controllers: [OrderController, OrderAdminController], + providers: [OrderService], +}) export class OrderModule {} diff --git a/src/features/order/order.service.ts b/src/features/order/order.service.ts index ac38ab9..ee09c29 100644 --- a/src/features/order/order.service.ts +++ b/src/features/order/order.service.ts @@ -9,181 +9,212 @@ import { AppError } from 'src/utils/appError'; @Injectable() export class OrderService { - constructor(@InjectModel('Order') private readonly orderModel: Model, - @InjectModel('Room') private readonly roomModel: Model) {} - + constructor( + @InjectModel('Order') private readonly orderModel: Model, + @InjectModel('Room') private readonly roomModel: Model, + ) {} - async createOrder(req: Request, createOrderDto: CreateOrderDto) { - // 檢查房間是否存在 - const room = await this.roomModel.findOne({ _id: createOrderDto.roomId }); - if (!room) { - throw new AppError(HttpStatus.NOT_FOUND, 'UserError', '此房型不存在'); - } - // 檢查訂房人數 - if (createOrderDto.peopleNum > room.maxPeople) { - throw new AppError(HttpStatus.NOT_FOUND, 'UserError', '超出該房型最大訂房人數'); - } - // 檢查房間該時段是否被預訂 - const existingOrders = await this.orderModel.find({ - roomId: createOrderDto.roomId, - $or: [ - { checkInDate: { $lt: createOrderDto.checkOutDate }, checkOutDate: { $gt: createOrderDto.checkInDate } }, - { checkInDate: { $lt: createOrderDto.checkInDate }, checkOutDate: { $gt: createOrderDto.checkOutDate } }, - ], - }); - if (existingOrders.length > 0) { - throw new AppError(HttpStatus.NOT_FOUND, 'UserError', '該房型已被預定'); - } - // 建立訂單 - const order = new this.orderModel(createOrderDto); - order.orderUserId = req['user']._id; - order.status = 1 - const result = await order.save(); - return getHttpResponse.successResponse({ - message: '新增訂單', - data: result, - }); - } - - async getallOrders(req: Request) { - const result = await this.orderModel.find({}); - const ids = result.map(order => order._id.toString()); - return getHttpResponse.successResponse({ - message: '取得所有訂單', - data: ids, - }); + async createOrder(req: Request, createOrderDto: CreateOrderDto) { + // 檢查房間是否存在 + const room = await this.roomModel.findOne({ _id: createOrderDto.roomId }); + if (!room) { + throw new AppError(HttpStatus.NOT_FOUND, 'UserError', '此房型不存在'); } - - async getMyOrders(req: Request) { - const orderUserId = req['user']._id; - const result = await this.orderModel.find({ - orderUserId: orderUserId, - status: 1, - }, '_id'); - const ids = result.map(order => order._id.toString()); - return getHttpResponse.successResponse({ - message: '取得所有訂單', - data: ids, - }); + // 檢查訂房人數 + if (createOrderDto.peopleNum > room.maxPeople) { + throw new AppError( + HttpStatus.NOT_FOUND, + 'UserError', + '超出該房型最大訂房人數', + ); } - - async getMyOrderDetail(id: string, req: Request) { - const result = await this.orderModel.findOne({ - _id: id, - status: 1, - }); - if (!result) { - throw new AppError(HttpStatus.NOT_FOUND, 'UserError', '此訂單不存在或已刪除'); - } - return getHttpResponse.successResponse({ - message: '取得訂單詳細資料', - data: result, - }); + // 檢查房間該時段是否被預訂 + const existingOrders = await this.orderModel.find({ + roomId: createOrderDto.roomId, + $or: [ + { + checkInDate: { $lt: createOrderDto.checkOutDate }, + checkOutDate: { $gt: createOrderDto.checkInDate }, + }, + { + checkInDate: { $lt: createOrderDto.checkInDate }, + checkOutDate: { $gt: createOrderDto.checkOutDate }, + }, + ], + }); + if (existingOrders.length > 0) { + throw new AppError(HttpStatus.NOT_FOUND, 'UserError', '該房型已被預定'); } + // 建立訂單 + const order = new this.orderModel(createOrderDto); + order.orderUserId = req['user']._id; + order.status = 1; + const result = await order.save(); + return getHttpResponse.successResponse({ + message: '新增訂單', + data: result, + }); + } - async deleteMyOrder(id: string, req: Request) { - const result = await this.orderModel.findByIdAndUpdate( - { - _id: id, - orderUserId: req['user']._id, - }, - { - status: -1, - }, - { - new: true, - runValidators: true - }, - ); - if (!result) { - throw new AppError(HttpStatus.NOT_FOUND, 'UserError', '此訂單不存在'); - } else if (result.status==-1) { - throw new AppError(HttpStatus.NOT_FOUND, 'UserError', '此訂單已被刪除'); - } - return getHttpResponse.successResponse({ - message: '刪除訂單', - }); - } + async getallOrders(req: Request) { + const result = await this.orderModel.find({}); + const ids = result.map((order) => order._id.toString()); + return getHttpResponse.successResponse({ + message: '取得所有訂單', + data: ids, + }); + } + + async getMyOrders(req: Request) { + const orderUserId = req['user']._id; + const result = await this.orderModel.find( + { + orderUserId: orderUserId, + status: 1, + }, + '_id', + ); + const ids = result.map((order) => order._id.toString()); + return getHttpResponse.successResponse({ + message: '取得所有訂單', + data: ids, + }); + } + + async getMyOrderDetail(id: string, req: Request) { + const result = await this.orderModel.findOne({ + _id: id, + status: 1, + }); + if (!result) { + throw new AppError( + HttpStatus.NOT_FOUND, + 'UserError', + '此訂單不存在或已刪除', + ); + } + return getHttpResponse.successResponse({ + message: '取得訂單詳細資料', + data: result, + }); + } - async deleteOrderAdmin(id: string, req: Request) { - const result = await this.orderModel.findByIdAndUpdate( - { - _id: id, - }, - { - status: -1, - }, - { - new: true, - runValidators: true - }, - ); - if (!result) { - throw new AppError(HttpStatus.NOT_FOUND, 'UserError', '此訂單不存在'); - } else if (result.status==-1) { - throw new AppError(HttpStatus.NOT_FOUND, 'UserError', '此訂單已被刪除'); - } - return getHttpResponse.successResponse({ - message: '刪除訂單', - }); - } + async deleteMyOrder(id: string, req: Request) { + const result = await this.orderModel.findByIdAndUpdate( + { + _id: id, + orderUserId: req['user']._id, + }, + { + status: -1, + }, + { + new: true, + runValidators: true, + }, + ); + if (!result) { + throw new AppError(HttpStatus.NOT_FOUND, 'UserError', '此訂單不存在'); + } else if (result.status == -1) { + throw new AppError(HttpStatus.NOT_FOUND, 'UserError', '此訂單已被刪除'); + } + return getHttpResponse.successResponse({ + message: '刪除訂單', + }); + } - async updateOrderAdmin(id: string, req: Request, updateOrderDto: CreateOrderDto) { - // 檢查房間是否存在 - const room = await this.roomModel.findOne({ _id: updateOrderDto.roomId }); - if (!room) { - throw new AppError(HttpStatus.NOT_FOUND, 'UserError', '此房型不存在'); - } - // 檢查訂房人數 - if (updateOrderDto.peopleNum > room.maxPeople) { - throw new AppError(HttpStatus.NOT_FOUND, 'UserError', '超出該房型最大訂房人數'); - } - // 檢查房間該時段是否被預訂 - const existingOrders = await this.orderModel.find({ - roomId: updateOrderDto.roomId, - $or: [ - { checkInDate: { $lt: updateOrderDto.checkOutDate }, checkOutDate: { $gt: updateOrderDto.checkInDate } }, - { checkInDate: { $lt: updateOrderDto.checkInDate }, checkOutDate: { $gt: updateOrderDto.checkOutDate } }, - ], - }); - if (existingOrders.length > 0) { - throw new AppError(HttpStatus.NOT_FOUND, 'UserError', '該房型已被預定'); - } - // 更新訂單 - const result = await this.orderModel.findByIdAndUpdate( - id, - { - roomId: updateOrderDto.roomId, - checkInDate: updateOrderDto.checkInDate, - checkOutDate: updateOrderDto.checkOutDate, - peopleNum: updateOrderDto.peopleNum, - userInfo: updateOrderDto.userInfo, - }, - { - new: true, - runValidators: true, - }, - ); - if (!result) { - throw new AppError(HttpStatus.NOT_FOUND, 'UserError', '此訂單不存在'); - } - return getHttpResponse.successResponse({ - message: '更新訂單', - data: result, - }); - } + async deleteOrderAdmin(id: string, req: Request) { + const result = await this.orderModel.findByIdAndUpdate( + { + _id: id, + }, + { + status: -1, + }, + { + new: true, + runValidators: true, + }, + ); + if (!result) { + throw new AppError(HttpStatus.NOT_FOUND, 'UserError', '此訂單不存在'); + } else if (result.status == -1) { + throw new AppError(HttpStatus.NOT_FOUND, 'UserError', '此訂單已被刪除'); + } + return getHttpResponse.successResponse({ + message: '刪除訂單', + }); + } - async getMyOrderAdmin(id: string, req: Request) { - const result = await this.orderModel.findOne({ - _id: id, - }); - if (!result) { - throw new AppError(HttpStatus.NOT_FOUND, 'UserError', '此訂單不存在'); - } - return getHttpResponse.successResponse({ - message: '取得訂單詳細資料', - data: result, - }); + async updateOrderAdmin( + id: string, + req: Request, + updateOrderDto: CreateOrderDto, + ) { + // 檢查房間是否存在 + const room = await this.roomModel.findOne({ _id: updateOrderDto.roomId }); + if (!room) { + throw new AppError(HttpStatus.NOT_FOUND, 'UserError', '此房型不存在'); + } + // 檢查訂房人數 + if (updateOrderDto.peopleNum > room.maxPeople) { + throw new AppError( + HttpStatus.NOT_FOUND, + 'UserError', + '超出該房型最大訂房人數', + ); } + // 檢查房間該時段是否被預訂 + const existingOrders = await this.orderModel.find({ + roomId: updateOrderDto.roomId, + $or: [ + { + checkInDate: { $lt: updateOrderDto.checkOutDate }, + checkOutDate: { $gt: updateOrderDto.checkInDate }, + }, + { + checkInDate: { $lt: updateOrderDto.checkInDate }, + checkOutDate: { $gt: updateOrderDto.checkOutDate }, + }, + ], + }); + if (existingOrders.length > 0) { + throw new AppError(HttpStatus.NOT_FOUND, 'UserError', '該房型已被預定'); + } + // 更新訂單 + const result = await this.orderModel.findByIdAndUpdate( + id, + { + roomId: updateOrderDto.roomId, + checkInDate: updateOrderDto.checkInDate, + checkOutDate: updateOrderDto.checkOutDate, + peopleNum: updateOrderDto.peopleNum, + userInfo: updateOrderDto.userInfo, + }, + { + new: true, + runValidators: true, + }, + ); + if (!result) { + throw new AppError(HttpStatus.NOT_FOUND, 'UserError', '此訂單不存在'); + } + return getHttpResponse.successResponse({ + message: '更新訂單', + data: result, + }); + } + async getMyOrderAdmin(id: string, req: Request) { + const result = await this.orderModel.findOne({ + _id: id, + }); + if (!result) { + throw new AppError(HttpStatus.NOT_FOUND, 'UserError', '此訂單不存在'); + } + return getHttpResponse.successResponse({ + message: '取得訂單詳細資料', + data: result, + }); + } } diff --git a/src/features/order/schemas/order.schema.ts b/src/features/order/schemas/order.schema.ts index d4cc533..7d88931 100644 --- a/src/features/order/schemas/order.schema.ts +++ b/src/features/order/schemas/order.schema.ts @@ -7,7 +7,7 @@ export class Order extends Document implements IOrder { @Prop({ type: MongooseSchema.Types.ObjectId, required: true }) roomId: MongooseSchema.Types.ObjectId; - @Prop({ type: Date, required: true}) + @Prop({ type: Date, required: true }) checkInDate: Date; @Prop({ type: Date, required: true }) diff --git a/src/features/room/dto/item.dto.ts b/src/features/room/dto/item.dto.ts index 1df9e71..60071cc 100644 --- a/src/features/room/dto/item.dto.ts +++ b/src/features/room/dto/item.dto.ts @@ -2,7 +2,6 @@ import { ApiProperty } from '@nestjs/swagger'; import { IsNotEmpty } from 'class-validator'; export class ItemDto { - @IsNotEmpty({ message: 'title 未填寫' }) title: string; diff --git a/src/features/room/dto/room.dto.ts b/src/features/room/dto/room.dto.ts index 556f08e..6e429cb 100644 --- a/src/features/room/dto/room.dto.ts +++ b/src/features/room/dto/room.dto.ts @@ -1,5 +1,11 @@ import { ApiProperty } from '@nestjs/swagger'; -import { IsArray, IsNotEmpty, IsString, Matches, ValidateNested } from 'class-validator'; +import { + IsArray, + IsNotEmpty, + IsString, + Matches, + ValidateNested, +} from 'class-validator'; import { ItemDto } from './item.dto'; import { Type } from 'class-transformer'; @@ -12,7 +18,8 @@ export class CreateRoomDto { name: string; @ApiProperty({ - example: '享受高級的住宿體驗,尊爵雙人房提供給您舒適寬敞的空間和精緻的裝潢。', + example: + '享受高級的住宿體驗,尊爵雙人房提供給您舒適寬敞的空間和精緻的裝潢。', description: 'Description', }) @IsNotEmpty({ message: 'description 未填寫' }) @@ -30,7 +37,7 @@ export class CreateRoomDto { example: [ 'https://fakeimg.pl/300/', 'https://fakeimg.pl/301/', - 'https://fakeimg.pl/302/' + 'https://fakeimg.pl/302/', ], description: 'imageUrlList 未填寫', }) @@ -69,10 +76,12 @@ export class CreateRoomDto { @ApiProperty({ type: ItemDto, - example: [{ - title: '平面電視', - isProvide: true, - }], + example: [ + { + title: '平面電視', + isProvide: true, + }, + ], description: 'Address', }) @ValidateNested({ each: true }) @@ -81,10 +90,12 @@ export class CreateRoomDto { @ApiProperty({ type: ItemDto, - example: [{ - title: '衛生紙', - isProvide: true, - }], + example: [ + { + title: '衛生紙', + isProvide: true, + }, + ], description: 'Address', }) @ValidateNested({ each: true }) @@ -92,8 +103,6 @@ export class CreateRoomDto { amenityInfo: ItemDto; } - - export class CreateRoomSuccessDto { @ApiProperty({ example: true }) status: boolean; @@ -105,11 +114,13 @@ export class CreateRoomSuccessDto { example: { _id: '658e628a4963529557a6561b', name: '尊爵雙人房', - description: '享受高級的住宿體驗,尊爵雙人房提供給您舒適寬敞的空間和精緻的裝潢。', + description: + '享受高級的住宿體驗,尊爵雙人房提供給您舒適寬敞的空間和精緻的裝潢。', imageUrl: 'https://fakeimg.pl/300/', - imageUrlList: ['https://fakeimg.pl/300/', - 'https://fakeimg.pl/301/', - 'https://fakeimg.pl/302/' + imageUrlList: [ + 'https://fakeimg.pl/300/', + 'https://fakeimg.pl/301/', + 'https://fakeimg.pl/302/', ], areaInfo: '24坪', bedInfo: '一張大床', @@ -120,13 +131,13 @@ export class CreateRoomSuccessDto { { title: '平面電視', isProvide: true, - } + }, ], amenityInfo: [ { title: '衛生紙', isProvide: true, - } + }, ], creator: '658b9367df4b59a38f24e143', createdAt: '2023-12-27T03:00:55.922Z', @@ -144,9 +155,7 @@ export class GetRoomSuccessDto { message: string; @ApiProperty({ - example: [ - "658e628a4963529557a6561b" - ], + example: ['658e628a4963529557a6561b'], }) data: object; } @@ -162,11 +171,13 @@ export class GetOneRoomSuccessDto { example: { _id: '658e628a4963529557a6561b', name: '尊爵雙人房', - description: '享受高級的住宿體驗,尊爵雙人房提供給您舒適寬敞的空間和精緻的裝潢。', + description: + '享受高級的住宿體驗,尊爵雙人房提供給您舒適寬敞的空間和精緻的裝潢。', imageUrl: 'https://fakeimg.pl/300/', - imageUrlList: ['https://fakeimg.pl/300/', - 'https://fakeimg.pl/301/', - 'https://fakeimg.pl/302/' + imageUrlList: [ + 'https://fakeimg.pl/300/', + 'https://fakeimg.pl/301/', + 'https://fakeimg.pl/302/', ], areaInfo: '24坪', bedInfo: '一張大床', @@ -177,13 +188,13 @@ export class GetOneRoomSuccessDto { { title: '平面電視', isProvide: true, - } + }, ], amenityInfo: [ { title: '衛生紙', isProvide: true, - } + }, ], creator: '658b9367df4b59a38f24e143', createdAt: '2023-12-27T03:00:55.922Z', @@ -204,11 +215,13 @@ export class UpdateRoomSuccessDto { example: { _id: '658e628a4963529557a6561b', name: '尊爵雙人房', - description: '享受高級的住宿體驗,尊爵雙人房提供給您舒適寬敞的空間和精緻的裝潢。', + description: + '享受高級的住宿體驗,尊爵雙人房提供給您舒適寬敞的空間和精緻的裝潢。', imageUrl: 'https://fakeimg.pl/300/', - imageUrlList: ['https://fakeimg.pl/300/', - 'https://fakeimg.pl/301/', - 'https://fakeimg.pl/302/' + imageUrlList: [ + 'https://fakeimg.pl/300/', + 'https://fakeimg.pl/301/', + 'https://fakeimg.pl/302/', ], areaInfo: '24坪', bedInfo: '一張大床', @@ -219,13 +232,13 @@ export class UpdateRoomSuccessDto { { title: '平面電視', isProvide: true, - } + }, ], amenityInfo: [ { title: '衛生紙', isProvide: true, - } + }, ], creator: '658b9367df4b59a38f24e143', createdAt: '2023-12-27T03:00:55.922Z', diff --git a/src/features/room/interfaces/item.interface.ts b/src/features/room/interfaces/item.interface.ts index ae1e808..42f1113 100644 --- a/src/features/room/interfaces/item.interface.ts +++ b/src/features/room/interfaces/item.interface.ts @@ -1,6 +1,4 @@ - - export interface IItem { - title: string; - isProvide: boolean; + title: string; + isProvide: boolean; } diff --git a/src/features/room/interfaces/room.interface.ts b/src/features/room/interfaces/room.interface.ts index ebc1465..badb606 100644 --- a/src/features/room/interfaces/room.interface.ts +++ b/src/features/room/interfaces/room.interface.ts @@ -16,4 +16,3 @@ export interface IRoom extends Document { amenityInfo: IItem[]; creator: string; } - diff --git a/src/features/room/room.controller.ts b/src/features/room/room.controller.ts index b43d443..26cf7a7 100644 --- a/src/features/room/room.controller.ts +++ b/src/features/room/room.controller.ts @@ -1,14 +1,37 @@ -import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Param, Post, Put, Req, UseGuards } from '@nestjs/common'; -import { ApiBearerAuth, ApiOkResponse, ApiOperation, ApiTags } from '@nestjs/swagger'; +import { + Body, + Controller, + Delete, + Get, + HttpCode, + HttpStatus, + Param, + Post, + Put, + Req, + UseGuards, +} from '@nestjs/common'; +import { + ApiBearerAuth, + ApiOkResponse, + ApiOperation, + ApiTags, +} from '@nestjs/swagger'; import { RolesGuard } from 'src/auth/guards/roles.guard'; import { ApiErrorDecorator } from 'src/common/decorators/error/error.decorator'; import { RoomService } from './room.service'; import { Roles } from 'src/auth/decorators/roles.decorator'; -import { CreateRoomDto, CreateRoomSuccessDto, DeleteRoomSuccessDto, GetOneRoomSuccessDto, GetRoomSuccessDto, UpdateRoomSuccessDto } from './dto/room.dto'; +import { + CreateRoomDto, + CreateRoomSuccessDto, + DeleteRoomSuccessDto, + GetOneRoomSuccessDto, + GetRoomSuccessDto, + UpdateRoomSuccessDto, +} from './dto/room.dto'; import { IsObjectIdPipe } from 'nestjs-object-id'; import { AuthGuard } from '@nestjs/passport'; - @ApiTags('Rooms - 房型') @ApiErrorDecorator( HttpStatus.INTERNAL_SERVER_ERROR, @@ -33,10 +56,10 @@ export class RoomController { @ApiOkResponse({ type: GetOneRoomSuccessDto }) async getRoomById( @Param('id', IsObjectIdPipe) id: string, - @Req() req: Request) { + @Req() req: Request, + ) { return await this.roomService.getRoomById(id, req); } - } @ApiTags('Admin/Rooms - 房型管理') @@ -50,48 +73,48 @@ export class RoomController { ) @Controller('api/v1/admin/rooms') export class RoomAdminController { - constructor(private readonly roomService: RoomService) {} + constructor(private readonly roomService: RoomService) {} - @Get('') - @Roles('admin') - @HttpCode(HttpStatus.OK) - @ApiOperation({ summary: '取得所有房型 Get all rooms' }) - @ApiOkResponse({ type: GetRoomSuccessDto }) - async getallNews(@Req() req: Request) { - return await this.roomService.getallRooms(req); - } + @Get('') + @Roles('admin') + @HttpCode(HttpStatus.OK) + @ApiOperation({ summary: '取得所有房型 Get all rooms' }) + @ApiOkResponse({ type: GetRoomSuccessDto }) + async getallNews(@Req() req: Request) { + return await this.roomService.getallRooms(req); + } - @Post('') - @Roles('admin') - @HttpCode(HttpStatus.OK) - @ApiOperation({ summary: '新增最新房型 Add a room' }) - @ApiOkResponse({ type: CreateRoomSuccessDto }) - async addNews(@Req() req: Request, @Body() createNewsDto: CreateRoomDto) { - return await this.roomService.createRoom(req, createNewsDto); - } + @Post('') + @Roles('admin') + @HttpCode(HttpStatus.OK) + @ApiOperation({ summary: '新增最新房型 Add a room' }) + @ApiOkResponse({ type: CreateRoomSuccessDto }) + async addNews(@Req() req: Request, @Body() createNewsDto: CreateRoomDto) { + return await this.roomService.createRoom(req, createNewsDto); + } - @Put(':id') - @Roles('admin') - @HttpCode(HttpStatus.OK) - @ApiOperation({ summary: '更新最新消息 Update latest news' }) - @ApiOkResponse({ type: UpdateRoomSuccessDto }) - async updateNews( - @Param('id', IsObjectIdPipe) id: string, - @Req() req: Request, - @Body() updateNewsDto: CreateRoomDto, - ) { - return await this.roomService.updateRoom(id, req, updateNewsDto); - } + @Put(':id') + @Roles('admin') + @HttpCode(HttpStatus.OK) + @ApiOperation({ summary: '更新最新消息 Update latest news' }) + @ApiOkResponse({ type: UpdateRoomSuccessDto }) + async updateNews( + @Param('id', IsObjectIdPipe) id: string, + @Req() req: Request, + @Body() updateNewsDto: CreateRoomDto, + ) { + return await this.roomService.updateRoom(id, req, updateNewsDto); + } - @Delete(':id') - @Roles('admin') - @HttpCode(HttpStatus.OK) - @ApiOperation({ summary: '刪除最新消息 Delete latest news' }) - @ApiOkResponse({ type: DeleteRoomSuccessDto }) - async deleteNews( - @Param('id', IsObjectIdPipe) id: string, - @Req() req: Request, - ) { - return await this.roomService.deleteRoom(id, req); - } + @Delete(':id') + @Roles('admin') + @HttpCode(HttpStatus.OK) + @ApiOperation({ summary: '刪除最新消息 Delete latest news' }) + @ApiOkResponse({ type: DeleteRoomSuccessDto }) + async deleteNews( + @Param('id', IsObjectIdPipe) id: string, + @Req() req: Request, + ) { + return await this.roomService.deleteRoom(id, req); + } } diff --git a/src/features/room/room.module.ts b/src/features/room/room.module.ts index 5d57bd3..f3a6b7a 100644 --- a/src/features/room/room.module.ts +++ b/src/features/room/room.module.ts @@ -7,6 +7,6 @@ import { RoomAdminController, RoomController } from './room.controller'; @Module({ imports: [MongooseModule.forFeature([{ name: 'Room', schema: RoomSchema }])], controllers: [RoomController, RoomAdminController], - providers: [RoomService] + providers: [RoomService], }) export class RoomModule {} diff --git a/src/features/room/room.service.ts b/src/features/room/room.service.ts index 98fcdce..18b7287 100644 --- a/src/features/room/room.service.ts +++ b/src/features/room/room.service.ts @@ -8,91 +8,93 @@ import { AppError } from 'src/utils/appError'; @Injectable() export class RoomService { - constructor(@InjectModel('Room') private readonly roomModel: Model) {} + constructor(@InjectModel('Room') private readonly roomModel: Model) {} - async getallRooms(req: Request) { - const result = await this.roomModel.find({ - status: 1 - }, '_id'); - const ids = result.map(order => order._id.toString()); - return getHttpResponse.successResponse({ - message: '取得所有房型', - data: ids, - }); - } + async getallRooms(req: Request) { + const result = await this.roomModel.find( + { + status: 1, + }, + '_id', + ); + const ids = result.map((order) => order._id.toString()); + return getHttpResponse.successResponse({ + message: '取得所有房型', + data: ids, + }); + } - async getRoomById(id: string, req: Request) { - const result = await this.roomModel.findOne({ - _id: id, - status: 1 - }); - if (!result) { - throw new AppError(HttpStatus.NOT_FOUND, 'UserError', '此房型不存在'); - } - return getHttpResponse.successResponse({ - message: '取得單一房型', - data: result, - }); + async getRoomById(id: string, req: Request) { + const result = await this.roomModel.findOne({ + _id: id, + status: 1, + }); + if (!result) { + throw new AppError(HttpStatus.NOT_FOUND, 'UserError', '此房型不存在'); } + return getHttpResponse.successResponse({ + message: '取得單一房型', + data: result, + }); + } - async createRoom(req: Request, createRoomDto: CreateRoomDto) { - const room = new this.roomModel(createRoomDto); - room.status = 1 - room.creator = req['user']._id; - const result = await room.save(); - return getHttpResponse.successResponse({ - message: '新增房型', - data: result, - }); - } - - async updateRoom(id: string, req: Request, updateRoomDto: CreateRoomDto) { - const result = await this.roomModel.findByIdAndUpdate( - id, - { - name: updateRoomDto.name, - description: updateRoomDto.description, - image: updateRoomDto.imageUrl, - imageUrlList: updateRoomDto.imageUrlList, - areaInfo: updateRoomDto.areaInfo, - bedInfo: updateRoomDto.bedInfo, - maxPeople: updateRoomDto.maxPeople, - price: updateRoomDto.price, - facilityInfo: updateRoomDto.facilityInfo, - amenityInfo: updateRoomDto.amenityInfo, - creator: req['user']._id, - }, - { - new: true, - runValidators: true, - }, - ); - if (!result) { - throw new AppError(HttpStatus.NOT_FOUND, 'UserError', '此房型不存在'); - } - return getHttpResponse.successResponse({ - message: '更新房型', - data: result, - }); - } + async createRoom(req: Request, createRoomDto: CreateRoomDto) { + const room = new this.roomModel(createRoomDto); + room.status = 1; + room.creator = req['user']._id; + const result = await room.save(); + return getHttpResponse.successResponse({ + message: '新增房型', + data: result, + }); + } - async deleteRoom(id: string, req: Request) { + async updateRoom(id: string, req: Request, updateRoomDto: CreateRoomDto) { const result = await this.roomModel.findByIdAndUpdate( - id, - { - status: -1, - }, - { - new: true, - runValidators: true - }, + id, + { + name: updateRoomDto.name, + description: updateRoomDto.description, + image: updateRoomDto.imageUrl, + imageUrlList: updateRoomDto.imageUrlList, + areaInfo: updateRoomDto.areaInfo, + bedInfo: updateRoomDto.bedInfo, + maxPeople: updateRoomDto.maxPeople, + price: updateRoomDto.price, + facilityInfo: updateRoomDto.facilityInfo, + amenityInfo: updateRoomDto.amenityInfo, + creator: req['user']._id, + }, + { + new: true, + runValidators: true, + }, ); if (!result) { - throw new AppError(HttpStatus.NOT_FOUND, 'UserError', '此房型不存在'); + throw new AppError(HttpStatus.NOT_FOUND, 'UserError', '此房型不存在'); } return getHttpResponse.successResponse({ - message: '刪除房型', + message: '更新房型', + data: result, }); - } + } + async deleteRoom(id: string, req: Request) { + const result = await this.roomModel.findByIdAndUpdate( + id, + { + status: -1, + }, + { + new: true, + runValidators: true, + }, + ); + if (!result) { + throw new AppError(HttpStatus.NOT_FOUND, 'UserError', '此房型不存在'); + } + return getHttpResponse.successResponse({ + message: '刪除房型', + }); + } } diff --git a/src/features/room/schemas/room.schema.ts b/src/features/room/schemas/room.schema.ts index aea9663..8624eae 100644 --- a/src/features/room/schemas/room.schema.ts +++ b/src/features/room/schemas/room.schema.ts @@ -40,7 +40,6 @@ export class Room extends Document implements IRoom { @Prop({ required: true }) creator: string; - } export const RoomSchema = SchemaFactory.createForClass(Room); diff --git a/src/features/url/interfaces/url.interface.ts b/src/features/url/interfaces/url.interface.ts index 4981d04..1f1f8e9 100644 --- a/src/features/url/interfaces/url.interface.ts +++ b/src/features/url/interfaces/url.interface.ts @@ -2,5 +2,5 @@ import { Document } from 'mongoose'; export interface IUrl extends Document { originalUrl: string; - shortUrl: string, -}; + shortUrl: string; +} diff --git a/src/features/url/schemas/url.schemas.ts b/src/features/url/schemas/url.schemas.ts index 8bb7e84..dfbed38 100644 --- a/src/features/url/schemas/url.schemas.ts +++ b/src/features/url/schemas/url.schemas.ts @@ -9,7 +9,6 @@ export class Url extends Document implements IUrl { @Prop({ required: true }) shortUrl: string; - } export const UrlSchema = SchemaFactory.createForClass(Url); diff --git a/src/features/url/url.controller.ts b/src/features/url/url.controller.ts index 79cec35..78d3140 100644 --- a/src/features/url/url.controller.ts +++ b/src/features/url/url.controller.ts @@ -16,15 +16,25 @@ export class UrlController { @Get(':shortUrl') @ApiOperation({ summary: '轉址短網址 Redirect short URL' }) - @ApiErrorDecorator(HttpStatus.BAD_REQUEST, 'UserError', '無此網址或網址已失效') + @ApiErrorDecorator( + HttpStatus.BAD_REQUEST, + 'UserError', + '無此網址或網址已失效', + ) @Redirect() - async redirectToOriginalUrl(@Param('shortUrl') shortUrl: string): Promise<{ url: string }> { + async redirectToOriginalUrl( + @Param('shortUrl') shortUrl: string, + ): Promise<{ url: string }> { const originalUrl = await this.urlService.getOriginalUrl(shortUrl); if (originalUrl) { return { url: originalUrl }; } else { - throw new AppError(HttpStatus.BAD_REQUEST, 'UserError', '無此網址或網址已失效'); + throw new AppError( + HttpStatus.BAD_REQUEST, + 'UserError', + '無此網址或網址已失效', + ); } } } diff --git a/src/features/url/url.module.ts b/src/features/url/url.module.ts index 2f777fe..b2aaf3e 100644 --- a/src/features/url/url.module.ts +++ b/src/features/url/url.module.ts @@ -5,9 +5,9 @@ import { UrlService } from './url.service'; import { UrlController } from './url.controller'; @Module({ - imports: [MongooseModule.forFeature([{ name: 'Url', schema: UrlSchema }])], - controllers: [UrlController], - providers: [UrlService], - exports: [UrlService], - }) -export class UrlModule {} \ No newline at end of file + imports: [MongooseModule.forFeature([{ name: 'Url', schema: UrlSchema }])], + controllers: [UrlController], + providers: [UrlService], + exports: [UrlService], +}) +export class UrlModule {} diff --git a/src/features/url/url.service.ts b/src/features/url/url.service.ts index 205b68f..a9267d7 100644 --- a/src/features/url/url.service.ts +++ b/src/features/url/url.service.ts @@ -6,30 +6,30 @@ import * as crypto from 'crypto'; @Injectable() export class UrlService { - constructor(@InjectModel('Url') private readonly urlModel: Model){} - - async shortenUrl(originalUrl: string): Promise { - const existingUrl = await this.urlModel.findOne({ originalUrl }).exec(); - - if (existingUrl) { - return existingUrl.shortUrl; - } - - const shortUrl = this.generateShortUrl(originalUrl); - const newUrl = new this.urlModel({ originalUrl, shortUrl }); - await newUrl.save(); - - return shortUrl; - } - - async getOriginalUrl(shortUrl: string): Promise { - const url = await this.urlModel.findOne({ shortUrl }).exec(); - - return url ? url.originalUrl : null; - } + constructor(@InjectModel('Url') private readonly urlModel: Model) {} + + async shortenUrl(originalUrl: string): Promise { + const existingUrl = await this.urlModel.findOne({ originalUrl }).exec(); - private generateShortUrl(originalUrl: string): string { - const hash = crypto.createHash('sha256').update(originalUrl).digest('hex'); - return hash.slice(0, 8); + if (existingUrl) { + return existingUrl.shortUrl; } + + const shortUrl = this.generateShortUrl(originalUrl); + const newUrl = new this.urlModel({ originalUrl, shortUrl }); + await newUrl.save(); + + return shortUrl; + } + + async getOriginalUrl(shortUrl: string): Promise { + const url = await this.urlModel.findOne({ shortUrl }).exec(); + + return url ? url.originalUrl : null; + } + + private generateShortUrl(originalUrl: string): string { + const hash = crypto.createHash('sha256').update(originalUrl).digest('hex'); + return hash.slice(0, 8); + } } diff --git a/src/main.ts b/src/main.ts index 61ef8e4..81cd040 100644 --- a/src/main.ts +++ b/src/main.ts @@ -11,7 +11,8 @@ async function bootstrap() { app.useGlobalFilters(new ErrorHandlerFilter()); const config = new DocumentBuilder() .setTitle('Hotel Reservation Backend') - .setDescription(`Building a Hotel Reservation API on the Backend with TypeScript.\n\nNote: After successful login, please click on "Authorize" and enter your access token.\n\nExample Code : + .setDescription( + `Building a Hotel Reservation API on the Backend with TypeScript.\n\nNote: After successful login, please click on "Authorize" and enter your access token.\n\nExample Code : fetch('/api/v1/home/news', { method: 'GET' }) .then(response => response.json()) @@ -19,7 +20,8 @@ async function bootstrap() { // { status: 'true', result: [{...}] } console.log(res); }); - `) + `, + ) .setVersion('1.0') .addServer(`http://localhost:${process.env.PORT}`, 'Local Environment') .addServer(process.env.PRODUCTION_URL, 'Production') // HTTPS scheme