Merging origin/dev into feat/assignment-page
This commit is contained in:
		
						commit
						2868435c62
					
				
					 87 changed files with 989 additions and 1002 deletions
				
			
		
							
								
								
									
										15
									
								
								README.md
									
										
									
									
									
								
							
							
						
						
									
										15
									
								
								README.md
									
										
									
									
									
								
							|  | @ -21,14 +21,16 @@ Alternatief kan je één van de volgende methodes gebruiken om de applicatie lok | ||||||
| 
 | 
 | ||||||
| ### Quick start | ### Quick start | ||||||
| 
 | 
 | ||||||
|  | Om de applicatie lokaal te draaien als kant-en-klare Docker-containers: | ||||||
|  | 
 | ||||||
| 1. Installeer Docker en Docker Compose op je systeem (zie [Docker](https://docs.docker.com/get-docker/) | 1. Installeer Docker en Docker Compose op je systeem (zie [Docker](https://docs.docker.com/get-docker/) | ||||||
|    en [Docker Compose](https://docs.docker.com/compose/)). |    en [Docker Compose](https://docs.docker.com/compose/)). | ||||||
| 2. Clone deze repository. | 2. Clone deze repository. | ||||||
| 3. In de backend, kopieer `.env.example` (of `.env.development.example`) naar `.env` en pas de variabelen aan waar | 3. In de backend, kopieer `.env.example` naar `.env` en pas de variabelen aan waar nodig. | ||||||
|    nodig. | 4. Voer `docker compose -f compose.staging.yml up --build` uit in de root van de repository. | ||||||
| 4. Voer `docker compose up` uit in de root van de repository. |  | ||||||
| 5. Optioneel: Configureer de applicatie aan de hand van | 5. Optioneel: Configureer de applicatie aan de hand van | ||||||
|    de [configuratiehandleiding](https://github.com/SELab-2/Dwengo-1/wiki/Administrator:-Productie-omgeving#dwengo-1-configuratie). |    de [configuratiehandleiding](https://github.com/SELab-2/Dwengo-1/wiki/Administrator:-Productie-omgeving#dwengo-1-configuratie). | ||||||
|  | 6. De applicatie is nu beschikbaar op [`http://localhost/`](http://localhost/) en [`http://localhost/api`](http://localhost/api). | ||||||
| 
 | 
 | ||||||
| ```bash | ```bash | ||||||
| docker compose version | docker compose version | ||||||
|  | @ -38,14 +40,13 @@ cp .env.example .env | ||||||
| # Pas .env aan | # Pas .env aan | ||||||
| nano .env | nano .env | ||||||
| cd .. | cd .. | ||||||
| docker compose up | docker compose -f compose.staging.yml up --build | ||||||
| # Configureer de applicatie |  | ||||||
| ``` | ``` | ||||||
| 
 | 
 | ||||||
| ### Handmatige installatie | ### Handmatige installatie en ontwikkeling | ||||||
| 
 | 
 | ||||||
| Zie de submappen voor de installatie-instructies van de [frontend](./frontend/README.md) | Zie de submappen voor de installatie-instructies van de [frontend](./frontend/README.md) | ||||||
| en [backend](./backend/README.md). | en [backend](./backend/README.md) en instructies voor het opzetten van een ontwikkelomgeving. | ||||||
| 
 | 
 | ||||||
| ## Architectuur | ## Architectuur | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -1,15 +1,24 @@ | ||||||
| # | # | ||||||
| # Basic configuration | # Development environment configuration | ||||||
|  | # | ||||||
|  | # You probably don't need to change these values, as this configuration takes | ||||||
|  | # the docker services and their default ports into account. | ||||||
| # | # | ||||||
| 
 | 
 | ||||||
| DWENGO_PORT=3000 # The port the backend will listen on | ### Dwengo ### | ||||||
|  | 
 | ||||||
|  | #DWENGO_PORT=3000 | ||||||
|  | #DWENGO_LEARNING_CONTENT_REPO_API_BASE_URL=https://dwengo.org/backend/api | ||||||
|  | #DWENGO_FALLBACK_LANGUAGE=nl | ||||||
|  | #DWENGO_RUN_MODE=dev | ||||||
|  | 
 | ||||||
| DWENGO_DB_HOST=localhost | DWENGO_DB_HOST=localhost | ||||||
| DWENGO_DB_PORT=5431 | DWENGO_DB_PORT=5431 | ||||||
|  | #DWENGO_DB_NAME=dwengo | ||||||
| DWENGO_DB_USERNAME=postgres | DWENGO_DB_USERNAME=postgres | ||||||
| DWENGO_DB_PASSWORD=postgres | DWENGO_DB_PASSWORD=postgres | ||||||
| DWENGO_DB_UPDATE=true | DWENGO_DB_UPDATE=true | ||||||
| 
 | #DWENGO_DB_CONTENT_PREFIX=u_ | ||||||
| # 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 | ||||||
|  | @ -17,12 +26,12 @@ DWENGO_AUTH_STUDENT_JWKS_ENDPOINT=http://localhost:7080/realms/student/protocol/ | ||||||
| DWENGO_AUTH_TEACHER_URL=http://localhost:7080/realms/teacher | DWENGO_AUTH_TEACHER_URL=http://localhost:7080/realms/teacher | ||||||
| DWENGO_AUTH_TEACHER_CLIENT_ID=dwengo | DWENGO_AUTH_TEACHER_CLIENT_ID=dwengo | ||||||
| DWENGO_AUTH_TEACHER_JWKS_ENDPOINT=http://localhost:7080/realms/teacher/protocol/openid-connect/certs | DWENGO_AUTH_TEACHER_JWKS_ENDPOINT=http://localhost:7080/realms/teacher/protocol/openid-connect/certs | ||||||
|  | #DWENGO_AUTH_AUDIENCE=account | ||||||
| 
 | 
 | ||||||
| # 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 | ||||||
|  | #DWENGO_CORS_ALLOWED_HEADERS=Authorization,Content-Type | ||||||
| 
 | 
 | ||||||
| # | ### Advanced configuration ### | ||||||
| # Advanced configuration |  | ||||||
| # |  | ||||||
| 
 | 
 | ||||||
| # LOKI_HOST=http://localhost:9001      # The address of the Loki instance, used for logging | DWENGO_LOGGING_LEVEL=debug | ||||||
|  | #DWENGO_LOGGING_LOKI_HOST=http://localhost:3102 | ||||||
|  |  | ||||||
|  | @ -1,27 +1,68 @@ | ||||||
| # | # | ||||||
| # Basic configuration | # Basic configuration | ||||||
| # | # | ||||||
|  | # Change the values of the variables below to match your environment! | ||||||
|  | # Default values are commented out. | ||||||
|  | # | ||||||
| 
 | 
 | ||||||
| DWENGO_PORT=3000 # The port the backend will listen on | ### Dwengo ### | ||||||
|  | 
 | ||||||
|  | # Port the backend will listen on | ||||||
|  | #DWENGO_PORT=3000 | ||||||
|  | # The hostname or IP address of the remote learning content API. | ||||||
|  | #DWENGO_LEARNING_CONTENT_REPO_API_BASE_URL=https://dwengo.org/backend/api | ||||||
|  | # The default fallback language. | ||||||
|  | #DWENGO_FALLBACK_LANGUAGE=nl | ||||||
|  | # Whether running in production mode or not. Possible values are "prod" or "dev". | ||||||
|  | #DWENGO_RUN_MODE=dev | ||||||
|  | 
 | ||||||
|  | # ! Change this! The hostname or IP address of the database | ||||||
|  | # If running your stack in docker, this should use the docker service name. | ||||||
| DWENGO_DB_HOST=domain-or-ip-of-database | DWENGO_DB_HOST=domain-or-ip-of-database | ||||||
| DWENGO_DB_PORT=5431 | # The port of the database. | ||||||
| 
 | #DWENGO_DB_PORT=5432 | ||||||
| # Change this to the actual credentials of the user Dwengo should use in the backend | # The name of the database. | ||||||
| DWENGO_DB_USERNAME=postgres | #DWENGO_DB_NAME=dwengo | ||||||
| DWENGO_DB_PASSWORD=postgres | # ! Change this! The username of the database user. | ||||||
| 
 | DWENGO_DB_USERNAME=username | ||||||
|  | # ! Change this! The password of the database user. | ||||||
|  | DWENGO_DB_PASSWORD=password | ||||||
|  | # Whether the database scheme needs to be updated. | ||||||
| # Set this to true when the database scheme needs to be updated. In that case, take a backup first. | # Set this to true when the database scheme needs to be updated. In that case, take a backup first. | ||||||
| DWENGO_DB_UPDATE=false | #DWENGO_DB_UPDATE=false | ||||||
|  | # The prefix used for custom user content. | ||||||
|  | #DWENGO_DB_CONTENT_PREFIX=u_ | ||||||
| 
 | 
 | ||||||
| # Data for the identity provider via which the students authenticate. | # ! Change this! The external URL for student authentication. Should be reachable by the client. | ||||||
| DWENGO_AUTH_STUDENT_URL=http://localhost:7080/realms/student | # E.g. https://sel2-1.ugent.be/idp/realms/student | ||||||
|  | DWENGO_AUTH_STUDENT_URL=http://hostname/idp/realms/student | ||||||
|  | # ! Change this! The client ID for student authentication. | ||||||
| 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 | # ! Change this! The internal URL for retrieving the JWKS for student authentication. | ||||||
| 
 | # Should be reachable by the backend. If running your stack in docker, this should use the docker service name. | ||||||
| # Data for the identity provider via which the teachers authenticate. | # E.g. http://idp:7080/realms/student/protocol/openid-connect/certs | ||||||
| DWENGO_AUTH_TEACHER_URL=http://localhost:7080/realms/teacher | DWENGO_AUTH_STUDENT_JWKS_ENDPOINT=http://hostname/realms/student/protocol/openid-connect/certs | ||||||
|  | # ! Change this! The external URL for teacher authentication. Should be reachable by the client. | ||||||
|  | # E.g. https://sel2-1.ugent.be/idp/realms/teacher | ||||||
|  | DWENGO_AUTH_TEACHER_URL=http://hostname/idp/realms/teacher | ||||||
|  | # ! Change this! The client ID for teacher authentication. | ||||||
| DWENGO_AUTH_TEACHER_CLIENT_ID=dwengo | DWENGO_AUTH_TEACHER_CLIENT_ID=dwengo | ||||||
| DWENGO_AUTH_TEACHER_JWKS_ENDPOINT=http://localhost:7080/realms/teacher/protocol/openid-connect/certs | # ! Change this! The internal URL for retrieving the JWKS for teacher authentication. | ||||||
|  | # Should be reachable by the backend. If running your stack in docker, this should use the docker service name. | ||||||
|  | # E.g. http://idp:7080/realms/teacher/protocol/openid-connect/certs | ||||||
|  | DWENGO_AUTH_TEACHER_JWKS_ENDPOINT=http://hostname/realms/teacher/protocol/openid-connect/certs | ||||||
|  | # The IDP audience | ||||||
|  | #DWENGO_AUTH_AUDIENCE=account | ||||||
| 
 | 
 | ||||||
| # The address of the Lokiinstance, used for logging | # Allowed origins for CORS requests. Separate multiple origins with a comma. | ||||||
| # LOKI_HOST=http://localhost:3102 | #DWENGO_CORS_ALLOWED_ORIGINS= | ||||||
|  | # Allowed headers for CORS requests. Separate multiple headers with a comma. | ||||||
|  | #DWENGO_CORS_ALLOWED_HEADERS=Authorization,Content-Type | ||||||
|  | 
 | ||||||
|  | ### Advanced configuration ### | ||||||
|  | 
 | ||||||
|  | # The logging level. Possible values are "debug", "info", "warn", "error". | ||||||
|  | #DWENGO_LOGGING_LEVEL=info | ||||||
|  | # The address of the Loki instance, a log aggregation system. | ||||||
|  | # If running your stack in docker, this should use the docker service name. | ||||||
|  | #DWENGO_LOGGING_LOKI_HOST=http://localhost:3102 | ||||||
|  |  | ||||||
|  | @ -1,28 +1,37 @@ | ||||||
| DWENGO_PORT=3000 # The port the backend will listen on | # | ||||||
| DWENGO_DB_HOST=db # Name of the database container | # Production environment configuration | ||||||
| DWENGO_DB_PORT=5431 | # | ||||||
|  | # Change the values of the variables below to match your production environment! | ||||||
|  | # See .env.example for more information. | ||||||
|  | # | ||||||
| 
 | 
 | ||||||
| # Change this to the actual credentials of the user Dwengo should use in the backend | ### Dwengo ### | ||||||
|  | 
 | ||||||
|  | DWENGO_PORT=3000 | ||||||
|  | #DWENGO_LEARNING_CONTENT_REPO_API_BASE_URL=https://dwengo.org/backend/api | ||||||
|  | #DWENGO_FALLBACK_LANGUAGE=nl | ||||||
|  | DWENGO_RUN_MODE=prod | ||||||
|  | 
 | ||||||
|  | DWENGO_DB_HOST=db | ||||||
|  | DWENGO_DB_PORT=5432 | ||||||
| DWENGO_DB_NAME=postgres | DWENGO_DB_NAME=postgres | ||||||
| DWENGO_DB_USERNAME=postgres | DWENGO_DB_USERNAME=postgres | ||||||
| DWENGO_DB_PASSWORD=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 | DWENGO_DB_UPDATE=false | ||||||
|  | #DWENGO_DB_CONTENT_PREFIX=u_ | ||||||
| 
 | 
 | ||||||
| # 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_URL=https://sel2-1.ugent.be/idp/realms/student | ||||||
| DWENGO_AUTH_STUDENT_CLIENT_ID=dwengo | 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 | 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_URL=https://sel2-1.ugent.be/idp/realms/teacher | ||||||
| DWENGO_AUTH_TEACHER_CLIENT_ID=dwengo | 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 | DWENGO_AUTH_TEACHER_JWKS_ENDPOINT=http://idp:7080/idp/realms/teacher/protocol/openid-connect/certs # Name of the idp container | ||||||
|  | #DWENGO_AUTH_AUDIENCE=account | ||||||
| 
 | 
 | ||||||
| # | #DWENGO_CORS_ALLOWED_ORIGINS= | ||||||
| # Advanced configuration | #DWENGO_CORS_ALLOWED_HEADERS=Authorization,Content-Type | ||||||
| # |  | ||||||
| 
 | 
 | ||||||
| # Logging and monitoring | ### Advanced configuration ### | ||||||
| 
 | 
 | ||||||
| # LOKI_HOST=http://logging:3102 # The address of the Loki instance, used for logging | DWENGO_LOGGING_LEVEL=info | ||||||
|  | DWENGO_LOGGING_LOKI_HOST=http://logging:3102 | ||||||
|  |  | ||||||
|  | @ -1,3 +1,13 @@ | ||||||
| PORT=3000 | # | ||||||
| DWENGO_DB_UPDATE=true | # Test environment configuration | ||||||
|  | # | ||||||
|  | # Should not need to be modified. | ||||||
|  | # See .env.example for more information. | ||||||
|  | # | ||||||
|  | 
 | ||||||
|  | ### Dwengo ### | ||||||
|  | 
 | ||||||
|  | DWENGO_PORT=3000 | ||||||
|  | 
 | ||||||
| DWENGO_DB_NAME=":memory:" | DWENGO_DB_NAME=":memory:" | ||||||
|  | DWENGO_DB_UPDATE=true | ||||||
|  |  | ||||||
|  | @ -30,6 +30,7 @@ COPY package-lock.json backend/package.json ./ | ||||||
| RUN npm install --silent --only=production | RUN npm install --silent --only=production | ||||||
| 
 | 
 | ||||||
| COPY ./docs /docs | COPY ./docs /docs | ||||||
|  | COPY ./backend/i18n /app/i18n | ||||||
| COPY --from=build-stage /app/backend/dist ./dist/ | COPY --from=build-stage /app/backend/dist ./dist/ | ||||||
| 
 | 
 | ||||||
| EXPOSE 3000 | EXPOSE 3000 | ||||||
|  |  | ||||||
|  | @ -4,23 +4,24 @@ | ||||||
| 
 | 
 | ||||||
| ```shell | ```shell | ||||||
| npm install | npm install | ||||||
|  | 
 | ||||||
|  | # Start de nodige services voor ontwikkeling | ||||||
|  | cd ../ # Ga naar de root van de repository | ||||||
|  | docker compose up -d | ||||||
| ``` | ``` | ||||||
| 
 | 
 | ||||||
| Setup the environment variables in a `.env` file in the root of the project. You can use the `.env.example` file as a template. | Zet de omgevingsvariabelen in een `.env` bestand in de root van het project. | ||||||
|  | Je kan het `.env.example` bestand als template gebruiken. | ||||||
| 
 | 
 | ||||||
| ### Development | ### Ontwikkeling | ||||||
| 
 | 
 | ||||||
| ```shell | ```shell | ||||||
|  | # Omgevingsvariabelen | ||||||
|  | cp .env.development.example .env.development.local | ||||||
|  | 
 | ||||||
| npm run dev | npm run dev | ||||||
| ``` | ``` | ||||||
| 
 | 
 | ||||||
| ### Production |  | ||||||
| 
 |  | ||||||
| ```shell |  | ||||||
| npm run build |  | ||||||
| npm run start |  | ||||||
| ``` |  | ||||||
| 
 |  | ||||||
| ### Tests | ### Tests | ||||||
| 
 | 
 | ||||||
| Voer volgend commando uit om de unit tests uit te voeren: | Voer volgend commando uit om de unit tests uit te voeren: | ||||||
|  | @ -29,6 +30,18 @@ Voer volgend commando uit om de unit tests uit te voeren: | ||||||
| npm run test:unit | npm run test:unit | ||||||
| ``` | ``` | ||||||
| 
 | 
 | ||||||
|  | ### Productie | ||||||
|  | 
 | ||||||
|  | ```shell | ||||||
|  | # Omgevingsvariabelen | ||||||
|  | cp .env.development.example .env | ||||||
|  | 
 | ||||||
|  | npm run build | ||||||
|  | npm run start | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | Zie ook de [installatiehandleiding](https://github.com/SELab-2/Dwengo-1/wiki/Administrator:-Productie-omgeving). | ||||||
|  | 
 | ||||||
| ## Keycloak configuratie | ## Keycloak configuratie | ||||||
| 
 | 
 | ||||||
| Tijdens development is het voldoende om gebruik te maken van de keycloak configuratie die automatisch ingeladen wordt. | Tijdens development is het voldoende om gebruik te maken van de keycloak configuratie die automatisch ingeladen wordt. | ||||||
|  |  | ||||||
|  | @ -9,6 +9,7 @@ import { EnvVars, getNumericEnvVar } from './util/envvars.js'; | ||||||
| import apiRouter from './routes/router.js'; | import apiRouter from './routes/router.js'; | ||||||
| import swaggerMiddleware from './swagger.js'; | import swaggerMiddleware from './swagger.js'; | ||||||
| import swaggerUi from 'swagger-ui-express'; | import swaggerUi from 'swagger-ui-express'; | ||||||
|  | import { errorHandler } from './middleware/error-handling/error-handler.js'; | ||||||
| 
 | 
 | ||||||
| const logger: Logger = getLogger(); | const logger: Logger = getLogger(); | ||||||
| 
 | 
 | ||||||
|  | @ -26,6 +27,8 @@ app.use('/api', apiRouter); | ||||||
| // Swagger
 | // Swagger
 | ||||||
| app.use('/api-docs', swaggerUi.serve, swaggerMiddleware); | app.use('/api-docs', swaggerUi.serve, swaggerMiddleware); | ||||||
| 
 | 
 | ||||||
|  | app.use(errorHandler); | ||||||
|  | 
 | ||||||
| async function startServer() { | async function startServer() { | ||||||
|     await initORM(); |     await initORM(); | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -1,12 +1,7 @@ | ||||||
| import { EnvVars, getEnvVar } from './util/envvars.js'; | import { EnvVars, getEnvVar } from './util/envvars.js'; | ||||||
| import { Language } from './entities/content/language.js'; |  | ||||||
| 
 | 
 | ||||||
| // API
 | // API
 | ||||||
| export const DWENGO_API_BASE = getEnvVar(EnvVars.LearningContentRepoApiBaseUrl); | export const DWENGO_API_BASE = getEnvVar(EnvVars.LearningContentRepoApiBaseUrl); | ||||||
| export const FALLBACK_LANG = getEnvVar(EnvVars.FallbackLanguage); | export const FALLBACK_LANG = getEnvVar(EnvVars.FallbackLanguage); | ||||||
| 
 | 
 | ||||||
| // Logging
 |  | ||||||
| export const LOG_LEVEL: string = 'development' === process.env.NODE_ENV ? 'debug' : 'info'; |  | ||||||
| export const LOKI_HOST: string = process.env.LOKI_HOST || 'http://localhost:3102'; |  | ||||||
| 
 |  | ||||||
| export const FALLBACK_SEQ_NUM = 1; | export const FALLBACK_SEQ_NUM = 1; | ||||||
|  |  | ||||||
|  | @ -37,7 +37,7 @@ export async function createAssignmentHandler(req: Request<AssignmentParams>, re | ||||||
|         return; |         return; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     res.status(201).json({ assignment: assignment }); |     res.status(201).json(assignment); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export async function getAssignmentHandler(req: Request<AssignmentParams>, res: Response): Promise<void> { | export async function getAssignmentHandler(req: Request<AssignmentParams>, res: Response): Promise<void> { | ||||||
|  | @ -62,13 +62,14 @@ export async function getAssignmentHandler(req: Request<AssignmentParams>, res: | ||||||
| export async function getAssignmentsSubmissionsHandler(req: Request<AssignmentParams>, res: Response): Promise<void> { | export async function getAssignmentsSubmissionsHandler(req: Request<AssignmentParams>, res: Response): Promise<void> { | ||||||
|     const classid = req.params.classid; |     const classid = req.params.classid; | ||||||
|     const assignmentNumber = +req.params.id; |     const assignmentNumber = +req.params.id; | ||||||
|  |     const full = req.query.full === 'true'; | ||||||
| 
 | 
 | ||||||
|     if (isNaN(assignmentNumber)) { |     if (isNaN(assignmentNumber)) { | ||||||
|         res.status(400).json({ error: 'Assignment id must be a number' }); |         res.status(400).json({ error: 'Assignment id must be a number' }); | ||||||
|         return; |         return; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     const submissions = await getAssignmentsSubmissions(classid, assignmentNumber); |     const submissions = await getAssignmentsSubmissions(classid, assignmentNumber, full); | ||||||
| 
 | 
 | ||||||
|     res.json({ |     res.json({ | ||||||
|         submissions: submissions, |         submissions: submissions, | ||||||
|  |  | ||||||
|  | @ -1,5 +1,5 @@ | ||||||
| import { Request, Response } from 'express'; | import { Request, Response } from 'express'; | ||||||
| import { createClass, getAllClasses, getClass, getClassStudents, getClassStudentsIds, getClassTeacherInvitations } from '../services/class.js'; | import { createClass, getAllClasses, getClass, getClassStudents, getClassStudentsIds, getClassTeacherInvitations } from '../services/classes.js'; | ||||||
| import { ClassDTO } from '../interfaces/class.js'; | import { ClassDTO } from '../interfaces/class.js'; | ||||||
| 
 | 
 | ||||||
| export async function getAllClassesHandler(req: Request, res: Response): Promise<void> { | export async function getAllClassesHandler(req: Request, res: Response): Promise<void> { | ||||||
|  | @ -28,11 +28,10 @@ export async function createClassHandler(req: Request, res: Response): Promise<v | ||||||
|         return; |         return; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     res.status(201).json({ class: cls }); |     res.status(201).json(cls); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export async function getClassHandler(req: Request, res: Response): Promise<void> { | export async function getClassHandler(req: Request, res: Response): Promise<void> { | ||||||
|     try { |  | ||||||
|     const classId = req.params.id; |     const classId = req.params.id; | ||||||
|     const cls = await getClass(classId); |     const cls = await getClass(classId); | ||||||
| 
 | 
 | ||||||
|  | @ -40,18 +39,8 @@ export async function getClassHandler(req: Request, res: Response): Promise<void | ||||||
|         res.status(404).json({ error: 'Class not found' }); |         res.status(404).json({ error: 'Class not found' }); | ||||||
|         return; |         return; | ||||||
|     } |     } | ||||||
|         cls.endpoints = { |  | ||||||
|             self: `${req.baseUrl}/${req.params.id}`, |  | ||||||
|             invitations: `${req.baseUrl}/${req.params.id}/invitations`, |  | ||||||
|             assignments: `${req.baseUrl}/${req.params.id}/assignments`, |  | ||||||
|             students: `${req.baseUrl}/${req.params.id}/students`, |  | ||||||
|         }; |  | ||||||
| 
 | 
 | ||||||
|     res.json(cls); |     res.json(cls); | ||||||
|     } catch (error) { |  | ||||||
|         console.error('Error fetching learning objects:', error); |  | ||||||
|         res.status(500).json({ error: 'Internal server error' }); |  | ||||||
|     } |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export async function getClassStudentsHandler(req: Request, res: Response): Promise<void> { | export async function getClassStudentsHandler(req: Request, res: Response): Promise<void> { | ||||||
|  | @ -67,7 +56,7 @@ export async function getClassStudentsHandler(req: Request, res: Response): Prom | ||||||
| 
 | 
 | ||||||
| export async function getTeacherInvitationsHandler(req: Request, res: Response): Promise<void> { | export async function getTeacherInvitationsHandler(req: Request, res: Response): Promise<void> { | ||||||
|     const classId = req.params.id; |     const classId = req.params.id; | ||||||
|     const full = req.query.full === 'true'; // TODO: not implemented yet
 |     const full = req.query.full === 'true'; | ||||||
| 
 | 
 | ||||||
|     const invitations = await getClassTeacherInvitations(classId, full); |     const invitations = await getClassTeacherInvitations(classId, full); | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -28,6 +28,11 @@ export async function getGroupHandler(req: Request<GroupParams>, res: Response): | ||||||
| 
 | 
 | ||||||
|     const group = await getGroup(classId, assignmentId, groupId, full); |     const group = await getGroup(classId, assignmentId, groupId, full); | ||||||
| 
 | 
 | ||||||
|  |     if (!group) { | ||||||
|  |         res.status(404).json({ error: 'Group not found' }); | ||||||
|  |         return; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     res.json(group); |     res.json(group); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | @ -66,12 +71,12 @@ export async function createGroupHandler(req: Request, res: Response): Promise<v | ||||||
|         return; |         return; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     res.status(201).json({ group: group }); |     res.status(201).json(group); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export async function getGroupSubmissionsHandler(req: Request, res: Response): Promise<void> { | export async function getGroupSubmissionsHandler(req: Request, res: Response): Promise<void> { | ||||||
|     const classId = req.params.classid; |     const classId = req.params.classid; | ||||||
|     // Const full = req.query.full === 'true';
 |     const full = req.query.full === 'true'; | ||||||
| 
 | 
 | ||||||
|     const assignmentId = +req.params.assignmentid; |     const assignmentId = +req.params.assignmentid; | ||||||
| 
 | 
 | ||||||
|  | @ -87,7 +92,7 @@ export async function getGroupSubmissionsHandler(req: Request, res: Response): P | ||||||
|         return; |         return; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     const submissions = await getGroupSubmissions(classId, assignmentId, groupId); |     const submissions = await getGroupSubmissions(classId, assignmentId, groupId, full); | ||||||
| 
 | 
 | ||||||
|     res.json({ |     res.json({ | ||||||
|         submissions: submissions, |         submissions: submissions, | ||||||
|  |  | ||||||
|  | @ -4,9 +4,9 @@ import { FilteredLearningObject, LearningObjectIdentifier, LearningPathIdentifie | ||||||
| import learningObjectService from '../services/learning-objects/learning-object-service.js'; | import learningObjectService from '../services/learning-objects/learning-object-service.js'; | ||||||
| import { EnvVars, getEnvVar } from '../util/envvars.js'; | import { EnvVars, getEnvVar } from '../util/envvars.js'; | ||||||
| import { Language } from '../entities/content/language.js'; | import { Language } from '../entities/content/language.js'; | ||||||
| import { BadRequestException } from '../exceptions.js'; |  | ||||||
| import attachmentService from '../services/learning-objects/attachment-service.js'; | import attachmentService from '../services/learning-objects/attachment-service.js'; | ||||||
| import { NotFoundError } from '@mikro-orm/core'; | import { NotFoundError } from '@mikro-orm/core'; | ||||||
|  | import { BadRequestException } from '../exceptions/bad-request-exception.js'; | ||||||
| 
 | 
 | ||||||
| function getLearningObjectIdentifierFromRequest(req: Request): LearningObjectIdentifier { | function getLearningObjectIdentifierFromRequest(req: Request): LearningObjectIdentifier { | ||||||
|     if (!req.params.hruid) { |     if (!req.params.hruid) { | ||||||
|  | @ -40,7 +40,7 @@ export async function getAllLearningObjects(req: Request, res: Response): Promis | ||||||
|         learningObjects = await learningObjectService.getLearningObjectIdsFromPath(learningPathId); |         learningObjects = await learningObjectService.getLearningObjectIdsFromPath(learningPathId); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     res.json(learningObjects); |     res.json({ learningObjects: learningObjects }); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export async function getLearningObject(req: Request, res: Response): Promise<void> { | export async function getLearningObject(req: Request, res: Response): Promise<void> { | ||||||
|  |  | ||||||
|  | @ -2,13 +2,14 @@ import { Request, Response } from 'express'; | ||||||
| import { themes } from '../data/themes.js'; | import { themes } from '../data/themes.js'; | ||||||
| import { FALLBACK_LANG } from '../config.js'; | import { FALLBACK_LANG } from '../config.js'; | ||||||
| import learningPathService from '../services/learning-paths/learning-path-service.js'; | import learningPathService from '../services/learning-paths/learning-path-service.js'; | ||||||
| import { BadRequestException, NotFoundException } from '../exceptions.js'; |  | ||||||
| import { Language } from '../entities/content/language.js'; | import { Language } from '../entities/content/language.js'; | ||||||
| import { | import { | ||||||
|     PersonalizationTarget, |     PersonalizationTarget, | ||||||
|     personalizedForGroup, |     personalizedForGroup, | ||||||
|     personalizedForStudent, |     personalizedForStudent, | ||||||
| } from '../services/learning-paths/learning-path-personalization-util.js'; | } from '../services/learning-paths/learning-path-personalization-util.js'; | ||||||
|  | import { BadRequestException } from '../exceptions/bad-request-exception.js'; | ||||||
|  | import { NotFoundException } from '../exceptions/not-found-exception.js'; | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  * Fetch learning paths based on query parameters. |  * Fetch learning paths based on query parameters. | ||||||
|  |  | ||||||
|  | @ -48,7 +48,7 @@ export async function getAllQuestionsHandler(req: Request, res: Response): Promi | ||||||
|     if (!questions) { |     if (!questions) { | ||||||
|         res.status(404).json({ error: `Questions not found.` }); |         res.status(404).json({ error: `Questions not found.` }); | ||||||
|     } else { |     } else { | ||||||
|         res.json(questions); |         res.json({ questions: questions }); | ||||||
|     } |     } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | @ -76,12 +76,12 @@ export async function getQuestionAnswersHandler(req: Request, res: Response): Pr | ||||||
|         return; |         return; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     const answers = getAnswersByQuestion(questionId, full); |     const answers = await getAnswersByQuestion(questionId, full); | ||||||
| 
 | 
 | ||||||
|     if (!answers) { |     if (!answers) { | ||||||
|         res.status(404).json({ error: `Questions not found.` }); |         res.status(404).json({ error: `Questions not found` }); | ||||||
|     } else { |     } else { | ||||||
|         res.json(answers); |         res.json({ answers: answers }); | ||||||
|     } |     } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | @ -96,7 +96,7 @@ export async function createQuestionHandler(req: Request, res: Response): Promis | ||||||
|     const question = await createQuestion(questionDTO); |     const question = await createQuestion(questionDTO); | ||||||
| 
 | 
 | ||||||
|     if (!question) { |     if (!question) { | ||||||
|         res.status(400).json({ error: 'Could not add question' }); |         res.status(400).json({ error: 'Could not create question' }); | ||||||
|     } else { |     } else { | ||||||
|         res.json(question); |         res.json(question); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  | @ -9,29 +9,21 @@ import { | ||||||
|     getStudentGroups, |     getStudentGroups, | ||||||
|     getStudentSubmissions, |     getStudentSubmissions, | ||||||
| } from '../services/students.js'; | } from '../services/students.js'; | ||||||
| import { ClassDTO } from '../interfaces/class.js'; |  | ||||||
| import { getAllAssignments } from '../services/assignments.js'; |  | ||||||
| import { getUserHandler } from './users.js'; |  | ||||||
| import { Student } from '../entities/users/student.entity.js'; |  | ||||||
| import { StudentDTO } from '../interfaces/student.js'; | import { StudentDTO } from '../interfaces/student.js'; | ||||||
| import { getStudentRepository } from '../data/repositories.js'; |  | ||||||
| import { UserDTO } from '../interfaces/user.js'; |  | ||||||
| 
 | 
 | ||||||
| // TODO: accept arguments (full, ...)
 | // TODO: accept arguments (full, ...)
 | ||||||
| // TODO: endpoints
 | // TODO: endpoints
 | ||||||
| export async function getAllStudentsHandler(req: Request, res: Response): Promise<void> { | export async function getAllStudentsHandler(req: Request, res: Response): Promise<void> { | ||||||
|     const full = req.query.full === 'true'; |     const full = req.query.full === 'true'; | ||||||
| 
 | 
 | ||||||
|     const studentRepository = getStudentRepository(); |     const students = await getAllStudents(full); | ||||||
| 
 |  | ||||||
|     const students: StudentDTO[] | string[] = full ? await getAllStudents() : await getAllStudents(); |  | ||||||
| 
 | 
 | ||||||
|     if (!students) { |     if (!students) { | ||||||
|         res.status(404).json({ error: `Student not found.` }); |         res.status(404).json({ error: `Student not found.` }); | ||||||
|         return; |         return; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     res.status(201).json(students); |     res.json({ students: students }); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export async function getStudentHandler(req: Request, res: Response): Promise<void> { | export async function getStudentHandler(req: Request, res: Response): Promise<void> { | ||||||
|  | @ -51,7 +43,7 @@ export async function getStudentHandler(req: Request, res: Response): Promise<vo | ||||||
|         return; |         return; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     res.status(201).json(user); |     res.json(user); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export async function createStudentHandler(req: Request, res: Response) { | export async function createStudentHandler(req: Request, res: Response) { | ||||||
|  | @ -65,6 +57,14 @@ export async function createStudentHandler(req: Request, res: Response) { | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     const newUser = await createStudent(userData); |     const newUser = await createStudent(userData); | ||||||
|  | 
 | ||||||
|  |     if (!newUser) { | ||||||
|  |         res.status(500).json({ | ||||||
|  |             error: 'Something went wrong while creating student', | ||||||
|  |         }); | ||||||
|  |         return; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     res.status(201).json(newUser); |     res.status(201).json(newUser); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | @ -88,7 +88,6 @@ export async function deleteStudentHandler(req: Request, res: Response) { | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export async function getStudentClassesHandler(req: Request, res: Response): Promise<void> { | export async function getStudentClassesHandler(req: Request, res: Response): Promise<void> { | ||||||
|     try { |  | ||||||
|     const full = req.query.full === 'true'; |     const full = req.query.full === 'true'; | ||||||
|     const username = req.params.id; |     const username = req.params.id; | ||||||
| 
 | 
 | ||||||
|  | @ -96,17 +95,7 @@ export async function getStudentClassesHandler(req: Request, res: Response): Pro | ||||||
| 
 | 
 | ||||||
|     res.json({ |     res.json({ | ||||||
|         classes: classes, |         classes: classes, | ||||||
|             endpoints: { |  | ||||||
|                 self: `${req.baseUrl}/${req.params.id}`, |  | ||||||
|                 classes: `${req.baseUrl}/${req.params.id}/invitations`, |  | ||||||
|                 questions: `${req.baseUrl}/${req.params.id}/assignments`, |  | ||||||
|                 students: `${req.baseUrl}/${req.params.id}/students`, |  | ||||||
|             }, |  | ||||||
|     }); |     }); | ||||||
|     } catch (error) { |  | ||||||
|         console.error('Error fetching learning objects:', error); |  | ||||||
|         res.status(500).json({ error: 'Internal server error' }); |  | ||||||
|     } |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // TODO
 | // TODO
 | ||||||
|  | @ -137,8 +126,9 @@ export async function getStudentGroupsHandler(req: Request, res: Response): Prom | ||||||
| 
 | 
 | ||||||
| export async function getStudentSubmissionsHandler(req: Request, res: Response): Promise<void> { | export async function getStudentSubmissionsHandler(req: Request, res: Response): Promise<void> { | ||||||
|     const username = req.params.id; |     const username = req.params.id; | ||||||
|  |     const full = req.query.full === 'true'; | ||||||
| 
 | 
 | ||||||
|     const submissions = await getStudentSubmissions(username); |     const submissions = await getStudentSubmissions(username, full); | ||||||
| 
 | 
 | ||||||
|     res.json({ |     res.json({ | ||||||
|         submissions: submissions, |         submissions: submissions, | ||||||
|  |  | ||||||
|  | @ -36,10 +36,11 @@ export async function createSubmissionHandler(req: Request, res: Response) { | ||||||
|     const submission = await createSubmission(submissionDTO); |     const submission = await createSubmission(submissionDTO); | ||||||
| 
 | 
 | ||||||
|     if (!submission) { |     if (!submission) { | ||||||
|         res.status(404).json({ error: 'Submission not added' }); |         res.status(400).json({ error: 'Failed to create submission' }); | ||||||
|     } else { |         return; | ||||||
|         res.json(submission); |  | ||||||
|     } |     } | ||||||
|  | 
 | ||||||
|  |     res.json(submission); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export async function deleteSubmissionHandler(req: Request, res: Response) { | export async function deleteSubmissionHandler(req: Request, res: Response) { | ||||||
|  | @ -53,7 +54,8 @@ export async function deleteSubmissionHandler(req: Request, res: Response) { | ||||||
| 
 | 
 | ||||||
|     if (!submission) { |     if (!submission) { | ||||||
|         res.status(404).json({ error: 'Submission not found' }); |         res.status(404).json({ error: 'Submission not found' }); | ||||||
|     } else { |         return; | ||||||
|         res.json(submission); |  | ||||||
|     } |     } | ||||||
|  | 
 | ||||||
|  |     res.json(submission); | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -4,33 +4,23 @@ import { | ||||||
|     deleteTeacher, |     deleteTeacher, | ||||||
|     getAllTeachers, |     getAllTeachers, | ||||||
|     getClassesByTeacher, |     getClassesByTeacher, | ||||||
|     getClassIdsByTeacher, |  | ||||||
|     getQuestionIdsByTeacher, |  | ||||||
|     getQuestionsByTeacher, |     getQuestionsByTeacher, | ||||||
|     getStudentIdsByTeacher, |  | ||||||
|     getStudentsByTeacher, |     getStudentsByTeacher, | ||||||
|     getTeacher, |     getTeacher, | ||||||
| } from '../services/teachers.js'; | } from '../services/teachers.js'; | ||||||
| import { ClassDTO } from '../interfaces/class.js'; |  | ||||||
| import { StudentDTO } from '../interfaces/student.js'; |  | ||||||
| import { QuestionDTO, QuestionId } from '../interfaces/question.js'; |  | ||||||
| import { Teacher } from '../entities/users/teacher.entity.js'; |  | ||||||
| import { TeacherDTO } from '../interfaces/teacher.js'; | import { TeacherDTO } from '../interfaces/teacher.js'; | ||||||
| import { getTeacherRepository } from '../data/repositories.js'; |  | ||||||
| 
 | 
 | ||||||
| export async function getAllTeachersHandler(req: Request, res: Response): Promise<void> { | export async function getAllTeachersHandler(req: Request, res: Response): Promise<void> { | ||||||
|     const full = req.query.full === 'true'; |     const full = req.query.full === 'true'; | ||||||
| 
 | 
 | ||||||
|     const teacherRepository = getTeacherRepository(); |     const teachers = await getAllTeachers(full); | ||||||
| 
 |  | ||||||
|     const teachers: TeacherDTO[] | string[] = full ? await getAllTeachers() : await getAllTeachers(); |  | ||||||
| 
 | 
 | ||||||
|     if (!teachers) { |     if (!teachers) { | ||||||
|         res.status(404).json({ error: `Teacher not found.` }); |         res.status(404).json({ error: `Teacher not found.` }); | ||||||
|         return; |         return; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     res.status(201).json(teachers); |     res.json({ teachers: teachers }); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export async function getTeacherHandler(req: Request, res: Response): Promise<void> { | export async function getTeacherHandler(req: Request, res: Response): Promise<void> { | ||||||
|  | @ -45,12 +35,12 @@ export async function getTeacherHandler(req: Request, res: Response): Promise<vo | ||||||
| 
 | 
 | ||||||
|     if (!user) { |     if (!user) { | ||||||
|         res.status(404).json({ |         res.status(404).json({ | ||||||
|             error: `User with username '${username}' not found.`, |             error: `Teacher '${username}' not found.`, | ||||||
|         }); |         }); | ||||||
|         return; |         return; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     res.status(201).json(user); |     res.json(user); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export async function createTeacherHandler(req: Request, res: Response) { | export async function createTeacherHandler(req: Request, res: Response) { | ||||||
|  | @ -64,6 +54,12 @@ export async function createTeacherHandler(req: Request, res: Response) { | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     const newUser = await createTeacher(userData); |     const newUser = await createTeacher(userData); | ||||||
|  | 
 | ||||||
|  |     if (!newUser) { | ||||||
|  |         res.status(400).json({ error: 'Failed to create teacher' }); | ||||||
|  |         return; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     res.status(201).json(newUser); |     res.status(201).json(newUser); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | @ -78,7 +74,7 @@ export async function deleteTeacherHandler(req: Request, res: Response) { | ||||||
|     const deletedUser = await deleteTeacher(username); |     const deletedUser = await deleteTeacher(username); | ||||||
|     if (!deletedUser) { |     if (!deletedUser) { | ||||||
|         res.status(404).json({ |         res.status(404).json({ | ||||||
|             error: `User with username '${username}' not found.`, |             error: `User '${username}' not found.`, | ||||||
|         }); |         }); | ||||||
|         return; |         return; | ||||||
|     } |     } | ||||||
|  | @ -87,7 +83,6 @@ export async function deleteTeacherHandler(req: Request, res: Response) { | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export async function getTeacherClassHandler(req: Request, res: Response): Promise<void> { | export async function getTeacherClassHandler(req: Request, res: Response): Promise<void> { | ||||||
|     try { |  | ||||||
|     const username = req.params.username as string; |     const username = req.params.username as string; | ||||||
|     const full = req.query.full === 'true'; |     const full = req.query.full === 'true'; | ||||||
| 
 | 
 | ||||||
|  | @ -96,17 +91,17 @@ export async function getTeacherClassHandler(req: Request, res: Response): Promi | ||||||
|         return; |         return; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|         const classes: ClassDTO[] | string[] = full ? await getClassesByTeacher(username) : await getClassIdsByTeacher(username); |     const classes = await getClassesByTeacher(username, full); | ||||||
| 
 | 
 | ||||||
|         res.status(201).json(classes); |     if (!classes) { | ||||||
|     } catch (error) { |         res.status(404).json({ error: 'Teacher not found' }); | ||||||
|         console.error('Error fetching classes by teacher:', error); |         return; | ||||||
|         res.status(500).json({ error: 'Internal server error' }); |  | ||||||
|     } |     } | ||||||
|  | 
 | ||||||
|  |     res.json({ classes: classes }); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export async function getTeacherStudentHandler(req: Request, res: Response): Promise<void> { | export async function getTeacherStudentHandler(req: Request, res: Response): Promise<void> { | ||||||
|     try { |  | ||||||
|     const username = req.params.username as string; |     const username = req.params.username as string; | ||||||
|     const full = req.query.full === 'true'; |     const full = req.query.full === 'true'; | ||||||
| 
 | 
 | ||||||
|  | @ -115,17 +110,17 @@ export async function getTeacherStudentHandler(req: Request, res: Response): Pro | ||||||
|         return; |         return; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|         const students: StudentDTO[] | string[] = full ? await getStudentsByTeacher(username) : await getStudentIdsByTeacher(username); |     const students = await getStudentsByTeacher(username, full); | ||||||
| 
 | 
 | ||||||
|         res.status(201).json(students); |     if (!students) { | ||||||
|     } catch (error) { |         res.status(404).json({ error: 'Teacher not found' }); | ||||||
|         console.error('Error fetching students by teacher:', error); |         return; | ||||||
|         res.status(500).json({ error: 'Internal server error' }); |  | ||||||
|     } |     } | ||||||
|  | 
 | ||||||
|  |     res.json({ students: students }); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export async function getTeacherQuestionHandler(req: Request, res: Response): Promise<void> { | export async function getTeacherQuestionHandler(req: Request, res: Response): Promise<void> { | ||||||
|     try { |  | ||||||
|     const username = req.params.username as string; |     const username = req.params.username as string; | ||||||
|     const full = req.query.full === 'true'; |     const full = req.query.full === 'true'; | ||||||
| 
 | 
 | ||||||
|  | @ -134,11 +129,12 @@ export async function getTeacherQuestionHandler(req: Request, res: Response): Pr | ||||||
|         return; |         return; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|         const questions: QuestionDTO[] | QuestionId[] = full ? await getQuestionsByTeacher(username) : await getQuestionIdsByTeacher(username); |     const questions = await getQuestionsByTeacher(username, full); | ||||||
| 
 | 
 | ||||||
|         res.status(201).json(questions); |     if (!questions) { | ||||||
|     } catch (error) { |         res.status(404).json({ error: 'Teacher not found' }); | ||||||
|         console.error('Error fetching questions by teacher:', error); |         return; | ||||||
|         res.status(500).json({ error: 'Internal server error' }); |  | ||||||
|     } |     } | ||||||
|  | 
 | ||||||
|  |     res.json({ questions: questions }); | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -1,91 +0,0 @@ | ||||||
| import { Request, Response } from 'express'; |  | ||||||
| import { UserService } from '../services/users.js'; |  | ||||||
| import { UserDTO } from '../interfaces/user.js'; |  | ||||||
| import { User } from '../entities/users/user.entity.js'; |  | ||||||
| 
 |  | ||||||
| export async function getAllUsersHandler<T extends User>(req: Request, res: Response, service: UserService<T>): Promise<void> { |  | ||||||
|     try { |  | ||||||
|         const full = req.query.full === 'true'; |  | ||||||
| 
 |  | ||||||
|         const users: UserDTO[] | string[] = full ? await service.getAllUsers() : await service.getAllUserIds(); |  | ||||||
| 
 |  | ||||||
|         if (!users) { |  | ||||||
|             res.status(404).json({ error: `Users not found.` }); |  | ||||||
|             return; |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         res.status(201).json(users); |  | ||||||
|     } catch (error) { |  | ||||||
|         console.error('❌ Error fetching users:', error); |  | ||||||
|         res.status(500).json({ error: 'Internal server error' }); |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| export async function getUserHandler<T extends User>(req: Request, res: Response, service: UserService<T>): Promise<void> { |  | ||||||
|     try { |  | ||||||
|         const username = req.params.username as string; |  | ||||||
| 
 |  | ||||||
|         if (!username) { |  | ||||||
|             res.status(400).json({ error: 'Missing required field: username' }); |  | ||||||
|             return; |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         const user = await service.getUserByUsername(username); |  | ||||||
| 
 |  | ||||||
|         if (!user) { |  | ||||||
|             res.status(404).json({ |  | ||||||
|                 error: `User with username '${username}' not found.`, |  | ||||||
|             }); |  | ||||||
|             return; |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         res.status(201).json(user); |  | ||||||
|     } catch (error) { |  | ||||||
|         console.error('❌ Error fetching users:', error); |  | ||||||
|         res.status(500).json({ error: 'Internal server error' }); |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| export async function createUserHandler<T extends User>(req: Request, res: Response, service: UserService<T>, UserClass: new () => T) { |  | ||||||
|     try { |  | ||||||
|         console.log('req', req); |  | ||||||
|         const userData = req.body as UserDTO; |  | ||||||
| 
 |  | ||||||
|         if (!userData.username || !userData.firstName || !userData.lastName) { |  | ||||||
|             res.status(400).json({ |  | ||||||
|                 error: 'Missing required fields: username, firstName, lastName', |  | ||||||
|             }); |  | ||||||
|             return; |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         const newUser = await service.createUser(userData, UserClass); |  | ||||||
|         res.status(201).json(newUser); |  | ||||||
|     } catch (error) { |  | ||||||
|         console.error('❌ Error creating user:', error); |  | ||||||
|         res.status(500).json({ error: 'Internal server error' }); |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| export async function deleteUserHandler<T extends User>(req: Request, res: Response, service: UserService<T>) { |  | ||||||
|     try { |  | ||||||
|         const username = req.params.username; |  | ||||||
| 
 |  | ||||||
|         if (!username) { |  | ||||||
|             res.status(400).json({ error: 'Missing required field: username' }); |  | ||||||
|             return; |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         const deletedUser = await service.deleteUser(username); |  | ||||||
|         if (!deletedUser) { |  | ||||||
|             res.status(404).json({ |  | ||||||
|                 error: `User with username '${username}' not found.`, |  | ||||||
|             }); |  | ||||||
|             return; |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         res.status(200).json(deletedUser); |  | ||||||
|     } catch (error) { |  | ||||||
|         console.error('❌ Error deleting user:', error); |  | ||||||
|         res.status(500).json({ error: 'Internal server error' }); |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  | @ -1,10 +1,12 @@ | ||||||
| import { EntityRepository, FilterQuery } from '@mikro-orm/core'; | import { EntityRepository, FilterQuery } from '@mikro-orm/core'; | ||||||
|  | import { EntityAlreadyExistsException } from '../exceptions/entity-already-exists-exception.js'; | ||||||
| 
 | 
 | ||||||
| export abstract class DwengoEntityRepository<T extends object> extends EntityRepository<T> { | export abstract class DwengoEntityRepository<T extends object> extends EntityRepository<T> { | ||||||
|     public async save(entity: T) { |     public async save(entity: T, options?: { preventOverwrite?: boolean }): Promise<void> { | ||||||
|         const em = this.getEntityManager(); |         if (options?.preventOverwrite && (await this.findOne(entity))) { | ||||||
|         em.persist(entity); |             throw new EntityAlreadyExistsException(`A ${this.getEntityName()} with this identifier already exists.`); | ||||||
|         await em.flush(); |         } | ||||||
|  |         await this.getEntityManager().persistAndFlush(entity); | ||||||
|     } |     } | ||||||
|     public async deleteWhere(query: FilterQuery<T>) { |     public async deleteWhere(query: FilterQuery<T>) { | ||||||
|         const toDelete = await this.findOne(query); |         const toDelete = await this.findOne(query); | ||||||
|  |  | ||||||
|  | @ -2,8 +2,6 @@ import { AnyEntity, EntityManager, EntityName, EntityRepository } from '@mikro-o | ||||||
| import { forkEntityManager } from '../orm.js'; | import { forkEntityManager } from '../orm.js'; | ||||||
| import { StudentRepository } from './users/student-repository.js'; | import { StudentRepository } from './users/student-repository.js'; | ||||||
| import { Student } from '../entities/users/student.entity.js'; | import { Student } from '../entities/users/student.entity.js'; | ||||||
| import { User } from '../entities/users/user.entity.js'; |  | ||||||
| import { UserRepository } from './users/user-repository.js'; |  | ||||||
| import { Teacher } from '../entities/users/teacher.entity.js'; | import { Teacher } from '../entities/users/teacher.entity.js'; | ||||||
| import { TeacherRepository } from './users/teacher-repository.js'; | import { TeacherRepository } from './users/teacher-repository.js'; | ||||||
| import { Class } from '../entities/classes/class.entity.js'; | import { Class } from '../entities/classes/class.entity.js'; | ||||||
|  |  | ||||||
|  | @ -1,5 +1,4 @@ | ||||||
| import { Student } from '../../entities/users/student.entity.js'; | import { Student } from '../../entities/users/student.entity.js'; | ||||||
| import { User } from '../../entities/users/user.entity.js'; |  | ||||||
| import { DwengoEntityRepository } from '../dwengo-entity-repository.js'; | import { DwengoEntityRepository } from '../dwengo-entity-repository.js'; | ||||||
| // Import { UserRepository } from './user-repository.js';
 | // Import { UserRepository } from './user-repository.js';
 | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -1,6 +1,5 @@ | ||||||
| import { Teacher } from '../../entities/users/teacher.entity.js'; | import { Teacher } from '../../entities/users/teacher.entity.js'; | ||||||
| import { DwengoEntityRepository } from '../dwengo-entity-repository.js'; | import { DwengoEntityRepository } from '../dwengo-entity-repository.js'; | ||||||
| import { UserRepository } from './user-repository.js'; |  | ||||||
| 
 | 
 | ||||||
| export class TeacherRepository extends DwengoEntityRepository<Teacher> { | export class TeacherRepository extends DwengoEntityRepository<Teacher> { | ||||||
|     public findByUsername(username: string): Promise<Teacher | null> { |     public findByUsername(username: string): Promise<Teacher | null> { | ||||||
|  |  | ||||||
|  | @ -1,4 +1,4 @@ | ||||||
| import { Collection, Entity, Enum, ManyToOne, OneToMany, PrimaryKey, Property } from '@mikro-orm/core'; | import { Entity, Enum, ManyToOne, OneToMany, PrimaryKey, Property } from '@mikro-orm/core'; | ||||||
| import { Class } from '../classes/class.entity.js'; | import { Class } from '../classes/class.entity.js'; | ||||||
| import { Group } from './group.entity.js'; | import { Group } from './group.entity.js'; | ||||||
| import { Language } from '../content/language.js'; | import { Language } from '../content/language.js'; | ||||||
|  |  | ||||||
|  | @ -1,4 +1,4 @@ | ||||||
| import { Collection, Entity, ManyToMany, ManyToOne, PrimaryKey } from '@mikro-orm/core'; | import { Entity, ManyToMany, ManyToOne, PrimaryKey } from '@mikro-orm/core'; | ||||||
| import { Assignment } from './assignment.entity.js'; | import { Assignment } from './assignment.entity.js'; | ||||||
| import { Student } from '../users/student.entity.js'; | import { Student } from '../users/student.entity.js'; | ||||||
| import { GroupRepository } from '../../data/assignments/group-repository.js'; | import { GroupRepository } from '../../data/assignments/group-repository.js'; | ||||||
|  |  | ||||||
|  | @ -3,6 +3,12 @@ import { Student } from '../users/student.entity.js'; | ||||||
| import { Class } from './class.entity.js'; | import { Class } from './class.entity.js'; | ||||||
| import { ClassJoinRequestRepository } from '../../data/classes/class-join-request-repository.js'; | import { ClassJoinRequestRepository } from '../../data/classes/class-join-request-repository.js'; | ||||||
| 
 | 
 | ||||||
|  | export enum ClassJoinRequestStatus { | ||||||
|  |     Open = 'open', | ||||||
|  |     Accepted = 'accepted', | ||||||
|  |     Declined = 'declined', | ||||||
|  | } | ||||||
|  | 
 | ||||||
| @Entity({ | @Entity({ | ||||||
|     repository: () => ClassJoinRequestRepository, |     repository: () => ClassJoinRequestRepository, | ||||||
| }) | }) | ||||||
|  | @ -22,9 +28,3 @@ export class ClassJoinRequest { | ||||||
|     @Enum(() => ClassJoinRequestStatus) |     @Enum(() => ClassJoinRequestStatus) | ||||||
|     status!: ClassJoinRequestStatus; |     status!: ClassJoinRequestStatus; | ||||||
| } | } | ||||||
| 
 |  | ||||||
| export enum ClassJoinRequestStatus { |  | ||||||
|     Open = 'open', |  | ||||||
|     Accepted = 'accepted', |  | ||||||
|     Declined = 'declined', |  | ||||||
| } |  | ||||||
|  |  | ||||||
|  | @ -13,12 +13,4 @@ export class Student extends User { | ||||||
| 
 | 
 | ||||||
|     @ManyToMany(() => Group) |     @ManyToMany(() => Group) | ||||||
|     groups!: Collection<Group>; |     groups!: Collection<Group>; | ||||||
| 
 |  | ||||||
|     constructor( |  | ||||||
|         public username: string, |  | ||||||
|         public firstName: string, |  | ||||||
|         public lastName: string |  | ||||||
|     ) { |  | ||||||
|         super(); |  | ||||||
|     } |  | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -7,12 +7,4 @@ import { TeacherRepository } from '../../data/users/teacher-repository.js'; | ||||||
| export class Teacher extends User { | export class Teacher extends User { | ||||||
|     @ManyToMany(() => Class) |     @ManyToMany(() => Class) | ||||||
|     classes!: Collection<Class>; |     classes!: Collection<Class>; | ||||||
| 
 |  | ||||||
|     constructor( |  | ||||||
|         public username: string, |  | ||||||
|         public firstName: string, |  | ||||||
|         public lastName: string |  | ||||||
|     ) { |  | ||||||
|         super(); |  | ||||||
|     } |  | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -1,42 +0,0 @@ | ||||||
| /** |  | ||||||
|  * Exception for HTTP 400 Bad Request |  | ||||||
|  */ |  | ||||||
| export class BadRequestException extends Error { |  | ||||||
|     public status = 400; |  | ||||||
| 
 |  | ||||||
|     constructor(error: string) { |  | ||||||
|         super(error); |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| /** |  | ||||||
|  * Exception for HTTP 401 Unauthorized |  | ||||||
|  */ |  | ||||||
| export class UnauthorizedException extends Error { |  | ||||||
|     status = 401; |  | ||||||
|     constructor(message: string = 'Unauthorized') { |  | ||||||
|         super(message); |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| /** |  | ||||||
|  * Exception for HTTP 403 Forbidden |  | ||||||
|  */ |  | ||||||
| export class ForbiddenException extends Error { |  | ||||||
|     status = 403; |  | ||||||
| 
 |  | ||||||
|     constructor(message: string = 'Forbidden') { |  | ||||||
|         super(message); |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| /** |  | ||||||
|  * Exception for HTTP 404 Not Found |  | ||||||
|  */ |  | ||||||
| export class NotFoundException extends Error { |  | ||||||
|     public status = 404; |  | ||||||
| 
 |  | ||||||
|     constructor(error: string) { |  | ||||||
|         super(error); |  | ||||||
|     } |  | ||||||
| } |  | ||||||
							
								
								
									
										10
									
								
								backend/src/exceptions/bad-request-exception.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								backend/src/exceptions/bad-request-exception.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,10 @@ | ||||||
|  | import { ExceptionWithHttpState } from './exception-with-http-state.js'; | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Exception for HTTP 400 Bad Request | ||||||
|  |  */ | ||||||
|  | export class BadRequestException extends ExceptionWithHttpState { | ||||||
|  |     constructor(error: string) { | ||||||
|  |         super(400, error); | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										12
									
								
								backend/src/exceptions/conflict-exception.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								backend/src/exceptions/conflict-exception.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,12 @@ | ||||||
|  | import { ExceptionWithHttpState } from './exception-with-http-state.js'; | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Exception for HTTP 409 Conflict | ||||||
|  |  */ | ||||||
|  | export class ConflictException extends ExceptionWithHttpState { | ||||||
|  |     public status = 409; | ||||||
|  | 
 | ||||||
|  |     constructor(error: string) { | ||||||
|  |         super(409, error); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -0,0 +1,7 @@ | ||||||
|  | import { ConflictException } from './conflict-exception.js'; | ||||||
|  | 
 | ||||||
|  | export class EntityAlreadyExistsException extends ConflictException { | ||||||
|  |     constructor(message: string) { | ||||||
|  |         super(message); | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										11
									
								
								backend/src/exceptions/exception-with-http-state.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								backend/src/exceptions/exception-with-http-state.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,11 @@ | ||||||
|  | /** | ||||||
|  |  * Exceptions which are associated with a HTTP error code. | ||||||
|  |  */ | ||||||
|  | export abstract class ExceptionWithHttpState extends Error { | ||||||
|  |     constructor( | ||||||
|  |         public status: number, | ||||||
|  |         public error: string | ||||||
|  |     ) { | ||||||
|  |         super(error); | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										12
									
								
								backend/src/exceptions/forbidden-exception.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								backend/src/exceptions/forbidden-exception.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,12 @@ | ||||||
|  | import { ExceptionWithHttpState } from './exception-with-http-state.js'; | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Exception for HTTP 403 Forbidden | ||||||
|  |  */ | ||||||
|  | export class ForbiddenException extends ExceptionWithHttpState { | ||||||
|  |     status = 403; | ||||||
|  | 
 | ||||||
|  |     constructor(message: string = 'Forbidden') { | ||||||
|  |         super(403, message); | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										12
									
								
								backend/src/exceptions/not-found-exception.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								backend/src/exceptions/not-found-exception.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,12 @@ | ||||||
|  | import { ExceptionWithHttpState } from './exception-with-http-state.js'; | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Exception for HTTP 404 Not Found | ||||||
|  |  */ | ||||||
|  | export class NotFoundException extends ExceptionWithHttpState { | ||||||
|  |     public status = 404; | ||||||
|  | 
 | ||||||
|  |     constructor(error: string) { | ||||||
|  |         super(404, error); | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										10
									
								
								backend/src/exceptions/unauthorized-exception.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								backend/src/exceptions/unauthorized-exception.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,10 @@ | ||||||
|  | import { ExceptionWithHttpState } from './exception-with-http-state.js'; | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Exception for HTTP 401 Unauthorized | ||||||
|  |  */ | ||||||
|  | export class UnauthorizedException extends ExceptionWithHttpState { | ||||||
|  |     constructor(message: string = 'Unauthorized') { | ||||||
|  |         super(401, message); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -2,7 +2,7 @@ import { FALLBACK_LANG } from '../config.js'; | ||||||
| import { Assignment } from '../entities/assignments/assignment.entity.js'; | import { Assignment } from '../entities/assignments/assignment.entity.js'; | ||||||
| import { Class } from '../entities/classes/class.entity.js'; | import { Class } from '../entities/classes/class.entity.js'; | ||||||
| import { languageMap } from '../entities/content/language.js'; | import { languageMap } from '../entities/content/language.js'; | ||||||
| import { GroupDTO, mapToGroupDTO } from './group.js'; | import { GroupDTO } from './group.js'; | ||||||
| 
 | 
 | ||||||
| export interface AssignmentDTO { | export interface AssignmentDTO { | ||||||
|     id: number; |     id: number; | ||||||
|  | @ -46,7 +46,5 @@ export function mapToAssignment(assignmentData: AssignmentDTO, cls: Class): Assi | ||||||
|     assignment.learningPathLanguage = languageMap[assignmentData.language] || FALLBACK_LANG; |     assignment.learningPathLanguage = languageMap[assignmentData.language] || FALLBACK_LANG; | ||||||
|     assignment.within = cls; |     assignment.within = cls; | ||||||
| 
 | 
 | ||||||
|     console.log(assignment); |  | ||||||
| 
 |  | ||||||
|     return assignment; |     return assignment; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -9,12 +9,6 @@ export interface ClassDTO { | ||||||
|     teachers: string[]; |     teachers: string[]; | ||||||
|     students: string[]; |     students: string[]; | ||||||
|     joinRequests: string[]; |     joinRequests: string[]; | ||||||
|     endpoints?: { |  | ||||||
|         self: string; |  | ||||||
|         invitations: string; |  | ||||||
|         assignments: string; |  | ||||||
|         students: string; |  | ||||||
|     }; |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export function mapToClassDTO(cls: Class): ClassDTO { | export function mapToClassDTO(cls: Class): ClassDTO { | ||||||
|  |  | ||||||
|  | @ -1,8 +1,6 @@ | ||||||
| import { Question } from '../entities/questions/question.entity.js'; | import { Question } from '../entities/questions/question.entity.js'; | ||||||
| import { UserDTO } from './user.js'; |  | ||||||
| import { LearningObjectIdentifier } from '../entities/content/learning-object-identifier.js'; | import { LearningObjectIdentifier } from '../entities/content/learning-object-identifier.js'; | ||||||
| import { mapToStudentDTO, StudentDTO } from './student.js'; | import { mapToStudentDTO, StudentDTO } from './student.js'; | ||||||
| import { TeacherDTO } from './teacher.js'; |  | ||||||
| 
 | 
 | ||||||
| export interface QuestionDTO { | export interface QuestionDTO { | ||||||
|     learningObjectIdentifier: LearningObjectIdentifier; |     learningObjectIdentifier: LearningObjectIdentifier; | ||||||
|  |  | ||||||
|  | @ -1,4 +1,5 @@ | ||||||
| import { Student } from '../entities/users/student.entity.js'; | import { Student } from '../entities/users/student.entity.js'; | ||||||
|  | import { getStudentRepository } from '../data/repositories.js'; | ||||||
| 
 | 
 | ||||||
| export interface StudentDTO { | export interface StudentDTO { | ||||||
|     id: string; |     id: string; | ||||||
|  | @ -23,7 +24,9 @@ export function mapToStudentDTO(student: Student): StudentDTO { | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export function mapToStudent(studentData: StudentDTO): Student { | export function mapToStudent(studentData: StudentDTO): Student { | ||||||
|     const student = new Student(studentData.username, studentData.firstName, studentData.lastName); |     return getStudentRepository().create({ | ||||||
| 
 |         username: studentData.username, | ||||||
|     return student; |         firstName: studentData.firstName, | ||||||
|  |         lastName: studentData.lastName, | ||||||
|  |     }); | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -2,13 +2,10 @@ import { Submission } from '../entities/assignments/submission.entity.js'; | ||||||
| import { Language } from '../entities/content/language.js'; | import { Language } from '../entities/content/language.js'; | ||||||
| import { GroupDTO, mapToGroupDTO } from './group.js'; | import { GroupDTO, mapToGroupDTO } from './group.js'; | ||||||
| import { mapToStudent, mapToStudentDTO, StudentDTO } from './student.js'; | import { mapToStudent, mapToStudentDTO, StudentDTO } from './student.js'; | ||||||
| import { mapToUser } from './user'; | import { LearningObjectIdentifier } from './learning-content.js'; | ||||||
| import { Student } from '../entities/users/student.entity'; |  | ||||||
| 
 | 
 | ||||||
| export interface SubmissionDTO { | export interface SubmissionDTO { | ||||||
|     learningObjectHruid: string; |     learningObjectIdentifier: LearningObjectIdentifier; | ||||||
|     learningObjectLanguage: Language; |  | ||||||
|     learningObjectVersion: number; |  | ||||||
| 
 | 
 | ||||||
|     submissionNumber?: number; |     submissionNumber?: number; | ||||||
|     submitter: StudentDTO; |     submitter: StudentDTO; | ||||||
|  | @ -17,11 +14,21 @@ export interface SubmissionDTO { | ||||||
|     content: string; |     content: string; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | export interface SubmissionDTOId { | ||||||
|  |     learningObjectHruid: string; | ||||||
|  |     learningObjectLanguage: Language; | ||||||
|  |     learningObjectVersion: number; | ||||||
|  | 
 | ||||||
|  |     submissionNumber?: number; | ||||||
|  | } | ||||||
|  | 
 | ||||||
| export function mapToSubmissionDTO(submission: Submission): SubmissionDTO { | export function mapToSubmissionDTO(submission: Submission): SubmissionDTO { | ||||||
|     return { |     return { | ||||||
|         learningObjectHruid: submission.learningObjectHruid, |         learningObjectIdentifier: { | ||||||
|         learningObjectLanguage: submission.learningObjectLanguage, |             hruid: submission.learningObjectHruid, | ||||||
|         learningObjectVersion: submission.learningObjectVersion, |             language: submission.learningObjectLanguage, | ||||||
|  |             version: submission.learningObjectVersion, | ||||||
|  |         }, | ||||||
| 
 | 
 | ||||||
|         submissionNumber: submission.submissionNumber, |         submissionNumber: submission.submissionNumber, | ||||||
|         submitter: mapToStudentDTO(submission.submitter), |         submitter: mapToStudentDTO(submission.submitter), | ||||||
|  | @ -31,11 +38,21 @@ export function mapToSubmissionDTO(submission: Submission): SubmissionDTO { | ||||||
|     }; |     }; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | export function mapToSubmissionDTOId(submission: Submission): SubmissionDTOId { | ||||||
|  |     return { | ||||||
|  |         learningObjectHruid: submission.learningObjectHruid, | ||||||
|  |         learningObjectLanguage: submission.learningObjectLanguage, | ||||||
|  |         learningObjectVersion: submission.learningObjectVersion, | ||||||
|  | 
 | ||||||
|  |         submissionNumber: submission.submissionNumber, | ||||||
|  |     }; | ||||||
|  | } | ||||||
|  | 
 | ||||||
| export function mapToSubmission(submissionDTO: SubmissionDTO): Submission { | export function mapToSubmission(submissionDTO: SubmissionDTO): Submission { | ||||||
|     const submission = new Submission(); |     const submission = new Submission(); | ||||||
|     submission.learningObjectHruid = submissionDTO.learningObjectHruid; |     submission.learningObjectHruid = submissionDTO.learningObjectIdentifier.hruid; | ||||||
|     submission.learningObjectLanguage = submissionDTO.learningObjectLanguage; |     submission.learningObjectLanguage = submissionDTO.learningObjectIdentifier.language; | ||||||
|     submission.learningObjectVersion = submissionDTO.learningObjectVersion; |     submission.learningObjectVersion = submissionDTO.learningObjectIdentifier.version!; | ||||||
|     // Submission.submissionNumber = submissionDTO.submissionNumber;
 |     // Submission.submissionNumber = submissionDTO.submissionNumber;
 | ||||||
|     submission.submitter = mapToStudent(submissionDTO.submitter); |     submission.submitter = mapToStudent(submissionDTO.submitter); | ||||||
|     // Submission.submissionTime = submissionDTO.time;
 |     // Submission.submissionTime = submissionDTO.time;
 | ||||||
|  |  | ||||||
|  | @ -1,4 +1,5 @@ | ||||||
| import { Teacher } from '../entities/users/teacher.entity.js'; | import { Teacher } from '../entities/users/teacher.entity.js'; | ||||||
|  | import { getTeacherRepository } from '../data/repositories.js'; | ||||||
| 
 | 
 | ||||||
| export interface TeacherDTO { | export interface TeacherDTO { | ||||||
|     id: string; |     id: string; | ||||||
|  | @ -22,8 +23,10 @@ export function mapToTeacherDTO(teacher: Teacher): TeacherDTO { | ||||||
|     }; |     }; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export function mapToTeacher(TeacherData: TeacherDTO): Teacher { | export function mapToTeacher(teacherData: TeacherDTO): Teacher { | ||||||
|     const teacher = new Teacher(TeacherData.username, TeacherData.firstName, TeacherData.lastName); |     return getTeacherRepository().create({ | ||||||
| 
 |         username: teacherData.username, | ||||||
|     return teacher; |         firstName: teacherData.firstName, | ||||||
|  |         lastName: teacherData.lastName, | ||||||
|  |     }); | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -1,7 +1,7 @@ | ||||||
| import { createLogger, format, Logger as WinstonLogger, transports } from 'winston'; | import { createLogger, format, Logger as WinstonLogger, transports } from 'winston'; | ||||||
| import LokiTransport from 'winston-loki'; | import LokiTransport from 'winston-loki'; | ||||||
| import { LokiLabels } from 'loki-logger-ts'; | import { LokiLabels } from 'loki-logger-ts'; | ||||||
| import { LOG_LEVEL, LOKI_HOST } from '../config.js'; | import { EnvVars, getEnvVar } from '../util/envvars.js'; | ||||||
| 
 | 
 | ||||||
| export class Logger extends WinstonLogger { | export class Logger extends WinstonLogger { | ||||||
|     constructor() { |     constructor() { | ||||||
|  | @ -22,10 +22,25 @@ function initializeLogger(): Logger { | ||||||
|         return logger; |         return logger; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     const logLevel = getEnvVar(EnvVars.LogLevel); | ||||||
|  | 
 | ||||||
|  |     const consoleTransport = new transports.Console({ | ||||||
|  |         level: getEnvVar(EnvVars.LogLevel), | ||||||
|  |         format: format.combine(format.cli(), format.colorize()), | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     if (getEnvVar(EnvVars.RunMode) === 'dev') { | ||||||
|  |         return createLogger({ | ||||||
|  |             transports: [consoleTransport], | ||||||
|  |         }); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     const lokiHost = getEnvVar(EnvVars.LokiHost); | ||||||
|  | 
 | ||||||
|     const lokiTransport: LokiTransport = new LokiTransport({ |     const lokiTransport: LokiTransport = new LokiTransport({ | ||||||
|         host: LOKI_HOST, |         host: lokiHost, | ||||||
|         labels: Labels, |         labels: Labels, | ||||||
|         level: LOG_LEVEL, |         level: logLevel, | ||||||
|         json: true, |         json: true, | ||||||
|         format: format.combine(format.timestamp(), format.json()), |         format: format.combine(format.timestamp(), format.json()), | ||||||
|         onConnectionError: (err) => { |         onConnectionError: (err) => { | ||||||
|  | @ -34,16 +49,11 @@ function initializeLogger(): Logger { | ||||||
|         }, |         }, | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|     const consoleTransport = new transports.Console({ |  | ||||||
|         level: LOG_LEVEL, |  | ||||||
|         format: format.combine(format.cli(), format.colorize()), |  | ||||||
|     }); |  | ||||||
| 
 |  | ||||||
|     logger = createLogger({ |     logger = createLogger({ | ||||||
|         transports: [lokiTransport, consoleTransport], |         transports: [lokiTransport, consoleTransport], | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|     logger.debug(`Logger initialized with level ${LOG_LEVEL}, Loki host ${LOKI_HOST}`); |     logger.debug(`Logger initialized with level ${logLevel} to Loki host ${lokiHost}`); | ||||||
|     return logger; |     return logger; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -6,7 +6,8 @@ import * as express from 'express'; | ||||||
| import * as jwt from 'jsonwebtoken'; | import * as jwt from 'jsonwebtoken'; | ||||||
| import { AuthenticatedRequest } from './authenticated-request.js'; | import { AuthenticatedRequest } from './authenticated-request.js'; | ||||||
| import { AuthenticationInfo } from './authentication-info.js'; | import { AuthenticationInfo } from './authentication-info.js'; | ||||||
| import { ForbiddenException, UnauthorizedException } from '../../exceptions.js'; | import { UnauthorizedException } from '../../exceptions/unauthorized-exception.js'; | ||||||
|  | import { ForbiddenException } from '../../exceptions/forbidden-exception.js'; | ||||||
| 
 | 
 | ||||||
| const JWKS_CACHE = true; | const JWKS_CACHE = true; | ||||||
| const JWKS_RATE_LIMIT = true; | const JWKS_RATE_LIMIT = true; | ||||||
|  |  | ||||||
							
								
								
									
										15
									
								
								backend/src/middleware/error-handling/error-handler.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								backend/src/middleware/error-handling/error-handler.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,15 @@ | ||||||
|  | import { NextFunction, Request, Response } from 'express'; | ||||||
|  | import { getLogger, Logger } from '../../logging/initalize.js'; | ||||||
|  | import { ExceptionWithHttpState } from '../../exceptions/exception-with-http-state.js'; | ||||||
|  | 
 | ||||||
|  | const logger: Logger = getLogger(); | ||||||
|  | 
 | ||||||
|  | export function errorHandler(err: unknown, req: Request, res: Response, _: NextFunction): void { | ||||||
|  |     if (err instanceof ExceptionWithHttpState) { | ||||||
|  |         logger.warn(`An error occurred while handling a request: ${err} (-> HTTP ${err.status})`); | ||||||
|  |         res.status(err.status).json(err); | ||||||
|  |     } else { | ||||||
|  |         logger.error(`Unexpected error occurred while handing a request: ${JSON.stringify(err)}`); | ||||||
|  |         res.status(500).json(err); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -3,7 +3,6 @@ import { PostgreSqlDriver } from '@mikro-orm/postgresql'; | ||||||
| import { EnvVars, getEnvVar, getNumericEnvVar } from './util/envvars.js'; | import { EnvVars, getEnvVar, getNumericEnvVar } from './util/envvars.js'; | ||||||
| import { SqliteDriver } from '@mikro-orm/sqlite'; | import { SqliteDriver } from '@mikro-orm/sqlite'; | ||||||
| import { MikroOrmLogger } from './logging/mikroOrmLogger.js'; | import { MikroOrmLogger } from './logging/mikroOrmLogger.js'; | ||||||
| import { LOG_LEVEL } from './config.js'; |  | ||||||
| 
 | 
 | ||||||
| // Import alle entity-bestanden handmatig
 | // Import alle entity-bestanden handmatig
 | ||||||
| import { User } from './entities/users/user.entity.js'; | import { User } from './entities/users/user.entity.js'; | ||||||
|  | @ -50,6 +49,7 @@ function config(testingMode: boolean = false): Options { | ||||||
|             dbName: getEnvVar(EnvVars.DbName), |             dbName: getEnvVar(EnvVars.DbName), | ||||||
|             subscribers: [new SqliteAutoincrementSubscriber()], |             subscribers: [new SqliteAutoincrementSubscriber()], | ||||||
|             entities: entities, |             entities: entities, | ||||||
|  |             persistOnCreate: false, // Do not implicitly save entities when they are created via `create`.
 | ||||||
|             // EntitiesTs: entitiesTs,
 |             // EntitiesTs: entitiesTs,
 | ||||||
| 
 | 
 | ||||||
|             // Workaround: vitest: `TypeError: Unknown file extension ".ts"` (ERR_UNKNOWN_FILE_EXTENSION)
 |             // Workaround: vitest: `TypeError: Unknown file extension ".ts"` (ERR_UNKNOWN_FILE_EXTENSION)
 | ||||||
|  | @ -66,10 +66,11 @@ function config(testingMode: boolean = false): Options { | ||||||
|         user: getEnvVar(EnvVars.DbUsername), |         user: getEnvVar(EnvVars.DbUsername), | ||||||
|         password: getEnvVar(EnvVars.DbPassword), |         password: getEnvVar(EnvVars.DbPassword), | ||||||
|         entities: entities, |         entities: entities, | ||||||
|  |         persistOnCreate: false, // Do not implicitly save entities when they are created via `create`.
 | ||||||
|         // EntitiesTs: entitiesTs,
 |         // EntitiesTs: entitiesTs,
 | ||||||
| 
 | 
 | ||||||
|         // Logging
 |         // Logging
 | ||||||
|         debug: LOG_LEVEL === 'debug', |         debug: getEnvVar(EnvVars.LogLevel) === 'debug', | ||||||
|         loggerFactory: (options: LoggerOptions) => new MikroOrmLogger(options), |         loggerFactory: (options: LoggerOptions) => new MikroOrmLogger(options), | ||||||
|     }; |     }; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -1,10 +1,7 @@ | ||||||
| import { Response, Router } from 'express'; | import { Response, Router } from 'express'; | ||||||
| import studentRouter from './students.js'; | import studentRouter from './students.js'; | ||||||
| import groupRouter from './groups.js'; | import teacherRouter from './teachers.js'; | ||||||
| import assignmentRouter from './assignments.js'; |  | ||||||
| import submissionRouter from './submissions.js'; |  | ||||||
| import classRouter from './classes.js'; | import classRouter from './classes.js'; | ||||||
| import questionRouter from './questions.js'; |  | ||||||
| import authRouter from './auth.js'; | import authRouter from './auth.js'; | ||||||
| import themeRoutes from './themes.js'; | import themeRoutes from './themes.js'; | ||||||
| import learningPathRoutes from './learning-paths.js'; | import learningPathRoutes from './learning-paths.js'; | ||||||
|  | @ -22,11 +19,8 @@ router.get('/', (_, res: Response) => { | ||||||
| }); | }); | ||||||
| 
 | 
 | ||||||
| router.use('/student', studentRouter /* #swagger.tags = ['Student'] */); | router.use('/student', studentRouter /* #swagger.tags = ['Student'] */); | ||||||
| router.use('/group', groupRouter /* #swagger.tags = ['Group'] */); | router.use('/teacher', teacherRouter /* #swagger.tags = ['Teacher'] */); | ||||||
| router.use('/assignment', assignmentRouter /* #swagger.tags = ['Assignment'] */); |  | ||||||
| router.use('/submission', submissionRouter /* #swagger.tags = ['Submission'] */); |  | ||||||
| router.use('/class', classRouter /* #swagger.tags = ['Class'] */); | router.use('/class', classRouter /* #swagger.tags = ['Class'] */); | ||||||
| router.use('/question', questionRouter /* #swagger.tags = ['Question'] */); |  | ||||||
| router.use('/auth', authRouter /* #swagger.tags = ['Auth'] */); | router.use('/auth', authRouter /* #swagger.tags = ['Auth'] */); | ||||||
| router.use('/theme', themeRoutes /* #swagger.tags = ['Theme'] */); | router.use('/theme', themeRoutes /* #swagger.tags = ['Theme'] */); | ||||||
| router.use('/learningPath', learningPathRoutes /* #swagger.tags = ['Learning Path'] */); | router.use('/learningPath', learningPathRoutes /* #swagger.tags = ['Learning Path'] */); | ||||||
|  |  | ||||||
|  | @ -9,7 +9,6 @@ import { | ||||||
|     getStudentHandler, |     getStudentHandler, | ||||||
|     getStudentSubmissionsHandler, |     getStudentSubmissionsHandler, | ||||||
| } from '../controllers/students.js'; | } from '../controllers/students.js'; | ||||||
| import { getStudentGroups } from '../services/students.js'; |  | ||||||
| const router = express.Router(); | const router = express.Router(); | ||||||
| 
 | 
 | ||||||
| // Root endpoint used to search objects
 | // Root endpoint used to search objects
 | ||||||
|  |  | ||||||
|  | @ -1,7 +1,6 @@ | ||||||
| import { getAssignmentRepository, getClassRepository, getGroupRepository, getSubmissionRepository } from '../data/repositories.js'; | import { getAssignmentRepository, getClassRepository, getGroupRepository, getSubmissionRepository } from '../data/repositories.js'; | ||||||
| import { Assignment } from '../entities/assignments/assignment.entity.js'; |  | ||||||
| import { AssignmentDTO, mapToAssignment, mapToAssignmentDTO, mapToAssignmentDTOId } from '../interfaces/assignment.js'; | import { AssignmentDTO, mapToAssignment, mapToAssignmentDTO, mapToAssignmentDTOId } from '../interfaces/assignment.js'; | ||||||
| import { mapToSubmissionDTO, SubmissionDTO } from '../interfaces/submission.js'; | import { mapToSubmissionDTO, mapToSubmissionDTOId, SubmissionDTO, SubmissionDTOId } from '../interfaces/submission.js'; | ||||||
| 
 | 
 | ||||||
| export async function getAllAssignments(classid: string, full: boolean): Promise<AssignmentDTO[]> { | export async function getAllAssignments(classid: string, full: boolean): Promise<AssignmentDTO[]> { | ||||||
|     const classRepository = getClassRepository(); |     const classRepository = getClassRepository(); | ||||||
|  | @ -21,7 +20,7 @@ export async function getAllAssignments(classid: string, full: boolean): Promise | ||||||
|     return assignments.map(mapToAssignmentDTOId); |     return assignments.map(mapToAssignmentDTOId); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export async function createAssignment(classid: string, assignmentData: AssignmentDTO): Promise<Assignment | null> { | export async function createAssignment(classid: string, assignmentData: AssignmentDTO): Promise<AssignmentDTO | null> { | ||||||
|     const classRepository = getClassRepository(); |     const classRepository = getClassRepository(); | ||||||
|     const cls = await classRepository.findById(classid); |     const cls = await classRepository.findById(classid); | ||||||
| 
 | 
 | ||||||
|  | @ -36,8 +35,9 @@ export async function createAssignment(classid: string, assignmentData: Assignme | ||||||
|         const newAssignment = assignmentRepository.create(assignment); |         const newAssignment = assignmentRepository.create(assignment); | ||||||
|         await assignmentRepository.save(newAssignment); |         await assignmentRepository.save(newAssignment); | ||||||
| 
 | 
 | ||||||
|         return newAssignment; |         return mapToAssignmentDTO(newAssignment); | ||||||
|     } catch (e) { |     } catch (e) { | ||||||
|  |         console.error(e); | ||||||
|         return null; |         return null; | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  | @ -60,7 +60,11 @@ export async function getAssignment(classid: string, id: number): Promise<Assign | ||||||
|     return mapToAssignmentDTO(assignment); |     return mapToAssignmentDTO(assignment); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export async function getAssignmentsSubmissions(classid: string, assignmentNumber: number): Promise<SubmissionDTO[]> { | export async function getAssignmentsSubmissions( | ||||||
|  |     classid: string, | ||||||
|  |     assignmentNumber: number, | ||||||
|  |     full: boolean | ||||||
|  | ): Promise<SubmissionDTO[] | SubmissionDTOId[]> { | ||||||
|     const classRepository = getClassRepository(); |     const classRepository = getClassRepository(); | ||||||
|     const cls = await classRepository.findById(classid); |     const cls = await classRepository.findById(classid); | ||||||
| 
 | 
 | ||||||
|  | @ -81,5 +85,9 @@ export async function getAssignmentsSubmissions(classid: string, assignmentNumbe | ||||||
|     const submissionRepository = getSubmissionRepository(); |     const submissionRepository = getSubmissionRepository(); | ||||||
|     const submissions = (await Promise.all(groups.map((group) => submissionRepository.findAllSubmissionsForGroup(group)))).flat(); |     const submissions = (await Promise.all(groups.map((group) => submissionRepository.findAllSubmissionsForGroup(group)))).flat(); | ||||||
| 
 | 
 | ||||||
|  |     if (full) { | ||||||
|         return submissions.map(mapToSubmissionDTO); |         return submissions.map(mapToSubmissionDTO); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     return submissions.map(mapToSubmissionDTOId); | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -1,5 +1,4 @@ | ||||||
| import { getClassRepository, getStudentRepository, getTeacherInvitationRepository, getTeacherRepository } from '../data/repositories.js'; | import { getClassRepository, getStudentRepository, getTeacherInvitationRepository, getTeacherRepository } from '../data/repositories.js'; | ||||||
| import { Class } from '../entities/classes/class.entity.js'; |  | ||||||
| import { ClassDTO, mapToClassDTO } from '../interfaces/class.js'; | import { ClassDTO, mapToClassDTO } from '../interfaces/class.js'; | ||||||
| import { mapToStudentDTO, StudentDTO } from '../interfaces/student.js'; | import { mapToStudentDTO, StudentDTO } from '../interfaces/student.js'; | ||||||
| import { mapToTeacherInvitationDTO, mapToTeacherInvitationDTOIds, TeacherInvitationDTO } from '../interfaces/teacher-invitation.js'; | import { mapToTeacherInvitationDTO, mapToTeacherInvitationDTOIds, TeacherInvitationDTO } from '../interfaces/teacher-invitation.js'; | ||||||
|  | @ -21,16 +20,14 @@ export async function getAllClasses(full: boolean): Promise<ClassDTO[] | string[ | ||||||
|     return classes.map((cls) => cls.classId!); |     return classes.map((cls) => cls.classId!); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export async function createClass(classData: ClassDTO): Promise<Class | null> { | export async function createClass(classData: ClassDTO): Promise<ClassDTO | null> { | ||||||
|     const teacherRepository = getTeacherRepository(); |     const teacherRepository = getTeacherRepository(); | ||||||
|     const teacherUsernames = classData.teachers || []; |     const teacherUsernames = classData.teachers || []; | ||||||
|     const teachers = (await Promise.all(teacherUsernames.map((id) => teacherRepository.findByUsername(id)))).filter((teacher) => teacher != null); |     const teachers = (await Promise.all(teacherUsernames.map((id) => teacherRepository.findByUsername(id)))).filter((teacher) => teacher !== null); | ||||||
| 
 | 
 | ||||||
|     const studentRepository = getStudentRepository(); |     const studentRepository = getStudentRepository(); | ||||||
|     const studentUsernames = classData.students || []; |     const studentUsernames = classData.students || []; | ||||||
|     const students = (await Promise.all(studentUsernames.map((id) => studentRepository.findByUsername(id)))).filter((student) => student != null); |     const students = (await Promise.all(studentUsernames.map((id) => studentRepository.findByUsername(id)))).filter((student) => student !== null); | ||||||
| 
 |  | ||||||
|     //Const cls = mapToClass(classData, teachers, students);
 |  | ||||||
| 
 | 
 | ||||||
|     const classRepository = getClassRepository(); |     const classRepository = getClassRepository(); | ||||||
| 
 | 
 | ||||||
|  | @ -42,7 +39,7 @@ export async function createClass(classData: ClassDTO): Promise<Class | null> { | ||||||
|         }); |         }); | ||||||
|         await classRepository.save(newClass); |         await classRepository.save(newClass); | ||||||
| 
 | 
 | ||||||
|         return newClass; |         return mapToClassDTO(newClass); | ||||||
|     } catch (e) { |     } catch (e) { | ||||||
|         logger.error(e); |         logger.error(e); | ||||||
|         return null; |         return null; | ||||||
|  | @ -1,4 +1,3 @@ | ||||||
| import { GroupRepository } from '../data/assignments/group-repository.js'; |  | ||||||
| import { | import { | ||||||
|     getAssignmentRepository, |     getAssignmentRepository, | ||||||
|     getClassRepository, |     getClassRepository, | ||||||
|  | @ -8,7 +7,7 @@ import { | ||||||
| } from '../data/repositories.js'; | } from '../data/repositories.js'; | ||||||
| import { Group } from '../entities/assignments/group.entity.js'; | import { Group } from '../entities/assignments/group.entity.js'; | ||||||
| import { GroupDTO, mapToGroupDTO, mapToGroupDTOId } from '../interfaces/group.js'; | import { GroupDTO, mapToGroupDTO, mapToGroupDTOId } from '../interfaces/group.js'; | ||||||
| import { mapToSubmissionDTO, SubmissionDTO } from '../interfaces/submission.js'; | import { mapToSubmissionDTO, mapToSubmissionDTOId, SubmissionDTO, SubmissionDTOId } from '../interfaces/submission.js'; | ||||||
| 
 | 
 | ||||||
| export async function getGroup(classId: string, assignmentNumber: number, groupNumber: number, full: boolean): Promise<GroupDTO | null> { | export async function getGroup(classId: string, assignmentNumber: number, groupNumber: number, full: boolean): Promise<GroupDTO | null> { | ||||||
|     const classRepository = getClassRepository(); |     const classRepository = getClassRepository(); | ||||||
|  | @ -43,7 +42,7 @@ export async function createGroup(groupData: GroupDTO, classid: string, assignme | ||||||
|     const studentRepository = getStudentRepository(); |     const studentRepository = getStudentRepository(); | ||||||
| 
 | 
 | ||||||
|     const memberUsernames = (groupData.members as string[]) || []; // TODO check if groupdata.members is a list
 |     const memberUsernames = (groupData.members as string[]) || []; // TODO check if groupdata.members is a list
 | ||||||
|     const members = (await Promise.all([...memberUsernames].map((id) => studentRepository.findByUsername(id)))).filter((student) => student != null); |     const members = (await Promise.all([...memberUsernames].map((id) => studentRepository.findByUsername(id)))).filter((student) => student !== null); | ||||||
| 
 | 
 | ||||||
|     console.log(members); |     console.log(members); | ||||||
| 
 | 
 | ||||||
|  | @ -103,7 +102,12 @@ export async function getAllGroups(classId: string, assignmentNumber: number, fu | ||||||
|     return groups.map(mapToGroupDTOId); |     return groups.map(mapToGroupDTOId); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export async function getGroupSubmissions(classId: string, assignmentNumber: number, groupNumber: number): Promise<SubmissionDTO[]> { | export async function getGroupSubmissions( | ||||||
|  |     classId: string, | ||||||
|  |     assignmentNumber: number, | ||||||
|  |     groupNumber: number, | ||||||
|  |     full: boolean | ||||||
|  | ): Promise<SubmissionDTO[] | SubmissionDTOId[]> { | ||||||
|     const classRepository = getClassRepository(); |     const classRepository = getClassRepository(); | ||||||
|     const cls = await classRepository.findById(classId); |     const cls = await classRepository.findById(classId); | ||||||
| 
 | 
 | ||||||
|  | @ -128,5 +132,9 @@ export async function getGroupSubmissions(classId: string, assignmentNumber: num | ||||||
|     const submissionRepository = getSubmissionRepository(); |     const submissionRepository = getSubmissionRepository(); | ||||||
|     const submissions = await submissionRepository.findAllSubmissionsForGroup(group); |     const submissions = await submissionRepository.findAllSubmissionsForGroup(group); | ||||||
| 
 | 
 | ||||||
|  |     if (full) { | ||||||
|         return submissions.map(mapToSubmissionDTO); |         return submissions.map(mapToSubmissionDTO); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     return submissions.map(mapToSubmissionDTOId); | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -45,6 +45,13 @@ export async function getLearningObjectById(hruid: string, language: string): Pr | ||||||
|     return filterData(metadata, htmlUrl); |     return filterData(metadata, htmlUrl); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | /** | ||||||
|  |  * Generic function to fetch learning paths | ||||||
|  |  */ | ||||||
|  | function fetchLearningPaths(arg0: string[], language: string, arg2: string): LearningPathResponse | PromiseLike<LearningPathResponse> { | ||||||
|  |     throw new Error('Function not implemented.'); | ||||||
|  | } | ||||||
|  | 
 | ||||||
| /** | /** | ||||||
|  * Generic function to fetch learning objects (full data or just HRUIDs) |  * Generic function to fetch learning objects (full data or just HRUIDs) | ||||||
|  */ |  */ | ||||||
|  | @ -85,6 +92,3 @@ export async function getLearningObjectsFromPath(hruid: string, language: string | ||||||
| export async function getLearningObjectIdsFromPath(hruid: string, language: string): Promise<string[]> { | export async function getLearningObjectIdsFromPath(hruid: string, language: string): Promise<string[]> { | ||||||
|     return (await fetchLearningObjects(hruid, false, language)) as string[]; |     return (await fetchLearningObjects(hruid, false, language)) as string[]; | ||||||
| } | } | ||||||
| function fetchLearningPaths(arg0: string[], language: string, arg2: string): LearningPathResponse | PromiseLike<LearningPathResponse> { |  | ||||||
|     throw new Error('Function not implemented.'); |  | ||||||
| } |  | ||||||
|  |  | ||||||
|  | @ -103,5 +103,5 @@ export async function deleteQuestion(questionId: QuestionId) { | ||||||
|         return null; |         return null; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     return question; |     return mapToQuestionDTO(question); | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -1,23 +1,20 @@ | ||||||
| import { getClassRepository, getGroupRepository, getStudentRepository, getSubmissionRepository } from '../data/repositories.js'; | import { getClassRepository, getGroupRepository, getStudentRepository, getSubmissionRepository } from '../data/repositories.js'; | ||||||
| import { Class } from '../entities/classes/class.entity.js'; |  | ||||||
| import { Student } from '../entities/users/student.entity.js'; |  | ||||||
| import { AssignmentDTO } from '../interfaces/assignment.js'; | import { AssignmentDTO } from '../interfaces/assignment.js'; | ||||||
| import { ClassDTO, mapToClassDTO } from '../interfaces/class.js'; | import { ClassDTO, mapToClassDTO } from '../interfaces/class.js'; | ||||||
| import { GroupDTO, mapToGroupDTO, mapToGroupDTOId } from '../interfaces/group.js'; | import { GroupDTO, mapToGroupDTO, mapToGroupDTOId } from '../interfaces/group.js'; | ||||||
| import { mapToStudent, mapToStudentDTO, StudentDTO } from '../interfaces/student.js'; | import { mapToStudent, mapToStudentDTO, StudentDTO } from '../interfaces/student.js'; | ||||||
| import { mapToSubmissionDTO, SubmissionDTO } from '../interfaces/submission.js'; | import { mapToSubmissionDTO, mapToSubmissionDTOId, SubmissionDTO, SubmissionDTOId } from '../interfaces/submission.js'; | ||||||
| import { getAllAssignments } from './assignments.js'; | import { getAllAssignments } from './assignments.js'; | ||||||
| import { UserService } from './users.js'; |  | ||||||
| 
 | 
 | ||||||
| export async function getAllStudents(): Promise<StudentDTO[]> { | export async function getAllStudents(full: boolean): Promise<StudentDTO[] | string[]> { | ||||||
|     const studentRepository = getStudentRepository(); |     const studentRepository = getStudentRepository(); | ||||||
|     const users = await studentRepository.findAll(); |     const students = await studentRepository.findAll(); | ||||||
|     return users.map(mapToStudentDTO); |  | ||||||
| } |  | ||||||
| 
 | 
 | ||||||
| export async function getAllStudentIds(): Promise<string[]> { |     if (full) { | ||||||
|     const users = await getAllStudents(); |         return students.map(mapToStudentDTO); | ||||||
|     return users.map((user) => user.username); |     } | ||||||
|  | 
 | ||||||
|  |     return students.map((student) => student.username); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export async function getStudent(username: string): Promise<StudentDTO | null> { | export async function getStudent(username: string): Promise<StudentDTO | null> { | ||||||
|  | @ -29,15 +26,9 @@ export async function getStudent(username: string): Promise<StudentDTO | null> { | ||||||
| export async function createStudent(userData: StudentDTO): Promise<StudentDTO | null> { | export async function createStudent(userData: StudentDTO): Promise<StudentDTO | null> { | ||||||
|     const studentRepository = getStudentRepository(); |     const studentRepository = getStudentRepository(); | ||||||
| 
 | 
 | ||||||
|     try { |     const newStudent = mapToStudent(userData); | ||||||
|         const newStudent = studentRepository.create(mapToStudent(userData)); |     await studentRepository.save(newStudent, { preventOverwrite: true }); | ||||||
|         await studentRepository.save(newStudent); |  | ||||||
| 
 |  | ||||||
|     return mapToStudentDTO(newStudent); |     return mapToStudentDTO(newStudent); | ||||||
|     } catch (e) { |  | ||||||
|         console.log(e); |  | ||||||
|         return null; |  | ||||||
|     } |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export async function deleteStudent(username: string): Promise<StudentDTO | null> { | export async function deleteStudent(username: string): Promise<StudentDTO | null> { | ||||||
|  | @ -88,9 +79,7 @@ export async function getStudentAssignments(username: string, full: boolean): Pr | ||||||
|     const classRepository = getClassRepository(); |     const classRepository = getClassRepository(); | ||||||
|     const classes = await classRepository.findByStudent(student); |     const classes = await classRepository.findByStudent(student); | ||||||
| 
 | 
 | ||||||
|     const assignments = (await Promise.all(classes.map(async (cls) => await getAllAssignments(cls.classId!, full)))).flat(); |     return (await Promise.all(classes.map(async (cls) => await getAllAssignments(cls.classId!, full)))).flat(); | ||||||
| 
 |  | ||||||
|     return assignments; |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export async function getStudentGroups(username: string, full: boolean): Promise<GroupDTO[]> { | export async function getStudentGroups(username: string, full: boolean): Promise<GroupDTO[]> { | ||||||
|  | @ -111,7 +100,7 @@ export async function getStudentGroups(username: string, full: boolean): Promise | ||||||
|     return groups.map(mapToGroupDTOId); |     return groups.map(mapToGroupDTOId); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export async function getStudentSubmissions(username: string): Promise<SubmissionDTO[]> { | export async function getStudentSubmissions(username: string, full: boolean): Promise<SubmissionDTO[] | SubmissionDTOId[]> { | ||||||
|     const studentRepository = getStudentRepository(); |     const studentRepository = getStudentRepository(); | ||||||
|     const student = await studentRepository.findByUsername(username); |     const student = await studentRepository.findByUsername(username); | ||||||
| 
 | 
 | ||||||
|  | @ -122,5 +111,9 @@ export async function getStudentSubmissions(username: string): Promise<Submissio | ||||||
|     const submissionRepository = getSubmissionRepository(); |     const submissionRepository = getSubmissionRepository(); | ||||||
|     const submissions = await submissionRepository.findAllSubmissionsForStudent(student); |     const submissions = await submissionRepository.findAllSubmissionsForStudent(student); | ||||||
| 
 | 
 | ||||||
|  |     if (full) { | ||||||
|         return submissions.map(mapToSubmissionDTO); |         return submissions.map(mapToSubmissionDTO); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     return submissions.map(mapToSubmissionDTOId); | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -32,7 +32,7 @@ export async function createSubmission(submissionDTO: SubmissionDTO) { | ||||||
|         return null; |         return null; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     return submission; |     return mapToSubmissionDTO(submission); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export async function deleteSubmission(learningObjectHruid: string, language: Language, version: number, submissionNumber: number) { | export async function deleteSubmission(learningObjectHruid: string, language: Language, version: number, submissionNumber: number) { | ||||||
|  |  | ||||||
|  | @ -1,28 +1,19 @@ | ||||||
| import { | import { getClassRepository, getLearningObjectRepository, getQuestionRepository, getTeacherRepository } from '../data/repositories.js'; | ||||||
|     getClassRepository, |  | ||||||
|     getLearningObjectRepository, |  | ||||||
|     getQuestionRepository, |  | ||||||
|     getStudentRepository, |  | ||||||
|     getTeacherRepository, |  | ||||||
| } from '../data/repositories.js'; |  | ||||||
| import { Teacher } from '../entities/users/teacher.entity.js'; |  | ||||||
| import { ClassDTO, mapToClassDTO } from '../interfaces/class.js'; | import { ClassDTO, mapToClassDTO } from '../interfaces/class.js'; | ||||||
| import { getClassStudents } from './class.js'; | import { getClassStudents } from './classes.js'; | ||||||
| import { StudentDTO } from '../interfaces/student.js'; | import { StudentDTO } from '../interfaces/student.js'; | ||||||
| import { mapToQuestionDTO, mapToQuestionId, QuestionDTO, QuestionId } from '../interfaces/question.js'; | import { mapToQuestionDTO, mapToQuestionId, QuestionDTO, QuestionId } from '../interfaces/question.js'; | ||||||
| import { UserService } from './users.js'; |  | ||||||
| import { mapToUser } from '../interfaces/user.js'; |  | ||||||
| import { mapToTeacher, mapToTeacherDTO, TeacherDTO } from '../interfaces/teacher.js'; | import { mapToTeacher, mapToTeacherDTO, TeacherDTO } from '../interfaces/teacher.js'; | ||||||
| 
 | 
 | ||||||
| export async function getAllTeachers(): Promise<TeacherDTO[]> { | export async function getAllTeachers(full: boolean): Promise<TeacherDTO[] | string[]> { | ||||||
|     const teacherRepository = getTeacherRepository(); |     const teacherRepository = getTeacherRepository(); | ||||||
|     const users = await teacherRepository.findAll(); |     const teachers = await teacherRepository.findAll(); | ||||||
|     return users.map(mapToTeacherDTO); |  | ||||||
| } |  | ||||||
| 
 | 
 | ||||||
| export async function getAllTeacherIds(): Promise<string[]> { |     if (full) { | ||||||
|     const users = await getAllTeachers(); |         return teachers.map(mapToTeacherDTO); | ||||||
|     return users.map((user) => user.username); |     } | ||||||
|  | 
 | ||||||
|  |     return teachers.map((teacher) => teacher.username); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export async function getTeacher(username: string): Promise<TeacherDTO | null> { | export async function getTeacher(username: string): Promise<TeacherDTO | null> { | ||||||
|  | @ -34,15 +25,10 @@ export async function getTeacher(username: string): Promise<TeacherDTO | null> { | ||||||
| export async function createTeacher(userData: TeacherDTO): Promise<TeacherDTO | null> { | export async function createTeacher(userData: TeacherDTO): Promise<TeacherDTO | null> { | ||||||
|     const teacherRepository = getTeacherRepository(); |     const teacherRepository = getTeacherRepository(); | ||||||
| 
 | 
 | ||||||
|     try { |     const newTeacher = mapToTeacher(userData); | ||||||
|         const newTeacher = teacherRepository.create(mapToTeacher(userData)); |     await teacherRepository.save(newTeacher, { preventOverwrite: true }); | ||||||
|         await teacherRepository.save(newTeacher); |  | ||||||
| 
 | 
 | ||||||
|     return mapToTeacherDTO(newTeacher); |     return mapToTeacherDTO(newTeacher); | ||||||
|     } catch (e) { |  | ||||||
|         console.log(e); |  | ||||||
|         return null; |  | ||||||
|     } |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export async function deleteTeacher(username: string): Promise<TeacherDTO | null> { | export async function deleteTeacher(username: string): Promise<TeacherDTO | null> { | ||||||
|  | @ -64,11 +50,11 @@ export async function deleteTeacher(username: string): Promise<TeacherDTO | null | ||||||
|     } |     } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export async function fetchClassesByTeacher(username: string): Promise<ClassDTO[]> { | export async function fetchClassesByTeacher(username: string): Promise<ClassDTO[] | null> { | ||||||
|     const teacherRepository = getTeacherRepository(); |     const teacherRepository = getTeacherRepository(); | ||||||
|     const teacher = await teacherRepository.findByUsername(username); |     const teacher = await teacherRepository.findByUsername(username); | ||||||
|     if (!teacher) { |     if (!teacher) { | ||||||
|         return []; |         return null; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     const classRepository = getClassRepository(); |     const classRepository = getClassRepository(); | ||||||
|  | @ -76,35 +62,49 @@ export async function fetchClassesByTeacher(username: string): Promise<ClassDTO[ | ||||||
|     return classes.map(mapToClassDTO); |     return classes.map(mapToClassDTO); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export async function getClassesByTeacher(username: string): Promise<ClassDTO[]> { | export async function getClassesByTeacher(username: string, full: boolean): Promise<ClassDTO[] | string[] | null> { | ||||||
|     return await fetchClassesByTeacher(username); |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| export async function getClassIdsByTeacher(username: string): Promise<string[]> { |  | ||||||
|     const classes = await fetchClassesByTeacher(username); |     const classes = await fetchClassesByTeacher(username); | ||||||
|  | 
 | ||||||
|  |     if (!classes) { | ||||||
|  |         return null; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     if (full) { | ||||||
|  |         return classes; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     return classes.map((cls) => cls.id); |     return classes.map((cls) => cls.id); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export async function fetchStudentsByTeacher(username: string) { | export async function fetchStudentsByTeacher(username: string): Promise<StudentDTO[] | null> { | ||||||
|     const classes = await getClassIdsByTeacher(username); |     const classes = (await getClassesByTeacher(username, false)) as string[]; | ||||||
|  | 
 | ||||||
|  |     if (!classes) { | ||||||
|  |         return null; | ||||||
|  |     } | ||||||
| 
 | 
 | ||||||
|     return (await Promise.all(classes.map(async (id) => getClassStudents(id)))).flat(); |     return (await Promise.all(classes.map(async (id) => getClassStudents(id)))).flat(); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export async function getStudentsByTeacher(username: string): Promise<StudentDTO[]> { | export async function getStudentsByTeacher(username: string, full: boolean): Promise<StudentDTO[] | string[] | null> { | ||||||
|     return await fetchStudentsByTeacher(username); |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| export async function getStudentIdsByTeacher(username: string): Promise<string[]> { |  | ||||||
|     const students = await fetchStudentsByTeacher(username); |     const students = await fetchStudentsByTeacher(username); | ||||||
|  | 
 | ||||||
|  |     if (!students) { | ||||||
|  |         return null; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     if (full) { | ||||||
|  |         return students; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     return students.map((student) => student.username); |     return students.map((student) => student.username); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export async function fetchTeacherQuestions(username: string): Promise<QuestionDTO[]> { | export async function fetchTeacherQuestions(username: string): Promise<QuestionDTO[] | null> { | ||||||
|     const teacherRepository = getTeacherRepository(); |     const teacherRepository = getTeacherRepository(); | ||||||
|     const teacher = await teacherRepository.findByUsername(username); |     const teacher = await teacherRepository.findByUsername(username); | ||||||
|     if (!teacher) { |     if (!teacher) { | ||||||
|         throw new Error(`Teacher with username '${username}' not found.`); |         return null; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     // Find all learning objects that this teacher manages
 |     // Find all learning objects that this teacher manages
 | ||||||
|  | @ -118,12 +118,16 @@ export async function fetchTeacherQuestions(username: string): Promise<QuestionD | ||||||
|     return questions.map(mapToQuestionDTO); |     return questions.map(mapToQuestionDTO); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export async function getQuestionsByTeacher(username: string): Promise<QuestionDTO[]> { | export async function getQuestionsByTeacher(username: string, full: boolean): Promise<QuestionDTO[] | QuestionId[] | null> { | ||||||
|     return await fetchTeacherQuestions(username); |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| export async function getQuestionIdsByTeacher(username: string): Promise<QuestionId[]> { |  | ||||||
|     const questions = await fetchTeacherQuestions(username); |     const questions = await fetchTeacherQuestions(username); | ||||||
| 
 | 
 | ||||||
|  |     if (!questions) { | ||||||
|  |         return null; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     if (full) { | ||||||
|  |         return questions; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     return questions.map(mapToQuestionId); |     return questions.map(mapToQuestionId); | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -1,41 +0,0 @@ | ||||||
| import { UserRepository } from '../data/users/user-repository.js'; |  | ||||||
| import { UserDTO, mapToUser, mapToUserDTO } from '../interfaces/user.js'; |  | ||||||
| import { User } from '../entities/users/user.entity.js'; |  | ||||||
| 
 |  | ||||||
| export class UserService<T extends User> { |  | ||||||
|     protected repository: UserRepository<T>; |  | ||||||
| 
 |  | ||||||
|     constructor(repository: UserRepository<T>) { |  | ||||||
|         this.repository = repository; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     async getAllUsers(): Promise<UserDTO[]> { |  | ||||||
|         const users = await this.repository.findAll(); |  | ||||||
|         return users.map(mapToUserDTO); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     async getAllUserIds(): Promise<string[]> { |  | ||||||
|         const users = await this.getAllUsers(); |  | ||||||
|         return users.map((user) => user.username); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     async getUserByUsername(username: string): Promise<UserDTO | null> { |  | ||||||
|         const user = await this.repository.findByUsername(username); |  | ||||||
|         return user ? mapToUserDTO(user) : null; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     async createUser(userData: UserDTO, UserClass: new () => T): Promise<T> { |  | ||||||
|         const newUser = mapToUser(userData, new UserClass()); |  | ||||||
|         await this.repository.save(newUser); |  | ||||||
|         return newUser; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     async deleteUser(username: string): Promise<UserDTO | null> { |  | ||||||
|         const user = await this.getUserByUsername(username); |  | ||||||
|         if (!user) { |  | ||||||
|             return null; |  | ||||||
|         } |  | ||||||
|         await this.repository.deleteByUsername(username); |  | ||||||
|         return mapToUserDTO(user); |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  | @ -4,20 +4,24 @@ const IDP_PREFIX = PREFIX + 'AUTH_'; | ||||||
| const STUDENT_IDP_PREFIX = IDP_PREFIX + 'STUDENT_'; | const STUDENT_IDP_PREFIX = IDP_PREFIX + 'STUDENT_'; | ||||||
| const TEACHER_IDP_PREFIX = IDP_PREFIX + 'TEACHER_'; | const TEACHER_IDP_PREFIX = IDP_PREFIX + 'TEACHER_'; | ||||||
| const CORS_PREFIX = PREFIX + 'CORS_'; | const CORS_PREFIX = PREFIX + 'CORS_'; | ||||||
|  | const LOGGING_PREFIX = PREFIX + 'LOGGING_'; | ||||||
| 
 | 
 | ||||||
| type EnvVar = { key: string; required?: boolean; defaultValue?: any }; | type EnvVar = { key: string; required?: boolean; defaultValue?: any }; | ||||||
| 
 | 
 | ||||||
| export const EnvVars: { [key: string]: EnvVar } = { | export const EnvVars: { [key: string]: EnvVar } = { | ||||||
|     Port: { key: PREFIX + 'PORT', defaultValue: 3000 }, |     Port: { key: PREFIX + 'PORT', defaultValue: 3000 }, | ||||||
|  |     LearningContentRepoApiBaseUrl: { key: PREFIX + 'LEARNING_CONTENT_REPO_API_BASE_URL', defaultValue: 'https://dwengo.org/backend/api' }, | ||||||
|  |     FallbackLanguage: { key: PREFIX + 'FALLBACK_LANGUAGE', defaultValue: 'nl' }, | ||||||
|  |     RunMode: { key: PREFIX + 'RUN_MODE', defaultValue: 'dev' }, | ||||||
|  | 
 | ||||||
|     DbHost: { key: DB_PREFIX + 'HOST', required: true }, |     DbHost: { key: DB_PREFIX + 'HOST', required: true }, | ||||||
|     DbPort: { key: DB_PREFIX + 'PORT', defaultValue: 5432 }, |     DbPort: { key: DB_PREFIX + 'PORT', defaultValue: 5432 }, | ||||||
|     DbName: { key: DB_PREFIX + 'NAME', defaultValue: 'dwengo' }, |     DbName: { key: DB_PREFIX + 'NAME', defaultValue: 'dwengo' }, | ||||||
|     DbUsername: { key: DB_PREFIX + 'USERNAME', required: true }, |     DbUsername: { key: DB_PREFIX + 'USERNAME', required: true }, | ||||||
|     DbPassword: { key: DB_PREFIX + 'PASSWORD', required: true }, |     DbPassword: { key: DB_PREFIX + 'PASSWORD', required: true }, | ||||||
|     DbUpdate: { key: DB_PREFIX + 'UPDATE', defaultValue: false }, |     DbUpdate: { key: DB_PREFIX + 'UPDATE', defaultValue: false }, | ||||||
|     LearningContentRepoApiBaseUrl: { key: PREFIX + 'LEARNING_CONTENT_REPO_API_BASE_URL', defaultValue: 'https://dwengo.org/backend/api' }, |  | ||||||
|     FallbackLanguage: { key: PREFIX + 'FALLBACK_LANGUAGE', defaultValue: 'nl' }, |  | ||||||
|     UserContentPrefix: { key: DB_PREFIX + 'USER_CONTENT_PREFIX', defaultValue: 'u_' }, |     UserContentPrefix: { key: DB_PREFIX + 'USER_CONTENT_PREFIX', defaultValue: 'u_' }, | ||||||
|  | 
 | ||||||
|     IdpStudentUrl: { key: STUDENT_IDP_PREFIX + 'URL', required: true }, |     IdpStudentUrl: { key: STUDENT_IDP_PREFIX + 'URL', required: true }, | ||||||
|     IdpStudentClientId: { key: STUDENT_IDP_PREFIX + 'CLIENT_ID', required: true }, |     IdpStudentClientId: { key: STUDENT_IDP_PREFIX + 'CLIENT_ID', required: true }, | ||||||
|     IdpStudentJwksEndpoint: { key: STUDENT_IDP_PREFIX + 'JWKS_ENDPOINT', required: true }, |     IdpStudentJwksEndpoint: { key: STUDENT_IDP_PREFIX + 'JWKS_ENDPOINT', required: true }, | ||||||
|  | @ -25,8 +29,12 @@ export const EnvVars: { [key: string]: EnvVar } = { | ||||||
|     IdpTeacherClientId: { key: TEACHER_IDP_PREFIX + 'CLIENT_ID', required: true }, |     IdpTeacherClientId: { key: TEACHER_IDP_PREFIX + 'CLIENT_ID', required: true }, | ||||||
|     IdpTeacherJwksEndpoint: { key: TEACHER_IDP_PREFIX + 'JWKS_ENDPOINT', required: true }, |     IdpTeacherJwksEndpoint: { key: TEACHER_IDP_PREFIX + 'JWKS_ENDPOINT', required: true }, | ||||||
|     IdpAudience: { key: IDP_PREFIX + 'AUDIENCE', defaultValue: 'account' }, |     IdpAudience: { key: IDP_PREFIX + 'AUDIENCE', defaultValue: 'account' }, | ||||||
|  | 
 | ||||||
|     CorsAllowedOrigins: { key: CORS_PREFIX + 'ALLOWED_ORIGINS', defaultValue: '' }, |     CorsAllowedOrigins: { key: CORS_PREFIX + 'ALLOWED_ORIGINS', defaultValue: '' }, | ||||||
|     CorsAllowedHeaders: { key: CORS_PREFIX + 'ALLOWED_HEADERS', defaultValue: 'Authorization,Content-Type' }, |     CorsAllowedHeaders: { key: CORS_PREFIX + 'ALLOWED_HEADERS', defaultValue: 'Authorization,Content-Type' }, | ||||||
|  | 
 | ||||||
|  |     LogLevel: { key: LOGGING_PREFIX + 'LEVEL', defaultValue: 'info' }, | ||||||
|  |     LokiHost: { key: LOGGING_PREFIX + 'LOKI_HOST', defaultValue: 'http://localhost:3102' }, | ||||||
| } as const; | } as const; | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  |  | ||||||
|  | @ -8,12 +8,12 @@ const logger: Logger = getLogger(); | ||||||
| 
 | 
 | ||||||
| export function loadTranslations<T>(language: string): T { | export function loadTranslations<T>(language: string): T { | ||||||
|     try { |     try { | ||||||
|         const filePath = path.join(process.cwd(), '_i18n', `${language}.yml`); |         const filePath = path.join(process.cwd(), 'i18n', `${language}.yml`); | ||||||
|         const yamlFile = fs.readFileSync(filePath, 'utf8'); |         const yamlFile = fs.readFileSync(filePath, 'utf8'); | ||||||
|         return yaml.load(yamlFile) as T; |         return yaml.load(yamlFile) as T; | ||||||
|     } catch (error) { |     } catch (error) { | ||||||
|         logger.warn(`Cannot load translation for ${language}, fallen back to dutch`, error); |         logger.warn(`Cannot load translation for ${language}, fallen back to dutch`, error); | ||||||
|         const fallbackPath = path.join(process.cwd(), '_i18n', `${FALLBACK_LANG}.yml`); |         const fallbackPath = path.join(process.cwd(), 'i18n', `${FALLBACK_LANG}.yml`); | ||||||
|         return yaml.load(fs.readFileSync(fallbackPath, 'utf8')) as T; |         return yaml.load(fs.readFileSync(fallbackPath, 'utf8')) as T; | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -1,5 +1,4 @@ | ||||||
| import { setupTestApp } from '../../setup-tests.js'; | import { setupTestApp } from '../../setup-tests.js'; | ||||||
| import { Student } from '../../../src/entities/users/student.entity.js'; |  | ||||||
| import { describe, it, expect, beforeAll } from 'vitest'; | import { describe, it, expect, beforeAll } from 'vitest'; | ||||||
| import { StudentRepository } from '../../../src/data/users/student-repository.js'; | import { StudentRepository } from '../../../src/data/users/student-repository.js'; | ||||||
| import { getStudentRepository } from '../../../src/data/repositories.js'; | import { getStudentRepository } from '../../../src/data/repositories.js'; | ||||||
|  | @ -30,7 +29,7 @@ describe('StudentRepository', () => { | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|     it('should return the queried student after he was added', async () => { |     it('should return the queried student after he was added', async () => { | ||||||
|         await studentRepository.insert(new Student(username, firstName, lastName)); |         await studentRepository.insert(studentRepository.create({ username, firstName, lastName })); | ||||||
| 
 | 
 | ||||||
|         const retrievedStudent = await studentRepository.findByUsername(username); |         const retrievedStudent = await studentRepository.findByUsername(username); | ||||||
|         expect(retrievedStudent).toBeTruthy(); |         expect(retrievedStudent).toBeTruthy(); | ||||||
|  |  | ||||||
|  | @ -2,7 +2,6 @@ import { describe, it, expect, beforeAll } from 'vitest'; | ||||||
| import { TeacherRepository } from '../../../src/data/users/teacher-repository'; | import { TeacherRepository } from '../../../src/data/users/teacher-repository'; | ||||||
| import { setupTestApp } from '../../setup-tests'; | import { setupTestApp } from '../../setup-tests'; | ||||||
| import { getTeacherRepository } from '../../../src/data/repositories'; | import { getTeacherRepository } from '../../../src/data/repositories'; | ||||||
| import { Teacher } from '../../../src/entities/users/teacher.entity'; |  | ||||||
| 
 | 
 | ||||||
| const username = 'testteacher'; | const username = 'testteacher'; | ||||||
| const firstName = 'John'; | const firstName = 'John'; | ||||||
|  | @ -30,7 +29,7 @@ describe('TeacherRepository', () => { | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|     it('should return the queried teacher after he was added', async () => { |     it('should return the queried teacher after he was added', async () => { | ||||||
|         await teacherRepository.insert(new Teacher(username, firstName, lastName)); |         await teacherRepository.insert(teacherRepository.create({ username, firstName, lastName })); | ||||||
| 
 | 
 | ||||||
|         const retrievedTeacher = await teacherRepository.findByUsername(username); |         const retrievedTeacher = await teacherRepository.findByUsername(username); | ||||||
|         expect(retrievedTeacher).toBeTruthy(); |         expect(retrievedTeacher).toBeTruthy(); | ||||||
|  |  | ||||||
|  | @ -1,7 +1,8 @@ | ||||||
| # | # | ||||||
| # This file is used to define the production environment for the project. | # Use this configuration to deploy the project on a server. | ||||||
| # It is used to deploy the project on a server. | # | ||||||
| # Should not be used for local development. | # This configuration builds the frontend and backend services as Docker images, | ||||||
|  | # and uses the paths for the services, instead of ports, and enables SSL. | ||||||
| # | # | ||||||
| services: | services: | ||||||
|     web: |     web: | ||||||
|  | @ -35,12 +36,16 @@ services: | ||||||
|             - 'traefik.http.services.api.loadbalancer.server.port=3000' |             - 'traefik.http.services.api.loadbalancer.server.port=3000' | ||||||
| 
 | 
 | ||||||
|     db: |     db: | ||||||
|         # Also see compose.yml |         extends: | ||||||
|  |             file: ./compose.yml | ||||||
|  |             service: db | ||||||
|         networks: |         networks: | ||||||
|             - dwengo-1 |             - dwengo-1 | ||||||
| 
 | 
 | ||||||
|     idp: |     idp: | ||||||
|         # Also see compose.yml |         extends: | ||||||
|  |             file: ./compose.yml | ||||||
|  |             service: idp | ||||||
|         # TODO Replace with proper production command |         # TODO Replace with proper production command | ||||||
|         command: ['start-dev', '--http-port', '7080', '--https-port', '7443', '--import-realm'] |         command: ['start-dev', '--http-port', '7080', '--https-port', '7443', '--import-realm'] | ||||||
|         networks: |         networks: | ||||||
|  | @ -92,7 +97,15 @@ services: | ||||||
|             - dwengo-1 |             - dwengo-1 | ||||||
| 
 | 
 | ||||||
|     logging: |     logging: | ||||||
|         # Also see compose.yml |         image: grafana/loki:latest | ||||||
|  |         ports: | ||||||
|  |             - '9001:3102' | ||||||
|  |             - '9095:9095' | ||||||
|  |         command: -config.file=/etc/loki/config.yaml | ||||||
|  |         restart: unless-stopped | ||||||
|  |         volumes: | ||||||
|  |             - ./config/loki/config.yml:/etc/loki/config.yaml | ||||||
|  |             - dwengo_loki_data:/loki | ||||||
|         networks: |         networks: | ||||||
|             - dwengo-1 |             - dwengo-1 | ||||||
| 
 | 
 | ||||||
|  | @ -107,6 +120,7 @@ services: | ||||||
| volumes: | volumes: | ||||||
|     dwengo_grafana_data: |     dwengo_grafana_data: | ||||||
|     dwengo_letsencrypt: |     dwengo_letsencrypt: | ||||||
|  |     dwengo_loki_data: | ||||||
| 
 | 
 | ||||||
| networks: | networks: | ||||||
|     dwengo-1: |     dwengo-1: | ||||||
|  | @ -32,8 +32,15 @@ services: | ||||||
|             - 'traefik.http.routers.api.rule=PathPrefix(`/api`)' |             - 'traefik.http.routers.api.rule=PathPrefix(`/api`)' | ||||||
|             - 'traefik.http.services.api.loadbalancer.server.port=3000' |             - 'traefik.http.services.api.loadbalancer.server.port=3000' | ||||||
| 
 | 
 | ||||||
|  |     db: | ||||||
|  |         extends: | ||||||
|  |             file: ./compose.yml | ||||||
|  |             service: db | ||||||
|  | 
 | ||||||
|     idp: |     idp: | ||||||
|         # Also see compose.yml |         extends: | ||||||
|  |             file: ./compose.yml | ||||||
|  |             service: idp | ||||||
|         labels: |         labels: | ||||||
|             - 'traefik.http.routers.idp.rule=PathPrefix(`/idp`)' |             - 'traefik.http.routers.idp.rule=PathPrefix(`/idp`)' | ||||||
|             - 'traefik.http.services.idp.loadbalancer.server.port=7080' |             - 'traefik.http.services.idp.loadbalancer.server.port=7080' | ||||||
|  | @ -60,6 +67,17 @@ services: | ||||||
|         volumes: |         volumes: | ||||||
|             - /var/run/docker.sock:/var/run/docker.sock:ro |             - /var/run/docker.sock:/var/run/docker.sock:ro | ||||||
| 
 | 
 | ||||||
|  |     logging: | ||||||
|  |         image: grafana/loki:latest | ||||||
|  |         ports: | ||||||
|  |             - '9001:3102' | ||||||
|  |             - '9095:9095' | ||||||
|  |         command: -config.file=/etc/loki/config.yaml | ||||||
|  |         restart: unless-stopped | ||||||
|  |         volumes: | ||||||
|  |             - ./config/loki/config.yml:/etc/loki/config.yaml | ||||||
|  |             - dwengo_loki_data:/loki | ||||||
|  | 
 | ||||||
|     dashboards: |     dashboards: | ||||||
|         image: grafana/grafana:latest |         image: grafana/grafana:latest | ||||||
|         ports: |         ports: | ||||||
|  | @ -70,3 +88,5 @@ services: | ||||||
| 
 | 
 | ||||||
| volumes: | volumes: | ||||||
|     dwengo_grafana_data: |     dwengo_grafana_data: | ||||||
|  |     dwengo_loki_data: | ||||||
|  |     dwengo_postgres_data: | ||||||
							
								
								
									
										12
									
								
								compose.yml
									
										
									
									
									
								
							
							
						
						
									
										12
									
								
								compose.yml
									
										
									
									
									
								
							|  | @ -36,17 +36,5 @@ services: | ||||||
|             KC_HEALTH_ENABLED: 'true' |             KC_HEALTH_ENABLED: 'true' | ||||||
|             KC_LOG_LEVEL: info |             KC_LOG_LEVEL: info | ||||||
| 
 | 
 | ||||||
|     logging: |  | ||||||
|         image: grafana/loki:latest |  | ||||||
|         ports: |  | ||||||
|             - '9001:3102' |  | ||||||
|             - '9095:9095' |  | ||||||
|         command: -config.file=/etc/loki/config.yaml |  | ||||||
|         restart: unless-stopped |  | ||||||
|         volumes: |  | ||||||
|             - ./config/loki/config.yml:/etc/loki/config.yaml |  | ||||||
|             - dwengo_loki_data:/loki |  | ||||||
| 
 |  | ||||||
| volumes: | volumes: | ||||||
|     dwengo_loki_data: |  | ||||||
|     dwengo_postgres_data: |     dwengo_postgres_data: | ||||||
|  |  | ||||||
|  | @ -19,7 +19,16 @@ See [Vite Configuration Reference](https://vite.dev/config/). | ||||||
| ## Project Setup | ## Project Setup | ||||||
| 
 | 
 | ||||||
| ```sh | ```sh | ||||||
|  | # Install dependencies | ||||||
| npm install | npm install | ||||||
|  | 
 | ||||||
|  | # Start necessary services for development | ||||||
|  | cd ../ # Go to the root of the repository | ||||||
|  | docker compose up -d | ||||||
|  | # Start the backend | ||||||
|  | cd backend | ||||||
|  | cp .env.development.example .env.development.local | ||||||
|  | npm run dev # or npm run build && npm run start | ||||||
| ``` | ``` | ||||||
| 
 | 
 | ||||||
| ### Compile and Hot-Reload for Development | ### Compile and Hot-Reload for Development | ||||||
|  |  | ||||||
|  | @ -42,7 +42,7 @@ | ||||||
|         "jsdom": "^26.0.0", |         "jsdom": "^26.0.0", | ||||||
|         "npm-run-all2": "^7.0.2", |         "npm-run-all2": "^7.0.2", | ||||||
|         "typescript": "~5.7.3", |         "typescript": "~5.7.3", | ||||||
|         "vite": "^6.1.0", |         "vite": "^6.1.2", | ||||||
|         "vite-plugin-vue-devtools": "^7.7.2", |         "vite-plugin-vue-devtools": "^7.7.2", | ||||||
|         "vitest": "^3.0.5", |         "vitest": "^3.0.5", | ||||||
|         "vue-tsc": "^2.2.2" |         "vue-tsc": "^2.2.2" | ||||||
|  |  | ||||||
|  | @ -1,10 +1,26 @@ | ||||||
| <script setup lang="ts"> | <script setup lang="ts"> | ||||||
|     import auth from "@/services/auth/auth-service.ts"; |     import auth from "@/services/auth/auth-service.ts"; | ||||||
|  |     import MenuBar from "@/components/MenuBar.vue"; | ||||||
|  |     import { useRoute } from "vue-router"; | ||||||
|  |     import { computed } from "vue"; | ||||||
|  | 
 | ||||||
|  |     const route = useRoute(); | ||||||
|     auth.loadUser(); |     auth.loadUser(); | ||||||
|  | 
 | ||||||
|  |     interface RouteMeta { | ||||||
|  |         requiresAuth?: boolean; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     const showMenuBar = computed(() => (route.meta as RouteMeta).requiresAuth && auth.authState.user); | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
| <template> | <template> | ||||||
|  |     <v-app> | ||||||
|  |         <menu-bar v-if="showMenuBar"></menu-bar> | ||||||
|  |         <v-main> | ||||||
|             <router-view /> |             <router-view /> | ||||||
|  |         </v-main> | ||||||
|  |     </v-app> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <style scoped></style> | <style scoped></style> | ||||||
|  |  | ||||||
|  | @ -1,47 +1,56 @@ | ||||||
| <script setup lang="ts"> | <script setup lang="ts"> | ||||||
| import ThemeCard from "@/components/ThemeCard.vue"; |     import ThemeCard from "@/components/ThemeCard.vue"; | ||||||
| import { ref, watchEffect, computed } from "vue"; |     import { ref, watchEffect, computed } from "vue"; | ||||||
| import { useI18n } from "vue-i18n"; |     import { useI18n } from "vue-i18n"; | ||||||
| import { AGE_TO_THEMES, THEMESITEMS } from "@/utils/constants.ts"; |     import { AGE_TO_THEMES, THEMESITEMS } from "@/utils/constants.ts"; | ||||||
| import { useThemeQuery } from "@/queries/themes.ts"; |     import { useThemeQuery } from "@/queries/themes.ts"; | ||||||
| 
 | 
 | ||||||
| const props = defineProps({ |     const props = defineProps({ | ||||||
|         selectedTheme: { type: String, required: true }, |         selectedTheme: { type: String, required: true }, | ||||||
|     selectedAge: { type: String, required: true } |         selectedAge: { type: String, required: true }, | ||||||
| }); |     }); | ||||||
| 
 | 
 | ||||||
| const { locale } = useI18n(); |     const { locale } = useI18n(); | ||||||
| const language = computed(() => locale.value); |     const language = computed(() => locale.value); | ||||||
| 
 | 
 | ||||||
| const { data: allThemes, isLoading, error } = useThemeQuery(language); |     const { data: allThemes, isLoading, error } = useThemeQuery(language); | ||||||
| 
 | 
 | ||||||
| const allCards = ref([]); |     const allCards = ref([]); | ||||||
| const cards = ref([]); |     const cards = ref([]); | ||||||
| 
 | 
 | ||||||
| watchEffect(() => { |     watchEffect(() => { | ||||||
|         const themes = allThemes.value ?? []; |         const themes = allThemes.value ?? []; | ||||||
|         allCards.value = themes; |         allCards.value = themes; | ||||||
| 
 | 
 | ||||||
|         if (props.selectedTheme) { |         if (props.selectedTheme) { | ||||||
|         cards.value = themes.filter((theme) => |             cards.value = themes.filter( | ||||||
|  |                 (theme) => | ||||||
|                     THEMESITEMS[props.selectedTheme]?.includes(theme.key) && |                     THEMESITEMS[props.selectedTheme]?.includes(theme.key) && | ||||||
|             AGE_TO_THEMES[props.selectedAge]?.includes(theme.key) |                     AGE_TO_THEMES[props.selectedAge]?.includes(theme.key), | ||||||
|             ); |             ); | ||||||
|         } else { |         } else { | ||||||
|             cards.value = themes; |             cards.value = themes; | ||||||
|         } |         } | ||||||
| }); |     }); | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
| 
 |  | ||||||
| <template> | <template> | ||||||
|     <v-container> |     <v-container> | ||||||
|         <div v-if="isLoading" class="text-center py-10"> |         <div | ||||||
|             <v-progress-circular indeterminate color="primary" /> |             v-if="isLoading" | ||||||
|  |             class="text-center py-10" | ||||||
|  |         > | ||||||
|  |             <v-progress-circular | ||||||
|  |                 indeterminate | ||||||
|  |                 color="primary" | ||||||
|  |             /> | ||||||
|             <p>Loading...</p> |             <p>Loading...</p> | ||||||
|         </div> |         </div> | ||||||
| 
 | 
 | ||||||
|         <div v-else-if="error" class="text-center py-10 text-error"> |         <div | ||||||
|  |             v-else-if="error" | ||||||
|  |             class="text-center py-10 text-error" | ||||||
|  |         > | ||||||
|             <v-icon large>mdi-alert-circle</v-icon> |             <v-icon large>mdi-alert-circle</v-icon> | ||||||
|             <p>Error loading: {{ error.message }}</p> |             <p>Error loading: {{ error.message }}</p> | ||||||
|         </div> |         </div> | ||||||
|  |  | ||||||
|  | @ -22,7 +22,7 @@ | ||||||
|         { name: "English", code: "en" }, |         { name: "English", code: "en" }, | ||||||
|         { name: "Nederlands", code: "nl" }, |         { name: "Nederlands", code: "nl" }, | ||||||
|         { name: "Français", code: "fr" }, |         { name: "Français", code: "fr" }, | ||||||
|         { name: "Deutsch", code: "de" } |         { name: "Deutsch", code: "de" }, | ||||||
|     ]); |     ]); | ||||||
| 
 | 
 | ||||||
|     // Logic to change the language of the website to the selected language |     // Logic to change the language of the website to the selected language | ||||||
|  | @ -31,29 +31,26 @@ | ||||||
|         localStorage.setItem("user-lang", langCode); |         localStorage.setItem("user-lang", langCode); | ||||||
|     }; |     }; | ||||||
| 
 | 
 | ||||||
|     // contains functionality to let the collapsed menu appear and disappear |     // Contains functionality to let the collapsed menu appear and disappear | ||||||
|     // when the screen size varies |     // When the screen size varies | ||||||
|     const drawer = ref(false); |     const drawer = ref(false); | ||||||
| 
 | 
 | ||||||
|     // when the user wants to logout, a popup is shown to verify this |     // When the user wants to logout, a popup is shown to verify this | ||||||
|     // if verified, the user should be logged out |     // If verified, the user should be logged out | ||||||
|     const performLogout = () => { |     const performLogout = () => { | ||||||
|         auth.logout(); |         auth.logout(); | ||||||
|     }; |     }; | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
| <template> | <template> | ||||||
|     <main> |  | ||||||
|         <v-app class="menu_collapsed"> |  | ||||||
|     <v-app-bar |     <v-app-bar | ||||||
|  |         class="app-bar" | ||||||
|         app |         app | ||||||
|                 style="background-color: #f6faf2" |  | ||||||
|     > |     > | ||||||
|                 <template v-slot:prepend> |         <v-app-bar-nav-icon | ||||||
|                     <v-app-bar-nav-icon @click="drawer = !drawer" /> |             class="menu_collapsed" | ||||||
|                 </template> |             @click="drawer = !drawer" | ||||||
| 
 |         /> | ||||||
|                 <v-app-bar-title> |  | ||||||
|         <router-link |         <router-link | ||||||
|             to="/user" |             to="/user" | ||||||
|             class="dwengo_home" |             class="dwengo_home" | ||||||
|  | @ -61,141 +58,36 @@ | ||||||
|             <div> |             <div> | ||||||
|                 <img |                 <img | ||||||
|                     class="dwengo_logo" |                     class="dwengo_logo" | ||||||
|                                 :src="dwengoLogo" |                     alt="Dwengo logo" | ||||||
|                                 style="width: 100px" |  | ||||||
|                             /> |  | ||||||
|                             <p |  | ||||||
|                                 class="caption" |  | ||||||
|                                 style="font-size: smaller" |  | ||||||
|                             > |  | ||||||
|                                 {{ t(`${role}`) }} |  | ||||||
|                             </p> |  | ||||||
|                         </div> |  | ||||||
|                     </router-link> |  | ||||||
|                 </v-app-bar-title> |  | ||||||
| 
 |  | ||||||
|                 <v-spacer></v-spacer> |  | ||||||
| 
 |  | ||||||
|                 <v-menu open-on-hover> |  | ||||||
|                     <template v-slot:activator="{ props }"> |  | ||||||
|                         <v-btn |  | ||||||
|                             v-bind="props" |  | ||||||
|                             icon |  | ||||||
|                             variant="text" |  | ||||||
|                         > |  | ||||||
|                             <v-icon |  | ||||||
|                                 icon="mdi-translate" |  | ||||||
|                                 size="small" |  | ||||||
|                                 color="#0e6942" |  | ||||||
|                             ></v-icon> |  | ||||||
|                         </v-btn> |  | ||||||
|                     </template> |  | ||||||
|                     <v-list> |  | ||||||
|                         <v-list-item |  | ||||||
|                             v-for="(language, index) in languages" |  | ||||||
|                             :key="index" |  | ||||||
|                             @click="changeLanguage(language.code)" |  | ||||||
|                         > |  | ||||||
|                             <v-list-item-title>{{ language.name }}</v-list-item-title> |  | ||||||
|                         </v-list-item> |  | ||||||
|                     </v-list> |  | ||||||
|                 </v-menu> |  | ||||||
| 
 |  | ||||||
|                 <v-btn |  | ||||||
|                     @click="performLogout" |  | ||||||
|                     text |  | ||||||
|                 > |  | ||||||
|                     <v-tooltip |  | ||||||
|                         :text="t('logout')" |  | ||||||
|                         location="bottom" |  | ||||||
|                     > |  | ||||||
|                         <template v-slot:activator="{ props }"> |  | ||||||
|                             <v-icon |  | ||||||
|                                 v-bind="props" |  | ||||||
|                                 icon="mdi-logout" |  | ||||||
|                                 size="x-large" |  | ||||||
|                                 color="#0e6942" |  | ||||||
|                             /> |  | ||||||
|                         </template> |  | ||||||
|                     </v-tooltip> |  | ||||||
|                 </v-btn> |  | ||||||
|             </v-app-bar> |  | ||||||
| 
 |  | ||||||
|             <v-navigation-drawer |  | ||||||
|                 v-model="drawer" |  | ||||||
|                 app |  | ||||||
|             > |  | ||||||
|                 <v-list> |  | ||||||
|                     <v-list-item |  | ||||||
|                         to="/user/assignment" |  | ||||||
|                         link |  | ||||||
|                     > |  | ||||||
|                         <v-list-item-content> |  | ||||||
|                             <v-list-item-title class="menu_item">{{ t("assignments") }}</v-list-item-title> |  | ||||||
|                         </v-list-item-content> |  | ||||||
|                     </v-list-item> |  | ||||||
| 
 |  | ||||||
|                     <v-list-item |  | ||||||
|                         to="/user/class" |  | ||||||
|                         link |  | ||||||
|                     > |  | ||||||
|                         <v-list-item-content> |  | ||||||
|                             <v-list-item-title class="menu_item">{{ t("classes") }}</v-list-item-title> |  | ||||||
|                         </v-list-item-content> |  | ||||||
|                     </v-list-item> |  | ||||||
| 
 |  | ||||||
|                     <v-list-item |  | ||||||
|                         to="/user/discussion" |  | ||||||
|                         link |  | ||||||
|                     > |  | ||||||
|                         <v-list-item-content> |  | ||||||
|                             <v-list-item-title class="menu_item">{{ t("discussions") }}</v-list-item-title> |  | ||||||
|                         </v-list-item-content> |  | ||||||
|                     </v-list-item> |  | ||||||
|                 </v-list> |  | ||||||
|             </v-navigation-drawer> |  | ||||||
|         </v-app> |  | ||||||
| 
 |  | ||||||
|         <nav class="menu"> |  | ||||||
|             <div class="left"> |  | ||||||
|                 <ul> |  | ||||||
|                     <li> |  | ||||||
|                         <router-link |  | ||||||
|                             to="/user" |  | ||||||
|                             class="dwengo_home" |  | ||||||
|                         > |  | ||||||
|                             <img |  | ||||||
|                                 class="dwengo_logo" |  | ||||||
|                     :src="dwengoLogo" |                     :src="dwengoLogo" | ||||||
|                 /> |                 /> | ||||||
|                 <p class="caption"> |                 <p class="caption"> | ||||||
|                     {{ t(`${role}`) }} |                     {{ t(`${role}`) }} | ||||||
|                 </p> |                 </p> | ||||||
|  |             </div> | ||||||
|         </router-link> |         </router-link> | ||||||
|                     </li> |         <v-toolbar-items class="menu"> | ||||||
|                     <li> |             <v-btn | ||||||
|                         <router-link |  | ||||||
|                             :to="`/user/assignment`" |  | ||||||
|                 class="menu_item" |                 class="menu_item" | ||||||
|  |                 variant="text" | ||||||
|  |                 to="/user/assignment" | ||||||
|             > |             > | ||||||
|                 {{ t("assignments") }} |                 {{ t("assignments") }} | ||||||
|                         </router-link> |             </v-btn> | ||||||
|                     </li> |             <v-btn | ||||||
|                     <li> |                 class="menu_item" | ||||||
|                         <router-link |                 variant="text" | ||||||
|                 to="/user/class" |                 to="/user/class" | ||||||
|                             class="menu_item" |  | ||||||
|                             >{{ t("classes") }}</router-link |  | ||||||
|             > |             > | ||||||
|                     </li> |                 {{ t("classes") }} | ||||||
|                     <li> |             </v-btn> | ||||||
|                         <router-link |             <v-btn | ||||||
|                             to="/user/discussion" |  | ||||||
|                 class="menu_item" |                 class="menu_item" | ||||||
|                             >{{ t("discussions") }} |                 variant="text" | ||||||
|                         </router-link> |                 to="/user/discussion" | ||||||
|                     </li> |             > | ||||||
|                     <li> |                 {{ t("discussions") }} | ||||||
|  |             </v-btn> | ||||||
|             <v-menu open-on-hover> |             <v-menu open-on-hover> | ||||||
|                 <template v-slot:activator="{ props }"> |                 <template v-slot:activator="{ props }"> | ||||||
|                     <v-btn |                     <v-btn | ||||||
|  | @ -220,35 +112,14 @@ | ||||||
|                     </v-list-item> |                     </v-list-item> | ||||||
|                 </v-list> |                 </v-list> | ||||||
|             </v-menu> |             </v-menu> | ||||||
|                     </li> |         </v-toolbar-items> | ||||||
|                 </ul> |         <v-spacer></v-spacer> | ||||||
|             </div> |  | ||||||
|             <div class="right"> |  | ||||||
|                 <li> |  | ||||||
|                     <!-- <v-btn |  | ||||||
|                         @click="performLogout" |  | ||||||
|                         to="/login" |  | ||||||
|                         style="background-color: transparent; box-shadow: none !important" |  | ||||||
|                     > |  | ||||||
|                         <v-tooltip |  | ||||||
|                             :text="t('logout')" |  | ||||||
|                             location="bottom" |  | ||||||
|                         > |  | ||||||
|                             <template v-slot:activator="{ props }"> |  | ||||||
|                                 <v-icon |  | ||||||
|                                     v-bind="props" |  | ||||||
|                                     icon="mdi-logout" |  | ||||||
|                                     size="x-large" |  | ||||||
|                                     color="#0e6942" |  | ||||||
|                                 ></v-icon> |  | ||||||
|                             </template> |  | ||||||
|                         </v-tooltip> |  | ||||||
|                     </v-btn> --> |  | ||||||
|         <v-dialog max-width="500"> |         <v-dialog max-width="500"> | ||||||
|             <template v-slot:activator="{ props: activatorProps }"> |             <template v-slot:activator="{ props: activatorProps }"> | ||||||
|                 <v-btn |                 <v-btn | ||||||
|                     v-bind="activatorProps" |                     v-bind="activatorProps" | ||||||
|                                 style="background-color: transparent; box-shadow: none !important" |                     :rounded="true" | ||||||
|  |                     variant="text" | ||||||
|                 > |                 > | ||||||
|                     <v-tooltip |                     <v-tooltip | ||||||
|                         :text="t('logout')" |                         :text="t('logout')" | ||||||
|  | @ -285,31 +156,56 @@ | ||||||
|                 </v-card> |                 </v-card> | ||||||
|             </template> |             </template> | ||||||
|         </v-dialog> |         </v-dialog> | ||||||
|                 </li> |  | ||||||
|                 <li> |  | ||||||
|         <v-avatar |         <v-avatar | ||||||
|             size="large" |             size="large" | ||||||
|             color="#0e6942" |             color="#0e6942" | ||||||
|                         style="font-size: large; font-weight: bold" |             class="user-button" | ||||||
|             >{{ initials }}</v-avatar |             >{{ initials }}</v-avatar | ||||||
|         > |         > | ||||||
|                 </li> |     </v-app-bar> | ||||||
|             </div> |     <v-navigation-drawer | ||||||
|         </nav> |         v-model="drawer" | ||||||
|         <router-view /> |         temporary | ||||||
|     </main> |         app | ||||||
|  |     > | ||||||
|  |         <v-list> | ||||||
|  |             <v-list-item | ||||||
|  |                 to="/user/assignment" | ||||||
|  |                 link | ||||||
|  |             > | ||||||
|  |                 <v-list-item-title class="menu_item">{{ t("assignments") }}</v-list-item-title> | ||||||
|  |             </v-list-item> | ||||||
|  | 
 | ||||||
|  |             <v-list-item | ||||||
|  |                 to="/user/class" | ||||||
|  |                 link | ||||||
|  |             > | ||||||
|  |                 <v-list-item-title class="menu_item">{{ t("classes") }}</v-list-item-title> | ||||||
|  |             </v-list-item> | ||||||
|  | 
 | ||||||
|  |             <v-list-item | ||||||
|  |                 to="/user/discussion" | ||||||
|  |                 link | ||||||
|  |             > | ||||||
|  |                 <v-list-item-title class="menu_item">{{ t("discussions") }}</v-list-item-title> | ||||||
|  |             </v-list-item> | ||||||
|  |         </v-list> | ||||||
|  |     </v-navigation-drawer> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <style scoped> | <style scoped> | ||||||
|  |     .app-bar { | ||||||
|  |         background-color: #f6faf2; | ||||||
|  |     } | ||||||
|     .menu { |     .menu { | ||||||
|         background-color: #f6faf2; |         background-color: #f6faf2; | ||||||
|         display: flex; |         display: flex; | ||||||
|         justify-content: space-between; |         justify-content: space-between; | ||||||
|     } |     } | ||||||
| 
 |     .user-button { | ||||||
|     .right { |         margin-right: 10px; | ||||||
|         align-items: center; |         font-size: large; | ||||||
|         padding: 10px; |         font-weight: bold; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     .right li { |     .right li { | ||||||
|  | @ -347,16 +243,19 @@ | ||||||
|         color: #0e6942; |         color: #0e6942; | ||||||
|         text-decoration: none; |         text-decoration: none; | ||||||
|         font-size: large; |         font-size: large; | ||||||
|     } |         text-transform: none; | ||||||
| 
 |  | ||||||
|     nav a.router-link-active { |  | ||||||
|         font-weight: bold; |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     @media (max-width: 700px) { |     @media (max-width: 700px) { | ||||||
|         .menu { |         .menu { | ||||||
|             display: none; |             display: none; | ||||||
|         } |         } | ||||||
|  |         .caption { | ||||||
|  |             font-size: smaller; | ||||||
|  |         } | ||||||
|  |         .dwengo_logo { | ||||||
|  |             width: 100px; | ||||||
|  |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     @media (min-width: 701px) { |     @media (min-width: 701px) { | ||||||
|  |  | ||||||
|  | @ -1,14 +1,14 @@ | ||||||
| <script setup lang="ts"> | <script setup lang="ts"> | ||||||
| import { useI18n } from "vue-i18n"; |     import { useI18n } from "vue-i18n"; | ||||||
| 
 | 
 | ||||||
| const { t } = useI18n(); |     const { t } = useI18n(); | ||||||
| 
 | 
 | ||||||
| defineProps<{ |     defineProps<{ | ||||||
|         path: string; |         path: string; | ||||||
|         title: string; |         title: string; | ||||||
|         description: string; |         description: string; | ||||||
|         image: string; |         image: string; | ||||||
| }>(); |     }>(); | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
| <template> | <template> | ||||||
|  | @ -31,7 +31,10 @@ defineProps<{ | ||||||
|         </v-card-title> |         </v-card-title> | ||||||
|         <v-card-text class="description flex-grow-1">{{ description }}</v-card-text> |         <v-card-text class="description flex-grow-1">{{ description }}</v-card-text> | ||||||
|         <v-card-actions> |         <v-card-actions> | ||||||
|             <v-btn :to="`theme/${path}`" variant="text"> |             <v-btn | ||||||
|  |                 :to="`theme/${path}`" | ||||||
|  |                 variant="text" | ||||||
|  |             > | ||||||
|                 {{ t("read-more") }} |                 {{ t("read-more") }} | ||||||
|             </v-btn> |             </v-btn> | ||||||
|         </v-card-actions> |         </v-card-actions> | ||||||
|  | @ -39,36 +42,36 @@ defineProps<{ | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <style scoped> | <style scoped> | ||||||
| .theme-card { |     .theme-card { | ||||||
|         display: flex; |         display: flex; | ||||||
|         flex-direction: column; |         flex-direction: column; | ||||||
|         height: 100%; |         height: 100%; | ||||||
|         padding: 1rem; |         padding: 1rem; | ||||||
|         cursor: pointer; |         cursor: pointer; | ||||||
| } |     } | ||||||
| 
 | 
 | ||||||
| .theme-card:hover { |     .theme-card:hover { | ||||||
|         background-color: rgba(0, 0, 0, 0.03); |         background-color: rgba(0, 0, 0, 0.03); | ||||||
| } |     } | ||||||
| 
 | 
 | ||||||
| .title-container { |     .title-container { | ||||||
|         display: flex; |         display: flex; | ||||||
|         align-items: center; |         align-items: center; | ||||||
|         gap: 10px; |         gap: 10px; | ||||||
|         text-align: left; |         text-align: left; | ||||||
|         justify-content: flex-start; |         justify-content: flex-start; | ||||||
| } |     } | ||||||
| 
 | 
 | ||||||
| .title-image { |     .title-image { | ||||||
|         flex-shrink: 0; |         flex-shrink: 0; | ||||||
|         border-radius: 5px; |         border-radius: 5px; | ||||||
|         margin-left: 0; |         margin-left: 0; | ||||||
| } |     } | ||||||
| 
 | 
 | ||||||
| .title { |     .title { | ||||||
|         flex-grow: 1; |         flex-grow: 1; | ||||||
|         white-space: normal; |         white-space: normal; | ||||||
|         overflow-wrap: break-word; |         overflow-wrap: break-word; | ||||||
|         word-break: break-word; |         word-break: break-word; | ||||||
| } |     } | ||||||
| </style> | </style> | ||||||
|  |  | ||||||
|  | @ -1,4 +1,4 @@ | ||||||
| import {apiConfig} from "@/config.ts"; | import { apiConfig } from "@/config.ts"; | ||||||
| 
 | 
 | ||||||
| export class BaseController { | export class BaseController { | ||||||
|     protected baseUrl: string; |     protected baseUrl: string; | ||||||
|  |  | ||||||
|  | @ -1,4 +1,4 @@ | ||||||
| import {ThemeController} from "@/controllers/themes.ts"; | import { ThemeController } from "@/controllers/themes.ts"; | ||||||
| 
 | 
 | ||||||
| export function controllerGetter<T>(Factory: new () => T): () => T { | export function controllerGetter<T>(Factory: new () => T): () => T { | ||||||
|     let instance: T | undefined; |     let instance: T | undefined; | ||||||
|  |  | ||||||
|  | @ -1,4 +1,4 @@ | ||||||
| import {BaseController} from "@/controllers/base-controller.ts"; | import { BaseController } from "@/controllers/base-controller.ts"; | ||||||
| 
 | 
 | ||||||
| export class ThemeController extends BaseController { | export class ThemeController extends BaseController { | ||||||
|     constructor() { |     constructor() { | ||||||
|  |  | ||||||
|  | @ -10,7 +10,7 @@ import i18n from "./i18n/i18n.ts"; | ||||||
| // Components
 | // Components
 | ||||||
| import App from "./App.vue"; | import App from "./App.vue"; | ||||||
| import router from "./router"; | import router from "./router"; | ||||||
| import { VueQueryPlugin, QueryClient } from '@tanstack/vue-query'; | import { VueQueryPlugin, QueryClient } from "@tanstack/vue-query"; | ||||||
| 
 | 
 | ||||||
| const app = createApp(App); | const app = createApp(App); | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -1,25 +1,22 @@ | ||||||
| import { useQuery } from '@tanstack/vue-query'; | import { useQuery } from "@tanstack/vue-query"; | ||||||
| import { getThemeController } from '@/controllers/controllers'; | import { getThemeController } from "@/controllers/controllers"; | ||||||
| import {type MaybeRefOrGetter, toValue} from "vue"; | import { type MaybeRefOrGetter, toValue } from "vue"; | ||||||
| 
 | 
 | ||||||
| const themeController = getThemeController(); | const themeController = getThemeController(); | ||||||
| 
 | 
 | ||||||
| export const useThemeQuery = (language: MaybeRefOrGetter<string>) => { | export const useThemeQuery = (language: MaybeRefOrGetter<string>) => | ||||||
|     return useQuery({ |     useQuery({ | ||||||
|         queryKey: ['themes', language], |         queryKey: ["themes", language], | ||||||
|         queryFn: () => { |         queryFn: () => { | ||||||
|             const lang = toValue(language); |             const lang = toValue(language); | ||||||
|             return themeController.getAll(lang); |             return themeController.getAll(lang); | ||||||
|         }, |         }, | ||||||
|         enabled: () => !!toValue(language), |         enabled: () => Boolean(toValue(language)), | ||||||
|     }); |     }); | ||||||
| }; |  | ||||||
| 
 | 
 | ||||||
| export const useThemeHruidsQuery = (themeKey: string | null) => { | export const useThemeHruidsQuery = (themeKey: string | null) => | ||||||
|     return useQuery({ |     useQuery({ | ||||||
|         queryKey: ['theme-hruids', themeKey], |         queryKey: ["theme-hruids", themeKey], | ||||||
|         queryFn: () => themeController.getHruidsByKey(themeKey!), |         queryFn: () => themeController.getHruidsByKey(themeKey!), | ||||||
|         enabled: !!themeKey, |         enabled: Boolean(themeKey), | ||||||
|     }); |     }); | ||||||
| }; |  | ||||||
| 
 |  | ||||||
|  |  | ||||||
|  | @ -1,11 +1,14 @@ | ||||||
| import apiClient from "@/services/api-client.ts"; | import apiClient from "@/services/api-client.ts"; | ||||||
| import type { FrontendAuthConfig } from "@/services/auth/auth.d.ts"; | import type { FrontendAuthConfig } from "@/services/auth/auth.d.ts"; | ||||||
| 
 | 
 | ||||||
|  | export const AUTH_CONFIG_ENDPOINT = "auth/config"; | ||||||
|  | 
 | ||||||
| /** | /** | ||||||
|  * Fetch the authentication configuration from the backend. |  * Fetch the authentication configuration from the backend. | ||||||
|  */ |  */ | ||||||
| export async function loadAuthConfig() { | export async function loadAuthConfig() { | ||||||
|     const authConfig = (await apiClient.get<FrontendAuthConfig>("auth/config")).data; |     const authConfigResponse = await apiClient.get<FrontendAuthConfig>(AUTH_CONFIG_ENDPOINT); | ||||||
|  |     const authConfig = authConfigResponse.data; | ||||||
|     return { |     return { | ||||||
|         student: { |         student: { | ||||||
|             authority: authConfig.student.authority, |             authority: authConfig.student.authority, | ||||||
|  |  | ||||||
|  | @ -5,7 +5,7 @@ | ||||||
| import { computed, reactive } from "vue"; | import { computed, reactive } from "vue"; | ||||||
| import type { AuthState, Role, UserManagersForRoles } from "@/services/auth/auth.d.ts"; | import type { AuthState, Role, UserManagersForRoles } from "@/services/auth/auth.d.ts"; | ||||||
| import { User, UserManager } from "oidc-client-ts"; | import { User, UserManager } from "oidc-client-ts"; | ||||||
| import { loadAuthConfig } from "@/services/auth/auth-config-loader.ts"; | import { AUTH_CONFIG_ENDPOINT, loadAuthConfig } from "@/services/auth/auth-config-loader.ts"; | ||||||
| import authStorage from "./auth-storage.ts"; | import authStorage from "./auth-storage.ts"; | ||||||
| import { loginRoute } from "@/config.ts"; | import { loginRoute } from "@/config.ts"; | ||||||
| import apiClient from "@/services/api-client.ts"; | import apiClient from "@/services/api-client.ts"; | ||||||
|  | @ -108,7 +108,7 @@ async function logout(): Promise<void> { | ||||||
| apiClient.interceptors.request.use( | apiClient.interceptors.request.use( | ||||||
|     async (reqConfig) => { |     async (reqConfig) => { | ||||||
|         const token = authState?.user?.access_token; |         const token = authState?.user?.access_token; | ||||||
|         if (token) { |         if (token && reqConfig.url !== AUTH_CONFIG_ENDPOINT) { | ||||||
|             reqConfig.headers.Authorization = `Bearer ${token}`; |             reqConfig.headers.Authorization = `Bearer ${token}`; | ||||||
|         } |         } | ||||||
|         return reqConfig; |         return reqConfig; | ||||||
|  |  | ||||||
|  | @ -1,33 +1,64 @@ | ||||||
| export const THEMES_KEYS = [ | export const THEMES_KEYS = [ | ||||||
|     "kiks", "art", "socialrobot", "agriculture", "wegostem", |     "kiks", | ||||||
|     "computational_thinking", "math_with_python", "python_programming", |     "art", | ||||||
|     "stem", "care", "chatbot", "physical_computing", "algorithms", "basics_ai" |     "socialrobot", | ||||||
|  |     "agriculture", | ||||||
|  |     "wegostem", | ||||||
|  |     "computational_thinking", | ||||||
|  |     "math_with_python", | ||||||
|  |     "python_programming", | ||||||
|  |     "stem", | ||||||
|  |     "care", | ||||||
|  |     "chatbot", | ||||||
|  |     "physical_computing", | ||||||
|  |     "algorithms", | ||||||
|  |     "basics_ai", | ||||||
| ]; | ]; | ||||||
| 
 | 
 | ||||||
| export const THEMESITEMS: Record<string, string[]> = { | export const THEMESITEMS: Record<string, string[]> = { | ||||||
|     "all": THEMES_KEYS, |     all: THEMES_KEYS, | ||||||
|     "culture": ["art", "wegostem", "chatbot"], |     culture: ["art", "wegostem", "chatbot"], | ||||||
|     "electricity-and-mechanics": ["socialrobot", "wegostem", "stem", "physical_computing"], |     "electricity-and-mechanics": ["socialrobot", "wegostem", "stem", "physical_computing"], | ||||||
|     "nature-and-climate": ["kiks", "agriculture"], |     "nature-and-climate": ["kiks", "agriculture"], | ||||||
|     "agriculture": ["agriculture"], |     agriculture: ["agriculture"], | ||||||
|     "society": ["kiks", "socialrobot", "care", "chatbot"], |     society: ["kiks", "socialrobot", "care", "chatbot"], | ||||||
|     "math": ["kiks", "math_with_python", "python_programming", "stem", "care", "basics_ai"], |     math: ["kiks", "math_with_python", "python_programming", "stem", "care", "basics_ai"], | ||||||
|     "technology": ["socialrobot", "wegostem", "computational_thinking", "stem", "physical_computing", "basics_ai"], |     technology: ["socialrobot", "wegostem", "computational_thinking", "stem", "physical_computing", "basics_ai"], | ||||||
|     "algorithms": ["math_with_python", "python_programming", "stem", "algorithms", "basics_ai"], |     algorithms: ["math_with_python", "python_programming", "stem", "algorithms", "basics_ai"], | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
|  | export const AGEITEMS = ["all", "primary-school", "lower-secondary", "upper-secondary", "high-school", "older"]; | ||||||
|  | 
 | ||||||
| export const AGE_TO_THEMES: Record<string, string[]> = { | export const AGE_TO_THEMES: Record<string, string[]> = { | ||||||
|     "all": THEMES_KEYS, |     all: THEMES_KEYS, | ||||||
|     "primary-school": ["wegostem", "computational_thinking", "physical_computing"], |     "primary-school": ["wegostem", "computational_thinking", "physical_computing"], | ||||||
|     "lower-secondary": ["socialrobot", "art", "wegostem", "computational_thinking", "physical_computing"], |     "lower-secondary": ["socialrobot", "art", "wegostem", "computational_thinking", "physical_computing"], | ||||||
|     "upper-secondary": ["kiks", "art", "socialrobot", "agriculture", |     "upper-secondary": [ | ||||||
|         "computational_thinking", "math_with_python", "python_programming", |         "kiks", | ||||||
|         "stem", "care", "chatbot", "algorithms", "basics_ai"], |         "art", | ||||||
|     "high-school": [ |         "socialrobot", | ||||||
|         "kiks", "art", "agriculture", "computational_thinking", "math_with_python", "python_programming", |         "agriculture", | ||||||
|         "stem", "care", "chatbot", "algorithms", "basics_ai" |         "computational_thinking", | ||||||
|  |         "math_with_python", | ||||||
|  |         "python_programming", | ||||||
|  |         "stem", | ||||||
|  |         "care", | ||||||
|  |         "chatbot", | ||||||
|  |         "algorithms", | ||||||
|  |         "basics_ai", | ||||||
|     ], |     ], | ||||||
|     "older": [ |     "high-school": [ | ||||||
|         "kiks", "computational_thinking", "algorithms", "basics_ai" |         "kiks", | ||||||
|     ] |         "art", | ||||||
|  |         "agriculture", | ||||||
|  |         "computational_thinking", | ||||||
|  |         "math_with_python", | ||||||
|  |         "python_programming", | ||||||
|  |         "stem", | ||||||
|  |         "care", | ||||||
|  |         "chatbot", | ||||||
|  |         "algorithms", | ||||||
|  |         "basics_ai", | ||||||
|  |     ], | ||||||
|  |     older: ["kiks", "computational_thinking", "algorithms", "basics_ai"], | ||||||
| }; | }; | ||||||
|  |  | ||||||
|  | @ -8,7 +8,7 @@ | ||||||
|     onMounted(async () => { |     onMounted(async () => { | ||||||
|         try { |         try { | ||||||
|             await auth.handleLoginCallback(); |             await auth.handleLoginCallback(); | ||||||
|             await router.replace("/"); // Redirect to home (or dashboard) |             await router.replace("/user"); // Redirect to theme page | ||||||
|         } catch (error) { |         } catch (error) { | ||||||
|             console.error("OIDC callback error:", error); |             console.error("OIDC callback error:", error); | ||||||
|         } |         } | ||||||
|  |  | ||||||
|  | @ -25,9 +25,10 @@ | ||||||
|             <div class="container_left"> |             <div class="container_left"> | ||||||
|                 <img |                 <img | ||||||
|                     :src="dwengoLogo" |                     :src="dwengoLogo" | ||||||
|  |                     alt="Dwengo logo" | ||||||
|                     style="align-self: center" |                     style="align-self: center" | ||||||
|                 /> |                 /> | ||||||
|                 <h> {{ t("homeTitle") }}</h> |                 <h1>{{ t("homeTitle") }}</h1> | ||||||
|                 <p class="info"> |                 <p class="info"> | ||||||
|                     {{ t("homeIntroduction1") }} |                     {{ t("homeIntroduction1") }} | ||||||
|                 </p> |                 </p> | ||||||
|  | @ -55,7 +56,7 @@ | ||||||
|                         width="125" |                         width="125" | ||||||
|                         src="/assets/home/innovative.png" |                         src="/assets/home/innovative.png" | ||||||
|                     ></v-img> |                     ></v-img> | ||||||
|                     <h class="big">{{ t("innovative") }}</h> |                     <h2 class="big">{{ t("innovative") }}</h2> | ||||||
|                 </div> |                 </div> | ||||||
|                 <div class="img_small"> |                 <div class="img_small"> | ||||||
|                     <v-img |                     <v-img | ||||||
|  | @ -63,7 +64,7 @@ | ||||||
|                         width="125" |                         width="125" | ||||||
|                         src="/assets/home/research_based.png" |                         src="/assets/home/research_based.png" | ||||||
|                     ></v-img> |                     ></v-img> | ||||||
|                     <h class="big">{{ t("researchBased") }}</h> |                     <h2 class="big">{{ t("researchBased") }}</h2> | ||||||
|                 </div> |                 </div> | ||||||
|                 <div class="img_small"> |                 <div class="img_small"> | ||||||
|                     <v-img |                     <v-img | ||||||
|  | @ -71,7 +72,7 @@ | ||||||
|                         width="125" |                         width="125" | ||||||
|                         src="/assets/home/inclusive.png" |                         src="/assets/home/inclusive.png" | ||||||
|                     ></v-img> |                     ></v-img> | ||||||
|                     <h class="big">{{ t("sociallyRelevant") }}</h> |                     <h2 class="big">{{ t("sociallyRelevant") }}</h2> | ||||||
|                 </div> |                 </div> | ||||||
|                 <div class="img_small"> |                 <div class="img_small"> | ||||||
|                     <v-img |                     <v-img | ||||||
|  | @ -79,7 +80,7 @@ | ||||||
|                         width="125" |                         width="125" | ||||||
|                         src="/assets/home/socially_relevant.png" |                         src="/assets/home/socially_relevant.png" | ||||||
|                     ></v-img> |                     ></v-img> | ||||||
|                     <h class="big">{{ t("inclusive") }}</h> |                     <h2 class="big">{{ t("inclusive") }}</h2> | ||||||
|                 </div> |                 </div> | ||||||
|             </div> |             </div> | ||||||
|             <div class="container_right"> |             <div class="container_right"> | ||||||
|  | @ -158,7 +159,7 @@ | ||||||
|         margin-bottom: 10px; |         margin-bottom: 10px; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     h { |     h2 { | ||||||
|         font-size: large; |         font-size: large; | ||||||
|         font-weight: bold; |         font-weight: bold; | ||||||
|         align-self: center; |         align-self: center; | ||||||
|  |  | ||||||
|  | @ -1,11 +1,7 @@ | ||||||
| <script setup lang="ts"> | <script setup lang="ts"></script> | ||||||
| 
 |  | ||||||
| </script> |  | ||||||
| 
 | 
 | ||||||
| <template> | <template> | ||||||
| <main></main> |     <main></main> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <style scoped> | <style scoped></style> | ||||||
| 
 |  | ||||||
| </style> |  | ||||||
|  |  | ||||||
|  | @ -1,13 +1,13 @@ | ||||||
| <script setup lang="ts"> | <script setup lang="ts"> | ||||||
| import {ref, watch} from "vue"; |     import { ref, watch } from "vue"; | ||||||
|     import {useI18n} from "vue-i18n"; |     import { useI18n } from "vue-i18n"; | ||||||
|     import {THEMESITEMS, AGE_TO_THEMES} from "@/utils/constants.ts"; |     import { THEMESITEMS, AGE_TO_THEMES } from "@/utils/constants.ts"; | ||||||
|     import BrowseThemes from "@/components/BrowseThemes.vue"; |     import BrowseThemes from "@/components/BrowseThemes.vue"; | ||||||
| 
 | 
 | ||||||
|     const {t, locale} = useI18n(); |     const { t, locale } = useI18n(); | ||||||
| 
 | 
 | ||||||
|     const selectedThemeKey = ref<string>('all'); |     const selectedThemeKey = ref<string>("all"); | ||||||
|     const selectedAgeKey = ref<string>('all'); |     const selectedAgeKey = ref<string>("all"); | ||||||
| 
 | 
 | ||||||
|     const allThemes = ref(Object.keys(THEMESITEMS)); |     const allThemes = ref(Object.keys(THEMESITEMS)); | ||||||
|     const availableThemes = ref([...allThemes.value]); |     const availableThemes = ref([...allThemes.value]); | ||||||
|  | @ -17,18 +17,17 @@ import {ref, watch} from "vue"; | ||||||
| 
 | 
 | ||||||
|     // Reset selection when language changes |     // Reset selection when language changes | ||||||
|     watch(locale, () => { |     watch(locale, () => { | ||||||
|         selectedThemeKey.value = 'all'; |         selectedThemeKey.value = "all"; | ||||||
|         selectedAgeKey.value = 'all'; |         selectedAgeKey.value = "all"; | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
| 
 |  | ||||||
|     watch(selectedThemeKey, () => { |     watch(selectedThemeKey, () => { | ||||||
|         if (selectedThemeKey.value === "all") { |         if (selectedThemeKey.value === "all") { | ||||||
|             availableAges.value = [...allAges.value]; // Reset to all ages |             availableAges.value = [...allAges.value]; // Reset to all ages | ||||||
|         } else { |         } else { | ||||||
|             const themes = THEMESITEMS[selectedThemeKey.value]; |             const themes = THEMESITEMS[selectedThemeKey.value]; | ||||||
|             availableAges.value = allAges.value.filter(age => |             availableAges.value = allAges.value.filter((age) => | ||||||
|                 AGE_TO_THEMES[age]?.some(theme => themes.includes(theme)) |                 AGE_TO_THEMES[age]?.some((theme) => themes.includes(theme)), | ||||||
|             ); |             ); | ||||||
|         } |         } | ||||||
|     }); |     }); | ||||||
|  | @ -38,32 +37,31 @@ import {ref, watch} from "vue"; | ||||||
|             availableThemes.value = [...allThemes.value]; // Reset to all themes |             availableThemes.value = [...allThemes.value]; // Reset to all themes | ||||||
|         } else { |         } else { | ||||||
|             const themes = AGE_TO_THEMES[selectedAgeKey.value]; |             const themes = AGE_TO_THEMES[selectedAgeKey.value]; | ||||||
|             availableThemes.value = allThemes.value.filter(theme => |             availableThemes.value = allThemes.value.filter((theme) => | ||||||
|                 THEMESITEMS[theme]?.some(theme => themes.includes(theme)) |                 THEMESITEMS[theme]?.some((theme) => themes.includes(theme)), | ||||||
|             ); |             ); | ||||||
|         } |         } | ||||||
|     }); |     }); | ||||||
| 
 |  | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
| <template> | <template> | ||||||
|     <div class="main-container"> |     <div class="main-container"> | ||||||
|         <h1 class="title">{{ t("themes") }}</h1> |         <h1 class="title">{{ t("themes") }}</h1> | ||||||
|         <v-container class="dropdowns"> |         <v-container class="dropdowns"> | ||||||
|             <v-select class="v-select" |             <v-select | ||||||
|  |                 class="v-select" | ||||||
|                 :label="t('choose-theme')" |                 :label="t('choose-theme')" | ||||||
|                       :items="availableThemes.map(theme => ({ title: t(`theme-options.${theme}`), value: theme }))" |                 :items="availableThemes.map((theme) => ({ title: t(`theme-options.${theme}`), value: theme }))" | ||||||
|                 v-model="selectedThemeKey" |                 v-model="selectedThemeKey" | ||||||
|                 item-title="title" |                 item-title="title" | ||||||
|                 item-value="value" |                 item-value="value" | ||||||
|                 variant="outlined" |                 variant="outlined" | ||||||
|             /> |             /> | ||||||
| 
 | 
 | ||||||
| 
 |  | ||||||
|             <v-select |             <v-select | ||||||
|                 class="v-select" |                 class="v-select" | ||||||
|                 :label="t('choose-age')" |                 :label="t('choose-age')" | ||||||
|                 :items="availableAges.map(age => ({ key: age, label: t(`age-options.${age}`), value: age }))" |                 :items="availableAges.map((age) => ({ key: age, label: t(`age-options.${age}`), value: age }))" | ||||||
|                 v-model="selectedAgeKey" |                 v-model="selectedAgeKey" | ||||||
|                 item-title="label" |                 item-title="label" | ||||||
|                 item-value="key" |                 item-value="key" | ||||||
|  | @ -71,55 +69,55 @@ import {ref, watch} from "vue"; | ||||||
|             ></v-select> |             ></v-select> | ||||||
|         </v-container> |         </v-container> | ||||||
| 
 | 
 | ||||||
|         <BrowseThemes :selectedTheme="selectedThemeKey ?? ''" :selectedAge="selectedAgeKey ?? ''"/> |         <BrowseThemes | ||||||
|  |             :selectedTheme="selectedThemeKey ?? ''" | ||||||
|  |             :selectedAge="selectedAgeKey ?? ''" | ||||||
|  |         /> | ||||||
|     </div> |     </div> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <style scoped> | <style scoped> | ||||||
| .main-container { |     .main-container { | ||||||
|         min-height: 100vh; |         min-height: 100vh; | ||||||
|         min-width: 100vw; |         min-width: 100vw; | ||||||
|         display: flex; |         display: flex; | ||||||
|         flex-direction: column; |         flex-direction: column; | ||||||
|         align-items: flex-start; |         align-items: flex-start; | ||||||
|         justify-content: flex-start; |         justify-content: flex-start; | ||||||
| } |     } | ||||||
| 
 | 
 | ||||||
| .title { |     .title { | ||||||
|         max-width: 50rem; |         max-width: 50rem; | ||||||
|         margin-left: 1rem; |         margin-left: 1rem; | ||||||
|         margin-top: 1rem; |         margin-top: 1rem; | ||||||
|         text-align: center; |         text-align: center; | ||||||
|         display: flex; |         display: flex; | ||||||
|         justify-content: center; |         justify-content: center; | ||||||
| } |     } | ||||||
| 
 | 
 | ||||||
| 
 |     .dropdowns { | ||||||
| .dropdowns { |  | ||||||
|         display: flex; |         display: flex; | ||||||
|         justify-content: space-between; |         justify-content: space-between; | ||||||
|         gap: 5rem; |         gap: 5rem; | ||||||
|         width: 80%; |         width: 80%; | ||||||
| } |     } | ||||||
| 
 | 
 | ||||||
| .v-select { |     .v-select { | ||||||
|         flex: 1; |         flex: 1; | ||||||
|         min-width: 100px; |         min-width: 100px; | ||||||
| } |     } | ||||||
| 
 | 
 | ||||||
| 
 |     @media (max-width: 768px) { | ||||||
| @media (max-width: 768px) { |  | ||||||
|         .main-container { |         .main-container { | ||||||
|             padding: 1rem; |             padding: 1rem; | ||||||
|         } |         } | ||||||
| } |     } | ||||||
| 
 | 
 | ||||||
| @media (max-width: 700px) { |     @media (max-width: 700px) { | ||||||
|         .dropdowns { |         .dropdowns { | ||||||
|             flex-direction: column; |             flex-direction: column; | ||||||
|             gap: 1rem; |             gap: 1rem; | ||||||
|             width: 80%; |             width: 80%; | ||||||
|         } |         } | ||||||
| } |     } | ||||||
| 
 |  | ||||||
| </style> | </style> | ||||||
|  |  | ||||||
							
								
								
									
										6
									
								
								package-lock.json
									
										
									
										generated
									
									
									
								
							
							
						
						
									
										6
									
								
								package-lock.json
									
										
									
										generated
									
									
									
								
							|  | @ -118,7 +118,7 @@ | ||||||
|                 "jsdom": "^26.0.0", |                 "jsdom": "^26.0.0", | ||||||
|                 "npm-run-all2": "^7.0.2", |                 "npm-run-all2": "^7.0.2", | ||||||
|                 "typescript": "~5.7.3", |                 "typescript": "~5.7.3", | ||||||
|                 "vite": "^6.1.0", |                 "vite": "^6.1.2", | ||||||
|                 "vite-plugin-vue-devtools": "^7.7.2", |                 "vite-plugin-vue-devtools": "^7.7.2", | ||||||
|                 "vitest": "^3.0.5", |                 "vitest": "^3.0.5", | ||||||
|                 "vue-tsc": "^2.2.2" |                 "vue-tsc": "^2.2.2" | ||||||
|  | @ -9821,7 +9821,9 @@ | ||||||
|             } |             } | ||||||
|         }, |         }, | ||||||
|         "node_modules/vite": { |         "node_modules/vite": { | ||||||
|             "version": "6.1.1", |             "version": "6.1.2", | ||||||
|  |             "resolved": "https://registry.npmjs.org/vite/-/vite-6.1.2.tgz", | ||||||
|  |             "integrity": "sha512-EiXfDyO/uNKhYOSlZ6+9qBz4H46A8Lr07pyjmb88KTbJ+xkXvnqtxvgtg2VxPU6Kfj8Ep0un9JLqdrCWLqIanw==", | ||||||
|             "dev": true, |             "dev": true, | ||||||
|             "license": "MIT", |             "license": "MIT", | ||||||
|             "dependencies": { |             "dependencies": { | ||||||
|  |  | ||||||
		Reference in a new issue
	
	 Joyelle Ndagijimana
						Joyelle Ndagijimana