Merge pull request #93 from SELab-2/chore/docker
feat: Dockerize project
This commit is contained in:
		
						commit
						edd643710d
					
				
					 18 changed files with 439 additions and 84 deletions
				
			
		
							
								
								
									
										7
									
								
								.dockerignore
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								.dockerignore
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,7 @@ | ||||||
|  | **/node_modules/ | ||||||
|  | **/dist | ||||||
|  | .git | ||||||
|  | npm-debug.log | ||||||
|  | .coverage | ||||||
|  | .coverage.* | ||||||
|  | .env | ||||||
|  | @ -1,10 +1,16 @@ | ||||||
| DWENGO_PORT=3000 | # | ||||||
|  | # Basic configuration | ||||||
|  | # | ||||||
|  | 
 | ||||||
|  | DWENGO_PORT=3000 # The port the backend will listen on | ||||||
| DWENGO_DB_HOST=localhost | DWENGO_DB_HOST=localhost | ||||||
| DWENGO_DB_PORT=5431 | DWENGO_DB_PORT=5431 | ||||||
| DWENGO_DB_USERNAME=postgres | DWENGO_DB_USERNAME=postgres | ||||||
| DWENGO_DB_PASSWORD=postgres | DWENGO_DB_PASSWORD=postgres | ||||||
| DWENGO_DB_UPDATE=true | DWENGO_DB_UPDATE=true | ||||||
| 
 | 
 | ||||||
|  | # Auth | ||||||
|  | 
 | ||||||
| DWENGO_AUTH_STUDENT_URL=http://localhost:7080/realms/student | DWENGO_AUTH_STUDENT_URL=http://localhost:7080/realms/student | ||||||
| DWENGO_AUTH_STUDENT_CLIENT_ID=dwengo | DWENGO_AUTH_STUDENT_CLIENT_ID=dwengo | ||||||
| DWENGO_AUTH_STUDENT_JWKS_ENDPOINT=http://localhost:7080/realms/student/protocol/openid-connect/certs | DWENGO_AUTH_STUDENT_JWKS_ENDPOINT=http://localhost:7080/realms/student/protocol/openid-connect/certs | ||||||
|  | @ -14,3 +20,9 @@ DWENGO_AUTH_TEACHER_JWKS_ENDPOINT=http://localhost:7080/realms/teacher/protocol/ | ||||||
| 
 | 
 | ||||||
| # Allow Vite dev-server to access the backend (for testing purposes). Don't forget to remove this in production! | # Allow Vite dev-server to access the backend (for testing purposes). Don't forget to remove this in production! | ||||||
| DWENGO_CORS_ALLOWED_ORIGINS=http://localhost:5173 | DWENGO_CORS_ALLOWED_ORIGINS=http://localhost:5173 | ||||||
|  | 
 | ||||||
|  | # | ||||||
|  | # Advanced configuration | ||||||
|  | # | ||||||
|  | 
 | ||||||
|  | # LOKI_HOST=http://localhost:9001      # The address of the Loki instance, used for logging | ||||||
|  |  | ||||||
|  | @ -1,6 +1,6 @@ | ||||||
| DWENGO_PORT=3000    # The port the backend will listen on | DWENGO_PORT=3000 # The port the backend will listen on | ||||||
| DWENGO_DB_HOST=domain-or-ip-of-database | DWENGO_DB_HOST=domain-or-ip-of-database | ||||||
| DWENGO_DB_PORT=5432 | DWENGO_DB_PORT=5431 | ||||||
| 
 | 
 | ||||||
| # Change this to the actual credentials of the user Dwengo should use in the backend | # Change this to the actual credentials of the user Dwengo should use in the backend | ||||||
| DWENGO_DB_USERNAME=postgres | DWENGO_DB_USERNAME=postgres | ||||||
|  |  | ||||||
							
								
								
									
										28
									
								
								backend/.env.production.example
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								backend/.env.production.example
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,28 @@ | ||||||
|  | DWENGO_PORT=3000 # The port the backend will listen on | ||||||
|  | DWENGO_DB_HOST=db # Name of the database container | ||||||
|  | DWENGO_DB_PORT=5431 | ||||||
|  | 
 | ||||||
|  | # Change this to the actual credentials of the user Dwengo should use in the backend | ||||||
|  | DWENGO_DB_NAME=postgres | ||||||
|  | DWENGO_DB_USERNAME=postgres | ||||||
|  | DWENGO_DB_PASSWORD=postgres | ||||||
|  | 
 | ||||||
|  | # Set this to true when the database scheme needs to be updated. In that case, take a backup first. | ||||||
|  | DWENGO_DB_UPDATE=false | ||||||
|  | 
 | ||||||
|  | # Data for the identity provider via which the students authenticate. | ||||||
|  | DWENGO_AUTH_STUDENT_URL=https://sel2-1.ugent.be/idp/realms/student | ||||||
|  | DWENGO_AUTH_STUDENT_CLIENT_ID=dwengo | ||||||
|  | DWENGO_AUTH_STUDENT_JWKS_ENDPOINT=http://idp:7080/idp/realms/student/protocol/openid-connect/certs # Name of the idp container | ||||||
|  | # Data for the identity provider via which the teachers authenticate. | ||||||
|  | DWENGO_AUTH_TEACHER_URL=https://sel2-1.ugent.be/idp/realms/teacher | ||||||
|  | DWENGO_AUTH_TEACHER_CLIENT_ID=dwengo | ||||||
|  | DWENGO_AUTH_TEACHER_JWKS_ENDPOINT=http://idp:7080/idp/realms/teacher/protocol/openid-connect/certs # Name of the idp container | ||||||
|  | 
 | ||||||
|  | # | ||||||
|  | # Advanced configuration | ||||||
|  | # | ||||||
|  | 
 | ||||||
|  | # Logging and monitoring | ||||||
|  | 
 | ||||||
|  | # LOKI_HOST=http://logging:3102 # The address of the Loki instance, used for logging | ||||||
							
								
								
									
										35
									
								
								backend/Dockerfile
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								backend/Dockerfile
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,35 @@ | ||||||
|  | FROM node:22 AS build-stage | ||||||
|  | 
 | ||||||
|  | WORKDIR /app | ||||||
|  | 
 | ||||||
|  | # Install dependencies | ||||||
|  | 
 | ||||||
|  | COPY package*.json ./ | ||||||
|  | COPY backend/package.json ./backend/ | ||||||
|  | 
 | ||||||
|  | RUN npm install --silent | ||||||
|  | 
 | ||||||
|  | # Build the backend | ||||||
|  | 
 | ||||||
|  | # Root tsconfig.json | ||||||
|  | COPY tsconfig.json ./ | ||||||
|  | 
 | ||||||
|  | WORKDIR /app/backend | ||||||
|  | 
 | ||||||
|  | COPY backend ./ | ||||||
|  | 
 | ||||||
|  | RUN npm run build | ||||||
|  | 
 | ||||||
|  | FROM node:22 AS production-stage | ||||||
|  | 
 | ||||||
|  | WORKDIR /app | ||||||
|  | 
 | ||||||
|  | COPY package-lock.json backend/package.json ./ | ||||||
|  | 
 | ||||||
|  | RUN npm install --silent --only=production | ||||||
|  | 
 | ||||||
|  | COPY --from=build-stage /app/backend/dist ./dist/ | ||||||
|  | 
 | ||||||
|  | EXPOSE 3000 | ||||||
|  | 
 | ||||||
|  | CMD ["node", "--env-file=.env", "dist/app.js"] | ||||||
|  | @ -1,23 +1,13 @@ | ||||||
| import express, { Express, Response } from 'express'; | import express, { Express } from 'express'; | ||||||
| import { initORM } from './orm.js'; | import { initORM } from './orm.js'; | ||||||
| 
 | 
 | ||||||
| import themeRoutes from './routes/themes.js'; |  | ||||||
| import learningPathRoutes from './routes/learning-paths.js'; |  | ||||||
| import learningObjectRoutes from './routes/learning-objects.js'; |  | ||||||
| 
 |  | ||||||
| import studentRouter from './routes/student.js'; |  | ||||||
| import groupRouter from './routes/group.js'; |  | ||||||
| import assignmentRouter from './routes/assignment.js'; |  | ||||||
| import submissionRouter from './routes/submission.js'; |  | ||||||
| import classRouter from './routes/class.js'; |  | ||||||
| import questionRouter from './routes/question.js'; |  | ||||||
| import authRouter from './routes/auth.js'; |  | ||||||
| import { authenticateUser } from './middleware/auth/auth.js'; | import { authenticateUser } from './middleware/auth/auth.js'; | ||||||
| import cors from './middleware/cors.js'; | import cors from './middleware/cors.js'; | ||||||
| import { getLogger, Logger } from './logging/initalize.js'; | import { getLogger, Logger } from './logging/initalize.js'; | ||||||
| import { responseTimeLogger } from './logging/responseTimeLogger.js'; | import { responseTimeLogger } from './logging/responseTimeLogger.js'; | ||||||
| import responseTime from 'response-time'; | import responseTime from 'response-time'; | ||||||
| import { EnvVars, getNumericEnvVar } from './util/envvars.js'; | import { EnvVars, getNumericEnvVar } from './util/envvars.js'; | ||||||
|  | import apiRouter from './routes/router.js'; | ||||||
| 
 | 
 | ||||||
| const logger: Logger = getLogger(); | const logger: Logger = getLogger(); | ||||||
| 
 | 
 | ||||||
|  | @ -29,30 +19,13 @@ app.use(express.json()); | ||||||
| app.use(responseTime(responseTimeLogger)); | app.use(responseTime(responseTimeLogger)); | ||||||
| app.use(authenticateUser); | app.use(authenticateUser); | ||||||
| 
 | 
 | ||||||
| // TODO Replace with Express routes
 | app.get('/api', apiRouter); | ||||||
| app.get('/', (_, res: Response) => { |  | ||||||
|     logger.debug('GET /'); |  | ||||||
|     res.json({ |  | ||||||
|         message: 'Hello Dwengo!🚀', |  | ||||||
|     }); |  | ||||||
| }); |  | ||||||
| 
 |  | ||||||
| app.use('/student', studentRouter); |  | ||||||
| app.use('/group', groupRouter); |  | ||||||
| app.use('/assignment', assignmentRouter); |  | ||||||
| app.use('/submission', submissionRouter); |  | ||||||
| app.use('/class', classRouter); |  | ||||||
| app.use('/question', questionRouter); |  | ||||||
| app.use('/auth', authRouter); |  | ||||||
| app.use('/theme', themeRoutes); |  | ||||||
| app.use('/learningPath', learningPathRoutes); |  | ||||||
| app.use('/learningObject', learningObjectRoutes); |  | ||||||
| 
 | 
 | ||||||
| async function startServer() { | async function startServer() { | ||||||
|     await initORM(); |     await initORM(); | ||||||
| 
 | 
 | ||||||
|     app.listen(port, () => { |     app.listen(port, () => { | ||||||
|         logger.info(`Server is running at http://localhost:${port}`); |         logger.info(`Server is running at http://localhost:${port}/api`); | ||||||
|     }); |     }); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
							
								
								
									
										35
									
								
								backend/src/routes/router.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								backend/src/routes/router.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,35 @@ | ||||||
|  | import { Response, Router } from 'express'; | ||||||
|  | import studentRouter from './student'; | ||||||
|  | import groupRouter from './group'; | ||||||
|  | import assignmentRouter from './assignment'; | ||||||
|  | import submissionRouter from './submission'; | ||||||
|  | import classRouter from './class'; | ||||||
|  | import questionRouter from './question'; | ||||||
|  | import authRouter from './auth'; | ||||||
|  | import themeRoutes from './themes'; | ||||||
|  | import learningPathRoutes from './learning-paths'; | ||||||
|  | import learningObjectRoutes from './learning-objects'; | ||||||
|  | import { getLogger, Logger } from '../logging/initalize'; | ||||||
|  | 
 | ||||||
|  | const router = Router(); | ||||||
|  | const logger: Logger = getLogger(); | ||||||
|  | 
 | ||||||
|  | router.get('/', (_, res: Response) => { | ||||||
|  |     logger.debug('GET /'); | ||||||
|  |     res.json({ | ||||||
|  |         message: 'Hello Dwengo!🚀', | ||||||
|  |     }); | ||||||
|  | }); | ||||||
|  | 
 | ||||||
|  | router.use('/student', studentRouter); | ||||||
|  | router.use('/group', groupRouter); | ||||||
|  | router.use('/assignment', assignmentRouter); | ||||||
|  | router.use('/submission', submissionRouter); | ||||||
|  | router.use('/class', classRouter); | ||||||
|  | router.use('/question', questionRouter); | ||||||
|  | router.use('/auth', authRouter); | ||||||
|  | router.use('/theme', themeRoutes); | ||||||
|  | router.use('/learningPath', learningPathRoutes); | ||||||
|  | router.use('/learningObject', learningObjectRoutes); | ||||||
|  | 
 | ||||||
|  | export default router; | ||||||
							
								
								
									
										72
									
								
								compose.override.yml
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										72
									
								
								compose.override.yml
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,72 @@ | ||||||
|  | # | ||||||
|  | # Use this configuration to test the production configuration locally. | ||||||
|  | # | ||||||
|  | # This configuration builds the frontend and backend services as Docker images, | ||||||
|  | # and uses the paths for the services, instead of ports. | ||||||
|  | # | ||||||
|  | services: | ||||||
|  |     web: | ||||||
|  |         build: | ||||||
|  |             context: . | ||||||
|  |             dockerfile: frontend/Dockerfile | ||||||
|  |         ports: | ||||||
|  |             - '8080:8080/tcp' | ||||||
|  |         restart: unless-stopped | ||||||
|  |         labels: | ||||||
|  |             - 'traefik.http.routers.web.rule=PathPrefix(`/`)' | ||||||
|  |             - 'traefik.http.services.web.loadbalancer.server.port=8080' | ||||||
|  | 
 | ||||||
|  |     api: | ||||||
|  |         build: | ||||||
|  |             context: . | ||||||
|  |             dockerfile: backend/Dockerfile | ||||||
|  |         ports: | ||||||
|  |             - '3000:3000/tcp' | ||||||
|  |         restart: unless-stopped | ||||||
|  |         volumes: | ||||||
|  |             - ./backend/.env:/app/.env | ||||||
|  |         depends_on: | ||||||
|  |             - db | ||||||
|  |             - logging | ||||||
|  |         labels: | ||||||
|  |             - 'traefik.http.routers.api.rule=PathPrefix(`/api`)' | ||||||
|  |             - 'traefik.http.services.api.loadbalancer.server.port=3000' | ||||||
|  | 
 | ||||||
|  |     idp: | ||||||
|  |         # Also see compose.yml | ||||||
|  |         labels: | ||||||
|  |             - 'traefik.http.routers.idp.rule=PathPrefix(`/idp`)' | ||||||
|  |             - 'traefik.http.services.idp.loadbalancer.server.port=7080' | ||||||
|  |         environment: | ||||||
|  |             PROXY_ADDRESS_FORWARDING: 'true' | ||||||
|  |             KC_HTTP_RELATIVE_PATH: '/idp' | ||||||
|  | 
 | ||||||
|  |     reverse-proxy: | ||||||
|  |         image: traefik:v3.3 | ||||||
|  |         command: | ||||||
|  |             # Enable web UI | ||||||
|  |             - '--api.insecure=true' | ||||||
|  | 
 | ||||||
|  |             # Add Docker provider | ||||||
|  |             - '--providers.docker=true' | ||||||
|  |             - '--providers.docker.exposedbydefault=true' | ||||||
|  | 
 | ||||||
|  |             # Add web entrypoint | ||||||
|  |             - '--entrypoints.web.address=:80/tcp' | ||||||
|  |         ports: | ||||||
|  |             - '9000:8080' | ||||||
|  |             - '80:80/tcp' | ||||||
|  |         restart: unless-stopped | ||||||
|  |         volumes: | ||||||
|  |             - /var/run/docker.sock:/var/run/docker.sock:ro | ||||||
|  | 
 | ||||||
|  |     dashboards: | ||||||
|  |         image: grafana/grafana:latest | ||||||
|  |         ports: | ||||||
|  |             - '9002:3000' | ||||||
|  |         volumes: | ||||||
|  |             - dwengo_grafana_data:/var/lib/grafana | ||||||
|  |         restart: unless-stopped | ||||||
|  | 
 | ||||||
|  | volumes: | ||||||
|  |     dwengo_grafana_data: | ||||||
							
								
								
									
										112
									
								
								compose.prod.yml
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										112
									
								
								compose.prod.yml
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,112 @@ | ||||||
|  | # | ||||||
|  | # This file is used to define the production environment for the project. | ||||||
|  | # It is used to deploy the project on a server. | ||||||
|  | # Should not be used for local development. | ||||||
|  | # | ||||||
|  | services: | ||||||
|  |     web: | ||||||
|  |         build: | ||||||
|  |             context: . | ||||||
|  |             dockerfile: frontend/Dockerfile | ||||||
|  |         restart: unless-stopped | ||||||
|  |         networks: | ||||||
|  |             - dwengo-1 | ||||||
|  |         labels: | ||||||
|  |             - 'traefik.enable=true' | ||||||
|  |             - 'traefik.http.routers.web.rule=PathPrefix(`/`)' | ||||||
|  |             - 'traefik.http.services.web.loadbalancer.server.port=8080' | ||||||
|  | 
 | ||||||
|  |     api: | ||||||
|  |         build: | ||||||
|  |             context: . | ||||||
|  |             dockerfile: backend/Dockerfile | ||||||
|  |         restart: unless-stopped | ||||||
|  |         volumes: | ||||||
|  |             # TODO Replace with environment keys | ||||||
|  |             - ./backend/.env:/app/.env | ||||||
|  |         depends_on: | ||||||
|  |             - db | ||||||
|  |             - logging | ||||||
|  |         networks: | ||||||
|  |             - dwengo-1 | ||||||
|  |         labels: | ||||||
|  |             - 'traefik.enable=true' | ||||||
|  |             - 'traefik.http.routers.api.rule=PathPrefix(`/api`)' | ||||||
|  |             - 'traefik.http.services.api.loadbalancer.server.port=3000' | ||||||
|  | 
 | ||||||
|  |     db: | ||||||
|  |         # Also see compose.yml | ||||||
|  |         networks: | ||||||
|  |             - dwengo-1 | ||||||
|  | 
 | ||||||
|  |     idp: | ||||||
|  |         # Also see compose.yml | ||||||
|  |         # TODO Replace with proper production command | ||||||
|  |         command: ['start-dev', '--http-port', '7080', '--https-port', '7443', '--import-realm'] | ||||||
|  |         networks: | ||||||
|  |             - dwengo-1 | ||||||
|  |         labels: | ||||||
|  |             - 'traefik.enable=true' | ||||||
|  |             - 'traefik.http.routers.idp.rule=PathPrefix(`/idp`)' | ||||||
|  |             - 'traefik.http.services.idp.loadbalancer.server.port=7080' | ||||||
|  |         env_file: | ||||||
|  |             - ./config/idp/.env | ||||||
|  |         environment: | ||||||
|  |             KC_HOSTNAME: 'sel2-1.ugent.be' | ||||||
|  |             PROXY_ADDRESS_FORWARDING: 'true' | ||||||
|  |             KC_PROXY_HEADERS: 'xforwarded' | ||||||
|  |             KC_HTTP_ENABLED: 'true' | ||||||
|  |             KC_HTTP_RELATIVE_PATH: '/idp' | ||||||
|  | 
 | ||||||
|  |     reverse-proxy: | ||||||
|  |         image: traefik:v3.3 | ||||||
|  |         ports: | ||||||
|  |             - '80:80/tcp' | ||||||
|  |             - '443:443/tcp' | ||||||
|  |         command: | ||||||
|  |             # Add Docker provider | ||||||
|  |             - '--providers.docker=true' | ||||||
|  |             - '--providers.docker.exposedbydefault=false' | ||||||
|  | 
 | ||||||
|  |             # Add web entrypoint | ||||||
|  |             - '--entrypoints.web.address=:80/tcp' | ||||||
|  |             - '--entrypoints.web.http.redirections.entryPoint.to=websecure' | ||||||
|  |             - '--entrypoints.web.http.redirections.entryPoint.scheme=https' | ||||||
|  | 
 | ||||||
|  |             # Add websecure entrypoint | ||||||
|  |             - '--entrypoints.websecure.address=:443/tcp' | ||||||
|  |             - '--entrypoints.websecure.http.tls=true' | ||||||
|  |             - '--entrypoints.websecure.http.tls.certResolver=letsencrypt' | ||||||
|  |             - '--entrypoints.websecure.http.tls.domains[0].main=sel2-1.ugent.be' | ||||||
|  | 
 | ||||||
|  |             # Certificates | ||||||
|  |             - '--certificatesresolvers.letsencrypt.acme.httpchallenge=true' | ||||||
|  |             - '--certificatesresolvers.letsencrypt.acme.httpchallenge.entrypoint=web' | ||||||
|  |             - '--certificatesresolvers.letsencrypt.acme.email=timo.demeyst@ugent.be' | ||||||
|  |             - '--certificatesresolvers.letsencrypt.acme.storage=/letsencrypt/acme.json' | ||||||
|  |         restart: unless-stopped | ||||||
|  |         volumes: | ||||||
|  |             - /var/run/docker.sock:/var/run/docker.sock:ro | ||||||
|  |             - dwengo_letsencrypt:/letsencrypt | ||||||
|  |         networks: | ||||||
|  |             - dwengo-1 | ||||||
|  | 
 | ||||||
|  |     logging: | ||||||
|  |         # Also see compose.yml | ||||||
|  |         networks: | ||||||
|  |             - dwengo-1 | ||||||
|  | 
 | ||||||
|  |     dashboards: | ||||||
|  |         image: grafana/grafana:latest | ||||||
|  |         ports: | ||||||
|  |             - '9002:3000' | ||||||
|  |         restart: unless-stopped | ||||||
|  |         volumes: | ||||||
|  |             - dwengo_grafana_data:/var/lib/grafana | ||||||
|  | 
 | ||||||
|  | volumes: | ||||||
|  |     dwengo_grafana_data: | ||||||
|  |     dwengo_letsencrypt: | ||||||
|  | 
 | ||||||
|  | networks: | ||||||
|  |     dwengo-1: | ||||||
|  | @ -1,38 +1,32 @@ | ||||||
|  | # | ||||||
|  | # Use this configuration during development. | ||||||
|  | # | ||||||
|  | # This configuration is suitable to access the services using their ports. | ||||||
|  | # | ||||||
| services: | services: | ||||||
|     db: |     db: | ||||||
|         image: postgres:latest |         image: postgres:latest | ||||||
|  |         ports: | ||||||
|  |             - '5431:5432' | ||||||
|  |         restart: unless-stopped | ||||||
|  |         volumes: | ||||||
|  |             - dwengo_postgres_data:/var/lib/postgresql/data | ||||||
|         environment: |         environment: | ||||||
|             POSTGRES_USER: postgres |             POSTGRES_USER: postgres | ||||||
|             POSTGRES_PASSWORD: postgres |             POSTGRES_PASSWORD: postgres | ||||||
|             POSTGRES_DB: postgres |             POSTGRES_DB: postgres | ||||||
|         ports: |  | ||||||
|             - '5431:5432' |  | ||||||
|         volumes: |  | ||||||
|             - dwengo_postgres_data:/var/lib/postgresql/data |  | ||||||
| 
 |  | ||||||
|     logging: |  | ||||||
|         image: grafana/loki:latest |  | ||||||
|         ports: |  | ||||||
|             - '3102:3102' |  | ||||||
|             - '9095:9095' |  | ||||||
|         volumes: |  | ||||||
|             - ./config/loki/config.yml:/etc/loki/config.yaml |  | ||||||
|             - dwengo_loki_data:/loki |  | ||||||
|         command: -config.file=/etc/loki/config.yaml |  | ||||||
|         restart: unless-stopped |  | ||||||
| 
 |  | ||||||
|     dashboards: |  | ||||||
|         image: grafana/grafana:latest |  | ||||||
|         ports: |  | ||||||
|             - '3100:3000' |  | ||||||
|         volumes: |  | ||||||
|             - dwengo_grafana_data:/var/lib/grafana |  | ||||||
|         restart: unless-stopped |  | ||||||
| 
 | 
 | ||||||
|     idp: # Based on: https://medium.com/@fingervinicius/easy-running-keycloak-with-docker-compose-b0d7a4ee2358 |     idp: # Based on: https://medium.com/@fingervinicius/easy-running-keycloak-with-docker-compose-b0d7a4ee2358 | ||||||
|         image: quay.io/keycloak/keycloak:latest |         image: quay.io/keycloak/keycloak:latest | ||||||
|  |         ports: | ||||||
|  |             - '7080:7080' | ||||||
|  |         #     - '7443:7443' | ||||||
|  |         command: ['start-dev', '--http-port', '7080', '--https-port', '7443', '--import-realm'] | ||||||
|  |         restart: unless-stopped | ||||||
|         volumes: |         volumes: | ||||||
|             - ./idp:/opt/keycloak/data/import |             - ./config/idp:/opt/keycloak/data/import | ||||||
|  |         depends_on: | ||||||
|  |             - db | ||||||
|         environment: |         environment: | ||||||
|             KC_HOSTNAME: localhost |             KC_HOSTNAME: localhost | ||||||
|             KC_HOSTNAME_PORT: 7080 |             KC_HOSTNAME_PORT: 7080 | ||||||
|  | @ -41,19 +35,18 @@ services: | ||||||
|             KC_BOOTSTRAP_ADMIN_PASSWORD: admin |             KC_BOOTSTRAP_ADMIN_PASSWORD: admin | ||||||
|             KC_HEALTH_ENABLED: 'true' |             KC_HEALTH_ENABLED: 'true' | ||||||
|             KC_LOG_LEVEL: info |             KC_LOG_LEVEL: info | ||||||
|         healthcheck: | 
 | ||||||
|             test: ['CMD', 'curl', '-f', 'http://localhost:7080/health/ready'] |     logging: | ||||||
|             interval: 15s |         image: grafana/loki:latest | ||||||
|             timeout: 2s |  | ||||||
|             retries: 15 |  | ||||||
|         command: ['start-dev', '--http-port', '7080', '--https-port', '7443', '--import-realm'] |  | ||||||
|         ports: |         ports: | ||||||
|             - '7080:7080' |             - '9001:3102' | ||||||
|             - '7443:7443' |             - '9095:9095' | ||||||
|         depends_on: |         command: -config.file=/etc/loki/config.yaml | ||||||
|             - db |         restart: unless-stopped | ||||||
|  |         volumes: | ||||||
|  |             - ./config/loki/config.yml:/etc/loki/config.yaml | ||||||
|  |             - dwengo_loki_data:/loki | ||||||
| 
 | 
 | ||||||
| volumes: | volumes: | ||||||
|     dwengo_postgres_data: |  | ||||||
|     dwengo_loki_data: |     dwengo_loki_data: | ||||||
|     dwengo_grafana_data: |     dwengo_postgres_data: | ||||||
|  | @ -620,7 +620,15 @@ | ||||||
|             "enabled": true, |             "enabled": true, | ||||||
|             "alwaysDisplayInConsole": false, |             "alwaysDisplayInConsole": false, | ||||||
|             "clientAuthenticatorType": "client-jwt", |             "clientAuthenticatorType": "client-jwt", | ||||||
|             "redirectUris": ["urn:ietf:wg:oauth:2.0:oob", "http://localhost:5173/*", "http://localhost:5173"], |             "redirectUris": [ | ||||||
|  |                 "urn:ietf:wg:oauth:2.0:oob", | ||||||
|  |                 "http://localhost:5173/*", | ||||||
|  |                 "http://localhost:5173", | ||||||
|  |                 "http://localhost/*", | ||||||
|  |                 "http://localhost", | ||||||
|  |                 "https://sel2-1.ugent.be/*", | ||||||
|  |                 "https://sel2-1.ugent.be" | ||||||
|  |             ], | ||||||
|             "webOrigins": ["+"], |             "webOrigins": ["+"], | ||||||
|             "notBefore": 0, |             "notBefore": 0, | ||||||
|             "bearerOnly": false, |             "bearerOnly": false, | ||||||
|  | @ -620,7 +620,15 @@ | ||||||
|             "enabled": true, |             "enabled": true, | ||||||
|             "alwaysDisplayInConsole": false, |             "alwaysDisplayInConsole": false, | ||||||
|             "clientAuthenticatorType": "client-secret", |             "clientAuthenticatorType": "client-secret", | ||||||
|             "redirectUris": ["urn:ietf:wg:oauth:2.0:oob", "http://localhost:5173/*", "http://localhost:5173"], |             "redirectUris": [ | ||||||
|  |                 "urn:ietf:wg:oauth:2.0:oob", | ||||||
|  |                 "http://localhost:5173/*", | ||||||
|  |                 "http://localhost:5173", | ||||||
|  |                 "http://localhost/*", | ||||||
|  |                 "http://localhost", | ||||||
|  |                 "https://sel2-1.ugent.be/*", | ||||||
|  |                 "https://sel2-1.ugent.be" | ||||||
|  |             ], | ||||||
|             "webOrigins": ["+"], |             "webOrigins": ["+"], | ||||||
|             "notBefore": 0, |             "notBefore": 0, | ||||||
|             "bearerOnly": false, |             "bearerOnly": false, | ||||||
							
								
								
									
										32
									
								
								config/nginx/nginx.conf
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								config/nginx/nginx.conf
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,32 @@ | ||||||
|  | worker_processes auto; | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | events { | ||||||
|  |     worker_connections 1024; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | http { | ||||||
|  |     include       mime.types; | ||||||
|  |     default_type  application/octet-stream; | ||||||
|  | 
 | ||||||
|  |     types { | ||||||
|  |         application/javascript mjs; | ||||||
|  |         text/css; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     server { | ||||||
|  |         listen 8080; | ||||||
|  | 
 | ||||||
|  |         location / { | ||||||
|  |             root /usr/share/nginx/html; | ||||||
|  |             try_files $uri $uri/ /index.html; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg)$ { | ||||||
|  |             root /usr/share/nginx/html; | ||||||
|  |             expires 1y; | ||||||
|  |             add_header Cache-Control "public"; | ||||||
|  |             try_files $uri =404; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										36
									
								
								frontend/Dockerfile
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								frontend/Dockerfile
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,36 @@ | ||||||
|  | FROM node:22 AS build-stage | ||||||
|  | 
 | ||||||
|  | # install simple http server for serving static content | ||||||
|  | RUN npm install -g http-server | ||||||
|  | 
 | ||||||
|  | WORKDIR /app | ||||||
|  | 
 | ||||||
|  | # Install dependencies | ||||||
|  | 
 | ||||||
|  | COPY package*.json ./ | ||||||
|  | COPY ./frontend/package.json ./frontend/ | ||||||
|  | 
 | ||||||
|  | RUN npm install --silent | ||||||
|  | 
 | ||||||
|  | # Build the frontend | ||||||
|  | 
 | ||||||
|  | # Root tsconfig.json | ||||||
|  | COPY tsconfig.json ./ | ||||||
|  | COPY assets ./assets/ | ||||||
|  | 
 | ||||||
|  | WORKDIR /app/frontend | ||||||
|  | 
 | ||||||
|  | COPY frontend ./ | ||||||
|  | 
 | ||||||
|  | RUN npx vite build | ||||||
|  | 
 | ||||||
|  | FROM nginx:stable AS production-stage | ||||||
|  | 
 | ||||||
|  | COPY config/nginx/nginx.conf /etc/nginx/nginx.conf | ||||||
|  | 
 | ||||||
|  | COPY --from=build-stage /app/assets /usr/share/nginx/html/assets | ||||||
|  | COPY --from=build-stage /app/frontend/dist /usr/share/nginx/html | ||||||
|  | 
 | ||||||
|  | EXPOSE 8080 | ||||||
|  | 
 | ||||||
|  | CMD ["nginx", "-g", "daemon off;"] | ||||||
|  | @ -1,5 +1,8 @@ | ||||||
| export const apiConfig = { | export const apiConfig = { | ||||||
|     baseUrl: window.location.hostname == "localhost" ? "http://localhost:3000" : window.location.origin, |     baseUrl: | ||||||
|  |         window.location.hostname === "localhost" && !(window.location.port === "80" || window.location.port === "") | ||||||
|  |             ? "http://localhost:3000/api" | ||||||
|  |             : window.location.origin + "/api", | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| export const loginRoute = "/login"; | export const loginRoute = "/login"; | ||||||
|  |  | ||||||
|  | @ -12,12 +12,13 @@ import apiClient from "@/services/api-client.ts"; | ||||||
| import router from "@/router"; | import router from "@/router"; | ||||||
| import type { AxiosError } from "axios"; | import type { AxiosError } from "axios"; | ||||||
| 
 | 
 | ||||||
| const authConfig = await loadAuthConfig(); | async function getUserManagers(): Promise<UserManagersForRoles> { | ||||||
| 
 |     const authConfig = await loadAuthConfig(); | ||||||
| const userManagers: UserManagersForRoles = { |     return { | ||||||
|     student: new UserManager(authConfig.student), |         student: new UserManager(authConfig.student), | ||||||
|     teacher: new UserManager(authConfig.teacher), |         teacher: new UserManager(authConfig.teacher), | ||||||
| }; |     }; | ||||||
|  | } | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  * Load the information about who is currently logged in from the IDP. |  * Load the information about who is currently logged in from the IDP. | ||||||
|  | @ -27,7 +28,7 @@ async function loadUser(): Promise<User | null> { | ||||||
|     if (!activeRole) { |     if (!activeRole) { | ||||||
|         return null; |         return null; | ||||||
|     } |     } | ||||||
|     const user = await userManagers[activeRole].getUser(); |     const user = await (await getUserManagers())[activeRole].getUser(); | ||||||
|     authState.user = user; |     authState.user = user; | ||||||
|     authState.accessToken = user?.access_token || null; |     authState.accessToken = user?.access_token || null; | ||||||
|     authState.activeRole = activeRole || null; |     authState.activeRole = activeRole || null; | ||||||
|  | @ -59,7 +60,7 @@ async function initiateLogin() { | ||||||
| async function loginAs(role: Role): Promise<void> { | async function loginAs(role: Role): Promise<void> { | ||||||
|     // Storing it in local storage so that it won't be lost when redirecting outside of the app.
 |     // Storing it in local storage so that it won't be lost when redirecting outside of the app.
 | ||||||
|     authStorage.setActiveRole(role); |     authStorage.setActiveRole(role); | ||||||
|     await userManagers[role].signinRedirect(); |     await (await getUserManagers())[role].signinRedirect(); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  | @ -70,7 +71,7 @@ async function handleLoginCallback(): Promise<void> { | ||||||
|     if (!activeRole) { |     if (!activeRole) { | ||||||
|         throw new Error("Login callback received, but the user is not logging in!"); |         throw new Error("Login callback received, but the user is not logging in!"); | ||||||
|     } |     } | ||||||
|     authState.user = (await userManagers[activeRole].signinCallback()) || null; |     authState.user = (await (await getUserManagers())[activeRole].signinCallback()) || null; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  | @ -84,7 +85,7 @@ async function renewToken() { | ||||||
|         return; |         return; | ||||||
|     } |     } | ||||||
|     try { |     try { | ||||||
|         return await userManagers[activeRole].signinSilent(); |         return await (await getUserManagers())[activeRole].signinSilent(); | ||||||
|     } catch (error) { |     } catch (error) { | ||||||
|         console.log("Can't renew the token:"); |         console.log("Can't renew the token:"); | ||||||
|         console.log(error); |         console.log(error); | ||||||
|  | @ -98,7 +99,7 @@ async function renewToken() { | ||||||
| async function logout(): Promise<void> { | async function logout(): Promise<void> { | ||||||
|     const activeRole = authStorage.getActiveRole(); |     const activeRole = authStorage.getActiveRole(); | ||||||
|     if (activeRole) { |     if (activeRole) { | ||||||
|         await userManagers[activeRole].signoutRedirect(); |         await (await getUserManagers())[activeRole].signoutRedirect(); | ||||||
|         authStorage.deleteActiveRole(); |         authStorage.deleteActiveRole(); | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
		Reference in a new issue
	
	 GitHub
							GitHub