├── .gitignore ├── .graphqlconfig.yml ├── .vscode └── launch.json ├── LICENSE ├── README.md ├── package-lock.json ├── package.json ├── prisma ├── datamodel.graphql ├── docker-compose.yml ├── prisma.yml └── seed.graphql ├── queries ├── booking.graphql └── queries.graphql ├── src ├── firebase.ts ├── generated │ ├── prisma.graphql │ └── prisma.ts ├── index.ts ├── resolvers │ ├── AuthPayload.ts │ ├── ExperiencesByCity.ts │ ├── Home.ts │ ├── Mutation │ │ ├── addPaymentMethod.ts │ │ ├── auth.ts │ │ └── book.ts │ ├── Query.ts │ ├── Subscription.ts │ ├── Viewer.ts │ └── index.ts ├── schema.graphql └── utils.ts ├── tsconfig.json └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (https://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # TypeScript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | # next.js build output 61 | .next 62 | -------------------------------------------------------------------------------- /.graphqlconfig.yml: -------------------------------------------------------------------------------- 1 | projects: 2 | app: 3 | schemaPath: src/schema.graphql 4 | includes: [ 5 | "schema.graphql", 6 | "prisma.graphql", 7 | "booking.graphql", 8 | "queries.graphql", 9 | ] 10 | extensions: 11 | endpoints: 12 | default: http://localhost:4000 13 | prisma: 14 | schemaPath: src/generated/prisma.graphql 15 | includes: [ 16 | "prisma.graphql", 17 | "seed.graphql", 18 | "datamodel.graphql", 19 | ] 20 | extensions: 21 | prisma: prisma/prisma.yml 22 | codegen: 23 | - generator: prisma-binding 24 | language: typescript 25 | output: 26 | binding: src/generated/prisma.ts 27 | -------------------------------------------------------------------------------- /.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": "Launch Program", 11 | "program": "${workspaceFolder}/src/index.ts", 12 | "preLaunchTask": "tsc: build - tsconfig.json", 13 | "sourceMaps": true, 14 | "outFiles": [ 15 | "${workspaceFolder}/dist/**/*.js" 16 | ] 17 | } 18 | ] 19 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Mark Petty 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # graphql-server-firebase-authentication-example 2 | 🔑 Adds firebase authentication to Apollo Server (or graphql-yoga). Firebase Authentication handles email & password, Google, Facebook, Twitter, Github signup and login 3 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "graphql-server-firebase-authentication-example", 3 | "version": "0.0.0", 4 | "scripts": { 5 | "dev-play": "npm-run-all --parallel start playground", 6 | "dev": "npm run start ", 7 | "start": "nodemon -e ts,graphql -x ts-node -r dotenv/config src/index.ts", 8 | "playground": "graphql playground", 9 | "build": "rm -rf dist && graphql codegen && tsc", 10 | "prisma": "prisma" 11 | }, 12 | "dependencies": { 13 | "apollo-server": "^2.0.0", 14 | "bcryptjs": "2.4.3", 15 | "firebase-admin": "^5.13.1", 16 | "graphql": "0.13.2", 17 | "graphql-import": "^0.6.0", 18 | "graphql-tag": "2.9.2", 19 | "graphql-tools": "3.0.5", 20 | "jsonwebtoken": "8.3.0", 21 | "prisma-binding": "2.1.1" 22 | }, 23 | "devDependencies": { 24 | "@types/bcryptjs": "2.4.1", 25 | "dotenv": "5.0.1", 26 | "graphql-cli": "2.16.5", 27 | "nodemon": "1.18.3", 28 | "npm-run-all": "4.1.3", 29 | "prisma": "1.12.0", 30 | "ts-node": "6.2.0", 31 | "typescript": "2.9.2" 32 | }, 33 | "prettier": { 34 | "semi": false, 35 | "trailingComma": "all", 36 | "singleQuote": true 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /prisma/datamodel.graphql: -------------------------------------------------------------------------------- 1 | type User { 2 | id: ID! @unique 3 | createdAt: DateTime! 4 | updatedAt: DateTime! 5 | firstName: String 6 | lastName: String 7 | password: String 8 | name: String 9 | email: String @unique 10 | emailVerified: Boolean 11 | phone: String 12 | responseRate: Float 13 | responseTime: Int 14 | isSuperHost: Boolean! @default(value: "false") 15 | ownedPlaces: [Place!]! 16 | location: Location 17 | bookings: [Booking!]! 18 | paymentAccount: [PaymentAccount!]! 19 | sentMessages: [Message!]! @relation(name: "SentMessages") 20 | receivedMessages: [Message!]! @relation(name: "ReceivedMessages") 21 | notifications: [Notification!]! 22 | profilePicture: Picture 23 | hostingExperiences: [Experience!]! 24 | } 25 | 26 | type Place { 27 | id: ID! @unique 28 | name: String 29 | size: PLACE_SIZES 30 | shortDescription: String! 31 | description: String! 32 | slug: String! 33 | maxGuests: Int! 34 | numBedrooms: Int! 35 | numBeds: Int! 36 | numBaths: Int! 37 | reviews: [Review!]! 38 | amenities: Amenities! 39 | host: User! 40 | pricing: Pricing! 41 | location: Location! 42 | views: Views! 43 | guestRequirements: GuestRequirements 44 | policies: Policies 45 | houseRules: HouseRules 46 | bookings: [Booking!]! 47 | pictures: [Picture!]! 48 | popularity: Int! 49 | } 50 | 51 | type Pricing { 52 | id: ID! @unique 53 | place: Place! 54 | monthlyDiscount: Int 55 | weeklyDiscount: Int 56 | perNight: Int! 57 | smartPricing: Boolean! @default(value: "false") 58 | basePrice: Int! 59 | averageWeekly: Int! 60 | averageMonthly: Int! 61 | cleaningFee: Int 62 | securityDeposit: Int 63 | extraGuests: Int 64 | weekendPricing: Int 65 | currency: CURRENCY 66 | } 67 | 68 | type GuestRequirements { 69 | id: ID! @unique 70 | govIssuedId: Boolean! @default(value: "false") 71 | recommendationsFromOtherHosts: Boolean! @default(value: "false") 72 | guestTripInformation: Boolean! @default(value: "false") 73 | place: Place! 74 | } 75 | 76 | type Policies { 77 | id: ID! @unique 78 | createdAt: DateTime! 79 | updatedAt: DateTime! 80 | checkInStartTime: Float! 81 | checkInEndTime: Float! 82 | checkoutTime: Float! 83 | place: Place! 84 | } 85 | 86 | type HouseRules { 87 | id: ID! @unique 88 | createdAt: DateTime! 89 | updatedAt: DateTime! 90 | suitableForChildren: Boolean 91 | suitableForInfants: Boolean 92 | petsAllowed: Boolean 93 | smokingAllowed: Boolean 94 | partiesAndEventsAllowed: Boolean 95 | additionalRules: String 96 | } 97 | 98 | type Views { 99 | id: ID! @unique 100 | lastWeek: Int! 101 | place: Place! 102 | } 103 | 104 | type Location { 105 | id: ID! @unique 106 | lat: Float! 107 | lng: Float! 108 | neighbourHood: Neighbourhood 109 | user: User 110 | place: Place 111 | address: String 112 | directions: String 113 | experience: Experience 114 | restaurant: Restaurant 115 | } 116 | 117 | type Neighbourhood { 118 | id: ID! @unique 119 | locations: [Location!]! 120 | name: String! 121 | slug: String! 122 | homePreview: Picture 123 | city: City! 124 | featured: Boolean! 125 | popularity: Int! 126 | } 127 | 128 | type City { 129 | id: ID! @unique 130 | name: String! 131 | neighbourhoods: [Neighbourhood!]! 132 | } 133 | 134 | type Picture { 135 | url: String! 136 | } 137 | 138 | type Experience { 139 | id: ID! @unique 140 | category: ExperienceCategory 141 | title: String! 142 | host: User! 143 | location: Location! 144 | pricePerPerson: Int! 145 | reviews: [Review!]! 146 | preview: Picture! 147 | popularity: Int! 148 | } 149 | 150 | type ExperienceCategory { 151 | id: ID! @unique 152 | mainColor: String! @default(value: "#123456") 153 | name: String! 154 | experience: Experience 155 | } 156 | 157 | type Amenities { 158 | id: ID! @unique 159 | place: Place! 160 | elevator: Boolean! @default(value: "false") 161 | petsAllowed: Boolean! @default(value: "false") 162 | internet: Boolean! @default(value: "false") 163 | kitchen: Boolean! @default(value: "false") 164 | wirelessInternet: Boolean! @default(value: "false") 165 | familyKidFriendly: Boolean! @default(value: "false") 166 | freeParkingOnPremises: Boolean! @default(value: "false") 167 | hotTub: Boolean! @default(value: "false") 168 | pool: Boolean! @default(value: "false") 169 | smokingAllowed: Boolean! @default(value: "false") 170 | wheelchairAccessible: Boolean! @default(value: "false") 171 | breakfast: Boolean! @default(value: "false") 172 | cableTv: Boolean! @default(value: "false") 173 | suitableForEvents: Boolean! @default(value: "false") 174 | dryer: Boolean! @default(value: "false") 175 | washer: Boolean! @default(value: "false") 176 | indoorFireplace: Boolean! @default(value: "false") 177 | tv: Boolean! @default(value: "false") 178 | heating: Boolean! @default(value: "false") 179 | hangers: Boolean! @default(value: "false") 180 | iron: Boolean! @default(value: "false") 181 | hairDryer: Boolean! @default(value: "false") 182 | doorman: Boolean! @default(value: "false") 183 | paidParkingOffPremises: Boolean! @default(value: "false") 184 | freeParkingOnStreet: Boolean! @default(value: "false") 185 | gym: Boolean! @default(value: "false") 186 | airConditioning: Boolean! @default(value: "false") 187 | shampoo: Boolean! @default(value: "false") 188 | essentials: Boolean! @default(value: "false") 189 | laptopFriendlyWorkspace: Boolean! @default(value: "false") 190 | privateEntrance: Boolean! @default(value: "false") 191 | buzzerWirelessIntercom: Boolean! @default(value: "false") 192 | babyBath: Boolean! @default(value: "false") 193 | babyMonitor: Boolean! @default(value: "false") 194 | babysitterRecommendations: Boolean! @default(value: "false") 195 | bathtub: Boolean! @default(value: "false") 196 | changingTable: Boolean! @default(value: "false") 197 | childrensBooksAndToys: Boolean! @default(value: "false") 198 | childrensDinnerware: Boolean! @default(value: "false") 199 | crib: Boolean! @default(value: "false") 200 | } 201 | 202 | type Review { 203 | id: ID! @unique 204 | createdAt: DateTime! 205 | text: String! 206 | stars: Int! 207 | accuracy: Int! 208 | location: Int! 209 | checkIn: Int! 210 | value: Int! 211 | cleanliness: Int! 212 | communication: Int! 213 | place: Place! 214 | experience: Experience 215 | } 216 | 217 | type Booking { 218 | id: ID! @unique 219 | createdAt: DateTime! 220 | bookee: User! 221 | place: Place! 222 | startDate: DateTime! 223 | endDate: DateTime! 224 | payment: Payment! 225 | } 226 | 227 | type Payment { 228 | id: ID! @unique 229 | createdAt: DateTime! 230 | serviceFee: Float! 231 | placePrice: Float! 232 | totalPrice: Float! 233 | booking: Booking! 234 | paymentMethod: PaymentAccount! 235 | } 236 | 237 | type PaymentAccount { 238 | id: ID! @unique 239 | createdAt: DateTime! 240 | type: PAYMENT_PROVIDER 241 | user: User! 242 | payments: [Payment!]! 243 | paypal: PaypalInformation 244 | creditcard: CreditCardInformation 245 | } 246 | 247 | type PaypalInformation { 248 | id: ID! @unique 249 | createdAt: DateTime! 250 | email: String! 251 | paymentAccount: PaymentAccount! 252 | } 253 | 254 | type CreditCardInformation { 255 | id: ID! @unique 256 | createdAt: DateTime! 257 | cardNumber: String! 258 | expiresOnMonth: Int! 259 | expiresOnYear: Int! 260 | securityCode: String! 261 | firstName: String! 262 | lastName: String! 263 | postalCode: String! 264 | country: String! 265 | paymentAccount: PaymentAccount 266 | } 267 | 268 | type Message { 269 | id: ID! @unique 270 | createdAt: DateTime! 271 | from: User! @relation(name: "SentMessages") 272 | to: User! @relation(name: "ReceivedMessages") 273 | deliveredAt: DateTime! 274 | readAt: DateTime! 275 | } 276 | 277 | type Notification { 278 | id: ID! @unique 279 | createdAt: DateTime! 280 | type: NOTIFICATION_TYPE 281 | user: User! 282 | link: String! 283 | readDate: DateTime! 284 | } 285 | 286 | type Restaurant { 287 | id: ID! @unique 288 | createdAt: DateTime! 289 | title: String! 290 | avgPricePerPerson: Int! 291 | pictures: [Picture!]! 292 | location: Location! 293 | isCurated: Boolean! @default(value: "true") 294 | slug: String! 295 | popularity: Int! 296 | } 297 | 298 | enum CURRENCY { 299 | CAD 300 | CHF 301 | EUR 302 | JPY 303 | USD 304 | ZAR 305 | } 306 | 307 | enum PLACE_SIZES { 308 | ENTIRE_HOUSE 309 | ENTIRE_APARTMENT 310 | ENTIRE_EARTH_HOUSE 311 | ENTIRE_CABIN 312 | ENTIRE_VILLA 313 | ENTIRE_PLACE 314 | ENTIRE_BOAT 315 | PRIVATE_ROOM 316 | } 317 | 318 | enum PAYMENT_PROVIDER { 319 | PAYPAL 320 | CREDIT_CARD 321 | } 322 | 323 | enum NOTIFICATION_TYPE { 324 | OFFER 325 | INSTANT_BOOK 326 | RESPONSIVENESS 327 | NEW_AMENITIES 328 | HOUSE_RULES 329 | } 330 | -------------------------------------------------------------------------------- /prisma/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | prisma: 4 | image: prismagraphql/prisma:1.8 5 | restart: always 6 | ports: 7 | - "4466:4466" 8 | environment: 9 | PRISMA_CONFIG: | 10 | port: 4466 11 | # uncomment the next line and provide the env var PRISMA_MANAGEMENT_API_SECRET=my-secret to activate cluster security 12 | managementApiSecret: mysecret123 13 | databases: 14 | default: 15 | connector: postgres 16 | host: postgres 17 | port: 5432 18 | user: prisma 19 | password: prisma 20 | migrations: true 21 | postgres: 22 | image: postgres 23 | restart: always 24 | environment: 25 | POSTGRES_USER: prisma 26 | POSTGRES_PASSWORD: prisma 27 | volumes: 28 | - postgres:/var/lib/postgresql/data 29 | volumes: 30 | postgres: 31 | -------------------------------------------------------------------------------- /prisma/prisma.yml: -------------------------------------------------------------------------------- 1 | endpoint: ${env:PRISMA_ENDPOINT} 2 | 3 | datamodel: datamodel.graphql 4 | 5 | # The secret is used to generate JTWs which allow to authenticate 6 | # against your Prisma service. You can use the `prisma token` command from the CLI 7 | # to generate a JWT based on the secret. When using the `prisma-binding` package, 8 | # you don't need to generate the JWTs manually as the library is doing that for you 9 | # (this is why you're passing it to the `Prisma` constructor). 10 | # Here, the secret is loaded as an environment variable from .env. 11 | secret: ${env:PRISMA_SECRET} 12 | 13 | # Defines how to seed data to the database upon the initial deploy. 14 | seed: 15 | import: seed.graphql 16 | 17 | hooks: 18 | post-deploy: 19 | - graphql get-schema -p prisma 20 | - graphql codegen -------------------------------------------------------------------------------- /prisma/seed.graphql: -------------------------------------------------------------------------------- 1 | mutation { 2 | 3 | experience: createExperience( 4 | data: { 5 | popularity: 3 6 | pricePerPerson: 33 7 | title: "Raise a glass to Prohibition" 8 | host: { 9 | create: { 10 | email: "test2@test.com" 11 | firstName: "Kitty" 12 | lastName: "Miller" 13 | isSuperHost: true 14 | phone: "+1123455667" 15 | password: "secret" 16 | location: { 17 | create: { 18 | address: "2 Salmon Way, Moss Landing, Monterey County, CA, USA" 19 | directions: "Follow the street to the end, then right" 20 | lat: 36.805235 21 | lng: 121.7892066 22 | neighbourHood: { 23 | create: { 24 | name: "Monterey Countey" 25 | slug: "monterey-countey" 26 | featured: true 27 | popularity: 1 28 | city: { create: { name: "Moss Landing" } } 29 | } 30 | } 31 | } 32 | } 33 | } 34 | } 35 | preview: { 36 | create: { 37 | url: "https://a0.muscache.com/im/pictures/cb14d34d-65cf-401d-ba7f-585ec37b43ef.jpg" 38 | } 39 | } 40 | location: { 41 | create: { 42 | address: "2 Salmon Way, Moss Landing, Monterey County, CA, USA" 43 | directions: "Follow the street to the end, then right" 44 | lat: 36.805235 45 | lng: 121.7892066 46 | neighbourHood: { 47 | create: { 48 | name: "Monterey Countey" 49 | slug: "monterey-countey" 50 | featured: true 51 | popularity: 1 52 | city: { create: { name: "Moss Landing" } } 53 | } 54 | } 55 | } 56 | } 57 | } 58 | ) { 59 | id 60 | } 61 | 62 | restaurant: createRestaurant( 63 | data: { 64 | title: "Chumley's" 65 | pictures: { 66 | create: { 67 | url: "https://a0.muscache.com/pictures/a9a1d433-bcde-4601-88a0-5f16871b8548.jpg" 68 | } 69 | } 70 | slug: "chumleys" 71 | popularity: 1 72 | avgPricePerPerson: 30 73 | location: { 74 | create: { 75 | address: "2 Salmon Way, Moss Landing, Monterey County, CA, USA" 76 | directions: "Follow the street to the end, then right" 77 | lat: 36.805235 78 | lng: 121.7892066 79 | neighbourHood: { 80 | create: { 81 | name: "Monterey Countey" 82 | slug: "monterey-countey" 83 | featured: true 84 | popularity: 1 85 | city: { create: { name: "Moss Landing" } } 86 | } 87 | } 88 | } 89 | } 90 | } 91 | ) { 92 | id 93 | } 94 | 95 | firstPlace: createPlace( 96 | data: { 97 | name: "Mushroom Dome Cabin: #1 on airbnb in the world" 98 | shortDescription: "With a geodesic dome loft & a large deck in the trees, you'll feel like you're in a tree house in the woods. We are in a quiet yet convenient location. Shaded by Oak and Madrone trees and next to a Redwood grove, you can enjoy the outdoors from the deck. In the summer, it is cool and in the winter you might get to hear the creek running below." 99 | description: "The space\\n\\nWe have 10 acres next to land without fences so you will get to enjoy nature: just hang out on the deck, take a hike in the woods, watch the hummingbirds, pet the goats, go to the beach or gaze at the stars - as long as the moon isn't full. ; ) During the summer, if there isn't any nightly fog, we can see the Milky Way here.\\n\\nTo check our availability, click on the \"Request to Book\" link." 100 | maxGuests: 3 101 | pictures: { 102 | create: { 103 | url: "https://a0.muscache.com/im/pictures/140333/3ab8f121_original.jpg?aki_policy=xx_large" 104 | } 105 | } 106 | numBedrooms: 1 107 | numBeds: 3 108 | numBaths: 1 109 | size: ENTIRE_CABIN 110 | slug: "mushroom-dome" 111 | popularity: 1 112 | host: { 113 | create: { 114 | email: "test@test.com" 115 | firstName: "John" 116 | lastName: "Doe" 117 | isSuperHost: true 118 | phone: "+1123455667" 119 | password: "secret" 120 | location: { 121 | create: { 122 | address: "2 Salmon Way, Moss Landing, Monterey County, CA, USA" 123 | directions: "Follow the street to the end, then right" 124 | lat: 36.805235 125 | lng: 121.7892066 126 | neighbourHood: { 127 | create: { 128 | name: "Monterey Countey" 129 | slug: "monterey-countey" 130 | featured: true 131 | popularity: 1 132 | city: { create: { name: "Moss Landing" } } 133 | } 134 | } 135 | } 136 | } 137 | } 138 | } 139 | amenities: { create: { airConditioning: false, essentials: true } } 140 | views: { create: { lastWeek: 0 } } 141 | guestRequirements: { 142 | create: { 143 | recommendationsFromOtherHosts: false 144 | guestTripInformation: false 145 | } 146 | } 147 | policies: { 148 | create: { 149 | checkInStartTime: 11.00 150 | checkInEndTime: 20.00 151 | checkoutTime: 10.00 152 | } 153 | } 154 | pricing: { 155 | create: { 156 | averageMonthly: 1000 157 | averageWeekly: 300 158 | basePrice: 100 159 | cleaningFee: 30 160 | extraGuests: 80 161 | perNight: 100 162 | securityDeposit: 500 163 | weeklyDiscount: 50 164 | smartPricing: false 165 | currency: USD 166 | } 167 | } 168 | houseRules: { 169 | create: { 170 | smokingAllowed: false 171 | petsAllowed: true 172 | suitableForInfants: false 173 | } 174 | } 175 | location: { 176 | create: { 177 | address: "2 Salmon Way, Moss Landing, Monterey County, CA, USA" 178 | directions: "Follow the street to the end, then right" 179 | lat: 36.805235 180 | lng: 121.7892066 181 | neighbourHood: { 182 | create: { 183 | name: "Monterey Countey" 184 | slug: "monterey-countey" 185 | featured: true 186 | popularity: 1 187 | city: { create: { name: "Moss Landing" } } 188 | } 189 | } 190 | } 191 | } 192 | } 193 | ) { 194 | id 195 | } 196 | 197 | secondPlace: createPlace( 198 | data: { 199 | name: "Apartment 1 of 4 with green terrace in Roma Norte" 200 | shortDescription: "We offer other options with incredible terraces: Wonderful little loft with enjoyable terrace. Green and relaxing in the heart of the bustling city." 201 | description: "We offer other options with incredible terraces: Wonderful little loft with enjoyable terrace. Green and relaxing in the heart of the bustling city." 202 | maxGuests: 3 203 | pictures: { 204 | create: { 205 | url: "https://a0.muscache.com/im/pictures/45880516/93bb5931_original.jpg?aki_policy=xx_large" 206 | } 207 | } 208 | numBedrooms: 1 209 | numBeds: 3 210 | numBaths: 1 211 | size: ENTIRE_CABIN 212 | slug: "mushroom-dome" 213 | popularity: 1 214 | host: { 215 | create: { 216 | email: "test14@test.com" 217 | firstName: "Jason" 218 | lastName: "Padmakumara" 219 | isSuperHost: true 220 | phone: "+1123455667" 221 | password: "secret" 222 | location: { 223 | create: { 224 | address: "2 Salmon Way, Moss Landing, Monterey County, CA, USA" 225 | directions: "Follow the street to the end, then right" 226 | lat: 36.805235 227 | lng: 121.7892066 228 | neighbourHood: { 229 | create: { 230 | name: "Monterey Countey" 231 | slug: "monterey-countey" 232 | featured: true 233 | popularity: 1 234 | city: { create: { name: "Moss Landing" } } 235 | } 236 | } 237 | } 238 | } 239 | } 240 | } 241 | amenities: { create: { airConditioning: false, essentials: true } } 242 | views: { create: { lastWeek: 0 } } 243 | guestRequirements: { 244 | create: { 245 | recommendationsFromOtherHosts: false 246 | guestTripInformation: false 247 | } 248 | } 249 | policies: { 250 | create: { 251 | checkInStartTime: 11.00 252 | checkInEndTime: 20.00 253 | checkoutTime: 10.00 254 | } 255 | } 256 | pricing: { 257 | create: { 258 | averageMonthly: 1000 259 | averageWeekly: 300 260 | basePrice: 100 261 | cleaningFee: 30 262 | extraGuests: 80 263 | perNight: 110 264 | securityDeposit: 500 265 | weeklyDiscount: 50 266 | smartPricing: false 267 | currency: USD 268 | } 269 | } 270 | houseRules: { 271 | create: { 272 | smokingAllowed: false 273 | petsAllowed: true 274 | suitableForInfants: false 275 | } 276 | } 277 | location: { 278 | create: { 279 | address: "Narvarte Oriente, Mexico City, CDMX, Mexico" 280 | directions: "Follow the street to the end, then right" 281 | lat: 19.398095 282 | lng: -99.149452 283 | neighbourHood: { 284 | create: { 285 | name: "Mexico City" 286 | slug: "mexico-city" 287 | featured: true 288 | popularity: 1 289 | city: { create: { name: "Mexico City" } } 290 | } 291 | } 292 | } 293 | } 294 | } 295 | ) { 296 | id 297 | } 298 | 299 | thirdPlace: createPlace( 300 | data: { 301 | name: "Urban Farmhouse at Curtis Park" 302 | shortDescription: "The Urban Farmhouse circa 1886 - meticulously converted in 2013. Situated adjacent to community garden. The updates afford you all the modern convenience you could ask for and charm you can only get from a building built in 1886. A true Charmer." 303 | description: "The Urban Farmhouse circa 1886 - meticulously converted in 2013. Situated adjacent to community garden. The updates afford you all the modern convenience you could ask for and charm you can only get from a building built in 1886. A true Charmer." 304 | maxGuests: 3 305 | pictures: { 306 | create: { 307 | url: "https://a0.muscache.com/im/pictures/ff6b760d-8782-4ccb-9e03-50aa720e3783.jpg?aki_policy=xx_large" 308 | } 309 | } 310 | numBedrooms: 1 311 | numBeds: 3 312 | numBaths: 1 313 | size: ENTIRE_CABIN 314 | slug: "mushroom-dome" 315 | popularity: 1 316 | host: { 317 | create: { 318 | email: "test12@test.com" 319 | # firstName: "Hans" 320 | # lastName: "Johanson" 321 | name: "Hans Johanson" 322 | isSuperHost: true 323 | phone: "+1123455667" 324 | # password: "secret" 325 | location: { 326 | create: { 327 | address: "2 Salmon Way, Moss Landing, Monterey County, CA, USA" 328 | directions: "Follow the street to the end, then right" 329 | lat: 36.805235 330 | lng: 121.7892066 331 | neighbourHood: { 332 | create: { 333 | name: "Monterey Countey" 334 | slug: "monterey-countey" 335 | featured: true 336 | popularity: 1 337 | city: { create: { name: "Moss Landing" } } 338 | } 339 | } 340 | } 341 | } 342 | } 343 | } 344 | amenities: { create: { airConditioning: false, essentials: true } } 345 | views: { create: { lastWeek: 0 } } 346 | guestRequirements: { 347 | create: { 348 | recommendationsFromOtherHosts: false 349 | guestTripInformation: false 350 | } 351 | } 352 | policies: { 353 | create: { 354 | checkInStartTime: 11.00 355 | checkInEndTime: 20.00 356 | checkoutTime: 10.00 357 | } 358 | } 359 | pricing: { 360 | create: { 361 | averageMonthly: 1000 362 | averageWeekly: 300 363 | basePrice: 100 364 | cleaningFee: 30 365 | extraGuests: 80 366 | perNight: 87 367 | securityDeposit: 500 368 | weeklyDiscount: 50 369 | smartPricing: false 370 | currency: USD 371 | } 372 | } 373 | houseRules: { 374 | create: { 375 | smokingAllowed: false 376 | petsAllowed: true 377 | suitableForInfants: false 378 | } 379 | } 380 | location: { 381 | create: { 382 | address: "W Crestline Ave, Littleton, CO 80120, USA" 383 | directions: "Follow the street to the end, then right" 384 | lat: 39.619115 385 | lng: -105.016560 386 | neighbourHood: { 387 | create: { 388 | name: "Denver" 389 | slug: "denver" 390 | featured: true 391 | popularity: 1 392 | city: { create: { name: "Denver" } } 393 | } 394 | } 395 | } 396 | } 397 | } 398 | ) { 399 | id 400 | } 401 | 402 | fourthPlace: createPlace( 403 | data: { 404 | name: "Underground Hygge" 405 | shortDescription: "This inspired dwelling nestled right into the breathtaking Columbia River Gorge mountainside. Reverently framed by the iconic round doorway, the wondrous views will entrance your imagination and inspire an unforgettable journey. Every nook of this little habitation will warm your sole, every cranny will charm your expedition of repose. Up the pathway, tucked into the earth, an unbelievable adventure awaits!" 406 | description: "This inspired dwelling nestled right into the breathtaking Columbia River Gorge mountainside. Reverently framed by the iconic round doorway, the wondrous views will entrance your imagination and inspire an unforgettable journey. Every nook of this little habitation will warm your sole, every cranny will charm your expedition of repose. Up the pathway, tucked into the earth, an unbelievable adventure awaits!" 407 | maxGuests: 3 408 | pictures: { 409 | create: { 410 | url: "https://a0.muscache.com/im/pictures/56bff280-aba3-42f3-af42-adc2814a72f4.jpg?aki_policy=xx_large" 411 | } 412 | } 413 | numBedrooms: 1 414 | numBeds: 3 415 | numBaths: 1 416 | size: ENTIRE_CABIN 417 | slug: "mushroom-dome" 418 | popularity: 1 419 | host: { 420 | create: { 421 | email: "test13@test.com" 422 | # firstName: "Leah" 423 | # lastName: "Dyer" 424 | name: "Leah Dyer" 425 | isSuperHost: true 426 | phone: "+1123455667" 427 | # password: "secret" 428 | location: { 429 | create: { 430 | address: "2 Salmon Way, Moss Landing, Monterey County, CA, USA" 431 | directions: "Follow the street to the end, then right" 432 | lat: 36.805235 433 | lng: 121.7892066 434 | neighbourHood: { 435 | create: { 436 | name: "Monterey Countey" 437 | slug: "monterey-countey" 438 | featured: true 439 | popularity: 1 440 | city: { create: { name: "Moss Landing" } } 441 | } 442 | } 443 | } 444 | } 445 | } 446 | } 447 | amenities: { create: { airConditioning: false, essentials: true } } 448 | views: { create: { lastWeek: 0 } } 449 | guestRequirements: { 450 | create: { 451 | recommendationsFromOtherHosts: false 452 | guestTripInformation: false 453 | } 454 | } 455 | policies: { 456 | create: { 457 | checkInStartTime: 11.00 458 | checkInEndTime: 20.00 459 | checkoutTime: 10.00 460 | } 461 | } 462 | pricing: { 463 | create: { 464 | averageMonthly: 1000 465 | averageWeekly: 300 466 | basePrice: 100 467 | cleaningFee: 30 468 | extraGuests: 80 469 | perNight: 69 470 | securityDeposit: 500 471 | weeklyDiscount: 50 472 | smartPricing: false 473 | currency: USD 474 | } 475 | } 476 | houseRules: { 477 | create: { 478 | smokingAllowed: false 479 | petsAllowed: true 480 | suitableForInfants: false 481 | } 482 | } 483 | location: { 484 | create: { 485 | address: "2600-2712 Entiat Way, Entiat, WA 98822, USA" 486 | directions: "Follow the street to the end, then right" 487 | lat: 47.631190 488 | lng: -120.220822 489 | neighbourHood: { 490 | create: { 491 | name: "Orondo" 492 | slug: "orondo" 493 | featured: true 494 | popularity: 1 495 | city: { create: { name: "Orondo" } } 496 | } 497 | } 498 | } 499 | } 500 | } 501 | ) { 502 | id 503 | } 504 | 505 | fifthPlace: createPlace( 506 | data: { 507 | name: "Romantic, Cozy Cottage Next to Downtown" 508 | shortDescription: "Comfy, cozy and romantic cottage less than 3 miles from Downtown. The Cleveland Cottage provides a private oasis within city limits that includes full kitchen, Wifi, parking, electric fireplace & entrance through a private courtyard with fire pit." 509 | description: "Comfy, cozy and romantic cottage less than 3 miles from Downtown. The Cleveland Cottage provides a private oasis within city limits that includes full kitchen, Wifi, parking, electric fireplace & entrance through a private courtyard with fire pit." 510 | maxGuests: 3 511 | pictures: { 512 | create: { 513 | url: "https://a0.muscache.com/im/pictures/100298057/ccd8c843_original.jpg?aki_policy=xx_large" 514 | } 515 | } 516 | numBedrooms: 1 517 | numBeds: 3 518 | numBaths: 1 519 | size: ENTIRE_CABIN 520 | slug: "mushroom-dome" 521 | popularity: 1 522 | host: { 523 | create: { 524 | email: "test15@test.com" 525 | # firstName: "Kris" 526 | # lastName: "Mao" 527 | name: "Kris Mao" 528 | isSuperHost: true 529 | phone: "+1123455667" 530 | 531 | location: { 532 | create: { 533 | address: "2 Salmon Way, Moss Landing, Monterey County, CA, USA" 534 | directions: "Follow the street to the end, then right" 535 | lat: 36.805235 536 | lng: 121.7892066 537 | neighbourHood: { 538 | create: { 539 | name: "Monterey Countey" 540 | slug: "monterey-countey" 541 | featured: true 542 | popularity: 1 543 | city: { create: { name: "Moss Landing" } } 544 | } 545 | } 546 | } 547 | } 548 | } 549 | } 550 | amenities: { create: { airConditioning: false, essentials: true } } 551 | views: { create: { lastWeek: 0 } } 552 | guestRequirements: { 553 | create: { 554 | recommendationsFromOtherHosts: false 555 | guestTripInformation: false 556 | } 557 | } 558 | policies: { 559 | create: { 560 | checkInStartTime: 11.00 561 | checkInEndTime: 20.00 562 | checkoutTime: 10.00 563 | } 564 | } 565 | pricing: { 566 | create: { 567 | averageMonthly: 1000 568 | averageWeekly: 300 569 | basePrice: 100 570 | cleaningFee: 30 571 | extraGuests: 80 572 | perNight: 110 573 | securityDeposit: 500 574 | weeklyDiscount: 50 575 | smartPricing: false 576 | currency: USD 577 | } 578 | } 579 | houseRules: { 580 | create: { 581 | smokingAllowed: false 582 | petsAllowed: true 583 | suitableForInfants: false 584 | } 585 | } 586 | location: { 587 | create: { 588 | address: "Greenwood, Nashville, TN 37206, USA" 589 | directions: "Follow the street to the end, then right" 590 | lat: 36.187977 591 | lng: -86.751635 592 | neighbourHood: { 593 | create: { 594 | name: "Nashville" 595 | slug: "nashville" 596 | featured: true 597 | popularity: 1 598 | city: { create: { name: "Nashville" } } 599 | } 600 | } 601 | } 602 | } 603 | } 604 | ) { 605 | id 606 | } 607 | 608 | } 609 | -------------------------------------------------------------------------------- /queries/booking.graphql: -------------------------------------------------------------------------------- 1 | mutation signup { 2 | signup( 3 | email: "a25@a.de" 4 | firstName: "Tom" 5 | lastName: "Hardy" 6 | password: "pw" 7 | phone: "+1132123123" 8 | ) { 9 | user { 10 | id 11 | } 12 | token 13 | } 14 | } 15 | 16 | mutation signin { 17 | login(email: "a25@a.de", password: "pw") { 18 | user { 19 | id 20 | } 21 | token 22 | } 23 | } 24 | 25 | mutation addPaymentMethod { 26 | addPaymentMethod( 27 | cardNumber: "4242424242424242" 28 | expiresOnMonth: 12 29 | expiresOnYear: 2020 30 | securityCode: "232" 31 | firstName: "Bob" 32 | lastName: "der Meister" 33 | postalCode: "12345" 34 | country: "Germany" 35 | ) { 36 | success 37 | } 38 | } 39 | 40 | mutation book { 41 | book( 42 | placeId: "" 43 | checkIn: "2017-11-19T11:57:44.828Z" 44 | checkOut: "2017-11-20T11:57:44.828Z" 45 | numGuests: 2 46 | ) { 47 | success 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /queries/queries.graphql: -------------------------------------------------------------------------------- 1 | ## possible queries 2 | { 3 | experiences { 4 | ...ExperienceFragment 5 | } 6 | 7 | topHomes { 8 | id 9 | size 10 | numBeds 11 | slug 12 | pricing { 13 | perNight 14 | } 15 | host { 16 | isSuperHost 17 | } 18 | images(first: 1) { 19 | url(width: 180 height: 270) 20 | } 21 | numReviews 22 | avgRating 23 | } 24 | 25 | topReservations { 26 | id 27 | slug 28 | title 29 | avgPricePerPerson 30 | pictures(first: 1) { 31 | url(width: 340 height: 227) 32 | } 33 | title 34 | } 35 | 36 | featuredDestinations { 37 | name 38 | homePreview { 39 | url(width: 180 height: 270) 40 | } 41 | slug 42 | } 43 | 44 | cityExperiences: experiencesByCity(cities: [ 45 | "New York", 46 | "Barcelona", 47 | "Paris", 48 | "Tokyo", 49 | "Los Angeles", 50 | "Lisbon", 51 | "San Francisco", 52 | "Sydney", 53 | "London", 54 | "Rome" 55 | ]) { 56 | city { 57 | name 58 | } 59 | experiences { 60 | ...ExperienceFragment 61 | } 62 | } 63 | } 64 | 65 | fragment ExperienceFragment on Experience { 66 | id 67 | title 68 | category { 69 | mainColor 70 | name 71 | } 72 | slug 73 | numRatings 74 | avgRating 75 | pricePerPerson 76 | } 77 | -------------------------------------------------------------------------------- /src/firebase.ts: -------------------------------------------------------------------------------- 1 | import * as firebaseAdmin from 'firebase-admin' 2 | 3 | import { AuthError, Context } from './utils' 4 | 5 | const admin = firebaseAdmin.initializeApp( 6 | { 7 | credential: firebaseAdmin.credential.cert({ 8 | projectId: process.env.GCP_PROJECT_ID, 9 | clientEmail: process.env.FIREBASE_CLIENT_EMAIL, 10 | privateKey: process.env.FIREBASE_PRIVATE_KEY.replace(/\\n/g, '\n'), 11 | }), 12 | databaseURL: process.env.FIREBASE_DATABASE_URL, 13 | }, 14 | 'server', 15 | ) 16 | 17 | //returns cookie token 18 | const createUserSessionToken = async (args, ctx: Context) => { 19 | // Get the ID token. 20 | const idToken = args.idToken.toString() 21 | 22 | // Only process if the user just signed in in the last 5 minutes. 23 | // To guard against ID token theft, reject and require re-authentication. 24 | const decodedIdToken = await admin.auth().verifyIdToken(idToken) 25 | if (!(new Date().getTime() / 1000 - decodedIdToken.auth_time < 5 * 60)) 26 | throw new AuthError({ message: 'Recent sign in required!' }) 27 | 28 | // Set session expiration to 5 days. 29 | const expiresIn = 60 * 60 * 24 * 5 * 1000 30 | 31 | // Create the session cookie. This will also verify the ID token in the process. 32 | // The session cookie will have the same claims as the ID token. 33 | // To only allow session cookie setting on recent sign-in, auth_time in ID token 34 | // can be checked to ensure user was recently signed in before creating a session cookie. 35 | const token = await admin 36 | .auth() 37 | .createSessionCookie(idToken, { expiresIn }) 38 | .catch(error => { 39 | console.log(error) 40 | throw new AuthError({ 41 | message: 'User Session Token Creation Error', 42 | stack: error, 43 | }) 44 | }) 45 | if (token) return token 46 | else throw new AuthError({ message: 'User Session Token Creation Error' }) 47 | } 48 | 49 | //Returns decoded User Claims 50 | const verifyUserSessionToken = async token => { 51 | //Verify session cookies tokens with firebase admin. 52 | //This is a low overhead operation. 53 | const user = await admin 54 | .auth() 55 | .verifySessionCookie(token, true /** checkRevoked */) 56 | 57 | if (user.id) return user 58 | else if (user.uid) { 59 | const { customClaims } = await getUserRecord(user.uid) 60 | return customClaims 61 | } else 62 | throw new AuthError({ message: 'User Session Token Verification Error' }) 63 | } 64 | 65 | //Sets properties into firebase user 66 | const setUserClaims = (uid, data) => admin.auth().setCustomUserClaims(uid, data) 67 | 68 | const getUserRecord = uid => admin.auth().getUser(uid) 69 | 70 | const verifyIdToken = idToken => admin.auth().verifyIdToken(idToken) 71 | 72 | export { 73 | createUserSessionToken, 74 | verifyUserSessionToken, 75 | setUserClaims, 76 | getUserRecord, 77 | verifyIdToken, 78 | } 79 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | const { ApolloServer } = require('apollo-server') 2 | import { Prisma } from './generated/prisma' 3 | import { resolvers, fragmentReplacements } from './resolvers' 4 | import { importSchema } from 'graphql-import' 5 | import { getUser } from './utils' 6 | 7 | const typeDefs = importSchema(`${__dirname}/schema.graphql`) 8 | 9 | const db = new Prisma({ 10 | fragmentReplacements, 11 | endpoint: process.env.PRISMA_ENDPOINT, 12 | secret: 'mysecret123', 13 | debug: true, 14 | }) 15 | 16 | const server = new ApolloServer({ 17 | typeDefs, 18 | resolvers, 19 | //optional parameter 20 | 21 | context: async req => { 22 | const user = await getUser(req) 23 | return { ...req, db, user } 24 | }, 25 | onHealthCheck: () => 26 | new Promise((resolve, reject) => { 27 | //database check or other asynchronous action 28 | }), 29 | introspection: true, 30 | playground: true, 31 | debug: true, 32 | }) 33 | 34 | server.listen().then(({ url }) => { 35 | console.log(`🚀 Server ready at ${url}`) 36 | console.log( 37 | `Try your health check at: ${url}.well-known/apollo/server-health`, 38 | ) 39 | }) 40 | -------------------------------------------------------------------------------- /src/resolvers/AuthPayload.ts: -------------------------------------------------------------------------------- 1 | import { Context } from '../utils' 2 | 3 | export const AuthPayload = { 4 | user: async ({ user: { id } }, args, ctx: Context, info) => { 5 | return ctx.db.query.user({ where: { id } }, info) 6 | }, 7 | } 8 | -------------------------------------------------------------------------------- /src/resolvers/ExperiencesByCity.ts: -------------------------------------------------------------------------------- 1 | import { Context } from '../utils' 2 | 3 | export const ExperiencesByCity = { 4 | city: async ({ id }, args, ctx: Context, info) => { 5 | return ctx.db.query.city({ where: { id } }, info) 6 | }, 7 | 8 | experiences: async ({ id }, args, ctx: Context, info) => { 9 | return ctx.db.query.experiences( 10 | { where: { location: { neighbourHood: { city: { id } } } } }, 11 | info, 12 | ) 13 | }, 14 | } 15 | -------------------------------------------------------------------------------- /src/resolvers/Home.ts: -------------------------------------------------------------------------------- 1 | import { Context } from '../utils' 2 | import gql from 'graphql-tag' 3 | 4 | export const Home = { 5 | // TODO rewrite this once this lands: https://github.com/graphcool/prisma/issues/1312 6 | numRatings: { 7 | fragment: `fragment NumRatings on Place { id }`, 8 | resolve: async ({ id }, args, ctx: Context, info) => { 9 | const reviews = await ctx.db.query.reviewsConnection( 10 | { where: { place: { id } } }, 11 | gql`{ aggregate { count } }`, 12 | ) 13 | return reviews.aggregate.count 14 | }, 15 | }, 16 | 17 | perNight: { 18 | fragment: `fragment PerNight on Place { pricing { perNight } }`, 19 | resolve: ({ pricing: { perNight } }) => perNight, 20 | }, 21 | 22 | // TODO rewrite this once this lands: https://github.com/graphcool/prisma/issues/1312 23 | avgRating: { 24 | fragment: `fragment AvgRating on Place { reviews { stars } }`, 25 | resolve: ({ reviews }) => { 26 | if (reviews.length > 0) { 27 | return ( 28 | reviews.reduce((acc, { stars }) => acc + stars, 0) / reviews.length 29 | ) 30 | } 31 | return null 32 | }, 33 | }, 34 | } 35 | -------------------------------------------------------------------------------- /src/resolvers/Mutation/addPaymentMethod.ts: -------------------------------------------------------------------------------- 1 | import { Context } from '../../utils' 2 | 3 | export async function addPaymentMethod(parent, args, ctx: Context, info) { 4 | await ctx.db.mutation.createPaymentAccount({ 5 | data: { 6 | creditcard: { create: args }, 7 | user: { connect: { id: ctx.user.id } }, 8 | }, 9 | }) 10 | 11 | // TODO: send email to user 12 | return { success: true } 13 | } 14 | -------------------------------------------------------------------------------- /src/resolvers/Mutation/auth.ts: -------------------------------------------------------------------------------- 1 | import { AuthError, Context } from '../../utils' 2 | 3 | import { 4 | verifyIdToken, 5 | getUserRecord, 6 | createUserSessionToken, 7 | setUserClaims, 8 | } from '../../firebase' 9 | 10 | export const auth = { 11 | async signup(parent, args, ctx: Context, info) { 12 | const { uid } = await verifyIdToken(args.idToken) 13 | 14 | const firebaseUser = await getUserRecord(uid) 15 | const { email, emailVerified, displayName, phoneNumber } = firebaseUser 16 | 17 | const user = await ctx.db.mutation.createUser({ 18 | data: { 19 | firstName: displayName, 20 | email: email, 21 | emailVerified, 22 | lastName: '', 23 | phone: phoneNumber, 24 | }, 25 | }) 26 | 27 | //Create firebase new cookie token 28 | //Persist prisma userId into firebase auth so we do not need to do a DB call. 29 | //Here we could save some authorization data also e.g. admin: true 30 | await setUserClaims(uid, { id: user.id, admin: false }) 31 | 32 | const token = await createUserSessionToken(args, ctx) 33 | 34 | return { 35 | token, 36 | user, 37 | } 38 | }, 39 | 40 | async login(parent, args, ctx: Context, info) { 41 | const { id } = await verifyIdToken(args.idToken) 42 | 43 | if (!id) new AuthError({ message: 'User is not registered' }) 44 | 45 | const user = await ctx.db.query.user({ where: { id } }) 46 | 47 | if (!user) { 48 | throw new AuthError({ message: 'User account does not exist' }) 49 | } 50 | 51 | return { 52 | token: createUserSessionToken(args, ctx), 53 | user, 54 | } 55 | }, 56 | } 57 | -------------------------------------------------------------------------------- /src/resolvers/Mutation/book.ts: -------------------------------------------------------------------------------- 1 | import { Context } from '../../utils' 2 | import gql from 'graphql-tag' 3 | 4 | export async function book(parent, args, ctx: Context, info) { 5 | const userId = ctx.user.id 6 | 7 | const paymentAccount = await getPaymentAccount(userId, ctx) 8 | if (!paymentAccount) { 9 | throw new Error(`You don't have a payment method yet`) 10 | } 11 | 12 | const alreadyBooked = await ctx.db.exists.Booking({ 13 | place: { id: args.placeId }, 14 | startDate_gte: args.checkIn, 15 | startDate_lte: args.checkOut, 16 | }) 17 | if (alreadyBooked) { 18 | throw new Error(`The requested time is not free.`) 19 | } 20 | 21 | const days = daysBetween(new Date(args.checkIn), new Date(args.checkOut)) 22 | const place = await ctx.db.query.place( 23 | { where: { id: args.placeId } }, 24 | gql`{ pricing { perNight } }`, 25 | ) 26 | 27 | if (!place) { 28 | throw new Error(`No such place found`) 29 | } 30 | 31 | const placePrice = days * place.pricing.perNight 32 | const totalPrice = placePrice * 1.2 33 | const serviceFee = placePrice * 0.2 34 | 35 | // TODO implement real stripe 36 | await payWithStripe() 37 | 38 | await ctx.db.mutation.createBooking({ 39 | data: { 40 | startDate: args.checkIn, 41 | endDate: args.checkOut, 42 | bookee: { connect: { id: userId } }, 43 | place: { connect: { id: args.placeId } }, 44 | payment: { 45 | create: { 46 | placePrice, 47 | totalPrice, 48 | serviceFee, 49 | paymentMethod: { connect: { id: paymentAccount.id } }, 50 | }, 51 | }, 52 | }, 53 | }) 54 | 55 | return { success: true } 56 | } 57 | 58 | function payWithStripe() { 59 | return Promise.resolve() 60 | } 61 | 62 | async function getPaymentAccount(userId: string, ctx: Context) { 63 | const paymentAccounts = await ctx.db.query.paymentAccounts( 64 | { where: { user: { id: userId } } }, 65 | gql`{ 66 | id 67 | creditcard { 68 | id 69 | cardNumber 70 | country 71 | expiresOnMonth 72 | expiresOnYear 73 | firstName 74 | lastName 75 | securityCode 76 | postalCode 77 | } 78 | }`, 79 | ) 80 | 81 | return paymentAccounts[0] 82 | } 83 | 84 | function daysBetween(date1: Date, date2: Date): number { 85 | // The number of milliseconds in one day 86 | const ONE_DAY = 1000 * 60 * 60 * 24 87 | 88 | // Convert both dates to milliseconds 89 | const date1Ms = date1.getTime() 90 | const date2Ms = date2.getTime() 91 | 92 | // Calculate the difference in milliseconds 93 | const difference_ms = Math.abs(date1Ms - date2Ms) 94 | 95 | return Math.round(difference_ms / ONE_DAY) 96 | } 97 | -------------------------------------------------------------------------------- /src/resolvers/Query.ts: -------------------------------------------------------------------------------- 1 | import { Context } from '../utils' 2 | import { WrapQuery } from 'graphql-tools' 3 | import { SelectionSetNode, Kind } from 'graphql' 4 | 5 | export const Query = { 6 | viewer: () => ({}), 7 | 8 | myLocation: async (parent, args, ctx, info) => { 9 | const id = ctx.user.id 10 | return ctx.db.query.user({ where: { id } }, info, { 11 | transforms: [ 12 | new WrapQuery( 13 | ['user'], 14 | (subtree: SelectionSetNode) => ({ 15 | kind: Kind.FIELD, 16 | name: { 17 | kind: Kind.NAME, 18 | value: 'location', 19 | }, 20 | selectionSet: subtree, 21 | }), 22 | // result => result && result.node, 23 | result => { 24 | // TODO clean me up 25 | console.log({ result }) 26 | return result && result.node 27 | }, 28 | ), 29 | ], 30 | }) 31 | }, 32 | 33 | topExperiences: async (parent, args, ctx: Context, info) => { 34 | return ctx.db.query.experiences({ orderBy: 'popularity_DESC' }, info) 35 | }, 36 | 37 | topHomes: async (parent, args, ctx: Context, info) => { 38 | return ctx.db.query.places({ orderBy: 'popularity_DESC' }, info) 39 | }, 40 | 41 | homesInPriceRange: async (parent, args, ctx: Context, info) => { 42 | const where = { 43 | AND: [ 44 | { pricing: { perNight_gte: args.min } }, 45 | { pricing: { perNight_lte: args.max } }, 46 | ], 47 | } 48 | return ctx.db.query.places({ where }, info) 49 | }, 50 | 51 | topReservations: async (parent, args, ctx: Context, info) => { 52 | return ctx.db.query.restaurants({ orderBy: 'popularity_DESC' }, info) 53 | }, 54 | 55 | featuredDestinations: async (parent, args, ctx: Context, info) => { 56 | return ctx.db.query.neighbourhoods( 57 | { orderBy: 'popularity_DESC', where: { featured: true } }, 58 | info, 59 | ) 60 | }, 61 | 62 | experiencesByCity: async (parent, { cities }, ctx: Context, info) => { 63 | return ctx.db.query.cities({ 64 | where: { 65 | name_in: cities, 66 | neighbourhoods_every: { 67 | locations_every: { 68 | experience: { 69 | id_gt: '0', 70 | }, 71 | }, 72 | }, 73 | }, 74 | }) 75 | }, 76 | } 77 | -------------------------------------------------------------------------------- /src/resolvers/Subscription.ts: -------------------------------------------------------------------------------- 1 | import { Context } from '../utils' 2 | 3 | export const Subscription = { 4 | city: { 5 | subscribe: async (parent, args, ctx: Context, info) => { 6 | return ctx.db.subscription.city({}, info) 7 | }, 8 | }, 9 | } 10 | -------------------------------------------------------------------------------- /src/resolvers/Viewer.ts: -------------------------------------------------------------------------------- 1 | import { Context } from '../utils' 2 | 3 | export const Viewer = { 4 | async bookings(_, args, ctx: Context, info) { 5 | const id = ctx.user.id 6 | return ctx.db.query.bookings({ where: { bookee: { id } } }, info) 7 | }, 8 | 9 | async me(_, args, ctx: Context, info) { 10 | const id = ctx.user.id 11 | return ctx.db.query.user({ where: { id: id } }, info) 12 | }, 13 | } 14 | -------------------------------------------------------------------------------- /src/resolvers/index.ts: -------------------------------------------------------------------------------- 1 | import { extractFragmentReplacements } from 'prisma-binding' 2 | import { Query } from './Query' 3 | import { Subscription } from './Subscription' 4 | import { auth } from './Mutation/auth' 5 | import { Home } from './Home' 6 | import { ExperiencesByCity } from './ExperiencesByCity' 7 | import { Viewer } from './Viewer' 8 | import { AuthPayload } from './AuthPayload' 9 | import { book } from './Mutation/book' 10 | import { addPaymentMethod } from './Mutation/addPaymentMethod' 11 | 12 | export const resolvers = { 13 | Query, 14 | Mutation: { 15 | ...auth, 16 | book, 17 | addPaymentMethod, 18 | }, 19 | Subscription, 20 | Viewer, 21 | ExperiencesByCity, 22 | Home, 23 | AuthPayload, 24 | } 25 | 26 | export const fragmentReplacements = extractFragmentReplacements(resolvers) 27 | -------------------------------------------------------------------------------- /src/schema.graphql: -------------------------------------------------------------------------------- 1 | # import * from "./generated/prisma.graphql" 2 | 3 | type Query { 4 | topExperiences: [Experience!]! 5 | topHomes: [Home!]! 6 | homesInPriceRange(min: Int!, max: Int!): [Home!]! 7 | topReservations: [Reservation!]! 8 | featuredDestinations: [Neighbourhood!]! 9 | experiencesByCity(cities: [String!]!): [ExperiencesByCity!]! 10 | viewer: Viewer 11 | myLocation: Location 12 | } 13 | 14 | type Mutation { 15 | signup( 16 | idToken: String! 17 | ): AuthPayload! 18 | login( 19 | idToken: String! 20 | ): AuthPayload! 21 | addPaymentMethod( 22 | cardNumber: String! 23 | expiresOnMonth: Int! 24 | expiresOnYear: Int! 25 | securityCode: String! 26 | firstName: String! 27 | lastName: String! 28 | postalCode: String! 29 | country: String! 30 | ): MutationResult! 31 | book( 32 | placeId: ID! 33 | checkIn: String! 34 | checkOut: String! 35 | numGuests: Int! 36 | ): MutationResult! 37 | } 38 | 39 | type Subscription { 40 | city: CitySubscriptionPayload 41 | } 42 | 43 | type Viewer { 44 | me: User! 45 | bookings: [Booking!]! 46 | } 47 | 48 | type AuthPayload { 49 | token: String! 50 | user: User! 51 | } 52 | 53 | type MutationResult { 54 | success: Boolean! 55 | } 56 | 57 | type ExperiencesByCity { 58 | experiences: [Experience!]! 59 | city: City! 60 | } 61 | 62 | type Home { 63 | id: ID! 64 | name: String 65 | description: String! 66 | numRatings: Int! 67 | avgRating: Float 68 | pictures(first: Int): [Picture!]! 69 | perNight: Int! 70 | } 71 | 72 | type Reservation { 73 | id: ID! 74 | title: String! 75 | avgPricePerPerson: Int! 76 | pictures: [Picture!]! 77 | location: Location! 78 | isCurated: Boolean! 79 | slug: String! 80 | popularity: Int! 81 | } 82 | 83 | type Experience { 84 | id: ID! 85 | category: ExperienceCategory 86 | title: String! 87 | location: Location! 88 | pricePerPerson: Int! 89 | reviews: [Review!]! 90 | preview: Picture! 91 | popularity: Int! 92 | } 93 | 94 | type Review { 95 | accuracy: Int! 96 | checkIn: Int! 97 | cleanliness: Int! 98 | communication: Int! 99 | createdAt: DateTime! 100 | id: ID! 101 | location: Int! 102 | stars: Int! 103 | text: String! 104 | value: Int! 105 | } 106 | 107 | type Neighbourhood { 108 | id: ID! 109 | name: String! 110 | slug: String! 111 | homePreview: Picture 112 | city: City! 113 | featured: Boolean! 114 | popularity: Int! 115 | } 116 | 117 | type Location { 118 | id: ID! 119 | lat: Float! 120 | lng: Float! 121 | address: String 122 | directions: String 123 | } 124 | 125 | type Picture { 126 | id: ID! 127 | url: String! 128 | } 129 | 130 | type City { 131 | id: ID! 132 | name: String! 133 | } 134 | 135 | type ExperienceCategory { 136 | id: ID! 137 | mainColor: String! 138 | name: String! 139 | experience: Experience 140 | } 141 | 142 | type User { 143 | bookings: [Booking!] 144 | createdAt: DateTime! 145 | email: String! 146 | firstName: String! 147 | hostingExperiences: [Experience!] 148 | id: ID! 149 | isSuperHost: Boolean! 150 | lastName: String! 151 | location: Location! 152 | notifications: [Notification!] 153 | ownedPlaces: [Place!] 154 | paymentAccount: [PaymentAccount!] 155 | phone: String! 156 | profilePicture: Picture 157 | receivedMessages: [Message!] 158 | responseRate: Float 159 | responseTime: Int 160 | sentMessages: [Message!] 161 | updatedAt: DateTime! 162 | token: String! 163 | } 164 | 165 | type PaymentAccount { 166 | id: ID! 167 | createdAt: DateTime! 168 | type: PAYMENT_PROVIDER 169 | user: User! 170 | payments: [Payment!]! 171 | paypal: PaypalInformation 172 | creditcard: CreditCardInformation 173 | } 174 | 175 | type Place { 176 | id: ID! 177 | name: String 178 | size: PLACE_SIZES 179 | shortDescription: String! 180 | description: String! 181 | slug: String! 182 | maxGuests: Int! 183 | numBedrooms: Int! 184 | numBeds: Int! 185 | numBaths: Int! 186 | reviews: [Review!]! 187 | amenities: Amenities! 188 | host: User! 189 | pricing: Pricing! 190 | location: Location! 191 | views: PlaceViews! 192 | guestRequirements: GuestRequirements 193 | policies: Policies 194 | houseRules: HouseRules 195 | bookings: [Booking!]! 196 | pictures: [Picture!] 197 | popularity: Int! 198 | } 199 | 200 | type Booking { 201 | id: ID! 202 | createdAt: DateTime! 203 | bookee: User! 204 | place: Place! 205 | startDate: DateTime! 206 | endDate: DateTime! 207 | payment: Payment! 208 | } 209 | 210 | type Notification { 211 | createdAt: DateTime! 212 | id: ID! 213 | link: String! 214 | readDate: DateTime! 215 | type: NOTIFICATION_TYPE 216 | user: User! 217 | } 218 | 219 | type Payment { 220 | booking: Booking! 221 | createdAt: DateTime! 222 | id: ID! 223 | paymentMethod: PaymentAccount! 224 | serviceFee: Float! 225 | } 226 | 227 | type PaypalInformation { 228 | createdAt: DateTime! 229 | email: String! 230 | id: ID! 231 | paymentAccount: PaymentAccount! 232 | } 233 | 234 | type CreditCardInformation { 235 | cardNumber: String! 236 | country: String! 237 | createdAt: DateTime! 238 | expiresOnMonth: Int! 239 | expiresOnYear: Int! 240 | firstName: String! 241 | id: ID! 242 | lastName: String! 243 | paymentAccount: PaymentAccount 244 | postalCode: String! 245 | securityCode: String! 246 | } 247 | 248 | type Message { 249 | createdAt: DateTime! 250 | deliveredAt: DateTime! 251 | id: ID! 252 | readAt: DateTime! 253 | } 254 | 255 | type Pricing { 256 | averageMonthly: Int! 257 | averageWeekly: Int! 258 | basePrice: Int! 259 | cleaningFee: Int 260 | currency: CURRENCY 261 | extraGuests: Int 262 | id: ID! 263 | monthlyDiscount: Int 264 | perNight: Int! 265 | securityDeposit: Int 266 | smartPricing: Boolean! 267 | weekendPricing: Int 268 | weeklyDiscount: Int 269 | } 270 | 271 | type PlaceViews { 272 | id: ID! 273 | lastWeek: Int! 274 | } 275 | 276 | type GuestRequirements { 277 | govIssuedId: Boolean! 278 | guestTripInformation: Boolean! 279 | id: ID! 280 | recommendationsFromOtherHosts: Boolean! 281 | } 282 | 283 | type Policies { 284 | checkInEndTime: Float! 285 | checkInStartTime: Float! 286 | checkoutTime: Float! 287 | createdAt: DateTime! 288 | id: ID! 289 | updatedAt: DateTime! 290 | } 291 | 292 | type HouseRules { 293 | additionalRules: String 294 | createdAt: DateTime! 295 | id: ID! 296 | partiesAndEventsAllowed: Boolean 297 | petsAllowed: Boolean 298 | smokingAllowed: Boolean 299 | suitableForChildren: Boolean 300 | suitableForInfants: Boolean 301 | updatedAt: DateTime! 302 | } 303 | 304 | type Amenities { 305 | airConditioning: Boolean! 306 | babyBath: Boolean! 307 | babyMonitor: Boolean! 308 | babysitterRecommendations: Boolean! 309 | bathtub: Boolean! 310 | breakfast: Boolean! 311 | buzzerWirelessIntercom: Boolean! 312 | cableTv: Boolean! 313 | changingTable: Boolean! 314 | childrensBooksAndToys: Boolean! 315 | childrensDinnerware: Boolean! 316 | crib: Boolean! 317 | doorman: Boolean! 318 | dryer: Boolean! 319 | elevator: Boolean! 320 | essentials: Boolean! 321 | familyKidFriendly: Boolean! 322 | freeParkingOnPremises: Boolean! 323 | freeParkingOnStreet: Boolean! 324 | gym: Boolean! 325 | hairDryer: Boolean! 326 | hangers: Boolean! 327 | heating: Boolean! 328 | hotTub: Boolean! 329 | id: ID! 330 | indoorFireplace: Boolean! 331 | internet: Boolean! 332 | iron: Boolean! 333 | kitchen: Boolean! 334 | laptopFriendlyWorkspace: Boolean! 335 | paidParkingOffPremises: Boolean! 336 | petsAllowed: Boolean! 337 | pool: Boolean! 338 | privateEntrance: Boolean! 339 | shampoo: Boolean! 340 | smokingAllowed: Boolean! 341 | suitableForEvents: Boolean! 342 | tv: Boolean! 343 | washer: Boolean! 344 | wheelchairAccessible: Boolean! 345 | wirelessInternet: Boolean! 346 | } -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | import { Prisma } from './generated/prisma' 2 | import { verifyUserSessionToken } from './firebase' 3 | 4 | export interface Context { 5 | db: Prisma 6 | req: any 7 | user: Auth 8 | } 9 | 10 | export interface Auth { 11 | id: string 12 | admin: boolean 13 | [key: string]: any 14 | } 15 | 16 | export async function getUser(ctx) { 17 | const Authorization = (ctx.req || ctx.request).get('Authorization') 18 | if (Authorization) { 19 | const token = Authorization.replace('Bearer ', '') 20 | const { id, admin } = (await verifyUserSessionToken(token)) as Auth 21 | return { id, admin } 22 | } 23 | return null 24 | } 25 | 26 | export class AuthError extends Error { 27 | constructor( 28 | error: { message: string; stack?: any } = { message: 'Not authorized' }, 29 | ) { 30 | super(error.message) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "moduleResolution": "node", 5 | "module": "commonjs", 6 | "sourceMap": true, 7 | "noUnusedLocals": true, 8 | "rootDir": "src", 9 | "outDir": "dist", 10 | "lib": [ 11 | "esnext", "dom" 12 | ] 13 | } 14 | } 15 | --------------------------------------------------------------------------------