├── .circleci └── config.yml ├── .dockerignore ├── .env ├── .eslintrc.js ├── .github └── workflows │ └── publish-github-docker.yml ├── .gitignore ├── .prettierrc ├── .vscode └── launch.json ├── Dockerfile ├── Procfile ├── README.md ├── database └── datamodel.graphql ├── docker-compose.yml ├── generated └── prisma-client │ ├── index.ts │ └── prisma-schema.ts ├── mock.ts ├── nest-cli.json ├── package.json ├── prisma.yml ├── redis.conf ├── src ├── app.controller.spec.ts ├── app.controller.ts ├── app.module.ts ├── app.service.ts ├── auth │ ├── auth.module.ts │ ├── auth.resolver.spec.ts │ ├── auth.resolver.ts │ ├── auth.service.spec.ts │ ├── auth.service.ts │ ├── facebook.strategy.ts │ ├── github │ │ ├── github.controller.spec.ts │ │ └── github.controller.ts │ ├── google.strategy.ts │ ├── graphql-auth.guard.ts │ ├── jwt.strategy.ts │ └── sign-up-input.dto.ts ├── aws │ ├── aws.service.spec.ts │ └── aws.service.ts ├── cron │ ├── cron.module.ts │ ├── cron.service.spec.ts │ └── cron.service.ts ├── graphql.options.ts ├── graphql.schema.generated.ts ├── hotel │ ├── hotel.module.ts │ ├── hotel.resolver.spec.ts │ ├── hotel.resolver.ts │ ├── hotel.service.spec.ts │ └── hotel.service.ts ├── main.ts ├── prisma │ ├── prisma.module.ts │ ├── prisma.service.spec.ts │ └── prisma.service.ts ├── schema │ └── gql-api.graphql ├── services │ ├── sendEmail.ts │ └── template │ │ └── reset.pug ├── shared │ └── decorators │ │ └── decorator.ts ├── transaction │ ├── transaction.module.ts │ ├── transaction.resolver.spec.ts │ └── transaction.resolver.ts ├── user │ ├── user.module.ts │ ├── user.resolver.spec.ts │ └── user.resolver.ts └── utils │ ├── FilesAndMockUpload.ts │ ├── data.ts │ ├── seed.ts │ ├── stripe.ts │ ├── upload.input.ts │ ├── upload.scalar.ts │ └── utils.module.ts ├── test ├── app.e2e-spec.ts └── jest-e2e.json ├── tsconfig.build.json ├── tsconfig.json ├── vercel.json └── yarn.lock /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2.1 2 | orbs: 3 | heroku: circleci/heroku@0.0.10 4 | workflows: 5 | heroku_deploy: 6 | jobs: 7 | - heroku/deploy-via-git -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | /dist 2 | /node_modules 3 | .env 4 | redis.conf -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | JWT_SECRET=myscret_JWT 2 | EMAIL_SEND=redias2048@gmail.com 3 | LOCAL_EMAIL=redias2048@gmail.com 4 | LOCAL_PASSWORD=pinodien 5 | 6 | STAGING_SECRET_KEY=12345678 7 | STAGING_EMAIL=andrewhavard1807@gmail.com 8 | STAGING_PASSWORD=pinodien -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: '@typescript-eslint/parser', 3 | parserOptions: { 4 | project: 'tsconfig.json', 5 | sourceType: 'module', 6 | }, 7 | plugins: ['@typescript-eslint/eslint-plugin'], 8 | extends: [ 9 | 'plugin:@typescript-eslint/eslint-recommended', 10 | 'plugin:@typescript-eslint/recommended', 11 | 'prettier', 12 | 'prettier/@typescript-eslint', 13 | ], 14 | root: true, 15 | env: { 16 | node: true, 17 | jest: true, 18 | }, 19 | rules: { 20 | '@typescript-eslint/interface-name-prefix': 'off', 21 | '@typescript-eslint/explicit-function-return-type': 'off', 22 | '@typescript-eslint/explicit-module-boundary-types': 'off', 23 | '@typescript-eslint/no-explicit-any': 'off', 24 | }, 25 | }; 26 | -------------------------------------------------------------------------------- /.github/workflows/publish-github-docker.yml: -------------------------------------------------------------------------------- 1 | name: Push Docker Image To DockerHub and Github Package 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | pull_request: 7 | branches: [master] 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - name: Checkout code 15 | uses: actions/checkout@v2 16 | - name: Push to GitHub Packages 17 | uses: docker/build-push-action@v1 18 | with: 19 | username: ${{ github.actor }} 20 | password: ${{ secrets.GITHUB_TOKEN }} 21 | registry: docker.pkg.github.com 22 | repository: php1301/doanfullstack-be-admin/nest-prisma-mongo-redis-v1 23 | tags: github-actions-docker-v1 24 | - name: Build and push Docker images To DockerHub 25 | uses: docker/build-push-action@v1 26 | with: 27 | username: ${{ secrets.DOCKER_USERNAME }} 28 | password: ${{ secrets.DOCKER_PASSWORD }} 29 | repository: phucpham1301/hotelprisma-v1.0 30 | tags: github-actions-docker-v1 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # compiled output 2 | /dist 3 | /node_modules 4 | /.env 5 | # Logs 6 | logs 7 | *.log 8 | npm-debug.log* 9 | yarn-debug.log* 10 | yarn-error.log* 11 | lerna-debug.log* 12 | 13 | # OS 14 | .DS_Store 15 | 16 | # Tests 17 | /coverage 18 | /.nyc_output 19 | 20 | # IDEs and editors 21 | /.idea 22 | .project 23 | .classpath 24 | .c9/ 25 | *.launch 26 | .settings/ 27 | *.sublime-workspace 28 | 29 | # IDE - VSCode 30 | .vscode/* 31 | !.vscode/settings.json 32 | !.vscode/tasks.json 33 | !.vscode/launch.json 34 | !.vscode/extensions.json 35 | .vercel 36 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all" 4 | } -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "node", 9 | "request": "launch", 10 | "name": "Debug Nest Framework", 11 | "args": ["${workspaceFolder}/src/main.ts"], 12 | "runtimeArgs": ["--nolazy", "-r", "ts-node/register"], 13 | "sourceMaps": true, 14 | "cwd": "${workspaceRoot}", 15 | "protocol": "inspector" 16 | } 17 | ] 18 | } -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:12.13-alpine As development 2 | 3 | WORKDIR /usr/src/app 4 | 5 | COPY package*.json ./ 6 | 7 | RUN npm install 8 | 9 | COPY . . 10 | 11 | RUN npm run build 12 | 13 | FROM node:12.13-alpine as production 14 | 15 | 16 | WORKDIR /usr/src/app 17 | 18 | COPY package*.json ./ 19 | 20 | RUN npm install 21 | 22 | COPY . . 23 | 24 | COPY --from=development /usr/src/app/dist ./dist 25 | 26 | CMD ["node", "dist/src/main"] -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: npm run start:prod 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Currently on offline mode for saving GCP fund 2 |

3 | Nest Logo 4 |

5 | 6 | [travis-image]: https://api.travis-ci.org/nestjs/nest.svg?branch=master 7 | [travis-url]: https://travis-ci.org/nestjs/nest 8 | [linux-image]: https://img.shields.io/travis/nestjs/nest/master.svg?label=linux 9 | [linux-url]: https://travis-ci.org/nestjs/nest 10 | 11 |

A progressive Node.js framework for building efficient and scalable server-side applications, heavily inspired by Angular.

12 |

13 | NPM Version 14 | Package License 15 | NPM Downloads 16 | Travis 17 | Linux 18 | Coverage 19 | Gitter 20 | Backers on Open Collective 21 | Sponsors on Open Collective 22 | 23 | 24 |

25 | 27 | 28 | ## Description 29 | 30 | [Nest](https://github.com/nestjs/nest) framework TypeScript starter repository. 31 | 32 | ## Installation 33 | 34 | ```bash 35 | $ npm install 36 | ``` 37 | 38 | ## Running the app 39 | 40 | ```bash 41 | # development 42 | $ npm run start 43 | 44 | # watch mode 45 | $ npm run start:dev 46 | 47 | # production mode 48 | $ npm run start:prod 49 | ``` 50 | 51 | ## Test 52 | 53 | ```bash 54 | # unit tests 55 | $ npm run test 56 | 57 | # e2e tests 58 | $ npm run test:e2e 59 | 60 | # test coverage 61 | $ npm run test:cov 62 | ``` 63 | 64 | ## Support 65 | 66 | 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). 67 | 68 | ## Stay in touch 69 | 70 | - Author - [Kamil Myśliwiec](https://kamilmysliwiec.com) 71 | - Website - [https://nestjs.com](https://nestjs.com/) 72 | - Twitter - [@nestframework](https://twitter.com/nestframework) 73 | 74 | ## License 75 | 76 | Nest is [MIT licensed](LICENSE). 77 | -------------------------------------------------------------------------------- /database/datamodel.graphql: -------------------------------------------------------------------------------- 1 | type User { 2 | id: ID! @id 3 | first_name: String! 4 | last_name: String! 5 | username: String! 6 | password: String! 7 | email: String! @unique 8 | role: String 9 | cellNumber: String @defailt(value: "0000") 10 | profile_pic_main: String 11 | cover_pic_main: String 12 | profile_pic: [Gallery] @relation(link: INLINE, name: "Profile") 13 | cover_pic: [Gallery] @relation(link: INLINE, name: "Cover") 14 | date_of_birth: String 15 | gender: String 16 | content: String 17 | notification: [Notification] @relation(link: INLINE, name: "Notification") 18 | unreadNotification: Int @default(value: 0) 19 | agent_location: Location @relation(link: INLINE) 20 | gallery: [Gallery] @relation(link: INLINE, name: "User_gallery") 21 | social_profile: Social @relation(link: INLINE) 22 | # 1 field sử dụng cùng 1 model thì phải khai báo relation name để specify các loại relations 23 | reviews_maked: [Reviews] @relation(name: "Reviews_maked") 24 | listed_posts: [Hotel] @relation(link: INLINE, name: "Hotel_created") 25 | favourite_post: [Hotel] @relation(link: INLINE, name: "Hotel_liked") 26 | reviewed_post: [Hotel] @relation(link: INLINE, name: "Hotel_reviewed") 27 | review_liked: [Reviews] @relation(name: "Review_liked") 28 | review_disliked: [Reviews] @relation(name: "Review_disliked") 29 | stripeId: String 30 | transaction_had: [Transaction] 31 | @relation(link: INLINE, name: "Transaction_had") 32 | transaction_maked: [Transaction] 33 | @relation(link: INLINE, name: "Transaction_maked") 34 | coupons_maked: [Coupon] @relation(link: INLINE, name: "Coupon_maked") 35 | uncheckTransactions: UncheckTransactions 36 | @relation(link: INLINE, name: "Uncheck_transactions") 37 | createdAt: DateTime! @createdAt 38 | updatedAt: DateTime! @updatedAt 39 | } 40 | type Transaction { 41 | TXID: ID! @id 42 | transactionSecretKey: String 43 | transactionHotelName: String 44 | transactionHotelId: String 45 | transactionHotelManager: User @relation(name: "Transaction_had") 46 | transactionHotelManagerId: String 47 | transactionHotelType: String 48 | transactionPrice: Int 49 | transactionAuthor: User @relation(name: "Transaction_maked") 50 | transactionAuthorId: String 51 | transactionAuthorName: String 52 | transactionAuthorEmail: String 53 | transactionAuthorContactNumber: String 54 | transactionAuthorSpecial: String ##Cho đủ field 55 | transactionAuthorNote: String 56 | transactionLocationLat: Float 57 | transactionLocationLng: Float 58 | transactionRoom: Int 59 | transactionGuest: Int 60 | transactionLocationFormattedAddress: String 61 | transactionRange: Int 62 | transactionStatus: String 63 | transactionCoupon: String 64 | transactionCouponType: Int 65 | transactionCouponValue: Int 66 | transactionStartDate: String 67 | transactionEndDate: String 68 | transactionStripeId: String 69 | createdAt: DateTime! @createdAt 70 | updatedAt: DateTime! @updatedAt 71 | } 72 | 73 | type Coupon { 74 | couponId: ID! @id 75 | couponName: String! @unique 76 | couponDescription: String 77 | couponAuthor: User @relation(name: "Coupon_maked") 78 | couponAuthorId: String 79 | couponType: Int #1 là percent 2 là number 80 | couponValue: Int 81 | couponQuantity: Int 82 | couponStartDate: String 83 | couponEndDate: String 84 | couponRange: String 85 | couponTarget: [Hotel] @relation(link: INLINE, name: "Hotel_coupons") 86 | createdAt: DateTime! @createdAt 87 | updatedAt: DateTime! @updatedAt 88 | } 89 | type Hotel { 90 | id: ID! @id 91 | peopleLiked: [User] @relation(name: "Hotel_liked") 92 | peopleReviewed: [User] @relation(name: "Hotel_reviewed") 93 | couponsAvailable: [Coupon] @relation(name: "Hotel_coupons") 94 | connectId: User @relation(name: "Hotel_created") 95 | agentId: String 96 | agentEmail: String 97 | agentName: String 98 | title: String! 99 | slug: String 100 | content: String 101 | status: String @defailt(value: "Public") 102 | price: Int 103 | isNegotiable: Boolean 104 | propertyType: String 105 | condition: String 106 | rating: Float 107 | ratingCount: Int 108 | contactNumber: String 109 | termsAndCondition: String 110 | amenities: [Amenities] @relation(link: INLINE) 111 | image: Image @relation(link: INLINE) 112 | location: [Location] @relation(link: INLINE) 113 | gallery: [Gallery] @relation(link: INLINE) 114 | categories: [Categories] @relation(link: INLINE) 115 | reviews: [Reviews] @relation(link: INLINE, name: "Hotel_reivews") 116 | createdAt: DateTime! @createdAt 117 | updatedAt: DateTime! @updatedAt 118 | } 119 | type Notification { 120 | id: ID! @id 121 | reviewAuthorName: String 122 | reviewedHotelName: String 123 | reviewTitle: String 124 | reviewText: String 125 | read: Boolean @default(value: false) 126 | old: Boolean @default(value: false) 127 | userNotificationId: String ## vì ko cần lấy thêm thông tin user làm gì (đã có trong payload nên parse thẳng id) 128 | peopleReviewedQuantity: Int 129 | query: String 130 | reviewAuthorProfilePic: String 131 | createdAt: DateTime! @createdAt 132 | updatedAt: DateTime! @updatedAt 133 | } 134 | type UncheckTransactions { 135 | id: ID! @id 136 | userUncheckTransactionsId: String 137 | userUncheckTransactions: User @relation(name: "Uncheck_transactions") 138 | totalPrice: Int @default(value: 0) 139 | totalTransactions: Int @default(value: 0) 140 | } 141 | type Reviews { 142 | reviewID: ID! @id 143 | reviewTitle: String 144 | reviewText: String 145 | sortOfTrip: String 146 | reviewAuthorId: User @relation(link: INLINE, name: "Reviews_maked") 147 | peopleLiked: [User] @relation(link: INLINE, name: "Review_liked") 148 | peopleDisliked: [User] @relation(link: INLINE, name: "Review_disliked") 149 | reviewAuthorFirstName: String 150 | reviewTips: String 151 | reviewAuthorLastName: String 152 | reviewAuthorEmail: String 153 | reviewOverall: Float 154 | reviewAuthorPic: String 155 | reviewedHotel: Hotel @relation(name: "Hotel_reivews") 156 | reviewedHotelId: ID 157 | reviewPics: [ReviewImages] @relation(link: INLINE) 158 | reviewDate: DateTime! @createdAt 159 | reviewOptional: [ReviewOptionals] @relation(link: INLINE) 160 | reviewFields: [ReviewFields] @relation(link: INLINE) 161 | } 162 | 163 | type ReviewOptionals { 164 | id: ID! @id 165 | option: String 166 | optionField: String 167 | } 168 | type ReviewFields { 169 | id: ID! @id 170 | rating: Int 171 | ratingFieldName: String 172 | } 173 | type Social { 174 | id: ID! @id 175 | facebook: String 176 | twitter: String 177 | linkedIn: String 178 | instagram: String 179 | } 180 | 181 | enum Gender { 182 | Male 183 | Female 184 | Other 185 | } 186 | 187 | type Amenities { 188 | id: ID! @id 189 | guestRoom: Int 190 | bedRoom: Int 191 | wifiAvailability: Boolean 192 | parkingAvailability: Boolean 193 | poolAvailability: Boolean 194 | airCondition: Boolean 195 | extraBedFacility: Boolean 196 | } 197 | 198 | type Image { 199 | id: ID! @id 200 | url: String 201 | thumb_url: String 202 | } 203 | type CategoryImages { 204 | id: ID! @id 205 | url: String 206 | } 207 | type ReviewImages { 208 | id: ID! @id 209 | url: String 210 | } 211 | type Gallery { 212 | id: ID! @id 213 | uid: String 214 | url: String 215 | signedRequest: String 216 | } 217 | 218 | type Location { 219 | id: ID! @id 220 | lat: Float 221 | lng: Float 222 | formattedAddress: String 223 | zipcode: String 224 | city: String 225 | state_long: String 226 | state_short: String 227 | country_long: String 228 | country_short: String 229 | } 230 | 231 | type Categories { 232 | id: ID! @id 233 | slug: String 234 | name: String 235 | image: CategoryImages @relation(link: INLINE) 236 | } 237 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.7' 2 | 3 | services: 4 | # API container 5 | api: 6 | container_name: api 7 | # build: 8 | # context: ./ 9 | image: phucpham1301/hotelprisma-v1.0:github-actions-docker-v1 10 | restart: on-failure 11 | volumes: 12 | - api:/usr/src/app 13 | ports: 14 | - 3000:3000 15 | env_file: 16 | - ./.env 17 | depends_on: 18 | - redis 19 | - prisma 20 | networks: 21 | - api 22 | redis: 23 | image: redis:latest 24 | container_name: redis 25 | restart: on-failure 26 | ports: 27 | - 6379:6379 28 | volumes: 29 | - redis-data:/var/lib/redis 30 | - ./redis.conf:/usr/local/etc/redis/redis.conf 31 | entrypoint: redis-server /usr/local/etc/redis/redis.conf 32 | networks: 33 | api: 34 | ipv4_address: 172.28.1.4 35 | prisma: 36 | image: prismagraphql/prisma:1.34 37 | restart: always 38 | ports: 39 | - '4466:4466' 40 | environment: 41 | PRISMA_CONFIG: | 42 | port: 4466 43 | databases: 44 | default: 45 | connector: mongo 46 | uri: mongodb://prisma:prisma@mongo:27017/HotelPrisma?authSource=admin 47 | database: HotelPrisma 48 | networks: 49 | - api 50 | depends_on: 51 | - mongo 52 | mongo: 53 | image: mongo:3.6 54 | restart: always 55 | environment: 56 | MONGO_INITDB_ROOT_USERNAME: prisma 57 | MONGO_INITDB_ROOT_PASSWORD: prisma 58 | ports: 59 | - '27017:27017' 60 | volumes: 61 | - mongo:/var/lib/mongo 62 | networks: 63 | - api 64 | volumes: 65 | mongo: 66 | api: 67 | redis-data: 68 | networks: 69 | api: 70 | ipam: 71 | driver: default 72 | config: 73 | - subnet: 172.28.0.0/16 74 | -------------------------------------------------------------------------------- /nest-cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "collection": "@nestjs/schematics", 3 | "sourceRoot": "src", 4 | "compilerOptions": { 5 | "assets": ["**/*.pug", "**/*.graphql"], 6 | "watchAssets": true 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "prisma-nest", 3 | "version": "0.0.1", 4 | "description": "", 5 | "author": "", 6 | "private": true, 7 | "license": "UNLICENSED", 8 | "scripts": { 9 | "prebuild": "rimraf dist", 10 | "build": "nest build", 11 | "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", 12 | "start": "node dist/src/main", 13 | "start:dev": "nest start --watch", 14 | "start:debug": "nest start --debug --watch", 15 | "start:prod": "node dist/src/main", 16 | "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", 17 | "test": "jest", 18 | "test:watch": "jest --watch", 19 | "test:cov": "jest --coverage", 20 | "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", 21 | "test:e2e": "jest --config ./test/jest-e2e.json" 22 | }, 23 | "dependencies": { 24 | "@nestjs-modules/mailer": "^1.5.1", 25 | "@nestjs/common": "^7.0.0", 26 | "@nestjs/config": "^0.5.0", 27 | "@nestjs/core": "^7.0.0", 28 | "@nestjs/graphql": "^7.4.1", 29 | "@nestjs/jwt": "^7.0.0", 30 | "@nestjs/passport": "^7.1.0", 31 | "@nestjs/platform-express": "^7.0.0", 32 | "@prisma/cli": "^2.0.1", 33 | "@types/lodash": "^4.14.159", 34 | "@types/node-mailjet": "^3.3.3", 35 | "@types/passport-facebook-token": "^0.4.34", 36 | "@types/passport-google-oauth20": "^2.0.3", 37 | "@types/passport-jwt": "^3.0.3", 38 | "@types/stripe": "^7.13.24", 39 | "@types/superagent": "^4.1.10", 40 | "@vercel/node": "^1.8.1", 41 | "apollo-server-express": "^2.14.5", 42 | "aws-sdk": "^2.709.0", 43 | "bcryptjs": "^2.4.3", 44 | "class-transformer": "^0.2.3", 45 | "class-validator": "^0.12.2", 46 | "cookie-parser": "^1.4.5", 47 | "global": "^4.4.0", 48 | "graphql": "14.6.0", 49 | "graphql-redis-subscriptions": "^2.2.1", 50 | "graphql-subscriptions": "^1.1.0", 51 | "graphql-tools": "^6.0.10", 52 | "ioredis": "^4.17.3", 53 | "lodash": "^4.17.19", 54 | "node-mailjet": "^3.3.1", 55 | "nodemailer": "^6.4.10", 56 | "passport": "^0.4.1", 57 | "passport-facebook-token": "^4.0.0", 58 | "passport-google-oauth20": "^2.0.0", 59 | "passport-jwt": "^4.0.0", 60 | "prisma": "^1.34.10", 61 | "prisma-binding": "^2.3.16", 62 | "prisma-client-lib": "^1.34.10", 63 | "pug": "^3.0.0", 64 | "reflect-metadata": "^0.1.13", 65 | "rimraf": "^3.0.2", 66 | "rxjs": "^6.5.4", 67 | "stripe": "^8.89.0", 68 | "superagent": "^6.0.0", 69 | "uuid": "^8.3.0" 70 | }, 71 | "devDependencies": { 72 | "@nestjs/cli": "^7.0.0", 73 | "@nestjs/schedule": "^0.4.0", 74 | "@nestjs/schematics": "^7.0.0", 75 | "@nestjs/testing": "^7.0.0", 76 | "@types/express": "^4.17.3", 77 | "@types/jest": "25.2.3", 78 | "@types/node": "^13.9.1", 79 | "@types/supertest": "^2.0.8", 80 | "@typescript-eslint/eslint-plugin": "3.0.2", 81 | "@typescript-eslint/parser": "3.0.2", 82 | "eslint": "7.1.0", 83 | "eslint-config-prettier": "^6.10.0", 84 | "eslint-plugin-import": "^2.20.1", 85 | "jest": "26.0.1", 86 | "prettier": "^1.19.1", 87 | "supertest": "^4.0.2", 88 | "ts-jest": "26.1.0", 89 | "ts-loader": "^6.2.1", 90 | "ts-node": "^8.6.2", 91 | "tsconfig-paths": "^3.9.0", 92 | "typescript": "^3.7.4" 93 | }, 94 | "jest": { 95 | "moduleFileExtensions": [ 96 | "js", 97 | "json", 98 | "ts" 99 | ], 100 | "rootDir": "src", 101 | "testRegex": ".spec.ts$", 102 | "transform": { 103 | "^.+\\.(t|j)s$": "ts-jest" 104 | }, 105 | "coverageDirectory": "../coverage", 106 | "testEnvironment": "node" 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /prisma.yml: -------------------------------------------------------------------------------- 1 | endpoint: ${env:PRISMA_ENDPOINT} 2 | datamodel: database/datamodel.graphql 3 | seed: 4 | # import: src/schema/seed.graphql 5 | run: ts-node src/utils/seed.ts 6 | generate: 7 | - generator: typescript-client 8 | output: ./generated/prisma-client/ 9 | -------------------------------------------------------------------------------- /src/app.controller.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { AppController } from './app.controller'; 3 | 4 | describe('AppController', () => { 5 | let appController: AppController; 6 | 7 | beforeEach(async () => { 8 | const app: TestingModule = await Test.createTestingModule({ 9 | controllers: [AppController], 10 | }).compile(); 11 | 12 | appController = app.get(AppController); 13 | }); 14 | 15 | describe('root', () => { 16 | it('should return alive', () => { 17 | expect(appController.alive()).toBe('alive!'); 18 | }); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /src/app.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Get } from '@nestjs/common'; 2 | 3 | @Controller() 4 | export class AppController { 5 | @Get('livecheck') 6 | alive(): string { 7 | return 'alive!'; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/app.module.ts: -------------------------------------------------------------------------------- 1 | import { Module, ValidationPipe } from '@nestjs/common'; 2 | import { PrismaModule } from './prisma/prisma.module'; 3 | import { GraphQLModule } from '@nestjs/graphql'; 4 | import { GraphqlOptions } from './graphql.options'; 5 | import { AppController } from './app.controller'; 6 | import { HotelModule } from './hotel/hotel.module'; 7 | import { AuthModule } from './auth/auth.module'; 8 | import { APP_PIPE } from '@nestjs/core'; 9 | import { ConfigModule } from '@nestjs/config'; 10 | import { ScheduleModule } from '@nestjs/schedule'; 11 | import { UserModule } from './user/user.module'; 12 | // import { PubSub } from 'graphql-subscriptions'; 13 | // import { RedisPubSub } from 'graphql-redis-subscriptions'; 14 | // import * as Redis from 'ioredis'; 15 | // document cho mail https://nest-modules.github.io/mailer/docs/mailer.html 16 | import { MailerModule } from '@nestjs-modules/mailer'; 17 | import { PugAdapter } from '@nestjs-modules/mailer/dist/adapters/pug.adapter'; 18 | import { AwsService } from './aws/aws.service'; 19 | import { UtilsModule } from './utils/utils.module'; 20 | import { TransactionModule } from './transaction/transaction.module'; 21 | import { StripeService } from './utils/stripe'; 22 | import { CronService } from './cron/cron.service'; 23 | import { CronModule } from './cron/cron.module'; 24 | import { MailService } from './services/sendEmail'; 25 | 26 | // Để validation work xuyên suốt app thì phải provide validation pipe 27 | @Module({ 28 | imports: [ 29 | // Bất đồng bộ, xài forRoot sẽ dính lỗi Plain => do auth ko lấy dc .env 30 | MailerModule.forRootAsync({ 31 | useFactory: () => ({ 32 | transport: { 33 | host: 'smtp.gmail.com', 34 | port: 587, 35 | secure: false, // upgrade later with STARTTLS 36 | auth: { 37 | user: process.env.LOCAL_EMAIL, 38 | pass: process.env.LOCAL_PASSWORD, 39 | }, 40 | }, 41 | defaults: { 42 | from: '"nest-modules" ', 43 | }, 44 | template: { 45 | // dir: process.cwd() + '/templates/', 46 | adapter: new PugAdapter(), 47 | options: { 48 | strict: true, 49 | }, 50 | }, 51 | }), 52 | }), 53 | ScheduleModule.forRoot(), 54 | ConfigModule.forRoot({ 55 | envFilePath: './.env', 56 | }), 57 | GraphQLModule.forRootAsync({ 58 | useClass: GraphqlOptions, 59 | }), 60 | PrismaModule, 61 | HotelModule, 62 | AuthModule, 63 | UserModule, 64 | UtilsModule, 65 | TransactionModule, 66 | CronModule, 67 | ], 68 | controllers: [AppController], 69 | providers: [ 70 | { 71 | provide: APP_PIPE, 72 | useClass: ValidationPipe, 73 | }, 74 | MailService, 75 | AwsService, 76 | StripeService, 77 | CronService, 78 | ], 79 | }) 80 | export class AppModule {} 81 | -------------------------------------------------------------------------------- /src/app.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | 3 | @Injectable() 4 | export class AppService { 5 | getHello(): string { 6 | return 'Hello World!'; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/auth/auth.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { AuthService } from './auth.service'; 3 | import { AuthResolver } from './auth.resolver'; 4 | import { PrismaModule } from 'src/prisma/prisma.module'; 5 | import { PassportModule } from '@nestjs/passport'; 6 | import { JwtModule } from '@nestjs/jwt'; 7 | import { JwtStrategy } from './jwt.strategy'; 8 | import { FacebookStrategy } from './facebook.strategy'; 9 | import { GoogleStrategy } from './google.strategy'; 10 | import { GithubController } from './github/github.controller'; 11 | 12 | @Module({ 13 | imports: [ 14 | PrismaModule, 15 | PassportModule.register({ 16 | defaultStrategy: 'jwt', 17 | }), 18 | JwtModule.register({ 19 | privateKey: process.env.JWT_SECRET, 20 | secretOrPrivateKey: process.env.JWT_SECRET, 21 | secret: process.env.JWT_SECRET, 22 | signOptions: { 23 | expiresIn: 3600, //1 giờ 24 | }, 25 | }), 26 | ], 27 | providers: [AuthService, AuthResolver, JwtStrategy, FacebookStrategy, GoogleStrategy], 28 | controllers: [GithubController], 29 | }) 30 | export class AuthModule {} 31 | -------------------------------------------------------------------------------- /src/auth/auth.resolver.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { AuthResolver } from './auth.resolver'; 3 | 4 | describe('AuthResolver', () => { 5 | let resolver: AuthResolver; 6 | 7 | beforeEach(async () => { 8 | const module: TestingModule = await Test.createTestingModule({ 9 | providers: [AuthResolver], 10 | }).compile(); 11 | 12 | resolver = module.get(AuthResolver); 13 | }); 14 | 15 | it('should be defined', () => { 16 | expect(resolver).toBeDefined(); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /src/auth/auth.resolver.ts: -------------------------------------------------------------------------------- 1 | import * as bcrypt from 'bcryptjs'; 2 | import * as jwt2 from 'jsonwebtoken'; 3 | import { Resolver, Mutation, Args } from '@nestjs/graphql'; 4 | import { JwtService } from '@nestjs/jwt'; 5 | import { PrismaService } from 'src/prisma/prisma.service'; 6 | import { LoginInput, User, UpdatePassword } from 'src/graphql.schema.generated'; 7 | import { Response } from 'express'; 8 | import { v4 as uuidv4 } from 'uuid'; 9 | import { ResGql, GqlUser } from 'src/shared/decorators/decorator'; 10 | import { SignUpInputDto } from './sign-up-input.dto'; 11 | import { UseGuards } from '@nestjs/common'; 12 | import { GqlAuthGuard } from './graphql-auth.guard'; 13 | 14 | @Resolver('Auth') 15 | export class AuthResolver { 16 | constructor( 17 | private readonly jwt: JwtService, 18 | private readonly prisma: PrismaService, 19 | ) {} 20 | 21 | @Mutation() 22 | async login( 23 | @Args('loginInput') { email, password }: LoginInput, 24 | @ResGql() res: Response, 25 | ) { 26 | const user = await this.prisma.client.user({ email }); 27 | if (!user) { 28 | throw Error('Email or password is incorrect'); 29 | } 30 | const valid = await bcrypt.compare(password, user.password); 31 | if (!valid) { 32 | throw Error('Password is incorrect'); 33 | } 34 | console.log('this is new'); 35 | console.log(process.env.JWT_SECRET); 36 | const jwt = jwt2.sign( 37 | { 38 | id: user.id, 39 | name: user.first_name + ' ' + user.last_name, 40 | roles: 'user', 41 | }, 42 | process.env.JWT_SECRET, 43 | ); 44 | console.log(jwt); 45 | console.log('jwt'); 46 | res.cookie('token', jwt, { 47 | domain: '.hotel-prisma.ml', 48 | httpOnly: false, 49 | sameSite: 'none', 50 | secure: true, 51 | }); 52 | return user; 53 | } 54 | @Mutation() 55 | async facebookLogin( 56 | @Args('email') email, 57 | @Args('accessToken') accessToken, 58 | @Args('socialInfo') socialInfo, 59 | @Args('socialId') socialId, 60 | @Args('socialProfileLink') socialProfileLink, 61 | @ResGql() res: Response, 62 | ) { 63 | // Có thể thêm biến type để tạo nhiều dạng social như google, github,... 64 | const user = await this.prisma.client.user({ email }); 65 | const profile_pic_main = `https://graph.facebook.com/${socialId}/picture?width=200&height=200`; 66 | if (!user) { 67 | const socialUser = await this.prisma.client.createUser({ 68 | email, 69 | username: socialId, 70 | first_name: socialInfo, 71 | last_name: '', 72 | password: uuidv4(), 73 | profile_pic_main, 74 | cover_pic_main: 'https://i.imgur.com/lXybeGM.png', 75 | social_profile: { 76 | create: { 77 | facebook: socialProfileLink, 78 | }, 79 | }, 80 | role: 'Normal', 81 | // username: signUpInputDto.username, 82 | }); 83 | const jwt = jwt2.sign( 84 | { 85 | id: socialUser.id, 86 | name: socialUser.first_name, 87 | accessToken, 88 | }, 89 | process.env.JWT_SECRET, 90 | ); 91 | res.cookie('token', jwt, { 92 | domain: '.hotel-prisma.ml', 93 | httpOnly: false, 94 | sameSite: 'none', 95 | secure: true, 96 | }); 97 | return socialUser; 98 | } 99 | // sign JWT hợp lệ 100 | // Việc jwt.verify là optional nhờ vào decorator GqlUser() 101 | const jwt = jwt2.sign( 102 | { 103 | id: user.id, 104 | name: user.first_name, 105 | accessToken, 106 | }, 107 | process.env.JWT_SECRET, 108 | ); 109 | res.cookie('token', jwt, { 110 | domain: '.hotel-prisma.ml', 111 | httpOnly: false, 112 | sameSite: 'none', 113 | secure: true, 114 | }); 115 | return user; 116 | } 117 | @Mutation() 118 | async googleLogin( 119 | @Args('email') email, 120 | @Args('accessToken') accessToken, 121 | @Args('socialInfo') socialInfo, 122 | @Args('socialId') socialId, 123 | @Args('profileImage') profileImage, 124 | @ResGql() res: Response, 125 | ) { 126 | // Có thể thêm biến type để tạo nhiều dạng social như google, github,... 127 | const user = await this.prisma.client.user({ email }); 128 | if (!user) { 129 | const socialUser = await this.prisma.client.createUser({ 130 | email, 131 | username: socialId, 132 | first_name: socialInfo, 133 | last_name: '', 134 | password: uuidv4(), 135 | profile_pic_main: profileImage.replace('s96-c', 's250-c'), 136 | cover_pic_main: 'https://i.imgur.com/lXybeGM.png', 137 | role: 'Normal', 138 | // username: signUpInputDto.username, 139 | }); 140 | const jwt = jwt2.sign( 141 | { 142 | id: socialUser.id, 143 | name: socialUser.first_name, 144 | accessToken, 145 | }, 146 | process.env.JWT_SECRET, 147 | ); 148 | res.cookie('token', jwt, { 149 | domain: '.hotel-prisma.ml', 150 | httpOnly: false, 151 | sameSite: 'none', 152 | secure: true, 153 | }); 154 | return socialUser; 155 | } 156 | // sign JWT hợp lệ 157 | // Việc jwt.verify là optional nhờ vào decorator GqlUser() 158 | const jwt = jwt2.sign( 159 | { 160 | id: user.id, 161 | name: user.first_name, 162 | accessToken, 163 | }, 164 | process.env.JWT_SECRET, 165 | ); 166 | res.cookie('token', jwt, { 167 | domain: '.hotel-prisma.ml', 168 | httpOnly: false, 169 | sameSite: 'none', 170 | secure: true, 171 | }); 172 | return user; 173 | } 174 | @Mutation() 175 | async signup( 176 | // DTO file eli5: Extends các field của class và gắn thêm các thuộc tính layer 177 | // Ở đây dùng để validate, rằng buộc type password 178 | @Args('signUpInput') signUpInputDto: SignUpInputDto, 179 | @ResGql() res: Response, 180 | ) { 181 | const emailExists = await this.prisma.client.$exists.user({ 182 | email: signUpInputDto.email, 183 | }); 184 | if (emailExists) { 185 | throw Error('Email is already in use'); 186 | } 187 | 188 | // Validate bằng username thì uncomment dòng dưới 189 | 190 | // const userExists = this.prisma.client.$exists.user({ 191 | // username: signUpInputDto.username, 192 | // }); 193 | 194 | // if (userExists) { 195 | // console.log(signUpInputDto.username) 196 | // throw Error('Username is already in use'); 197 | // } 198 | if (!signUpInputDto.username) { 199 | throw Error('username only contain alphabet letters or numbers'); 200 | } 201 | const password = await bcrypt.hash(signUpInputDto.password, 10); 202 | const user = await this.prisma.client.createUser({ 203 | ...signUpInputDto, 204 | password, 205 | first_name: signUpInputDto.first_name, 206 | last_name: signUpInputDto.last_name, 207 | role: 'Normal', 208 | // username: signUpInputDto.username, 209 | }); 210 | console.log(process.env.JWT_SECRET); 211 | const jwt = jwt2.sign( 212 | { 213 | id: user.id, 214 | name: user.first_name + ' ' + user.last_name, 215 | roles: 'user', 216 | }, 217 | process.env.JWT_SECRET, 218 | ); 219 | res.cookie('token', jwt, { 220 | domain: '.hotel-prisma.ml', 221 | httpOnly: false, 222 | sameSite: 'none', 223 | secure: true, 224 | }); 225 | return user; 226 | } 227 | @Mutation() 228 | @UseGuards(GqlAuthGuard) 229 | async updatePassword( 230 | @GqlUser() user: User, 231 | @Args('password') 232 | { oldPassword, confirmPassword, newPassword }: UpdatePassword, // @ResGql() res: Response, 233 | ) { 234 | const userValid = await this.prisma.client.user({ id: user.id }); 235 | if (!userValid) { 236 | throw Error('Must login'); 237 | } 238 | 239 | const valid = await bcrypt.compare(oldPassword, user.password); 240 | if (!valid) { 241 | throw Error('Old Password is not right'); 242 | } 243 | if (newPassword === oldPassword) { 244 | throw Error('New Password and Old Password field must be different'); 245 | } 246 | if (confirmPassword !== newPassword) { 247 | console.log('false'); 248 | throw Error('Password Confirm and New Password field must be the same'); 249 | } 250 | const password = await bcrypt.hash(newPassword, 10); 251 | console.log('hashed ' + password); 252 | // const jwt = jwt2.sign( 253 | // { 254 | // id: user.id, 255 | // name: user.first_name + ' ' + user.last_name, 256 | // roles: 'user', 257 | // }, 258 | // process.env.JWT_SECRET, 259 | // ); 260 | // res.cookie('token', jwt, { httpOnly: false }); 261 | return this.prisma.client.updateUser({ 262 | where: { 263 | id: user.id, 264 | }, 265 | data: { 266 | password, 267 | }, 268 | }); 269 | } 270 | @Mutation() 271 | async changePasswordFromForgetPassword( 272 | @Args('password') password, 273 | @Args('email') email, 274 | @ResGql() res: Response, 275 | ) { 276 | try { 277 | const userValid = await this.prisma.client.user({ email }); 278 | if (!userValid) { 279 | throw Error('Invalid user'); 280 | } 281 | 282 | const newPassword = await bcrypt.hash(password, 10); 283 | const updatedUser = await this.prisma.client.updateUser({ 284 | where: { 285 | email, 286 | }, 287 | data: { 288 | password: newPassword, 289 | }, 290 | }); 291 | const jwt = jwt2.sign( 292 | { 293 | id: updatedUser.id, 294 | name: updatedUser.first_name, 295 | roles: 'user', 296 | }, 297 | process.env.JWT_SECRET, 298 | ); 299 | res.clearCookie('reset-password', { domain: '.hotel-prisma.ml' }); //Khi làm development thì bỏ domain đi 300 | // res.cookie('token', jwt, { httpOnly: false }); 301 | return updatedUser; 302 | } catch (e) { 303 | console.log(e); 304 | } 305 | } 306 | } 307 | -------------------------------------------------------------------------------- /src/auth/auth.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { AuthService } from './auth.service'; 3 | 4 | describe('AuthService', () => { 5 | let service: AuthService; 6 | 7 | beforeEach(async () => { 8 | const module: TestingModule = await Test.createTestingModule({ 9 | providers: [AuthService], 10 | }).compile(); 11 | 12 | service = module.get(AuthService); 13 | }); 14 | 15 | it('should be defined', () => { 16 | expect(service).toBeDefined(); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /src/auth/auth.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { PrismaService } from 'src/prisma/prisma.service'; 3 | import { User } from 'src/graphql.schema.generated'; 4 | 5 | @Injectable() 6 | export class AuthService { 7 | constructor(private readonly prisma: PrismaService) {} 8 | async validate({ id }): Promise { 9 | const user = await this.prisma.client.user({ id }); 10 | if (!user) throw Error('Authenticate validation error'); 11 | return user 12 | } 13 | async socialValidate({ email }): Promise { 14 | const user = await this.prisma.client.user({ email }); 15 | console.log(user) 16 | if (!user) throw Error('Authenticate validation error'); 17 | return user 18 | } 19 | // Check có user ko bằng cách find Id 20 | } 21 | -------------------------------------------------------------------------------- /src/auth/facebook.strategy.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { PassportStrategy } from '@nestjs/passport'; 3 | import * as FacebookTokenStrategy from 'passport-facebook-token'; 4 | import { AuthService } from './auth.service'; 5 | import { config } from 'dotenv'; 6 | config(); 7 | @Injectable() 8 | export class FacebookStrategy extends PassportStrategy( 9 | FacebookTokenStrategy, 10 | 'facebook-token', 11 | ) { 12 | constructor(private readonly authService: AuthService) { 13 | super({ 14 | clientID: process.env.FACEBOOK_APP_ID, 15 | clientSecret: process.env.FACEBOOK_SECRET_KEY, 16 | }); 17 | } 18 | 19 | async validate( 20 | request: any, 21 | accessToken: string, 22 | refreshToken: string, 23 | profile: any, 24 | done: any, 25 | ) { 26 | try { 27 | console.log(accessToken); 28 | console.log(`Got a profile: `, profile); 29 | 30 | const jwt = 'placeholderJWT'; 31 | const user = { 32 | jwt, 33 | }; 34 | done(null, user); 35 | return this.authService.validate(profile); 36 | } catch (err) { 37 | console.log(`Got an error: `, err); 38 | done(err, false); 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/auth/github/github.controller.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { GithubController } from './github.controller'; 3 | 4 | describe('Github Controller', () => { 5 | let controller: GithubController; 6 | 7 | beforeEach(async () => { 8 | const module: TestingModule = await Test.createTestingModule({ 9 | controllers: [GithubController], 10 | }).compile(); 11 | 12 | controller = module.get(GithubController); 13 | }); 14 | 15 | it('should be defined', () => { 16 | expect(controller).toBeDefined(); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /src/auth/github/github.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Post, Res, Body } from '@nestjs/common'; 2 | import { PrismaService } from 'src/prisma/prisma.service'; 3 | import { v4 as uuidv4 } from 'uuid'; 4 | import { Response } from 'express'; 5 | import * as superagent from 'superagent'; 6 | import * as jwt2 from 'jsonwebtoken'; 7 | // Vì passport cho github strategy còn hạn chế nên làm theo express style 8 | @Controller('github') 9 | export class GithubController { 10 | constructor(private readonly prisma: PrismaService) {} 11 | @Post('callback') 12 | async githubAuth(@Body() code, @Res() res: Response) { 13 | try { 14 | console.log(code); 15 | const accessToken = await superagent 16 | .post('https://github.com/login/oauth/access_token') 17 | .send({ 18 | client_id: process.env.GITHUB_CLIENT_ID, 19 | client_secret: process.env.GITHUB_CLIENT_SECRET, 20 | code: code.code, 21 | }) 22 | .set('Accept', 'application/json'); 23 | const userTokenRequest = accessToken.body.access_token; 24 | const userPayload = await superagent 25 | .get('https://api.github.com/user') 26 | .set('user-agent', 'github') // docs bảo bắt buộc mọi req đén api.github.com phải có header user-agent 27 | .set('Authorization', `Bearer ${userTokenRequest}`); //request bằng header Authorization 28 | const userEmails = await superagent //1 số users hide github email, phải get riêng 29 | .get('https://api.github.com/user/emails') 30 | .set('user-agent', 'github') // docs bảo bắt buộc mọi req đén api.github.com phải có header user-agent 31 | .set('Authorization', `Bearer ${userTokenRequest}`); //request bằng header Authorization 32 | const userInfo = userPayload.body; 33 | const user = await this.prisma.client.user({ 34 | email: userEmails.body[0].email, 35 | }); 36 | if (user) { 37 | const jwt = jwt2.sign( 38 | { 39 | id: user.id, 40 | name: user.first_name, 41 | accessToken, 42 | }, 43 | process.env.JWT_SECRET, 44 | ); 45 | res.cookie('token', jwt, { 46 | domain: '.hotel-prisma.ml', 47 | httpOnly: false, 48 | sameSite: 'none', 49 | secure: true, 50 | }); 51 | const userSendToClient = { 52 | first_name: user.first_name, 53 | last_name: user.last_name, 54 | cover_pic_main: user.cover_pic_main, 55 | profile_pic_main: user.profile_pic_main, 56 | email: user.email, 57 | id: user.id, 58 | role: user.role, 59 | }; 60 | res.status(200).json({ userSendToClient }); 61 | return user; 62 | } 63 | // userEmails.body.map(async i => { 64 | // console.log(i.email); 65 | // }); 66 | const userCreated = await this.prisma.client.createUser({ 67 | first_name: userInfo.login, 68 | last_name: '', 69 | password: uuidv4(), 70 | username: userInfo.node_id, 71 | content: userInfo.bio || 'Nothing', 72 | email: userEmails.body[0].email, 73 | profile_pic_main: userInfo.avatar_url, 74 | cover_pic_main: 'https://i.imgur.com/lXybeGM.png', 75 | role: 'Normal', 76 | }); 77 | const jwt = jwt2.sign( 78 | { 79 | id: userCreated.id, 80 | name: userCreated.first_name, 81 | accessToken, 82 | }, 83 | process.env.JWT_SECRET, 84 | ); 85 | res.cookie('token', jwt, { 86 | domain: '.hotel-prisma.ml', 87 | httpOnly: false, 88 | sameSite: 'none', 89 | secure: true, 90 | }); 91 | const userSendToClient = { 92 | first_name: userCreated.first_name, 93 | last_name: userCreated.last_name, 94 | cover_pic_main: userCreated.cover_pic_main, 95 | profile_pic_main: userCreated.profile_pic_main, 96 | email: userCreated.email, 97 | id: userCreated.id, 98 | role: userCreated.role, 99 | }; 100 | res.status(200).json({ userSendToClient }); 101 | return userCreated; 102 | } catch (e) { 103 | console.log(e); 104 | } 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/auth/google.strategy.ts: -------------------------------------------------------------------------------- 1 | import { PassportStrategy } from '@nestjs/passport'; 2 | import { Strategy, VerifyCallback } from 'passport-google-oauth20'; 3 | import { config } from 'dotenv'; 4 | 5 | import { Injectable } from '@nestjs/common'; 6 | 7 | config(); 8 | 9 | @Injectable() 10 | export class GoogleStrategy extends PassportStrategy(Strategy, 'google') { 11 | 12 | constructor() { 13 | super({ 14 | clientID: process.env.GOOGLE_CLIENT_ID, 15 | clientSecret: process.env.GOOGLE_SECRET, 16 | // callbackURL: 'http://localhost:3000/google/redirect', 17 | // scope: ['email', 'profile'], 18 | }); 19 | } 20 | 21 | async validate (accessToken: string, refreshToken: string, profile: any, done: VerifyCallback): Promise { 22 | const { name, emails, photos } = profile 23 | const user = { 24 | email: emails[0].value, 25 | firstName: name.givenName, 26 | lastName: name.familyName, 27 | picture: photos[0].value, 28 | accessToken 29 | } 30 | done(null, user); 31 | } 32 | } -------------------------------------------------------------------------------- /src/auth/graphql-auth.guard.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, ExecutionContext } from '@nestjs/common'; 2 | import { AuthGuard } from '@nestjs/passport'; 3 | import { GqlExecutionContext } from '@nestjs/graphql'; 4 | @Injectable() 5 | // Nếu muốn sử dụng trực tiếp ko thông qua decorator GqlUser() 6 | // Hoặc tự config được github strategy thì xài uncomment dòng dưới 7 | // export class GqlAuthGuard extends AuthGuard(['jwt','facebook-token, google']) { 8 | export class GqlAuthGuard extends AuthGuard('jwt') { 9 | getRequest(context: ExecutionContext) { 10 | const ctx = GqlExecutionContext.create(context); 11 | return ctx.getContext().req; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/auth/jwt.strategy.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { PassportStrategy } from '@nestjs/passport'; 3 | import { Strategy } from 'passport-jwt'; 4 | import { Request } from 'express'; 5 | import { AuthService } from './auth.service'; 6 | 7 | // Lấy và validate token ở đây 8 | //Lấy từ cookie, và pass .env JWT_SECRET 9 | // Để parse cookies thì defined global cookie-parser 10 | const cookieExtractor = (req: Request): string | null => { 11 | let token = null; 12 | if (req && req.cookies) { 13 | token = req.cookies.token; 14 | } 15 | return token; 16 | }; 17 | 18 | @Injectable() 19 | export class JwtStrategy extends PassportStrategy(Strategy) { 20 | constructor(private readonly authService: AuthService) { 21 | super({ 22 | jwtFromRequest: cookieExtractor, 23 | secretOrKey: process.env.JWT_SECRET, 24 | }); 25 | } 26 | validate(payload) { 27 | return this.authService.validate(payload); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/auth/sign-up-input.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsEmail, MinLength, IsAlphanumeric } from 'class-validator'; 2 | import { SignUpInput } from '../graphql.schema.generated'; 3 | 4 | export class SignUpInputDto extends SignUpInput { 5 | @IsEmail() 6 | readonly email: string 7 | @MinLength(6) 8 | readonly password: string 9 | @IsAlphanumeric() 10 | readonly username: string 11 | } 12 | -------------------------------------------------------------------------------- /src/aws/aws.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { AwsService } from './aws.service'; 3 | 4 | describe('AwsService', () => { 5 | let service: AwsService; 6 | 7 | beforeEach(async () => { 8 | const module: TestingModule = await Test.createTestingModule({ 9 | providers: [AwsService], 10 | }).compile(); 11 | 12 | service = module.get(AwsService); 13 | }); 14 | 15 | it('should be defined', () => { 16 | expect(service).toBeDefined(); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /src/aws/aws.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import * as AWS from 'aws-sdk'; 3 | 4 | @Injectable() 5 | export class AwsService { 6 | public async uploadFile(file: any): Promise { 7 | const urlArray = []; 8 | // console.log( file); 9 | const AWS_S3_BUCKET_NAME = process.env.AWS_S3_BUCKET_NAME; 10 | const s3 = new AWS.S3({ 11 | accessKeyId: process.env.AWS_S3_ACCESS_KEY_ID, 12 | secretAccessKey: process.env.AWS_S3_SECRET_ACCESS_KEY, 13 | }); 14 | await file.map(async (file, i: number) => { 15 | const urlKey = `filePath/${file.originalname}`; 16 | const params = { 17 | Body: file.buffer, 18 | Bucket: AWS_S3_BUCKET_NAME, 19 | Key: urlKey, 20 | ACL: 'public-read', //Để đọc được url 21 | }; 22 | // console.log(params); 23 | 24 | // getSignedUrl chỉ có callback, có thể convert thành Promise như cách dưới 25 | // Hoặc xài package bluebird 26 | const url = new Promise((resolve, reject) => { 27 | s3.getSignedUrl('putObject', params, (err, data) => { 28 | if (err) { 29 | console.log(err); 30 | reject(err); 31 | // res.json({success: false, error: err}) 32 | } 33 | const returnData = { 34 | // Có thể xài lib uuid() 35 | uid: `${file.originalname}-${file.size - i}`, 36 | signedRequest: data, 37 | url: `https://${AWS_S3_BUCKET_NAME}.s3.amazonaws.com/filePath/${file.originalname}`, 38 | }; 39 | resolve(returnData); 40 | }); 41 | }); 42 | urlArray.push(url); 43 | 44 | // Upload lên aws 45 | const data = await s3 46 | .putObject(params) 47 | .promise() 48 | .then( 49 | data => { 50 | return urlKey; 51 | }, 52 | err => { 53 | console.log(err); 54 | return err; 55 | }, 56 | ); 57 | }); 58 | return Promise.all(urlArray).then(r => r); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/cron/cron.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { PrismaModule } from 'src/prisma/prisma.module'; 3 | import { MailService } from 'src/services/sendEmail'; 4 | @Module({ 5 | providers: [MailService], 6 | imports: [PrismaModule], 7 | }) 8 | export class CronModule {} 9 | -------------------------------------------------------------------------------- /src/cron/cron.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { CronService } from './cron.service'; 3 | 4 | describe('CronService', () => { 5 | let service: CronService; 6 | 7 | beforeEach(async () => { 8 | const module: TestingModule = await Test.createTestingModule({ 9 | providers: [CronService], 10 | }).compile(); 11 | 12 | service = module.get(CronService); 13 | }); 14 | 15 | it('should be defined', () => { 16 | expect(service).toBeDefined(); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /src/cron/cron.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, Logger } from '@nestjs/common'; 2 | import { Cron, CronExpression } from '@nestjs/schedule'; 3 | import { PrismaService } from 'src/prisma/prisma.service'; 4 | import { MailService } from 'src/services/sendEmail'; 5 | @Injectable() 6 | export class CronService { 7 | constructor( 8 | private readonly prisma: PrismaService, 9 | private readonly mailService: MailService, 10 | ) {} 11 | // private readonly mailService: MailService; 12 | private readonly logger = new Logger(CronService.name); 13 | 14 | @Cron(CronExpression.EVERY_DAY_AT_9PM, { 15 | name: 'notiCron', 16 | }) 17 | async handleCron() { 18 | // Có thể ứng dụng làm chức năng subscribe blog, newsletter 19 | // Demo mẫu gửi email 20 | // Thường cronn email nên service bên thứ 3 21 | // Tham khảo easycron.com 22 | // Timeout delay lại time 23 | // Interval executes callback đúng sau x asterisk 24 | // Có thể dùng gọi sự kiện delete prisma hay process trong project này là coupon hết hạn,... 25 | // Ở đây mock nhanh bằng cách gửi về process.env.LOCAL_EMAIL (admin) 26 | const pendingTransactions = await this.prisma.client 27 | .transactionsConnection({ 28 | where: { 29 | transactionHotelManagerId: '5f2b768aa7b11b00078d09d8', 30 | transactionStatus: 'PENDING', 31 | }, 32 | }) 33 | .aggregate() 34 | .count(); 35 | const message = `You are having ${pendingTransactions} pending transactions`; 36 | this.logger.debug(message); 37 | return this.mailService.sendContact( 38 | 'CRON ABOUT NUMBER OF PENDING TRANSACTIONS', 39 | 'minha1403@gmail.com', 40 | message, 41 | '00000x00000', 42 | ); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/graphql.options.ts: -------------------------------------------------------------------------------- 1 | import { GqlModuleOptions, GqlOptionsFactory } from '@nestjs/graphql'; 2 | import { Injectable } from '@nestjs/common'; 3 | import { join } from 'path'; 4 | 5 | @Injectable() 6 | export class GraphqlOptions implements GqlOptionsFactory { 7 | createGqlOptions(): Promise | GqlModuleOptions { 8 | return { 9 | context: ({ req, res }) => ({ req, res }), 10 | typePaths: ['./src/*/*.graphql'], // path for gql schema files 11 | installSubscriptionHandlers: true, 12 | resolverValidationOptions: { 13 | requireResolversForResolveType: false, 14 | }, 15 | definitions: { 16 | // will generate .ts types from gql schema files 17 | path: join(process.cwd(), 'src/graphql.schema.generated.ts'), 18 | outputAs: 'class', 19 | }, 20 | debug: true, 21 | introspection: true, 22 | playground: true, 23 | cors: false, 24 | uploads: { 25 | maxFileSize: 10000000, // 10 MB 26 | maxFiles: 5, 27 | }, 28 | }; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/graphql.schema.generated.ts: -------------------------------------------------------------------------------- 1 | 2 | /** ------------------------------------------------------ 3 | * THIS FILE WAS AUTOMATICALLY GENERATED (DO NOT MODIFY) 4 | * ------------------------------------------------------- 5 | */ 6 | 7 | /* tslint:disable */ 8 | /* eslint-disable */ 9 | export class AddHotelInput { 10 | hotelName?: string; 11 | pricePerNight?: number; 12 | hotelDetails?: string; 13 | guest?: number; 14 | rooms?: number; 15 | price?: number; 16 | hotelPhotos?: ImageInput[]; 17 | location?: LocationInput[]; 18 | locationDescription?: string; 19 | contactNumber?: string; 20 | propertyType?: string; 21 | isNegotiable?: boolean; 22 | wifiAvailability?: boolean; 23 | airCondition?: boolean; 24 | parking?: boolean; 25 | poolAvailability?: boolean; 26 | extraBed?: boolean; 27 | } 28 | 29 | export class AmenitiesSearchInput { 30 | wifiAvailability?: boolean; 31 | poolAvailability?: boolean; 32 | parkingAvailability?: boolean; 33 | airCondition?: boolean; 34 | rooms?: number; 35 | guest?: number; 36 | } 37 | 38 | export class CategoriesInput { 39 | slug?: string; 40 | name?: string; 41 | image?: ImageInput; 42 | } 43 | 44 | export class ContactInput { 45 | subject: string; 46 | message?: string; 47 | email?: string; 48 | cellNumber?: string; 49 | } 50 | 51 | export class CouponCheckedPayload { 52 | couponId?: string; 53 | couponName?: string; 54 | couponValue?: number; 55 | couponType?: number; 56 | } 57 | 58 | export class CouponInput { 59 | couponName: string; 60 | couponDescription?: string; 61 | couponType?: number; 62 | couponValue?: number; 63 | couponQuantity?: number; 64 | couponStartDate?: string; 65 | couponEndDate?: string; 66 | couponRange?: number; 67 | } 68 | 69 | export class DeletePhotosInput { 70 | id: string; 71 | } 72 | 73 | export class ImageInput { 74 | url?: string; 75 | } 76 | 77 | export class LocationInput { 78 | lat?: number; 79 | lng?: number; 80 | formattedAddress?: string; 81 | zipcode?: string; 82 | city?: string; 83 | state_long?: string; 84 | state_short?: string; 85 | country_long?: string; 86 | country_short?: string; 87 | } 88 | 89 | export class LoginInput { 90 | email: string; 91 | password: string; 92 | } 93 | 94 | export class MockInput { 95 | title?: string[]; 96 | } 97 | 98 | export class ReviewFieldInput { 99 | rating?: number; 100 | ratingFieldName?: string; 101 | } 102 | 103 | export class ReviewInput { 104 | reviewOverall?: number; 105 | reviewTitle?: string; 106 | reviewTips?: string; 107 | reviewText?: string; 108 | sortOfTrip?: string; 109 | reviewFieldInput?: ReviewFieldInput[]; 110 | reviewOptionals?: ReviewOptionalsInput[]; 111 | reviewPics?: ImageInput[]; 112 | } 113 | 114 | export class ReviewOptionalsInput { 115 | option?: string; 116 | optionField?: string; 117 | } 118 | 119 | export class SearchInput { 120 | minPrice?: number; 121 | maxPrice?: number; 122 | } 123 | 124 | export class SignUpInput { 125 | username?: string; 126 | first_name?: string; 127 | last_name?: string; 128 | email: string; 129 | password: string; 130 | profile_pic_main?: string; 131 | cover_pic_main?: string; 132 | } 133 | 134 | export class SocialInput { 135 | facebook?: string; 136 | instagram?: string; 137 | twitter?: string; 138 | } 139 | 140 | export class TransactionInput { 141 | transactionHotelName?: string; 142 | transactionHotelManagerId?: string; 143 | transactionHotelType?: string; 144 | transactionPrice?: number; 145 | transactionAuthorName?: string; 146 | transactionAuthorEmail?: string; 147 | transactionAuthorContactNumber?: string; 148 | transactionAuthorSpecial?: string; 149 | transactionAuthorNote?: string; 150 | transactionLocationFormattedAddress?: string; 151 | transactionLocationLat?: number; 152 | transactionLocationLng?: number; 153 | transactionRoom?: number; 154 | transactionGuest?: number; 155 | transactionRange?: number; 156 | transactionStartDate?: string; 157 | transactionEndDate?: string; 158 | transactionStripeId?: string; 159 | } 160 | 161 | export class UpdatePassword { 162 | confirmPassword: string; 163 | oldPassword: string; 164 | newPassword: string; 165 | } 166 | 167 | export class UpdatePhotosInput { 168 | url?: string; 169 | uid?: string; 170 | signedRequest?: string; 171 | } 172 | 173 | export class UpdateProfileInput { 174 | first_name?: string; 175 | last_name?: string; 176 | date_of_birth?: string; 177 | gender?: string; 178 | email?: string; 179 | content?: string; 180 | agent_location?: LocationInput; 181 | cellNumber?: string; 182 | } 183 | 184 | export class Amenities { 185 | id: string; 186 | guestRoom?: number; 187 | bedRoom?: number; 188 | wifiAvailability?: boolean; 189 | parkingAvailability?: boolean; 190 | poolAvailability?: boolean; 191 | airCondition?: boolean; 192 | extraBedFacility?: boolean; 193 | } 194 | 195 | export class AuthPayload { 196 | id?: string; 197 | email?: string; 198 | first_name?: string; 199 | last_name?: string; 200 | profile_pic_main?: string; 201 | cover_pic_main?: string; 202 | role?: string; 203 | } 204 | 205 | export class Categories { 206 | id: string; 207 | slug?: string; 208 | name?: string; 209 | image?: Image; 210 | } 211 | 212 | export class Coupon { 213 | couponId: string; 214 | couponName: string; 215 | couponDescription?: string; 216 | couponAuthor?: User; 217 | couponAuthorId?: string; 218 | couponType?: number; 219 | couponValue?: number; 220 | couponQuantity?: number; 221 | couponStartDate?: string; 222 | couponEndDate?: string; 223 | couponRange?: number; 224 | couponTarget?: Hotel[]; 225 | createdAt: string; 226 | updatedAt: string; 227 | } 228 | 229 | export class Gallery { 230 | id: string; 231 | uid?: string; 232 | url?: string; 233 | signedRequest?: string; 234 | } 235 | 236 | export class Hotel { 237 | id: string; 238 | peopleLiked?: User[]; 239 | peopleReviewed?: User[]; 240 | couponsAvailable?: Coupon[]; 241 | connectId?: User; 242 | agentId?: string; 243 | agentEmail?: string; 244 | agentName?: string; 245 | title?: string; 246 | slug?: string; 247 | status?: string; 248 | content?: string; 249 | price?: number; 250 | isNegotiable?: boolean; 251 | propertyType?: string; 252 | condition?: string; 253 | rating?: number; 254 | ratingCount?: number; 255 | contactNumber?: string; 256 | termsAndCondition?: string; 257 | amenities?: Amenities[]; 258 | image?: Image; 259 | location?: Location[]; 260 | gallery?: Gallery[]; 261 | categories?: Categories[]; 262 | reviews?: Reviews[]; 263 | createdAt: string; 264 | updatedAt: string; 265 | } 266 | 267 | export class HotelPhotos { 268 | url: string; 269 | } 270 | 271 | export class Image { 272 | id: string; 273 | url?: string; 274 | thumb_url?: string; 275 | } 276 | 277 | export class Location { 278 | id: string; 279 | lat?: number; 280 | lng?: number; 281 | formattedAddress?: string; 282 | zipcode?: string; 283 | city?: string; 284 | state_long?: string; 285 | state_short?: string; 286 | country_long?: string; 287 | country_short?: string; 288 | } 289 | 290 | export class Message { 291 | reviewAuthorName?: string; 292 | reviewAuthorId?: string; 293 | reviewedHotelId?: Hotel; 294 | reviewedHotelName?: string; 295 | hotelManagerId?: string; 296 | reviewTitle?: string; 297 | reviewText?: string; 298 | peopleReviewedQuanity?: number; 299 | peopleReviewedArr?: User[]; 300 | } 301 | 302 | export abstract class IMutation { 303 | abstract signup(signUpInput?: SignUpInput): AuthPayload | Promise; 304 | 305 | abstract login(loginInput?: LoginInput): AuthPayload | Promise; 306 | 307 | abstract facebookLogin(email?: string, accessToken?: string, socialInfo?: string, socialId?: string, socialProfileLink?: string): AuthPayload | Promise; 308 | 309 | abstract googleLogin(email?: string, accessToken?: string, socialInfo?: string, socialId?: string, profileImage?: string): AuthPayload | Promise; 310 | 311 | abstract createHotel(addHotelInput?: AddHotelInput, location?: LocationInput[], image?: ImageInput[], categories?: CategoriesInput[]): Hotel | Promise; 312 | 313 | abstract sortHotel(type?: string): Hotel[] | Promise; 314 | 315 | abstract likeHotel(id: string): Hotel | Promise; 316 | 317 | abstract dislikeHotel(id: string): Hotel | Promise; 318 | 319 | abstract filterHotels(search?: SearchInput, amenities?: AmenitiesSearchInput): Hotel[] | Promise; 320 | 321 | abstract createLocation(location?: LocationInput): Location | Promise; 322 | 323 | abstract updateProfile(profile?: UpdateProfileInput, location?: LocationInput, social?: SocialInput): User | Promise; 324 | 325 | abstract updatePhotos(photos?: UpdatePhotosInput[]): User | Promise; 326 | 327 | abstract deletePhotos(photos?: DeletePhotosInput[]): User | Promise; 328 | 329 | abstract setProfilePic(url?: string): User | Promise; 330 | 331 | abstract setCoverPic(url?: string): User | Promise; 332 | 333 | abstract updatePassword(password?: UpdatePassword): User | Promise; 334 | 335 | abstract forgetPassword(email?: string): User | Promise; 336 | 337 | abstract changePasswordFromForgetPassword(email?: string, password?: string): User | Promise; 338 | 339 | abstract sendContact(contact?: ContactInput): User | Promise; 340 | 341 | abstract makeReviews(reviews?: ReviewInput, hotelId?: string): Hotel | Promise; 342 | 343 | abstract likeOrDislikeReview(id: string, type?: number): Reviews | Promise; 344 | 345 | abstract checkNotification(id: string): User | Promise; 346 | 347 | abstract readNotification(query?: string): User | Promise; 348 | 349 | abstract deleteAllNotifications(id: string): User | Promise; 350 | 351 | abstract createTransaction(transaction?: TransactionInput, hotelId?: string, userId?: string, coupon?: CouponCheckedPayload): Transaction | Promise; 352 | 353 | abstract createCoupon(coupon?: CouponInput, hotelsId?: string[], type?: number): Coupon[] | Promise; 354 | 355 | abstract checkCoupon(hotelId?: string, couponName?: string): Coupon | Promise; 356 | 357 | abstract processTransactions(id?: string[], type?: number): Transaction | Promise; 358 | 359 | abstract updateTotalUnreadTransactions(): User | Promise; 360 | 361 | abstract deleteCoupons(id?: string[]): Coupon[] | Promise; 362 | 363 | abstract updateStripeId(stripeId?: string, type?: string): User | Promise; 364 | } 365 | 366 | export class Notification { 367 | id: string; 368 | reviewAuthorName?: string; 369 | reviewedHotelName?: string; 370 | reviewTitle?: string; 371 | reviewText?: string; 372 | userNotificationId?: string; 373 | peopleReviewedQuantity?: number; 374 | query?: string; 375 | reviewAuthorProfilePic?: string; 376 | read?: boolean; 377 | old?: boolean; 378 | createdAt: string; 379 | updatedAt: string; 380 | } 381 | 382 | export abstract class IQuery { 383 | abstract allAmenities(): Amenities[] | Promise; 384 | 385 | abstract imageId(id: string): Hotel | Promise; 386 | 387 | abstract locationId(id: string): Location | Promise; 388 | 389 | abstract locations(): Location[] | Promise; 390 | 391 | abstract galleryId(id: string): Gallery | Promise; 392 | 393 | abstract galleries(): Gallery[] | Promise; 394 | 395 | abstract categoryId(id: string): Categories | Promise; 396 | 397 | abstract allCategories(): Categories[] | Promise; 398 | 399 | abstract userPosts(id: string): User | Promise; 400 | 401 | abstract favouritePosts(id: string): User | Promise; 402 | 403 | abstract favouritePostsHeart(id: string): User | Promise; 404 | 405 | abstract getUserInfo(id: string): User | Promise; 406 | 407 | abstract getUserGallery(id: string): User | Promise; 408 | 409 | abstract getUserReviews(id: string): User | Promise; 410 | 411 | abstract getUserNotification(id: string): Notification[] | Promise; 412 | 413 | abstract getUserUnreadNotification(id: string): User | Promise; 414 | 415 | abstract getHotelInfo(id: string): Hotel | Promise; 416 | 417 | abstract getHotelReviews(id: string): Reviews[] | Promise; 418 | 419 | abstract getHotelCoupons(id: string): Coupon[] | Promise; 420 | 421 | abstract getHotelManagerCoupons(): Coupon[] | Promise; 422 | 423 | abstract getReviewsLikeDislike(id: string): Reviews | Promise; 424 | 425 | abstract getAllHotels(location?: LocationInput, type?: string, search?: SearchInput, amenities?: AmenitiesSearchInput, property?: string[]): Hotel[] | Promise; 426 | 427 | abstract getFilteredHotels(location?: string, search?: SearchInput, amenities?: AmenitiesSearchInput, property?: string[]): Hotel[] | Promise; 428 | 429 | abstract getTransactionsHaving(orderBy?: string): Transaction[] | Promise; 430 | 431 | abstract getTransactionDetails(transactionSecretKey?: string): Transaction[] | Promise; 432 | 433 | abstract getTotalUnreadTransactions(): User | Promise; 434 | 435 | abstract getVendorStripeId(id?: string): User | Promise; 436 | } 437 | 438 | export class ReviewFields { 439 | id: string; 440 | rating?: number; 441 | ratingFieldName?: string; 442 | } 443 | 444 | export class ReviewOptionals { 445 | id: string; 446 | option?: string; 447 | optionField?: string; 448 | } 449 | 450 | export class Reviews { 451 | reviewID: string; 452 | reviewTitle?: string; 453 | reviewText?: string; 454 | peopleLiked?: User[]; 455 | peopleDisliked?: User[]; 456 | reviewTips?: string; 457 | sortOfTrip?: string; 458 | reviewAuthorId?: User; 459 | reviewAuthorFirstName?: string; 460 | reviewAuthorLastName?: string; 461 | reviewAuthorEmail?: string; 462 | reviewOverall?: number; 463 | reviewAuthorPic?: string; 464 | reviewedHotel?: Hotel; 465 | reviewedHotelId?: string; 466 | reviewPics?: Image[]; 467 | reviewDate?: string; 468 | reviewOptional?: ReviewOptionals[]; 469 | reviewFields?: ReviewFields[]; 470 | createdAt: string; 471 | updatedAt: string; 472 | } 473 | 474 | export class Social { 475 | facebook?: string; 476 | twitter?: string; 477 | linkedIn?: string; 478 | instagram?: string; 479 | } 480 | 481 | export abstract class ISubscription { 482 | abstract unreadNotification(channelId?: string): Unread | Promise; 483 | 484 | abstract notificationBell(channelId?: string): Notification | Promise; 485 | 486 | abstract realtimeReviews(hotelId?: string): Reviews | Promise; 487 | 488 | abstract realtimeLikeDislike(reviewID?: string): Reviews | Promise; 489 | 490 | abstract realtimeNotificationTransaction(userId?: string): TransactionNotification | Promise; 491 | } 492 | 493 | export class Transaction { 494 | TXID: string; 495 | transactionSecretKey?: string; 496 | transactionHotelName?: string; 497 | transactionHotelManager?: User; 498 | transactionHotelManagerId?: string; 499 | transactionHotelId?: string; 500 | transactionHotelType?: string; 501 | transactionPrice?: number; 502 | transactionAuthor?: User; 503 | transactionAuthorId?: string; 504 | transactionAuthorName?: string; 505 | transactionAuthorEmail?: string; 506 | transactionAuthorContactNumber?: string; 507 | transactionAuthorSpecial?: string; 508 | transactionAuthorNote?: string; 509 | transactionLocationLat?: number; 510 | transactionLocationLng?: number; 511 | transactionLocationFormattedAddress?: string; 512 | transactionRoom?: number; 513 | transactionGuest?: number; 514 | transactionRange?: number; 515 | transactionStatus?: string; 516 | transactionCoupon?: string; 517 | transactionCouponType?: number; 518 | transactionCouponValue?: number; 519 | transactionStartDate?: string; 520 | transactionEndDate?: string; 521 | transactionStripeId?: string; 522 | createdAt: string; 523 | updatedAt: string; 524 | } 525 | 526 | export class TransactionNotification { 527 | TXID: string; 528 | transactionPrice?: number; 529 | } 530 | 531 | export class UncheckTransactions { 532 | id: string; 533 | userUncheckTransactionsId?: string; 534 | userUncheckTransactions?: User; 535 | totalPrice?: number; 536 | totalTransactions?: number; 537 | } 538 | 539 | export class Unread { 540 | unreadNotification?: number; 541 | } 542 | 543 | export class User { 544 | id: string; 545 | first_name: string; 546 | last_name: string; 547 | username: string; 548 | stripeId?: string; 549 | password: string; 550 | email: string; 551 | role?: string; 552 | cellNumber?: string; 553 | profile_pic_main?: string; 554 | cover_pic_main?: string; 555 | profile_pic?: Image[]; 556 | cover_pic?: Image[]; 557 | date_of_birth?: string; 558 | gender?: string; 559 | content?: string; 560 | agent_location?: Location; 561 | gallery?: Gallery[]; 562 | social_profile?: Social; 563 | reviews_maked?: ReviewFields[]; 564 | listed_posts?: Hotel[]; 565 | notification?: Notification[]; 566 | unreadNotification?: number; 567 | favourite_post?: Hotel[]; 568 | reviewed_post?: Hotel[]; 569 | review_liked?: Reviews[]; 570 | review_disliked?: Reviews[]; 571 | transaction_had?: Transaction[]; 572 | transaction_maked?: Transaction[]; 573 | uncheckTransactions?: UncheckTransactions; 574 | coupons_maked?: Coupon[]; 575 | createdAt: string; 576 | updatedAt: string; 577 | } 578 | -------------------------------------------------------------------------------- /src/hotel/hotel.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { HotelService } from './hotel.service'; 3 | import { HotelResolver } from './hotel.resolver'; 4 | import { PrismaModule } from 'src/prisma/prisma.module'; 5 | 6 | @Module({ 7 | providers: [HotelService, HotelResolver], 8 | imports: [PrismaModule], 9 | }) 10 | export class HotelModule {} 11 | -------------------------------------------------------------------------------- /src/hotel/hotel.resolver.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { HotelResolver } from './hotel.resolver'; 3 | 4 | describe('HotelResolver', () => { 5 | let resolver: HotelResolver; 6 | 7 | beforeEach(async () => { 8 | const module: TestingModule = await Test.createTestingModule({ 9 | providers: [HotelResolver], 10 | }).compile(); 11 | 12 | resolver = module.get(HotelResolver); 13 | }); 14 | 15 | it('should be defined', () => { 16 | expect(resolver).toBeDefined(); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /src/hotel/hotel.resolver.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Resolver, 3 | Mutation, 4 | Args, 5 | Parent, 6 | Query, 7 | ResolveField, 8 | } from '@nestjs/graphql'; 9 | import { PrismaService } from 'src/prisma/prisma.service'; 10 | import { 11 | AddHotelInput, 12 | LocationInput, 13 | ImageInput, 14 | // CategoriesInput, 15 | User, 16 | Hotel, 17 | // Reviews, 18 | SearchInput, 19 | AmenitiesSearchInput, 20 | } from 'src/graphql.schema.generated'; 21 | import { UseGuards } from '@nestjs/common'; 22 | import { GqlAuthGuard } from 'src/auth/graphql-auth.guard'; 23 | import { GqlUser } from 'src/shared/decorators/decorator'; 24 | 25 | @Resolver('Hotel') 26 | export class HotelResolver { 27 | constructor(private readonly prisma: PrismaService) {} 28 | format = s => 29 | s 30 | .toLowerCase() 31 | .split(/\s|%20/) 32 | .filter(Boolean) 33 | .join('-'); 34 | 35 | // Các field được connect nhau thì cần resolve theo đúng id, để mutation và query return được subfield 36 | @ResolveField() 37 | async connectId(@Parent() { id }: Hotel) { 38 | // console.log(id); 39 | return this.prisma.client.hotel({ id }).connectId(); 40 | } 41 | @ResolveField() 42 | async peopleLiked(@Parent() { id }: Hotel) { 43 | // console.log(id); 44 | return this.prisma.client.hotel({ id }).peopleLiked(); 45 | } 46 | @ResolveField() 47 | async peopleReviewed(@Parent() { id }: Hotel) { 48 | return await this.prisma.client.hotel({ id }).peopleReviewed(); 49 | } 50 | // Mẹo để có id resolve trong trường hợp parent - data trả về khác type - khai báo schema file thêm 1 field chứa id đó 51 | // Nếu schema đó có link:INLINE (chứa) thì ko cần 52 | @ResolveField() 53 | async reviews(@Parent() hotel: Hotel) { 54 | // Nếu vừa dùng cho cả query và mutation thì nên ráng bóc ra 1 cái stable từ cái id ta đã gán connect 55 | // const id = reviewedHotelId 56 | // const id = reviewedHotelId ? reviewedHotelId : user[0].reviewedHotelId; 57 | const id = hotel.id; 58 | // console.log(id); 59 | // Có thể ko cần ghi trong fragment field đã resolved 60 | const fragment = `fragment getUserInfoFromReview on User 61 | { 62 | reviewTitle 63 | reviewedHotelId 64 | reviewID 65 | reviewText 66 | peopleLiked { 67 | id 68 | } 69 | peopleDisliked { 70 | id 71 | } 72 | sortOfTrip 73 | reviewAuthorEmail 74 | reviewOverall 75 | reviewTips 76 | reviewAuthorPic 77 | reviewPics{ 78 | url 79 | } 80 | reviewDate 81 | reviewOptional{ 82 | option 83 | optionField 84 | } 85 | reviewFields{ 86 | rating 87 | ratingFieldName 88 | } 89 | reviewAuthorFirstName 90 | reviewAuthorLastName 91 | reviewAuthorId 92 | { 93 | id 94 | first_name 95 | last_name 96 | username 97 | password 98 | email 99 | cellNumber 100 | profile_pic 101 | { 102 | id 103 | url 104 | } 105 | cover_pic 106 | { 107 | id 108 | url 109 | } 110 | date_of_birth 111 | gender 112 | content 113 | agent_location{ 114 | id 115 | lat 116 | lng 117 | formattedAddress 118 | zipcode 119 | city 120 | state_long 121 | state_short 122 | country_long 123 | country_short 124 | } 125 | gallery 126 | { 127 | id 128 | url 129 | } 130 | social_profile{ 131 | id 132 | facebook 133 | twitter 134 | linkedIn 135 | instagram 136 | } 137 | 138 | createdAt 139 | updatedAt 140 | } 141 | }`; 142 | // console.log(user[0].reviewedHotelId); 143 | return this.prisma.client 144 | .hotel({ id: id }) 145 | .reviews({ orderBy: 'reviewDate_DESC' }) 146 | .$fragment(fragment); 147 | } 148 | @ResolveField() 149 | async location(@Parent() { id }: Hotel) { 150 | // console.log('id tu con' + id); 151 | return this.prisma.client.hotel({ id }).location(); 152 | } 153 | @ResolveField() 154 | async amenities(@Parent() { id }: Hotel) { 155 | return this.prisma.client.hotel({ id }).amenities(); 156 | } 157 | @ResolveField() 158 | async image(@Parent() { id }: Hotel) { 159 | return this.prisma.client.hotel({ id }).image(); 160 | } 161 | @ResolveField() 162 | async gallery(@Parent() { id }: Hotel) { 163 | return this.prisma.client.hotel({ id }).gallery(); 164 | } 165 | @ResolveField() 166 | async categories(@Parent() { id }: Hotel) { 167 | return this.prisma.client.hotel({ id }).categories(); 168 | } 169 | @Query() 170 | async locationId(@Args('id') id) { 171 | return this.prisma.client.location({ id }); 172 | } 173 | @Query() 174 | async imageId(@Args('id') id) { 175 | return this.prisma.client.image({ id }); 176 | } 177 | @Query() 178 | async galleryId(@Args('id') id) { 179 | return this.prisma.client.gallery({ id }); 180 | } 181 | @Query() 182 | async categoryId(@Args('id') id) { 183 | return this.prisma.client.categories({ id }); 184 | } 185 | @Query() 186 | async allAmenities() { 187 | return this.prisma.client.amenitieses(); 188 | } 189 | @Query() 190 | async locations() { 191 | return this.prisma.client.locations(); 192 | } 193 | @Query() 194 | async galleries() { 195 | return this.prisma.client.galleries(); 196 | } 197 | @Query() 198 | async allCategories() { 199 | return this.prisma.client.categorieses(); 200 | } 201 | @Query() 202 | async getHotelInfo(@Args('id') id) { 203 | return this.prisma.client.hotel({ id }); 204 | } 205 | @Query() 206 | async getHotelReviews(@Args('id') id) { 207 | const fragment = `fragment getHotelReviews on Reviews 208 | { 209 | reviewTitle 210 | reviewedHotelId 211 | reviewID 212 | reviewText 213 | peopleLiked { 214 | id 215 | } 216 | peopleDisliked { 217 | id 218 | } 219 | sortOfTrip 220 | reviewAuthorEmail 221 | reviewOverall 222 | reviewTips 223 | reviewAuthorPic 224 | reviewPics{ 225 | url 226 | } 227 | reviewDate 228 | reviewOptional{ 229 | option 230 | optionField 231 | } 232 | reviewFields{ 233 | rating 234 | ratingFieldName 235 | } 236 | reviewAuthorFirstName 237 | reviewAuthorLastName 238 | reviewAuthorId 239 | { 240 | id 241 | first_name 242 | last_name 243 | username 244 | password 245 | email 246 | cellNumber 247 | profile_pic 248 | { 249 | id 250 | url 251 | } 252 | cover_pic 253 | { 254 | id 255 | url 256 | } 257 | date_of_birth 258 | gender 259 | content 260 | agent_location{ 261 | id 262 | lat 263 | lng 264 | formattedAddress 265 | zipcode 266 | city 267 | state_long 268 | state_short 269 | country_long 270 | country_short 271 | } 272 | gallery 273 | { 274 | id 275 | url 276 | } 277 | social_profile{ 278 | id 279 | facebook 280 | twitter 281 | linkedIn 282 | instagram 283 | } 284 | 285 | createdAt 286 | updatedAt 287 | } 288 | }`; 289 | return this.prisma.client 290 | .hotel({ id }) 291 | .reviews({ orderBy: 'reviewDate_DESC' }) 292 | .$fragment(fragment); 293 | } 294 | @Query() 295 | async getAllHotels( 296 | @Args('type') type, 297 | @Args('search') search: SearchInput, 298 | @Args('amenities') amenities: AmenitiesSearchInput, 299 | @Args('property') property: string[], 300 | @Args('location') location: LocationInput, 301 | ) { 302 | console.log('location'); 303 | console.log(location); 304 | console.log('query'); 305 | console.log(type); 306 | console.log('search'); 307 | console.log(search); 308 | console.log('amenities'); 309 | console.log(amenities); 310 | JSON.stringify(amenities) === '{}' ? (amenities = undefined) : amenities; 311 | console.log(amenities); 312 | console.log('property'); 313 | console.log(property); 314 | JSON.stringify(property) === '{}' || JSON.stringify(property) === '[]' 315 | ? (property = undefined) 316 | : property; 317 | console.log(property); 318 | return this.prisma.client.hotels({ 319 | where: { 320 | amenities_some: { 321 | wifiAvailability: amenities && amenities.wifiAvailability, 322 | parkingAvailability: amenities && amenities.parkingAvailability, 323 | poolAvailability: amenities && amenities.poolAvailability, 324 | airCondition: amenities && amenities.airCondition, 325 | bedRoom: amenities && amenities.rooms, 326 | guestRoom: amenities && amenities.guest, 327 | }, 328 | location_some: { 329 | // formattedAddress_contains: location.formattedAddress, 330 | // city_contains: location.city, 331 | // state_short_contains: location.state_short, 332 | country_long_contains: location && location.country_long, 333 | country_short_contains: location && location.country_short, 334 | }, 335 | propertyType_in: property, 336 | AND: [ 337 | { 338 | price_gte: search && search.minPrice, 339 | }, 340 | { 341 | price_lte: search && search.maxPrice, 342 | }, 343 | ], 344 | }, 345 | orderBy: type, 346 | }); 347 | } 348 | @Query() 349 | async getHotelCoupons(@Args('id') id) { 350 | const fragment = `fragment getHotelCoupons on Hotel { 351 | couponId 352 | couponName 353 | couponDescription 354 | couponAuthor{ 355 | email 356 | } 357 | couponAuthorId 358 | couponType 359 | couponValue 360 | couponQuantity 361 | couponStartDate 362 | couponEndDate 363 | couponTarget{ 364 | id 365 | slug 366 | title 367 | } 368 | createdAt 369 | updatedAt 370 | }`; 371 | return this.prisma.client 372 | .hotel({ id }) 373 | .couponsAvailable({ orderBy: 'createdAt_DESC' }) 374 | .$fragment(fragment); 375 | } 376 | @Query() 377 | @UseGuards(GqlAuthGuard) 378 | async getHotelManagerCoupons(@GqlUser() user: User) { 379 | const fragment = `fragment getHotelManagerCouponsFrag on User{ 380 | couponId 381 | couponName 382 | couponDescription 383 | couponType 384 | couponValue 385 | couponQuantity 386 | couponStartDate 387 | couponEndDate 388 | couponRange 389 | couponTarget{ 390 | slug 391 | id 392 | } 393 | createdAt 394 | updatedAt 395 | }`; 396 | return this.prisma.client 397 | .user({ id: user.id }) 398 | .coupons_maked({ 399 | orderBy: 'createdAt_DESC', 400 | }) 401 | .$fragment(fragment); 402 | } 403 | // Test riêng filter 404 | @Query() 405 | async getFilteredHotels( 406 | @Args('search') search: SearchInput, 407 | @Args('amenities') amenities: AmenitiesSearchInput, 408 | @Args('property') property: string[], 409 | ) { 410 | return this.prisma.client.hotels({ 411 | where: { 412 | amenities_some: { 413 | wifiAvailability: amenities && amenities.wifiAvailability, 414 | parkingAvailability: amenities && amenities.parkingAvailability, 415 | poolAvailability: amenities && amenities.poolAvailability, 416 | airCondition: amenities && amenities.airCondition, 417 | bedRoom: amenities && amenities.rooms, 418 | guestRoom: amenities && amenities.guest, 419 | }, 420 | propertyType_in: property, 421 | AND: [ 422 | { 423 | price_gte: search && search.minPrice, 424 | }, 425 | { 426 | price_lte: search && search.maxPrice, 427 | }, 428 | ], 429 | }, 430 | }); 431 | } 432 | @Mutation() 433 | // Bóc data từ Input và gọi ORM.createHotel và gán cho các key trong model của prisma 434 | @UseGuards(GqlAuthGuard) 435 | async createHotel( 436 | @GqlUser() user: User, 437 | @Args('addHotelInput') 438 | { 439 | hotelName, 440 | pricePerNight, 441 | hotelDetails, 442 | guest, 443 | rooms, 444 | // price, 445 | // hotelPhotos, 446 | locationDescription, 447 | contactNumber, 448 | wifiAvailability, 449 | airCondition, 450 | isNegotiable, 451 | propertyType, 452 | parking, 453 | poolAvailability, 454 | extraBed, 455 | }: AddHotelInput, // Có thể sử dụng Dto cho bớt dài dòng 456 | @Args('location') 457 | items: LocationInput[], 458 | @Args('image') 459 | imageItems: ImageInput[], 460 | // @Args('categories') 461 | // categoryItems: CategoriesInput[], 462 | ) { 463 | // console.log(user.id); 464 | // console.log(imageItems); 465 | const newHotel = await this.prisma.client.createHotel({ 466 | // Của prisma 467 | agentId: user.id, 468 | connectId: { 469 | connect: { 470 | id: user.id, 471 | }, 472 | }, 473 | // Bind agentId vào user.id bằng connect 474 | agentEmail: user.email, 475 | agentName: user.first_name + ' ' + user.last_name, 476 | title: hotelName, 477 | slug: this.format(hotelName), 478 | content: hotelDetails, 479 | price: pricePerNight, 480 | isNegotiable, 481 | propertyType, 482 | termsAndCondition: locationDescription, 483 | contactNumber, 484 | amenities: { 485 | create: { 486 | guestRoom: guest, 487 | bedRoom: rooms, 488 | wifiAvailability, 489 | airCondition, 490 | parkingAvailability: parking, 491 | poolAvailability, 492 | extraBedFacility: extraBed, 493 | }, 494 | }, 495 | image: { 496 | create: { 497 | url: imageItems[0].url, 498 | thumb_url: imageItems[1] ? imageItems[1].url : imageItems[0].url, 499 | }, 500 | }, 501 | location: { 502 | create: items, 503 | }, 504 | gallery: { 505 | create: imageItems, 506 | }, 507 | // 2 có 1 field con là object (cấp 3 hoặc array) 508 | // categories: { 509 | // create: categoryItems.map(i => ({ 510 | // slug: i.slug, 511 | // name: i.name, 512 | // image: { 513 | // create: i.image, 514 | // }, 515 | // })), 516 | // }, 517 | }); 518 | return newHotel; 519 | } 520 | @Mutation() 521 | async sortHotel(@Args('type') type) { 522 | return this.prisma.client.hotels({ orderBy: type }); 523 | } 524 | @Mutation() 525 | async filterHotels() // @Args('search') search: SearchInput, 526 | // @Args('amenities') amenities: AmenitiesSearchInput, 527 | { 528 | return this.prisma.client.hotels(); 529 | } 530 | } 531 | -------------------------------------------------------------------------------- /src/hotel/hotel.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { HotelService } from './hotel.service'; 3 | 4 | describe('HotelService', () => { 5 | let service: HotelService; 6 | 7 | beforeEach(async () => { 8 | const module: TestingModule = await Test.createTestingModule({ 9 | providers: [HotelService], 10 | }).compile(); 11 | 12 | service = module.get(HotelService); 13 | }); 14 | 15 | it('should be defined', () => { 16 | expect(service).toBeDefined(); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /src/hotel/hotel.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | 3 | @Injectable() 4 | export class HotelService {} 5 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { NestFactory } from '@nestjs/core'; 2 | import { AppModule } from './app.module'; 3 | import * as cookierParser from 'cookie-parser'; 4 | async function bootstrap() { 5 | const app = await NestFactory.create(AppModule); 6 | app.use(cookierParser()); 7 | app.enableCors({ 8 | credentials: true, 9 | origin: true, 10 | }); 11 | await app.listen(process.env.PORT || 3000); 12 | } 13 | bootstrap(); 14 | -------------------------------------------------------------------------------- /src/prisma/prisma.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { PrismaService } from './prisma.service'; 3 | 4 | @Module({ 5 | providers: [PrismaService], 6 | exports: [PrismaService], 7 | }) 8 | export class PrismaModule {} 9 | -------------------------------------------------------------------------------- /src/prisma/prisma.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { PrismaService } from './prisma.service'; 3 | 4 | describe('PrismaService', () => { 5 | let service: PrismaService; 6 | 7 | beforeEach(async () => { 8 | const module: TestingModule = await Test.createTestingModule({ 9 | providers: [PrismaService], 10 | }).compile(); 11 | 12 | service = module.get(PrismaService); 13 | }); 14 | 15 | it('should be defined', () => { 16 | expect(service).toBeDefined(); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /src/prisma/prisma.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { Prisma } from 'generated/prisma-client'; 3 | @Injectable() 4 | export class PrismaService { 5 | client: Prisma; 6 | 7 | constructor() { 8 | this.client = new Prisma({ 9 | endpoint: process.env.PRISMA_DOCKER || 'http://prisma:4466', 10 | }); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/schema/gql-api.graphql: -------------------------------------------------------------------------------- 1 | ##GraphQL SDL 2 | type User { 3 | id: ID! 4 | first_name: String! 5 | last_name: String! 6 | username: String! 7 | stripeId: String 8 | password: String! 9 | email: String! 10 | role: String 11 | cellNumber: String 12 | profile_pic_main: String 13 | cover_pic_main: String 14 | profile_pic: [Image] 15 | cover_pic: [Image] 16 | date_of_birth: String 17 | gender: String 18 | content: String 19 | agent_location: Location 20 | gallery: [Gallery] 21 | social_profile: Social 22 | reviews_maked: [ReviewFields] 23 | listed_posts: [Hotel] 24 | notification: [Notification] 25 | unreadNotification: Int 26 | favourite_post: [Hotel] 27 | reviewed_post: [Hotel] 28 | review_liked: [Reviews] 29 | review_disliked: [Reviews] 30 | transaction_had: [Transaction] 31 | transaction_maked: [Transaction] 32 | uncheckTransactions: UncheckTransactions 33 | coupons_maked: [Coupon] 34 | createdAt: String! 35 | updatedAt: String! 36 | } 37 | 38 | # enum Gender { 39 | # Male 40 | # Female 41 | # Other 42 | # } 43 | type Hotel { 44 | id: ID! 45 | peopleLiked: [User] 46 | peopleReviewed: [User] 47 | couponsAvailable: [Coupon] 48 | connectId: User 49 | agentId: String 50 | agentEmail: String 51 | agentName: String 52 | title: String 53 | slug: String 54 | status: String 55 | content: String 56 | price: Int 57 | isNegotiable: Boolean 58 | propertyType: String 59 | condition: String 60 | rating: Float 61 | ratingCount: Int 62 | contactNumber: String 63 | termsAndCondition: String 64 | amenities: [Amenities] 65 | image: Image 66 | location: [Location] 67 | gallery: [Gallery] 68 | categories: [Categories] 69 | reviews: [Reviews] 70 | createdAt: String! 71 | updatedAt: String! 72 | } 73 | 74 | type Transaction { 75 | TXID: ID! 76 | transactionSecretKey: String 77 | transactionHotelName: String 78 | transactionHotelManager: User 79 | transactionHotelManagerId: String 80 | transactionHotelId: String 81 | transactionHotelType: String 82 | transactionPrice: Int 83 | transactionAuthor: User 84 | transactionAuthorId: String 85 | transactionAuthorName: String 86 | transactionAuthorEmail: String 87 | transactionAuthorContactNumber: String 88 | transactionAuthorSpecial: String ##Cho đủ field 89 | transactionAuthorNote: String 90 | transactionLocationLat: Float 91 | transactionLocationLng: Float 92 | transactionLocationFormattedAddress: String 93 | transactionRoom: Int 94 | transactionGuest: Int 95 | transactionRange: Int 96 | transactionStatus: String 97 | transactionCoupon: String 98 | transactionCouponType: Int 99 | transactionCouponValue: Int 100 | transactionStartDate: String 101 | transactionEndDate: String 102 | transactionStripeId: String 103 | createdAt: String! 104 | updatedAt: String! 105 | } 106 | 107 | type Coupon { 108 | couponId: ID! 109 | couponName: String! 110 | couponDescription: String 111 | couponAuthor: User 112 | couponAuthorId: String 113 | couponType: Int 114 | couponValue: Int 115 | couponQuantity: Int 116 | couponStartDate: String 117 | couponEndDate: String 118 | couponRange: Int 119 | couponTarget: [Hotel] 120 | createdAt: String! 121 | updatedAt: String! 122 | } 123 | type Reviews { 124 | reviewID: ID! 125 | reviewTitle: String 126 | reviewText: String 127 | peopleLiked: [User] 128 | peopleDisliked: [User] 129 | reviewTips: String 130 | sortOfTrip: String 131 | reviewAuthorId: User 132 | reviewAuthorFirstName: String 133 | reviewAuthorLastName: String 134 | reviewAuthorEmail: String 135 | reviewOverall: Float 136 | reviewAuthorPic: String 137 | reviewedHotel: Hotel 138 | # Có thể ko cần cái này 139 | reviewedHotelId: ID 140 | reviewPics: [Image] 141 | reviewDate: String 142 | reviewOptional: [ReviewOptionals] 143 | reviewFields: [ReviewFields] 144 | createdAt: String! 145 | updatedAt: String! 146 | } 147 | 148 | type ReviewOptionals { 149 | id: ID! 150 | option: String 151 | optionField: String 152 | } 153 | type ReviewFields { 154 | id: ID! 155 | rating: Int 156 | ratingFieldName: String 157 | } 158 | type Amenities { 159 | id: ID! 160 | guestRoom: Int 161 | bedRoom: Int 162 | wifiAvailability: Boolean 163 | parkingAvailability: Boolean 164 | poolAvailability: Boolean 165 | airCondition: Boolean 166 | extraBedFacility: Boolean 167 | } 168 | 169 | type Image { 170 | id: ID! 171 | url: String 172 | thumb_url: String 173 | } 174 | type Gallery { 175 | id: ID! 176 | uid: String 177 | url: String 178 | signedRequest: String 179 | } 180 | 181 | type Location { 182 | id: ID! 183 | lat: Float 184 | lng: Float 185 | formattedAddress: String 186 | zipcode: String 187 | city: String 188 | state_long: String 189 | state_short: String 190 | country_long: String 191 | country_short: String 192 | } 193 | 194 | type Categories { 195 | id: ID! 196 | slug: String 197 | name: String 198 | image: Image 199 | } 200 | type AuthPayload { 201 | id: ID 202 | email: String 203 | first_name: String 204 | last_name: String 205 | profile_pic_main: String 206 | cover_pic_main: String 207 | role: String 208 | } 209 | type HotelPhotos { 210 | url: String! 211 | } 212 | type Social { 213 | facebook: String 214 | twitter: String 215 | linkedIn: String 216 | instagram: String 217 | } 218 | type Query { 219 | allAmenities: [Amenities!]! 220 | imageId(id: ID!): Hotel! 221 | locationId(id: ID!): Location! 222 | locations: [Location] 223 | galleryId(id: ID!): Gallery! 224 | galleries: [Gallery!]! 225 | categoryId(id: ID!): Categories! 226 | allCategories: [Categories!]! 227 | userPosts(id: ID!): User 228 | favouritePosts(id: ID!): User 229 | favouritePostsHeart(id: ID!): User 230 | getUserInfo(id: ID!): User 231 | getUserGallery(id: ID!): User 232 | getUserReviews(id: ID!): User 233 | getUserNotification(id: ID!): [Notification] 234 | getUserUnreadNotification(id: ID!): User 235 | getHotelInfo(id: ID!): Hotel 236 | getHotelReviews(id: ID!): [Reviews] ## Nen tao them 1 folder model Review 237 | getHotelCoupons(id: ID!): [Coupon] 238 | getHotelManagerCoupons: [Coupon] 239 | getReviewsLikeDislike(id: ID!): Reviews 240 | getAllHotels( 241 | location: LocationInput 242 | type: String 243 | search: SearchInput 244 | amenities: AmenitiesSearchInput 245 | property: [String] 246 | ): [Hotel] 247 | getFilteredHotels( 248 | location: String 249 | search: SearchInput 250 | amenities: AmenitiesSearchInput 251 | property: [String] 252 | ): [Hotel] 253 | getTransactionsHaving(orderBy: String): [Transaction] 254 | getTransactionDetails(transactionSecretKey: String): [Transaction] ## Trả về Fragmentable Array 255 | getTotalUnreadTransactions: User 256 | getVendorStripeId(id: String): User 257 | } 258 | type Mutation { 259 | signup(signUpInput: SignUpInput): AuthPayload! 260 | login(loginInput: LoginInput): AuthPayload 261 | facebookLogin(email: String, accessToken: String, socialInfo: String, socialId: String, socialProfileLink: String): AuthPayload 262 | googleLogin(email: String, accessToken: String, socialInfo: String, socialId: String, profileImage: String): AuthPayload 263 | createHotel( 264 | addHotelInput: AddHotelInput 265 | location: [LocationInput] 266 | image: [ImageInput] 267 | categories: [CategoriesInput] 268 | ): Hotel! 269 | sortHotel(type: String): [Hotel] 270 | likeHotel(id: ID!): Hotel 271 | dislikeHotel(id: ID!): Hotel 272 | filterHotels(search: SearchInput, amenities: AmenitiesSearchInput): [Hotel] 273 | createLocation(location: LocationInput): Location! 274 | updateProfile( 275 | profile: UpdateProfileInput 276 | location: LocationInput 277 | social: SocialInput 278 | ): User 279 | updatePhotos(photos: [UpdatePhotosInput]): User 280 | deletePhotos(photos: [DeletePhotosInput]): User 281 | setProfilePic(url: String): User 282 | setCoverPic(url: String): User 283 | updatePassword(password: UpdatePassword): User! 284 | forgetPassword(email: String): User 285 | changePasswordFromForgetPassword(email: String, password: String): User 286 | sendContact(contact: ContactInput): User 287 | makeReviews(reviews: ReviewInput, hotelId: ID): Hotel 288 | likeOrDislikeReview(id: ID!, type: Int): Reviews 289 | checkNotification(id: ID!): User 290 | readNotification(query: String): User 291 | deleteAllNotifications(id: ID!): User 292 | createTransaction( 293 | transaction: TransactionInput 294 | hotelId: String 295 | userId: String 296 | coupon: CouponCheckedPayload 297 | ): Transaction ##userId có thể không có - không bắt login 298 | createCoupon(coupon: CouponInput, hotelsId: [String], type: Int): [Coupon] 299 | checkCoupon(hotelId: String, couponName: String): Coupon 300 | processTransactions(id: [String], type: Int): Transaction 301 | updateTotalUnreadTransactions: User 302 | deleteCoupons(id: [String]): [Coupon] 303 | updateStripeId(stripeId: String, type: String): User 304 | } 305 | input ReviewInput { 306 | reviewOverall: Float 307 | reviewTitle: String 308 | reviewTips: String 309 | reviewText: String 310 | sortOfTrip: String 311 | reviewFieldInput: [ReviewFieldInput] 312 | reviewOptionals: [ReviewOptionalsInput] 313 | reviewPics: [ImageInput] 314 | } 315 | input ReviewFieldInput { 316 | rating: Int 317 | ratingFieldName: String 318 | } 319 | input ReviewOptionalsInput { 320 | option: String 321 | optionField: String 322 | } 323 | input UpdateProfileInput { 324 | first_name: String 325 | last_name: String 326 | date_of_birth: String 327 | gender: String 328 | email: String 329 | content: String 330 | agent_location: LocationInput 331 | cellNumber: String 332 | } 333 | 334 | input UpdatePhotosInput { 335 | url: String 336 | uid: String 337 | signedRequest: String 338 | } 339 | input DeletePhotosInput { 340 | id: ID! 341 | } 342 | 343 | input UpdatePassword { 344 | confirmPassword: String! 345 | oldPassword: String! 346 | newPassword: String! 347 | } 348 | input SocialInput { 349 | facebook: String 350 | instagram: String 351 | twitter: String 352 | } 353 | input LocationInput { 354 | lat: Float 355 | lng: Float 356 | formattedAddress: String 357 | zipcode: String 358 | city: String 359 | state_long: String 360 | state_short: String 361 | country_long: String 362 | country_short: String 363 | } 364 | input ImageInput { 365 | url: String 366 | } 367 | input CategoriesInput { 368 | slug: String 369 | name: String 370 | image: ImageInput 371 | } 372 | input AddHotelInput { 373 | hotelName: String 374 | pricePerNight: Int 375 | hotelDetails: String 376 | guest: Int 377 | rooms: Int 378 | price: Int 379 | # Co the khong can nhung nen ghi vo cho de hieu 380 | hotelPhotos: [ImageInput] 381 | location: [LocationInput] 382 | locationDescription: String 383 | contactNumber: String 384 | propertyType: String 385 | isNegotiable: Boolean 386 | wifiAvailability: Boolean 387 | airCondition: Boolean 388 | parking: Boolean 389 | poolAvailability: Boolean 390 | extraBed: Boolean 391 | } 392 | input SignUpInput { 393 | username: String 394 | first_name: String 395 | last_name: String 396 | email: String! 397 | password: String! 398 | profile_pic_main: String 399 | cover_pic_main: String 400 | } 401 | 402 | input LoginInput { 403 | email: String! 404 | password: String! 405 | } 406 | 407 | input ContactInput { 408 | subject: String! 409 | message: String 410 | email: String 411 | cellNumber: String 412 | } 413 | input MockInput { 414 | title: [String] 415 | } 416 | input AmenitiesSearchInput { 417 | wifiAvailability: Boolean 418 | poolAvailability: Boolean 419 | parkingAvailability: Boolean 420 | airCondition: Boolean 421 | rooms: Int 422 | guest: Int 423 | } 424 | input SearchInput { 425 | minPrice: Int 426 | maxPrice: Int 427 | } 428 | input TransactionInput { 429 | transactionHotelName: String 430 | transactionHotelManagerId: String 431 | transactionHotelType: String 432 | transactionPrice: Int 433 | transactionAuthorName: String 434 | transactionAuthorEmail: String 435 | transactionAuthorContactNumber: String 436 | transactionAuthorSpecial: String ##Cho đủ field 437 | transactionAuthorNote: String 438 | transactionLocationFormattedAddress: String 439 | transactionLocationLat: Float 440 | transactionLocationLng: Float 441 | transactionRoom: Int 442 | transactionGuest: Int 443 | transactionRange: Int 444 | transactionStartDate: String 445 | transactionEndDate: String 446 | transactionStripeId: String 447 | } 448 | input CouponInput { 449 | couponName: String! 450 | couponDescription: String 451 | couponType: Int 452 | couponValue: Int 453 | couponQuantity: Int 454 | couponStartDate: String 455 | couponEndDate: String 456 | couponRange: Int 457 | } 458 | input CouponCheckedPayload { 459 | couponId: ID 460 | couponName: String 461 | couponValue: Int 462 | couponType: Int 463 | } 464 | # Realtime Section 465 | type Subscription { 466 | unreadNotification(channelId: String): Unread 467 | notificationBell(channelId: String): Notification 468 | realtimeReviews(hotelId: String): Reviews 469 | realtimeLikeDislike(reviewID: String): Reviews 470 | realtimeNotificationTransaction(userId: String): TransactionNotification 471 | } 472 | type Message { 473 | # Có thể cho user nếu muốn routing nữa 474 | reviewAuthorName: String 475 | reviewAuthorId: String 476 | reviewedHotelId: Hotel 477 | reviewedHotelName: String 478 | hotelManagerId: String 479 | reviewTitle: String 480 | reviewText: String 481 | peopleReviewedQuanity: Int 482 | peopleReviewedArr: [User] 483 | } 484 | type Notification { 485 | id: ID! 486 | reviewAuthorName: String 487 | reviewedHotelName: String 488 | reviewTitle: String 489 | reviewText: String 490 | userNotificationId: String 491 | peopleReviewedQuantity: Int 492 | query: String 493 | reviewAuthorProfilePic: String 494 | read: Boolean 495 | old: Boolean 496 | createdAt: String! 497 | updatedAt: String! 498 | } 499 | type TransactionNotification { 500 | TXID: ID! 501 | transactionPrice: Int 502 | } 503 | type UncheckTransactions { 504 | id: ID! 505 | userUncheckTransactionsId: String 506 | userUncheckTransactions: User 507 | totalPrice: Int 508 | totalTransactions: Int 509 | } 510 | type Unread { 511 | unreadNotification: Int 512 | } -------------------------------------------------------------------------------- /src/services/sendEmail.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { MailerService } from '@nestjs-modules/mailer'; 3 | import * as mailjet from 'node-mailjet'; 4 | @Injectable() 5 | export class MailService { 6 | constructor(private readonly mailerService: MailerService) {} 7 | public sendEmail(email, code): void { 8 | // console.log(email, process.env.LOCAL_EMAIL, process.env.LOCAL_PASSWORD); 9 | this.mailerService 10 | .sendMail({ 11 | to: email, 12 | from: process.env.LOCAL_EMAIL, 13 | subject: 'Reset password Link ✔', 14 | template: process.cwd() + '/dist/services/template/reset', 15 | context: { 16 | email, 17 | code, 18 | }, 19 | }) 20 | .then(success => { 21 | console.log(success + email); 22 | }) 23 | .catch(err => { 24 | console.log(err); 25 | }); 26 | } 27 | public sendContact(subject, email, messsage, cellNumber): void { 28 | this.mailerService 29 | .sendMail({ 30 | from: email, 31 | to: process.env.LOCAL_EMAIL, 32 | subject, 33 | html: `
34 |

${subject}

35 |

From: ${email}

36 |

Message: ${messsage}

37 |

Phone: ${cellNumber}

38 |
`, 39 | }) 40 | .then(success => { 41 | console.log(success + email); 42 | }) 43 | .catch(err => { 44 | console.log(err); 45 | }); 46 | } 47 | public mockMailjet(data): void { 48 | console.log(data.authorEmail); 49 | const request = mailjet 50 | .connect(process.env.MJ_APIKEY_PUBLIC, process.env.MJ_APIKEY_PRIVATE) 51 | .post('send', { version: 'v3.1' }) 52 | .request({ 53 | Messages: [ 54 | { 55 | From: { 56 | Email: process.env.LOCAL_MJ_EMAIL, 57 | Name: 'Palace TripFinder', 58 | }, 59 | To: [ 60 | { 61 | Email: data.authorEmail, 62 | Name: data.authorName, 63 | }, 64 | ], 65 | TemplateID: 1645231, 66 | TemplateLanguage: true, 67 | Subject: `Receipt about ${data.hotelName} Room: ${data.room} Guest: ${data.guest} of ${data.authorName}`, 68 | Variables: { 69 | firstname: data.authorName, 70 | startDate: data.startDate, 71 | endDate: data.endDate, 72 | hotelName: data.hotelName, 73 | guest: data.guest, 74 | room: data.room, 75 | couponName: data.couponName, 76 | total_price: data.total_price, 77 | order_date: data.order_date, 78 | order_id: data.order_id, 79 | }, 80 | }, 81 | ], 82 | }); 83 | request 84 | .then(result => { 85 | console.log(result.body); 86 | }) 87 | .catch(err => { 88 | console.log(err.statusCode); 89 | }); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/services/template/reset.pug: -------------------------------------------------------------------------------- 1 | doctype html 2 | head 3 | title Reset Password 4 | .container 5 | .row 6 | .col-xs-12.col-sm-6.col-md-4.text-center 7 | h3 Reset Password 8 | h2 #{email} 9 | h2 DO NOT GIVE THIS LINK TO ANYONE 10 | h2 Your password reset link: 11 | https://vercel-v2.hotel-prisma.ml/change-password?code=#{code} 12 | //- .row 13 | //- .col-xs-12.col-sm-6.col-md-4.extra-padding 14 | //- label.idleLabel(for='newPassword') New Password 15 | //- .input-group 16 | //- input#newPassword.form-control.isRequired(type='password' name='newPassword' title='enter your new password' maxlength='50') 17 | //- span.input-group-addon(title='Invalid password') 18 | //- i.fa.fa-lg.fa-fw.fa-times.text-danger 19 | //- .row 20 | //- .col-xs-12.col-sm-6.col-md-4.extra-padding 21 | //- label.idleLabel(for='newPasswordConfirm') Verify password 22 | //- .input-group 23 | //- input#newPasswordConfirm.form-control.isRequired(type='password' name='newPasswordConfirm') 24 | //- span.input-group-addon(title='Passwords match') 25 | //- i.fa.fa-lg.fa-fw.fa-check.text-success 26 | style. 27 | textarea { 28 | resize: none; 29 | } 30 | .idleLabel { 31 | position: absolute; 32 | color: #888888; 33 | margin: 7px 13px; 34 | font-weight: normal; 35 | z-index: 3; 36 | } 37 | .activeLabel { 38 | position: absolute; 39 | color: grey; 40 | font-size: x-small; 41 | margin: -5px 10px; 42 | background-color: #FFFFFF; 43 | border-left: solid #888888 1px; 44 | border-right: solid #888888 1px; 45 | padding: 0 4px; 46 | z-index: 3; 47 | } 48 | .extra-padding { 49 | margin-bottom: 0.5em; 50 | } 51 | -------------------------------------------------------------------------------- /src/shared/decorators/decorator.ts: -------------------------------------------------------------------------------- 1 | import { createParamDecorator, ExecutionContext } from '@nestjs/common'; 2 | import { Response } from 'express'; 3 | import { User } from 'src/graphql.schema.generated'; 4 | import { GqlExecutionContext } from '@nestjs/graphql'; 5 | 6 | // Giống module.export (req, res, next) bên express 7 | // Để access req và user objects dễ dàng từ graphql ctx thì ta có thể 8 | // Tự tạo custom decorator(thực hiện các logic) 9 | // Thay thế cho jwt.verify 10 | // Chi tiết hơn tại: https://docs.nestjs.com/custom-decorators 11 | export const ResGql = createParamDecorator( 12 | // (data, [root, args, ctx, info]): Response => ctx.res, 13 | (data: unknown, context: ExecutionContext): Response => 14 | GqlExecutionContext.create(context).getContext().res, 15 | ); 16 | 17 | export const GqlUser = createParamDecorator( 18 | // (data, [root, args, ctx, info]): User => ctx.req && ctx.req.user, 19 | (data: unknown, context: ExecutionContext): User => { 20 | const ctx = GqlExecutionContext.create(context).getContext(); 21 | return ctx.req && ctx.req.user; 22 | }, 23 | ); 24 | -------------------------------------------------------------------------------- /src/transaction/transaction.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { TransactionResolver } from './transaction.resolver'; 3 | import { PrismaModule } from 'src/prisma/prisma.module'; 4 | import { MailService } from 'src/services/sendEmail'; 5 | import { RedisPubSub } from 'graphql-redis-subscriptions'; 6 | import * as Redis from 'ioredis'; 7 | @Module({ 8 | providers: [ 9 | TransactionResolver, 10 | MailService, 11 | { 12 | provide: 'PUB_SUB', 13 | useFactory: () => { 14 | const options = { 15 | host: 'redis', 16 | port: 6379, 17 | password: 'no', 18 | }; 19 | return new RedisPubSub({ 20 | publisher: new Redis(options), 21 | subscriber: new Redis(options), 22 | }); 23 | }, 24 | // useValue: new PubSub(), 25 | }, 26 | ], 27 | imports: [PrismaModule], 28 | }) 29 | export class TransactionModule {} 30 | -------------------------------------------------------------------------------- /src/transaction/transaction.resolver.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { TransactionResolver } from './transaction.resolver'; 3 | 4 | describe('TransactionResolver', () => { 5 | let resolver: TransactionResolver; 6 | 7 | beforeEach(async () => { 8 | const module: TestingModule = await Test.createTestingModule({ 9 | providers: [TransactionResolver], 10 | }).compile(); 11 | 12 | resolver = module.get(TransactionResolver); 13 | }); 14 | 15 | it('should be defined', () => { 16 | expect(resolver).toBeDefined(); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /src/transaction/transaction.resolver.ts: -------------------------------------------------------------------------------- 1 | import { Resolver, Mutation, Args, Query, Subscription } from '@nestjs/graphql'; 2 | import { SchedulerRegistry } from '@nestjs/schedule'; 3 | import { UseGuards, Inject } from '@nestjs/common'; 4 | import { v4 as uuidv4 } from 'uuid'; 5 | import { PrismaService } from 'src/prisma/prisma.service'; 6 | import { MailService } from 'src/services/sendEmail'; 7 | import { 8 | TransactionInput, 9 | User, 10 | CouponCheckedPayload, 11 | TransactionNotification, 12 | } from 'src/graphql.schema.generated'; 13 | import { GqlUser } from 'src/shared/decorators/decorator'; 14 | import { GqlAuthGuard } from 'src/auth/graphql-auth.guard'; 15 | import { PubSubEngine } from 'graphql-subscriptions'; 16 | 17 | @Resolver('Transaction') 18 | export class TransactionResolver { 19 | constructor( 20 | private readonly prisma: PrismaService, 21 | private readonly mailService: MailService, 22 | @Inject('PUB_SUB') private pubSub: PubSubEngine, 23 | private schedulerRegistry: SchedulerRegistry, 24 | ) {} 25 | expire = coupon => { 26 | const date = new Date(); 27 | const endDate = new Date(coupon); 28 | return date > endDate; 29 | }; 30 | @Query() 31 | @UseGuards(GqlAuthGuard) 32 | async getTransactionsHaving(@GqlUser() user: User) { 33 | const fragment = `fragment transactionFragment on User{ 34 | TXID 35 | transactionSecretKey 36 | transactionHotelName 37 | transactionHotelId 38 | transactionHotelManager{ 39 | first_name 40 | last_name 41 | email 42 | cellNumber 43 | } 44 | transactionHotelManagerId 45 | transactionHotelType 46 | transactionPrice 47 | transactionAuthorId 48 | transactionAuthorName 49 | transactionAuthorEmail 50 | transactionAuthorContactNumber 51 | transactionAuthorSpecial 52 | transactionAuthorNote 53 | transactionLocationLat 54 | transactionLocationLng 55 | transactionLocationFormattedAddress 56 | transactionRoom 57 | transactionGuest 58 | transactionRange 59 | transactionStatus 60 | transactionCoupon 61 | transactionCouponType 62 | transactionCouponValue 63 | transactionStripeId 64 | transactionStartDate 65 | transactionEndDate 66 | }`; 67 | const job = this.schedulerRegistry.getCronJob('notiCron'); 68 | job.stop(); 69 | console.log(job.lastDate()); 70 | return this.prisma.client 71 | .user({ id: user.id }) 72 | .transaction_had({ 73 | orderBy: 'createdAt_DESC', 74 | }) 75 | .$fragment(fragment); 76 | } 77 | @Query() 78 | async getTransactionDetails( 79 | @Args('transactionSecretKey') transactionSecretKey, 80 | ) { 81 | const fragment = `fragment getTransactionDetailsFragment on Transaction{ 82 | transactionHotelManager{ 83 | first_name 84 | last_name 85 | email 86 | cellNumber 87 | } 88 | transactionAuthorName 89 | transactionHotelName 90 | transactionLocationLat 91 | transactionHotelType 92 | transactionPrice 93 | transactionLocationLng 94 | transactionLocationFormattedAddress 95 | transactionRoom 96 | transactionGuest 97 | transactionRange 98 | transactionStatus 99 | transactionCoupon 100 | transactionCouponType 101 | transactionCouponValue 102 | transactionStartDate 103 | transactionEndDate 104 | }`; 105 | return this.prisma.client 106 | .transactions({ 107 | where: { 108 | transactionSecretKey, 109 | }, 110 | }) 111 | .$fragment(fragment); 112 | } 113 | @Query() 114 | @UseGuards(GqlAuthGuard) 115 | async getTotalUnreadTransactions(@GqlUser() user: User) { 116 | const fragment = `fragment totalUnreadTransactions on User { 117 | uncheckTransactions{ 118 | userUncheckTransactionsId 119 | totalPrice 120 | totalTransactions 121 | } 122 | }`; 123 | return await this.prisma.client.user({ id: user.id }).$fragment(fragment); 124 | } 125 | @Mutation() 126 | async checkCoupon(@Args('hotelId') hotelId, @Args('couponName') couponName) { 127 | // Strict thêm chỉ áp dụng cho hotel này 128 | // Field nào unique là đều kiếm được 129 | const fragment = `fragment checkCoupon on Coupon{ 130 | couponQuantity 131 | couponName 132 | couponId 133 | couponType 134 | couponValue 135 | }`; 136 | const couponPayload = await this.prisma.client 137 | .hotel({ id: hotelId }) 138 | .couponsAvailable({ 139 | where: { 140 | couponName, 141 | }, 142 | }) 143 | .$fragment(fragment); 144 | console.log(couponPayload); 145 | 146 | // Coupon hết số lượng 147 | if (couponPayload && couponPayload[0].couponQuantity === 0) { 148 | throw Error('Coupon is running out of uses'); 149 | } 150 | // Coupon hết hạn 151 | if (couponPayload && this.expire(couponPayload[0].couponEndDate)) { 152 | throw Error('Coupon is expired'); 153 | } 154 | if (couponPayload && couponPayload[0].couponType) { 155 | await this.prisma.client.updateCoupon({ 156 | where: { 157 | couponId: couponPayload[0].couponId, 158 | }, 159 | data: { 160 | couponQuantity: couponPayload[0].couponQuantity - 1, 161 | }, 162 | }); 163 | return couponPayload[0]; 164 | } 165 | throw Error('Coupon is invalid for this Hotel'); 166 | } 167 | @Mutation() 168 | async createTransaction( 169 | @Args('transaction') transaction: TransactionInput, 170 | @Args('hotelId') hotelId: string, 171 | @Args('coupon') coupon: CouponCheckedPayload, 172 | @Args('userId') userId, 173 | ) { 174 | console.log(userId); 175 | // Không nhập coupon 176 | 177 | if (!coupon) { 178 | console.log('no coupon'); 179 | const transactionPayload = await this.prisma.client.createTransaction({ 180 | transactionSecretKey: uuidv4(), 181 | transactionHotelId: hotelId, 182 | transactionHotelName: transaction.transactionHotelName, 183 | transactionHotelManagerId: transaction.transactionHotelManagerId, 184 | transactionHotelManager: { 185 | connect: { 186 | id: transaction.transactionHotelManagerId, 187 | }, 188 | }, 189 | transactionHotelType: transaction.transactionHotelType, 190 | transactionPrice: transaction.transactionPrice, 191 | transactionAuthor: userId && { 192 | connect: { 193 | id: userId, 194 | }, 195 | }, 196 | transactionAuthorId: userId || 'non-member', 197 | transactionAuthorName: transaction.transactionAuthorName, 198 | transactionAuthorEmail: transaction.transactionAuthorEmail, 199 | transactionAuthorContactNumber: 200 | transaction.transactionAuthorContactNumber, 201 | transactionAuthorSpecial: transaction.transactionAuthorSpecial, 202 | transactionAuthorNote: transaction.transactionAuthorNote, 203 | transactionStartDate: transaction.transactionStartDate, 204 | transactionEndDate: transaction.transactionEndDate, 205 | transactionRange: transaction.transactionRange, 206 | transactionLocationLat: transaction.transactionLocationLat, 207 | transactionLocationLng: transaction.transactionLocationLng, 208 | transactionLocationFormattedAddress: 209 | transaction.transactionLocationFormattedAddress, 210 | transactionRoom: transaction.transactionRoom, 211 | transactionGuest: transaction.transactionGuest, 212 | transactionStatus: 'PENDING', 213 | transactionStripeId: transaction.transactionStripeId, 214 | transactionCoupon: 'None', 215 | transactionCouponType: 3, // Không xài coupon 216 | transactionCouponValue: 0, 217 | }); 218 | const date = new Date(); 219 | date.getFullYear() + '-' + (date.getMonth() + 1) + '-' + date.getDate(); 220 | const dataMail = { 221 | authorEmail: transaction.transactionAuthorEmail, 222 | authorName: transaction.transactionAuthorName, 223 | firstName: transaction.transactionAuthorName, 224 | startDate: transaction.transactionStartDate, 225 | endDate: transaction.transactionEndDate, 226 | hotelName: transaction.transactionHotelName, 227 | couponName: 'No Coupon', 228 | room: transactionPayload.transactionRoom, 229 | guest: transactionPayload.transactionGuest, 230 | total_price: transactionPayload.transactionPrice, 231 | order_date: 232 | date.getFullYear() + 233 | '-' + 234 | (date.getMonth() + 1) + 235 | '-' + 236 | date.getDate(), 237 | order_id: transactionPayload.transactionSecretKey, 238 | }; 239 | this.mailService.mockMailjet(dataMail); 240 | const transactionNotiPayload = { 241 | TXID: transactionPayload.TXID, 242 | transactionPrice: transaction.transactionPrice, 243 | transactionHotelManagerId: transaction.transactionHotelManagerId, 244 | }; 245 | // Đáng lẽ nên refactor lại 1 loại form noti, truyền type là review, like, transaction 246 | this.pubSub.publish('realtimeNotiTransaction', transactionNotiPayload); 247 | const totalTransactions = 248 | (await this.prisma.client 249 | .user({ 250 | id: transaction.transactionHotelManagerId, 251 | }) 252 | .uncheckTransactions() 253 | .totalTransactions()) || 0; 254 | const totalPrice = 255 | (await this.prisma.client 256 | .user({ 257 | id: transaction.transactionHotelManagerId, 258 | }) 259 | .uncheckTransactions() 260 | .totalPrice()) || 0; 261 | console.log(totalTransactions); 262 | console.log(totalPrice); 263 | // upsert để tối ưu database storage 264 | await this.prisma.client.updateUser({ 265 | where: { 266 | id: transaction.transactionHotelManagerId, 267 | }, 268 | data: { 269 | uncheckTransactions: { 270 | upsert: { 271 | update: { 272 | totalTransactions: totalTransactions + 1, 273 | totalPrice: totalPrice + transaction.transactionPrice, 274 | }, 275 | create: { 276 | userUncheckTransactionsId: 277 | transaction.transactionHotelManagerId, 278 | totalTransactions: totalTransactions + 1, 279 | totalPrice: totalPrice + transaction.transactionPrice, 280 | }, 281 | }, 282 | }, 283 | }, 284 | }); 285 | return transactionPayload; 286 | } 287 | 288 | // Có coupon và nhập coupon hợp lệ 289 | if (coupon) { 290 | console.log('coupon'); 291 | // Nếu muốn update riêng từng Hotel vs 1 coupon thì create bên Hotel 292 | const transactionPayload = await this.prisma.client.createTransaction({ 293 | transactionSecretKey: uuidv4(), 294 | transactionHotelId: hotelId, 295 | transactionHotelName: transaction.transactionHotelName, 296 | transactionHotelManagerId: transaction.transactionHotelManagerId, 297 | transactionHotelManager: { 298 | connect: { 299 | id: transaction.transactionHotelManagerId, 300 | }, 301 | }, 302 | transactionHotelType: transaction.transactionHotelType, 303 | transactionPrice: transaction.transactionPrice, 304 | transactionAuthor: userId && { 305 | connect: { 306 | id: userId, 307 | }, 308 | }, 309 | transactionAuthorId: userId || 'non-member', 310 | transactionAuthorName: transaction.transactionAuthorName, 311 | transactionAuthorEmail: transaction.transactionAuthorEmail, 312 | transactionAuthorContactNumber: 313 | transaction.transactionAuthorContactNumber, 314 | transactionAuthorSpecial: transaction.transactionAuthorSpecial, 315 | transactionAuthorNote: transaction.transactionAuthorNote, 316 | transactionStartDate: transaction.transactionStartDate, 317 | transactionEndDate: transaction.transactionEndDate, 318 | transactionRange: transaction.transactionRange, 319 | transactionLocationLat: transaction.transactionLocationLat, 320 | transactionLocationLng: transaction.transactionLocationLng, 321 | transactionLocationFormattedAddress: 322 | transaction.transactionLocationFormattedAddress, 323 | transactionRoom: transaction.transactionRoom, 324 | transactionGuest: transaction.transactionGuest, 325 | transactionStatus: 'PENDING', 326 | transactionStripeId: transaction.transactionStripeId, 327 | transactionCoupon: coupon.couponName, 328 | transactionCouponType: coupon.couponType, 329 | transactionCouponValue: coupon.couponValue, 330 | }); 331 | const date = new Date(); 332 | date.getFullYear() + '-' + (date.getMonth() + 1) + '-' + date.getDate(); 333 | const dataMail = { 334 | authorEmail: transaction.transactionAuthorEmail, 335 | authorName: transaction.transactionAuthorName, 336 | firstName: transaction.transactionAuthorName, 337 | startDate: transaction.transactionStartDate, 338 | endDate: transaction.transactionEndDate, 339 | hotelName: transaction.transactionHotelName, 340 | couponName: coupon.couponName, 341 | room: transactionPayload.transactionRoom, 342 | guest: transactionPayload.transactionGuest, 343 | total_price: transactionPayload.transactionPrice, 344 | order_date: 345 | date.getFullYear() + 346 | '-' + 347 | (date.getMonth() + 1) + 348 | '-' + 349 | date.getDate(), 350 | order_id: transactionPayload.transactionSecretKey, 351 | }; 352 | this.mailService.mockMailjet(dataMail); 353 | const transactionNotiPayload = { 354 | TXID: transactionPayload.TXID, 355 | transactionPrice: transaction.transactionPrice, 356 | transactionHotelManagerId: transaction.transactionHotelManagerId, 357 | }; 358 | // Đáng lẽ nên refactor lại 1 loại form noti, truyền type là review, like, transaction 359 | this.pubSub.publish('realtimeNotiTransaction', transactionNotiPayload); 360 | const totalTransactions = 361 | (await this.prisma.client 362 | .user({ 363 | id: transaction.transactionHotelManagerId, 364 | }) 365 | .uncheckTransactions() 366 | .totalTransactions()) || 0; 367 | const totalPrice = 368 | (await this.prisma.client 369 | .user({ 370 | id: transaction.transactionHotelManagerId, 371 | }) 372 | .uncheckTransactions() 373 | .totalPrice()) || 0; 374 | console.log(totalTransactions); 375 | console.log(totalPrice); 376 | // upsert để tối ưu database storage 377 | await this.prisma.client.updateUser({ 378 | where: { 379 | id: transaction.transactionHotelManagerId, 380 | }, 381 | data: { 382 | uncheckTransactions: { 383 | upsert: { 384 | update: { 385 | totalTransactions: totalTransactions + 1, 386 | totalPrice: totalPrice + transaction.transactionPrice, 387 | }, 388 | create: { 389 | userUncheckTransactionsId: 390 | transaction.transactionHotelManagerId, 391 | totalTransactions: totalTransactions + 1, 392 | totalPrice: totalPrice + transaction.transactionPrice, 393 | }, 394 | }, 395 | }, 396 | }, 397 | }); 398 | return transactionPayload; 399 | } 400 | } 401 | @Mutation() 402 | @UseGuards(GqlAuthGuard) 403 | async updateTotalUnreadTransactions(@GqlUser() user: User) { 404 | return this.prisma.client.updateUser({ 405 | where: { 406 | id: user.id, 407 | }, 408 | data: { 409 | uncheckTransactions: { 410 | update: { 411 | totalPrice: 0, 412 | totalTransactions: 0, 413 | }, 414 | }, 415 | }, 416 | }); 417 | } 418 | @UseGuards(GqlAuthGuard) 419 | @Mutation() 420 | async processTransactions( 421 | @GqlUser() user: User, 422 | @Args('id') id, 423 | @Args('type') type, 424 | ) { 425 | console.log(id); 426 | // Sử dụng cho demo - set tất cả pending 427 | if (type === 3) { 428 | return await this.prisma.client.updateManyTransactions({ 429 | where: { 430 | transactionHotelManagerId: user.id, 431 | }, 432 | data: { 433 | transactionStatus: 'PENDING', 434 | }, 435 | }); 436 | } 437 | return await this.prisma.client.updateManyTransactions({ 438 | where: { 439 | transactionHotelManagerId: user.id, 440 | TXID_in: id, 441 | }, 442 | data: { 443 | transactionStatus: type === 1 ? 'DONE' : 'CANCELLED', 444 | }, 445 | }); 446 | } 447 | @UseGuards(GqlAuthGuard) 448 | @Mutation() 449 | async deleteCoupons(@GqlUser() user: User, @Args('id') id) { 450 | // Ko show id của coupon ra -> gurantee ko xóa bậy bạ 451 | // Delete many không có in 452 | console.log(id); 453 | return id.map(i => { 454 | return this.prisma.client.deleteCoupon({ 455 | couponId: i, 456 | }); 457 | }); 458 | } 459 | @Subscription(returns => TransactionNotification, { 460 | resolve: payloads => payloads, 461 | filter: (payloads, variables) => 462 | payloads.transactionHotelManagerId === variables.userId, 463 | }) 464 | realtimeNotificationTransaction() { 465 | return this.pubSub.asyncIterator('realtimeNotiTransaction'); 466 | } 467 | } 468 | -------------------------------------------------------------------------------- /src/user/user.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { UserResolver } from './user.resolver'; 3 | import { PrismaModule } from 'src/prisma/prisma.module'; 4 | import { MailService } from 'src/services/sendEmail'; 5 | import { RedisPubSub } from 'graphql-redis-subscriptions'; 6 | import * as Redis from 'ioredis'; 7 | @Module({ 8 | providers: [ 9 | UserResolver, 10 | MailService, 11 | { 12 | provide: 'PUB_SUB', 13 | useFactory: () => { 14 | const options = { 15 | host: 'redis', 16 | port: 6379, 17 | password: 'no', 18 | }; 19 | return new RedisPubSub({ 20 | publisher: new Redis(options), 21 | subscriber: new Redis(options), 22 | }); 23 | }, 24 | // useValue: new PubSub(), 25 | }, 26 | ], 27 | imports: [PrismaModule], 28 | }) 29 | export class UserModule {} 30 | -------------------------------------------------------------------------------- /src/user/user.resolver.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { UserResolver } from './user.resolver'; 3 | 4 | describe('UserResolver', () => { 5 | let resolver: UserResolver; 6 | 7 | beforeEach(async () => { 8 | const module: TestingModule = await Test.createTestingModule({ 9 | providers: [UserResolver], 10 | }).compile(); 11 | 12 | resolver = module.get(UserResolver); 13 | }); 14 | 15 | it('should be defined', () => { 16 | expect(resolver).toBeDefined(); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /src/user/user.resolver.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Resolver, 3 | ResolveField, 4 | Parent, 5 | Args, 6 | Query, 7 | Mutation, 8 | Subscription, 9 | } from '@nestjs/graphql'; 10 | import { PrismaService } from 'src/prisma/prisma.service'; 11 | import { v4 as uuidv4 } from 'uuid'; 12 | import { 13 | User, 14 | Reviews, 15 | UpdatePhotosInput, 16 | ReviewInput, 17 | SocialInput, 18 | UpdateProfileInput, 19 | DeletePhotosInput, 20 | LocationInput, 21 | ContactInput, 22 | Unread, 23 | Notification, 24 | CouponInput, 25 | } from 'src/graphql.schema.generated'; 26 | import { GqlUser, ResGql } from 'src/shared/decorators/decorator'; 27 | import { Inject, UseGuards } from '@nestjs/common'; 28 | import { Response } from 'express'; 29 | import { GqlAuthGuard } from 'src/auth/graphql-auth.guard'; 30 | import { MailService } from 'src/services/sendEmail'; 31 | import { ID_Input } from 'generated/prisma-client'; 32 | import { PubSubEngine } from 'graphql-subscriptions'; 33 | import _ = require('lodash'); 34 | import { isMongoId } from 'class-validator'; 35 | @Resolver('User') 36 | export class UserResolver { 37 | constructor( 38 | @Inject('PUB_SUB') private pubSub: PubSubEngine, 39 | private readonly prisma: PrismaService, 40 | private readonly mailService: MailService, 41 | ) {} 42 | @ResolveField() 43 | async agent_location(@Parent() { id }: User) { 44 | return this.prisma.client.user({ id }).agent_location(); 45 | } 46 | @ResolveField() 47 | async cover_pic(@Parent() { id }: User) { 48 | return this.prisma.client.user({ id }).cover_pic(); 49 | } 50 | @ResolveField() 51 | async profile_pic(@Parent() { id }: User) { 52 | return this.prisma.client.user({ id }).profile_pic(); 53 | } 54 | @ResolveField() 55 | async gallery(@Parent() { id }: User) { 56 | return this.prisma.client.user({ id }).gallery(); 57 | } 58 | @ResolveField() 59 | async notification(@Parent() { id }: User) { 60 | return this.prisma.client 61 | .user({ id }) 62 | .notification({ orderBy: 'createdAt_DESC' }); 63 | } 64 | 65 | // Tương tự populate trong express 66 | // Parent chính là data type của object gần nhất nest khi query 67 | // Không ai nest thì parent data type là chính nó 68 | // Trả về parent tương ứng của User.listed_posts 69 | // @UseGuards(GqlAuthGuard 70 | @ResolveField() 71 | async social_profile(@Parent() { id }: User) { 72 | return this.prisma.client.user({ id }).social_profile(); 73 | } 74 | // @ResolveField() 75 | // async listed_posts(@Parent() hotel: Hotel[], @Parent() user: User) { 76 | // // Chú ý các quan hệ N-1, 1-N 77 | // // listed_posts bi populated, gán listed_posts vào model nó kế thừa 78 | // // Mẹo cho các field là mảng - nested object - dùng fragment 79 | // // nên log ra data để biết đang loại data gì 80 | // // nên connect dựa vào id hay gì để có thể lợi dụng id đó query đúng 81 | // // Data trả về nhiều cấp nên log ra để biết của cái nào và check null 82 | // console.log(user); 83 | // const userId = (user && user.id) || (hotel[0] && hotel[0].agentId); 84 | // // console.log(user); 85 | // // first id fragment 86 | // // console.log('id từ cha' + userId); 87 | // const fragment = `fragment Somefrag on User 88 | // { 89 | // id 90 | // title 91 | // content 92 | // slug 93 | // price 94 | // status 95 | // isNegotiable 96 | // propertyType 97 | // condition 98 | // contactNumber 99 | // termsAndCondition 100 | // rating 101 | // ratingCount 102 | // amenities{ 103 | // id 104 | // guestRoom 105 | // bedRoom 106 | // wifiAvailability 107 | // parkingAvailability 108 | // poolAvailability 109 | // airCondition 110 | // extraBedFacility 111 | // } 112 | // image{ 113 | // id 114 | // url 115 | // } 116 | // location{ 117 | // id 118 | // lat 119 | // lng 120 | // formattedAddress 121 | // zipcode 122 | // city 123 | // state_long 124 | // state_short 125 | // country_long 126 | // country_short 127 | // } 128 | // gallery{ 129 | // id 130 | // url 131 | // } 132 | // categories{ 133 | // id 134 | // slug 135 | // name 136 | // image 137 | // { 138 | // id 139 | // url 140 | // } 141 | // } 142 | // createdAt 143 | // updatedAt 144 | // }`; 145 | // return await this.prisma.client 146 | // .user({ id: userId }) 147 | // .listed_posts() 148 | // .$fragment(fragment); 149 | // } 150 | // @ResolveField() 151 | // async favourite_post(@Parent() hotel: Hotel[], @Parent() user: User) { 152 | // const userId = (user && user.id) || (hotel[0] && hotel[0].agentId); 153 | // const fragment = `fragment fragFavourite on User 154 | // { 155 | // id 156 | // title 157 | // content 158 | // slug 159 | // price 160 | // status 161 | // isNegotiable 162 | // propertyType 163 | // condition 164 | // contactNumber 165 | // termsAndCondition 166 | // amenities{ 167 | // id 168 | // guestRoom 169 | // bedRoom 170 | // wifiAvailability 171 | // parkingAvailability 172 | // poolAvailability 173 | // airCondition 174 | // extraBedFacility 175 | // } 176 | // image{ 177 | // url 178 | // } 179 | // location{ 180 | // id 181 | // lat 182 | // lng 183 | // formattedAddress 184 | // zipcode 185 | // city 186 | // state_long 187 | // state_short 188 | // country_long 189 | // country_short 190 | // } 191 | // gallery{ 192 | // url 193 | // } 194 | // categories{ 195 | // id 196 | // slug 197 | // name 198 | // image 199 | // { 200 | // id 201 | // url 202 | // } 203 | // } 204 | // createdAt 205 | // updatedAt 206 | // }`; 207 | // return await this.prisma.client 208 | // .user({ id: userId }) 209 | // .favourite_post() 210 | // .$fragment(fragment); 211 | // } 212 | @ResolveField() 213 | async review_liked(@Parent() user: Reviews[]) { 214 | console.log(user); 215 | } 216 | @ResolveField() 217 | async review_disliked(@Parent() { id }: User) { 218 | console.log(id); 219 | } 220 | @Query() 221 | async userPosts(@Args('id') id: ID_Input) { 222 | const fragment = `fragment fragFavourite on User 223 | { 224 | listed_posts{ 225 | id 226 | title 227 | content 228 | slug 229 | price 230 | status 231 | isNegotiable 232 | propertyType 233 | condition 234 | contactNumber 235 | rating 236 | termsAndCondition 237 | amenities{ 238 | id 239 | guestRoom 240 | bedRoom 241 | wifiAvailability 242 | parkingAvailability 243 | poolAvailability 244 | airCondition 245 | extraBedFacility 246 | } 247 | image{ 248 | url 249 | } 250 | location{ 251 | id 252 | lat 253 | lng 254 | formattedAddress 255 | zipcode 256 | city 257 | state_long 258 | state_short 259 | country_long 260 | country_short 261 | } 262 | gallery{ 263 | url 264 | } 265 | categories{ 266 | id 267 | slug 268 | name 269 | image 270 | { 271 | id 272 | url 273 | } 274 | } 275 | createdAt 276 | updatedAt 277 | } 278 | favourite_post{ 279 | id 280 | title 281 | content 282 | slug 283 | price 284 | status 285 | isNegotiable 286 | propertyType 287 | condition 288 | contactNumber 289 | termsAndCondition 290 | amenities{ 291 | id 292 | guestRoom 293 | bedRoom 294 | wifiAvailability 295 | parkingAvailability 296 | poolAvailability 297 | airCondition 298 | extraBedFacility 299 | } 300 | image{ 301 | url 302 | } 303 | location{ 304 | id 305 | lat 306 | lng 307 | formattedAddress 308 | zipcode 309 | city 310 | state_long 311 | state_short 312 | country_long 313 | country_short 314 | } 315 | gallery{ 316 | url 317 | } 318 | categories{ 319 | id 320 | slug 321 | name 322 | image 323 | { 324 | id 325 | url 326 | } 327 | } 328 | createdAt 329 | updatedAt 330 | } 331 | }`; 332 | return this.prisma.client.user({ id }).$fragment(fragment); 333 | } 334 | @Query() 335 | async favouritePosts(@Args('id') id) { 336 | return this.prisma.client.user({ id }).favourite_post(); 337 | } 338 | @Query() 339 | async getUserGallery(@Args('id') id) { 340 | return this.prisma.client.user({ id }).gallery(); 341 | } 342 | @UseGuards(GqlAuthGuard) 343 | @Query() 344 | async favouritePostsHeart(@Args('id') id, @GqlUser() user: User) { 345 | console.log(user.id); 346 | return await this.prisma.client.user({ id: user.id }).favourite_post({ 347 | where: { 348 | id: id, 349 | }, 350 | }); 351 | } 352 | @Query() 353 | async getUserInfo(@Args('id') id) { 354 | return this.prisma.client.user({ id }); 355 | } 356 | @Query() 357 | async getUserReviews(@Args('id') id) { 358 | return this.prisma.client.user({ id }).reviews_maked(); 359 | } 360 | @Query() 361 | async getUserNotification(@Args('id') id) { 362 | return this.prisma.client 363 | .user({ id }) 364 | .notification({ orderBy: 'createdAt_DESC' }); 365 | } 366 | @Query() 367 | async getUserUnreadNotification(@Args('id') id) { 368 | const fragment = `fragment getUnreadNotificationNumber on User{ 369 | unreadNotification 370 | }`; 371 | return this.prisma.client.user({ id }).$fragment(fragment); 372 | } 373 | @Query() 374 | async getReviewsLikeDislike(@Args('id') id) { 375 | const fragment = `fragment likeAndDislike on User{ 376 | reviewID 377 | peopleLiked { 378 | id 379 | } 380 | peopleDisliked { 381 | id 382 | } 383 | }`; 384 | return this.prisma.client.reviews({ reviewID: id }).$fragment(fragment); 385 | } 386 | @Query() 387 | async getVendorStripeId(@Args('id') id) { 388 | const fragment = `fragment vendorStripeIdFrag on User{ 389 | stripeId 390 | }`; 391 | return await this.prisma.client.user({ id }).$fragment(fragment); 392 | } 393 | @Mutation() 394 | @UseGuards(GqlAuthGuard) 395 | async likeHotel(@GqlUser() user: User, @Args('id') id) { 396 | console.log(user.id + 'OK'); 397 | console.log(id); 398 | return await this.prisma.client.updateHotel({ 399 | where: { 400 | id, 401 | }, 402 | data: { 403 | peopleLiked: { 404 | connect: { id: user.id }, 405 | }, 406 | }, 407 | }); 408 | } 409 | @Mutation() 410 | @UseGuards(GqlAuthGuard) 411 | async dislikeHotel(@GqlUser() user: User, @Args('id') id) { 412 | console.log(user.id + 'dislike'); 413 | return this.prisma.client.updateHotel({ 414 | where: { 415 | id, 416 | }, 417 | data: { 418 | peopleLiked: { 419 | disconnect: { id: user.id }, 420 | }, 421 | }, 422 | }); 423 | } 424 | @Mutation() 425 | @UseGuards(GqlAuthGuard) 426 | async updateProfile( 427 | @GqlUser() user: User, 428 | @Args('profile') 429 | { 430 | first_name, 431 | last_name, 432 | date_of_birth, 433 | gender, 434 | email, 435 | content, 436 | cellNumber, 437 | }: UpdateProfileInput, 438 | @Args('location') 439 | locationInput: LocationInput, 440 | @Args('social') 441 | socialInput: SocialInput, 442 | ) { 443 | return await this.prisma.client.updateUser({ 444 | where: { 445 | id: user.id, 446 | }, 447 | data: { 448 | first_name, 449 | last_name, 450 | date_of_birth, 451 | gender, 452 | email, 453 | content, 454 | cellNumber, 455 | agent_location: { 456 | create: { 457 | ...locationInput, 458 | }, 459 | }, 460 | social_profile: { 461 | create: { 462 | ...socialInput, 463 | }, 464 | }, 465 | }, 466 | }); 467 | } 468 | @Mutation() 469 | @UseGuards(GqlAuthGuard) 470 | async updatePhotos( 471 | @GqlUser() user: User, 472 | @Args('photos') url: UpdatePhotosInput[], 473 | ) { 474 | return await this.prisma.client.updateUser({ 475 | where: { 476 | id: user.id, 477 | }, 478 | data: { 479 | gallery: { 480 | create: url, 481 | }, 482 | // cover_pic: { 483 | // create: photos.map(i => ({ 484 | // url: i.cover_pic.url, 485 | // })), 486 | // }, 487 | // profile_pic: { 488 | // create: photos.map(i => ({ 489 | // url: i.profile_pic.url, 490 | // })), 491 | // }, 492 | // profile_pic_main: profile_pic[0].url, 493 | // cover_pic: { 494 | // create: cover_pic.map(i => ({ 495 | // url: i.url, 496 | // })), 497 | // }, 498 | // profile_pic: { 499 | // create: profile_pic.map(i => ({ 500 | // url: i.url, 501 | // })), 502 | // }, 503 | }, 504 | }); 505 | } 506 | @Mutation() 507 | @UseGuards(GqlAuthGuard) 508 | async deletePhotos( 509 | @GqlUser() user: User, 510 | @Args('photos') id: DeletePhotosInput[], 511 | ) { 512 | console.log(id); 513 | id.map(async i => { 514 | return await this.prisma.client.updateUser({ 515 | where: { 516 | id: user.id, 517 | }, 518 | data: { 519 | gallery: { 520 | delete: { 521 | id: i.id, 522 | }, 523 | }, 524 | }, 525 | }); 526 | }); 527 | } 528 | @Mutation() 529 | @UseGuards(GqlAuthGuard) 530 | async setProfilePic(@GqlUser() user: User, @Args('url') url) { 531 | return this.prisma.client.updateUser({ 532 | where: { 533 | id: user.id, 534 | }, 535 | data: { 536 | profile_pic_main: url, 537 | }, 538 | }); 539 | } 540 | @Mutation() 541 | @UseGuards(GqlAuthGuard) 542 | async setCoverPic(@GqlUser() user: User, @Args('url') url) { 543 | return this.prisma.client.updateUser({ 544 | where: { 545 | id: user.id, 546 | }, 547 | data: { 548 | cover_pic_main: url, 549 | }, 550 | }); 551 | } 552 | @Mutation() 553 | @UseGuards(GqlAuthGuard) 554 | async sendContact( 555 | @GqlUser() user: User, 556 | @Args('contact') { message, cellNumber, subject }: ContactInput, 557 | ) { 558 | return this.mailService.sendContact( 559 | subject, 560 | user.email, 561 | message, 562 | cellNumber, 563 | ); 564 | } 565 | @Mutation() 566 | @UseGuards(GqlAuthGuard) 567 | async likeOrDislikeReview( 568 | @GqlUser() user: User, 569 | @Args('id') id, 570 | @Args('type') type, 571 | ) { 572 | const checkUserLiked = await this.prisma.client.$exists.reviews({ 573 | reviewID: id, 574 | peopleLiked_some: { 575 | id: user.id, 576 | }, 577 | }); 578 | const checkUserDisliked = await this.prisma.client.$exists.reviews({ 579 | reviewID: id, 580 | peopleDisliked_some: { 581 | id: user.id, 582 | }, 583 | }); 584 | const fragment = `fragment likeAndDislikeRealtime on Reviews{ 585 | reviewID 586 | peopleLiked{ 587 | id 588 | } 589 | peopleDisliked{ 590 | id 591 | } 592 | }`; 593 | // console.log('Check like'); 594 | // console.log(checkUserLiked); 595 | // console.log('Check dislike'); 596 | // console.log(checkUserDisliked); 597 | // Like lần đầu 598 | 599 | if (type === 1 && !checkUserDisliked) { 600 | await this.prisma.client.updateReviews({ 601 | where: { 602 | reviewID: id, 603 | }, 604 | data: { 605 | peopleLiked: { 606 | connect: { 607 | id: user.id, 608 | }, 609 | }, 610 | }, 611 | }); 612 | const likeAndDislikePayloads = await this.prisma.client 613 | .reviews({ 614 | reviewID: id, 615 | }) 616 | .$fragment(fragment); 617 | return this.pubSub.publish('realtimeLikeDislike', likeAndDislikePayloads); 618 | } 619 | // Dislike lần đầu 620 | if (type === 2 && !checkUserLiked) { 621 | await this.prisma.client.updateReviews({ 622 | where: { 623 | reviewID: id, 624 | }, 625 | data: { 626 | peopleDisliked: { 627 | connect: { 628 | id: user.id, 629 | }, 630 | }, 631 | }, 632 | }); 633 | const likeAndDislikePayloads = await this.prisma.client 634 | .reviews({ 635 | reviewID: id, 636 | }) 637 | .$fragment(fragment); 638 | return this.pubSub.publish('realtimeLikeDislike', likeAndDislikePayloads); 639 | } 640 | // Like và bỏ dislike 641 | if (type == 1 && checkUserDisliked) { 642 | await this.prisma.client.updateReviews({ 643 | where: { 644 | reviewID: id, 645 | }, 646 | data: { 647 | peopleLiked: { 648 | connect: { 649 | id: user.id, 650 | }, 651 | }, 652 | peopleDisliked: { 653 | disconnect: { 654 | id: user.id, 655 | }, 656 | }, 657 | }, 658 | }); 659 | const likeAndDislikePayloads = await this.prisma.client 660 | .reviews({ 661 | reviewID: id, 662 | }) 663 | .$fragment(fragment); 664 | return this.pubSub.publish('realtimeLikeDislike', likeAndDislikePayloads); 665 | } 666 | // Dislike và bỏ like 667 | if (type == 2 && checkUserLiked) { 668 | await this.prisma.client.updateReviews({ 669 | where: { 670 | reviewID: id, 671 | }, 672 | data: { 673 | peopleLiked: { 674 | disconnect: { 675 | id: user.id, 676 | }, 677 | }, 678 | peopleDisliked: { 679 | connect: { 680 | id: user.id, 681 | }, 682 | }, 683 | }, 684 | }); 685 | const likeAndDislikePayloads = await this.prisma.client 686 | .reviews({ 687 | reviewID: id, 688 | }) 689 | .$fragment(fragment); 690 | return this.pubSub.publish('realtimeLikeDislike', likeAndDislikePayloads); 691 | } 692 | } 693 | @Mutation() 694 | async forgetPassword(@Args('email') email: string, @ResGql() res: Response) { 695 | console.log('Email input is ', email); 696 | const user = await this.prisma.client.user({ email }); 697 | if (!user) { 698 | throw Error('Email not exists'); 699 | } 700 | const code = uuidv4(); 701 | res.cookie('reset-password', code, { 702 | domain: '.hotel-prisma.ml', 703 | httpOnly: false, 704 | sameSite: 'none', 705 | secure: true, 706 | }); 707 | return this.mailService.sendEmail(email, code); 708 | } 709 | @Mutation() 710 | async checkNotification(@Args('id') id) { 711 | await this.prisma.client.updateManyNotifications({ 712 | where: { 713 | userNotificationId: id, 714 | }, 715 | data: { 716 | old: true, 717 | }, 718 | }); 719 | return await this.prisma.client.updateUser({ 720 | where: { 721 | id, 722 | }, 723 | data: { 724 | unreadNotification: 0, 725 | }, 726 | }); 727 | } 728 | @Mutation() 729 | async readNotification(@Args('id') query) { 730 | return await this.prisma.client.updateManyNotifications({ 731 | where: { 732 | query, 733 | }, 734 | data: { 735 | read: true, 736 | }, 737 | }); 738 | } 739 | @Mutation() 740 | async deleteAllNotifications(@Args('id') id) { 741 | return await this.prisma.client.deleteManyNotifications({ 742 | userNotificationId: id, 743 | }); 744 | } 745 | @Mutation() 746 | @UseGuards(GqlAuthGuard) 747 | async createCoupon( 748 | @GqlUser() user: User, 749 | @Args('coupon') coupon: CouponInput, 750 | @Args('type') type: number, 751 | @Args('hotelsId') hotelsId: any[], 752 | ) { 753 | console.log(type); 754 | // Tạo cho tất cả hotel 755 | if (type === 1) { 756 | const hotels = await this.prisma.client.hotels({ 757 | where: { 758 | agentId: user.id, 759 | }, 760 | }); 761 | const coupons = await this.prisma.client.createCoupon({ 762 | couponName: coupon.couponName, 763 | couponDescription: coupon.couponDescription, 764 | couponQuantity: coupon.couponQuantity, 765 | couponAuthor: { 766 | connect: { 767 | id: user.id, 768 | }, 769 | }, 770 | couponAuthorId: user.id, 771 | couponType: coupon.couponType, 772 | couponValue: coupon.couponValue, 773 | couponStartDate: coupon.couponStartDate, 774 | couponEndDate: coupon.couponEndDate, 775 | }); 776 | return hotels.map(async i => { 777 | return await this.prisma.client.updateHotel({ 778 | where: { 779 | id: i.id, 780 | }, 781 | data: { 782 | couponsAvailable: { 783 | connect: { 784 | couponId: coupons.couponId, 785 | }, 786 | }, 787 | }, 788 | }); 789 | }); 790 | } 791 | // Tạo cho hotels có id trong hotelId 792 | if (type == 2) { 793 | let isValid = true; 794 | await Promise.all( 795 | hotelsId.map(async i => { 796 | if (!isMongoId(i)) { 797 | isValid = false; 798 | throw Error(`'${i}' is not a valid MongoID`); 799 | } 800 | const hotelExists = await this.prisma.client 801 | .user({ id: user.id }) 802 | .listed_posts({ where: { id: i } }); 803 | if (hotelExists && hotelExists.length === 0) { 804 | isValid = false; 805 | throw Error(`'${i}' is not one of your Hotels`); 806 | } 807 | }), 808 | ); 809 | if (isValid) { 810 | const coupons = await this.prisma.client.createCoupon({ 811 | couponName: coupon.couponName, 812 | couponDescription: coupon.couponDescription, 813 | couponQuantity: coupon.couponQuantity, 814 | couponAuthor: { 815 | connect: { 816 | id: user.id, 817 | }, 818 | }, 819 | couponAuthorId: user.id, 820 | couponType: coupon.couponType, 821 | couponValue: coupon.couponValue, 822 | couponStartDate: coupon.couponStartDate, 823 | couponEndDate: coupon.couponEndDate, 824 | }); 825 | 826 | return hotelsId.map(async i => { 827 | // console.log(i); 828 | // console.log(isValid); 829 | // if (!isValid) 830 | // throw Error(`ID '${i}' is not exists in your current Hotels`); 831 | return await this.prisma.client.updateHotel({ 832 | where: { 833 | id: i, 834 | }, 835 | data: { 836 | couponsAvailable: { 837 | connect: { 838 | couponId: coupons.couponId, 839 | }, 840 | }, 841 | }, 842 | }); 843 | }); 844 | } 845 | } 846 | } 847 | @Mutation() 848 | @UseGuards(GqlAuthGuard) 849 | async makeReviews( 850 | @GqlUser() user: User, 851 | @Args('hotelId') id: ID_Input, 852 | @Args('reviews') review: ReviewInput, 853 | ) { 854 | // console.log(id); 855 | console.log(user.id + ' Id nguoi reviewed'); 856 | const hotelManagerId = await this.prisma.client.hotel({ id }).agentId(); 857 | const reviewedHotelName = await this.prisma.client.hotel({ id }).title(); 858 | const slug = await this.prisma.client.hotel({ id }).slug(); 859 | const updateRatingCount = await this.prisma.client 860 | .reviewsesConnection({ 861 | where: { 862 | reviewedHotelId: id, 863 | }, 864 | }) 865 | .aggregate() 866 | .count(); 867 | console.log(updateRatingCount); 868 | const query = `post/${slug}/${id}#reviews`; 869 | // const unreadNotification = await this.prisma.client 870 | // .notificationsConnection({ 871 | // where: { 872 | // userNotificationId: user.id, 873 | // old: false, 874 | // }, 875 | // }) 876 | // .aggregate() 877 | // .count(); 878 | // console.log(unreadNotification); 879 | const peopleReviewedArr = await this.prisma.client 880 | .hotel({ id }) 881 | .peopleReviewed(); 882 | // console.log(JSON.stringify(peopleReviewedArr) + 'First'); 883 | const hotelMangerIndex = 884 | peopleReviewedArr.findIndex(id => hotelManagerId === id.id) === -1 885 | ? 1 886 | : 0; 887 | if (hotelMangerIndex === 1) { 888 | console.log('updated hotel manager'); 889 | await this.prisma.client.updateHotel({ 890 | where: { 891 | id, 892 | }, 893 | data: { 894 | peopleReviewed: { 895 | connect: { 896 | id: hotelManagerId, 897 | }, 898 | }, 899 | }, 900 | }); 901 | } 902 | const index = 903 | peopleReviewedArr.findIndex(id => user.id === id.id) === -1 ? 1 : 0; 904 | console.log(index); 905 | if (index === 1) { 906 | console.log('updated'); 907 | await this.prisma.client.updateHotel({ 908 | where: { 909 | id, 910 | }, 911 | data: { 912 | peopleReviewed: { 913 | connect: { 914 | id: user.id, 915 | }, 916 | }, 917 | }, 918 | }); 919 | } 920 | this.pubSub.publish('notificationBell', { 921 | notificationBell: { 922 | reviewAuthorName: user.first_name + ' ' + user.last_name, 923 | reviewedHotelName, 924 | reviewedAuthorId: user.id, 925 | reviewTitle: review.reviewTitle, 926 | reviewText: review.reviewText, 927 | reviewAuthorProfilePic: user.profile_pic_main, 928 | peopleReviewedQuantity: peopleReviewedArr.length, 929 | query, 930 | read: false, 931 | peopleReviewedArr, 932 | }, 933 | }); 934 | peopleReviewedArr && peopleReviewedArr.length > 0 935 | ? peopleReviewedArr.map(async i => { 936 | if (i.id !== user.id) { 937 | const unreadNotification = await this.prisma.client 938 | .notificationsConnection({ 939 | where: { 940 | userNotificationId: i.id, 941 | old: false, 942 | }, 943 | }) 944 | .aggregate() 945 | .count(); 946 | // console.log(i.id + unreadNotification); 947 | this.pubSub.publish('unreadNotification', { 948 | unreadNotification: { 949 | reviewedAuthorId: i.id, 950 | unreadNotification: unreadNotification + 1, 951 | }, 952 | }); 953 | console.log(unreadNotification, i.id); 954 | await this.prisma.client.updateUser({ 955 | where: { 956 | id: i.id, 957 | }, 958 | data: { 959 | notification: { 960 | create: { 961 | reviewAuthorName: user.first_name + ' ' + user.last_name, 962 | reviewedHotelName, 963 | reviewTitle: review.reviewTitle, 964 | reviewText: review.reviewText, 965 | userNotificationId: i.id, 966 | query, 967 | reviewAuthorProfilePic: user.profile_pic_main, 968 | peopleReviewedQuantity: peopleReviewedArr.length, 969 | }, 970 | }, 971 | unreadNotification: unreadNotification + 1, 972 | }, 973 | }); 974 | } 975 | }) 976 | : ''; 977 | await this.prisma.client.updateHotel({ 978 | where: { id }, 979 | data: { 980 | ratingCount: updateRatingCount + 1, 981 | }, 982 | }); 983 | // console.log(JSON.stringify(peopleReviewedArr) + 'Second'); 984 | const reviewSub = await this.prisma.client.createReviews({ 985 | reviewOverall: review.reviewOverall, 986 | reviewTitle: review.reviewTitle, 987 | reviewText: review.reviewText, 988 | sortOfTrip: review.sortOfTrip, 989 | reviewTips: review.reviewTips, 990 | reviewAuthorId: { 991 | connect: { 992 | id: user.id, 993 | }, 994 | }, 995 | reviewAuthorEmail: user.email, 996 | reviewAuthorFirstName: user.first_name, 997 | reviewAuthorLastName: user.last_name, 998 | reviewAuthorPic: user.profile_pic_main, 999 | reviewFields: { 1000 | create: review.reviewFieldInput, 1001 | }, 1002 | reviewPics: { 1003 | create: review.reviewPics, 1004 | }, 1005 | reviewedHotel: { 1006 | connect: { 1007 | id: id, 1008 | }, 1009 | }, 1010 | reviewedHotelId: id, 1011 | reviewOptional: { 1012 | create: review.reviewOptionals, 1013 | }, 1014 | }); 1015 | // console.log(reviewToSubscribe); 1016 | const fragment = `fragment someFrag on Reviews { 1017 | reviewID 1018 | reviewTitle 1019 | reviewText 1020 | sortOfTrip 1021 | reviewAuthorId { 1022 | id 1023 | } 1024 | reviewAuthorFirstName 1025 | reviewAuthorLastName 1026 | reviewAuthorEmail 1027 | reviewOverall 1028 | reviewAuthorPic 1029 | reviewedHotelId 1030 | reviewTips 1031 | reviewPics { 1032 | url 1033 | } 1034 | reviewDate 1035 | reviewOptional { 1036 | option 1037 | optionField 1038 | } 1039 | reviewFields { 1040 | rating 1041 | ratingFieldName 1042 | } 1043 | }`; 1044 | const reviewToSubscribe = await this.prisma.client 1045 | .reviews({ 1046 | reviewID: reviewSub.reviewID, 1047 | }) 1048 | .$fragment(fragment); 1049 | this.pubSub.publish('realtimeReviews', reviewToSubscribe); 1050 | // console.log(reviewToSubscribe); 1051 | return reviewSub; 1052 | } 1053 | @Mutation() 1054 | @UseGuards(GqlAuthGuard) 1055 | async updateStripeId( 1056 | @GqlUser() user: User, 1057 | @Args('stripeId') stripeId, 1058 | @Args('type') type, 1059 | ) { 1060 | if (user.role === 'Normal') { 1061 | return await this.prisma.client.updateUser({ 1062 | where: { 1063 | id: user.id, 1064 | }, 1065 | data: { 1066 | stripeId, 1067 | role: type || 'Normal', 1068 | }, 1069 | }); 1070 | } 1071 | return await this.prisma.client.updateUser({ 1072 | where: { 1073 | id: user.id, 1074 | }, 1075 | data: { 1076 | role: type, 1077 | }, 1078 | }); 1079 | } 1080 | @Subscription(returns => Unread, { 1081 | // So data và biến filter channel 1082 | // filter(payloads, variables) { 1083 | // console.log('Nguoi comment' + payloads.notification.reviewAuthorId); 1084 | // // return variables.channelId[1].userId === '5f2b5c8aa7b11b00078d09b1'; 1085 | // return _.forEach( variables.channelId, function(i){ 1086 | // return i.userId !==payloads.notification.reviewAuthorId 1087 | // }) 1088 | // // return variables.channelId.forEach(i => { 1089 | // // console.log(i.userId) 1090 | // // return i.userId === '5f2b5c8aa7b11b00078d09b1'; 1091 | // // }); 1092 | // console.log(_.find(payloads.notification.peopleReviewedArr, { 1093 | // id: variables.channelId, 1094 | // })) 1095 | // console.log(_.find(payloads.notification.peopleReviewedArr, { 1096 | // id: variables.channelId, 1097 | // }) !== undefined) 1098 | // console.log( _.find(payloads.notification.peopleReviewedArr, { 1099 | // id: variables.channelId, 1100 | // }) !== 'undefined' && 1101 | // variables.channelId !== payloads.notification.reviewAuthorId) 1102 | // return true; 1103 | // }, 1104 | filter: (payloads, variables) => 1105 | // Special 1106 | // _.find(payloads.unreadNotification.peopleReviewedArr, { 1107 | // id: variables.channelId, 1108 | // }) !== undefined && 1109 | // variables.channelId !== payloads.unreadNotification.reviewedAuthorId, 1110 | payloads.unreadNotification.reviewedAuthorId === variables.channelId, 1111 | // payloads.notification.peopleReviewedArr.map(id => { 1112 | // console.log(payloads.notification.peopleReviewedArr) 1113 | // console.log(variables.channelId + 'Id lang nghe'); 1114 | // console.log(payloads.notification.reviewAuthorId + ' id nguoi comment'); 1115 | // console.log( 1116 | // id.id === variables.channelId && 1117 | // id.id !== payloads.notification.reviewAuthorId + ' True false', 1118 | // ); 1119 | // console.log(id.id); 1120 | // const test = _.find(payloads.notification.peopleReviewedArr, {id:variables.channelId}); 1121 | // console.log(test); 1122 | // (id.id === variables.channelId && 1123 | // id.id !== payloads.notification.reviewAuthorId); 1124 | // }), 1125 | }) 1126 | // Hứng 1127 | unreadNotification() { 1128 | return this.pubSub.asyncIterator('unreadNotification'); 1129 | } 1130 | 1131 | @Subscription(returns => Notification, { 1132 | // resolve(this: PrismaService, payloads, variables, anotherSub: PubSubEngine) { 1133 | // console.log(payloads) 1134 | // console.log(variables.channelId) 1135 | // }, 1136 | // filter(payloads, variables) { 1137 | // // console.log(payloads); 1138 | // // console.log(variables); 1139 | // console.log( 1140 | // _.find(payloads.notificationBell.peopleReviewedArr, { 1141 | // id: variables.channelId, 1142 | // }) !== undefined && 1143 | // variables.channelId !== payloads.notificationBell.reviewAuthorId, 1144 | // ); 1145 | // return ( 1146 | // _.find(payloads.notificationBell.peopleReviewedArr, { 1147 | // id: variables.channelId, 1148 | // }) !== undefined && 1149 | // variables.channelId !== payloads.notificationBell.reviewAuthorId 1150 | // ); 1151 | // }, 1152 | filter: (payloads, variables) => 1153 | _.find(payloads.notificationBell.peopleReviewedArr, { 1154 | id: variables.channelId, 1155 | }) !== undefined && 1156 | variables.channelId !== payloads.notificationBell.reviewedAuthorId, 1157 | }) 1158 | notificationBell() { 1159 | return this.pubSub.asyncIterator('notificationBell'); 1160 | } 1161 | 1162 | @Subscription(returns => Reviews, { 1163 | resolve(this: PrismaService, payloads, variables) { 1164 | return payloads; 1165 | }, 1166 | filter: (payloads, variables) => 1167 | payloads.reviewedHotelId === variables.hotelId, 1168 | }) 1169 | realtimeReviews() { 1170 | return this.pubSub.asyncIterator('realtimeReviews'); 1171 | } 1172 | @Subscription(returns => Reviews, { 1173 | resolve: payloads => payloads, 1174 | filter: (payloads, variables) => payloads.reviewID === variables.reviewID, 1175 | }) 1176 | realtimeLikeDislike() { 1177 | return this.pubSub.asyncIterator('realtimeLikeDislike'); 1178 | } 1179 | } 1180 | -------------------------------------------------------------------------------- /src/utils/FilesAndMockUpload.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Controller, 3 | Post, 4 | UploadedFiles, 5 | UseInterceptors, 6 | Body, 7 | Res, 8 | Req, 9 | } from '@nestjs/common'; 10 | import { Response, Request } from 'express'; 11 | import { AwsService } from 'src/aws/aws.service'; 12 | import { FilesInterceptor } from '@nestjs/platform-express'; 13 | import * as fs from 'fs'; 14 | import * as jwt2 from 'jsonwebtoken'; 15 | 16 | import { StripeService } from './stripe'; 17 | 18 | @Controller('api') 19 | export class UploadController { 20 | constructor(private aws: AwsService, private stripeApi: StripeService) {} 21 | @Post('uploadFile') 22 | @UseInterceptors(FilesInterceptor('files[]')) 23 | async uploadFile(@UploadedFiles() files) { 24 | // console.log(files) 25 | return await this.aws.uploadFile(files); 26 | } 27 | @Post('mock-payment') 28 | async mockPayment(@Body() data, @Res() res: Response) { 29 | // console.log(amount.amount); 30 | this.stripeApi.handleClientCard(data, res); 31 | } 32 | @Post('mock-stripe') 33 | async mockStripe(@Body() plan, @Res() res: Response, @Req() req: Request) { 34 | this.stripeApi.createStripeAccount(plan, res, req); 35 | } 36 | @Post('access-mock-stripe') 37 | async accessMockStripe(@Body() account, @Res() res: Response) { 38 | this.stripeApi.accessStripeDashboardConnected(account, res); 39 | } 40 | @Post('mock') 41 | async mock(@Body() receivedMockData: string[]) { 42 | console.log(receivedMockData); 43 | fs.writeFile('mock.ts', JSON.stringify(receivedMockData, null, 4), err => { 44 | if (err) throw err; 45 | }); 46 | console.log('MockData exported'); 47 | // Có thể viết class lấy hàm như aws làm hoặc execute prisma seed 48 | } 49 | // @Post('mock-mail-jet') 50 | // async mockMailjet(@Body() content) { 51 | // const request = mailjet.post('send', { version: 'v3.1' }).request({ 52 | // Messages: [ 53 | // { 54 | // From: { 55 | // Email: '19520854@gm.uit.edu.vn', 56 | // Name: 'Palace TripFinder', 57 | // }, 58 | // To: [ 59 | // { 60 | // Email: 'passenger1@example.com', 61 | // Name: 'passenger 1', 62 | // }, 63 | // ], 64 | // TemplateID: 1645231, 65 | // TemplateLanguage: true, 66 | // Subject: 'Palace', 67 | // Variables: { 68 | // firstname: 'Default value', 69 | // startDate: '10-01-2001', 70 | // endDate: '20-01-2001', 71 | // hotelName: '', 72 | // guest: '1', 73 | // room: '1', 74 | // couponName: 'name', 75 | // total_price: 'Default value', 76 | // order_date: 'Default value', 77 | // order_id: 'Default value', 78 | // }, 79 | // }, 80 | // ], 81 | // }); 82 | // request 83 | // .then(result => { 84 | // console.log(result.body); 85 | // }) 86 | // .catch(err => { 87 | // console.log(err.statusCode); 88 | // }); 89 | // } 90 | } 91 | -------------------------------------------------------------------------------- /src/utils/seed.ts: -------------------------------------------------------------------------------- 1 | import { mockData, ClientPrisma } from './data'; 2 | import * as _ from 'lodash'; 3 | // constructor 4 | // const hotel = new Prisma({ 5 | // endpoint: 'http://localhost:4466/abcdef/devxyz', 6 | // secret: 'secreetttttt', 7 | // }) //custom endpoint 8 | 9 | // Có thể hoàn toàn ko cần làm file seed.ts 10 | // Call truyền đối số data vào hàm và gọi luôn 11 | // Nhưng chủ yếu để biết chạy chạy seed mongodb graphql bằng cli (prisma seed) 12 | 13 | const format = s => 14 | s 15 | .toLowerCase() 16 | .split(/\s|%20/) 17 | .filter(Boolean) 18 | .join('-'); 19 | const setup = async (dataMock: any[]) => { 20 | const hotel = new ClientPrisma(); 21 | dataMock.forEach(async i => { 22 | hotel 23 | .createHotel({ 24 | agentId: '5f9e75ed51915100072a4e1d', 25 | connectId: { 26 | connect: { 27 | email: 'duyminhpham1201@gmail.com', 28 | }, 29 | }, 30 | title: i.title, 31 | agentEmail: "phucpham1301@gmail.com", 32 | agentName: "Phuc Pham", 33 | slug: format(i.slug), 34 | content: i.content, 35 | price: parseInt(i.price), 36 | isNegotiable: i.isNegotiable, 37 | propertyType: i.propertyType, 38 | condition: i.condition, 39 | termsAndCondition: i.termsAndCondition, 40 | contactNumber: i.contactNumber, 41 | rating: i.rating, 42 | ratingCount: i.ratingCount, 43 | status: i.status, 44 | image: { 45 | create: _.omit(i.image, 'id'), 46 | }, 47 | location: { 48 | create: _.omit(i.location, 'id'), 49 | }, 50 | gallery: { 51 | create: i.gallery.map(v => ({ 52 | url: v.url, 53 | })), 54 | }, 55 | amenities: { 56 | create: { 57 | guestRoom: i.amenities[0].guestRoom, 58 | bedRoom: i.amenities[1].bedRoom, 59 | wifiAvailability: i.amenities[2].wifiAvailability, 60 | parkingAvailability: i.amenities[3].parkingAvailability, 61 | poolAvailability: i.amenities[4].poolAvailability, 62 | airCondition: i.amenities[5].airCondition, 63 | extraBedFacility: i.amenities[6].extraBedFacility, 64 | }, 65 | }, 66 | // categories: { 67 | // create: i.categories, 68 | // }, 69 | }) 70 | .catch(e => console.log(e)); 71 | }); 72 | }; 73 | setup(mockData); 74 | // data:[{ 75 | // connectId:"123id" 76 | // title: "abc" 77 | // name:"123abccc" 78 | // }, 79 | // { 80 | // connectId:"vcvcc" 81 | // title: "xyz" 82 | // name:"223" 83 | // }] 84 | -------------------------------------------------------------------------------- /src/utils/stripe.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, Res, Req } from '@nestjs/common'; 2 | import { Response, Request } from 'express'; 3 | import Stripe from 'stripe'; 4 | // import { stripe } from './data'; 5 | @Injectable() 6 | export class StripeService { 7 | // Stripe ko cung cấp injectable service 8 | // Có thể tạo DTO 9 | // Session flow url cho mỗi stripe 10 | // Sử dụng validation pipe 11 | // construct events với webhook 12 | 13 | private readonly stripe: Stripe = new Stripe(process.env.STRIPE_SECRET, { 14 | apiVersion: '2020-08-27', 15 | }); 16 | 17 | public async createStripeAccount( 18 | plan, 19 | @Res() res: Response, 20 | @Req() req: Request, 21 | ): Promise { 22 | const data = req.body; 23 | try { 24 | const account = await this.stripe.accounts.create({ 25 | type: 'express', 26 | country: 'US', 27 | email: plan.email, 28 | capabilities: { 29 | card_payments: { requested: true }, 30 | transfers: { requested: true }, 31 | }, 32 | 33 | business_type: 'individual', // trong test mode + Chặn payment bằng cách đổi qua country qua NZ 34 | individual: { // Allow all bằng cách tạo account có các field thủ công hoặc tạo xong và bỏ field SSN 35 | id_number: '000000000', 36 | ssn_last_4: '0000', 37 | }, 38 | // external_account: data.external_account, 39 | // tos_acceptance:{ 40 | // date: Math.round((new Date()).getTime()/1000), 41 | // ip: req.ip, 42 | 43 | // }, 44 | // business_profile: { 45 | // url: data.url, 46 | // mcc: '7623', 47 | // }, 48 | // company: { 49 | // name: data.name, 50 | // phone: data.phone, 51 | // tax_id: data.tax_id, 52 | // address: { 53 | // line1: data.line1, 54 | // line2: data.line2, 55 | // state: data.state, 56 | // postal_code: data.postal_code, 57 | // }, 58 | // }, 59 | }); 60 | const accountLink = await this.stripe.accountLinks.create({ 61 | account: account.id, 62 | refresh_url: 'https://vercel-v2.hotel-prisma.ml/error', // Khi link ko còn valid 63 | return_url: `https://vercel-v2.hotel-prisma.ml/processing?accountId=${account.id}&plan=${plan.type}`, // redirect //Dev mode thì đang http 64 | type: 'account_onboarding', 65 | }); 66 | res.status(200).json({ accountLink }); 67 | } catch (e) { 68 | res.status(400).json({ code: 'error', message: e.message }); 69 | } 70 | } 71 | public async accessStripeDashboardConnected(account, @Res() res: Response) { 72 | try { 73 | const link = await this.stripe.accounts.createLoginLink(account.id); 74 | res.status(200).json({ link }); 75 | } catch (e) { 76 | throw Error(e.message); 77 | } 78 | } 79 | public async handleClientCard(data, @Res() res: Response): Promise { 80 | console.log(data); 81 | try { 82 | const paymentIntents = await this.stripe.paymentIntents.create( 83 | { 84 | amount: data.amount, 85 | currency: 'usd', 86 | }, 87 | { 88 | stripeAccount: data.stripeId || '', 89 | }, 90 | ); 91 | res.status(200).json({ client_secret: paymentIntents.client_secret }); 92 | } catch (e) { 93 | res.status(500).json({ statusCode: 500, message: e.message }); 94 | } 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/utils/upload.input.ts: -------------------------------------------------------------------------------- 1 | import { InputType, Field } from "@nestjs/graphql"; 2 | import { Upload } from "./upload.scalar"; 3 | 4 | @InputType() 5 | export class UploadUserProfilePicInput { 6 | @Field() 7 | file : Upload 8 | } -------------------------------------------------------------------------------- /src/utils/upload.scalar.ts: -------------------------------------------------------------------------------- 1 | import { Scalar } from "@nestjs/graphql"; 2 | import { GraphQLUpload } from 'graphql-upload'; 3 | 4 | @Scalar('Upload') 5 | export class Upload { 6 | description = 'File upload scalar type' 7 | 8 | parseValue(value){ 9 | return GraphQLUpload.parseValue(value); 10 | } 11 | 12 | serialize(value){ 13 | return GraphQLUpload.serialize(value); 14 | } 15 | 16 | parseLiteral(ast){ 17 | return GraphQLUpload.parseLiteral(ast, ast.value); 18 | } 19 | } -------------------------------------------------------------------------------- /src/utils/utils.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { UploadController } from './FilesAndMockUpload'; 3 | import { AwsService } from 'src/aws/aws.service'; 4 | import { PrismaService } from 'src/prisma/prisma.service'; 5 | import { StripeService } from './stripe'; 6 | 7 | @Module({ 8 | controllers: [UploadController], 9 | providers: [AwsService, PrismaService, StripeService], 10 | exports:[StripeService], 11 | }) 12 | export class UtilsModule {} 13 | -------------------------------------------------------------------------------- /test/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { INestApplication } from '@nestjs/common'; 3 | import * as request from 'supertest'; 4 | import { AppModule } from './../src/app.module'; 5 | 6 | describe('AppController (e2e)', () => { 7 | let app: INestApplication; 8 | 9 | beforeEach(async () => { 10 | const moduleFixture: TestingModule = await Test.createTestingModule({ 11 | imports: [AppModule], 12 | }).compile(); 13 | 14 | app = moduleFixture.createNestApplication(); 15 | await app.init(); 16 | }); 17 | 18 | it('/ (GET)', () => { 19 | return request(app.getHttpServer()) 20 | .get('/') 21 | .expect(200) 22 | .expect('Hello World!'); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /test/jest-e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "moduleFileExtensions": ["js", "json", "ts"], 3 | "rootDir": ".", 4 | "testEnvironment": "node", 5 | "testRegex": ".e2e-spec.ts$", 6 | "transform": { 7 | "^.+\\.(t|j)s$": "ts-jest" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["node_modules", "test", "dist", "**/*spec.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "declaration": true, 5 | "removeComments": true, 6 | "emitDecoratorMetadata": true, 7 | "experimentalDecorators": true, 8 | "allowSyntheticDefaultImports": true, 9 | "target": "es2017", 10 | "sourceMap": true, 11 | "outDir": "./dist", 12 | "baseUrl": "./", 13 | "incremental": true 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /vercel.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 2, 3 | "name": "nestjs-now", 4 | "builds": [ 5 | { 6 | "src": "dist/src/main.js", 7 | "use": "@vercel/node" 8 | } 9 | ], 10 | "routes": [ 11 | { 12 | "src": "/(.*)", 13 | "dest": "dist/src/main.js" 14 | } 15 | ] 16 | } --------------------------------------------------------------------------------