diff --git a/CHANGELOG.md b/CHANGELOG.md index f358bc25159e87d84b5c5d9eff17c6eb9b5eaf17..f0c8b42ad7d7ea8fa3e695689252300e0f67c6a2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ 2024-05-07 - add exercise name, rework exercise panel add dialogs 2024-05-07 - add learning objectives page to the instructor view 2024-05-07 - change the tab icon to the INJECT logo +2024-05-05 - improve Nginx deployment, add support for AAI and HTTPS 2024-05-03 - improve Codegen pipeline and separate it from the codebase 2024-04-21 - improvements to the login UI, onboarding UI, added support for disabling AAI 2024-04-20 - add initial implementation of AAI to Frontend diff --git a/docker/nginx-deployment/.env b/docker/nginx-deployment/.env index 433962b2773ff1758d9f43d3ac9b8b07f52a426a..0ce56e32dcbd8e778f2d6b213c4283936afb2824 100644 --- a/docker/nginx-deployment/.env +++ b/docker/nginx-deployment/.env @@ -1,5 +1,32 @@ +# sets up appropriate hostname for the Nginx server +EXTERNAL_HOST=inject.localhost + +# disable AAI, uncomment the next two lines to enable it +INJECT_NOAUTH=true +VITE_NOAUTH=$INJECT_NOAUTH + +# host parametres that should be setup for client and server to interact correctly +INJECT_HOST_ADDRESSES=$EXTERNAL_HOST +VITE_HTTP_HOST=$EXTERNAL_HOST +VITE_HTTP_WS=ws://$EXTERNAL_HOST/inject/api/v1/subscription +CORS_ALLOWED_ORIGINS=http://$EXTERNAL_HOST + +# for certbot generation, use with certbot-generator.yml +CERTBOT_EMAIL=noemail@inject.localhost + +# for email sending of AAI mails +INJECT_EMAIL_HOST= +INJECT_EMAIL_PORT= +INJECT_EMAIL_HOST_USER= +INJECT_EMAIL_HOST_PASSWORD= +INJECT_EMAIL_SENDER_ADDRESS= + +# enable logging to a specific file +# INJECT_LOGS= + +# enable debug mode for the backend +INJECT_DEBUG= + +# nginx settings, do not touch unless you know what you are doing NGINX_DEFAULT_REQUEST=index.html NGINX_ROOT=dist -HOST_ADDRESSES='localhost,localhost:5173' -VITE_HTTP_HOST=localhost -VITE_HTTP_WS=ws://localhost/inject/api/v1/subscription \ No newline at end of file diff --git a/docker/nginx-deployment/01-substitute-env.sh b/docker/nginx-deployment/01-substitute-env.sh new file mode 100755 index 0000000000000000000000000000000000000000..106f33ec8a4bb11500661b57f27bfb6e06bc4f58 --- /dev/null +++ b/docker/nginx-deployment/01-substitute-env.sh @@ -0,0 +1,4 @@ +#!/usr/bin/env sh + +echo 'Substituting environment variables in nginx.conf.template' +envsubst '${EXTERNAL_HOST}' < /etc/nginx/nginx.conf.template > /etc/nginx/nginx.conf \ No newline at end of file diff --git a/docker/nginx-deployment/README.md b/docker/nginx-deployment/README.md new file mode 100644 index 0000000000000000000000000000000000000000..954ae48fc3cb8a1cf7f62621eea173725894d918 --- /dev/null +++ b/docker/nginx-deployment/README.md @@ -0,0 +1,66 @@ +# Deployment (Nginx) + +INJECT in additional to basal deployment of Frontend and Backend containers, it's also to possible to utilize a simplified deployment method via Docker Compose. This method simplifies the deployment by creating necessary settings, pairings between Frontend and Backend and also creates a singular endpoint for accessing INJECT. + +This solution consists of 3+1 containers: Frontend, Backend, Nginx Proxy (as a front-facing router) and CertBot. + +Frontend and Backend in this configuration are setup to communicate under a single endpoint, this is then handled by Nginx Proxy which forwards appropriate requests either to Frontend (for fetching static app data) or Backend (for interacting the proxy). Certbot handles generation of HTTPS certificates and it's only included for usage if it's assumed to host via HTTPS. + +Certbot in the current state only simplifies creation of new certificates but it's not automated. + +# Environment variables + +For environment variables please refer to `.env`. The example file in the repository contains ideal set of environment variables allowing user to setup the platform. + +```env +# sets up appropriate hostname for the Nginx server +EXTERNAL_HOST=inject.localhost + +# disable AAI, uncomment the next two lines to enable it +INJECT_NOAUTH=true +VITE_NOAUTH=$INJECT_NOAUTH + +# host parametres that should be setup for client and server to interact correctly +INJECT_HOST_ADDRESSES=$EXTERNAL_HOST +VITE_HTTP_HOST=$EXTERNAL_HOST +VITE_HTTP_WS=ws://$EXTERNAL_HOST/ttxbackend/api/v1/subscription +CORS_ALLOWED_ORIGINS=http://$EXTERNAL_HOST + +# for certbot generation, use with certbot-generator.yml +CERTBOT_EMAIL=noemail@inject.localhost + +# for email sending of AAI mails +INJECT_EMAIL_HOST= +INJECT_EMAIL_PORT= +INJECT_EMAIL_HOST_USER= +INJECT_EMAIL_HOST_PASSWORD= +INJECT_EMAIL_SENDER_ADDRESS= + +# enable logging to a specific file +# INJECT_LOGS= + +# enable debug mode for the backend +INJECT_DEBUG= + +# nginx settings, do not touch unless you know what you are doing +NGINX_DEFAULT_REQUEST=index.html +NGINX_ROOT=dist +``` + +Special care needs to be given to host parameters block, which establishes the link between the clients (users of the frontend application) and the server (backend). In case it's necessary to setup HTTPS it's needed to ensure that the connection strings set the protocol appopriately (`http` vs `https`, `ws` vs `wss`). The most important config of all is `EXTERNAL_HOST` which commands under which domain the INJECT platform will run on. + +# HTTP Deployment + +For HTTP deployment please refer to `deploy.sh` script in the folder, this script launches three Docker compose layers which form a complete script. The first one is the basal binding compose file (`docker-compose.yml`), which establishes the basic bindings between Frontend, Backend and Nginx Proxy. The second one (`compose-from-registry.yml`) sets up appopriate import paths for Frontend and Backend images, this can be interchangibly swapped for `compose-from-monorepo.yml` for development purposes. The last one (`compose-with-nginx.yml`) sets up Nginx Proxy to run in HTTP mode under port `80` and generates necessary nginx configuration from `nginx.conf.template`. + +# HTTPS Deployment + +In comparision to HTTP Deployment, HTTPS differs majorly in one way which is requirement to have HTTPS certificates generated. These certificates can be generated via `generate-cert.sh` script which will output certificates in the `./certbot` directory of the compose script for given `EXTERNAL_HOST` setup in `.env` file. + +It's necessary to ensure that `VITE_HTTP_HOST`, `VITE_HTTP_WS`, `CORS_ALLOWED_ORIGINS` have correctly setup prepending protocols, i.e. `https` and `wss`. If not, it will cause connection errors. + +When the certificates are generated, the only step needed to take is to execute `deploy-with-https.sh` which will run INJECT in HTTPS mode. The script is similar to `deploy.sh` with one caveat and that's usage of `compose-with-nginx-https.yml` instead of `compose-with-nginx.yml` which by essence does the same but uses different NGINX template (`nginx-https.conf.template`) and runs on ports `80`, `443`. + +# Disabling AAI + +To disable AAI please refer to `.env` file and patch env variables `INJECT_NOAUTH` and `VITE_NOAUTH`. diff --git a/docker/nginx-deployment/certbot-generator.yml b/docker/nginx-deployment/certbot-generator.yml new file mode 100644 index 0000000000000000000000000000000000000000..45a448942e8baa8fd95973da7eac548e98d2ca30 --- /dev/null +++ b/docker/nginx-deployment/certbot-generator.yml @@ -0,0 +1,21 @@ +services: + nginx: + image: nginx:latest + env_file: + - .env + ports: + - 80:80 + - 443:443 + volumes: + - ./01-substitute-env.sh:/docker-entrypoint.d/01-substitute-env.sh:ro + - ./nginx-certbot.conf.template:/etc/nginx/nginx.conf.template:ro + - ./certbot/www/:/var/www/certbot/:rw + - ./certbot/conf/:/etc/letsencrypt/:rw + certbot: + depends_on: + - nginx + image: certbot/certbot:latest + volumes: + - ./certbot/www/:/var/www/certbot/:rw + - ./certbot/conf/:/etc/letsencrypt/:rw + command: certonly --webroot --webroot-path=/var/www/certbot --email $CERTBOT_EMAIL --agree-tos --no-eff-email --force-renewal -d $EXTERNAL_HOST \ No newline at end of file diff --git a/docker/nginx-deployment/compose-from-monorepo.yml b/docker/nginx-deployment/compose-from-monorepo.yml index 9caaea30ce140d030bba92e90781bc009cd5fa71..26ab4b00424f412e4c605295362c64f0c2480ae9 100644 --- a/docker/nginx-deployment/compose-from-monorepo.yml +++ b/docker/nginx-deployment/compose-from-monorepo.yml @@ -1,5 +1,3 @@ -version: '2' - services: frontend: build: diff --git a/docker/nginx-deployment/compose-from-registry.yml b/docker/nginx-deployment/compose-from-registry.yml index 190887d177c6d3f22dd48b284aa90654307c7637..fdc4a28f96f49fa704ff494174c975c4704381f8 100644 --- a/docker/nginx-deployment/compose-from-registry.yml +++ b/docker/nginx-deployment/compose-from-registry.yml @@ -1 +1,5 @@ -# todo: establish a way to pull the latest image from the registry \ No newline at end of file +services: + frontend: + image: gitlab.fi.muni.cz:5050/inject/container-registry/frontend:latest + backend: + image: gitlab.fi.muni.cz:5050/inject/container-registry/backend:latest \ No newline at end of file diff --git a/docker/nginx-deployment/compose-with-nginx-https.yml b/docker/nginx-deployment/compose-with-nginx-https.yml new file mode 100644 index 0000000000000000000000000000000000000000..ef502d33d1ce37ae1640c9f8f11486a635ef099d --- /dev/null +++ b/docker/nginx-deployment/compose-with-nginx-https.yml @@ -0,0 +1,8 @@ +services: + nginx: + ports: + - 443:443 + volumes: + - ./nginx-https.conf.template:/etc/nginx/nginx.conf.template:ro + - ./certbot/www/:/var/www/certbot/:rw + - ./certbot/conf/:/etc/letsencrypt/:rw diff --git a/docker/nginx-deployment/compose-with-nginx.yml b/docker/nginx-deployment/compose-with-nginx.yml new file mode 100644 index 0000000000000000000000000000000000000000..1a27f87d29a62590b3b0a2d4255157cc0bbed94d --- /dev/null +++ b/docker/nginx-deployment/compose-with-nginx.yml @@ -0,0 +1,4 @@ +services: + nginx: + volumes: + - ./nginx.conf.template:/etc/nginx/nginx.conf.template:ro \ No newline at end of file diff --git a/docker/nginx-deployment/deploy-with-https.sh b/docker/nginx-deployment/deploy-with-https.sh new file mode 100755 index 0000000000000000000000000000000000000000..2ff0232f4083b7ffd069c32284c2627ed438c4c3 --- /dev/null +++ b/docker/nginx-deployment/deploy-with-https.sh @@ -0,0 +1 @@ +docker compose -f docker-compose.yml -f compose-from-registry.yml -f compose-with-nginx-https.yml up -d \ No newline at end of file diff --git a/docker/nginx-deployment/deploy.sh b/docker/nginx-deployment/deploy.sh new file mode 100755 index 0000000000000000000000000000000000000000..1b0b6f7bddd35e72b71247b24523b470d44f3321 --- /dev/null +++ b/docker/nginx-deployment/deploy.sh @@ -0,0 +1 @@ +docker compose -f docker-compose.yml -f compose-from-registry.yml -f compose-with-nginx.yml up -d \ No newline at end of file diff --git a/docker/nginx-deployment/docker-compose.yml b/docker/nginx-deployment/docker-compose.yml index ce036d87c5ca91ad113123b0f612278426dd888a..3f14c5238ffc92b03d28fdc6f2dca75fdce0727d 100644 --- a/docker/nginx-deployment/docker-compose.yml +++ b/docker/nginx-deployment/docker-compose.yml @@ -1,5 +1,3 @@ -version: '2' - services: frontend: networks: @@ -19,14 +17,13 @@ services: ports: - 80:80 volumes: - - ./nginx.conf:/etc/nginx/nginx.conf + - ./01-substitute-env.sh:/docker-entrypoint.d/01-substitute-env.sh:ro depends_on: - frontend - backend backend: env_file: - .env - command: gunicorn networks: inject-network: aliases: diff --git a/docker/nginx-deployment/generate-cert.sh b/docker/nginx-deployment/generate-cert.sh new file mode 100755 index 0000000000000000000000000000000000000000..49164703c857a0dadba95e6e17ed8da46599c6c1 --- /dev/null +++ b/docker/nginx-deployment/generate-cert.sh @@ -0,0 +1 @@ +docker compose -f ./certbot-generator.yml up \ No newline at end of file diff --git a/docker/nginx-deployment/nginx-certbot.conf.template b/docker/nginx-deployment/nginx-certbot.conf.template new file mode 100644 index 0000000000000000000000000000000000000000..6322b7ddbb06254308ade04b7c474a6ed2619600 --- /dev/null +++ b/docker/nginx-deployment/nginx-certbot.conf.template @@ -0,0 +1,17 @@ +events {} +http { + server { + return 403; + } + + server { + listen 80; + server_name $EXTERNAL_HOST; + + server_tokens off; + + location /.well-known/acme-challenge/ { + root /var/www/certbot; + } + } +} \ No newline at end of file diff --git a/docker/nginx-deployment/nginx-https.conf.template b/docker/nginx-deployment/nginx-https.conf.template new file mode 100644 index 0000000000000000000000000000000000000000..e1e0bc5a1fd7ca7de70c1fe35fca36ed32acf436 --- /dev/null +++ b/docker/nginx-deployment/nginx-https.conf.template @@ -0,0 +1,65 @@ +events {} +http { + map $http_upgrade $connection_upgrade { + default upgrade; + '' close; + } + + upstream websocket { + server backend:8000; + } + + server { + return 403; + } + + server { + listen 80; + server_name $EXTERNAL_HOST; + + server_tokens off; + + location / { + return 301 https://$host$request_uri; + } + } + + server { + listen 443 ssl; + server_name $EXTERNAL_HOST; + + ssl_certificate /etc/letsencrypt/live/$EXTERNAL_HOST/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/$EXTERNAL_HOST/privkey.pem; + + location /inject/api/v1/subscription { + proxy_pass http://websocket/inject/api/v1/subscription; + proxy_http_version 1.1; + proxy_set_header Connection $connection_upgrade; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Host $host; + proxy_pass_request_headers on; + + client_max_body_size 10m; + client_body_buffer_size 128k; + + proxy_connect_timeout 90; + proxy_send_timeout 90; + proxy_read_timeout 90; + } + location /inject/ { + proxy_pass http://backend:8000/inject/; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection $connection_upgrade; + proxy_set_header Host $host; + proxy_pass_request_headers on; + } + location / { + proxy_pass http://frontend/; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection $connection_upgrade; + proxy_set_header Host $host; + } + } +} \ No newline at end of file diff --git a/docker/nginx-deployment/nginx.conf b/docker/nginx-deployment/nginx.conf.template similarity index 83% rename from docker/nginx-deployment/nginx.conf rename to docker/nginx-deployment/nginx.conf.template index d4cadfae709c08bad9a83fec15cb0768f9051d6d..591efca633cdfed8773e1463f119212b225b9d71 100644 --- a/docker/nginx-deployment/nginx.conf +++ b/docker/nginx-deployment/nginx.conf.template @@ -8,15 +8,22 @@ http { upstream websocket { server backend:8000; } + + server { + return 403; + } server { listen 80; + server_name localhost $EXTERNAL_HOST; + location /inject/api/v1/subscription { - proxy_pass http://websocket/inject/api/v1/graphql; + proxy_pass http://websocket/inject/api/v1/subscription; proxy_http_version 1.1; proxy_set_header Connection $connection_upgrade; proxy_set_header Upgrade $http_upgrade; proxy_set_header Host $host; + proxy_pass_request_headers on; client_max_body_size 10m; client_body_buffer_size 128k; @@ -31,6 +38,7 @@ http { proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection $connection_upgrade; proxy_set_header Host $host; + proxy_pass_request_headers on; } location / { proxy_pass http://frontend/; diff --git a/frontend/src/logic/GraphiQL/index.tsx b/frontend/src/logic/GraphiQL/index.tsx index db694df46fc3e947e56251627796282649310130..fbf347ebaee213f2e02b456b2e480269baa46ee2 100644 --- a/frontend/src/logic/GraphiQL/index.tsx +++ b/frontend/src/logic/GraphiQL/index.tsx @@ -1,4 +1,5 @@ import { Button } from '@blueprintjs/core' +import { createGraphiQLFetcher } from '@graphiql/toolkit' import { useAuthIdentity } from '@inject/graphql/auth' import { useHost } from '@inject/graphql/connection/host' import { useWs } from '@inject/graphql/connection/ws' @@ -7,9 +8,8 @@ import { usePopupContext } from '@inject/shared/popup/PopupContext' import csrfFetch from '@inject/shared/utils/csrfFetch' import { Suspense, lazy, useEffect, useState } from 'react' -const GraphiQL = await lazy(() => import('graphiql')) +const GraphiQL = lazy(() => import('graphiql')) import('graphiql/graphiql.css') -const { createGraphiQLFetcher } = await import('@graphiql/toolkit') const GraphiQLPage = /* @__PURE__ */ () => { const ws = useWs() diff --git a/frontend/src/logic/Login/index.tsx b/frontend/src/logic/Login/index.tsx index 9f3adeb7978a3eedd4de47b3302e2a06ec3bd045..a4fc76a2865fb3494a59e4ae6146391c8a873853 100644 --- a/frontend/src/logic/Login/index.tsx +++ b/frontend/src/logic/Login/index.tsx @@ -35,7 +35,10 @@ const Login = () => { setState(LoginState.LOGIN_SUCCESS) localStorage.clear() setTimeout(() => { - apollo.refetchQueries({ include: [IdentityDocument] }) + apollo.writeQuery({ + query: IdentityDocument, + data: { identity: data.login?.user }, + }) nav('/') }, 1000) // Redirect to home page after 1 second, this hack allows Cookie to set properly } diff --git a/frontend/src/pages/(navbar)/graphiql.tsx b/frontend/src/pages/(navbar)/graphiql.tsx index 6a176d7243d04e0d0adbc4eb52d5441929336b18..f5b96b241e3c62b570a4b0777ee34e2fd80de646 100644 --- a/frontend/src/pages/(navbar)/graphiql.tsx +++ b/frontend/src/pages/(navbar)/graphiql.tsx @@ -1,5 +1,6 @@ -/* eslint-disable react/display-name */ -import GraphiQLPage from '@/logic/GraphiQL' +import { lazy } from 'react' + +const GraphiQLPage = lazy(() => import('@/logic/GraphiQL')) export const GraphiQL = () => <GraphiQLPage /> diff --git a/frontend/substituteEnv.sh b/frontend/substituteEnv.sh old mode 100644 new mode 100755 diff --git a/frontend/vite.config.mts b/frontend/vite.config.mts index 865f3ba9114a840d79fe5c0b5427f5ba3f12a361..3358d20e191134b88b8b87161c63a6e044ce31ef 100644 --- a/frontend/vite.config.mts +++ b/frontend/vite.config.mts @@ -90,44 +90,6 @@ export default () => { }, output: { compact: true, - manualChunks(id: string) { - if (id.includes('react-pdf') || id.includes('@cyntler')) { - return 'react-pdf'; - } - if (id.includes('@blueprint')) { - return '@blueprint'; - } - if (id.includes('@emotion')) { - return '@emotion'; - } - if (id.includes('react')) { - return 'react'; - } - if (id.includes('frontend/frontend')) { - return '@inject/frontend'; - } - if (id.includes('frontend/shared')) { - return '@inject/shared'; - } - if (id.includes('frontend/graphql')) { - return '@inject/graphql'; - } - if (id.includes('d3')) { - return 'd3'; - } - if (id.includes('framer')) { - return 'framer'; - } - if (id.includes('@apollo') || id.includes('subscriptions-transport-ws') || id.includes('graphql-tag') || id.includes('graphql')) { - return '@apollo'; - } - if (id.includes('ahooks')) { - return 'ahooks'; - } - if (id.includes('lodash')) { - return 'lodash'; - } - } } }, terserOptions: { diff --git a/shared/config/index.ts b/shared/config/index.ts index 043d83ca57b4da416e9d36a440b9d3f414832487..9659fafde1fe501781c0c86dc412803da1ceef3c 100644 --- a/shared/config/index.ts +++ b/shared/config/index.ts @@ -5,7 +5,7 @@ export const httpGraphql = (hostAddress: string) => export const httpHello = (hostAddress: string) => `${currentProtocol()}://${hostAddress}/inject/api/v1/version` export const wsGraphql = (hostAddress: string) => - `ws://${hostAddress}/inject/api/v1/subscription/` + `ws${currentProtocol() === 'https' ? 's' : ''}://${hostAddress}/ttxbackend/api/v1/subscription/` export const uploadDefinitionUrl = (hostAddress: string) => `${currentProtocol()}://${hostAddress}/inject/api/v1/exercise_definition/upload-definition` export const validateDefinitionUrl = (hostAddress: string) =>