Merge remote-tracking branch 'origin' into chore/github-actions

This commit is contained in:
Timo De Meyst 2025-04-04 12:18:11 +02:00
commit 7058263085
280 changed files with 8530 additions and 3085 deletions

View file

@ -13,7 +13,10 @@ jobs:
- -
name: Checkout name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
-
name: Copy environment variables to correct file
run: cp /home/dev/.backend.env backend/.env
- -
name: Start docker name: Start docker
run: docker compose -f compose.yml -f compose.prod.yml up --build -d run: docker compose -f compose.production.yml up --build -d

View file

@ -43,6 +43,6 @@ jobs:
with: with:
auto_fix: true auto_fix: true
eslint: true eslint: true
eslint_args: '--config eslint.config.ts' eslint_args: "--config eslint.config.ts --ignore-pattern '**/prettier.config.js'"
prettier: true prettier: true
commit_message: 'style: fix linting issues met ${linter}' commit_message: 'style: fix linting issues met ${linter}'

View file

@ -1,32 +0,0 @@
# This workflow will do a clean installation of node dependencies, cache/restore them, build the source code and run tests across different versions of node
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-nodejs
name: Testing
on:
push:
branches: [ "dev" ]
pull_request:
branches: [ "dev" ]
types: ["synchronize", "ready_for_review", "opened", "reopened"]
jobs:
test:
name: Run unit tests
if: '! github.event.pull_request.draft'
runs-on: [self-hosted, Linux, X64]
strategy:
matrix:
node-version: [22.x]
# See supported Node.js release schedule at https://nodejs.org/en/about/releases/
steps:
- uses: actions/checkout@v4
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
cache: 'npm'
- run: npm ci
- run: npm run test:unit

2
.gitignore vendored
View file

@ -737,4 +737,4 @@ flycheck_*.el
# network security # network security
/network-security.data /network-security.data
docs/.venv

View file

@ -21,31 +21,28 @@ 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
git clone https://github.com/SELab-2/Dwengo-1.git git clone https://github.com/SELab-2/Dwengo-1.git
cd Dwengo-1/backend docker compose -f compose.staging.yml up --build
cp .env.example .env # Gebruikt backend/.env.staging
# Pas .env aan
nano .env
cd ..
docker compose up
# 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

View file

@ -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

View file

@ -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

View file

@ -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

21
backend/.env.staging Normal file
View file

@ -0,0 +1,21 @@
PORT=3000
DWENGO_DB_HOST=db
DWENGO_DB_PORT=5432
DWENGO_DB_USERNAME=postgres
DWENGO_DB_PASSWORD=postgres
DWENGO_DB_UPDATE=false
DWENGO_AUTH_STUDENT_URL=http://localhost/idp/realms/student
DWENGO_AUTH_STUDENT_CLIENT_ID=dwengo
DWENGO_AUTH_STUDENT_JWKS_ENDPOINT=http://idp:7080/idp/realms/student/protocol/openid-connect/certs
DWENGO_AUTH_TEACHER_URL=http://localhost/idp/realms/teacher
DWENGO_AUTH_TEACHER_CLIENT_ID=dwengo
DWENGO_AUTH_TEACHER_JWKS_ENDPOINT=http://idp:7080/idp/realms/teacher/protocol/openid-connect/certs
# 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/,127.0.0.1:80,http://127.0.0.1,http://localhost:80,http://127.0.0.1:80,localhost
DWENGO_CORS_ALLOWED_ORIGINS=http://localhost/*,http://idp:7080,https://idp:7080
# Logging and monitoring
LOKI_HOST=http://logging:3102

13
backend/.env.test Normal file
View file

@ -0,0 +1,13 @@
#
# 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

View file

@ -1,3 +0,0 @@
PORT=3000
DWENGO_DB_UPDATE=true
DWENGO_DB_NAME=":memory:"

View file

@ -1,37 +1,51 @@
FROM node:22 AS build-stage FROM node:22 AS build-stage
WORKDIR /app WORKDIR /app/dwengo
# Install dependencies # Install dependencies
COPY package*.json ./ COPY package*.json ./
COPY backend/package.json ./backend/ COPY backend/package.json ./backend/
# Backend depends on common
COPY common/package.json ./common/
RUN npm install --silent RUN npm install --silent
# Build the backend # Build the backend
# Root tsconfig.json # Root tsconfig.json
COPY tsconfig.json ./ COPY tsconfig.json tsconfig.build.json ./
WORKDIR /app/backend COPY backend ./backend
COPY common ./common
COPY backend ./ COPY docs ./docs
COPY docs /app/docs
RUN npm run build RUN npm run build
FROM node:22 AS production-stage FROM node:22 AS production-stage
WORKDIR /app WORKDIR /app/dwengo
COPY package-lock.json backend/package.json ./ # Copy static files
COPY ./backend/i18n ./i18n
# Copy built files
COPY --from=build-stage /app/dwengo/common/dist ./common/dist
COPY --from=build-stage /app/dwengo/backend/dist ./backend/dist
COPY package*.json ./
COPY backend/package.json ./backend/
# Backend depends on common
COPY common/package.json ./common/
RUN npm install --silent --only=production RUN npm install --silent --only=production
COPY ./docs /docs COPY ./docs ./docs
COPY --from=build-stage /app/backend/dist ./dist/ COPY ./backend/i18n ./backend/i18n
COPY ./backend/.env ./backend/.env
EXPOSE 3000 EXPOSE 3000
CMD ["node", "--env-file=.env", "dist/app.js"] CMD ["node", "--env-file=/app/dwengo/backend/.env", "/app/dwengo/backend/dist/app.js"]

View file

@ -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,20 @@ Voer volgend commando uit om de unit tests uit te voeren:
npm run test:unit npm run test:unit
``` ```
### Productie
```shell
# Omgevingsvariabelen
cp .env.example .env
# Configureer de .env file met de juiste waarden!
nano .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.

View file

@ -1,7 +0,0 @@
// Can be placed in dotenv but found it redundant
// Import dotenv from "dotenv";
// Load .env file
// Dotenv.config();
export const DWENGO_API_BASE = 'https://dwengo.org/backend/api';
export const FALLBACK_LANG = 'nl';
export const FALLBACK_SEQ_NUM = 1;

View file

@ -8,14 +8,4 @@ export default [
globals: globals.node, globals: globals.node,
}, },
}, },
{
files: ['tests/**/*.ts'],
languageOptions: {
globals: globals.node,
},
rules: {
'no-console': 'off',
},
},
]; ];

View file

@ -28,9 +28,9 @@ curricula_page:
contact: '' contact: ''
teaser: https://www.youtube.com/embed/2B6gZ9HdQ1Y teaser: https://www.youtube.com/embed/2B6gZ9HdQ1Y
basics_ai: basics_ai:
title: Basisprincipes van AI title: Grundlagen der KI
sub_title: Basisprincipes van AI sub_title: Grundlagen der KI
description: 'Onder dit lesthema bundelen we verschillende activiteiten waarin de basisprincipes van artificiële intelligentie (AI) aan bod komen. Leerlingen leren wat AI is, hoe het werkt en hoe het kan worden toegepast in verschillende domeinen.' description: 'Dieses Thema bündelt verschiedene Aktivitäten, in denen die grundlegenden Prinzipien der künstlichen Intelligenz (KI) behandelt werden. Die Schüler lernen, was KI ist, wie sie funktioniert und wie sie in verschiedenen Bereichen angewendet werden kann.'
contact: '' contact: ''
kiks: kiks:
title: KI und Klima title: KI und Klima

View file

@ -28,10 +28,11 @@ curricula_page:
contact: '' contact: ''
teaser: https://www.youtube.com/embed/2B6gZ9HdQ1Y teaser: https://www.youtube.com/embed/2B6gZ9HdQ1Y
basics_ai: basics_ai:
title: Basisprincipes van AI title: Basics of AI
sub_title: Basisprincipes van AI sub_title: Basics of AI
description: 'Onder dit lesthema bundelen we verschillende activiteiten waarin de basisprincipes van artificiële intelligentie (AI) aan bod komen. Leerlingen leren wat AI is, hoe het werkt en hoe het kan worden toegepast in verschillende domeinen.' description: 'This theme brings together various activities covering the basic principles of Artificial Intelligence (AI). Students learn what AI is, how it works, and how it can be applied in different domains.'
contact: '' contact: ''
kiks: kiks:
title: AI and Climate title: AI and Climate
sub_title: KIKS sub_title: KIKS

View file

@ -28,9 +28,9 @@ curricula_page:
contact: '' contact: ''
teaser: https://www.youtube.com/embed/2B6gZ9HdQ1Y teaser: https://www.youtube.com/embed/2B6gZ9HdQ1Y
basics_ai: basics_ai:
title: Basisprincipes van AI title: Principes de base de lIA
sub_title: Basisprincipes van AI sub_title: Principes de base de lIA
description: 'Onder dit lesthema bundelen we verschillende activiteiten waarin de basisprincipes van artificiële intelligentie (AI) aan bod komen. Leerlingen leren wat AI is, hoe het werkt en hoe het kan worden toegepast in verschillende domeinen.' description: 'Ce thème rassemble différentes activités portant sur les principes fondamentaux de lintelligence artificielle (IA). Les élèves apprennent ce quest lIA, comment elle fonctionne et comment elle peut être appliquée dans divers domaines.'
contact: '' contact: ''
kiks: kiks:
title: 'IA et changement climatique' title: 'IA et changement climatique'

View file

@ -1,17 +1,19 @@
{ {
"name": "dwengo-1-backend", "name": "@dwengo-1/backend",
"version": "0.1.1", "version": "0.1.1",
"description": "Backend for Dwengo-1", "description": "Backend for Dwengo-1",
"private": true, "private": true,
"type": "module", "type": "module",
"main": "dist/app.js",
"scripts": { "scripts": {
"build": "cross-env NODE_ENV=production tsc --project tsconfig.json", "build": "cross-env NODE_ENV=production tsc --build",
"dev": "cross-env NODE_ENV=development tsx watch --env-file=.env.development.local src/app.ts", "dev": "cross-env NODE_ENV=development tsx watch --env-file=.env.development.local src/app.ts",
"start": "cross-env NODE_ENV=production node --env-file=.env dist/app.js", "start": "cross-env NODE_ENV=production node --env-file=.env dist/app.js",
"format": "prettier --write src/", "format": "prettier --write src/",
"format-check": "prettier --check src/", "format-check": "prettier --check src/",
"lint": "eslint . --fix", "lint": "eslint . --fix",
"test:unit": "vitest" "pretest:unit": "npm run build",
"test:unit": "vitest --run"
}, },
"dependencies": { "dependencies": {
"@mikro-orm/core": "6.4.9", "@mikro-orm/core": "6.4.9",
@ -24,6 +26,7 @@
"cross": "^1.0.0", "cross": "^1.0.0",
"cross-env": "^7.0.3", "cross-env": "^7.0.3",
"dotenv": "^16.4.7", "dotenv": "^16.4.7",
"dwengo-1-common": "^0.1.1",
"express": "^5.0.1", "express": "^5.0.1",
"express-jwt": "^8.5.1", "express-jwt": "^8.5.1",
"gift-pegjs": "^1.0.2", "gift-pegjs": "^1.0.2",

View file

@ -5,15 +5,16 @@ import cors from './middleware/cors.js';
import { getLogger, Logger } from './logging/initalize.js'; import { getLogger, Logger } from './logging/initalize.js';
import { responseTimeLogger } from './logging/responseTimeLogger.js'; import { responseTimeLogger } from './logging/responseTimeLogger.js';
import responseTime from 'response-time'; import responseTime from 'response-time';
import { EnvVars, getNumericEnvVar } from './util/envvars.js'; import { envVars, getNumericEnvVar } from './util/envVars.js';
import apiRouter from './routes/router.js'; 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();
const app: Express = express(); const app: Express = express();
const port: string | number = getNumericEnvVar(EnvVars.Port); const port: string | number = getNumericEnvVar(envVars.Port);
app.use(express.json()); app.use(express.json());
app.use(cors); app.use(cors);
@ -26,7 +27,9 @@ app.use('/api', apiRouter);
// Swagger // Swagger
app.use('/api-docs', swaggerUi.serve, swaggerMiddleware); app.use('/api-docs', swaggerUi.serve, swaggerMiddleware);
async function startServer() { app.use(errorHandler);
async function startServer(): Promise<void> {
await initORM(); await initORM();
app.listen(port, () => { app.listen(port, () => {

View file

@ -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;

View file

@ -1,8 +1,8 @@
import { Request, Response } from 'express'; import { Request, Response } from 'express';
import { createAssignment, getAllAssignments, getAssignment, getAssignmentsSubmissions } from '../services/assignments.js'; import { createAssignment, getAllAssignments, getAssignment, getAssignmentsSubmissions } from '../services/assignments.js';
import { AssignmentDTO } from '../interfaces/assignment.js'; import { AssignmentDTO } from '@dwengo-1/common/interfaces/assignment';
// Typescript is annoy with with parameter forwarding from class.ts // Typescript is annoying with parameter forwarding from class.ts
interface AssignmentParams { interface AssignmentParams {
classid: string; classid: string;
id: string; id: string;
@ -37,11 +37,11 @@ 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> {
const id = +req.params.id; const id = Number(req.params.id);
const classid = req.params.classid; const classid = req.params.classid;
if (isNaN(id)) { if (isNaN(id)) {
@ -61,14 +61,15 @@ 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 = Number(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,

View file

@ -1,16 +1,16 @@
import { EnvVars, getEnvVar } from '../util/envvars.js'; import { envVars, getEnvVar } from '../util/envVars.js';
type FrontendIdpConfig = { interface FrontendIdpConfig {
authority: string; authority: string;
clientId: string; clientId: string;
scope: string; scope: string;
responseType: string; responseType: string;
}; }
type FrontendAuthConfig = { interface FrontendAuthConfig {
student: FrontendIdpConfig; student: FrontendIdpConfig;
teacher: FrontendIdpConfig; teacher: FrontendIdpConfig;
}; }
const SCOPE = 'openid profile email'; const SCOPE = 'openid profile email';
const RESPONSE_TYPE = 'code'; const RESPONSE_TYPE = 'code';
@ -18,14 +18,14 @@ const RESPONSE_TYPE = 'code';
export function getFrontendAuthConfig(): FrontendAuthConfig { export function getFrontendAuthConfig(): FrontendAuthConfig {
return { return {
student: { student: {
authority: getEnvVar(EnvVars.IdpStudentUrl), authority: getEnvVar(envVars.IdpStudentUrl),
clientId: getEnvVar(EnvVars.IdpStudentClientId), clientId: getEnvVar(envVars.IdpStudentClientId),
scope: SCOPE, scope: SCOPE,
responseType: RESPONSE_TYPE, responseType: RESPONSE_TYPE,
}, },
teacher: { teacher: {
authority: getEnvVar(EnvVars.IdpTeacherUrl), authority: getEnvVar(envVars.IdpTeacherUrl),
clientId: getEnvVar(EnvVars.IdpTeacherClientId), clientId: getEnvVar(envVars.IdpTeacherClientId),
scope: SCOPE, scope: SCOPE,
responseType: RESPONSE_TYPE, responseType: RESPONSE_TYPE,
}, },

View file

@ -1,6 +1,6 @@
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 '@dwengo-1/common/interfaces/class';
export async function getAllClassesHandler(req: Request, res: Response): Promise<void> { export async function getAllClassesHandler(req: Request, res: Response): Promise<void> {
const full = req.query.full === 'true'; const full = req.query.full === 'true';
@ -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);

View file

@ -0,0 +1,18 @@
import { BadRequestException } from '../exceptions/bad-request-exception.js';
/**
* Checks for the presence of required fields and throws a BadRequestException
* if any are missing.
*
* @param fields - An object with key-value pairs to validate.
*/
export function requireFields(fields: Record<string, unknown>): void {
const missing = Object.entries(fields)
.filter(([_, value]) => value === undefined || value === null || value === '')
.map(([key]) => key);
if (missing.length > 0) {
const message = `Missing required field${missing.length > 1 ? 's' : ''}: ${missing.join(', ')}`;
throw new BadRequestException(message);
}
}

View file

@ -1,6 +1,6 @@
import { Request, Response } from 'express'; import { Request, Response } from 'express';
import { createGroup, getAllGroups, getGroup, getGroupSubmissions } from '../services/groups.js'; import { createGroup, getAllGroups, getGroup, getGroupSubmissions } from '../services/groups.js';
import { GroupDTO } from '../interfaces/group.js'; import { GroupDTO } from '@dwengo-1/common/interfaces/group';
// Typescript is annoywith with parameter forwarding from class.ts // Typescript is annoywith with parameter forwarding from class.ts
interface GroupParams { interface GroupParams {
@ -12,14 +12,14 @@ interface GroupParams {
export async function getGroupHandler(req: Request<GroupParams>, res: Response): Promise<void> { export async function getGroupHandler(req: Request<GroupParams>, 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 = Number(req.params.assignmentid);
if (isNaN(assignmentId)) { if (isNaN(assignmentId)) {
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 groupId = +req.params.groupid!; // Can't be undefined const groupId = Number(req.params.groupid!); // Can't be undefined
if (isNaN(groupId)) { if (isNaN(groupId)) {
res.status(400).json({ error: 'Group id must be a number' }); res.status(400).json({ error: 'Group id must be a number' });
@ -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);
} }
@ -35,7 +40,7 @@ export async function getAllGroupsHandler(req: Request, res: Response): Promise<
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 = Number(req.params.assignmentid);
if (isNaN(assignmentId)) { if (isNaN(assignmentId)) {
res.status(400).json({ error: 'Assignment id must be a number' }); res.status(400).json({ error: 'Assignment id must be a number' });
@ -51,7 +56,7 @@ export async function getAllGroupsHandler(req: Request, res: Response): Promise<
export async function createGroupHandler(req: Request, res: Response): Promise<void> { export async function createGroupHandler(req: Request, res: Response): Promise<void> {
const classid = req.params.classid; const classid = req.params.classid;
const assignmentId = +req.params.assignmentid; const assignmentId = Number(req.params.assignmentid);
if (isNaN(assignmentId)) { if (isNaN(assignmentId)) {
res.status(400).json({ error: 'Assignment id must be a number' }); res.status(400).json({ error: 'Assignment id must be a number' });
@ -66,28 +71,28 @@ 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 = Number(req.params.assignmentid);
if (isNaN(assignmentId)) { if (isNaN(assignmentId)) {
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 groupId = +req.params.groupid!; // Can't be undefined const groupId = Number(req.params.groupid); // Can't be undefined
if (isNaN(groupId)) { if (isNaN(groupId)) {
res.status(400).json({ error: 'Group id must be a number' }); res.status(400).json({ error: 'Group id must be a number' });
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,

View file

@ -1,20 +1,20 @@
import { Request, Response } from 'express'; import { Request, Response } from 'express';
import { FALLBACK_LANG } from '../config.js'; import { FALLBACK_LANG } from '../config.js';
import { FilteredLearningObject, LearningObjectIdentifier, LearningPathIdentifier } from '../interfaces/learning-content.js';
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 { Language } from '@dwengo-1/common/util/language';
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 { BadRequestException } from '../exceptions/bad-request-exception.js';
import { NotFoundException } from '../exceptions/not-found-exception.js';
import { envVars, getEnvVar } from '../util/envVars.js';
import { FilteredLearningObject, LearningObjectIdentifier, LearningPathIdentifier } from '@dwengo-1/common/interfaces/learning-content';
function getLearningObjectIdentifierFromRequest(req: Request): LearningObjectIdentifier { function getLearningObjectIdentifierFromRequest(req: Request): LearningObjectIdentifier {
if (!req.params.hruid) { if (!req.params.hruid) {
throw new BadRequestException('HRUID is required.'); throw new BadRequestException('HRUID is required.');
} }
return { return {
hruid: req.params.hruid as string, hruid: req.params.hruid,
language: (req.query.language || getEnvVar(EnvVars.FallbackLanguage)) as Language, language: (req.query.language || getEnvVar(envVars.FallbackLanguage)) as Language,
version: parseInt(req.query.version as string), version: parseInt(req.query.version as string),
}; };
} }
@ -24,7 +24,7 @@ function getLearningPathIdentifierFromRequest(req: Request): LearningPathIdentif
throw new BadRequestException('HRUID is required.'); throw new BadRequestException('HRUID is required.');
} }
return { return {
hruid: req.params.hruid as string, hruid: req.params.hruid,
language: (req.query.language as Language) || FALLBACK_LANG, language: (req.query.language as Language) || FALLBACK_LANG,
}; };
} }
@ -40,13 +40,18 @@ 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> {
const learningObjectId = getLearningObjectIdentifierFromRequest(req); const learningObjectId = getLearningObjectIdentifierFromRequest(req);
const learningObject = await learningObjectService.getLearningObjectById(learningObjectId); const learningObject = await learningObjectService.getLearningObjectById(learningObjectId);
if (!learningObject) {
throw new NotFoundException('Learning object not found');
}
res.json(learningObject); res.json(learningObject);
} }
@ -63,7 +68,7 @@ export async function getAttachment(req: Request, res: Response): Promise<void>
const attachment = await attachmentService.getAttachment(learningObjectId, name); const attachment = await attachmentService.getAttachment(learningObjectId, name);
if (!attachment) { if (!attachment) {
throw new NotFoundError(`Attachment ${name} not found`); throw new NotFoundException(`Attachment ${name} not found`);
} }
res.setHeader('Content-Type', attachment.mimeType).send(attachment.content); res.setHeader('Content-Type', attachment.mimeType).send(attachment.content);
} }

View file

@ -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 '@dwengo-1/common/util/language';
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.

View file

@ -1,9 +1,9 @@
import { Request, Response } from 'express'; import { Request, Response } from 'express';
import { createQuestion, deleteQuestion, getAllQuestions, getAnswersByQuestion, getQuestion } from '../services/questions.js'; import { createQuestion, deleteQuestion, getAllQuestions, getAnswersByQuestion, getQuestion } from '../services/questions.js';
import { QuestionDTO, QuestionId } from '../interfaces/question.js';
import { FALLBACK_LANG, FALLBACK_SEQ_NUM } from '../config.js'; import { FALLBACK_LANG, FALLBACK_SEQ_NUM } from '../config.js';
import { LearningObjectIdentifier } from '../entities/content/learning-object-identifier.js'; import { LearningObjectIdentifier } from '../entities/content/learning-object-identifier.js';
import { Language } from '../entities/content/language.js'; import { QuestionDTO, QuestionId } from '@dwengo-1/common/interfaces/question';
import { Language } from '@dwengo-1/common/util/language';
function getObjectId(req: Request, res: Response): LearningObjectIdentifier | null { function getObjectId(req: Request, res: Response): LearningObjectIdentifier | null {
const { hruid, version } = req.params; const { hruid, version } = req.params;
@ -17,7 +17,7 @@ function getObjectId(req: Request, res: Response): LearningObjectIdentifier | nu
return { return {
hruid, hruid,
language: (lang as Language) || FALLBACK_LANG, language: (lang as Language) || FALLBACK_LANG,
version: +version, version: Number(version),
}; };
} }
@ -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);
} }

View file

@ -1,112 +1,67 @@
import { Request, Response } from 'express'; import { Request, Response } from 'express';
import { import {
createClassJoinRequest,
createStudent, createStudent,
deleteClassJoinRequest,
deleteStudent, deleteStudent,
getAllStudents, getAllStudents,
getJoinRequestByStudentClass,
getJoinRequestsByStudent,
getStudent, getStudent,
getStudentAssignments, getStudentAssignments,
getStudentClasses, getStudentClasses,
getStudentGroups, getStudentGroups,
getStudentQuestions,
getStudentSubmissions, getStudentSubmissions,
} from '../services/students.js'; } from '../services/students.js';
import { ClassDTO } from '../interfaces/class.js'; import { requireFields } from './error-helper.js';
import { getAllAssignments } from '../services/assignments.js'; import { StudentDTO } from '@dwengo-1/common/interfaces/student';
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<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: StudentDTO[] | string[] = await getAllStudents(full);
const students: StudentDTO[] | string[] = full ? await getAllStudents() : await getAllStudents(); res.json({ students });
if (!students) {
res.status(404).json({ error: `Student not found.` });
return;
}
res.status(201).json(students);
} }
export async function getStudentHandler(req: Request, res: Response): Promise<void> { export async function getStudentHandler(req: Request, res: Response): Promise<void> {
const username = req.params.username; const username = req.params.username;
requireFields({ username });
if (!username) { const student = await getStudent(username);
res.status(400).json({ error: 'Missing required field: username' });
return; res.json({ student });
} }
const user = await getStudent(username); export async function createStudentHandler(req: Request, res: Response): Promise<void> {
const username = req.body.username;
const firstName = req.body.firstName;
const lastName = req.body.lastName;
requireFields({ username, firstName, lastName });
if (!user) {
res.status(404).json({
error: `User with username '${username}' not found.`,
});
return;
}
res.status(201).json(user);
}
export async function createStudentHandler(req: Request, res: Response) {
const userData = req.body as StudentDTO; const userData = req.body as StudentDTO;
if (!userData.username || !userData.firstName || !userData.lastName) { const student = await createStudent(userData);
res.status(400).json({ res.json({ student });
error: 'Missing required fields: username, firstName, lastName',
});
return;
} }
const newUser = await createStudent(userData); export async function deleteStudentHandler(req: Request, res: Response): Promise<void> {
res.status(201).json(newUser);
}
export async function deleteStudentHandler(req: Request, res: Response) {
const username = req.params.username; const username = req.params.username;
requireFields({ username });
if (!username) { const student = await deleteStudent(username);
res.status(400).json({ error: 'Missing required field: username' }); res.json({ student });
return;
}
const deletedUser = await deleteStudent(username);
if (!deletedUser) {
res.status(404).json({
error: `User with username '${username}' not found.`,
});
return;
}
res.status(200).json(deletedUser);
} }
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.username;
requireFields({ username });
const classes = await getStudentClasses(username, full); const classes = await getStudentClasses(username, full);
res.json({ res.json({ 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
@ -115,32 +70,75 @@ export async function getStudentClassesHandler(req: Request, res: Response): Pro
// Have this assignment. // Have this assignment.
export async function getStudentAssignmentsHandler(req: Request, res: Response): Promise<void> { export async function getStudentAssignmentsHandler(req: Request, res: Response): Promise<void> {
const full = req.query.full === 'true'; const full = req.query.full === 'true';
const username = req.params.id; const username = req.params.username;
requireFields({ username });
const assignments = getStudentAssignments(username, full); const assignments = getStudentAssignments(username, full);
res.json({ res.json({ assignments });
assignments: assignments,
});
} }
export async function getStudentGroupsHandler(req: Request, res: Response): Promise<void> { export async function getStudentGroupsHandler(req: Request, res: Response): Promise<void> {
const full = req.query.full === 'true'; const full = req.query.full === 'true';
const username = req.params.id; const username = req.params.username;
requireFields({ username });
const groups = await getStudentGroups(username, full); const groups = await getStudentGroups(username, full);
res.json({ res.json({ groups });
groups: groups,
});
} }
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.username;
const full = req.query.full === 'true';
requireFields({ username });
const submissions = await getStudentSubmissions(username); const submissions = await getStudentSubmissions(username, full);
res.json({ res.json({ submissions });
submissions: submissions, }
});
export async function getStudentQuestionsHandler(req: Request, res: Response): Promise<void> {
const full = req.query.full === 'true';
const username = req.params.username;
requireFields({ username });
const questions = await getStudentQuestions(username, full);
res.json({ questions });
}
export async function createStudentRequestHandler(req: Request, res: Response): Promise<void> {
const username = req.params.username;
const classId = req.body.classId;
requireFields({ username, classId });
const request = await createClassJoinRequest(username, classId);
res.json({ request });
}
export async function getStudentRequestsHandler(req: Request, res: Response): Promise<void> {
const username = req.params.username;
requireFields({ username });
const requests = await getJoinRequestsByStudent(username);
res.json({ requests });
}
export async function getStudentRequestHandler(req: Request, res: Response): Promise<void> {
const username = req.params.username;
const classId = req.params.classId;
requireFields({ username, classId });
const request = await getJoinRequestByStudentClass(username, classId);
res.json({ request });
}
export async function deleteClassJoinRequestHandler(req: Request, res: Response): Promise<void> {
const username = req.params.username;
const classId = req.params.classId;
requireFields({ username, classId });
const request = await deleteClassJoinRequest(username, classId);
res.json({ request });
} }

View file

@ -1,7 +1,7 @@
import { Request, Response } from 'express'; import { Request, Response } from 'express';
import { createSubmission, deleteSubmission, getSubmission } from '../services/submissions.js'; import { createSubmission, deleteSubmission, getSubmission } from '../services/submissions.js';
import { Language, languageMap } from '../entities/content/language.js'; import { SubmissionDTO } from '@dwengo-1/common/interfaces/submission';
import { SubmissionDTO } from '../interfaces/submission'; import { Language, languageMap } from '@dwengo-1/common/util/language';
interface SubmissionParams { interface SubmissionParams {
hruid: string; hruid: string;
@ -10,7 +10,7 @@ interface SubmissionParams {
export async function getSubmissionHandler(req: Request<SubmissionParams>, res: Response): Promise<void> { export async function getSubmissionHandler(req: Request<SubmissionParams>, res: Response): Promise<void> {
const lohruid = req.params.hruid; const lohruid = req.params.hruid;
const submissionNumber = +req.params.id; const submissionNumber = Number(req.params.id);
if (isNaN(submissionNumber)) { if (isNaN(submissionNumber)) {
res.status(400).json({ error: 'Submission number is not a number' }); res.status(400).json({ error: 'Submission number is not a number' });
@ -30,21 +30,22 @@ export async function getSubmissionHandler(req: Request<SubmissionParams>, res:
res.json(submission); res.json(submission);
} }
export async function createSubmissionHandler(req: Request, res: Response) { export async function createSubmissionHandler(req: Request, res: Response): Promise<void> {
const submissionDTO = req.body as SubmissionDTO; const submissionDTO = req.body as SubmissionDTO;
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);
}
} }
export async function deleteSubmissionHandler(req: Request, res: Response) { res.json(submission);
}
export async function deleteSubmissionHandler(req: Request, res: Response): Promise<void> {
const hruid = req.params.hruid; const hruid = req.params.hruid;
const submissionNumber = +req.params.id; const submissionNumber = Number(req.params.id);
const lang = languageMap[req.query.language as string] || Language.Dutch; const lang = languageMap[req.query.language as string] || Language.Dutch;
const version = (req.query.version || 1) as number; const version = (req.query.version || 1) as number;
@ -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);
} }
}

View file

@ -4,141 +4,97 @@ import {
deleteTeacher, deleteTeacher,
getAllTeachers, getAllTeachers,
getClassesByTeacher, getClassesByTeacher,
getClassIdsByTeacher, getJoinRequestsByClass,
getQuestionIdsByTeacher,
getQuestionsByTeacher,
getStudentIdsByTeacher,
getStudentsByTeacher, getStudentsByTeacher,
getTeacher, getTeacher,
getTeacherQuestions,
updateClassJoinRequestStatus,
} from '../services/teachers.js'; } from '../services/teachers.js';
import { ClassDTO } from '../interfaces/class.js'; import { requireFields } from './error-helper.js';
import { StudentDTO } from '../interfaces/student.js'; import { TeacherDTO } from '@dwengo-1/common/interfaces/teacher';
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<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: TeacherDTO[] | string[] = await getAllTeachers(full);
const teachers: TeacherDTO[] | string[] = full ? await getAllTeachers() : await getAllTeachers(); res.json({ teachers });
if (!teachers) {
res.status(404).json({ error: `Teacher not found.` });
return;
}
res.status(201).json(teachers);
} }
export async function getTeacherHandler(req: Request, res: Response): Promise<void> { export async function getTeacherHandler(req: Request, res: Response): Promise<void> {
const username = req.params.username; const username = req.params.username;
requireFields({ username });
if (!username) { const teacher = await getTeacher(username);
res.status(400).json({ error: 'Missing required field: username' });
return; res.json({ teacher });
} }
const user = await getTeacher(username); export async function createTeacherHandler(req: Request, res: Response): Promise<void> {
const username = req.body.username;
const firstName = req.body.firstName;
const lastName = req.body.lastName;
requireFields({ username, firstName, lastName });
if (!user) {
res.status(404).json({
error: `User with username '${username}' not found.`,
});
return;
}
res.status(201).json(user);
}
export async function createTeacherHandler(req: Request, res: Response) {
const userData = req.body as TeacherDTO; const userData = req.body as TeacherDTO;
if (!userData.username || !userData.firstName || !userData.lastName) { const teacher = await createTeacher(userData);
res.status(400).json({ res.json({ teacher });
error: 'Missing required fields: username, firstName, lastName',
});
return;
} }
const newUser = await createTeacher(userData); export async function deleteTeacherHandler(req: Request, res: Response): Promise<void> {
res.status(201).json(newUser);
}
export async function deleteTeacherHandler(req: Request, res: Response) {
const username = req.params.username; const username = req.params.username;
requireFields({ username });
if (!username) { const teacher = await deleteTeacher(username);
res.status(400).json({ error: 'Missing required field: username' }); res.json({ teacher });
return;
}
const deletedUser = await deleteTeacher(username);
if (!deletedUser) {
res.status(404).json({
error: `User with username '${username}' not found.`,
});
return;
}
res.status(200).json(deletedUser);
} }
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;
const username = req.params.username as string;
const full = req.query.full === 'true'; const full = req.query.full === 'true';
requireFields({ username });
if (!username) { const classes = await getClassesByTeacher(username, full);
res.status(400).json({ error: 'Missing required field: username' });
return;
}
const classes: ClassDTO[] | string[] = full ? await getClassesByTeacher(username) : await getClassIdsByTeacher(username); res.json({ classes });
res.status(201).json(classes);
} catch (error) {
console.error('Error fetching classes by teacher:', error);
res.status(500).json({ error: 'Internal server error' });
}
} }
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;
const username = req.params.username as string;
const full = req.query.full === 'true'; const full = req.query.full === 'true';
requireFields({ username });
if (!username) { const students = await getStudentsByTeacher(username, full);
res.status(400).json({ error: 'Missing required field: username' });
return;
}
const students: StudentDTO[] | string[] = full ? await getStudentsByTeacher(username) : await getStudentIdsByTeacher(username); res.json({ students });
res.status(201).json(students);
} catch (error) {
console.error('Error fetching students by teacher:', error);
res.status(500).json({ error: 'Internal server error' });
}
} }
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;
const username = req.params.username as string;
const full = req.query.full === 'true'; const full = req.query.full === 'true';
requireFields({ username });
if (!username) { const questions = await getTeacherQuestions(username, full);
res.status(400).json({ error: 'Missing required field: username' });
return; res.json({ questions });
} }
const questions: QuestionDTO[] | QuestionId[] = full ? await getQuestionsByTeacher(username) : await getQuestionIdsByTeacher(username); export async function getStudentJoinRequestHandler(req: Request, res: Response): Promise<void> {
const username = req.query.username as string;
const classId = req.params.classId;
requireFields({ username, classId });
res.status(201).json(questions); const joinRequests = await getJoinRequestsByClass(classId);
} catch (error) { res.json({ joinRequests });
console.error('Error fetching questions by teacher:', error);
res.status(500).json({ error: 'Internal server error' });
} }
export async function updateStudentJoinRequestHandler(req: Request, res: Response): Promise<void> {
const studentUsername = req.query.studentUsername as string;
const classId = req.params.classId;
const accepted = req.body.accepted !== 'false'; // Default = true
requireFields({ studentUsername, classId });
const request = await updateClassJoinRequestStatus(studentUsername, classId, accepted);
res.json({ request });
} }

View file

@ -3,26 +3,30 @@ import { themes } from '../data/themes.js';
import { loadTranslations } from '../util/translation-helper.js'; import { loadTranslations } from '../util/translation-helper.js';
interface Translations { interface Translations {
curricula_page: { curricula_page: Record<string, { title: string; description?: string }>;
[key: string]: { title: string; description?: string };
};
} }
export function getThemes(req: Request, res: Response) { export function getThemesHandler(req: Request, res: Response): void {
const language = (req.query.language as string)?.toLowerCase() || 'nl'; const language = ((req.query.language as string) || 'nl').toLowerCase();
const translations = loadTranslations<Translations>(language); const translations = loadTranslations<Translations>(language);
const themeList = themes.map((theme) => ({ const themeList = themes.map((theme) => ({
key: theme.title, key: theme.title,
title: translations.curricula_page[theme.title]?.title || theme.title, title: translations.curricula_page[theme.title].title || theme.title,
description: translations.curricula_page[theme.title]?.description, description: translations.curricula_page[theme.title].description,
image: `https://dwengo.org/images/curricula/logo_${theme.title}.png`, image: `https://dwengo.org/images/curricula/logo_${theme.title}.png`,
})); }));
res.json(themeList); res.json(themeList);
} }
export function getThemeByTitle(req: Request, res: Response) { export function getHruidsByThemeHandler(req: Request, res: Response): void {
const themeKey = req.params.theme; const themeKey = req.params.theme;
if (!themeKey) {
res.status(400).json({ error: 'Missing required field: theme' });
return;
}
const theme = themes.find((t) => t.title === themeKey); const theme = themes.find((t) => t.title === themeKey);
if (theme) { if (theme) {

View file

@ -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' });
}
}

View file

@ -3,13 +3,13 @@ import { Assignment } from '../../entities/assignments/assignment.entity.js';
import { Class } from '../../entities/classes/class.entity.js'; import { Class } from '../../entities/classes/class.entity.js';
export class AssignmentRepository extends DwengoEntityRepository<Assignment> { export class AssignmentRepository extends DwengoEntityRepository<Assignment> {
public findByClassAndId(within: Class, id: number): Promise<Assignment | null> { public async findByClassAndId(within: Class, id: number): Promise<Assignment | null> {
return this.findOne({ within: within, id: id }); return this.findOne({ within: within, id: id });
} }
public findAllAssignmentsInClass(within: Class): Promise<Assignment[]> { public async findAllAssignmentsInClass(within: Class): Promise<Assignment[]> {
return this.findAll({ where: { within: within } }); return this.findAll({ where: { within: within } });
} }
public deleteByClassAndId(within: Class, id: number): Promise<void> { public async deleteByClassAndId(within: Class, id: number): Promise<void> {
return this.deleteWhere({ within: within, id: id }); return this.deleteWhere({ within: within, id: id });
} }
} }

View file

@ -4,7 +4,7 @@ import { Assignment } from '../../entities/assignments/assignment.entity.js';
import { Student } from '../../entities/users/student.entity.js'; import { Student } from '../../entities/users/student.entity.js';
export class GroupRepository extends DwengoEntityRepository<Group> { export class GroupRepository extends DwengoEntityRepository<Group> {
public findByAssignmentAndGroupNumber(assignment: Assignment, groupNumber: number): Promise<Group | null> { public async findByAssignmentAndGroupNumber(assignment: Assignment, groupNumber: number): Promise<Group | null> {
return this.findOne( return this.findOne(
{ {
assignment: assignment, assignment: assignment,
@ -13,16 +13,16 @@ export class GroupRepository extends DwengoEntityRepository<Group> {
{ populate: ['members'] } { populate: ['members'] }
); );
} }
public findAllGroupsForAssignment(assignment: Assignment): Promise<Group[]> { public async findAllGroupsForAssignment(assignment: Assignment): Promise<Group[]> {
return this.findAll({ return this.findAll({
where: { assignment: assignment }, where: { assignment: assignment },
populate: ['members'], populate: ['members'],
}); });
} }
public findAllGroupsWithStudent(student: Student): Promise<Group[]> { public async findAllGroupsWithStudent(student: Student): Promise<Group[]> {
return this.find({ members: student }, { populate: ['members'] }); return this.find({ members: student }, { populate: ['members'] });
} }
public deleteByAssignmentAndGroupNumber(assignment: Assignment, groupNumber: number) { public async deleteByAssignmentAndGroupNumber(assignment: Assignment, groupNumber: number): Promise<void> {
return this.deleteWhere({ return this.deleteWhere({
assignment: assignment, assignment: assignment,
groupNumber: groupNumber, groupNumber: groupNumber,

View file

@ -5,7 +5,10 @@ import { LearningObjectIdentifier } from '../../entities/content/learning-object
import { Student } from '../../entities/users/student.entity.js'; import { Student } from '../../entities/users/student.entity.js';
export class SubmissionRepository extends DwengoEntityRepository<Submission> { export class SubmissionRepository extends DwengoEntityRepository<Submission> {
public findSubmissionByLearningObjectAndSubmissionNumber(loId: LearningObjectIdentifier, submissionNumber: number): Promise<Submission | null> { public async findSubmissionByLearningObjectAndSubmissionNumber(
loId: LearningObjectIdentifier,
submissionNumber: number
): Promise<Submission | null> {
return this.findOne({ return this.findOne({
learningObjectHruid: loId.hruid, learningObjectHruid: loId.hruid,
learningObjectLanguage: loId.language, learningObjectLanguage: loId.language,
@ -14,7 +17,7 @@ export class SubmissionRepository extends DwengoEntityRepository<Submission> {
}); });
} }
public findMostRecentSubmissionForStudent(loId: LearningObjectIdentifier, submitter: Student): Promise<Submission | null> { public async findMostRecentSubmissionForStudent(loId: LearningObjectIdentifier, submitter: Student): Promise<Submission | null> {
return this.findOne( return this.findOne(
{ {
learningObjectHruid: loId.hruid, learningObjectHruid: loId.hruid,
@ -26,7 +29,7 @@ export class SubmissionRepository extends DwengoEntityRepository<Submission> {
); );
} }
public findMostRecentSubmissionForGroup(loId: LearningObjectIdentifier, group: Group): Promise<Submission | null> { public async findMostRecentSubmissionForGroup(loId: LearningObjectIdentifier, group: Group): Promise<Submission | null> {
return this.findOne( return this.findOne(
{ {
learningObjectHruid: loId.hruid, learningObjectHruid: loId.hruid,
@ -38,15 +41,15 @@ export class SubmissionRepository extends DwengoEntityRepository<Submission> {
); );
} }
public findAllSubmissionsForGroup(group: Group): Promise<Submission[]> { public async findAllSubmissionsForGroup(group: Group): Promise<Submission[]> {
return this.find({ onBehalfOf: group }); return this.find({ onBehalfOf: group });
} }
public findAllSubmissionsForStudent(student: Student): Promise<Submission[]> { public async findAllSubmissionsForStudent(student: Student): Promise<Submission[]> {
return this.find({ submitter: student }); return this.find({ submitter: student });
} }
public deleteSubmissionByLearningObjectAndSubmissionNumber(loId: LearningObjectIdentifier, submissionNumber: number): Promise<void> { public async deleteSubmissionByLearningObjectAndSubmissionNumber(loId: LearningObjectIdentifier, submissionNumber: number): Promise<void> {
return this.deleteWhere({ return this.deleteWhere({
learningObjectHruid: loId.hruid, learningObjectHruid: loId.hruid,
learningObjectLanguage: loId.language, learningObjectLanguage: loId.language,

View file

@ -2,15 +2,19 @@ import { DwengoEntityRepository } from '../dwengo-entity-repository.js';
import { Class } from '../../entities/classes/class.entity.js'; import { Class } from '../../entities/classes/class.entity.js';
import { ClassJoinRequest } from '../../entities/classes/class-join-request.entity.js'; import { ClassJoinRequest } from '../../entities/classes/class-join-request.entity.js';
import { Student } from '../../entities/users/student.entity.js'; import { Student } from '../../entities/users/student.entity.js';
import { ClassJoinRequestStatus } from '@dwengo-1/common/util/class-join-request';
export class ClassJoinRequestRepository extends DwengoEntityRepository<ClassJoinRequest> { export class ClassJoinRequestRepository extends DwengoEntityRepository<ClassJoinRequest> {
public findAllRequestsBy(requester: Student): Promise<ClassJoinRequest[]> { public async findAllRequestsBy(requester: Student): Promise<ClassJoinRequest[]> {
return this.findAll({ where: { requester: requester } }); return this.findAll({ where: { requester: requester } });
} }
public findAllOpenRequestsTo(clazz: Class): Promise<ClassJoinRequest[]> { public async findAllOpenRequestsTo(clazz: Class): Promise<ClassJoinRequest[]> {
return this.findAll({ where: { class: clazz } }); return this.findAll({ where: { class: clazz, status: ClassJoinRequestStatus.Open } }); // TODO check if works like this
} }
public deleteBy(requester: Student, clazz: Class): Promise<void> { public async findByStudentAndClass(requester: Student, clazz: Class): Promise<ClassJoinRequest | null> {
return this.findOne({ requester, class: clazz });
}
public async deleteBy(requester: Student, clazz: Class): Promise<void> {
return this.deleteWhere({ requester: requester, class: clazz }); return this.deleteWhere({ requester: requester, class: clazz });
} }
} }

View file

@ -4,20 +4,20 @@ import { Student } from '../../entities/users/student.entity.js';
import { Teacher } from '../../entities/users/teacher.entity'; import { Teacher } from '../../entities/users/teacher.entity';
export class ClassRepository extends DwengoEntityRepository<Class> { export class ClassRepository extends DwengoEntityRepository<Class> {
public findById(id: string): Promise<Class | null> { public async findById(id: string): Promise<Class | null> {
return this.findOne({ classId: id }, { populate: ['students', 'teachers'] }); return this.findOne({ classId: id }, { populate: ['students', 'teachers'] });
} }
public deleteById(id: string): Promise<void> { public async deleteById(id: string): Promise<void> {
return this.deleteWhere({ classId: id }); return this.deleteWhere({ classId: id });
} }
public findByStudent(student: Student): Promise<Class[]> { public async findByStudent(student: Student): Promise<Class[]> {
return this.find( return this.find(
{ students: student }, { students: student },
{ populate: ['students', 'teachers'] } // Voegt student en teacher objecten toe { populate: ['students', 'teachers'] } // Voegt student en teacher objecten toe
); );
} }
public findByTeacher(teacher: Teacher): Promise<Class[]> { public async findByTeacher(teacher: Teacher): Promise<Class[]> {
return this.find({ teachers: teacher }, { populate: ['students', 'teachers'] }); return this.find({ teachers: teacher }, { populate: ['students', 'teachers'] });
} }
} }

View file

@ -4,16 +4,16 @@ import { TeacherInvitation } from '../../entities/classes/teacher-invitation.ent
import { Teacher } from '../../entities/users/teacher.entity.js'; import { Teacher } from '../../entities/users/teacher.entity.js';
export class TeacherInvitationRepository extends DwengoEntityRepository<TeacherInvitation> { export class TeacherInvitationRepository extends DwengoEntityRepository<TeacherInvitation> {
public findAllInvitationsForClass(clazz: Class): Promise<TeacherInvitation[]> { public async findAllInvitationsForClass(clazz: Class): Promise<TeacherInvitation[]> {
return this.findAll({ where: { class: clazz } }); return this.findAll({ where: { class: clazz } });
} }
public findAllInvitationsBy(sender: Teacher): Promise<TeacherInvitation[]> { public async findAllInvitationsBy(sender: Teacher): Promise<TeacherInvitation[]> {
return this.findAll({ where: { sender: sender } }); return this.findAll({ where: { sender: sender } });
} }
public findAllInvitationsFor(receiver: Teacher): Promise<TeacherInvitation[]> { public async findAllInvitationsFor(receiver: Teacher): Promise<TeacherInvitation[]> {
return this.findAll({ where: { receiver: receiver } }); return this.findAll({ where: { receiver: receiver } });
} }
public deleteBy(clazz: Class, sender: Teacher, receiver: Teacher): Promise<void> { public async deleteBy(clazz: Class, sender: Teacher, receiver: Teacher): Promise<void> {
return this.deleteWhere({ return this.deleteWhere({
sender: sender, sender: sender,
receiver: receiver, receiver: receiver,

View file

@ -1,10 +1,10 @@
import { DwengoEntityRepository } from '../dwengo-entity-repository.js'; import { DwengoEntityRepository } from '../dwengo-entity-repository.js';
import { Attachment } from '../../entities/content/attachment.entity.js'; import { Attachment } from '../../entities/content/attachment.entity.js';
import { Language } from '../../entities/content/language'; import { Language } from '@dwengo-1/common/util/language';
import { LearningObjectIdentifier } from '../../entities/content/learning-object-identifier'; import { LearningObjectIdentifier } from '../../entities/content/learning-object-identifier';
export class AttachmentRepository extends DwengoEntityRepository<Attachment> { export class AttachmentRepository extends DwengoEntityRepository<Attachment> {
public findByLearningObjectIdAndName(learningObjectId: LearningObjectIdentifier, name: string): Promise<Attachment | null> { public async findByLearningObjectIdAndName(learningObjectId: LearningObjectIdentifier, name: string): Promise<Attachment | null> {
return this.findOne({ return this.findOne({
learningObject: { learningObject: {
hruid: learningObjectId.hruid, hruid: learningObjectId.hruid,
@ -15,7 +15,11 @@ export class AttachmentRepository extends DwengoEntityRepository<Attachment> {
}); });
} }
public findByMostRecentVersionOfLearningObjectAndName(hruid: string, language: Language, attachmentName: string): Promise<Attachment | null> { public async findByMostRecentVersionOfLearningObjectAndName(
hruid: string,
language: Language,
attachmentName: string
): Promise<Attachment | null> {
return this.findOne( return this.findOne(
{ {
learningObject: { learningObject: {

View file

@ -1,11 +1,11 @@
import { DwengoEntityRepository } from '../dwengo-entity-repository.js'; import { DwengoEntityRepository } from '../dwengo-entity-repository.js';
import { LearningObject } from '../../entities/content/learning-object.entity.js'; import { LearningObject } from '../../entities/content/learning-object.entity.js';
import { LearningObjectIdentifier } from '../../entities/content/learning-object-identifier.js'; import { LearningObjectIdentifier } from '../../entities/content/learning-object-identifier.js';
import { Language } from '../../entities/content/language.js'; import { Language } from '@dwengo-1/common/util/language';
import { Teacher } from '../../entities/users/teacher.entity.js'; import { Teacher } from '../../entities/users/teacher.entity.js';
export class LearningObjectRepository extends DwengoEntityRepository<LearningObject> { export class LearningObjectRepository extends DwengoEntityRepository<LearningObject> {
public findByIdentifier(identifier: LearningObjectIdentifier): Promise<LearningObject | null> { public async findByIdentifier(identifier: LearningObjectIdentifier): Promise<LearningObject | null> {
return this.findOne( return this.findOne(
{ {
hruid: identifier.hruid, hruid: identifier.hruid,
@ -18,7 +18,7 @@ export class LearningObjectRepository extends DwengoEntityRepository<LearningObj
); );
} }
public findLatestByHruidAndLanguage(hruid: string, language: Language) { public async findLatestByHruidAndLanguage(hruid: string, language: Language): Promise<LearningObject | null> {
return this.findOne( return this.findOne(
{ {
hruid: hruid, hruid: hruid,
@ -33,7 +33,7 @@ export class LearningObjectRepository extends DwengoEntityRepository<LearningObj
); );
} }
public findAllByTeacher(teacher: Teacher): Promise<LearningObject[]> { public async findAllByTeacher(teacher: Teacher): Promise<LearningObject[]> {
return this.find( return this.find(
{ admins: teacher }, { admins: teacher },
{ populate: ['admins'] } // Make sure to load admin relations { populate: ['admins'] } // Make sure to load admin relations

View file

@ -1,9 +1,9 @@
import { DwengoEntityRepository } from '../dwengo-entity-repository.js'; import { DwengoEntityRepository } from '../dwengo-entity-repository.js';
import { LearningPath } from '../../entities/content/learning-path.entity.js'; import { LearningPath } from '../../entities/content/learning-path.entity.js';
import { Language } from '../../entities/content/language.js'; import { Language } from '@dwengo-1/common/util/language';
export class LearningPathRepository extends DwengoEntityRepository<LearningPath> { export class LearningPathRepository extends DwengoEntityRepository<LearningPath> {
public findByHruidAndLanguage(hruid: string, language: Language): Promise<LearningPath | null> { public async findByHruidAndLanguage(hruid: string, language: Language): Promise<LearningPath | null> {
return this.findOne({ hruid: hruid, language: language }, { populate: ['nodes', 'nodes.transitions'] }); return this.findOne({ hruid: hruid, language: language }, { populate: ['nodes', 'nodes.transitions'] });
} }

View file

@ -1,12 +1,14 @@
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();
} }
public async deleteWhere(query: FilterQuery<T>) { await this.getEntityManager().persistAndFlush(entity);
}
public async deleteWhere(query: FilterQuery<T>): Promise<void> {
const toDelete = await this.findOne(query); const toDelete = await this.findOne(query);
const em = this.getEntityManager(); const em = this.getEntityManager();
if (toDelete) { if (toDelete) {

View file

@ -4,7 +4,7 @@ import { Question } from '../../entities/questions/question.entity.js';
import { Teacher } from '../../entities/users/teacher.entity.js'; import { Teacher } from '../../entities/users/teacher.entity.js';
export class AnswerRepository extends DwengoEntityRepository<Answer> { export class AnswerRepository extends DwengoEntityRepository<Answer> {
public createAnswer(answer: { toQuestion: Question; author: Teacher; content: string }): Promise<Answer> { public async createAnswer(answer: { toQuestion: Question; author: Teacher; content: string }): Promise<Answer> {
const answerEntity = this.create({ const answerEntity = this.create({
toQuestion: answer.toQuestion, toQuestion: answer.toQuestion,
author: answer.author, author: answer.author,
@ -13,13 +13,13 @@ export class AnswerRepository extends DwengoEntityRepository<Answer> {
}); });
return this.insert(answerEntity); return this.insert(answerEntity);
} }
public findAllAnswersToQuestion(question: Question): Promise<Answer[]> { public async findAllAnswersToQuestion(question: Question): Promise<Answer[]> {
return this.findAll({ return this.findAll({
where: { toQuestion: question }, where: { toQuestion: question },
orderBy: { sequenceNumber: 'ASC' }, orderBy: { sequenceNumber: 'ASC' },
}); });
} }
public removeAnswerByQuestionAndSequenceNumber(question: Question, sequenceNumber: number): Promise<void> { public async removeAnswerByQuestionAndSequenceNumber(question: Question, sequenceNumber: number): Promise<void> {
return this.deleteWhere({ return this.deleteWhere({
toQuestion: question, toQuestion: question,
sequenceNumber: sequenceNumber, sequenceNumber: sequenceNumber,

View file

@ -5,7 +5,7 @@ import { Student } from '../../entities/users/student.entity.js';
import { LearningObject } from '../../entities/content/learning-object.entity.js'; import { LearningObject } from '../../entities/content/learning-object.entity.js';
export class QuestionRepository extends DwengoEntityRepository<Question> { export class QuestionRepository extends DwengoEntityRepository<Question> {
public createQuestion(question: { loId: LearningObjectIdentifier; author: Student; content: string }): Promise<Question> { public async createQuestion(question: { loId: LearningObjectIdentifier; author: Student; content: string }): Promise<Question> {
const questionEntity = this.create({ const questionEntity = this.create({
learningObjectHruid: question.loId.hruid, learningObjectHruid: question.loId.hruid,
learningObjectLanguage: question.loId.language, learningObjectLanguage: question.loId.language,
@ -21,7 +21,7 @@ export class QuestionRepository extends DwengoEntityRepository<Question> {
questionEntity.content = question.content; questionEntity.content = question.content;
return this.insert(questionEntity); return this.insert(questionEntity);
} }
public findAllQuestionsAboutLearningObject(loId: LearningObjectIdentifier): Promise<Question[]> { public async findAllQuestionsAboutLearningObject(loId: LearningObjectIdentifier): Promise<Question[]> {
return this.findAll({ return this.findAll({
where: { where: {
learningObjectHruid: loId.hruid, learningObjectHruid: loId.hruid,
@ -33,7 +33,7 @@ export class QuestionRepository extends DwengoEntityRepository<Question> {
}, },
}); });
} }
public removeQuestionByLearningObjectAndSequenceNumber(loId: LearningObjectIdentifier, sequenceNumber: number): Promise<void> { public async removeQuestionByLearningObjectAndSequenceNumber(loId: LearningObjectIdentifier, sequenceNumber: number): Promise<void> {
return this.deleteWhere({ return this.deleteWhere({
learningObjectHruid: loId.hruid, learningObjectHruid: loId.hruid,
learningObjectLanguage: loId.language, learningObjectLanguage: loId.language,
@ -54,4 +54,11 @@ export class QuestionRepository extends DwengoEntityRepository<Question> {
orderBy: { timestamp: 'ASC' }, orderBy: { timestamp: 'ASC' },
}); });
} }
public async findAllByAuthor(author: Student): Promise<Question[]> {
return this.findAll({
where: { author },
orderBy: { timestamp: 'DESC' }, // New to old
});
}
} }

View file

@ -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';
@ -36,8 +34,8 @@ let entityManager: EntityManager | undefined;
/** /**
* Execute all the database operations within the function f in a single transaction. * Execute all the database operations within the function f in a single transaction.
*/ */
export function transactional<T>(f: () => Promise<T>) { export async function transactional<T>(f: () => Promise<T>): Promise<void> {
entityManager?.transactional(f); await entityManager?.transactional(f);
} }
function repositoryGetter<T extends AnyEntity, R extends EntityRepository<T>>(entity: EntityName<T>): () => R { function repositoryGetter<T extends AnyEntity, R extends EntityRepository<T>>(entity: EntityName<T>): () => R {

View file

@ -1,7 +1,4 @@
export interface Theme { import { Theme } from '@dwengo-1/common/interfaces/theme';
title: string;
hruids: string[];
}
export const themes: Theme[] = [ export const themes: Theme[] = [
{ {

View file

@ -1,15 +1,11 @@
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';
// Export class StudentRepository extends UserRepository<Student> {}
export class StudentRepository extends DwengoEntityRepository<Student> { export class StudentRepository extends DwengoEntityRepository<Student> {
public findByUsername(username: string): Promise<Student | null> { public async findByUsername(username: string): Promise<Student | null> {
return this.findOne({ username: username }); return this.findOne({ username: username });
} }
public deleteByUsername(username: string): Promise<void> { public async deleteByUsername(username: string): Promise<void> {
return this.deleteWhere({ username: username }); return this.deleteWhere({ username: username });
} }
} }

View file

@ -1,12 +1,11 @@
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 async findByUsername(username: string): Promise<Teacher | null> {
return this.findOne({ username: username }); return this.findOne({ username: username });
} }
public deleteByUsername(username: string): Promise<void> { public async deleteByUsername(username: string): Promise<void> {
return this.deleteWhere({ username: username }); return this.deleteWhere({ username: username });
} }
} }

View file

@ -2,10 +2,10 @@ import { DwengoEntityRepository } from '../dwengo-entity-repository.js';
import { User } from '../../entities/users/user.entity.js'; import { User } from '../../entities/users/user.entity.js';
export class UserRepository<T extends User> extends DwengoEntityRepository<T> { export class UserRepository<T extends User> extends DwengoEntityRepository<T> {
public findByUsername(username: string): Promise<T | null> { public async findByUsername(username: string): Promise<T | null> {
return this.findOne({ username } as Partial<T>); return this.findOne({ username } as Partial<T>);
} }
public deleteByUsername(username: string): Promise<void> { public async deleteByUsername(username: string): Promise<void> {
return this.deleteWhere({ username } as Partial<T>); return this.deleteWhere({ username } as Partial<T>);
} }
} }

View file

@ -1,7 +1,7 @@
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 '@dwengo-1/common/util/language';
import { AssignmentRepository } from '../../data/assignments/assignment-repository.js'; import { AssignmentRepository } from '../../data/assignments/assignment-repository.js';
@Entity({ @Entity({

View file

@ -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';

View file

@ -1,8 +1,8 @@
import { Student } from '../users/student.entity.js'; import { Student } from '../users/student.entity.js';
import { Group } from './group.entity.js'; import { Group } from './group.entity.js';
import { Entity, Enum, ManyToOne, PrimaryKey, Property } from '@mikro-orm/core'; import { Entity, Enum, ManyToOne, PrimaryKey, Property } from '@mikro-orm/core';
import { Language } from '../content/language.js';
import { SubmissionRepository } from '../../data/assignments/submission-repository.js'; import { SubmissionRepository } from '../../data/assignments/submission-repository.js';
import { Language } from '@dwengo-1/common/util/language';
@Entity({ repository: () => SubmissionRepository }) @Entity({ repository: () => SubmissionRepository })
export class Submission { export class Submission {
@ -16,10 +16,10 @@ export class Submission {
learningObjectLanguage!: Language; learningObjectLanguage!: Language;
@PrimaryKey({ type: 'numeric' }) @PrimaryKey({ type: 'numeric' })
learningObjectVersion: number = 1; learningObjectVersion = 1;
@PrimaryKey({ type: 'integer', autoincrement: true }) @PrimaryKey({ type: 'integer', autoincrement: true })
submissionNumber!: number; submissionNumber?: number;
@ManyToOne({ @ManyToOne({
entity: () => Student, entity: () => Student,

View file

@ -2,6 +2,7 @@ import { Entity, Enum, ManyToOne } from '@mikro-orm/core';
import { Student } from '../users/student.entity.js'; 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';
import { ClassJoinRequestStatus } from '@dwengo-1/common/util/class-join-request';
@Entity({ @Entity({
repository: () => ClassJoinRequestRepository, repository: () => ClassJoinRequestRepository,
@ -22,9 +23,3 @@ export class ClassJoinRequest {
@Enum(() => ClassJoinRequestStatus) @Enum(() => ClassJoinRequestStatus)
status!: ClassJoinRequestStatus; status!: ClassJoinRequestStatus;
} }
export enum ClassJoinRequestStatus {
Open = 'open',
Accepted = 'accepted',
Declined = 'declined',
}

View file

@ -0,0 +1,10 @@
import { Embeddable, Property } from '@mikro-orm/core';
@Embeddable()
export class EducationalGoal {
@Property({ type: 'string' })
source!: string;
@Property({ type: 'string' })
id!: string;
}

View file

@ -1,9 +1,11 @@
import { Language } from './language.js'; import { Language } from '@dwengo-1/common/util/language';
export class LearningObjectIdentifier { export class LearningObjectIdentifier {
constructor( constructor(
public hruid: string, public hruid: string,
public language: Language, public language: Language,
public version: number public version: number
) {} ) {
// Do nothing
}
} }

View file

@ -1,28 +1,12 @@
import { Embeddable, Embedded, Entity, Enum, ManyToMany, OneToMany, PrimaryKey, Property } from '@mikro-orm/core'; import { Embedded, Entity, Enum, ManyToMany, OneToMany, PrimaryKey, Property } from '@mikro-orm/core';
import { Language } from './language.js';
import { Attachment } from './attachment.entity.js'; import { Attachment } from './attachment.entity.js';
import { Teacher } from '../users/teacher.entity.js'; import { Teacher } from '../users/teacher.entity.js';
import { DwengoContentType } from '../../services/learning-objects/processing/content-type.js'; import { DwengoContentType } from '../../services/learning-objects/processing/content-type.js';
import { v4 } from 'uuid'; import { v4 } from 'uuid';
import { LearningObjectRepository } from '../../data/content/learning-object-repository.js'; import { LearningObjectRepository } from '../../data/content/learning-object-repository.js';
import { EducationalGoal } from './educational-goal.entity.js';
@Embeddable() import { ReturnValue } from './return-value.entity.js';
export class EducationalGoal { import { Language } from '@dwengo-1/common/util/language';
@Property({ type: 'string' })
source!: string;
@Property({ type: 'string' })
id!: string;
}
@Embeddable()
export class ReturnValue {
@Property({ type: 'string' })
callbackUrl!: string;
@Property({ type: 'json' })
callbackSchema!: string;
}
@Entity({ repository: () => LearningObjectRepository }) @Entity({ repository: () => LearningObjectRepository })
export class LearningObject { export class LearningObject {
@ -36,7 +20,7 @@ export class LearningObject {
language!: Language; language!: Language;
@PrimaryKey({ type: 'number' }) @PrimaryKey({ type: 'number' })
version: number = 1; version = 1;
@Property({ type: 'uuid', unique: true }) @Property({ type: 'uuid', unique: true })
uuid = v4(); uuid = v4();
@ -62,7 +46,7 @@ export class LearningObject {
targetAges?: number[] = []; targetAges?: number[] = [];
@Property({ type: 'bool' }) @Property({ type: 'bool' })
teacherExclusive: boolean = false; teacherExclusive = false;
@Property({ type: 'array' }) @Property({ type: 'array' })
skosConcepts: string[] = []; skosConcepts: string[] = [];
@ -74,10 +58,10 @@ export class LearningObject {
educationalGoals: EducationalGoal[] = []; educationalGoals: EducationalGoal[] = [];
@Property({ type: 'string' }) @Property({ type: 'string' })
copyright: string = ''; copyright = '';
@Property({ type: 'string' }) @Property({ type: 'string' })
license: string = ''; license = '';
@Property({ type: 'smallint', nullable: true }) @Property({ type: 'smallint', nullable: true })
difficulty?: number; difficulty?: number;
@ -91,7 +75,7 @@ export class LearningObject {
returnValue!: ReturnValue; returnValue!: ReturnValue;
@Property({ type: 'bool' }) @Property({ type: 'bool' })
available: boolean = true; available = true;
@Property({ type: 'string', nullable: true }) @Property({ type: 'string', nullable: true })
contentLocation?: string; contentLocation?: string;

View file

@ -1,7 +1,7 @@
import { Entity, Enum, ManyToOne, OneToMany, PrimaryKey, Property, Rel } from '@mikro-orm/core'; import { Entity, Enum, ManyToOne, OneToMany, PrimaryKey, Property, Rel } from '@mikro-orm/core';
import { Language } from './language.js';
import { LearningPath } from './learning-path.entity.js'; import { LearningPath } from './learning-path.entity.js';
import { LearningPathTransition } from './learning-path-transition.entity.js'; import { LearningPathTransition } from './learning-path-transition.entity.js';
import { Language } from '@dwengo-1/common/util/language';
@Entity() @Entity()
export class LearningPathNode { export class LearningPathNode {

View file

@ -1,8 +1,8 @@
import { Entity, Enum, ManyToMany, OneToMany, PrimaryKey, Property } from '@mikro-orm/core'; import { Entity, Enum, ManyToMany, OneToMany, PrimaryKey, Property } from '@mikro-orm/core';
import { Language } from './language.js';
import { Teacher } from '../users/teacher.entity.js'; import { Teacher } from '../users/teacher.entity.js';
import { LearningPathRepository } from '../../data/content/learning-path-repository.js'; import { LearningPathRepository } from '../../data/content/learning-path-repository.js';
import { LearningPathNode } from './learning-path-node.entity.js'; import { LearningPathNode } from './learning-path-node.entity.js';
import { Language } from '@dwengo-1/common/util/language';
@Entity({ repository: () => LearningPathRepository }) @Entity({ repository: () => LearningPathRepository })
export class LearningPath { export class LearningPath {

View file

@ -0,0 +1,10 @@
import { Embeddable, Property } from '@mikro-orm/core';
@Embeddable()
export class ReturnValue {
@Property({ type: 'string' })
callbackUrl!: string;
@Property({ type: 'json' })
callbackSchema!: string;
}

View file

@ -1,7 +1,7 @@
import { Entity, Enum, ManyToOne, PrimaryKey, Property } from '@mikro-orm/core'; import { Entity, Enum, ManyToOne, PrimaryKey, Property } from '@mikro-orm/core';
import { Language } from '../content/language.js';
import { Student } from '../users/student.entity.js'; import { Student } from '../users/student.entity.js';
import { QuestionRepository } from '../../data/questions/question-repository.js'; import { QuestionRepository } from '../../data/questions/question-repository.js';
import { Language } from '@dwengo-1/common/util/language';
@Entity({ repository: () => QuestionRepository }) @Entity({ repository: () => QuestionRepository })
export class Question { export class Question {
@ -15,7 +15,7 @@ export class Question {
learningObjectLanguage!: Language; learningObjectLanguage!: Language;
@PrimaryKey({ type: 'number' }) @PrimaryKey({ type: 'number' })
learningObjectVersion: number = 1; learningObjectVersion = 1;
@PrimaryKey({ type: 'integer', autoincrement: true }) @PrimaryKey({ type: 'integer', autoincrement: true })
sequenceNumber?: number; sequenceNumber?: number;

View file

@ -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();
}
} }

View file

@ -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();
}
} }

View file

@ -6,8 +6,8 @@ export abstract class User {
username!: string; username!: string;
@Property() @Property()
firstName: string = ''; firstName = '';
@Property() @Property()
lastName: string = ''; lastName = '';
} }

View file

@ -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);
}
}

View 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);
}
}

View 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);
}
}

View file

@ -0,0 +1,7 @@
import { ConflictException } from './conflict-exception.js';
export class EntityAlreadyExistsException extends ConflictException {
constructor(message: string) {
super(message);
}
}

View 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);
}
}

View 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 = 'Forbidden') {
super(403, message);
}
}

View 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);
}
}

View 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 = 'Unauthorized') {
super(401, message);
}
}

View file

@ -1,14 +1,7 @@
import { mapToUserDTO, UserDTO } from './user.js'; import { mapToUserDTO } from './user.js';
import { mapToQuestionDTO, mapToQuestionId, QuestionDTO, QuestionId } from './question.js'; import { mapToQuestionDTO, mapToQuestionDTOId } from './question.js';
import { Answer } from '../entities/questions/answer.entity.js'; import { Answer } from '../entities/questions/answer.entity.js';
import { AnswerDTO, AnswerId } from '@dwengo-1/common/interfaces/answer';
export interface AnswerDTO {
author: UserDTO;
toQuestion: QuestionDTO;
sequenceNumber: number;
timestamp: string;
content: string;
}
/** /**
* Convert a Question entity to a DTO format. * Convert a Question entity to a DTO format.
@ -23,16 +16,10 @@ export function mapToAnswerDTO(answer: Answer): AnswerDTO {
}; };
} }
export interface AnswerId { export function mapToAnswerDTOId(answer: Answer): AnswerId {
author: string;
toQuestion: QuestionId;
sequenceNumber: number;
}
export function mapToAnswerId(answer: AnswerDTO): AnswerId {
return { return {
author: answer.author.username, author: answer.author.username,
toQuestion: mapToQuestionId(answer.toQuestion), toQuestion: mapToQuestionDTOId(answer.toQuestion),
sequenceNumber: answer.sequenceNumber, sequenceNumber: answer.sequenceNumber!,
}; };
} }

View file

@ -1,18 +1,9 @@
import { languageMap } from '@dwengo-1/common/util/language';
import { FALLBACK_LANG } from '../config.js'; 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 { getLogger } from '../logging/initalize.js';
import { GroupDTO, mapToGroupDTO } from './group.js'; import { AssignmentDTO } from '@dwengo-1/common/interfaces/assignment';
export interface AssignmentDTO {
id: number;
class: string; // Id of class 'within'
title: string;
description: string;
learningPath: string;
language: string;
groups?: GroupDTO[] | string[]; // TODO
}
export function mapToAssignmentDTOId(assignment: Assignment): AssignmentDTO { export function mapToAssignmentDTOId(assignment: Assignment): AssignmentDTO {
return { return {
@ -46,7 +37,7 @@ 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); getLogger().debug(assignment);
return assignment; return assignment;
} }

View file

@ -2,20 +2,7 @@ import { Collection } from '@mikro-orm/core';
import { Class } from '../entities/classes/class.entity.js'; import { Class } from '../entities/classes/class.entity.js';
import { Student } from '../entities/users/student.entity.js'; import { Student } from '../entities/users/student.entity.js';
import { Teacher } from '../entities/users/teacher.entity.js'; import { Teacher } from '../entities/users/teacher.entity.js';
import { ClassDTO } from '@dwengo-1/common/interfaces/class';
export interface ClassDTO {
id: string;
displayName: string;
teachers: string[];
students: string[];
joinRequests: string[];
endpoints?: {
self: string;
invitations: string;
assignments: string;
students: string;
};
}
export function mapToClassDTO(cls: Class): ClassDTO { export function mapToClassDTO(cls: Class): ClassDTO {
return { return {

View file

@ -1,12 +1,7 @@
import { Group } from '../entities/assignments/group.entity.js'; import { Group } from '../entities/assignments/group.entity.js';
import { AssignmentDTO, mapToAssignmentDTO } from './assignment.js'; import { mapToAssignmentDTO } from './assignment.js';
import { mapToStudentDTO, StudentDTO } from './student.js'; import { mapToStudentDTO } from './student.js';
import { GroupDTO } from '@dwengo-1/common/interfaces/group';
export interface GroupDTO {
assignment: number | AssignmentDTO;
groupNumber: number;
members: string[] | StudentDTO[];
}
export function mapToGroupDTO(group: Group): GroupDTO { export function mapToGroupDTO(group: Group): GroupDTO {
return { return {

View file

@ -1,26 +1,21 @@
import { Question } from '../entities/questions/question.entity.js'; import { Question } from '../entities/questions/question.entity.js';
import { UserDTO } from './user.js'; import { mapToStudentDTO } from './student.js';
import { LearningObjectIdentifier } from '../entities/content/learning-object-identifier.js'; import { QuestionDTO, QuestionId } from '@dwengo-1/common/interfaces/question';
import { mapToStudentDTO, StudentDTO } from './student.js'; import { LearningObjectIdentifier } from '@dwengo-1/common/interfaces/learning-content';
import { TeacherDTO } from './teacher.js';
export interface QuestionDTO { function getLearningObjectIdentifier(question: Question): LearningObjectIdentifier {
learningObjectIdentifier: LearningObjectIdentifier; return {
sequenceNumber?: number; hruid: question.learningObjectHruid,
author: StudentDTO; language: question.learningObjectLanguage,
timestamp?: string; version: question.learningObjectVersion,
content: string; };
} }
/** /**
* Convert a Question entity to a DTO format. * Convert a Question entity to a DTO format.
*/ */
export function mapToQuestionDTO(question: Question): QuestionDTO { export function mapToQuestionDTO(question: Question): QuestionDTO {
const learningObjectIdentifier = { const learningObjectIdentifier = getLearningObjectIdentifier(question);
hruid: question.learningObjectHruid,
language: question.learningObjectLanguage,
version: question.learningObjectVersion,
};
return { return {
learningObjectIdentifier, learningObjectIdentifier,
@ -31,14 +26,11 @@ export function mapToQuestionDTO(question: Question): QuestionDTO {
}; };
} }
export interface QuestionId { export function mapToQuestionDTOId(question: Question): QuestionId {
learningObjectIdentifier: LearningObjectIdentifier; const learningObjectIdentifier = getLearningObjectIdentifier(question);
sequenceNumber: number;
}
export function mapToQuestionId(question: QuestionDTO): QuestionId {
return { return {
learningObjectIdentifier: question.learningObjectIdentifier, learningObjectIdentifier,
sequenceNumber: question.sequenceNumber!, sequenceNumber: question.sequenceNumber!,
}; };
} }

View file

@ -0,0 +1,23 @@
import { mapToStudentDTO } from './student.js';
import { ClassJoinRequest } from '../entities/classes/class-join-request.entity.js';
import { getClassJoinRequestRepository } from '../data/repositories.js';
import { Student } from '../entities/users/student.entity.js';
import { Class } from '../entities/classes/class.entity.js';
import { ClassJoinRequestDTO } from '@dwengo-1/common/interfaces/class-join-request';
import { ClassJoinRequestStatus } from '@dwengo-1/common/util/class-join-request';
export function mapToStudentRequestDTO(request: ClassJoinRequest): ClassJoinRequestDTO {
return {
requester: mapToStudentDTO(request.requester),
class: request.class.classId!,
status: request.status,
};
}
export function mapToStudentRequest(student: Student, cls: Class): ClassJoinRequest {
return getClassJoinRequestRepository().create({
requester: student,
class: cls,
status: ClassJoinRequestStatus.Open,
});
}

View file

@ -1,17 +1,6 @@
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 { import { StudentDTO } from '@dwengo-1/common/interfaces/student';
id: string;
username: string;
firstName: string;
lastName: string;
endpoints?: {
classes: string;
questions: string;
invitations: string;
groups: string;
};
}
export function mapToStudentDTO(student: Student): StudentDTO { export function mapToStudentDTO(student: Student): StudentDTO {
return { return {
@ -23,7 +12,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,
});
} }

View file

@ -1,27 +1,15 @@
import { Submission } from '../entities/assignments/submission.entity.js'; import { Submission } from '../entities/assignments/submission.entity.js';
import { Language } from '../entities/content/language.js'; import { mapToGroupDTO } from './group.js';
import { GroupDTO, mapToGroupDTO } from './group.js'; import { mapToStudent, mapToStudentDTO } from './student.js';
import { mapToStudent, mapToStudentDTO, StudentDTO } from './student.js'; import { SubmissionDTO, SubmissionDTOId } from '@dwengo-1/common/interfaces/submission';
import { mapToUser } from './user';
import { Student } from '../entities/users/student.entity';
export interface SubmissionDTO {
learningObjectHruid: string;
learningObjectLanguage: Language;
learningObjectVersion: number;
submissionNumber?: number;
submitter: StudentDTO;
time?: Date;
group?: GroupDTO;
content: string;
}
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 +19,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;

View file

@ -1,12 +1,7 @@
import { TeacherInvitation } from '../entities/classes/teacher-invitation.entity.js'; import { TeacherInvitation } from '../entities/classes/teacher-invitation.entity.js';
import { ClassDTO, mapToClassDTO } from './class.js'; import { mapToClassDTO } from './class.js';
import { mapToUserDTO, UserDTO } from './user.js'; import { mapToUserDTO } from './user.js';
import { TeacherInvitationDTO } from '@dwengo-1/common/interfaces/teacher-invitation';
export interface TeacherInvitationDTO {
sender: string | UserDTO;
receiver: string | UserDTO;
class: string | ClassDTO;
}
export function mapToTeacherInvitationDTO(invitation: TeacherInvitation): TeacherInvitationDTO { export function mapToTeacherInvitationDTO(invitation: TeacherInvitation): TeacherInvitationDTO {
return { return {

View file

@ -1,17 +1,6 @@
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 { import { TeacherDTO } from '@dwengo-1/common/interfaces/teacher';
id: string;
username: string;
firstName: string;
lastName: string;
endpoints?: {
classes: string;
questions: string;
invitations: string;
groups: string;
};
}
export function mapToTeacherDTO(teacher: Teacher): TeacherDTO { export function mapToTeacherDTO(teacher: Teacher): TeacherDTO {
return { return {
@ -22,8 +11,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,
});
} }

View file

@ -1,17 +1,5 @@
import { User } from '../entities/users/user.entity.js'; import { User } from '../entities/users/user.entity.js';
import { UserDTO } from '@dwengo-1/common/interfaces/user';
export interface UserDTO {
id?: string;
username: string;
firstName: string;
lastName: string;
endpoints?: {
self: string;
classes: string;
questions: string;
invitations: string;
};
}
export function mapToUserDTO(user: User): UserDTO { export function mapToUserDTO(user: User): UserDTO {
return { return {

View file

@ -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() {
@ -9,7 +9,7 @@ export class Logger extends WinstonLogger {
} }
} }
const Labels: LokiLabels = { const lokiLabels: LokiLabels = {
source: 'Dwengo-Backend', source: 'Dwengo-Backend',
service: 'API', service: 'API',
host: 'localhost', host: 'localhost',
@ -22,28 +22,38 @@ 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: lokiLabels,
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): void => {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.error(`Connection error: ${err}`); console.error(`Connection error: ${err}`);
}, },
}); });
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;
} }

View file

@ -5,35 +5,54 @@ import { LokiLabels } from 'loki-logger-ts';
export class MikroOrmLogger extends DefaultLogger { export class MikroOrmLogger extends DefaultLogger {
private logger: Logger = getLogger(); private logger: Logger = getLogger();
log(namespace: LoggerNamespace, message: string, context?: LogContext) { static createMessage(namespace: LoggerNamespace, messageArg: string, context?: LogContext): unknown {
const labels: LokiLabels = {
service: 'ORM',
};
let message: string;
if (context?.label) {
message = `[${namespace}] (${context.label}) ${messageArg}`;
} else {
message = `[${namespace}] ${messageArg}`;
}
return {
message: message,
labels: labels,
context: context,
};
}
log(namespace: LoggerNamespace, message: string, context?: LogContext): void {
if (!this.isEnabled(namespace, context)) { if (!this.isEnabled(namespace, context)) {
return; return;
} }
switch (namespace) { switch (namespace) {
case 'query': case 'query':
this.logger.debug(this.createMessage(namespace, message, context)); this.logger.debug(MikroOrmLogger.createMessage(namespace, message, context));
break; break;
case 'query-params': case 'query-params':
// TODO Which log level should this be? // TODO Which log level should this be?
this.logger.info(this.createMessage(namespace, message, context)); this.logger.info(MikroOrmLogger.createMessage(namespace, message, context));
break; break;
case 'schema': case 'schema':
this.logger.info(this.createMessage(namespace, message, context)); this.logger.info(MikroOrmLogger.createMessage(namespace, message, context));
break; break;
case 'discovery': case 'discovery':
this.logger.debug(this.createMessage(namespace, message, context)); this.logger.debug(MikroOrmLogger.createMessage(namespace, message, context));
break; break;
case 'info': case 'info':
this.logger.info(this.createMessage(namespace, message, context)); this.logger.info(MikroOrmLogger.createMessage(namespace, message, context));
break; break;
case 'deprecated': case 'deprecated':
this.logger.warn(this.createMessage(namespace, message, context)); this.logger.warn(MikroOrmLogger.createMessage(namespace, message, context));
break; break;
default: default:
switch (context?.level) { switch (context?.level) {
case 'info': case 'info':
this.logger.info(this.createMessage(namespace, message, context)); this.logger.info(MikroOrmLogger.createMessage(namespace, message, context));
break; break;
case 'warning': case 'warning':
this.logger.warn(message); this.logger.warn(message);
@ -47,23 +66,4 @@ export class MikroOrmLogger extends DefaultLogger {
} }
} }
} }
private createMessage(namespace: LoggerNamespace, messageArg: string, context?: LogContext) {
const labels: LokiLabels = {
service: 'ORM',
};
let message: string;
if (context?.label) {
message = `[${namespace}] (${context?.label}) ${messageArg}`;
} else {
message = `[${namespace}] ${messageArg}`;
}
return {
message: message,
labels: labels,
context: context,
};
}
} }

View file

@ -1,7 +1,7 @@
import { getLogger, Logger } from './initalize.js'; import { getLogger, Logger } from './initalize.js';
import { Request, Response } from 'express'; import { Request, Response } from 'express';
export function responseTimeLogger(req: Request, res: Response, time: number) { export function responseTimeLogger(req: Request, res: Response, time: number): void {
const logger: Logger = getLogger(); const logger: Logger = getLogger();
const method = req.method; const method = req.method;

View file

@ -1,12 +1,13 @@
import { EnvVars, getEnvVar } from '../../util/envvars.js'; import { envVars, getEnvVar } from '../../util/envVars.js';
import { expressjwt } from 'express-jwt'; import { expressjwt } from 'express-jwt';
import * as jwt from 'jsonwebtoken';
import { JwtPayload } from 'jsonwebtoken'; import { JwtPayload } from 'jsonwebtoken';
import jwksClient from 'jwks-rsa'; import jwksClient from 'jwks-rsa';
import * as express from 'express'; import * as express from 'express';
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;
@ -32,12 +33,12 @@ function createJwksClient(uri: string): jwksClient.JwksClient {
const idpConfigs = { const idpConfigs = {
student: { student: {
issuer: getEnvVar(EnvVars.IdpStudentUrl), issuer: getEnvVar(envVars.IdpStudentUrl),
jwksClient: createJwksClient(getEnvVar(EnvVars.IdpStudentJwksEndpoint)), jwksClient: createJwksClient(getEnvVar(envVars.IdpStudentJwksEndpoint)),
}, },
teacher: { teacher: {
issuer: getEnvVar(EnvVars.IdpTeacherUrl), issuer: getEnvVar(envVars.IdpTeacherUrl),
jwksClient: createJwksClient(getEnvVar(EnvVars.IdpTeacherJwksEndpoint)), jwksClient: createJwksClient(getEnvVar(envVars.IdpTeacherJwksEndpoint)),
}, },
}; };
@ -63,7 +64,7 @@ const verifyJwtToken = expressjwt({
} }
return signingKey.getPublicKey(); return signingKey.getPublicKey();
}, },
audience: getEnvVar(EnvVars.IdpAudience), audience: getEnvVar(envVars.IdpAudience),
algorithms: [JWT_ALGORITHM], algorithms: [JWT_ALGORITHM],
credentialsRequired: false, credentialsRequired: false,
requestProperty: REQUEST_PROPERTY_FOR_JWT_PAYLOAD, requestProperty: REQUEST_PROPERTY_FOR_JWT_PAYLOAD,
@ -74,7 +75,7 @@ const verifyJwtToken = expressjwt({
*/ */
function getAuthenticationInfo(req: AuthenticatedRequest): AuthenticationInfo | undefined { function getAuthenticationInfo(req: AuthenticatedRequest): AuthenticationInfo | undefined {
if (!req.jwtPayload) { if (!req.jwtPayload) {
return; return undefined;
} }
const issuer = req.jwtPayload.iss; const issuer = req.jwtPayload.iss;
let accountType: 'student' | 'teacher'; let accountType: 'student' | 'teacher';
@ -84,8 +85,9 @@ function getAuthenticationInfo(req: AuthenticatedRequest): AuthenticationInfo |
} else if (issuer === idpConfigs.teacher.issuer) { } else if (issuer === idpConfigs.teacher.issuer) {
accountType = 'teacher'; accountType = 'teacher';
} else { } else {
return; return undefined;
} }
return { return {
accountType: accountType, accountType: accountType,
username: req.jwtPayload[JWT_PROPERTY_NAMES.username]!, username: req.jwtPayload[JWT_PROPERTY_NAMES.username]!,
@ -100,10 +102,10 @@ function getAuthenticationInfo(req: AuthenticatedRequest): AuthenticationInfo |
* Add the AuthenticationInfo object with the information about the current authentication to the request in order * Add the AuthenticationInfo object with the information about the current authentication to the request in order
* to avoid that the routers have to deal with the JWT token. * to avoid that the routers have to deal with the JWT token.
*/ */
const addAuthenticationInfo = (req: AuthenticatedRequest, res: express.Response, next: express.NextFunction) => { function addAuthenticationInfo(req: AuthenticatedRequest, _res: express.Response, next: express.NextFunction): void {
req.auth = getAuthenticationInfo(req); req.auth = getAuthenticationInfo(req);
next(); next();
}; }
export const authenticateUser = [verifyJwtToken, addAuthenticationInfo]; export const authenticateUser = [verifyJwtToken, addAuthenticationInfo];
@ -113,9 +115,8 @@ export const authenticateUser = [verifyJwtToken, addAuthenticationInfo];
* @param accessCondition Predicate over the current AuthenticationInfo. Access is only granted when this evaluates * @param accessCondition Predicate over the current AuthenticationInfo. Access is only granted when this evaluates
* to true. * to true.
*/ */
export const authorize = export function authorize(accessCondition: (auth: AuthenticationInfo) => boolean) {
(accessCondition: (auth: AuthenticationInfo) => boolean) => return (req: AuthenticatedRequest, _res: express.Response, next: express.NextFunction): void => {
(req: AuthenticatedRequest, res: express.Response, next: express.NextFunction): void => {
if (!req.auth) { if (!req.auth) {
throw new UnauthorizedException(); throw new UnauthorizedException();
} else if (!accessCondition(req.auth)) { } else if (!accessCondition(req.auth)) {
@ -124,6 +125,7 @@ export const authorize =
next(); next();
} }
}; };
}
/** /**
* Middleware which rejects all unauthenticated users, but accepts all authenticated users. * Middleware which rejects all unauthenticated users, but accepts all authenticated users.

View file

@ -1,11 +1,11 @@
/** /**
* Object with information about the user who is currently logged in. * Object with information about the user who is currently logged in.
*/ */
export type AuthenticationInfo = { export interface AuthenticationInfo {
accountType: 'student' | 'teacher'; accountType: 'student' | 'teacher';
username: string; username: string;
name?: string; name?: string;
firstName?: string; firstName?: string;
lastName?: string; lastName?: string;
email?: string; email?: string;
}; }

View file

@ -1,7 +1,7 @@
import cors from 'cors'; import cors from 'cors';
import { EnvVars, getEnvVar } from '../util/envvars.js'; import { envVars, getEnvVar } from '../util/envVars.js';
export default cors({ export default cors({
origin: getEnvVar(EnvVars.CorsAllowedOrigins).split(','), origin: getEnvVar(envVars.CorsAllowedOrigins).split(','),
allowedHeaders: getEnvVar(EnvVars.CorsAllowedHeaders).split(','), allowedHeaders: getEnvVar(envVars.CorsAllowedHeaders).split(','),
}); });

View 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);
}
}

View file

@ -1,9 +1,8 @@
import { LoggerOptions, Options } from '@mikro-orm/core'; import { LoggerOptions, Options } from '@mikro-orm/core';
import { PostgreSqlDriver } from '@mikro-orm/postgresql'; 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';
@ -43,33 +42,35 @@ const entities = [
Question, Question,
]; ];
function config(testingMode: boolean = false): Options { function config(testingMode = false): Options {
if (testingMode) { if (testingMode) {
return { return {
driver: SqliteDriver, driver: SqliteDriver,
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)
// (see https://mikro-orm.io/docs/guide/project-setup#testing-the-endpoint) // (see https://mikro-orm.io/docs/guide/project-setup#testing-the-endpoint)
dynamicImportProvider: (id) => import(id), dynamicImportProvider: async (id) => import(id),
}; };
} }
return { return {
driver: PostgreSqlDriver, driver: PostgreSqlDriver,
host: getEnvVar(EnvVars.DbHost), host: getEnvVar(envVars.DbHost),
port: getNumericEnvVar(EnvVars.DbPort), port: getNumericEnvVar(envVars.DbPort),
dbName: getEnvVar(EnvVars.DbName), dbName: getEnvVar(envVars.DbName),
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),
}; };
} }

View file

@ -1,10 +1,10 @@
import { EntityManager, MikroORM } from '@mikro-orm/core'; import { EntityManager, MikroORM } from '@mikro-orm/core';
import config from './mikro-orm.config.js'; import config from './mikro-orm.config.js';
import { EnvVars, getEnvVar } from './util/envvars.js'; import { envVars, getEnvVar } from './util/envVars.js';
import { getLogger, Logger } from './logging/initalize.js'; import { getLogger, Logger } from './logging/initalize.js';
let orm: MikroORM | undefined; let orm: MikroORM | undefined;
export async function initORM(testingMode: boolean = false) { export async function initORM(testingMode = false): Promise<void> {
const logger: Logger = getLogger(); const logger: Logger = getLogger();
logger.info('Initializing ORM'); logger.info('Initializing ORM');
@ -12,7 +12,7 @@ export async function initORM(testingMode: boolean = false) {
orm = await MikroORM.init(config(testingMode)); orm = await MikroORM.init(config(testingMode));
// Update the database scheme if necessary and enabled. // Update the database scheme if necessary and enabled.
if (getEnvVar(EnvVars.DbUpdate)) { if (getEnvVar(envVars.DbUpdate)) {
await orm.schema.updateSchema(); await orm.schema.updateSchema();
} else { } else {
const diff = await orm.schema.getUpdateSchemaSQL(); const diff = await orm.schema.getUpdateSchemaSQL();

View file

@ -19,7 +19,7 @@ router.get('/:id', getAssignmentHandler);
router.get('/:id/submissions', getAssignmentsSubmissionsHandler); router.get('/:id/submissions', getAssignmentsSubmissionsHandler);
router.get('/:id/questions', (req, res) => { router.get('/:id/questions', (_req, res) => {
res.json({ res.json({
questions: ['0'], questions: ['0'],
}); });

View file

@ -4,21 +4,21 @@ import { authenticatedOnly, studentsOnly, teachersOnly } from '../middleware/aut
const router = express.Router(); const router = express.Router();
// Returns auth configuration for frontend // Returns auth configuration for frontend
router.get('/config', (req, res) => { router.get('/config', (_req, res) => {
res.json(getFrontendAuthConfig()); res.json(getFrontendAuthConfig());
}); });
router.get('/testAuthenticatedOnly', authenticatedOnly, (req, res) => { router.get('/testAuthenticatedOnly', authenticatedOnly, (_req, res) => {
/* #swagger.security = [{ "student": [ ] }, { "teacher": [ ] }] */ /* #swagger.security = [{ "student": [ ] }, { "teacher": [ ] }] */
res.json({ message: 'If you see this, you should be authenticated!' }); res.json({ message: 'If you see this, you should be authenticated!' });
}); });
router.get('/testStudentsOnly', studentsOnly, (req, res) => { router.get('/testStudentsOnly', studentsOnly, (_req, res) => {
/* #swagger.security = [{ "student": [ ] }] */ /* #swagger.security = [{ "student": [ ] }] */
res.json({ message: 'If you see this, you should be a student!' }); res.json({ message: 'If you see this, you should be a student!' });
}); });
router.get('/testTeachersOnly', teachersOnly, (req, res) => { router.get('/testTeachersOnly', teachersOnly, (_req, res) => {
/* #swagger.security = [{ "teacher": [ ] }] */ /* #swagger.security = [{ "teacher": [ ] }] */
res.json({ message: 'If you see this, you should be a teacher!' }); res.json({ message: 'If you see this, you should be a teacher!' });
}); });

View file

@ -14,7 +14,7 @@ router.get('/:groupid', getGroupHandler);
router.get('/:groupid', getGroupSubmissionsHandler); router.get('/:groupid', getGroupSubmissionsHandler);
// The list of questions a group has made // The list of questions a group has made
router.get('/:id/questions', (req, res) => { router.get('/:id/questions', (_req, res) => {
res.json({ res.json({
questions: ['0'], questions: ['0'],
}); });

View file

@ -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'] */);

View file

@ -0,0 +1,19 @@
import express from 'express';
import {
createStudentRequestHandler,
deleteClassJoinRequestHandler,
getStudentRequestHandler,
getStudentRequestsHandler,
} from '../controllers/students.js';
const router = express.Router({ mergeParams: true });
router.get('/', getStudentRequestsHandler);
router.post('/', createStudentRequestHandler);
router.get('/:classId', getStudentRequestHandler);
router.delete('/:classId', deleteClassJoinRequestHandler);
export default router;

View file

@ -7,9 +7,11 @@ import {
getStudentClassesHandler, getStudentClassesHandler,
getStudentGroupsHandler, getStudentGroupsHandler,
getStudentHandler, getStudentHandler,
getStudentQuestionsHandler,
getStudentSubmissionsHandler, getStudentSubmissionsHandler,
} from '../controllers/students.js'; } from '../controllers/students.js';
import { getStudentGroups } from '../services/students.js'; import joinRequestRouter from './student-join-requests.js';
const router = express.Router(); const router = express.Router();
// Root endpoint used to search objects // Root endpoint used to search objects
@ -17,30 +19,26 @@ router.get('/', getAllStudentsHandler);
router.post('/', createStudentHandler); router.post('/', createStudentHandler);
router.delete('/', deleteStudentHandler);
router.delete('/:username', deleteStudentHandler); router.delete('/:username', deleteStudentHandler);
// Information about a student's profile // Information about a student's profile
router.get('/:username', getStudentHandler); router.get('/:username', getStudentHandler);
// The list of classes a student is in // The list of classes a student is in
router.get('/:id/classes', getStudentClassesHandler); router.get('/:username/classes', getStudentClassesHandler);
// The list of submissions a student has made // The list of submissions a student has made
router.get('/:id/submissions', getStudentSubmissionsHandler); router.get('/:username/submissions', getStudentSubmissionsHandler);
// The list of assignments a student has // The list of assignments a student has
router.get('/:id/assignments', getStudentAssignmentsHandler); router.get('/:username/assignments', getStudentAssignmentsHandler);
// The list of groups a student is in // The list of groups a student is in
router.get('/:id/groups', getStudentGroupsHandler); router.get('/:username/groups', getStudentGroupsHandler);
// A list of questions a user has created // A list of questions a user has created
router.get('/:id/questions', (req, res) => { router.get('/:username/questions', getStudentQuestionsHandler);
res.json({
questions: ['0'], router.use('/:username/joinRequests', joinRequestRouter);
});
});
export default router; export default router;

Some files were not shown because too many files have changed in this diff Show more