├── .gitignore ├── Auth-Service ├── .dockerignore ├── .gitignore ├── .stignore ├── Dockerfile ├── README.md ├── jest.config.js ├── okteto.yml ├── package.json ├── src │ ├── app.ts │ ├── auth-svc.yml │ ├── index.ts │ ├── models │ │ └── user.ts │ ├── routes │ │ ├── __test__ │ │ │ ├── current-user.test.ts │ │ │ ├── signin.test.ts │ │ │ ├── signout.test.ts │ │ │ └── signup.test.ts │ │ ├── current-user.ts │ │ ├── signin.ts │ │ ├── signout.ts │ │ └── signup.ts │ ├── services │ │ └── password.ts │ ├── swaggergen.ts │ └── test │ │ └── setup.ts ├── tsconfig.json └── yarn.lock ├── Expiration-Service ├── .dockerignore ├── .gitignore ├── Dockerfile ├── README.md ├── jest.config.js ├── okteto.yml ├── package.json ├── src │ ├── __mocks__ │ │ └── nats-wrapper.ts │ ├── events │ │ ├── listeners │ │ │ ├── order-created-listener.ts │ │ │ └── queGroupName.ts │ │ └── publishers │ │ │ └── expiration-complete-publisher.ts │ ├── index.ts │ ├── nats-wrapper.ts │ └── queues │ │ └── expiration-queue.ts └── tsconfig.json ├── Frontend ├── .dockerignore ├── .gitignore ├── .stignore ├── Dockerfile ├── README.md ├── api │ └── build-client.js ├── components │ └── header.js ├── hooks │ └── use-request.js ├── next.config.js ├── okteto.yml ├── package.json ├── pages │ ├── _app.js │ ├── auth │ │ ├── signin.js │ │ ├── signout.js │ │ └── signup.js │ ├── index.js │ ├── orders │ │ ├── [orderId].js │ │ └── index.js │ └── tickets │ │ ├── [ticketId].js │ │ └── new.js └── yarn.lock ├── LICENSE ├── Library └── common │ ├── .gitignore │ ├── README.md │ ├── build │ ├── errors │ │ ├── bad-request-error.d.ts │ │ ├── bad-request-error.js │ │ ├── custom-errror.d.ts │ │ ├── custom-errror.js │ │ ├── database-connection-error.d.ts │ │ ├── database-connection-error.js │ │ ├── not-authorized-error.d.ts │ │ ├── not-authorized-error.js │ │ ├── not-found-error.d.ts │ │ ├── not-found-error.js │ │ ├── request-validation-error.d.ts │ │ └── request-validation-error.js │ ├── events │ │ ├── base-listener.d.ts │ │ ├── base-listener.js │ │ ├── base-publisher.d.ts │ │ ├── base-publisher.js │ │ ├── expiration-complete-event.d.ts │ │ ├── expiration-complete-event.js │ │ ├── order-cancelled-event.d.ts │ │ ├── order-cancelled-event.js │ │ ├── order-created-event.d.ts │ │ ├── order-created-event.js │ │ ├── payment-created-event.d.ts │ │ ├── payment-created-event.js │ │ ├── subjects.d.ts │ │ ├── subjects.js │ │ ├── ticket-created-event.d.ts │ │ ├── ticket-created-event.js │ │ ├── ticket-updated-events.d.ts │ │ ├── ticket-updated-events.js │ │ └── types │ │ │ ├── order-status.d.ts │ │ │ └── order-status.js │ ├── index.d.ts │ ├── index.js │ └── middlewares │ │ ├── current-user.d.ts │ │ ├── current-user.js │ │ ├── error-handler.d.ts │ │ ├── error-handler.js │ │ ├── require-auth.d.ts │ │ ├── require-auth.js │ │ ├── validate-requests.d.ts │ │ └── validate-requests.js │ ├── package.json │ ├── src │ ├── errors │ │ ├── bad-request-error.ts │ │ ├── custom-errror.ts │ │ ├── database-connection-error.ts │ │ ├── not-authorized-error.ts │ │ ├── not-found-error.ts │ │ └── request-validation-error.ts │ ├── events │ │ ├── base-listener.ts │ │ ├── base-publisher.ts │ │ ├── expiration-complete-event.ts │ │ ├── order-cancelled-event.ts │ │ ├── order-created-event.ts │ │ ├── payment-created-event.ts │ │ ├── subjects.ts │ │ ├── ticket-created-event.ts │ │ ├── ticket-updated-events.ts │ │ └── types │ │ │ └── order-status.ts │ ├── index.ts │ └── middlewares │ │ ├── current-user.ts │ │ ├── error-handler.ts │ │ ├── require-auth.ts │ │ └── validate-requests.ts │ ├── tsconfig.json │ └── yarn.lock ├── Orders-Service ├── .dockerignore ├── .gitignore ├── .stignore ├── Dockerfile ├── README.md ├── jest.config.js ├── okteto.yml ├── package.json ├── src │ ├── __mocks__ │ │ └── nats-wrapper.ts │ ├── app.ts │ ├── events │ │ ├── listeners │ │ │ ├── __test__ │ │ │ │ ├── expiration-complete-listener.test.ts │ │ │ │ ├── ticket-created-listener.test.ts │ │ │ │ └── ticket-updated-listener.test.ts │ │ │ ├── expiration-complete-listener.ts │ │ │ ├── payment-created-listener.ts │ │ │ ├── queue-group-name.ts │ │ │ ├── ticket-created-listener.ts │ │ │ └── ticket-updated-listener.ts │ │ └── publishers │ │ │ ├── order-cancelled-publisher.ts │ │ │ └── order-created-publisher.ts │ ├── index.ts │ ├── models │ │ ├── order.ts │ │ └── ticket.ts │ ├── nats-wrapper.ts │ ├── orders-svc.yml │ ├── 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.json └── yarn.lock ├── Payment-Service ├── .dockerignore ├── .gitignore ├── .stignore ├── Dockerfile ├── README.md ├── jest.config.js ├── okteto.yml ├── package.json ├── src │ ├── __mocks__ │ │ ├── nats-wrapper.ts │ │ └── stripe.ts │ ├── app.ts │ ├── events │ │ ├── listeners │ │ │ ├── __test__ │ │ │ │ ├── order-cancelled-listener.test.ts │ │ │ │ └── order-created-listener.test.ts │ │ │ ├── order-cancelled-listener.ts │ │ │ ├── order-created-listener.ts │ │ │ └── queue-group-name.ts │ │ └── publishers │ │ │ └── payment-created-publisher.ts │ ├── index.ts │ ├── models │ │ ├── orders.ts │ │ └── payment.ts │ ├── nats-wrapper.ts │ ├── payment-svc.yml │ ├── routes │ │ ├── __test__ │ │ │ └── new.test.ts │ │ └── new.ts │ ├── stripe.ts │ └── test │ │ └── setup.ts ├── tsconfig.json └── yarn.lock ├── README.md ├── Serverless-with-knative-serving └── blue-deployment │ ├── fntblu-ksvc.yaml │ └── fntblu-sa.yaml ├── Terraform └── Azure │ ├── .gitignore │ ├── README.md │ ├── aks.tf │ ├── azuread.tf │ ├── main.tf │ ├── output.tf │ └── vars.tf ├── Ticket-Service ├── .dockerignore ├── .gitignore ├── .stignore ├── Dockerfile ├── README.md ├── jest.config.js ├── okteto.yml ├── package.json ├── src │ ├── __mocks__ │ │ └── nats-wrapper.ts │ ├── app.ts │ ├── events │ │ ├── listeners │ │ │ ├── __test__ │ │ │ │ ├── order-cancelled-listener.test.ts │ │ │ │ └── order-created-listener.test.ts │ │ │ ├── order-cancelled-listener.ts │ │ │ ├── order-created-listener.ts │ │ │ └── queue-group-name.ts │ │ └── publishers │ │ │ ├── ticket-created-publisher.ts │ │ │ └── ticket-updated-publisher.ts │ ├── index.ts │ ├── models │ │ ├── __test__ │ │ │ └── ticket.test.ts │ │ └── ticket.ts │ ├── nats-wrapper.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 │ └── ticket-svc.yml ├── tsconfig.json └── yarn.lock ├── gitops ├── BackendServices │ ├── Auth-srv │ │ ├── auth-depl.yaml │ │ ├── auth-svc.yaml │ │ └── authsrv-sa.yaml │ ├── Expiration-srv │ │ ├── exprn-depl.yaml │ │ └── exprnsrv-sa.yaml │ ├── Order-srv │ │ ├── order-depl.yaml │ │ ├── order-svc.yaml │ │ └── ordersrv-sa.yaml │ ├── Payment-srv │ │ ├── payment-depl.yaml │ │ ├── payment-svc.yaml │ │ └── paymentsrv-sa.yaml │ └── Ticket-srv │ │ ├── ticket-depl.yaml │ │ ├── ticket-svc.yaml │ │ └── ticketsrv-sa.yaml ├── DBs │ ├── Auth-srv-DB │ │ ├── authdb-sa.yaml │ │ ├── authdb-sts.yaml │ │ └── authdb-svc.yaml │ ├── Expiration-srv-DB │ │ ├── expdb-sa.yaml │ │ ├── expdb-sts.yaml │ │ └── expdb-svc.yaml │ ├── Order-srv-DB │ │ ├── orderdb-sa.yaml │ │ ├── orderdb-sts.yaml │ │ └── orderdb-svc.yaml │ ├── Payment-srv-DB │ │ ├── paymentdb-sa.yaml │ │ ├── paymentdb-sts.yaml │ │ └── paymentdb-svc.yaml │ └── Ticket-srv-DB │ │ ├── ticketdb-sa.yaml │ │ ├── ticketdb-sts.yaml │ │ └── ticketdb-svc.yaml ├── EventBus │ ├── nats-depl.yaml │ ├── nats-sa.yaml │ └── nats-svc.yaml ├── Frontend │ ├── Frontend-depl.yaml │ ├── Frontend-svc.yaml │ ├── frontend-istioingressgateway.yaml │ ├── frontend-istiovirtualservice.yaml │ └── frontend-sa.yaml ├── IstioIngressGateway │ ├── authsrv-istioingressgateway.yaml │ ├── ordersrv-istioingressgateway.yaml │ ├── paymentsrv-istioingressgateway.yaml │ └── ticketsrv-istioingressgateway.yaml ├── IstioVirtualService │ ├── authsrv-istiovirtualservice.yaml │ ├── ordersrv-istiovirtualservice.yaml │ ├── paymentsrv-istiovirtualservice.yaml │ └── ticketsrv-istiovirtualservice.yaml ├── configmap │ └── host-cm.yaml └── sealed-secrets │ ├── sealed-jwt-secret.yaml │ └── sealed-stripekey-secret.yaml ├── kubernetes ├── dev │ ├── README.md │ ├── dev-dbs │ │ ├── auth-mongo-depl.yaml │ │ ├── expiration-redis-depl.yaml │ │ ├── orders-mongo-depl.yaml │ │ ├── payments-mongo-depl.yaml │ │ └── tickets-mongo-depl.yaml │ ├── eventbus │ │ └── nats-depl.yaml │ ├── istio │ │ ├── add-ons │ │ │ ├── grafana-install.yaml │ │ │ ├── jaeger-install.yaml │ │ │ ├── kiali-install.yaml │ │ │ └── prometheus-install.yaml │ │ ├── ingress-virtualservice │ │ │ ├── README.md │ │ │ ├── destination-config.yaml │ │ │ └── msc-gateway.yaml │ │ └── install │ │ │ ├── istio-install-manifest.yaml │ │ │ └── peerauth.yaml │ ├── mcs │ │ ├── auth-depl.yaml │ │ ├── expiration-depl.yaml │ │ ├── frontend-depl.yaml │ │ ├── host-cm.yaml │ │ ├── orders-depl.yaml │ │ ├── payments-depl.yaml │ │ └── tickets-depl.yaml │ ├── namespace │ │ ├── mcs-istio.yaml │ │ └── ticket_mcs_dev_namespace.yaml │ └── setup │ │ └── longhorm-install.yaml └── staging │ ├── README.md │ ├── cluster-setup │ ├── argocd-install │ │ ├── argocd-install.yaml │ │ └── argocd-namespace.yaml │ ├── istio-install │ │ ├── addons │ │ │ ├── grafana-install.yaml │ │ │ ├── jaeger-install.yaml │ │ │ ├── kiali-install.yaml │ │ │ └── prometheus-install.yaml │ │ └── install-manifest-istio.yaml │ ├── knative-install │ │ ├── knative-crd-install.yaml │ │ ├── knative-istio-controller-install.yaml │ │ └── knative-serving-core-install.yaml │ ├── longhorn-install │ │ └── longhorn-install.yaml │ ├── sealed-secrets-install │ │ └── sealed-secret-install.yaml │ └── tekton-install │ │ └── tekton-install.yaml │ ├── gitops-setup │ └── argocd-app-config.yaml │ └── namespace │ ├── ticketing-backend-ns.yaml │ └── ticketing-ns.yaml └── pictures ├── glotixz-app-argocd-deploy.png ├── glotixz-grafana-overview.PNG ├── glotixz-jaeger-overview.PNG ├── glotixz-jaeger-traces-overview.PNG ├── glotixz-kiali-overview.PNG ├── glotixz-longhorn.PNG ├── glotixz-quay-repository.PNG ├── glotixz-swagger-spec.PNG ├── nats-eventbus.png ├── ultimate-stack-overview.png └── ultimate-stack-overview.svg /Auth-Service/.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /Auth-Service/.stignore: -------------------------------------------------------------------------------- 1 | .git 2 | # Runtime data 3 | pids 4 | *.pid 5 | *.seed 6 | *.pid.lock 7 | 8 | # Directory for instrumented libs generated by jscoverage/JSCover 9 | lib-cov 10 | 11 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 12 | .grunt 13 | 14 | # Bower dependency directory (https://bower.io/) 15 | bower_components 16 | 17 | # node-waf configuration 18 | .lock-wscript 19 | 20 | # Compiled binary addons (https://nodejs.org/api/addons.html) 21 | build/Release 22 | 23 | # Dependency directories 24 | node_modules 25 | jspm_packages 26 | 27 | # Optional npm cache directory 28 | .npm 29 | 30 | # Optional eslint cache 31 | .eslintcache 32 | 33 | # Optional REPL history 34 | .node_repl_history 35 | 36 | # Output of 'npm pack' 37 | *.tgz 38 | 39 | # Yarn Integrity file 40 | .yarn-integrity 41 | 42 | # parcel-bundler cache (https://parceljs.org/) 43 | .cache 44 | 45 | # next.js build output 46 | .next 47 | 48 | # nuxt.js build output 49 | .nuxt 50 | 51 | # vuepress build output 52 | .vuepress/dist 53 | 54 | # Serverless directories 55 | .serverless 56 | 57 | -------------------------------------------------------------------------------- /Auth-Service/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM quay.io/narendev/fishnode:alpine 2 | 3 | WORKDIR /app 4 | ENV PORT=4000 5 | 6 | COPY package.json . 7 | RUN yarn install --production 8 | COPY . . 9 | 10 | EXPOSE ${PORT} 11 | 12 | CMD [ "yarn","start" ] -------------------------------------------------------------------------------- /Auth-Service/README.md: -------------------------------------------------------------------------------- 1 | # Authentication Service 2 | 3 | 4 | #### creating openAPI swagger docs. 5 | 6 | -------------------------------------------------------------------------------- /Auth-Service/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: "ts-jest", 3 | testEnvironment: "node", 4 | setupFilesAfterEnv: ["./src/test/setup.ts"], 5 | }; 6 | 7 | // "jest": { 8 | // "preset": "ts-jest", 9 | // "testEnvironment": "node", 10 | // "setupFilesAfterEnv": [ 11 | // "./src/test/setup.ts" 12 | // ] 13 | // }, 14 | -------------------------------------------------------------------------------- /Auth-Service/okteto.yml: -------------------------------------------------------------------------------- 1 | name: mcs-auth-service 2 | autocreate: true 3 | image: quay.io/narendev/fishnode:1.0 4 | command: fish 5 | sync: 6 | - .:/usr/src/app 7 | forward: 8 | - 9229:9229 9 | - 3000:3000 10 | persistentVolume: {} 11 | annotations: 12 | sidecar.istio.io/inject: "false" 13 | -------------------------------------------------------------------------------- /Auth-Service/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "auth-service", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "ts-node-dev src/index.ts", 8 | "test": "jest --watchAll --no-cache" 9 | }, 10 | "keywords": [], 11 | "author": "", 12 | "license": "ISC", 13 | "dependencies": { 14 | "@types/cookie-session": "^2.0.42", 15 | "@types/cors": "^2.8.10", 16 | "@types/express": "^4.17.11", 17 | "@types/jsonwebtoken": "^8.5.0", 18 | "@types/mongoose": "^5.10.3", 19 | "@types/swagger-jsdoc": "^6.0.0", 20 | "@types/swagger-ui-express": "^4.1.2", 21 | "@types/yamljs": "^0.2.31", 22 | "@wowowow/common": "^1.0.11", 23 | "cookie-session": "^1.4.0", 24 | "cors": "^2.8.5", 25 | "express": "^4.17.1", 26 | "express-async-errors": "^3.1.1", 27 | "express-validator": "^6.10.0", 28 | "jsonwebtoken": "^8.5.1", 29 | "mongoose": "5.10.19", 30 | "swagger-jsdoc": "6.0.0", 31 | "swagger-ui-express": "^4.1.6", 32 | "ts-node-dev": "^1.1.6", 33 | "typescript": "^4.2.2", 34 | "yamljs": "^0.3.0" 35 | }, 36 | "devDependencies": { 37 | "@types/jest": "^26.0.20", 38 | "@types/supertest": "^2.0.10", 39 | "jest": "^26.6.3", 40 | "mongodb-memory-server": "^6.9.3", 41 | "supertest": "^6.1.3", 42 | "ts-jest": "^26.5.3" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Auth-Service/src/app.ts: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | import "express-async-errors"; 3 | import { json } from "body-parser"; 4 | import cookieSession from "cookie-session"; 5 | import cors from "cors"; 6 | import { errorHandler, NotFoundError } from "@wowowow/common"; 7 | // import swaggerJSDoc from 'swagger-jsdoc' 8 | import swaggerUI from 'swagger-ui-express' 9 | 10 | import { currentUserRouter } from "./routes/current-user"; 11 | import { signInRouter } from "./routes/signin"; 12 | import { signOutRouter } from "./routes/signout"; 13 | import { signUpRouter } from "./routes/signup"; 14 | // import { options } from "./swaggergen"; 15 | import YAML from 'yamljs' 16 | 17 | const app = express(); 18 | 19 | // const swaggerSpec = swaggerJSDoc(options) 20 | 21 | const swaggeryml = YAML.load("src/auth-svc.yml") 22 | const options = { 23 | explorer:true 24 | } 25 | 26 | const hostValues = process.env.ALLOWED_HOSTS; 27 | 28 | const hostArray = hostValues!.split(",") 29 | 30 | 31 | const corsOptions = { 32 | origin: hostArray, 33 | credentials: true, 34 | exposedHeaders: ["set-cookie"] 35 | } 36 | 37 | app.use(cors(corsOptions)); 38 | app.set("trust proxy", true); 39 | app.use(json()); 40 | app.use( 41 | cookieSession({ 42 | signed: false, 43 | // secure:true //! when this is set to true, it'll only work on connection coming with https:// 44 | }) 45 | ); 46 | app.use('/api/auth/docs',swaggerUI.serve,swaggerUI.setup(swaggeryml,options)) 47 | app.use(currentUserRouter); 48 | app.use(signInRouter); 49 | app.use(signOutRouter); 50 | app.use(signUpRouter); 51 | 52 | app.all("*", async () => { 53 | throw new NotFoundError(); 54 | }); 55 | 56 | app.use(errorHandler); 57 | 58 | export { app }; 59 | -------------------------------------------------------------------------------- /Auth-Service/src/index.ts: -------------------------------------------------------------------------------- 1 | import mongoose from "mongoose"; 2 | import { app } from "./app"; 3 | 4 | // PORT value 5 | const PORT = process.env.PORT || 4000; 6 | 7 | // mongoose connection 8 | const start = async () => { 9 | if (!process.env.JWTSECRET) { 10 | throw new Error("JWTSECRET must be defined"); 11 | } 12 | if (!process.env.AUTH_MONGO_DB_URI) { 13 | throw new Error("AUTH_MONGO_DB_URI must be defined") 14 | } 15 | if(!process.env.ALLOWED_HOSTS){ 16 | throw new Error("ALLOWED_HOSTS value must be defined") 17 | } 18 | try { 19 | // ! this must be changed to use environment variable 20 | await mongoose.connect(process.env.AUTH_MONGO_DB_URI, { 21 | useNewUrlParser: true, 22 | useUnifiedTopology: true, 23 | useCreateIndex: true, 24 | }); 25 | console.log("Connected to MongoDb"); 26 | } catch (err) { 27 | console.error(err); 28 | } 29 | app.listen(PORT, () => { 30 | console.log(`Auth service listening on Port ${PORT}!`); 31 | }); 32 | }; 33 | 34 | start(); 35 | -------------------------------------------------------------------------------- /Auth-Service/src/models/user.ts: -------------------------------------------------------------------------------- 1 | import mongoose from "mongoose"; 2 | import { Password } from "../services/password"; 3 | 4 | // An interface that describes the properties 5 | // that are required to create a new User 6 | interface UserAttrs { 7 | email: string; 8 | password: string; 9 | } 10 | 11 | // AN interface that describe the properties that 12 | // a User Model has 13 | interface UserModel extends mongoose.Model { 14 | build(attrs: UserAttrs): UserDoc; 15 | } 16 | 17 | // An interface that describes the properties that 18 | // a User document has 19 | interface UserDoc extends mongoose.Document { 20 | email: string; 21 | password: string; 22 | } 23 | 24 | const userSchema = new mongoose.Schema( 25 | { 26 | email: { 27 | type: String, 28 | required: true, 29 | }, 30 | password: { 31 | type: String, 32 | required: true, 33 | }, 34 | }, 35 | { 36 | toJSON: { 37 | transform(doc,ret) { 38 | ret.id = ret._id 39 | delete ret._id; 40 | delete ret.password; 41 | delete ret.__v; //! it is possible to remove __v by mentioning versionkey to false in the toJSON object, 42 | }, 43 | }, 44 | } 45 | ); 46 | 47 | // ! this method will be performed before saving it to the database 48 | userSchema.pre("save", async function (done) { 49 | if (this.isModified("password")) { 50 | const hashed = await Password.toHash(this.get("password")); 51 | 52 | this.set("password", hashed); 53 | done(); 54 | } 55 | }); 56 | 57 | userSchema.statics.build = (attrs: UserAttrs) => { 58 | return new User(attrs); 59 | }; 60 | 61 | const User = mongoose.model("User", userSchema); 62 | 63 | export { User }; 64 | -------------------------------------------------------------------------------- /Auth-Service/src/routes/__test__/current-user.test.ts: -------------------------------------------------------------------------------- 1 | import request from "supertest"; 2 | import { app } from "../../app"; 3 | 4 | it("responds the current user details", async () => { 5 | const cookie = await global.signup(); 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("we get a null response 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(undefined); 23 | }); 24 | -------------------------------------------------------------------------------- /Auth-Service/src/routes/__test__/signin.test.ts: -------------------------------------------------------------------------------- 1 | import request from "supertest"; 2 | import { app } from "../../app"; 3 | 4 | it("fails when a email that deos not exist is supplied ", async () => { 5 | await request(app) 6 | .post("/api/users/signin") 7 | .send({ 8 | email: "test@test.com", 9 | password: "laskhdadlkh", 10 | }) 11 | .expect(400); 12 | }); 13 | 14 | it("fails when a incorrect password 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: "password123", 28 | }) 29 | .expect(400); 30 | }); 31 | 32 | it("cookie is got when valid credentails are given", 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 | -------------------------------------------------------------------------------- /Auth-Service/src/routes/__test__/signout.test.ts: -------------------------------------------------------------------------------- 1 | import request from "supertest"; 2 | import { app } from "../../app"; 3 | 4 | it("clears the cookie after sign-out", async () => { 5 | await request(app) 6 | .post("/api/users/signup") 7 | .send({ 8 | email: "test@test.com", 9 | password: "ohskdPIASKOHE", 10 | }) 11 | .expect(201); 12 | 13 | const response = await request(app) 14 | .post("/api/users/signout") 15 | .send({}) 16 | .expect(200); 17 | 18 | expect(response.get("Set-Cookie")).toBeDefined(); 19 | }); 20 | -------------------------------------------------------------------------------- /Auth-Service/src/routes/current-user.ts: -------------------------------------------------------------------------------- 1 | import express, { Response, Request } from "express"; 2 | import { currentUser } from "@wowowow/common"; 3 | 4 | const router = express.Router(); 5 | 6 | router.get( 7 | "/api/users/currentuser", 8 | currentUser, 9 | (req: Request, res: Response) => { 10 | res.send({ currentUser: req.currentUser || null }); 11 | } 12 | ); 13 | 14 | export { router as currentUserRouter }; 15 | -------------------------------------------------------------------------------- /Auth-Service/src/routes/signin.ts: -------------------------------------------------------------------------------- 1 | import express, { Request, Response } from "express"; 2 | import { body } from "express-validator"; 3 | import jwt from "jsonwebtoken"; 4 | import { BadRequestError, validateRequest } from "@wowowow/common"; 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("A password must be supplied"), 18 | ], 19 | validateRequest, 20 | async (req: Request, res: Response) => { 21 | const { email, password } = req.body; 22 | const existingUser = await User.findOne({ email }); 23 | if (!existingUser) { 24 | throw new BadRequestError("login request failed"); 25 | } 26 | const passwordsMatch = await Password.compare( 27 | existingUser.password, 28 | password 29 | ); 30 | 31 | if (!passwordsMatch) { 32 | throw new BadRequestError("Invalid Credentials"); 33 | } 34 | 35 | // Generating JWT 36 | const userJwt = jwt.sign( 37 | { id: existingUser.id, email: existingUser.email }, 38 | process.env.JWTSECRET! 39 | ); 40 | 41 | // Storing it on session object 42 | req.session = { jwt: userJwt }; 43 | 44 | res.status(200).send(existingUser); 45 | } 46 | ); 47 | 48 | export { router as signInRouter }; 49 | -------------------------------------------------------------------------------- /Auth-Service/src/routes/signout.ts: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | 3 | const router = express.Router(); 4 | 5 | router.post("/api/users/signout", (req, res) => { 6 | req.session = null; 7 | res.send({}); 8 | }); 9 | 10 | export { router as signOutRouter }; 11 | -------------------------------------------------------------------------------- /Auth-Service/src/routes/signup.ts: -------------------------------------------------------------------------------- 1 | import express, { Request, Response } from "express"; 2 | import { body } from "express-validator"; 3 | import jwt from "jsonwebtoken"; 4 | import { BadRequestError,validateRequest } from "@wowowow/common"; 5 | import { User } from "../models/user"; 6 | 7 | 8 | 9 | 10 | const router = express.Router(); 11 | 12 | 13 | 14 | 15 | 16 | router.post( 17 | "/api/users/signup", 18 | [ 19 | body("email").isEmail().withMessage("Email must be valid!"), 20 | body("password") 21 | .trim() 22 | .isLength({ min: 4, max: 20 }) 23 | .withMessage("Password must be between 4 and 20 characters"), 24 | ],validateRequest, 25 | async (req: Request, res: Response) => { 26 | 27 | const { email, password } = req.body; 28 | 29 | const existingUser = await User.findOne({ email }); 30 | 31 | if (existingUser) { 32 | throw new BadRequestError("Email in use"); 33 | } 34 | 35 | const user = User.build({ email, password }); 36 | await user.save(); 37 | 38 | // Generating JWT 39 | const userJwt = jwt.sign( 40 | { id: user.id, email: user.email }, 41 | process.env.JWTSECRET! 42 | ); 43 | 44 | // Storing it on session object 45 | req.session = { jwt: userJwt }; 46 | 47 | res.status(201).send(user); 48 | } 49 | ); 50 | 51 | export { router as signUpRouter }; 52 | -------------------------------------------------------------------------------- /Auth-Service/src/services/password.ts: -------------------------------------------------------------------------------- 1 | import { scrypt, randomBytes } 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 | 11 | return `${buf.toString("hex")}.${salt}`; 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 | 17 | return buf.toString("hex") === hashedPassword; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Auth-Service/src/swaggergen.ts: -------------------------------------------------------------------------------- 1 | // const swaggerDefinition = { 2 | // openapi: "3.0.2", 3 | // info: { 4 | // title: "Auth Service", 5 | // version: "v1-beta", 6 | // description: 7 | // "This is the authentication microservice for the ticketing App with handles user registrations and authentications (such as sign-up, sign-out, sing-in)", 8 | // }, 9 | // contact: { 10 | // name: "Narendran", 11 | // url: "https://github.com/narenarjun/ultimate-stack", 12 | // }, 13 | // license: { 14 | // name: "MIT", 15 | // url: "https://github.com/narenarjun/ultimate-stack/blob/master/LICENSE", 16 | // }, 17 | // servers: [ 18 | // { 19 | // url: `${ 20 | // process.env.SERVICE_BASE_URL 21 | // ? process.env.SERVICE_BASE_URL 22 | // : "localhost:4000" 23 | // }`, 24 | // description: "Staging Server", 25 | // }, 26 | // ], 27 | // }; 28 | 29 | // const options = { 30 | // swaggerDefinition, 31 | // // Path to the fields containing OpenAPI definitions 32 | // // src/auth-svc.yml 33 | // apis: ["src/**/*.ts"], 34 | // }; 35 | 36 | // export { options }; 37 | -------------------------------------------------------------------------------- /Auth-Service/src/test/setup.ts: -------------------------------------------------------------------------------- 1 | import { MongoMemoryServer } from "mongodb-memory-server"; 2 | import mongoose from "mongoose"; 3 | import request from "supertest"; 4 | import { app } from "../app"; 5 | 6 | declare global{ 7 | namespace NodeJS { 8 | interface Global { 9 | signup(): Promise 10 | } 11 | } 12 | } 13 | 14 | let mongo: any; 15 | beforeAll(async () => { 16 | //! setting environment variable for jwtsecret for test environment 17 | process.env.JWTSECRET = "qwer123qwer"; 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.signup = 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({ email, password }) 47 | .expect(201); 48 | 49 | const cookie = response.get("Set-Cookie"); 50 | return cookie; 51 | }; 52 | -------------------------------------------------------------------------------- /Expiration-Service/.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /Expiration-Service/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM quay.io/narendev/fishnode:alpine 2 | 3 | WORKDIR /app 4 | 5 | COPY package.json . 6 | RUN yarn install --production 7 | COPY . . 8 | 9 | CMD [ "yarn","start" ] -------------------------------------------------------------------------------- /Expiration-Service/README.md: -------------------------------------------------------------------------------- 1 | # Expiration Service 2 | -------------------------------------------------------------------------------- /Expiration-Service/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: "ts-jest", 3 | testEnvironment: "node", 4 | setupFilesAfterEnv: ["./src/test/setup.ts"], 5 | }; 6 | 7 | // "jest": { 8 | // "preset": "ts-jest", 9 | // "testEnvironment": "node", 10 | // "setupFilesAfterEnv": [ 11 | // "./src/test/setup.ts" 12 | // ] 13 | // }, 14 | -------------------------------------------------------------------------------- /Expiration-Service/okteto.yml: -------------------------------------------------------------------------------- 1 | name: mcs-expiration-service 2 | autocreate: true 3 | image: quay.io/narendev/fishnode:1.0 4 | command: fish 5 | sync: 6 | - .:/usr/src/app 7 | forward: 8 | - 9229:9229 9 | - 3000:3000 10 | persistentVolume: {} 11 | annotations: 12 | sidecar.istio.io/inject: "false" 13 | -------------------------------------------------------------------------------- /Expiration-Service/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "expiration-service", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "ts-node-dev src/index.ts", 8 | "test": "jest --watchAll --no-cache" 9 | }, 10 | "keywords": [], 11 | "author": "", 12 | "license": "ISC", 13 | "dependencies": { 14 | "@types/bull": "^3.15.0", 15 | "@wowowow/common": "^1.0.11", 16 | "bull": "^3.20.1", 17 | "node-nats-streaming": "^0.3.2", 18 | "ts-node-dev": "^1.1.6", 19 | "typescript": "^4.2.2" 20 | }, 21 | "devDependencies": { 22 | "@types/jest": "^26.0.20", 23 | "jest": "^26.6.3", 24 | "ts-jest": "^26.5.3" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Expiration-Service/src/__mocks__/nats-wrapper.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 | -------------------------------------------------------------------------------- /Expiration-Service/src/events/listeners/order-created-listener.ts: -------------------------------------------------------------------------------- 1 | import { Listener, OrderCreatedEvent, Subjects } from "@wowowow/common"; 2 | import { Message } from "node-nats-streaming"; 3 | import { expirationQueue } from "../../queues/expiration-queue"; 4 | import { queueGroupName } from "./queGroupName"; 5 | 6 | export class OrderCreatedListener extends Listener { 7 | subject: Subjects.OrderCreated = Subjects.OrderCreated; 8 | queueGroupName = queueGroupName; 9 | 10 | async onMessage(data: OrderCreatedEvent["data"], msg: Message) { 11 | const delay = new Date(data.expiredAt).getTime() - new Date().getTime(); 12 | 13 | await expirationQueue.add( 14 | { 15 | orderId: data.id, 16 | }, 17 | { 18 | delay, 19 | } 20 | ); 21 | 22 | msg.ack(); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Expiration-Service/src/events/listeners/queGroupName.ts: -------------------------------------------------------------------------------- 1 | export const queueGroupName = 'expiration-service' -------------------------------------------------------------------------------- /Expiration-Service/src/events/publishers/expiration-complete-publisher.ts: -------------------------------------------------------------------------------- 1 | import { ExpirationCompleteEvent, Publisher, Subjects } from "@wowowow/common"; 2 | 3 | export class ExpirationCompletePublisher extends Publisher { 4 | subject: Subjects.ExpirationComplete = Subjects.ExpirationComplete; 5 | } 6 | -------------------------------------------------------------------------------- /Expiration-Service/src/index.ts: -------------------------------------------------------------------------------- 1 | import { OrderCreatedListener } from "./events/listeners/order-created-listener"; 2 | import { natsWrapper } from "./nats-wrapper"; 3 | 4 | // mongoose connection 5 | const start = async () => { 6 | if (!process.env.NATS_CLIENT_ID) { 7 | throw new Error("NATS_CLIENT_ID must be defined"); 8 | } 9 | if (!process.env.NATS_URL) { 10 | throw new Error("NATS_URL must be defined"); 11 | } 12 | if (!process.env.NATS_CLUSTER_ID) { 13 | throw new Error("NATS_CLUSTER_ID must be defined"); 14 | } 15 | 16 | if (!process.env.REDIS_HOST) { 17 | throw new Error("REDIS_HOST must be defined") 18 | } 19 | 20 | try { 21 | await natsWrapper.connect( 22 | process.env.NATS_CLUSTER_ID, 23 | process.env.NATS_CLIENT_ID, 24 | process.env.NATS_URL 25 | ); 26 | 27 | // ? Graceful shutdown for NATS streaming server 28 | natsWrapper.client.on("close", () => { 29 | console.log("NATS connection closed!"); 30 | process.exit(); 31 | }); 32 | 33 | process.on("SIGINT", () => natsWrapper.client.close()); 34 | process.on("SIGTERM", () => natsWrapper.client.close()); 35 | 36 | new OrderCreatedListener(natsWrapper.client).listen(); 37 | } catch (err) { 38 | console.error(err); 39 | } 40 | }; 41 | 42 | start(); 43 | -------------------------------------------------------------------------------- /Expiration-Service/src/nats-wrapper.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("Cannot access NATS client before connecting"); 9 | } 10 | return this._client; 11 | } 12 | 13 | connect(clusterId: string, clientId: string, url: string): Promise { 14 | this._client = nats.connect(clusterId, clientId, { url }); 15 | 16 | return new Promise((resolve, reject) => { 17 | this.client.on("connect", () => { 18 | console.log("Connected to NATS"); 19 | resolve(); 20 | }); 21 | 22 | this.client.on("error", (err) => { 23 | reject(err); 24 | }); 25 | }); 26 | } 27 | } 28 | 29 | export const natsWrapper = new NatsWrapper(); 30 | -------------------------------------------------------------------------------- /Expiration-Service/src/queues/expiration-queue.ts: -------------------------------------------------------------------------------- 1 | import Queue from "bull"; 2 | import { ExpirationCompletePublisher } from "../events/publishers/expiration-complete-publisher"; 3 | import { natsWrapper } from "../nats-wrapper"; 4 | 5 | interface Payload { 6 | orderId: string; 7 | } 8 | 9 | const expirationQueue = new Queue("order:expiration", { 10 | redis: { 11 | host: process.env.REDIS_HOST, 12 | }, 13 | }); 14 | 15 | expirationQueue.process(async (job) => { 16 | new ExpirationCompletePublisher(natsWrapper.client).publish({ 17 | orderId: job.data.orderId, 18 | }); 19 | }); 20 | 21 | export { expirationQueue }; 22 | -------------------------------------------------------------------------------- /Frontend/.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .next -------------------------------------------------------------------------------- /Frontend/.stignore: -------------------------------------------------------------------------------- 1 | .git 2 | # Runtime data 3 | pids 4 | *.pid 5 | *.seed 6 | *.pid.lock 7 | 8 | # Directory for instrumented libs generated by jscoverage/JSCover 9 | lib-cov 10 | 11 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 12 | .grunt 13 | 14 | # Bower dependency directory (https://bower.io/) 15 | bower_components 16 | 17 | # node-waf configuration 18 | .lock-wscript 19 | 20 | # Compiled binary addons (https://nodejs.org/api/addons.html) 21 | build/Release 22 | 23 | # Dependency directories 24 | node_modules 25 | jspm_packages 26 | 27 | # Optional npm cache directory 28 | .npm 29 | 30 | # Optional eslint cache 31 | .eslintcache 32 | 33 | # Optional REPL history 34 | .node_repl_history 35 | 36 | # Output of 'npm pack' 37 | *.tgz 38 | 39 | # Yarn Integrity file 40 | .yarn-integrity 41 | 42 | # parcel-bundler cache (https://parceljs.org/) 43 | .cache 44 | 45 | # next.js build output 46 | .next 47 | 48 | # nuxt.js build output 49 | .nuxt 50 | 51 | # vuepress build output 52 | .vuepress/dist 53 | 54 | # Serverless directories 55 | .serverless 56 | 57 | -------------------------------------------------------------------------------- /Frontend/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM quay.io/narendev/fishnode:alpine 2 | 3 | WORKDIR /app 4 | 5 | COPY package.json . 6 | 7 | RUN yarn install 8 | 9 | COPY . . 10 | 11 | # RUN yarn build 12 | 13 | EXPOSE 3001 14 | 15 | CMD ["yarn","dev"] -------------------------------------------------------------------------------- /Frontend/README.md: -------------------------------------------------------------------------------- 1 | # must do before prod build 2 | 3 | update the urls used in the signin,signup and signout 4 | maybe introduce environment variable to prefix baseurl 5 | change url in buildclient api too 6 | 7 | > NOTE : 8 | > To list all the running ports and addresses in linux , use `netstat -ltup` 9 | 10 | -------------------------------------------------------------------------------- /Frontend/api/build-client.js: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | 3 | // ! the baseurls must be updated for prod build 4 | 5 | const BuildClient = ({ req }) => { 6 | if (typeof window === "undefined") { 7 | // We are on the server 8 | 9 | return axios.create({ 10 | baseURL: "http://istio-ingressgateway.istio-system.svc.cluster.local", 11 | headers: req.headers, 12 | }); 13 | } else { 14 | // We must be on the browser 15 | 16 | return axios.create({ 17 | baseUrl: `http://${process.env.NEXT_PUBLIC_BASEURL}`, 18 | }); 19 | } 20 | 21 | }; 22 | 23 | export default BuildClient; 24 | -------------------------------------------------------------------------------- /Frontend/components/header.js: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | 3 | const Header = ({ 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 | 35 | export default Header; 36 | -------------------------------------------------------------------------------- /Frontend/hooks/use-request.js: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | import { useState } from "react"; 3 | 4 | const useRequest = ({ url, method, body, onSuccess }) => { 5 | const [errors, setErrors] = useState(null); 6 | 7 | const doRequest = async (props = {}) => { 8 | try { 9 | setErrors(null); 10 | const response = await axios[method]( 11 | url, 12 | { ...body, ...props }, 13 | { 14 | headers: { 15 | "Content-Type": "application/json", 16 | }, 17 | withCredentials: true, 18 | } 19 | ); 20 | 21 | if (onSuccess) { 22 | onSuccess(response.data); 23 | } 24 | 25 | return response.data; 26 | } catch (err) { 27 | setErrors( 28 |
    29 |

    Ooops....

    30 |
      31 | {err.response.data.errors.map((err) => ( 32 |
    • {err.message}
    • 33 | ))} 34 |
    35 |
    36 | ); 37 | } 38 | }; 39 | 40 | return { doRequest, errors }; 41 | }; 42 | 43 | export default useRequest; 44 | -------------------------------------------------------------------------------- /Frontend/next.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | webpackDevMiddleware: (config) => { 3 | config.watchOptions.poll = 300; 4 | return config; 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /Frontend/okteto.yml: -------------------------------------------------------------------------------- 1 | name: microservice-ticketing-app-frontend 2 | autocreate: true 3 | image: quay.io/narendev/fishnode:1.0 4 | command: fish 5 | sync: 6 | - .:/usr/src/app 7 | forward: 8 | - 9229:9229 9 | - 3000:3000 10 | persistentVolume: {} 11 | annotations: 12 | sidecar.istio.io/inject: "false" 13 | 14 | -------------------------------------------------------------------------------- /Frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "client", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "dev": "next dev -p 3001", 8 | "build" :"next build", 9 | "start": "next start -p 3002" 10 | }, 11 | "keywords": [], 12 | "author": "", 13 | "license": "ISC", 14 | "dependencies": { 15 | "axios": "^0.21.1", 16 | "bootstrap": "^4.6.0", 17 | "next": "^10.0.7", 18 | "react": "^17.0.1", 19 | "react-dom": "^17.0.1", 20 | "react-stripe-checkout": "^2.6.3" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Frontend/pages/_app.js: -------------------------------------------------------------------------------- 1 | import "bootstrap/dist/css/bootstrap.css"; 2 | import BuildClient from "../api/build-client"; 3 | 4 | import Header from "../components/header"; 5 | 6 | const AppComponent = ({ Component, pageProps, currentUser }) => { 7 | return ( 8 |
    9 |
    10 |
    11 | 12 |
    13 |
    14 | ); 15 | }; 16 | 17 | AppComponent.getInitialProps = async (appContext) => { 18 | const client = BuildClient(appContext.ctx); 19 | 20 | let urlvl = "/api/users/currentuser"; 21 | 22 | if (typeof window !== undefined) { 23 | urlvl = `http://${process.env.NEXT_PUBLIC_BASEURL}/api/users/currentuser`; 24 | } 25 | 26 | const { data } = await client.get(urlvl); 27 | 28 | 29 | let pageProps = {}; 30 | if (appContext.Component.getInitialProps) { 31 | pageProps = await appContext.Component.getInitialProps( 32 | appContext.ctx, 33 | client, 34 | data.currentUser 35 | ); 36 | } 37 | 38 | return { 39 | pageProps, 40 | ...data, 41 | }; 42 | }; 43 | 44 | export default AppComponent; 45 | -------------------------------------------------------------------------------- /Frontend/pages/auth/signin.js: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from "react"; 2 | import Router from "next/router"; 3 | import useRequest from "../../hooks/use-request"; 4 | 5 | const SignIn = () => { 6 | const [email, setEmail] = useState(""); 7 | const [password, setPassword] = useState(""); 8 | const { doRequest, errors } = useRequest({ 9 | url: `http://${process.env.NEXT_PUBLIC_BASEURL}/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 | console.log("this is called"); 21 | console.log("the value of base url:", process.env.NEXT_PUBLIC_BASEURL); 22 | await doRequest(); 23 | }; 24 | 25 | return ( 26 |
    27 |

    Sign In

    28 |
    29 | 30 | setEmail(e.target.value)} 33 | className="form-control" 34 | /> 35 |
    36 |
    37 | 38 | setPassword(e.target.value)} 41 | type="password" 42 | className="form-control" 43 | /> 44 |
    45 | {errors} 46 | 47 |
    48 | ); 49 | }; 50 | 51 | export default SignIn; 52 | -------------------------------------------------------------------------------- /Frontend/pages/auth/signout.js: -------------------------------------------------------------------------------- 1 | import { useEffect } from "react"; 2 | import Router from "next/router"; 3 | import useRequest from "../../hooks/use-request"; 4 | 5 | const SignOut = () => { 6 | const { doRequest } = useRequest({ 7 | url: `http://${process.env.NEXT_PUBLIC_BASEURL}/api/users/signout`, 8 | method: "post", 9 | body: {}, 10 | onSuccess: () => Router.push("/"), 11 | }); 12 | 13 | useEffect(() => { 14 | doRequest(); 15 | }, []); 16 | 17 | return
    Signing you out...
    ; 18 | }; 19 | 20 | export default SignOut; 21 | -------------------------------------------------------------------------------- /Frontend/pages/auth/signup.js: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from "react"; 2 | import Router from "next/router"; 3 | import useRequest from "../../hooks/use-request"; 4 | 5 | const SignUp = () => { 6 | const [email, setEmail] = useState(""); 7 | const [password, setPassword] = useState(""); 8 | const { doRequest, errors } = useRequest({ 9 | url: `http://${process.env.NEXT_PUBLIC_BASEURL}/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 | 21 | await doRequest(); 22 | }; 23 | 24 | return ( 25 |
    26 |

    Sign Up

    27 |
    28 | 29 | setEmail(e.target.value)} 32 | className="form-control" 33 | /> 34 |
    35 |
    36 | 37 | setPassword(e.target.value)} 40 | type="password" 41 | className="form-control" 42 | /> 43 |
    44 | {errors} 45 | 46 |
    47 | ); 48 | }; 49 | export default SignUp; 50 | -------------------------------------------------------------------------------- /Frontend/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 | return ( 18 |
    19 |

    Tickets

    20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | {ticketList} 29 |
    TitlePriceLink
    30 |
    31 | ); 32 | }; 33 | 34 | LandingPage.getInitialProps = async (context, client, currentUser) => { 35 | 36 | 37 | let urlvl = "/api/tickets"; 38 | 39 | if (typeof window !== undefined) { 40 | urlvl = `http://${process.env.NEXT_PUBLIC_BASEURL}/api/tickets`; 41 | } 42 | 43 | 44 | const { data } = await client.get(urlvl); 45 | return { tickets: data }; 46 | }; 47 | 48 | export default LandingPage; 49 | -------------------------------------------------------------------------------- /Frontend/pages/orders/[orderId].js: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from "react"; 2 | import StripeCheckout from "react-stripe-checkout"; 3 | import Router from "next/router"; 4 | import useRequest from "../../hooks/use-request"; 5 | 6 | const OrderShow = ({ order, currentUser }) => { 7 | const [timeLeft, setTimeLeft] = useState(0); 8 | const { doRequest, errors } = useRequest({ 9 | url: `http://${process.env.NEXT_PUBLIC_BASEURL}/api/payments`, 10 | method: "post", 11 | body: { 12 | orderId: order.id, 13 | }, 14 | onSuccess: () => Router.push("/orders"), 15 | }); 16 | 17 | useEffect(() => { 18 | const findTimeLeft = () => { 19 | const msLeft = new Date(order.expiresAt) - new Date(); 20 | setTimeLeft(Math.round(msLeft / 1000)); 21 | }; 22 | 23 | findTimeLeft(); 24 | const timerId = setInterval(findTimeLeft, 1000); 25 | 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= {`${process.env.NEXT_PUBLIC_STRIPE_KEY}`} 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 | 52 | let urlvl = `/api/orders/${orderId}`; 53 | 54 | if (typeof window !== undefined) { 55 | urlvl = `http://${process.env.NEXT_PUBLIC_BASEURL}/api/orders/${orderId}`; 56 | } 57 | 58 | const { data } = await client.get(urlvl); 59 | return { order: data }; 60 | }; 61 | 62 | export default OrderShow; 63 | -------------------------------------------------------------------------------- /Frontend/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 | 17 | 18 | let urlvl = "/api/orders"; 19 | 20 | if (typeof window !== undefined) { 21 | urlvl = `http://${process.env.NEXT_PUBLIC_BASEURL}/api/orders`; 22 | } 23 | 24 | 25 | const { data } = await client.get(urlvl); 26 | 27 | return { orders: data }; 28 | }; 29 | 30 | export default OrderIndex; 31 | -------------------------------------------------------------------------------- /Frontend/pages/tickets/[ticketId].js: -------------------------------------------------------------------------------- 1 | import Router from 'next/router'; 2 | import useRequest from '../../hooks/use-request'; 3 | 4 | const TicketShow = ({ ticket }) => { 5 | const { doRequest, errors } = useRequest({ 6 | url: `http://${process.env.NEXT_PUBLIC_BASEURL}/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 | 30 | let urlvl = `/api/tickets/${ticketId}`; 31 | 32 | if (typeof window !== undefined) { 33 | urlvl = `http://${process.env.NEXT_PUBLIC_BASEURL}/api/tickets/${ticketId}`; 34 | } 35 | 36 | 37 | const { data } = await client.get(urlvl); 38 | 39 | return { ticket: data }; 40 | }; 41 | 42 | export default TicketShow; 43 | -------------------------------------------------------------------------------- /Frontend/pages/tickets/new.js: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | import Router from "next/router"; 3 | import useRequest from "../../hooks/use-request"; 4 | 5 | // ! ticket service url must be given 6 | const NewTicket = () => { 7 | const [title, setTitle] = useState(""); 8 | const [price, setPrice] = useState(""); 9 | const { doRequest, errors } = useRequest({ 10 | url: `http://${process.env.NEXT_PUBLIC_BASEURL}/api/tickets`, 11 | method: "post", 12 | body: { 13 | title, 14 | price, 15 | }, 16 | onSuccess: () => Router.push("/"), 17 | }); 18 | 19 | const onSubmit = (event) => { 20 | event.preventDefault(); 21 | 22 | doRequest(); 23 | }; 24 | 25 | const onBlur = () => { 26 | const value = parseFloat(price); 27 | 28 | if (isNaN(value)) { 29 | return; 30 | } 31 | 32 | setPrice(value.toFixed(2)); 33 | }; 34 | 35 | return ( 36 |
    37 |

    Create a Ticket

    38 |
    39 |
    40 | 41 | setTitle(e.target.value)} 44 | className="form-control" 45 | /> 46 |
    47 |
    48 | 49 | setPrice(e.target.value)} 53 | className="form-control" 54 | /> 55 |
    56 | {errors} 57 | 58 |
    59 |
    60 | ); 61 | }; 62 | 63 | export default NewTicket; 64 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2021, Narendran 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software is furnished to do so, 8 | subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 15 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 16 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 17 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 18 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /Library/common/README.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nrndev/ultimate-stack/5b94132675a54490489d8f65757e70b60043d428/Library/common/README.md -------------------------------------------------------------------------------- /Library/common/build/errors/bad-request-error.d.ts: -------------------------------------------------------------------------------- 1 | import { CustomError } from "./custom-errror"; 2 | export declare class BadRequestError extends CustomError { 3 | message: string; 4 | statusCode: number; 5 | constructor(message: string); 6 | serializeErrors(): { 7 | message: string; 8 | }[]; 9 | } 10 | -------------------------------------------------------------------------------- /Library/common/build/errors/bad-request-error.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __extends = (this && this.__extends) || (function () { 3 | var extendStatics = function (d, b) { 4 | extendStatics = Object.setPrototypeOf || 5 | ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || 6 | function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; }; 7 | return extendStatics(d, b); 8 | }; 9 | return function (d, b) { 10 | if (typeof b !== "function" && b !== null) 11 | throw new TypeError("Class extends value " + String(b) + " is not a constructor or null"); 12 | extendStatics(d, b); 13 | function __() { this.constructor = d; } 14 | d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); 15 | }; 16 | })(); 17 | Object.defineProperty(exports, "__esModule", { value: true }); 18 | exports.BadRequestError = void 0; 19 | var custom_errror_1 = require("./custom-errror"); 20 | var BadRequestError = /** @class */ (function (_super) { 21 | __extends(BadRequestError, _super); 22 | function BadRequestError(message) { 23 | var _this = _super.call(this, message) || this; 24 | _this.message = message; 25 | _this.statusCode = 400; 26 | Object.setPrototypeOf(_this, BadRequestError.prototype); 27 | return _this; 28 | } 29 | BadRequestError.prototype.serializeErrors = function () { 30 | return [{ message: this.message }]; 31 | }; 32 | return BadRequestError; 33 | }(custom_errror_1.CustomError)); 34 | exports.BadRequestError = BadRequestError; 35 | -------------------------------------------------------------------------------- /Library/common/build/errors/custom-errror.d.ts: -------------------------------------------------------------------------------- 1 | export declare abstract class CustomError extends Error { 2 | abstract statusCode: number; 3 | constructor(message: string); 4 | abstract serializeErrors(): { 5 | message: string; 6 | field?: string; 7 | }[]; 8 | } 9 | -------------------------------------------------------------------------------- /Library/common/build/errors/custom-errror.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __extends = (this && this.__extends) || (function () { 3 | var extendStatics = function (d, b) { 4 | extendStatics = Object.setPrototypeOf || 5 | ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || 6 | function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; }; 7 | return extendStatics(d, b); 8 | }; 9 | return function (d, b) { 10 | if (typeof b !== "function" && b !== null) 11 | throw new TypeError("Class extends value " + String(b) + " is not a constructor or null"); 12 | extendStatics(d, b); 13 | function __() { this.constructor = d; } 14 | d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); 15 | }; 16 | })(); 17 | Object.defineProperty(exports, "__esModule", { value: true }); 18 | exports.CustomError = void 0; 19 | var CustomError = /** @class */ (function (_super) { 20 | __extends(CustomError, _super); 21 | function CustomError(message) { 22 | var _this = _super.call(this, message) || this; 23 | Object.setPrototypeOf(_this, CustomError.prototype); 24 | return _this; 25 | } 26 | return CustomError; 27 | }(Error)); 28 | exports.CustomError = CustomError; 29 | -------------------------------------------------------------------------------- /Library/common/build/errors/database-connection-error.d.ts: -------------------------------------------------------------------------------- 1 | import { CustomError } from "./custom-errror"; 2 | export declare class DatabaseConnectionError extends CustomError { 3 | statusCode: number; 4 | reason: string; 5 | constructor(); 6 | serializeErrors(): { 7 | message: string; 8 | }[]; 9 | } 10 | -------------------------------------------------------------------------------- /Library/common/build/errors/database-connection-error.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __extends = (this && this.__extends) || (function () { 3 | var extendStatics = function (d, b) { 4 | extendStatics = Object.setPrototypeOf || 5 | ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || 6 | function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; }; 7 | return extendStatics(d, b); 8 | }; 9 | return function (d, b) { 10 | if (typeof b !== "function" && b !== null) 11 | throw new TypeError("Class extends value " + String(b) + " is not a constructor or null"); 12 | extendStatics(d, b); 13 | function __() { this.constructor = d; } 14 | d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); 15 | }; 16 | })(); 17 | Object.defineProperty(exports, "__esModule", { value: true }); 18 | exports.DatabaseConnectionError = void 0; 19 | var custom_errror_1 = require("./custom-errror"); 20 | var DatabaseConnectionError = /** @class */ (function (_super) { 21 | __extends(DatabaseConnectionError, _super); 22 | function DatabaseConnectionError() { 23 | var _this = _super.call(this, 'Error connecting to DB') || this; 24 | _this.statusCode = 500; 25 | _this.reason = "Error connection to database"; 26 | // Only because we are extending a built in class 27 | Object.setPrototypeOf(_this, DatabaseConnectionError.prototype); 28 | return _this; 29 | } 30 | DatabaseConnectionError.prototype.serializeErrors = function () { 31 | return [ 32 | { 33 | message: this.reason, 34 | }, 35 | ]; 36 | }; 37 | return DatabaseConnectionError; 38 | }(custom_errror_1.CustomError)); 39 | exports.DatabaseConnectionError = DatabaseConnectionError; 40 | -------------------------------------------------------------------------------- /Library/common/build/errors/not-authorized-error.d.ts: -------------------------------------------------------------------------------- 1 | import { CustomError } from "./custom-errror"; 2 | export declare class NotAuthorizedError extends CustomError { 3 | statusCode: number; 4 | constructor(); 5 | serializeErrors(): { 6 | message: string; 7 | }[]; 8 | } 9 | -------------------------------------------------------------------------------- /Library/common/build/errors/not-authorized-error.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __extends = (this && this.__extends) || (function () { 3 | var extendStatics = function (d, b) { 4 | extendStatics = Object.setPrototypeOf || 5 | ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || 6 | function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; }; 7 | return extendStatics(d, b); 8 | }; 9 | return function (d, b) { 10 | if (typeof b !== "function" && b !== null) 11 | throw new TypeError("Class extends value " + String(b) + " is not a constructor or null"); 12 | extendStatics(d, b); 13 | function __() { this.constructor = d; } 14 | d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); 15 | }; 16 | })(); 17 | Object.defineProperty(exports, "__esModule", { value: true }); 18 | exports.NotAuthorizedError = void 0; 19 | var custom_errror_1 = require("./custom-errror"); 20 | var NotAuthorizedError = /** @class */ (function (_super) { 21 | __extends(NotAuthorizedError, _super); 22 | function NotAuthorizedError() { 23 | var _this = _super.call(this, "Not Authorized !!") || this; 24 | _this.statusCode = 401; 25 | Object.setPrototypeOf(_this, NotAuthorizedError.prototype); 26 | return _this; 27 | } 28 | NotAuthorizedError.prototype.serializeErrors = function () { 29 | return [{ message: "Not Authorized !!" }]; 30 | }; 31 | return NotAuthorizedError; 32 | }(custom_errror_1.CustomError)); 33 | exports.NotAuthorizedError = NotAuthorizedError; 34 | -------------------------------------------------------------------------------- /Library/common/build/errors/not-found-error.d.ts: -------------------------------------------------------------------------------- 1 | import { CustomError } from "./custom-errror"; 2 | export declare class NotFoundError extends CustomError { 3 | statusCode: number; 4 | constructor(); 5 | serializeErrors(): { 6 | message: string; 7 | }[]; 8 | } 9 | -------------------------------------------------------------------------------- /Library/common/build/errors/not-found-error.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __extends = (this && this.__extends) || (function () { 3 | var extendStatics = function (d, b) { 4 | extendStatics = Object.setPrototypeOf || 5 | ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || 6 | function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; }; 7 | return extendStatics(d, b); 8 | }; 9 | return function (d, b) { 10 | if (typeof b !== "function" && b !== null) 11 | throw new TypeError("Class extends value " + String(b) + " is not a constructor or null"); 12 | extendStatics(d, b); 13 | function __() { this.constructor = d; } 14 | d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); 15 | }; 16 | })(); 17 | Object.defineProperty(exports, "__esModule", { value: true }); 18 | exports.NotFoundError = void 0; 19 | var custom_errror_1 = require("./custom-errror"); 20 | var NotFoundError = /** @class */ (function (_super) { 21 | __extends(NotFoundError, _super); 22 | function NotFoundError() { 23 | var _this = _super.call(this, "Route not found") || this; 24 | _this.statusCode = 404; 25 | Object.setPrototypeOf(_this, NotFoundError.prototype); 26 | return _this; 27 | } 28 | NotFoundError.prototype.serializeErrors = function () { 29 | return [{ message: 'Not found' }]; 30 | }; 31 | return NotFoundError; 32 | }(custom_errror_1.CustomError)); 33 | exports.NotFoundError = NotFoundError; 34 | -------------------------------------------------------------------------------- /Library/common/build/errors/request-validation-error.d.ts: -------------------------------------------------------------------------------- 1 | import { ValidationError } from "express-validator"; 2 | import { CustomError } from "./custom-errror"; 3 | export declare class RequestValidationError extends CustomError { 4 | errors: ValidationError[]; 5 | statusCode: number; 6 | constructor(errors: ValidationError[]); 7 | serializeErrors(): { 8 | message: any; 9 | field: string; 10 | }[]; 11 | } 12 | -------------------------------------------------------------------------------- /Library/common/build/errors/request-validation-error.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __extends = (this && this.__extends) || (function () { 3 | var extendStatics = function (d, b) { 4 | extendStatics = Object.setPrototypeOf || 5 | ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || 6 | function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; }; 7 | return extendStatics(d, b); 8 | }; 9 | return function (d, b) { 10 | if (typeof b !== "function" && b !== null) 11 | throw new TypeError("Class extends value " + String(b) + " is not a constructor or null"); 12 | extendStatics(d, b); 13 | function __() { this.constructor = d; } 14 | d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); 15 | }; 16 | })(); 17 | Object.defineProperty(exports, "__esModule", { value: true }); 18 | exports.RequestValidationError = void 0; 19 | var custom_errror_1 = require("./custom-errror"); 20 | var RequestValidationError = /** @class */ (function (_super) { 21 | __extends(RequestValidationError, _super); 22 | function RequestValidationError(errors) { 23 | var _this = _super.call(this, 'Invalid request parameters') || this; 24 | _this.errors = errors; 25 | _this.statusCode = 400; 26 | // Only because we are extending a built in class 27 | Object.setPrototypeOf(_this, RequestValidationError.prototype); 28 | return _this; 29 | } 30 | RequestValidationError.prototype.serializeErrors = function () { 31 | return this.errors.map(function (err) { 32 | return { message: err.msg, field: err.param }; 33 | }); 34 | }; 35 | return RequestValidationError; 36 | }(custom_errror_1.CustomError)); 37 | exports.RequestValidationError = RequestValidationError; 38 | -------------------------------------------------------------------------------- /Library/common/build/events/base-listener.d.ts: -------------------------------------------------------------------------------- 1 | import { Message, Stan } from "node-nats-streaming"; 2 | import { Subjects } from "./subjects"; 3 | interface Event { 4 | subject: Subjects; 5 | data: any; 6 | } 7 | export declare abstract class Listener { 8 | abstract subject: T["subject"]; 9 | abstract queueGroupName: string; 10 | abstract onMessage(data: T["data"], msg: Message): void; 11 | protected client: Stan; 12 | protected ackwait: number; 13 | constructor(client: Stan); 14 | subscriptionOptions(): import("node-nats-streaming").SubscriptionOptions; 15 | listen(): void; 16 | parseMessage(msg: Message): any; 17 | } 18 | export {}; 19 | -------------------------------------------------------------------------------- /Library/common/build/events/base-listener.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | exports.Listener = void 0; 4 | var Listener = /** @class */ (function () { 5 | function Listener(client) { 6 | this.ackwait = 5 * 1000; 7 | this.client = client; 8 | } 9 | Listener.prototype.subscriptionOptions = function () { 10 | return this.client 11 | .subscriptionOptions() 12 | .setDeliverAllAvailable() 13 | .setManualAckMode(true) 14 | .setAckWait(this.ackwait) 15 | .setDurableName(this.queueGroupName); 16 | }; 17 | Listener.prototype.listen = function () { 18 | var _this = this; 19 | var subscription = this.client.subscribe(this.subject, this.queueGroupName, this.subscriptionOptions()); 20 | subscription.on("message", function (msg) { 21 | console.log("Message received: " + _this.subject + " / " + _this.queueGroupName); 22 | var parsedData = _this.parseMessage(msg); 23 | _this.onMessage(parsedData, msg); 24 | }); 25 | }; 26 | Listener.prototype.parseMessage = function (msg) { 27 | var data = msg.getData(); 28 | return typeof data === "string" 29 | ? JSON.parse(data) 30 | : JSON.parse(data.toString("utf8")); 31 | }; 32 | return Listener; 33 | }()); 34 | exports.Listener = Listener; 35 | -------------------------------------------------------------------------------- /Library/common/build/events/base-publisher.d.ts: -------------------------------------------------------------------------------- 1 | import { Stan } from "node-nats-streaming"; 2 | import { Subjects } from "./subjects"; 3 | interface Event { 4 | subject: Subjects; 5 | data: any; 6 | } 7 | export declare abstract class Publisher { 8 | abstract subject: T["subject"]; 9 | protected client: Stan; 10 | constructor(client: Stan); 11 | publish(data: T["data"]): Promise; 12 | } 13 | export {}; 14 | -------------------------------------------------------------------------------- /Library/common/build/events/base-publisher.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | exports.Publisher = void 0; 4 | var Publisher = /** @class */ (function () { 5 | function Publisher(client) { 6 | this.client = client; 7 | } 8 | Publisher.prototype.publish = function (data) { 9 | var _this = this; 10 | return new Promise(function (resolve, reject) { 11 | _this.client.publish(_this.subject, JSON.stringify(data), function (err) { 12 | if (err) { 13 | return reject(err); 14 | } 15 | console.log("Event published to subject", _this.subject); 16 | resolve(); 17 | }); 18 | }); 19 | }; 20 | return Publisher; 21 | }()); 22 | exports.Publisher = Publisher; 23 | -------------------------------------------------------------------------------- /Library/common/build/events/expiration-complete-event.d.ts: -------------------------------------------------------------------------------- 1 | import { Subjects } from "./subjects"; 2 | export interface ExpirationCompleteEvent { 3 | subject: Subjects.ExpirationComplete; 4 | data: { 5 | orderId: string; 6 | }; 7 | } 8 | -------------------------------------------------------------------------------- /Library/common/build/events/expiration-complete-event.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | -------------------------------------------------------------------------------- /Library/common/build/events/order-cancelled-event.d.ts: -------------------------------------------------------------------------------- 1 | import { Subjects } from "./subjects"; 2 | export interface OrderCancelledEvent { 3 | subject: Subjects.OrderCancelled; 4 | data: { 5 | id: string; 6 | version: number; 7 | ticket: { 8 | id: string; 9 | }; 10 | }; 11 | } 12 | -------------------------------------------------------------------------------- /Library/common/build/events/order-cancelled-event.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | -------------------------------------------------------------------------------- /Library/common/build/events/order-created-event.d.ts: -------------------------------------------------------------------------------- 1 | import { Subjects } from "./subjects"; 2 | import { OrderStatus } from "./types/order-status"; 3 | export interface OrderCreatedEvent { 4 | subject: Subjects.OrderCreated; 5 | data: { 6 | id: string; 7 | version: number; 8 | status: OrderStatus; 9 | userId: string; 10 | expiredAt: string; 11 | ticket: { 12 | id: string; 13 | price: number; 14 | }; 15 | }; 16 | } 17 | -------------------------------------------------------------------------------- /Library/common/build/events/order-created-event.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | -------------------------------------------------------------------------------- /Library/common/build/events/payment-created-event.d.ts: -------------------------------------------------------------------------------- 1 | import { Subjects } from "./subjects"; 2 | export interface PaymentCreatedEvent { 3 | subject: Subjects.PaymentCreated; 4 | data: { 5 | id: string; 6 | orderId: string; 7 | stripeId: string; 8 | }; 9 | } 10 | -------------------------------------------------------------------------------- /Library/common/build/events/payment-created-event.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | -------------------------------------------------------------------------------- /Library/common/build/events/subjects.d.ts: -------------------------------------------------------------------------------- 1 | export declare enum Subjects { 2 | TicketCreated = "ticket:created", 3 | TicketUpdated = "ticket:updated", 4 | OrderCreated = "order:created", 5 | OrderCancelled = "order:cancelled", 6 | ExpirationComplete = "expiration:complete", 7 | PaymentCreated = "payment:created" 8 | } 9 | -------------------------------------------------------------------------------- /Library/common/build/events/subjects.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | exports.Subjects = void 0; 4 | var Subjects; 5 | (function (Subjects) { 6 | Subjects["TicketCreated"] = "ticket:created"; 7 | Subjects["TicketUpdated"] = "ticket:updated"; 8 | Subjects["OrderCreated"] = "order:created"; 9 | Subjects["OrderCancelled"] = "order:cancelled"; 10 | Subjects["ExpirationComplete"] = "expiration:complete"; 11 | Subjects["PaymentCreated"] = "payment:created"; 12 | })(Subjects = exports.Subjects || (exports.Subjects = {})); 13 | -------------------------------------------------------------------------------- /Library/common/build/events/ticket-created-event.d.ts: -------------------------------------------------------------------------------- 1 | import { Subjects } from "./subjects"; 2 | export interface TicketCreatedEvent { 3 | subject: Subjects.TicketCreated; 4 | data: { 5 | id: string; 6 | version: number; 7 | title: string; 8 | price: number; 9 | userId: string; 10 | }; 11 | } 12 | -------------------------------------------------------------------------------- /Library/common/build/events/ticket-created-event.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | -------------------------------------------------------------------------------- /Library/common/build/events/ticket-updated-events.d.ts: -------------------------------------------------------------------------------- 1 | import { Subjects } from "./subjects"; 2 | export interface TicketUpdatedEvent { 3 | subject: Subjects.TicketUpdated; 4 | data: { 5 | id: string; 6 | version: number; 7 | title: string; 8 | price: number; 9 | userId: string; 10 | orderId?: string; 11 | }; 12 | } 13 | -------------------------------------------------------------------------------- /Library/common/build/events/ticket-updated-events.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | -------------------------------------------------------------------------------- /Library/common/build/events/types/order-status.d.ts: -------------------------------------------------------------------------------- 1 | export declare enum OrderStatus { 2 | Created = "created", 3 | Cancelled = "cancelled", 4 | AwaitingPayment = "awaiting:payment", 5 | Complete = "complete" 6 | } 7 | -------------------------------------------------------------------------------- /Library/common/build/events/types/order-status.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | exports.OrderStatus = void 0; 4 | var OrderStatus; 5 | (function (OrderStatus) { 6 | // when the order has been created, but the ticket 7 | // it is trying to order has not been reserved 8 | OrderStatus["Created"] = "created"; 9 | // The ticket the order is trying to reserve has already 10 | // been reserved, or when the user has cancelled the order. 11 | // The order expires before payment 12 | OrderStatus["Cancelled"] = "cancelled"; 13 | // The order has successfully reserved the ticket 14 | OrderStatus["AwaitingPayment"] = "awaiting:payment"; 15 | // The order has reserved the ticket and the usern has provided 16 | // payment successfully 17 | OrderStatus["Complete"] = "complete"; 18 | })(OrderStatus = exports.OrderStatus || (exports.OrderStatus = {})); 19 | -------------------------------------------------------------------------------- /Library/common/build/index.d.ts: -------------------------------------------------------------------------------- 1 | export * from "./errors/bad-request-error"; 2 | export * from "./errors/custom-errror"; 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 "./middlewares/current-user"; 8 | export * from "./middlewares/error-handler"; 9 | export * from "./middlewares/require-auth"; 10 | export * from "./middlewares/validate-requests"; 11 | export * from "./events/base-listener"; 12 | export * from "./events/base-publisher"; 13 | export * from "./events/subjects"; 14 | export * from "./events/ticket-created-event"; 15 | export * from "./events/ticket-updated-events"; 16 | export * from "./events/types/order-status"; 17 | export * from "./events/order-created-event"; 18 | export * from "./events/order-cancelled-event"; 19 | export * from "./events/expiration-complete-event"; 20 | export * from './events/payment-created-event'; 21 | -------------------------------------------------------------------------------- /Library/common/build/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { 3 | if (k2 === undefined) k2 = k; 4 | Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } }); 5 | }) : (function(o, m, k, k2) { 6 | if (k2 === undefined) k2 = k; 7 | o[k2] = m[k]; 8 | })); 9 | var __exportStar = (this && this.__exportStar) || function(m, exports) { 10 | for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p); 11 | }; 12 | Object.defineProperty(exports, "__esModule", { value: true }); 13 | __exportStar(require("./errors/bad-request-error"), exports); 14 | __exportStar(require("./errors/custom-errror"), exports); 15 | __exportStar(require("./errors/database-connection-error"), exports); 16 | __exportStar(require("./errors/not-authorized-error"), exports); 17 | __exportStar(require("./errors/not-found-error"), exports); 18 | __exportStar(require("./errors/request-validation-error"), exports); 19 | __exportStar(require("./middlewares/current-user"), exports); 20 | __exportStar(require("./middlewares/error-handler"), exports); 21 | __exportStar(require("./middlewares/require-auth"), exports); 22 | __exportStar(require("./middlewares/validate-requests"), exports); 23 | __exportStar(require("./events/base-listener"), exports); 24 | __exportStar(require("./events/base-publisher"), exports); 25 | __exportStar(require("./events/subjects"), exports); 26 | __exportStar(require("./events/ticket-created-event"), exports); 27 | __exportStar(require("./events/ticket-updated-events"), exports); 28 | __exportStar(require("./events/types/order-status"), exports); 29 | __exportStar(require("./events/order-created-event"), exports); 30 | __exportStar(require("./events/order-cancelled-event"), exports); 31 | __exportStar(require("./events/expiration-complete-event"), exports); 32 | __exportStar(require("./events/payment-created-event"), exports); 33 | -------------------------------------------------------------------------------- /Library/common/build/middlewares/current-user.d.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response, NextFunction } from "express"; 2 | interface UserPayload { 3 | id: string; 4 | email: string; 5 | } 6 | declare global { 7 | namespace Express { 8 | interface Request { 9 | currentUser?: UserPayload; 10 | } 11 | } 12 | } 13 | export declare const currentUser: (req: Request, res: Response, next: NextFunction) => void; 14 | export {}; 15 | -------------------------------------------------------------------------------- /Library/common/build/middlewares/current-user.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __importDefault = (this && this.__importDefault) || function (mod) { 3 | return (mod && mod.__esModule) ? mod : { "default": mod }; 4 | }; 5 | Object.defineProperty(exports, "__esModule", { value: true }); 6 | exports.currentUser = void 0; 7 | var jsonwebtoken_1 = __importDefault(require("jsonwebtoken")); 8 | var currentUser = function (req, res, next) { 9 | var _a; 10 | if (!((_a = req.session) === null || _a === void 0 ? void 0 : _a.jwt)) { 11 | return next(); 12 | } 13 | try { 14 | var payload = jsonwebtoken_1.default.verify(req.session.jwt, process.env.JWTSECRET); 15 | req.currentUser = payload; 16 | } 17 | catch (err) { } 18 | next(); 19 | }; 20 | exports.currentUser = currentUser; 21 | -------------------------------------------------------------------------------- /Library/common/build/middlewares/error-handler.d.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response, NextFunction } from "express"; 2 | export declare const errorHandler: (err: Error, req: Request, res: Response, next: NextFunction) => Response> | undefined; 3 | -------------------------------------------------------------------------------- /Library/common/build/middlewares/error-handler.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | exports.errorHandler = void 0; 4 | var custom_errror_1 = require("../errors/custom-errror"); 5 | var errorHandler = function (err, req, res, next) { 6 | if (err instanceof custom_errror_1.CustomError) { 7 | return res.status(err.statusCode).send({ errors: err.serializeErrors() }); 8 | } 9 | console.error(err); 10 | res.status(400).send({ 11 | errors: [{ message: "Something went wrong" }], 12 | }); 13 | }; 14 | exports.errorHandler = errorHandler; 15 | -------------------------------------------------------------------------------- /Library/common/build/middlewares/require-auth.d.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response, NextFunction } from "express"; 2 | export declare const requireAuth: (req: Request, res: Response, next: NextFunction) => void; 3 | -------------------------------------------------------------------------------- /Library/common/build/middlewares/require-auth.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | exports.requireAuth = void 0; 4 | var not_authorized_error_1 = require("../errors/not-authorized-error"); 5 | var requireAuth = function (req, res, next) { 6 | if (!req.currentUser) { 7 | throw new not_authorized_error_1.NotAuthorizedError(); 8 | } 9 | next(); 10 | }; 11 | exports.requireAuth = requireAuth; 12 | -------------------------------------------------------------------------------- /Library/common/build/middlewares/validate-requests.d.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response, NextFunction } from "express"; 2 | export declare const validateRequest: (req: Request, res: Response, next: NextFunction) => void; 3 | -------------------------------------------------------------------------------- /Library/common/build/middlewares/validate-requests.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | exports.validateRequest = void 0; 4 | var express_validator_1 = require("express-validator"); 5 | var request_validation_error_1 = require("../errors/request-validation-error"); 6 | var validateRequest = function (req, res, next) { 7 | var errors = express_validator_1.validationResult(req); 8 | if (!errors.isEmpty()) { 9 | throw new request_validation_error_1.RequestValidationError(errors.array()); 10 | } 11 | next(); 12 | }; 13 | exports.validateRequest = validateRequest; 14 | -------------------------------------------------------------------------------- /Library/common/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@wowowow/common", 3 | "version": "1.0.11", 4 | "description": "", 5 | "main": "./build/index.js", 6 | "types": "./build/index.d.ts", 7 | "files": [ 8 | "build/**/*" 9 | ], 10 | "scripts": { 11 | "clean": "del ./build/*", 12 | "build": "yarn clean && tsc" 13 | }, 14 | "keywords": [], 15 | "author": "", 16 | "license": "ISC", 17 | "dependencies": { 18 | "@types/cookie-session": "^2.0.42", 19 | "@types/express": "^4.17.11", 20 | "@types/jsonwebtoken": "^8.5.0", 21 | "cookie-session": "^1.4.0", 22 | "del-cli": "^3.0.1", 23 | "express": "^4.17.1", 24 | "express-validator": "^6.10.0", 25 | "jsonwebtoken": "^8.5.1", 26 | "node-nats-streaming": "^0.3.2", 27 | "typescript": "^4.2.3" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Library/common/src/errors/bad-request-error.ts: -------------------------------------------------------------------------------- 1 | import { CustomError } from "./custom-errror"; 2 | 3 | export class BadRequestError extends CustomError { 4 | statusCode = 400; 5 | constructor(public message: string) { 6 | super(message); 7 | Object.setPrototypeOf(this, BadRequestError.prototype); 8 | } 9 | 10 | serializeErrors() { 11 | return [{ message: this.message }]; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Library/common/src/errors/custom-errror.ts: -------------------------------------------------------------------------------- 1 | export abstract class CustomError extends Error { 2 | abstract statusCode: number; 3 | constructor(message:string) { 4 | super(message); 5 | 6 | Object.setPrototypeOf(this, CustomError.prototype); 7 | } 8 | abstract serializeErrors(): { message: string; field?: string }[]; 9 | } 10 | -------------------------------------------------------------------------------- /Library/common/src/errors/database-connection-error.ts: -------------------------------------------------------------------------------- 1 | import { CustomError } from "./custom-errror"; 2 | 3 | export class DatabaseConnectionError extends CustomError { 4 | statusCode = 500; 5 | reason = "Error connection to database"; 6 | constructor() { 7 | super('Error connecting to DB'); 8 | 9 | // Only because we are extending a built in class 10 | Object.setPrototypeOf(this, DatabaseConnectionError.prototype); 11 | } 12 | serializeErrors() { 13 | return [ 14 | { 15 | message: this.reason, 16 | }, 17 | ]; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Library/common/src/errors/not-authorized-error.ts: -------------------------------------------------------------------------------- 1 | import { CustomError } from "./custom-errror"; 2 | 3 | export class NotAuthorizedError extends CustomError { 4 | statusCode = 401; 5 | constructor() { 6 | super("Not Authorized !!"); 7 | 8 | Object.setPrototypeOf(this, NotAuthorizedError.prototype); 9 | } 10 | 11 | serializeErrors() { 12 | return [{ message: "Not Authorized !!" }]; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /Library/common/src/errors/not-found-error.ts: -------------------------------------------------------------------------------- 1 | import { CustomError } from "./custom-errror"; 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 | -------------------------------------------------------------------------------- /Library/common/src/errors/request-validation-error.ts: -------------------------------------------------------------------------------- 1 | import { ValidationError } from "express-validator"; 2 | import { CustomError } from "./custom-errror"; 3 | 4 | export class RequestValidationError extends CustomError { 5 | statusCode = 400; 6 | constructor(public errors: ValidationError[]) { 7 | super('Invalid request parameters'); 8 | 9 | // Only because we are extending a built in class 10 | Object.setPrototypeOf(this, RequestValidationError.prototype); 11 | } 12 | serializeErrors() { 13 | return this.errors.map((err) => { 14 | return { message: err.msg, field: err.param }; 15 | }); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Library/common/src/events/base-listener.ts: -------------------------------------------------------------------------------- 1 | import { Message, Stan } from "node-nats-streaming"; 2 | import { Subjects } from "./subjects"; 3 | 4 | interface Event { 5 | subject: Subjects; 6 | data: any; 7 | } 8 | 9 | export abstract class Listener { 10 | abstract subject: T["subject"]; 11 | abstract queueGroupName: string; 12 | abstract onMessage(data: T["data"], msg: Message): void; 13 | protected client: Stan; 14 | protected ackwait = 5 * 1000; 15 | constructor(client: Stan) { 16 | this.client = client; 17 | } 18 | 19 | subscriptionOptions() { 20 | return this.client 21 | .subscriptionOptions() 22 | .setDeliverAllAvailable() 23 | .setManualAckMode(true) 24 | .setAckWait(this.ackwait) 25 | .setDurableName(this.queueGroupName); 26 | } 27 | 28 | listen() { 29 | const subscription = this.client.subscribe( 30 | this.subject, 31 | this.queueGroupName, 32 | this.subscriptionOptions() 33 | ); 34 | subscription.on("message", (msg: Message) => { 35 | console.log(`Message received: ${this.subject} / ${this.queueGroupName}`); 36 | 37 | const parsedData = this.parseMessage(msg); 38 | this.onMessage(parsedData, msg); 39 | }); 40 | } 41 | parseMessage(msg: Message) { 42 | const data = msg.getData(); 43 | return typeof data === "string" 44 | ? JSON.parse(data) 45 | : JSON.parse(data.toString("utf8")); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /Library/common/src/events/base-publisher.ts: -------------------------------------------------------------------------------- 1 | import { Stan } from "node-nats-streaming"; 2 | import { Subjects } from "./subjects"; 3 | 4 | interface Event { 5 | subject: Subjects; 6 | data: any; 7 | } 8 | 9 | export abstract class Publisher { 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) => { 20 | if (err) { 21 | return reject(err); 22 | } 23 | 24 | console.log("Event published to subject", this.subject); 25 | 26 | resolve(); 27 | }); 28 | }); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Library/common/src/events/expiration-complete-event.ts: -------------------------------------------------------------------------------- 1 | import { Subjects } from "./subjects"; 2 | 3 | export interface ExpirationCompleteEvent { 4 | subject: Subjects.ExpirationComplete; 5 | data: { 6 | orderId: string; 7 | }; 8 | } 9 | -------------------------------------------------------------------------------- /Library/common/src/events/order-cancelled-event.ts: -------------------------------------------------------------------------------- 1 | import { Subjects } from "./subjects"; 2 | import { OrderStatus } from "./types/order-status"; 3 | 4 | export interface OrderCancelledEvent { 5 | subject: Subjects.OrderCancelled; 6 | data: { 7 | id: string; 8 | version:number; 9 | ticket: { 10 | id: string; 11 | }; 12 | }; 13 | } 14 | -------------------------------------------------------------------------------- /Library/common/src/events/order-created-event.ts: -------------------------------------------------------------------------------- 1 | import { Subjects } from "./subjects"; 2 | import { OrderStatus } from "./types/order-status"; 3 | 4 | export interface OrderCreatedEvent { 5 | subject: Subjects.OrderCreated; 6 | data: { 7 | id: string; 8 | version: number; 9 | status: OrderStatus; 10 | userId: string; 11 | expiredAt: string; 12 | ticket: { 13 | id: string; 14 | price: number; 15 | }; 16 | }; 17 | } 18 | -------------------------------------------------------------------------------- /Library/common/src/events/payment-created-event.ts: -------------------------------------------------------------------------------- 1 | import { Subjects } from "./subjects"; 2 | 3 | export interface PaymentCreatedEvent { 4 | subject: Subjects.PaymentCreated; 5 | data: { 6 | id: string; 7 | orderId: string; 8 | stripeId: string; 9 | }; 10 | } 11 | -------------------------------------------------------------------------------- /Library/common/src/events/subjects.ts: -------------------------------------------------------------------------------- 1 | export enum Subjects { 2 | TicketCreated = "ticket:created", 3 | TicketUpdated = "ticket:updated", 4 | 5 | OrderCreated = "order:created", 6 | OrderCancelled = "order:cancelled", 7 | 8 | ExpirationComplete = "expiration:complete", 9 | PaymentCreated = "payment:created", 10 | } 11 | -------------------------------------------------------------------------------- /Library/common/src/events/ticket-created-event.ts: -------------------------------------------------------------------------------- 1 | import { Subjects } from "./subjects"; 2 | 3 | export interface TicketCreatedEvent { 4 | subject: Subjects.TicketCreated; 5 | data: { 6 | id: string; 7 | version:number; 8 | title: string; 9 | price: number; 10 | userId: string; 11 | }; 12 | } 13 | -------------------------------------------------------------------------------- /Library/common/src/events/ticket-updated-events.ts: -------------------------------------------------------------------------------- 1 | import { Subjects } from "./subjects"; 2 | 3 | export interface TicketUpdatedEvent { 4 | subject: Subjects.TicketUpdated; 5 | data: { 6 | id: string; 7 | version: number; 8 | title: string; 9 | price: number; 10 | userId: string; 11 | orderId?: string; 12 | }; 13 | } 14 | -------------------------------------------------------------------------------- /Library/common/src/events/types/order-status.ts: -------------------------------------------------------------------------------- 1 | export enum OrderStatus { 2 | // when the order has been created, but the ticket 3 | // it is trying to order has not been reserved 4 | Created = "created", 5 | 6 | // The ticket the order is trying to reserve has already 7 | // been reserved, or when the user has cancelled the order. 8 | // The order expires before payment 9 | Cancelled = "cancelled", 10 | 11 | // The order has successfully reserved the ticket 12 | AwaitingPayment = "awaiting:payment", 13 | 14 | // The order has reserved the ticket and the usern has provided 15 | // payment successfully 16 | Complete = "complete", 17 | } 18 | -------------------------------------------------------------------------------- /Library/common/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./errors/bad-request-error"; 2 | export * from "./errors/custom-errror"; 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 | 8 | export * from "./middlewares/current-user"; 9 | export * from "./middlewares/error-handler"; 10 | export * from "./middlewares/require-auth"; 11 | export * from "./middlewares/validate-requests"; 12 | 13 | export * from "./events/base-listener"; 14 | export * from "./events/base-publisher"; 15 | export * from "./events/subjects"; 16 | export * from "./events/ticket-created-event"; 17 | export * from "./events/ticket-updated-events"; 18 | 19 | export * from "./events/types/order-status"; 20 | export * from "./events/order-created-event"; 21 | export * from "./events/order-cancelled-event"; 22 | export * from "./events/expiration-complete-event"; 23 | export * from './events/payment-created-event' -------------------------------------------------------------------------------- /Library/common/src/middlewares/current-user.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response, NextFunction } from "express"; 2 | import jwt from "jsonwebtoken"; 3 | 4 | interface UserPayload { 5 | id: string; 6 | email: string; 7 | } 8 | 9 | // ! manipulating a existing type definition 10 | declare global { 11 | namespace Express { 12 | interface Request { 13 | currentUser?: UserPayload; 14 | } 15 | } 16 | } 17 | 18 | export const currentUser = ( 19 | req: Request, 20 | res: Response, 21 | next: NextFunction 22 | ) => { 23 | if (!req.session?.jwt) { 24 | return next(); 25 | } 26 | try { 27 | const payload = jwt.verify( 28 | req.session.jwt, 29 | process.env.JWTSECRET! 30 | ) as UserPayload; 31 | req.currentUser = payload; 32 | } catch (err) {} 33 | next(); 34 | }; 35 | -------------------------------------------------------------------------------- /Library/common/src/middlewares/error-handler.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response, NextFunction } from "express"; 2 | import { CustomError } from "../errors/custom-errror"; 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 | console.error(err); 15 | 16 | res.status(400).send({ 17 | errors: [{ message: "Something went wrong" }], 18 | }); 19 | }; 20 | -------------------------------------------------------------------------------- /Library/common/src/middlewares/require-auth.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response, NextFunction } from "express"; 2 | import jwt from "jsonwebtoken"; 3 | import { NotAuthorizedError } from "../errors/not-authorized-error"; 4 | 5 | export const requireAuth = ( 6 | req: Request, 7 | res: Response, 8 | next: NextFunction 9 | ) => { 10 | if (!req.currentUser) { 11 | throw new NotAuthorizedError(); 12 | } 13 | next(); 14 | }; 15 | -------------------------------------------------------------------------------- /Library/common/src/middlewares/validate-requests.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response, NextFunction } from "express"; 2 | import { validationResult } from "express-validator"; 3 | import { RequestValidationError } from "../errors/request-validation-error"; 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 | -------------------------------------------------------------------------------- /Orders-Service/.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /Orders-Service/.stignore: -------------------------------------------------------------------------------- 1 | .git 2 | # Runtime data 3 | pids 4 | *.pid 5 | *.seed 6 | *.pid.lock 7 | 8 | # Directory for instrumented libs generated by jscoverage/JSCover 9 | lib-cov 10 | 11 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 12 | .grunt 13 | 14 | # Bower dependency directory (https://bower.io/) 15 | bower_components 16 | 17 | # node-waf configuration 18 | .lock-wscript 19 | 20 | # Compiled binary addons (https://nodejs.org/api/addons.html) 21 | build/Release 22 | 23 | # Dependency directories 24 | node_modules 25 | jspm_packages 26 | 27 | # Optional npm cache directory 28 | .npm 29 | 30 | # Optional eslint cache 31 | .eslintcache 32 | 33 | # Optional REPL history 34 | .node_repl_history 35 | 36 | # Output of 'npm pack' 37 | *.tgz 38 | 39 | # Yarn Integrity file 40 | .yarn-integrity 41 | 42 | # parcel-bundler cache (https://parceljs.org/) 43 | .cache 44 | 45 | # next.js build output 46 | .next 47 | 48 | # nuxt.js build output 49 | .nuxt 50 | 51 | # vuepress build output 52 | .vuepress/dist 53 | 54 | # Serverless directories 55 | .serverless 56 | 57 | -------------------------------------------------------------------------------- /Orders-Service/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM quay.io/narendev/fishnode:alpine 2 | 3 | WORKDIR /app 4 | ENV PORT=4002 5 | 6 | 7 | COPY package.json . 8 | RUN yarn install --production 9 | COPY . . 10 | 11 | EXPOSE ${PORT} 12 | 13 | CMD [ "yarn","start" ] -------------------------------------------------------------------------------- /Orders-Service/README.md: -------------------------------------------------------------------------------- 1 | # Orders Service 2 | -------------------------------------------------------------------------------- /Orders-Service/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: "ts-jest", 3 | testEnvironment: "node", 4 | setupFilesAfterEnv: ["./src/test/setup.ts"], 5 | }; 6 | 7 | // "jest": { 8 | // "preset": "ts-jest", 9 | // "testEnvironment": "node", 10 | // "setupFilesAfterEnv": [ 11 | // "./src/test/setup.ts" 12 | // ] 13 | // }, 14 | -------------------------------------------------------------------------------- /Orders-Service/okteto.yml: -------------------------------------------------------------------------------- 1 | name: mcs-orders-service 2 | autocreate: true 3 | image: quay.io/narendev/fishnode:1.0 4 | command: fish 5 | sync: 6 | - .:/usr/src/app 7 | forward: 8 | - 9229:9229 9 | - 3000:3000 10 | persistentVolume: {} 11 | annotations: 12 | sidecar.istio.io/inject: "false" 13 | -------------------------------------------------------------------------------- /Orders-Service/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "orders-service", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "ts-node-dev src/index.ts", 8 | "test": "jest --watchAll --no-cache" 9 | }, 10 | "keywords": [], 11 | "author": "", 12 | "license": "ISC", 13 | "dependencies": { 14 | "@types/cookie-session": "^2.0.42", 15 | "@types/cors": "^2.8.10", 16 | "@types/express": "^4.17.11", 17 | "@types/jsonwebtoken": "^8.5.0", 18 | "@types/mongoose": "^5.10.3", 19 | "@types/swagger-ui-express": "^4.1.2", 20 | "@types/yamljs": "^0.2.31", 21 | "@wowowow/common": "^1.0.11", 22 | "cookie-session": "^1.4.0", 23 | "cors": "^2.8.5", 24 | "express": "^4.17.1", 25 | "express-async-errors": "^3.1.1", 26 | "express-validator": "^6.10.0", 27 | "jsonwebtoken": "^8.5.1", 28 | "mongoose": "5.10.19", 29 | "mongoose-update-if-current": "^1.4.0", 30 | "node-nats-streaming": "^0.3.2", 31 | "swagger-ui-express": "^4.1.6", 32 | "ts-node-dev": "^1.1.6", 33 | "typescript": "^4.2.2", 34 | "yamljs": "^0.3.0" 35 | }, 36 | "devDependencies": { 37 | "@types/jest": "^26.0.20", 38 | "@types/supertest": "^2.0.10", 39 | "jest": "^26.6.3", 40 | "mongodb-memory-server": "^6.9.3", 41 | "supertest": "^6.1.3", 42 | "ts-jest": "^26.5.3" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Orders-Service/src/__mocks__/nats-wrapper.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 | -------------------------------------------------------------------------------- /Orders-Service/src/app.ts: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | import "express-async-errors"; 3 | import { json } from "body-parser"; 4 | import cookieSession from "cookie-session"; 5 | import cors from "cors"; 6 | import { errorHandler, NotFoundError, currentUser } from "@wowowow/common"; 7 | import { deleteOrderRouter } from "./routes/delete"; 8 | import { newOrderRouter } from "./routes/new"; 9 | import { indexOrderRouter } from "./routes"; 10 | import { showOrderRouter } from "./routes/show"; 11 | import YAML from "yamljs"; 12 | import swaggerUI from "swagger-ui-express"; 13 | 14 | const app = express(); 15 | 16 | const swaggeryml = YAML.load("src/orders-svc.yml"); 17 | 18 | const options = { 19 | explorer: true, 20 | }; 21 | 22 | const hostValues = process.env.ALLOWED_HOSTS; 23 | 24 | const hostArray = hostValues!.split(",") 25 | 26 | 27 | const corsOptions = { 28 | origin: hostArray, 29 | credentials: true, 30 | exposedHeaders: ["set-cookie"] 31 | } 32 | 33 | app.use(cors(corsOptions)); 34 | app.set("trust proxy", true); 35 | app.use(json()); 36 | app.use( 37 | cookieSession({ 38 | signed: false, 39 | // secure:true //! when this is set to true, it'll only work on connection coming with https:// 40 | }) 41 | ); 42 | 43 | app.use(currentUser); 44 | 45 | app.use( 46 | "/api/orders/docs", 47 | swaggerUI.serve, 48 | swaggerUI.setup(swaggeryml, options) 49 | ); 50 | app.use(deleteOrderRouter); 51 | app.use(newOrderRouter); 52 | app.use(indexOrderRouter); 53 | app.use(showOrderRouter); 54 | 55 | app.all("*", async () => { 56 | throw new NotFoundError(); 57 | }); 58 | 59 | app.use(errorHandler); 60 | 61 | export { app }; 62 | -------------------------------------------------------------------------------- /Orders-Service/src/events/listeners/__test__/ticket-created-listener.test.ts: -------------------------------------------------------------------------------- 1 | import mongoose from "mongoose"; 2 | 3 | import { TicketCreatedEvent } from "@wowowow/common"; 4 | import { natsWrapper } from "../../../nats-wrapper"; 5 | import { TicketCreatedListener } from "../ticket-created-listener"; 6 | import { Message } from "node-nats-streaming"; 7 | import { Ticket } from "../../../models/ticket"; 8 | 9 | const setup = async () => { 10 | // create an instance of the listener 11 | const listener = new TicketCreatedListener(natsWrapper.client); 12 | 13 | // create a fake data object 14 | const data: TicketCreatedEvent["data"] = { 15 | version: 0, 16 | id: new mongoose.Types.ObjectId().toHexString(), 17 | title: "concert 333", 18 | price: 32, 19 | userId: new mongoose.Types.ObjectId().toHexString(), 20 | }; 21 | 22 | // create a fake message object 23 | // @ts-ignore 24 | const msg: Message = { 25 | ack: jest.fn(), 26 | }; 27 | 28 | return { listener, data, msg }; 29 | }; 30 | 31 | it("creates and saves a ticket", async () => { 32 | const { listener, data, msg } = await setup(); 33 | 34 | // call the onMessage function with the data object + message object 35 | await listener.onMessage(data, msg); 36 | 37 | // write assertions to make sure a ticket was created! 38 | const ticket = await Ticket.findById(data.id); 39 | 40 | expect(ticket).toBeDefined(); 41 | expect(ticket!.title).toEqual(data.title); 42 | expect(ticket!.price).toEqual(data.price); 43 | }); 44 | 45 | it("acks the message", async () => { 46 | const { listener, data, msg } = await setup(); 47 | 48 | // call the onMessage function with the data object + message object 49 | await listener.onMessage(data, msg); 50 | 51 | // write assertions to make sure ack function is called 52 | expect(msg.ack).toHaveBeenCalled(); 53 | }); 54 | -------------------------------------------------------------------------------- /Orders-Service/src/events/listeners/expiration-complete-listener.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ExpirationCompleteEvent, 3 | Listener, 4 | OrderStatus, 5 | Subjects, 6 | } from "@wowowow/common"; 7 | import { Message } from "node-nats-streaming"; 8 | import { Order } from "../../models/order"; 9 | import { OrderCancelledPublisher } from "../publishers/order-cancelled-publisher"; 10 | import { queueGroupName } from "./queue-group-name"; 11 | 12 | export class ExpirationCompleteListener extends Listener { 13 | subject: Subjects.ExpirationComplete = Subjects.ExpirationComplete; 14 | queueGroupName = queueGroupName; 15 | 16 | async onMessage(data: ExpirationCompleteEvent["data"], msg: Message) { 17 | const order = await Order.findById(data.orderId).populate("ticket"); 18 | 19 | if (!order) { 20 | throw new Error("order not found"); 21 | } 22 | 23 | // ! if the orders payment is complete, we shouldn't cancel that order 24 | if (order.status === OrderStatus.Complete) { 25 | return msg.ack(); 26 | } 27 | 28 | order.set({ 29 | status: OrderStatus.Cancelled, 30 | }); 31 | 32 | await order.save(); 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 | -------------------------------------------------------------------------------- /Orders-Service/src/events/listeners/payment-created-listener.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Listener, 3 | OrderStatus, 4 | PaymentCreatedEvent, 5 | Subjects, 6 | } from "@wowowow/common"; 7 | import { Message } from "node-nats-streaming"; 8 | import { Order } from "../../models/order"; 9 | import { queueGroupName } from "./queue-group-name"; 10 | 11 | export class PaymentCreatedListener extends Listener { 12 | subject: Subjects.PaymentCreated = Subjects.PaymentCreated; 13 | queueGroupName = queueGroupName; 14 | 15 | async onMessage(data: PaymentCreatedEvent["data"], msg: Message) { 16 | const order = await Order.findById(data.orderId); 17 | 18 | if (!order) { 19 | throw new Error("Order not found"); 20 | } 21 | order.set({ 22 | status: OrderStatus.Complete, 23 | }); 24 | await order.save(); 25 | 26 | msg.ack(); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Orders-Service/src/events/listeners/queue-group-name.ts: -------------------------------------------------------------------------------- 1 | export const queueGroupName = "orders-service"; 2 | -------------------------------------------------------------------------------- /Orders-Service/src/events/listeners/ticket-created-listener.ts: -------------------------------------------------------------------------------- 1 | import { Listener, Subjects, TicketCreatedEvent } from "@wowowow/common"; 2 | import { Message } from "node-nats-streaming"; 3 | import { Ticket } from "../../models/ticket"; 4 | import { queueGroupName } from "./queue-group-name"; 5 | 6 | export class TicketCreatedListener extends Listener { 7 | subject: Subjects.TicketCreated = Subjects.TicketCreated; 8 | queueGroupName = queueGroupName; 9 | 10 | async onMessage(data: TicketCreatedEvent["data"], msg: Message) { 11 | const { id, title, price } = data; 12 | const ticket = Ticket.build({ 13 | id, 14 | title, 15 | price, 16 | }); 17 | await ticket.save(); 18 | 19 | msg.ack(); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Orders-Service/src/events/listeners/ticket-updated-listener.ts: -------------------------------------------------------------------------------- 1 | import { Listener, Subjects, TicketUpdatedEvent } from "@wowowow/common"; 2 | import { Message } from "node-nats-streaming"; 3 | import { Ticket } from "../../models/ticket"; 4 | import { queueGroupName } from "./queue-group-name"; 5 | 6 | export class TicketUpdatedListener extends Listener { 7 | subject: Subjects.TicketUpdated = Subjects.TicketUpdated; 8 | queueGroupName = queueGroupName; 9 | 10 | async onMessage(data: TicketUpdatedEvent["data"], msg: Message) { 11 | const ticket = await Ticket.findByEvent(data); 12 | 13 | if (!ticket) { 14 | throw new Error("Ticket not found"); 15 | } 16 | 17 | const { title, price } = data; 18 | ticket.set({ title, price }); 19 | await ticket.save(); 20 | 21 | msg.ack(); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Orders-Service/src/events/publishers/order-cancelled-publisher.ts: -------------------------------------------------------------------------------- 1 | import { Publisher, Subjects, OrderCancelledEvent } from "@wowowow/common"; 2 | 3 | export class OrderCancelledPublisher extends Publisher { 4 | subject: Subjects.OrderCancelled = Subjects.OrderCancelled; 5 | } 6 | -------------------------------------------------------------------------------- /Orders-Service/src/events/publishers/order-created-publisher.ts: -------------------------------------------------------------------------------- 1 | import { OrderCreatedEvent, Publisher, Subjects } from "@wowowow/common"; 2 | 3 | export class OrderCreatedPublisher extends Publisher { 4 | subject: Subjects.OrderCreated = Subjects.OrderCreated; 5 | } 6 | -------------------------------------------------------------------------------- /Orders-Service/src/models/order.ts: -------------------------------------------------------------------------------- 1 | import mongoose from "mongoose"; 2 | import { OrderStatus } from "@wowowow/common"; 3 | import { TicketDoc } from "./ticket"; 4 | import { updateIfCurrentPlugin } from "mongoose-update-if-current"; 5 | 6 | export { OrderStatus }; 7 | 8 | interface OrdersAttrs { 9 | userId: string; 10 | status: OrderStatus; 11 | expiresAt: Date; 12 | ticket: TicketDoc; //! this has to be dealt with 13 | } 14 | 15 | interface OrderDoc extends mongoose.Document { 16 | userId: string; 17 | status: OrderStatus; 18 | version: number; 19 | expiresAt: Date; 20 | ticket: TicketDoc; //! this has to be dealt with 21 | } 22 | 23 | interface OrderModel extends mongoose.Model { 24 | build(attrs: OrdersAttrs): OrderDoc; 25 | } 26 | 27 | const orderSchema = new mongoose.Schema( 28 | { 29 | userId: { 30 | type: String, 31 | required: true, 32 | }, 33 | status: { 34 | type: String, 35 | required: true, 36 | enum: Object.values(OrderStatus), 37 | default: OrderStatus.Created, 38 | }, 39 | expiresAt: { 40 | type: mongoose.Schema.Types.Date, 41 | }, 42 | ticket: { 43 | type: mongoose.Schema.Types.ObjectId, 44 | ref: "Ticket", 45 | }, 46 | }, 47 | { 48 | toJSON: { 49 | transform(doc, ret) { 50 | ret.id = ret._id; 51 | delete ret._id; 52 | }, 53 | }, 54 | } 55 | ); 56 | 57 | orderSchema.set("versionKey", "version"); 58 | orderSchema.plugin(updateIfCurrentPlugin); 59 | 60 | orderSchema.statics.build = (attrs: OrdersAttrs) => { 61 | return new Order(attrs); 62 | }; 63 | 64 | const Order = mongoose.model("Order", orderSchema); 65 | 66 | export { Order }; 67 | -------------------------------------------------------------------------------- /Orders-Service/src/nats-wrapper.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("Cannot access NATS client before connecting"); 9 | } 10 | return this._client; 11 | } 12 | 13 | connect(clusterId: string, clientId: string, url: string): Promise { 14 | this._client = nats.connect(clusterId, clientId, { url }); 15 | 16 | return new Promise((resolve, reject) => { 17 | this.client.on("connect", () => { 18 | console.log("Connected to NATS"); 19 | resolve(); 20 | }); 21 | 22 | this.client.on("error", (err) => { 23 | reject(err); 24 | }); 25 | }); 26 | } 27 | } 28 | 29 | export const natsWrapper = new NatsWrapper(); 30 | -------------------------------------------------------------------------------- /Orders-Service/src/routes/__test__/index.test.ts: -------------------------------------------------------------------------------- 1 | import mongoose from "mongoose"; 2 | import request from "supertest"; 3 | import { app } from "../../app"; 4 | import { Ticket } from "../../models/ticket"; 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 | // create 3 tickets 20 | const ticketOne = await buildTicket(); 21 | const ticketTwo = await buildTicket(); 22 | const ticketThree = await buildTicket(); 23 | 24 | const userOne = global.signin(); 25 | const userTwo = global.signin(); 26 | 27 | // create 1 order as User #1 28 | await request(app) 29 | .post("/api/orders") 30 | .set("Cookie", userOne) 31 | .send({ ticketId: ticketOne.id }) 32 | .expect(201); 33 | 34 | // create 2 orders as User #2 35 | const { body: orderOne } = await request(app) 36 | .post("/api/orders") 37 | .set("Cookie", userTwo) 38 | .send({ ticketId: ticketTwo.id }) 39 | .expect(201); 40 | 41 | const { body: orderTwo } = await request(app) 42 | .post("/api/orders") 43 | .set("Cookie", userTwo) 44 | .send({ ticketId: ticketThree.id }) 45 | .expect(201); 46 | 47 | // Make request to get orders for User #2 48 | const response = await request(app) 49 | .get("/api/orders") 50 | .set("Cookie", userTwo) 51 | .expect(200); 52 | 53 | // Make sure we only got the orders for User #2 54 | expect(response.body.length).toEqual(2); 55 | expect(response.body[0].id).toEqual(orderOne.id); 56 | expect(response.body[1].id).toEqual(orderTwo.id); 57 | expect(response.body[0].ticket.id).toEqual(ticketTwo.id); 58 | expect(response.body[1].ticket.id).toEqual(ticketThree.id); 59 | }); 60 | -------------------------------------------------------------------------------- /Orders-Service/src/routes/__test__/show.test.ts: -------------------------------------------------------------------------------- 1 | import mongoose from "mongoose"; 2 | import request from "supertest"; 3 | import { app } from "../../app"; 4 | import { Ticket } from "../../models/ticket"; 5 | 6 | it("fetches the order", async () => { 7 | // Create a ticket 8 | const ticket = Ticket.build({ 9 | id: mongoose.Types.ObjectId().toHexString(), 10 | title: "conccert 22", 11 | price: 42, 12 | }); 13 | 14 | await ticket.save(); 15 | 16 | const user = global.signin(); 17 | 18 | // make a request to build an order with this ticket 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 | // make request to fetch the order 26 | const { body: fetchedOrder } = await request(app) 27 | .get(`/api/orders/${order.id}`) 28 | .set("Cookie", user) 29 | .send() 30 | .expect(200); 31 | 32 | expect(fetchedOrder.id).toEqual(order.id); 33 | }); 34 | 35 | it("returns an error if one user tries to fetch another users order", async () => { 36 | // Create a ticket 37 | const ticket = Ticket.build({ 38 | id: mongoose.Types.ObjectId().toHexString(), 39 | title: "conccert 22", 40 | price: 42, 41 | }); 42 | 43 | await ticket.save(); 44 | 45 | const user = global.signin(); 46 | 47 | // make a request to build an order with this ticket 48 | const { body: order } = await request(app) 49 | .post("/api/orders") 50 | .set("Cookie", user) 51 | .send({ ticketId: ticket.id }) 52 | .expect(201); 53 | 54 | // make request to fetch the order 55 | await request(app) 56 | .get(`/api/orders/${order.id}`) 57 | .set("Cookie", global.signin()) 58 | .send() 59 | .expect(401); 60 | }); 61 | -------------------------------------------------------------------------------- /Orders-Service/src/routes/delete.ts: -------------------------------------------------------------------------------- 1 | import { 2 | NotAuthorizedError, 3 | NotFoundError, 4 | OrderStatus, 5 | requireAuth, 6 | } from "@wowowow/common"; 7 | import express, { Request, Response } from "express"; 8 | import { OrderCancelledPublisher } from "../events/publishers/order-cancelled-publisher"; 9 | import { Order } from "../models/order"; 10 | import { natsWrapper } from "../nats-wrapper"; 11 | 12 | const router = express.Router(); 13 | 14 | // ! it maybe considered as PATCH method too 15 | router.delete( 16 | "/api/orders/:orderId", 17 | requireAuth, 18 | async (req: Request, res: Response) => { 19 | const { orderId } = req.params; 20 | 21 | const order = await Order.findById(orderId).populate("ticket"); 22 | 23 | if (!order) { 24 | throw new NotFoundError(); 25 | } 26 | 27 | if (order.userId !== req.currentUser!.id) { 28 | throw new NotAuthorizedError(); 29 | } 30 | 31 | order.status = OrderStatus.Cancelled; 32 | await order.save(); 33 | 34 | // an event must be published about this order being cancelled! 35 | new OrderCancelledPublisher(natsWrapper.client).publish({ 36 | id: order.id, 37 | version: order.version, 38 | ticket: { 39 | id: order.ticket.id, 40 | }, 41 | }); 42 | res.status(204).send(order); 43 | } 44 | ); 45 | 46 | export { router as deleteOrderRouter }; 47 | -------------------------------------------------------------------------------- /Orders-Service/src/routes/index.ts: -------------------------------------------------------------------------------- 1 | import { requireAuth } from "@wowowow/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 | res.send(orders); 13 | }); 14 | 15 | export { router as indexOrderRouter }; 16 | -------------------------------------------------------------------------------- /Orders-Service/src/routes/show.ts: -------------------------------------------------------------------------------- 1 | import { 2 | NotAuthorizedError, 3 | NotFoundError, 4 | requireAuth, 5 | } from "@wowowow/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 | res.send(order); 26 | } 27 | ); 28 | 29 | export { router as showOrderRouter }; 30 | -------------------------------------------------------------------------------- /Orders-Service/src/test/setup.ts: -------------------------------------------------------------------------------- 1 | import { MongoMemoryServer } from "mongodb-memory-server"; 2 | import mongoose from "mongoose"; 3 | import jwt from "jsonwebtoken"; 4 | 5 | declare global { 6 | namespace NodeJS { 7 | interface Global { 8 | signin(): string[]; 9 | } 10 | } 11 | } 12 | 13 | jest.mock("../nats-wrapper.ts"); 14 | 15 | let mongo: any; 16 | beforeAll(async () => { 17 | //! setting environment variable for jwtsecret for test environment 18 | process.env.JWTSECRET = "qwer123qwer"; 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 | const collections = await mongoose.connection.db.collections(); 31 | 32 | for (let collection of collections) { 33 | await collection.deleteMany({}); 34 | } 35 | }); 36 | 37 | afterAll(async () => { 38 | await mongo.stop(); 39 | await mongoose.connection.close(); 40 | }); 41 | 42 | global.signin = () => { 43 | // Build a JWT payload for tests. {id,email} 44 | const payload = { 45 | id: new mongoose.Types.ObjectId().toHexString(), 46 | email: "test@test.com", 47 | }; 48 | 49 | // creating the JWT 50 | const token = jwt.sign(payload, process.env.JWTSECRET!); 51 | 52 | // building session object {jwt: JWT_Object} 53 | const session = { jwt: token }; 54 | 55 | // Turning that session into JSON 56 | const sessionJSON = JSON.stringify(session); 57 | 58 | // Taking the JSON and encoding it as base64 59 | const base64 = Buffer.from(sessionJSON).toString("base64"); 60 | 61 | // return a string thats the cookie with the encoded data 62 | return [`express:sess=${base64}`]; 63 | }; 64 | -------------------------------------------------------------------------------- /Payment-Service/.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /Payment-Service/.stignore: -------------------------------------------------------------------------------- 1 | .git 2 | # Runtime data 3 | pids 4 | *.pid 5 | *.seed 6 | *.pid.lock 7 | 8 | # Directory for instrumented libs generated by jscoverage/JSCover 9 | lib-cov 10 | 11 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 12 | .grunt 13 | 14 | # Bower dependency directory (https://bower.io/) 15 | bower_components 16 | 17 | # node-waf configuration 18 | .lock-wscript 19 | 20 | # Compiled binary addons (https://nodejs.org/api/addons.html) 21 | build/Release 22 | 23 | # Dependency directories 24 | node_modules 25 | jspm_packages 26 | 27 | # Optional npm cache directory 28 | .npm 29 | 30 | # Optional eslint cache 31 | .eslintcache 32 | 33 | # Optional REPL history 34 | .node_repl_history 35 | 36 | # Output of 'npm pack' 37 | *.tgz 38 | 39 | # Yarn Integrity file 40 | .yarn-integrity 41 | 42 | # parcel-bundler cache (https://parceljs.org/) 43 | .cache 44 | 45 | # next.js build output 46 | .next 47 | 48 | # nuxt.js build output 49 | .nuxt 50 | 51 | # vuepress build output 52 | .vuepress/dist 53 | 54 | # Serverless directories 55 | .serverless 56 | 57 | -------------------------------------------------------------------------------- /Payment-Service/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM quay.io/narendev/fishnode:alpine 2 | 3 | WORKDIR /app 4 | ENV PORT=4004 5 | 6 | COPY package.json . 7 | RUN yarn install --production 8 | COPY . . 9 | 10 | EXPOSE ${PORT} 11 | 12 | CMD [ "yarn","start" ] -------------------------------------------------------------------------------- /Payment-Service/README.md: -------------------------------------------------------------------------------- 1 | # Payment Service 2 | -------------------------------------------------------------------------------- /Payment-Service/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: "ts-jest", 3 | testEnvironment: "node", 4 | setupFilesAfterEnv: ["./src/test/setup.ts"], 5 | }; 6 | 7 | // "jest": { 8 | // "preset": "ts-jest", 9 | // "testEnvironment": "node", 10 | // "setupFilesAfterEnv": [ 11 | // "./src/test/setup.ts" 12 | // ] 13 | // }, 14 | -------------------------------------------------------------------------------- /Payment-Service/okteto.yml: -------------------------------------------------------------------------------- 1 | name: mcs-payment-service 2 | autocreate: true 3 | image: quay.io/narendev/fishnode:1.0 4 | command: fish 5 | sync: 6 | - .:/usr/src/app 7 | forward: 8 | - 9229:9229 9 | - 3000:3000 10 | persistentVolume: {} 11 | annotations: 12 | sidecar.istio.io/inject: "false" 13 | -------------------------------------------------------------------------------- /Payment-Service/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "payment-service", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "ts-node-dev src/index.ts", 8 | "test": "jest --watchAll --no-cache" 9 | }, 10 | "keywords": [], 11 | "author": "", 12 | "license": "ISC", 13 | "dependencies": { 14 | "@types/cookie-session": "^2.0.42", 15 | "@types/cors": "^2.8.10", 16 | "@types/express": "^4.17.11", 17 | "@types/jsonwebtoken": "^8.5.0", 18 | "@types/mongoose": "^5.10.3", 19 | "@types/swagger-ui-express": "^4.1.2", 20 | "@types/yamljs": "^0.2.31", 21 | "@wowowow/common": "^1.0.11", 22 | "cookie-session": "^1.4.0", 23 | "cors": "^2.8.5", 24 | "express": "^4.17.1", 25 | "express-async-errors": "^3.1.1", 26 | "express-validator": "^6.10.0", 27 | "jsonwebtoken": "^8.5.1", 28 | "mongoose": "5.10.19", 29 | "mongoose-update-if-current": "^1.4.0", 30 | "node-nats-streaming": "^0.3.2", 31 | "stripe": "^8.138.0", 32 | "swagger-ui-express": "^4.1.6", 33 | "ts-node-dev": "^1.1.6", 34 | "typescript": "^4.2.2", 35 | "yamljs": "^0.3.0" 36 | }, 37 | "devDependencies": { 38 | "@types/jest": "^26.0.20", 39 | "@types/supertest": "^2.0.10", 40 | "jest": "^26.6.3", 41 | "mongodb-memory-server": "^6.9.3", 42 | "supertest": "^6.1.3", 43 | "ts-jest": "^26.5.3" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /Payment-Service/src/__mocks__/nats-wrapper.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 | -------------------------------------------------------------------------------- /Payment-Service/src/__mocks__/stripe.ts: -------------------------------------------------------------------------------- 1 | export const stripe = { 2 | charges :{ 3 | create: jest.fn().mockResolvedValue({}) 4 | } 5 | } -------------------------------------------------------------------------------- /Payment-Service/src/app.ts: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | import "express-async-errors"; 3 | import { json } from "body-parser"; 4 | import cookieSession from "cookie-session"; 5 | import cors from "cors"; 6 | import { errorHandler, NotFoundError, currentUser } from "@wowowow/common"; 7 | import { createChargeRouter } from "./routes/new"; 8 | 9 | import swaggerUI from "swagger-ui-express"; 10 | import YAML from "yamljs"; 11 | 12 | const app = express(); 13 | 14 | const swaggeryml = YAML.load("src/payment-svc.yml"); 15 | 16 | const options = { 17 | explorer: true, 18 | }; 19 | 20 | const hostValues = process.env.ALLOWED_HOSTS; 21 | 22 | const hostArray = hostValues!.split(",") 23 | 24 | 25 | const corsOptions = { 26 | origin: hostArray, 27 | credentials: true, 28 | exposedHeaders: ["set-cookie"] 29 | } 30 | 31 | app.use(cors(corsOptions)); 32 | app.set("trust proxy", true); 33 | app.use(json()); 34 | app.use( 35 | cookieSession({ 36 | signed: false, 37 | // secure:true //! when this is set to true, it'll only work on connection coming with https:// 38 | }) 39 | ); 40 | 41 | app.use(currentUser); 42 | app.use( 43 | "/api/payments/docs", 44 | swaggerUI.serve, 45 | swaggerUI.setup(swaggeryml, options) 46 | ); 47 | 48 | app.use(createChargeRouter); 49 | 50 | app.all("*", async () => { 51 | throw new NotFoundError(); 52 | }); 53 | 54 | app.use(errorHandler); 55 | 56 | export { app }; 57 | -------------------------------------------------------------------------------- /Payment-Service/src/events/listeners/__test__/order-cancelled-listener.test.ts: -------------------------------------------------------------------------------- 1 | import mongoose from 'mongoose'; 2 | import { Message } from 'node-nats-streaming'; 3 | import { OrderStatus, OrderCancelledEvent } from '@wowowow/common'; 4 | import { OrderCancelledListener } from '../order-cancelled-listener'; 5 | import { natsWrapper } from '../../../nats-wrapper'; 6 | import { Order } from '../../../models/orders'; 7 | 8 | const setup = async () => { 9 | const listener = new OrderCancelledListener(natsWrapper.client); 10 | 11 | const order = Order.build({ 12 | id: mongoose.Types.ObjectId().toHexString(), 13 | status: OrderStatus.Created, 14 | price: 10, 15 | userId: 'asldkfj', 16 | version: 0, 17 | }); 18 | await order.save(); 19 | 20 | const data: OrderCancelledEvent['data'] = { 21 | id: order.id, 22 | version: 1, 23 | ticket: { 24 | id: 'asldkfj', 25 | }, 26 | }; 27 | 28 | // @ts-ignore 29 | const msg: Message = { 30 | ack: jest.fn(), 31 | }; 32 | 33 | return { listener, data, msg, order }; 34 | }; 35 | 36 | it('updates the status of the order', async () => { 37 | const { listener, data, msg, order } = await setup(); 38 | 39 | await listener.onMessage(data, msg); 40 | 41 | const updatedOrder = await Order.findById(order.id); 42 | 43 | expect(updatedOrder!.status).toEqual(OrderStatus.Cancelled); 44 | }); 45 | 46 | it('acks the message', async () => { 47 | const { listener, data, msg, order } = await setup(); 48 | 49 | await listener.onMessage(data, msg); 50 | 51 | expect(msg.ack).toHaveBeenCalled(); 52 | }); 53 | -------------------------------------------------------------------------------- /Payment-Service/src/events/listeners/__test__/order-created-listener.test.ts: -------------------------------------------------------------------------------- 1 | import mongoose from 'mongoose'; 2 | import { Message } from 'node-nats-streaming'; 3 | import { OrderCreatedEvent, OrderStatus } from '@wowowow/common'; 4 | import { natsWrapper } from '../../../nats-wrapper'; 5 | import { OrderCreatedListener } from '../order-created-listener'; 6 | import { Order } from '../../../models/orders'; 7 | 8 | const setup = async () => { 9 | const listener = new OrderCreatedListener(natsWrapper.client); 10 | 11 | const data: OrderCreatedEvent['data'] = { 12 | id: mongoose.Types.ObjectId().toHexString(), 13 | version: 0, 14 | expiredAt: 'sdfasdf', 15 | userId: 'alsdaafdjf', 16 | status: OrderStatus.Created, 17 | ticket: { 18 | id: 'algdsavj', 19 | price: 101, 20 | }, 21 | }; 22 | 23 | // @ts-ignore 24 | const msg: Message = { 25 | ack: jest.fn(), 26 | }; 27 | 28 | return { listener, data, msg }; 29 | }; 30 | 31 | it('replicates the order info', async () => { 32 | const { listener, data, msg } = await setup(); 33 | 34 | await listener.onMessage(data, msg); 35 | 36 | const order = await Order.findById(data.id); 37 | 38 | expect(order!.price).toEqual(data.ticket.price); 39 | }); 40 | 41 | it('acks the message', async () => { 42 | const { listener, data, msg } = await setup(); 43 | 44 | await listener.onMessage(data, msg); 45 | 46 | expect(msg.ack).toHaveBeenCalled(); 47 | }); 48 | -------------------------------------------------------------------------------- /Payment-Service/src/events/listeners/order-cancelled-listener.ts: -------------------------------------------------------------------------------- 1 | import { 2 | OrderCancelledEvent, 3 | Subjects, 4 | Listener, 5 | OrderStatus, 6 | } from '@wowowow/common'; 7 | import { Message } from 'node-nats-streaming'; 8 | import {queueGroupName} from './queue-group-name' 9 | import { Order } from '../../models/orders'; 10 | 11 | export class OrderCancelledListener extends Listener { 12 | subject: Subjects.OrderCancelled = Subjects.OrderCancelled; 13 | queueGroupName = queueGroupName; 14 | 15 | async onMessage(data: OrderCancelledEvent['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('Order not found'); 23 | } 24 | 25 | order.set({ status: OrderStatus.Cancelled }); 26 | await order.save(); 27 | 28 | msg.ack(); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Payment-Service/src/events/listeners/order-created-listener.ts: -------------------------------------------------------------------------------- 1 | import { Listener, OrderCreatedEvent, Subjects } from "@wowowow/common"; 2 | import { Message } from "node-nats-streaming"; 3 | import { Order } from "../../models/orders"; 4 | import { queueGroupName } from "./queue-group-name"; 5 | 6 | export class OrderCreatedListener extends Listener{ 7 | subject: Subjects.OrderCreated = Subjects.OrderCreated; 8 | queueGroupName = queueGroupName; 9 | 10 | async onMessage(data: OrderCreatedEvent['data'], msg: Message) { 11 | const order = Order.build({ 12 | id: data.id, 13 | price: data.ticket.price, 14 | status: data.status, 15 | userId: data.userId, 16 | version: data.version 17 | }) 18 | 19 | await order.save(); 20 | 21 | msg.ack() 22 | 23 | } 24 | } -------------------------------------------------------------------------------- /Payment-Service/src/events/listeners/queue-group-name.ts: -------------------------------------------------------------------------------- 1 | export const queueGroupName = "payments-service"; 2 | -------------------------------------------------------------------------------- /Payment-Service/src/events/publishers/payment-created-publisher.ts: -------------------------------------------------------------------------------- 1 | import { PaymentCreatedEvent, Publisher, Subjects } from "@wowowow/common"; 2 | 3 | export class PaymentCreatedPublisher extends Publisher { 4 | subject: Subjects.PaymentCreated = Subjects.PaymentCreated; 5 | } 6 | -------------------------------------------------------------------------------- /Payment-Service/src/models/orders.ts: -------------------------------------------------------------------------------- 1 | import mongoose from "mongoose"; 2 | import { updateIfCurrentPlugin } from "mongoose-update-if-current"; 3 | import { OrderStatus } from "@wowowow/common"; 4 | 5 | interface OrderAttrs { 6 | id: string; 7 | version: number; 8 | userId: string; 9 | price: number; 10 | status: OrderStatus; 11 | } 12 | 13 | interface OrderDoc extends mongoose.Document { 14 | version: number; 15 | userId: string; 16 | price: number; 17 | status: OrderStatus; 18 | } 19 | 20 | interface OrderModel extends mongoose.Model { 21 | build(attrs: OrderAttrs): OrderDoc; 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: OrderAttrs) => { 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 | -------------------------------------------------------------------------------- /Payment-Service/src/models/payment.ts: -------------------------------------------------------------------------------- 1 | import mongoose from "mongoose"; 2 | 3 | interface PaymentAttrs { 4 | orderId: string; 5 | stripeId: string; 6 | } 7 | 8 | interface PaymentDoc extends mongoose.Document { 9 | orderId: string; 10 | stripeId: string; 11 | } 12 | 13 | interface PaymentModel extends mongoose.Model { 14 | build(attrs: PaymentAttrs): PaymentDoc; 15 | } 16 | 17 | const paymentSchema = new mongoose.Schema( 18 | { 19 | orderId: { 20 | required: true, 21 | type: String, 22 | }, 23 | stripeId: { 24 | required: true, 25 | type: String, 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: PaymentAttrs) => { 39 | return new Payment(attrs); 40 | }; 41 | 42 | const Payment = mongoose.model( 43 | "Payment", 44 | paymentSchema 45 | ); 46 | 47 | export { Payment }; 48 | -------------------------------------------------------------------------------- /Payment-Service/src/nats-wrapper.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("Cannot access NATS client before connecting"); 9 | } 10 | return this._client; 11 | } 12 | 13 | connect(clusterId: string, clientId: string, url: string): Promise { 14 | this._client = nats.connect(clusterId, clientId, { url }); 15 | 16 | return new Promise((resolve, reject) => { 17 | this.client.on("connect", () => { 18 | console.log("Connected to NATS"); 19 | resolve(); 20 | }); 21 | 22 | this.client.on("error", (err) => { 23 | reject(err); 24 | }); 25 | }); 26 | } 27 | } 28 | 29 | export const natsWrapper = new NatsWrapper(); 30 | -------------------------------------------------------------------------------- /Payment-Service/src/stripe.ts: -------------------------------------------------------------------------------- 1 | import Stripe from 'stripe' 2 | 3 | export const stripe = new Stripe(process.env.STRIPE_KEY!,{ 4 | apiVersion:'2020-08-27' 5 | } ) -------------------------------------------------------------------------------- /Payment-Service/src/test/setup.ts: -------------------------------------------------------------------------------- 1 | import { MongoMemoryServer } from "mongodb-memory-server"; 2 | import mongoose from "mongoose"; 3 | import request from "supertest"; 4 | import { app } from "../app"; 5 | import jwt from "jsonwebtoken"; 6 | 7 | declare global { 8 | namespace NodeJS { 9 | interface Global { 10 | signin(id?:string): string[]; 11 | } 12 | } 13 | } 14 | 15 | jest.mock("../nats-wrapper.ts"); 16 | 17 | let mongo: any; 18 | beforeAll(async () => { 19 | //! setting environment variable for jwtsecret for test environment 20 | process.env.JWTSECRET = "qwer123qwer"; 21 | mongo = new MongoMemoryServer(); 22 | const mongoUri = await mongo.getUri(); 23 | 24 | await mongoose.connect(mongoUri, { 25 | useNewUrlParser: true, 26 | useUnifiedTopology: true, 27 | }); 28 | }); 29 | 30 | beforeEach(async () => { 31 | jest.clearAllMocks(); 32 | const collections = await mongoose.connection.db.collections(); 33 | 34 | for (let collection of collections) { 35 | await collection.deleteMany({}); 36 | } 37 | }); 38 | 39 | afterAll(async () => { 40 | await mongo.stop(); 41 | await mongoose.connection.close(); 42 | }); 43 | 44 | global.signin = (id?: string) => { 45 | // Build a JWT payload for tests. {id,email} 46 | const payload = { 47 | id: id || new mongoose.Types.ObjectId().toHexString(), 48 | email: "test@test.com", 49 | }; 50 | 51 | // creating the JWT 52 | const token = jwt.sign(payload, process.env.JWTSECRET!); 53 | 54 | // building session object {jwt: JWT_Object} 55 | const session = { jwt: token }; 56 | 57 | // Turning that session into JSON 58 | const sessionJSON = JSON.stringify(session); 59 | 60 | // Taking the JSON and encoding it as base64 61 | const base64 = Buffer.from(sessionJSON).toString("base64"); 62 | 63 | // return a string thats the cookie with the encoded data 64 | return [`express:sess=${base64}`]; 65 | }; 66 | -------------------------------------------------------------------------------- /Serverless-with-knative-serving/blue-deployment/fntblu-ksvc.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: serving.knative.dev/v1 2 | kind: Service 3 | metadata: 4 | name: bluedeployment 5 | labels: 6 | type: stage-depl 7 | version: v1 8 | spec: 9 | template: 10 | metadata: 11 | name: bluedeployment-v1 12 | spec: 13 | serviceAccountName: glotixz-fntbluksvc 14 | containers: 15 | - name: nextjs-bluedepl 16 | image: quay.io/ultimatestack/frontend:blue 17 | ports: 18 | - containerPort: 3001 19 | env: 20 | - name: NEXT_PUBLIC_BASEURL 21 | value: 'e20b4706-9ba3-4496-a857-b8b531dd5a38.k8s.civo.com' 22 | # value: '' 23 | - name: NEXT_PUBLIC_STRIPE_KEY 24 | value: 'pk_test_51IWeSrSGJ6cJaAGg6Be4Ty2WnNfEwASS11HO2syVLoAJGesykRVi9K19rzTjTBUBySdezIcFwHzDLyp2oX4BIssn007PlgW6Za' 25 | livenessProbe: 26 | exec: 27 | command: 28 | - cat 29 | - /app/README.md 30 | readinessProbe: 31 | exec: 32 | command: 33 | - cat 34 | - /app/README.md 35 | -------------------------------------------------------------------------------- /Serverless-with-knative-serving/blue-deployment/fntblu-sa.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ServiceAccount 3 | metadata: 4 | name: glotixz-fntbluksvc 5 | labels: 6 | account: fntbluksvc 7 | type: ksvc -------------------------------------------------------------------------------- /Terraform/Azure/.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.toptal.com/developers/gitignore/api/terraform 3 | # Edit at https://www.toptal.com/developers/gitignore?templates=terraform 4 | 5 | ### Terraform ### 6 | # Local .terraform directories 7 | **/.terraform/* 8 | 9 | # .tfstate files 10 | *.tfstate 11 | *.tfstate.* 12 | 13 | # Crash log files 14 | crash.log 15 | 16 | # Ignore any .tfvars files that are generated automatically for each Terraform run. Most 17 | # .tfvars files are managed as part of configuration and so should be included in 18 | # version control. 19 | # 20 | # example.tfvars 21 | 22 | # Ignore override files as they are usually used to override resources locally and so 23 | # are not checked in 24 | override.tf 25 | override.tf.json 26 | *_override.tf 27 | *_override.tf.json 28 | 29 | # Include override files you do wish to add to version control using negated pattern 30 | # !example_override.tf 31 | 32 | # Include tfplan files to ignore the plan output of command: terraform plan -out=tfplan 33 | # example: *tfplan* 34 | 35 | # End of https://www.toptal.com/developers/gitignore/api/terraform 36 | 37 | -------------------------------------------------------------------------------- /Terraform/Azure/README.md: -------------------------------------------------------------------------------- 1 | # Creating Azure Kubernetes Service (AKS) Cluster with Terraform 2 | 3 | ### ✨✨ Sign in to Azure CLI: 4 | 5 | To create resources with terraform, we first need to authenticate our session with loging into the azure with the azure cli from the terminal which will take care of setting the authentication context which will be used by terraform when planning, creating and deleting resources. 6 | 7 | ``` 8 | $>> az login 9 | ``` 10 | 11 | ### ✨✨ Initialize: 12 | We need to initialize by the following command which will download all the necessary providers required by `main.tf` or `provider.tf` file. 13 | ```bash 14 | $>> terraform init 15 | ``` 16 | 17 | ### ✨✨ Plan: 18 | 19 | Terraform plan will do a dry run of comparing the resources in the provider/platform of choice( eg: azure) with regards to the code and will give out result whether it can be created successfully or not. Plan step is a very useful safeguard step to check and do the necessary corrections required before applying and creating the actual resources. 20 | 21 | ```bash 22 | $>> terraform plan 23 | ``` 24 | ### ✨✨ Apply: 25 | applying out code which will provision all the resources as it was succeded in the plan step. 26 | ```bash 27 | $>> terraform apply -auto-approve 28 | ``` 29 | 30 | 31 | We can use the terraform [`workspace`](https://www.terraform.io/docs/language/state/workspaces.html) feature to create `dev`, `staging` and `production` stage cluster with the same codes. -------------------------------------------------------------------------------- /Terraform/Azure/aks.tf: -------------------------------------------------------------------------------- 1 | resource "azurerm_kubernetes_cluster" "dev-cluster" { 2 | name = "dev-cluster" 3 | location = azurerm_resource_group.dev-cluster.location 4 | resource_group_name = azurerm_resource_group.dev-cluster.name 5 | dns_prefix = "dev-cluster" 6 | 7 | default_node_pool { 8 | name = "default" 9 | node_count = 3 10 | vm_size = "Standard_D3_v2" 11 | } 12 | 13 | service_principal { 14 | client_id = azuread_service_principal.aks-dev-cluster.application_id 15 | client_secret = random_password.aks-dev-cluster-password.result 16 | } 17 | } 18 | 19 | -------------------------------------------------------------------------------- /Terraform/Azure/azuread.tf: -------------------------------------------------------------------------------- 1 | resource "random_password" "aks-dev-cluster-password" { 2 | length = 32 3 | special = true 4 | } 5 | 6 | resource "azuread_application" "aks-dev-cluster" { 7 | name = "aks-dev-cluster" 8 | available_to_other_tenants = false 9 | } 10 | 11 | resource "azuread_service_principal" "aks-dev-cluster" { 12 | application_id = azuread_application.aks-dev-cluster.application_id 13 | } 14 | 15 | resource "azuread_service_principal_password" "aks-dev-cluster" { 16 | service_principal_id = azuread_service_principal 17 | value = random_password.aks-dev-cluster-password.result 18 | end_date_relative = "17520h" 19 | } 20 | -------------------------------------------------------------------------------- /Terraform/Azure/main.tf: -------------------------------------------------------------------------------- 1 | provider "azurerm" { 2 | features {} 3 | } 4 | 5 | provider "azuread" {} 6 | 7 | 8 | # creating a resource group 9 | resource "azurerm_resource_group" "dev-cluster" { 10 | name = "dev-cluster" 11 | location = var.location 12 | } 13 | -------------------------------------------------------------------------------- /Terraform/Azure/output.tf: -------------------------------------------------------------------------------- 1 | output "client_certificate" { 2 | value = azurerm_kubernetes_cluster.dev-cluster.kube_config.0.client_certificate 3 | } 4 | 5 | output "kube_config" { 6 | value = azurerm_kubernetes_cluster.dev-cluster.kube_config_raw 7 | } 8 | -------------------------------------------------------------------------------- /Terraform/Azure/vars.tf: -------------------------------------------------------------------------------- 1 | variable "location" { 2 | type = string 3 | default = "westus" 4 | } 5 | -------------------------------------------------------------------------------- /Ticket-Service/.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /Ticket-Service/.stignore: -------------------------------------------------------------------------------- 1 | .git 2 | # Runtime data 3 | pids 4 | *.pid 5 | *.seed 6 | *.pid.lock 7 | 8 | # Directory for instrumented libs generated by jscoverage/JSCover 9 | lib-cov 10 | 11 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 12 | .grunt 13 | 14 | # Bower dependency directory (https://bower.io/) 15 | bower_components 16 | 17 | # node-waf configuration 18 | .lock-wscript 19 | 20 | # Compiled binary addons (https://nodejs.org/api/addons.html) 21 | build/Release 22 | 23 | # Dependency directories 24 | node_modules 25 | jspm_packages 26 | 27 | # Optional npm cache directory 28 | .npm 29 | 30 | # Optional eslint cache 31 | .eslintcache 32 | 33 | # Optional REPL history 34 | .node_repl_history 35 | 36 | # Output of 'npm pack' 37 | *.tgz 38 | 39 | # Yarn Integrity file 40 | .yarn-integrity 41 | 42 | # parcel-bundler cache (https://parceljs.org/) 43 | .cache 44 | 45 | # next.js build output 46 | .next 47 | 48 | # nuxt.js build output 49 | .nuxt 50 | 51 | # vuepress build output 52 | .vuepress/dist 53 | 54 | # Serverless directories 55 | .serverless 56 | 57 | -------------------------------------------------------------------------------- /Ticket-Service/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM quay.io/narendev/fishnode:alpine 2 | 3 | WORKDIR /app 4 | ENV PORT=4001 5 | 6 | COPY package.json . 7 | RUN yarn install --production 8 | COPY . . 9 | 10 | EXPOSE ${PORT} 11 | 12 | CMD [ "yarn","start" ] -------------------------------------------------------------------------------- /Ticket-Service/README.md: -------------------------------------------------------------------------------- 1 | # Ticket Service 2 | -------------------------------------------------------------------------------- /Ticket-Service/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: "ts-jest", 3 | testEnvironment: "node", 4 | setupFilesAfterEnv: ["./src/test/setup.ts"], 5 | }; 6 | 7 | // "jest": { 8 | // "preset": "ts-jest", 9 | // "testEnvironment": "node", 10 | // "setupFilesAfterEnv": [ 11 | // "./src/test/setup.ts" 12 | // ] 13 | // }, 14 | -------------------------------------------------------------------------------- /Ticket-Service/okteto.yml: -------------------------------------------------------------------------------- 1 | name: mcs-ticket-service 2 | autocreate: true 3 | image: quay.io/narendev/fishnode:1.0 4 | command: fish 5 | sync: 6 | - .:/usr/src/app 7 | forward: 8 | - 9229:9229 9 | - 3000:3000 10 | persistentVolume: {} 11 | annotations: 12 | sidecar.istio.io/inject: "false" 13 | -------------------------------------------------------------------------------- /Ticket-Service/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ticket-service", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "ts-node-dev src/index.ts", 8 | "test": "jest --watchAll --no-cache" 9 | }, 10 | "keywords": [], 11 | "author": "", 12 | "license": "ISC", 13 | "dependencies": { 14 | "@types/cookie-session": "^2.0.42", 15 | "@types/cors": "^2.8.10", 16 | "@types/express": "^4.17.11", 17 | "@types/jsonwebtoken": "^8.5.0", 18 | "@types/mongoose": "^5.10.3", 19 | "@types/swagger-ui-express": "^4.1.2", 20 | "@types/yamljs": "^0.2.31", 21 | "@wowowow/common": "^1.0.11", 22 | "cookie-session": "^1.4.0", 23 | "cors": "^2.8.5", 24 | "express": "^4.17.1", 25 | "express-async-errors": "^3.1.1", 26 | "express-validator": "^6.10.0", 27 | "jsonwebtoken": "^8.5.1", 28 | "mongoose": "5.10.19", 29 | "mongoose-update-if-current": "^1.4.0", 30 | "node-nats-streaming": "^0.3.2", 31 | "swagger-ui-express": "^4.1.6", 32 | "ts-node-dev": "^1.1.6", 33 | "typescript": "^4.2.2", 34 | "yamljs": "^0.3.0" 35 | }, 36 | "devDependencies": { 37 | "@types/jest": "^26.0.20", 38 | "@types/supertest": "^2.0.10", 39 | "jest": "^26.6.3", 40 | "mongodb-memory-server": "^6.9.3", 41 | "supertest": "^6.1.3", 42 | "ts-jest": "^26.5.3" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Ticket-Service/src/__mocks__/nats-wrapper.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 | -------------------------------------------------------------------------------- /Ticket-Service/src/app.ts: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | import "express-async-errors"; 3 | import { json } from "body-parser"; 4 | import cookieSession from "cookie-session"; 5 | import cors from "cors"; 6 | import { errorHandler, NotFoundError, currentUser } from "@wowowow/common"; 7 | import { createTicketRouter } from "./routes/new"; 8 | import { showTicketRouter } from "./routes/show"; 9 | import { indexTicketRouter } from "./routes"; 10 | import { updateTicketRouter } from "./routes/update"; 11 | 12 | import YAML from "yamljs"; 13 | import swaggerUI from "swagger-ui-express"; 14 | 15 | const app = express(); 16 | 17 | const swaggeryml = YAML.load("src/ticket-svc.yml"); 18 | const options = { 19 | explorer: true, 20 | }; 21 | 22 | const hostValues = process.env.ALLOWED_HOSTS; 23 | 24 | const hostArray = hostValues!.split(",") 25 | 26 | 27 | const corsOptions = { 28 | origin: hostArray, 29 | credentials: true, 30 | exposedHeaders: ["set-cookie"] 31 | } 32 | 33 | app.use(cors(corsOptions)); 34 | app.set("trust proxy", true); 35 | app.use(json()); 36 | app.use( 37 | cookieSession({ 38 | signed: false, 39 | // secure:true //! when this is set to true, it'll only work on connection coming with https:// 40 | }) 41 | ); 42 | 43 | app.use(currentUser); 44 | 45 | app.use( 46 | "/api/tickets/doc", 47 | swaggerUI.serve, 48 | swaggerUI.setup(swaggeryml, options) 49 | ); 50 | 51 | app.use(createTicketRouter); 52 | app.use(showTicketRouter); 53 | app.use(indexTicketRouter); 54 | app.use(updateTicketRouter); 55 | 56 | app.all("*", async () => { 57 | throw new NotFoundError(); 58 | }); 59 | 60 | app.use(errorHandler); 61 | 62 | export { app }; 63 | -------------------------------------------------------------------------------- /Ticket-Service/src/events/listeners/__test__/order-cancelled-listener.test.ts: -------------------------------------------------------------------------------- 1 | import { Ticket } from "../../../models/ticket"; 2 | import { natsWrapper } from "../../../nats-wrapper"; 3 | import { OrderCancelledListener } from "../order-cancelled-listener"; 4 | import mongoose from "mongoose"; 5 | import { OrderCancelledEvent } from "@wowowow/common"; 6 | import { Message } from "node-nats-streaming"; 7 | 8 | const setup = async () => { 9 | const listener = new OrderCancelledListener(natsWrapper.client); 10 | 11 | const orderId = mongoose.Types.ObjectId().toHexString(); 12 | const ticket = Ticket.build({ 13 | title: "concert", 14 | price: 20, 15 | userId: "asdf", 16 | }); 17 | ticket.set({ orderId }); 18 | await ticket.save(); 19 | 20 | const data: OrderCancelledEvent["data"] = { 21 | id: orderId, 22 | version: 0, 23 | ticket: { 24 | id: ticket.id, 25 | }, 26 | }; 27 | 28 | // @ts-ignore 29 | const msg: Message = { 30 | ack: jest.fn(), 31 | }; 32 | 33 | return { msg, data, ticket, orderId, listener }; 34 | }; 35 | 36 | it("updates the ticket, publishes an event, and acks the message", async () => { 37 | const { msg, data, ticket, orderId, listener } = await setup(); 38 | 39 | await listener.onMessage(data, msg); 40 | 41 | const updatedTicket = await Ticket.findById(ticket.id); 42 | expect(updatedTicket!.orderId).not.toBeDefined(); 43 | expect(msg.ack).toHaveBeenCalled(); 44 | expect(natsWrapper.client.publish).toHaveBeenCalled(); 45 | }); 46 | -------------------------------------------------------------------------------- /Ticket-Service/src/events/listeners/order-cancelled-listener.ts: -------------------------------------------------------------------------------- 1 | import { Listener, OrderCancelledEvent, Subjects } from "@wowowow/common"; 2 | import { Message } from "node-nats-streaming"; 3 | import { Ticket } from "../../models/ticket"; 4 | import { TicketUpdatedPublisher } from "../publishers/ticket-updated-publisher"; 5 | import { queueGroupName } from "./queue-group-name"; 6 | 7 | export class OrderCancelledListener extends Listener { 8 | subject: Subjects.OrderCancelled = Subjects.OrderCancelled; 9 | queueGroupName = queueGroupName; 10 | 11 | async onMessage(data: OrderCancelledEvent["data"], msg: Message) { 12 | const ticket = await Ticket.findById(data.ticket.id); 13 | 14 | if (!ticket) { 15 | throw new Error("Ticket not found"); 16 | } 17 | 18 | ticket.set({ orderId: undefined }); 19 | await ticket.save(); 20 | 21 | await new TicketUpdatedPublisher(this.client).publish({ 22 | id: ticket.id, 23 | orderId: ticket.orderId, 24 | userId: ticket.userId, 25 | price: ticket.price, 26 | title: ticket.title, 27 | version: ticket.version, 28 | }); 29 | 30 | msg.ack(); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Ticket-Service/src/events/listeners/order-created-listener.ts: -------------------------------------------------------------------------------- 1 | import { Message } from "node-nats-streaming"; 2 | import { Listener, OrderCreatedEvent, Subjects } from "@wowowow/common"; 3 | import { queueGroupName } from "./queue-group-name"; 4 | import { Ticket } from "../../models/ticket"; 5 | import { TicketUpdatedPublisher } from "../publishers/ticket-updated-publisher"; 6 | 7 | export class OrderCreatedListener extends Listener { 8 | subject: Subjects.OrderCreated = Subjects.OrderCreated; 9 | queueGroupName = queueGroupName; 10 | 11 | async onMessage(data: OrderCreatedEvent["data"], msg: Message) { 12 | // Find the ticket that the order is reserving 13 | const ticket = await Ticket.findById(data.ticket.id); 14 | 15 | // If no ticket, throw error 16 | if (!ticket) { 17 | throw new Error("Ticket not found"); 18 | } 19 | 20 | // Mark the ticket as being reserved by setting its orderId property 21 | ticket.set({ orderId: data.id }); 22 | 23 | // Save the ticket 24 | await ticket.save(); 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 | // ack the message 35 | msg.ack(); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Ticket-Service/src/events/listeners/queue-group-name.ts: -------------------------------------------------------------------------------- 1 | export const queueGroupName = "tickets-service"; 2 | -------------------------------------------------------------------------------- /Ticket-Service/src/events/publishers/ticket-created-publisher.ts: -------------------------------------------------------------------------------- 1 | import { Publisher, Subjects, TicketCreatedEvent } from "@wowowow/common"; 2 | 3 | export class TicketCreatedPublisher extends Publisher { 4 | subject: Subjects.TicketCreated = Subjects.TicketCreated; 5 | } 6 | -------------------------------------------------------------------------------- /Ticket-Service/src/events/publishers/ticket-updated-publisher.ts: -------------------------------------------------------------------------------- 1 | import { Publisher, Subjects, TicketUpdatedEvent } from "@wowowow/common"; 2 | 3 | export class TicketUpdatedPublisher extends Publisher { 4 | subject: Subjects.TicketUpdated = Subjects.TicketUpdated; 5 | } 6 | -------------------------------------------------------------------------------- /Ticket-Service/src/models/__test__/ticket.test.ts: -------------------------------------------------------------------------------- 1 | import { Ticket } from "../ticket"; 2 | 3 | it("it implements optimistic concurrency control[occ] when a document is updated ", async (done) => { 4 | // Create an instance of a ticket 5 | const ticket = Ticket.build({ 6 | title: "concert 22", 7 | price: 23, 8 | userId: "2223", 9 | }); 10 | 11 | // Save the ticket to the database 12 | await ticket.save(); 13 | 14 | // fetch the ticket twice 15 | const firstInstance = await Ticket.findById(ticket.id); 16 | const secondInstance = await Ticket.findById(ticket.id); 17 | 18 | // make two separate changes to the tickets we fetched 19 | firstInstance!.set({ price: 10 }); 20 | secondInstance!.set({ price: 13 }); 21 | 22 | // save the first fetched ticket 23 | await firstInstance!.save(); 24 | 25 | // save the second fetched ticket and expect an error 26 | try { 27 | await secondInstance!.save(); 28 | } catch (err) { 29 | return done(); 30 | } 31 | 32 | throw new Error("Should not reach this point"); 33 | }); 34 | 35 | it("it increments the version number on multiple saves", async (done) => { 36 | const ticket = Ticket.build({ 37 | title: "concert 29", 38 | price: 20, 39 | userId: "wawawa", 40 | }); 41 | 42 | await ticket.save(); 43 | expect(ticket.version).toEqual(0); 44 | await ticket.save(); 45 | expect(ticket.version).toEqual(1); 46 | await ticket.save(); 47 | expect(ticket.version).toEqual(2); 48 | }); 49 | -------------------------------------------------------------------------------- /Ticket-Service/src/models/ticket.ts: -------------------------------------------------------------------------------- 1 | import mongoose from "mongoose"; 2 | import { updateIfCurrentPlugin } from "mongoose-update-if-current"; 3 | 4 | interface TicketAttrs { 5 | title: string; 6 | price: number; 7 | userId: string; 8 | } 9 | 10 | interface TicketDoc extends mongoose.Document { 11 | title: string; 12 | price: number; 13 | userId: string; 14 | version: number; 15 | orderId?: string; 16 | } 17 | 18 | interface TicketModel extends mongoose.Model { 19 | build(attrs: TicketAttrs): TicketDoc; 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: TicketAttrs) => { 54 | return new Ticket(attrs); 55 | }; 56 | 57 | const Ticket = mongoose.model("Ticket", ticketSchema); 58 | 59 | export { Ticket }; 60 | -------------------------------------------------------------------------------- /Ticket-Service/src/nats-wrapper.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("Cannot access NATS client before connecting"); 9 | } 10 | return this._client; 11 | } 12 | 13 | connect(clusterId: string, clientId: string, url: string): Promise { 14 | this._client = nats.connect(clusterId, clientId, { url }); 15 | 16 | return new Promise((resolve, reject) => { 17 | this.client.on("connect", () => { 18 | console.log("Connected to NATS"); 19 | resolve(); 20 | }); 21 | 22 | this.client.on("error", (err) => { 23 | reject(err); 24 | }); 25 | }); 26 | } 27 | } 28 | 29 | export const natsWrapper = new NatsWrapper(); 30 | -------------------------------------------------------------------------------- /Ticket-Service/src/routes/__test__/index.test.ts: -------------------------------------------------------------------------------- 1 | import request from "supertest"; 2 | import { app } from "../../app"; 3 | 4 | const createTicket = () => { 5 | return request(app).post("/api/tickets").set("Cookie", global.signin()).send({ 6 | title: "asdasdfa", 7 | price: 20, 8 | }); 9 | }; 10 | 11 | it("can fetch a list of tickets ", async () => { 12 | await createTicket(); 13 | await createTicket(); 14 | await createTicket(); 15 | await createTicket(); 16 | 17 | const response = await request(app).get("/api/tickets").send().expect(200); 18 | 19 | expect(response.body.length).toEqual(3); 20 | }); 21 | -------------------------------------------------------------------------------- /Ticket-Service/src/routes/__test__/show.test.ts: -------------------------------------------------------------------------------- 1 | import request from "supertest"; 2 | import { app } from "../../app"; 3 | import mongoose from "mongoose"; 4 | 5 | it("returns a 404 if the ticket is 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 = "lkashdiewh"; 12 | const price = 47; 13 | 14 | const responce = 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/${responce.body.id}`) 25 | .send() 26 | .expect(200); 27 | 28 | expect(ticketResponse.body.title).toEqual(title); 29 | expect(ticketResponse.body.price).toEqual(price); 30 | }); 31 | -------------------------------------------------------------------------------- /Ticket-Service/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 | 11 | res.send(tickets); 12 | }); 13 | 14 | export { router as indexTicketRouter }; 15 | -------------------------------------------------------------------------------- /Ticket-Service/src/routes/new.ts: -------------------------------------------------------------------------------- 1 | import express, { Request, Response } from "express"; 2 | import { body } from "express-validator"; 3 | import { requireAuth, validateRequest } from "@wowowow/common"; 4 | import { Ticket } from "../models/ticket"; 5 | import { TicketCreatedPublisher } from "../events/publishers/ticket-created-publisher"; 6 | import { natsWrapper } from "../nats-wrapper"; 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 | 23 | const ticket = Ticket.build({ 24 | title, 25 | price, 26 | userId: req.currentUser!.id, 27 | }); 28 | 29 | await ticket.save(); 30 | await new TicketCreatedPublisher(natsWrapper.client).publish({ 31 | id: ticket.id, 32 | title: ticket.title, 33 | price: ticket.price, 34 | userId: ticket.userId, 35 | version: ticket.version, 36 | }); 37 | 38 | res.status(201).send(ticket); 39 | } 40 | ); 41 | 42 | export { router as createTicketRouter }; 43 | -------------------------------------------------------------------------------- /Ticket-Service/src/routes/show.ts: -------------------------------------------------------------------------------- 1 | import { NotFoundError } from "@wowowow/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 | res.send(ticket); 15 | }); 16 | 17 | export { router as showTicketRouter }; 18 | -------------------------------------------------------------------------------- /Ticket-Service/src/routes/update.ts: -------------------------------------------------------------------------------- 1 | import { 2 | BadRequestError, 3 | NotAuthorizedError, 4 | NotFoundError, 5 | requireAuth, 6 | validateRequest, 7 | } from "@wowowow/common"; 8 | import express, { Request, Response } from "express"; 9 | import { body } from "express-validator"; 10 | import { TicketUpdatedPublisher } from "../events/publishers/ticket-updated-publisher"; 11 | 12 | import { Ticket } from "../models/ticket"; 13 | import { natsWrapper } from "../nats-wrapper"; 14 | 15 | const router = express.Router(); 16 | 17 | router.put( 18 | "/api/tickets/:id", 19 | requireAuth, 20 | [ 21 | body("title").not().isEmpty().withMessage("Title is required"), 22 | body("price") 23 | .isFloat({ gt: 0 }) 24 | .withMessage("Price must be provided and must be greater than 0 "), 25 | ], 26 | validateRequest, 27 | async (req: Request, res: Response) => { 28 | const ticket = await Ticket.findById(req.params.id); 29 | 30 | if (!ticket) { 31 | throw new NotFoundError(); 32 | } 33 | 34 | if (ticket.orderId) { 35 | throw new BadRequestError("Cannot edit a reserved ticket"); 36 | } 37 | 38 | if (ticket.userId !== req.currentUser!.id) { 39 | throw new NotAuthorizedError(); 40 | } 41 | 42 | ticket.set({ 43 | title: req.body.title, 44 | price: req.body.price, 45 | }); 46 | 47 | await ticket.save(); 48 | 49 | new TicketUpdatedPublisher(natsWrapper.client).publish({ 50 | id: ticket.id, 51 | title: ticket.title, 52 | price: ticket.price, 53 | userId: ticket.userId, 54 | version: ticket.version, 55 | }); 56 | 57 | res.status(201).send(ticket); 58 | } 59 | ); 60 | 61 | export { router as updateTicketRouter }; 62 | -------------------------------------------------------------------------------- /Ticket-Service/src/test/setup.ts: -------------------------------------------------------------------------------- 1 | import { MongoMemoryServer } from "mongodb-memory-server"; 2 | import mongoose from "mongoose"; 3 | import request from "supertest"; 4 | import { app } from "../app"; 5 | import jwt from "jsonwebtoken"; 6 | 7 | declare global { 8 | namespace NodeJS { 9 | interface Global { 10 | signin(): string[]; 11 | } 12 | } 13 | } 14 | 15 | jest.mock("../nats-wrapper.ts"); 16 | 17 | let mongo: any; 18 | beforeAll(async () => { 19 | //! setting environment variable for jwtsecret for test environment 20 | process.env.JWTSECRET = "qwer123qwer"; 21 | mongo = new MongoMemoryServer(); 22 | const mongoUri = await mongo.getUri(); 23 | 24 | await mongoose.connect(mongoUri, { 25 | useNewUrlParser: true, 26 | useUnifiedTopology: true, 27 | }); 28 | }); 29 | 30 | beforeEach(async () => { 31 | jest.clearAllMocks(); 32 | const collections = await mongoose.connection.db.collections(); 33 | 34 | for (let collection of collections) { 35 | await collection.deleteMany({}); 36 | } 37 | }); 38 | 39 | afterAll(async () => { 40 | await mongo.stop(); 41 | await mongoose.connection.close(); 42 | }); 43 | 44 | global.signin = () => { 45 | // Build a JWT payload for tests. {id,email} 46 | const payload = { 47 | id: new mongoose.Types.ObjectId().toHexString(), 48 | email: "test@test.com", 49 | }; 50 | 51 | // creating the JWT 52 | const token = jwt.sign(payload, process.env.JWTSECRET!); 53 | 54 | // building session object {jwt: JWT_Object} 55 | const session = { jwt: token }; 56 | 57 | // Turning that session into JSON 58 | const sessionJSON = JSON.stringify(session); 59 | 60 | // Taking the JSON and encoding it as base64 61 | const base64 = Buffer.from(sessionJSON).toString("base64"); 62 | 63 | // return a string thats the cookie with the encoded data 64 | return [`express:sess=${base64}`]; 65 | }; 66 | -------------------------------------------------------------------------------- /gitops/BackendServices/Auth-srv/auth-depl.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: auth-depl 5 | labels: 6 | type: stage-depl 7 | svcname: auth-svc 8 | version: v1 9 | spec: 10 | replicas: 1 11 | selector: 12 | matchLabels: 13 | app: auth 14 | version: v1 15 | template: 16 | metadata: 17 | labels: 18 | app: auth 19 | version: v1 20 | spec: 21 | serviceAccountName: glotixz-authsrv 22 | containers: 23 | - name: auth 24 | image: quay.io/ultimatestack/auth-svc:v1.2-beta 25 | imagePullPolicy: Always 26 | ports: 27 | - containerPort: 4000 28 | env: 29 | - name: AUTH_MONGO_DB_URI 30 | value: 'mongodb://authdb-sts-srv:27017/auth' 31 | - name: JWTSECRET 32 | valueFrom: 33 | secretKeyRef: 34 | name: jwt-secret 35 | key: JWT_KEY 36 | envFrom: 37 | - configMapRef: 38 | name: host-config -------------------------------------------------------------------------------- /gitops/BackendServices/Auth-srv/auth-svc.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: auth-srv 5 | labels: 6 | app: auth 7 | service: auth 8 | spec: 9 | selector: 10 | app: auth 11 | ports: 12 | - name: http-auth 13 | protocol: TCP 14 | port: 4000 15 | targetPort: 4000 16 | type: ClusterIP -------------------------------------------------------------------------------- /gitops/BackendServices/Auth-srv/authsrv-sa.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ServiceAccount 3 | metadata: 4 | name: glotixz-authsrv 5 | labels: 6 | account: authsrv 7 | type: depl -------------------------------------------------------------------------------- /gitops/BackendServices/Expiration-srv/exprn-depl.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: expiration-depl 5 | labels: 6 | type: stage-depl 7 | svcname: expiration-svc 8 | version: v1 9 | spec: 10 | replicas: 1 11 | selector: 12 | matchLabels: 13 | app: expiration 14 | version: v1 15 | template: 16 | metadata: 17 | labels: 18 | app: expiration 19 | version: v1 20 | spec: 21 | serviceAccountName: glotixz-expirationsrv 22 | containers: 23 | - name: expiration 24 | image: quay.io/ultimatestack/expiration-svc:v1-beta 25 | env: 26 | - name: NATS_CLIENT_ID 27 | valueFrom: 28 | fieldRef: 29 | fieldPath: metadata.name 30 | - name: NATS_URL 31 | value: "http://nats-srv:4222" 32 | - name: NATS_CLUSTER_ID 33 | value: ticketing 34 | - name: REDIS_HOST 35 | value: expiration-redis-srv 36 | -------------------------------------------------------------------------------- /gitops/BackendServices/Expiration-srv/exprnsrv-sa.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ServiceAccount 3 | metadata: 4 | name: glotixz-expirationsrv 5 | labels: 6 | account: expirationsrv 7 | type: depl -------------------------------------------------------------------------------- /gitops/BackendServices/Order-srv/order-depl.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: order-depl 5 | labels: 6 | type: stage-depl 7 | svcname: order-svc 8 | version: v1 9 | spec: 10 | replicas: 1 11 | selector: 12 | matchLabels: 13 | app: order 14 | version: v1 15 | template: 16 | metadata: 17 | labels: 18 | app: order 19 | version: v1 20 | spec: 21 | serviceAccountName: glotixz-ordersrv 22 | containers: 23 | - name: order 24 | image: quay.io/ultimatestack/orders-svc:v1.2-beta 25 | imagePullPolicy: Always 26 | ports: 27 | - containerPort: 4002 28 | env: 29 | - name: NATS_CLIENT_ID 30 | valueFrom: 31 | fieldRef: 32 | fieldPath: metadata.name 33 | - name: NATS_URL 34 | value: 'http://nats-srv:4222' 35 | - name: NATS_CLUSTER_ID 36 | value: ticketing 37 | - name: ORDERS_MONGO_DB_URI 38 | value: 'mongodb://orderdb-sts-srv:27017/orders' 39 | - name: JWTSECRET 40 | valueFrom: 41 | secretKeyRef: 42 | name: jwt-secret 43 | key: JWT_KEY 44 | envFrom: 45 | - configMapRef: 46 | name: host-config -------------------------------------------------------------------------------- /gitops/BackendServices/Order-srv/order-svc.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: order-srv 5 | labels: 6 | app: order 7 | service: order 8 | spec: 9 | selector: 10 | app: order 11 | ports: 12 | - name: http-order 13 | protocol: TCP 14 | port: 4002 15 | targetPort: 4002 16 | type: ClusterIP -------------------------------------------------------------------------------- /gitops/BackendServices/Order-srv/ordersrv-sa.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ServiceAccount 3 | metadata: 4 | name: glotixz-ordersrv 5 | labels: 6 | account: ordersrv 7 | type: depl -------------------------------------------------------------------------------- /gitops/BackendServices/Payment-srv/payment-depl.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: payments-depl 5 | labels: 6 | type: stage-depl 7 | svcname: payment-svc 8 | version: v1 9 | spec: 10 | replicas: 1 11 | selector: 12 | matchLabels: 13 | app: payments 14 | version: v1 15 | template: 16 | metadata: 17 | labels: 18 | app: payments 19 | version: v1 20 | spec: 21 | serviceAccountName: glotixz-paymentsrv 22 | containers: 23 | - name: payments 24 | image: quay.io/ultimatestack/payment-svc:v1.2-beta 25 | imagePullPolicy: Always 26 | ports: 27 | - containerPort: 4004 28 | env: 29 | - name: NATS_CLIENT_ID 30 | valueFrom: 31 | fieldRef: 32 | fieldPath: metadata.name 33 | - name: NATS_URL 34 | value: 'http://nats-srv:4222' 35 | - name: NATS_CLUSTER_ID 36 | value: ticketing 37 | - name: PAYMENTS_MONGO_DB_URI 38 | value: 'mongodb://paymentdb-sts-srv:27017/payments' 39 | - name: JWTSECRET 40 | valueFrom: 41 | secretKeyRef: 42 | name: jwt-secret 43 | key: JWT_KEY 44 | - name: STRIPE_KEY 45 | valueFrom: 46 | secretKeyRef: 47 | name: stripe-secret 48 | key: STRIPE_KEY 49 | envFrom: 50 | - configMapRef: 51 | name: host-config -------------------------------------------------------------------------------- /gitops/BackendServices/Payment-srv/payment-svc.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: payments-srv 5 | labels: 6 | app: payments 7 | service: payments 8 | spec: 9 | selector: 10 | app: payments 11 | ports: 12 | - name: http-payments 13 | protocol: TCP 14 | port: 4004 15 | targetPort: 4004 16 | type: ClusterIP 17 | -------------------------------------------------------------------------------- /gitops/BackendServices/Payment-srv/paymentsrv-sa.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ServiceAccount 3 | metadata: 4 | name: glotixz-paymentsrv 5 | labels: 6 | account: paymentsrv 7 | type: depl -------------------------------------------------------------------------------- /gitops/BackendServices/Ticket-srv/ticket-depl.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: tickets-depl 5 | labels: 6 | type: stage-depl 7 | svcname: tickets-svc 8 | version: v1 9 | spec: 10 | replicas: 1 11 | selector: 12 | matchLabels: 13 | app: tickets 14 | version: v1 15 | template: 16 | metadata: 17 | labels: 18 | app: tickets 19 | version: v1 20 | spec: 21 | serviceAccountName: glotixz-ticketsrv 22 | containers: 23 | - name: tickets 24 | image: quay.io/ultimatestack/ticket-svc:v1.2-beta 25 | imagePullPolicy: Always 26 | ports: 27 | - containerPort: 4001 28 | env: 29 | - name: NATS_CLIENT_ID 30 | valueFrom: 31 | fieldRef: 32 | fieldPath: metadata.name 33 | - name: NATS_URL 34 | value: "http://nats-srv:4222" 35 | - name: NATS_CLUSTER_ID 36 | value: ticketing 37 | - name: TICKETS_MONGO_DB_URI 38 | value: "mongodb://ticketdb-sts-srv:27017/tickets" 39 | - name: JWTSECRET 40 | valueFrom: 41 | secretKeyRef: 42 | name: jwt-secret 43 | key: JWT_KEY 44 | envFrom: 45 | - configMapRef: 46 | name: host-config -------------------------------------------------------------------------------- /gitops/BackendServices/Ticket-srv/ticket-svc.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: tickets-srv 5 | labels: 6 | app: tickets 7 | service: tickets 8 | spec: 9 | selector: 10 | app: tickets 11 | ports: 12 | - name: http-tickets 13 | protocol: TCP 14 | port: 4001 15 | targetPort: 4001 16 | type: ClusterIP 17 | -------------------------------------------------------------------------------- /gitops/BackendServices/Ticket-srv/ticketsrv-sa.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ServiceAccount 3 | metadata: 4 | name: glotixz-ticketsrv 5 | labels: 6 | account: ticketsrv 7 | type: depl -------------------------------------------------------------------------------- /gitops/DBs/Auth-srv-DB/authdb-sa.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ServiceAccount 3 | metadata: 4 | name: glotixz-authdb 5 | labels: 6 | account: authdb 7 | type: sts -------------------------------------------------------------------------------- /gitops/DBs/Auth-srv-DB/authdb-sts.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: StatefulSet 3 | metadata: 4 | name: authdb-sts 5 | labels: 6 | env-type: stagedb 7 | version: v1 8 | spec: 9 | selector: 10 | matchLabels: 11 | app: authdb-mongo 12 | serviceName: authdb-sts-srv 13 | replicas: 1 14 | template: 15 | metadata: 16 | labels: 17 | app: authdb-mongo 18 | spec: 19 | serviceAccountName: glotixz-authdb 20 | containers: 21 | - name: authdb-mongo 22 | image: mongo 23 | imagePullPolicy: Always 24 | volumeMounts: 25 | - name: authdb-presistent 26 | mountPath: /data/db 27 | volumeClaimTemplates: 28 | - metadata: 29 | name: authdb-presistent 30 | spec: 31 | storageClassName: longhorn 32 | accessModes: [ "ReadWriteOnce" ] 33 | resources: 34 | requests: 35 | storage: 1Gi 36 | -------------------------------------------------------------------------------- /gitops/DBs/Auth-srv-DB/authdb-svc.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: authdb-sts-srv 5 | spec: 6 | selector: 7 | app: authdb-mongo 8 | ports: 9 | - port: 27017 10 | targetPort: 27017 11 | protocol: TCP 12 | name: authdb 13 | type: ClusterIP 14 | -------------------------------------------------------------------------------- /gitops/DBs/Expiration-srv-DB/expdb-sa.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ServiceAccount 3 | metadata: 4 | name: glotixz-expdb 5 | labels: 6 | account: expdb 7 | type: sts -------------------------------------------------------------------------------- /gitops/DBs/Expiration-srv-DB/expdb-sts.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: StatefulSet 3 | metadata: 4 | name: expiration-redis-sts 5 | labels: 6 | env-type: stagedb 7 | version: v1 8 | spec: 9 | selector: 10 | matchLabels: 11 | app: expiration-redis 12 | serviceName: expiration-redis-srv 13 | replicas: 1 14 | template: 15 | metadata: 16 | labels: 17 | app: expiration-redis 18 | spec: 19 | serviceAccountName: glotixz-expdb 20 | containers: 21 | - name: expiration-redis 22 | image: redis 23 | imagePullPolicy: Always 24 | volumeMounts: 25 | - name: data 26 | mountPath: /data 27 | - name: conf 28 | mountPath: /etc/redis 29 | volumeClaimTemplates: 30 | - metadata: 31 | name: data 32 | spec: 33 | storageClassName: longhorn 34 | accessModes: ["ReadWriteOnce"] 35 | resources: 36 | requests: 37 | storage: 500Mi 38 | - metadata: 39 | name: conf 40 | spec: 41 | storageClassName: longhorn 42 | accessModes: ["ReadWriteOnce"] 43 | resources: 44 | requests: 45 | storage: 10Mi 46 | -------------------------------------------------------------------------------- /gitops/DBs/Expiration-srv-DB/expdb-svc.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: expiration-redis-srv 5 | spec: 6 | selector: 7 | app: expiration-redis 8 | ports: 9 | - name: db 10 | protocol: TCP 11 | port: 6379 12 | targetPort: 6379 13 | type: ClusterIP 14 | -------------------------------------------------------------------------------- /gitops/DBs/Order-srv-DB/orderdb-sa.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ServiceAccount 3 | metadata: 4 | name: glotixz-orderdb 5 | labels: 6 | account: orderdb 7 | type: sts -------------------------------------------------------------------------------- /gitops/DBs/Order-srv-DB/orderdb-sts.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: StatefulSet 3 | metadata: 4 | name: orderdb-sts 5 | labels: 6 | env-type: stagedb 7 | version: v1 8 | spec: 9 | selector: 10 | matchLabels: 11 | app: orderdb-mongo 12 | serviceName: orderdb-sts-srv 13 | replicas: 1 14 | template: 15 | metadata: 16 | labels: 17 | app: orderdb-mongo 18 | spec: 19 | serviceAccountName: glotixz-orderdb 20 | containers: 21 | - name: orderdb-mongo 22 | image: mongo 23 | imagePullPolicy: Always 24 | volumeMounts: 25 | - name: orderdb-presistent 26 | mountPath: /data/db 27 | volumeClaimTemplates: 28 | - metadata: 29 | name: orderdb-presistent 30 | spec: 31 | storageClassName: longhorn 32 | accessModes: [ "ReadWriteOnce" ] 33 | resources: 34 | requests: 35 | storage: 1Gi 36 | -------------------------------------------------------------------------------- /gitops/DBs/Order-srv-DB/orderdb-svc.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: orderdb-sts-srv 5 | spec: 6 | selector: 7 | app: orderdb-mongo 8 | ports: 9 | - port: 27017 10 | targetPort: 27017 11 | protocol: TCP 12 | name: orderdb 13 | type: ClusterIP 14 | -------------------------------------------------------------------------------- /gitops/DBs/Payment-srv-DB/paymentdb-sa.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ServiceAccount 3 | metadata: 4 | name: glotixz-paymentdb 5 | labels: 6 | account: paymentdb 7 | type: sts -------------------------------------------------------------------------------- /gitops/DBs/Payment-srv-DB/paymentdb-sts.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: StatefulSet 3 | metadata: 4 | name: paymentdb-sts 5 | labels: 6 | env-type: stagedb 7 | version: v1 8 | spec: 9 | selector: 10 | matchLabels: 11 | app: paymentdb-mongo 12 | serviceName: paymentdb-sts-srv 13 | replicas: 1 14 | template: 15 | metadata: 16 | labels: 17 | app: paymentdb-mongo 18 | spec: 19 | serviceAccountName: glotixz-paymentdb 20 | containers: 21 | - name: paymentdb-mongo 22 | image: mongo 23 | imagePullPolicy: Always 24 | volumeMounts: 25 | - name: paymentdb-presistent 26 | mountPath: /data/db 27 | volumeClaimTemplates: 28 | - metadata: 29 | name: paymentdb-presistent 30 | spec: 31 | storageClassName: longhorn 32 | accessModes: [ "ReadWriteOnce" ] 33 | resources: 34 | requests: 35 | storage: 1Gi 36 | -------------------------------------------------------------------------------- /gitops/DBs/Payment-srv-DB/paymentdb-svc.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: paymentdb-sts-srv 5 | spec: 6 | selector: 7 | app: paymentdb-mongo 8 | ports: 9 | - port: 27017 10 | targetPort: 27017 11 | protocol: TCP 12 | name: paymentdb 13 | type: ClusterIP 14 | -------------------------------------------------------------------------------- /gitops/DBs/Ticket-srv-DB/ticketdb-sa.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ServiceAccount 3 | metadata: 4 | name: glotixz-ticketdb 5 | labels: 6 | account: ticketdb 7 | type: sts -------------------------------------------------------------------------------- /gitops/DBs/Ticket-srv-DB/ticketdb-sts.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: StatefulSet 3 | metadata: 4 | name: ticketdb-sts 5 | labels: 6 | env-type: stagedb 7 | version: v1 8 | spec: 9 | selector: 10 | matchLabels: 11 | app: ticketdb-mongo 12 | serviceName: ticketdb-sts-srv 13 | replicas: 1 14 | template: 15 | metadata: 16 | labels: 17 | app: ticketdb-mongo 18 | spec: 19 | serviceAccountName: glotixz-ticketdb 20 | containers: 21 | - name: ticketdb-mongo 22 | image: mongo 23 | imagePullPolicy: Always 24 | volumeMounts: 25 | - name: ticketdb-presistent 26 | mountPath: /data/db 27 | volumeClaimTemplates: 28 | - metadata: 29 | name: ticketdb-presistent 30 | spec: 31 | storageClassName: longhorn 32 | accessModes: [ "ReadWriteOnce" ] 33 | resources: 34 | requests: 35 | storage: 1Gi 36 | -------------------------------------------------------------------------------- /gitops/DBs/Ticket-srv-DB/ticketdb-svc.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: ticketdb-sts-srv 5 | spec: 6 | selector: 7 | app: ticketdb-mongo 8 | ports: 9 | - port: 27017 10 | targetPort: 27017 11 | protocol: TCP 12 | name: ticketdb 13 | type: ClusterIP 14 | -------------------------------------------------------------------------------- /gitops/EventBus/nats-depl.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: nats-deployment 5 | labels: 6 | dkind: eventbus 7 | version: v1 8 | spec: 9 | replicas: 1 10 | selector: 11 | matchLabels: 12 | app: nats 13 | template: 14 | metadata: 15 | labels: 16 | app: nats 17 | spec: 18 | serviceAccountName: glotixz-nats 19 | containers: 20 | - name: nats 21 | image: nats-streaming:0.21.1 22 | imagePullPolicy: Always 23 | args: 24 | [ 25 | "-p", 26 | "4222", 27 | "-m", 28 | "8222", 29 | "-hbi", 30 | "5s", 31 | "-hbt", 32 | "5s", 33 | "-hbf", 34 | "2", 35 | "-SD", 36 | "-cid", 37 | "ticketing", 38 | ] 39 | -------------------------------------------------------------------------------- /gitops/EventBus/nats-sa.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ServiceAccount 3 | metadata: 4 | name: glotixz-nats 5 | labels: 6 | account: nats 7 | type: depl -------------------------------------------------------------------------------- /gitops/EventBus/nats-svc.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: nats-srv 5 | spec: 6 | selector: 7 | app: nats 8 | ports: 9 | - name: tcp-client 10 | protocol: TCP 11 | port: 4222 12 | targetPort: 4222 13 | - name: tcp-monitoring 14 | protocol: TCP 15 | port: 8222 16 | targetPort: 8222 17 | type: ClusterIP 18 | 19 | -------------------------------------------------------------------------------- /gitops/Frontend/Frontend-depl.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: glotixz-frontend 5 | labels: 6 | type: stage-depl 7 | svcname: frontend-svc 8 | version: v1 9 | deployment: bluedeployment 10 | spec: 11 | replicas: 1 12 | selector: 13 | matchLabels: 14 | app: glotixz-frontend 15 | version: v1 16 | template: 17 | metadata: 18 | labels: 19 | app: glotixz-frontend 20 | version: v1 21 | spec: 22 | serviceAccountName: glotixz-frontend 23 | containers: 24 | - name: glotixz-frontend 25 | image: quay.io/ultimatestack/frontend:blue 26 | imagePullPolicy: Always 27 | ports: 28 | - containerPort: 3001 29 | env: 30 | - name: NEXT_PUBLIC_BASEURL 31 | value: 'e20b4706-9ba3-4496-a857-b8b531dd5a38.k8s.civo.com' 32 | - name: NEXT_PUBLIC_STRIPE_KEY 33 | value: 'pk_test_51IWeSrSGJ6cJaAGg6Be4Ty2WnNfEwASS11HO2syVLoAJGesykRVi9K19rzTjTBUBySdezIcFwHzDLyp2oX4BIssn007PlgW6Za' 34 | -------------------------------------------------------------------------------- /gitops/Frontend/Frontend-svc.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: frontend-srv 5 | labels: 6 | app: glotixz-frontend 7 | service: frontend 8 | spec: 9 | selector: 10 | app: glotixz-frontend 11 | ports: 12 | - name: http-frontend 13 | protocol: TCP 14 | port: 3001 15 | targetPort: 3001 16 | type: ClusterIP 17 | 18 | -------------------------------------------------------------------------------- /gitops/Frontend/frontend-istioingressgateway.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: networking.istio.io/v1beta1 2 | kind: Gateway 3 | metadata: 4 | name: glotixz-frontend-gateway 5 | spec: 6 | selector: 7 | istio: ingressgateway # use istio default controller 8 | servers: 9 | - port: 10 | number: 80 11 | name: http 12 | protocol: HTTP 13 | hosts: 14 | - "*" -------------------------------------------------------------------------------- /gitops/Frontend/frontend-istiovirtualservice.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: networking.istio.io/v1beta1 2 | kind: VirtualService 3 | metadata: 4 | name: vs-frontend-service 5 | spec: 6 | hosts: 7 | - "*" 8 | gateways: 9 | - glotixz-frontend-gateway 10 | http: 11 | - match: 12 | - uri: 13 | prefix: / 14 | route: 15 | - destination: 16 | host: frontend-srv 17 | port: 18 | number: 3001 19 | -------------------------------------------------------------------------------- /gitops/Frontend/frontend-sa.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ServiceAccount 3 | metadata: 4 | name: glotixz-frontend 5 | labels: 6 | account: frontendsrv 7 | type: depl -------------------------------------------------------------------------------- /gitops/IstioIngressGateway/authsrv-istioingressgateway.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: networking.istio.io/v1beta1 2 | kind: Gateway 3 | metadata: 4 | name: glotixz-authsrv-gateway 5 | spec: 6 | selector: 7 | istio: ingressgateway # use istio default controller 8 | servers: 9 | - port: 10 | number: 80 11 | name: http 12 | protocol: HTTP 13 | hosts: 14 | - "*" -------------------------------------------------------------------------------- /gitops/IstioIngressGateway/ordersrv-istioingressgateway.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: networking.istio.io/v1beta1 2 | kind: Gateway 3 | metadata: 4 | name: glotixz-ordersrv-gateway 5 | spec: 6 | selector: 7 | istio: ingressgateway # use istio default controller 8 | servers: 9 | - port: 10 | number: 80 11 | name: http 12 | protocol: HTTP 13 | hosts: 14 | - "*" -------------------------------------------------------------------------------- /gitops/IstioIngressGateway/paymentsrv-istioingressgateway.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: networking.istio.io/v1beta1 2 | kind: Gateway 3 | metadata: 4 | name: glotixz-paymentsrv-gateway 5 | spec: 6 | selector: 7 | istio: ingressgateway # use istio default controller 8 | servers: 9 | - port: 10 | number: 80 11 | name: http 12 | protocol: HTTP 13 | hosts: 14 | - "*" -------------------------------------------------------------------------------- /gitops/IstioIngressGateway/ticketsrv-istioingressgateway.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: networking.istio.io/v1beta1 2 | kind: Gateway 3 | metadata: 4 | name: glotixz-ticketsrv-gateway 5 | spec: 6 | selector: 7 | istio: ingressgateway # use istio default controller 8 | servers: 9 | - port: 10 | number: 80 11 | name: http 12 | protocol: HTTP 13 | hosts: 14 | - "*" -------------------------------------------------------------------------------- /gitops/IstioVirtualService/authsrv-istiovirtualservice.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: networking.istio.io/v1beta1 2 | kind: VirtualService 3 | metadata: 4 | name: vs-auth-service 5 | spec: 6 | hosts: 7 | - "*" 8 | gateways: 9 | - glotixz-authsrv-gateway 10 | http: 11 | - match: 12 | - uri: 13 | prefix: /api/users 14 | - uri: 15 | prefix: /api/auth 16 | route: 17 | - destination: 18 | host: auth-srv 19 | port: 20 | number: 4000 21 | -------------------------------------------------------------------------------- /gitops/IstioVirtualService/ordersrv-istiovirtualservice.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: networking.istio.io/v1beta1 2 | kind: VirtualService 3 | metadata: 4 | name: vs-orders-service 5 | spec: 6 | hosts: 7 | - "*" 8 | gateways: 9 | - glotixz-ordersrv-gateway 10 | http: 11 | - match: 12 | - uri: 13 | prefix: /api/orders 14 | route: 15 | - destination: 16 | host: order-srv 17 | port: 18 | number: 4002 19 | -------------------------------------------------------------------------------- /gitops/IstioVirtualService/paymentsrv-istiovirtualservice.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: networking.istio.io/v1beta1 2 | kind: VirtualService 3 | metadata: 4 | name: vs-payments-service 5 | spec: 6 | hosts: 7 | - "*" 8 | gateways: 9 | - glotixz-paymentsrv-gateway 10 | http: 11 | - match: 12 | - uri: 13 | prefix: /api/payments 14 | route: 15 | - destination: 16 | host: payments-srv 17 | port: 18 | number: 4004 19 | -------------------------------------------------------------------------------- /gitops/IstioVirtualService/ticketsrv-istiovirtualservice.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: networking.istio.io/v1beta1 2 | kind: VirtualService 3 | metadata: 4 | name: vs-ticket-service 5 | spec: 6 | hosts: 7 | - "*" 8 | gateways: 9 | - glotixz-ticketsrv-gateway 10 | http: 11 | - match: 12 | - uri: 13 | prefix: /api/tickets 14 | route: 15 | - destination: 16 | host: tickets-srv 17 | port: 18 | number: 4001 -------------------------------------------------------------------------------- /gitops/configmap/host-cm.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ConfigMap 3 | metadata: 4 | name: host-config 5 | data: 6 | ALLOWED_HOSTS: http://bluedeployment.glotixz.e20b4706-9ba3-4496-a857-b8b531dd5a38.k8s.civo.com,http://localhost:3001,http://127.0.0.1,http://e20b4706-9ba3-4496-a857-b8b531dd5a38.k8s.civo.com 7 | -------------------------------------------------------------------------------- /gitops/sealed-secrets/sealed-jwt-secret.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: bitnami.com/v1alpha1 2 | kind: SealedSecret 3 | metadata: 4 | annotations: 5 | sealedsecrets.bitnami.com/namespace-wide: "true" 6 | creationTimestamp: null 7 | name: jwt-secret 8 | namespace: glotixz-backend 9 | spec: 10 | encryptedData: 11 | JWT_KEY: AgC+9qfreo9wN5WVR0OZa05WIMGCZoFFNjZ3zgM9qAY6ARqK/E6R8NLer+AWNXL9WDjjJdpsYYWpW+SSJTHPuoBe/fXIEQC8rEA4QfGgBgdJzhL8VlyIr8GqJEwu92ZQ7Sl0JK63SyxbXmSiPBd63Chpjzk8lkSUI+EPn5AeZ+uHUz8feaZ4rAHEfVNkelGKlEINnDr9iw3CqvbEseS90w3sT7lHgAeMi5ubTre2M+0AIfMy0QbkREVOAqJcTzJIC2R5MdYZNVzm2Oe2MNon8K3lwSt+rFhZBMPNm2M+yPQtITy6CEbfyrRlCFoXNS4AZkqnxAp+dKbc4FIMLss5CtJxyahgJ8GMmPYDIT5g27+T4UIo84gwLP9WbvzFsGtRHDt9AAqZ8PUIQUmckqOtSLaH209n39VpwuKY92PXsWkePD5NWSERlAKOWxl5ZpvFKqk4aUTOraRulX5QbMqHGcbHtn4JLubQ9SC8nT9yRYZnObqHpfOpGe9F+vlnQ8h7J25byJap46/YFjxghTLM3IMPP0OI7iitc0Rpm9hGJU4FgE9gLDNRZdmAtV4vXn7ICG0acq2AE99uhKrUL8HcII/ssOD1TwwXJrJSxlMxmouoAmrq9Xcw6JJv7DqOxcET2FFNhgLKmnmetPFuRXz/oTtkeCYdpSVwNdCp+zDmk3as0v/sa6EoqyAVDHY4ax3L7SAucb62q+AtrfxnMvIlE25tQv4= 12 | template: 13 | metadata: 14 | annotations: 15 | sealedsecrets.bitnami.com/namespace-wide: "true" 16 | creationTimestamp: null 17 | name: jwt-secret 18 | namespace: glotixz-backend 19 | 20 | -------------------------------------------------------------------------------- /gitops/sealed-secrets/sealed-stripekey-secret.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: bitnami.com/v1alpha1 2 | kind: SealedSecret 3 | metadata: 4 | annotations: 5 | sealedsecrets.bitnami.com/namespace-wide: "true" 6 | creationTimestamp: null 7 | name: stripe-secret 8 | namespace: glotixz-backend 9 | spec: 10 | encryptedData: 11 | STRIPE_KEY: AgCJFkhQQc4gfadTjbbRkFhbcqjZ19i37XyExFfZaTG3qrsSJa7zy9vcnNIV61if5MkGLAUBpnWBrhCtexWgzmESLOYxGTUMiK08PjAYG7dTOcxFheGdRQJOGe+W8vkLZPd2eWeNVavNXMLHtQUqPmuribxyZ07DzBx0WtIrHuQHIDvrygAGfHnmC4ABfyEeuwW5lSHaqu4zOtek4w6NHXTWQSInWqpaHjIdqWP8rjsm/U9Jkj2MCzHcoHVHIxYp6MC2iK1q6gwdZrYcCJdEH4HsqvY64U/9/KC+gcW8yyOBqMKywZFNkhP9STXlIHCtni+2TPrDhTy2psJjKX4iLdHMdffNo2R/7UIiwH09pZN86CbSjJKctMr7D+GTOiYFMyDoh+k3IW4GR5Ky6vzRapVvmDjIKG3OYwhXRq8Xg6LrM1YGaH+c20eRrcIkqMoYUNd5Rwa/X4IUCNhDlYfZBwYsawnK89kfUN9ZneTtn3hFwpmslEKDI/l6qgdf7J4ixrvvjFPG/LMk3JEfiGLyfS8eEsTlCh++17sCXZdJFIHEVrUrMHRKYhEgECz7AiyKloEWNH2cmr6SeH34EEzR+XXkWxT7R8YAIvW0+EstsWaWta3Wee8xm7SWVmFsHfmoAPRDA0vePabpxdQrs4f3DZD9UIOoz+T7f3jGXWi4f3oOBzrFN5yEvPX243tT2GcmiQlhh1dUD0nsSiXqYzPJHRdT56SHPO6dc86bDNBtEM8R+5MMXh5I9s6kykLydk7O7uQtMv6aypnmFhEy9DypOEULGNPmv/9IzE7SbxhPJLN3KHfbZq5I6MqsuHUlX0D/sz0Az/a/wZSmNlQktg== 12 | template: 13 | metadata: 14 | annotations: 15 | sealedsecrets.bitnami.com/namespace-wide: "true" 16 | creationTimestamp: null 17 | name: stripe-secret 18 | namespace: glotixz-backend 19 | 20 | -------------------------------------------------------------------------------- /kubernetes/dev/README.md: -------------------------------------------------------------------------------- 1 | ## Dev k8s yamls 2 | 3 | This folder holds yamls files which represent the various used in various of development & test steps. The yamls under this folder are by means not suitable for production environments 4 | 5 | ### files under [dev-dbs]('./dev-dbs') folder 6 | they are stateless DB deployments which are using `Ephemeral storage` pod and they are not presistent only good in development/test environments 7 | 8 | #### testing out istio and using istioingressgateway 9 | 10 | 11 | Installing isito with `default` profile in the dev cluster. 12 | 13 | we can dump the profile and see what are the options provided before installing. 14 | 15 | ```bash 16 | istioctl profile dump default 17 | ``` 18 | 19 | installing: 20 | 21 | ```bash 22 | $ > istioctl install --set profile=default 23 | ``` 24 | 25 | ##### (creating manifest)[https://istio.io/latest/docs/setup/install/istioctl/#generate-a-manifest-before-installation] 26 | ```bash 27 | $ > istioctl manifest generate --set profile=default > istio-install-manifest.yaml 28 | 29 | ``` 30 | 31 | 32 | istio sidecar auto injection enabled by labeling the namespace by adding `istio-injection=enabled` label in the yaml 33 | 34 | or with kubectl 35 | 36 | ```bash 37 | kubectl label ns msc-dev istio-injection=enabled 38 | ``` 39 | 40 | ### config map 41 | 42 | A config map called `host-config` is used to deliver whitelisted/approved domain names to apply CORS on the server. 43 | 44 | 45 | 46 | > 📚 NOTE: Most of the dev work on v1-beta is done. ready to push towards staging -------------------------------------------------------------------------------- /kubernetes/dev/dev-dbs/auth-mongo-depl.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: auth-mongo-depl 5 | labels: 6 | db-type: dev 7 | spec: 8 | replicas: 1 9 | selector: 10 | matchLabels: 11 | app: auth-mongo 12 | template: 13 | metadata: 14 | labels: 15 | app: auth-mongo 16 | spec: 17 | containers: 18 | - name: auth-mongo 19 | image: mongo 20 | --- 21 | apiVersion: v1 22 | kind: Service 23 | metadata: 24 | name: auth-mongo-srv 25 | spec: 26 | selector: 27 | app: auth-mongo 28 | ports: 29 | - name: mongo-db 30 | protocol: TCP 31 | port: 27017 32 | targetPort: 27017 33 | type: ClusterIP 34 | -------------------------------------------------------------------------------- /kubernetes/dev/dev-dbs/expiration-redis-depl.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: expiration-redis-depl 5 | labels: 6 | db-type: dev 7 | spec: 8 | replicas: 1 9 | selector: 10 | matchLabels: 11 | app: expiration-redis 12 | template: 13 | metadata: 14 | labels: 15 | app: expiration-redis 16 | spec: 17 | containers: 18 | - name: expiration-redis 19 | image: redis 20 | --- 21 | apiVersion: v1 22 | kind: Service 23 | metadata: 24 | name: expiration-redis-srv 25 | spec: 26 | selector: 27 | app: expiration-redis 28 | ports: 29 | - name: db 30 | protocol: TCP 31 | port: 6379 32 | targetPort: 6379 33 | type: ClusterIP 34 | -------------------------------------------------------------------------------- /kubernetes/dev/dev-dbs/orders-mongo-depl.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: orders-mongo-depl 5 | labels: 6 | db-type: dev 7 | spec: 8 | replicas: 1 9 | selector: 10 | matchLabels: 11 | app: orders-mongo 12 | template: 13 | metadata: 14 | labels: 15 | app: orders-mongo 16 | spec: 17 | containers: 18 | - name: orders-mongo 19 | image: mongo 20 | --- 21 | apiVersion: v1 22 | kind: Service 23 | metadata: 24 | name: orders-mongo-srv 25 | spec: 26 | selector: 27 | app: orders-mongo 28 | ports: 29 | - name: mongo-db 30 | protocol: TCP 31 | port: 27017 32 | targetPort: 27017 33 | type: ClusterIP 34 | -------------------------------------------------------------------------------- /kubernetes/dev/dev-dbs/payments-mongo-depl.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: payments-mongo-depl 5 | labels: 6 | db-type: dev 7 | spec: 8 | replicas: 1 9 | selector: 10 | matchLabels: 11 | app: payments-mongo 12 | template: 13 | metadata: 14 | labels: 15 | app: payments-mongo 16 | spec: 17 | containers: 18 | - name: payments-mongo 19 | image: mongo 20 | --- 21 | apiVersion: v1 22 | kind: Service 23 | metadata: 24 | name: payments-mongo-srv 25 | spec: 26 | selector: 27 | app: payments-mongo 28 | ports: 29 | - name: mongo-db 30 | protocol: TCP 31 | port: 27017 32 | targetPort: 27017 33 | type: ClusterIP 34 | -------------------------------------------------------------------------------- /kubernetes/dev/dev-dbs/tickets-mongo-depl.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: tickets-mongo-depl 5 | labels: 6 | db-type: dev 7 | spec: 8 | replicas: 1 9 | selector: 10 | matchLabels: 11 | app: tickets-mongo 12 | template: 13 | metadata: 14 | labels: 15 | app: tickets-mongo 16 | # annotations: 17 | # sidecar.istio.io/inject: "false" 18 | spec: 19 | containers: 20 | - name: tickets-mongo 21 | image: mongo 22 | --- 23 | apiVersion: v1 24 | kind: Service 25 | metadata: 26 | name: tickets-mongo-srv 27 | spec: 28 | selector: 29 | app: tickets-mongo 30 | ports: 31 | - name: mongo-db 32 | protocol: TCP 33 | port: 27017 34 | targetPort: 27017 35 | type: ClusterIP 36 | -------------------------------------------------------------------------------- /kubernetes/dev/eventbus/nats-depl.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: nats-deployment 5 | labels: 6 | dkind: eventbus 7 | spec: 8 | replicas: 1 9 | selector: 10 | matchLabels: 11 | app: nats 12 | template: 13 | metadata: 14 | labels: 15 | app: nats 16 | # annotations: 17 | # sidecar.istio.io/inject: "false" 18 | spec: 19 | containers: 20 | - name: nats 21 | image: nats-streaming:0.21.1 22 | args: 23 | [ 24 | "-p", 25 | "4222", 26 | "-m", 27 | "8222", 28 | "-hbi", 29 | "5s", 30 | "-hbt", 31 | "5s", 32 | "-hbf", 33 | "2", 34 | "-SD", 35 | "-cid", 36 | "ticketing", 37 | ] 38 | 39 | --- 40 | apiVersion: v1 41 | kind: Service 42 | metadata: 43 | name: nats-srv 44 | spec: 45 | selector: 46 | app: nats 47 | ports: 48 | - name: tcp-client 49 | protocol: TCP 50 | port: 4222 51 | targetPort: 4222 52 | - name: tcp-monitoring 53 | protocol: TCP 54 | port: 8222 55 | targetPort: 8222 56 | type: ClusterIP 57 | 58 | -------------------------------------------------------------------------------- /kubernetes/dev/istio/ingress-virtualservice/README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | ['./destination-config.yaml']('./destination-config.yaml') and ['./msc-gateway.yaml']('./msc-gateway.yaml') are not applied in the dev cluster. 5 | 6 | 7 | More istio mTLS related steps on deployment level till todo. 8 | 9 | few more hiccups, 10 | 11 | Kiali dashboard throws `KIA1106` error, --> needs to be resolved 12 | 13 | prometheus , kiali, grafana and jaeger are added istio namespace to monitor and observe the pods 14 | 15 | -------------------------------------------------------------------------------- /kubernetes/dev/istio/ingress-virtualservice/destination-config.yaml: -------------------------------------------------------------------------------- 1 | # apiVersion: networking.istio.io/v1beta1 2 | # kind: DestinationRule 3 | # metadata: 4 | # name: ticket-des 5 | # spec: 6 | # host: tickets-srv.msc-dev.svc.cluster.local 7 | # trafficPolicy: 8 | # tls: 9 | # mode: ISTIO_MUTUAL 10 | -------------------------------------------------------------------------------- /kubernetes/dev/istio/install/peerauth.yaml: -------------------------------------------------------------------------------- 1 | # apiVersion: "security.istio.io/v1beta1" 2 | # kind: "PeerAuthentication" 3 | # metadata: 4 | # name: "default" 5 | # namespace: "istio-system" 6 | # spec: 7 | # mtls: 8 | # mode: DISABLE 9 | 10 | # --- 11 | 12 | # apiVersion: "security.istio.io/v1beta1" 13 | # kind: "PeerAuthentication" 14 | # metadata: 15 | # name: "custom-ds-ns" 16 | # namespace: "msc-istio" 17 | # spec: 18 | # mtls: 19 | # mode: DISABLE 20 | -------------------------------------------------------------------------------- /kubernetes/dev/mcs/auth-depl.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: auth-depl 5 | labels: 6 | type: dev-depl 7 | svcname: auth-svc 8 | version: v1 9 | spec: 10 | replicas: 1 11 | selector: 12 | matchLabels: 13 | app: auth 14 | version: v1 15 | template: 16 | metadata: 17 | labels: 18 | app: auth 19 | version: v1 20 | spec: 21 | containers: 22 | - name: auth 23 | image: quay.io/ultimatestack/auth-svc:v1.2-beta 24 | imagePullPolicy: Always 25 | ports: 26 | - containerPort: 4000 27 | env: 28 | - name: AUTH_MONGO_DB_URI 29 | value: 'mongodb://auth-mongo-srv:27017/auth' 30 | - name: JWTSECRET 31 | valueFrom: 32 | secretKeyRef: 33 | name: jwt-secret 34 | key: JWT_KEY 35 | envFrom: 36 | - configMapRef: 37 | name: host-config 38 | --- 39 | apiVersion: v1 40 | kind: Service 41 | metadata: 42 | name: auth-srv 43 | labels: 44 | app: auth 45 | service: auth 46 | spec: 47 | selector: 48 | app: auth 49 | ports: 50 | - name: http-auth 51 | protocol: TCP 52 | port: 4000 53 | targetPort: 4000 54 | # type: LoadBalancer 55 | type: ClusterIP 56 | 57 | -------------------------------------------------------------------------------- /kubernetes/dev/mcs/expiration-depl.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: expiration-depl 5 | labels: 6 | type: dev-depl 7 | svcname: expiration-svc 8 | version: v1 9 | spec: 10 | replicas: 1 11 | selector: 12 | matchLabels: 13 | app: expiration 14 | version: v1 15 | template: 16 | metadata: 17 | labels: 18 | app: expiration 19 | version: v1 20 | spec: 21 | containers: 22 | - name: expiration 23 | image: quay.io/ultimatestack/expiration-svc:v1-beta 24 | env: 25 | - name: NATS_CLIENT_ID 26 | valueFrom: 27 | fieldRef: 28 | fieldPath: metadata.name 29 | - name: NATS_URL 30 | value: "http://nats-srv:4222" 31 | - name: NATS_CLUSTER_ID 32 | value: ticketing 33 | - name: REDIS_HOST 34 | value: expiration-redis-srv 35 | -------------------------------------------------------------------------------- /kubernetes/dev/mcs/frontend-depl.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: frontend-depl 5 | labels: 6 | type: dev-depl 7 | svcname: frontend-svc 8 | version: v1 9 | spec: 10 | replicas: 1 11 | selector: 12 | matchLabels: 13 | app: frontend 14 | version: v1 15 | template: 16 | metadata: 17 | labels: 18 | app: frontend 19 | version: v1 20 | spec: 21 | containers: 22 | - name: frontend 23 | image: quay.io/ultimatestack/frontend 24 | imagePullPolicy: Always 25 | ports: 26 | - containerPort: 3001 27 | env: 28 | - name: NEXT_PUBLIC_BASEURL 29 | value: '437b59f7-2960-4267-b2e0-7124e5d3df04.k8s.civo.com' 30 | - name: NEXT_PUBLIC_STRIPE_KEY 31 | value: 'pk_test_51IWeSrSGJ6cJaAGg6Be4Ty2WnNfEwASS11HO2syVLoAJGesykRVi9K19rzTjTBUBySdezIcFwHzDLyp2oX4BIssn007PlgW6Za' 32 | 33 | --- 34 | apiVersion: v1 35 | kind: Service 36 | metadata: 37 | name: frontend-srv 38 | labels: 39 | app: frontend 40 | service: frontend 41 | spec: 42 | selector: 43 | app: frontend 44 | ports: 45 | - name: http-frontend 46 | protocol: TCP 47 | port: 3001 48 | targetPort: 3001 49 | # type: LoadBalancer 50 | type: ClusterIP 51 | 52 | -------------------------------------------------------------------------------- /kubernetes/dev/mcs/host-cm.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ConfigMap 3 | metadata: 4 | name: host-config 5 | 6 | data: 7 | ALLOWED_HOSTS: http://localhost:3001,http://127.0.0.1,http://437b59f7-2960-4267-b2e0-7124e5d3df04.k8s.civo.com -------------------------------------------------------------------------------- /kubernetes/dev/mcs/orders-depl.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: orders-depl 5 | labels: 6 | type: dev-depl 7 | svcname: orders-svc 8 | version: v1 9 | spec: 10 | replicas: 1 11 | selector: 12 | matchLabels: 13 | app: orders 14 | version: v1 15 | template: 16 | metadata: 17 | labels: 18 | app: orders 19 | version: v1 20 | spec: 21 | containers: 22 | - name: orders 23 | image: quay.io/ultimatestack/orders-svc:v1.2-beta 24 | imagePullPolicy: Always 25 | ports: 26 | - containerPort: 4002 27 | env: 28 | - name: NATS_CLIENT_ID 29 | valueFrom: 30 | fieldRef: 31 | fieldPath: metadata.name 32 | - name: NATS_URL 33 | value: 'http://nats-srv:4222' 34 | - name: NATS_CLUSTER_ID 35 | value: ticketing 36 | - name: ORDERS_MONGO_DB_URI 37 | value: 'mongodb://orders-mongo-srv:27017/orders' 38 | - name: JWTSECRET 39 | valueFrom: 40 | secretKeyRef: 41 | name: jwt-secret 42 | key: JWT_KEY 43 | envFrom: 44 | - configMapRef: 45 | name: host-config 46 | --- 47 | apiVersion: v1 48 | kind: Service 49 | metadata: 50 | name: orders-srv 51 | labels: 52 | app: orders 53 | service: orders 54 | spec: 55 | selector: 56 | app: orders 57 | ports: 58 | - name: http-orders 59 | protocol: TCP 60 | port: 4002 61 | targetPort: 4002 62 | # type: LoadBalancer 63 | type: ClusterIP 64 | 65 | -------------------------------------------------------------------------------- /kubernetes/dev/mcs/payments-depl.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: payments-depl 5 | labels: 6 | type: dev-depl 7 | svcname: payment-svc 8 | version: v1 9 | spec: 10 | replicas: 1 11 | selector: 12 | matchLabels: 13 | app: payments 14 | version: v1 15 | template: 16 | metadata: 17 | labels: 18 | app: payments 19 | version: v1 20 | spec: 21 | containers: 22 | - name: payments 23 | image: quay.io/ultimatestack/payment-svc:v1.2-beta 24 | imagePullPolicy: Always 25 | ports: 26 | - containerPort: 4004 27 | env: 28 | - name: NATS_CLIENT_ID 29 | valueFrom: 30 | fieldRef: 31 | fieldPath: metadata.name 32 | - name: NATS_URL 33 | value: 'http://nats-srv:4222' 34 | - name: NATS_CLUSTER_ID 35 | value: ticketing 36 | - name: PAYMENTS_MONGO_DB_URI 37 | value: 'mongodb://payments-mongo-srv:27017/payments' 38 | - name: JWTSECRET 39 | valueFrom: 40 | secretKeyRef: 41 | name: jwt-secret 42 | key: JWT_KEY 43 | - name: STRIPE_KEY 44 | valueFrom: 45 | secretKeyRef: 46 | name: stripe-secret 47 | key: STRIPE_KEY 48 | envFrom: 49 | - configMapRef: 50 | name: host-config 51 | --- 52 | apiVersion: v1 53 | kind: Service 54 | metadata: 55 | name: payments-srv 56 | labels: 57 | app: payments 58 | service: payments 59 | spec: 60 | selector: 61 | app: payments 62 | ports: 63 | - name: http-payments 64 | protocol: TCP 65 | port: 4004 66 | targetPort: 4004 67 | # type: LoadBalancer 68 | type: ClusterIP 69 | -------------------------------------------------------------------------------- /kubernetes/dev/mcs/tickets-depl.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: tickets-depl 5 | labels: 6 | type: dev-depl 7 | svcname: tickets-svc 8 | version: v1 9 | spec: 10 | replicas: 1 11 | selector: 12 | matchLabels: 13 | app: tickets 14 | version: v1 15 | template: 16 | metadata: 17 | labels: 18 | app: tickets 19 | version: v1 20 | spec: 21 | containers: 22 | - name: tickets 23 | image: quay.io/ultimatestack/ticket-svc:v1.2-beta 24 | imagePullPolicy: Always 25 | ports: 26 | - containerPort: 4001 27 | env: 28 | - name: NATS_CLIENT_ID 29 | valueFrom: 30 | fieldRef: 31 | fieldPath: metadata.name 32 | - name: NATS_URL 33 | value: "http://nats-srv:4222" 34 | - name: NATS_CLUSTER_ID 35 | value: ticketing 36 | - name: TICKETS_MONGO_DB_URI 37 | value: "mongodb://tickets-mongo-srv:27017/tickets" 38 | - name: JWTSECRET 39 | valueFrom: 40 | secretKeyRef: 41 | name: jwt-secret 42 | key: JWT_KEY 43 | envFrom: 44 | - configMapRef: 45 | name: host-config 46 | --- 47 | apiVersion: v1 48 | kind: Service 49 | metadata: 50 | name: tickets-srv 51 | labels: 52 | app: tickets 53 | service: tickets 54 | spec: 55 | selector: 56 | app: tickets 57 | ports: 58 | - name: http-tickets 59 | protocol: TCP 60 | port: 4001 61 | targetPort: 4001 62 | # type: LoadBalancer 63 | type: ClusterIP 64 | -------------------------------------------------------------------------------- /kubernetes/dev/namespace/mcs-istio.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Namespace 3 | metadata: 4 | name: msc-istio 5 | labels: 6 | istio-injection: enabled 7 | -------------------------------------------------------------------------------- /kubernetes/dev/namespace/ticket_mcs_dev_namespace.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Namespace 3 | metadata: 4 | name: msc-dev 5 | 6 | -------------------------------------------------------------------------------- /kubernetes/staging/cluster-setup/argocd-install/argocd-namespace.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Namespace 3 | metadata: 4 | name: argocd 5 | 6 | -------------------------------------------------------------------------------- /kubernetes/staging/gitops-setup/argocd-app-config.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: argoproj.io/v1alpha1 2 | kind: Application 3 | metadata: 4 | name: glotixz-app-backends-deploy 5 | namespace: argocd 6 | spec: 7 | project: default 8 | source: 9 | repoURL: https://github.com/narenarjun/ultimate-stack.git 10 | targetRevision: HEAD 11 | path: gitops 12 | directory: 13 | recurse: true 14 | destination: 15 | server: https://kubernetes.default.svc 16 | namespace: glotixz-backend 17 | syncPolicy: 18 | automated: 19 | prune: false 20 | selfHeal: true -------------------------------------------------------------------------------- /kubernetes/staging/namespace/ticketing-backend-ns.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Namespace 3 | metadata: 4 | name: glotixz-backend 5 | labels: 6 | istio-injection: enabled 7 | -------------------------------------------------------------------------------- /kubernetes/staging/namespace/ticketing-ns.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Namespace 3 | metadata: 4 | name: glotixz 5 | -------------------------------------------------------------------------------- /pictures/glotixz-app-argocd-deploy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nrndev/ultimate-stack/5b94132675a54490489d8f65757e70b60043d428/pictures/glotixz-app-argocd-deploy.png -------------------------------------------------------------------------------- /pictures/glotixz-grafana-overview.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nrndev/ultimate-stack/5b94132675a54490489d8f65757e70b60043d428/pictures/glotixz-grafana-overview.PNG -------------------------------------------------------------------------------- /pictures/glotixz-jaeger-overview.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nrndev/ultimate-stack/5b94132675a54490489d8f65757e70b60043d428/pictures/glotixz-jaeger-overview.PNG -------------------------------------------------------------------------------- /pictures/glotixz-jaeger-traces-overview.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nrndev/ultimate-stack/5b94132675a54490489d8f65757e70b60043d428/pictures/glotixz-jaeger-traces-overview.PNG -------------------------------------------------------------------------------- /pictures/glotixz-kiali-overview.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nrndev/ultimate-stack/5b94132675a54490489d8f65757e70b60043d428/pictures/glotixz-kiali-overview.PNG -------------------------------------------------------------------------------- /pictures/glotixz-longhorn.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nrndev/ultimate-stack/5b94132675a54490489d8f65757e70b60043d428/pictures/glotixz-longhorn.PNG -------------------------------------------------------------------------------- /pictures/glotixz-quay-repository.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nrndev/ultimate-stack/5b94132675a54490489d8f65757e70b60043d428/pictures/glotixz-quay-repository.PNG -------------------------------------------------------------------------------- /pictures/glotixz-swagger-spec.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nrndev/ultimate-stack/5b94132675a54490489d8f65757e70b60043d428/pictures/glotixz-swagger-spec.PNG -------------------------------------------------------------------------------- /pictures/nats-eventbus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nrndev/ultimate-stack/5b94132675a54490489d8f65757e70b60043d428/pictures/nats-eventbus.png -------------------------------------------------------------------------------- /pictures/ultimate-stack-overview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nrndev/ultimate-stack/5b94132675a54490489d8f65757e70b60043d428/pictures/ultimate-stack-overview.png --------------------------------------------------------------------------------