From 54a5d317ae16d3ad6cbc25cd5e820515a18b796c Mon Sep 17 00:00:00 2001 From: Tay Yi Hsuen Date: Thu, 26 Oct 2023 23:05:03 +0800 Subject: [PATCH] Create release 0.3.0 (#191) Add HTTPS support --------- Co-authored-by: Lee Chun Wei <47494777+chunweii@users.noreply.github.com> --- .../admin-service-deployment.yaml | 2 +- .../collaboration-service-deployment.yaml | 2 +- .../frontend-deployment.yaml | 2 +- .../gke-prod-manifests/frontend-ingress.yaml | 13 +++ .../gke-prod-manifests/frontend-service.yaml | 1 - .../gateway-deployment.yaml | 16 +++- .../gateway-http-ingress.yaml | 13 +++ .../gke-prod-manifests/gateway-service.yaml | 7 +- .../gateway-wscollaboration-ingress.yaml | 13 +++ .../gateway-wsmatch-ingress.yaml | 13 +++ .../gke-prod-manifests/gke-managed-cert.yaml | 11 +++ .../matching-service-deployment.yaml | 2 +- .../question-service-deployment.yaml | 2 +- .../user-service-deployment.yaml | 2 +- .../prod-dockerfiles/Dockerfile.frontend-prod | 4 +- docker-compose.yml | 10 ++- frontend/package.json | 6 +- frontend/providers/MatchmakingProvider.tsx | 7 +- .../firebase-client/useDeleteOwnAccount.ts | 1 + .../src/gateway-address/gateway-address.ts | 12 +-- frontend/src/hooks/useCollaboration.tsx | 5 +- frontend/src/pages/api/userHandler.ts | 3 +- frontend/src/pages/interviews/find-match.tsx | 7 +- services/gateway/README.md | 2 +- services/gateway/src/app.ts | 52 ++++++++--- services/gateway/src/auth/auth.ts | 4 +- .../src/proxied_routes/proxied_routes.ts | 26 +++--- services/gateway/src/proxy/proxy.ts | 12 ++- services/user-service/openapiDoc.json | 6 ++ services/user-service/src/routes/index.ts | 86 +++++++++++++------ services/user-service/systemtest/app.test.ts | 39 ++++++++- .../user-service/test/routes/index.test.ts | 57 ++++++++++-- start-app-no-docker.sh | 2 +- yarn.lock | 26 ++---- 34 files changed, 347 insertions(+), 119 deletions(-) create mode 100644 deployment/gke-prod-manifests/frontend-ingress.yaml create mode 100644 deployment/gke-prod-manifests/gateway-http-ingress.yaml create mode 100644 deployment/gke-prod-manifests/gateway-wscollaboration-ingress.yaml create mode 100644 deployment/gke-prod-manifests/gateway-wsmatch-ingress.yaml create mode 100644 deployment/gke-prod-manifests/gke-managed-cert.yaml diff --git a/deployment/gke-prod-manifests/admin-service-deployment.yaml b/deployment/gke-prod-manifests/admin-service-deployment.yaml index 94f8357c..7a28ae35 100644 --- a/deployment/gke-prod-manifests/admin-service-deployment.yaml +++ b/deployment/gke-prod-manifests/admin-service-deployment.yaml @@ -36,6 +36,6 @@ spec: # You must specify requests for CPU to autoscale # based on CPU utilization requests: - cpu: "250m" + cpu: "100m" restartPolicy: Always status: {} diff --git a/deployment/gke-prod-manifests/collaboration-service-deployment.yaml b/deployment/gke-prod-manifests/collaboration-service-deployment.yaml index 90c9d724..692ee763 100644 --- a/deployment/gke-prod-manifests/collaboration-service-deployment.yaml +++ b/deployment/gke-prod-manifests/collaboration-service-deployment.yaml @@ -51,6 +51,6 @@ spec: # You must specify requests for CPU to autoscale # based on CPU utilization requests: - cpu: "250m" + cpu: "100m" restartPolicy: Always status: {} diff --git a/deployment/gke-prod-manifests/frontend-deployment.yaml b/deployment/gke-prod-manifests/frontend-deployment.yaml index d1ae9eef..4e299d0c 100644 --- a/deployment/gke-prod-manifests/frontend-deployment.yaml +++ b/deployment/gke-prod-manifests/frontend-deployment.yaml @@ -28,6 +28,6 @@ spec: # You must specify requests for CPU to autoscale # based on CPU utilization requests: - cpu: "250m" + cpu: "100m" restartPolicy: Always status: {} diff --git a/deployment/gke-prod-manifests/frontend-ingress.yaml b/deployment/gke-prod-manifests/frontend-ingress.yaml new file mode 100644 index 00000000..86a8e8a7 --- /dev/null +++ b/deployment/gke-prod-manifests/frontend-ingress.yaml @@ -0,0 +1,13 @@ +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: frontend-ingress + annotations: + networking.gke.io/managed-certificates: gke-managed-cert + kubernetes.io/ingress.class: "gce" +spec: + defaultBackend: + service: + name: frontend-service + port: + number: 3000 diff --git a/deployment/gke-prod-manifests/frontend-service.yaml b/deployment/gke-prod-manifests/frontend-service.yaml index 9f0ca682..a72a5a12 100644 --- a/deployment/gke-prod-manifests/frontend-service.yaml +++ b/deployment/gke-prod-manifests/frontend-service.yaml @@ -12,6 +12,5 @@ spec: targetPort: 3000 selector: io.kompose.service: frontend - type: LoadBalancer status: loadBalancer: {} diff --git a/deployment/gke-prod-manifests/gateway-deployment.yaml b/deployment/gke-prod-manifests/gateway-deployment.yaml index cdf967c7..37d8493b 100644 --- a/deployment/gke-prod-manifests/gateway-deployment.yaml +++ b/deployment/gke-prod-manifests/gateway-deployment.yaml @@ -24,20 +24,30 @@ spec: secretKeyRef: name: firebase-service-account key: firebase-service-account - - name: PORT + - name: HTTP_PROXY_PORT value: "4000" + - name: WS_MATCH_PROXY_PORT + value: "4002" + - name: WS_COLLABORATION_PROXY_PORT + value: "4003" - name: FRONTEND_ADDRESS - value: "http://www.codeparty.org:3000" + value: "https://www.codeparty.org" image: asia-southeast1-docker.pkg.dev/peerprep-group11-prod/codeparty-prod-images/gateway:latest name: gateway ports: - containerPort: 4000 hostPort: 4000 protocol: TCP + - containerPort: 4002 + hostPort: 4002 + protocol: TCP + - containerPort: 4003 + hostPort: 4003 + protocol: TCP resources: # You must specify requests for CPU to autoscale # based on CPU utilization requests: - cpu: "250m" + cpu: "100m" restartPolicy: Always status: {} diff --git a/deployment/gke-prod-manifests/gateway-http-ingress.yaml b/deployment/gke-prod-manifests/gateway-http-ingress.yaml new file mode 100644 index 00000000..2af19c15 --- /dev/null +++ b/deployment/gke-prod-manifests/gateway-http-ingress.yaml @@ -0,0 +1,13 @@ +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: gateway-http-ingress + annotations: + networking.gke.io/managed-certificates: gke-managed-cert + kubernetes.io/ingress.class: "gce" +spec: + defaultBackend: + service: + name: frontend-service + port: + number: 4000 diff --git a/deployment/gke-prod-manifests/gateway-service.yaml b/deployment/gke-prod-manifests/gateway-service.yaml index 0bbcf2d9..64815258 100644 --- a/deployment/gke-prod-manifests/gateway-service.yaml +++ b/deployment/gke-prod-manifests/gateway-service.yaml @@ -10,8 +10,13 @@ spec: - name: "4000" port: 4000 targetPort: 4000 + - name: "4002" + port: 4002 + targetPort: 4002 + - name: "4003" + port: 4003 + targetPort: 4003 selector: io.kompose.service: gateway - type: LoadBalancer status: loadBalancer: {} diff --git a/deployment/gke-prod-manifests/gateway-wscollaboration-ingress.yaml b/deployment/gke-prod-manifests/gateway-wscollaboration-ingress.yaml new file mode 100644 index 00000000..72714ddc --- /dev/null +++ b/deployment/gke-prod-manifests/gateway-wscollaboration-ingress.yaml @@ -0,0 +1,13 @@ +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: gateway-wscollaboration-ingress + annotations: + networking.gke.io/managed-certificates: gke-managed-cert + kubernetes.io/ingress.class: "gce" +spec: + defaultBackend: + service: + name: gateway-service + port: + number: 4003 diff --git a/deployment/gke-prod-manifests/gateway-wsmatch-ingress.yaml b/deployment/gke-prod-manifests/gateway-wsmatch-ingress.yaml new file mode 100644 index 00000000..3f6c2064 --- /dev/null +++ b/deployment/gke-prod-manifests/gateway-wsmatch-ingress.yaml @@ -0,0 +1,13 @@ +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: gateway-wsmatch-ingress + annotations: + networking.gke.io/managed-certificates: gke-managed-cert + kubernetes.io/ingress.class: "gce" +spec: + defaultBackend: + service: + name: gateway-service + port: + number: 4002 diff --git a/deployment/gke-prod-manifests/gke-managed-cert.yaml b/deployment/gke-prod-manifests/gke-managed-cert.yaml new file mode 100644 index 00000000..4390e533 --- /dev/null +++ b/deployment/gke-prod-manifests/gke-managed-cert.yaml @@ -0,0 +1,11 @@ +apiVersion: networking.gke.io/v1 +kind: ManagedCertificate +metadata: + name: gke-managed-cert +spec: + domains: + - codeparty.org + - www.codeparty.org + - api.codeparty.org + - wsmatch.codeparty.org + - wscollab.codeparty.org diff --git a/deployment/gke-prod-manifests/matching-service-deployment.yaml b/deployment/gke-prod-manifests/matching-service-deployment.yaml index fea7a690..44471cdf 100644 --- a/deployment/gke-prod-manifests/matching-service-deployment.yaml +++ b/deployment/gke-prod-manifests/matching-service-deployment.yaml @@ -36,6 +36,6 @@ spec: # You must specify requests for CPU to autoscale # based on CPU utilization requests: - cpu: "250m" + cpu: "100m" restartPolicy: Always status: {} diff --git a/deployment/gke-prod-manifests/question-service-deployment.yaml b/deployment/gke-prod-manifests/question-service-deployment.yaml index d54a5a59..c3492713 100644 --- a/deployment/gke-prod-manifests/question-service-deployment.yaml +++ b/deployment/gke-prod-manifests/question-service-deployment.yaml @@ -36,6 +36,6 @@ spec: # You must specify requests for CPU to autoscale # based on CPU utilization requests: - cpu: "250m" + cpu: "100m" restartPolicy: Always status: {} diff --git a/deployment/gke-prod-manifests/user-service-deployment.yaml b/deployment/gke-prod-manifests/user-service-deployment.yaml index ee8293f4..cca09053 100644 --- a/deployment/gke-prod-manifests/user-service-deployment.yaml +++ b/deployment/gke-prod-manifests/user-service-deployment.yaml @@ -36,6 +36,6 @@ spec: # You must specify requests for CPU to autoscale # based on CPU utilization requests: - cpu: "250m" + cpu: "100m" restartPolicy: Always status: {} diff --git a/deployment/prod-dockerfiles/Dockerfile.frontend-prod b/deployment/prod-dockerfiles/Dockerfile.frontend-prod index 8d47756c..d8e4826b 100644 --- a/deployment/prod-dockerfiles/Dockerfile.frontend-prod +++ b/deployment/prod-dockerfiles/Dockerfile.frontend-prod @@ -23,7 +23,9 @@ RUN yarn prisma generate ARG NEXT_PUBLIC_FRONTEND_FIREBASE_CONFIG_ARG ENV NEXT_PUBLIC_FRONTEND_FIREBASE_CONFIG=$NEXT_PUBLIC_FRONTEND_FIREBASE_CONFIG_ARG -ENV NEXT_PUBLIC_GATEWAY_ADDRESS="http://api.codeparty.org:4000/" +ENV NEXT_PUBLIC_HTTP_PROXY_GATEWAY_ADDRESS="https://api.codeparty.org/" +ENV NEXT_PUBLIC_WS_MATCH_PROXY_GATEWAY_ADDRESS="https://wsmatch.codeparty.org" +ENV NEXT_PUBLIC_WS_COLLABORATION_PROXY_GATEWAY_ADDRESS="https://wscollab.codeparty.org" RUN yarn build diff --git a/docker-compose.yml b/docker-compose.yml index 67f21e31..215f3800 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -79,8 +79,12 @@ services: - ./services/gateway:/app/services/gateway ports: - "4000:4000" + - "4002:4002" + - "4003:4003" environment: - PORT: 4000 + HTTP_PROXY_PORT: 4000 + WS_MATCH_PROXY_PORT: 4002 + WS_COLLABORATION_PROXY_PORT: 4003 FIREBASE_SERVICE_ACCOUNT: ${FIREBASE_SERVICE_ACCOUNT} FRONTEND_ADDRESS: "http://localhost:3000" @@ -95,5 +99,7 @@ services: ports: - "3000:3000" environment: - NEXT_PUBLIC_GATEWAY_ADDRESS: "http://localhost:4000/" + NEXT_PUBLIC_HTTP_PROXY_GATEWAY_ADDRESS: "http://localhost:4000/" + NEXT_PUBLIC_WS_MATCH_PROXY_GATEWAY_ADDRESS: "http://localhost:4002/" + NEXT_PUBLIC_WS_COLLABORATION_PROXY_GATEWAY_ADDRESS: "http://localhost:4003/" NEXT_PUBLIC_FRONTEND_FIREBASE_CONFIG: ${NEXT_PUBLIC_FRONTEND_FIREBASE_CONFIG} diff --git a/frontend/package.json b/frontend/package.json index 0e247edc..dcb12397 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -30,9 +30,6 @@ "@radix-ui/react-tabs": "^1.0.4", "@tanstack/react-query": "^5.0.0", "@tanstack/react-table": "^8.10.4", - "@types/node": "20.6.0", - "@types/react": "18.2.21", - "@types/react-dom": "18.2.7", "@uiball/loaders": "^1.3.0", "autoprefixer": "10.4.15", "class-variance-authority": "^0.7.0", @@ -62,7 +59,8 @@ "devDependencies": { "@types/diff-match-patch": "^1.0.34", "@types/lodash": "^4.14.199", - "@types/react": "^18.2.30", + "@types/node": "^20.8.8", + "@types/react": "^18.2.31", "@types/react-dom": "^18.2.14", "@types/socket.io-client": "^3.0.0", "eslint": "^8.51.0", diff --git a/frontend/providers/MatchmakingProvider.tsx b/frontend/providers/MatchmakingProvider.tsx index eaeded30..aa5818a8 100644 --- a/frontend/providers/MatchmakingProvider.tsx +++ b/frontend/providers/MatchmakingProvider.tsx @@ -8,9 +8,9 @@ import React, { import { io, Socket } from "socket.io-client"; import { Match } from "@prisma/client"; import { AuthContext } from "@/contexts/AuthContext"; -import {matchSocketAddress} from "@/gateway-address/gateway-address"; +import {wsMatchProxyGatewayAddress} from "@/gateway-address/gateway-address"; -const SERVER_URL = matchSocketAddress; +const SERVER_URL = wsMatchProxyGatewayAddress; interface MatchmakingContextValue { socket: Socket | null; @@ -57,8 +57,7 @@ export const MatchmakingProvider: React.FC = ({ query: { username: generateRandomNumber() }, extraHeaders: { "User-Id-Token": token - }, - path: "/match/socket.io" + } }); setSocket(newSocket); newSocket.connect(); diff --git a/frontend/src/firebase-client/useDeleteOwnAccount.ts b/frontend/src/firebase-client/useDeleteOwnAccount.ts index 63759825..11fa14a2 100644 --- a/frontend/src/firebase-client/useDeleteOwnAccount.ts +++ b/frontend/src/firebase-client/useDeleteOwnAccount.ts @@ -15,6 +15,7 @@ export const useDeleteOwnAccount = () => { method: "DELETE", headers: { "User-Id-Token": idToken, + "User-Id": currentUser.uid }, }); // This will delete the user from the Firebase Authentication database diff --git a/frontend/src/gateway-address/gateway-address.ts b/frontend/src/gateway-address/gateway-address.ts index f734bed1..a1ef68d1 100644 --- a/frontend/src/gateway-address/gateway-address.ts +++ b/frontend/src/gateway-address/gateway-address.ts @@ -5,10 +5,10 @@ * - Leave NEXT_PUBLIC_GATEWAY_ADDRESS empty for dev environments * - For prod, pass in a separate address to NEXT_PUBLIC_GATEWAY_ADDRESS */ -const gatewayAddress = process.env.NEXT_PUBLIC_GATEWAY_ADDRESS || "http://localhost:4000/" +const httpProxyGatewayAddress = process.env.NEXT_PUBLIC_HTTP_PROXY_GATEWAY_ADDRESS || "http://localhost:4000/"; +export const wsMatchProxyGatewayAddress = process.env.NEXT_PUBLIC_WS_MATCH_PROXY_GATEWAY_ADDRESS || "http://localhost:4002"; +export const wsCollaborationProxyGatewayAddress = process.env.NEXT_PUBLIC_WS_COLLABORATION_PROXY_GATEWAY_ADDRESS + || "http://localhost:4003"; -export const userApiPathAddress = gatewayAddress + "api/user-service/"; -export const questionApiPathAddress = gatewayAddress + "api/question-service/"; - -export const collaborationSocketAddress = gatewayAddress + "collaboration/socket.io"; -export const matchSocketAddress = gatewayAddress + "match/socket.io"; +export const userApiPathAddress = httpProxyGatewayAddress + "api/user-service/"; +export const questionApiPathAddress = httpProxyGatewayAddress + "api/question-service/"; diff --git a/frontend/src/hooks/useCollaboration.tsx b/frontend/src/hooks/useCollaboration.tsx index 7a470188..1f47adda 100644 --- a/frontend/src/hooks/useCollaboration.tsx +++ b/frontend/src/hooks/useCollaboration.tsx @@ -7,7 +7,7 @@ import { } from "../../../utils/shared-ot"; import { TextOp } from "ot-text-unicode"; import { Room, connect } from "twilio-video"; -import {collaborationSocketAddress} from "@/gateway-address/gateway-address"; +import {wsCollaborationProxyGatewayAddress} from "@/gateway-address/gateway-address"; import {AuthContext} from "@/contexts/AuthContext"; type UseCollaborationProps = { @@ -47,11 +47,10 @@ const useCollaboration = ({ roomId, userId, disableVideo }: UseCollaborationProp if (currentUser) { currentUser.getIdToken(true).then( (token) => { - const socketConnection = io(collaborationSocketAddress, { + const socketConnection = io(wsCollaborationProxyGatewayAddress, { extraHeaders: { "User-Id-Token": token }, - path: "/collaboration/socket.io" }); setSocket(socketConnection); diff --git a/frontend/src/pages/api/userHandler.ts b/frontend/src/pages/api/userHandler.ts index aa6df926..67c7158b 100644 --- a/frontend/src/pages/api/userHandler.ts +++ b/frontend/src/pages/api/userHandler.ts @@ -3,7 +3,7 @@ import { EditableUser } from "@/types/UserTypes"; export const updateUserByUid = async (user: EditableUser, currentUser: any) => { try { - const url = `${userApiPathAddress}${currentUser.uid}}`; + const url = `${userApiPathAddress}${currentUser.uid}`; const idToken = await currentUser.getIdToken(true); console.log("user", user); @@ -14,6 +14,7 @@ export const updateUserByUid = async (user: EditableUser, currentUser: any) => { headers: { "Content-Type": "application/json", "User-Id-Token": idToken, + "User-Id": currentUser.uid }, body: JSON.stringify(user), }); diff --git a/frontend/src/pages/interviews/find-match.tsx b/frontend/src/pages/interviews/find-match.tsx index 45e287f1..dbb915c2 100644 --- a/frontend/src/pages/interviews/find-match.tsx +++ b/frontend/src/pages/interviews/find-match.tsx @@ -15,14 +15,19 @@ export default function FindMatch() { }; useEffect(() => { + let timeout: ReturnType | null = null; if (match) { router.push("/interviews/match-found"); } else { - setTimeout(() => { + timeout = setTimeout(() => { cancelLooking(); router.push("/interviews/match-not-found"); }, 30000); } + return () => { + if (timeout) + clearTimeout(timeout); + } // eslint-disable-next-line react-hooks/exhaustive-deps }, [match, router]); diff --git a/services/gateway/README.md b/services/gateway/README.md index d4b21c52..e6cf6d05 100644 --- a/services/gateway/README.md +++ b/services/gateway/README.md @@ -24,7 +24,7 @@ The below code shows a sample route that is being proxied from the frontend to t } ``` -This code is part of the `proxied_routes` list in `src/proxied_routes/proxied_routes.ts` file. +This code is part of the `http_proxied_routes` list in `src/proxied_routes/proxied_routes.ts` file. Explanation: * `url` - The initial path. Assuming that the gateway address is `YYY://localhost:4000`, the frontend would call `YYY://localhost:4000/users` diff --git a/services/gateway/src/app.ts b/services/gateway/src/app.ts index 68464cd2..8b19d5f6 100644 --- a/services/gateway/src/app.ts +++ b/services/gateway/src/app.ts @@ -3,30 +3,58 @@ import cors from 'cors'; import { setupLogging } from "./logging/logging"; import { setupAdmin, setupUserIdMatch, setupIsLoggedIn } from "./auth/auth"; import { setupProxies } from "./proxy/proxy"; -import { proxied_routes } from "./proxied_routes/proxied_routes"; +import {http_proxied_routes, wsCollaborationProxiedRoutes, wsMatchProxiedRoutes} from "./proxied_routes/proxied_routes"; import {frontendAddress} from "./proxied_routes/service_names"; +import {createProxyMiddleware} from "http-proxy-middleware"; -const app : Express = express(); +const httpApp : Express = express(); +const wsMatchApp : Express = express(); +const wsCollaborationApp : Express = express(); + const corsOptions = { origin: frontendAddress, methods: ["GET", "POST", "PUT", "DELETE", "OPTIONS"] } -const port : number = parseInt(process.env.PORT || "4000"); +const httpProxyPort : number = parseInt(process.env.HTTP_PROXY_PORT || "4000"); +const wsMatchProxyPort : number = parseInt(process.env.WS_MATCH_PROXY_PORT || "4002"); +const wsCollaborationProxyPort : number = parseInt(process.env.WS_COLLABORATION_PROXY_PORT || "4003"); -app.use(cors(corsOptions)) +httpApp.use(cors(corsOptions)); +wsMatchApp.use(cors(corsOptions)); +wsCollaborationApp.use(cors(corsOptions)); /** * WARNING: Do not add body parsing middleware to the Gateway. * Otherwise, proxying POST requests with request body would not work. */ -setupLogging(app); -setupIsLoggedIn(app, proxied_routes); -setupUserIdMatch(app, proxied_routes); -setupAdmin(app, proxied_routes); -setupProxies(app, proxied_routes); - -app.listen(port, () => { - console.log(`Example app listening at http://localhost:${port}`) +setupLogging(httpApp); +setupLogging(wsMatchApp); +setupLogging(wsCollaborationApp); + +setupIsLoggedIn(httpApp, http_proxied_routes); +setupIsLoggedIn(wsMatchApp, wsMatchProxiedRoutes); +setupIsLoggedIn(wsCollaborationApp, wsCollaborationProxiedRoutes); + + +setupUserIdMatch(httpApp, http_proxied_routes); +setupAdmin(httpApp, http_proxied_routes); +setupProxies(httpApp, http_proxied_routes); + +const wsMatchProxyMiddleware = createProxyMiddleware(wsMatchProxiedRoutes[0].proxy); +wsMatchApp.use(wsMatchProxiedRoutes[0].url, wsMatchProxyMiddleware); +const wsCollaborationProxyMiddleware = createProxyMiddleware(wsCollaborationProxiedRoutes[0].proxy); +wsCollaborationApp.use(wsCollaborationProxiedRoutes[0].url, wsCollaborationProxyMiddleware); + +httpApp.listen(httpProxyPort, () => { + console.log(`Gateway HTTP proxy listening on port ${httpProxyPort}`); +}) + +wsMatchApp.listen(wsMatchProxyPort, () => { + console.log(`Gateway WebSockets Match Proxy listening on port ${wsMatchProxyPort}`); +}) + +wsCollaborationApp.listen(wsCollaborationProxyPort, () => { + console.log(`Gateway WebSockets Collaboration Proxy listening on port ${wsCollaborationProxyPort}`); }) diff --git a/services/gateway/src/auth/auth.ts b/services/gateway/src/auth/auth.ts index 2aac2d48..0d7a93b5 100644 --- a/services/gateway/src/auth/auth.ts +++ b/services/gateway/src/auth/auth.ts @@ -4,6 +4,7 @@ import {frontendAddress} from "../proxied_routes/service_names"; const redirectLink = frontendAddress; const userIdTokenHeader = "User-Id-Token"; +const userIdHeader = "User-Id"; export const setupIsLoggedIn = (app : Express, routes : any[]) => { routes.forEach(r => { @@ -32,8 +33,9 @@ export const setupUserIdMatch = (app : Express, routes : any[]) => { routes.forEach(r => { app.use(r.url, function(req : express.Request, res : express.Response, next : express.NextFunction) { if (r.user_match_required_methods.includes(req.method)) { + console.log(req.params) const idToken = req.get(userIdTokenHeader); - const paramUid = req.params.uid; + const paramUid = req.get(userIdHeader); if (!idToken || !paramUid) { res.redirect(redirectLink) } else { diff --git a/services/gateway/src/proxied_routes/proxied_routes.ts b/services/gateway/src/proxied_routes/proxied_routes.ts index c2b55ac8..1ffe07d9 100644 --- a/services/gateway/src/proxied_routes/proxied_routes.ts +++ b/services/gateway/src/proxied_routes/proxied_routes.ts @@ -7,7 +7,7 @@ import { userServiceAddress, } from "./service_names"; -export const proxied_routes: ProxiedRoute[] = [ +export const http_proxied_routes: ProxiedRoute[] = [ { url: "/api/user-service", admin_required_methods: [], // Empty, so no admin verification is done for all methods to the user-service @@ -58,28 +58,30 @@ export const proxied_routes: ProxiedRoute[] = [ changeOrigin: true, }, }, +]; + +export const wsMatchProxiedRoutes: ProxiedRoute[] = [ { - url: "/collaboration/socket.io", + url: "/", admin_required_methods: [], user_match_required_methods: [], // No need for exact user match here proxy: { - target: collaborationServiceAddress, + target: matchingServiceAddress, changeOrigin: true, - pathRewrite: { - "^/collaboration/socket.io": "socket.io", - }, + ws: true }, }, +] + +export const wsCollaborationProxiedRoutes: ProxiedRoute[] = [ { - url: "/match/socket.io", + url: "/", admin_required_methods: [], user_match_required_methods: [], // No need for exact user match here proxy: { - target: matchingServiceAddress, + target: collaborationServiceAddress, changeOrigin: true, - pathRewrite: { - "^/match/socket.io": "socket.io", - }, + ws: true }, }, -]; +] diff --git a/services/gateway/src/proxy/proxy.ts b/services/gateway/src/proxy/proxy.ts index a826fe56..484f7abc 100644 --- a/services/gateway/src/proxy/proxy.ts +++ b/services/gateway/src/proxy/proxy.ts @@ -2,7 +2,13 @@ import { createProxyMiddleware } from 'http-proxy-middleware'; import {Express} from "express"; export const setupProxies = (app : Express, routes : any[]) => { - routes.forEach(r => { - app.use(r.url, createProxyMiddleware(r.proxy)); - }) + var proxyMiddlewareArray : any[] = [] + for (let i = 0; i < routes.length; i++) { + const proxyMiddleware = createProxyMiddleware(routes[i].proxy); + app.use(routes[i].url, proxyMiddleware); + if (routes[i].proxy.ws) { + proxyMiddlewareArray.push(proxyMiddleware); + } + } + return proxyMiddlewareArray; } diff --git a/services/user-service/openapiDoc.json b/services/user-service/openapiDoc.json index 6fdf8c16..3dce4a77 100644 --- a/services/user-service/openapiDoc.json +++ b/services/user-service/openapiDoc.json @@ -68,6 +68,9 @@ "200": { "description": "OK" }, + "400": { + "description": "Bad Request" + }, "404": { "description": "Not Found" }, @@ -92,6 +95,9 @@ "204": { "description": "No Content" }, + "400": { + "description": "Bad Request" + }, "404": { "description": "Not Found" }, diff --git a/services/user-service/src/routes/index.ts b/services/user-service/src/routes/index.ts index 360b2644..e540ef92 100644 --- a/services/user-service/src/routes/index.ts +++ b/services/user-service/src/routes/index.ts @@ -41,38 +41,72 @@ indexRouter.get( indexRouter.put( "/:uid", function (req: express.Request, res: express.Response) { - userDatabaseFunctions - .updateUserByUid(req.params.uid, req.body) - .then((result) => { - res.status(200).json(result); - }) - .catch((error) => { - if (error.code === "P2025") { - res.status(404).end(); - } else { - // Server side error such as database not being available - res.status(500).end(); - } - }); + /** + * Need to check that header UID was not tampered with. + * + * Attack Scenario: + * 1) User 2 wants to edit profile of user 1. + * 2) This should be blocked by the gateway since path param uid = 1 and header uid = 1 + * but user 2 only has authentication token for user 2 + * 3) User 2 could change header uid = 2 to pass authentication, but retain path param uid = 1 + * to attempt to change user 1's data + * 4) Hence, need to check that param uid = header uid + */ + const pathUid = req.params.uid; + const headerUid = req.get("User-Id"); + if (pathUid !== headerUid) { + res.status(400).end(); + } else { + userDatabaseFunctions + .updateUserByUid(req.params.uid, req.body) + .then((result) => { + res.status(200).json(result); + }) + .catch((error) => { + if (error.code === "P2025") { + res.status(404).end(); + } else { + // Server side error such as database not being available + res.status(500).end(); + } + }); + } } ); indexRouter.delete( "/:uid", function (req: express.Request, res: express.Response) { - userDatabaseFunctions - .deleteUserByUid(req.params.uid) - .then(() => { - res.status(204).end(); - }) - .catch((error) => { - if (error.code === "P2025") { - res.status(404).end(); - } else { - // Server side error such as database not being available - res.status(500).end(); - } - }); + /** + * Need to check that header UID was not tampered with. + * + * Attack Scenario: + * 1) User 2 wants to edit profile of user 1. + * 2) This should be blocked by the gateway since path param uid = 1 and header uid = 1 + * but user 2 only has authentication token for user 2 + * 3) User 2 could change header uid = 2 to pass authentication, but retain path param uid = 1 + * to attempt to change user 1's data + * 4) Hence, need to check that param uid = header uid + */ + const pathUid = req.params.uid; + const headerUid = req.get("User-Id"); + if (pathUid !== headerUid) { + res.status(400).end(); + } else { + userDatabaseFunctions + .deleteUserByUid(req.params.uid) + .then(() => { + res.status(204).end(); + }) + .catch((error) => { + if (error.code === "P2025") { + res.status(404).end(); + } else { + // Server side error such as database not being available + res.status(500).end(); + } + }) + } } ); diff --git a/services/user-service/systemtest/app.test.ts b/services/user-service/systemtest/app.test.ts index 3c5069d0..eaf746c0 100644 --- a/services/user-service/systemtest/app.test.ts +++ b/services/user-service/systemtest/app.test.ts @@ -12,6 +12,8 @@ const updatedNewUser = { uid: '1', displayName: 'Test User', photoUrl: "fakeUrl" const updatePayload = { matchDifficulty: 1 }; +const userIdHeader = "User-Id"; + describe('/index', () => { describe('Sample App Workflow', () => { it('Step 1: Add user 1 to database should pass with status 201', async () => { @@ -28,9 +30,21 @@ describe('/index', () => { expect(response.body).toStrictEqual(fullNewUser); }) + it('Step 3a: Update details of user 1 from database by user 2 should fail with error 400', async () => { + // The function being tested + const response = await request(app) + .put('/api/user-service/1') + .set(userIdHeader, "2") + .send(updatePayload); + expect(response.status).toStrictEqual(400); + }) + it('Step 3: Update details of user 1 from database should pass', async () => { // The function being tested - const response = await request(app).put('/api/user-service/1').send(updatePayload); + const response = await request(app) + .put('/api/user-service/1') + .set(userIdHeader, "1") + .send(updatePayload); expect(response.status).toStrictEqual(200); expect(response.body).toStrictEqual(updatedNewUser); }) @@ -47,8 +61,19 @@ describe('/index', () => { expect(response.status).toStrictEqual(200); }) + it('Step 6a: Delete user 1 from database by user 2 should fail with status 400', async () => { + const response = await request(app) + .delete('/api/user-service/1') + .set(userIdHeader, "2") + .send(); + expect(response.status).toStrictEqual(400); + }) + it('Step 6: Delete user 1 from database', async () => { - const response = await request(app).delete('/api/user-service/1').send(); + const response = await request(app) + .delete('/api/user-service/1') + .set(userIdHeader, "1") + .send(); expect(response.status).toStrictEqual(204); }) @@ -60,13 +85,19 @@ describe('/index', () => { it('Step 8: Update details of now deleted user 1 should fail', async () => { // The function being tested - const response = await request(app).put('/api/user-service/1').send(updatePayload); + const response = await request(app) + .put('/api/user-service/1') + .set(userIdHeader, "1") + .send(updatePayload); expect(response.status).toStrictEqual(404); }) it('Step 9: Deleting the now deleted user 1 should fail', async () => { // The function being tested - const response = await request(app).delete('/api/user-service/1').send(); + const response = await request(app) + .delete('/api/user-service/1') + .set(userIdHeader, "1") + .send(); expect(response.status).toStrictEqual(404); }) }) diff --git a/services/user-service/test/routes/index.test.ts b/services/user-service/test/routes/index.test.ts index 8b3bfe06..6d2b5abd 100644 --- a/services/user-service/test/routes/index.test.ts +++ b/services/user-service/test/routes/index.test.ts @@ -9,6 +9,7 @@ import request from 'supertest'; vi.mock('../../src/db/functions') const app = express(); +const userIdHeader = "User-Id"; app.use(indexRouter); const fullNewUser = { uid: '1', displayName: 'Test User', photoUrl: "fakeUrl", matchDifficulty: 0, @@ -93,7 +94,10 @@ describe('/index', () => { userDatabaseFunctionsMock.updateUserByUid.mockResolvedValueOnce(fullNewUser); // The function being tested - const response = await request(app).put('/1').send(); + const response = await request(app) + .put('/1') + .set(userIdHeader, "1") + .send(); expect(response.status).toStrictEqual(200); expect(response.body).toStrictEqual(fullNewUser); }) @@ -106,7 +110,10 @@ describe('/index', () => { })); // The function being tested - const response = await request(app).put('/1').send(); + const response = await request(app) + .put('/1') + .set(userIdHeader, "1") + .send(); expect(response.status).toStrictEqual(404); }) @@ -115,9 +122,25 @@ describe('/index', () => { userDatabaseFunctionsMock.updateUserByUid.mockRejectedValueOnce(new Error()); // The function being tested - const response = await request(app).put('/1').send(); + const response = await request(app) + .put('/1') + .set(userIdHeader, "1") + .send(); expect(response.status).toStrictEqual(500); }) + + it('[PUT] /1 but the header UID does not match path param UID', async () => { + + // Used to get back the user + userDatabaseFunctionsMock.updateUserByUid.mockResolvedValueOnce(fullNewUser); + + // The function being tested + const response = await request(app) + .put('/1') + .set(userIdHeader, "2") + .send(); + expect(response.status).toStrictEqual(400); + }) }) describe('deleteUserByUid', () => { @@ -127,7 +150,10 @@ describe('/index', () => { userDatabaseFunctionsMock.deleteUserByUid.mockResolvedValueOnce(fullNewUser); // The function being tested - const response = await request(app).delete('/1').send(); + const response = await request(app) + .delete('/1') + .set(userIdHeader, "1") + .send(); expect(response.status).toStrictEqual(204); }) @@ -139,7 +165,10 @@ describe('/index', () => { })); // The function being tested - const response = await request(app).delete('/1').send(); + const response = await request(app) + .delete('/1') + .set(userIdHeader, "1") + .send(); expect(response.status).toStrictEqual(404); }) @@ -148,8 +177,24 @@ describe('/index', () => { userDatabaseFunctionsMock.deleteUserByUid.mockRejectedValueOnce(new Error()); // The function being tested - const response = await request(app).delete('/1').send(); + const response = await request(app) + .delete('/1') + .set(userIdHeader, "1") + .send(); expect(response.status).toStrictEqual(500); }) + + it('[DELETE] /1 but the header UID does not match path param UID', async () => { + + // Used to get back the user + userDatabaseFunctionsMock.deleteUserByUid.mockResolvedValueOnce(fullNewUser); + + // The function being tested + const response = await request(app) + .delete('/1') + .set(userIdHeader, "2") + .send(); + expect(response.status).toStrictEqual(400); + }) }) }) diff --git a/start-app-no-docker.sh b/start-app-no-docker.sh index 87a27f64..99273b34 100755 --- a/start-app-no-docker.sh +++ b/start-app-no-docker.sh @@ -6,7 +6,7 @@ prepend() { done } -(yarn && yarn prisma generate && \ +(yarn install --frozen-lockfile && yarn prisma generate && \ trap 'kill 0' INT TERM; \ (yarn workspace frontend dev:local | prepend "frontend: ") & \ (yarn workspace user-service dev:local | prepend "user-service: ") & \ diff --git a/yarn.lock b/yarn.lock index f5a017e6..19c965c4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3193,18 +3193,13 @@ dependencies: "@types/node" "*" -"@types/node@*", "@types/node@>=10.0.0", "@types/node@>=12.12.47", "@types/node@>=13.7.0", "@types/node@^20.8.7": +"@types/node@*", "@types/node@>=10.0.0", "@types/node@>=12.12.47", "@types/node@>=13.7.0", "@types/node@^20.8.7", "@types/node@^20.8.8": version "20.8.8" resolved "https://registry.yarnpkg.com/@types/node/-/node-20.8.8.tgz#adee050b422061ad5255fc38ff71b2bb96ea2a0e" integrity sha512-YRsdVxq6OaLfmR9Hy816IMp33xOBjfyOgUd77ehqg96CFywxAPbDbXvAsuN2KVg2HOT8Eh6uAfU+l4WffwPVrQ== dependencies: undici-types "~5.25.1" -"@types/node@20.6.0": - version "20.6.0" - resolved "https://registry.yarnpkg.com/@types/node/-/node-20.6.0.tgz#9d7daa855d33d4efec8aea88cd66db1c2f0ebe16" - integrity sha512-najjVq5KN2vsH2U/xyh2opaSEz6cZMR2SetLIlxlj08nOcmPOemJmUK2o4kUzfLqfrWE0PIrNeE16XhYDd3nqg== - "@types/parse-json@^4.0.0": version "4.0.1" resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.1.tgz#27f7559836ad796cea31acb63163b203756a5b4e" @@ -3225,10 +3220,10 @@ resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.6.tgz#7cb33992049fd7340d5b10c0098e104184dfcd2a" integrity sha512-+0autS93xyXizIYiyL02FCY8N+KkKPhILhcUSA276HxzreZ16kl+cmwvV2qAM/PuCCwPXzOXOWhiPcw20uSFcA== -"@types/react-dom@18.2.7": - version "18.2.7" - resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-18.2.7.tgz#67222a08c0a6ae0a0da33c3532348277c70abb63" - integrity sha512-GRaAEriuT4zp9N4p1i8BDBYmEyfo+xQ3yHjJU4eiK5NDa1RmUZG+unZABUTK4/Ox/M+GaHwb6Ow8rUITrtjszA== +"@types/react-dom@^18.2.14": + version "18.2.14" + resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-18.2.14.tgz#c01ba40e5bb57fc1dc41569bb3ccdb19eab1c539" + integrity sha512-V835xgdSVmyQmI1KLV2BEIUgqEuinxp9O4G6g3FqO/SqLac049E53aysv0oEFD2kHfejeKU+ZqL2bcFWj9gLAQ== dependencies: "@types/react" "*" @@ -3239,7 +3234,7 @@ dependencies: "@types/react" "*" -"@types/react@*": +"@types/react@*", "@types/react@^18.2.31": version "18.2.31" resolved "https://registry.yarnpkg.com/@types/react/-/react-18.2.31.tgz#74ae2630e4aa9af599584157abd3b95d96fb9b40" integrity sha512-c2UnPv548q+5DFh03y8lEDeMfDwBn9G3dRwfkrxQMo/dOtRHUUO57k6pHvBIfH/VF4Nh+98mZ5aaSe+2echD5g== @@ -3248,15 +3243,6 @@ "@types/scheduler" "*" csstype "^3.0.2" -"@types/react@18.2.21": - version "18.2.21" - resolved "https://registry.yarnpkg.com/@types/react/-/react-18.2.21.tgz#774c37fd01b522d0b91aed04811b58e4e0514ed9" - integrity sha512-neFKG/sBAwGxHgXiIxnbm3/AAVQ/cMRS93hvBpg8xYRbeQSPVABp9U2bRnPf0iI4+Ucdv3plSxKK+3CW2ENJxA== - dependencies: - "@types/prop-types" "*" - "@types/scheduler" "*" - csstype "^3.0.2" - "@types/rimraf@^3.0.2": version "3.0.2" resolved "https://registry.yarnpkg.com/@types/rimraf/-/rimraf-3.0.2.tgz#a63d175b331748e5220ad48c901d7bbf1f44eef8"