├── .gitignore ├── auth ├── .dockerignore ├── .gitignore ├── Dockerfile ├── src │ ├── routes │ │ ├── sign-out.ts │ │ ├── current-user.ts │ │ ├── __test__ │ │ │ ├── sign-out.test.ts │ │ │ ├── current-user.test.ts │ │ │ ├── sign-in.test.ts │ │ │ └── sign-up.test.ts │ │ ├── sign-up.ts │ │ └── sign-in.ts │ ├── helpers │ │ └── password.ts │ ├── index.ts │ ├── app.ts │ ├── test │ │ └── setup.ts │ └── models │ │ └── user.ts ├── package.json └── tsconfig.json ├── tickets ├── .dockerignore ├── .gitignore ├── Dockerfile ├── src │ ├── routes │ │ ├── index.ts │ │ ├── show.ts │ │ ├── __test__ │ │ │ ├── index.test.ts │ │ │ ├── show.test.ts │ │ │ ├── create.test.ts │ │ │ └── update.test.ts │ │ ├── create.ts │ │ └── update.ts │ ├── index.ts │ ├── app.ts │ ├── models │ │ └── ticket.ts │ └── test │ │ └── setup.ts ├── package.json └── tsconfig.json ├── ssl_error.png ├── ssl_after_type.png ├── .github └── workflows │ ├── tests-auth.yml │ ├── test-tickets.yml │ ├── codacy-analysis.yml │ └── codeql-analysis.yml ├── skaffold.yaml ├── infra └── k8s │ ├── auth-mongo-depl.yaml │ ├── tickets-mongo-depl.yaml │ ├── ingress-srv.yaml │ ├── auth-depl.yaml │ └── tickets-depl.yaml ├── LICENSE └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | common 3 | -------------------------------------------------------------------------------- /auth/.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /tickets/.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /auth/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | package-lock.json 3 | -------------------------------------------------------------------------------- /tickets/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | package-lock.json 3 | -------------------------------------------------------------------------------- /ssl_error.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/profile/ticket-app-microservice/main/ssl_error.png -------------------------------------------------------------------------------- /ssl_after_type.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/profile/ticket-app-microservice/main/ssl_after_type.png -------------------------------------------------------------------------------- /auth/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:alpine 2 | ENV CI=true 3 | 4 | WORKDIR /app 5 | COPY package.json . 6 | RUN npm install --only=prod 7 | COPY . . 8 | 9 | CMD ["npm", "start"] 10 | -------------------------------------------------------------------------------- /tickets/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:alpine 2 | ENV CI=true 3 | 4 | WORKDIR /app 5 | COPY package.json . 6 | RUN npm install --only=prod 7 | COPY . . 8 | 9 | CMD ["npm", "start"] 10 | -------------------------------------------------------------------------------- /.github/workflows/tests-auth.yml: -------------------------------------------------------------------------------- 1 | name: tests-auth 2 | on: [pull_request] 3 | jobs: 4 | build: 5 | runs-on: ubuntu-latest 6 | steps: 7 | - uses: actions/checkout@v2 8 | - run: cd auth && npm i && npm run test:ci 9 | -------------------------------------------------------------------------------- /.github/workflows/test-tickets.yml: -------------------------------------------------------------------------------- 1 | name: tests-tickets 2 | on: [pull_request] 3 | jobs: 4 | build: 5 | runs-on: ubuntu-latest 6 | steps: 7 | - uses: actions/checkout@v2 8 | - run: cd tickets && npm i && npm run test:ci 9 | -------------------------------------------------------------------------------- /auth/src/routes/sign-out.ts: -------------------------------------------------------------------------------- 1 | import { Router } from "express"; 2 | 3 | const router = Router(); 4 | 5 | router.post('/api/users/sign-out', (req, res) => { 6 | req.session = null; 7 | res.send({}); 8 | }); 9 | 10 | export { router as signOutRouter }; 11 | -------------------------------------------------------------------------------- /tickets/src/routes/index.ts: -------------------------------------------------------------------------------- 1 | import { Router, Request, Response } from 'express'; 2 | import { Ticket } from '../models/ticket'; 3 | 4 | const router = Router(); 5 | 6 | router.get('/api/tickets', async (req: Request, res: Response) => { 7 | const tickets = await Ticket.find({}); 8 | 9 | res.send(tickets); 10 | }); 11 | 12 | export { router as indexTicketRouter }; 13 | -------------------------------------------------------------------------------- /auth/src/helpers/password.ts: -------------------------------------------------------------------------------- 1 | import bcrypt from "bcrypt"; 2 | 3 | export class Password { 4 | static async toHash(password: string) { 5 | const saltRounds = 10; 6 | return bcrypt.hashSync(password, saltRounds); 7 | } 8 | 9 | static async compare(plainPassword: string, hashedPassword: string) { 10 | return bcrypt.compareSync(plainPassword, hashedPassword); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /auth/src/routes/current-user.ts: -------------------------------------------------------------------------------- 1 | import { currentUser } from "@mtickets/common"; 2 | import { Router } from "express"; 3 | 4 | const router = Router(); 5 | 6 | const middlewares = [ 7 | currentUser, 8 | ]; 9 | 10 | router.get('/api/users/current-user', ...middlewares, (req, res) => { 11 | res.send({ 12 | currentUser: req.currentUser || null 13 | }) 14 | }); 15 | 16 | export { router as currentUserRouter }; 17 | -------------------------------------------------------------------------------- /tickets/src/routes/show.ts: -------------------------------------------------------------------------------- 1 | import { NotFoundError } from '@mtickets/common'; 2 | import { Router, Request, Response } from 'express'; 3 | import { Ticket } from '../models/ticket'; 4 | 5 | const router = 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 | -------------------------------------------------------------------------------- /tickets/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: 'asldkf', 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 | 16 | const response = await request(app).get('/api/tickets').send().expect(200); 17 | 18 | expect(response.body.length).toEqual(3); 19 | }); 20 | -------------------------------------------------------------------------------- /skaffold.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: skaffold/v2alpha3 2 | kind: Config 3 | deploy: 4 | kubectl: 5 | manifests: 6 | - ./infra/k8s/* 7 | build: 8 | local: 9 | push: false 10 | artifacts: 11 | - image: muhammadrza/auth 12 | context: auth 13 | docker: 14 | dockerfile: Dockerfile 15 | sync: 16 | manual: 17 | - src: 'src/**/*.ts' 18 | dest: . 19 | - image: muhammadrza/tickets 20 | context: tickets 21 | docker: 22 | dockerfile: Dockerfile 23 | sync: 24 | manual: 25 | - src: 'src/**/*.ts' 26 | dest: . 27 | -------------------------------------------------------------------------------- /auth/src/routes/__test__/sign-out.test.ts: -------------------------------------------------------------------------------- 1 | import request from 'supertest'; 2 | import { app } from '../../app'; 3 | 4 | it('clears the cookie after signing out', async () => { 5 | await request(app) 6 | .post('/api/users/sign-up') 7 | .send({ 8 | email: 'test@test.com', 9 | password: 'password' 10 | }) 11 | .expect(201); 12 | 13 | const response = await request(app) 14 | .post('/api/users/sign-out') 15 | .send({}) 16 | .expect(200); 17 | 18 | expect(response.get('Set-Cookie')[0]).toEqual( 19 | 'session=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT; httponly' 20 | ); 21 | }); 22 | -------------------------------------------------------------------------------- /infra/k8s/auth-mongo-depl.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: auth-mongo-depl 5 | spec: 6 | replicas: 1 7 | selector: 8 | matchLabels: 9 | app: auth-mongo 10 | template: 11 | metadata: 12 | labels: 13 | app: auth-mongo 14 | spec: 15 | containers: 16 | - name: auth-mongo 17 | image: mongo 18 | --- 19 | apiVersion: v1 20 | kind: Service 21 | metadata: 22 | name: auth-mongo-srv 23 | spec: 24 | selector: 25 | app: auth-mongo 26 | ports: 27 | - name: db 28 | protocol: TCP 29 | port: 27017 30 | targetPort: 27017 31 | -------------------------------------------------------------------------------- /infra/k8s/tickets-mongo-depl.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: tickets-mongo-depl 5 | spec: 6 | replicas: 1 7 | selector: 8 | matchLabels: 9 | app: tickets-mongo 10 | template: 11 | metadata: 12 | labels: 13 | app: tickets-mongo 14 | spec: 15 | containers: 16 | - name: tickets-mongo 17 | image: mongo 18 | --- 19 | apiVersion: v1 20 | kind: Service 21 | metadata: 22 | name: tickets-mongo-srv 23 | spec: 24 | selector: 25 | app: tickets-mongo 26 | ports: 27 | - name: db 28 | protocol: TCP 29 | port: 27017 30 | targetPort: 27017 31 | -------------------------------------------------------------------------------- /auth/src/index.ts: -------------------------------------------------------------------------------- 1 | import mongoose from "mongoose"; 2 | 3 | import { app } from "./app"; 4 | 5 | const start = async () => { 6 | try { 7 | await mongoose.connect('mongodb://auth-mongo-srv', { 8 | useNewUrlParser: true, 9 | useUnifiedTopology: true, 10 | useCreateIndex: true 11 | }); 12 | console.log('Connected MongoDB'); 13 | } catch (e) { 14 | console.error(e); 15 | } 16 | 17 | app.listen(3000, () => { 18 | console.log('Listening on port 3000!!'); 19 | }); 20 | } 21 | 22 | if(!process.env.JWT_KEY) { 23 | throw Error('JWT_KEY must be defined'); 24 | } 25 | 26 | start(); 27 | -------------------------------------------------------------------------------- /tickets/src/index.ts: -------------------------------------------------------------------------------- 1 | import mongoose from "mongoose"; 2 | 3 | import { app } from "./app"; 4 | 5 | const start = async () => { 6 | try { 7 | await mongoose.connect('mongodb://tickets-mongo-srv', { 8 | useNewUrlParser: true, 9 | useUnifiedTopology: true, 10 | useCreateIndex: true 11 | }); 12 | console.log('Connected MongoDB'); 13 | } catch (e) { 14 | console.error(e); 15 | } 16 | 17 | app.listen(3000, () => { 18 | console.log('Listening on port 3000!!'); 19 | }); 20 | } 21 | 22 | if(!process.env.JWT_KEY) { 23 | throw Error('JWT_KEY must be defined'); 24 | } 25 | 26 | start(); 27 | -------------------------------------------------------------------------------- /auth/src/routes/__test__/current-user.test.ts: -------------------------------------------------------------------------------- 1 | import request from 'supertest'; 2 | import { app } from '../../app'; 3 | 4 | it('responds with details about the current user', async () => { 5 | const cookie = await global.signIn(); 6 | 7 | const response = await request(app) 8 | .get('/api/users/current-user') 9 | .set('Cookie', cookie) 10 | .send() 11 | .expect(200); 12 | 13 | expect(response.body.currentUser.email).toEqual('test@test.com'); 14 | }); 15 | 16 | it('responds with null if not authenticated', async () => { 17 | const response = await request(app) 18 | .get('/api/users/current-user') 19 | .send() 20 | .expect(200); 21 | 22 | expect(response.body.currentUser).toEqual(null); 23 | }); 24 | -------------------------------------------------------------------------------- /infra/k8s/ingress-srv.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: networking.k8s.io/v1 2 | kind: Ingress 3 | metadata: 4 | name: ingress-srv 5 | annotations: 6 | kubernetes.io/ingress.class: nginx 7 | nginx.ingress.kubernetes.io/use-regex: 'true' 8 | spec: 9 | rules: 10 | - host: ticket-app.dev 11 | http: 12 | paths: 13 | - path: /api/users/?(.*) 14 | pathType: Prefix 15 | backend: 16 | service: 17 | name: auth-srv 18 | port: 19 | number: 3000 20 | - path: /api/tickets/?(.*) 21 | pathType: Prefix 22 | backend: 23 | service: 24 | name: tickets-srv 25 | port: 26 | number: 3000 27 | -------------------------------------------------------------------------------- /infra/k8s/auth-depl.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: auth-depl 5 | spec: 6 | replicas: 1 7 | selector: 8 | matchLabels: 9 | app: auth 10 | template: 11 | metadata: 12 | labels: 13 | app: auth 14 | spec: 15 | containers: 16 | - name: auth 17 | image: muhammadrza/auth 18 | env: 19 | - name: JWT_KEY 20 | valueFrom: 21 | secretKeyRef: 22 | name: jwt-secret 23 | key: JWT_KEY 24 | --- 25 | apiVersion: v1 26 | kind: Service 27 | metadata: 28 | name: auth-srv 29 | spec: 30 | selector: 31 | app: auth 32 | ports: 33 | - name: auth 34 | protocol: TCP 35 | port: 3000 36 | targetPort: 3000 37 | -------------------------------------------------------------------------------- /infra/k8s/tickets-depl.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: tickets-depl 5 | spec: 6 | replicas: 1 7 | selector: 8 | matchLabels: 9 | app: tickets 10 | template: 11 | metadata: 12 | labels: 13 | app: tickets 14 | spec: 15 | containers: 16 | - name: tickets 17 | image: muhammadrza/tickets 18 | env: 19 | - name: JWT_KEY 20 | valueFrom: 21 | secretKeyRef: 22 | name: jwt-secret 23 | key: JWT_KEY 24 | --- 25 | apiVersion: v1 26 | kind: Service 27 | metadata: 28 | name: tickets-srv 29 | spec: 30 | selector: 31 | app: tickets 32 | ports: 33 | - name: tickets 34 | protocol: TCP 35 | port: 3000 36 | targetPort: 3000 37 | -------------------------------------------------------------------------------- /tickets/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 not found', async () => { 6 | const id = new mongoose.Types.ObjectId().toHexString(); 7 | 8 | await request(app).get(`/api/tickets/${id}`).send().expect(404); 9 | }); 10 | 11 | it('returns the ticket if the ticket is found', async () => { 12 | const title = 'concert'; 13 | const price = 20; 14 | 15 | const response = await request(app) 16 | .post('/api/tickets') 17 | .set('Cookie', global.signin()) 18 | .send({ 19 | title, 20 | price, 21 | }) 22 | .expect(201); 23 | 24 | const ticketResponse = await request(app) 25 | .get(`/api/tickets/${response.body.id}`) 26 | .send() 27 | .expect(200); 28 | 29 | expect(ticketResponse.body.title).toEqual(title); 30 | expect(ticketResponse.body.price).toEqual(price); 31 | }); 32 | -------------------------------------------------------------------------------- /tickets/src/routes/create.ts: -------------------------------------------------------------------------------- 1 | import { requireAuth, validateRequest } from '@mtickets/common'; 2 | import { Router, Request, Response } from 'express'; 3 | import { body } from 'express-validator'; 4 | import { Ticket } from '../models/ticket'; 5 | 6 | const router = Router(); 7 | 8 | const validationSchema = [ 9 | body('title').not().isEmpty().withMessage('Title is required'), 10 | body('price') 11 | .isFloat({ gt: 0 }) 12 | .withMessage('Price must be greater than 0'), 13 | ]; 14 | 15 | const middlewares = [ 16 | requireAuth, 17 | validationSchema, 18 | validateRequest 19 | ]; 20 | 21 | router.post('/api/tickets', ...middlewares, async (req: Request, res: Response) => { 22 | const { title, price } = req.body; 23 | 24 | const ticket = Ticket.build({ 25 | title, 26 | price, 27 | userId: req.currentUser!.id, 28 | }); 29 | await ticket.save(); 30 | 31 | res.status(201).send(ticket); 32 | } 33 | ); 34 | 35 | export { router as createTicketRouter }; 36 | -------------------------------------------------------------------------------- /auth/src/app.ts: -------------------------------------------------------------------------------- 1 | import { errorHandler, NotFoundError } from "@mtickets/common"; 2 | import "express-async-errors"; 3 | import express from "express"; 4 | import { json } from "body-parser"; 5 | import cookieSession from "cookie-session"; 6 | 7 | import { currentUserRouter } from "./routes/current-user"; 8 | import { signInRouter } from "./routes/sign-in"; 9 | import { signOutRouter } from "./routes/sign-out"; 10 | import { signUpRouter } from "./routes/sign-up"; 11 | 12 | const app = express(); 13 | 14 | app.set('trust proxy', true); 15 | 16 | app.use(json()); 17 | app.use(cookieSession({ 18 | name: 'session', 19 | signed: false, 20 | secure: process.env.NODE_ENV !== 'test', 21 | // Cookie Options 22 | maxAge: 24 * 60 * 60 * 1000 // 24 hours 23 | })); 24 | 25 | app.use(currentUserRouter); 26 | app.use(signInRouter); 27 | app.use(signOutRouter); 28 | app.use(signUpRouter); 29 | 30 | 31 | app.use("*", async () => { 32 | throw new NotFoundError(); 33 | }); 34 | 35 | app.use(errorHandler); 36 | 37 | export { app }; 38 | -------------------------------------------------------------------------------- /tickets/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 { errorHandler, NotFoundError, currentUser } from '@mtickets/common'; 6 | import { createTicketRouter } from './routes/create'; 7 | import { showTicketRouter } from './routes/show'; 8 | import { indexTicketRouter } from './routes/index'; 9 | import { updateTicketRouter } from './routes/update'; 10 | 11 | const app = express(); 12 | app.set('trust proxy', true); 13 | app.use(json()); 14 | app.use( 15 | cookieSession({ 16 | name: 'session', 17 | signed: false, 18 | secure: process.env.NODE_ENV !== 'test', 19 | // Cookie Options 20 | maxAge: 24 * 60 * 60 * 1000 // 24 hours 21 | }) 22 | ); 23 | app.use(currentUser); 24 | 25 | app.use(createTicketRouter); 26 | app.use(showTicketRouter); 27 | app.use(indexTicketRouter); 28 | app.use(updateTicketRouter); 29 | 30 | app.all('*', async (req, res) => { 31 | throw new NotFoundError(); 32 | }); 33 | 34 | app.use(errorHandler); 35 | 36 | export { app }; 37 | -------------------------------------------------------------------------------- /tickets/src/models/ticket.ts: -------------------------------------------------------------------------------- 1 | import mongoose from 'mongoose'; 2 | 3 | interface TicketAttrs { 4 | title: string; 5 | price: number; 6 | userId: string; 7 | } 8 | 9 | interface TicketDoc extends mongoose.Document { 10 | title: number; 11 | price: number; 12 | userId: string; 13 | } 14 | 15 | interface TicketModel extends mongoose.Model { 16 | build(attrs: TicketAttrs): TicketDoc; 17 | } 18 | 19 | const ticketSchema = new mongoose.Schema( 20 | { 21 | title: { 22 | type: String, 23 | required: true, 24 | }, 25 | price: { 26 | type: Number, 27 | required: true, 28 | }, 29 | userId: { 30 | type: String, 31 | required: true, 32 | }, 33 | }, 34 | { 35 | toJSON: { 36 | transform(doc, ret) { 37 | ret.id = ret._id; 38 | delete ret._id; 39 | delete ret.__v; 40 | }, 41 | }, 42 | } 43 | ); 44 | 45 | ticketSchema.statics.build = (attrs: TicketAttrs) => { 46 | return new Ticket(attrs); 47 | }; 48 | 49 | const Ticket = mongoose.model('Ticket', ticketSchema); 50 | 51 | export { Ticket }; 52 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Aslan Muhammad-Rza 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ticket-app-microservice 2 | 3 | ###### Tested Ubuntu 20.04.1 LTS (VM) 4 | 5 | #### 1. [Install docker](https://docs.docker.com/engine/install/ubuntu/) 6 | #### 1. [Install kubectl](https://kubernetes.io/docs/tasks/tools/install-kubectl/) 7 | #### 1. [Install minikube](https://minikube.sigs.k8s.io/docs/start/) 8 | #### 2. [Install skaffold](https://skaffold.dev/docs/install/) 9 | 10 | ## Start your cluster: 11 | #### Run `minikube start` 12 | #### Run `minikube addons enable ingress` 13 | 14 | ## Create jwt secret 15 | #### Run `kubectl create secret generic jwt-secret --from-literal=JWT_KEY=secret` 16 | 17 | ## Add ip for to redirect domain 18 | #### Run `minikube ip` // Example result: `192.168.49.2` 19 | #### Add `192.168.49.2 ticket-app.dev` in `/etc/hosts` 20 | 21 | 22 | ## Start skaffold 23 | #### Run `skaffold dev` 24 | 25 | ## If you see on the below error please type on the screen `thisisunsafe` 26 | ![Invalid ssl](./ssl_error.png) 27 | 28 | ## It should look like: 29 | ![Fixed ssl](./ssl_after_type.png) 30 | 31 | ## Get secret list (optional) 32 | ###### Run `kubectl get secrets` 33 | 34 | ###### Permission for docker `exec /usr/bin/sg docker newgrp $(id -gn)` 35 | -------------------------------------------------------------------------------- /tickets/src/routes/update.ts: -------------------------------------------------------------------------------- 1 | import { 2 | validateRequest, 3 | NotFoundError, 4 | requireAuth, 5 | NotAuthorizedError, 6 | } from '@mtickets/common'; 7 | import { Router, Request, Response } from 'express'; 8 | import { body } from 'express-validator'; 9 | import { Ticket } from '../models/ticket'; 10 | 11 | const router = Router(); 12 | 13 | const validationSchema = [ 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 | 20 | const middlewares = [ 21 | requireAuth, 22 | validationSchema, 23 | validateRequest, 24 | ]; 25 | 26 | router.put('/api/tickets/:id', ...middlewares, async (req: Request, res: Response) => { 27 | const ticket = await Ticket.findById(req.params.id); 28 | 29 | if (!ticket) { 30 | throw new NotFoundError(); 31 | } 32 | 33 | if (ticket.userId !== req.currentUser!.id) { 34 | throw new NotAuthorizedError(); 35 | } 36 | 37 | ticket.set({ 38 | title: req.body.title, 39 | price: req.body.price, 40 | }); 41 | await ticket.save(); 42 | 43 | res.send(ticket); 44 | } 45 | ); 46 | 47 | export { router as updateTicketRouter }; 48 | -------------------------------------------------------------------------------- /auth/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "auth", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "ts-node-dev --poll src/index.ts", 8 | "test": "jest --watchAll --no-cache", 9 | "test:ci": "jest" 10 | }, 11 | "jest": { 12 | "preset": "ts-jest", 13 | "testEnvironment": "node", 14 | "setupFilesAfterEnv": [ 15 | "./src/test/setup.ts" 16 | ] 17 | }, 18 | "dependencies": { 19 | "@mtickets/common": "^1.0.4", 20 | "@types/bcrypt": "^3.0.0", 21 | "@types/cookie-session": "^2.0.42", 22 | "@types/express": "^4.17.9", 23 | "@types/jsonwebtoken": "^8.5.0", 24 | "@types/mongoose": "^5.10.3", 25 | "bcrypt": "^5.0.0", 26 | "cookie-session": "^1.4.0", 27 | "express": "^4.17.1", 28 | "express-async-errors": "^3.1.1", 29 | "express-validator": "^6.9.0", 30 | "jsonwebtoken": "^8.5.1", 31 | "mongoose": "^5.11.9", 32 | "ts-node-dev": "^1.1.1", 33 | "typescript": "^4.1.3" 34 | }, 35 | "devDependencies": { 36 | "@types/jest": "^26.0.19", 37 | "@types/supertest": "^2.0.10", 38 | "jest": "^26.6.3", 39 | "mongodb-memory-server": "^6.9.2", 40 | "supertest": "^6.0.1", 41 | "ts-jest": "^26.4.4" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /tickets/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tickets", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "ts-node-dev --poll src/index.ts", 8 | "test": "jest --watchAll --no-cache", 9 | "test:ci": "jest" 10 | }, 11 | "jest": { 12 | "preset": "ts-jest", 13 | "testEnvironment": "node", 14 | "setupFilesAfterEnv": [ 15 | "./src/test/setup.ts" 16 | ] 17 | }, 18 | "dependencies": { 19 | "@mtickets/common": "^1.0.4", 20 | "@types/bcrypt": "^3.0.0", 21 | "@types/cookie-session": "^2.0.42", 22 | "@types/express": "^4.17.9", 23 | "@types/jsonwebtoken": "^8.5.0", 24 | "@types/mongoose": "^5.10.3", 25 | "bcrypt": "^5.0.0", 26 | "cookie-session": "^1.4.0", 27 | "express": "^4.17.1", 28 | "express-async-errors": "^3.1.1", 29 | "express-validator": "^6.9.0", 30 | "jsonwebtoken": "^8.5.1", 31 | "mongoose": "^5.11.9", 32 | "ts-node-dev": "^1.1.1", 33 | "typescript": "^4.1.3" 34 | }, 35 | "devDependencies": { 36 | "@types/jest": "^26.0.19", 37 | "@types/supertest": "^2.0.10", 38 | "jest": "^26.6.3", 39 | "mongodb-memory-server": "^6.9.2", 40 | "supertest": "^6.0.1", 41 | "ts-jest": "^26.4.4" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /auth/src/routes/__test__/sign-in.test.ts: -------------------------------------------------------------------------------- 1 | import request from 'supertest'; 2 | import { app } from '../../app'; 3 | 4 | it('fails when a email that does not exist is supplied', async () => { 5 | await request(app) 6 | .post('/api/users/sign-in') 7 | .send({ 8 | email: 'test@test.com', 9 | password: 'password' 10 | }) 11 | .expect(400); 12 | }); 13 | 14 | it('fails when an incorrect password is supplied', async () => { 15 | await request(app) 16 | .post('/api/users/sign-up') 17 | .send({ 18 | email: 'test@test.com', 19 | password: 'password' 20 | }) 21 | .expect(201); 22 | 23 | await request(app) 24 | .post('/api/users/sign-in') 25 | .send({ 26 | email: 'test@test.com', 27 | password: '123' 28 | }) 29 | .expect(400); 30 | }); 31 | 32 | it('responds with a cookie when given valid credentials', async () => { 33 | await request(app) 34 | .post('/api/users/sign-up') 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/sign-in') 43 | .send({ 44 | email: 'test@test.com', 45 | password: 'password' 46 | }) 47 | .expect(200); 48 | 49 | expect(response.get('Set-Cookie')).toBeDefined(); 50 | }); 51 | -------------------------------------------------------------------------------- /tickets/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 | let mongo: any; 14 | beforeAll(async () => { 15 | process.env.JWT_KEY = 'secret'; 16 | 17 | mongo = new MongoMemoryServer(); 18 | const mongoUri = await mongo.getUri(); 19 | 20 | await mongoose.connect(mongoUri, { 21 | useNewUrlParser: true, 22 | useUnifiedTopology: true, 23 | }); 24 | }); 25 | 26 | beforeEach(async () => { 27 | const collections = await mongoose.connection.db.collections(); 28 | 29 | for (let collection of collections) { 30 | await collection.deleteMany({}); 31 | } 32 | }); 33 | 34 | afterAll(async () => { 35 | await mongo.stop(); 36 | await mongoose.connection.close(); 37 | }); 38 | 39 | global.signin = () => { 40 | // Build a JWT payload. { id, email } 41 | const payload = { 42 | id: new mongoose.Types.ObjectId().toHexString(), 43 | email: 'test@test.com', 44 | }; 45 | const token = jwt.sign(payload, process.env.JWT_KEY!); 46 | const session = { jwt: token }; 47 | const sessionJSON = JSON.stringify(session); 48 | const base64 = Buffer.from(sessionJSON).toString('base64'); 49 | 50 | return [`session=${base64}`]; 51 | }; 52 | -------------------------------------------------------------------------------- /auth/src/test/setup.ts: -------------------------------------------------------------------------------- 1 | import { MongoMemoryServer } from 'mongodb-memory-server'; 2 | import mongoose from 'mongoose'; 3 | import request from 'supertest'; 4 | 5 | import { app } from '../app'; 6 | 7 | declare global { 8 | namespace NodeJS { 9 | interface Global { 10 | signIn(): Promise; 11 | } 12 | } 13 | } 14 | 15 | let mongo: any; 16 | 17 | beforeAll(async () => { 18 | process.env.JWT_KEY = 'secret'; 19 | 20 | mongo = new MongoMemoryServer(); 21 | const mongoUri = await mongo.getUri(); 22 | 23 | await mongoose.connect(mongoUri, { 24 | useNewUrlParser: true, 25 | useUnifiedTopology: true 26 | }); 27 | }); 28 | 29 | beforeEach(async () => { 30 | 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 = async () => { 43 | const email = 'test@test.com'; 44 | const password = 'password'; 45 | 46 | const response = await request(app) 47 | .post('/api/users/sign-up') 48 | .send({ 49 | email, 50 | password 51 | }) 52 | .expect(201); 53 | 54 | const cookie = response.get('Set-Cookie'); 55 | 56 | return cookie; 57 | }; 58 | -------------------------------------------------------------------------------- /auth/src/models/user.ts: -------------------------------------------------------------------------------- 1 | import mongoose from "mongoose"; 2 | 3 | import { Password } from "../helpers/password"; 4 | 5 | /** 6 | * User attributes. 7 | */ 8 | interface IUserAttrs { 9 | email: string; 10 | password: string; 11 | } 12 | 13 | /** 14 | * User document. 15 | */ 16 | export interface IUserDocument extends IUserAttrs, mongoose.Document {} 17 | 18 | /** 19 | * User model. 20 | */ 21 | interface IUserModel extends mongoose.Model { 22 | build(attrs: IUserAttrs): IUserDocument; 23 | } 24 | 25 | const userSchema = new mongoose.Schema({ 26 | email: { 27 | type: String, 28 | required: true 29 | }, 30 | password: { 31 | type: String, 32 | required: true, 33 | } 34 | }, 35 | { 36 | toJSON: { 37 | transform: function (doc, ret) { 38 | ret.id = ret._id; 39 | delete ret._id; 40 | delete ret.password; 41 | delete ret.__v; 42 | } 43 | } 44 | } 45 | ); 46 | 47 | userSchema.pre('save', async function (done) { 48 | if(this.isModified('password')) { 49 | const hashedPassword = await Password.toHash(this.get('password')); 50 | this.set('password', hashedPassword); 51 | } 52 | done(); 53 | }); 54 | 55 | userSchema.statics.build = (attrs: IUserDocument) => { 56 | return new User(attrs); 57 | } 58 | 59 | const User = mongoose.model('User', userSchema); 60 | 61 | export { User }; 62 | -------------------------------------------------------------------------------- /auth/src/routes/sign-up.ts: -------------------------------------------------------------------------------- 1 | import { validateRequest, BadRequestError } from "@mtickets/common"; 2 | import { Router, Request, Response } from "express"; 3 | import { body } from "express-validator"; 4 | import jwt from "jsonwebtoken"; 5 | 6 | import { IUserDocument, User } from "../models/user"; 7 | 8 | const router = Router(); 9 | 10 | const validationSchema = [ 11 | body('email').isEmail().withMessage('Email must be valid'), 12 | body('password').trim() 13 | .isLength({ min: 4, max: 22}) 14 | .withMessage('Password must be between 4 and 22 characters') 15 | ]; 16 | 17 | const middlewares = [ 18 | validationSchema, 19 | validateRequest 20 | ]; 21 | 22 | router.post('/api/users/sign-up', ...middlewares, async (req: Request, res: Response) => { 23 | 24 | const { email, password } = req.body; 25 | const lowerCaseEmail = email.toLowerCase(); 26 | 27 | const existingUser: Array = await User.findOne({ email: lowerCaseEmail }).lean(); 28 | 29 | if(existingUser) { 30 | throw new BadRequestError('Email is use'); 31 | } 32 | 33 | const user = User.build({ email: lowerCaseEmail, password }); 34 | await user.save(); 35 | 36 | /** Generate web token. */ 37 | const userJwt = jwt.sign({ 38 | id: user._id, 39 | email: user.email 40 | }, process.env.JWT_KEY!); 41 | 42 | /** Store token on session object. */ 43 | req.session = { 44 | jwt: userJwt 45 | }; 46 | 47 | res.status(201).send(user) 48 | 49 | }); 50 | 51 | export { router as signUpRouter }; 52 | -------------------------------------------------------------------------------- /auth/src/routes/sign-in.ts: -------------------------------------------------------------------------------- 1 | import { validateRequest, BadRequestError } from "@mtickets/common"; 2 | import { Router, Request, Response } from "express"; 3 | import { body } from "express-validator"; 4 | import jwt from "jsonwebtoken"; 5 | 6 | import { User } from "../models/user"; 7 | import { Password } from "../helpers/password"; 8 | 9 | const router = Router(); 10 | 11 | const validationSchema = [ 12 | body('email').isEmail().withMessage('Email must be valid'), 13 | body('password').trim() 14 | .notEmpty() 15 | .withMessage('Password is required') 16 | ]; 17 | 18 | const middlewares = [ 19 | validationSchema, 20 | validateRequest 21 | ]; 22 | 23 | router.post('/api/users/sign-in', ...middlewares, async (req: Request, res: Response) => { 24 | const { email, password } = req.body; 25 | 26 | const existingUser = await User.findOne({ email }).lean(); 27 | if(!existingUser) { 28 | throw new BadRequestError('Invalid credentials'); 29 | } 30 | 31 | const isPasswordMatch = await Password.compare(password, existingUser.password); 32 | if(!isPasswordMatch) { 33 | throw new BadRequestError('Invalid credentials'); 34 | } 35 | 36 | /** Generate web token. */ 37 | const userJwt = jwt.sign({ 38 | id: existingUser._id, 39 | email: existingUser.email 40 | }, process.env.JWT_KEY!); 41 | 42 | /** Store token on session object. */ 43 | req.session = { 44 | jwt: userJwt 45 | }; 46 | 47 | res.status(200).send({ 48 | message: 'Success' 49 | }); 50 | 51 | }); 52 | 53 | export { router as signInRouter }; 54 | -------------------------------------------------------------------------------- /.github/workflows/codacy-analysis.yml: -------------------------------------------------------------------------------- 1 | # This workflow checks out code, performs a Codacy security scan 2 | # and integrates the results with the 3 | # GitHub Advanced Security code scanning feature. For more information on 4 | # the Codacy security scan action usage and parameters, see 5 | # https://github.com/codacy/codacy-analysis-cli-action. 6 | # For more information on Codacy Analysis CLI in general, see 7 | # https://github.com/codacy/codacy-analysis-cli. 8 | 9 | name: Codacy Security Scan 10 | 11 | on: 12 | push: 13 | branches: [ main ] 14 | pull_request: 15 | branches: [ main ] 16 | 17 | jobs: 18 | codacy-security-scan: 19 | name: Codacy Security Scan 20 | runs-on: ubuntu-latest 21 | steps: 22 | # Checkout the repository to the GitHub Actions runner 23 | - name: Checkout code 24 | uses: actions/checkout@v2 25 | 26 | # Execute Codacy Analysis CLI and generate a SARIF output with the security issues identified during the analysis 27 | - name: Run Codacy Analysis CLI 28 | uses: codacy/codacy-analysis-cli-action@1.1.0 29 | with: 30 | # Check https://github.com/codacy/codacy-analysis-cli#project-token to get your project token from your Codacy repository 31 | # You can also omit the token and run the tools that support default configurations 32 | project-token: ${{ secrets.CODACY_PROJECT_TOKEN }} 33 | verbose: true 34 | output: results.sarif 35 | format: sarif 36 | # Adjust severity of non-security issues 37 | gh-code-scanning-compat: true 38 | # Force 0 exit code to allow SARIF file generation 39 | # This will handover control about PR rejection to the GitHub side 40 | max-allowed-issues: 2147483647 41 | 42 | # Upload the SARIF file generated in the previous step 43 | - name: Upload SARIF results file 44 | uses: github/codeql-action/upload-sarif@v1 45 | with: 46 | sarif_file: results.sarif 47 | -------------------------------------------------------------------------------- /auth/src/routes/__test__/sign-up.test.ts: -------------------------------------------------------------------------------- 1 | import request from 'supertest'; 2 | import { app } from '../../app'; 3 | 4 | it('returns a 201 on successful signup', async () => { 5 | return request(app) 6 | .post('/api/users/sign-up') 7 | .send({ 8 | email: 'test@test.com', 9 | password: 'password' 10 | }) 11 | .expect(201); 12 | }); 13 | 14 | it('returns a 400 with an invalid email', async () => { 15 | return request(app) 16 | .post('/api/users/sign-up') 17 | .send({ 18 | email: 'abc', 19 | password: 'password' 20 | }) 21 | .expect(400); 22 | }); 23 | 24 | it('returns a 400 with an invalid password', async () => { 25 | return request(app) 26 | .post('/api/users/sign-up') 27 | .send({ 28 | email: 'abc', 29 | password: 'p' 30 | }) 31 | .expect(400); 32 | }); 33 | 34 | it('returns a 400 with missing email and password', async () => { 35 | await request(app) 36 | .post('/api/users/sign-up') 37 | .send({ 38 | email: 'test@test.com' 39 | }) 40 | .expect(400); 41 | 42 | await request(app) 43 | .post('/api/users/sign-up') 44 | .send({ 45 | password: '123' 46 | }) 47 | .expect(400); 48 | }); 49 | 50 | it('disallows duplicate emails', async () => { 51 | await request(app) 52 | .post('/api/users/sign-up') 53 | .send({ 54 | email: 'test@test.com', 55 | password: 'password' 56 | }) 57 | .expect(201); 58 | 59 | await request(app) 60 | .post('/api/users/sign-up') 61 | .send({ 62 | email: 'test@test.com', 63 | password: 'password' 64 | }) 65 | .expect(400); 66 | }); 67 | 68 | it('sets a cookie after successful signup', async () => { 69 | const response = await request(app) 70 | .post('/api/users/sign-up') 71 | .send({ 72 | email: 'test@test.com', 73 | password: 'password' 74 | }) 75 | .expect(201); 76 | 77 | expect(response.get('Set-Cookie')).toBeDefined(); 78 | }); 79 | -------------------------------------------------------------------------------- /tickets/src/routes/__test__/create.test.ts: -------------------------------------------------------------------------------- 1 | import request from 'supertest'; 2 | import { app } from '../../app'; 3 | import { Ticket } from '../../models/ticket'; 4 | 5 | it('has a route handler listening to /api/tickets for post requests', async () => { 6 | const response = await request(app).post('/api/tickets').send({}); 7 | 8 | expect(response.status).not.toEqual(404); 9 | }); 10 | 11 | it('can only be accessed if the user is signed in', async () => { 12 | await request(app).post('/api/tickets').send({}).expect(401); 13 | }); 14 | 15 | it('returns a status other than 401 if the user is signed in', async () => { 16 | const response = await request(app) 17 | .post('/api/tickets') 18 | .set('Cookie', global.signin()) 19 | .send({}); 20 | 21 | expect(response.status).not.toEqual(401); 22 | }); 23 | 24 | it('returns an error if an invalid title is provided', async () => { 25 | await request(app) 26 | .post('/api/tickets') 27 | .set('Cookie', global.signin()) 28 | .send({ 29 | title: '', 30 | price: 10, 31 | }) 32 | .expect(400); 33 | 34 | await request(app) 35 | .post('/api/tickets') 36 | .set('Cookie', global.signin()) 37 | .send({ 38 | price: 10, 39 | }) 40 | .expect(400); 41 | }); 42 | 43 | it('returns an error if an invalid price is provided', async () => { 44 | await request(app) 45 | .post('/api/tickets') 46 | .set('Cookie', global.signin()) 47 | .send({ 48 | title: 'asldkjf', 49 | price: -10, 50 | }) 51 | .expect(400); 52 | 53 | await request(app) 54 | .post('/api/tickets') 55 | .set('Cookie', global.signin()) 56 | .send({ 57 | title: 'laskdfj', 58 | }) 59 | .expect(400); 60 | }); 61 | 62 | it('creates a ticket with valid inputs', async () => { 63 | let tickets = await Ticket.find({}); 64 | expect(tickets.length).toEqual(0); 65 | 66 | const title = 'asldkfj'; 67 | 68 | await request(app) 69 | .post('/api/tickets') 70 | .set('Cookie', global.signin()) 71 | .send({ 72 | title, 73 | price: 20, 74 | }) 75 | .expect(201); 76 | 77 | tickets = await Ticket.find({}); 78 | expect(tickets.length).toEqual(1); 79 | expect(tickets[0].price).toEqual(20); 80 | expect(tickets[0].title).toEqual(title); 81 | }); 82 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ main ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ main ] 20 | schedule: 21 | - cron: '29 9 * * 1' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | 28 | strategy: 29 | fail-fast: false 30 | matrix: 31 | language: [ 'javascript' ] 32 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] 33 | # Learn more: 34 | # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed 35 | 36 | steps: 37 | - name: Checkout repository 38 | uses: actions/checkout@v2 39 | 40 | # Initializes the CodeQL tools for scanning. 41 | - name: Initialize CodeQL 42 | uses: github/codeql-action/init@v1 43 | with: 44 | languages: ${{ matrix.language }} 45 | # If you wish to specify custom queries, you can do so here or in a config file. 46 | # By default, queries listed here will override any specified in a config file. 47 | # Prefix the list here with "+" to use these queries and those in the config file. 48 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 49 | 50 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 51 | # If this step fails, then you should remove it and run the build manually (see below) 52 | - name: Autobuild 53 | uses: github/codeql-action/autobuild@v1 54 | 55 | # ℹ️ Command-line programs to run using the OS shell. 56 | # 📚 https://git.io/JvXDl 57 | 58 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 59 | # and modify them (or add more) to build your code if your project 60 | # uses a compiled language 61 | 62 | #- run: | 63 | # make bootstrap 64 | # make release 65 | 66 | - name: Perform CodeQL Analysis 67 | uses: github/codeql-action/analyze@v1 68 | -------------------------------------------------------------------------------- /tickets/src/routes/__test__/update.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 provided id does not exist', async () => { 6 | const id = new mongoose.Types.ObjectId().toHexString(); 7 | await request(app) 8 | .put(`/api/tickets/${id}`) 9 | .set('Cookie', global.signin()) 10 | .send({ 11 | title: 'aslkdfj', 12 | price: 20, 13 | }) 14 | .expect(404); 15 | }); 16 | 17 | it('returns a 401 if the user is not authenticated', async () => { 18 | const id = new mongoose.Types.ObjectId().toHexString(); 19 | await request(app) 20 | .put(`/api/tickets/${id}`) 21 | .send({ 22 | title: 'aslkdfj', 23 | price: 20, 24 | }) 25 | .expect(401); 26 | }); 27 | 28 | it('returns a 401 if the user does not own the ticket', async () => { 29 | const response = await request(app) 30 | .post('/api/tickets') 31 | .set('Cookie', global.signin()) 32 | .send({ 33 | title: 'asldkfj', 34 | price: 20, 35 | }); 36 | 37 | await request(app) 38 | .put(`/api/tickets/${response.body.id}`) 39 | .set('Cookie', global.signin()) 40 | .send({ 41 | title: 'alskdjflskjdf', 42 | price: 1000, 43 | }) 44 | .expect(401); 45 | }); 46 | 47 | it('returns a 400 if the user provides an invalid title or price', async () => { 48 | const cookie = global.signin(); 49 | 50 | const response = await request(app) 51 | .post('/api/tickets') 52 | .set('Cookie', cookie) 53 | .send({ 54 | title: 'asldkfj', 55 | price: 20, 56 | }); 57 | 58 | await request(app) 59 | .put(`/api/tickets/${response.body.id}`) 60 | .set('Cookie', cookie) 61 | .send({ 62 | title: '', 63 | price: 20, 64 | }) 65 | .expect(400); 66 | 67 | await request(app) 68 | .put(`/api/tickets/${response.body.id}`) 69 | .set('Cookie', cookie) 70 | .send({ 71 | title: 'alskdfjj', 72 | price: -10, 73 | }) 74 | .expect(400); 75 | }); 76 | 77 | it('updates the ticket provided valid inputs', async () => { 78 | const cookie = global.signin(); 79 | 80 | const response = await request(app) 81 | .post('/api/tickets') 82 | .set('Cookie', cookie) 83 | .send({ 84 | title: 'asldkfj', 85 | price: 20, 86 | }); 87 | 88 | await request(app) 89 | .put(`/api/tickets/${response.body.id}`) 90 | .set('Cookie', cookie) 91 | .send({ 92 | title: 'new title', 93 | price: 100, 94 | }) 95 | .expect(200); 96 | 97 | const ticketResponse = await request(app) 98 | .get(`/api/tickets/${response.body.id}`) 99 | .send(); 100 | 101 | expect(ticketResponse.body.title).toEqual('new title'); 102 | expect(ticketResponse.body.price).toEqual(100); 103 | }); 104 | -------------------------------------------------------------------------------- /auth/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Basic Options */ 4 | // "incremental": true, /* Enable incremental compilation */ 5 | "target": "es5", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */ 6 | "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */ 7 | // "lib": [], /* Specify library files to be included in the compilation. */ 8 | // "allowJs": true, /* Allow javascript files to be compiled. */ 9 | // "checkJs": true, /* Report errors in .js files. */ 10 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ 11 | // "declaration": true, /* Generates corresponding '.d.ts' file. */ 12 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ 13 | // "sourceMap": true, /* Generates corresponding '.map' file. */ 14 | // "outFile": "./", /* Concatenate and emit output to single file. */ 15 | // "outDir": "./", /* Redirect output structure to the directory. */ 16 | // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 17 | // "composite": true, /* Enable project compilation */ 18 | // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ 19 | // "removeComments": true, /* Do not emit comments to output. */ 20 | // "noEmit": true, /* Do not emit outputs. */ 21 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 22 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 23 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 24 | 25 | /* Strict Type-Checking Options */ 26 | "strict": true, /* Enable all strict type-checking options. */ 27 | // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ 28 | // "strictNullChecks": true, /* Enable strict null checks. */ 29 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */ 30 | // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ 31 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ 32 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 33 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 34 | 35 | /* Additional Checks */ 36 | // "noUnusedLocals": true, /* Report errors on unused locals. */ 37 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 38 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 39 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 40 | 41 | /* Module Resolution Options */ 42 | // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 43 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 44 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 45 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 46 | // "typeRoots": [], /* List of folders to include type definitions from. */ 47 | // "types": [], /* Type declaration files to be included in compilation. */ 48 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 49 | "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 50 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 51 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 52 | 53 | /* Source Map Options */ 54 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 55 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 56 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 57 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 58 | 59 | /* Experimental Options */ 60 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 61 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 62 | 63 | /* Advanced Options */ 64 | "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */ 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /tickets/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Basic Options */ 4 | // "incremental": true, /* Enable incremental compilation */ 5 | "target": "es5", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */ 6 | "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */ 7 | // "lib": [], /* Specify library files to be included in the compilation. */ 8 | // "allowJs": true, /* Allow javascript files to be compiled. */ 9 | // "checkJs": true, /* Report errors in .js files. */ 10 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ 11 | // "declaration": true, /* Generates corresponding '.d.ts' file. */ 12 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ 13 | // "sourceMap": true, /* Generates corresponding '.map' file. */ 14 | // "outFile": "./", /* Concatenate and emit output to single file. */ 15 | // "outDir": "./", /* Redirect output structure to the directory. */ 16 | // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 17 | // "composite": true, /* Enable project compilation */ 18 | // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ 19 | // "removeComments": true, /* Do not emit comments to output. */ 20 | // "noEmit": true, /* Do not emit outputs. */ 21 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 22 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 23 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 24 | 25 | /* Strict Type-Checking Options */ 26 | "strict": true, /* Enable all strict type-checking options. */ 27 | // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ 28 | // "strictNullChecks": true, /* Enable strict null checks. */ 29 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */ 30 | // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ 31 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ 32 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 33 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 34 | 35 | /* Additional Checks */ 36 | // "noUnusedLocals": true, /* Report errors on unused locals. */ 37 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 38 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 39 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 40 | 41 | /* Module Resolution Options */ 42 | // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 43 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 44 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 45 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 46 | // "typeRoots": [], /* List of folders to include type definitions from. */ 47 | // "types": [], /* Type declaration files to be included in compilation. */ 48 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 49 | "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 50 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 51 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 52 | 53 | /* Source Map Options */ 54 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 55 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 56 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 57 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 58 | 59 | /* Experimental Options */ 60 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 61 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 62 | 63 | /* Advanced Options */ 64 | "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */ 65 | } 66 | } 67 | --------------------------------------------------------------------------------