├── .gitignore ├── README.md ├── api-gateway ├── .deploy.example.env ├── .gitignore ├── .production.example.env ├── Dockerfile ├── appspec.yml ├── aws │ └── codedeploy │ │ ├── AfterInstall │ │ ├── 0_changePermissions.sh │ │ ├── 1_installPackages.sh │ │ └── 2_buildBundle.sh │ │ ├── ApplicationStart.sh │ │ └── BeforeInstall │ │ ├── 0_stopExistingProcess.sh │ │ └── 1_removePreviousInstallation.sh ├── babel.config.js ├── ecosystem.config.js ├── package.json ├── src │ ├── adapters │ │ ├── ListingsService.js │ │ └── UsersService.js │ ├── graphql │ │ ├── resolvers │ │ │ ├── Mutation │ │ │ │ ├── createListing.js │ │ │ │ ├── createUser.js │ │ │ │ ├── createUserSession.js │ │ │ │ ├── deleteUserSession.js │ │ │ │ └── index.js │ │ │ ├── Query │ │ │ │ ├── index.js │ │ │ │ ├── listings.js │ │ │ │ └── userSession.js │ │ │ ├── UserSession.js │ │ │ └── index.js │ │ └── typeDefs.js │ ├── helpers │ │ └── accessEnv.js │ ├── index.js │ └── server │ │ ├── formatGraphQLErrors.js │ │ ├── injectSession.js │ │ └── startServer.js ├── webpack.config.js └── yarn.lock ├── classifieds-app ├── .env.example ├── .gitignore ├── package.json ├── src │ ├── api │ │ └── graphqlClient.js │ ├── components │ │ ├── Root │ │ │ ├── AccountDetails │ │ │ │ ├── Account │ │ │ │ │ ├── Account.js │ │ │ │ │ └── index.js │ │ │ │ ├── AccountDetails.js │ │ │ │ ├── Login │ │ │ │ │ ├── Login.js │ │ │ │ │ └── index.js │ │ │ │ ├── SignUp │ │ │ │ │ ├── SignUp.js │ │ │ │ │ └── index.js │ │ │ │ └── index.js │ │ │ ├── Listings │ │ │ │ ├── AddListing │ │ │ │ │ ├── AddListing.js │ │ │ │ │ └── index.js │ │ │ │ ├── Listings.js │ │ │ │ └── index.js │ │ │ ├── Root.js │ │ │ └── index.js │ │ └── shared │ │ │ ├── TextInput.js │ │ │ └── Textarea.js │ ├── index.html │ ├── index.js │ ├── store │ │ ├── ducks │ │ │ ├── index.js │ │ │ └── session.js │ │ └── index.js │ └── theme.js └── yarn.lock ├── docker-compose.yml ├── listings-service ├── .deploy.example.env ├── .gitignore ├── .production.example.env ├── .sequelizerc ├── Dockerfile ├── appspec.yml ├── aws │ └── codedeploy │ │ ├── AfterInstall │ │ ├── 0_changePermissions.sh │ │ ├── 1_installPackages.sh │ │ ├── 2_runMigrations.sh │ │ └── 3_buildBundle.sh │ │ ├── ApplicationStart.sh │ │ └── BeforeInstall │ │ ├── 0_stopExistingProcess.sh │ │ └── 1_removePreviousInstallation.sh ├── babel.config.js ├── ecosystem.config.js ├── package.json ├── sequelize │ ├── config.js │ └── migrations │ │ └── 20191221122228-create-listings.js ├── src │ ├── db │ │ ├── connection.js │ │ └── models.js │ ├── helpers │ │ └── accessEnv.js │ ├── index.js │ └── server │ │ ├── routes.js │ │ └── startServer.js ├── webpack.config.js └── yarn.lock ├── node-deploy ├── .gitignore ├── package.json ├── src │ ├── helpers │ │ └── accessEnv.js │ └── index.js └── yarn.lock ├── terraform ├── .gitignore ├── api-gateway.tf ├── codedeploy-app │ ├── iam-instance-policies.tf │ ├── iam-role-policies.tf │ ├── iam-roles.tf │ ├── main.tf │ ├── outputs.tf │ ├── s3.tf │ └── variables.tf ├── key-pairs.tf ├── listings-service.tf ├── mysql-db │ ├── main.tf │ ├── outputs.tf │ └── variables.tf ├── networking.tf ├── node-server │ ├── main.tf │ ├── outputs.tf │ └── variables.tf ├── outputs.tf ├── providers.tf ├── terraform.example.tfvars ├── users-service.tf └── variables.tf └── users-service ├── .deploy.example.env ├── .gitignore ├── .production.example.env ├── .sequelizerc ├── Dockerfile ├── appspec.yml ├── aws └── codedeploy │ ├── AfterInstall │ ├── 0_changePermissions.sh │ ├── 1_installPackages.sh │ ├── 2_runMigrations.sh │ └── 3_buildBundle.sh │ ├── ApplicationStart.sh │ └── BeforeInstall │ ├── 0_stopExistingProcess.sh │ └── 1_removePreviousInstallation.sh ├── babel.config.js ├── ecosystem.config.js ├── package.json ├── sequelize ├── config.js └── migrations │ ├── 20191221125737-create-users.js │ └── 20191221150800-create-userSessions.js ├── src ├── db │ ├── connection.js │ └── models.js ├── helpers │ ├── accessEnv.js │ ├── generateUUID.js │ ├── hashPassword.js │ └── passwordCompareSync.js ├── index.js └── server │ ├── routes.js │ └── startServer.js ├── webpack.config.js └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /appspec.yml 3 | /deploy.lock -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Microservices Demo 2 | 3 | Uses the following technologies: 4 | 5 | - Docker (and Docker Compose) 6 | - React 7 | - Node.js 8 | - MySQL 9 | - Sequelize 10 | 11 | ## Setup 12 | 13 | ```sh 14 | # in main directory 15 | docker-compose up 16 | 17 | # in a separate terminal, inside classifieds-app 18 | yarn 19 | yarn watch 20 | ``` 21 | 22 | ## Deploy 23 | 24 | Check out my video series for a step-by-step tutorial on how to deploy this. -------------------------------------------------------------------------------- /api-gateway/.deploy.example.env: -------------------------------------------------------------------------------- 1 | APPLICATION_NAME=api-gateway 2 | AWS_ACCESS_KEY_ID= 3 | AWS_ACCESS_KEY_SECRET= 4 | CODEDEPLOY_DEPLOYMENT_GROUP_NAME=dev -------------------------------------------------------------------------------- /api-gateway/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .deploy.env 73 | .production.env 74 | .env 75 | .env.test 76 | 77 | # parcel-bundler cache (https://parceljs.org/) 78 | .cache 79 | 80 | # Next.js build output 81 | .next 82 | 83 | # Nuxt.js build / generate output 84 | .nuxt 85 | dist 86 | 87 | # Gatsby files 88 | .cache/ 89 | # Comment in the public line in if your project uses Gatsby and not Next.js 90 | # https://nextjs.org/blog/next-9-1#public-directory-support 91 | # public 92 | 93 | # vuepress build output 94 | .vuepress/dist 95 | 96 | # Serverless directories 97 | .serverless/ 98 | 99 | # FuseBox cache 100 | .fusebox/ 101 | 102 | # DynamoDB Local files 103 | .dynamodb/ 104 | 105 | # TernJS port file 106 | .tern-port 107 | 108 | # Stores VSCode versions used for testing VSCode extensions 109 | .vscode-test 110 | -------------------------------------------------------------------------------- /api-gateway/.production.example.env: -------------------------------------------------------------------------------- 1 | LISTINGS_SERVICE_URI=http://10.0.1.5 2 | USERS_SERVICE_URI=http://10.0.1.6 -------------------------------------------------------------------------------- /api-gateway/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:12 2 | 3 | COPY . /opt/app 4 | 5 | WORKDIR /opt/app/api-gateway 6 | 7 | RUN yarn 8 | 9 | EXPOSE 3000 10 | 11 | CMD yarn watch 12 | -------------------------------------------------------------------------------- /api-gateway/appspec.yml: -------------------------------------------------------------------------------- 1 | # will be moved into root directory 2 | version: 0.0 3 | os: linux 4 | files: 5 | - source: / 6 | destination: /opt/microservices-demo 7 | hooks: 8 | BeforeInstall: 9 | - location: api-gateway/aws/codedeploy/BeforeInstall/0_stopExistingProcess.sh 10 | timeout: 300 11 | runas: root 12 | - location: api-gateway/aws/codedeploy/BeforeInstall/1_removePreviousInstallation.sh 13 | timeout: 300 14 | runas: ec2-user 15 | AfterInstall: 16 | - location: api-gateway/aws/codedeploy/AfterInstall/0_changePermissions.sh 17 | timeout: 300 18 | runas: root 19 | - location: api-gateway/aws/codedeploy/AfterInstall/1_installPackages.sh 20 | timeout: 300 21 | runas: ec2-user 22 | - location: api-gateway/aws/codedeploy/AfterInstall/2_buildBundle.sh 23 | timeout: 300 24 | runas: ec2-user 25 | ApplicationStart: 26 | - location: api-gateway/aws/codedeploy/ApplicationStart.sh 27 | timeout: 300 28 | runas: root 29 | -------------------------------------------------------------------------------- /api-gateway/aws/codedeploy/AfterInstall/0_changePermissions.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | cd /opt 3 | chown -R ec2-user ./microservices-demo 4 | -------------------------------------------------------------------------------- /api-gateway/aws/codedeploy/AfterInstall/1_installPackages.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | cd /opt/microservices-demo/api-gateway 3 | mv .production.env .env 4 | yarn 5 | -------------------------------------------------------------------------------- /api-gateway/aws/codedeploy/AfterInstall/2_buildBundle.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | cd /opt/microservices-demo/api-gateway 3 | yarn build -------------------------------------------------------------------------------- /api-gateway/aws/codedeploy/ApplicationStart.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | cd /opt/microservices-demo/api-gateway 3 | pm2 start 4 | -------------------------------------------------------------------------------- /api-gateway/aws/codedeploy/BeforeInstall/0_stopExistingProcess.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | deployment_dir=/opt/microservices-demo/api-gateway 4 | if [ -d "$deployment_dir" ] && [ -x "$deployment_dir" ]; then 5 | cd /opt/microservices-demo/api-gateway 6 | 7 | # we have to do this because it throws an error if it can't find the process... and then the whole build breaks 8 | node -e 'try{require("child_process").execSync("pm2 stop api-gateway")}catch(e){}'; 9 | fi 10 | -------------------------------------------------------------------------------- /api-gateway/aws/codedeploy/BeforeInstall/1_removePreviousInstallation.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | deployment_dir=/opt/microservices-demo/api-gateway 4 | if [ -d "$deployment_dir" ] && [ -x "$deployment_dir" ]; then 5 | cd /opt/microservices-demo/api-gateway 6 | 7 | rm -rf * 8 | fi -------------------------------------------------------------------------------- /api-gateway/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: [ 3 | [ 4 | "module-resolver", 5 | { 6 | alias: { 7 | "#root": "./src" 8 | } 9 | } 10 | ] 11 | ], 12 | presets: [ 13 | [ 14 | "@babel/preset-env", 15 | { 16 | targets: { 17 | node: "current" 18 | } 19 | } 20 | ] 21 | ] 22 | }; 23 | -------------------------------------------------------------------------------- /api-gateway/ecosystem.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | apps: [ 3 | { 4 | name: "api-gateway", 5 | script: "dist/bundle.js", 6 | env: { 7 | PORT: 80, 8 | NODE_ENV: "production" 9 | } 10 | } 11 | ] 12 | }; 13 | -------------------------------------------------------------------------------- /api-gateway/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "api-gateway", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "license": "MIT", 6 | "scripts": { 7 | "build": "webpack --progress", 8 | "deploy": "node-deploy $(pwd)", 9 | "watch": "babel-watch -L src/index.js", 10 | "linkall": "yarn link node-deploy" 11 | }, 12 | "devDependencies": { 13 | "babel-watch": "^7.0.0", 14 | "node-deploy": "../node-deploy", 15 | "nodemon": "^2.0.2" 16 | }, 17 | "dependencies": { 18 | "@babel/core": "~7.7.7", 19 | "@babel/polyfill": "~7.7.0", 20 | "@babel/preset-env": "~7.7.7", 21 | "apollo-server": "~2.9.14", 22 | "apollo-server-express": "~2.9.14", 23 | "babel-loader": "~8.0.6", 24 | "babel-plugin-module-resolver": "~4.0.0", 25 | "cookie-parser": "~1.4.4", 26 | "cors": "~2.8.5", 27 | "dotenv": "~8.2.0", 28 | "express": "~4.17.1", 29 | "got": "~10.1.0", 30 | "lodash": "~4.17.15", 31 | "webpack": "~4.41.5", 32 | "webpack-cli": "~3.3.10", 33 | "webpack-node-externals": "~1.7.2" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /api-gateway/src/adapters/ListingsService.js: -------------------------------------------------------------------------------- 1 | import got from "got"; 2 | 3 | import accessEnv from "#root/helpers/accessEnv"; 4 | 5 | const LISTINGS_SERVICE_URI = accessEnv("LISTINGS_SERVICE_URI"); 6 | 7 | export default class ListingsService { 8 | static async createListing({ description, title }) { 9 | const body = await got.post(`${LISTINGS_SERVICE_URI}/listings`, { json: { description, title } }).json(); 10 | return body; 11 | } 12 | 13 | static async fetchAllListings() { 14 | const body = await got.get(`${LISTINGS_SERVICE_URI}/listings`).json(); 15 | return body; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /api-gateway/src/adapters/UsersService.js: -------------------------------------------------------------------------------- 1 | import got from "got"; 2 | 3 | import accessEnv from "#root/helpers/accessEnv"; 4 | 5 | const USERS_SERVICE_URI = accessEnv("USERS_SERVICE_URI"); 6 | 7 | export default class UsersService { 8 | static async createUser({ email, password }) { 9 | const body = await got.post(`${USERS_SERVICE_URI}/users`, { json: { email, password } }).json(); 10 | return body; 11 | } 12 | 13 | static async fetchUser({ userId }) { 14 | const body = await got.get(`${USERS_SERVICE_URI}/users/${userId}`).json(); 15 | return body; 16 | } 17 | 18 | static async createUserSession({ email, password }) { 19 | const body = await got.post(`${USERS_SERVICE_URI}/sessions`, { json: { email, password } }).json(); 20 | return body; 21 | } 22 | 23 | static async deleteUserSession({ sessionId }) { 24 | const body = await got.delete(`${USERS_SERVICE_URI}/sessions/${sessionId}`).json(); 25 | return body; 26 | } 27 | 28 | static async fetchUserSession({ sessionId }) { 29 | const body = await got.get(`${USERS_SERVICE_URI}/sessions/${sessionId}`).json(); 30 | return body; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /api-gateway/src/graphql/resolvers/Mutation/createListing.js: -------------------------------------------------------------------------------- 1 | import ListingsService from "#root/adapters/ListingsService"; 2 | 3 | const createListingResolver = async (obj, { description, title }, context) => { 4 | if (!context.res.locals.userSession) throw new Error("Not logged in!"); 5 | return await ListingsService.createListing({ description, title }); 6 | }; 7 | 8 | export default createListingResolver; 9 | -------------------------------------------------------------------------------- /api-gateway/src/graphql/resolvers/Mutation/createUser.js: -------------------------------------------------------------------------------- 1 | import UsersService from "#root/adapters/UsersService"; 2 | 3 | const createUserResolver = async (obj, { email, password }) => { 4 | return await UsersService.createUser({ email, password }); 5 | }; 6 | 7 | export default createUserResolver; 8 | -------------------------------------------------------------------------------- /api-gateway/src/graphql/resolvers/Mutation/createUserSession.js: -------------------------------------------------------------------------------- 1 | import UsersService from "#root/adapters/UsersService"; 2 | 3 | const createUserSessionResolver = async (obj, { email, password }, context) => { 4 | const userSession = await UsersService.createUserSession({ email, password }); 5 | 6 | context.res.cookie("userSessionId", userSession.id, { httpOnly: true }); 7 | 8 | return userSession; 9 | }; 10 | 11 | export default createUserSessionResolver; 12 | -------------------------------------------------------------------------------- /api-gateway/src/graphql/resolvers/Mutation/deleteUserSession.js: -------------------------------------------------------------------------------- 1 | import UsersService from "#root/adapters/UsersService"; 2 | 3 | const deleteUserSessionResolver = async (obj, { sessionId }, context) => { 4 | await UsersService.deleteUserSession({ sessionId }); 5 | 6 | context.res.clearCookie("userSessionId"); 7 | 8 | return true; 9 | }; 10 | 11 | export default deleteUserSessionResolver; 12 | -------------------------------------------------------------------------------- /api-gateway/src/graphql/resolvers/Mutation/index.js: -------------------------------------------------------------------------------- 1 | export { default as createListing } from "./createListing"; 2 | export { default as createUser } from "./createUser"; 3 | export { default as createUserSession } from "./createUserSession"; 4 | export { default as deleteUserSession } from "./deleteUserSession"; 5 | -------------------------------------------------------------------------------- /api-gateway/src/graphql/resolvers/Query/index.js: -------------------------------------------------------------------------------- 1 | export { default as listings } from "./listings"; 2 | export { default as userSession } from "./userSession"; 3 | -------------------------------------------------------------------------------- /api-gateway/src/graphql/resolvers/Query/listings.js: -------------------------------------------------------------------------------- 1 | import ListingsService from "#root/adapters/ListingsService"; 2 | 3 | const listingsResolver = async () => { 4 | return await ListingsService.fetchAllListings(); 5 | }; 6 | 7 | export default listingsResolver; 8 | -------------------------------------------------------------------------------- /api-gateway/src/graphql/resolvers/Query/userSession.js: -------------------------------------------------------------------------------- 1 | const userSessionResolver = async (obj, args, context) => { 2 | if (args.me !== true) throw new Error("Unsupported argument value"); 3 | return context.res.locals.userSession; 4 | }; 5 | 6 | export default userSessionResolver; 7 | -------------------------------------------------------------------------------- /api-gateway/src/graphql/resolvers/UserSession.js: -------------------------------------------------------------------------------- 1 | import UsersService from "#root/adapters/UsersService"; 2 | 3 | const UserSession = { 4 | user: async userSession => { 5 | return await UsersService.fetchUser({ userId: userSession.userId }); 6 | } 7 | }; 8 | 9 | export default UserSession; 10 | -------------------------------------------------------------------------------- /api-gateway/src/graphql/resolvers/index.js: -------------------------------------------------------------------------------- 1 | import * as Mutation from "./Mutation"; 2 | import * as Query from "./Query"; 3 | import UserSession from "./UserSession"; 4 | 5 | const resolvers = { Mutation, Query, UserSession }; 6 | 7 | export default resolvers; 8 | -------------------------------------------------------------------------------- /api-gateway/src/graphql/typeDefs.js: -------------------------------------------------------------------------------- 1 | import { gql } from "apollo-server"; 2 | 3 | const typeDefs = gql` 4 | scalar Date 5 | 6 | type Listing { 7 | description: String! 8 | id: ID! 9 | title: String! 10 | } 11 | 12 | type User { 13 | email: String! 14 | id: ID! 15 | } 16 | 17 | type UserSession { 18 | createdAt: Date! 19 | expiresAt: Date! 20 | id: ID! 21 | user: User! 22 | } 23 | 24 | type Mutation { 25 | createListing(description: String!, title: String!): Listing! 26 | createUser(email: String!, password: String!): User! 27 | createUserSession(email: String!, password: String!): UserSession! 28 | deleteUserSession(sessionId: ID!): Boolean! 29 | } 30 | 31 | type Query { 32 | listings: [Listing!]! 33 | userSession(me: Boolean!): UserSession 34 | } 35 | `; 36 | 37 | export default typeDefs; 38 | -------------------------------------------------------------------------------- /api-gateway/src/helpers/accessEnv.js: -------------------------------------------------------------------------------- 1 | // accesses a variable inside of process.env, throwing an error if it's not found 2 | // always run this method in advance (i.e. upon initialisation) so that the error is thrown as early as possible 3 | // caching the values improves performance - accessing process.env many times is bad 4 | 5 | const cache = {}; 6 | 7 | const accessEnv = (key, defaultValue) => { 8 | if (!(key in process.env)) { 9 | if (defaultValue) return defaultValue; 10 | throw new Error(`${key} not found in process.env!`); 11 | } 12 | 13 | if (cache[key]) return cache[key]; 14 | 15 | cache[key] = process.env[key]; 16 | 17 | return process.env[key]; 18 | }; 19 | 20 | export default accessEnv; 21 | -------------------------------------------------------------------------------- /api-gateway/src/index.js: -------------------------------------------------------------------------------- 1 | import "@babel/polyfill"; 2 | import "dotenv/config"; 3 | 4 | import "#root/server/startServer"; 5 | -------------------------------------------------------------------------------- /api-gateway/src/server/formatGraphQLErrors.js: -------------------------------------------------------------------------------- 1 | import _ from "lodash"; 2 | 3 | const formatGraphQLErrors = error => { 4 | const errorDetails = _.get(error, "originalError.response.body"); 5 | try { 6 | if (errorDetails) return JSON.parse(errorDetails); 7 | } catch (e) {} 8 | 9 | return null; 10 | }; 11 | 12 | export default formatGraphQLErrors; 13 | -------------------------------------------------------------------------------- /api-gateway/src/server/injectSession.js: -------------------------------------------------------------------------------- 1 | import UsersService from "#root/adapters/UsersService"; 2 | 3 | const injectSession = async (req, res, next) => { 4 | if (req.cookies.userSessionId) { 5 | const userSession = await UsersService.fetchUserSession({ sessionId: req.cookies.userSessionId }); 6 | res.locals.userSession = userSession; 7 | } 8 | 9 | return next(); 10 | }; 11 | 12 | export default injectSession; 13 | -------------------------------------------------------------------------------- /api-gateway/src/server/startServer.js: -------------------------------------------------------------------------------- 1 | import { ApolloServer } from "apollo-server-express"; 2 | import cookieParser from "cookie-parser"; 3 | import cors from "cors"; 4 | import express from "express"; 5 | 6 | import resolvers from "#root/graphql/resolvers"; 7 | import typeDefs from "#root/graphql/typeDefs"; 8 | import accessEnv from "#root/helpers/accessEnv"; 9 | 10 | import formatGraphQLErrors from "./formatGraphQLErrors"; 11 | import injectSession from "./injectSession"; 12 | 13 | const PORT = accessEnv("PORT", 7000); 14 | 15 | const apolloServer = new ApolloServer({ 16 | context: a => a, 17 | formatError: formatGraphQLErrors, 18 | resolvers, 19 | typeDefs 20 | }); 21 | 22 | const app = express(); 23 | 24 | app.use(cookieParser()); 25 | 26 | app.use( 27 | cors({ 28 | origin: (origin, cb) => cb(null, true), 29 | credentials: true 30 | }) 31 | ); 32 | 33 | app.use(injectSession); 34 | 35 | apolloServer.applyMiddleware({ app, cors: false, path: "/graphql" }); 36 | 37 | app.listen(PORT, "0.0.0.0", () => { 38 | console.info(`API gateway listening on ${PORT}`); 39 | }); 40 | -------------------------------------------------------------------------------- /api-gateway/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const webpackNodeExternals = require("webpack-node-externals"); 3 | 4 | module.exports = { 5 | entry: "./src/index.js", 6 | externals: [webpackNodeExternals()], 7 | mode: "production", 8 | module: { 9 | rules: [ 10 | { 11 | test: /\.js$/, 12 | use: "babel-loader" 13 | } 14 | ] 15 | }, 16 | output: { 17 | filename: "bundle.js", 18 | path: path.resolve(__dirname, "./dist") 19 | }, 20 | target: "node" 21 | }; 22 | -------------------------------------------------------------------------------- /classifieds-app/.env.example: -------------------------------------------------------------------------------- 1 | SERVICES_URI=http://localhost:7000 -------------------------------------------------------------------------------- /classifieds-app/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | 78 | # Next.js build output 79 | .next 80 | 81 | # Nuxt.js build / generate output 82 | .nuxt 83 | dist 84 | 85 | # Gatsby files 86 | .cache/ 87 | # Comment in the public line in if your project uses Gatsby and not Next.js 88 | # https://nextjs.org/blog/next-9-1#public-directory-support 89 | # public 90 | 91 | # vuepress build output 92 | .vuepress/dist 93 | 94 | # Serverless directories 95 | .serverless/ 96 | 97 | # FuseBox cache 98 | .fusebox/ 99 | 100 | # DynamoDB Local files 101 | .dynamodb/ 102 | 103 | # TernJS port file 104 | .tern-port 105 | 106 | # Stores VSCode versions used for testing VSCode extensions 107 | .vscode-test 108 | -------------------------------------------------------------------------------- /classifieds-app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "classifieds-app", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "license": "MIT", 6 | "scripts": { 7 | "watch": "parcel --port=7001 src/index.html" 8 | }, 9 | "devDependencies": { 10 | "parcel-bundler": "~1.12.4" 11 | }, 12 | "dependencies": { 13 | "@types/react": "^16.8.0", 14 | "apollo-cache-inmemory": "~1.6.5", 15 | "apollo-client": "^2.6.4", 16 | "apollo-link-http": "~1.5.16", 17 | "graphql": "^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0", 18 | "graphql-tag": "~2.10.1", 19 | "react": "^16.8.0", 20 | "react-apollo": "~3.1.3", 21 | "react-dom": ">= 16.3.0", 22 | "react-hook-form": "~3.29.4", 23 | "react-redux": "~7.1.3", 24 | "redux": "^2.0.0 || ^3.0.0 || ^4.0.0-0", 25 | "styled-components": "~4.4.1", 26 | "yup": "~0.28.0" 27 | }, 28 | "alias": { 29 | "#root": "./src" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /classifieds-app/src/api/graphqlClient.js: -------------------------------------------------------------------------------- 1 | import { ApolloClient } from "apollo-client"; 2 | import { HttpLink } from "apollo-link-http"; 3 | import { InMemoryCache } from "apollo-cache-inmemory"; 4 | 5 | export const cache = new InMemoryCache(); 6 | 7 | const client = new ApolloClient({ 8 | cache, 9 | link: new HttpLink({ 10 | credentials: "include", 11 | uri: process.env.SERVICES_URI + "/graphql" 12 | }) 13 | }); 14 | 15 | export default client; 16 | -------------------------------------------------------------------------------- /classifieds-app/src/components/Root/AccountDetails/Account/Account.js: -------------------------------------------------------------------------------- 1 | import { useMutation } from "@apollo/react-hooks"; 2 | import gql from "graphql-tag"; 3 | import React from "react"; 4 | import { useDispatch, useSelector } from "react-redux"; 5 | import styled from "styled-components"; 6 | 7 | import { clearSession } from "#root/store/ducks/session"; 8 | 9 | const mutation = gql` 10 | mutation($sessionId: ID!) { 11 | deleteUserSession(sessionId: $sessionId) 12 | } 13 | `; 14 | 15 | const Email = styled.div` 16 | color: ${props => props.theme.nero}; 17 | font-size: 1rem; 18 | margin-top: 0.25rem; 19 | `; 20 | 21 | const LogoutLink = styled.a.attrs({ href: "#" })` 22 | color: blue; 23 | display: block; 24 | margin-top: 0.25rem; 25 | `; 26 | 27 | const Wrapper = styled.div` 28 | color: ${props => props.theme.mortar}; 29 | font-size: 0.9rem; 30 | `; 31 | 32 | const Account = () => { 33 | const [deleteUserSession] = useMutation(mutation); 34 | const dispatch = useDispatch(); 35 | const session = useSelector(state => state.session); 36 | 37 | return ( 38 | 39 | Logged in as 40 | {session.user.email} 41 | { 43 | evt.preventDefault(); 44 | dispatch(clearSession()); 45 | deleteUserSession({ variables: { sessionId: session.id } }); // no need for async 46 | }} 47 | > 48 | (Logout) 49 | 50 | 51 | ); 52 | }; 53 | 54 | export default Account; 55 | -------------------------------------------------------------------------------- /classifieds-app/src/components/Root/AccountDetails/Account/index.js: -------------------------------------------------------------------------------- 1 | import Account from "./Account"; 2 | 3 | export default Account; 4 | -------------------------------------------------------------------------------- /classifieds-app/src/components/Root/AccountDetails/AccountDetails.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import { useSelector } from "react-redux"; 3 | 4 | import Account from "./Account"; 5 | import Login from "./Login"; 6 | import SignUp from "./SignUp"; 7 | 8 | const AccountDetails = () => { 9 | const session = useSelector(state => state.session); 10 | const [isSigningUp, setIsSigningUp] = useState(false); 11 | 12 | if (session) return ; 13 | 14 | return isSigningUp ? ( 15 | setIsSigningUp(false)} /> 16 | ) : ( 17 | setIsSigningUp(true)} /> 18 | ); 19 | }; 20 | 21 | export default AccountDetails; 22 | -------------------------------------------------------------------------------- /classifieds-app/src/components/Root/AccountDetails/Login/Login.js: -------------------------------------------------------------------------------- 1 | import { useMutation } from "@apollo/react-hooks"; 2 | import gql from "graphql-tag"; 3 | import React from "react"; 4 | import useForm from "react-hook-form"; 5 | import { useDispatch } from "react-redux"; 6 | import styled from "styled-components"; 7 | 8 | import TextInput from "#root/components/shared/TextInput"; 9 | import { setSession } from "#root/store/ducks/session"; 10 | 11 | const Label = styled.label` 12 | display: block; 13 | 14 | :not(:first-child) { 15 | margin-top: 0.75rem; 16 | } 17 | `; 18 | 19 | const LabelText = styled.strong` 20 | display: block; 21 | font-size: 0.9rem; 22 | margin-bottom: 0.25rem; 23 | `; 24 | 25 | const LoginButton = styled.button` 26 | display: inline-block; 27 | margin-top: 0.5rem; 28 | `; 29 | 30 | const OrSignUp = styled.span` 31 | font-size: 0.9rem; 32 | `; 33 | 34 | const mutation = gql` 35 | mutation($email: String!, $password: String!) { 36 | createUserSession(email: $email, password: $password) { 37 | id 38 | user { 39 | email 40 | id 41 | } 42 | } 43 | } 44 | `; 45 | 46 | const Login = ({ onChangeToSignUp: pushChangeToSignUp }) => { 47 | const dispatch = useDispatch(); 48 | const { 49 | formState: { isSubmitting }, 50 | handleSubmit, 51 | register 52 | } = useForm(); 53 | const [createUserSession] = useMutation(mutation); 54 | 55 | const onSubmit = handleSubmit(async ({ email, password }) => { 56 | const { 57 | data: { createUserSession: createdSession } 58 | } = await createUserSession({ variables: { email, password } }); 59 | dispatch(setSession(createdSession)); 60 | }); 61 | 62 | return ( 63 |
64 | 68 | 72 | 73 | Login 74 | {" "} 75 | 76 | or{" "} 77 | { 80 | evt.preventDefault(); 81 | pushChangeToSignUp(); 82 | }} 83 | > 84 | Sign Up 85 | 86 | 87 |
88 | ); 89 | }; 90 | 91 | export default Login; 92 | -------------------------------------------------------------------------------- /classifieds-app/src/components/Root/AccountDetails/Login/index.js: -------------------------------------------------------------------------------- 1 | import Login from "./Login"; 2 | 3 | export default Login; 4 | -------------------------------------------------------------------------------- /classifieds-app/src/components/Root/AccountDetails/SignUp/SignUp.js: -------------------------------------------------------------------------------- 1 | import { useMutation } from "@apollo/react-hooks"; 2 | import gql from "graphql-tag"; 3 | import React from "react"; 4 | import useForm from "react-hook-form"; 5 | import styled from "styled-components"; 6 | import * as yup from "yup"; 7 | 8 | import TextInput from "#root/components/shared/TextInput"; 9 | 10 | const Label = styled.label` 11 | display: block; 12 | 13 | :not(:first-child) { 14 | margin-top: 0.75rem; 15 | } 16 | `; 17 | 18 | const LabelText = styled.strong` 19 | display: block; 20 | font-size: 0.9rem; 21 | margin-bottom: 0.25rem; 22 | `; 23 | 24 | const SignUpButton = styled.button` 25 | display: inline-block; 26 | margin-top: 0.5rem; 27 | `; 28 | 29 | const OrSignUp = styled.span` 30 | font-size: 0.9rem; 31 | `; 32 | 33 | const mutation = gql` 34 | mutation($email: String!, $password: String!) { 35 | createUser(email: $email, password: $password) { 36 | id 37 | } 38 | } 39 | `; 40 | 41 | const validationSchema = yup.object().shape({ 42 | email: yup.string().required(), 43 | password: yup 44 | .string() 45 | .required() 46 | .test("sameAsConfirmPassword", "${path} is not the same as the confirmation password", function() { 47 | return this.parent.password === this.parent.confirmPassword; 48 | }) 49 | }); 50 | 51 | const SignUp = ({ onChangeToLogin: pushChangeToLogin }) => { 52 | const { 53 | formState: { isSubmitting, isValid }, 54 | handleSubmit, 55 | register, 56 | reset 57 | } = useForm({ mode: "onChange", validationSchema }); 58 | const [createUser] = useMutation(mutation); 59 | 60 | const onSubmit = handleSubmit(async ({ email, password }) => { 61 | await createUser({ variables: { email, password } }); 62 | reset(); 63 | pushChangeToLogin(); 64 | }); 65 | 66 | return ( 67 |
68 | 72 | 76 | 80 | 81 | Sign Up 82 | {" "} 83 | 84 | or{" "} 85 | { 88 | evt.preventDefault(); 89 | pushChangeToLogin(); 90 | }} 91 | > 92 | Login 93 | 94 | 95 |
96 | ); 97 | }; 98 | 99 | export default SignUp; 100 | -------------------------------------------------------------------------------- /classifieds-app/src/components/Root/AccountDetails/SignUp/index.js: -------------------------------------------------------------------------------- 1 | import SignUp from "./SignUp"; 2 | 3 | export default SignUp; 4 | -------------------------------------------------------------------------------- /classifieds-app/src/components/Root/AccountDetails/index.js: -------------------------------------------------------------------------------- 1 | import AccountDetails from "./AccountDetails"; 2 | 3 | export default AccountDetails; 4 | -------------------------------------------------------------------------------- /classifieds-app/src/components/Root/Listings/AddListing/AddListing.js: -------------------------------------------------------------------------------- 1 | import { useMutation } from "@apollo/react-hooks"; 2 | import gql from "graphql-tag"; 3 | import React from "react"; 4 | import useForm from "react-hook-form"; 5 | import { useSelector } from "react-redux"; 6 | import styled from "styled-components"; 7 | 8 | import Textarea from "#root/components/shared/Textarea"; 9 | import TextInput from "#root/components/shared/TextInput"; 10 | 11 | const mutation = gql` 12 | mutation($description: String!, $title: String!) { 13 | createListing(description: $description, title: $title) { 14 | id 15 | } 16 | } 17 | `; 18 | 19 | const Button = styled.button` 20 | margin-top: 0.5rem; 21 | `; 22 | 23 | const Form = styled.form` 24 | background-color: ${props => props.theme.whiteSmoke}; 25 | margin-top: 1rem; 26 | padding: 1rem; 27 | `; 28 | 29 | const Label = styled.label` 30 | display: block; 31 | 32 | :not(:first-child) { 33 | margin-top: 0.5rem; 34 | } 35 | `; 36 | 37 | const LabelText = styled.strong` 38 | display: block; 39 | font-size: 0.9rem; 40 | margin-bottom: 0.5rem; 41 | `; 42 | 43 | const AddListing = ({ onAddListing: pushAddListing }) => { 44 | const [createListing] = useMutation(mutation); 45 | const { 46 | formState: { isSubmitting }, 47 | handleSubmit, 48 | register, 49 | reset 50 | } = useForm(); 51 | const session = useSelector(state => state.session); 52 | 53 | if (!session) return

Login to add listings.

; 54 | 55 | const onSubmit = handleSubmit(async ({ description, title }) => { 56 | await createListing({ variables: { description, title } }); 57 | reset(); 58 | pushAddListing(); 59 | }); 60 | 61 | return ( 62 |
63 | 67 |