├── .github ├── dependabot.yml └── workflows │ ├── deploy-auth.yml │ ├── deploy-client.yml │ ├── deploy-expiration.yml │ ├── deploy-manifests.yml │ ├── deploy-orders.yml │ ├── deploy-payments.yml │ ├── deploy-tickets.yml │ ├── publish-common-package-to-github.yaml.disabled │ ├── publish-common-package-to-npmjs.yaml.disabled │ ├── tests-auth.yml │ ├── tests-orders.yml │ ├── tests-payments.yml │ └── tests-tickets.yml ├── .gitignore ├── .prettierignore ├── .prettierrc ├── Readme.md ├── app ├── auth │ ├── .dockerignore │ ├── Dockerfile │ ├── package.json │ ├── src │ │ ├── app.ts │ │ ├── index.ts │ │ ├── models │ │ │ └── User.ts │ │ ├── routes │ │ │ ├── __test__ │ │ │ │ ├── current-user.test.ts │ │ │ │ ├── signin.test.ts │ │ │ │ ├── signout.test.ts │ │ │ │ └── signup.test.ts │ │ │ ├── current-user.ts │ │ │ ├── metrics.ts │ │ │ ├── signin.ts │ │ │ ├── signout.ts │ │ │ └── signup.ts │ │ ├── services │ │ │ └── password.ts │ │ └── test │ │ │ └── setup.ts │ └── tsconfig.json ├── client │ ├── .dockerignore │ ├── Dockerfile │ ├── jsconfig.json │ ├── next.config.js │ ├── package.json │ └── src │ │ ├── api │ │ └── build-client.js │ │ ├── components │ │ └── Header.js │ │ ├── hooks │ │ └── use-request.js │ │ └── pages │ │ ├── _app.js │ │ ├── auth │ │ ├── signin.js │ │ ├── signout.js │ │ └── signup.js │ │ ├── index.js │ │ ├── orders │ │ ├── [orderId].js │ │ └── index.js │ │ └── tickets │ │ ├── [ticketId].js │ │ └── new.js ├── common │ ├── .gitignore │ ├── package.json │ ├── src │ │ ├── errors │ │ │ ├── bad-request-error.ts │ │ │ ├── custom-error.ts │ │ │ ├── database-connection-error.ts │ │ │ ├── not-authorized-error.ts │ │ │ ├── not-found-error.ts │ │ │ └── request-validation-error.ts │ │ ├── events │ │ │ ├── AListener.ts │ │ │ ├── APublisher.ts │ │ │ ├── ESubjects.ts │ │ │ ├── IExpirationCompleteEvent.ts │ │ │ ├── IOrderCancelledEvent.ts │ │ │ ├── IOrderCreatedEvent.ts │ │ │ ├── IPaymentCreatedEvent.ts │ │ │ ├── ITicketCreatedEvent.ts │ │ │ ├── ITicketUpdatedEvent.ts │ │ │ └── types │ │ │ │ └── EOrderStatus.ts │ │ ├── index.ts │ │ └── middlewares │ │ │ ├── current-user.ts │ │ │ ├── error-handler.ts │ │ │ ├── require-auth.ts │ │ │ └── validate-request.ts │ └── tsconfig.json ├── expiration │ ├── .dockerignore │ ├── Dockerfile │ ├── package.json │ ├── src │ │ ├── NatsWrapper.ts │ │ ├── __mocks__ │ │ │ └── NatsWrapper.ts │ │ ├── events │ │ │ ├── listeneres │ │ │ │ ├── OrderCreatedListener.ts │ │ │ │ └── queueGroupName.ts │ │ │ └── publishers │ │ │ │ └── ExpirationCompletePublisher.ts │ │ ├── index.ts │ │ └── queues │ │ │ └── expiration-queue.ts │ └── tsconfig.json ├── nats-test │ ├── package.json │ ├── src │ │ ├── events │ │ │ ├── TicketCreatedListener.ts │ │ │ └── TicketCreatedPublisher.ts │ │ ├── listener.ts │ │ └── publisher.ts │ └── tsconfig.json ├── orders │ ├── .dockerignore │ ├── .eslintignore │ ├── .eslintrc.json │ ├── .huskyrc.json │ ├── Dockerfile │ ├── package.json │ ├── src │ │ ├── NatsWrapper.ts │ │ ├── __mocks__ │ │ │ └── NatsWrapper.ts │ │ ├── app.ts │ │ ├── events │ │ │ ├── listeners │ │ │ │ ├── ExpirationCompleteListener.ts │ │ │ │ ├── PaymentCreatedListener.ts │ │ │ │ ├── TicketCreatedListener.ts │ │ │ │ ├── TicketUpdatedListener.ts │ │ │ │ ├── __test__ │ │ │ │ │ ├── ExpirationCompleteListener.ts │ │ │ │ │ ├── TicketCreatedListener.test.ts │ │ │ │ │ └── TicketUpdatedLister.test.ts │ │ │ │ └── queueGroupName.ts │ │ │ └── publishers │ │ │ │ ├── OrderCancelledPublisher.ts │ │ │ │ └── OrderCreatedPublisher.ts │ │ ├── index.ts │ │ ├── models │ │ │ ├── Order.ts │ │ │ └── Ticket.ts │ │ ├── routes │ │ │ ├── __test__ │ │ │ │ ├── delete.test.ts │ │ │ │ ├── index.test.ts │ │ │ │ ├── new.test.ts │ │ │ │ └── show.test.ts │ │ │ ├── delete.ts │ │ │ ├── index.ts │ │ │ ├── new.ts │ │ │ └── show.ts │ │ └── test │ │ │ └── setup.ts │ ├── tsconfig.eslint.json │ └── tsconfig.json ├── payments │ ├── .dockerignore │ ├── Dockerfile │ ├── package.json │ ├── src │ │ ├── NatsWrapper.ts │ │ ├── __mocks__ │ │ │ ├── NatsWrapper.ts │ │ │ └── stripe.ts.disabled │ │ ├── app.ts │ │ ├── events │ │ │ ├── listeners │ │ │ │ ├── OrderCancelledListener.ts │ │ │ │ ├── OrderCreatedListener.ts │ │ │ │ ├── __test__ │ │ │ │ │ ├── OrderCancelledListener.test.ts │ │ │ │ │ └── OrderCreatedListener.test.ts │ │ │ │ └── queueGroupName.ts │ │ │ └── publishers │ │ │ │ └── PaymentCreatedPublisher.ts │ │ ├── index.ts │ │ ├── models │ │ │ ├── Order.ts │ │ │ └── Payment.ts │ │ ├── routes │ │ │ ├── __test__ │ │ │ │ └── new.test.ts │ │ │ └── new.ts │ │ ├── stripeApp.ts │ │ └── test │ │ │ └── setup.ts │ └── tsconfig.json └── tickets │ ├── .dockerignore │ ├── .eslintignore │ ├── .eslintrc.json │ ├── .huskyrc.json │ ├── Dockerfile │ ├── package.json │ ├── src │ ├── NatsWrapper.ts │ ├── __mocks__ │ │ └── NatsWrapper.ts │ ├── app.ts │ ├── events │ │ ├── listeners │ │ │ ├── OrderCancelledListener.ts │ │ │ ├── OrderCreatedListener.ts │ │ │ ├── __test__ │ │ │ │ ├── OrderCancelledListener.test.ts │ │ │ │ └── OrderCreatedListener.test.ts │ │ │ └── queueGroupName.ts │ │ └── publishers │ │ │ ├── TicketCreatedPublisher.ts │ │ │ └── TicketUpdatedPublisher.ts │ ├── index.ts │ ├── models │ │ ├── Ticket.ts │ │ └── __test__ │ │ │ └── Ticket.test.ts │ ├── routes │ │ ├── __test__ │ │ │ ├── index.test.ts │ │ │ ├── new.test.ts │ │ │ ├── show.test.ts │ │ │ └── update.test.ts │ │ ├── index.ts │ │ ├── new.ts │ │ ├── show.ts │ │ └── update.ts │ └── test │ │ └── setup.ts │ ├── tsconfig.eslint.json │ └── tsconfig.json ├── docs ├── 01-auth-service.md ├── 02-client-service.md ├── 03-shared-library.md ├── 04-tickets-service.md ├── 05-messaging.md ├── 06-orders-service.md ├── 07-expiration-service.md ├── 08-payments-service.md └── Development.md ├── k8s └── helm │ └── microservices │ ├── Chart.yaml │ └── charts │ ├── auth │ ├── Chart.yaml │ └── templates │ │ ├── auth-clusterIP.yaml │ │ ├── auth-deployment.yaml │ │ ├── auth-mongo-clusterIP.yaml │ │ └── auth-mongo-deployment.yaml │ ├── client │ ├── Chart.yaml │ └── templates │ │ ├── client-clusterIP.yaml │ │ ├── client-deployment.yaml │ │ └── ingress-config.yaml │ ├── expiration │ ├── Chart.yaml │ └── templates │ │ ├── expiration-deployment.yaml │ │ ├── expiration-redis-clusterIP.yaml │ │ └── expiration-redis-deployment.yaml │ ├── nats │ ├── Chart.yaml │ └── templates │ │ ├── nats-clusterIP.yaml │ │ └── nats-deployment.yaml │ ├── orders │ ├── Chart.yaml │ └── templates │ │ ├── orders-clusterIP.yaml │ │ ├── orders-deployment.yaml │ │ ├── orders-mongo-clusterIP.yaml │ │ └── orders-mongo-deployment.yaml │ ├── payments │ ├── Chart.yaml │ └── templates │ │ ├── payments-clusterIP.yaml │ │ ├── payments-deployment.yaml │ │ ├── payments-mongo-clusterIP.yaml │ │ └── payments-mongo-deployment.yaml │ └── tickets │ ├── Chart.yaml │ └── templates │ ├── tickets-clusterIP.yaml │ ├── tickets-deployment.yaml │ ├── tickets-mongo-clusterIP.yaml │ └── tickets-mongo-deployment.yaml └── skaffold └── skaffold.yaml /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: 'github-actions' 4 | directory: '/' 5 | schedule: 6 | interval: 'monthly' 7 | - package-ecosystem: 'npm' 8 | directory: '/app/common/' 9 | schedule: 10 | interval: 'monthly' 11 | - package-ecosystem: 'npm' 12 | directory: '/app/auth/' 13 | schedule: 14 | interval: 'monthly' 15 | - package-ecosystem: 'docker' 16 | directory: '/app/auth/' 17 | schedule: 18 | interval: 'monthly' 19 | - package-ecosystem: 'npm' 20 | directory: '/app/client/' 21 | schedule: 22 | interval: 'monthly' 23 | - package-ecosystem: 'docker' 24 | directory: '/app/client/' 25 | schedule: 26 | interval: 'monthly' 27 | - package-ecosystem: 'npm' 28 | directory: '/app/tickets/' 29 | schedule: 30 | interval: 'monthly' 31 | - package-ecosystem: 'docker' 32 | directory: '/app/tickets/' 33 | schedule: 34 | interval: 'monthly' 35 | - package-ecosystem: 'npm' 36 | directory: '/app/nats-test/' 37 | schedule: 38 | interval: 'monthly' 39 | - package-ecosystem: 'npm' 40 | directory: '/app/orders/' 41 | schedule: 42 | interval: 'monthly' 43 | - package-ecosystem: 'docker' 44 | directory: '/app/orders/' 45 | schedule: 46 | interval: 'monthly' 47 | - package-ecosystem: 'npm' 48 | directory: '/app/expiration/' 49 | schedule: 50 | interval: 'monthly' 51 | - package-ecosystem: 'docker' 52 | directory: '/app/expiration/' 53 | schedule: 54 | interval: 'monthly' 55 | - package-ecosystem: 'npm' 56 | directory: '/app/payments/' 57 | schedule: 58 | interval: 'monthly' 59 | - package-ecosystem: 'docker' 60 | directory: '/app/payments/' 61 | schedule: 62 | interval: 'monthly' 63 | -------------------------------------------------------------------------------- /.github/workflows/deploy-auth.yml: -------------------------------------------------------------------------------- 1 | name: deploy-auth 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | paths: 8 | - 'app/auth/**' 9 | 10 | jobs: 11 | build: 12 | runs-on: ubuntu-20.04 13 | steps: 14 | - uses: actions/checkout@v2 15 | - run: cd app/auth && docker build -t webmakaka/microservices-auth . 16 | - run: docker login -u $DOCKER_USERNAME -p $DOCKER_PASSWORD 17 | env: 18 | DOCKER_USERNAME: ${{secrets.DOCKER_USERNAME}} 19 | DOCKER_PASSWORD: ${{secrets.DOCKER_PASSWORD}} 20 | - run: docker push webmakaka/microservices-auth 21 | # - uses: digitalocean/actions-doctl@v2 22 | # with: 23 | # token: ${{secretes.DIGITAL_OCEAN_ACCESS_TOKEN}} 24 | # - run: doctl kubernetes cluster kubeconfig save 25 | # - run: kubectl rollout restart deployment auth-depl 26 | -------------------------------------------------------------------------------- /.github/workflows/deploy-client.yml: -------------------------------------------------------------------------------- 1 | name: deploy-client 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | paths: 8 | - 'app/client/**' 9 | 10 | jobs: 11 | build: 12 | runs-on: ubuntu-20.04 13 | steps: 14 | - uses: actions/checkout@v2 15 | - run: cd app/client && docker build -t webmakaka/microservices-client . 16 | - run: docker login -u $DOCKER_USERNAME -p $DOCKER_PASSWORD 17 | env: 18 | DOCKER_USERNAME: ${{secrets.DOCKER_USERNAME}} 19 | DOCKER_PASSWORD: ${{secrets.DOCKER_PASSWORD}} 20 | - run: docker push webmakaka/microservices-client 21 | # - uses: digitalocean/actions-doctl@v2 22 | # with: 23 | # token: ${{secretes.DIGITAL_OCEAN_ACCESS_TOKEN}} 24 | # - run: doctl kubernetes cluster kubeconfig save 25 | # - run: kubectl rollout restart deployment client-depl 26 | -------------------------------------------------------------------------------- /.github/workflows/deploy-expiration.yml: -------------------------------------------------------------------------------- 1 | name: deploy-expiration 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | paths: 8 | - 'app/expiration/**' 9 | 10 | jobs: 11 | build: 12 | runs-on: ubuntu-20.04 13 | steps: 14 | - uses: actions/checkout@v2 15 | - run: cd app/expiration && docker build -t webmakaka/microservices-expiration . 16 | - run: docker login -u $DOCKER_USERNAME -p $DOCKER_PASSWORD 17 | env: 18 | DOCKER_USERNAME: ${{secrets.DOCKER_USERNAME}} 19 | DOCKER_PASSWORD: ${{secrets.DOCKER_PASSWORD}} 20 | - run: docker push webmakaka/microservices-expiration 21 | # - uses: digitalocean/actions-doctl@v2 22 | # with: 23 | # token: ${{secretes.DIGITAL_OCEAN_ACCESS_TOKEN}} 24 | # - run: doctl kubernetes cluster kubeconfig save 25 | # - run: kubectl rollout restart deployment expiration-depl 26 | -------------------------------------------------------------------------------- /.github/workflows/deploy-manifests.yml: -------------------------------------------------------------------------------- 1 | # name: deploy-manifests 2 | 3 | # on: 4 | # push: 5 | # branches: 6 | # - master 7 | # paths: 8 | # - 'k8s/**' 9 | 10 | # jobs: 11 | # build: 12 | # runs-on: ubuntu-20.04 13 | # steps: 14 | # - uses: actions/checkout@v2 15 | # - uses: digitalocean/actions-doctl@v2 16 | # with: 17 | # token: ${{secretes.DIGITAL_OCEAN_ACCESS_TOKEN}} 18 | # - run: doctl kubernetes cluster kubeconfig save 19 | # - run: kubectl apply -f k8s/common && kubectl apply -f k8s/prod 20 | -------------------------------------------------------------------------------- /.github/workflows/deploy-orders.yml: -------------------------------------------------------------------------------- 1 | name: deploy-orders 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | paths: 8 | - 'app/orders/**' 9 | 10 | jobs: 11 | build: 12 | runs-on: ubuntu-20.04 13 | steps: 14 | - uses: actions/checkout@v2 15 | - run: cd app/orders && docker build -t webmakaka/microservices-orders . 16 | - run: docker login -u $DOCKER_USERNAME -p $DOCKER_PASSWORD 17 | env: 18 | DOCKER_USERNAME: ${{secrets.DOCKER_USERNAME}} 19 | DOCKER_PASSWORD: ${{secrets.DOCKER_PASSWORD}} 20 | - run: docker push webmakaka/microservices-orders 21 | # - uses: digitalocean/actions-doctl@v2 22 | # with: 23 | # token: ${{secretes.DIGITAL_OCEAN_ACCESS_TOKEN}} 24 | # - run: doctl kubernetes cluster kubeconfig save 25 | # - run: kubectl rollout restart deployment orders-depl 26 | -------------------------------------------------------------------------------- /.github/workflows/deploy-payments.yml: -------------------------------------------------------------------------------- 1 | name: deploy-payments 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | paths: 8 | - 'app/payments/**' 9 | 10 | jobs: 11 | build: 12 | runs-on: ubuntu-20.04 13 | steps: 14 | - uses: actions/checkout@v2 15 | - run: cd app/payments && docker build -t webmakaka/microservices-payments . 16 | - run: docker login -u $DOCKER_USERNAME -p $DOCKER_PASSWORD 17 | env: 18 | DOCKER_USERNAME: ${{secrets.DOCKER_USERNAME}} 19 | DOCKER_PASSWORD: ${{secrets.DOCKER_PASSWORD}} 20 | - run: docker push webmakaka/microservices-payments 21 | # - uses: digitalocean/actions-doctl@v2 22 | # with: 23 | # token: ${{secretes.DIGITAL_OCEAN_ACCESS_TOKEN}} 24 | # - run: doctl kubernetes cluster kubeconfig save 25 | # - run: kubectl rollout restart deployment payments-depl 26 | -------------------------------------------------------------------------------- /.github/workflows/deploy-tickets.yml: -------------------------------------------------------------------------------- 1 | name: deploy-tickets 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | paths: 8 | - 'app/tickets/**' 9 | 10 | jobs: 11 | build: 12 | runs-on: ubuntu-20.04 13 | steps: 14 | - uses: actions/checkout@v2 15 | - run: cd app/tickets && docker build -t webmakaka/microservices-tickets . 16 | - run: docker login -u $DOCKER_USERNAME -p $DOCKER_PASSWORD 17 | env: 18 | DOCKER_USERNAME: ${{secrets.DOCKER_USERNAME}} 19 | DOCKER_PASSWORD: ${{secrets.DOCKER_PASSWORD}} 20 | - run: docker push webmakaka/microservices-tickets 21 | # - uses: digitalocean/actions-doctl@v2 22 | # with: 23 | # token: ${{secretes.DIGITAL_OCEAN_ACCESS_TOKEN}} 24 | # - run: doctl kubernetes cluster kubeconfig save 25 | # - run: kubectl rollout restart deployment tickets-depl 26 | -------------------------------------------------------------------------------- /.github/workflows/publish-common-package-to-github.yaml.disabled: -------------------------------------------------------------------------------- 1 | name: Publish Package to GitHub 2 | on: 3 | push: 4 | branches: 5 | - master 6 | paths: 7 | - 'app/common/**' 8 | # on: 9 | # release: 10 | # types: 11 | # - published 12 | jobs: 13 | # ci: 14 | # runs-on: Ubuntu-20.04 15 | # steps: 16 | # - uses: actions/checkout@v2 17 | # - uses: actions/setup-node@v1 18 | # with: 19 | # node-version: '14.x' 20 | # - name: Installing packages 21 | # run: cd app/common/ && npm ci 22 | # - name: Running linter 23 | # run: npm run lint 24 | publish-package: 25 | runs-on: Ubuntu-20.04 26 | # needs: ci 27 | steps: 28 | - uses: actions/checkout@v2 29 | - uses: actions/setup-node@v2 30 | with: 31 | node-version: '14.x' 32 | registry-url: https://npm.pkg.github.com 33 | scope: '@webmakaka' 34 | - name: Installing packages 35 | #run: cd app/common/ && npm install && npm update @types/express-serve-static-core --depth 2 && npm update @types/serve-static --depth 2 36 | run: | 37 | cd app/common/ 38 | npm install 39 | npm update @types/express-serve-static-core --depth 2 40 | npm update @types/serve-static --depth 2 41 | - name: Publishing Package 42 | # && npm version patch 43 | run: | 44 | cd app/common/ 45 | npm version patch 46 | npm run build 47 | npm publish 48 | env: 49 | NODE_AUTH_TOKEN: ${{ secrets.NODE_AUTH_TOKEN }} 50 | -------------------------------------------------------------------------------- /.github/workflows/publish-common-package-to-npmjs.yaml.disabled: -------------------------------------------------------------------------------- 1 | name: Publish Package to NpmJS 2 | on: 3 | push: 4 | branches: 5 | - master 6 | paths: 7 | - 'app/common/**' 8 | # on: 9 | # release: 10 | # types: 11 | # - published 12 | jobs: 13 | # ci: 14 | # runs-on: Ubuntu-20.04 15 | # steps: 16 | # - uses: actions/checkout@v2 17 | # - uses: actions/setup-node@v1 18 | # with: 19 | # node-version: '14.x' 20 | # - name: Installing packages 21 | # run: cd app/common/ && npm ci 22 | # - name: Running linter 23 | # run: npm run lint 24 | publish-package: 25 | runs-on: Ubuntu-20.04 26 | # needs: ci 27 | steps: 28 | - uses: actions/checkout@v2 29 | - uses: actions/setup-node@v2 30 | with: 31 | node-version: '14.x' 32 | registry-url: https://registry.npmjs.org 33 | scope: '@webmak' 34 | - name: Installing packages 35 | run: | 36 | cd app/common/ 37 | npm install 38 | npm update @types/express-serve-static-core --depth 2 39 | npm update @types/serve-static --depth 2 40 | - name: Publishing Package 41 | run: | 42 | cd app/common/ 43 | npm version patch 44 | npm run build 45 | npm publish --access=public 46 | env: 47 | NODE_AUTH_TOKEN: ${{ secrets.NPMJS_AUTH_TOKEN }} 48 | -------------------------------------------------------------------------------- /.github/workflows/tests-auth.yml: -------------------------------------------------------------------------------- 1 | name: tests-auth 2 | 3 | on: 4 | push: 5 | paths: 6 | - 'app/auth/**' 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-20.04 11 | steps: 12 | - uses: actions/checkout@v2 13 | - run: cd app/auth && npm install && npm run test:ci 14 | -------------------------------------------------------------------------------- /.github/workflows/tests-orders.yml: -------------------------------------------------------------------------------- 1 | name: tests-orders 2 | 3 | on: 4 | push: 5 | paths: 6 | - 'app/orders/**' 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-20.04 11 | steps: 12 | - uses: actions/checkout@v2 13 | - run: cd app/orders && npm install && npm run test:ci 14 | -------------------------------------------------------------------------------- /.github/workflows/tests-payments.yml: -------------------------------------------------------------------------------- 1 | name: tests-payments 2 | 3 | on: 4 | push: 5 | paths: 6 | - 'app/payments/**' 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-20.04 11 | steps: 12 | - uses: actions/checkout@v2 13 | - run: cd app/payments && npm install && npm run test:ci 14 | -------------------------------------------------------------------------------- /.github/workflows/tests-tickets.yml: -------------------------------------------------------------------------------- 1 | name: tests-tickets 2 | 3 | on: 4 | push: 5 | paths: 6 | - 'app/tickets/**' 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-20.04 11 | steps: 12 | - uses: actions/checkout@v2 13 | - run: cd app/tickets && npm install && npm run test:ci 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.~ 2 | *.log 3 | build/ 4 | node_modules/ 5 | package-lock.json -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | # *.test.js 3 | # *.spec.js -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "bracketSpacing": true 4 | } 5 | -------------------------------------------------------------------------------- /app/auth/.dockerignore: -------------------------------------------------------------------------------- 1 | **/.eslintignore 2 | **/.eslintrc 3 | **/.prettierignore 4 | **/.prettierrc 5 | 6 | **/node_modules/ 7 | **/node_modules_linux/ 8 | **/Dockerfile 9 | **/.dockerignore 10 | **/.gitignore 11 | **/.git 12 | **/README.md 13 | **/LICENSE 14 | **/.vscode 15 | 16 | **/test/ 17 | **/.next/ -------------------------------------------------------------------------------- /app/auth/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:lts-alpine3.13 2 | 3 | WORKDIR /app 4 | COPY package.json ./ 5 | #RUN npm config set //npm.pkg.github.com/:_authToken=43fb630d62fa9b844d62c9faf11f4e6e9b62exxx 6 | #RUN npm config set @webmakaka:registry https://npm.pkg.github.com/webmakaka 7 | RUN npm install --only=prod 8 | COPY ./ ./ 9 | CMD ["npm", "start"] 10 | -------------------------------------------------------------------------------- /app/auth/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "auth", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "NODE_PATH=./src ts-node-dev src/index.ts", 8 | "start:dev": "NODE_PATH=./src nodemon src/index.ts", 9 | "test": "NODE_PATH=./src jest --watchAll --no-cache", 10 | "test:ci": "NODE_PATH=./src jest" 11 | }, 12 | "jest": { 13 | "preset": "ts-jest", 14 | "testEnvironment": "node", 15 | "setupFilesAfterEnv": [ 16 | "./src/test/setup.ts" 17 | ] 18 | }, 19 | "keywords": [], 20 | "author": "", 21 | "license": "ISC", 22 | "dependencies": { 23 | "@types/cookie-session": "^2.0.42", 24 | "@types/express": "^4.17.11", 25 | "@types/jsonwebtoken": "^8.5.0", 26 | "@types/mongoose": "^5.10.3", 27 | "@webmak/microservices-common": "^1.0.6", 28 | "cookie-session": "^1.4.0", 29 | "express": "^4.17.1", 30 | "express-async-errors": "^3.1.1", 31 | "express-validator": "^6.10.0", 32 | "jsonwebtoken": "^8.5.1", 33 | "mongoose": "^5.12.0", 34 | "prom-client": "^13.1.0", 35 | "ts-node-dev": "^1.1.6", 36 | "typescript": "^4.2.3" 37 | }, 38 | "devDependencies": { 39 | "@types/jest": "^26.0.20", 40 | "@types/supertest": "^2.0.10", 41 | "jest": "^26.6.3", 42 | "mongodb-memory-server": "^6.9.6", 43 | "supertest": "^6.1.3", 44 | "ts-jest": "^26.5.3" 45 | }, 46 | "volta": { 47 | "node": "14.16.0", 48 | "npm": "7.6.0" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /app/auth/src/app.ts: -------------------------------------------------------------------------------- 1 | import { errorHandler, NotFoundError } from '@webmak/microservices-common'; 2 | import { json } from 'body-parser'; 3 | import cookieSession from 'cookie-session'; 4 | import express, { Request, Response } from 'express'; 5 | import 'express-async-errors'; 6 | import { currentUserRouter } from 'routes/current-user'; 7 | import { metricsRouter } from 'routes/metrics'; 8 | import { signinRouter } from 'routes/signin'; 9 | import { signoutRouter } from 'routes/signout'; 10 | import { signupRouter } from 'routes/signup'; 11 | 12 | const app = express(); 13 | app.set('trust proxy', true); 14 | app.use(json()); 15 | app.use( 16 | cookieSession({ 17 | signed: false, 18 | secure: process.env.NODE_ENV !== 'test', 19 | }) 20 | ); 21 | 22 | app.use(metricsRouter); 23 | app.use(currentUserRouter); 24 | app.use(signinRouter); 25 | app.use(signoutRouter); 26 | app.use(signupRouter); 27 | 28 | app.all('*', async (_req: Request, _res: Response) => { 29 | throw new NotFoundError(); 30 | }); 31 | 32 | app.use(errorHandler); 33 | 34 | export { app }; 35 | -------------------------------------------------------------------------------- /app/auth/src/index.ts: -------------------------------------------------------------------------------- 1 | import { app } from 'app'; 2 | import mongoose from 'mongoose'; 3 | 4 | const start = async () => { 5 | if (!process.env.JWT_KEY) { 6 | throw new Error('[Auth] JWT_KEY must be defined'); 7 | } 8 | 9 | if (!process.env.MONGO_URI) { 10 | throw new Error('[Auth] MONGO_URI must be defined'); 11 | } 12 | 13 | try { 14 | await mongoose.connect(process.env.MONGO_URI, { 15 | useNewUrlParser: true, 16 | useUnifiedTopology: true, 17 | useCreateIndex: true, 18 | }); 19 | console.log('[Auth] Connected to MongoDB'); 20 | } catch (err) { 21 | console.log(err); 22 | } 23 | app.listen(3000, () => { 24 | console.log('[Auth] Listening on port 3000'); 25 | }); 26 | }; 27 | 28 | start(); 29 | -------------------------------------------------------------------------------- /app/auth/src/models/User.ts: -------------------------------------------------------------------------------- 1 | import mongoose from 'mongoose'; 2 | import { Password } from 'services/password'; 3 | 4 | interface IUserAttrs { 5 | email: string; 6 | password: string; 7 | } 8 | 9 | interface IUserModel extends mongoose.Model { 10 | build(attrs: IUserAttrs): IUserDoc; 11 | } 12 | 13 | interface IUserDoc extends mongoose.Document { 14 | email: string; 15 | password: string; 16 | } 17 | 18 | const userSchema = new mongoose.Schema( 19 | { 20 | email: { 21 | type: String, 22 | required: true, 23 | }, 24 | password: { 25 | type: String, 26 | required: true, 27 | }, 28 | }, 29 | { 30 | toJSON: { 31 | transform(doc, ret) { 32 | ret.id = ret._id; 33 | delete ret._id; 34 | delete ret.password; 35 | delete ret.__v; 36 | }, 37 | }, 38 | } 39 | ); 40 | 41 | userSchema.pre('save', async function (done) { 42 | if (this.isModified('password')) { 43 | const hashed = await Password.toHash(this.get('password')); 44 | this.set('password', hashed); 45 | } 46 | done(); 47 | }); 48 | 49 | userSchema.statics.build = (attrs: IUserAttrs) => { 50 | return new User(attrs); 51 | }; 52 | 53 | const User = mongoose.model('User', userSchema); 54 | 55 | export {User}; 56 | -------------------------------------------------------------------------------- /app/auth/src/routes/__test__/current-user.test.ts: -------------------------------------------------------------------------------- 1 | import { app } from 'app'; 2 | import request from 'supertest'; 3 | 4 | it('response with details about the current user', async () => { 5 | const cookie = await global.signin(); 6 | 7 | const response = await request(app) 8 | .get('/api/users/currentuser') 9 | .set('Cookie', cookie) 10 | .send() 11 | .expect(200); 12 | 13 | expect(response.body.currentUser.email).toEqual('test@test.com'); 14 | }); 15 | 16 | it('response with null if not authenticated', async () => { 17 | const response = await request(app) 18 | .get('/api/users/currentuser') 19 | .send() 20 | .expect(200); 21 | 22 | expect(response.body.currentUser).toEqual(null); 23 | }); 24 | -------------------------------------------------------------------------------- /app/auth/src/routes/__test__/signin.test.ts: -------------------------------------------------------------------------------- 1 | import {app} from 'app'; 2 | import request from 'supertest'; 3 | 4 | it('fails when a email that does not exist is supplied', async () => { 5 | await request(app) 6 | .post('/api/users/signin') 7 | .send({ 8 | email: 'test@test.com', 9 | password: 'password', 10 | }) 11 | .expect(400); 12 | }); 13 | 14 | it('fails when an incorrect passord is supplied', async () => { 15 | await request(app) 16 | .post('/api/users/signup') 17 | .send({ 18 | email: 'test@test.com', 19 | password: 'password', 20 | }) 21 | .expect(201); 22 | 23 | await request(app) 24 | .post('/api/users/signin') 25 | .send({ 26 | email: 'test@test.com', 27 | password: 'password1', 28 | }) 29 | .expect(400); 30 | }); 31 | 32 | it('responds with a cookie when given valid credentials', async () => { 33 | await request(app) 34 | .post('/api/users/signup') 35 | .send({ 36 | email: 'test@test.com', 37 | password: 'password', 38 | }) 39 | .expect(201); 40 | 41 | const response = await request(app) 42 | .post('/api/users/signin') 43 | .send({ 44 | email: 'test@test.com', 45 | password: 'password', 46 | }) 47 | .expect(200); 48 | 49 | expect(response.get('Set-Cookie')).toBeDefined(); 50 | }); 51 | -------------------------------------------------------------------------------- /app/auth/src/routes/__test__/signout.test.ts: -------------------------------------------------------------------------------- 1 | import { app } from 'app'; 2 | import request from 'supertest'; 3 | 4 | it('clears the cookie after signing out', async () => { 5 | await request(app) 6 | .post('/api/users/signup') 7 | .send({ 8 | email: 'test@test.com', 9 | password: 'password', 10 | }) 11 | .expect(201); 12 | 13 | const response = await request(app) 14 | .post('/api/users/signout') 15 | .send({}) 16 | .expect(200); 17 | 18 | // console.log(response.get('Set-Cookie')); 19 | 20 | expect(response.get('Set-Cookie')[0]).toEqual( 21 | 'express:sess=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT; httponly' 22 | ); 23 | }); 24 | -------------------------------------------------------------------------------- /app/auth/src/routes/__test__/signup.test.ts: -------------------------------------------------------------------------------- 1 | import {app} from 'app'; 2 | import request from 'supertest'; 3 | 4 | it('returns a 201 on successful signup', async () => { 5 | return request(app) 6 | .post('/api/users/signup') 7 | .send({ 8 | email: 'test@test.com', 9 | password: 'password', 10 | }) 11 | .expect(201); 12 | }); 13 | 14 | it('returns a 400 with an invalid email', async () => { 15 | return request(app) 16 | .post('/api/users/signup') 17 | .send({ 18 | email: 'test', 19 | password: 'password', 20 | }) 21 | .expect(400); 22 | }); 23 | 24 | it('returns a 400 with an invalid password', async () => { 25 | return request(app) 26 | .post('/api/users/signup') 27 | .send({ 28 | email: 'test@test.com', 29 | password: 'p', 30 | }) 31 | .expect(400); 32 | }); 33 | 34 | it('returns a 400 with missing email and password', async () => { 35 | await request(app) 36 | .post('/api/users/signup') 37 | .send({ email: 'test@test.com' }) 38 | .expect(400); 39 | 40 | await request(app) 41 | .post('/api/users/signup') 42 | .send({ 43 | password: 'password', 44 | }) 45 | .expect(400); 46 | }); 47 | 48 | it('disallows duplicate emails', async () => { 49 | await request(app) 50 | .post('/api/users/signup') 51 | .send({ 52 | email: 'test@test.com', 53 | password: 'password', 54 | }) 55 | .expect(201); 56 | 57 | await request(app) 58 | .post('/api/users/signup') 59 | .send({ 60 | email: 'test@test.com', 61 | password: 'password', 62 | }) 63 | .expect(400); 64 | }); 65 | 66 | it('sets a cookie after successful signup', async () => { 67 | const response = await request(app) 68 | .post('/api/users/signup') 69 | .send({ 70 | email: 'test@test.com', 71 | password: 'password', 72 | }) 73 | .expect(201); 74 | 75 | expect(response.get('Set-Cookie')).toBeDefined(); 76 | }); 77 | -------------------------------------------------------------------------------- /app/auth/src/routes/current-user.ts: -------------------------------------------------------------------------------- 1 | import { currentUser } from '@webmak/microservices-common'; 2 | import express from 'express'; 3 | 4 | const router = express.Router(); 5 | 6 | router.get('/api/users/currentuser', currentUser, (req, res) => { 7 | return res.send({ currentUser: req.currentUser || null }); 8 | }); 9 | 10 | export { router as currentUserRouter }; 11 | -------------------------------------------------------------------------------- /app/auth/src/routes/metrics.ts: -------------------------------------------------------------------------------- 1 | import express, { Request, Response } from 'express'; 2 | import * as client from 'prom-client'; 3 | 4 | const router = express.Router(); 5 | 6 | // Create a Registry which registers the metrics 7 | const register = new client.Registry(); 8 | // Add a default label which is added to all metrics 9 | register.setDefaultLabels({ 10 | app: 'microservice-prometheus-auth-app', 11 | }); 12 | // Enable the collection of default metrics 13 | client.collectDefaultMetrics({ register }); 14 | 15 | router.get('/metrics', async (_req: Request, res: Response) => { 16 | res.setHeader('Content-Type', register.contentType); 17 | const result = await register.metrics(); 18 | return res.send(result); 19 | }); 20 | 21 | export { router as metricsRouter }; 22 | -------------------------------------------------------------------------------- /app/auth/src/routes/signin.ts: -------------------------------------------------------------------------------- 1 | import { BadRequestError, validateRequest } from '@webmak/microservices-common'; 2 | import express, { Request, Response } from 'express'; 3 | import { body } from 'express-validator'; 4 | import jwt from 'jsonwebtoken'; 5 | import { User } from 'models/User'; 6 | import { Password } from 'services/password'; 7 | 8 | const router = express.Router(); 9 | 10 | router.post( 11 | '/api/users/signin', 12 | [ 13 | body('email').isEmail().withMessage('Email must be valid'), 14 | body('password') 15 | .trim() 16 | .notEmpty() 17 | .withMessage('You must supply a password'), 18 | ], 19 | validateRequest, 20 | async (req: Request, res: Response) => { 21 | const { email, password } = req.body; 22 | 23 | const existingUser = await User.findOne({ email }); 24 | 25 | if (!existingUser) { 26 | throw new BadRequestError('[Auth] Invalid credentials'); 27 | } 28 | 29 | const passwordsMatch = await Password.compare( 30 | existingUser.password, 31 | password 32 | ); 33 | 34 | if (!passwordsMatch) { 35 | throw new BadRequestError('[Auth] Invalid Credentials'); 36 | } 37 | 38 | const userJwt = jwt.sign( 39 | { 40 | id: existingUser.id, 41 | email: existingUser.email, 42 | }, 43 | process.env.JWT_KEY! 44 | ); 45 | req.session = { 46 | jwt: userJwt, 47 | }; 48 | 49 | return res.status(200).send(existingUser); 50 | } 51 | ); 52 | 53 | export { router as signinRouter }; 54 | -------------------------------------------------------------------------------- /app/auth/src/routes/signout.ts: -------------------------------------------------------------------------------- 1 | import express, { Request, Response } from 'express'; 2 | 3 | const router = express.Router(); 4 | 5 | router.post('/api/users/signout', (req: Request, res: Response) => { 6 | req.session = null; 7 | return res.send({}); 8 | }); 9 | 10 | export {router as signoutRouter}; 11 | -------------------------------------------------------------------------------- /app/auth/src/routes/signup.ts: -------------------------------------------------------------------------------- 1 | import { BadRequestError, validateRequest } from '@webmak/microservices-common'; 2 | import express, { Request, Response } from 'express'; 3 | import { body } from 'express-validator'; 4 | import jwt from 'jsonwebtoken'; 5 | import { User } from 'models/User'; 6 | 7 | const router = express.Router(); 8 | 9 | router.post( 10 | '/api/users/signup', 11 | [ 12 | body('email').isEmail().withMessage('Email must be valid'), 13 | body('password') 14 | .trim() 15 | .isLength({ min: 4, max: 20 }) 16 | .withMessage('Password must be between 4 and 20 characters'), 17 | ], 18 | validateRequest, 19 | async (req: Request, res: Response) => { 20 | const { email, password } = req.body; 21 | 22 | const existingUser = await User.findOne({ email }); 23 | 24 | if (existingUser) { 25 | throw new BadRequestError('[Auth] Email in use'); 26 | } 27 | 28 | const user = User.build({ email, password }); 29 | await user.save(); 30 | 31 | const userJwt = jwt.sign( 32 | { 33 | id: user.id, 34 | email: user.email, 35 | }, 36 | process.env.JWT_KEY! 37 | ); 38 | req.session = { 39 | jwt: userJwt, 40 | }; 41 | 42 | return res.status(201).send(user); 43 | } 44 | ); 45 | 46 | export { router as signupRouter }; 47 | -------------------------------------------------------------------------------- /app/auth/src/services/password.ts: -------------------------------------------------------------------------------- 1 | import { randomBytes, scrypt } from 'crypto'; 2 | import { promisify } from 'util'; 3 | 4 | const scryptAsync = promisify(scrypt); 5 | 6 | export class Password { 7 | static async toHash(password: string) { 8 | const salt = randomBytes(8).toString('hex'); 9 | const buf = (await scryptAsync(password, salt, 64)) as Buffer; 10 | return `${buf.toString('hex')}.${salt}`; 11 | } 12 | 13 | static async compare(storedPassword: string, suppliedPassword: string) { 14 | const [hashedPassword, salt] = storedPassword.split('.'); 15 | const buf = (await scryptAsync(suppliedPassword, salt, 64)) as Buffer; 16 | return buf.toString('hex') === hashedPassword; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /app/auth/src/test/setup.ts: -------------------------------------------------------------------------------- 1 | import { app } from 'app'; 2 | import {MongoMemoryServer} from 'mongodb-memory-server'; 3 | import mongoose from 'mongoose'; 4 | import request from 'supertest'; 5 | 6 | declare global { 7 | namespace NodeJS { 8 | interface Global { 9 | signin(): Promise; 10 | } 11 | } 12 | } 13 | 14 | let mongo: any; 15 | 16 | beforeAll(async () => { 17 | process.env.JWT_KEY = 'MY_JWT_SECRET'; 18 | mongo = new MongoMemoryServer(); 19 | const mongoUri = await mongo.getUri(); 20 | 21 | await mongoose.connect(mongoUri, { 22 | useNewUrlParser: true, 23 | useUnifiedTopology: true, 24 | }); 25 | }); 26 | 27 | beforeEach(async () => { 28 | const collections = await mongoose.connection.db.collections(); 29 | 30 | for (let collection of collections) { 31 | await collection.deleteMany({}); 32 | } 33 | }); 34 | 35 | afterAll(async () => { 36 | await mongo.stop(); 37 | await mongoose.connection.close(); 38 | }); 39 | 40 | global.signin = async () => { 41 | const email = 'test@test.com'; 42 | const password = 'password'; 43 | 44 | const response = await request(app) 45 | .post('/api/users/signup') 46 | .send({ 47 | email, 48 | password, 49 | }) 50 | .expect(201); 51 | 52 | const cookie = response.get('Set-Cookie'); 53 | return cookie; 54 | }; 55 | -------------------------------------------------------------------------------- /app/client/.dockerignore: -------------------------------------------------------------------------------- 1 | **/.eslintignore 2 | **/.eslintrc 3 | **/.prettierignore 4 | **/.prettierrc 5 | 6 | **/node_modules/ 7 | **/node_modules_linux/ 8 | **/Dockerfile 9 | **/.dockerignore 10 | **/.gitignore 11 | **/.git 12 | **/README.md 13 | **/LICENSE 14 | **/.vscode 15 | 16 | **/test/ 17 | **/.next/ -------------------------------------------------------------------------------- /app/client/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:lts-alpine3.13 2 | 3 | WORKDIR /app 4 | COPY package.json ./ 5 | RUN npm install --only=prod 6 | COPY ./ ./ 7 | CMD ["npm", "run", "dev"] -------------------------------------------------------------------------------- /app/client/jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": "src" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /app/client/next.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | webpackDevMiddleware: (config) => { 3 | config.watchOptions.poll = 300; 4 | return config; 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /app/client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "client", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "", 8 | "dev": "next" 9 | }, 10 | "keywords": [], 11 | "author": "", 12 | "license": "ISC", 13 | "dependencies": { 14 | "axios": "^0.21.1", 15 | "bootstrap": "^4.6.0", 16 | "next": "^10.0.8", 17 | "react": "^17.0.1", 18 | "react-dom": "^17.0.1", 19 | "react-stripe-checkout": "^2.6.3" 20 | }, 21 | "volta": { 22 | "node": "14.16.0", 23 | "npm": "7.6.0" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /app/client/src/api/build-client.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | 3 | export default ({ req }) => { 4 | if (typeof window === 'undefined') { 5 | return axios.create({ 6 | baseURL: 'http://192-168-49-2.kubernetes.default.svc.cluster.local', 7 | headers: req.headers, 8 | }); 9 | } else { 10 | return axios.create({ 11 | baseUrl: '/', 12 | }); 13 | } 14 | }; 15 | -------------------------------------------------------------------------------- /app/client/src/components/Header.js: -------------------------------------------------------------------------------- 1 | import Link from 'next/link'; 2 | 3 | export default ({ currentUser }) => { 4 | const links = [ 5 | !currentUser && { label: 'Sign Up', href: '/auth/signup' }, 6 | !currentUser && { label: 'Sign In', href: '/auth/signin' }, 7 | currentUser && { label: 'Sell Tickets', href: '/tickets/new' }, 8 | currentUser && { label: 'My Orders', href: '/orders' }, 9 | currentUser && { label: 'Sign Out', href: '/auth/signout' }, 10 | ] 11 | .filter((linkConfig) => linkConfig) 12 | .map(({ label, href }) => { 13 | return ( 14 |
  • 15 | 16 | {label} 17 | 18 |
  • 19 | ); 20 | }); 21 | 22 | return ( 23 | 32 | ); 33 | }; 34 | -------------------------------------------------------------------------------- /app/client/src/hooks/use-request.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import { useState } from 'react'; 3 | 4 | export default ({ url, method, body, onSuccess }) => { 5 | const [errors, setErrros] = useState(null); 6 | 7 | const doRequest = async (props = {}) => { 8 | try { 9 | setErrros(null); 10 | const response = await axios[method](url, { ...body, ...props }); 11 | 12 | if (onSuccess) { 13 | onSuccess(response.data); 14 | } 15 | return response.data; 16 | } catch (err) { 17 | setErrros( 18 |
    19 |

    Oooops....

    20 |
      21 | {err.response.data.errors.map((err) => ( 22 |
    • {err.message}
    • 23 | ))} 24 |
    25 |
    26 | ); 27 | } 28 | }; 29 | 30 | return { doRequest, errors }; 31 | }; 32 | -------------------------------------------------------------------------------- /app/client/src/pages/_app.js: -------------------------------------------------------------------------------- 1 | import buildClient from 'api/build-client'; 2 | import 'bootstrap/dist/css/bootstrap.css'; 3 | import Header from 'components/Header'; 4 | 5 | const AppComponent = ({ Component, pageProps, currentUser }) => { 6 | return ( 7 |
    8 |
    9 |
    10 | 11 |
    12 |
    13 | ); 14 | }; 15 | 16 | AppComponent.getInitialProps = async (appContext) => { 17 | const client = buildClient(appContext.ctx); 18 | const { data } = await client.get('/api/users/currentuser'); 19 | 20 | let pageProps = {}; 21 | if (appContext.Component.getInitialProps) { 22 | pageProps = await appContext.Component.getInitialProps( 23 | appContext.ctx, 24 | client, 25 | data.currentUser 26 | ); 27 | } 28 | 29 | return { 30 | pageProps, 31 | ...data, 32 | }; 33 | }; 34 | 35 | export default AppComponent; 36 | -------------------------------------------------------------------------------- /app/client/src/pages/auth/signin.js: -------------------------------------------------------------------------------- 1 | import useRequest from 'hooks/use-request'; 2 | import Router from 'next/router'; 3 | import { useState } from 'react'; 4 | 5 | export default () => { 6 | const [email, setEmail] = useState(''); 7 | const [password, setPassword] = useState(''); 8 | const { doRequest, errors } = useRequest({ 9 | url: '/api/users/signin', 10 | method: 'post', 11 | body: { 12 | email, 13 | password, 14 | }, 15 | onSuccess: () => Router.push('/'), 16 | }); 17 | 18 | const onSubmit = async (event) => { 19 | event.preventDefault(); 20 | await doRequest(); 21 | }; 22 | 23 | return ( 24 |
    25 |
    26 |

    Sign In

    27 |
    28 | 29 | setEmail(e.target.value)} 33 | > 34 |
    35 |
    36 | 37 | setPassword(e.target.value)} 42 | > 43 |
    44 | 45 | {errors} 46 | 47 |
    48 |
    49 | ); 50 | }; 51 | -------------------------------------------------------------------------------- /app/client/src/pages/auth/signout.js: -------------------------------------------------------------------------------- 1 | import useRequest from 'hooks/use-request'; 2 | import Router from 'next/router'; 3 | import { useEffect } from 'react'; 4 | 5 | export default () => { 6 | const { doRequest } = useRequest({ 7 | url: '/api/users/signout', 8 | method: 'post', 9 | body: {}, 10 | onSuccess: () => Router.push('/'), 11 | }); 12 | 13 | useEffect(() => { 14 | doRequest(); 15 | }, []); 16 | return
    Signing you out ...
    ; 17 | }; 18 | -------------------------------------------------------------------------------- /app/client/src/pages/auth/signup.js: -------------------------------------------------------------------------------- 1 | import useRequest from 'hooks/use-request'; 2 | import Router from 'next/router'; 3 | import { useState } from 'react'; 4 | 5 | export default () => { 6 | const [email, setEmail] = useState(''); 7 | const [password, setPassword] = useState(''); 8 | const { doRequest, errors } = useRequest({ 9 | url: '/api/users/signup', 10 | method: 'post', 11 | body: { 12 | email, 13 | password, 14 | }, 15 | onSuccess: () => Router.push('/'), 16 | }); 17 | 18 | const onSubmit = async (event) => { 19 | event.preventDefault(); 20 | await doRequest(); 21 | }; 22 | 23 | return ( 24 |
    25 |
    26 |

    Sign Up

    27 |
    28 | 29 | setEmail(e.target.value)} 33 | > 34 |
    35 |
    36 | 37 | setPassword(e.target.value)} 42 | > 43 |
    44 | 45 | {errors} 46 | 47 |
    48 |
    49 | ); 50 | }; 51 | -------------------------------------------------------------------------------- /app/client/src/pages/index.js: -------------------------------------------------------------------------------- 1 | import Link from 'next/link'; 2 | 3 | const LandingPage = ({ currentUser, tickets }) => { 4 | const ticketList = tickets.map((ticket) => { 5 | return ( 6 | 7 | {ticket.title} 8 | {ticket.price} 9 | 10 | 11 | View 12 | 13 | 14 | 15 | ); 16 | }); 17 | 18 | return ( 19 |
    20 |

    Tickets

    21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | {ticketList} 30 |
    TitlePriceLink
    31 |
    32 | ); 33 | }; 34 | 35 | LandingPage.getInitialProps = async (context, client, currentUser) => { 36 | const { data } = await client.get('/api/tickets'); 37 | 38 | return { tickets: data }; 39 | }; 40 | 41 | export default LandingPage; 42 | -------------------------------------------------------------------------------- /app/client/src/pages/orders/[orderId].js: -------------------------------------------------------------------------------- 1 | import useRequest from 'hooks/use-request'; 2 | import Router from 'next/router'; 3 | import { useEffect, useState } from 'react'; 4 | import StripeCheckout from 'react-stripe-checkout'; 5 | 6 | const OrderShow = ({ order, currentUser }) => { 7 | const [timeLeft, setTimeLeft] = useState(0); 8 | 9 | const { doRequest, errors } = useRequest({ 10 | url: '/api/payments', 11 | method: 'post', 12 | body: { 13 | orderId: order.id, 14 | }, 15 | onSuccess: () => Router.push('/orders'), 16 | }); 17 | 18 | useEffect(() => { 19 | const findTimeLeft = () => { 20 | const msLeft = new Date(order.expiresAt) - new Date(); 21 | setTimeLeft(Math.round(msLeft / 1000)); 22 | }; 23 | 24 | findTimeLeft(); 25 | const timerId = setInterval(findTimeLeft, 1000); 26 | return () => { 27 | clearInterval(timerId); 28 | }; 29 | }, [order]); 30 | 31 | if (timeLeft < 0) { 32 | return
    Order Expired
    ; 33 | } 34 | 35 | return ( 36 |
    37 | Time left to pay: {timeLeft} seconds
    38 | doRequest({ token: id })} 40 | stripeKey="pk_test_LQWgtToPhlyDhBDmdn9ULxmn00WVghorjW" 41 | amount={order.ticket.price * 100} 42 | email={currentUser.email} 43 | /> 44 | {errors} 45 |
    46 | ); 47 | }; 48 | 49 | OrderShow.getInitialProps = async (context, client) => { 50 | const { orderId } = context.query; 51 | const { data } = await client.get(`/api/orders/${orderId}`); 52 | 53 | return { order: data }; 54 | }; 55 | 56 | export default OrderShow; 57 | -------------------------------------------------------------------------------- /app/client/src/pages/orders/index.js: -------------------------------------------------------------------------------- 1 | const OrderIndex = ({ orders }) => { 2 | return ( 3 |
      4 | {orders.map((order) => { 5 | return ( 6 |
    • 7 | {order.ticket.title} - {order.status} 8 |
    • 9 | ); 10 | })} 11 |
    12 | ); 13 | }; 14 | 15 | OrderIndex.getInitialProps = async (context, client) => { 16 | const { data } = await client.get('/api/orders'); 17 | return { orders: data }; 18 | }; 19 | 20 | export default OrderIndex; 21 | -------------------------------------------------------------------------------- /app/client/src/pages/tickets/[ticketId].js: -------------------------------------------------------------------------------- 1 | import useRequest from 'hooks/use-request'; 2 | import Router from 'next/router'; 3 | 4 | const TicketShow = ({ ticket }) => { 5 | const { doRequest, errors } = useRequest({ 6 | url: '/api/orders', 7 | method: 'post', 8 | body: { 9 | ticketId: ticket.id, 10 | }, 11 | onSuccess: (order) => 12 | Router.push('/orders/[orderId]', `/orders/${order.id}`), 13 | }); 14 | 15 | return ( 16 |
    17 |

    {ticket.title}

    18 |

    Price: {ticket.price}

    19 | {errors} 20 | 23 |
    24 | ); 25 | }; 26 | 27 | TicketShow.getInitialProps = async (context, client) => { 28 | const { ticketId } = context.query; 29 | const { data } = await client.get(`/api/tickets/${ticketId}`); 30 | 31 | return { ticket: data }; 32 | }; 33 | 34 | export default TicketShow; 35 | -------------------------------------------------------------------------------- /app/client/src/pages/tickets/new.js: -------------------------------------------------------------------------------- 1 | import useRequest from 'hooks/use-request'; 2 | import Router from 'next/router'; 3 | import { useState } from 'react'; 4 | 5 | export const NewTicket = () => { 6 | const [title, setTitle] = useState(''); 7 | const [price, setPrice] = useState(''); 8 | const { doRequest, errors } = useRequest({ 9 | url: '/api/tickets', 10 | method: 'post', 11 | body: { 12 | title, 13 | price, 14 | }, 15 | onSuccess: () => Router.push('/'), 16 | }); 17 | 18 | const onSubmit = (event) => { 19 | event.preventDefault(); 20 | doRequest(); 21 | }; 22 | 23 | const onBlur = () => { 24 | const value = parseFloat(price); 25 | 26 | if (isNaN(value)) { 27 | return; 28 | } 29 | 30 | setPrice(value.toFixed(2)); 31 | }; 32 | 33 | return ( 34 |
    35 |

    Create a Ticket

    36 |
    37 |
    38 | 39 | setTitle(e.target.value)} 43 | /> 44 |
    45 |
    46 | 47 | setPrice(e.target.value)} 51 | onBlur={onBlur} 52 | /> 53 |
    54 | {errors} 55 | 56 |
    57 |
    58 | ); 59 | }; 60 | 61 | export default NewTicket; 62 | -------------------------------------------------------------------------------- /app/common/.gitignore: -------------------------------------------------------------------------------- 1 | build/ 2 | node_modules/ -------------------------------------------------------------------------------- /app/common/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@webmak/microservices-common", 3 | "version": "1.0.6", 4 | "description": "microservices-common", 5 | "main": "./build/index.js", 6 | "types": "./build/index.d.ts", 7 | "files": [ 8 | "build/**/*" 9 | ], 10 | "scripts": { 11 | "clean": "del ./build/*", 12 | "build": "npm run clean && ttsc" 13 | }, 14 | "keywords": [], 15 | "author": "", 16 | "repository": { 17 | "type": "git", 18 | "url": "git+https://github.com/webmakaka/Microservices-with-Node-JS-and-React-Improved.git" 19 | }, 20 | "license": "ISC", 21 | "devDependencies": { 22 | "del-cli": "^3.0.1", 23 | "typescript": "^4.2.3" 24 | }, 25 | "dependencies": { 26 | "@types/cookie-session": "^2.0.42", 27 | "@types/express": "^4.17.11", 28 | "@types/jsonwebtoken": "^8.5.0", 29 | "@zerollup/ts-transform-paths": "^1.7.18", 30 | "cookie-session": "^1.4.0", 31 | "express": "^4.17.1", 32 | "express-validator": "^6.10.0", 33 | "jsonwebtoken": "^8.5.1", 34 | "node-nats-streaming": "^0.3.2", 35 | "ttypescript": "^1.5.12" 36 | }, 37 | "volta": { 38 | "node": "14.16.0", 39 | "npm": "7.6.0" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /app/common/src/errors/bad-request-error.ts: -------------------------------------------------------------------------------- 1 | import { CustomError } from 'errors/custom-error'; 2 | 3 | export class BadRequestError extends CustomError { 4 | statusCode = 400; 5 | 6 | constructor(public message: string) { 7 | super(message); 8 | 9 | Object.setPrototypeOf(this, BadRequestError.prototype); 10 | } 11 | 12 | serializeErrors() { 13 | return [{ message: this.message }]; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /app/common/src/errors/custom-error.ts: -------------------------------------------------------------------------------- 1 | export abstract class CustomError extends Error { 2 | abstract statusCode: number; 3 | 4 | constructor(message: string) { 5 | super(message); 6 | Object.setPrototypeOf(this, CustomError.prototype); 7 | } 8 | 9 | abstract serializeErrors(): { message: string; field?: string }[]; 10 | } 11 | -------------------------------------------------------------------------------- /app/common/src/errors/database-connection-error.ts: -------------------------------------------------------------------------------- 1 | import { CustomError } from 'errors/custom-error'; 2 | 3 | export class DatabaseConnectionError extends CustomError { 4 | statusCode = 500; 5 | reason = 'Error connecting to database'; 6 | 7 | constructor() { 8 | super('Error connecting to db'); 9 | 10 | Object.setPrototypeOf(this, DatabaseConnectionError.prototype); 11 | } 12 | 13 | serializeErrors() { 14 | return [{ message: this.reason }]; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /app/common/src/errors/not-authorized-error.ts: -------------------------------------------------------------------------------- 1 | import { CustomError } from 'errors/custom-error'; 2 | 3 | export class NotAuthorizedError extends CustomError { 4 | statusCode = 401; 5 | 6 | constructor() { 7 | super('Not authorized'); 8 | 9 | Object.setPrototypeOf(this, NotAuthorizedError.prototype); 10 | } 11 | 12 | serializeErrors() { 13 | return [ 14 | { 15 | message: 'Not authorized', 16 | }, 17 | ]; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /app/common/src/errors/not-found-error.ts: -------------------------------------------------------------------------------- 1 | import { CustomError } from 'errors/custom-error'; 2 | 3 | export class NotFoundError extends CustomError { 4 | statusCode = 404; 5 | 6 | constructor() { 7 | super('Route not found'); 8 | 9 | Object.setPrototypeOf(this, NotFoundError.prototype); 10 | } 11 | 12 | serializeErrors() { 13 | return [{ message: 'Not Found' }]; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /app/common/src/errors/request-validation-error.ts: -------------------------------------------------------------------------------- 1 | import { CustomError } from 'errors/custom-error'; 2 | import { ValidationError } from 'express-validator'; 3 | 4 | export class RequestValidationError extends CustomError { 5 | statusCode = 400; 6 | 7 | constructor(private errors: ValidationError[]) { 8 | super('Invalid request parameters'); 9 | 10 | Object.setPrototypeOf(this, RequestValidationError.prototype); 11 | } 12 | 13 | serializeErrors() { 14 | return this.errors.map((err) => { 15 | return { message: err.msg, field: err.param }; 16 | }); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /app/common/src/events/AListener.ts: -------------------------------------------------------------------------------- 1 | import { ESubjects } from 'events/ESubjects'; 2 | import { Message, Stan } from 'node-nats-streaming'; 3 | 4 | interface IEvent { 5 | subject: ESubjects; 6 | data: any; 7 | } 8 | 9 | export abstract class AListener { 10 | abstract subject: T['subject']; 11 | abstract queueGroupName: string; 12 | abstract onMessage(data: T['data'], msg: Message): void; 13 | 14 | protected client: Stan; 15 | protected ackWait = 5 * 1000; 16 | 17 | constructor(client: Stan) { 18 | this.client = client; 19 | } 20 | 21 | subscriptionOptions() { 22 | return this.client 23 | .subscriptionOptions() 24 | .setDeliverAllAvailable() 25 | .setManualAckMode(true) 26 | .setAckWait(this.ackWait) 27 | .setDurableName(this.queueGroupName); 28 | } 29 | 30 | listen() { 31 | const subscription = this.client.subscribe( 32 | this.subject, 33 | this.queueGroupName, 34 | this.subscriptionOptions() 35 | ); 36 | 37 | subscription.on('message', (msg: Message) => { 38 | console.log(`Message received: ${this.subject} / ${this.queueGroupName}`); 39 | 40 | const parsedData = this.parseMessage(msg); 41 | this.onMessage(parsedData, msg); 42 | }); 43 | } 44 | 45 | parseMessage(msg: Message) { 46 | const data = msg.getData(); 47 | return typeof data === 'string' 48 | ? JSON.parse(data) 49 | : JSON.parse(data.toString('utf8')); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /app/common/src/events/APublisher.ts: -------------------------------------------------------------------------------- 1 | import { ESubjects } from 'events/ESubjects'; 2 | import { Stan } from 'node-nats-streaming'; 3 | 4 | interface IEvent { 5 | subject: ESubjects; 6 | data: any; 7 | } 8 | 9 | export abstract class APublisher { 10 | abstract subject: T['subject']; 11 | protected client: Stan; 12 | 13 | constructor(client: Stan) { 14 | this.client = client; 15 | } 16 | 17 | publish(data: T['data']): Promise { 18 | return new Promise((resolve, reject) => { 19 | this.client.publish(this.subject, JSON.stringify(data), (err: any) => { 20 | if (err) { 21 | return reject(err); 22 | } 23 | console.log('Event published to subject', this.subject); 24 | resolve(); 25 | }); 26 | }); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /app/common/src/events/ESubjects.ts: -------------------------------------------------------------------------------- 1 | export enum ESubjects { 2 | TicketCreated = 'ticket:created', 3 | TicketUpdated = 'ticket:updated', 4 | 5 | OrderCreated = 'order:created', 6 | OrderCancelled = 'order:cancelled', 7 | 8 | ExpirationComplete = 'expiration:complete', 9 | 10 | PaymentCreated = 'payment:created', 11 | } 12 | -------------------------------------------------------------------------------- /app/common/src/events/IExpirationCompleteEvent.ts: -------------------------------------------------------------------------------- 1 | import { ESubjects } from 'events/ESubjects'; 2 | 3 | export interface IExpirationCompleteEvent { 4 | subject: ESubjects.ExpirationComplete; 5 | data: { 6 | orderId: string; 7 | }; 8 | } 9 | -------------------------------------------------------------------------------- /app/common/src/events/IOrderCancelledEvent.ts: -------------------------------------------------------------------------------- 1 | import { ESubjects } from 'events/ESubjects'; 2 | 3 | export interface IOrderCancelledEvent { 4 | subject: ESubjects.OrderCancelled; 5 | data: { 6 | id: string; 7 | version: number; 8 | ticket: { 9 | id: string; 10 | }; 11 | }; 12 | } 13 | -------------------------------------------------------------------------------- /app/common/src/events/IOrderCreatedEvent.ts: -------------------------------------------------------------------------------- 1 | import { ESubjects } from 'events/ESubjects'; 2 | import { EOrderStatus } from 'events/types/EOrderStatus'; 3 | 4 | export interface IOrderCreatedEvent { 5 | subject: ESubjects.OrderCreated; 6 | data: { 7 | id: string; 8 | version: number; 9 | status: EOrderStatus; 10 | userId: string; 11 | expiresAt: string; 12 | ticket: { 13 | id: string; 14 | price: number; 15 | }; 16 | }; 17 | } 18 | -------------------------------------------------------------------------------- /app/common/src/events/IPaymentCreatedEvent.ts: -------------------------------------------------------------------------------- 1 | import { ESubjects } from 'events/ESubjects'; 2 | 3 | export interface IPaymentCreatedEvent { 4 | subject: ESubjects.PaymentCreated; 5 | data: { 6 | id: string; 7 | orderId: string; 8 | stripeId: string; 9 | }; 10 | } 11 | -------------------------------------------------------------------------------- /app/common/src/events/ITicketCreatedEvent.ts: -------------------------------------------------------------------------------- 1 | import { ESubjects } from 'events/ESubjects'; 2 | 3 | export interface ITicketCreatedEvent { 4 | subject: ESubjects.TicketCreated; 5 | data: { 6 | id: string; 7 | version: number; 8 | title: string; 9 | price: number; 10 | userId: string; 11 | }; 12 | } 13 | -------------------------------------------------------------------------------- /app/common/src/events/ITicketUpdatedEvent.ts: -------------------------------------------------------------------------------- 1 | import { ESubjects } from 'events/ESubjects'; 2 | 3 | export interface ITicketUpdatedEvent { 4 | subject: ESubjects.TicketUpdated; 5 | data: { 6 | id: string; 7 | version: number; 8 | title: string; 9 | price: number; 10 | userId: string; 11 | orderId?: string; 12 | }; 13 | } 14 | -------------------------------------------------------------------------------- /app/common/src/events/types/EOrderStatus.ts: -------------------------------------------------------------------------------- 1 | export enum EOrderStatus { 2 | Created = 'created', 3 | Cancelled = 'cancelled', 4 | AwaitingPayment = 'awaiting:payment', 5 | Complete = 'complete', 6 | } 7 | -------------------------------------------------------------------------------- /app/common/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from 'errors/bad-request-error'; 2 | export * from 'errors/custom-error'; 3 | export * from 'errors/database-connection-error'; 4 | export * from 'errors/not-authorized-error'; 5 | export * from 'errors/not-found-error'; 6 | export * from 'errors/request-validation-error'; 7 | export * from 'events/AListener'; 8 | export * from 'events/APublisher'; 9 | export * from 'events/ESubjects'; 10 | export * from 'events/IExpirationCompleteEvent'; 11 | export * from 'events/IOrderCancelledEvent'; 12 | export * from 'events/IOrderCreatedEvent'; 13 | export * from 'events/IPaymentCreatedEvent'; 14 | export * from 'events/ITicketCreatedEvent'; 15 | export * from 'events/ITicketUpdatedEvent'; 16 | export * from 'events/types/EOrderStatus'; 17 | export * from 'middlewares/current-user'; 18 | export * from 'middlewares/error-handler'; 19 | export * from 'middlewares/require-auth'; 20 | export * from 'middlewares/validate-request'; 21 | -------------------------------------------------------------------------------- /app/common/src/middlewares/current-user.ts: -------------------------------------------------------------------------------- 1 | import { NextFunction, Request, Response } from 'express'; 2 | import * as jwt from 'jsonwebtoken'; 3 | 4 | interface UserPayload { 5 | id: string; 6 | email: string; 7 | } 8 | 9 | declare global { 10 | namespace Express { 11 | interface Request { 12 | currentUser?: UserPayload; 13 | } 14 | } 15 | } 16 | 17 | export const currentUser = ( 18 | req: Request, 19 | _res: Response, 20 | next: NextFunction 21 | ) => { 22 | if (!req.session?.jwt) { 23 | return next(); 24 | } 25 | 26 | try { 27 | const payload = jwt.verify( 28 | req.session.jwt, 29 | process.env.JWT_KEY! 30 | ) as UserPayload; 31 | req.currentUser = payload; 32 | } catch (err) {} 33 | 34 | next(); 35 | }; 36 | -------------------------------------------------------------------------------- /app/common/src/middlewares/error-handler.ts: -------------------------------------------------------------------------------- 1 | import { CustomError } from 'errors/custom-error'; 2 | import { NextFunction, Request, Response } from 'express'; 3 | 4 | export const errorHandler = ( 5 | err: Error, 6 | _req: Request, 7 | res: Response, 8 | _next: NextFunction 9 | ) => { 10 | if (err instanceof CustomError) { 11 | return res.status(err.statusCode).send({ errors: err.serializeErrors() }); 12 | } 13 | 14 | return res.status(400).send({ 15 | errors: [{ message: 'Something went wrong' }], 16 | }); 17 | }; 18 | -------------------------------------------------------------------------------- /app/common/src/middlewares/require-auth.ts: -------------------------------------------------------------------------------- 1 | import { NotAuthorizedError } from 'errors/not-authorized-error'; 2 | import { NextFunction, Request, Response } from 'express'; 3 | 4 | export const requireAuth = ( 5 | req: Request, 6 | _res: Response, 7 | next: NextFunction 8 | ) => { 9 | if (!req.currentUser) { 10 | throw new NotAuthorizedError(); 11 | } 12 | 13 | next(); 14 | }; 15 | -------------------------------------------------------------------------------- /app/common/src/middlewares/validate-request.ts: -------------------------------------------------------------------------------- 1 | import { RequestValidationError } from 'errors/request-validation-error'; 2 | import { NextFunction, Request, Response } from 'express'; 3 | import { validationResult } from 'express-validator'; 4 | 5 | export const validateRequest = ( 6 | req: Request, 7 | res: Response, 8 | next: NextFunction 9 | ) => { 10 | const errors = validationResult(req); 11 | 12 | if (!errors.isEmpty()) { 13 | throw new RequestValidationError(errors.array()); 14 | } 15 | 16 | next(); 17 | }; 18 | -------------------------------------------------------------------------------- /app/expiration/.dockerignore: -------------------------------------------------------------------------------- 1 | **/.eslintignore 2 | **/.eslintrc 3 | **/.prettierignore 4 | **/.prettierrc 5 | 6 | **/node_modules/ 7 | **/node_modules_linux/ 8 | **/Dockerfile 9 | **/.dockerignore 10 | **/.gitignore 11 | **/.git 12 | **/README.md 13 | **/LICENSE 14 | **/.vscode 15 | 16 | **/test/ 17 | **/.next/ -------------------------------------------------------------------------------- /app/expiration/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:lts-alpine3.13 2 | 3 | WORKDIR /app 4 | COPY package.json ./ 5 | RUN npm install --only=prod 6 | COPY ./ ./ 7 | CMD ["npm", "start"] -------------------------------------------------------------------------------- /app/expiration/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "expiration", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "NODE_PATH=./src ts-node-dev src/index.ts", 8 | "test": "NODE_PATH=./src jest --watchAll --no-cache" 9 | }, 10 | "jest": { 11 | "preset": "ts-jest", 12 | "testEnvironment": "node", 13 | "setupFilesAfterEnv": [ 14 | "./src/test/setup.ts" 15 | ] 16 | }, 17 | "keywords": [], 18 | "author": "", 19 | "license": "ISC", 20 | "dependencies": { 21 | "@types/bull": "^3.15.0", 22 | "@webmak/microservices-common": "^1.0.6", 23 | "bull": "^3.20.1", 24 | "node-nats-streaming": "^0.3.2", 25 | "ts-node-dev": "^1.1.6", 26 | "typescript": "^4.2.3" 27 | }, 28 | "devDependencies": { 29 | "@types/jest": "^26.0.20", 30 | "jest": "^26.6.3", 31 | "ts-jest": "^26.5.3" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /app/expiration/src/NatsWrapper.ts: -------------------------------------------------------------------------------- 1 | import nats, { Stan } from 'node-nats-streaming'; 2 | 3 | class NatsWrapper { 4 | private _client?: Stan; 5 | 6 | get client() { 7 | if (!this._client) { 8 | throw new Error( 9 | '[Expiration] Cannot access NATS client before connecting' 10 | ); 11 | } 12 | 13 | return this._client; 14 | } 15 | 16 | connect(clusterId: string, clientId: string, url: string) { 17 | this._client = nats.connect(clusterId, clientId, { url }); 18 | 19 | return new Promise((resolve, reject) => { 20 | this.client.on('connect', () => { 21 | console.log('[Expiration] Connected to NATS'); 22 | resolve('OK!'); 23 | }); 24 | 25 | this.client.on('error', (err) => { 26 | reject(err); 27 | }); 28 | }); 29 | } 30 | } 31 | 32 | export const natsWrapper = new NatsWrapper(); 33 | -------------------------------------------------------------------------------- /app/expiration/src/__mocks__/NatsWrapper.ts: -------------------------------------------------------------------------------- 1 | export const natsWrapper = { 2 | client: { 3 | publish: jest 4 | .fn() 5 | .mockImplementationOnce( 6 | (subject: string, data: string, callback: () => void) => { 7 | callback(); 8 | } 9 | ), 10 | }, 11 | }; 12 | -------------------------------------------------------------------------------- /app/expiration/src/events/listeneres/OrderCreatedListener.ts: -------------------------------------------------------------------------------- 1 | import { 2 | AListener, 3 | ESubjects, 4 | IOrderCreatedEvent, 5 | } from '@webmak/microservices-common'; 6 | import { queueGroupName } from 'events/listeneres/queueGroupName'; 7 | import { Message } from 'node-nats-streaming'; 8 | import { expirationQueue } from 'queues/expiration-queue'; 9 | 10 | export class OrderCreatedListener extends AListener { 11 | subject: ESubjects.OrderCreated = ESubjects.OrderCreated; 12 | queueGroupName = queueGroupName; 13 | 14 | async onMessage(data: IOrderCreatedEvent['data'], msg: Message) { 15 | const delay = new Date(data.expiresAt).getTime() - new Date().getTime(); 16 | 17 | await expirationQueue.add( 18 | { 19 | orderId: data.id, 20 | }, 21 | { 22 | delay, 23 | } 24 | ); 25 | 26 | msg.ack(); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /app/expiration/src/events/listeneres/queueGroupName.ts: -------------------------------------------------------------------------------- 1 | export const queueGroupName = 'expiration-service'; 2 | -------------------------------------------------------------------------------- /app/expiration/src/events/publishers/ExpirationCompletePublisher.ts: -------------------------------------------------------------------------------- 1 | import { 2 | APublisher, 3 | ESubjects, 4 | IExpirationCompleteEvent, 5 | } from '@webmak/microservices-common'; 6 | 7 | export class ExpirationCompletePublisher extends APublisher { 8 | subject: ESubjects.ExpirationComplete = ESubjects.ExpirationComplete; 9 | } 10 | -------------------------------------------------------------------------------- /app/expiration/src/index.ts: -------------------------------------------------------------------------------- 1 | import { OrderCreatedListener } from 'events/listeneres/OrderCreatedListener'; 2 | import { natsWrapper } from 'NatsWrapper'; 3 | 4 | const start = async () => { 5 | if (!process.env.NATS_CLUSTER_ID) { 6 | throw new Error('[Expiration] NATS_CLUSTER_ID must be defined'); 7 | } 8 | 9 | if (!process.env.NATS_CLIENT_ID) { 10 | throw new Error('[Expiration] NATS_CLIENT_ID must be defined'); 11 | } 12 | 13 | if (!process.env.NATS_URL) { 14 | throw new Error('[Expiration] NATS_URL must be defined'); 15 | } 16 | 17 | try { 18 | await natsWrapper.connect( 19 | process.env.NATS_CLUSTER_ID, 20 | process.env.NATS_CLIENT_ID, 21 | process.env.NATS_URL 22 | ); 23 | 24 | natsWrapper.client.on('close', () => { 25 | console.log('[Expiration] NATS connection closed!'); 26 | process.exit(); 27 | }); 28 | 29 | process.on('SIGINT', () => natsWrapper.client.close()); 30 | process.on('SIGTERM', () => natsWrapper.client.close()); 31 | 32 | new OrderCreatedListener(natsWrapper.client).listen(); 33 | } catch (err) { 34 | console.log(err); 35 | } 36 | }; 37 | 38 | start(); 39 | -------------------------------------------------------------------------------- /app/expiration/src/queues/expiration-queue.ts: -------------------------------------------------------------------------------- 1 | import Queue from 'bull'; 2 | import { ExpirationCompletePublisher } from 'events/publishers/ExpirationCompletePublisher'; 3 | import { natsWrapper } from 'NatsWrapper'; 4 | interface Payload { 5 | orderId: string; 6 | } 7 | 8 | const expirationQueue = new Queue('order:expiration', { 9 | redis: { 10 | host: process.env.REDIS_HOST, 11 | }, 12 | }); 13 | 14 | expirationQueue.process(async (job) => { 15 | new ExpirationCompletePublisher(natsWrapper.client).publish({ 16 | orderId: job.data.orderId, 17 | }); 18 | }); 19 | 20 | export { expirationQueue }; 21 | -------------------------------------------------------------------------------- /app/nats-test/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nats-test", 3 | "version": "1.0.1", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "publish": "NODE_PATH=./src ts-node-dev --notify false src/publisher.ts", 8 | "listen": "NODE_PATH=./src ts-node-dev --notify false src/listener.ts" 9 | }, 10 | "keywords": [], 11 | "author": "", 12 | "license": "ISC", 13 | "dependencies": { 14 | "@types/node": "^14.14.34", 15 | "@webmak/microservices-common": "^1.0.6", 16 | "node-nats-streaming": "^0.3.2", 17 | "ts-node-dev": "^1.1.6", 18 | "typescript": "^4.2.3" 19 | }, 20 | "volta": { 21 | "node": "14.16.0", 22 | "npm": "7.6.0" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /app/nats-test/src/events/TicketCreatedListener.ts: -------------------------------------------------------------------------------- 1 | import { 2 | AListener, 3 | ESubjects, 4 | ITicketCreatedEvent, 5 | } from '@webmak/microservices-common'; 6 | import { Message } from 'node-nats-streaming'; 7 | 8 | export class TicketCreatedListener extends AListener { 9 | subject: ESubjects.TicketCreated = ESubjects.TicketCreated; 10 | queueGroupName = 'payments-service'; 11 | 12 | onMessage(data: ITicketCreatedEvent['data'], msg: Message) { 13 | console.log('Event data! ', data); 14 | msg.ack(); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /app/nats-test/src/events/TicketCreatedPublisher.ts: -------------------------------------------------------------------------------- 1 | import { 2 | APublisher, 3 | ESubjects, 4 | ITicketCreatedEvent, 5 | } from '@webmak/microservices-common'; 6 | 7 | export class TicketCreatedPublisher extends APublisher { 8 | subject: ESubjects.TicketCreated = ESubjects.TicketCreated; 9 | } 10 | -------------------------------------------------------------------------------- /app/nats-test/src/listener.ts: -------------------------------------------------------------------------------- 1 | import { randomBytes } from 'crypto'; 2 | import { TicketCreatedListener } from 'events/TicketCreatedListener'; 3 | import nats from 'node-nats-streaming'; 4 | 5 | console.clear(); 6 | 7 | const stan = nats.connect('ticketing', randomBytes(4).toString('hex'), { 8 | url: 'http://localhost:4222', 9 | }); 10 | 11 | stan.on('connect', () => { 12 | console.log('Listener connected to NATS'); 13 | 14 | stan.on('close', () => { 15 | console.log('NATS connection closed!'); 16 | process.exit(); 17 | }); 18 | new TicketCreatedListener(stan).listen(); 19 | }); 20 | 21 | process.on('SIGINT', () => stan.close()); 22 | process.on('SIGTERM', () => stan.close()); 23 | -------------------------------------------------------------------------------- /app/nats-test/src/publisher.ts: -------------------------------------------------------------------------------- 1 | import { TicketCreatedPublisher } from 'events/TicketCreatedPublisher'; 2 | import nats from 'node-nats-streaming'; 3 | 4 | console.clear(); 5 | 6 | const stan = nats.connect('ticketing', 'abc', { 7 | url: 'http://localhost:4222', 8 | }); 9 | 10 | stan.on('connect', async () => { 11 | console.log('Publisher connected to NATS'); 12 | const publisher = new TicketCreatedPublisher(stan); 13 | try { 14 | await publisher.publish({ 15 | id: '123', 16 | title: 'concert', 17 | price: 20, 18 | userId: '321', 19 | }); 20 | } catch (err) { 21 | console.error(err); 22 | } 23 | }); 24 | -------------------------------------------------------------------------------- /app/nats-test/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Basic Options */ 4 | // "incremental": true, /* Enable incremental compilation */ 5 | "target": "es5" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */, 6 | "module": "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */, 7 | // "lib": [], /* Specify library files to be included in the compilation. */ 8 | // "allowJs": true, /* Allow javascript files to be compiled. */ 9 | // "checkJs": true, /* Report errors in .js files. */ 10 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ 11 | // "declaration": true, /* Generates corresponding '.d.ts' file. */ 12 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ 13 | // "sourceMap": true, /* Generates corresponding '.map' file. */ 14 | // "outFile": "./", /* Concatenate and emit output to single file. */ 15 | // "outDir": "./", /* Redirect output structure to the directory. */ 16 | // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 17 | // "composite": true, /* Enable project compilation */ 18 | // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ 19 | // "removeComments": true, /* Do not emit comments to output. */ 20 | // "noEmit": true, /* Do not emit outputs. */ 21 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 22 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 23 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 24 | 25 | /* Strict Type-Checking Options */ 26 | "strict": true /* Enable all strict type-checking options. */, 27 | // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ 28 | // "strictNullChecks": true, /* Enable strict null checks. */ 29 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */ 30 | // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ 31 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ 32 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 33 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 34 | 35 | /* Additional Checks */ 36 | // "noUnusedLocals": true, /* Report errors on unused locals. */ 37 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 38 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 39 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 40 | 41 | /* Module Resolution Options */ 42 | // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 43 | "baseUrl": "./src", 44 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 45 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 46 | // "typeRoots": [], /* List of folders to include type definitions from. */ 47 | // "types": [], /* Type declaration files to be included in compilation. */ 48 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 49 | "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */, 50 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 51 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 52 | 53 | /* Source Map Options */ 54 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 55 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 56 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 57 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 58 | 59 | /* Experimental Options */ 60 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 61 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 62 | 63 | /* Advanced Options */ 64 | "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */ 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /app/orders/.dockerignore: -------------------------------------------------------------------------------- 1 | **/.eslintignore 2 | **/.eslintrc 3 | **/.prettierignore 4 | **/.prettierrc 5 | 6 | **/node_modules/ 7 | **/node_modules_linux/ 8 | **/Dockerfile 9 | **/.dockerignore 10 | **/.gitignore 11 | **/.git 12 | **/README.md 13 | **/LICENSE 14 | **/.vscode 15 | 16 | **/test/ 17 | **/.next/ -------------------------------------------------------------------------------- /app/orders/.eslintignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | -------------------------------------------------------------------------------- /app/orders/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es2021": true 5 | }, 6 | "extends": [ 7 | "eslint:recommended", 8 | "plugin:@typescript-eslint/recommended", 9 | "plugin:@typescript-eslint/recommended-requiring-type-checking" 10 | ], 11 | "parser": "@typescript-eslint/parser", 12 | "parserOptions": { 13 | "ecmaVersion": 12, 14 | "project": "tsconfig.eslint.json" 15 | }, 16 | "plugins": ["@typescript-eslint"], 17 | "rules": { 18 | "prefer-const": "error", 19 | "@typescript-eslint/no-unused-vars": "off", 20 | "@typescript-eslint/no-unused-params": "off" 21 | }, 22 | "overrides": [ 23 | { 24 | "files": ["tests/**/*.ts"], 25 | "env": { "jest": true, "node": true } 26 | } 27 | ] 28 | } 29 | -------------------------------------------------------------------------------- /app/orders/.huskyrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "hooks": { 3 | "pre-commit": "lint-staged", 4 | "pre-push": "npm run test" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /app/orders/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:lts-alpine3.13 2 | 3 | WORKDIR /app 4 | COPY package.json ./ 5 | #RUN npm config set //npm.pkg.github.com/:_authToken=43fb630d62fa9b844d62c9faf11f4e6e9b62exxx 6 | #RUN npm config set @webmakaka:registry https://npm.pkg.github.com/webmakaka 7 | RUN npm install --only=prod 8 | COPY ./ ./ 9 | CMD ["npm", "start"] -------------------------------------------------------------------------------- /app/orders/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "orders", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "NODE_PATH=./src ts-node-dev src/index.ts", 8 | "test": "NODE_PATH=./src jest --watchAll --no-cache", 9 | "test:ci": "NODE_PATH=./src jest", 10 | "lint": "eslint src --ext .js,.ts,.jsx,.tsx", 11 | "precommit": "lint-staged" 12 | }, 13 | "jest": { 14 | "preset": "ts-jest", 15 | "testEnvironment": "node", 16 | "setupFilesAfterEnv": [ 17 | "./src/test/setup.ts" 18 | ] 19 | }, 20 | "keywords": [], 21 | "author": "", 22 | "license": "ISC", 23 | "dependencies": { 24 | "@types/cookie-session": "^2.0.42", 25 | "@types/express": "^4.17.11", 26 | "@types/jsonwebtoken": "^8.5.0", 27 | "@types/mongoose": "^5.10.3", 28 | "@webmak/microservices-common": "^1.0.6", 29 | "cookie-session": "^1.4.0", 30 | "express": "^4.17.1", 31 | "express-async-errors": "^3.1.1", 32 | "express-validator": "^6.10.0", 33 | "jsonwebtoken": "^8.5.1", 34 | "mongoose": "^5.12.0", 35 | "mongoose-update-if-current": "^1.4.0", 36 | "ts-node-dev": "^1.1.6", 37 | "typescript": "^4.2.3" 38 | }, 39 | "devDependencies": { 40 | "@types/jest": "^26.0.20", 41 | "@types/supertest": "^2.0.10", 42 | "@typescript-eslint/eslint-plugin": "^4.17.0", 43 | "@typescript-eslint/parser": "^4.17.0", 44 | "eslint": "^7.22.0", 45 | "husky": "^6.0.0", 46 | "jest": "^26.6.3", 47 | "lint-staged": "^10.5.4", 48 | "mongodb-memory-server": "^6.9.6", 49 | "supertest": "^6.1.3", 50 | "ts-jest": "^26.5.3" 51 | }, 52 | "volta": { 53 | "node": "14.16.0", 54 | "npm": "7.6.0" 55 | }, 56 | "lint-staged": { 57 | "*.{js, jsx}": [ 58 | "node_modules/.bin/eslint --max-warnings=0", 59 | "prettier --write", 60 | "git add" 61 | ] 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /app/orders/src/NatsWrapper.ts: -------------------------------------------------------------------------------- 1 | import nats, { Stan } from 'node-nats-streaming'; 2 | 3 | class NatsWrapper { 4 | private _client?: Stan; 5 | 6 | get client() { 7 | if (!this._client) { 8 | throw new Error('[Orders] Cannot access NATS client before connecting'); 9 | } 10 | 11 | return this._client; 12 | } 13 | 14 | connect(clusterId: string, clientId: string, url: string) { 15 | this._client = nats.connect(clusterId, clientId, { url }); 16 | 17 | return new Promise((resolve, reject) => { 18 | this.client.on('connect', () => { 19 | console.log('[Orders] Connected to NATS'); 20 | resolve('OK!'); 21 | }); 22 | 23 | this.client.on('error', (err) => { 24 | reject(err); 25 | }); 26 | }); 27 | } 28 | } 29 | 30 | export const natsWrapper = new NatsWrapper(); 31 | -------------------------------------------------------------------------------- /app/orders/src/__mocks__/NatsWrapper.ts: -------------------------------------------------------------------------------- 1 | export const natsWrapper = { 2 | client: { 3 | publish: jest 4 | .fn() 5 | .mockImplementationOnce( 6 | (subject: string, data: string, callback: () => void) => { 7 | callback(); 8 | } 9 | ), 10 | }, 11 | }; 12 | -------------------------------------------------------------------------------- /app/orders/src/app.ts: -------------------------------------------------------------------------------- 1 | import { 2 | currentUser, 3 | errorHandler, 4 | NotFoundError, 5 | } from '@webmak/microservices-common'; 6 | import { json } from 'body-parser'; 7 | import cookieSession from 'cookie-session'; 8 | import express from 'express'; 9 | import 'express-async-errors'; 10 | import { deleteOrderRouter } from 'routes/delete'; 11 | import { indexOrderRouter } from 'routes/index'; 12 | import { newOrderRouter } from 'routes/new'; 13 | import { showOrderRouter } from 'routes/show'; 14 | 15 | const app = express(); 16 | app.set('trust proxy', true); 17 | app.use(json()); 18 | app.use( 19 | cookieSession({ 20 | signed: false, 21 | secure: process.env.NODE_ENV !== 'test', 22 | }) 23 | ); 24 | 25 | app.use(currentUser); 26 | app.use(indexOrderRouter); 27 | app.use(newOrderRouter); 28 | app.use(showOrderRouter); 29 | app.use(deleteOrderRouter); 30 | 31 | app.all('*', async (req, res) => { 32 | throw new NotFoundError(); 33 | }); 34 | 35 | app.use(errorHandler); 36 | 37 | export { app }; 38 | -------------------------------------------------------------------------------- /app/orders/src/events/listeners/ExpirationCompleteListener.ts: -------------------------------------------------------------------------------- 1 | import { 2 | AListener, 3 | EOrderStatus, 4 | ESubjects, 5 | IExpirationCompleteEvent, 6 | } from '@webmak/microservices-common'; 7 | import { queueGroupName } from 'events/listeners/queueGroupName'; 8 | import { OrderCancelledPublisher } from 'events/publishers/OrderCancelledPublisher'; 9 | import { Order } from 'models/Order'; 10 | import { Message } from 'node-nats-streaming'; 11 | 12 | export class ExpirationCompleteListener extends AListener { 13 | queueGroupName = queueGroupName; 14 | subject: ESubjects.ExpirationComplete = ESubjects.ExpirationComplete; 15 | 16 | async onMessage(data: IExpirationCompleteEvent['data'], msg: Message) { 17 | const order = await Order.findById(data.orderId).populate('ticket'); 18 | 19 | if (!order) { 20 | throw new Error('[Orders] Order not found'); 21 | } 22 | 23 | if (order.status === EOrderStatus.Complete) { 24 | return msg.ack(); 25 | } 26 | 27 | order.set({ 28 | status: EOrderStatus.Cancelled, 29 | }); 30 | 31 | await order.save(); 32 | 33 | await new OrderCancelledPublisher(this.client).publish({ 34 | id: order.id, 35 | version: order.version, 36 | ticket: { 37 | id: order.ticket.id, 38 | }, 39 | }); 40 | 41 | msg.ack(); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /app/orders/src/events/listeners/PaymentCreatedListener.ts: -------------------------------------------------------------------------------- 1 | import { 2 | AListener, 3 | EOrderStatus, 4 | ESubjects, 5 | IPaymentCreatedEvent, 6 | } from '@webmak/microservices-common'; 7 | import { queueGroupName } from 'events/listeners/queueGroupName'; 8 | import { Order } from 'models/Order'; 9 | import { Message } from 'node-nats-streaming'; 10 | 11 | export class PaymentCreatedListener extends AListener { 12 | subject: ESubjects.PaymentCreated = ESubjects.PaymentCreated; 13 | 14 | queueGroupName = queueGroupName; 15 | 16 | async onMessage(data: IPaymentCreatedEvent['data'], msg: Message) { 17 | const order = await Order.findById(data.orderId); 18 | 19 | if (!order) { 20 | throw new Error('[Orders] Order not found'); 21 | } 22 | 23 | order.set({ 24 | status: EOrderStatus.Complete, 25 | }); 26 | 27 | await order.save(); 28 | 29 | msg.ack(); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /app/orders/src/events/listeners/TicketCreatedListener.ts: -------------------------------------------------------------------------------- 1 | import { 2 | AListener, 3 | ESubjects, 4 | ITicketCreatedEvent, 5 | } from '@webmak/microservices-common'; 6 | import { queueGroupName } from 'events/listeners/queueGroupName'; 7 | import { Ticket } from 'models/Ticket'; 8 | import { Message } from 'node-nats-streaming'; 9 | 10 | export class TicketCreatedListener extends AListener { 11 | subject: ESubjects.TicketCreated = ESubjects.TicketCreated; 12 | queueGroupName = queueGroupName; 13 | 14 | async onMessage(data: ITicketCreatedEvent['data'], msg: Message) { 15 | const { id, title, price } = data; 16 | const ticket = Ticket.build({ 17 | id, 18 | title, 19 | price, 20 | }); 21 | 22 | await ticket.save(); 23 | 24 | msg.ack(); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /app/orders/src/events/listeners/TicketUpdatedListener.ts: -------------------------------------------------------------------------------- 1 | import { 2 | AListener, 3 | ESubjects, 4 | ITicketUpdatedEvent, 5 | } from '@webmak/microservices-common'; 6 | import { queueGroupName } from 'events/listeners/queueGroupName'; 7 | import { Ticket } from 'models/Ticket'; 8 | import { Message } from 'node-nats-streaming'; 9 | 10 | export class TicketUpdatedListener extends AListener { 11 | subject: ESubjects.TicketUpdated = ESubjects.TicketUpdated; 12 | queueGroupName = queueGroupName; 13 | 14 | async onMessage(data: ITicketUpdatedEvent['data'], msg: Message) { 15 | const ticket = await Ticket.findByEvent(data); 16 | 17 | if (!ticket) { 18 | throw new Error('Ticket not found'); 19 | } 20 | 21 | const { title, price } = data; 22 | 23 | ticket.set({ 24 | title, 25 | price, 26 | }); 27 | 28 | await ticket.save(); 29 | 30 | msg.ack(); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /app/orders/src/events/listeners/__test__/ExpirationCompleteListener.ts: -------------------------------------------------------------------------------- 1 | import { 2 | EOrderStatus, 3 | IExpirationCompleteEvent, 4 | } from '@webmak/microservices-common'; 5 | import { ExpirationCompleteListener } from 'events/listeners/ExpirationCompleteListener'; 6 | import { Order } from 'models/Order'; 7 | import { Ticket } from 'models/Ticket'; 8 | import mongoose from 'mongoose'; 9 | import { Message } from 'node-nats-streaming'; 10 | import { natsWrapper } from '__mocks__/NatsWrapper'; 11 | 12 | const setup = async () => { 13 | // @ts-ignore 14 | const listener = new ExpirationCompleteListener(natsWrapper.client); 15 | 16 | const ticket = Ticket.build({ 17 | id: mongoose.Types.ObjectId().toHexString(), 18 | title: 'concert', 19 | price: 20, 20 | }); 21 | 22 | await ticket.save(); 23 | 24 | const order = Order.build({ 25 | status: EOrderStatus.Created, 26 | userId: 'sdfadsf', 27 | expiresAt: new Date(), 28 | ticket, 29 | }); 30 | 31 | await order.save(); 32 | 33 | const data: IExpirationCompleteEvent['data'] = { 34 | orderId: order.id, 35 | }; 36 | 37 | // @ts-ignore 38 | const msg: Message = { 39 | ack: jest.fn(), 40 | }; 41 | 42 | return { listener, order, ticket, data, msg }; 43 | }; 44 | 45 | it('updates the order status to cancelled', async () => { 46 | const { listener, order, data, msg } = await setup(); 47 | 48 | await listener.onMessage(data, msg); 49 | 50 | const updatedOrder = await Order.findById(order.id); 51 | expect(updatedOrder!.status).toEqual(EOrderStatus.Cancelled); 52 | }); 53 | 54 | it('emit an OrderCancelled event', async () => { 55 | const { listener, order, data, msg } = await setup(); 56 | 57 | await listener.onMessage(data, msg); 58 | 59 | expect(natsWrapper.client.publish).toHaveBeenCalled(); 60 | 61 | const eventData = JSON.parse( 62 | (natsWrapper.client.publish as jest.Mock).mock.calls[0][1] 63 | ); 64 | 65 | expect(eventData.id).toEqual(order.id); 66 | }); 67 | 68 | it('ack the message', async () => { 69 | const { listener, data, msg } = await setup(); 70 | 71 | await listener.onMessage(data, msg); 72 | 73 | expect(msg.ack).toHaveBeenCalled(); 74 | }); 75 | -------------------------------------------------------------------------------- /app/orders/src/events/listeners/__test__/TicketCreatedListener.test.ts: -------------------------------------------------------------------------------- 1 | import { ITicketCreatedEvent } from '@webmak/microservices-common'; 2 | import { TicketCreatedListener } from 'events/listeners/TicketCreatedListener'; 3 | import { Ticket } from 'models/Ticket'; 4 | import mongoose from 'mongoose'; 5 | import { Message } from 'node-nats-streaming'; 6 | import { natsWrapper } from '__mocks__/NatsWrapper'; 7 | 8 | const setup = async () => { 9 | // @ts-ignore 10 | const listener = new TicketCreatedListener(natsWrapper.client); 11 | 12 | const data: ITicketCreatedEvent['data'] = { 13 | version: 0, 14 | id: new mongoose.Types.ObjectId().toHexString(), 15 | title: 'concert', 16 | price: 10, 17 | userId: new mongoose.Types.ObjectId().toHexString(), 18 | }; 19 | 20 | // @ts-ignore 21 | const msg: Message = { 22 | ack: jest.fn(), 23 | }; 24 | 25 | return { listener, data, msg }; 26 | }; 27 | 28 | it('creates and saves a ticket', async () => { 29 | const { listener, data, msg } = await setup(); 30 | await listener.onMessage(data, msg); 31 | 32 | const ticket = await Ticket.findById(data.id); 33 | 34 | expect(ticket).toBeDefined(); 35 | expect(ticket!.title).toEqual(data.title); 36 | expect(ticket!.price).toEqual(data.price); 37 | }); 38 | 39 | it('acks the message', async () => { 40 | const { listener, data, msg } = await setup(); 41 | await listener.onMessage(data, msg); 42 | expect(msg.ack).toHaveBeenCalled(); 43 | }); 44 | -------------------------------------------------------------------------------- /app/orders/src/events/listeners/__test__/TicketUpdatedLister.test.ts: -------------------------------------------------------------------------------- 1 | import { ITicketUpdatedEvent } from '@webmak/microservices-common'; 2 | import { TicketUpdatedListener } from 'events/listeners/TicketUpdatedListener'; 3 | import { Ticket } from 'models/Ticket'; 4 | import mongoose from 'mongoose'; 5 | import { Message } from 'node-nats-streaming'; 6 | import { natsWrapper } from '__mocks__/NatsWrapper'; 7 | 8 | const setup = async () => { 9 | // @ts-ignore 10 | const listener = new TicketUpdatedListener(natsWrapper.client); 11 | const ticket = Ticket.build({ 12 | id: mongoose.Types.ObjectId().toHexString(), 13 | title: 'concert', 14 | price: 20, 15 | }); 16 | 17 | await ticket.save(); 18 | 19 | const data: ITicketUpdatedEvent['data'] = { 20 | id: ticket.id, 21 | version: ticket.version + 1, 22 | title: 'new concert', 23 | price: 999, 24 | userId: 'asdfada', 25 | }; 26 | 27 | // @ts-ignore 28 | const msg: Message = { 29 | ack: jest.fn(), 30 | }; 31 | 32 | return { msg, data, ticket, listener }; 33 | }; 34 | 35 | it('finds, updates and saves a ticket', async () => { 36 | const { msg, data, ticket, listener } = await setup(); 37 | await listener.onMessage(data, msg); 38 | 39 | const updatedTicket = await Ticket.findById(ticket.id); 40 | 41 | expect(updatedTicket!.title).toEqual(data.title); 42 | expect(updatedTicket!.price).toEqual(data.price); 43 | expect(updatedTicket!.version).toEqual(data.version); 44 | }); 45 | 46 | it('acks the message', async () => { 47 | const { msg, data, listener } = await setup(); 48 | 49 | await listener.onMessage(data, msg); 50 | 51 | expect(msg.ack).toHaveBeenCalled(); 52 | }); 53 | 54 | it('does not call ack if the event has a skipped version number', async () => { 55 | const { msg, data, listener } = await setup(); 56 | 57 | data.version = 10; 58 | 59 | try { 60 | await listener.onMessage(data, msg); 61 | } catch (err) {} 62 | 63 | expect(msg.ack).not.toHaveBeenCalled(); 64 | }); 65 | -------------------------------------------------------------------------------- /app/orders/src/events/listeners/queueGroupName.ts: -------------------------------------------------------------------------------- 1 | export const queueGroupName = 'orders-service'; 2 | -------------------------------------------------------------------------------- /app/orders/src/events/publishers/OrderCancelledPublisher.ts: -------------------------------------------------------------------------------- 1 | import { 2 | APublisher, 3 | ESubjects, 4 | IOrderCancelledEvent, 5 | } from '@webmak/microservices-common'; 6 | 7 | export class OrderCancelledPublisher extends APublisher { 8 | subject: ESubjects.OrderCancelled = ESubjects.OrderCancelled; 9 | } 10 | -------------------------------------------------------------------------------- /app/orders/src/events/publishers/OrderCreatedPublisher.ts: -------------------------------------------------------------------------------- 1 | import { 2 | APublisher, 3 | ESubjects, 4 | IOrderCreatedEvent, 5 | } from '@webmak/microservices-common'; 6 | 7 | export class OrderCreatedPublisher extends APublisher { 8 | subject: ESubjects.OrderCreated = ESubjects.OrderCreated; 9 | } 10 | -------------------------------------------------------------------------------- /app/orders/src/index.ts: -------------------------------------------------------------------------------- 1 | import { app } from 'app'; 2 | import { ExpirationCompleteListener } from 'events/listeners/ExpirationCompleteListener'; 3 | import { PaymentCreatedListener } from 'events/listeners/PaymentCreatedListener'; 4 | import { TicketCreatedListener } from 'events/listeners/TicketCreatedListener'; 5 | import { TicketUpdatedListener } from 'events/listeners/TicketUpdatedListener'; 6 | import mongoose from 'mongoose'; 7 | import { natsWrapper } from 'NatsWrapper'; 8 | 9 | const start = async () => { 10 | if (!process.env.JWT_KEY) { 11 | throw new Error('[Orders] JWT_KEY must be defined'); 12 | } 13 | 14 | if (!process.env.MONGO_URI) { 15 | throw new Error('[Orders] MONGO_URI must be defined'); 16 | } 17 | 18 | if (!process.env.NATS_CLUSTER_ID) { 19 | throw new Error('[Orders] NATS_CLUSTER_ID must be defined'); 20 | } 21 | 22 | if (!process.env.NATS_CLIENT_ID) { 23 | throw new Error('[Orders] NATS_CLIENT_ID must be defined'); 24 | } 25 | 26 | if (!process.env.NATS_URL) { 27 | throw new Error('[Orders] NATS_URL must be defined'); 28 | } 29 | 30 | try { 31 | await natsWrapper.connect( 32 | process.env.NATS_CLUSTER_ID, 33 | process.env.NATS_CLIENT_ID, 34 | process.env.NATS_URL 35 | ); 36 | 37 | natsWrapper.client.on('close', () => { 38 | console.log('[Orders] NATS connection closed!'); 39 | process.exit(); 40 | }); 41 | 42 | process.on('SIGINT', () => natsWrapper.client.close()); 43 | process.on('SIGTERM', () => natsWrapper.client.close()); 44 | 45 | new TicketCreatedListener(natsWrapper.client).listen(); 46 | new TicketUpdatedListener(natsWrapper.client).listen(); 47 | new ExpirationCompleteListener(natsWrapper.client).listen(); 48 | new PaymentCreatedListener(natsWrapper.client).listen(); 49 | 50 | await mongoose.connect(process.env.MONGO_URI, { 51 | useNewUrlParser: true, 52 | useUnifiedTopology: true, 53 | useCreateIndex: true, 54 | }); 55 | 56 | console.log('[Orders] Connected to MongoDB'); 57 | } catch (err) { 58 | console.log(err); 59 | } 60 | }; 61 | 62 | app.listen(3000, () => { 63 | console.log('[Orders] Listening on port 3000'); 64 | }); 65 | 66 | start(); 67 | -------------------------------------------------------------------------------- /app/orders/src/models/Order.ts: -------------------------------------------------------------------------------- 1 | import { EOrderStatus } from '@webmak/microservices-common'; 2 | import { ITicketDoc } from 'models/Ticket'; 3 | import mongoose from 'mongoose'; 4 | import { updateIfCurrentPlugin } from 'mongoose-update-if-current'; 5 | 6 | interface IOrderAttrs { 7 | userId: string; 8 | status: EOrderStatus; 9 | expiresAt: Date; 10 | ticket: ITicketDoc; 11 | } 12 | 13 | interface IOrderDoc extends mongoose.Document { 14 | userId: string; 15 | status: EOrderStatus; 16 | expiresAt: Date; 17 | ticket: ITicketDoc; 18 | version: number; 19 | } 20 | 21 | interface IOrderModel extends mongoose.Model { 22 | build(attrs: IOrderAttrs): IOrderDoc; 23 | } 24 | 25 | const orderSchema = new mongoose.Schema( 26 | { 27 | userId: { 28 | type: String, 29 | required: true, 30 | }, 31 | status: { 32 | type: String, 33 | required: true, 34 | emum: Object.values(EOrderStatus), 35 | default: EOrderStatus.Created, 36 | }, 37 | expiresAt: { 38 | type: mongoose.Schema.Types.Date, 39 | }, 40 | ticket: { 41 | type: mongoose.Schema.Types.ObjectId, 42 | ref: 'Ticket', 43 | }, 44 | }, 45 | { 46 | toJSON: { 47 | transform(_doc, ret) { 48 | ret.id = ret._id; 49 | delete ret._id; 50 | }, 51 | }, 52 | } 53 | ); 54 | 55 | orderSchema.set('versionKey', 'version'); 56 | orderSchema.plugin(updateIfCurrentPlugin); 57 | 58 | orderSchema.statics.build = (attrs: IOrderAttrs) => { 59 | return new Order(attrs); 60 | }; 61 | 62 | const Order = mongoose.model('Order', orderSchema); 63 | 64 | export { Order }; 65 | export { EOrderStatus }; 66 | -------------------------------------------------------------------------------- /app/orders/src/models/Ticket.ts: -------------------------------------------------------------------------------- 1 | import { EOrderStatus, Order } from 'models/Order'; 2 | import mongoose from 'mongoose'; 3 | import { updateIfCurrentPlugin } from 'mongoose-update-if-current'; 4 | 5 | interface ITicketAttrs { 6 | id: string; 7 | title: string; 8 | price: number; 9 | } 10 | 11 | export interface ITicketDoc extends mongoose.Document { 12 | title: string; 13 | price: number; 14 | version: number; 15 | isReserved(): Promise; 16 | } 17 | 18 | interface ITicketModel extends mongoose.Model { 19 | build(attrs: ITicketAttrs): ITicketDoc; 20 | findByEvent(event: { 21 | id: string; 22 | version: number; 23 | }): Promise; 24 | } 25 | 26 | const ticketSchema = new mongoose.Schema( 27 | { 28 | title: { 29 | type: String, 30 | required: true, 31 | }, 32 | price: { 33 | type: Number, 34 | required: true, 35 | min: 0, 36 | }, 37 | }, 38 | { 39 | toJSON: { 40 | transform(_doc, ret) { 41 | ret.id = ret._id; 42 | delete ret._id; 43 | }, 44 | }, 45 | } 46 | ); 47 | 48 | ticketSchema.set('versionKey', 'version'); 49 | ticketSchema.plugin(updateIfCurrentPlugin); 50 | 51 | ticketSchema.statics.findByEvent = (event: { id: string; version: number }) => { 52 | return Ticket.findOne({ 53 | _id: event.id, 54 | version: event.version - 1, 55 | }); 56 | }; 57 | ticketSchema.statics.build = (attrs: ITicketAttrs) => { 58 | return new Ticket({ 59 | _id: attrs.id, 60 | title: attrs.title, 61 | price: attrs.price, 62 | }); 63 | }; 64 | 65 | ticketSchema.methods.isReserved = async function () { 66 | const existingOrder = await Order.findOne({ 67 | // @ts-ignore: Unreachable code error 68 | ticket: this, 69 | status: { 70 | $in: [ 71 | EOrderStatus.Created, 72 | EOrderStatus.AwaitingPayment, 73 | EOrderStatus.Complete, 74 | ], 75 | }, 76 | }); 77 | 78 | return !!existingOrder; 79 | }; 80 | 81 | const Ticket = mongoose.model('Ticket', ticketSchema); 82 | 83 | export { Ticket }; 84 | -------------------------------------------------------------------------------- /app/orders/src/routes/__test__/delete.test.ts: -------------------------------------------------------------------------------- 1 | import { app } from 'app'; 2 | import { EOrderStatus, Order } from 'models/Order'; 3 | import { Ticket } from 'models/Ticket'; 4 | import mongoose from 'mongoose'; 5 | import request from 'supertest'; 6 | import { natsWrapper } from '__mocks__/NatsWrapper'; 7 | 8 | it('marks an order as cancelled', async () => { 9 | const ticket = Ticket.build({ 10 | id: mongoose.Types.ObjectId().toHexString(), 11 | title: 'concert', 12 | price: 20, 13 | }); 14 | 15 | await ticket.save(); 16 | 17 | const user = global.signin(); 18 | 19 | const { body: order } = await request(app) 20 | .post('/api/orders') 21 | .set('Cookie', user) 22 | .send({ ticketId: ticket.id }) 23 | .expect(201); 24 | 25 | await request(app) 26 | .delete(`/api/orders/${order.id}`) 27 | .set('Cookie', user) 28 | .send() 29 | .expect(204); 30 | 31 | const updatedOrder = await Order.findById(order.id); 32 | expect(updatedOrder!.status).toEqual(EOrderStatus.Cancelled); 33 | }); 34 | 35 | it('emits a order cancelled event', async () => { 36 | const ticket = Ticket.build({ 37 | id: mongoose.Types.ObjectId().toHexString(), 38 | title: 'concert', 39 | price: 20, 40 | }); 41 | 42 | await ticket.save(); 43 | 44 | const user = global.signin(); 45 | 46 | const { body: order } = await request(app) 47 | .post('/api/orders') 48 | .set('Cookie', user) 49 | .send({ ticketId: ticket.id }) 50 | .expect(201); 51 | 52 | await request(app) 53 | .delete(`/api/orders/${order.id}`) 54 | .set('Cookie', user) 55 | .send() 56 | .expect(204); 57 | 58 | expect(natsWrapper.client.publish).toHaveBeenCalled(); 59 | }); 60 | -------------------------------------------------------------------------------- /app/orders/src/routes/__test__/index.test.ts: -------------------------------------------------------------------------------- 1 | import { app } from 'app'; 2 | import { Ticket } from 'models/Ticket'; 3 | import mongoose from 'mongoose'; 4 | import request from 'supertest'; 5 | 6 | const buildTicket = async () => { 7 | const ticket = Ticket.build({ 8 | id: mongoose.Types.ObjectId().toHexString(), 9 | title: 'concert', 10 | price: 20, 11 | }); 12 | 13 | await ticket.save(); 14 | 15 | return ticket; 16 | }; 17 | 18 | it('fetches orders for an particular user', async () => { 19 | const ticketOne = await buildTicket(); 20 | const ticketTwo = await buildTicket(); 21 | const ticketThree = await buildTicket(); 22 | 23 | const userOne = global.signin(); 24 | const userTwo = global.signin(); 25 | 26 | // Create one order as User #1 27 | await request(app) 28 | .post('/api/orders') 29 | .set('Cookie', userOne) 30 | .send({ 31 | ticketId: ticketOne.id, 32 | }) 33 | .expect(201); 34 | 35 | // Create two orders as User #2 36 | const { body: orderOne } = await request(app) 37 | .post('/api/orders') 38 | .set('Cookie', userTwo) 39 | .send({ 40 | ticketId: ticketTwo.id, 41 | }) 42 | .expect(201); 43 | 44 | const { body: orderTwo } = await request(app) 45 | .post('/api/orders') 46 | .set('Cookie', userTwo) 47 | .send({ 48 | ticketId: ticketThree.id, 49 | }) 50 | .expect(201); 51 | 52 | const response = await request(app) 53 | .get('/api/orders') 54 | .set('Cookie', userTwo) 55 | .expect(200); 56 | 57 | expect(response.body.length).toEqual(2); 58 | expect(response.body[0].id).toEqual(orderOne.id); 59 | expect(response.body[1].id).toEqual(orderTwo.id); 60 | expect(response.body[0].ticket.id).toEqual(ticketTwo.id); 61 | expect(response.body[1].ticket.id).toEqual(ticketThree.id); 62 | }); 63 | -------------------------------------------------------------------------------- /app/orders/src/routes/__test__/new.test.ts: -------------------------------------------------------------------------------- 1 | import { app } from 'app'; 2 | import { EOrderStatus, Order } from 'models/Order'; 3 | import { Ticket } from 'models/Ticket'; 4 | import mongoose from 'mongoose'; 5 | import request from 'supertest'; 6 | import { natsWrapper } from '__mocks__/NatsWrapper'; 7 | 8 | it('returns an error if the ticket does not exist', async () => { 9 | const ticketId = mongoose.Types.ObjectId(); 10 | await request(app) 11 | .post('/api/orders') 12 | .set('Cookie', global.signin()) 13 | .send({ 14 | ticketId, 15 | }) 16 | .expect(404); 17 | }); 18 | 19 | it('returns an error if the ticket is already reserved', async () => { 20 | const ticket = Ticket.build({ 21 | id: mongoose.Types.ObjectId().toHexString(), 22 | title: 'concert', 23 | price: 20, 24 | }); 25 | 26 | await ticket.save(); 27 | 28 | const order = Order.build({ 29 | ticket, 30 | userId: 'dfsdfsdfee', 31 | status: EOrderStatus.Created, 32 | expiresAt: new Date(), 33 | }); 34 | 35 | await order.save(); 36 | await request(app) 37 | .post('/api/orders') 38 | .set('Cookie', global.signin()) 39 | .send({ ticketId: ticket.id }) 40 | .expect(400); 41 | }); 42 | 43 | it('reserves a ticket', async () => { 44 | const ticket = Ticket.build({ 45 | id: mongoose.Types.ObjectId().toHexString(), 46 | title: 'concert', 47 | price: 20, 48 | }); 49 | 50 | await ticket.save(); 51 | 52 | await request(app) 53 | .post('/api/orders') 54 | .set('Cookie', global.signin()) 55 | .send({ ticketId: ticket.id }) 56 | .expect(201); 57 | }); 58 | 59 | it('emits an order created event', async () => { 60 | const ticket = Ticket.build({ 61 | id: mongoose.Types.ObjectId().toHexString(), 62 | title: 'concert', 63 | price: 20, 64 | }); 65 | 66 | await ticket.save(); 67 | 68 | await request(app) 69 | .post('/api/orders') 70 | .set('Cookie', global.signin()) 71 | .send({ ticketId: ticket.id }) 72 | .expect(201); 73 | 74 | expect(natsWrapper.client.publish).toHaveBeenCalled(); 75 | }); 76 | -------------------------------------------------------------------------------- /app/orders/src/routes/__test__/show.test.ts: -------------------------------------------------------------------------------- 1 | import { app } from 'app'; 2 | import { Ticket } from 'models/Ticket'; 3 | import mongoose from 'mongoose'; 4 | import request from 'supertest'; 5 | 6 | it('tetch the order', async () => { 7 | const ticket = Ticket.build({ 8 | id: mongoose.Types.ObjectId().toHexString(), 9 | title: 'concert', 10 | price: 20, 11 | }); 12 | 13 | await ticket.save(); 14 | 15 | const user = global.signin(); 16 | 17 | const { body: order } = await request(app) 18 | .post('/api/orders') 19 | .set('Cookie', user) 20 | .send({ ticketId: ticket.id }) 21 | .expect(201); 22 | 23 | const { body: fetchedOrder } = await request(app) 24 | .get(`/api/orders/${order.id}`) 25 | .set('Cookie', user) 26 | .send() 27 | .expect(200); 28 | 29 | expect(fetchedOrder.id).toEqual(order.id); 30 | }); 31 | 32 | it('returns an error if one user tries to fetch another users order', async () => { 33 | const ticket = Ticket.build({ 34 | id: mongoose.Types.ObjectId().toHexString(), 35 | title: 'concert', 36 | price: 20, 37 | }); 38 | 39 | await ticket.save(); 40 | 41 | const user = global.signin(); 42 | 43 | const { body: order } = await request(app) 44 | .post('/api/orders') 45 | .set('Cookie', user) 46 | .send({ ticketId: ticket.id }) 47 | .expect(201); 48 | 49 | await request(app) 50 | .get(`/api/orders/${order.id}`) 51 | .set('Cookie', global.signin()) 52 | .send() 53 | .expect(401); 54 | }); 55 | -------------------------------------------------------------------------------- /app/orders/src/routes/delete.ts: -------------------------------------------------------------------------------- 1 | import { 2 | NotAuthorizedError, 3 | NotFoundError, 4 | requireAuth, 5 | } from '@webmak/microservices-common'; 6 | import { OrderCancelledPublisher } from 'events/publishers/OrderCancelledPublisher'; 7 | import express, { Request, Response } from 'express'; 8 | import { EOrderStatus, Order } from 'models/Order'; 9 | import { natsWrapper } from 'NatsWrapper'; 10 | 11 | const router = express.Router(); 12 | 13 | router.delete( 14 | '/api/orders/:orderId', 15 | requireAuth, 16 | async (req: Request, res: Response) => { 17 | const order = await Order.findById(req.params.orderId).populate('ticket'); 18 | 19 | if (!order) { 20 | throw new NotFoundError(); 21 | } 22 | 23 | if (order.userId !== req.currentUser!.id) { 24 | throw new NotAuthorizedError(); 25 | } 26 | 27 | order.status = EOrderStatus.Cancelled; 28 | await order.save(); 29 | 30 | new OrderCancelledPublisher(natsWrapper.client).publish({ 31 | id: order.id, 32 | version: order.version, 33 | ticket: { 34 | id: order.ticket.id, 35 | }, 36 | }); 37 | 38 | return res.status(204).send(order); 39 | } 40 | ); 41 | 42 | export { router as deleteOrderRouter }; 43 | -------------------------------------------------------------------------------- /app/orders/src/routes/index.ts: -------------------------------------------------------------------------------- 1 | import { requireAuth } from '@webmak/microservices-common'; 2 | import express, { Request, Response } from 'express'; 3 | import { Order } from 'models/Order'; 4 | 5 | const router = express.Router(); 6 | 7 | router.get('/api/orders', requireAuth, async (req: Request, res: Response) => { 8 | const orders = await Order.find({ 9 | userId: req.currentUser!.id, 10 | }).populate('ticket'); 11 | 12 | return res.send(orders); 13 | }); 14 | 15 | export { router as indexOrderRouter }; 16 | -------------------------------------------------------------------------------- /app/orders/src/routes/new.ts: -------------------------------------------------------------------------------- 1 | import { 2 | BadRequestError, 3 | EOrderStatus, 4 | NotFoundError, 5 | requireAuth, 6 | validateRequest, 7 | } from '@webmak/microservices-common'; 8 | import { OrderCreatedPublisher } from 'events/publishers/OrderCreatedPublisher'; 9 | import express, { Request, Response } from 'express'; 10 | import { body } from 'express-validator'; 11 | import { Order } from 'models/Order'; 12 | import { Ticket } from 'models/Ticket'; 13 | import mongoose from 'mongoose'; 14 | import { natsWrapper } from 'NatsWrapper'; 15 | 16 | const router = express.Router(); 17 | 18 | const EXPIRATION_WINDOW_SECONDS = 15 * 60; 19 | 20 | router.post( 21 | '/api/orders', 22 | requireAuth, 23 | [ 24 | body('ticketId') 25 | .not() 26 | .isEmpty() 27 | .custom((input: string) => mongoose.Types.ObjectId.isValid(input)) 28 | .withMessage('TicketId must be provided'), 29 | ], 30 | validateRequest, 31 | async (req: Request, res: Response) => { 32 | const { ticketId } = req.body; 33 | 34 | const ticket = await Ticket.findById(ticketId); 35 | 36 | if (!ticket) { 37 | throw new NotFoundError(); 38 | } 39 | 40 | const isReserved = await ticket.isReserved(); 41 | 42 | if (isReserved) { 43 | throw new BadRequestError('[Orders] Ticket is already reserved'); 44 | } 45 | 46 | const expiration = new Date(); 47 | expiration.setSeconds(expiration.getSeconds() + EXPIRATION_WINDOW_SECONDS); 48 | 49 | const order = Order.build({ 50 | userId: req.currentUser!.id, 51 | status: EOrderStatus.Created, 52 | expiresAt: expiration, 53 | ticket, 54 | }); 55 | 56 | await order.save(); 57 | 58 | new OrderCreatedPublisher(natsWrapper.client).publish({ 59 | id: order.id, 60 | version: order.version, 61 | status: order.status, 62 | userId: order.userId, 63 | expiresAt: order.expiresAt.toISOString(), 64 | ticket: { 65 | id: ticket.id, 66 | price: ticket.price, 67 | }, 68 | }); 69 | 70 | return res.status(201).send(order); 71 | } 72 | ); 73 | 74 | export { router as newOrderRouter }; 75 | -------------------------------------------------------------------------------- /app/orders/src/routes/show.ts: -------------------------------------------------------------------------------- 1 | import { 2 | NotAuthorizedError, 3 | NotFoundError, 4 | requireAuth, 5 | } from '@webmak/microservices-common'; 6 | import express, { Request, Response } from 'express'; 7 | import { Order } from 'models/Order'; 8 | 9 | const router = express.Router(); 10 | 11 | router.get( 12 | '/api/orders/:orderId', 13 | requireAuth, 14 | async (req: Request, res: Response) => { 15 | const order = await Order.findById(req.params.orderId).populate('ticket'); 16 | 17 | if (!order) { 18 | throw new NotFoundError(); 19 | } 20 | 21 | if (order.userId !== req.currentUser!.id) { 22 | throw new NotAuthorizedError(); 23 | } 24 | 25 | return res.send(order); 26 | } 27 | ); 28 | 29 | export { router as showOrderRouter }; 30 | -------------------------------------------------------------------------------- /app/orders/src/test/setup.ts: -------------------------------------------------------------------------------- 1 | import jwt from 'jsonwebtoken'; 2 | import { MongoMemoryServer } from 'mongodb-memory-server'; 3 | import mongoose from 'mongoose'; 4 | 5 | declare global { 6 | namespace NodeJS { 7 | interface Global { 8 | signin(): string[]; 9 | } 10 | } 11 | } 12 | 13 | jest.mock('__mocks__/NatsWrapper'); 14 | 15 | let mongo: any; 16 | 17 | beforeAll(async () => { 18 | process.env.JWT_KEY = 'MY_JWT_SECRET'; 19 | mongo = new MongoMemoryServer(); 20 | const mongoUri = await mongo.getUri(); 21 | 22 | await mongoose.connect(mongoUri, { 23 | useNewUrlParser: true, 24 | useUnifiedTopology: true, 25 | }); 26 | }); 27 | 28 | beforeEach(async () => { 29 | jest.clearAllMocks(); 30 | 31 | const collections = await mongoose.connection.db.collections(); 32 | 33 | for (let collection of collections) { 34 | await collection.deleteMany({}); 35 | } 36 | }); 37 | 38 | afterAll(async () => { 39 | await mongo.stop(); 40 | await mongoose.connection.close(); 41 | }); 42 | 43 | global.signin = () => { 44 | const payload = { 45 | id: new mongoose.Types.ObjectId().toHexString(), 46 | email: 'test@test.com', 47 | }; 48 | 49 | const token = jwt.sign(payload, process.env.JWT_KEY!); 50 | 51 | const session = { jwt: token }; 52 | 53 | const sessionJSON = JSON.stringify(session); 54 | 55 | const base64 = Buffer.from(sessionJSON).toString('base64'); 56 | 57 | return [`express:sess=${base64}`]; 58 | }; 59 | -------------------------------------------------------------------------------- /app/orders/tsconfig.eslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "types": ["jest"] 5 | }, 6 | "include": ["src", "tests"] 7 | } 8 | -------------------------------------------------------------------------------- /app/orders/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Basic Options */ 4 | // "incremental": true, /* Enable incremental compilation */ 5 | "target": "es5" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */, 6 | "module": "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */, 7 | // "lib": [], /* Specify library files to be included in the compilation. */ 8 | // "allowJs": true, /* Allow javascript files to be compiled. */ 9 | // "checkJs": true, /* Report errors in .js files. */ 10 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ 11 | // "declaration": true, /* Generates corresponding '.d.ts' file. */ 12 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ 13 | // "sourceMap": true, /* Generates corresponding '.map' file. */ 14 | // "outFile": "./", /* Concatenate and emit output to single file. */ 15 | // "outDir": "./", /* Redirect output structure to the directory. */ 16 | // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 17 | // "composite": true, /* Enable project compilation */ 18 | // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ 19 | // "removeComments": true, /* Do not emit comments to output. */ 20 | // "noEmit": true, /* Do not emit outputs. */ 21 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 22 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 23 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 24 | 25 | /* Strict Type-Checking Options */ 26 | "strict": true /* Enable all strict type-checking options. */, 27 | // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ 28 | // "strictNullChecks": true, /* Enable strict null checks. */ 29 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */ 30 | // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ 31 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ 32 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 33 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 34 | 35 | /* Additional Checks */ 36 | // "noUnusedLocals": true, /* Report errors on unused locals. */ 37 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 38 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 39 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 40 | 41 | /* Module Resolution Options */ 42 | // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 43 | "baseUrl": "./src", 44 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 45 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 46 | // "typeRoots": [], /* List of folders to include type definitions from. */ 47 | // "types": [], /* Type declaration files to be included in compilation. */ 48 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 49 | "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */, 50 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 51 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 52 | 53 | /* Source Map Options */ 54 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 55 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 56 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 57 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 58 | 59 | /* Experimental Options */ 60 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 61 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 62 | 63 | /* Advanced Options */ 64 | "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */ 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /app/payments/.dockerignore: -------------------------------------------------------------------------------- 1 | **/.eslintignore 2 | **/.eslintrc 3 | **/.prettierignore 4 | **/.prettierrc 5 | 6 | **/node_modules/ 7 | **/node_modules_linux/ 8 | **/Dockerfile 9 | **/.dockerignore 10 | **/.gitignore 11 | **/.git 12 | **/README.md 13 | **/LICENSE 14 | **/.vscode 15 | 16 | **/test/ 17 | **/.next/ -------------------------------------------------------------------------------- /app/payments/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:lts-alpine3.13 2 | 3 | WORKDIR /app 4 | COPY package.json ./ 5 | RUN npm install --only=prod 6 | COPY ./ ./ 7 | CMD ["npm", "start"] -------------------------------------------------------------------------------- /app/payments/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "payments", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "NODE_PATH=./src ts-node-dev src/index.ts", 8 | "test": "NODE_PATH=./src jest --watchAll --no-cache", 9 | "test:ci": "NODE_PATH=./src jest" 10 | }, 11 | "jest": { 12 | "preset": "ts-jest", 13 | "testEnvironment": "node", 14 | "setupFilesAfterEnv": [ 15 | "./src/test/setup.ts" 16 | ] 17 | }, 18 | "keywords": [], 19 | "author": "", 20 | "license": "ISC", 21 | "dependencies": { 22 | "@types/cookie-session": "^2.0.42", 23 | "@types/express": "^4.17.11", 24 | "@types/jsonwebtoken": "^8.5.0", 25 | "@types/mongoose": "^5.10.3", 26 | "@webmak/microservices-common": "^1.0.6", 27 | "cookie-session": "^1.4.0", 28 | "express": "^4.17.1", 29 | "express-async-errors": "^3.1.1", 30 | "express-validator": "^6.10.0", 31 | "jsonwebtoken": "^8.5.1", 32 | "mongoose": "^5.12.0", 33 | "mongoose-update-if-current": "^1.4.0", 34 | "stripe": "^8.138.0", 35 | "ts-node-dev": "^1.1.6", 36 | "typescript": "^4.2.3" 37 | }, 38 | "devDependencies": { 39 | "@types/jest": "^26.0.20", 40 | "@types/supertest": "^2.0.10", 41 | "jest": "^26.6.3", 42 | "mongodb-memory-server": "^6.9.6", 43 | "supertest": "^6.1.3", 44 | "ts-jest": "^26.5.3" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /app/payments/src/NatsWrapper.ts: -------------------------------------------------------------------------------- 1 | import nats, { Stan } from 'node-nats-streaming'; 2 | 3 | class NatsWrapper { 4 | private _client?: Stan; 5 | 6 | get client() { 7 | if (!this._client) { 8 | throw new Error('[Payments] Cannot access NATS client before connecting'); 9 | } 10 | 11 | return this._client; 12 | } 13 | 14 | connect(clusterId: string, clientId: string, url: string) { 15 | this._client = nats.connect(clusterId, clientId, { url }); 16 | 17 | return new Promise((resolve, reject) => { 18 | this.client.on('connect', () => { 19 | console.log('Connected to NATS'); 20 | resolve('OK!'); 21 | }); 22 | 23 | this.client.on('error', (err) => { 24 | reject(err); 25 | }); 26 | }); 27 | } 28 | } 29 | 30 | export const natsWrapper = new NatsWrapper(); 31 | -------------------------------------------------------------------------------- /app/payments/src/__mocks__/NatsWrapper.ts: -------------------------------------------------------------------------------- 1 | export const natsWrapper = { 2 | client: { 3 | publish: jest 4 | .fn() 5 | .mockImplementationOnce( 6 | (subject: string, data: string, callback: () => void) => { 7 | callback(); 8 | } 9 | ), 10 | }, 11 | }; 12 | -------------------------------------------------------------------------------- /app/payments/src/__mocks__/stripe.ts.disabled: -------------------------------------------------------------------------------- 1 | export const stripe = { 2 | charges: { 3 | create: jest.fn().mockResolvedValue({}), 4 | }, 5 | }; 6 | -------------------------------------------------------------------------------- /app/payments/src/app.ts: -------------------------------------------------------------------------------- 1 | import { 2 | currentUser, 3 | errorHandler, 4 | NotFoundError, 5 | } from '@webmak/microservices-common'; 6 | import { json } from 'body-parser'; 7 | import cookieSession from 'cookie-session'; 8 | import express from 'express'; 9 | import 'express-async-errors'; 10 | import { createChargeRouter } from 'routes/new'; 11 | 12 | const app = express(); 13 | app.set('trust proxy', true); 14 | app.use(json()); 15 | app.use( 16 | cookieSession({ 17 | signed: false, 18 | secure: process.env.NODE_ENV !== 'test', 19 | }) 20 | ); 21 | 22 | app.use(currentUser); 23 | app.use(createChargeRouter); 24 | 25 | app.all('*', async (_req, _res) => { 26 | throw new NotFoundError(); 27 | }); 28 | 29 | app.use(errorHandler); 30 | 31 | export { app }; 32 | -------------------------------------------------------------------------------- /app/payments/src/events/listeners/OrderCancelledListener.ts: -------------------------------------------------------------------------------- 1 | import { 2 | AListener, 3 | EOrderStatus, 4 | ESubjects, 5 | IOrderCancelledEvent, 6 | } from '@webmak/microservices-common'; 7 | import { queueGroupName } from 'events/listeners/queueGroupName'; 8 | import { Order } from 'models/Order'; 9 | import { Message } from 'node-nats-streaming'; 10 | 11 | export class OrderCancelledListener extends AListener { 12 | subject: ESubjects.OrderCancelled = ESubjects.OrderCancelled; 13 | queueGroupName = queueGroupName; 14 | 15 | async onMessage(data: IOrderCancelledEvent['data'], msg: Message) { 16 | const order = await Order.findOne({ 17 | _id: data.id, 18 | version: data.version - 1, 19 | }); 20 | 21 | if (!order) { 22 | throw new Error('[Payments] Order not found'); 23 | } 24 | 25 | order.set({ status: EOrderStatus.Cancelled }); 26 | await order.save(); 27 | 28 | msg.ack(); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /app/payments/src/events/listeners/OrderCreatedListener.ts: -------------------------------------------------------------------------------- 1 | import { 2 | AListener, 3 | ESubjects, 4 | IOrderCreatedEvent, 5 | } from '@webmak/microservices-common'; 6 | import { queueGroupName } from 'events/listeners/queueGroupName'; 7 | import { Order } from 'models/Order'; 8 | import { Message } from 'node-nats-streaming'; 9 | 10 | export class OrderCreatedListener extends AListener { 11 | subject: ESubjects.OrderCreated = ESubjects.OrderCreated; 12 | queueGroupName = queueGroupName; 13 | 14 | async onMessage(data: IOrderCreatedEvent['data'], msg: Message) { 15 | const order = Order.build({ 16 | id: data.id, 17 | price: data.ticket.price, 18 | status: data.status, 19 | userId: data.userId, 20 | version: data.version, 21 | }); 22 | 23 | await order.save(); 24 | 25 | msg.ack(); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /app/payments/src/events/listeners/__test__/OrderCancelledListener.test.ts: -------------------------------------------------------------------------------- 1 | import { 2 | EOrderStatus, 3 | IOrderCancelledEvent, 4 | } from '@webmak/microservices-common'; 5 | import { OrderCancelledListener } from 'events/listeners/OrderCancelledListener'; 6 | import { Order } from 'models/Order'; 7 | import mongoose from 'mongoose'; 8 | import { Message } from 'node-nats-streaming'; 9 | import { natsWrapper } from '__mocks__/NatsWrapper'; 10 | 11 | const setup = async () => { 12 | // @ts-ignore 13 | const listener = new OrderCancelledListener(natsWrapper.client); 14 | 15 | const order = Order.build({ 16 | id: mongoose.Types.ObjectId().toHexString(), 17 | status: EOrderStatus.Created, 18 | price: 10, 19 | userId: 'alskdjf', 20 | version: 0, 21 | }); 22 | 23 | await order.save(); 24 | 25 | const data: IOrderCancelledEvent['data'] = { 26 | id: order.id, 27 | version: order.version + 1, 28 | ticket: { 29 | id: 'alskdjf', 30 | }, 31 | }; 32 | 33 | //@ts-ignore 34 | const msg: Message = { 35 | ack: jest.fn(), 36 | }; 37 | 38 | return { listener, data, msg, order }; 39 | }; 40 | 41 | it('updates the status of the order', async () => { 42 | const { listener, data, msg, order } = await setup(); 43 | 44 | await listener.onMessage(data, msg); 45 | 46 | const updateOrder = await Order.findById(order.id); 47 | 48 | expect(updateOrder!.status).toEqual(EOrderStatus.Cancelled); 49 | }); 50 | 51 | it('acks the message', async () => { 52 | const { listener, data, msg } = await setup(); 53 | 54 | await listener.onMessage(data, msg); 55 | 56 | expect(msg.ack).toHaveBeenCalled(); 57 | }); 58 | -------------------------------------------------------------------------------- /app/payments/src/events/listeners/__test__/OrderCreatedListener.test.ts: -------------------------------------------------------------------------------- 1 | import { EOrderStatus, IOrderCreatedEvent } from '@webmak/microservices-common'; 2 | import { OrderCreatedListener } from 'events/listeners/OrderCreatedListener'; 3 | import { Order } from 'models/Order'; 4 | import mongoose from 'mongoose'; 5 | import { Message } from 'node-nats-streaming'; 6 | import { natsWrapper } from '__mocks__/NatsWrapper'; 7 | 8 | const setup = async () => { 9 | // @ts-ignore 10 | const listener = new OrderCreatedListener(natsWrapper.client); 11 | 12 | const data: IOrderCreatedEvent['data'] = { 13 | id: mongoose.Types.ObjectId().toHexString(), 14 | version: 0, 15 | expiresAt: 'alskdfjf', 16 | userId: 'alskdjf', 17 | status: EOrderStatus.Created, 18 | ticket: { 19 | id: 'alskdfj', 20 | price: 10, 21 | }, 22 | }; 23 | 24 | //@ts-ignore 25 | const msg: Message = { 26 | ack: jest.fn(), 27 | }; 28 | 29 | return { listener, data, msg }; 30 | }; 31 | 32 | it('replicated the order info', async () => { 33 | const { listener, data, msg } = await setup(); 34 | 35 | await listener.onMessage(data, msg); 36 | 37 | const order = await Order.findById(data.id); 38 | 39 | expect(order!.price).toEqual(data.ticket.price); 40 | }); 41 | 42 | it('acks the message', async () => { 43 | const { listener, data, msg } = await setup(); 44 | 45 | await listener.onMessage(data, msg); 46 | 47 | expect(msg.ack).toHaveBeenCalled(); 48 | }); 49 | -------------------------------------------------------------------------------- /app/payments/src/events/listeners/queueGroupName.ts: -------------------------------------------------------------------------------- 1 | export const queueGroupName = 'payments-service'; 2 | -------------------------------------------------------------------------------- /app/payments/src/events/publishers/PaymentCreatedPublisher.ts: -------------------------------------------------------------------------------- 1 | import { 2 | APublisher, 3 | ESubjects, 4 | IPaymentCreatedEvent, 5 | } from '@webmak/microservices-common'; 6 | 7 | export class PaymentCreatedPublisher extends APublisher { 8 | subject: ESubjects.PaymentCreated = ESubjects.PaymentCreated; 9 | } 10 | -------------------------------------------------------------------------------- /app/payments/src/index.ts: -------------------------------------------------------------------------------- 1 | import { app } from 'app'; 2 | import { OrderCancelledListener } from 'events/listeners/OrderCancelledListener'; 3 | import { OrderCreatedListener } from 'events/listeners/OrderCreatedListener'; 4 | import mongoose from 'mongoose'; 5 | import { natsWrapper } from 'NatsWrapper'; 6 | 7 | const start = async () => { 8 | if (!process.env.JWT_KEY) { 9 | throw new Error('[Payments] JWT_KEY must be defined'); 10 | } 11 | 12 | if (!process.env.MONGO_URI) { 13 | throw new Error('[Payments] MONGO_URI must be defined'); 14 | } 15 | 16 | if (!process.env.NATS_CLUSTER_ID) { 17 | throw new Error('[Payments] NATS_CLUSTER_ID must be defined'); 18 | } 19 | 20 | if (!process.env.NATS_CLIENT_ID) { 21 | throw new Error('[Payments] NATS_CLIENT_ID must be defined'); 22 | } 23 | 24 | if (!process.env.NATS_URL) { 25 | throw new Error('[Payments] NATS_URL must be defined'); 26 | } 27 | 28 | if (!process.env.STRIPE_KEY) { 29 | throw new Error('[Payments] STRIPE_KEY must be defined'); 30 | } 31 | 32 | try { 33 | await natsWrapper.connect( 34 | process.env.NATS_CLUSTER_ID, 35 | process.env.NATS_CLIENT_ID, 36 | process.env.NATS_URL 37 | ); 38 | 39 | natsWrapper.client.on('close', () => { 40 | console.log('[Payments] NATS connection closed!'); 41 | process.exit(); 42 | }); 43 | 44 | process.on('SIGINT', () => natsWrapper.client.close()); 45 | process.on('SIGTERM', () => natsWrapper.client.close()); 46 | 47 | new OrderCreatedListener(natsWrapper.client).listen(); 48 | new OrderCancelledListener(natsWrapper.client).listen(); 49 | 50 | await mongoose.connect(process.env.MONGO_URI, { 51 | useNewUrlParser: true, 52 | useUnifiedTopology: true, 53 | useCreateIndex: true, 54 | }); 55 | 56 | console.log('[Payments] Connected to MongoDB'); 57 | } catch (err) { 58 | console.log(err); 59 | } 60 | }; 61 | 62 | app.listen(3000, () => { 63 | console.log('[Payments] Listening on port 3000'); 64 | }); 65 | 66 | start(); 67 | -------------------------------------------------------------------------------- /app/payments/src/models/Order.ts: -------------------------------------------------------------------------------- 1 | import { EOrderStatus } from '@webmak/microservices-common'; 2 | import mongoose from 'mongoose'; 3 | import { updateIfCurrentPlugin } from 'mongoose-update-if-current'; 4 | 5 | interface IOrderAttrs { 6 | id: string; 7 | version: number; 8 | userId: string; 9 | price: number; 10 | status: EOrderStatus; 11 | } 12 | 13 | interface IOrderDoc extends mongoose.Document { 14 | version: number; 15 | userId: string; 16 | price: number; 17 | status: EOrderStatus; 18 | } 19 | 20 | interface IOrderModel extends mongoose.Model { 21 | build(attrs: IOrderAttrs): IOrderDoc; 22 | } 23 | 24 | const orderSchema = new mongoose.Schema( 25 | { 26 | userId: { 27 | type: String, 28 | required: true, 29 | }, 30 | price: { 31 | type: Number, 32 | required: true, 33 | }, 34 | status: { 35 | type: String, 36 | required: true, 37 | }, 38 | }, 39 | { 40 | toJSON: { 41 | transform(_doc, ret) { 42 | ret.id = ret._id; 43 | delete ret._id; 44 | }, 45 | }, 46 | } 47 | ); 48 | 49 | orderSchema.set('versionKey', 'version'); 50 | orderSchema.plugin(updateIfCurrentPlugin); 51 | 52 | orderSchema.statics.build = (attrs: IOrderAttrs) => { 53 | return new Order({ 54 | _id: attrs.id, 55 | version: attrs.version, 56 | price: attrs.price, 57 | userId: attrs.userId, 58 | status: attrs.status, 59 | }); 60 | }; 61 | 62 | const Order = mongoose.model('Order', orderSchema); 63 | 64 | export { Order }; 65 | -------------------------------------------------------------------------------- /app/payments/src/models/Payment.ts: -------------------------------------------------------------------------------- 1 | import mongoose from 'mongoose'; 2 | 3 | interface IPaymentAttrs { 4 | orderId: string; 5 | stripeId: string; 6 | } 7 | 8 | interface IPaymentDoc extends mongoose.Document { 9 | orderId: string; 10 | stripeId: string; 11 | } 12 | 13 | interface IPaymentModel extends mongoose.Model { 14 | build(attrs: IPaymentAttrs): IPaymentDoc; 15 | } 16 | 17 | const paymentSchema = new mongoose.Schema( 18 | { 19 | orderId: { 20 | type: String, 21 | required: true, 22 | }, 23 | stripeId: { 24 | type: String, 25 | required: true, 26 | }, 27 | }, 28 | { 29 | toJSON: { 30 | transform(_doc, ret) { 31 | ret.id = ret._id; 32 | delete ret._id; 33 | }, 34 | }, 35 | } 36 | ); 37 | 38 | paymentSchema.statics.build = (attrs: IPaymentAttrs) => { 39 | return new Payment(attrs); 40 | }; 41 | 42 | const Payment = mongoose.model( 43 | 'Payment', 44 | paymentSchema 45 | ); 46 | 47 | export { Payment }; 48 | -------------------------------------------------------------------------------- /app/payments/src/routes/__test__/new.test.ts: -------------------------------------------------------------------------------- 1 | import { EOrderStatus } from '@webmak/microservices-common'; 2 | import { app } from 'app'; 3 | import { Order } from 'models/Order'; 4 | import { Payment } from 'models/Payment'; 5 | import mongoose from 'mongoose'; 6 | import { stripe } from 'stripeApp'; 7 | import request from 'supertest'; 8 | 9 | // jest.mock('__mocks__/stripe'); 10 | 11 | it('returns a 404 when purchasing an order that does not exist', async () => { 12 | await request(app) 13 | .post('/api/payments') 14 | .set('Cookie', global.signin()) 15 | .send({ 16 | token: 'asdfkfj', 17 | orderId: mongoose.Types.ObjectId().toHexString(), 18 | }) 19 | .expect(404); 20 | }); 21 | 22 | it('returns a 401 when purchasing an order that does not belogns to the user', async () => { 23 | const order = Order.build({ 24 | id: mongoose.Types.ObjectId().toHexString(), 25 | userId: mongoose.Types.ObjectId().toHexString(), 26 | version: 0, 27 | price: 20, 28 | status: EOrderStatus.Created, 29 | }); 30 | 31 | await order.save(); 32 | 33 | await request(app) 34 | .post('/api/payments') 35 | .set('Cookie', global.signin()) 36 | .send({ 37 | token: 'asdfkfj', 38 | orderId: order.id, 39 | }) 40 | .expect(401); 41 | }); 42 | 43 | it('returns a 400 when purchasing a cancelled order', async () => { 44 | const userId = mongoose.Types.ObjectId().toHexString(); 45 | const order = Order.build({ 46 | id: mongoose.Types.ObjectId().toHexString(), 47 | userId, 48 | version: 0, 49 | price: 20, 50 | status: EOrderStatus.Cancelled, 51 | }); 52 | 53 | await order.save(); 54 | 55 | await request(app) 56 | .post('/api/payments') 57 | .set('Cookie', global.signin(userId)) 58 | .send({ 59 | orderId: order.id, 60 | token: 'asdlkfj', 61 | }) 62 | .expect(400); 63 | }); 64 | 65 | it('returns a 201 with valid inputs', async () => { 66 | const userId = mongoose.Types.ObjectId().toHexString(); 67 | const price = Math.floor(Math.random() * 1000000); 68 | const order = Order.build({ 69 | id: mongoose.Types.ObjectId().toHexString(), 70 | userId, 71 | version: 0, 72 | price, 73 | status: EOrderStatus.Created, 74 | }); 75 | 76 | await order.save(); 77 | 78 | await request(app) 79 | .post('/api/payments') 80 | .set('Cookie', global.signin(userId)) 81 | .send({ 82 | token: 'tok_visa', 83 | orderId: order.id, 84 | }) 85 | .expect(201); 86 | 87 | const stripeCharges = await stripe.charges.list({ limit: 50 }); 88 | const stripeCharge = stripeCharges.data.find((charge: any) => { 89 | return charge.amount === price * 100; 90 | }); 91 | 92 | expect(stripeCharge).toBeDefined(); 93 | expect(stripeCharge?.currency).toEqual('usd'); 94 | 95 | const payment = await Payment.findOne({ 96 | orderId: order.id, 97 | stripeId: stripeCharge!.id, 98 | }); 99 | 100 | expect(payment).not.toBeNull(); 101 | }); 102 | -------------------------------------------------------------------------------- /app/payments/src/routes/new.ts: -------------------------------------------------------------------------------- 1 | import { 2 | BadRequestError, 3 | EOrderStatus, 4 | NotAuthorizedError, 5 | NotFoundError, 6 | requireAuth, 7 | validateRequest, 8 | } from '@webmak/microservices-common'; 9 | import { PaymentCreatedPublisher } from 'events/publishers/PaymentCreatedPublisher'; 10 | import express, { Request, Response } from 'express'; 11 | import { body } from 'express-validator'; 12 | import { Order } from 'models/Order'; 13 | import { Payment } from 'models/Payment'; 14 | import { natsWrapper } from 'NatsWrapper'; 15 | import { stripe } from 'stripeApp'; 16 | 17 | const router = express.Router(); 18 | router.post( 19 | '/api/payments', 20 | requireAuth, 21 | [body('token').not().isEmpty(), body('orderId').not().isEmpty()], 22 | validateRequest, 23 | async (req: Request, res: Response) => { 24 | const { token, orderId } = req.body; 25 | 26 | const order = await Order.findById(orderId); 27 | 28 | if (!order) { 29 | throw new NotFoundError(); 30 | } 31 | 32 | if (order.userId !== req.currentUser!.id) { 33 | throw new NotAuthorizedError(); 34 | } 35 | 36 | if (order.status === EOrderStatus.Cancelled) { 37 | throw new BadRequestError('[Payments] Cannot pay for an cancelled order'); 38 | } 39 | 40 | const charge = await stripe.charges.create({ 41 | currency: 'usd', 42 | amount: order.price * 100, 43 | source: token, 44 | }); 45 | 46 | const payment = Payment.build({ 47 | orderId, 48 | stripeId: charge.id, 49 | }); 50 | 51 | await payment.save(); 52 | 53 | new PaymentCreatedPublisher(natsWrapper.client).publish({ 54 | id: payment.id, 55 | orderId: payment.orderId, 56 | stripeId: payment.stripeId, 57 | }); 58 | 59 | return res.status(201).send({ id: payment.id }); 60 | } 61 | ); 62 | 63 | export { router as createChargeRouter }; 64 | -------------------------------------------------------------------------------- /app/payments/src/stripeApp.ts: -------------------------------------------------------------------------------- 1 | import Stripe from 'stripe'; 2 | 3 | export const stripe = new Stripe(process.env.STRIPE_KEY!, { 4 | apiVersion: '2020-08-27', 5 | }); 6 | -------------------------------------------------------------------------------- /app/payments/src/test/setup.ts: -------------------------------------------------------------------------------- 1 | import jwt from 'jsonwebtoken'; 2 | import { MongoMemoryServer } from 'mongodb-memory-server'; 3 | import mongoose from 'mongoose'; 4 | 5 | declare global { 6 | namespace NodeJS { 7 | interface Global { 8 | signin(id?: string): string[]; 9 | } 10 | } 11 | } 12 | 13 | jest.setTimeout(10000); 14 | jest.mock('__mocks__/NatsWrapper'); 15 | 16 | process.env.STRIPE_KEY = ''; 17 | 18 | let mongo: any; 19 | 20 | beforeAll(async () => { 21 | process.env.JWT_KEY = 'MY_JWT_SECRET'; 22 | mongo = new MongoMemoryServer(); 23 | const mongoUri = await mongo.getUri(); 24 | 25 | await mongoose.connect(mongoUri, { 26 | useNewUrlParser: true, 27 | useUnifiedTopology: true, 28 | }); 29 | }); 30 | 31 | beforeEach(async () => { 32 | jest.clearAllMocks(); 33 | 34 | const collections = await mongoose.connection.db.collections(); 35 | 36 | for (let collection of collections) { 37 | await collection.deleteMany({}); 38 | } 39 | }); 40 | 41 | afterAll(async () => { 42 | await mongo.stop(); 43 | await mongoose.connection.close(); 44 | }); 45 | 46 | global.signin = (id?: string) => { 47 | const payload = { 48 | id: id || new mongoose.Types.ObjectId().toHexString(), 49 | email: 'test@test.com', 50 | }; 51 | 52 | const token = jwt.sign(payload, process.env.JWT_KEY!); 53 | 54 | const session = { jwt: token }; 55 | 56 | const sessionJSON = JSON.stringify(session); 57 | 58 | const base64 = Buffer.from(sessionJSON).toString('base64'); 59 | 60 | return [`express:sess=${base64}`]; 61 | }; 62 | -------------------------------------------------------------------------------- /app/tickets/.dockerignore: -------------------------------------------------------------------------------- 1 | **/.eslintignore 2 | **/.eslintrc 3 | **/.prettierignore 4 | **/.prettierrc 5 | 6 | **/node_modules/ 7 | **/node_modules_linux/ 8 | **/Dockerfile 9 | **/.dockerignore 10 | **/.gitignore 11 | **/.git 12 | **/README.md 13 | **/LICENSE 14 | **/.vscode 15 | 16 | **/test/ 17 | **/.next/ -------------------------------------------------------------------------------- /app/tickets/.eslintignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | -------------------------------------------------------------------------------- /app/tickets/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es2021": true 5 | }, 6 | "extends": [ 7 | "eslint:recommended", 8 | "plugin:@typescript-eslint/recommended", 9 | "plugin:@typescript-eslint/recommended-requiring-type-checking" 10 | ], 11 | "parser": "@typescript-eslint/parser", 12 | "parserOptions": { 13 | "ecmaVersion": 12, 14 | "project": "tsconfig.eslint.json" 15 | }, 16 | "plugins": ["@typescript-eslint"], 17 | "rules": { 18 | "prefer-const": "error", 19 | "@typescript-eslint/no-unused-vars": "off", 20 | "@typescript-eslint/no-unused-params": "off" 21 | }, 22 | "overrides": [ 23 | { 24 | "files": ["tests/**/*.ts"], 25 | "env": { "jest": true, "node": true } 26 | } 27 | ] 28 | } 29 | -------------------------------------------------------------------------------- /app/tickets/.huskyrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "hooks": { 3 | "pre-commit": "lint-staged", 4 | "pre-push": "npm run test" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /app/tickets/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:lts-alpine3.13 2 | 3 | WORKDIR /app 4 | COPY package.json ./ 5 | # RUN npm config set //npm.pkg.github.com/:_authToken=43fb630d62fa9b844d62c9faf11f4e6e9b62exxx 6 | # RUN npm config set @webmakaka:registry https://npm.pkg.github.com/webmakaka 7 | RUN npm install --only=prod 8 | COPY ./ ./ 9 | CMD ["npm", "start"] -------------------------------------------------------------------------------- /app/tickets/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tickets", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "NODE_PATH=./src ts-node-dev src/index.ts", 8 | "test": "NODE_PATH=./src jest --watchAll --no-cache", 9 | "test:ci": "NODE_PATH=./src jest", 10 | "lint": "eslint src --ext .js,.ts,.jsx,.tsx", 11 | "precommit": "lint-staged" 12 | }, 13 | "jest": { 14 | "preset": "ts-jest", 15 | "testEnvironment": "node", 16 | "setupFilesAfterEnv": [ 17 | "./src/test/setup.ts" 18 | ] 19 | }, 20 | "keywords": [], 21 | "author": "", 22 | "license": "ISC", 23 | "dependencies": { 24 | "@types/cookie-session": "^2.0.42", 25 | "@types/express": "^4.17.11", 26 | "@types/jsonwebtoken": "^8.5.0", 27 | "@types/mongoose": "^5.10.3", 28 | "@webmak/microservices-common": "^1.0.6", 29 | "cookie-session": "^1.4.0", 30 | "express": "^4.17.1", 31 | "express-async-errors": "^3.1.1", 32 | "express-validator": "^6.10.0", 33 | "jsonwebtoken": "^8.5.1", 34 | "mongoose": "^5.12.0", 35 | "mongoose-update-if-current": "^1.4.0", 36 | "ts-node-dev": "^1.1.6", 37 | "typescript": "^4.2.3" 38 | }, 39 | "devDependencies": { 40 | "@types/jest": "^26.0.20", 41 | "@types/supertest": "^2.0.10", 42 | "@typescript-eslint/eslint-plugin": "^4.17.0", 43 | "@typescript-eslint/parser": "^4.17.0", 44 | "eslint": "^7.22.0", 45 | "husky": "^6.0.0", 46 | "jest": "^26.6.3", 47 | "lint-staged": "^10.5.4", 48 | "mongodb-memory-server": "^6.9.6", 49 | "supertest": "^6.1.3", 50 | "ts-jest": "^26.5.3" 51 | }, 52 | "lint-staged": { 53 | "*.{js, jsx}": [ 54 | "node_modules/.bin/eslint --max-warnings=0", 55 | "prettier --write", 56 | "git add" 57 | ] 58 | }, 59 | "volta": { 60 | "node": "14.16.0", 61 | "npm": "7.6.0" 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /app/tickets/src/NatsWrapper.ts: -------------------------------------------------------------------------------- 1 | import nats, { Stan } from 'node-nats-streaming'; 2 | 3 | class NatsWrapper { 4 | private _client?: Stan; 5 | 6 | get client() { 7 | if (!this._client) { 8 | throw new Error('[Tickets] Cannot access NATS client before connecting'); 9 | } 10 | return this._client; 11 | } 12 | 13 | connect(clusterId: string, clientId: string, url: string) { 14 | this._client = nats.connect(clusterId, clientId, { url }); 15 | 16 | return new Promise((resolve, reject) => { 17 | this.client.on('connect', () => { 18 | console.log('[Tickets] Connected to NATS'); 19 | resolve('OK!'); 20 | }); 21 | 22 | this.client.on('error', (err) => { 23 | reject(err); 24 | }); 25 | }); 26 | } 27 | } 28 | 29 | export const natsWrapper = new NatsWrapper(); 30 | -------------------------------------------------------------------------------- /app/tickets/src/__mocks__/NatsWrapper.ts: -------------------------------------------------------------------------------- 1 | export const natsWrapper = { 2 | client: { 3 | publish: jest 4 | .fn() 5 | .mockImplementation( 6 | (_subject: string, _data: string, callback: () => void) => { 7 | callback(); 8 | } 9 | ), 10 | }, 11 | }; 12 | -------------------------------------------------------------------------------- /app/tickets/src/app.ts: -------------------------------------------------------------------------------- 1 | import { 2 | currentUser, 3 | errorHandler, 4 | NotFoundError, 5 | } from '@webmak/microservices-common'; 6 | import { json } from 'body-parser'; 7 | import cookieSession from 'cookie-session'; 8 | import express from 'express'; 9 | import 'express-async-errors'; 10 | import { indexTicketRouter } from 'routes/index'; 11 | import { createTicketRouter } from 'routes/new'; 12 | import { showTicketRouter } from 'routes/show'; 13 | import { updateTicketRouter } from 'routes/update'; 14 | 15 | const app = express(); 16 | app.set('trust proxy', true); 17 | app.use(json()); 18 | app.use( 19 | cookieSession({ 20 | signed: false, 21 | secure: process.env.NODE_ENV !== 'test', 22 | }) 23 | ); 24 | 25 | app.use(currentUser); 26 | app.use(createTicketRouter); 27 | app.use(showTicketRouter); 28 | app.use(indexTicketRouter); 29 | app.use(updateTicketRouter); 30 | 31 | app.all('*', async (req, res) => { 32 | throw new NotFoundError(); 33 | }); 34 | 35 | app.use(errorHandler); 36 | 37 | export { app }; 38 | -------------------------------------------------------------------------------- /app/tickets/src/events/listeners/OrderCancelledListener.ts: -------------------------------------------------------------------------------- 1 | import { 2 | AListener, 3 | ESubjects, 4 | IOrderCancelledEvent, 5 | } from '@webmak/microservices-common'; 6 | import { queueGroupName } from 'events/listeners/queueGroupName'; 7 | import { TicketUpdatedPublisher } from 'events/publishers/TicketUpdatedPublisher'; 8 | import { Ticket } from 'models/Ticket'; 9 | import { Message } from 'node-nats-streaming'; 10 | 11 | export class OrderCancelledListener extends AListener { 12 | subject: ESubjects.OrderCancelled = ESubjects.OrderCancelled; 13 | queueGroupName = queueGroupName; 14 | 15 | async onMessage(data: IOrderCancelledEvent['data'], msg: Message) { 16 | const ticket = await Ticket.findById(data.ticket.id); 17 | if (!ticket) { 18 | throw new Error('[Tickets] Ticket not found'); 19 | } 20 | 21 | ticket.set({ orderId: undefined }); 22 | 23 | await ticket.save(); 24 | 25 | await new TicketUpdatedPublisher(this.client).publish({ 26 | id: ticket.id, 27 | price: ticket.price, 28 | title: ticket.title, 29 | userId: ticket.userId, 30 | orderId: ticket.orderId, 31 | version: ticket.version, 32 | }); 33 | 34 | msg.ack(); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /app/tickets/src/events/listeners/OrderCreatedListener.ts: -------------------------------------------------------------------------------- 1 | import { 2 | AListener, 3 | ESubjects, 4 | IOrderCreatedEvent, 5 | } from '@webmak/microservices-common'; 6 | import { queueGroupName } from 'events/listeners/queueGroupName'; 7 | import { TicketUpdatedPublisher } from 'events/publishers/TicketUpdatedPublisher'; 8 | import { Ticket } from 'models/Ticket'; 9 | import { Message } from 'node-nats-streaming'; 10 | 11 | export class OrderCreatedListener extends AListener { 12 | subject: ESubjects.OrderCreated = ESubjects.OrderCreated; 13 | queueGroupName = queueGroupName; 14 | 15 | async onMessage(data: IOrderCreatedEvent['data'], msg: Message) { 16 | const ticket = await Ticket.findById(data.ticket.id); 17 | if (!ticket) { 18 | throw new Error('[Tickets] Ticket not found'); 19 | } 20 | 21 | ticket.set({ orderId: data.id }); 22 | 23 | await ticket.save(); 24 | await new TicketUpdatedPublisher(this.client).publish({ 25 | id: ticket.id, 26 | price: ticket.price, 27 | title: ticket.title, 28 | userId: ticket.userId, 29 | orderId: ticket.orderId, 30 | version: ticket.version, 31 | }); 32 | 33 | msg.ack(); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /app/tickets/src/events/listeners/__test__/OrderCancelledListener.test.ts: -------------------------------------------------------------------------------- 1 | import { IOrderCancelledEvent } from '@webmak/microservices-common'; 2 | import { OrderCancelledListener } from 'events/listeners/OrderCancelledListener'; 3 | import { Ticket } from 'models/Ticket'; 4 | import mongoose from 'mongoose'; 5 | import { Message } from 'node-nats-streaming'; 6 | import { natsWrapper } from '__mocks__//NatsWrapper'; 7 | 8 | const setup = async () => { 9 | // @ts-ignore 10 | const listener = new OrderCancelledListener(natsWrapper.client); 11 | 12 | const orderId = mongoose.Types.ObjectId().toHexString(); 13 | 14 | const ticket = Ticket.build({ 15 | title: 'concert', 16 | price: 99, 17 | userId: 'asdf', 18 | }); 19 | 20 | ticket.set({ orderId }); 21 | await ticket.save(); 22 | 23 | const data: IOrderCancelledEvent['data'] = { 24 | id: mongoose.Types.ObjectId().toHexString(), 25 | version: 0, 26 | ticket: { 27 | id: ticket.id, 28 | }, 29 | }; 30 | 31 | // @ts-ignore 32 | const msg: Message = { 33 | ack: jest.fn(), 34 | }; 35 | 36 | return { listener, ticket, data, msg, orderId }; 37 | }; 38 | 39 | it('updates the ticket, publishes an event and acks the message', async () => { 40 | const { listener, ticket, data, msg } = await setup(); 41 | 42 | await listener.onMessage(data, msg); 43 | 44 | const updatedTicket = await Ticket.findById(ticket.id); 45 | expect(updatedTicket!.orderId).not.toBeDefined(); 46 | expect(msg.ack).toHaveBeenCalled(); 47 | expect(natsWrapper.client.publish).toHaveBeenCalled(); 48 | }); 49 | -------------------------------------------------------------------------------- /app/tickets/src/events/listeners/__test__/OrderCreatedListener.test.ts: -------------------------------------------------------------------------------- 1 | import { EOrderStatus, IOrderCreatedEvent } from '@webmak/microservices-common'; 2 | import { OrderCreatedListener } from 'events/listeners/OrderCreatedListener'; 3 | import { Ticket } from 'models/Ticket'; 4 | import mongoose from 'mongoose'; 5 | import { Message } from 'node-nats-streaming'; 6 | import { natsWrapper } from '__mocks__/NatsWrapper'; 7 | 8 | const setup = async () => { 9 | // @ts-ignore 10 | const listener = new OrderCreatedListener(natsWrapper.client); 11 | 12 | const ticket = Ticket.build({ 13 | title: 'concert', 14 | price: 99, 15 | userId: 'asdf', 16 | }); 17 | 18 | await ticket.save(); 19 | 20 | const data: IOrderCreatedEvent['data'] = { 21 | id: mongoose.Types.ObjectId().toHexString(), 22 | version: 0, 23 | status: EOrderStatus.Created, 24 | userId: 'adfadsfa', 25 | expiresAt: 'adfadsf', 26 | ticket: { 27 | id: ticket.id, 28 | price: ticket.price, 29 | }, 30 | }; 31 | 32 | // @ts-ignore 33 | const msg: Message = { 34 | ack: jest.fn(), 35 | }; 36 | 37 | return { listener, ticket, data, msg }; 38 | }; 39 | 40 | it('sets the userId of the ticket', async () => { 41 | const { listener, ticket, data, msg } = await setup(); 42 | await listener.onMessage(data, msg); 43 | const updatedTicket = await Ticket.findById(ticket.id); 44 | expect(updatedTicket!.orderId).toEqual(data.id); 45 | }); 46 | 47 | it('acks the message', async () => { 48 | const { listener, data, msg } = await setup(); 49 | 50 | await listener.onMessage(data, msg); 51 | expect(msg.ack).toHaveBeenCalled(); 52 | }); 53 | 54 | it('publishes a ticket updated event', async () => { 55 | const { listener, data, msg } = await setup(); 56 | 57 | await listener.onMessage(data, msg); 58 | expect(natsWrapper.client.publish).toHaveBeenCalled(); 59 | 60 | const ticketUpdatedData = JSON.parse( 61 | (natsWrapper.client.publish as jest.Mock).mock.calls[0][1] 62 | ); 63 | 64 | expect(data.id).toEqual(ticketUpdatedData.orderId); 65 | }); 66 | -------------------------------------------------------------------------------- /app/tickets/src/events/listeners/queueGroupName.ts: -------------------------------------------------------------------------------- 1 | export const queueGroupName = 'tickets-service'; 2 | -------------------------------------------------------------------------------- /app/tickets/src/events/publishers/TicketCreatedPublisher.ts: -------------------------------------------------------------------------------- 1 | import { 2 | APublisher, 3 | ESubjects, 4 | ITicketCreatedEvent, 5 | } from '@webmak/microservices-common'; 6 | 7 | export class TicketCreatedPublisher extends APublisher { 8 | subject: ESubjects.TicketCreated = ESubjects.TicketCreated; 9 | } 10 | -------------------------------------------------------------------------------- /app/tickets/src/events/publishers/TicketUpdatedPublisher.ts: -------------------------------------------------------------------------------- 1 | import { 2 | APublisher, 3 | ESubjects, 4 | ITicketUpdatedEvent, 5 | } from '@webmak/microservices-common'; 6 | 7 | export class TicketUpdatedPublisher extends APublisher { 8 | subject: ESubjects.TicketUpdated = ESubjects.TicketUpdated; 9 | } 10 | -------------------------------------------------------------------------------- /app/tickets/src/index.ts: -------------------------------------------------------------------------------- 1 | import { app } from 'app'; 2 | import { OrderCancelledListener } from 'events/listeners/OrderCancelledListener'; 3 | import { OrderCreatedListener } from 'events/listeners/OrderCreatedListener'; 4 | import mongoose from 'mongoose'; 5 | import { natsWrapper } from 'NatsWrapper'; 6 | 7 | const start = async () => { 8 | if (!process.env.JWT_KEY) { 9 | throw new Error('[Tickets] JWT_KEY must be defined'); 10 | } 11 | 12 | if (!process.env.MONGO_URI) { 13 | throw new Error('[Tickets] MONGO_URI must be defined'); 14 | } 15 | 16 | if (!process.env.NATS_CLUSTER_ID) { 17 | throw new Error('[Tickets] NATS_CLUSTER_ID must be defined'); 18 | } 19 | 20 | if (!process.env.NATS_CLIENT_ID) { 21 | throw new Error('[Tickets] NATS_CLIENT_ID must be defined'); 22 | } 23 | 24 | if (!process.env.NATS_URL) { 25 | throw new Error('[Tickets] NATS_URL must be defined'); 26 | } 27 | 28 | try { 29 | await natsWrapper.connect( 30 | process.env.NATS_CLUSTER_ID, 31 | process.env.NATS_CLIENT_ID, 32 | process.env.NATS_URL 33 | ); 34 | 35 | natsWrapper.client.on('close', () => { 36 | console.log('[Tickets] NATS connection closed!'); 37 | process.exit(); 38 | }); 39 | 40 | process.on('SIGINT', () => natsWrapper.client.close()); 41 | process.on('SIGTERM', () => natsWrapper.client.close()); 42 | 43 | new OrderCreatedListener(natsWrapper.client).listen(); 44 | new OrderCancelledListener(natsWrapper.client).listen(); 45 | 46 | await mongoose.connect(process.env.MONGO_URI, { 47 | useNewUrlParser: true, 48 | useUnifiedTopology: true, 49 | useCreateIndex: true, 50 | }); 51 | 52 | console.log('[Tickets] Connected to MongoDB'); 53 | } catch (err) { 54 | console.log(err); 55 | } 56 | }; 57 | 58 | app.listen(3000, () => { 59 | console.log('[Tickets] Listening on port 3000'); 60 | }); 61 | 62 | start(); 63 | -------------------------------------------------------------------------------- /app/tickets/src/models/Ticket.ts: -------------------------------------------------------------------------------- 1 | import mongoose from 'mongoose'; 2 | import { updateIfCurrentPlugin } from 'mongoose-update-if-current'; 3 | 4 | interface ITicketAttrs { 5 | title: string; 6 | price: number; 7 | userId: string; 8 | } 9 | 10 | interface ITicketDoc extends mongoose.Document { 11 | title: string; 12 | price: number; 13 | userId: string; 14 | version: number; 15 | orderId?: string; 16 | } 17 | 18 | interface ITicketModel extends mongoose.Model { 19 | build(attrs: ITicketAttrs): ITicketDoc; 20 | } 21 | 22 | const ticketSchema = new mongoose.Schema( 23 | { 24 | title: { 25 | type: String, 26 | required: true, 27 | }, 28 | price: { 29 | type: Number, 30 | required: true, 31 | }, 32 | userId: { 33 | type: String, 34 | required: true, 35 | }, 36 | orderId: { 37 | type: String, 38 | }, 39 | }, 40 | { 41 | toJSON: { 42 | transform(_doc, ret) { 43 | ret.id = ret._id; 44 | delete ret._id; 45 | }, 46 | }, 47 | } 48 | ); 49 | 50 | ticketSchema.set('versionKey', 'version'); 51 | ticketSchema.plugin(updateIfCurrentPlugin); 52 | 53 | ticketSchema.statics.build = (attrs: ITicketAttrs) => { 54 | return new Ticket(attrs); 55 | }; 56 | 57 | const Ticket = mongoose.model('Ticket', ticketSchema); 58 | 59 | export { Ticket }; 60 | -------------------------------------------------------------------------------- /app/tickets/src/models/__test__/Ticket.test.ts: -------------------------------------------------------------------------------- 1 | import { Ticket } from 'models/Ticket'; 2 | 3 | it('Implements optimistic concurrency control', async (done) => { 4 | const ticket = Ticket.build({ 5 | title: 'concert', 6 | price: 5, 7 | userId: '123', 8 | }); 9 | 10 | await ticket.save(); 11 | 12 | const firsetInstance = await Ticket.findById(ticket.id); 13 | const secondInstance = await Ticket.findById(ticket.id); 14 | 15 | firsetInstance!.set({ price: 10 }); 16 | secondInstance!.set({ price: 15 }); 17 | 18 | await firsetInstance!.save(); 19 | 20 | try { 21 | await secondInstance!.save(); 22 | } catch (err) { 23 | return done(); 24 | } 25 | 26 | throw new Error('Should not reach this point'); 27 | }); 28 | 29 | it('Increments the version number on multiple saves', async () => { 30 | const ticket = Ticket.build({ 31 | title: 'concert', 32 | price: 20, 33 | userId: '123', 34 | }); 35 | 36 | await ticket.save(); 37 | expect(ticket.version).toEqual(0); 38 | 39 | await ticket.save(); 40 | expect(ticket.version).toEqual(1); 41 | 42 | await ticket.save(); 43 | expect(ticket.version).toEqual(2); 44 | }); 45 | -------------------------------------------------------------------------------- /app/tickets/src/routes/__test__/index.test.ts: -------------------------------------------------------------------------------- 1 | import { app } from 'app'; 2 | import request from 'supertest'; 3 | 4 | const createTicket = () => { 5 | return request(app).post('/api/tickets').set('Cookie', global.signin()).send({ 6 | title: 'asldkf', 7 | price: 10, 8 | }); 9 | }; 10 | 11 | it('can fetch a list of tickets', async () => { 12 | await createTicket(); 13 | await createTicket(); 14 | await createTicket(); 15 | 16 | const response = await request(app).get('/api/tickets').send().expect(200); 17 | 18 | expect(response.body.length).toEqual(3); 19 | }); 20 | -------------------------------------------------------------------------------- /app/tickets/src/routes/__test__/new.test.ts: -------------------------------------------------------------------------------- 1 | import { app } from 'app'; 2 | import { Ticket } from 'models/Ticket'; 3 | import request from 'supertest'; 4 | import { natsWrapper } from '__mocks__/NatsWrapper'; 5 | 6 | it('has a route handler listening to /api/tickets for post requests', async () => { 7 | const response = await request(app).post('/api/tickets').send({}); 8 | 9 | expect(response.status).not.toEqual(404); 10 | }); 11 | 12 | it('can only be accessed if the user is signed in', async () => { 13 | await request(app).post('/api/tickets').send({}).expect(401); 14 | }); 15 | 16 | it('returns a status other than 401 if the user is signed in', async () => { 17 | const response = await request(app) 18 | .post('/api/tickets') 19 | .set('Cookie', global.signin()) 20 | .send({}); 21 | 22 | expect(response.status).not.toEqual(401); 23 | }); 24 | 25 | it('returns an error if an invalid title is provided', async () => { 26 | await request(app) 27 | .post('/api/tickets') 28 | .set('Cookie', global.signin()) 29 | .send({ 30 | title: '', 31 | price: 10, 32 | }) 33 | .expect(400); 34 | 35 | await request(app) 36 | .post('/api/tickets') 37 | .set('Cookie', global.signin()) 38 | .send({ 39 | price: 10, 40 | }) 41 | .expect(400); 42 | }); 43 | 44 | it('returns an error if an invalid price is provided', async () => { 45 | await request(app) 46 | .post('/api/tickets') 47 | .set('Cookie', global.signin()) 48 | .send({ 49 | title: 'asdfgkjf', 50 | price: -10, 51 | }) 52 | .expect(400); 53 | 54 | await request(app) 55 | .post('/api/tickets') 56 | .set('Cookie', global.signin()) 57 | .send({ 58 | title: 'asdfgkjf', 59 | }) 60 | .expect(400); 61 | }); 62 | 63 | it('creates a ticket with valid inputs', async () => { 64 | let tickets = await Ticket.find({}); 65 | expect(tickets.length).toEqual(0); 66 | 67 | const title = 'asdfgkjf'; 68 | 69 | await request(app) 70 | .post('/api/tickets') 71 | .set('Cookie', global.signin()) 72 | .send({ 73 | title, 74 | price: 20, 75 | }) 76 | .expect(201); 77 | 78 | tickets = await Ticket.find({}); 79 | 80 | expect(tickets.length).toEqual(1); 81 | expect(tickets[0].price).toEqual(20); 82 | expect(tickets[0].title).toEqual(title); 83 | }); 84 | 85 | it('publishes an event', async () => { 86 | const title = 'asdfgkjf'; 87 | 88 | await request(app) 89 | .post('/api/tickets') 90 | .set('Cookie', global.signin()) 91 | .send({ 92 | title, 93 | price: 20, 94 | }) 95 | .expect(201); 96 | 97 | expect(natsWrapper.client.publish).toHaveBeenCalled(); 98 | }); 99 | -------------------------------------------------------------------------------- /app/tickets/src/routes/__test__/show.test.ts: -------------------------------------------------------------------------------- 1 | import { app } from 'app'; 2 | import mongoose from 'mongoose'; 3 | import request from 'supertest'; 4 | 5 | it('returns a 404 if the ticket is not found', async () => { 6 | const id = new mongoose.Types.ObjectId().toHexString(); 7 | await request(app).get(`/api/tickets/${id}`).send().expect(404); 8 | }); 9 | 10 | it('returns the ticket if the ticket is found', async () => { 11 | const title = 'concert'; 12 | const price = 20; 13 | 14 | const response = await request(app) 15 | .post('/api/tickets') 16 | .set('Cookie', global.signin()) 17 | .send({ 18 | title, 19 | price, 20 | }) 21 | .expect(201); 22 | 23 | const ticketResponse = await request(app) 24 | .get(`/api/tickets/${response.body.id}`) 25 | .send() 26 | .expect(200); 27 | 28 | expect(ticketResponse.body.title).toEqual(title); 29 | expect(ticketResponse.body.price).toEqual(price); 30 | }); 31 | -------------------------------------------------------------------------------- /app/tickets/src/routes/__test__/update.test.ts: -------------------------------------------------------------------------------- 1 | import { app } from 'app'; 2 | import { Ticket } from 'models/Ticket'; 3 | import mongoose from 'mongoose'; 4 | import request from 'supertest'; 5 | import { natsWrapper } from '__mocks__/NatsWrapper'; 6 | 7 | it('return a 404 if the provided id does not exist', async () => { 8 | const id = mongoose.Types.ObjectId().toHexString(); 9 | 10 | await request(app) 11 | .put(`/api/tickets/${id}`) 12 | .set('Cookie', global.signin()) 13 | .send({ 14 | title: 'aslkdfj', 15 | price: 20, 16 | }) 17 | .expect(404); 18 | }); 19 | 20 | it('returns a 401 if the user is not authenticated', async () => { 21 | const id = mongoose.Types.ObjectId().toHexString(); 22 | 23 | await request(app) 24 | .put(`/api/tickets/${id}`) 25 | .send({ 26 | title: 'aslkdfj', 27 | price: 20, 28 | }) 29 | .expect(401); 30 | }); 31 | 32 | it('return a 401 if the user does not own the ticket', async () => { 33 | const response = await request(app) 34 | .post('/api/tickets') 35 | .set('Cookie', global.signin()) 36 | .send({ 37 | title: 'aslkdfj', 38 | price: 20, 39 | }) 40 | .expect(201); 41 | 42 | await request(app) 43 | .put(`/api/tickets/${response.body.id}`) 44 | .set('Cookie', global.signin()) 45 | .send({ 46 | title: 'aslkdfj1', 47 | price: 10, 48 | }) 49 | .expect(401); 50 | }); 51 | 52 | it('return a 400 if the user provides an invalid title or price', async () => { 53 | const cookie = global.signin(); 54 | 55 | const response = await request(app) 56 | .post('/api/tickets') 57 | .set('Cookie', cookie) 58 | .send({ 59 | title: 'aslkdfj', 60 | price: 20, 61 | }) 62 | .expect(201); 63 | 64 | await request(app) 65 | .put(`/api/tickets/${response.body.id}`) 66 | .set('Cookie', cookie) 67 | .send({ 68 | title: '', 69 | price: 20, 70 | }) 71 | .expect(400); 72 | 73 | await request(app) 74 | .put(`/api/tickets/${response.body.id}`) 75 | .set('Cookie', cookie) 76 | .send({ 77 | title: 'aslkdfj', 78 | price: -10, 79 | }) 80 | .expect(400); 81 | }); 82 | 83 | it('updates the ticket provided valid input', async () => { 84 | const cookie = global.signin(); 85 | 86 | const response = await request(app) 87 | .post('/api/tickets') 88 | .set('Cookie', cookie) 89 | .send({ 90 | title: 'aslkdfj', 91 | price: 20, 92 | }) 93 | .expect(201); 94 | 95 | await request(app) 96 | .put(`/api/tickets/${response.body.id}`) 97 | .set('Cookie', cookie) 98 | .send({ 99 | title: 'new title', 100 | price: 100, 101 | }) 102 | .expect(200); 103 | 104 | const ticketResponse = await request(app) 105 | .get(`/api/tickets/${response.body.id}`) 106 | .send(); 107 | 108 | expect(ticketResponse.body.title).toEqual('new title'); 109 | expect(ticketResponse.body.price).toEqual(100); 110 | }); 111 | 112 | it('publishes an event', async () => { 113 | const cookie = global.signin(); 114 | 115 | const response = await request(app) 116 | .post('/api/tickets') 117 | .set('Cookie', cookie) 118 | .send({ 119 | title: 'aslkdfj', 120 | price: 20, 121 | }) 122 | .expect(201); 123 | 124 | await request(app) 125 | .put(`/api/tickets/${response.body.id}`) 126 | .set('Cookie', cookie) 127 | .send({ 128 | title: 'new title', 129 | price: 100, 130 | }) 131 | .expect(200); 132 | 133 | expect(natsWrapper.client.publish).toHaveBeenCalled(); 134 | }); 135 | 136 | it('rejects updated if the ticket is reserved', async () => { 137 | const cookie = global.signin(); 138 | 139 | const response = await request(app) 140 | .post('/api/tickets') 141 | .set('Cookie', cookie) 142 | .send({ 143 | title: 'aslkdfj', 144 | price: 20, 145 | }) 146 | .expect(201); 147 | 148 | const ticket = await Ticket.findById(response.body.id); 149 | ticket!.set({ orderId: mongoose.Types.ObjectId().toHexString() }); 150 | await ticket!.save(); 151 | 152 | await request(app) 153 | .put(`/api/tickets/${response.body.id}`) 154 | .set('Cookie', cookie) 155 | .send({ 156 | title: 'new title', 157 | price: 100, 158 | }) 159 | .expect(400); 160 | }); 161 | -------------------------------------------------------------------------------- /app/tickets/src/routes/index.ts: -------------------------------------------------------------------------------- 1 | import express, { Request, Response } from 'express'; 2 | import { Ticket } from 'models/Ticket'; 3 | 4 | const router = express.Router(); 5 | 6 | router.get('/api/tickets', async (_req: Request, res: Response) => { 7 | const tickets = await Ticket.find({ 8 | orderId: undefined 9 | }); 10 | return res.send(tickets); 11 | }); 12 | 13 | export { router as indexTicketRouter }; 14 | -------------------------------------------------------------------------------- /app/tickets/src/routes/new.ts: -------------------------------------------------------------------------------- 1 | import { requireAuth, validateRequest } from '@webmak/microservices-common'; 2 | import { TicketCreatedPublisher } from 'events/publishers/TicketCreatedPublisher'; 3 | import express, { Request, Response } from 'express'; 4 | import { body } from 'express-validator'; 5 | import { Ticket } from 'models/Ticket'; 6 | import { natsWrapper } from 'NatsWrapper'; 7 | 8 | const router = express.Router(); 9 | 10 | router.post( 11 | '/api/tickets', 12 | requireAuth, 13 | [ 14 | body('title').not().isEmpty().withMessage('Title is required'), 15 | body('price') 16 | .isFloat({ gt: 0 }) 17 | .withMessage('Price must be greater than 0'), 18 | ], 19 | validateRequest, 20 | async (req: Request, res: Response) => { 21 | const { title, price } = req.body; 22 | const ticket = Ticket.build({ 23 | title, 24 | price, 25 | userId: req.currentUser!.id, 26 | }); 27 | await ticket.save(); 28 | await new TicketCreatedPublisher(natsWrapper.client).publish({ 29 | id: ticket.id, 30 | title: ticket.title, 31 | price: ticket.price, 32 | userId: ticket.userId, 33 | version: ticket.version, 34 | }); 35 | return res.status(201).send(ticket); 36 | } 37 | ); 38 | 39 | export { router as createTicketRouter }; 40 | -------------------------------------------------------------------------------- /app/tickets/src/routes/show.ts: -------------------------------------------------------------------------------- 1 | import { NotFoundError } from '@webmak/microservices-common'; 2 | import express, { Request, Response } from 'express'; 3 | import { Ticket } from 'models/Ticket'; 4 | 5 | const router = express.Router(); 6 | 7 | router.get('/api/tickets/:id', async (req: Request, res: Response) => { 8 | const ticket = await Ticket.findById(req.params.id); 9 | 10 | if (!ticket) { 11 | throw new NotFoundError(); 12 | } 13 | 14 | return res.status(200).send(ticket); 15 | }); 16 | 17 | export { router as showTicketRouter }; 18 | -------------------------------------------------------------------------------- /app/tickets/src/routes/update.ts: -------------------------------------------------------------------------------- 1 | import { 2 | BadRequestError, 3 | NotAuthorizedError, 4 | NotFoundError, 5 | requireAuth, 6 | validateRequest, 7 | } from '@webmak/microservices-common'; 8 | import { TicketUpdatedPublisher } from 'events/publishers/TicketUpdatedPublisher'; 9 | import express, { Request, Response } from 'express'; 10 | import { body } from 'express-validator'; 11 | import { Ticket } from 'models/Ticket'; 12 | import { natsWrapper } from 'NatsWrapper'; 13 | 14 | const router = express.Router(); 15 | 16 | router.put( 17 | '/api/tickets/:id', 18 | requireAuth, 19 | [ 20 | body('title').not().isEmpty().withMessage('Title is required'), 21 | body('price') 22 | .isFloat({ gt: 0 }) 23 | .withMessage('Price must be provided and must be greater than 0'), 24 | ], 25 | validateRequest, 26 | async (req: Request, res: Response) => { 27 | const ticket = await Ticket.findById(req.params.id); 28 | 29 | if (!ticket) { 30 | throw new NotFoundError(); 31 | } 32 | 33 | if (ticket.orderId) { 34 | throw new BadRequestError('[Orders] Cannot edit a reserved ticket'); 35 | } 36 | 37 | if (ticket.userId !== req.currentUser!.id) { 38 | throw new NotAuthorizedError(); 39 | } 40 | 41 | ticket.set({ 42 | title: req.body.title, 43 | price: req.body.price, 44 | }); 45 | 46 | await ticket.save(); 47 | new TicketUpdatedPublisher(natsWrapper.client).publish({ 48 | id: ticket.id, 49 | title: ticket.title, 50 | price: ticket.price, 51 | userId: ticket.userId, 52 | version: ticket.version, 53 | }); 54 | 55 | return res.status(200).send(ticket); 56 | } 57 | ); 58 | 59 | export { router as updateTicketRouter }; 60 | -------------------------------------------------------------------------------- /app/tickets/src/test/setup.ts: -------------------------------------------------------------------------------- 1 | import jwt from 'jsonwebtoken'; 2 | import { MongoMemoryServer } from 'mongodb-memory-server'; 3 | import mongoose from 'mongoose'; 4 | 5 | declare global { 6 | namespace NodeJS { 7 | interface Global { 8 | signin(): string[]; 9 | } 10 | } 11 | } 12 | 13 | jest.setTimeout(10000); 14 | jest.mock('__mocks__/NatsWrapper'); 15 | 16 | let mongo: any; 17 | 18 | beforeAll(async () => { 19 | process.env.JWT_KEY = 'MY_JWT_SECRET'; 20 | mongo = new MongoMemoryServer(); 21 | const mongoUri = await mongo.getUri(); 22 | 23 | await mongoose.connect(mongoUri, { 24 | useNewUrlParser: true, 25 | useUnifiedTopology: true, 26 | }); 27 | }); 28 | 29 | beforeEach(async () => { 30 | jest.clearAllMocks(); 31 | const collections = await mongoose.connection.db.collections(); 32 | 33 | for (let collection of collections) { 34 | await collection.deleteMany({}); 35 | } 36 | }); 37 | 38 | afterAll(async () => { 39 | await mongo.stop(); 40 | await mongoose.connection.close(); 41 | }); 42 | 43 | global.signin = () => { 44 | const payload = { 45 | id: new mongoose.Types.ObjectId().toHexString(), 46 | email: 'test@test.com', 47 | }; 48 | 49 | const token = jwt.sign(payload, process.env.JWT_KEY!); 50 | 51 | const session = { jwt: token }; 52 | 53 | const sessionJSON = JSON.stringify(session); 54 | 55 | const base64 = Buffer.from(sessionJSON).toString('base64'); 56 | 57 | return [`express:sess=${base64}`]; 58 | }; 59 | -------------------------------------------------------------------------------- /app/tickets/tsconfig.eslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "types": ["jest"] 5 | }, 6 | "include": ["src", "tests"] 7 | } 8 | -------------------------------------------------------------------------------- /app/tickets/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Basic Options */ 4 | // "incremental": true, /* Enable incremental compilation */ 5 | "target": "es5" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */, 6 | "module": "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */, 7 | // "lib": [], /* Specify library files to be included in the compilation. */ 8 | // "allowJs": true, /* Allow javascript files to be compiled. */ 9 | // "checkJs": true, /* Report errors in .js files. */ 10 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ 11 | // "declaration": true, /* Generates corresponding '.d.ts' file. */ 12 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ 13 | // "sourceMap": true, /* Generates corresponding '.map' file. */ 14 | // "outFile": "./", /* Concatenate and emit output to single file. */ 15 | // "outDir": "./", /* Redirect output structure to the directory. */ 16 | // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 17 | // "composite": true, /* Enable project compilation */ 18 | // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ 19 | // "removeComments": true, /* Do not emit comments to output. */ 20 | // "noEmit": true, /* Do not emit outputs. */ 21 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 22 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 23 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 24 | 25 | /* Strict Type-Checking Options */ 26 | "strict": true /* Enable all strict type-checking options. */, 27 | // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ 28 | // "strictNullChecks": true, /* Enable strict null checks. */ 29 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */ 30 | // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ 31 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ 32 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 33 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 34 | 35 | /* Additional Checks */ 36 | // "noUnusedLocals": true, /* Report errors on unused locals. */ 37 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 38 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 39 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 40 | 41 | /* Module Resolution Options */ 42 | // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 43 | 44 | "baseUrl": "./src", 45 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 46 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 47 | // "typeRoots": [], /* List of folders to include type definitions from. */ 48 | // "types": [], /* Type declaration files to be included in compilation. */ 49 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 50 | "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */, 51 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 52 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 53 | 54 | /* Source Map Options */ 55 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 56 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 57 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 58 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 59 | 60 | /* Experimental Options */ 61 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 62 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 63 | 64 | /* Advanced Options */ 65 | "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */ 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /docs/02-client-service.md: -------------------------------------------------------------------------------- 1 | # [Stephen Grider] Microservices with Node JS and React [ENG, 2021] 2 | 3 |
    4 | 5 | ## Client Service 6 | 7 |
    8 | 9 | ### 11. Integrating a Server-Side-Rendered React App 10 | 11 |
    12 | 13 | $ cd app 14 | $ mkdir client 15 | $ cd client 16 | $ npm init -y 17 | $ npm install react react-dom next 18 | $ npm install bootstrap axios 19 | 20 |
    21 | 22 | http://SERVICENAME.NAMESPACE.svc.cluster.local 23 | 24 |
    25 | 26 | ``` 27 | $ kubectl get ingress 28 | NAME CLASS HOSTS ADDRESS PORTS AGE 29 | ingress-svc ticketing.dev 192.168.49.2 80 8m41s 30 | ``` 31 | 32 |
    33 | 34 | ``` 35 | // NOT WORKS for ME. 36 | http://ingress-svc.default.svc.cluster.local 37 | ``` 38 | 39 |
    40 | 41 | ``` 42 | $ kubectl exec -it auth-deployment-9f9c79479-v865d -- nslookup ingress-svc 43 | Server: 10.96.0.10 44 | Address: 10.96.0.10:53 45 | 46 | ** server can't find ingress-svc.cluster.local: NXDOMAIN 47 | 48 | ** server can't find ingress-svc.default.svc.cluster.local: NXDOMAIN 49 | 50 | ** server can't find ingress-svc.svc.cluster.local: NXDOMAIN 51 | 52 | ** server can't find ingress-svc.cluster.local: NXDOMAIN 53 | 54 | ** server can't find ingress-svc.svc.cluster.local: NXDOMAIN 55 | 56 | ** server can't find ingress-svc.default.svc.cluster.local: NXDOMAIN 57 | 58 | command terminated with exit code 1 59 | ``` 60 | 61 |
    62 | 63 | ``` 64 | $ kubectl exec -ti auth-deployment-868cdc5b6c-66jdv -- nslookup 192.168.49.2 65 | Server: 10.96.0.10 66 | Address: 10.96.0.10:53 67 | 68 | 2.49.168.192.in-addr.arpa name = 192-168-49-2.kubernetes.default.svc.cluster.local 69 | ``` 70 | 71 |
    72 | 73 | https://ticketing.dev/ 74 | 75 | 114 | 115 |
    116 | 117 | ### 22. Back to the Client 118 | 119 | ``` 120 | $ cd client 121 | $ npm install react-stripe-checkout 122 | ``` 123 | 124 |
    125 | 126 | **Testing card** 127 | 128 | https://stripe.com/docs/testing 129 | 130 |
    131 | 132 | 4242 4242 4242 4242 133 | 134 | - Any 3 digits 135 | 136 |
    137 | 138 | --- 139 | 140 |
    141 | 142 | **Marley** 143 | 144 | Any questions in english: Telegram Chat 145 | Любые вопросы на русском: Телеграм чат 146 | -------------------------------------------------------------------------------- /docs/03-shared-library.md: -------------------------------------------------------------------------------- 1 | # [Stephen Grider] Microservices with Node JS and React [ENG, 2021] 2 | 3 |
    4 | 5 | ## Share Library 6 | 7 |
    8 | 9 | ### 12. Code Sharing and Reuse Between Services 10 | 11 |
    12 | 13 | $ cd app 14 | $ mkdir common 15 | $ cd common/ 16 | $ npm init -y 17 | $ npm install --save-dev typescript del-cli 18 | 19 | $ tsc --init 20 | 21 |
    22 | 23 | **tsconfig.json** 24 | 25 | "baseUrl": "./src" 26 | "declaration": true 27 | "outDir": "./build" 28 | 29 |
    30 | 31 | ``` 32 | $ npm install --save \ 33 | express \ 34 | express-validator \ 35 | cookie-session \ 36 | jsonwebtoken \ 37 | @types/cookie-session \ 38 | @types/express \ 39 | @types/jsonwebtoken 40 | ``` 41 | 42 |
    43 | 44 | ``` 45 | $ npm install --save-dev ttypescript 46 | $ npm install --save-dev @zerollup/ts-transform-paths 47 | ``` 48 | 49 |
    50 | 51 | $ npm run build 52 | 53 |
    54 | 55 | **If the error will occur:** 56 | 57 | ``` 58 | node_modules/@types/express/index.d.ts:58:29 - error TS2694: Namespace 'serveStatic' has no exported member 'RequestHandlerConstructor'. 59 | 60 | 58 var static: serveStatic.RequestHandlerConstructor; 61 | ~~~~~~~~~~~~~~~~~~~~~~~~~ 62 | 63 | 64 | Found 1 error. 65 | 66 | ``` 67 | 68 |
    69 | 70 | **Fix it** 71 | 72 | ``` 73 | $ npm update @types/express-serve-static-core --depth 2 74 | $ npm update @types/serve-static --depth 2 75 | ``` 76 | 77 |
    78 | 79 | **Relocating Shared Code** 80 | 81 | ``` 82 | auth/src/errors move to common/src/errors 83 | auth/src/middlewares move to common/src/middlewares 84 | ``` 85 | 86 |
    87 | 88 | **Publishing NodeJS Packages in Github** 89 | 90 | https://docs.github.com/en/actions/guides/publishing-nodejs-packages 91 | 92 |
    93 | 94 | **Article about compiling packages with "baseUrl": "./src" params** 95 | 96 | https://mitchellsimoens.com/2019/08/07/why-typescript-paths-failed-me/ 97 | 98 |
    99 | 100 | **Helpful video for publishing library with Github Actions to npmjs** 101 | https://www.youtube.com/watch?v=0Te32Rx2FXM 102 | 103 |
    104 | 105 | 114 | 115 | ``` 116 | $ cd auth 117 | $ npm install @webmak/microservices-common 118 | ``` 119 | 120 |
    121 | 122 | ### Updating the Common Module (if needed) 123 | 124 |
    125 | 126 | ``` 127 | $ cd auth 128 | $ ncu -u 129 | ``` 130 | 131 |
    132 | 133 | --- 134 | 135 |
    136 | 137 | **Marley** 138 | 139 | Any questions in english: Telegram Chat 140 | Любые вопросы на русском: Телеграм чат 141 | -------------------------------------------------------------------------------- /docs/04-tickets-service.md: -------------------------------------------------------------------------------- 1 | # [Stephen Grider] Microservices with Node JS and React [ENG, 2021] 2 | 3 |
    4 | 5 | ## Tickets Service 6 | 7 |
    8 | 9 | ### 13. Create-Read-Update-Destroy Server Setup 10 | 11 | $ cd app 12 | $ mkdir tickets 13 | 14 | copy some files from auth project 15 | 16 | $ npm install 17 | 18 |
    19 | 20 | $ npm run test 21 | 22 |
    23 | 24 | ### Tests 25 | 26 |
    27 | 28 | ``` 29 | // SIGN UP 30 | $ curl \ 31 | --insecure \ 32 | --cookie-jar /tmp/cookies.txt \ 33 | --data '{"email":"marley@example.com", "password":"123456789"}' \ 34 | --header "Content-Type: application/json" \ 35 | --request POST \ 36 | --url https://ticketing.dev/api/users/signup \ 37 | | jq 38 | ``` 39 | 40 |
    41 | 42 | ``` 43 | // SIGN IN 44 | $ curl \ 45 | --data '{"email":"marley@example.com", "password":"123456789"}' \ 46 | --header "Content-Type: application/json" \ 47 | --request POST \ 48 | --url http://ticketing.dev/api/users/signin \ 49 | | jq 50 | ``` 51 | 52 |
    53 | 54 | ``` 55 | // GET CURRENT USER 56 | $ curl \ 57 | --insecure \ 58 | --cookie /tmp/cookies.txt \ 59 | --header "Content-Type: application/json" \ 60 | --request GET \ 61 | --url https://ticketing.dev/api/users/currentuser \ 62 | | jq 63 | ``` 64 | 65 |
    66 | 67 | ``` 68 | // CREATE TICKET 69 | $ curl \ 70 | --insecure \ 71 | --cookie /tmp/cookies.txt \ 72 | --data '{"title":"concert", "price":10}' \ 73 | --header "Content-Type: application/json" \ 74 | --request POST \ 75 | --url https://ticketing.dev/api/tickets \ 76 | | jq 77 | ``` 78 | 79 |
    80 | 81 | **response:** 82 | 83 | ``` 84 | { 85 | "__v": 0, 86 | "id": "6037eaacbcc4a0001acb6d50", 87 | "price": 10, 88 | "title": "concert", 89 | "userId": "6037e544e1fbed001c74094e" 90 | } 91 | ``` 92 | 93 |
    94 | 95 | ``` 96 | // GET TICKET 97 | $ curl \ 98 | --insecure \ 99 | --header "Content-Type: application/json" \ 100 | --request GET \ 101 | --url https://ticketing.dev/api/tickets/6037eaacbcc4a0001acb6d50 \ 102 | | jq 103 | ``` 104 | 105 |
    106 | 107 | **response:** 108 | 109 | ``` 110 | { 111 | "__v": 0, 112 | "id": "6037eaacbcc4a0001acb6d50", 113 | "price": 10, 114 | "title": "concert", 115 | "userId": "6037e544e1fbed001c74094e" 116 | } 117 | ``` 118 | 119 |
    120 | 121 | ``` 122 | // GET ALL TICKET 123 | $ curl \ 124 | --insecure \ 125 | --header "Content-Type: application/json" \ 126 | --request GET \ 127 | --url https://ticketing.dev/api/tickets/ \ 128 | | jq 129 | ``` 130 | 131 |
    132 | 133 | ``` 134 | // UPDATE TICKET 135 | $ curl \ 136 | --insecure \ 137 | --cookie /tmp/cookies.txt \ 138 | --data '{"title":"new concert", "price":100}' \ 139 | --header "Content-Type: application/json" \ 140 | --request PUT \ 141 | --url https://ticketing.dev/api/tickets/603b0e8036b9f80019154277 \ 142 | | jq 143 | ``` 144 | 145 |
    146 | 147 | --- 148 | 149 |
    150 | 151 | **Marley** 152 | 153 | Any questions in english: Telegram Chat 154 | Любые вопросы на русском: Телеграм чат 155 | -------------------------------------------------------------------------------- /docs/05-messaging.md: -------------------------------------------------------------------------------- 1 | # [Stephen Grider] Microservices with Node JS and React [ENG, 2021] 2 | 3 |
    4 | 5 | ## Messaging (NATS Streaming Server) Service 6 | 7 |
    8 | 9 | ### 14. NATS Streaming Server - An Event Bus Implementation 10 | 11 |
    12 | 13 | $ cd app 14 | $ mkdir nats-test 15 | $ cd nats-test 16 | $ npm init -y 17 | $ npm install --save node-nats-streaming ts-node-dev typescript @types/node 18 | 19 |
    20 | 21 | $ tsc --init 22 | 23 | ``` 24 | $ kubectl get pods 25 | NAME READY STATUS RESTARTS AGE 26 | auth-deployment-59d887f775-h9nxq 1/1 Running 0 109m 27 | auth-mongo-deployment-d79ff8f7c-lj92m 1/1 Running 0 109m 28 | client-deployment-65576ffc88-vxvv4 1/1 Running 0 109m 29 | nats-deployment-6bf654c867-cvc2q 1/1 Running 0 109m 30 | tickets-deployment-5c57b69d6c-kbb6c 1/1 Running 0 109m 31 | tickets-mongo-deployment-869f7b4c75-9q48f 1/1 Running 0 109m 32 | ``` 33 | 34 |
    35 | 36 | $ kubectl port-forward nats-deployment-6bf654c867-cvc2q 4222:4222 37 | $ kubectl port-forward nats-deployment-6bf654c867-cvc2q 8222:8222 38 | 39 |
    40 | 41 | $ cd nats-test 42 | $ npm run publish 43 | $ npm run listen 44 | 45 |
    46 | 47 | console with publisher: rs + [Enter] 48 | 49 |
    50 | 51 | **browser web ui with stats** 52 | 53 | http://localhost:8222/streaming 54 | http://localhost:8222/streaming/channelsz?subs=1 55 | 56 |
    57 | 58 | $ cd common 59 | $ npm install --save node-nats-streaming 60 | $ npm version patch 61 | 62 |
    63 | 64 | **push sources to github** 65 | 66 |
    67 | 68 | ``` 69 | $ cd tickets/ 70 | $ ncu -u 71 | $ npm update @webmak/microservices-common 72 | ``` 73 | 74 |
    75 | 76 | ``` 77 | $ cd nats-test/ 78 | $ npm install @webmak/microservices-common 79 | ``` 80 | 81 | 87 | 88 |
    89 | 90 | $ kubectl port-forward nats-deployment-7d9dfd65d9-jjhgq 4222:4222 91 | $ cd app/nats-test 92 | $ npm run listen 93 | 94 |
    95 | 96 | ``` 97 | // CREATE TICKET 98 | ``` 99 | 100 |
    101 | 102 | **returns:** 103 | 104 | ``` 105 | *** 106 | [tickets] Event published to subject ticket:created 107 | *** 108 | ``` 109 | 110 |
    111 | 112 | ``` 113 | Listener connected to NATS 114 | Message received: ticket:created / payments-service 115 | Event data! { 116 | id: '603b0e8036b9f80019154277', 117 | title: 'concert', 118 | price: 10, 119 | userId: '603b06996ed60700194e7eee' 120 | } 121 | ``` 122 | 123 |
    124 | 125 | ``` 126 | // UPDATE TICKET 127 | ``` 128 | 129 |
    130 | 131 | **returns:** 132 | 133 | ``` 134 | *** 135 | [tickets] Event published to subject ticket:updated 136 | *** 137 | ``` 138 | 139 |
    140 | 141 | **Run Tests:** 142 | 143 | $ cd tickets 144 | $ npm run test 145 | 146 |
    147 | 148 | **returns:** 149 | 150 | ``` 151 | Test Suites: 4 passed, 4 total 152 | Tests: 16 passed, 16 total 153 | Snapshots: 0 total 154 | Time: 14.75 s 155 | Ran all test suites. 156 | ``` 157 | 158 |
    159 | 160 | --- 161 | 162 |
    163 | 164 | **Marley** 165 | 166 | Any questions in english: Telegram Chat 167 | Любые вопросы на русском: Телеграм чат 168 | -------------------------------------------------------------------------------- /docs/06-orders-service.md: -------------------------------------------------------------------------------- 1 | # [Stephen Grider] Microservices with Node JS and React [ENG, 2021] 2 | 3 |
    4 | 5 | ## Orders Service 6 | 7 |
    8 | 9 | ### 17. Cross-Service Data Replication In Action 10 | 11 |
    12 | 13 | ``` 14 | $ cd common 15 | $ npm version patch 16 | ``` 17 | 18 | **push changes to github** 19 | 20 | ``` 21 | $ cd orders/ 22 | $ npm install @webmak/microservices-common 23 | $ npm run test 24 | ``` 25 | 26 |
    27 | 28 | **returns:** 29 | 30 | ``` 31 | Test Suites: 4 passed, 4 total 32 | Tests: 2 todo, 7 passed, 9 total 33 | Snapshots: 0 total 34 | Time: 14.434 s 35 | Ran all test suites. 36 | ``` 37 | 38 |
    39 | 40 | ### 18. Understanding Event Flow 41 | 42 |
    43 | 44 | ``` 45 | $ cd common 46 | $ npm version patch 47 | ``` 48 | 49 | **push changes to github** 50 | 51 | ``` 52 | $ cd orders/ 53 | $ npm install @webmak/microservices-common 54 | $ npm run test 55 | ``` 56 | 57 |
    58 | 59 | ### 19. Listening for Events and Handling Concurrency Issues 60 | 61 |
    62 | 63 | $ cd tickets 64 | $ npm install --save mongoose-update-if-current 65 | 66 | $ cd orders 67 | $ npm install --save mongoose-update-if-current 68 | 69 |
    70 | 71 | **push changes to github** 72 | 73 |
    74 | 75 | $ cd orders 76 | $ npm update @webmak/microservices-common 77 | 78 |
    79 | 80 | $ cd tickets 81 | $ npm update @webmak/microservices-common 82 | 83 |
    84 | 85 | $ cd tickets 86 | $ npm run test 87 | 88 |
    89 | 90 | $ cd orders 91 | $ npm run test 92 | 93 |
    94 | 95 | All Tests are OK! 96 | 97 |
    98 | 99 | --- 100 | 101 |
    102 | 103 | **Marley** 104 | 105 | Any questions in english: Telegram Chat 106 | Любые вопросы на русском: Телеграм чат 107 | -------------------------------------------------------------------------------- /docs/07-expiration-service.md: -------------------------------------------------------------------------------- 1 | # [Stephen Grider] Microservices with Node JS and React [ENG, 2021] 2 | 3 |
    4 | 5 | ## Expiration Service 6 | 7 |
    8 | 9 | ### 20. Worker Services 10 | 11 |
    12 | 13 | $ cd expiration 14 | $ npm install --save bull @types/bull 15 | 16 |
    17 | 18 | **push changes to github** 19 | 20 |
    21 | 22 | $ cd expiration 23 | $ npm update @webmak/microservices-common 24 | 25 |
    26 | 27 | $ cd orders 28 | $ npm update @webmak/microservices-common 29 | 30 |
    31 | 32 | $ cd orders 33 | $ npm run test 34 | 35 |
    36 | 37 | ``` 38 | // CREATE TICKET 39 | // CREATE ORDER 40 | ``` 41 | 42 |
    43 | 44 | **After 15 minutes:** 45 | 46 | ``` 47 | [expiration] Event published to subject expiration:complete 48 | [orders] Message received: expiration:complete / orders-service 49 | [orders] Event published to subject order:cancelled 50 | [tickets] Message received: order:cancelled / tickets-service 51 | [tickets] Event published to subject ticket:updated 52 | [orders] Message received: ticket:updated / orders-service 53 | ``` 54 | 55 |
    56 | 57 | --- 58 | 59 |
    60 | 61 | **Marley** 62 | 63 | Any questions in english: Telegram Chat 64 | Любые вопросы на русском: Телеграм чат 65 | -------------------------------------------------------------------------------- /docs/08-payments-service.md: -------------------------------------------------------------------------------- 1 | # [Stephen Grider] Microservices with Node JS and React [ENG, 2021] 2 | 3 |
    4 | 5 | ## Payments Service 6 | 7 |
    8 | 9 | ### 21. Handling Payments 10 | 11 | $ cd payments 12 | $ npm install --save mongoose-update-if-current 13 | $ npm install --save stripe 14 | 15 |
    16 | 17 | stripe.com 18 | 19 | Developers --> API keys 20 | 21 |
    22 | 23 | **Creating a Stripe Secret** 24 | 25 | ``` 26 | $ kubectl create secret generic stripe-secret --from-literal=STRIPE_KEY= 27 | ``` 28 | 29 |
    30 | 31 | $ cd payments 32 | $ npm update @webmak/microservices-common 33 | 34 |
    35 | 36 | Need to add for tests to file 37 | 38 |
    39 | 40 | payments/src/test/setup.ts 41 | 42 |
    43 | 44 | $ npm run test 45 | 46 |
    47 | 48 | ``` 49 | Test Suites: 3 passed, 3 total 50 | Tests: 8 passed, 8 total 51 | Snapshots: 0 total 52 | Time: 15.825 s 53 | Ran all test suites. 54 | ``` 55 | 56 |
    57 | 58 | --- 59 | 60 |
    61 | 62 | **Marley** 63 | 64 | Any questions in english: Telegram Chat 65 | Любые вопросы на русском: Телеграм чат 66 | -------------------------------------------------------------------------------- /docs/Development.md: -------------------------------------------------------------------------------- 1 | # [Stephen Grider] Microservices with Node JS and React [ENG, 2021] 2 | 3 |
    4 | 5 | ### [Auth Service](./01-auth-service.md) 6 | 7 | ### [Client Service](./02-client-service.md) 8 | 9 | ### [Shared Library](./03-shared-library.md) 10 | 11 | ### [Tickets Service](./04-tickets-service.md) 12 | 13 | ### [Messaging (NATS Streaming Server) Service](./05-messaging.md) 14 | 15 | ### [Orders Service](./06-orders-service.md) 16 | 17 | ### [Expiration Service](./07-expiration-service.md) 18 | 19 | ### [Payments Service](./08-payments-service.md) 20 | 21 |
    22 | 23 | --- 24 | 25 |
    26 | 27 | **Marley** 28 | 29 | Any questions in english: Telegram Chat 30 | Любые вопросы на русском: Телеграм чат 31 | -------------------------------------------------------------------------------- /k8s/helm/microservices/Chart.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | appVersion: '0.0.1' 3 | description: Microservices with Node JS and React 4 | name: microservices 5 | version: 0.0.1 6 | -------------------------------------------------------------------------------- /k8s/helm/microservices/charts/auth/Chart.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | appVersion: '0.0.1' 3 | description: Auth Service for Microservices with Node JS and React 4 | name: auth 5 | version: 0.0.1 6 | -------------------------------------------------------------------------------- /k8s/helm/microservices/charts/auth/templates/auth-clusterIP.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: auth-svc 5 | spec: 6 | selector: 7 | app: auth 8 | ports: 9 | - name: auth 10 | protocol: TCP 11 | port: 3000 12 | targetPort: 3000 13 | -------------------------------------------------------------------------------- /k8s/helm/microservices/charts/auth/templates/auth-deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: auth-deployment 5 | spec: 6 | replicas: 1 7 | selector: 8 | matchLabels: 9 | app: auth 10 | template: 11 | metadata: 12 | labels: 13 | app: auth 14 | spec: 15 | containers: 16 | - name: auth 17 | image: webmakaka/microservices-auth 18 | env: 19 | - name: MONGO_URI 20 | value: 'mongodb://auth-mongo-svc:27017/auth' 21 | - name: JWT_KEY 22 | valueFrom: 23 | secretKeyRef: 24 | name: jwt-secret 25 | key: JWT_KEY 26 | -------------------------------------------------------------------------------- /k8s/helm/microservices/charts/auth/templates/auth-mongo-clusterIP.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: auth-mongo-svc 5 | spec: 6 | selector: 7 | app: auth-mongo 8 | ports: 9 | - name: db 10 | protocol: TCP 11 | port: 27017 12 | targetPort: 27017 13 | -------------------------------------------------------------------------------- /k8s/helm/microservices/charts/auth/templates/auth-mongo-deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: auth-mongo-deployment 5 | spec: 6 | replicas: 1 7 | selector: 8 | matchLabels: 9 | app: auth-mongo 10 | template: 11 | metadata: 12 | labels: 13 | app: auth-mongo 14 | spec: 15 | containers: 16 | - name: auth-mongo 17 | image: mongo:4.4.4-bionic 18 | -------------------------------------------------------------------------------- /k8s/helm/microservices/charts/client/Chart.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | appVersion: '0.0.1' 3 | description: Client Service for Microservices with Node JS and React 4 | name: client 5 | version: 0.0.1 6 | -------------------------------------------------------------------------------- /k8s/helm/microservices/charts/client/templates/client-clusterIP.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: client-svc 5 | spec: 6 | selector: 7 | app: client 8 | ports: 9 | - name: client 10 | protocol: TCP 11 | port: 3000 12 | targetPort: 3000 13 | -------------------------------------------------------------------------------- /k8s/helm/microservices/charts/client/templates/client-deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: client-deployment 5 | spec: 6 | replicas: 1 7 | selector: 8 | matchLabels: 9 | app: client 10 | template: 11 | metadata: 12 | labels: 13 | app: client 14 | spec: 15 | containers: 16 | - name: client 17 | image: webmakaka/microservices-client 18 | # env: 19 | # - name: JWT_KEY 20 | # valueFrom: 21 | # secretKeyRef: 22 | # name: jwt-secret 23 | # key: JWT_KEY 24 | -------------------------------------------------------------------------------- /k8s/helm/microservices/charts/client/templates/ingress-config.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: networking.k8s.io/v1 2 | kind: Ingress 3 | metadata: 4 | name: ingress-svc 5 | annotations: 6 | kubernetes.io/ingress.class: nginx 7 | nginx.ingress.kubernetes.io/use-regex: 'true' 8 | spec: 9 | rules: 10 | - host: ticketing.dev 11 | http: 12 | paths: 13 | - path: /api/payments/?(.*) 14 | pathType: 'Prefix' 15 | backend: 16 | service: 17 | name: payments-svc 18 | port: 19 | number: 3000 20 | - path: /api/users/?(.*) 21 | pathType: 'Prefix' 22 | backend: 23 | service: 24 | name: auth-svc 25 | port: 26 | number: 3000 27 | - path: /api/tickets/?(.*) 28 | pathType: 'Prefix' 29 | backend: 30 | service: 31 | name: tickets-svc 32 | port: 33 | number: 3000 34 | - path: /api/orders/?(.*) 35 | pathType: 'Prefix' 36 | backend: 37 | service: 38 | name: orders-svc 39 | port: 40 | number: 3000 41 | - path: /?(.*) 42 | pathType: 'Prefix' 43 | backend: 44 | service: 45 | name: client-svc 46 | port: 47 | number: 3000 48 | -------------------------------------------------------------------------------- /k8s/helm/microservices/charts/expiration/Chart.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | appVersion: '0.0.1' 3 | description: Expiration Service for Microservices with Node JS and React 4 | name: expiration 5 | version: 0.0.1 6 | -------------------------------------------------------------------------------- /k8s/helm/microservices/charts/expiration/templates/expiration-deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: expiration-deployment 5 | spec: 6 | replicas: 1 7 | selector: 8 | matchLabels: 9 | app: expiration 10 | template: 11 | metadata: 12 | labels: 13 | app: expiration 14 | spec: 15 | containers: 16 | - name: expiration 17 | image: webmakaka/microservices-expiration 18 | env: 19 | - name: NATS_CLUSTER_ID 20 | value: 'ticketing' 21 | - name: NATS_CLIENT_ID 22 | valueFrom: 23 | fieldRef: 24 | fieldPath: metadata.name 25 | - name: NATS_URL 26 | value: 'http://nats-svc:4222' 27 | - name: REDIS_HOST 28 | value: expiration-redis-svc 29 | -------------------------------------------------------------------------------- /k8s/helm/microservices/charts/expiration/templates/expiration-redis-clusterIP.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: expiration-redis-svc 5 | spec: 6 | selector: 7 | app: expiration-redis 8 | ports: 9 | - name: expiration-redis 10 | protocol: TCP 11 | port: 6379 12 | targetPort: 6379 13 | -------------------------------------------------------------------------------- /k8s/helm/microservices/charts/expiration/templates/expiration-redis-deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: expiration-redis-deployment 5 | spec: 6 | replicas: 1 7 | selector: 8 | matchLabels: 9 | app: expiration-redis 10 | template: 11 | metadata: 12 | labels: 13 | app: expiration-redis 14 | spec: 15 | containers: 16 | - name: expiration-redis 17 | image: redis:6.2.1-alpine 18 | -------------------------------------------------------------------------------- /k8s/helm/microservices/charts/nats/Chart.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | appVersion: '0.0.1' 3 | description: Nats Service for Microservices with Node JS and React 4 | name: nats 5 | version: 0.0.1 6 | -------------------------------------------------------------------------------- /k8s/helm/microservices/charts/nats/templates/nats-clusterIP.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: nats-svc 5 | spec: 6 | selector: 7 | app: nats 8 | ports: 9 | - name: nats-client 10 | protocol: TCP 11 | port: 4222 12 | targetPort: 4222 13 | - name: nats-monitoring 14 | protocol: TCP 15 | port: 8222 16 | targetPort: 8222 17 | -------------------------------------------------------------------------------- /k8s/helm/microservices/charts/nats/templates/nats-deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: nats-deployment 5 | spec: 6 | replicas: 1 7 | selector: 8 | matchLabels: 9 | app: nats 10 | template: 11 | metadata: 12 | labels: 13 | app: nats 14 | spec: 15 | containers: 16 | - name: nats 17 | image: nats-streaming:0.20-alpine3.12 18 | args: 19 | [ 20 | '-p', 21 | '4222', 22 | '-m', 23 | '8222', 24 | '-hbi', 25 | '5s', 26 | '-hbt', 27 | '5s', 28 | '-hbf', 29 | '2', 30 | '-SD', 31 | '-cid', 32 | 'ticketing', 33 | ] 34 | env: 35 | - name: MONGO_URI 36 | value: 'mongodb://nats-mongo-svc:27017/nats' 37 | - name: JWT_KEY 38 | valueFrom: 39 | secretKeyRef: 40 | name: jwt-secret 41 | key: JWT_KEY 42 | -------------------------------------------------------------------------------- /k8s/helm/microservices/charts/orders/Chart.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | appVersion: '0.0.1' 3 | description: Orders Service for Microservices with Node JS and React 4 | name: orders 5 | version: 0.0.1 6 | -------------------------------------------------------------------------------- /k8s/helm/microservices/charts/orders/templates/orders-clusterIP.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: orders-svc 5 | spec: 6 | selector: 7 | app: orders 8 | ports: 9 | - name: orders 10 | protocol: TCP 11 | port: 3000 12 | targetPort: 3000 13 | -------------------------------------------------------------------------------- /k8s/helm/microservices/charts/orders/templates/orders-deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: orders-deployment 5 | spec: 6 | replicas: 1 7 | selector: 8 | matchLabels: 9 | app: orders 10 | template: 11 | metadata: 12 | labels: 13 | app: orders 14 | spec: 15 | containers: 16 | - name: orders 17 | image: webmakaka/microservices-orders 18 | env: 19 | - name: NATS_CLUSTER_ID 20 | value: 'ticketing' 21 | - name: NATS_CLIENT_ID 22 | valueFrom: 23 | fieldRef: 24 | fieldPath: metadata.name 25 | - name: NATS_URL 26 | value: 'http://nats-svc:4222' 27 | - name: MONGO_URI 28 | value: 'mongodb://orders-mongo-svc:27017/orders' 29 | - name: JWT_KEY 30 | valueFrom: 31 | secretKeyRef: 32 | name: jwt-secret 33 | key: JWT_KEY 34 | -------------------------------------------------------------------------------- /k8s/helm/microservices/charts/orders/templates/orders-mongo-clusterIP.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: orders-mongo-svc 5 | spec: 6 | selector: 7 | app: orders-mongo 8 | ports: 9 | - name: db 10 | protocol: TCP 11 | port: 27017 12 | targetPort: 27017 13 | -------------------------------------------------------------------------------- /k8s/helm/microservices/charts/orders/templates/orders-mongo-deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: orders-mongo-deployment 5 | spec: 6 | replicas: 1 7 | selector: 8 | matchLabels: 9 | app: orders-mongo 10 | template: 11 | metadata: 12 | labels: 13 | app: orders-mongo 14 | spec: 15 | containers: 16 | - name: orders-mongo 17 | image: mongo:4.4.4-bionic 18 | -------------------------------------------------------------------------------- /k8s/helm/microservices/charts/payments/Chart.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | appVersion: '0.0.1' 3 | description: Payments Service for Microservices with Node JS and React 4 | name: payments 5 | version: 0.0.1 6 | -------------------------------------------------------------------------------- /k8s/helm/microservices/charts/payments/templates/payments-clusterIP.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: payments-svc 5 | spec: 6 | selector: 7 | app: payments 8 | ports: 9 | - name: payments 10 | protocol: TCP 11 | port: 3000 12 | targetPort: 3000 13 | -------------------------------------------------------------------------------- /k8s/helm/microservices/charts/payments/templates/payments-deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: payments-deployment 5 | spec: 6 | replicas: 1 7 | selector: 8 | matchLabels: 9 | app: payments 10 | template: 11 | metadata: 12 | labels: 13 | app: payments 14 | spec: 15 | containers: 16 | - name: payments 17 | image: webmakaka/microservices-payments 18 | env: 19 | - name: NATS_CLUSTER_ID 20 | value: 'ticketing' 21 | - name: NATS_CLIENT_ID 22 | valueFrom: 23 | fieldRef: 24 | fieldPath: metadata.name 25 | - name: NATS_URL 26 | value: 'http://nats-svc:4222' 27 | - name: MONGO_URI 28 | value: 'mongodb://payments-mongo-svc:27017/payments' 29 | - name: JWT_KEY 30 | valueFrom: 31 | secretKeyRef: 32 | name: jwt-secret 33 | key: JWT_KEY 34 | - name: STRIPE_KEY 35 | valueFrom: 36 | secretKeyRef: 37 | name: stripe-secret 38 | key: STRIPE_KEY 39 | -------------------------------------------------------------------------------- /k8s/helm/microservices/charts/payments/templates/payments-mongo-clusterIP.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: payments-mongo-svc 5 | spec: 6 | selector: 7 | app: payments-mongo 8 | ports: 9 | - name: db 10 | protocol: TCP 11 | port: 27017 12 | targetPort: 27017 13 | -------------------------------------------------------------------------------- /k8s/helm/microservices/charts/payments/templates/payments-mongo-deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: payments-mongo-deployment 5 | spec: 6 | replicas: 1 7 | selector: 8 | matchLabels: 9 | app: payments-mongo 10 | template: 11 | metadata: 12 | labels: 13 | app: payments-mongo 14 | spec: 15 | containers: 16 | - name: payments-mongo 17 | image: mongo:4.4.4-bionic 18 | -------------------------------------------------------------------------------- /k8s/helm/microservices/charts/tickets/Chart.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | appVersion: '0.0.1' 3 | description: Tickets Service for Microservices with Node JS and React 4 | name: tickets 5 | version: 0.0.1 6 | -------------------------------------------------------------------------------- /k8s/helm/microservices/charts/tickets/templates/tickets-clusterIP.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: tickets-svc 5 | spec: 6 | selector: 7 | app: tickets 8 | ports: 9 | - name: tickets 10 | protocol: TCP 11 | port: 3000 12 | targetPort: 3000 13 | -------------------------------------------------------------------------------- /k8s/helm/microservices/charts/tickets/templates/tickets-deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: tickets-deployment 5 | spec: 6 | replicas: 1 7 | selector: 8 | matchLabels: 9 | app: tickets 10 | template: 11 | metadata: 12 | labels: 13 | app: tickets 14 | spec: 15 | containers: 16 | - name: tickets 17 | image: webmakaka/microservices-tickets 18 | env: 19 | - name: NATS_CLUSTER_ID 20 | value: 'ticketing' 21 | - name: NATS_CLIENT_ID 22 | valueFrom: 23 | fieldRef: 24 | fieldPath: metadata.name 25 | - name: NATS_URL 26 | value: 'http://nats-svc:4222' 27 | - name: MONGO_URI 28 | value: 'mongodb://tickets-mongo-svc:27017/tickets' 29 | - name: JWT_KEY 30 | valueFrom: 31 | secretKeyRef: 32 | name: jwt-secret 33 | key: JWT_KEY 34 | -------------------------------------------------------------------------------- /k8s/helm/microservices/charts/tickets/templates/tickets-mongo-clusterIP.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: tickets-mongo-svc 5 | spec: 6 | selector: 7 | app: tickets-mongo 8 | ports: 9 | - name: db 10 | protocol: TCP 11 | port: 27017 12 | targetPort: 27017 13 | -------------------------------------------------------------------------------- /k8s/helm/microservices/charts/tickets/templates/tickets-mongo-deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: tickets-mongo-deployment 5 | spec: 6 | replicas: 1 7 | selector: 8 | matchLabels: 9 | app: tickets-mongo 10 | template: 11 | metadata: 12 | labels: 13 | app: tickets-mongo 14 | spec: 15 | containers: 16 | - name: tickets-mongo 17 | image: mongo:4.4.4-bionic 18 | -------------------------------------------------------------------------------- /skaffold/skaffold.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: skaffold/v2beta26 2 | kind: Config 3 | build: 4 | # local: 5 | # push: true 6 | # tagPolicy: 7 | # sha256: {} 8 | artifacts: 9 | - image: webmakaka/microservices-auth 10 | context: ../app/auth 11 | docker: 12 | dockerfile: Dockerfile 13 | sync: 14 | manual: 15 | - src: 'src/**/*.ts' 16 | dest: . 17 | - image: webmakaka/microservices-client 18 | context: ../app/client 19 | docker: 20 | dockerfile: Dockerfile 21 | sync: 22 | manual: 23 | - src: 'src/**/*.js' 24 | dest: . 25 | - image: webmakaka/microservices-tickets 26 | context: ../app/tickets 27 | docker: 28 | dockerfile: Dockerfile 29 | sync: 30 | manual: 31 | - src: 'src/**/*.ts' 32 | dest: . 33 | - image: webmakaka/microservices-orders 34 | context: ../app/orders 35 | docker: 36 | dockerfile: Dockerfile 37 | sync: 38 | manual: 39 | - src: 'src/**/*.ts' 40 | dest: . 41 | - image: webmakaka/microservices-expiration 42 | context: ../app/expiration 43 | docker: 44 | dockerfile: Dockerfile 45 | sync: 46 | manual: 47 | - src: 'src/**/*.ts' 48 | dest: . 49 | - image: webmakaka/microservices-payments 50 | context: ../app/payments 51 | docker: 52 | dockerfile: Dockerfile 53 | sync: 54 | manual: 55 | - src: 'src/**/*.ts' 56 | dest: . 57 | # deploy: 58 | # helm: 59 | # releases: 60 | # - name: microservices-helm 61 | # namespace: default 62 | # recreatePods: true 63 | # chartPath: ../k8s/helm/microservices/ 64 | # skipBuildDependencies: true 65 | 66 | # artifactOverrides: 67 | # auth: 68 | # image: webmakaka/microservices-auth 69 | 70 | # imageStrategy: 71 | # helm: {} 72 | 73 | deploy: 74 | kubectl: 75 | manifests: 76 | - ../k8s/helm/microservices/charts/auth/templates/* 77 | - ../k8s/helm/microservices/charts/client/templates/* 78 | - ../k8s/helm/microservices/charts/tickets/templates/* 79 | - ../k8s/helm/microservices/charts/nats/templates/* 80 | - ../k8s/helm/microservices/charts/orders/templates/* 81 | - ../k8s/helm/microservices/charts/expiration/templates/* 82 | - ../k8s/helm/microservices/charts/payments/templates/* 83 | --------------------------------------------------------------------------------