diff --git a/.github/workflows/backend-testing.yml b/.github/workflows/backend-testing.yml new file mode 100644 index 00000000..0d0b1f3f --- /dev/null +++ b/.github/workflows/backend-testing.yml @@ -0,0 +1,45 @@ +# This workflow will do a clean installation of node dependencies, cache/restore them, run backend tests across different versions of node (here 22.x) +# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-nodejs + +name: Backend Testing + +# Workflow runs when: +# - a backend js/ts file on "dev" changes +# - a non-draft PR to "dev" with backend js/ts files is opened, is reopened, or changes +# - a draft PR to "dev" with backend js/ts files is marked as ready for review +on: + push: + branches: [ "dev" ] + paths: + - 'backend/src/**.[jt]s' + - 'backend/tests/**.[jt]s' + - 'backend/vitest.config.ts' + pull_request: + branches: [ "dev" ] + types: ["synchronize", "ready_for_review", "opened", "reopened"] + paths: + - 'backend/src/**.[jt]s' + - 'backend/tests/**.[jt]s' + - 'backend/vitest.config.ts' + + +jobs: + test: + name: Run backend unit tests + if: '! github.event.pull_request.draft' + runs-on: [self-hosted, Linux, X64] + + strategy: + matrix: + node-version: [22.x] + # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ + + steps: + - uses: actions/checkout@v4 + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node-version }} + cache: 'npm' + - run: npm ci + - run: npm run test:unit -w backend diff --git a/.github/workflows/deployment.yml b/.github/workflows/deployment.yml index 865f4524..2eb42efa 100644 --- a/.github/workflows/deployment.yml +++ b/.github/workflows/deployment.yml @@ -13,7 +13,10 @@ jobs: - name: Checkout uses: actions/checkout@v4 + - + name: Copy environment variables to correct file + run: cp /home/dev/.backend.env backend/.env - name: Start docker - run: docker compose -f compose.yml -f compose.prod.yml up --build -d + run: docker compose -f compose.production.yml up --build -d \ No newline at end of file diff --git a/.github/workflows/frontend-testing.yml b/.github/workflows/frontend-testing.yml new file mode 100644 index 00000000..ff7bde4d --- /dev/null +++ b/.github/workflows/frontend-testing.yml @@ -0,0 +1,54 @@ +# This workflow will do a clean installation of node dependencies, cache/restore them, run frontend tests across different versions of node (here 22.x) +# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-nodejs + +name: Frontend Testing + +# Workflow runs when: +# - a frontend js/ts/vue/css file on "dev" changes +# - a non-draft PR to "dev" with frontend js/ts/vue/css files is opened, is reopened, or changes +# - a draft PR to "dev" with frontend js/ts/vue/css files is marked as ready for review +on: + push: + branches: [ "dev" ] + paths: + - 'frontend/src/**.[jt]s' + - 'frontend/src/**.vue' + - 'frontend/src/**.css' + - 'frontend/tests/**.[jt]s' + - 'frontend/tests/**.vue' + - 'frontend/tests/**.css' + - 'frontend/vitest.config.ts' + - 'frontend/playwright.config.ts' + pull_request: + branches: [ "dev" ] + types: ["synchronize", "ready_for_review", "opened", "reopened"] + paths: + - 'frontend/src/**.[jt]s' + - 'frontend/src/**.vue' + - 'frontend/src/**.css' + - 'frontend/tests/**.[jt]s' + - 'frontend/tests/**.vue' + - 'frontend/tests/**.css' + - 'frontend/vitest.config.ts' + - 'frontend/playwright.config.ts' + +jobs: + test: + name: Run frontend unit tests + if: '! github.event.pull_request.draft' + runs-on: [self-hosted, Linux, X64] + + strategy: + matrix: + node-version: [22.x] + # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ + + steps: + - uses: actions/checkout@v4 + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node-version }} + cache: 'npm' + - run: npm ci + - run: npm run test:unit -w frontend diff --git a/.github/workflows/lint-action.yml b/.github/workflows/lint-action.yml index 32823417..7fd77d28 100644 --- a/.github/workflows/lint-action.yml +++ b/.github/workflows/lint-action.yml @@ -43,6 +43,6 @@ jobs: with: auto_fix: true eslint: true - eslint_args: '--config eslint.config.ts' + eslint_args: "--config eslint.config.ts --ignore-pattern '**/prettier.config.js'" prettier: true - commit_message: 'style: fix linting issues met ${linter}' \ No newline at end of file + commit_message: 'style: fix linting issues met ${linter}' diff --git a/.gitignore b/.gitignore index d28e7d73..d3905d1f 100644 --- a/.gitignore +++ b/.gitignore @@ -737,4 +737,4 @@ flycheck_*.el # network security /network-security.data - +docs/.venv diff --git a/README.md b/README.md index 3526b53d..0499b037 100644 --- a/README.md +++ b/README.md @@ -35,12 +35,8 @@ Om de applicatie lokaal te draaien als kant-en-klare Docker-containers: ```bash docker compose version git clone https://github.com/SELab-2/Dwengo-1.git -cd Dwengo-1/backend -cp .env.example .env -# Pas .env aan -nano .env -cd .. docker compose -f compose.staging.yml up --build +# Gebruikt backend/.env.staging ``` ### Handmatige installatie en ontwikkeling diff --git a/backend/.env.staging b/backend/.env.staging new file mode 100644 index 00000000..bedfb0b7 --- /dev/null +++ b/backend/.env.staging @@ -0,0 +1,21 @@ +PORT=3000 +DWENGO_DB_HOST=db +DWENGO_DB_PORT=5432 +DWENGO_DB_USERNAME=postgres +DWENGO_DB_PASSWORD=postgres +DWENGO_DB_UPDATE=false + +DWENGO_AUTH_STUDENT_URL=http://localhost/idp/realms/student +DWENGO_AUTH_STUDENT_CLIENT_ID=dwengo +DWENGO_AUTH_STUDENT_JWKS_ENDPOINT=http://idp:7080/idp/realms/student/protocol/openid-connect/certs +DWENGO_AUTH_TEACHER_URL=http://localhost/idp/realms/teacher +DWENGO_AUTH_TEACHER_CLIENT_ID=dwengo +DWENGO_AUTH_TEACHER_JWKS_ENDPOINT=http://idp:7080/idp/realms/teacher/protocol/openid-connect/certs + +# Allow Vite dev-server to access the backend (for testing purposes). Don't forget to remove this in production! +#DWENGO_CORS_ALLOWED_ORIGINS=http://localhost/,127.0.0.1:80,http://127.0.0.1,http://localhost:80,http://127.0.0.1:80,localhost +DWENGO_CORS_ALLOWED_ORIGINS=http://localhost/*,http://idp:7080,https://idp:7080 + +# Logging and monitoring + +LOKI_HOST=http://logging:3102 diff --git a/backend/.env.test b/backend/.env.test index b8a81003..535628cd 100644 --- a/backend/.env.test +++ b/backend/.env.test @@ -1,3 +1,13 @@ -PORT=3000 -DWENGO_DB_UPDATE=true +# +# Test environment configuration +# +# Should not need to be modified. +# See .env.example for more information. +# + +### Dwengo ### + +DWENGO_PORT=3000 + DWENGO_DB_NAME=":memory:" +DWENGO_DB_UPDATE=true diff --git a/backend/.env.test.example b/backend/.env.test.example deleted file mode 100644 index 535628cd..00000000 --- a/backend/.env.test.example +++ /dev/null @@ -1,13 +0,0 @@ -# -# Test environment configuration -# -# Should not need to be modified. -# See .env.example for more information. -# - -### Dwengo ### - -DWENGO_PORT=3000 - -DWENGO_DB_NAME=":memory:" -DWENGO_DB_UPDATE=true diff --git a/backend/Dockerfile b/backend/Dockerfile index 7c63c4b8..bb3464c3 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -1,38 +1,50 @@ FROM node:22 AS build-stage -WORKDIR /app +WORKDIR /app/dwengo # Install dependencies COPY package*.json ./ COPY backend/package.json ./backend/ +# Backend depends on common +COPY common/package.json ./common/ RUN npm install --silent # Build the backend # Root tsconfig.json -COPY tsconfig.json ./ +COPY tsconfig.json tsconfig.build.json ./ -WORKDIR /app/backend - -COPY backend ./ -COPY docs /app/docs +COPY backend ./backend +COPY common ./common +COPY docs ./docs RUN npm run build FROM node:22 AS production-stage -WORKDIR /app +WORKDIR /app/dwengo -COPY package-lock.json backend/package.json ./ +# Copy static files + +COPY ./backend/i18n ./i18n + +# Copy built files + +COPY --from=build-stage /app/dwengo/common/dist ./common/dist +COPY --from=build-stage /app/dwengo/backend/dist ./backend/dist + +COPY package*.json ./ +COPY backend/package.json ./backend/ +# Backend depends on common +COPY common/package.json ./common/ RUN npm install --silent --only=production -COPY ./docs /docs -COPY ./backend/i18n /app/i18n -COPY --from=build-stage /app/backend/dist ./dist/ +COPY ./docs ./docs +COPY ./backend/i18n ./backend/i18n EXPOSE 3000 -CMD ["node", "--env-file=.env", "dist/app.js"] +CMD ["node", "--env-file=/app/dwengo/backend/.env", "/app/dwengo/backend/dist/app.js"] diff --git a/backend/README.md b/backend/README.md index 8a78ed14..ded42bd8 100644 --- a/backend/README.md +++ b/backend/README.md @@ -34,7 +34,9 @@ npm run test:unit ```shell # Omgevingsvariabelen -cp .env.development.example .env +cp .env.example .env +# Configureer de .env file met de juiste waarden! +nano .env npm run build npm run start diff --git a/backend/config.js b/backend/config.js deleted file mode 100644 index be42027c..00000000 --- a/backend/config.js +++ /dev/null @@ -1,7 +0,0 @@ -// Can be placed in dotenv but found it redundant -// Import dotenv from "dotenv"; -// Load .env file -// Dotenv.config(); -export const DWENGO_API_BASE = 'https://dwengo.org/backend/api'; -export const FALLBACK_LANG = 'nl'; -export const FALLBACK_SEQ_NUM = 1; diff --git a/backend/eslint.config.ts b/backend/eslint.config.ts index 6b696021..f5f225b2 100644 --- a/backend/eslint.config.ts +++ b/backend/eslint.config.ts @@ -8,14 +8,4 @@ export default [ globals: globals.node, }, }, - - { - files: ['tests/**/*.ts'], - languageOptions: { - globals: globals.node, - }, - rules: { - 'no-console': 'off', - }, - }, ]; diff --git a/backend/package.json b/backend/package.json index 4e3b890d..83db321f 100644 --- a/backend/package.json +++ b/backend/package.json @@ -1,17 +1,19 @@ { - "name": "dwengo-1-backend", + "name": "@dwengo-1/backend", "version": "0.1.1", "description": "Backend for Dwengo-1", "private": true, "type": "module", + "main": "dist/app.js", "scripts": { - "build": "cross-env NODE_ENV=production tsc --project tsconfig.json", + "build": "cross-env NODE_ENV=production tsc --build", "dev": "cross-env NODE_ENV=development tsx watch --env-file=.env.development.local src/app.ts", "start": "cross-env NODE_ENV=production node --env-file=.env dist/app.js", "format": "prettier --write src/", "format-check": "prettier --check src/", "lint": "eslint . --fix", - "test:unit": "vitest" + "pretest:unit": "npm run build", + "test:unit": "vitest --run" }, "dependencies": { "@mikro-orm/core": "6.4.9", @@ -24,6 +26,7 @@ "cross": "^1.0.0", "cross-env": "^7.0.3", "dotenv": "^16.4.7", + "dwengo-1-common": "^0.1.1", "express": "^5.0.1", "express-jwt": "^8.5.1", "gift-pegjs": "^1.0.2", diff --git a/backend/src/app.ts b/backend/src/app.ts index 07a84126..cf10a6df 100644 --- a/backend/src/app.ts +++ b/backend/src/app.ts @@ -5,7 +5,7 @@ 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'; @@ -14,7 +14,7 @@ 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(express.json()); app.use(cors); @@ -29,7 +29,7 @@ app.use('/api-docs', swaggerUi.serve, swaggerMiddleware); app.use(errorHandler); -async function startServer() { +async function startServer(): Promise { await initORM(); app.listen(port, () => { diff --git a/backend/src/config.ts b/backend/src/config.ts index b9974a3b..9b4702b5 100644 --- a/backend/src/config.ts +++ b/backend/src/config.ts @@ -1,7 +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); export const FALLBACK_SEQ_NUM = 1; diff --git a/backend/src/controllers/assignments.ts b/backend/src/controllers/assignments.ts index 16dbb310..1520fc10 100644 --- a/backend/src/controllers/assignments.ts +++ b/backend/src/controllers/assignments.ts @@ -1,8 +1,8 @@ import { Request, Response } from 'express'; import { createAssignment, getAllAssignments, getAssignment, getAssignmentsSubmissions } from '../services/assignments.js'; -import { AssignmentDTO } from '../interfaces/assignment.js'; +import { AssignmentDTO } from '@dwengo-1/common/interfaces/assignment'; -// Typescript is annoy with with parameter forwarding from class.ts +// Typescript is annoying with parameter forwarding from class.ts interface AssignmentParams { classid: string; id: string; @@ -41,7 +41,7 @@ export async function createAssignmentHandler(req: Request, re } export async function getAssignmentHandler(req: Request, res: Response): Promise { - const id = +req.params.id; + const id = Number(req.params.id); const classid = req.params.classid; if (isNaN(id)) { @@ -61,7 +61,7 @@ export async function getAssignmentHandler(req: Request, res: export async function getAssignmentsSubmissionsHandler(req: Request, res: Response): Promise { const classid = req.params.classid; - const assignmentNumber = +req.params.id; + const assignmentNumber = Number(req.params.id); const full = req.query.full === 'true'; if (isNaN(assignmentNumber)) { diff --git a/backend/src/controllers/auth.ts b/backend/src/controllers/auth.ts index 409ead0c..b87eaf7b 100644 --- a/backend/src/controllers/auth.ts +++ b/backend/src/controllers/auth.ts @@ -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, }, diff --git a/backend/src/controllers/classes.ts b/backend/src/controllers/classes.ts index 7526f7c4..a041bf22 100644 --- a/backend/src/controllers/classes.ts +++ b/backend/src/controllers/classes.ts @@ -1,6 +1,6 @@ import { Request, Response } from 'express'; import { createClass, getAllClasses, getClass, getClassStudents, getClassStudentsIds, getClassTeacherInvitations } from '../services/classes.js'; -import { ClassDTO } from '../interfaces/class.js'; +import { ClassDTO } from '@dwengo-1/common/interfaces/class'; export async function getAllClassesHandler(req: Request, res: Response): Promise { const full = req.query.full === 'true'; diff --git a/backend/src/controllers/error-helper.ts b/backend/src/controllers/error-helper.ts new file mode 100644 index 00000000..a902560f --- /dev/null +++ b/backend/src/controllers/error-helper.ts @@ -0,0 +1,18 @@ +import { BadRequestException } from '../exceptions/bad-request-exception.js'; + +/** + * Checks for the presence of required fields and throws a BadRequestException + * if any are missing. + * + * @param fields - An object with key-value pairs to validate. + */ +export function requireFields(fields: Record): void { + const missing = Object.entries(fields) + .filter(([_, value]) => value === undefined || value === null || value === '') + .map(([key]) => key); + + if (missing.length > 0) { + const message = `Missing required field${missing.length > 1 ? 's' : ''}: ${missing.join(', ')}`; + throw new BadRequestException(message); + } +} diff --git a/backend/src/controllers/groups.ts b/backend/src/controllers/groups.ts index 38d5d5d0..989066a6 100644 --- a/backend/src/controllers/groups.ts +++ b/backend/src/controllers/groups.ts @@ -1,6 +1,6 @@ import { Request, Response } from 'express'; import { createGroup, getAllGroups, getGroup, getGroupSubmissions } from '../services/groups.js'; -import { GroupDTO } from '../interfaces/group.js'; +import { GroupDTO } from '@dwengo-1/common/interfaces/group'; // Typescript is annoywith with parameter forwarding from class.ts interface GroupParams { @@ -12,14 +12,14 @@ interface GroupParams { export async function getGroupHandler(req: Request, res: Response): Promise { const classId = req.params.classid; const full = req.query.full === 'true'; - const assignmentId = +req.params.assignmentid; + const assignmentId = Number(req.params.assignmentid); if (isNaN(assignmentId)) { res.status(400).json({ error: 'Assignment id must be a number' }); return; } - const groupId = +req.params.groupid!; // Can't be undefined + const groupId = Number(req.params.groupid!); // Can't be undefined if (isNaN(groupId)) { res.status(400).json({ error: 'Group id must be a number' }); @@ -40,7 +40,7 @@ export async function getAllGroupsHandler(req: Request, res: Response): Promise< const classId = req.params.classid; const full = req.query.full === 'true'; - const assignmentId = +req.params.assignmentid; + const assignmentId = Number(req.params.assignmentid); if (isNaN(assignmentId)) { res.status(400).json({ error: 'Assignment id must be a number' }); @@ -56,7 +56,7 @@ export async function getAllGroupsHandler(req: Request, res: Response): Promise< export async function createGroupHandler(req: Request, res: Response): Promise { const classid = req.params.classid; - const assignmentId = +req.params.assignmentid; + const assignmentId = Number(req.params.assignmentid); if (isNaN(assignmentId)) { res.status(400).json({ error: 'Assignment id must be a number' }); @@ -78,14 +78,14 @@ export async function getGroupSubmissionsHandler(req: Request, res: Response): P const classId = req.params.classid; const full = req.query.full === 'true'; - const assignmentId = +req.params.assignmentid; + const assignmentId = Number(req.params.assignmentid); if (isNaN(assignmentId)) { res.status(400).json({ error: 'Assignment id must be a number' }); return; } - const groupId = +req.params.groupid!; // Can't be undefined + const groupId = Number(req.params.groupid); // Can't be undefined if (isNaN(groupId)) { res.status(400).json({ error: 'Group id must be a number' }); diff --git a/backend/src/controllers/learning-objects.ts b/backend/src/controllers/learning-objects.ts index 53eb1ded..a2510631 100644 --- a/backend/src/controllers/learning-objects.ts +++ b/backend/src/controllers/learning-objects.ts @@ -1,20 +1,20 @@ 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 { Language } from '../entities/content/language.js'; +import { Language } from '@dwengo-1/common/util/language'; import attachmentService from '../services/learning-objects/attachment-service.js'; -import { NotFoundError } from '@mikro-orm/core'; import { BadRequestException } from '../exceptions/bad-request-exception.js'; +import { NotFoundException } from '../exceptions/not-found-exception.js'; +import { envVars, getEnvVar } from '../util/envVars.js'; +import { FilteredLearningObject, LearningObjectIdentifier, LearningPathIdentifier } from '@dwengo-1/common/interfaces/learning-content'; function getLearningObjectIdentifierFromRequest(req: Request): LearningObjectIdentifier { 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, }; } @@ -47,6 +47,11 @@ export async function getLearningObject(req: Request, res: Response): Promise const attachment = await attachmentService.getAttachment(learningObjectId, name); if (!attachment) { - throw new NotFoundError(`Attachment ${name} not found`); + throw new NotFoundException(`Attachment ${name} not found`); } res.setHeader('Content-Type', attachment.mimeType).send(attachment.content); } diff --git a/backend/src/controllers/learning-paths.ts b/backend/src/controllers/learning-paths.ts index 04e44b59..0097d568 100644 --- a/backend/src/controllers/learning-paths.ts +++ b/backend/src/controllers/learning-paths.ts @@ -2,7 +2,7 @@ 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 { Language } from '../entities/content/language.js'; +import { Language } from '@dwengo-1/common/util/language'; import { PersonalizationTarget, personalizedForGroup, diff --git a/backend/src/controllers/questions.ts b/backend/src/controllers/questions.ts index 00a51329..b5b764ac 100644 --- a/backend/src/controllers/questions.ts +++ b/backend/src/controllers/questions.ts @@ -1,9 +1,9 @@ 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'; +import { QuestionDTO, QuestionId } from '@dwengo-1/common/interfaces/question'; +import { Language } from '@dwengo-1/common/util/language'; function getObjectId(req: Request, res: Response): LearningObjectIdentifier | null { const { hruid, version } = req.params; @@ -17,7 +17,7 @@ function getObjectId(req: Request, res: Response): LearningObjectIdentifier | nu return { hruid, language: (lang as Language) || FALLBACK_LANG, - version: +version, + version: Number(version), }; } diff --git a/backend/src/controllers/students.ts b/backend/src/controllers/students.ts index 8ce5b11a..51488a2a 100644 --- a/backend/src/controllers/students.ts +++ b/backend/src/controllers/students.ts @@ -1,101 +1,67 @@ import { Request, Response } from 'express'; import { + createClassJoinRequest, createStudent, + deleteClassJoinRequest, deleteStudent, getAllStudents, + getJoinRequestByStudentClass, + getJoinRequestsByStudent, getStudent, getStudentAssignments, getStudentClasses, getStudentGroups, + getStudentQuestions, getStudentSubmissions, } from '../services/students.js'; -import { StudentDTO } from '../interfaces/student.js'; +import { requireFields } from './error-helper.js'; +import { StudentDTO } from '@dwengo-1/common/interfaces/student'; -// TODO: accept arguments (full, ...) -// TODO: endpoints export async function getAllStudentsHandler(req: Request, res: Response): Promise { const full = req.query.full === 'true'; - const students = await getAllStudents(full); + const students: StudentDTO[] | string[] = await getAllStudents(full); - if (!students) { - res.status(404).json({ error: `Student not found.` }); - return; - } - - res.json({ students: students }); + res.json({ students }); } export async function getStudentHandler(req: Request, res: Response): Promise { const username = req.params.username; + requireFields({ username }); - if (!username) { - res.status(400).json({ error: 'Missing required field: username' }); - return; - } + const student = await getStudent(username); - const user = await getStudent(username); - - if (!user) { - res.status(404).json({ - error: `User with username '${username}' not found.`, - }); - return; - } - - res.json(user); + res.json({ student }); } -export async function createStudentHandler(req: Request, res: Response) { +export async function createStudentHandler(req: Request, res: Response): Promise { + const username = req.body.username; + const firstName = req.body.firstName; + const lastName = req.body.lastName; + requireFields({ username, firstName, lastName }); + 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); + const student = await createStudent(userData); + res.json({ student }); } -export async function deleteStudentHandler(req: Request, res: Response) { +export async function deleteStudentHandler(req: Request, res: Response): Promise { const username = req.params.username; + requireFields({ 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); + const student = await deleteStudent(username); + res.json({ student }); } export async function getStudentClassesHandler(req: Request, res: Response): Promise { const full = req.query.full === 'true'; - const username = req.params.id; + const username = req.params.username; + requireFields({ username }); const classes = await getStudentClasses(username, full); - res.json({ - classes: classes, - }); + res.json({ classes }); } // TODO @@ -104,33 +70,75 @@ export async function getStudentClassesHandler(req: Request, res: Response): Pro // Have this assignment. export async function getStudentAssignmentsHandler(req: Request, res: Response): Promise { const full = req.query.full === 'true'; - const username = req.params.id; + const username = req.params.username; + requireFields({ username }); const assignments = getStudentAssignments(username, full); - res.json({ - assignments: assignments, - }); + res.json({ assignments }); } export async function getStudentGroupsHandler(req: Request, res: Response): Promise { const full = req.query.full === 'true'; - const username = req.params.id; + const username = req.params.username; + requireFields({ username }); const groups = await getStudentGroups(username, full); - res.json({ - groups: groups, - }); + res.json({ groups }); } export async function getStudentSubmissionsHandler(req: Request, res: Response): Promise { - const username = req.params.id; + const username = req.params.username; const full = req.query.full === 'true'; + requireFields({ username }); const submissions = await getStudentSubmissions(username, full); - res.json({ - submissions: submissions, - }); + res.json({ submissions }); +} + +export async function getStudentQuestionsHandler(req: Request, res: Response): Promise { + const full = req.query.full === 'true'; + const username = req.params.username; + requireFields({ username }); + + const questions = await getStudentQuestions(username, full); + + res.json({ questions }); +} + +export async function createStudentRequestHandler(req: Request, res: Response): Promise { + const username = req.params.username; + const classId = req.body.classId; + requireFields({ username, classId }); + + const request = await createClassJoinRequest(username, classId); + res.json({ request }); +} + +export async function getStudentRequestsHandler(req: Request, res: Response): Promise { + const username = req.params.username; + requireFields({ username }); + + const requests = await getJoinRequestsByStudent(username); + res.json({ requests }); +} + +export async function getStudentRequestHandler(req: Request, res: Response): Promise { + const username = req.params.username; + const classId = req.params.classId; + requireFields({ username, classId }); + + const request = await getJoinRequestByStudentClass(username, classId); + res.json({ request }); +} + +export async function deleteClassJoinRequestHandler(req: Request, res: Response): Promise { + const username = req.params.username; + const classId = req.params.classId; + requireFields({ username, classId }); + + const request = await deleteClassJoinRequest(username, classId); + res.json({ request }); } diff --git a/backend/src/controllers/submissions.ts b/backend/src/controllers/submissions.ts index 512ac22e..239eb6d7 100644 --- a/backend/src/controllers/submissions.ts +++ b/backend/src/controllers/submissions.ts @@ -1,7 +1,7 @@ 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'; +import { SubmissionDTO } from '@dwengo-1/common/interfaces/submission'; +import { Language, languageMap } from '@dwengo-1/common/util/language'; interface SubmissionParams { hruid: string; @@ -10,7 +10,7 @@ interface SubmissionParams { export async function getSubmissionHandler(req: Request, res: Response): Promise { const lohruid = req.params.hruid; - const submissionNumber = +req.params.id; + const submissionNumber = Number(req.params.id); if (isNaN(submissionNumber)) { res.status(400).json({ error: 'Submission number is not a number' }); @@ -30,7 +30,7 @@ export async function getSubmissionHandler(req: Request, res: res.json(submission); } -export async function createSubmissionHandler(req: Request, res: Response) { +export async function createSubmissionHandler(req: Request, res: Response): Promise { const submissionDTO = req.body as SubmissionDTO; const submission = await createSubmission(submissionDTO); @@ -43,9 +43,9 @@ export async function createSubmissionHandler(req: Request, res: Response) { res.json(submission); } -export async function deleteSubmissionHandler(req: Request, res: Response) { +export async function deleteSubmissionHandler(req: Request, res: Response): Promise { const hruid = req.params.hruid; - const submissionNumber = +req.params.id; + const submissionNumber = Number(req.params.id); const lang = languageMap[req.query.language as string] || Language.Dutch; const version = (req.query.version || 1) as number; diff --git a/backend/src/controllers/teachers.ts b/backend/src/controllers/teachers.ts index 7376abed..9275ca92 100644 --- a/backend/src/controllers/teachers.ts +++ b/backend/src/controllers/teachers.ts @@ -4,137 +4,97 @@ import { deleteTeacher, getAllTeachers, getClassesByTeacher, - getQuestionsByTeacher, + getJoinRequestsByClass, getStudentsByTeacher, getTeacher, + getTeacherQuestions, + updateClassJoinRequestStatus, } from '../services/teachers.js'; -import { TeacherDTO } from '../interfaces/teacher.js'; +import { requireFields } from './error-helper.js'; +import { TeacherDTO } from '@dwengo-1/common/interfaces/teacher'; export async function getAllTeachersHandler(req: Request, res: Response): Promise { const full = req.query.full === 'true'; - const teachers = await getAllTeachers(full); + const teachers: TeacherDTO[] | string[] = await getAllTeachers(full); - if (!teachers) { - res.status(404).json({ error: `Teacher not found.` }); - return; - } - - res.json({ teachers: teachers }); + res.json({ teachers }); } export async function getTeacherHandler(req: Request, res: Response): Promise { const username = req.params.username; + requireFields({ username }); - if (!username) { - res.status(400).json({ error: 'Missing required field: username' }); - return; - } + const teacher = await getTeacher(username); - const user = await getTeacher(username); - - if (!user) { - res.status(404).json({ - error: `Teacher '${username}' not found.`, - }); - return; - } - - res.json(user); + res.json({ teacher }); } -export async function createTeacherHandler(req: Request, res: Response) { +export async function createTeacherHandler(req: Request, res: Response): Promise { + const username = req.body.username; + const firstName = req.body.firstName; + const lastName = req.body.lastName; + requireFields({ username, firstName, lastName }); + 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); + const teacher = await createTeacher(userData); + res.json({ teacher }); } -export async function deleteTeacherHandler(req: Request, res: Response) { +export async function deleteTeacherHandler(req: Request, res: Response): Promise { const username = req.params.username; + requireFields({ 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); + const teacher = await deleteTeacher(username); + res.json({ teacher }); } export async function getTeacherClassHandler(req: Request, res: Response): Promise { - const username = req.params.username as string; + const username = req.params.username; const full = req.query.full === 'true'; - - if (!username) { - res.status(400).json({ error: 'Missing required field: username' }); - return; - } + requireFields({ username }); const classes = await getClassesByTeacher(username, full); - if (!classes) { - res.status(404).json({ error: 'Teacher not found' }); - return; - } - - res.json({ classes: classes }); + res.json({ classes }); } export async function getTeacherStudentHandler(req: Request, res: Response): Promise { - const username = req.params.username as string; + const username = req.params.username; const full = req.query.full === 'true'; - - if (!username) { - res.status(400).json({ error: 'Missing required field: username' }); - return; - } + requireFields({ username }); const students = await getStudentsByTeacher(username, full); - if (!students) { - res.status(404).json({ error: 'Teacher not found' }); - return; - } - - res.json({ students: students }); + res.json({ students }); } export async function getTeacherQuestionHandler(req: Request, res: Response): Promise { - const username = req.params.username as string; + const username = req.params.username; const full = req.query.full === 'true'; + requireFields({ username }); - if (!username) { - res.status(400).json({ error: 'Missing required field: username' }); - return; - } + const questions = await getTeacherQuestions(username, full); - const questions = await getQuestionsByTeacher(username, full); - - if (!questions) { - res.status(404).json({ error: 'Teacher not found' }); - return; - } - - res.json({ questions: questions }); + res.json({ questions }); +} + +export async function getStudentJoinRequestHandler(req: Request, res: Response): Promise { + const username = req.query.username as string; + const classId = req.params.classId; + requireFields({ username, classId }); + + const joinRequests = await getJoinRequestsByClass(classId); + res.json({ joinRequests }); +} + +export async function updateStudentJoinRequestHandler(req: Request, res: Response): Promise { + const studentUsername = req.query.studentUsername as string; + const classId = req.params.classId; + const accepted = req.body.accepted !== 'false'; // Default = true + requireFields({ studentUsername, classId }); + + const request = await updateClassJoinRequestStatus(studentUsername, classId, accepted); + res.json({ request }); } diff --git a/backend/src/controllers/themes.ts b/backend/src/controllers/themes.ts index 8406cf5f..b34d0c80 100644 --- a/backend/src/controllers/themes.ts +++ b/backend/src/controllers/themes.ts @@ -3,25 +3,23 @@ import { themes } from '../data/themes.js'; import { loadTranslations } from '../util/translation-helper.js'; interface Translations { - curricula_page: { - [key: string]: { title: string; description?: string }; - }; + curricula_page: Record; } -export function getThemesHandler(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(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 getHruidsByThemeHandler(req: Request, res: Response) { +export function getHruidsByThemeHandler(req: Request, res: Response): void { const themeKey = req.params.theme; if (!themeKey) { diff --git a/backend/src/data/assignments/assignment-repository.ts b/backend/src/data/assignments/assignment-repository.ts index c3c457d3..3de5031d 100644 --- a/backend/src/data/assignments/assignment-repository.ts +++ b/backend/src/data/assignments/assignment-repository.ts @@ -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 { - public findByClassAndId(within: Class, id: number): Promise { + public async findByClassAndId(within: Class, id: number): Promise { return this.findOne({ within: within, id: id }); } - public findAllAssignmentsInClass(within: Class): Promise { + public async findAllAssignmentsInClass(within: Class): Promise { return this.findAll({ where: { within: within } }); } - public deleteByClassAndId(within: Class, id: number): Promise { + public async deleteByClassAndId(within: Class, id: number): Promise { return this.deleteWhere({ within: within, id: id }); } } diff --git a/backend/src/data/assignments/group-repository.ts b/backend/src/data/assignments/group-repository.ts index eb1b09e2..f06080f7 100644 --- a/backend/src/data/assignments/group-repository.ts +++ b/backend/src/data/assignments/group-repository.ts @@ -4,7 +4,7 @@ import { Assignment } from '../../entities/assignments/assignment.entity.js'; import { Student } from '../../entities/users/student.entity.js'; export class GroupRepository extends DwengoEntityRepository { - public findByAssignmentAndGroupNumber(assignment: Assignment, groupNumber: number): Promise { + public async findByAssignmentAndGroupNumber(assignment: Assignment, groupNumber: number): Promise { return this.findOne( { assignment: assignment, @@ -13,16 +13,16 @@ export class GroupRepository extends DwengoEntityRepository { { populate: ['members'] } ); } - public findAllGroupsForAssignment(assignment: Assignment): Promise { + public async findAllGroupsForAssignment(assignment: Assignment): Promise { return this.findAll({ where: { assignment: assignment }, populate: ['members'], }); } - public findAllGroupsWithStudent(student: Student): Promise { + public async findAllGroupsWithStudent(student: Student): Promise { return this.find({ members: student }, { populate: ['members'] }); } - public deleteByAssignmentAndGroupNumber(assignment: Assignment, groupNumber: number) { + public async deleteByAssignmentAndGroupNumber(assignment: Assignment, groupNumber: number): Promise { return this.deleteWhere({ assignment: assignment, groupNumber: groupNumber, diff --git a/backend/src/data/assignments/submission-repository.ts b/backend/src/data/assignments/submission-repository.ts index 251823fa..f5090adc 100644 --- a/backend/src/data/assignments/submission-repository.ts +++ b/backend/src/data/assignments/submission-repository.ts @@ -5,7 +5,10 @@ import { LearningObjectIdentifier } from '../../entities/content/learning-object import { Student } from '../../entities/users/student.entity.js'; export class SubmissionRepository extends DwengoEntityRepository { - public findSubmissionByLearningObjectAndSubmissionNumber(loId: LearningObjectIdentifier, submissionNumber: number): Promise { + public async findSubmissionByLearningObjectAndSubmissionNumber( + loId: LearningObjectIdentifier, + submissionNumber: number + ): Promise { return this.findOne({ learningObjectHruid: loId.hruid, learningObjectLanguage: loId.language, @@ -14,7 +17,7 @@ export class SubmissionRepository extends DwengoEntityRepository { }); } - public findMostRecentSubmissionForStudent(loId: LearningObjectIdentifier, submitter: Student): Promise { + public async findMostRecentSubmissionForStudent(loId: LearningObjectIdentifier, submitter: Student): Promise { return this.findOne( { learningObjectHruid: loId.hruid, @@ -26,7 +29,7 @@ export class SubmissionRepository extends DwengoEntityRepository { ); } - public findMostRecentSubmissionForGroup(loId: LearningObjectIdentifier, group: Group): Promise { + public async findMostRecentSubmissionForGroup(loId: LearningObjectIdentifier, group: Group): Promise { return this.findOne( { learningObjectHruid: loId.hruid, @@ -38,15 +41,15 @@ export class SubmissionRepository extends DwengoEntityRepository { ); } - public findAllSubmissionsForGroup(group: Group): Promise { + public async findAllSubmissionsForGroup(group: Group): Promise { return this.find({ onBehalfOf: group }); } - public findAllSubmissionsForStudent(student: Student): Promise { + public async findAllSubmissionsForStudent(student: Student): Promise { return this.find({ submitter: student }); } - public deleteSubmissionByLearningObjectAndSubmissionNumber(loId: LearningObjectIdentifier, submissionNumber: number): Promise { + public async deleteSubmissionByLearningObjectAndSubmissionNumber(loId: LearningObjectIdentifier, submissionNumber: number): Promise { return this.deleteWhere({ learningObjectHruid: loId.hruid, learningObjectLanguage: loId.language, diff --git a/backend/src/data/classes/class-join-request-repository.ts b/backend/src/data/classes/class-join-request-repository.ts index c1443c1c..0d9ab6e1 100644 --- a/backend/src/data/classes/class-join-request-repository.ts +++ b/backend/src/data/classes/class-join-request-repository.ts @@ -2,15 +2,19 @@ import { DwengoEntityRepository } from '../dwengo-entity-repository.js'; import { Class } from '../../entities/classes/class.entity.js'; import { ClassJoinRequest } from '../../entities/classes/class-join-request.entity.js'; import { Student } from '../../entities/users/student.entity.js'; +import { ClassJoinRequestStatus } from '@dwengo-1/common/util/class-join-request'; export class ClassJoinRequestRepository extends DwengoEntityRepository { - public findAllRequestsBy(requester: Student): Promise { + public async findAllRequestsBy(requester: Student): Promise { return this.findAll({ where: { requester: requester } }); } - public findAllOpenRequestsTo(clazz: Class): Promise { - return this.findAll({ where: { class: clazz } }); + public async findAllOpenRequestsTo(clazz: Class): Promise { + return this.findAll({ where: { class: clazz, status: ClassJoinRequestStatus.Open } }); // TODO check if works like this } - public deleteBy(requester: Student, clazz: Class): Promise { + public async findByStudentAndClass(requester: Student, clazz: Class): Promise { + return this.findOne({ requester, class: clazz }); + } + public async deleteBy(requester: Student, clazz: Class): Promise { return this.deleteWhere({ requester: requester, class: clazz }); } } diff --git a/backend/src/data/classes/class-repository.ts b/backend/src/data/classes/class-repository.ts index 0ceed98e..f4e0723f 100644 --- a/backend/src/data/classes/class-repository.ts +++ b/backend/src/data/classes/class-repository.ts @@ -4,20 +4,20 @@ import { Student } from '../../entities/users/student.entity.js'; import { Teacher } from '../../entities/users/teacher.entity'; export class ClassRepository extends DwengoEntityRepository { - public findById(id: string): Promise { + public async findById(id: string): Promise { return this.findOne({ classId: id }, { populate: ['students', 'teachers'] }); } - public deleteById(id: string): Promise { + public async deleteById(id: string): Promise { return this.deleteWhere({ classId: id }); } - public findByStudent(student: Student): Promise { + public async findByStudent(student: Student): Promise { return this.find( { students: student }, { populate: ['students', 'teachers'] } // Voegt student en teacher objecten toe ); } - public findByTeacher(teacher: Teacher): Promise { + public async findByTeacher(teacher: Teacher): Promise { return this.find({ teachers: teacher }, { populate: ['students', 'teachers'] }); } } diff --git a/backend/src/data/classes/teacher-invitation-repository.ts b/backend/src/data/classes/teacher-invitation-repository.ts index 6b94deec..ce059ca8 100644 --- a/backend/src/data/classes/teacher-invitation-repository.ts +++ b/backend/src/data/classes/teacher-invitation-repository.ts @@ -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 { - public findAllInvitationsForClass(clazz: Class): Promise { + public async findAllInvitationsForClass(clazz: Class): Promise { return this.findAll({ where: { class: clazz } }); } - public findAllInvitationsBy(sender: Teacher): Promise { + public async findAllInvitationsBy(sender: Teacher): Promise { return this.findAll({ where: { sender: sender } }); } - public findAllInvitationsFor(receiver: Teacher): Promise { + public async findAllInvitationsFor(receiver: Teacher): Promise { return this.findAll({ where: { receiver: receiver } }); } - public deleteBy(clazz: Class, sender: Teacher, receiver: Teacher): Promise { + public async deleteBy(clazz: Class, sender: Teacher, receiver: Teacher): Promise { return this.deleteWhere({ sender: sender, receiver: receiver, diff --git a/backend/src/data/content/attachment-repository.ts b/backend/src/data/content/attachment-repository.ts index 95c5ab1c..69178b1c 100644 --- a/backend/src/data/content/attachment-repository.ts +++ b/backend/src/data/content/attachment-repository.ts @@ -1,10 +1,10 @@ import { DwengoEntityRepository } from '../dwengo-entity-repository.js'; import { Attachment } from '../../entities/content/attachment.entity.js'; -import { Language } from '../../entities/content/language'; +import { Language } from '@dwengo-1/common/util/language'; import { LearningObjectIdentifier } from '../../entities/content/learning-object-identifier'; export class AttachmentRepository extends DwengoEntityRepository { - public findByLearningObjectIdAndName(learningObjectId: LearningObjectIdentifier, name: string): Promise { + public async findByLearningObjectIdAndName(learningObjectId: LearningObjectIdentifier, name: string): Promise { return this.findOne({ learningObject: { hruid: learningObjectId.hruid, @@ -15,7 +15,11 @@ export class AttachmentRepository extends DwengoEntityRepository { }); } - public findByMostRecentVersionOfLearningObjectAndName(hruid: string, language: Language, attachmentName: string): Promise { + public async findByMostRecentVersionOfLearningObjectAndName( + hruid: string, + language: Language, + attachmentName: string + ): Promise { return this.findOne( { learningObject: { diff --git a/backend/src/data/content/learning-object-repository.ts b/backend/src/data/content/learning-object-repository.ts index 49b4c536..889a1594 100644 --- a/backend/src/data/content/learning-object-repository.ts +++ b/backend/src/data/content/learning-object-repository.ts @@ -1,11 +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.js'; +import { Language } from '@dwengo-1/common/util/language'; import { Teacher } from '../../entities/users/teacher.entity.js'; export class LearningObjectRepository extends DwengoEntityRepository { - public findByIdentifier(identifier: LearningObjectIdentifier): Promise { + public async findByIdentifier(identifier: LearningObjectIdentifier): Promise { return this.findOne( { hruid: identifier.hruid, @@ -18,7 +18,7 @@ export class LearningObjectRepository extends DwengoEntityRepository { return this.findOne( { hruid: hruid, @@ -33,7 +33,7 @@ export class LearningObjectRepository extends DwengoEntityRepository { + public async findAllByTeacher(teacher: Teacher): Promise { return this.find( { admins: teacher }, { populate: ['admins'] } // Make sure to load admin relations diff --git a/backend/src/data/content/learning-path-repository.ts b/backend/src/data/content/learning-path-repository.ts index a2f9b47e..87035f21 100644 --- a/backend/src/data/content/learning-path-repository.ts +++ b/backend/src/data/content/learning-path-repository.ts @@ -1,9 +1,9 @@ import { DwengoEntityRepository } from '../dwengo-entity-repository.js'; import { LearningPath } from '../../entities/content/learning-path.entity.js'; -import { Language } from '../../entities/content/language.js'; +import { Language } from '@dwengo-1/common/util/language'; export class LearningPathRepository extends DwengoEntityRepository { - public findByHruidAndLanguage(hruid: string, language: Language): Promise { + public async findByHruidAndLanguage(hruid: string, language: Language): Promise { return this.findOne({ hruid: hruid, language: language }, { populate: ['nodes', 'nodes.transitions'] }); } diff --git a/backend/src/data/dwengo-entity-repository.ts b/backend/src/data/dwengo-entity-repository.ts index ce894e4b..1267c726 100644 --- a/backend/src/data/dwengo-entity-repository.ts +++ b/backend/src/data/dwengo-entity-repository.ts @@ -8,7 +8,7 @@ export abstract class DwengoEntityRepository extends EntityRep } await this.getEntityManager().persistAndFlush(entity); } - public async deleteWhere(query: FilterQuery) { + public async deleteWhere(query: FilterQuery): Promise { const toDelete = await this.findOne(query); const em = this.getEntityManager(); if (toDelete) { diff --git a/backend/src/data/questions/answer-repository.ts b/backend/src/data/questions/answer-repository.ts index a28342bd..a50bfd28 100644 --- a/backend/src/data/questions/answer-repository.ts +++ b/backend/src/data/questions/answer-repository.ts @@ -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 { - public createAnswer(answer: { toQuestion: Question; author: Teacher; content: string }): Promise { + public async createAnswer(answer: { toQuestion: Question; author: Teacher; content: string }): Promise { const answerEntity = this.create({ toQuestion: answer.toQuestion, author: answer.author, @@ -13,13 +13,13 @@ export class AnswerRepository extends DwengoEntityRepository { }); return this.insert(answerEntity); } - public findAllAnswersToQuestion(question: Question): Promise { + public async findAllAnswersToQuestion(question: Question): Promise { return this.findAll({ where: { toQuestion: question }, orderBy: { sequenceNumber: 'ASC' }, }); } - public removeAnswerByQuestionAndSequenceNumber(question: Question, sequenceNumber: number): Promise { + public async removeAnswerByQuestionAndSequenceNumber(question: Question, sequenceNumber: number): Promise { return this.deleteWhere({ toQuestion: question, sequenceNumber: sequenceNumber, diff --git a/backend/src/data/questions/question-repository.ts b/backend/src/data/questions/question-repository.ts index 9207e1dd..2d165abc 100644 --- a/backend/src/data/questions/question-repository.ts +++ b/backend/src/data/questions/question-repository.ts @@ -5,7 +5,7 @@ import { Student } from '../../entities/users/student.entity.js'; import { LearningObject } from '../../entities/content/learning-object.entity.js'; export class QuestionRepository extends DwengoEntityRepository { - public createQuestion(question: { loId: LearningObjectIdentifier; author: Student; content: string }): Promise { + public async createQuestion(question: { loId: LearningObjectIdentifier; author: Student; content: string }): Promise { const questionEntity = this.create({ learningObjectHruid: question.loId.hruid, learningObjectLanguage: question.loId.language, @@ -21,7 +21,7 @@ export class QuestionRepository extends DwengoEntityRepository { questionEntity.content = question.content; return this.insert(questionEntity); } - public findAllQuestionsAboutLearningObject(loId: LearningObjectIdentifier): Promise { + public async findAllQuestionsAboutLearningObject(loId: LearningObjectIdentifier): Promise { return this.findAll({ where: { learningObjectHruid: loId.hruid, @@ -33,7 +33,7 @@ export class QuestionRepository extends DwengoEntityRepository { }, }); } - public removeQuestionByLearningObjectAndSequenceNumber(loId: LearningObjectIdentifier, sequenceNumber: number): Promise { + public async removeQuestionByLearningObjectAndSequenceNumber(loId: LearningObjectIdentifier, sequenceNumber: number): Promise { return this.deleteWhere({ learningObjectHruid: loId.hruid, learningObjectLanguage: loId.language, @@ -54,4 +54,11 @@ export class QuestionRepository extends DwengoEntityRepository { orderBy: { timestamp: 'ASC' }, }); } + + public async findAllByAuthor(author: Student): Promise { + return this.findAll({ + where: { author }, + orderBy: { timestamp: 'DESC' }, // New to old + }); + } } diff --git a/backend/src/data/repositories.ts b/backend/src/data/repositories.ts index cdeb50c1..f09c3c75 100644 --- a/backend/src/data/repositories.ts +++ b/backend/src/data/repositories.ts @@ -34,8 +34,8 @@ let entityManager: EntityManager | undefined; /** * Execute all the database operations within the function f in a single transaction. */ -export function transactional(f: () => Promise) { - entityManager?.transactional(f); +export async function transactional(f: () => Promise): Promise { + await entityManager?.transactional(f); } function repositoryGetter>(entity: EntityName): () => R { diff --git a/backend/src/data/themes.ts b/backend/src/data/themes.ts index b0fc930c..0a2272e6 100644 --- a/backend/src/data/themes.ts +++ b/backend/src/data/themes.ts @@ -1,7 +1,4 @@ -export interface Theme { - title: string; - hruids: string[]; -} +import { Theme } from '@dwengo-1/common/interfaces/theme'; export const themes: Theme[] = [ { diff --git a/backend/src/data/users/student-repository.ts b/backend/src/data/users/student-repository.ts index a13fbb22..2efca048 100644 --- a/backend/src/data/users/student-repository.ts +++ b/backend/src/data/users/student-repository.ts @@ -1,14 +1,11 @@ import { Student } from '../../entities/users/student.entity.js'; import { DwengoEntityRepository } from '../dwengo-entity-repository.js'; -// Import { UserRepository } from './user-repository.js'; - -// Export class StudentRepository extends UserRepository {} export class StudentRepository extends DwengoEntityRepository { - public findByUsername(username: string): Promise { + public async findByUsername(username: string): Promise { return this.findOne({ username: username }); } - public deleteByUsername(username: string): Promise { + public async deleteByUsername(username: string): Promise { return this.deleteWhere({ username: username }); } } diff --git a/backend/src/data/users/teacher-repository.ts b/backend/src/data/users/teacher-repository.ts index 825b4d18..aa915627 100644 --- a/backend/src/data/users/teacher-repository.ts +++ b/backend/src/data/users/teacher-repository.ts @@ -2,10 +2,10 @@ import { Teacher } from '../../entities/users/teacher.entity.js'; import { DwengoEntityRepository } from '../dwengo-entity-repository.js'; export class TeacherRepository extends DwengoEntityRepository { - public findByUsername(username: string): Promise { + public async findByUsername(username: string): Promise { return this.findOne({ username: username }); } - public deleteByUsername(username: string): Promise { + public async deleteByUsername(username: string): Promise { return this.deleteWhere({ username: username }); } } diff --git a/backend/src/data/users/user-repository.ts b/backend/src/data/users/user-repository.ts index 21497b79..44eb0bc7 100644 --- a/backend/src/data/users/user-repository.ts +++ b/backend/src/data/users/user-repository.ts @@ -2,10 +2,10 @@ import { DwengoEntityRepository } from '../dwengo-entity-repository.js'; import { User } from '../../entities/users/user.entity.js'; export class UserRepository extends DwengoEntityRepository { - public findByUsername(username: string): Promise { + public async findByUsername(username: string): Promise { return this.findOne({ username } as Partial); } - public deleteByUsername(username: string): Promise { + public async deleteByUsername(username: string): Promise { return this.deleteWhere({ username } as Partial); } } diff --git a/backend/src/entities/assignments/assignment.entity.ts b/backend/src/entities/assignments/assignment.entity.ts index daa71ed6..36b24344 100644 --- a/backend/src/entities/assignments/assignment.entity.ts +++ b/backend/src/entities/assignments/assignment.entity.ts @@ -1,7 +1,7 @@ import { Entity, Enum, ManyToOne, OneToMany, PrimaryKey, Property } from '@mikro-orm/core'; import { Class } from '../classes/class.entity.js'; import { Group } from './group.entity.js'; -import { Language } from '../content/language.js'; +import { Language } from '@dwengo-1/common/util/language'; import { AssignmentRepository } from '../../data/assignments/assignment-repository.js'; @Entity({ diff --git a/backend/src/entities/assignments/submission.entity.ts b/backend/src/entities/assignments/submission.entity.ts index fbaa2791..80b9a8fb 100644 --- a/backend/src/entities/assignments/submission.entity.ts +++ b/backend/src/entities/assignments/submission.entity.ts @@ -1,8 +1,8 @@ import { Student } from '../users/student.entity.js'; import { Group } from './group.entity.js'; import { Entity, Enum, ManyToOne, PrimaryKey, Property } from '@mikro-orm/core'; -import { Language } from '../content/language.js'; import { SubmissionRepository } from '../../data/assignments/submission-repository.js'; +import { Language } from '@dwengo-1/common/util/language'; @Entity({ repository: () => SubmissionRepository }) export class Submission { @@ -16,7 +16,7 @@ export class Submission { learningObjectLanguage!: Language; @PrimaryKey({ type: 'numeric' }) - learningObjectVersion: number = 1; + learningObjectVersion = 1; @PrimaryKey({ type: 'integer', autoincrement: true }) submissionNumber?: number; diff --git a/backend/src/entities/classes/class-join-request.entity.ts b/backend/src/entities/classes/class-join-request.entity.ts index fdf13aa9..907c0199 100644 --- a/backend/src/entities/classes/class-join-request.entity.ts +++ b/backend/src/entities/classes/class-join-request.entity.ts @@ -2,12 +2,7 @@ import { Entity, Enum, ManyToOne } from '@mikro-orm/core'; import { Student } from '../users/student.entity.js'; import { Class } from './class.entity.js'; import { ClassJoinRequestRepository } from '../../data/classes/class-join-request-repository.js'; - -export enum ClassJoinRequestStatus { - Open = 'open', - Accepted = 'accepted', - Declined = 'declined', -} +import { ClassJoinRequestStatus } from '@dwengo-1/common/util/class-join-request'; @Entity({ repository: () => ClassJoinRequestRepository, diff --git a/backend/src/entities/content/educational-goal.entity.ts b/backend/src/entities/content/educational-goal.entity.ts new file mode 100644 index 00000000..fafe1a01 --- /dev/null +++ b/backend/src/entities/content/educational-goal.entity.ts @@ -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; +} diff --git a/backend/src/entities/content/learning-object-identifier.ts b/backend/src/entities/content/learning-object-identifier.ts index 3c020bd7..09a9c057 100644 --- a/backend/src/entities/content/learning-object-identifier.ts +++ b/backend/src/entities/content/learning-object-identifier.ts @@ -1,9 +1,11 @@ -import { Language } from './language.js'; +import { Language } from '@dwengo-1/common/util/language'; export class LearningObjectIdentifier { constructor( public hruid: string, public language: Language, public version: number - ) {} + ) { + // Do nothing + } } diff --git a/backend/src/entities/content/learning-object.entity.ts b/backend/src/entities/content/learning-object.entity.ts index 9eda22ba..ff858fe6 100644 --- a/backend/src/entities/content/learning-object.entity.ts +++ b/backend/src/entities/content/learning-object.entity.ts @@ -1,28 +1,12 @@ -import { Embeddable, Embedded, Entity, Enum, ManyToMany, OneToMany, PrimaryKey, Property } from '@mikro-orm/core'; -import { Language } from './language.js'; +import { Embedded, Entity, Enum, ManyToMany, OneToMany, PrimaryKey, Property } from '@mikro-orm/core'; 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'; +import { Language } from '@dwengo-1/common/util/language'; @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; diff --git a/backend/src/entities/content/learning-path-node.entity.ts b/backend/src/entities/content/learning-path-node.entity.ts index 03499270..3016c367 100644 --- a/backend/src/entities/content/learning-path-node.entity.ts +++ b/backend/src/entities/content/learning-path-node.entity.ts @@ -1,7 +1,7 @@ import { Entity, Enum, ManyToOne, OneToMany, PrimaryKey, Property, Rel } from '@mikro-orm/core'; -import { Language } from './language.js'; import { LearningPath } from './learning-path.entity.js'; import { LearningPathTransition } from './learning-path-transition.entity.js'; +import { Language } from '@dwengo-1/common/util/language'; @Entity() export class LearningPathNode { diff --git a/backend/src/entities/content/learning-path.entity.ts b/backend/src/entities/content/learning-path.entity.ts index 888cc0cf..203af86d 100644 --- a/backend/src/entities/content/learning-path.entity.ts +++ b/backend/src/entities/content/learning-path.entity.ts @@ -1,8 +1,8 @@ import { Entity, Enum, ManyToMany, OneToMany, PrimaryKey, Property } from '@mikro-orm/core'; -import { Language } from './language.js'; import { Teacher } from '../users/teacher.entity.js'; import { LearningPathRepository } from '../../data/content/learning-path-repository.js'; import { LearningPathNode } from './learning-path-node.entity.js'; +import { Language } from '@dwengo-1/common/util/language'; @Entity({ repository: () => LearningPathRepository }) export class LearningPath { diff --git a/backend/src/entities/content/return-value.entity.ts b/backend/src/entities/content/return-value.entity.ts new file mode 100644 index 00000000..d38b0693 --- /dev/null +++ b/backend/src/entities/content/return-value.entity.ts @@ -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; +} diff --git a/backend/src/entities/questions/question.entity.ts b/backend/src/entities/questions/question.entity.ts index 058ba6b3..5e691f70 100644 --- a/backend/src/entities/questions/question.entity.ts +++ b/backend/src/entities/questions/question.entity.ts @@ -1,7 +1,7 @@ import { Entity, Enum, ManyToOne, PrimaryKey, Property } from '@mikro-orm/core'; -import { Language } from '../content/language.js'; import { Student } from '../users/student.entity.js'; import { QuestionRepository } from '../../data/questions/question-repository.js'; +import { Language } from '@dwengo-1/common/util/language'; @Entity({ repository: () => QuestionRepository }) export class Question { @@ -15,7 +15,7 @@ export class Question { learningObjectLanguage!: Language; @PrimaryKey({ type: 'number' }) - learningObjectVersion: number = 1; + learningObjectVersion = 1; @PrimaryKey({ type: 'integer', autoincrement: true }) sequenceNumber?: number; diff --git a/backend/src/entities/users/user.entity.ts b/backend/src/entities/users/user.entity.ts index 1f35a0f8..15637110 100644 --- a/backend/src/entities/users/user.entity.ts +++ b/backend/src/entities/users/user.entity.ts @@ -6,8 +6,8 @@ export abstract class User { username!: string; @Property() - firstName: string = ''; + firstName = ''; @Property() - lastName: string = ''; + lastName = ''; } diff --git a/backend/src/exceptions/forbidden-exception.ts b/backend/src/exceptions/forbidden-exception.ts index 5712e0c8..4c58d1d5 100644 --- a/backend/src/exceptions/forbidden-exception.ts +++ b/backend/src/exceptions/forbidden-exception.ts @@ -6,7 +6,7 @@ import { ExceptionWithHttpState } from './exception-with-http-state.js'; export class ForbiddenException extends ExceptionWithHttpState { status = 403; - constructor(message: string = 'Forbidden') { + constructor(message = 'Forbidden') { super(403, message); } } diff --git a/backend/src/exceptions/unauthorized-exception.ts b/backend/src/exceptions/unauthorized-exception.ts index 7ea9aca8..54aa7cf9 100644 --- a/backend/src/exceptions/unauthorized-exception.ts +++ b/backend/src/exceptions/unauthorized-exception.ts @@ -4,7 +4,7 @@ import { ExceptionWithHttpState } from './exception-with-http-state.js'; * Exception for HTTP 401 Unauthorized */ export class UnauthorizedException extends ExceptionWithHttpState { - constructor(message: string = 'Unauthorized') { + constructor(message = 'Unauthorized') { super(401, message); } } diff --git a/backend/src/interfaces/answer.ts b/backend/src/interfaces/answer.ts index 493fd3c0..1f0d0625 100644 --- a/backend/src/interfaces/answer.ts +++ b/backend/src/interfaces/answer.ts @@ -1,14 +1,7 @@ -import { mapToUserDTO, UserDTO } from './user.js'; -import { mapToQuestionDTO, mapToQuestionId, QuestionDTO, QuestionId } from './question.js'; +import { mapToUserDTO } from './user.js'; +import { mapToQuestionDTO, mapToQuestionDTOId } from './question.js'; import { Answer } from '../entities/questions/answer.entity.js'; - -export interface AnswerDTO { - author: UserDTO; - toQuestion: QuestionDTO; - sequenceNumber: number; - timestamp: string; - content: string; -} +import { AnswerDTO, AnswerId } from '@dwengo-1/common/interfaces/answer'; /** * Convert a Question entity to a DTO format. @@ -23,16 +16,10 @@ export function mapToAnswerDTO(answer: Answer): AnswerDTO { }; } -export interface AnswerId { - author: string; - toQuestion: QuestionId; - sequenceNumber: number; -} - -export function mapToAnswerId(answer: AnswerDTO): AnswerId { +export function mapToAnswerDTOId(answer: Answer): AnswerId { return { author: answer.author.username, - toQuestion: mapToQuestionId(answer.toQuestion), - sequenceNumber: answer.sequenceNumber, + toQuestion: mapToQuestionDTOId(answer.toQuestion), + sequenceNumber: answer.sequenceNumber!, }; } diff --git a/backend/src/interfaces/assignment.ts b/backend/src/interfaces/assignment.ts index eefa8c96..d48a9083 100644 --- a/backend/src/interfaces/assignment.ts +++ b/backend/src/interfaces/assignment.ts @@ -1,18 +1,9 @@ +import { languageMap } from '@dwengo-1/common/util/language'; 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'; - -export interface AssignmentDTO { - id: number; - class: string; // Id of class 'within' - title: string; - description: string; - learningPath: string; - language: string; - groups?: GroupDTO[] | string[]; // TODO -} +import { getLogger } from '../logging/initalize.js'; +import { AssignmentDTO } from '@dwengo-1/common/interfaces/assignment'; export function mapToAssignmentDTOId(assignment: Assignment): AssignmentDTO { return { @@ -46,5 +37,7 @@ export function mapToAssignment(assignmentData: AssignmentDTO, cls: Class): Assi assignment.learningPathLanguage = languageMap[assignmentData.language] || FALLBACK_LANG; assignment.within = cls; + getLogger().debug(assignment); + return assignment; } diff --git a/backend/src/interfaces/class.ts b/backend/src/interfaces/class.ts index ea1d4901..7b07fcf2 100644 --- a/backend/src/interfaces/class.ts +++ b/backend/src/interfaces/class.ts @@ -2,14 +2,7 @@ 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[]; -} +import { ClassDTO } from '@dwengo-1/common/interfaces/class'; export function mapToClassDTO(cls: Class): ClassDTO { return { diff --git a/backend/src/interfaces/group.ts b/backend/src/interfaces/group.ts index a25c5b8e..1a169b2b 100644 --- a/backend/src/interfaces/group.ts +++ b/backend/src/interfaces/group.ts @@ -1,12 +1,7 @@ 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[]; -} +import { mapToAssignmentDTO } from './assignment.js'; +import { mapToStudentDTO } from './student.js'; +import { GroupDTO } from '@dwengo-1/common/interfaces/group'; export function mapToGroupDTO(group: Group): GroupDTO { return { diff --git a/backend/src/interfaces/question.ts b/backend/src/interfaces/question.ts index 0da87eb7..48d64f11 100644 --- a/backend/src/interfaces/question.ts +++ b/backend/src/interfaces/question.ts @@ -1,24 +1,21 @@ import { Question } from '../entities/questions/question.entity.js'; -import { LearningObjectIdentifier } from '../entities/content/learning-object-identifier.js'; -import { mapToStudentDTO, StudentDTO } from './student.js'; +import { mapToStudentDTO } from './student.js'; +import { QuestionDTO, QuestionId } from '@dwengo-1/common/interfaces/question'; +import { LearningObjectIdentifier } from '@dwengo-1/common/interfaces/learning-content'; -export interface QuestionDTO { - learningObjectIdentifier: LearningObjectIdentifier; - sequenceNumber?: number; - author: StudentDTO; - timestamp?: string; - content: string; +function getLearningObjectIdentifier(question: Question): LearningObjectIdentifier { + return { + hruid: question.learningObjectHruid, + language: question.learningObjectLanguage, + version: question.learningObjectVersion, + }; } /** * 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, - }; + const learningObjectIdentifier = getLearningObjectIdentifier(question); return { learningObjectIdentifier, @@ -29,14 +26,11 @@ export function mapToQuestionDTO(question: Question): QuestionDTO { }; } -export interface QuestionId { - learningObjectIdentifier: LearningObjectIdentifier; - sequenceNumber: number; -} +export function mapToQuestionDTOId(question: Question): QuestionId { + const learningObjectIdentifier = getLearningObjectIdentifier(question); -export function mapToQuestionId(question: QuestionDTO): QuestionId { return { - learningObjectIdentifier: question.learningObjectIdentifier, + learningObjectIdentifier, sequenceNumber: question.sequenceNumber!, }; } diff --git a/backend/src/interfaces/student-request.ts b/backend/src/interfaces/student-request.ts new file mode 100644 index 00000000..d97f5eb5 --- /dev/null +++ b/backend/src/interfaces/student-request.ts @@ -0,0 +1,23 @@ +import { mapToStudentDTO } from './student.js'; +import { ClassJoinRequest } from '../entities/classes/class-join-request.entity.js'; +import { getClassJoinRequestRepository } from '../data/repositories.js'; +import { Student } from '../entities/users/student.entity.js'; +import { Class } from '../entities/classes/class.entity.js'; +import { ClassJoinRequestDTO } from '@dwengo-1/common/interfaces/class-join-request'; +import { ClassJoinRequestStatus } from '@dwengo-1/common/util/class-join-request'; + +export function mapToStudentRequestDTO(request: ClassJoinRequest): ClassJoinRequestDTO { + return { + requester: mapToStudentDTO(request.requester), + class: request.class.classId!, + status: request.status, + }; +} + +export function mapToStudentRequest(student: Student, cls: Class): ClassJoinRequest { + return getClassJoinRequestRepository().create({ + requester: student, + class: cls, + status: ClassJoinRequestStatus.Open, + }); +} diff --git a/backend/src/interfaces/student.ts b/backend/src/interfaces/student.ts index ecce8f89..06e173a1 100644 --- a/backend/src/interfaces/student.ts +++ b/backend/src/interfaces/student.ts @@ -1,18 +1,6 @@ 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; - }; -} +import { StudentDTO } from '@dwengo-1/common/interfaces/student'; export function mapToStudentDTO(student: Student): StudentDTO { return { diff --git a/backend/src/interfaces/submission.ts b/backend/src/interfaces/submission.ts index 98cc4f22..b4ed4a2b 100644 --- a/backend/src/interfaces/submission.ts +++ b/backend/src/interfaces/submission.ts @@ -1,26 +1,7 @@ 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; -} +import { mapToGroupDTO } from './group.js'; +import { mapToStudent, mapToStudentDTO } from './student.js'; +import { SubmissionDTO, SubmissionDTOId } from '@dwengo-1/common/interfaces/submission'; export function mapToSubmissionDTO(submission: Submission): SubmissionDTO { return { diff --git a/backend/src/interfaces/teacher-invitation.ts b/backend/src/interfaces/teacher-invitation.ts index cddef566..d9cb9915 100644 --- a/backend/src/interfaces/teacher-invitation.ts +++ b/backend/src/interfaces/teacher-invitation.ts @@ -1,12 +1,7 @@ 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; -} +import { mapToClassDTO } from './class.js'; +import { mapToUserDTO } from './user.js'; +import { TeacherInvitationDTO } from '@dwengo-1/common/interfaces/teacher-invitation'; export function mapToTeacherInvitationDTO(invitation: TeacherInvitation): TeacherInvitationDTO { return { diff --git a/backend/src/interfaces/teacher.ts b/backend/src/interfaces/teacher.ts index 31b4723f..f7e1745f 100644 --- a/backend/src/interfaces/teacher.ts +++ b/backend/src/interfaces/teacher.ts @@ -1,18 +1,6 @@ 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; - }; -} +import { TeacherDTO } from '@dwengo-1/common/interfaces/teacher'; export function mapToTeacherDTO(teacher: Teacher): TeacherDTO { return { diff --git a/backend/src/interfaces/user.ts b/backend/src/interfaces/user.ts index 58f0dd5a..f4413b5e 100644 --- a/backend/src/interfaces/user.ts +++ b/backend/src/interfaces/user.ts @@ -1,17 +1,5 @@ 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; - }; -} +import { UserDTO } from '@dwengo-1/common/interfaces/user'; export function mapToUserDTO(user: User): UserDTO { return { diff --git a/backend/src/logging/initalize.ts b/backend/src/logging/initalize.ts index 4d14e8ab..5c94a25f 100644 --- a/backend/src/logging/initalize.ts +++ b/backend/src/logging/initalize.ts @@ -1,7 +1,7 @@ import { createLogger, format, Logger as WinstonLogger, transports } from 'winston'; import LokiTransport from 'winston-loki'; import { LokiLabels } from 'loki-logger-ts'; -import { EnvVars, getEnvVar } from '../util/envvars.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,28 @@ function initializeLogger(): Logger { return logger; } - const logLevel = getEnvVar(EnvVars.LogLevel); + const logLevel = getEnvVar(envVars.LogLevel); const consoleTransport = new transports.Console({ - level: getEnvVar(EnvVars.LogLevel), + level: getEnvVar(envVars.LogLevel), format: format.combine(format.cli(), format.colorize()), }); - if (getEnvVar(EnvVars.RunMode) === 'dev') { + if (getEnvVar(envVars.RunMode) === 'dev') { return createLogger({ transports: [consoleTransport], }); } - const lokiHost = getEnvVar(EnvVars.LokiHost); + const lokiHost = getEnvVar(envVars.LokiHost); const lokiTransport: LokiTransport = new LokiTransport({ host: lokiHost, - labels: Labels, + 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}`); }, diff --git a/backend/src/logging/mikroOrmLogger.ts b/backend/src/logging/mikroOrmLogger.ts index 25bbac13..9cb797a8 100644 --- a/backend/src/logging/mikroOrmLogger.ts +++ b/backend/src/logging/mikroOrmLogger.ts @@ -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, - }; - } } diff --git a/backend/src/logging/responseTimeLogger.ts b/backend/src/logging/responseTimeLogger.ts index c1bb1e33..7fcc6c93 100644 --- a/backend/src/logging/responseTimeLogger.ts +++ b/backend/src/logging/responseTimeLogger.ts @@ -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; diff --git a/backend/src/middleware/auth/auth.ts b/backend/src/middleware/auth/auth.ts index 4e651b4b..a91932ea 100644 --- a/backend/src/middleware/auth/auth.ts +++ b/backend/src/middleware/auth/auth.ts @@ -1,9 +1,9 @@ -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 { UnauthorizedException } from '../../exceptions/unauthorized-exception.js'; @@ -33,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)), }, }; @@ -64,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, @@ -75,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'; @@ -85,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]!, @@ -101,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]; @@ -114,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)) { @@ -125,6 +125,7 @@ export const authorize = next(); } }; +} /** * Middleware which rejects all unauthenticated users, but accepts all authenticated users. diff --git a/backend/src/middleware/auth/authentication-info.d.ts b/backend/src/middleware/auth/authentication-info.d.ts index 4b060dfa..e8f0d48c 100644 --- a/backend/src/middleware/auth/authentication-info.d.ts +++ b/backend/src/middleware/auth/authentication-info.d.ts @@ -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; -}; +} diff --git a/backend/src/middleware/cors.ts b/backend/src/middleware/cors.ts index 3d2c9be0..48e0704d 100644 --- a/backend/src/middleware/cors.ts +++ b/backend/src/middleware/cors.ts @@ -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(','), }); diff --git a/backend/src/middleware/error-handling/error-handler.ts b/backend/src/middleware/error-handling/error-handler.ts index f2806938..d7315603 100644 --- a/backend/src/middleware/error-handling/error-handler.ts +++ b/backend/src/middleware/error-handling/error-handler.ts @@ -4,7 +4,7 @@ import { ExceptionWithHttpState } from '../../exceptions/exception-with-http-sta const logger: Logger = getLogger(); -export function errorHandler(err: unknown, req: Request, res: Response, _: NextFunction): void { +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); diff --git a/backend/src/mikro-orm.config.ts b/backend/src/mikro-orm.config.ts index 9e7800b0..eb0e5f7a 100644 --- a/backend/src/mikro-orm.config.ts +++ b/backend/src/mikro-orm.config.ts @@ -1,6 +1,6 @@ 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'; @@ -42,11 +42,11 @@ 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`. @@ -54,23 +54,23 @@ function config(testingMode: boolean = false): Options { // 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: getEnvVar(EnvVars.LogLevel) === 'debug', + debug: getEnvVar(envVars.LogLevel) === 'debug', loggerFactory: (options: LoggerOptions) => new MikroOrmLogger(options), }; } diff --git a/backend/src/orm.ts b/backend/src/orm.ts index 93feea7a..3e6e26c8 100644 --- a/backend/src/orm.ts +++ b/backend/src/orm.ts @@ -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 { 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(); diff --git a/backend/src/routes/assignments.ts b/backend/src/routes/assignments.ts index a733d093..3652dcc6 100644 --- a/backend/src/routes/assignments.ts +++ b/backend/src/routes/assignments.ts @@ -19,7 +19,7 @@ router.get('/:id', getAssignmentHandler); router.get('/:id/submissions', getAssignmentsSubmissionsHandler); -router.get('/:id/questions', (req, res) => { +router.get('/:id/questions', (_req, res) => { res.json({ questions: ['0'], }); diff --git a/backend/src/routes/auth.ts b/backend/src/routes/auth.ts index 778e51fd..4a1f27d2 100644 --- a/backend/src/routes/auth.ts +++ b/backend/src/routes/auth.ts @@ -4,21 +4,21 @@ 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!' }); }); diff --git a/backend/src/routes/groups.ts b/backend/src/routes/groups.ts index 0c9692b0..1486edce 100644 --- a/backend/src/routes/groups.ts +++ b/backend/src/routes/groups.ts @@ -11,10 +11,10 @@ router.post('/', createGroupHandler); // Information about a group (members, ... [TODO DOC]) router.get('/:groupid', getGroupHandler); -router.get('/:groupid', getGroupSubmissionsHandler); +router.get('/:groupid/submissions', getGroupSubmissionsHandler); // The list of questions a group has made -router.get('/:id/questions', (req, res) => { +router.get('/:id/questions', (_req, res) => { res.json({ questions: ['0'], }); diff --git a/backend/src/routes/student-join-requests.ts b/backend/src/routes/student-join-requests.ts new file mode 100644 index 00000000..daf79f09 --- /dev/null +++ b/backend/src/routes/student-join-requests.ts @@ -0,0 +1,19 @@ +import express from 'express'; +import { + createStudentRequestHandler, + deleteClassJoinRequestHandler, + getStudentRequestHandler, + getStudentRequestsHandler, +} from '../controllers/students.js'; + +const router = express.Router({ mergeParams: true }); + +router.get('/', getStudentRequestsHandler); + +router.post('/', createStudentRequestHandler); + +router.get('/:classId', getStudentRequestHandler); + +router.delete('/:classId', deleteClassJoinRequestHandler); + +export default router; diff --git a/backend/src/routes/students.ts b/backend/src/routes/students.ts index 6efbab39..0f5d5349 100644 --- a/backend/src/routes/students.ts +++ b/backend/src/routes/students.ts @@ -7,8 +7,11 @@ import { getStudentClassesHandler, getStudentGroupsHandler, getStudentHandler, + getStudentQuestionsHandler, getStudentSubmissionsHandler, } from '../controllers/students.js'; +import joinRequestRouter from './student-join-requests.js'; + const router = express.Router(); // Root endpoint used to search objects @@ -16,30 +19,26 @@ router.get('/', getAllStudentsHandler); router.post('/', createStudentHandler); -router.delete('/', deleteStudentHandler); - router.delete('/:username', deleteStudentHandler); // Information about a student's profile router.get('/:username', getStudentHandler); // The list of classes a student is in -router.get('/:id/classes', getStudentClassesHandler); +router.get('/:username/classes', getStudentClassesHandler); // The list of submissions a student has made -router.get('/:id/submissions', getStudentSubmissionsHandler); +router.get('/:username/submissions', getStudentSubmissionsHandler); // The list of assignments a student has -router.get('/:id/assignments', getStudentAssignmentsHandler); +router.get('/:username/assignments', getStudentAssignmentsHandler); // The list of groups a student is in -router.get('/:id/groups', getStudentGroupsHandler); +router.get('/:username/groups', getStudentGroupsHandler); // A list of questions a user has created -router.get('/:id/questions', (req, res) => { - res.json({ - questions: ['0'], - }); -}); +router.get('/:username/questions', getStudentQuestionsHandler); + +router.use('/:username/joinRequests', joinRequestRouter); export default router; diff --git a/backend/src/routes/submissions.ts b/backend/src/routes/submissions.ts index 4db93027..8e9831b9 100644 --- a/backend/src/routes/submissions.ts +++ b/backend/src/routes/submissions.ts @@ -3,7 +3,7 @@ import { createSubmissionHandler, deleteSubmissionHandler, getSubmissionHandler const router = express.Router({ mergeParams: true }); // Root endpoint used to search objects -router.get('/', (req, res) => { +router.get('/', (_req, res) => { res.json({ submissions: ['0', '1'], }); diff --git a/backend/src/routes/teachers.ts b/backend/src/routes/teachers.ts index c04e1575..a6106a80 100644 --- a/backend/src/routes/teachers.ts +++ b/backend/src/routes/teachers.ts @@ -3,10 +3,12 @@ import { createTeacherHandler, deleteTeacherHandler, getAllTeachersHandler, + getStudentJoinRequestHandler, getTeacherClassHandler, getTeacherHandler, getTeacherQuestionHandler, getTeacherStudentHandler, + updateStudentJoinRequestHandler, } from '../controllers/teachers.js'; const router = express.Router(); @@ -15,8 +17,6 @@ router.get('/', getAllTeachersHandler); router.post('/', createTeacherHandler); -router.delete('/', deleteTeacherHandler); - router.get('/:username', getTeacherHandler); router.delete('/:username', deleteTeacherHandler); @@ -27,8 +27,12 @@ router.get('/:username/students', getTeacherStudentHandler); router.get('/:username/questions', getTeacherQuestionHandler); +router.get('/:username/joinRequests/:classId', getStudentJoinRequestHandler); + +router.put('/:username/joinRequests/:classId/:studentUsername', updateStudentJoinRequestHandler); + // Invitations to other classes a teacher received -router.get('/:id/invitations', (req, res) => { +router.get('/:id/invitations', (_req, res) => { res.json({ invitations: ['0'], }); diff --git a/backend/src/services/assignments.ts b/backend/src/services/assignments.ts index a21a96fa..e86b69b2 100644 --- a/backend/src/services/assignments.ts +++ b/backend/src/services/assignments.ts @@ -1,6 +1,9 @@ import { getAssignmentRepository, getClassRepository, getGroupRepository, getSubmissionRepository } from '../data/repositories.js'; -import { AssignmentDTO, mapToAssignment, mapToAssignmentDTO, mapToAssignmentDTOId } from '../interfaces/assignment.js'; -import { mapToSubmissionDTO, mapToSubmissionDTOId, SubmissionDTO, SubmissionDTOId } from '../interfaces/submission.js'; +import { mapToAssignment, mapToAssignmentDTO, mapToAssignmentDTOId } from '../interfaces/assignment.js'; +import { mapToSubmissionDTO, mapToSubmissionDTOId } from '../interfaces/submission.js'; +import { AssignmentDTO } from '@dwengo-1/common/interfaces/assignment'; +import { SubmissionDTO, SubmissionDTOId } from '@dwengo-1/common/interfaces/submission'; +import { getLogger } from '../logging/initalize.js'; export async function getAllAssignments(classid: string, full: boolean): Promise { const classRepository = getClassRepository(); @@ -37,7 +40,7 @@ export async function createAssignment(classid: string, assignmentData: Assignme return mapToAssignmentDTO(newAssignment); } catch (e) { - console.error(e); + getLogger().error(e); return null; } } @@ -83,7 +86,7 @@ export async function getAssignmentsSubmissions( const groups = await groupRepository.findAllGroupsForAssignment(assignment); const submissionRepository = getSubmissionRepository(); - const submissions = (await Promise.all(groups.map((group) => submissionRepository.findAllSubmissionsForGroup(group)))).flat(); + const submissions = (await Promise.all(groups.map(async (group) => submissionRepository.findAllSubmissionsForGroup(group)))).flat(); if (full) { return submissions.map(mapToSubmissionDTO); diff --git a/backend/src/services/classes.ts b/backend/src/services/classes.ts index 5b1e3cfc..754277cf 100644 --- a/backend/src/services/classes.ts +++ b/backend/src/services/classes.ts @@ -1,11 +1,27 @@ import { getClassRepository, getStudentRepository, getTeacherInvitationRepository, getTeacherRepository } from '../data/repositories.js'; -import { ClassDTO, mapToClassDTO } from '../interfaces/class.js'; -import { mapToStudentDTO, StudentDTO } from '../interfaces/student.js'; -import { mapToTeacherInvitationDTO, mapToTeacherInvitationDTOIds, TeacherInvitationDTO } from '../interfaces/teacher-invitation.js'; +import { mapToClassDTO } from '../interfaces/class.js'; +import { mapToStudentDTO } from '../interfaces/student.js'; +import { mapToTeacherInvitationDTO, mapToTeacherInvitationDTOIds } from '../interfaces/teacher-invitation.js'; import { getLogger } from '../logging/initalize.js'; +import { NotFoundException } from '../exceptions/not-found-exception.js'; +import { Class } from '../entities/classes/class.entity.js'; +import { ClassDTO } from '@dwengo-1/common/interfaces/class'; +import { TeacherInvitationDTO } from '@dwengo-1/common/interfaces/teacher-invitation'; +import { StudentDTO } from '@dwengo-1/common/interfaces/student'; const logger = getLogger(); +export async function fetchClass(classId: string): Promise { + const classRepository = getClassRepository(); + const cls = await classRepository.findById(classId); + + if (!cls) { + throw new NotFoundException('Class with id not found'); + } + + return cls; +} + export async function getAllClasses(full: boolean): Promise { const classRepository = getClassRepository(); const classes = await classRepository.find({}, { populate: ['students', 'teachers'] }); @@ -23,11 +39,15 @@ export async function getAllClasses(full: boolean): Promise { const teacherRepository = getTeacherRepository(); const teacherUsernames = classData.teachers || []; - const teachers = (await Promise.all(teacherUsernames.map((id) => teacherRepository.findByUsername(id)))).filter((teacher) => teacher !== null); + const teachers = (await Promise.all(teacherUsernames.map(async (id) => teacherRepository.findByUsername(id)))).filter( + (teacher) => teacher !== null + ); const studentRepository = getStudentRepository(); const studentUsernames = classData.students || []; - const students = (await Promise.all(studentUsernames.map((id) => studentRepository.findByUsername(id)))).filter((student) => student !== null); + const students = (await Promise.all(studentUsernames.map(async (id) => studentRepository.findByUsername(id)))).filter( + (student) => student !== null + ); const classRepository = getClassRepository(); diff --git a/backend/src/services/groups.ts b/backend/src/services/groups.ts index 4a1cbbf0..346c1ee1 100644 --- a/backend/src/services/groups.ts +++ b/backend/src/services/groups.ts @@ -6,8 +6,11 @@ import { getSubmissionRepository, } from '../data/repositories.js'; import { Group } from '../entities/assignments/group.entity.js'; -import { GroupDTO, mapToGroupDTO, mapToGroupDTOId } from '../interfaces/group.js'; -import { mapToSubmissionDTO, mapToSubmissionDTOId, SubmissionDTO, SubmissionDTOId } from '../interfaces/submission.js'; +import { mapToGroupDTO, mapToGroupDTOId } from '../interfaces/group.js'; +import { mapToSubmissionDTO, mapToSubmissionDTOId } from '../interfaces/submission.js'; +import { GroupDTO } from '@dwengo-1/common/interfaces/group'; +import { SubmissionDTO, SubmissionDTOId } from '@dwengo-1/common/interfaces/submission'; +import { getLogger } from '../logging/initalize.js'; export async function getGroup(classId: string, assignmentNumber: number, groupNumber: number, full: boolean): Promise { const classRepository = getClassRepository(); @@ -42,9 +45,11 @@ export async function createGroup(groupData: GroupDTO, classid: string, assignme const studentRepository = getStudentRepository(); const memberUsernames = (groupData.members as string[]) || []; // TODO check if groupdata.members is a list - const members = (await Promise.all([...memberUsernames].map((id) => studentRepository.findByUsername(id)))).filter((student) => student !== null); + const members = (await Promise.all([...memberUsernames].map(async (id) => studentRepository.findByUsername(id)))).filter( + (student) => student !== null + ); - console.log(members); + getLogger().debug(members); const classRepository = getClassRepository(); const cls = await classRepository.findById(classid); @@ -70,7 +75,7 @@ export async function createGroup(groupData: GroupDTO, classid: string, assignme return newGroup; } catch (e) { - console.log(e); + getLogger().error(e); return null; } } @@ -94,8 +99,7 @@ export async function getAllGroups(classId: string, assignmentNumber: number, fu const groups = await groupRepository.findAllGroupsForAssignment(assignment); if (full) { - console.log('full'); - console.log(groups); + getLogger().debug({ full: full, groups: groups }); return groups.map(mapToGroupDTO); } diff --git a/backend/src/services/learning-objects.ts b/backend/src/services/learning-objects.ts index faa77cb4..436c4a08 100644 --- a/backend/src/services/learning-objects.ts +++ b/backend/src/services/learning-objects.ts @@ -1,6 +1,13 @@ import { DWENGO_API_BASE } from '../config.js'; import { fetchWithLogging } from '../util/api-helper.js'; -import { FilteredLearningObject, LearningObjectMetadata, LearningObjectNode, LearningPathResponse } from '../interfaces/learning-content.js'; + +import { + FilteredLearningObject, + LearningObjectMetadata, + LearningObjectNode, + LearningPathResponse, +} from '@dwengo-1/common/interfaces/learning-content'; +import { getLogger } from '../logging/initalize.js'; function filterData(data: LearningObjectMetadata, htmlUrl: string): FilteredLearningObject { return { @@ -37,7 +44,7 @@ export async function getLearningObjectById(hruid: string, language: string): Pr ); if (!metadata) { - console.error(`⚠️ WARNING: Learning object "${hruid}" not found.`); + getLogger().error(`⚠️ WARNING: Learning object "${hruid}" not found.`); return null; } @@ -48,7 +55,7 @@ export async function getLearningObjectById(hruid: string, language: string): Pr /** * Generic function to fetch learning paths */ -function fetchLearningPaths(arg0: string[], language: string, arg2: string): LearningPathResponse | PromiseLike { +function fetchLearningPaths(_arg0: string[], _language: string, _arg2: string): LearningPathResponse | PromiseLike { throw new Error('Function not implemented.'); } @@ -60,7 +67,7 @@ async function fetchLearningObjects(hruid: string, full: boolean, language: stri const learningPathResponse: LearningPathResponse = await fetchLearningPaths([hruid], language, `Learning path for HRUID "${hruid}"`); if (!learningPathResponse.success || !learningPathResponse.data?.length) { - console.error(`⚠️ WARNING: Learning path "${hruid}" exists but contains no learning objects.`); + getLogger().error(`⚠️ WARNING: Learning path "${hruid}" exists but contains no learning objects.`); return []; } @@ -74,7 +81,7 @@ async function fetchLearningObjects(hruid: string, full: boolean, language: stri objects.filter((obj): obj is FilteredLearningObject => obj !== null) ); } catch (error) { - console.error('❌ Error fetching learning objects:', error); + getLogger().error('❌ Error fetching learning objects:', error); return []; } } diff --git a/backend/src/services/learning-objects/attachment-service.ts b/backend/src/services/learning-objects/attachment-service.ts index aacc7187..46fc5e03 100644 --- a/backend/src/services/learning-objects/attachment-service.ts +++ b/backend/src/services/learning-objects/attachment-service.ts @@ -1,9 +1,10 @@ import { getAttachmentRepository } from '../../data/repositories.js'; import { Attachment } from '../../entities/content/attachment.entity.js'; -import { LearningObjectIdentifier } from '../../interfaces/learning-content.js'; + +import { LearningObjectIdentifier } from '@dwengo-1/common/interfaces/learning-content'; const attachmentService = { - getAttachment(learningObjectId: LearningObjectIdentifier, attachmentName: string): Promise { + async getAttachment(learningObjectId: LearningObjectIdentifier, attachmentName: string): Promise { const attachmentRepo = getAttachmentRepository(); if (learningObjectId.version) { diff --git a/backend/src/services/learning-objects/database-learning-object-provider.ts b/backend/src/services/learning-objects/database-learning-object-provider.ts index bab0b9b1..361153f5 100644 --- a/backend/src/services/learning-objects/database-learning-object-provider.ts +++ b/backend/src/services/learning-objects/database-learning-object-provider.ts @@ -1,13 +1,12 @@ import { LearningObjectProvider } from './learning-object-provider.js'; -import { FilteredLearningObject, LearningObjectIdentifier, LearningPathIdentifier } from '../../interfaces/learning-content.js'; import { getLearningObjectRepository, getLearningPathRepository } from '../../data/repositories.js'; -import { Language } from '../../entities/content/language.js'; import { LearningObject } from '../../entities/content/learning-object.entity.js'; import { getUrlStringForLearningObject } from '../../util/links.js'; import processingService from './processing/processing-service.js'; import { NotFoundError } from '@mikro-orm/core'; import learningObjectService from './learning-object-service.js'; import { getLogger, Logger } from '../../logging/initalize.js'; +import { FilteredLearningObject, LearningObjectIdentifier, LearningPathIdentifier } from '@dwengo-1/common/interfaces/learning-content'; const logger: Logger = getLogger(); @@ -41,10 +40,10 @@ function convertLearningObject(learningObject: LearningObject | null): FilteredL }; } -function findLearningObjectEntityById(id: LearningObjectIdentifier): Promise { +async function findLearningObjectEntityById(id: LearningObjectIdentifier): Promise { const learningObjectRepo = getLearningObjectRepository(); - return learningObjectRepo.findLatestByHruidAndLanguage(id.hruid, id.language as Language); + return learningObjectRepo.findLatestByHruidAndLanguage(id.hruid, id.language); } /** @@ -65,11 +64,11 @@ const databaseLearningObjectProvider: LearningObjectProvider = { async getLearningObjectHTML(id: LearningObjectIdentifier): Promise { const learningObjectRepo = getLearningObjectRepository(); - const learningObject = await learningObjectRepo.findLatestByHruidAndLanguage(id.hruid, id.language as Language); + const learningObject = await learningObjectRepo.findLatestByHruidAndLanguage(id.hruid, id.language); if (!learningObject) { return null; } - return await processingService.render(learningObject, (id) => findLearningObjectEntityById(id)); + return await processingService.render(learningObject, async (id) => findLearningObjectEntityById(id)); }, /** @@ -96,7 +95,7 @@ const databaseLearningObjectProvider: LearningObjectProvider = { throw new NotFoundError('The learning path with the given ID could not be found.'); } const learningObjects = await Promise.all( - learningPath.nodes.map((it) => { + learningPath.nodes.map(async (it) => { const learningObject = learningObjectService.getLearningObjectById({ hruid: it.learningObjectHruid, language: it.language, diff --git a/backend/src/services/learning-objects/dwengo-api-learning-object-provider.ts b/backend/src/services/learning-objects/dwengo-api-learning-object-provider.ts index dfee329d..d67b69ae 100644 --- a/backend/src/services/learning-objects/dwengo-api-learning-object-provider.ts +++ b/backend/src/services/learning-objects/dwengo-api-learning-object-provider.ts @@ -1,5 +1,8 @@ import { DWENGO_API_BASE } from '../../config.js'; import { fetchWithLogging } from '../../util/api-helper.js'; +import dwengoApiLearningPathProvider from '../learning-paths/dwengo-api-learning-path-provider.js'; +import { LearningObjectProvider } from './learning-object-provider.js'; +import { getLogger, Logger } from '../../logging/initalize.js'; import { FilteredLearningObject, LearningObjectIdentifier, @@ -7,10 +10,7 @@ import { LearningObjectNode, LearningPathIdentifier, LearningPathResponse, -} from '../../interfaces/learning-content.js'; -import dwengoApiLearningPathProvider from '../learning-paths/dwengo-api-learning-path-provider.js'; -import { LearningObjectProvider } from './learning-object-provider.js'; -import { getLogger, Logger } from '../../logging/initalize.js'; +} from '@dwengo-1/common/interfaces/learning-content'; const logger: Logger = getLogger(); @@ -66,12 +66,13 @@ async function fetchLearningObjects(learningPathId: LearningPathIdentifier, full } const objects = await Promise.all( - nodes.map(async (node) => - dwengoApiLearningObjectProvider.getLearningObjectById({ + nodes.map(async (node) => { + const learningObjectId: LearningObjectIdentifier = { hruid: node.learningobject_hruid, language: learningPathId.language, - }) - ) + }; + return dwengoApiLearningObjectProvider.getLearningObjectById(learningObjectId); + }) ); return objects.filter((obj): obj is FilteredLearningObject => obj !== null); } catch (error) { @@ -90,7 +91,7 @@ const dwengoApiLearningObjectProvider: LearningObjectProvider = { metadataUrl, `Metadata for Learning Object HRUID "${id.hruid}" (language ${id.language})`, { - params: id, + params: { ...id }, } ); @@ -123,7 +124,7 @@ const dwengoApiLearningObjectProvider: LearningObjectProvider = { async getLearningObjectHTML(id: LearningObjectIdentifier): Promise { const htmlUrl = `${DWENGO_API_BASE}/learningObject/getRaw`; const html = await fetchWithLogging(htmlUrl, `Metadata for Learning Object HRUID "${id.hruid}" (language ${id.language})`, { - params: id, + params: { ...id }, }); if (!html) { diff --git a/backend/src/services/learning-objects/learning-object-provider.ts b/backend/src/services/learning-objects/learning-object-provider.ts index 81b4d228..a0fcb552 100644 --- a/backend/src/services/learning-objects/learning-object-provider.ts +++ b/backend/src/services/learning-objects/learning-object-provider.ts @@ -1,4 +1,4 @@ -import { FilteredLearningObject, LearningObjectIdentifier, LearningPathIdentifier } from '../../interfaces/learning-content.js'; +import { FilteredLearningObject, LearningObjectIdentifier, LearningPathIdentifier } from '@dwengo-1/common/interfaces/learning-content'; export interface LearningObjectProvider { /** diff --git a/backend/src/services/learning-objects/learning-object-service.ts b/backend/src/services/learning-objects/learning-object-service.ts index 8289660b..5a06f0f2 100644 --- a/backend/src/services/learning-objects/learning-object-service.ts +++ b/backend/src/services/learning-objects/learning-object-service.ts @@ -1,11 +1,11 @@ -import { FilteredLearningObject, LearningObjectIdentifier, LearningPathIdentifier } from '../../interfaces/learning-content.js'; import dwengoApiLearningObjectProvider from './dwengo-api-learning-object-provider.js'; import { LearningObjectProvider } from './learning-object-provider.js'; -import { EnvVars, getEnvVar } from '../../util/envvars.js'; +import { envVars, getEnvVar } from '../../util/envVars.js'; import databaseLearningObjectProvider from './database-learning-object-provider.js'; +import { FilteredLearningObject, LearningObjectIdentifier, LearningPathIdentifier } from '@dwengo-1/common/interfaces/learning-content'; function getProvider(id: LearningObjectIdentifier): LearningObjectProvider { - if (id.hruid.startsWith(getEnvVar(EnvVars.UserContentPrefix))) { + if (id.hruid.startsWith(getEnvVar(envVars.UserContentPrefix))) { return databaseLearningObjectProvider; } return dwengoApiLearningObjectProvider; @@ -18,28 +18,28 @@ const learningObjectService = { /** * Fetches a single learning object by its HRUID */ - getLearningObjectById(id: LearningObjectIdentifier): Promise { + async getLearningObjectById(id: LearningObjectIdentifier): Promise { return getProvider(id).getLearningObjectById(id); }, /** * Fetch full learning object data (metadata) */ - getLearningObjectsFromPath(id: LearningPathIdentifier): Promise { + async getLearningObjectsFromPath(id: LearningPathIdentifier): Promise { return getProvider(id).getLearningObjectsFromPath(id); }, /** * Fetch only learning object HRUIDs */ - getLearningObjectIdsFromPath(id: LearningPathIdentifier): Promise { + async getLearningObjectIdsFromPath(id: LearningPathIdentifier): Promise { return getProvider(id).getLearningObjectIdsFromPath(id); }, /** * Obtain a HTML-rendering of the learning object with the given identifier (as a string). */ - getLearningObjectHTML(id: LearningObjectIdentifier): Promise { + async getLearningObjectHTML(id: LearningObjectIdentifier): Promise { return getProvider(id).getLearningObjectHTML(id); }, }; diff --git a/backend/src/services/learning-objects/processing/audio/audio-processor.ts b/backend/src/services/learning-objects/processing/audio/audio-processor.ts index 592669d5..227eae13 100644 --- a/backend/src/services/learning-objects/processing/audio/audio-processor.ts +++ b/backend/src/services/learning-objects/processing/audio/audio-processor.ts @@ -14,7 +14,7 @@ class AudioProcessor extends StringProcessor { super(DwengoContentType.AUDIO_MPEG); } - protected renderFn(audioUrl: string): string { + override renderFn(audioUrl: string): string { return DOMPurify.sanitize(`