Merge branch 'dev' into github-actions/testing

This commit is contained in:
Timo De Meyst 2025-04-01 08:56:19 +02:00 committed by GitHub
commit 05fa51603a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
236 changed files with 10405 additions and 2452 deletions

7
.dockerignore Normal file
View file

@ -0,0 +1,7 @@
**/node_modules/
**/dist
.git
npm-debug.log
.coverage
.coverage.*
.env

19
.github/workflows/deployment.yml vendored Normal file
View file

@ -0,0 +1,19 @@
name: Deployment
on:
push:
branches:
- main
jobs:
docker:
name: Deploy with docker
runs-on: [self-hosted, Linux, X64]
steps:
-
name: Checkout
uses: actions/checkout@v4
-
name: Start docker
run: docker compose -f compose.yml -f compose.prod.yml up --build -d

View file

@ -45,4 +45,4 @@ jobs:
eslint: true
eslint_args: '--config eslint.config.ts'
prettier: true
commit_message: 'style: fix linting issues met ${linter}'
commit_message: 'style: fix linting issues met ${linter}'

View file

@ -21,14 +21,16 @@ Alternatief kan je één van de volgende methodes gebruiken om de applicatie lok
### Quick start
Om de applicatie lokaal te draaien als kant-en-klare Docker-containers:
1. Installeer Docker en Docker Compose op je systeem (zie [Docker](https://docs.docker.com/get-docker/)
en [Docker Compose](https://docs.docker.com/compose/)).
2. Clone deze repository.
3. In de backend, kopieer `.env.example` (of `.env.development.example`) naar `.env` en pas de variabelen aan waar
nodig.
4. Voer `docker compose up` uit in de root van de repository.
3. In de backend, kopieer `.env.example` naar `.env` en pas de variabelen aan waar nodig.
4. Voer `docker compose -f compose.staging.yml up --build` uit in de root van de repository.
5. Optioneel: Configureer de applicatie aan de hand van
de [configuratiehandleiding](https://github.com/SELab-2/Dwengo-1/wiki/Administrator:-Productie-omgeving#dwengo-1-configuratie).
6. De applicatie is nu beschikbaar op [`http://localhost/`](http://localhost/) en [`http://localhost/api`](http://localhost/api).
```bash
docker compose version
@ -38,14 +40,13 @@ cp .env.example .env
# Pas .env aan
nano .env
cd ..
docker compose up
# Configureer de applicatie
docker compose -f compose.staging.yml up --build
```
### Handmatige installatie
### Handmatige installatie en ontwikkeling
Zie de submappen voor de installatie-instructies van de [frontend](./frontend/README.md)
en [backend](./backend/README.md).
en [backend](./backend/README.md) en instructies voor het opzetten van een ontwikkelomgeving.
## Architectuur

View file

@ -1,9 +1,24 @@
DWENGO_PORT=3000
#
# 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 ###
#DWENGO_PORT=3000
#DWENGO_LEARNING_CONTENT_REPO_API_BASE_URL=https://dwengo.org/backend/api
#DWENGO_FALLBACK_LANGUAGE=nl
#DWENGO_RUN_MODE=dev
DWENGO_DB_HOST=localhost
DWENGO_DB_PORT=5431
#DWENGO_DB_NAME=dwengo
DWENGO_DB_USERNAME=postgres
DWENGO_DB_PASSWORD=postgres
DWENGO_DB_UPDATE=true
#DWENGO_DB_CONTENT_PREFIX=u_
DWENGO_AUTH_STUDENT_URL=http://localhost:7080/realms/student
DWENGO_AUTH_STUDENT_CLIENT_ID=dwengo
@ -11,6 +26,12 @@ DWENGO_AUTH_STUDENT_JWKS_ENDPOINT=http://localhost:7080/realms/student/protocol/
DWENGO_AUTH_TEACHER_URL=http://localhost:7080/realms/teacher
DWENGO_AUTH_TEACHER_CLIENT_ID=dwengo
DWENGO_AUTH_TEACHER_JWKS_ENDPOINT=http://localhost:7080/realms/teacher/protocol/openid-connect/certs
#DWENGO_AUTH_AUDIENCE=account
# Allow Vite dev-server to access the backend (for testing purposes). Don't forget to remove this in production!
DWENGO_CORS_ALLOWED_ORIGINS=http://localhost:5173
#DWENGO_CORS_ALLOWED_HEADERS=Authorization,Content-Type
### Advanced configuration ###
DWENGO_LOGGING_LEVEL=debug
#DWENGO_LOGGING_LOKI_HOST=http://localhost:3102

View file

@ -1,22 +1,68 @@
DWENGO_PORT=3000 # The port the backend will listen on
#
# Basic configuration
#
# Change the values of the variables below to match your environment!
# Default values are commented out.
#
### Dwengo ###
# Port the backend will listen on
#DWENGO_PORT=3000
# The hostname or IP address of the remote learning content API.
#DWENGO_LEARNING_CONTENT_REPO_API_BASE_URL=https://dwengo.org/backend/api
# The default fallback language.
#DWENGO_FALLBACK_LANGUAGE=nl
# Whether running in production mode or not. Possible values are "prod" or "dev".
#DWENGO_RUN_MODE=dev
# ! Change this! The hostname or IP address of the database
# If running your stack in docker, this should use the docker service name.
DWENGO_DB_HOST=domain-or-ip-of-database
DWENGO_DB_PORT=5432
# Change this to the actual credentials of the user Dwengo should use in the backend
DWENGO_DB_USERNAME=postgres
DWENGO_DB_PASSWORD=postgres
# The port of the database.
#DWENGO_DB_PORT=5432
# The name of the database.
#DWENGO_DB_NAME=dwengo
# ! Change this! The username of the database user.
DWENGO_DB_USERNAME=username
# ! Change this! The password of the database user.
DWENGO_DB_PASSWORD=password
# Whether the database scheme needs to be updated.
# Set this to true when the database scheme needs to be updated. In that case, take a backup first.
DWENGO_DB_UPDATE=false
#DWENGO_DB_UPDATE=false
# The prefix used for custom user content.
#DWENGO_DB_CONTENT_PREFIX=u_
# Data for the identity provider via which the students authenticate.
DWENGO_AUTH_STUDENT_URL=http://localhost:7080/realms/student
# ! Change this! The external URL for student authentication. Should be reachable by the client.
# E.g. https://sel2-1.ugent.be/idp/realms/student
DWENGO_AUTH_STUDENT_URL=http://hostname/idp/realms/student
# ! Change this! The client ID for student authentication.
DWENGO_AUTH_STUDENT_CLIENT_ID=dwengo
DWENGO_AUTH_STUDENT_JWKS_ENDPOINT=http://localhost:7080/realms/student/protocol/openid-connect/certs
# Data for the identity provider via which the teachers authenticate.
DWENGO_AUTH_TEACHER_URL=http://localhost:7080/realms/teacher
# ! Change this! The internal URL for retrieving the JWKS for student authentication.
# Should be reachable by the backend. If running your stack in docker, this should use the docker service name.
# E.g. http://idp:7080/realms/student/protocol/openid-connect/certs
DWENGO_AUTH_STUDENT_JWKS_ENDPOINT=http://hostname/realms/student/protocol/openid-connect/certs
# ! Change this! The external URL for teacher authentication. Should be reachable by the client.
# E.g. https://sel2-1.ugent.be/idp/realms/teacher
DWENGO_AUTH_TEACHER_URL=http://hostname/idp/realms/teacher
# ! Change this! The client ID for teacher authentication.
DWENGO_AUTH_TEACHER_CLIENT_ID=dwengo
DWENGO_AUTH_TEACHER_JWKS_ENDPOINT=http://localhost:7080/realms/teacher/protocol/openid-connect/certs
# ! Change this! The internal URL for retrieving the JWKS for teacher authentication.
# Should be reachable by the backend. If running your stack in docker, this should use the docker service name.
# E.g. http://idp:7080/realms/teacher/protocol/openid-connect/certs
DWENGO_AUTH_TEACHER_JWKS_ENDPOINT=http://hostname/realms/teacher/protocol/openid-connect/certs
# The IDP audience
#DWENGO_AUTH_AUDIENCE=account
# LOKI_HOST=http://localhost:3102 # The address of the Loki instance, used for logging
# Allowed origins for CORS requests. Separate multiple origins with a comma.
#DWENGO_CORS_ALLOWED_ORIGINS=
# Allowed headers for CORS requests. Separate multiple headers with a comma.
#DWENGO_CORS_ALLOWED_HEADERS=Authorization,Content-Type
### Advanced configuration ###
# The logging level. Possible values are "debug", "info", "warn", "error".
#DWENGO_LOGGING_LEVEL=info
# The address of the Loki instance, a log aggregation system.
# If running your stack in docker, this should use the docker service name.
#DWENGO_LOGGING_LOKI_HOST=http://localhost:3102

View file

@ -0,0 +1,37 @@
#
# Production environment configuration
#
# Change the values of the variables below to match your production environment!
# See .env.example for more information.
#
### Dwengo ###
DWENGO_PORT=3000
#DWENGO_LEARNING_CONTENT_REPO_API_BASE_URL=https://dwengo.org/backend/api
#DWENGO_FALLBACK_LANGUAGE=nl
DWENGO_RUN_MODE=prod
DWENGO_DB_HOST=db
DWENGO_DB_PORT=5432
DWENGO_DB_NAME=postgres
DWENGO_DB_USERNAME=postgres
DWENGO_DB_PASSWORD=postgres
DWENGO_DB_UPDATE=false
#DWENGO_DB_CONTENT_PREFIX=u_
DWENGO_AUTH_STUDENT_URL=https://sel2-1.ugent.be/idp/realms/student
DWENGO_AUTH_STUDENT_CLIENT_ID=dwengo
DWENGO_AUTH_STUDENT_JWKS_ENDPOINT=http://idp:7080/idp/realms/student/protocol/openid-connect/certs # Name of the idp container
DWENGO_AUTH_TEACHER_URL=https://sel2-1.ugent.be/idp/realms/teacher
DWENGO_AUTH_TEACHER_CLIENT_ID=dwengo
DWENGO_AUTH_TEACHER_JWKS_ENDPOINT=http://idp:7080/idp/realms/teacher/protocol/openid-connect/certs # Name of the idp container
#DWENGO_AUTH_AUDIENCE=account
#DWENGO_CORS_ALLOWED_ORIGINS=
#DWENGO_CORS_ALLOWED_HEADERS=Authorization,Content-Type
### Advanced configuration ###
DWENGO_LOGGING_LEVEL=info
DWENGO_LOGGING_LOKI_HOST=http://logging:3102

13
backend/.env.test.example 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

38
backend/Dockerfile Normal file
View file

@ -0,0 +1,38 @@
FROM node:22 AS build-stage
WORKDIR /app
# Install dependencies
COPY package*.json ./
COPY backend/package.json ./backend/
RUN npm install --silent
# Build the backend
# Root tsconfig.json
COPY tsconfig.json ./
WORKDIR /app/backend
COPY backend ./
COPY docs /app/docs
RUN npm run build
FROM node:22 AS production-stage
WORKDIR /app
COPY package-lock.json backend/package.json ./
RUN npm install --silent --only=production
COPY ./docs /docs
COPY ./backend/i18n /app/i18n
COPY --from=build-stage /app/backend/dist ./dist/
EXPOSE 3000
CMD ["node", "--env-file=.env", "dist/app.js"]

View file

@ -4,23 +4,24 @@
```shell
npm install
# Start de nodige services voor ontwikkeling
cd ../ # Ga naar de root van de repository
docker compose up -d
```
Setup the environment variables in a `.env` file in the root of the project. You can use the `.env.example` file as a template.
Zet de omgevingsvariabelen in een `.env` bestand in de root van het project.
Je kan het `.env.example` bestand als template gebruiken.
### Development
### Ontwikkeling
```shell
# Omgevingsvariabelen
cp .env.development.example .env.development.local
npm run dev
```
### Production
```shell
npm run build
npm run start
```
### Tests
Voer volgend commando uit om de unit tests uit te voeren:
@ -29,6 +30,18 @@ Voer volgend commando uit om de unit tests uit te voeren:
npm run test:unit
```
### Productie
```shell
# Omgevingsvariabelen
cp .env.development.example .env
npm run build
npm run start
```
Zie ook de [installatiehandleiding](https://github.com/SELab-2/Dwengo-1/wiki/Administrator:-Productie-omgeving).
## Keycloak configuratie
Tijdens development is het voldoende om gebruik te maken van de keycloak configuratie die automatisch ingeladen wordt.

7
backend/config.js Normal file
View file

@ -0,0 +1,7 @@
// 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,
},
},
{
files: ['tests/**/*.ts'],
languageOptions: {
globals: globals.node,
},
rules: {
'no-console': 'off',
},
},
];

View file

@ -28,9 +28,9 @@ curricula_page:
contact: ''
teaser: https://www.youtube.com/embed/2B6gZ9HdQ1Y
basics_ai:
title: Basisprincipes van AI
sub_title: Basisprincipes van 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.'
title: Grundlagen der KI
sub_title: Grundlagen der KI
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: ''
kiks:
title: KI und Klima

View file

@ -28,10 +28,11 @@ curricula_page:
contact: ''
teaser: https://www.youtube.com/embed/2B6gZ9HdQ1Y
basics_ai:
title: Basisprincipes van AI
sub_title: Basisprincipes van 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.'
title: Basics of AI
sub_title: Basics of AI
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: ''
kiks:
title: AI and Climate
sub_title: KIKS

View file

@ -28,9 +28,9 @@ curricula_page:
contact: ''
teaser: https://www.youtube.com/embed/2B6gZ9HdQ1Y
basics_ai:
title: Basisprincipes van AI
sub_title: Basisprincipes van 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.'
title: Principes de base de lIA
sub_title: Principes de base de lIA
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: ''
kiks:
title: 'IA et changement climatique'

View file

@ -1,6 +1,6 @@
{
"name": "dwengo-1-backend",
"version": "0.0.1",
"version": "0.1.1",
"description": "Backend for Dwengo-1",
"private": true,
"type": "module",
@ -11,7 +11,7 @@
"format": "prettier --write src/",
"format-check": "prettier --check src/",
"lint": "eslint . --fix",
"test:unit": "vitest"
"test:unit": "vitest --run"
},
"dependencies": {
"@mikro-orm/core": "6.4.9",
@ -34,6 +34,7 @@
"loki-logger-ts": "^1.0.2",
"marked": "^15.0.7",
"response-time": "^2.3.3",
"swagger-ui-express": "^5.0.1",
"uuid": "^11.1.0",
"winston": "^3.17.0",
"winston-loki": "^6.1.3"
@ -45,6 +46,7 @@
"@types/js-yaml": "^4.0.9",
"@types/node": "^22.13.4",
"@types/response-time": "^2.3.8",
"@types/swagger-ui-express": "^4.1.8",
"globals": "^15.15.0",
"ts-node": "^10.9.2",
"tsx": "^4.19.3",

View file

@ -1,58 +1,39 @@
import express, { Express, Response } from 'express';
import express, { Express } from 'express';
import { initORM } from './orm.js';
import themeRoutes from './routes/themes.js';
import learningPathRoutes from './routes/learning-paths.js';
import learningObjectRoutes from './routes/learning-objects.js';
import studentRouter from './routes/student.js';
import groupRouter from './routes/group.js';
import assignmentRouter from './routes/assignment.js';
import submissionRouter from './routes/submission.js';
import classRouter from './routes/class.js';
import questionRouter from './routes/question.js';
import authRouter from './routes/auth.js';
import { authenticateUser } from './middleware/auth/auth.js';
import cors from './middleware/cors.js';
import { getLogger, Logger } from './logging/initalize.js';
import { responseTimeLogger } from './logging/responseTimeLogger.js';
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 swaggerMiddleware from './swagger.js';
import swaggerUi from 'swagger-ui-express';
import { errorHandler } from './middleware/error-handling/error-handler.js';
const logger: Logger = getLogger();
const app: Express = express();
const port: string | number = getNumericEnvVar(EnvVars.Port);
const port: string | number = getNumericEnvVar(envVars.Port);
app.use(cors);
app.use(express.json());
app.use(responseTime(responseTimeLogger));
app.use(cors);
app.use(authenticateUser);
// Add response time logging
app.use(responseTime(responseTimeLogger));
// TODO Replace with Express routes
app.get('/', (_, res: Response) => {
logger.debug('GET /');
res.json({
message: 'Hello Dwengo!🚀',
});
});
app.use('/api', apiRouter);
app.use('/student', studentRouter);
app.use('/group', groupRouter);
app.use('/assignment', assignmentRouter);
app.use('/submission', submissionRouter);
app.use('/class', classRouter);
app.use('/question', questionRouter);
app.use('/auth', authRouter);
app.use('/theme', themeRoutes);
app.use('/learningPath', learningPathRoutes);
app.use('/learningObject', learningObjectRoutes);
// Swagger
app.use('/api-docs', swaggerUi.serve, swaggerMiddleware);
async function startServer() {
app.use(errorHandler);
async function startServer(): Promise<void> {
await initORM();
app.listen(port, () => {
logger.info(`Server is running at http://localhost:${port}`);
logger.info(`Server is running at http://localhost:${port}/api`);
});
}

View file

@ -1,9 +1,7 @@
import { EnvVars, getEnvVar } from './util/envvars.js';
import { envVars, getEnvVar } from './util/envVars.js';
// API
export const DWENGO_API_BASE = getEnvVar(EnvVars.LearningContentRepoApiBaseUrl);
export const FALLBACK_LANG = getEnvVar(EnvVars.FallbackLanguage);
export const DWENGO_API_BASE = getEnvVar(envVars.LearningContentRepoApiBaseUrl);
export const FALLBACK_LANG = getEnvVar(envVars.FallbackLanguage);
// Logging
export const LOG_LEVEL: string = 'development' === process.env.NODE_ENV ? 'debug' : 'info';
export const LOKI_HOST: string = process.env.LOKI_HOST || 'http://localhost:3102';
export const FALLBACK_SEQ_NUM = 1;

View file

@ -0,0 +1,77 @@
import { Request, Response } from 'express';
import { createAssignment, getAllAssignments, getAssignment, getAssignmentsSubmissions } from '../services/assignments.js';
import { AssignmentDTO } from '../interfaces/assignment.js';
// Typescript is annoying with parameter forwarding from class.ts
interface AssignmentParams {
classid: string;
id: string;
}
export async function getAllAssignmentsHandler(req: Request<AssignmentParams>, res: Response): Promise<void> {
const classid = req.params.classid;
const full = req.query.full === 'true';
const assignments = await getAllAssignments(classid, full);
res.json({
assignments: assignments,
});
}
export async function createAssignmentHandler(req: Request<AssignmentParams>, res: Response): Promise<void> {
const classid = req.params.classid;
const assignmentData = req.body as AssignmentDTO;
if (!assignmentData.description || !assignmentData.language || !assignmentData.learningPath || !assignmentData.title) {
res.status(400).json({
error: 'Missing one or more required fields: title, description, learningPath, language',
});
return;
}
const assignment = await createAssignment(classid, assignmentData);
if (!assignment) {
res.status(500).json({ error: 'Could not create assignment ' });
return;
}
res.status(201).json(assignment);
}
export async function getAssignmentHandler(req: Request<AssignmentParams>, res: Response): Promise<void> {
const id = Number(req.params.id);
const classid = req.params.classid;
if (isNaN(id)) {
res.status(400).json({ error: 'Assignment id must be a number' });
return;
}
const assignment = await getAssignment(classid, id);
if (!assignment) {
res.status(404).json({ error: 'Assignment not found' });
return;
}
res.json(assignment);
}
export async function getAssignmentsSubmissionsHandler(req: Request<AssignmentParams>, res: Response): Promise<void> {
const classid = req.params.classid;
const assignmentNumber = Number(req.params.id);
const full = req.query.full === 'true';
if (isNaN(assignmentNumber)) {
res.status(400).json({ error: 'Assignment id must be a number' });
return;
}
const submissions = await getAssignmentsSubmissions(classid, assignmentNumber, full);
res.json({
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;
clientId: string;
scope: string;
responseType: string;
};
}
type FrontendAuthConfig = {
interface FrontendAuthConfig {
student: FrontendIdpConfig;
teacher: FrontendIdpConfig;
};
}
const SCOPE = 'openid profile email';
const RESPONSE_TYPE = 'code';
@ -18,14 +18,14 @@ const RESPONSE_TYPE = 'code';
export function getFrontendAuthConfig(): FrontendAuthConfig {
return {
student: {
authority: getEnvVar(EnvVars.IdpStudentUrl),
clientId: getEnvVar(EnvVars.IdpStudentClientId),
authority: getEnvVar(envVars.IdpStudentUrl),
clientId: getEnvVar(envVars.IdpStudentClientId),
scope: SCOPE,
responseType: RESPONSE_TYPE,
},
teacher: {
authority: getEnvVar(EnvVars.IdpTeacherUrl),
clientId: getEnvVar(EnvVars.IdpTeacherClientId),
authority: getEnvVar(envVars.IdpTeacherUrl),
clientId: getEnvVar(envVars.IdpTeacherClientId),
scope: SCOPE,
responseType: RESPONSE_TYPE,
},

View file

@ -0,0 +1,66 @@
import { Request, Response } from 'express';
import { createClass, getAllClasses, getClass, getClassStudents, getClassStudentsIds, getClassTeacherInvitations } from '../services/classes.js';
import { ClassDTO } from '../interfaces/class.js';
export async function getAllClassesHandler(req: Request, res: Response): Promise<void> {
const full = req.query.full === 'true';
const classes = await getAllClasses(full);
res.json({
classes: classes,
});
}
export async function createClassHandler(req: Request, res: Response): Promise<void> {
const classData = req.body as ClassDTO;
if (!classData.displayName) {
res.status(400).json({
error: 'Missing one or more required fields: displayName',
});
return;
}
const cls = await createClass(classData);
if (!cls) {
res.status(500).json({ error: 'Something went wrong while creating class' });
return;
}
res.status(201).json(cls);
}
export async function getClassHandler(req: Request, res: Response): Promise<void> {
const classId = req.params.id;
const cls = await getClass(classId);
if (!cls) {
res.status(404).json({ error: 'Class not found' });
return;
}
res.json(cls);
}
export async function getClassStudentsHandler(req: Request, res: Response): Promise<void> {
const classId = req.params.id;
const full = req.query.full === 'true';
const students = full ? await getClassStudents(classId) : await getClassStudentsIds(classId);
res.json({
students: students,
});
}
export async function getTeacherInvitationsHandler(req: Request, res: Response): Promise<void> {
const classId = req.params.id;
const full = req.query.full === 'true';
const invitations = await getClassTeacherInvitations(classId, full);
res.json({
invitations: invitations,
});
}

View file

@ -0,0 +1,100 @@
import { Request, Response } from 'express';
import { createGroup, getAllGroups, getGroup, getGroupSubmissions } from '../services/groups.js';
import { GroupDTO } from '../interfaces/group.js';
// Typescript is annoywith with parameter forwarding from class.ts
interface GroupParams {
classid: string;
assignmentid: string;
groupid?: string;
}
export async function getGroupHandler(req: Request<GroupParams>, res: Response): Promise<void> {
const classId = req.params.classid;
const full = req.query.full === 'true';
const assignmentId = Number(req.params.assignmentid);
if (isNaN(assignmentId)) {
res.status(400).json({ error: 'Assignment id must be a number' });
return;
}
const groupId = Number(req.params.groupid!); // Can't be undefined
if (isNaN(groupId)) {
res.status(400).json({ error: 'Group id must be a number' });
return;
}
const group = await getGroup(classId, assignmentId, groupId, full);
if (!group) {
res.status(404).json({ error: 'Group not found' });
return;
}
res.json(group);
}
export async function getAllGroupsHandler(req: Request, res: Response): Promise<void> {
const classId = req.params.classid;
const full = req.query.full === 'true';
const assignmentId = Number(req.params.assignmentid);
if (isNaN(assignmentId)) {
res.status(400).json({ error: 'Assignment id must be a number' });
return;
}
const groups = await getAllGroups(classId, assignmentId, full);
res.json({
groups: groups,
});
}
export async function createGroupHandler(req: Request, res: Response): Promise<void> {
const classid = req.params.classid;
const assignmentId = Number(req.params.assignmentid);
if (isNaN(assignmentId)) {
res.status(400).json({ error: 'Assignment id must be a number' });
return;
}
const groupData = req.body as GroupDTO;
const group = await createGroup(groupData, classid, assignmentId);
if (!group) {
res.status(500).json({ error: 'Something went wrong while creating group' });
return;
}
res.status(201).json(group);
}
export async function getGroupSubmissionsHandler(req: Request, res: Response): Promise<void> {
const classId = req.params.classid;
const full = req.query.full === 'true';
const assignmentId = Number(req.params.assignmentid);
if (isNaN(assignmentId)) {
res.status(400).json({ error: 'Assignment id must be a number' });
return;
}
const groupId = Number(req.params.groupid); // Can't be undefined
if (isNaN(groupId)) {
res.status(400).json({ error: 'Group id must be a number' });
return;
}
const submissions = await getGroupSubmissions(classId, assignmentId, groupId, full);
res.json({
submissions: submissions,
});
}

View file

@ -2,19 +2,19 @@ import { Request, Response } from 'express';
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 { EnvVars, getEnvVar } from '../util/envvars.js';
import { envVars, getEnvVar } from '../util/envVars.js';
import { Language } from '../entities/content/language.js';
import { BadRequestException } from '../exceptions.js';
import attachmentService from '../services/learning-objects/attachment-service.js';
import { NotFoundError } from '@mikro-orm/core';
import { BadRequestException } from '../exceptions/bad-request-exception.js';
function getLearningObjectIdentifierFromRequest(req: Request): LearningObjectIdentifier {
if (!req.params.hruid) {
throw new BadRequestException('HRUID is required.');
}
return {
hruid: req.params.hruid as string,
language: (req.query.language || getEnvVar(EnvVars.FallbackLanguage)) as Language,
hruid: req.params.hruid,
language: (req.query.language || getEnvVar(envVars.FallbackLanguage)) as Language,
version: parseInt(req.query.version as string),
};
}
@ -24,7 +24,7 @@ function getLearningPathIdentifierFromRequest(req: Request): LearningPathIdentif
throw new BadRequestException('HRUID is required.');
}
return {
hruid: req.params.hruid as string,
hruid: req.params.hruid,
language: (req.query.language as Language) || FALLBACK_LANG,
};
}
@ -40,7 +40,7 @@ export async function getAllLearningObjects(req: Request, res: Response): Promis
learningObjects = await learningObjectService.getLearningObjectIdsFromPath(learningPathId);
}
res.json(learningObjects);
res.json({ learningObjects: learningObjects });
}
export async function getLearningObject(req: Request, res: Response): Promise<void> {

View file

@ -2,13 +2,14 @@ import { Request, Response } from 'express';
import { themes } from '../data/themes.js';
import { FALLBACK_LANG } from '../config.js';
import learningPathService from '../services/learning-paths/learning-path-service.js';
import { BadRequestException, NotFoundException } from '../exceptions.js';
import { Language } from '../entities/content/language.js';
import {
PersonalizationTarget,
personalizedForGroup,
personalizedForStudent,
} 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.

View file

@ -1,45 +0,0 @@
import { Request, Response } from 'express';
import { themes } from '../data/themes.js';
import { FALLBACK_LANG } from '../config.js';
import { getLogger } from '../logging/initalize.js';
import learningPathService from '../services/learning-paths/learning-path-service.js';
import { Language } from '../entities/content/language.js';
/**
* Fetch learning paths based on query parameters.
*/
export async function getLearningPaths(req: Request, res: Response): Promise<void> {
try {
const hruids = req.query.hruid;
const themeKey = req.query.theme as string;
const searchQuery = req.query.search as string;
const language = (req.query.language as Language) || FALLBACK_LANG;
let hruidList;
if (hruids) {
hruidList = Array.isArray(hruids) ? hruids.map(String) : [String(hruids)];
} else if (themeKey) {
const theme = themes.find((t) => t.title === themeKey);
if (theme) {
hruidList = theme.hruids;
} else {
res.status(404).json({
error: `Theme "${themeKey}" not found.`,
});
return;
}
} else if (searchQuery) {
const searchResults = await learningPathService.searchLearningPaths(searchQuery, language);
res.json(searchResults);
return;
} else {
hruidList = themes.flatMap((theme) => theme.hruids);
}
const learningPaths = await learningPathService.fetchLearningPaths(hruidList, language, `HRUIDs: ${hruidList.join(', ')}`);
res.json(learningPaths.data);
} catch (error) {
getLogger().error('❌ Unexpected error fetching learning paths:', error);
res.status(500).json({ error: 'Internal server error' });
}
}

View file

@ -0,0 +1,119 @@
import { Request, Response } from 'express';
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 { LearningObjectIdentifier } from '../entities/content/learning-object-identifier.js';
import { Language } from '../entities/content/language.js';
function getObjectId(req: Request, res: Response): LearningObjectIdentifier | null {
const { hruid, version } = req.params;
const lang = req.query.lang;
if (!hruid || !version) {
res.status(400).json({ error: 'Missing required parameters.' });
return null;
}
return {
hruid,
language: (lang as Language) || FALLBACK_LANG,
version: Number(version),
};
}
function getQuestionId(req: Request, res: Response): QuestionId | null {
const seq = req.params.seq;
const learningObjectIdentifier = getObjectId(req, res);
if (!learningObjectIdentifier) {
return null;
}
return {
learningObjectIdentifier,
sequenceNumber: seq ? Number(seq) : FALLBACK_SEQ_NUM,
};
}
export async function getAllQuestionsHandler(req: Request, res: Response): Promise<void> {
const objectId = getObjectId(req, res);
const full = req.query.full === 'true';
if (!objectId) {
return;
}
const questions = await getAllQuestions(objectId, full);
if (!questions) {
res.status(404).json({ error: `Questions not found.` });
} else {
res.json({ questions: questions });
}
}
export async function getQuestionHandler(req: Request, res: Response): Promise<void> {
const questionId = getQuestionId(req, res);
if (!questionId) {
return;
}
const question = await getQuestion(questionId);
if (!question) {
res.status(404).json({ error: `Question not found.` });
} else {
res.json(question);
}
}
export async function getQuestionAnswersHandler(req: Request, res: Response): Promise<void> {
const questionId = getQuestionId(req, res);
const full = req.query.full === 'true';
if (!questionId) {
return;
}
const answers = await getAnswersByQuestion(questionId, full);
if (!answers) {
res.status(404).json({ error: `Questions not found` });
} else {
res.json({ answers: answers });
}
}
export async function createQuestionHandler(req: Request, res: Response): Promise<void> {
const questionDTO = req.body as QuestionDTO;
if (!questionDTO.learningObjectIdentifier || !questionDTO.author || !questionDTO.content) {
res.status(400).json({ error: 'Missing required fields: identifier and content' });
return;
}
const question = await createQuestion(questionDTO);
if (!question) {
res.status(400).json({ error: 'Could not create question' });
} else {
res.json(question);
}
}
export async function deleteQuestionHandler(req: Request, res: Response): Promise<void> {
const questionId = getQuestionId(req, res);
if (!questionId) {
return;
}
const question = await deleteQuestion(questionId);
if (!question) {
res.status(400).json({ error: 'Could not find nor delete question' });
} else {
res.json(question);
}
}

View file

@ -0,0 +1,134 @@
import { Request, Response } from 'express';
import {
createStudent,
deleteStudent,
getAllStudents,
getStudent,
getStudentAssignments,
getStudentClasses,
getStudentGroups,
getStudentSubmissions,
} from '../services/students.js';
import { StudentDTO } from '../interfaces/student.js';
// TODO: accept arguments (full, ...)
// TODO: endpoints
export async function getAllStudentsHandler(req: Request, res: Response): Promise<void> {
const full = req.query.full === 'true';
const students = await getAllStudents(full);
if (!students) {
res.status(404).json({ error: `Student not found.` });
return;
}
res.json({ students: students });
}
export async function getStudentHandler(req: Request, res: Response): Promise<void> {
const username = req.params.username;
if (!username) {
res.status(400).json({ error: 'Missing required field: username' });
return;
}
const user = await getStudent(username);
if (!user) {
res.status(404).json({
error: `User with username '${username}' not found.`,
});
return;
}
res.json(user);
}
export async function createStudentHandler(req: Request, res: Response): Promise<void> {
const userData = req.body as StudentDTO;
if (!userData.username || !userData.firstName || !userData.lastName) {
res.status(400).json({
error: 'Missing required fields: username, firstName, lastName',
});
return;
}
const newUser = await createStudent(userData);
if (!newUser) {
res.status(500).json({
error: 'Something went wrong while creating student',
});
return;
}
res.status(201).json(newUser);
}
export async function deleteStudentHandler(req: Request, res: Response): Promise<void> {
const username = req.params.username;
if (!username) {
res.status(400).json({ error: 'Missing required field: username' });
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> {
const full = req.query.full === 'true';
const username = req.params.id;
const classes = await getStudentClasses(username, full);
res.json({ classes: classes });
}
// TODO
// Might not be fully correct depending on if
// A class has an assignment, that all students
// Have this assignment.
export async function getStudentAssignmentsHandler(req: Request, res: Response): Promise<void> {
const full = req.query.full === 'true';
const username = req.params.id;
const assignments = getStudentAssignments(username, full);
res.json({
assignments: assignments,
});
}
export async function getStudentGroupsHandler(req: Request, res: Response): Promise<void> {
const full = req.query.full === 'true';
const username = req.params.id;
const groups = await getStudentGroups(username, full);
res.json({
groups: groups,
});
}
export async function getStudentSubmissionsHandler(req: Request, res: Response): Promise<void> {
const username = req.params.id;
const full = req.query.full === 'true';
const submissions = await getStudentSubmissions(username, full);
res.json({
submissions: submissions,
});
}

View file

@ -0,0 +1,61 @@
import { Request, Response } from 'express';
import { createSubmission, deleteSubmission, getSubmission } from '../services/submissions.js';
import { Language, languageMap } from '../entities/content/language.js';
import { SubmissionDTO } from '../interfaces/submission';
interface SubmissionParams {
hruid: string;
id: number;
}
export async function getSubmissionHandler(req: Request<SubmissionParams>, res: Response): Promise<void> {
const lohruid = req.params.hruid;
const submissionNumber = Number(req.params.id);
if (isNaN(submissionNumber)) {
res.status(400).json({ error: 'Submission number is not a number' });
return;
}
const lang = languageMap[req.query.language as string] || Language.Dutch;
const version = (req.query.version || 1) as number;
const submission = await getSubmission(lohruid, lang, version, submissionNumber);
if (!submission) {
res.status(404).json({ error: 'Submission not found' });
return;
}
res.json(submission);
}
export async function createSubmissionHandler(req: Request, res: Response): Promise<void> {
const submissionDTO = req.body as SubmissionDTO;
const submission = await createSubmission(submissionDTO);
if (!submission) {
res.status(400).json({ error: 'Failed to create submission' });
return;
}
res.json(submission);
}
export async function deleteSubmissionHandler(req: Request, res: Response): Promise<void> {
const hruid = req.params.hruid;
const submissionNumber = Number(req.params.id);
const lang = languageMap[req.query.language as string] || Language.Dutch;
const version = (req.query.version || 1) as number;
const submission = await deleteSubmission(hruid, lang, version, submissionNumber);
if (!submission) {
res.status(404).json({ error: 'Submission not found' });
return;
}
res.json(submission);
}

View file

@ -0,0 +1,140 @@
import { Request, Response } from 'express';
import {
createTeacher,
deleteTeacher,
getAllTeachers,
getClassesByTeacher,
getQuestionsByTeacher,
getStudentsByTeacher,
getTeacher,
} from '../services/teachers.js';
import { TeacherDTO } from '../interfaces/teacher.js';
export async function getAllTeachersHandler(req: Request, res: Response): Promise<void> {
const full = req.query.full === 'true';
const teachers = await getAllTeachers(full);
if (!teachers) {
res.status(404).json({ error: `Teacher not found.` });
return;
}
res.json({ teachers: teachers });
}
export async function getTeacherHandler(req: Request, res: Response): Promise<void> {
const username = req.params.username;
if (!username) {
res.status(400).json({ error: 'Missing required field: username' });
return;
}
const user = await getTeacher(username);
if (!user) {
res.status(404).json({
error: `Teacher '${username}' not found.`,
});
return;
}
res.json(user);
}
export async function createTeacherHandler(req: Request, res: Response): Promise<void> {
const userData = req.body as TeacherDTO;
if (!userData.username || !userData.firstName || !userData.lastName) {
res.status(400).json({
error: 'Missing required fields: username, firstName, lastName',
});
return;
}
const newUser = await createTeacher(userData);
if (!newUser) {
res.status(400).json({ error: 'Failed to create teacher' });
return;
}
res.status(201).json(newUser);
}
export async function deleteTeacherHandler(req: Request, res: Response): Promise<void> {
const username = req.params.username;
if (!username) {
res.status(400).json({ error: 'Missing required field: username' });
return;
}
const deletedUser = await deleteTeacher(username);
if (!deletedUser) {
res.status(404).json({
error: `User '${username}' not found.`,
});
return;
}
res.status(200).json(deletedUser);
}
export async function getTeacherClassHandler(req: Request, res: Response): Promise<void> {
const username = req.params.username;
const full = req.query.full === 'true';
if (!username) {
res.status(400).json({ error: 'Missing required field: username' });
return;
}
const classes = await getClassesByTeacher(username, full);
if (!classes) {
res.status(404).json({ error: 'Teacher not found' });
return;
}
res.json({ classes: classes });
}
export async function getTeacherStudentHandler(req: Request, res: Response): Promise<void> {
const username = req.params.username;
const full = req.query.full === 'true';
if (!username) {
res.status(400).json({ error: 'Missing required field: username' });
return;
}
const students = await getStudentsByTeacher(username, full);
if (!students) {
res.status(404).json({ error: 'Teacher not found' });
return;
}
res.json({ students: students });
}
export async function getTeacherQuestionHandler(req: Request, res: Response): Promise<void> {
const username = req.params.username;
const full = req.query.full === 'true';
if (!username) {
res.status(400).json({ error: 'Missing required field: username' });
return;
}
const questions = await getQuestionsByTeacher(username, full);
if (!questions) {
res.status(404).json({ error: 'Teacher not found' });
return;
}
res.json({ questions: questions });
}

View file

@ -1,28 +1,32 @@
import { Request, Response } from 'express';
import { themes } from '../data/themes.js';
import { loadTranslations } from '../util/translationHelper.js';
import { loadTranslations } from '../util/translation-helper.js';
interface Translations {
curricula_page: {
[key: string]: { title: string; description?: string };
};
curricula_page: Record<string, { title: string; description?: string }>;
}
export function getThemes(req: Request, res: Response) {
const language = (req.query.language as string)?.toLowerCase() || 'nl';
export function getThemesHandler(req: Request, res: Response): void {
const language = ((req.query.language as string) || 'nl').toLowerCase();
const translations = loadTranslations<Translations>(language);
const themeList = themes.map((theme) => ({
key: theme.title,
title: translations.curricula_page[theme.title]?.title || theme.title,
description: translations.curricula_page[theme.title]?.description,
title: translations.curricula_page[theme.title].title || theme.title,
description: translations.curricula_page[theme.title].description,
image: `https://dwengo.org/images/curricula/logo_${theme.title}.png`,
}));
res.json(themeList);
}
export function getThemeByTitle(req: Request, res: Response) {
export function getHruidsByThemeHandler(req: Request, res: Response): void {
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);
if (theme) {

View file

@ -3,13 +3,13 @@ import { Assignment } from '../../entities/assignments/assignment.entity.js';
import { Class } from '../../entities/classes/class.entity.js';
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 });
}
public findAllAssignmentsInClass(within: Class): Promise<Assignment[]> {
public async findAllAssignmentsInClass(within: Class): Promise<Assignment[]> {
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 });
}
}

View file

@ -1,18 +1,28 @@
import { DwengoEntityRepository } from '../dwengo-entity-repository.js';
import { Group } from '../../entities/assignments/group.entity.js';
import { Assignment } from '../../entities/assignments/assignment.entity.js';
import { Student } from '../../entities/users/student.entity.js';
export class GroupRepository extends DwengoEntityRepository<Group> {
public findByAssignmentAndGroupNumber(assignment: Assignment, groupNumber: number): Promise<Group | null> {
return this.findOne({
assignment: assignment,
groupNumber: groupNumber,
public async findByAssignmentAndGroupNumber(assignment: Assignment, groupNumber: number): Promise<Group | null> {
return this.findOne(
{
assignment: assignment,
groupNumber: groupNumber,
},
{ populate: ['members'] }
);
}
public async findAllGroupsForAssignment(assignment: Assignment): Promise<Group[]> {
return this.findAll({
where: { assignment: assignment },
populate: ['members'],
});
}
public findAllGroupsForAssignment(assignment: Assignment): Promise<Group[]> {
return this.findAll({ where: { assignment: assignment } });
public async findAllGroupsWithStudent(student: Student): Promise<Group[]> {
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({
assignment: assignment,
groupNumber: groupNumber,

View file

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

View file

@ -4,13 +4,13 @@ import { ClassJoinRequest } from '../../entities/classes/class-join-request.enti
import { Student } from '../../entities/users/student.entity.js';
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 } });
}
public findAllOpenRequestsTo(clazz: Class): Promise<ClassJoinRequest[]> {
public async findAllOpenRequestsTo(clazz: Class): Promise<ClassJoinRequest[]> {
return this.findAll({ where: { class: clazz } });
}
public deleteBy(requester: Student, clazz: Class): Promise<void> {
public async deleteBy(requester: Student, clazz: Class): Promise<void> {
return this.deleteWhere({ requester: requester, class: clazz });
}
}

View file

@ -1,11 +1,23 @@
import { DwengoEntityRepository } from '../dwengo-entity-repository.js';
import { Class } from '../../entities/classes/class.entity.js';
import { Student } from '../../entities/users/student.entity.js';
import { Teacher } from '../../entities/users/teacher.entity';
export class ClassRepository extends DwengoEntityRepository<Class> {
public findById(id: string): Promise<Class | null> {
return this.findOne({ classId: id });
public async findById(id: string): Promise<Class | null> {
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 });
}
public async findByStudent(student: Student): Promise<Class[]> {
return this.find(
{ students: student },
{ populate: ['students', 'teachers'] } // Voegt student en teacher objecten toe
);
}
public async findByTeacher(teacher: Teacher): Promise<Class[]> {
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';
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 } });
}
public findAllInvitationsBy(sender: Teacher): Promise<TeacherInvitation[]> {
public async findAllInvitationsBy(sender: Teacher): Promise<TeacherInvitation[]> {
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 } });
}
public deleteBy(clazz: Class, sender: Teacher, receiver: Teacher): Promise<void> {
public async deleteBy(clazz: Class, sender: Teacher, receiver: Teacher): Promise<void> {
return this.deleteWhere({
sender: sender,
receiver: receiver,

View file

@ -4,7 +4,7 @@ import { Language } from '../../entities/content/language';
import { LearningObjectIdentifier } from '../../entities/content/learning-object-identifier';
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({
learningObject: {
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(
{
learningObject: {

View file

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

View file

@ -3,7 +3,7 @@ import { LearningPath } from '../../entities/content/learning-path.entity.js';
import { Language } from '../../entities/content/language.js';
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'] });
}

View file

@ -1,12 +1,14 @@
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> {
public async save(entity: T) {
const em = this.getEntityManager();
em.persist(entity);
await em.flush();
public async save(entity: T, options?: { preventOverwrite?: boolean }): Promise<void> {
if (options?.preventOverwrite && (await this.findOne(entity))) {
throw new EntityAlreadyExistsException(`A ${this.getEntityName()} with this identifier already exists.`);
}
await this.getEntityManager().persistAndFlush(entity);
}
public async deleteWhere(query: FilterQuery<T>) {
public async deleteWhere(query: FilterQuery<T>): Promise<void> {
const toDelete = await this.findOne(query);
const em = this.getEntityManager();
if (toDelete) {

View file

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

View file

@ -2,9 +2,10 @@ import { DwengoEntityRepository } from '../dwengo-entity-repository.js';
import { Question } from '../../entities/questions/question.entity.js';
import { LearningObjectIdentifier } from '../../entities/content/learning-object-identifier.js';
import { Student } from '../../entities/users/student.entity.js';
import { LearningObject } from '../../entities/content/learning-object.entity.js';
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({
learningObjectHruid: question.loId.hruid,
learningObjectLanguage: question.loId.language,
@ -20,7 +21,7 @@ export class QuestionRepository extends DwengoEntityRepository<Question> {
questionEntity.content = question.content;
return this.insert(questionEntity);
}
public findAllQuestionsAboutLearningObject(loId: LearningObjectIdentifier): Promise<Question[]> {
public async findAllQuestionsAboutLearningObject(loId: LearningObjectIdentifier): Promise<Question[]> {
return this.findAll({
where: {
learningObjectHruid: loId.hruid,
@ -32,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({
learningObjectHruid: loId.hruid,
learningObjectLanguage: loId.language,
@ -40,4 +41,17 @@ export class QuestionRepository extends DwengoEntityRepository<Question> {
sequenceNumber: sequenceNumber,
});
}
public async findAllByLearningObjects(learningObjects: LearningObject[]): Promise<Question[]> {
const objectIdentifiers = learningObjects.map((lo) => ({
learningObjectHruid: lo.hruid,
learningObjectLanguage: lo.language,
learningObjectVersion: lo.version,
}));
return this.findAll({
where: { $or: objectIdentifiers },
orderBy: { timestamp: 'ASC' },
});
}
}

View file

@ -2,8 +2,6 @@ import { AnyEntity, EntityManager, EntityName, EntityRepository } from '@mikro-o
import { forkEntityManager } from '../orm.js';
import { StudentRepository } from './users/student-repository.js';
import { Student } from '../entities/users/student.entity.js';
import { User } from '../entities/users/user.entity.js';
import { UserRepository } from './users/user-repository.js';
import { Teacher } from '../entities/users/teacher.entity.js';
import { TeacherRepository } from './users/teacher-repository.js';
import { Class } from '../entities/classes/class.entity.js';
@ -36,8 +34,8 @@ let entityManager: EntityManager | undefined;
/**
* Execute all the database operations within the function f in a single transaction.
*/
export function transactional<T>(f: () => Promise<T>) {
entityManager?.transactional(f);
export async function transactional<T>(f: () => Promise<T>): Promise<void> {
await entityManager?.transactional(f);
}
function repositoryGetter<T extends AnyEntity, R extends EntityRepository<T>>(entity: EntityName<T>): () => R {
@ -54,7 +52,6 @@ function repositoryGetter<T extends AnyEntity, R extends EntityRepository<T>>(en
}
/* Users */
export const getUserRepository = repositoryGetter<User, UserRepository>(User);
export const getStudentRepository = repositoryGetter<Student, StudentRepository>(Student);
export const getTeacherRepository = repositoryGetter<Teacher, TeacherRepository>(Teacher);

View file

@ -1,11 +1,11 @@
import { DwengoEntityRepository } from '../dwengo-entity-repository.js';
import { Student } from '../../entities/users/student.entity.js';
import { DwengoEntityRepository } from '../dwengo-entity-repository.js';
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 });
}
public deleteByUsername(username: string): Promise<void> {
public async deleteByUsername(username: string): Promise<void> {
return this.deleteWhere({ username: username });
}
}

View file

@ -1,11 +1,11 @@
import { DwengoEntityRepository } from '../dwengo-entity-repository.js';
import { Teacher } from '../../entities/users/teacher.entity.js';
import { DwengoEntityRepository } from '../dwengo-entity-repository.js';
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 });
}
public deleteByUsername(username: string): Promise<void> {
public async deleteByUsername(username: string): Promise<void> {
return this.deleteWhere({ username: username });
}
}

View file

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

View file

@ -4,13 +4,18 @@ import { Group } from './group.entity.js';
import { Language } from '../content/language.js';
import { AssignmentRepository } from '../../data/assignments/assignment-repository.js';
@Entity({ repository: () => AssignmentRepository })
@Entity({
repository: () => AssignmentRepository,
})
export class Assignment {
@ManyToOne({ entity: () => Class, primary: true })
@ManyToOne({
entity: () => Class,
primary: true,
})
within!: Class;
@PrimaryKey({ type: 'number' })
id!: number;
@PrimaryKey({ type: 'number', autoincrement: true })
id?: number;
@Property({ type: 'string' })
title!: string;
@ -21,9 +26,14 @@ export class Assignment {
@Property({ type: 'string' })
learningPathHruid!: string;
@Enum({ items: () => Language })
@Enum({
items: () => Language,
})
learningPathLanguage!: Language;
@OneToMany({ entity: () => Group, mappedBy: 'assignment' })
@OneToMany({
entity: () => Group,
mappedBy: 'assignment',
})
groups!: Group[];
}

View file

@ -3,7 +3,9 @@ import { Assignment } from './assignment.entity.js';
import { Student } from '../users/student.entity.js';
import { GroupRepository } from '../../data/assignments/group-repository.js';
@Entity({ repository: () => GroupRepository })
@Entity({
repository: () => GroupRepository,
})
export class Group {
@ManyToOne({
entity: () => Assignment,
@ -11,8 +13,8 @@ export class Group {
})
assignment!: Assignment;
@PrimaryKey({ type: 'integer' })
groupNumber!: number;
@PrimaryKey({ type: 'integer', autoincrement: true })
groupNumber?: number;
@ManyToMany({
entity: () => Student,

View file

@ -16,10 +16,10 @@ export class Submission {
learningObjectLanguage!: Language;
@PrimaryKey({ type: 'numeric' })
learningObjectVersion: number = 1;
learningObjectVersion = 1;
@PrimaryKey({ type: 'integer' })
submissionNumber!: number;
@PrimaryKey({ type: 'integer', autoincrement: true })
submissionNumber?: number;
@ManyToOne({
entity: () => Student,

View file

@ -3,7 +3,15 @@ import { Student } from '../users/student.entity.js';
import { Class } from './class.entity.js';
import { ClassJoinRequestRepository } from '../../data/classes/class-join-request-repository.js';
@Entity({ repository: () => ClassJoinRequestRepository })
export enum ClassJoinRequestStatus {
Open = 'open',
Accepted = 'accepted',
Declined = 'declined',
}
@Entity({
repository: () => ClassJoinRequestRepository,
})
export class ClassJoinRequest {
@ManyToOne({
entity: () => Student,
@ -20,9 +28,3 @@ export class ClassJoinRequest {
@Enum(() => ClassJoinRequestStatus)
status!: ClassJoinRequestStatus;
}
export enum ClassJoinRequestStatus {
Open = 'open',
Accepted = 'accepted',
Declined = 'declined',
}

View file

@ -4,10 +4,12 @@ import { Teacher } from '../users/teacher.entity.js';
import { Student } from '../users/student.entity.js';
import { ClassRepository } from '../../data/classes/class-repository.js';
@Entity({ repository: () => ClassRepository })
@Entity({
repository: () => ClassRepository,
})
export class Class {
@PrimaryKey()
classId = v4();
classId? = v4();
@Property({ type: 'string' })
displayName!: string;

View file

@ -7,9 +7,6 @@ import { TeacherInvitationRepository } from '../../data/classes/teacher-invitati
* Invitation of a teacher into a class (in order to teach it).
*/
@Entity({ repository: () => TeacherInvitationRepository })
@Entity({
repository: () => TeacherInvitationRepository,
})
export class TeacherInvitation {
@ManyToOne({
entity: () => Teacher,

View file

@ -2,7 +2,9 @@ import { Entity, ManyToOne, PrimaryKey, Property } from '@mikro-orm/core';
import { LearningObject } from './learning-object.entity.js';
import { AttachmentRepository } from '../../data/content/attachment-repository.js';
@Entity({ repository: () => AttachmentRepository })
@Entity({
repository: () => AttachmentRepository,
})
export class Attachment {
@ManyToOne({
entity: () => LearningObject,

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

@ -184,3 +184,10 @@ export enum Language {
Zhuang = 'za',
Zulu = 'zu',
}
export const languageMap: Record<string, Language> = {
nl: Language.Dutch,
fr: Language.French,
en: Language.English,
de: Language.German,
};

View file

@ -5,5 +5,7 @@ export class LearningObjectIdentifier {
public hruid: string,
public language: Language,
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 { Teacher } from '../users/teacher.entity.js';
import { DwengoContentType } from '../../services/learning-objects/processing/content-type.js';
import { v4 } from 'uuid';
import { LearningObjectRepository } from '../../data/content/learning-object-repository.js';
@Embeddable()
export class EducationalGoal {
@Property({ type: 'string' })
source!: string;
@Property({ type: 'string' })
id!: string;
}
@Embeddable()
export class ReturnValue {
@Property({ type: 'string' })
callbackUrl!: string;
@Property({ type: 'json' })
callbackSchema!: string;
}
import { EducationalGoal } from './educational-goal.entity.js';
import { ReturnValue } from './return-value.entity.js';
@Entity({ repository: () => LearningObjectRepository })
export class LearningObject {
@ -36,7 +20,7 @@ export class LearningObject {
language!: Language;
@PrimaryKey({ type: 'number' })
version: number = 1;
version = 1;
@Property({ type: 'uuid', unique: true })
uuid = v4();
@ -62,7 +46,7 @@ export class LearningObject {
targetAges?: number[] = [];
@Property({ type: 'bool' })
teacherExclusive: boolean = false;
teacherExclusive = false;
@Property({ type: 'array' })
skosConcepts: string[] = [];
@ -74,10 +58,10 @@ export class LearningObject {
educationalGoals: EducationalGoal[] = [];
@Property({ type: 'string' })
copyright: string = '';
copyright = '';
@Property({ type: 'string' })
license: string = '';
license = '';
@Property({ type: 'smallint', nullable: true })
difficulty?: number;
@ -91,7 +75,7 @@ export class LearningObject {
returnValue!: ReturnValue;
@Property({ type: 'bool' })
available: boolean = true;
available = true;
@Property({ type: 'string', nullable: true })
contentLocation?: string;

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

@ -15,7 +15,7 @@ export class Question {
learningObjectLanguage!: Language;
@PrimaryKey({ type: 'number' })
learningObjectVersion: number = 1;
learningObjectVersion = 1;
@PrimaryKey({ type: 'integer', autoincrement: true })
sequenceNumber?: number;

View file

@ -13,12 +13,4 @@ export class Student extends User {
@ManyToMany(() => 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 {
@ManyToMany(() => 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;
@Property()
firstName: string = '';
firstName = '';
@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

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

View file

@ -0,0 +1,53 @@
import { FALLBACK_LANG } from '../config.js';
import { Assignment } from '../entities/assignments/assignment.entity.js';
import { Class } from '../entities/classes/class.entity.js';
import { languageMap } from '../entities/content/language.js';
import { GroupDTO } from './group.js';
import { getLogger } from '../logging/initalize.js';
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 {
return {
id: assignment.id!,
class: assignment.within.classId!,
title: assignment.title,
description: assignment.description,
learningPath: assignment.learningPathHruid,
language: assignment.learningPathLanguage,
// Groups: assignment.groups.map(group => group.groupNumber),
};
}
export function mapToAssignmentDTO(assignment: Assignment): AssignmentDTO {
return {
id: assignment.id!,
class: assignment.within.classId!,
title: assignment.title,
description: assignment.description,
learningPath: assignment.learningPathHruid,
language: assignment.learningPathLanguage,
// Groups: assignment.groups.map(mapToGroupDTO),
};
}
export function mapToAssignment(assignmentData: AssignmentDTO, cls: Class): Assignment {
const assignment = new Assignment();
assignment.title = assignmentData.title;
assignment.description = assignmentData.description;
assignment.learningPathHruid = assignmentData.learningPath;
assignment.learningPathLanguage = languageMap[assignmentData.language] || FALLBACK_LANG;
assignment.within = cls;
getLogger().debug(assignment);
return assignment;
}

View file

@ -0,0 +1,31 @@
import { Collection } from '@mikro-orm/core';
import { Class } from '../entities/classes/class.entity.js';
import { Student } from '../entities/users/student.entity.js';
import { Teacher } from '../entities/users/teacher.entity.js';
export interface ClassDTO {
id: string;
displayName: string;
teachers: string[];
students: string[];
joinRequests: string[];
}
export function mapToClassDTO(cls: Class): ClassDTO {
return {
id: cls.classId!,
displayName: cls.displayName,
teachers: cls.teachers.map((teacher) => teacher.username),
students: cls.students.map((student) => student.username),
joinRequests: [], // TODO
};
}
export function mapToClass(classData: ClassDTO, students: Collection<Student>, teachers: Collection<Teacher>): Class {
const cls = new Class();
cls.displayName = classData.displayName;
cls.students = students;
cls.teachers = teachers;
return cls;
}

View file

@ -0,0 +1,25 @@
import { Group } from '../entities/assignments/group.entity.js';
import { AssignmentDTO, mapToAssignmentDTO } from './assignment.js';
import { mapToStudentDTO, StudentDTO } from './student.js';
export interface GroupDTO {
assignment: number | AssignmentDTO;
groupNumber: number;
members: string[] | StudentDTO[];
}
export function mapToGroupDTO(group: Group): GroupDTO {
return {
assignment: mapToAssignmentDTO(group.assignment), // ERROR: , group.assignment.within),
groupNumber: group.groupNumber!,
members: group.members.map(mapToStudentDTO),
};
}
export function mapToGroupDTOId(group: Group): GroupDTO {
return {
assignment: group.assignment.id!,
groupNumber: group.groupNumber!,
members: group.members.map((member) => member.username),
};
}

View file

@ -58,7 +58,7 @@ export interface EducationalGoal {
export interface ReturnValue {
callback_url: string;
callback_schema: Record<string, any>;
callback_schema: Record<string, unknown>;
}
export interface LearningObjectMetadata {

View file

@ -0,0 +1,5 @@
// TODO: implement something like this but with named endpoints
export interface List<T> {
items: T[];
endpoints?: string[];
}

View file

@ -0,0 +1,42 @@
import { Question } from '../entities/questions/question.entity.js';
import { LearningObjectIdentifier } from '../entities/content/learning-object-identifier.js';
import { mapToStudentDTO, StudentDTO } from './student.js';
export interface QuestionDTO {
learningObjectIdentifier: LearningObjectIdentifier;
sequenceNumber?: number;
author: StudentDTO;
timestamp?: string;
content: string;
}
/**
* Convert a Question entity to a DTO format.
*/
export function mapToQuestionDTO(question: Question): QuestionDTO {
const learningObjectIdentifier = {
hruid: question.learningObjectHruid,
language: question.learningObjectLanguage,
version: question.learningObjectVersion,
};
return {
learningObjectIdentifier,
sequenceNumber: question.sequenceNumber!,
author: mapToStudentDTO(question.author),
timestamp: question.timestamp.toISOString(),
content: question.content,
};
}
export interface QuestionId {
learningObjectIdentifier: LearningObjectIdentifier;
sequenceNumber: number;
}
export function mapToQuestionId(question: QuestionDTO): QuestionId {
return {
learningObjectIdentifier: question.learningObjectIdentifier,
sequenceNumber: question.sequenceNumber!,
};
}

View file

@ -0,0 +1,32 @@
import { Student } from '../entities/users/student.entity.js';
import { getStudentRepository } from '../data/repositories.js';
export interface StudentDTO {
id: string;
username: string;
firstName: string;
lastName: string;
endpoints?: {
classes: string;
questions: string;
invitations: string;
groups: string;
};
}
export function mapToStudentDTO(student: Student): StudentDTO {
return {
id: student.username,
username: student.username,
firstName: student.firstName,
lastName: student.lastName,
};
}
export function mapToStudent(studentData: StudentDTO): Student {
return getStudentRepository().create({
username: studentData.username,
firstName: studentData.firstName,
lastName: studentData.lastName,
});
}

View file

@ -0,0 +1,64 @@
import { Submission } from '../entities/assignments/submission.entity.js';
import { Language } from '../entities/content/language.js';
import { GroupDTO, mapToGroupDTO } from './group.js';
import { mapToStudent, mapToStudentDTO, StudentDTO } from './student.js';
import { LearningObjectIdentifier } from './learning-content.js';
export interface SubmissionDTO {
learningObjectIdentifier: LearningObjectIdentifier;
submissionNumber?: number;
submitter: StudentDTO;
time?: Date;
group?: GroupDTO;
content: string;
}
export interface SubmissionDTOId {
learningObjectHruid: string;
learningObjectLanguage: Language;
learningObjectVersion: number;
submissionNumber?: number;
}
export function mapToSubmissionDTO(submission: Submission): SubmissionDTO {
return {
learningObjectIdentifier: {
hruid: submission.learningObjectHruid,
language: submission.learningObjectLanguage,
version: submission.learningObjectVersion,
},
submissionNumber: submission.submissionNumber,
submitter: mapToStudentDTO(submission.submitter),
time: submission.submissionTime,
group: submission.onBehalfOf ? mapToGroupDTO(submission.onBehalfOf) : undefined,
content: submission.content,
};
}
export function mapToSubmissionDTOId(submission: Submission): SubmissionDTOId {
return {
learningObjectHruid: submission.learningObjectHruid,
learningObjectLanguage: submission.learningObjectLanguage,
learningObjectVersion: submission.learningObjectVersion,
submissionNumber: submission.submissionNumber,
};
}
export function mapToSubmission(submissionDTO: SubmissionDTO): Submission {
const submission = new Submission();
submission.learningObjectHruid = submissionDTO.learningObjectIdentifier.hruid;
submission.learningObjectLanguage = submissionDTO.learningObjectIdentifier.language;
submission.learningObjectVersion = submissionDTO.learningObjectIdentifier.version!;
// Submission.submissionNumber = submissionDTO.submissionNumber;
submission.submitter = mapToStudent(submissionDTO.submitter);
// Submission.submissionTime = submissionDTO.time;
// Submission.onBehalfOf = submissionDTO.group!;
// TODO fix group
submission.content = submissionDTO.content;
return submission;
}

View file

@ -0,0 +1,25 @@
import { TeacherInvitation } from '../entities/classes/teacher-invitation.entity.js';
import { ClassDTO, mapToClassDTO } from './class.js';
import { mapToUserDTO, UserDTO } from './user.js';
export interface TeacherInvitationDTO {
sender: string | UserDTO;
receiver: string | UserDTO;
class: string | ClassDTO;
}
export function mapToTeacherInvitationDTO(invitation: TeacherInvitation): TeacherInvitationDTO {
return {
sender: mapToUserDTO(invitation.sender),
receiver: mapToUserDTO(invitation.receiver),
class: mapToClassDTO(invitation.class),
};
}
export function mapToTeacherInvitationDTOIds(invitation: TeacherInvitation): TeacherInvitationDTO {
return {
sender: invitation.sender.username,
receiver: invitation.receiver.username,
class: invitation.class.classId!,
};
}

View file

@ -0,0 +1,32 @@
import { Teacher } from '../entities/users/teacher.entity.js';
import { getTeacherRepository } from '../data/repositories.js';
export interface TeacherDTO {
id: string;
username: string;
firstName: string;
lastName: string;
endpoints?: {
classes: string;
questions: string;
invitations: string;
groups: string;
};
}
export function mapToTeacherDTO(teacher: Teacher): TeacherDTO {
return {
id: teacher.username,
username: teacher.username,
firstName: teacher.firstName,
lastName: teacher.lastName,
};
}
export function mapToTeacher(teacherData: TeacherDTO): Teacher {
return getTeacherRepository().create({
username: teacherData.username,
firstName: teacherData.firstName,
lastName: teacherData.lastName,
});
}

View file

@ -0,0 +1,30 @@
import { User } from '../entities/users/user.entity.js';
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 {
return {
id: user.username,
username: user.username,
firstName: user.firstName,
lastName: user.lastName,
};
}
export function mapToUser<T extends User>(userData: UserDTO, userInstance: T): T {
userInstance.username = userData.username;
userInstance.firstName = userData.firstName;
userInstance.lastName = userData.lastName;
return userInstance;
}

View file

@ -1,7 +1,7 @@
import { createLogger, format, Logger as WinstonLogger, transports } from 'winston';
import LokiTransport from 'winston-loki';
import { LokiLabels } from 'loki-logger-ts';
import { LOG_LEVEL, LOKI_HOST } from '../config.js';
import { envVars, getEnvVar } from '../util/envVars.js';
export class Logger extends WinstonLogger {
constructor() {
@ -9,7 +9,7 @@ export class Logger extends WinstonLogger {
}
}
const Labels: LokiLabels = {
const lokiLabels: LokiLabels = {
source: 'Dwengo-Backend',
service: 'API',
host: 'localhost',
@ -22,28 +22,38 @@ function initializeLogger(): Logger {
return logger;
}
const logLevel = getEnvVar(envVars.LogLevel);
const consoleTransport = new transports.Console({
level: getEnvVar(envVars.LogLevel),
format: format.combine(format.cli(), format.colorize()),
});
if (getEnvVar(envVars.RunMode) === 'dev') {
return createLogger({
transports: [consoleTransport],
});
}
const lokiHost = getEnvVar(envVars.LokiHost);
const lokiTransport: LokiTransport = new LokiTransport({
host: LOKI_HOST,
labels: Labels,
level: LOG_LEVEL,
host: lokiHost,
labels: lokiLabels,
level: logLevel,
json: true,
format: format.combine(format.timestamp(), format.json()),
onConnectionError: (err) => {
onConnectionError: (err): void => {
// eslint-disable-next-line no-console
console.error(`Connection error: ${err}`);
},
});
const consoleTransport = new transports.Console({
level: LOG_LEVEL,
format: format.combine(format.cli(), format.colorize()),
});
logger = createLogger({
transports: [lokiTransport, consoleTransport],
});
logger.debug(`Logger initialized with level ${LOG_LEVEL}, Loki host ${LOKI_HOST}`);
logger.debug(`Logger initialized with level ${logLevel} to Loki host ${lokiHost}`);
return logger;
}

View file

@ -5,35 +5,54 @@ import { LokiLabels } from 'loki-logger-ts';
export class MikroOrmLogger extends DefaultLogger {
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)) {
return;
}
switch (namespace) {
case 'query':
this.logger.debug(this.createMessage(namespace, message, context));
this.logger.debug(MikroOrmLogger.createMessage(namespace, message, context));
break;
case 'query-params':
// TODO Which log level should this be?
this.logger.info(this.createMessage(namespace, message, context));
this.logger.info(MikroOrmLogger.createMessage(namespace, message, context));
break;
case 'schema':
this.logger.info(this.createMessage(namespace, message, context));
this.logger.info(MikroOrmLogger.createMessage(namespace, message, context));
break;
case 'discovery':
this.logger.debug(this.createMessage(namespace, message, context));
this.logger.debug(MikroOrmLogger.createMessage(namespace, message, context));
break;
case 'info':
this.logger.info(this.createMessage(namespace, message, context));
this.logger.info(MikroOrmLogger.createMessage(namespace, message, context));
break;
case 'deprecated':
this.logger.warn(this.createMessage(namespace, message, context));
this.logger.warn(MikroOrmLogger.createMessage(namespace, message, context));
break;
default:
switch (context?.level) {
case 'info':
this.logger.info(this.createMessage(namespace, message, context));
this.logger.info(MikroOrmLogger.createMessage(namespace, message, context));
break;
case 'warning':
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 { 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 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 * as jwt from 'jsonwebtoken';
import { JwtPayload } from 'jsonwebtoken';
import jwksClient from 'jwks-rsa';
import * as express from 'express';
import * as jwt from 'jsonwebtoken';
import { AuthenticatedRequest } from './authenticated-request.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_RATE_LIMIT = true;
@ -32,12 +33,12 @@ function createJwksClient(uri: string): jwksClient.JwksClient {
const idpConfigs = {
student: {
issuer: getEnvVar(EnvVars.IdpStudentUrl),
jwksClient: createJwksClient(getEnvVar(EnvVars.IdpStudentJwksEndpoint)),
issuer: getEnvVar(envVars.IdpStudentUrl),
jwksClient: createJwksClient(getEnvVar(envVars.IdpStudentJwksEndpoint)),
},
teacher: {
issuer: getEnvVar(EnvVars.IdpTeacherUrl),
jwksClient: createJwksClient(getEnvVar(EnvVars.IdpTeacherJwksEndpoint)),
issuer: getEnvVar(envVars.IdpTeacherUrl),
jwksClient: createJwksClient(getEnvVar(envVars.IdpTeacherJwksEndpoint)),
},
};
@ -63,7 +64,7 @@ const verifyJwtToken = expressjwt({
}
return signingKey.getPublicKey();
},
audience: getEnvVar(EnvVars.IdpAudience),
audience: getEnvVar(envVars.IdpAudience),
algorithms: [JWT_ALGORITHM],
credentialsRequired: false,
requestProperty: REQUEST_PROPERTY_FOR_JWT_PAYLOAD,
@ -74,7 +75,7 @@ const verifyJwtToken = expressjwt({
*/
function getAuthenticationInfo(req: AuthenticatedRequest): AuthenticationInfo | undefined {
if (!req.jwtPayload) {
return;
return undefined;
}
const issuer = req.jwtPayload.iss;
let accountType: 'student' | 'teacher';
@ -84,8 +85,9 @@ function getAuthenticationInfo(req: AuthenticatedRequest): AuthenticationInfo |
} else if (issuer === idpConfigs.teacher.issuer) {
accountType = 'teacher';
} else {
return;
return undefined;
}
return {
accountType: accountType,
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
* 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);
next();
};
}
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
* to true.
*/
export const authorize =
(accessCondition: (auth: AuthenticationInfo) => boolean) =>
(req: AuthenticatedRequest, res: express.Response, next: express.NextFunction): void => {
export function authorize(accessCondition: (auth: AuthenticationInfo) => boolean) {
return (req: AuthenticatedRequest, _res: express.Response, next: express.NextFunction): void => {
if (!req.auth) {
throw new UnauthorizedException();
} else if (!accessCondition(req.auth)) {
@ -124,6 +125,7 @@ export const authorize =
next();
}
};
}
/**
* 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.
*/
export type AuthenticationInfo = {
export interface AuthenticationInfo {
accountType: 'student' | 'teacher';
username: string;
name?: string;
firstName?: string;
lastName?: string;
email?: string;
};
}

View file

@ -1,7 +1,7 @@
import cors from 'cors';
import { EnvVars, getEnvVar } from '../util/envvars.js';
import { envVars, getEnvVar } from '../util/envVars.js';
export default cors({
origin: getEnvVar(EnvVars.CorsAllowedOrigins).split(','),
allowedHeaders: getEnvVar(EnvVars.CorsAllowedHeaders).split(','),
origin: getEnvVar(envVars.CorsAllowedOrigins).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 { 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 { MikroOrmLogger } from './logging/mikroOrmLogger.js';
import { LOG_LEVEL } from './config.js';
// Import alle entity-bestanden handmatig
import { User } from './entities/users/user.entity.js';
@ -43,33 +42,35 @@ const entities = [
Question,
];
function config(testingMode: boolean = false): Options {
function config(testingMode = false): Options {
if (testingMode) {
return {
driver: SqliteDriver,
dbName: getEnvVar(EnvVars.DbName),
dbName: getEnvVar(envVars.DbName),
subscribers: [new SqliteAutoincrementSubscriber()],
entities: entities,
persistOnCreate: false, // Do not implicitly save entities when they are created via `create`.
// EntitiesTs: entitiesTs,
// Workaround: vitest: `TypeError: Unknown file extension ".ts"` (ERR_UNKNOWN_FILE_EXTENSION)
// (see https://mikro-orm.io/docs/guide/project-setup#testing-the-endpoint)
dynamicImportProvider: (id) => import(id),
dynamicImportProvider: async (id) => import(id),
};
}
return {
driver: PostgreSqlDriver,
host: getEnvVar(EnvVars.DbHost),
port: getNumericEnvVar(EnvVars.DbPort),
dbName: getEnvVar(EnvVars.DbName),
user: getEnvVar(EnvVars.DbUsername),
password: getEnvVar(EnvVars.DbPassword),
host: getEnvVar(envVars.DbHost),
port: getNumericEnvVar(envVars.DbPort),
dbName: getEnvVar(envVars.DbName),
user: getEnvVar(envVars.DbUsername),
password: getEnvVar(envVars.DbPassword),
entities: entities,
persistOnCreate: false, // Do not implicitly save entities when they are created via `create`.
// EntitiesTs: entitiesTs,
// Logging
debug: LOG_LEVEL === 'debug',
debug: getEnvVar(envVars.LogLevel) === 'debug',
loggerFactory: (options: LoggerOptions) => new MikroOrmLogger(options),
};
}

View file

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

View file

@ -1,45 +0,0 @@
import express from 'express';
const router = express.Router();
// Root endpoint used to search objects
router.get('/', (req, res) => {
res.json({
assignments: ['0', '1'],
});
});
// Information about an assignment with id 'id'
router.get('/:id', (req, res) => {
res.json({
id: req.params.id,
title: 'Dit is een test assignment',
description: 'Een korte beschrijving',
groups: ['0'],
learningPath: '0',
class: '0',
links: {
self: `${req.baseUrl}/${req.params.id}`,
submissions: `${req.baseUrl}/${req.params.id}`,
},
});
});
router.get('/:id/submissions', (req, res) => {
res.json({
submissions: ['0'],
});
});
router.get('/:id/groups', (req, res) => {
res.json({
groups: ['0'],
});
});
router.get('/:id/questions', (req, res) => {
res.json({
questions: ['0'],
});
});
export default router;

View file

@ -0,0 +1,30 @@
import express from 'express';
import {
createAssignmentHandler,
getAllAssignmentsHandler,
getAssignmentHandler,
getAssignmentsSubmissionsHandler,
} from '../controllers/assignments.js';
import groupRouter from './groups.js';
const router = express.Router({ mergeParams: true });
// Root endpoint used to search objects
router.get('/', getAllAssignmentsHandler);
router.post('/', createAssignmentHandler);
// Information about an assignment with id 'id'
router.get('/:id', getAssignmentHandler);
router.get('/:id/submissions', getAssignmentsSubmissionsHandler);
router.get('/:id/questions', (_req, res) => {
res.json({
questions: ['0'],
});
});
router.use('/:assignmentid/groups', groupRouter);
export default router;

View file

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

View file

@ -1,46 +0,0 @@
import express from 'express';
const router = express.Router();
// Root endpoint used to search objects
router.get('/', (req, res) => {
res.json({
classes: ['0', '1'],
});
});
// Information about an class with id 'id'
router.get('/:id', (req, res) => {
res.json({
id: req.params.id,
displayName: 'Klas 4B',
teachers: ['0'],
students: ['0'],
joinRequests: ['0'],
links: {
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`,
},
});
});
router.get('/:id/invitations', (req, res) => {
res.json({
invitations: ['0'],
});
});
router.get('/:id/assignments', (req, res) => {
res.json({
assignments: ['0'],
});
});
router.get('/:id/students', (req, res) => {
res.json({
students: ['0'],
});
});
export default router;

View file

@ -0,0 +1,27 @@
import express from 'express';
import {
createClassHandler,
getAllClassesHandler,
getClassHandler,
getClassStudentsHandler,
getTeacherInvitationsHandler,
} from '../controllers/classes.js';
import assignmentRouter from './assignments.js';
const router = express.Router();
// Root endpoint used to search objects
router.get('/', getAllClassesHandler);
router.post('/', createClassHandler);
// Information about an class with id 'id'
router.get('/:id', getClassHandler);
router.get('/:id/teacher-invitations', getTeacherInvitationsHandler);
router.get('/:id/students', getClassStudentsHandler);
router.use('/:classid/assignments', assignmentRouter);
export default router;

View file

@ -1,31 +0,0 @@
import express from 'express';
const router = express.Router();
// Root endpoint used to search objects
router.get('/', (req, res) => {
res.json({
groups: ['0', '1'],
});
});
// Information about a group (members, ... [TODO DOC])
router.get('/:id', (req, res) => {
res.json({
id: req.params.id,
assignment: '0',
students: ['0'],
submissions: ['0'],
// Reference to other endpoint
// Should be less hardcoded
questions: `/group/${req.params.id}/question`,
});
});
// The list of questions a group has made
router.get('/:id/question', (req, res) => {
res.json({
questions: ['0'],
});
});
export default router;

View file

@ -0,0 +1,23 @@
import express from 'express';
import { createGroupHandler, getAllGroupsHandler, getGroupHandler, getGroupSubmissionsHandler } from '../controllers/groups.js';
const router = express.Router({ mergeParams: true });
// Root endpoint used to search objects
router.get('/', getAllGroupsHandler);
router.post('/', createGroupHandler);
// Information about a group (members, ... [TODO DOC])
router.get('/:groupid', getGroupHandler);
router.get('/:groupid', getGroupSubmissionsHandler);
// The list of questions a group has made
router.get('/:id/questions', (_req, res) => {
res.json({
questions: ['0'],
});
});
export default router;

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