diff --git a/README.md b/README.md index dc09bbfc..3526b53d 100644 --- a/README.md +++ b/README.md @@ -21,14 +21,16 @@ Alternatief kan je één van de volgende methodes gebruiken om de applicatie lok ### 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/) en [Docker Compose](https://docs.docker.com/compose/)). 2. Clone deze repository. -3. In de backend, kopieer `.env.example` (of `.env.development.example`) naar `.env` en pas de variabelen aan waar - nodig. -4. Voer `docker compose up` uit in de root van de repository. +3. In de backend, kopieer `.env.example` naar `.env` en pas de variabelen aan waar nodig. +4. Voer `docker compose -f compose.staging.yml up --build` uit in de root van de repository. 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). +6. De applicatie is nu beschikbaar op [`http://localhost/`](http://localhost/) en [`http://localhost/api`](http://localhost/api). ```bash docker compose version @@ -38,14 +40,13 @@ cp .env.example .env # Pas .env aan nano .env cd .. -docker compose up -# Configureer de applicatie +docker compose -f compose.staging.yml up --build ``` -### Handmatige installatie +### Handmatige installatie en ontwikkeling 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 diff --git a/backend/.env.development.example b/backend/.env.development.example index 466e1b7b..d03a6744 100644 --- a/backend/.env.development.example +++ b/backend/.env.development.example @@ -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_PORT=5431 +#DWENGO_DB_NAME=dwengo DWENGO_DB_USERNAME=postgres DWENGO_DB_PASSWORD=postgres DWENGO_DB_UPDATE=true - -# Auth +#DWENGO_DB_CONTENT_PREFIX=u_ DWENGO_AUTH_STUDENT_URL=http://localhost:7080/realms/student 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_CLIENT_ID=dwengo 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_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 diff --git a/backend/.env.example b/backend/.env.example index 68cef35d..8873515c 100644 --- a/backend/.env.example +++ b/backend/.env.example @@ -1,27 +1,68 @@ # # 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_PORT=5431 - -# Change this to the actual credentials of the user Dwengo should use in the backend -DWENGO_DB_USERNAME=postgres -DWENGO_DB_PASSWORD=postgres - +# The port of the database. +#DWENGO_DB_PORT=5432 +# The name of the database. +#DWENGO_DB_NAME=dwengo +# ! 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. -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. -DWENGO_AUTH_STUDENT_URL=http://localhost:7080/realms/student +# ! Change this! The external URL for student authentication. Should be reachable by the client. +# 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_JWKS_ENDPOINT=http://localhost:7080/realms/student/protocol/openid-connect/certs - -# Data for the identity provider via which the teachers authenticate. -DWENGO_AUTH_TEACHER_URL=http://localhost:7080/realms/teacher +# ! 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. +# E.g. http://idp:7080/realms/student/protocol/openid-connect/certs +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_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 -# LOKI_HOST=http://localhost:3102 +# Allowed origins for CORS requests. Separate multiple origins with a comma. +#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 diff --git a/backend/.env.production.example b/backend/.env.production.example index 390409d1..4f36cf53 100644 --- a/backend/.env.production.example +++ b/backend/.env.production.example @@ -1,28 +1,37 @@ -DWENGO_PORT=3000 # The port the backend will listen on -DWENGO_DB_HOST=db # Name of the database container -DWENGO_DB_PORT=5431 +# +# Production environment configuration +# +# 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_USERNAME=postgres DWENGO_DB_PASSWORD=postgres - -# Set this to true when the database scheme needs to be updated. In that case, take a backup first. DWENGO_DB_UPDATE=false +#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_CLIENT_ID=dwengo DWENGO_AUTH_STUDENT_JWKS_ENDPOINT=http://idp:7080/idp/realms/student/protocol/openid-connect/certs # Name of the idp container -# Data for the identity provider via which the teachers authenticate. DWENGO_AUTH_TEACHER_URL=https://sel2-1.ugent.be/idp/realms/teacher DWENGO_AUTH_TEACHER_CLIENT_ID=dwengo DWENGO_AUTH_TEACHER_JWKS_ENDPOINT=http://idp:7080/idp/realms/teacher/protocol/openid-connect/certs # Name of the idp container +#DWENGO_AUTH_AUDIENCE=account -# -# Advanced configuration -# +#DWENGO_CORS_ALLOWED_ORIGINS= +#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 diff --git a/backend/.env.test.example b/backend/.env.test.example index b8a81003..535628cd 100644 --- a/backend/.env.test.example +++ b/backend/.env.test.example @@ -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_UPDATE=true diff --git a/backend/Dockerfile b/backend/Dockerfile index bd7db2ff..7c63c4b8 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -30,6 +30,7 @@ COPY package-lock.json backend/package.json ./ RUN npm install --silent --only=production COPY ./docs /docs +COPY ./backend/i18n /app/i18n COPY --from=build-stage /app/backend/dist ./dist/ EXPOSE 3000 diff --git a/backend/README.md b/backend/README.md index 442cea82..8a78ed14 100644 --- a/backend/README.md +++ b/backend/README.md @@ -4,23 +4,24 @@ ```shell 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 +# Omgevingsvariabelen +cp .env.development.example .env.development.local + npm run dev ``` -### Production - -```shell -npm run build -npm run start -``` - ### Tests 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 ``` +### 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 Tijdens development is het voldoende om gebruik te maken van de keycloak configuratie die automatisch ingeladen wordt. diff --git a/backend/_i18n/de.yml b/backend/i18n/de.yml similarity index 100% rename from backend/_i18n/de.yml rename to backend/i18n/de.yml diff --git a/backend/_i18n/en.yml b/backend/i18n/en.yml similarity index 100% rename from backend/_i18n/en.yml rename to backend/i18n/en.yml diff --git a/backend/_i18n/fr.yml b/backend/i18n/fr.yml similarity index 100% rename from backend/_i18n/fr.yml rename to backend/i18n/fr.yml diff --git a/backend/_i18n/nl.yml b/backend/i18n/nl.yml similarity index 100% rename from backend/_i18n/nl.yml rename to backend/i18n/nl.yml diff --git a/backend/src/config.ts b/backend/src/config.ts index 69af5d74..b9974a3b 100644 --- a/backend/src/config.ts +++ b/backend/src/config.ts @@ -1,12 +1,7 @@ import { EnvVars, getEnvVar } from './util/envvars.js'; -import { Language } from './entities/content/language.js'; // API export const DWENGO_API_BASE = getEnvVar(EnvVars.LearningContentRepoApiBaseUrl); 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; diff --git a/backend/src/controllers/assignments.ts b/backend/src/controllers/assignments.ts index 03332469..16dbb310 100644 --- a/backend/src/controllers/assignments.ts +++ b/backend/src/controllers/assignments.ts @@ -37,7 +37,7 @@ export async function createAssignmentHandler(req: Request, re return; } - res.status(201).json({ assignment: assignment }); + res.status(201).json(assignment); } export async function getAssignmentHandler(req: Request, res: Response): Promise { @@ -62,13 +62,14 @@ export async function getAssignmentHandler(req: Request, res: export async function getAssignmentsSubmissionsHandler(req: Request, res: Response): Promise { const classid = req.params.classid; const assignmentNumber = +req.params.id; + const full = req.query.full === 'true'; if (isNaN(assignmentNumber)) { res.status(400).json({ error: 'Assignment id must be a number' }); return; } - const submissions = await getAssignmentsSubmissions(classid, assignmentNumber); + const submissions = await getAssignmentsSubmissions(classid, assignmentNumber, full); res.json({ submissions: submissions, diff --git a/backend/src/controllers/classes.ts b/backend/src/controllers/classes.ts index ca2f5698..7526f7c4 100644 --- a/backend/src/controllers/classes.ts +++ b/backend/src/controllers/classes.ts @@ -1,5 +1,5 @@ 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'; export async function getAllClassesHandler(req: Request, res: Response): Promise { @@ -28,30 +28,19 @@ export async function createClassHandler(req: Request, res: Response): Promise { - try { - const classId = req.params.id; - const cls = await getClass(classId); + const classId = req.params.id; + const cls = await getClass(classId); - if (!cls) { - res.status(404).json({ error: 'Class not found' }); - 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); - } catch (error) { - console.error('Error fetching learning objects:', error); - res.status(500).json({ error: 'Internal server error' }); + if (!cls) { + res.status(404).json({ error: 'Class not found' }); + return; } + + res.json(cls); } export async function getClassStudentsHandler(req: Request, res: Response): Promise { @@ -67,7 +56,7 @@ export async function getClassStudentsHandler(req: Request, res: Response): Prom export async function getTeacherInvitationsHandler(req: Request, res: Response): Promise { 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); diff --git a/backend/src/controllers/groups.ts b/backend/src/controllers/groups.ts index b7bfd212..38d5d5d0 100644 --- a/backend/src/controllers/groups.ts +++ b/backend/src/controllers/groups.ts @@ -28,6 +28,11 @@ export async function getGroupHandler(req: Request, res: Response): const group = await getGroup(classId, assignmentId, groupId, full); + if (!group) { + res.status(404).json({ error: 'Group not found' }); + return; + } + res.json(group); } @@ -66,12 +71,12 @@ export async function createGroupHandler(req: Request, res: Response): Promise { const classId = req.params.classid; - // Const full = req.query.full === 'true'; + const full = req.query.full === 'true'; const assignmentId = +req.params.assignmentid; @@ -87,7 +92,7 @@ export async function getGroupSubmissionsHandler(req: Request, res: Response): P return; } - const submissions = await getGroupSubmissions(classId, assignmentId, groupId); + const submissions = await getGroupSubmissions(classId, assignmentId, groupId, full); res.json({ submissions: submissions, diff --git a/backend/src/controllers/learning-objects.ts b/backend/src/controllers/learning-objects.ts index a74d745d..b91338b7 100644 --- a/backend/src/controllers/learning-objects.ts +++ b/backend/src/controllers/learning-objects.ts @@ -39,7 +39,7 @@ export async function getAllLearningObjects(req: Request, res: Response): Promis learningObjects = await learningObjectService.getLearningObjectIdsFromPath(learningPathId); } - res.json(learningObjects); + res.json({ learningObjects: learningObjects }); } export async function getLearningObject(req: Request, res: Response): Promise { diff --git a/backend/src/controllers/questions.ts b/backend/src/controllers/questions.ts index 917b48ae..00a51329 100644 --- a/backend/src/controllers/questions.ts +++ b/backend/src/controllers/questions.ts @@ -48,7 +48,7 @@ export async function getAllQuestionsHandler(req: Request, res: Response): Promi if (!questions) { res.status(404).json({ error: `Questions not found.` }); } else { - res.json(questions); + res.json({ questions: questions }); } } @@ -76,12 +76,12 @@ export async function getQuestionAnswersHandler(req: Request, res: Response): Pr return; } - const answers = getAnswersByQuestion(questionId, full); + const answers = await getAnswersByQuestion(questionId, full); if (!answers) { - res.status(404).json({ error: `Questions not found.` }); + res.status(404).json({ error: `Questions not found` }); } 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); if (!question) { - res.status(400).json({ error: 'Could not add question' }); + res.status(400).json({ error: 'Could not create question' }); } else { res.json(question); } diff --git a/backend/src/controllers/students.ts b/backend/src/controllers/students.ts index 6c253cff..8ce5b11a 100644 --- a/backend/src/controllers/students.ts +++ b/backend/src/controllers/students.ts @@ -9,29 +9,21 @@ import { getStudentGroups, getStudentSubmissions, } 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 { getStudentRepository } from '../data/repositories.js'; -import { UserDTO } from '../interfaces/user.js'; // TODO: accept arguments (full, ...) // TODO: endpoints export async function getAllStudentsHandler(req: Request, res: Response): Promise { const full = req.query.full === 'true'; - const studentRepository = getStudentRepository(); - - const students: StudentDTO[] | string[] = full ? await getAllStudents() : await getAllStudents(); + const students = await getAllStudents(full); if (!students) { res.status(404).json({ error: `Student not found.` }); return; } - res.status(201).json(students); + res.json({ students: students }); } export async function getStudentHandler(req: Request, res: Response): Promise { @@ -51,7 +43,7 @@ export async function getStudentHandler(req: Request, res: Response): Promise { - try { - const full = req.query.full === 'true'; - const username = req.params.id; + const full = req.query.full === 'true'; + const username = req.params.id; - const classes = await getStudentClasses(username, full); + const classes = await getStudentClasses(username, full); - res.json({ - 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' }); - } + res.json({ + classes: classes, + }); } // TODO @@ -137,8 +126,9 @@ export async function getStudentGroupsHandler(req: Request, res: Response): Prom export async function getStudentSubmissionsHandler(req: Request, res: Response): Promise { const username = req.params.id; + const full = req.query.full === 'true'; - const submissions = await getStudentSubmissions(username); + const submissions = await getStudentSubmissions(username, full); res.json({ submissions: submissions, diff --git a/backend/src/controllers/submissions.ts b/backend/src/controllers/submissions.ts index 1e66dbe9..512ac22e 100644 --- a/backend/src/controllers/submissions.ts +++ b/backend/src/controllers/submissions.ts @@ -36,10 +36,11 @@ export async function createSubmissionHandler(req: Request, res: Response) { const submission = await createSubmission(submissionDTO); if (!submission) { - res.status(404).json({ error: 'Submission not added' }); - } else { - res.json(submission); + res.status(400).json({ error: 'Failed to create submission' }); + return; } + + res.json(submission); } export async function deleteSubmissionHandler(req: Request, res: Response) { @@ -53,7 +54,8 @@ export async function deleteSubmissionHandler(req: Request, res: Response) { if (!submission) { res.status(404).json({ error: 'Submission not found' }); - } else { - res.json(submission); + return; } + + res.json(submission); } diff --git a/backend/src/controllers/teachers.ts b/backend/src/controllers/teachers.ts index 52e5e713..7376abed 100644 --- a/backend/src/controllers/teachers.ts +++ b/backend/src/controllers/teachers.ts @@ -4,33 +4,23 @@ import { deleteTeacher, getAllTeachers, getClassesByTeacher, - getClassIdsByTeacher, - getQuestionIdsByTeacher, getQuestionsByTeacher, - getStudentIdsByTeacher, getStudentsByTeacher, getTeacher, } 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 { getTeacherRepository } from '../data/repositories.js'; export async function getAllTeachersHandler(req: Request, res: Response): Promise { const full = req.query.full === 'true'; - const teacherRepository = getTeacherRepository(); - - const teachers: TeacherDTO[] | string[] = full ? await getAllTeachers() : await getAllTeachers(); + const teachers = await getAllTeachers(full); if (!teachers) { res.status(404).json({ error: `Teacher not found.` }); return; } - res.status(201).json(teachers); + res.json({ teachers: teachers }); } export async function getTeacherHandler(req: Request, res: Response): Promise { @@ -45,12 +35,12 @@ export async function getTeacherHandler(req: Request, res: Response): Promise { - try { - const username = req.params.username as string; - const full = req.query.full === 'true'; + const username = req.params.username as string; + const full = req.query.full === 'true'; - if (!username) { - res.status(400).json({ error: 'Missing required field: username' }); - return; - } - - const classes: ClassDTO[] | string[] = full ? await getClassesByTeacher(username) : await getClassIdsByTeacher(username); - - res.status(201).json(classes); - } catch (error) { - console.error('Error fetching classes by teacher:', error); - res.status(500).json({ error: 'Internal server error' }); + if (!username) { + res.status(400).json({ error: 'Missing required field: username' }); + return; } + + const classes = await getClassesByTeacher(username, full); + + if (!classes) { + res.status(404).json({ error: 'Teacher not found' }); + return; + } + + res.json({ classes: classes }); } export async function getTeacherStudentHandler(req: Request, res: Response): Promise { - try { - const username = req.params.username as string; - const full = req.query.full === 'true'; + const username = req.params.username as string; + const full = req.query.full === 'true'; - if (!username) { - res.status(400).json({ error: 'Missing required field: username' }); - return; - } - - const students: StudentDTO[] | string[] = full ? await getStudentsByTeacher(username) : await getStudentIdsByTeacher(username); - - res.status(201).json(students); - } catch (error) { - console.error('Error fetching students by teacher:', error); - res.status(500).json({ error: 'Internal server error' }); + if (!username) { + res.status(400).json({ error: 'Missing required field: username' }); + return; } + + const students = await getStudentsByTeacher(username, full); + + if (!students) { + res.status(404).json({ error: 'Teacher not found' }); + return; + } + + res.json({ students: students }); } export async function getTeacherQuestionHandler(req: Request, res: Response): Promise { - try { - const username = req.params.username as string; - const full = req.query.full === 'true'; + const username = req.params.username as string; + const full = req.query.full === 'true'; - if (!username) { - res.status(400).json({ error: 'Missing required field: username' }); - return; - } - - const questions: QuestionDTO[] | QuestionId[] = full ? await getQuestionsByTeacher(username) : await getQuestionIdsByTeacher(username); - - res.status(201).json(questions); - } catch (error) { - console.error('Error fetching questions by teacher:', error); - res.status(500).json({ error: 'Internal server error' }); + if (!username) { + res.status(400).json({ error: 'Missing required field: username' }); + return; } + + const questions = await getQuestionsByTeacher(username, full); + + if (!questions) { + res.status(404).json({ error: 'Teacher not found' }); + return; + } + + res.json({ questions: questions }); } diff --git a/backend/src/controllers/users.ts b/backend/src/controllers/users.ts deleted file mode 100644 index 850c6549..00000000 --- a/backend/src/controllers/users.ts +++ /dev/null @@ -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(req: Request, res: Response, service: UserService): Promise { - 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(req: Request, res: Response, service: UserService): Promise { - 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(req: Request, res: Response, service: UserService, 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(req: Request, res: Response, service: UserService) { - 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' }); - } -} diff --git a/backend/src/data/repositories.ts b/backend/src/data/repositories.ts index 02385109..cdeb50c1 100644 --- a/backend/src/data/repositories.ts +++ b/backend/src/data/repositories.ts @@ -2,8 +2,6 @@ import { AnyEntity, EntityManager, EntityName, EntityRepository } from '@mikro-o import { forkEntityManager } from '../orm.js'; import { StudentRepository } from './users/student-repository.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 { TeacherRepository } from './users/teacher-repository.js'; import { Class } from '../entities/classes/class.entity.js'; diff --git a/backend/src/data/users/student-repository.ts b/backend/src/data/users/student-repository.ts index 0792678d..a13fbb22 100644 --- a/backend/src/data/users/student-repository.ts +++ b/backend/src/data/users/student-repository.ts @@ -1,5 +1,4 @@ import { Student } from '../../entities/users/student.entity.js'; -import { User } from '../../entities/users/user.entity.js'; import { DwengoEntityRepository } from '../dwengo-entity-repository.js'; // Import { UserRepository } from './user-repository.js'; diff --git a/backend/src/data/users/teacher-repository.ts b/backend/src/data/users/teacher-repository.ts index 2b2bee75..825b4d18 100644 --- a/backend/src/data/users/teacher-repository.ts +++ b/backend/src/data/users/teacher-repository.ts @@ -1,6 +1,5 @@ import { Teacher } from '../../entities/users/teacher.entity.js'; import { DwengoEntityRepository } from '../dwengo-entity-repository.js'; -import { UserRepository } from './user-repository.js'; export class TeacherRepository extends DwengoEntityRepository { public findByUsername(username: string): Promise { diff --git a/backend/src/entities/assignments/assignment.entity.ts b/backend/src/entities/assignments/assignment.entity.ts index 692e2112..daa71ed6 100644 --- a/backend/src/entities/assignments/assignment.entity.ts +++ b/backend/src/entities/assignments/assignment.entity.ts @@ -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 { Group } from './group.entity.js'; import { Language } from '../content/language.js'; diff --git a/backend/src/entities/assignments/group.entity.ts b/backend/src/entities/assignments/group.entity.ts index 213e0f38..cfe21f7f 100644 --- a/backend/src/entities/assignments/group.entity.ts +++ b/backend/src/entities/assignments/group.entity.ts @@ -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 { Student } from '../users/student.entity.js'; import { GroupRepository } from '../../data/assignments/group-repository.js'; diff --git a/backend/src/entities/classes/class-join-request.entity.ts b/backend/src/entities/classes/class-join-request.entity.ts index bdef1f52..fdf13aa9 100644 --- a/backend/src/entities/classes/class-join-request.entity.ts +++ b/backend/src/entities/classes/class-join-request.entity.ts @@ -3,6 +3,12 @@ import { Student } from '../users/student.entity.js'; import { Class } from './class.entity.js'; import { ClassJoinRequestRepository } from '../../data/classes/class-join-request-repository.js'; +export enum ClassJoinRequestStatus { + Open = 'open', + Accepted = 'accepted', + Declined = 'declined', +} + @Entity({ repository: () => ClassJoinRequestRepository, }) @@ -22,9 +28,3 @@ export class ClassJoinRequest { @Enum(() => ClassJoinRequestStatus) status!: ClassJoinRequestStatus; } - -export enum ClassJoinRequestStatus { - Open = 'open', - Accepted = 'accepted', - Declined = 'declined', -} diff --git a/backend/src/interfaces/assignment.ts b/backend/src/interfaces/assignment.ts index 8f6120b6..eefa8c96 100644 --- a/backend/src/interfaces/assignment.ts +++ b/backend/src/interfaces/assignment.ts @@ -2,7 +2,7 @@ import { FALLBACK_LANG } from '../config.js'; import { Assignment } from '../entities/assignments/assignment.entity.js'; import { Class } from '../entities/classes/class.entity.js'; import { languageMap } from '../entities/content/language.js'; -import { GroupDTO, mapToGroupDTO } from './group.js'; +import { GroupDTO } from './group.js'; export interface AssignmentDTO { id: number; @@ -46,7 +46,5 @@ export function mapToAssignment(assignmentData: AssignmentDTO, cls: Class): Assi assignment.learningPathLanguage = languageMap[assignmentData.language] || FALLBACK_LANG; assignment.within = cls; - console.log(assignment); - return assignment; } diff --git a/backend/src/interfaces/class.ts b/backend/src/interfaces/class.ts index 371e3cae..ea1d4901 100644 --- a/backend/src/interfaces/class.ts +++ b/backend/src/interfaces/class.ts @@ -9,12 +9,6 @@ export interface ClassDTO { teachers: string[]; students: string[]; joinRequests: string[]; - endpoints?: { - self: string; - invitations: string; - assignments: string; - students: string; - }; } export function mapToClassDTO(cls: Class): ClassDTO { diff --git a/backend/src/interfaces/question.ts b/backend/src/interfaces/question.ts index 8cca42f6..0da87eb7 100644 --- a/backend/src/interfaces/question.ts +++ b/backend/src/interfaces/question.ts @@ -1,8 +1,6 @@ import { Question } from '../entities/questions/question.entity.js'; -import { UserDTO } from './user.js'; import { LearningObjectIdentifier } from '../entities/content/learning-object-identifier.js'; import { mapToStudentDTO, StudentDTO } from './student.js'; -import { TeacherDTO } from './teacher.js'; export interface QuestionDTO { learningObjectIdentifier: LearningObjectIdentifier; diff --git a/backend/src/interfaces/submission.ts b/backend/src/interfaces/submission.ts index fbaf520d..98cc4f22 100644 --- a/backend/src/interfaces/submission.ts +++ b/backend/src/interfaces/submission.ts @@ -2,13 +2,10 @@ import { Submission } from '../entities/assignments/submission.entity.js'; import { Language } from '../entities/content/language.js'; import { GroupDTO, mapToGroupDTO } from './group.js'; import { mapToStudent, mapToStudentDTO, StudentDTO } from './student.js'; -import { mapToUser } from './user'; -import { Student } from '../entities/users/student.entity'; +import { LearningObjectIdentifier } from './learning-content.js'; export interface SubmissionDTO { - learningObjectHruid: string; - learningObjectLanguage: Language; - learningObjectVersion: number; + learningObjectIdentifier: LearningObjectIdentifier; submissionNumber?: number; submitter: StudentDTO; @@ -17,11 +14,21 @@ export interface SubmissionDTO { content: string; } +export interface SubmissionDTOId { + learningObjectHruid: string; + learningObjectLanguage: Language; + learningObjectVersion: number; + + submissionNumber?: number; +} + export function mapToSubmissionDTO(submission: Submission): SubmissionDTO { return { - learningObjectHruid: submission.learningObjectHruid, - learningObjectLanguage: submission.learningObjectLanguage, - learningObjectVersion: submission.learningObjectVersion, + learningObjectIdentifier: { + hruid: submission.learningObjectHruid, + language: submission.learningObjectLanguage, + version: submission.learningObjectVersion, + }, submissionNumber: submission.submissionNumber, 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 { const submission = new Submission(); - submission.learningObjectHruid = submissionDTO.learningObjectHruid; - submission.learningObjectLanguage = submissionDTO.learningObjectLanguage; - submission.learningObjectVersion = submissionDTO.learningObjectVersion; + submission.learningObjectHruid = submissionDTO.learningObjectIdentifier.hruid; + submission.learningObjectLanguage = submissionDTO.learningObjectIdentifier.language; + submission.learningObjectVersion = submissionDTO.learningObjectIdentifier.version!; // Submission.submissionNumber = submissionDTO.submissionNumber; submission.submitter = mapToStudent(submissionDTO.submitter); // Submission.submissionTime = submissionDTO.time; diff --git a/backend/src/logging/initalize.ts b/backend/src/logging/initalize.ts index 1ff761c9..4d14e8ab 100644 --- a/backend/src/logging/initalize.ts +++ b/backend/src/logging/initalize.ts @@ -1,7 +1,7 @@ import { createLogger, format, Logger as WinstonLogger, transports } from 'winston'; import LokiTransport from 'winston-loki'; 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 { constructor() { @@ -22,10 +22,25 @@ function initializeLogger(): 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({ - host: LOKI_HOST, + host: lokiHost, labels: Labels, - level: LOG_LEVEL, + level: logLevel, json: true, format: format.combine(format.timestamp(), format.json()), 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({ 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; } diff --git a/backend/src/mikro-orm.config.ts b/backend/src/mikro-orm.config.ts index c9cf6ed9..f2fb2bae 100644 --- a/backend/src/mikro-orm.config.ts +++ b/backend/src/mikro-orm.config.ts @@ -3,7 +3,6 @@ import { PostgreSqlDriver } from '@mikro-orm/postgresql'; import { EnvVars, getEnvVar, getNumericEnvVar } from './util/envvars.js'; import { SqliteDriver } from '@mikro-orm/sqlite'; import { MikroOrmLogger } from './logging/mikroOrmLogger.js'; -import { LOG_LEVEL } from './config.js'; // Import alle entity-bestanden handmatig import { User } from './entities/users/user.entity.js'; @@ -69,7 +68,7 @@ function config(testingMode: boolean = false): Options { // EntitiesTs: entitiesTs, // Logging - debug: LOG_LEVEL === 'debug', + debug: getEnvVar(EnvVars.LogLevel) === 'debug', loggerFactory: (options: LoggerOptions) => new MikroOrmLogger(options), }; } diff --git a/backend/src/routes/router.ts b/backend/src/routes/router.ts index 639857a7..99d4312c 100644 --- a/backend/src/routes/router.ts +++ b/backend/src/routes/router.ts @@ -1,10 +1,7 @@ import { Response, Router } from 'express'; import studentRouter from './students.js'; -import groupRouter from './groups.js'; -import assignmentRouter from './assignments.js'; -import submissionRouter from './submissions.js'; +import teacherRouter from './teachers.js'; import classRouter from './classes.js'; -import questionRouter from './questions.js'; import authRouter from './auth.js'; import themeRoutes from './themes.js'; import learningPathRoutes from './learning-paths.js'; @@ -22,11 +19,8 @@ router.get('/', (_, res: Response) => { }); router.use('/student', studentRouter /* #swagger.tags = ['Student'] */); -router.use('/group', groupRouter /* #swagger.tags = ['Group'] */); -router.use('/assignment', assignmentRouter /* #swagger.tags = ['Assignment'] */); -router.use('/submission', submissionRouter /* #swagger.tags = ['Submission'] */); +router.use('/teacher', teacherRouter /* #swagger.tags = ['Teacher'] */); router.use('/class', classRouter /* #swagger.tags = ['Class'] */); -router.use('/question', questionRouter /* #swagger.tags = ['Question'] */); router.use('/auth', authRouter /* #swagger.tags = ['Auth'] */); router.use('/theme', themeRoutes /* #swagger.tags = ['Theme'] */); router.use('/learningPath', learningPathRoutes /* #swagger.tags = ['Learning Path'] */); diff --git a/backend/src/routes/students.ts b/backend/src/routes/students.ts index 7ed7a666..6efbab39 100644 --- a/backend/src/routes/students.ts +++ b/backend/src/routes/students.ts @@ -9,7 +9,6 @@ import { getStudentHandler, getStudentSubmissionsHandler, } from '../controllers/students.js'; -import { getStudentGroups } from '../services/students.js'; const router = express.Router(); // Root endpoint used to search objects diff --git a/backend/src/services/assignments.ts b/backend/src/services/assignments.ts index be121810..a21a96fa 100644 --- a/backend/src/services/assignments.ts +++ b/backend/src/services/assignments.ts @@ -1,7 +1,6 @@ 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 { mapToSubmissionDTO, SubmissionDTO } from '../interfaces/submission.js'; +import { mapToSubmissionDTO, mapToSubmissionDTOId, SubmissionDTO, SubmissionDTOId } from '../interfaces/submission.js'; export async function getAllAssignments(classid: string, full: boolean): Promise { const classRepository = getClassRepository(); @@ -21,7 +20,7 @@ export async function getAllAssignments(classid: string, full: boolean): Promise return assignments.map(mapToAssignmentDTOId); } -export async function createAssignment(classid: string, assignmentData: AssignmentDTO): Promise { +export async function createAssignment(classid: string, assignmentData: AssignmentDTO): Promise { const classRepository = getClassRepository(); const cls = await classRepository.findById(classid); @@ -36,8 +35,9 @@ export async function createAssignment(classid: string, assignmentData: Assignme const newAssignment = assignmentRepository.create(assignment); await assignmentRepository.save(newAssignment); - return newAssignment; + return mapToAssignmentDTO(newAssignment); } catch (e) { + console.error(e); return null; } } @@ -60,7 +60,11 @@ export async function getAssignment(classid: string, id: number): Promise { +export async function getAssignmentsSubmissions( + classid: string, + assignmentNumber: number, + full: boolean +): Promise { const classRepository = getClassRepository(); const cls = await classRepository.findById(classid); @@ -81,5 +85,9 @@ export async function getAssignmentsSubmissions(classid: string, assignmentNumbe const submissionRepository = getSubmissionRepository(); const submissions = (await Promise.all(groups.map((group) => submissionRepository.findAllSubmissionsForGroup(group)))).flat(); - return submissions.map(mapToSubmissionDTO); + if (full) { + return submissions.map(mapToSubmissionDTO); + } + + return submissions.map(mapToSubmissionDTOId); } diff --git a/backend/src/services/class.ts b/backend/src/services/classes.ts similarity index 94% rename from backend/src/services/class.ts rename to backend/src/services/classes.ts index 9f6e1efe..5b1e3cfc 100644 --- a/backend/src/services/class.ts +++ b/backend/src/services/classes.ts @@ -1,5 +1,4 @@ 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 { mapToStudentDTO, StudentDTO } from '../interfaces/student.js'; import { mapToTeacherInvitationDTO, mapToTeacherInvitationDTOIds, TeacherInvitationDTO } from '../interfaces/teacher-invitation.js'; @@ -21,16 +20,14 @@ export async function getAllClasses(full: boolean): Promise cls.classId!); } -export async function createClass(classData: ClassDTO): Promise { +export async function createClass(classData: ClassDTO): Promise { const teacherRepository = getTeacherRepository(); 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 studentUsernames = classData.students || []; - const students = (await Promise.all(studentUsernames.map((id) => studentRepository.findByUsername(id)))).filter((student) => student != null); - - //Const cls = mapToClass(classData, teachers, students); + const students = (await Promise.all(studentUsernames.map((id) => studentRepository.findByUsername(id)))).filter((student) => student !== null); const classRepository = getClassRepository(); @@ -42,7 +39,7 @@ export async function createClass(classData: ClassDTO): Promise { }); await classRepository.save(newClass); - return newClass; + return mapToClassDTO(newClass); } catch (e) { logger.error(e); return null; diff --git a/backend/src/services/groups.ts b/backend/src/services/groups.ts index 91091703..4a1cbbf0 100644 --- a/backend/src/services/groups.ts +++ b/backend/src/services/groups.ts @@ -1,4 +1,3 @@ -import { GroupRepository } from '../data/assignments/group-repository.js'; import { getAssignmentRepository, getClassRepository, @@ -8,7 +7,7 @@ import { } from '../data/repositories.js'; import { Group } from '../entities/assignments/group.entity.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 { const classRepository = getClassRepository(); @@ -43,7 +42,7 @@ export async function createGroup(groupData: GroupDTO, classid: string, assignme const studentRepository = getStudentRepository(); 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); @@ -103,7 +102,12 @@ export async function getAllGroups(classId: string, assignmentNumber: number, fu return groups.map(mapToGroupDTOId); } -export async function getGroupSubmissions(classId: string, assignmentNumber: number, groupNumber: number): Promise { +export async function getGroupSubmissions( + classId: string, + assignmentNumber: number, + groupNumber: number, + full: boolean +): Promise { const classRepository = getClassRepository(); const cls = await classRepository.findById(classId); @@ -128,5 +132,9 @@ export async function getGroupSubmissions(classId: string, assignmentNumber: num const submissionRepository = getSubmissionRepository(); const submissions = await submissionRepository.findAllSubmissionsForGroup(group); - return submissions.map(mapToSubmissionDTO); + if (full) { + return submissions.map(mapToSubmissionDTO); + } + + return submissions.map(mapToSubmissionDTOId); } diff --git a/backend/src/services/learning-objects.ts b/backend/src/services/learning-objects.ts index fb579471..faa77cb4 100644 --- a/backend/src/services/learning-objects.ts +++ b/backend/src/services/learning-objects.ts @@ -45,6 +45,13 @@ export async function getLearningObjectById(hruid: string, language: string): Pr return filterData(metadata, htmlUrl); } +/** + * Generic function to fetch learning paths + */ +function fetchLearningPaths(arg0: string[], language: string, arg2: string): LearningPathResponse | PromiseLike { + throw new Error('Function not implemented.'); +} + /** * 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 { return (await fetchLearningObjects(hruid, false, language)) as string[]; } -function fetchLearningPaths(arg0: string[], language: string, arg2: string): LearningPathResponse | PromiseLike { - throw new Error('Function not implemented.'); -} diff --git a/backend/src/services/questions.ts b/backend/src/services/questions.ts index ee003bcd..0e52440f 100644 --- a/backend/src/services/questions.ts +++ b/backend/src/services/questions.ts @@ -103,5 +103,5 @@ export async function deleteQuestion(questionId: QuestionId) { return null; } - return question; + return mapToQuestionDTO(question); } diff --git a/backend/src/services/students.ts b/backend/src/services/students.ts index 5099a18d..4775c8a4 100644 --- a/backend/src/services/students.ts +++ b/backend/src/services/students.ts @@ -5,19 +5,18 @@ import { AssignmentDTO } from '../interfaces/assignment.js'; import { ClassDTO, mapToClassDTO } from '../interfaces/class.js'; import { GroupDTO, mapToGroupDTO, mapToGroupDTOId } from '../interfaces/group.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 { UserService } from './users.js'; -export async function getAllStudents(): Promise { +export async function getAllStudents(full: boolean): Promise { const studentRepository = getStudentRepository(); - const users = await studentRepository.findAll(); - return users.map(mapToStudentDTO); -} + const students = await studentRepository.findAll(); -export async function getAllStudentIds(): Promise { - const users = await getAllStudents(); - return users.map((user) => user.username); + if (full) { + return students.map(mapToStudentDTO); + } + + return students.map((student) => student.username); } export async function getStudent(username: string): Promise { @@ -111,7 +110,7 @@ export async function getStudentGroups(username: string, full: boolean): Promise return groups.map(mapToGroupDTOId); } -export async function getStudentSubmissions(username: string): Promise { +export async function getStudentSubmissions(username: string, full: boolean): Promise { const studentRepository = getStudentRepository(); const student = await studentRepository.findByUsername(username); @@ -122,5 +121,9 @@ export async function getStudentSubmissions(username: string): Promise { +export async function getAllTeachers(full: boolean): Promise { const teacherRepository = getTeacherRepository(); - const users = await teacherRepository.findAll(); - return users.map(mapToTeacherDTO); -} + const teachers = await teacherRepository.findAll(); -export async function getAllTeacherIds(): Promise { - const users = await getAllTeachers(); - return users.map((user) => user.username); + if (full) { + return teachers.map(mapToTeacherDTO); + } + + return teachers.map((teacher) => teacher.username); } export async function getTeacher(username: string): Promise { @@ -64,11 +64,11 @@ export async function deleteTeacher(username: string): Promise { +export async function fetchClassesByTeacher(username: string): Promise { const teacherRepository = getTeacherRepository(); const teacher = await teacherRepository.findByUsername(username); if (!teacher) { - return []; + return null; } const classRepository = getClassRepository(); @@ -76,35 +76,49 @@ export async function fetchClassesByTeacher(username: string): Promise { - return await fetchClassesByTeacher(username); -} - -export async function getClassIdsByTeacher(username: string): Promise { +export async function getClassesByTeacher(username: string, full: boolean): Promise { const classes = await fetchClassesByTeacher(username); + + if (!classes) { + return null; + } + + if (full) { + return classes; + } + return classes.map((cls) => cls.id); } -export async function fetchStudentsByTeacher(username: string) { - const classes = await getClassIdsByTeacher(username); +export async function fetchStudentsByTeacher(username: string): Promise { + const classes = (await getClassesByTeacher(username, false)) as string[]; + + if (!classes) { + return null; + } return (await Promise.all(classes.map(async (id) => getClassStudents(id)))).flat(); } -export async function getStudentsByTeacher(username: string): Promise { - return await fetchStudentsByTeacher(username); -} - -export async function getStudentIdsByTeacher(username: string): Promise { +export async function getStudentsByTeacher(username: string, full: boolean): Promise { const students = await fetchStudentsByTeacher(username); + + if (!students) { + return null; + } + + if (full) { + return students; + } + return students.map((student) => student.username); } -export async function fetchTeacherQuestions(username: string): Promise { +export async function fetchTeacherQuestions(username: string): Promise { const teacherRepository = getTeacherRepository(); const teacher = await teacherRepository.findByUsername(username); if (!teacher) { - throw new Error(`Teacher with username '${username}' not found.`); + return null; } // Find all learning objects that this teacher manages @@ -118,12 +132,16 @@ export async function fetchTeacherQuestions(username: string): Promise { - return await fetchTeacherQuestions(username); -} - -export async function getQuestionIdsByTeacher(username: string): Promise { +export async function getQuestionsByTeacher(username: string, full: boolean): Promise { const questions = await fetchTeacherQuestions(username); + if (!questions) { + return null; + } + + if (full) { + return questions; + } + return questions.map(mapToQuestionId); } diff --git a/backend/src/services/users.ts b/backend/src/services/users.ts deleted file mode 100644 index 65ed5d4c..00000000 --- a/backend/src/services/users.ts +++ /dev/null @@ -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 { - protected repository: UserRepository; - - constructor(repository: UserRepository) { - this.repository = repository; - } - - async getAllUsers(): Promise { - const users = await this.repository.findAll(); - return users.map(mapToUserDTO); - } - - async getAllUserIds(): Promise { - const users = await this.getAllUsers(); - return users.map((user) => user.username); - } - - async getUserByUsername(username: string): Promise { - const user = await this.repository.findByUsername(username); - return user ? mapToUserDTO(user) : null; - } - - async createUser(userData: UserDTO, UserClass: new () => T): Promise { - const newUser = mapToUser(userData, new UserClass()); - await this.repository.save(newUser); - return newUser; - } - - async deleteUser(username: string): Promise { - const user = await this.getUserByUsername(username); - if (!user) { - return null; - } - await this.repository.deleteByUsername(username); - return mapToUserDTO(user); - } -} diff --git a/backend/src/util/envvars.ts b/backend/src/util/envvars.ts index 115291af..c37c79d5 100644 --- a/backend/src/util/envvars.ts +++ b/backend/src/util/envvars.ts @@ -4,20 +4,24 @@ const IDP_PREFIX = PREFIX + 'AUTH_'; const STUDENT_IDP_PREFIX = IDP_PREFIX + 'STUDENT_'; const TEACHER_IDP_PREFIX = IDP_PREFIX + 'TEACHER_'; const CORS_PREFIX = PREFIX + 'CORS_'; +const LOGGING_PREFIX = PREFIX + 'LOGGING_'; type EnvVar = { key: string; required?: boolean; defaultValue?: any }; export const EnvVars: { [key: string]: EnvVar } = { 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 }, DbPort: { key: DB_PREFIX + 'PORT', defaultValue: 5432 }, DbName: { key: DB_PREFIX + 'NAME', defaultValue: 'dwengo' }, DbUsername: { key: DB_PREFIX + 'USERNAME', required: true }, DbPassword: { key: DB_PREFIX + 'PASSWORD', required: true }, 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_' }, + IdpStudentUrl: { key: STUDENT_IDP_PREFIX + 'URL', required: true }, IdpStudentClientId: { key: STUDENT_IDP_PREFIX + 'CLIENT_ID', 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 }, IdpTeacherJwksEndpoint: { key: TEACHER_IDP_PREFIX + 'JWKS_ENDPOINT', required: true }, IdpAudience: { key: IDP_PREFIX + 'AUDIENCE', defaultValue: 'account' }, + CorsAllowedOrigins: { key: CORS_PREFIX + 'ALLOWED_ORIGINS', defaultValue: '' }, 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; /** diff --git a/backend/src/util/translation-helper.ts b/backend/src/util/translation-helper.ts index d0a83b02..55b354c6 100644 --- a/backend/src/util/translation-helper.ts +++ b/backend/src/util/translation-helper.ts @@ -8,12 +8,12 @@ const logger: Logger = getLogger(); export function loadTranslations(language: string): T { 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'); return yaml.load(yamlFile) as T; } catch (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; } } diff --git a/compose.prod.yml b/compose.production.yml similarity index 82% rename from compose.prod.yml rename to compose.production.yml index 8825796e..e6f140d4 100644 --- a/compose.prod.yml +++ b/compose.production.yml @@ -1,7 +1,8 @@ # -# This file is used to define the production environment for the project. -# It is used to deploy the project on a server. -# Should not be used for local development. +# Use this configuration to deploy the project on a server. +# +# 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: web: @@ -35,12 +36,16 @@ services: - 'traefik.http.services.api.loadbalancer.server.port=3000' db: - # Also see compose.yml + extends: + file: ./compose.yml + service: db networks: - dwengo-1 idp: - # Also see compose.yml + extends: + file: ./compose.yml + service: idp # TODO Replace with proper production command command: ['start-dev', '--http-port', '7080', '--https-port', '7443', '--import-realm'] networks: @@ -92,7 +97,15 @@ services: - dwengo-1 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: - dwengo-1 @@ -107,6 +120,7 @@ services: volumes: dwengo_grafana_data: dwengo_letsencrypt: + dwengo_loki_data: networks: dwengo-1: diff --git a/compose.override.yml b/compose.staging.yml similarity index 79% rename from compose.override.yml rename to compose.staging.yml index 5c35441e..8357e773 100644 --- a/compose.override.yml +++ b/compose.staging.yml @@ -32,8 +32,15 @@ services: - 'traefik.http.routers.api.rule=PathPrefix(`/api`)' - 'traefik.http.services.api.loadbalancer.server.port=3000' + db: + extends: + file: ./compose.yml + service: db + idp: - # Also see compose.yml + extends: + file: ./compose.yml + service: idp labels: - 'traefik.http.routers.idp.rule=PathPrefix(`/idp`)' - 'traefik.http.services.idp.loadbalancer.server.port=7080' @@ -60,6 +67,17 @@ services: volumes: - /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: image: grafana/grafana:latest ports: @@ -70,3 +88,5 @@ services: volumes: dwengo_grafana_data: + dwengo_loki_data: + dwengo_postgres_data: diff --git a/compose.yml b/compose.yml index 1276c1af..9435f6f8 100644 --- a/compose.yml +++ b/compose.yml @@ -36,17 +36,5 @@ services: KC_HEALTH_ENABLED: 'true' 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: - dwengo_loki_data: dwengo_postgres_data: diff --git a/frontend/README.md b/frontend/README.md index f798f404..d4b3b63d 100644 --- a/frontend/README.md +++ b/frontend/README.md @@ -19,7 +19,16 @@ See [Vite Configuration Reference](https://vite.dev/config/). ## Project Setup ```sh +# Install dependencies 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 diff --git a/frontend/package.json b/frontend/package.json index e8133004..e6ce1426 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -42,7 +42,7 @@ "jsdom": "^26.0.0", "npm-run-all2": "^7.0.2", "typescript": "~5.7.3", - "vite": "^6.1.0", + "vite": "^6.1.2", "vite-plugin-vue-devtools": "^7.7.2", "vitest": "^3.0.5", "vue-tsc": "^2.2.2" diff --git a/frontend/src/App.vue b/frontend/src/App.vue index 14e1178d..d37d117d 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -1,11 +1,25 @@ diff --git a/frontend/src/components/BrowseThemes.vue b/frontend/src/components/BrowseThemes.vue index eeea2c81..97ff8352 100644 --- a/frontend/src/components/BrowseThemes.vue +++ b/frontend/src/components/BrowseThemes.vue @@ -1,47 +1,56 @@ - diff --git a/frontend/src/components/ThemeCard.vue b/frontend/src/components/ThemeCard.vue index ac9b3904..7064b63c 100644 --- a/frontend/src/components/ThemeCard.vue +++ b/frontend/src/components/ThemeCard.vue @@ -1,14 +1,14 @@ diff --git a/frontend/src/controllers/base-controller.ts b/frontend/src/controllers/base-controller.ts index adc0c8c0..c6024e52 100644 --- a/frontend/src/controllers/base-controller.ts +++ b/frontend/src/controllers/base-controller.ts @@ -1,4 +1,4 @@ -import {apiConfig} from "@/config.ts"; +import { apiConfig } from "@/config.ts"; export class BaseController { protected baseUrl: string; diff --git a/frontend/src/controllers/controllers.ts b/frontend/src/controllers/controllers.ts index b3bbeb61..ad526276 100644 --- a/frontend/src/controllers/controllers.ts +++ b/frontend/src/controllers/controllers.ts @@ -1,4 +1,4 @@ -import {ThemeController} from "@/controllers/themes.ts"; +import { ThemeController } from "@/controllers/themes.ts"; export function controllerGetter(Factory: new () => T): () => T { let instance: T | undefined; diff --git a/frontend/src/controllers/themes.ts b/frontend/src/controllers/themes.ts index 447c9248..bc76985e 100644 --- a/frontend/src/controllers/themes.ts +++ b/frontend/src/controllers/themes.ts @@ -1,4 +1,4 @@ -import {BaseController} from "@/controllers/base-controller.ts"; +import { BaseController } from "@/controllers/base-controller.ts"; export class ThemeController extends BaseController { constructor() { diff --git a/frontend/src/queries/themes.ts b/frontend/src/queries/themes.ts index 9892c141..4568b7b4 100644 --- a/frontend/src/queries/themes.ts +++ b/frontend/src/queries/themes.ts @@ -1,25 +1,22 @@ -import { useQuery } from '@tanstack/vue-query'; -import { getThemeController } from '@/controllers/controllers'; -import {type MaybeRefOrGetter, toValue} from "vue"; +import { useQuery } from "@tanstack/vue-query"; +import { getThemeController } from "@/controllers/controllers"; +import { type MaybeRefOrGetter, toValue } from "vue"; const themeController = getThemeController(); -export const useThemeQuery = (language: MaybeRefOrGetter) => { - return useQuery({ - queryKey: ['themes', language], +export const useThemeQuery = (language: MaybeRefOrGetter) => + useQuery({ + queryKey: ["themes", language], queryFn: () => { const lang = toValue(language); return themeController.getAll(lang); }, - enabled: () => !!toValue(language), + enabled: () => Boolean(toValue(language)), }); -}; -export const useThemeHruidsQuery = (themeKey: string | null) => { - return useQuery({ - queryKey: ['theme-hruids', themeKey], +export const useThemeHruidsQuery = (themeKey: string | null) => + useQuery({ + queryKey: ["theme-hruids", themeKey], queryFn: () => themeController.getHruidsByKey(themeKey!), - enabled: !!themeKey, + enabled: Boolean(themeKey), }); -}; - diff --git a/frontend/src/router/index.ts b/frontend/src/router/index.ts index a4a64f35..2cdedca6 100644 --- a/frontend/src/router/index.ts +++ b/frontend/src/router/index.ts @@ -1,5 +1,4 @@ import { createRouter, createWebHistory } from "vue-router"; -import MenuBar from "@/components/MenuBar.vue"; import SingleAssignment from "@/views/assignments/SingleAssignment.vue"; import SingleClass from "@/views/classes/SingleClass.vue"; import SingleDiscussion from "@/views/discussions/SingleDiscussion.vue"; @@ -41,7 +40,6 @@ const router = createRouter({ { path: "/user", - component: MenuBar, meta: { requiresAuth: true }, children: [ { diff --git a/frontend/src/services/auth/auth-config-loader.ts b/frontend/src/services/auth/auth-config-loader.ts index ef5b63c3..b9ef983c 100644 --- a/frontend/src/services/auth/auth-config-loader.ts +++ b/frontend/src/services/auth/auth-config-loader.ts @@ -1,11 +1,14 @@ import apiClient from "@/services/api-client/api-client.ts"; import type { FrontendAuthConfig } from "@/services/auth/auth.d.ts"; +export const AUTH_CONFIG_ENDPOINT = "auth/config"; + /** * Fetch the authentication configuration from the backend. */ export async function loadAuthConfig() { - const authConfig = (await apiClient.get("auth/config")).data; + const authConfigResponse = await apiClient.get(AUTH_CONFIG_ENDPOINT); + const authConfig = authConfigResponse.data; return { student: { authority: authConfig.student.authority, diff --git a/frontend/src/services/auth/auth-service.ts b/frontend/src/services/auth/auth-service.ts index 2963d8f4..cf0bf11f 100644 --- a/frontend/src/services/auth/auth-service.ts +++ b/frontend/src/services/auth/auth-service.ts @@ -5,7 +5,7 @@ import { computed, reactive } from "vue"; import type { AuthState, Role, UserManagersForRoles } from "@/services/auth/auth.d.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 { loginRoute } from "@/config.ts"; import apiClient from "@/services/api-client/api-client.ts"; @@ -108,7 +108,7 @@ async function logout(): Promise { apiClient.interceptors.request.use( async (reqConfig) => { const token = authState?.user?.access_token; - if (token) { + if (token && reqConfig.url !== AUTH_CONFIG_ENDPOINT) { reqConfig.headers.Authorization = `Bearer ${token}`; } return reqConfig; diff --git a/frontend/src/utils/constants.ts b/frontend/src/utils/constants.ts index e046d9c3..b56437d7 100644 --- a/frontend/src/utils/constants.ts +++ b/frontend/src/utils/constants.ts @@ -1,37 +1,64 @@ export const THEMES_KEYS = [ - "kiks", "art", "socialrobot", "agriculture", "wegostem", - "computational_thinking", "math_with_python", "python_programming", - "stem", "care", "chatbot", "physical_computing", "algorithms", "basics_ai" + "kiks", + "art", + "socialrobot", + "agriculture", + "wegostem", + "computational_thinking", + "math_with_python", + "python_programming", + "stem", + "care", + "chatbot", + "physical_computing", + "algorithms", + "basics_ai", ]; export const THEMESITEMS: Record = { - "all": THEMES_KEYS, - "culture": ["art", "wegostem", "chatbot"], + all: THEMES_KEYS, + culture: ["art", "wegostem", "chatbot"], "electricity-and-mechanics": ["socialrobot", "wegostem", "stem", "physical_computing"], "nature-and-climate": ["kiks", "agriculture"], - "agriculture": ["agriculture"], - "society": ["kiks", "socialrobot", "care", "chatbot"], - "math": ["kiks", "math_with_python", "python_programming", "stem", "care", "basics_ai"], - "technology": ["socialrobot", "wegostem", "computational_thinking", "stem", "physical_computing", "basics_ai"], - "algorithms": ["math_with_python", "python_programming", "stem", "algorithms", "basics_ai"], + agriculture: ["agriculture"], + society: ["kiks", "socialrobot", "care", "chatbot"], + math: ["kiks", "math_with_python", "python_programming", "stem", "care", "basics_ai"], + technology: ["socialrobot", "wegostem", "computational_thinking", "stem", "physical_computing", "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 AGEITEMS = ["all", "primary-school", "lower-secondary", "upper-secondary", "high-school", "older"]; export const AGE_TO_THEMES: Record = { - "all": THEMES_KEYS, + all: THEMES_KEYS, "primary-school": ["wegostem", "computational_thinking", "physical_computing"], "lower-secondary": ["socialrobot", "art", "wegostem", "computational_thinking", "physical_computing"], - "upper-secondary": ["kiks", "art", "socialrobot", "agriculture", - "computational_thinking", "math_with_python", "python_programming", - "stem", "care", "chatbot", "algorithms", "basics_ai"], - "high-school": [ - "kiks", "art", "agriculture", "computational_thinking", "math_with_python", "python_programming", - "stem", "care", "chatbot", "algorithms", "basics_ai" + "upper-secondary": [ + "kiks", + "art", + "socialrobot", + "agriculture", + "computational_thinking", + "math_with_python", + "python_programming", + "stem", + "care", + "chatbot", + "algorithms", + "basics_ai", ], - "older": [ - "kiks", "computational_thinking", "algorithms", "basics_ai" - ] + "high-school": [ + "kiks", + "art", + "agriculture", + "computational_thinking", + "math_with_python", + "python_programming", + "stem", + "care", + "chatbot", + "algorithms", + "basics_ai", + ], + older: ["kiks", "computational_thinking", "algorithms", "basics_ai"], }; diff --git a/frontend/src/views/CallbackPage.vue b/frontend/src/views/CallbackPage.vue index 306dfe10..7f4d4958 100644 --- a/frontend/src/views/CallbackPage.vue +++ b/frontend/src/views/CallbackPage.vue @@ -8,7 +8,7 @@ onMounted(async () => { try { await auth.handleLoginCallback(); - await router.replace("/"); // Redirect to home (or dashboard) + await router.replace("/user"); // Redirect to theme page } catch (error) { console.error("OIDC callback error:", error); } diff --git a/frontend/src/views/HomePage.vue b/frontend/src/views/HomePage.vue index 0fdc5caf..62da76b6 100644 --- a/frontend/src/views/HomePage.vue +++ b/frontend/src/views/HomePage.vue @@ -27,9 +27,10 @@
Dwengo logo - {{ t("homeTitle") }} +

{{ t("homeTitle") }}

{{ t("homeIntroduction1") }}

@@ -57,7 +58,7 @@ width="125" src="/assets/home/innovative.png" > - {{ t("innovative") }} +

{{ t("innovative") }}

- {{ t("researchBased") }} +

{{ t("researchBased") }}

- {{ t("sociallyRelevant") }} +

{{ t("sociallyRelevant") }}

- {{ t("inclusive") }} +

{{ t("inclusive") }}

@@ -160,7 +161,7 @@ margin-bottom: 10px; } - h { + h2 { font-size: large; font-weight: bold; align-self: center; diff --git a/frontend/src/views/SingleTheme.vue b/frontend/src/views/SingleTheme.vue index 73336fa3..1a35a59f 100644 --- a/frontend/src/views/SingleTheme.vue +++ b/frontend/src/views/SingleTheme.vue @@ -1,11 +1,7 @@ - + - + diff --git a/frontend/src/views/homepage/UserHomePage.vue b/frontend/src/views/homepage/UserHomePage.vue index b6cdea17..c4f933a5 100644 --- a/frontend/src/views/homepage/UserHomePage.vue +++ b/frontend/src/views/homepage/UserHomePage.vue @@ -1,13 +1,13 @@ diff --git a/package-lock.json b/package-lock.json index 204b1cf1..c54f2f5f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -118,7 +118,7 @@ "jsdom": "^26.0.0", "npm-run-all2": "^7.0.2", "typescript": "~5.7.3", - "vite": "^6.1.0", + "vite": "^6.1.2", "vite-plugin-vue-devtools": "^7.7.2", "vitest": "^3.0.5", "vue-tsc": "^2.2.2" @@ -9821,7 +9821,9 @@ } }, "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, "license": "MIT", "dependencies": {