diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..5fbca23a --- /dev/null +++ b/.dockerignore @@ -0,0 +1,7 @@ +**/node_modules/ +**/dist +.git +npm-debug.log +.coverage +.coverage.* +.env \ No newline at end of file diff --git a/backend/.env.development.example b/backend/.env.development.example index 247ff054..466e1b7b 100644 --- a/backend/.env.development.example +++ b/backend/.env.development.example @@ -1,10 +1,16 @@ -DWENGO_PORT=3000 +# +# Basic configuration +# + +DWENGO_PORT=3000 # The port the backend will listen on DWENGO_DB_HOST=localhost DWENGO_DB_PORT=5431 DWENGO_DB_USERNAME=postgres DWENGO_DB_PASSWORD=postgres DWENGO_DB_UPDATE=true +# Auth + DWENGO_AUTH_STUDENT_URL=http://localhost:7080/realms/student DWENGO_AUTH_STUDENT_CLIENT_ID=dwengo DWENGO_AUTH_STUDENT_JWKS_ENDPOINT=http://localhost:7080/realms/student/protocol/openid-connect/certs @@ -14,3 +20,9 @@ DWENGO_AUTH_TEACHER_JWKS_ENDPOINT=http://localhost:7080/realms/teacher/protocol/ # Allow Vite dev-server to access the backend (for testing purposes). Don't forget to remove this in production! DWENGO_CORS_ALLOWED_ORIGINS=http://localhost:5173 + +# +# Advanced configuration +# + +# LOKI_HOST=http://localhost:9001 # The address of the Loki instance, used for logging diff --git a/backend/.env.example b/backend/.env.example index 105a1654..68cef35d 100644 --- a/backend/.env.example +++ b/backend/.env.example @@ -1,6 +1,10 @@ -DWENGO_PORT=3000 # The port the backend will listen on +# +# Basic configuration +# + +DWENGO_PORT=3000 # The port the backend will listen on DWENGO_DB_HOST=domain-or-ip-of-database -DWENGO_DB_PORT=5432 +DWENGO_DB_PORT=5431 # Change this to the actual credentials of the user Dwengo should use in the backend DWENGO_DB_USERNAME=postgres @@ -19,4 +23,5 @@ DWENGO_AUTH_TEACHER_URL=http://localhost:7080/realms/teacher DWENGO_AUTH_TEACHER_CLIENT_ID=dwengo DWENGO_AUTH_TEACHER_JWKS_ENDPOINT=http://localhost:7080/realms/teacher/protocol/openid-connect/certs -# LOKI_HOST=http://localhost:3102 # The address of the Loki instance, used for logging +# The address of the Lokiinstance, used for logging +# LOKI_HOST=http://localhost:3102 diff --git a/backend/.env.production.example b/backend/.env.production.example new file mode 100644 index 00000000..390409d1 --- /dev/null +++ b/backend/.env.production.example @@ -0,0 +1,28 @@ +DWENGO_PORT=3000 # The port the backend will listen on +DWENGO_DB_HOST=db # Name of the database container +DWENGO_DB_PORT=5431 + +# Change this to the actual credentials of the user Dwengo should use in the backend +DWENGO_DB_NAME=postgres +DWENGO_DB_USERNAME=postgres +DWENGO_DB_PASSWORD=postgres + +# Set this to true when the database scheme needs to be updated. In that case, take a backup first. +DWENGO_DB_UPDATE=false + +# Data for the identity provider via which the students authenticate. +DWENGO_AUTH_STUDENT_URL=https://sel2-1.ugent.be/idp/realms/student +DWENGO_AUTH_STUDENT_CLIENT_ID=dwengo +DWENGO_AUTH_STUDENT_JWKS_ENDPOINT=http://idp:7080/idp/realms/student/protocol/openid-connect/certs # Name of the idp container +# Data for the identity provider via which the teachers authenticate. +DWENGO_AUTH_TEACHER_URL=https://sel2-1.ugent.be/idp/realms/teacher +DWENGO_AUTH_TEACHER_CLIENT_ID=dwengo +DWENGO_AUTH_TEACHER_JWKS_ENDPOINT=http://idp:7080/idp/realms/teacher/protocol/openid-connect/certs # Name of the idp container + +# +# Advanced configuration +# + +# Logging and monitoring + +# LOKI_HOST=http://logging:3102 # The address of the Loki instance, used for logging diff --git a/backend/.env.test b/backend/.env.test.example similarity index 100% rename from backend/.env.test rename to backend/.env.test.example diff --git a/backend/Dockerfile b/backend/Dockerfile new file mode 100644 index 00000000..bd7db2ff --- /dev/null +++ b/backend/Dockerfile @@ -0,0 +1,37 @@ +FROM node:22 AS build-stage + +WORKDIR /app + +# Install dependencies + +COPY package*.json ./ +COPY backend/package.json ./backend/ + +RUN npm install --silent + +# Build the backend + +# Root tsconfig.json +COPY tsconfig.json ./ + +WORKDIR /app/backend + +COPY backend ./ +COPY docs /app/docs + +RUN npm run build + +FROM node:22 AS production-stage + +WORKDIR /app + +COPY package-lock.json backend/package.json ./ + +RUN npm install --silent --only=production + +COPY ./docs /docs +COPY --from=build-stage /app/backend/dist ./dist/ + +EXPOSE 3000 + +CMD ["node", "--env-file=.env", "dist/app.js"] diff --git a/backend/config.js b/backend/config.js new file mode 100644 index 00000000..be42027c --- /dev/null +++ b/backend/config.js @@ -0,0 +1,7 @@ +// Can be placed in dotenv but found it redundant +// Import dotenv from "dotenv"; +// Load .env file +// Dotenv.config(); +export const DWENGO_API_BASE = 'https://dwengo.org/backend/api'; +export const FALLBACK_LANG = 'nl'; +export const FALLBACK_SEQ_NUM = 1; diff --git a/backend/package.json b/backend/package.json index d548b52f..4e3b890d 100644 --- a/backend/package.json +++ b/backend/package.json @@ -1,6 +1,6 @@ { "name": "dwengo-1-backend", - "version": "0.0.1", + "version": "0.1.1", "description": "Backend for Dwengo-1", "private": true, "type": "module", @@ -34,6 +34,7 @@ "loki-logger-ts": "^1.0.2", "marked": "^15.0.7", "response-time": "^2.3.3", + "swagger-ui-express": "^5.0.1", "uuid": "^11.1.0", "winston": "^3.17.0", "winston-loki": "^6.1.3" @@ -45,6 +46,7 @@ "@types/js-yaml": "^4.0.9", "@types/node": "^22.13.4", "@types/response-time": "^2.3.8", + "@types/swagger-ui-express": "^4.1.8", "globals": "^15.15.0", "ts-node": "^10.9.2", "tsx": "^4.19.3", diff --git a/backend/src/app.ts b/backend/src/app.ts index 6307793d..0c5e8892 100644 --- a/backend/src/app.ts +++ b/backend/src/app.ts @@ -1,58 +1,36 @@ -import express, { Express, Response } from 'express'; +import express, { Express } from 'express'; import { initORM } from './orm.js'; - -import themeRoutes from './routes/themes.js'; -import learningPathRoutes from './routes/learning-paths.js'; -import learningObjectRoutes from './routes/learning-objects.js'; - -import studentRouter from './routes/student.js'; -import groupRouter from './routes/group.js'; -import assignmentRouter from './routes/assignment.js'; -import submissionRouter from './routes/submission.js'; -import classRouter from './routes/class.js'; -import questionRouter from './routes/question.js'; -import authRouter from './routes/auth.js'; import { authenticateUser } from './middleware/auth/auth.js'; import cors from './middleware/cors.js'; import { getLogger, Logger } from './logging/initalize.js'; import { responseTimeLogger } from './logging/responseTimeLogger.js'; import responseTime from 'response-time'; import { EnvVars, getNumericEnvVar } from './util/envvars.js'; +import apiRouter from './routes/router.js'; +import swaggerMiddleware from './swagger.js'; +import swaggerUi from 'swagger-ui-express'; const logger: Logger = getLogger(); const app: Express = express(); const port: string | number = getNumericEnvVar(EnvVars.Port); -app.use(cors); app.use(express.json()); -app.use(responseTime(responseTimeLogger)); +app.use(cors); app.use(authenticateUser); +// Add response time logging +app.use(responseTime(responseTimeLogger)); -// TODO Replace with Express routes -app.get('/', (_, res: Response) => { - logger.debug('GET /'); - res.json({ - message: 'Hello Dwengo!🚀', - }); -}); +app.use('/api', apiRouter); -app.use('/student', studentRouter); -app.use('/group', groupRouter); -app.use('/assignment', assignmentRouter); -app.use('/submission', submissionRouter); -app.use('/class', classRouter); -app.use('/question', questionRouter); -app.use('/auth', authRouter); -app.use('/theme', themeRoutes); -app.use('/learningPath', learningPathRoutes); -app.use('/learningObject', learningObjectRoutes); +// Swagger +app.use('/api-docs', swaggerUi.serve, swaggerMiddleware); async function startServer() { await initORM(); app.listen(port, () => { - logger.info(`Server is running at http://localhost:${port}`); + logger.info(`Server is running at http://localhost:${port}/api`); }); } diff --git a/backend/src/config.ts b/backend/src/config.ts index 6cf388cc..69af5d74 100644 --- a/backend/src/config.ts +++ b/backend/src/config.ts @@ -1,4 +1,5 @@ import { EnvVars, getEnvVar } from './util/envvars.js'; +import { Language } from './entities/content/language.js'; // API export const DWENGO_API_BASE = getEnvVar(EnvVars.LearningContentRepoApiBaseUrl); @@ -7,3 +8,5 @@ export const FALLBACK_LANG = getEnvVar(EnvVars.FallbackLanguage); // Logging export const LOG_LEVEL: string = 'development' === process.env.NODE_ENV ? 'debug' : 'info'; export const LOKI_HOST: string = process.env.LOKI_HOST || 'http://localhost:3102'; + +export const FALLBACK_SEQ_NUM = 1; diff --git a/backend/src/controllers/assignments.ts b/backend/src/controllers/assignments.ts new file mode 100644 index 00000000..03332469 --- /dev/null +++ b/backend/src/controllers/assignments.ts @@ -0,0 +1,76 @@ +import { Request, Response } from 'express'; +import { createAssignment, getAllAssignments, getAssignment, getAssignmentsSubmissions } from '../services/assignments.js'; +import { AssignmentDTO } from '../interfaces/assignment.js'; + +// Typescript is annoy with with parameter forwarding from class.ts +interface AssignmentParams { + classid: string; + id: string; +} + +export async function getAllAssignmentsHandler(req: Request, res: Response): Promise { + const classid = req.params.classid; + const full = req.query.full === 'true'; + + const assignments = await getAllAssignments(classid, full); + + res.json({ + assignments: assignments, + }); +} + +export async function createAssignmentHandler(req: Request, res: Response): Promise { + const classid = req.params.classid; + const assignmentData = req.body as AssignmentDTO; + + if (!assignmentData.description || !assignmentData.language || !assignmentData.learningPath || !assignmentData.title) { + res.status(400).json({ + error: 'Missing one or more required fields: title, description, learningPath, language', + }); + return; + } + + const assignment = await createAssignment(classid, assignmentData); + + if (!assignment) { + res.status(500).json({ error: 'Could not create assignment ' }); + return; + } + + res.status(201).json({ assignment: assignment }); +} + +export async function getAssignmentHandler(req: Request, res: Response): Promise { + const id = +req.params.id; + const classid = req.params.classid; + + if (isNaN(id)) { + res.status(400).json({ error: 'Assignment id must be a number' }); + return; + } + + const assignment = await getAssignment(classid, id); + + if (!assignment) { + res.status(404).json({ error: 'Assignment not found' }); + return; + } + + res.json(assignment); +} + +export async function getAssignmentsSubmissionsHandler(req: Request, res: Response): Promise { + const classid = req.params.classid; + const assignmentNumber = +req.params.id; + + if (isNaN(assignmentNumber)) { + res.status(400).json({ error: 'Assignment id must be a number' }); + return; + } + + const submissions = await getAssignmentsSubmissions(classid, assignmentNumber); + + res.json({ + submissions: submissions, + }); +} diff --git a/backend/src/controllers/classes.ts b/backend/src/controllers/classes.ts new file mode 100644 index 00000000..ca2f5698 --- /dev/null +++ b/backend/src/controllers/classes.ts @@ -0,0 +1,77 @@ +import { Request, Response } from 'express'; +import { createClass, getAllClasses, getClass, getClassStudents, getClassStudentsIds, getClassTeacherInvitations } from '../services/class.js'; +import { ClassDTO } from '../interfaces/class.js'; + +export async function getAllClassesHandler(req: Request, res: Response): Promise { + const full = req.query.full === 'true'; + const classes = await getAllClasses(full); + + res.json({ + classes: classes, + }); +} + +export async function createClassHandler(req: Request, res: Response): Promise { + const classData = req.body as ClassDTO; + + if (!classData.displayName) { + res.status(400).json({ + error: 'Missing one or more required fields: displayName', + }); + return; + } + + const cls = await createClass(classData); + + if (!cls) { + res.status(500).json({ error: 'Something went wrong while creating class' }); + return; + } + + res.status(201).json({ class: cls }); +} + +export async function getClassHandler(req: Request, res: Response): Promise { + try { + const classId = req.params.id; + const cls = await getClass(classId); + + if (!cls) { + res.status(404).json({ error: 'Class not found' }); + return; + } + cls.endpoints = { + self: `${req.baseUrl}/${req.params.id}`, + invitations: `${req.baseUrl}/${req.params.id}/invitations`, + assignments: `${req.baseUrl}/${req.params.id}/assignments`, + students: `${req.baseUrl}/${req.params.id}/students`, + }; + + res.json(cls); + } catch (error) { + console.error('Error fetching learning objects:', error); + res.status(500).json({ error: 'Internal server error' }); + } +} + +export async function getClassStudentsHandler(req: Request, res: Response): Promise { + const classId = req.params.id; + const full = req.query.full === 'true'; + + const students = full ? await getClassStudents(classId) : await getClassStudentsIds(classId); + + res.json({ + students: students, + }); +} + +export async function getTeacherInvitationsHandler(req: Request, res: Response): Promise { + const classId = req.params.id; + const full = req.query.full === 'true'; // TODO: not implemented yet + + const invitations = await getClassTeacherInvitations(classId, full); + + res.json({ + invitations: invitations, + }); +} diff --git a/backend/src/controllers/groups.ts b/backend/src/controllers/groups.ts new file mode 100644 index 00000000..b7bfd212 --- /dev/null +++ b/backend/src/controllers/groups.ts @@ -0,0 +1,95 @@ +import { Request, Response } from 'express'; +import { createGroup, getAllGroups, getGroup, getGroupSubmissions } from '../services/groups.js'; +import { GroupDTO } from '../interfaces/group.js'; + +// Typescript is annoywith with parameter forwarding from class.ts +interface GroupParams { + classid: string; + assignmentid: string; + groupid?: string; +} + +export async function getGroupHandler(req: Request, res: Response): Promise { + const classId = req.params.classid; + const full = req.query.full === 'true'; + const assignmentId = +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 + + if (isNaN(groupId)) { + res.status(400).json({ error: 'Group id must be a number' }); + return; + } + + const group = await getGroup(classId, assignmentId, groupId, full); + + res.json(group); +} + +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; + + if (isNaN(assignmentId)) { + res.status(400).json({ error: 'Assignment id must be a number' }); + return; + } + + const groups = await getAllGroups(classId, assignmentId, full); + + res.json({ + groups: groups, + }); +} + +export async function createGroupHandler(req: Request, res: Response): Promise { + const classid = req.params.classid; + const assignmentId = +req.params.assignmentid; + + if (isNaN(assignmentId)) { + res.status(400).json({ error: 'Assignment id must be a number' }); + return; + } + + const groupData = req.body as GroupDTO; + const group = await createGroup(groupData, classid, assignmentId); + + if (!group) { + res.status(500).json({ error: 'Something went wrong while creating group' }); + return; + } + + res.status(201).json({ group: group }); +} + +export async function getGroupSubmissionsHandler(req: Request, res: Response): Promise { + const classId = req.params.classid; + // Const full = req.query.full === 'true'; + + const assignmentId = +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 + + if (isNaN(groupId)) { + res.status(400).json({ error: 'Group id must be a number' }); + return; + } + + const submissions = await getGroupSubmissions(classId, assignmentId, groupId); + + res.json({ + submissions: submissions, + }); +} diff --git a/backend/src/controllers/learningObjects.ts b/backend/src/controllers/learningObjects.ts deleted file mode 100644 index e69de29b..00000000 diff --git a/backend/src/controllers/learningPaths.ts b/backend/src/controllers/learningPaths.ts deleted file mode 100644 index 0a7ff0ae..00000000 --- a/backend/src/controllers/learningPaths.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { Request, Response } from 'express'; -import { themes } from '../data/themes.js'; -import { FALLBACK_LANG } from '../config.js'; -import { getLogger } from '../logging/initalize.js'; -import learningPathService from '../services/learning-paths/learning-path-service.js'; -import { Language } from '../entities/content/language.js'; -/** - * Fetch learning paths based on query parameters. - */ -export async function getLearningPaths(req: Request, res: Response): Promise { - try { - const hruids = req.query.hruid; - const themeKey = req.query.theme as string; - const searchQuery = req.query.search as string; - const language = (req.query.language as Language) || FALLBACK_LANG; - - let hruidList; - - if (hruids) { - hruidList = Array.isArray(hruids) ? hruids.map(String) : [String(hruids)]; - } else if (themeKey) { - const theme = themes.find((t) => t.title === themeKey); - if (theme) { - hruidList = theme.hruids; - } else { - res.status(404).json({ - error: `Theme "${themeKey}" not found.`, - }); - return; - } - } else if (searchQuery) { - const searchResults = await learningPathService.searchLearningPaths(searchQuery, language); - res.json(searchResults); - return; - } else { - hruidList = themes.flatMap((theme) => theme.hruids); - } - - const learningPaths = await learningPathService.fetchLearningPaths(hruidList, language, `HRUIDs: ${hruidList.join(', ')}`); - res.json(learningPaths.data); - } catch (error) { - getLogger().error('❌ Unexpected error fetching learning paths:', error); - res.status(500).json({ error: 'Internal server error' }); - } -} diff --git a/backend/src/controllers/questions.ts b/backend/src/controllers/questions.ts new file mode 100644 index 00000000..917b48ae --- /dev/null +++ b/backend/src/controllers/questions.ts @@ -0,0 +1,119 @@ +import { Request, Response } from 'express'; +import { createQuestion, deleteQuestion, getAllQuestions, getAnswersByQuestion, getQuestion } from '../services/questions.js'; +import { QuestionDTO, QuestionId } from '../interfaces/question.js'; +import { FALLBACK_LANG, FALLBACK_SEQ_NUM } from '../config.js'; +import { LearningObjectIdentifier } from '../entities/content/learning-object-identifier.js'; +import { Language } from '../entities/content/language.js'; + +function getObjectId(req: Request, res: Response): LearningObjectIdentifier | null { + const { hruid, version } = req.params; + const lang = req.query.lang; + + if (!hruid || !version) { + res.status(400).json({ error: 'Missing required parameters.' }); + return null; + } + + return { + hruid, + language: (lang as Language) || FALLBACK_LANG, + version: +version, + }; +} + +function getQuestionId(req: Request, res: Response): QuestionId | null { + const seq = req.params.seq; + const learningObjectIdentifier = getObjectId(req, res); + + if (!learningObjectIdentifier) { + return null; + } + + return { + learningObjectIdentifier, + sequenceNumber: seq ? Number(seq) : FALLBACK_SEQ_NUM, + }; +} + +export async function getAllQuestionsHandler(req: Request, res: Response): Promise { + const objectId = getObjectId(req, res); + const full = req.query.full === 'true'; + + if (!objectId) { + return; + } + + const questions = await getAllQuestions(objectId, full); + + if (!questions) { + res.status(404).json({ error: `Questions not found.` }); + } else { + res.json(questions); + } +} + +export async function getQuestionHandler(req: Request, res: Response): Promise { + const questionId = getQuestionId(req, res); + + if (!questionId) { + return; + } + + const question = await getQuestion(questionId); + + if (!question) { + res.status(404).json({ error: `Question not found.` }); + } else { + res.json(question); + } +} + +export async function getQuestionAnswersHandler(req: Request, res: Response): Promise { + const questionId = getQuestionId(req, res); + const full = req.query.full === 'true'; + + if (!questionId) { + return; + } + + const answers = getAnswersByQuestion(questionId, full); + + if (!answers) { + res.status(404).json({ error: `Questions not found.` }); + } else { + res.json(answers); + } +} + +export async function createQuestionHandler(req: Request, res: Response): Promise { + const questionDTO = req.body as QuestionDTO; + + if (!questionDTO.learningObjectIdentifier || !questionDTO.author || !questionDTO.content) { + res.status(400).json({ error: 'Missing required fields: identifier and content' }); + return; + } + + const question = await createQuestion(questionDTO); + + if (!question) { + res.status(400).json({ error: 'Could not add question' }); + } else { + res.json(question); + } +} + +export async function deleteQuestionHandler(req: Request, res: Response): Promise { + const questionId = getQuestionId(req, res); + + if (!questionId) { + return; + } + + const question = await deleteQuestion(questionId); + + if (!question) { + res.status(400).json({ error: 'Could not find nor delete question' }); + } else { + res.json(question); + } +} diff --git a/backend/src/controllers/students.ts b/backend/src/controllers/students.ts new file mode 100644 index 00000000..6c253cff --- /dev/null +++ b/backend/src/controllers/students.ts @@ -0,0 +1,146 @@ +import { Request, Response } from 'express'; +import { + createStudent, + deleteStudent, + getAllStudents, + getStudent, + getStudentAssignments, + getStudentClasses, + getStudentGroups, + getStudentSubmissions, +} from '../services/students.js'; +import { ClassDTO } from '../interfaces/class.js'; +import { getAllAssignments } from '../services/assignments.js'; +import { getUserHandler } from './users.js'; +import { Student } from '../entities/users/student.entity.js'; +import { StudentDTO } from '../interfaces/student.js'; +import { getStudentRepository } from '../data/repositories.js'; +import { UserDTO } from '../interfaces/user.js'; + +// TODO: accept arguments (full, ...) +// TODO: endpoints +export async function getAllStudentsHandler(req: Request, res: Response): Promise { + const full = req.query.full === 'true'; + + const studentRepository = getStudentRepository(); + + const students: StudentDTO[] | string[] = full ? await getAllStudents() : await getAllStudents(); + + if (!students) { + res.status(404).json({ error: `Student not found.` }); + return; + } + + res.status(201).json(students); +} + +export async function getStudentHandler(req: Request, res: Response): Promise { + const username = req.params.username; + + if (!username) { + res.status(400).json({ error: 'Missing required field: username' }); + return; + } + + const user = await getStudent(username); + + if (!user) { + res.status(404).json({ + error: `User with username '${username}' not found.`, + }); + return; + } + + res.status(201).json(user); +} + +export async function createStudentHandler(req: Request, res: Response) { + 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); + res.status(201).json(newUser); +} + +export async function deleteStudentHandler(req: Request, res: Response) { + const username = req.params.username; + + if (!username) { + res.status(400).json({ error: 'Missing required field: username' }); + return; + } + + const deletedUser = await deleteStudent(username); + if (!deletedUser) { + res.status(404).json({ + error: `User with username '${username}' not found.`, + }); + return; + } + + res.status(200).json(deletedUser); +} + +export async function getStudentClassesHandler(req: Request, res: Response): Promise { + try { + const full = req.query.full === 'true'; + const username = req.params.id; + + const classes = await getStudentClasses(username, full); + + res.json({ + classes: classes, + endpoints: { + self: `${req.baseUrl}/${req.params.id}`, + classes: `${req.baseUrl}/${req.params.id}/invitations`, + questions: `${req.baseUrl}/${req.params.id}/assignments`, + students: `${req.baseUrl}/${req.params.id}/students`, + }, + }); + } catch (error) { + console.error('Error fetching learning objects:', error); + res.status(500).json({ error: 'Internal server error' }); + } +} + +// TODO +// Might not be fully correct depending on if +// A class has an assignment, that all students +// Have this assignment. +export async function getStudentAssignmentsHandler(req: Request, res: Response): Promise { + const full = req.query.full === 'true'; + const username = req.params.id; + + const assignments = getStudentAssignments(username, full); + + res.json({ + assignments: assignments, + }); +} + +export async function getStudentGroupsHandler(req: Request, res: Response): Promise { + const full = req.query.full === 'true'; + const username = req.params.id; + + const groups = await getStudentGroups(username, full); + + res.json({ + groups: groups, + }); +} + +export async function getStudentSubmissionsHandler(req: Request, res: Response): Promise { + const username = req.params.id; + + const submissions = await getStudentSubmissions(username); + + res.json({ + submissions: submissions, + }); +} diff --git a/backend/src/controllers/submissions.ts b/backend/src/controllers/submissions.ts new file mode 100644 index 00000000..1e66dbe9 --- /dev/null +++ b/backend/src/controllers/submissions.ts @@ -0,0 +1,59 @@ +import { Request, Response } from 'express'; +import { createSubmission, deleteSubmission, getSubmission } from '../services/submissions.js'; +import { Language, languageMap } from '../entities/content/language.js'; +import { SubmissionDTO } from '../interfaces/submission'; + +interface SubmissionParams { + hruid: string; + id: number; +} + +export async function getSubmissionHandler(req: Request, res: Response): Promise { + const lohruid = req.params.hruid; + const submissionNumber = +req.params.id; + + if (isNaN(submissionNumber)) { + res.status(400).json({ error: 'Submission number is not a number' }); + return; + } + + const lang = languageMap[req.query.language as string] || Language.Dutch; + const version = (req.query.version || 1) as number; + + const submission = await getSubmission(lohruid, lang, version, submissionNumber); + + if (!submission) { + res.status(404).json({ error: 'Submission not found' }); + return; + } + + res.json(submission); +} + +export async function createSubmissionHandler(req: Request, res: Response) { + const submissionDTO = req.body as SubmissionDTO; + + const submission = await createSubmission(submissionDTO); + + if (!submission) { + res.status(404).json({ error: 'Submission not added' }); + } else { + res.json(submission); + } +} + +export async function deleteSubmissionHandler(req: Request, res: Response) { + const hruid = req.params.hruid; + const submissionNumber = +req.params.id; + + const lang = languageMap[req.query.language as string] || Language.Dutch; + const version = (req.query.version || 1) as number; + + const submission = await deleteSubmission(hruid, lang, version, submissionNumber); + + if (!submission) { + res.status(404).json({ error: 'Submission not found' }); + } else { + res.json(submission); + } +} diff --git a/backend/src/controllers/teachers.ts b/backend/src/controllers/teachers.ts new file mode 100644 index 00000000..52e5e713 --- /dev/null +++ b/backend/src/controllers/teachers.ts @@ -0,0 +1,144 @@ +import { Request, Response } from 'express'; +import { + createTeacher, + deleteTeacher, + getAllTeachers, + getClassesByTeacher, + getClassIdsByTeacher, + getQuestionIdsByTeacher, + getQuestionsByTeacher, + getStudentIdsByTeacher, + getStudentsByTeacher, + getTeacher, +} from '../services/teachers.js'; +import { ClassDTO } from '../interfaces/class.js'; +import { StudentDTO } from '../interfaces/student.js'; +import { QuestionDTO, QuestionId } from '../interfaces/question.js'; +import { Teacher } from '../entities/users/teacher.entity.js'; +import { TeacherDTO } from '../interfaces/teacher.js'; +import { getTeacherRepository } from '../data/repositories.js'; + +export async function getAllTeachersHandler(req: Request, res: Response): Promise { + const full = req.query.full === 'true'; + + const teacherRepository = getTeacherRepository(); + + const teachers: TeacherDTO[] | string[] = full ? await getAllTeachers() : await getAllTeachers(); + + if (!teachers) { + res.status(404).json({ error: `Teacher not found.` }); + return; + } + + res.status(201).json(teachers); +} + +export async function getTeacherHandler(req: Request, res: Response): Promise { + const username = req.params.username; + + if (!username) { + res.status(400).json({ error: 'Missing required field: username' }); + return; + } + + const user = await getTeacher(username); + + if (!user) { + res.status(404).json({ + error: `User with username '${username}' not found.`, + }); + return; + } + + res.status(201).json(user); +} + +export async function createTeacherHandler(req: Request, res: Response) { + const userData = req.body as TeacherDTO; + + if (!userData.username || !userData.firstName || !userData.lastName) { + res.status(400).json({ + error: 'Missing required fields: username, firstName, lastName', + }); + return; + } + + const newUser = await createTeacher(userData); + res.status(201).json(newUser); +} + +export async function deleteTeacherHandler(req: Request, res: Response) { + const username = req.params.username; + + if (!username) { + res.status(400).json({ error: 'Missing required field: username' }); + return; + } + + const deletedUser = await deleteTeacher(username); + if (!deletedUser) { + res.status(404).json({ + error: `User with username '${username}' not found.`, + }); + return; + } + + res.status(200).json(deletedUser); +} + +export async function getTeacherClassHandler(req: Request, res: Response): Promise { + try { + const username = req.params.username as string; + const full = req.query.full === 'true'; + + if (!username) { + res.status(400).json({ error: 'Missing required field: username' }); + return; + } + + const classes: ClassDTO[] | string[] = full ? await getClassesByTeacher(username) : await getClassIdsByTeacher(username); + + res.status(201).json(classes); + } catch (error) { + console.error('Error fetching classes by teacher:', error); + res.status(500).json({ error: 'Internal server error' }); + } +} + +export async function getTeacherStudentHandler(req: Request, res: Response): Promise { + try { + const username = req.params.username as string; + const full = req.query.full === 'true'; + + if (!username) { + res.status(400).json({ error: 'Missing required field: username' }); + return; + } + + const students: StudentDTO[] | string[] = full ? await getStudentsByTeacher(username) : await getStudentIdsByTeacher(username); + + res.status(201).json(students); + } catch (error) { + console.error('Error fetching students by teacher:', error); + res.status(500).json({ error: 'Internal server error' }); + } +} + +export async function getTeacherQuestionHandler(req: Request, res: Response): Promise { + try { + const username = req.params.username as string; + const full = req.query.full === 'true'; + + if (!username) { + res.status(400).json({ error: 'Missing required field: username' }); + return; + } + + const questions: QuestionDTO[] | QuestionId[] = full ? await getQuestionsByTeacher(username) : await getQuestionIdsByTeacher(username); + + res.status(201).json(questions); + } catch (error) { + console.error('Error fetching questions by teacher:', error); + res.status(500).json({ error: 'Internal server error' }); + } +} diff --git a/backend/src/controllers/themes.ts b/backend/src/controllers/themes.ts index fe1eb818..61a1a834 100644 --- a/backend/src/controllers/themes.ts +++ b/backend/src/controllers/themes.ts @@ -1,6 +1,6 @@ import { Request, Response } from 'express'; import { themes } from '../data/themes.js'; -import { loadTranslations } from '../util/translationHelper.js'; +import { loadTranslations } from '../util/translation-helper.js'; interface Translations { curricula_page: { diff --git a/backend/src/controllers/users.ts b/backend/src/controllers/users.ts new file mode 100644 index 00000000..850c6549 --- /dev/null +++ b/backend/src/controllers/users.ts @@ -0,0 +1,91 @@ +import { Request, Response } from 'express'; +import { UserService } from '../services/users.js'; +import { UserDTO } from '../interfaces/user.js'; +import { User } from '../entities/users/user.entity.js'; + +export async function getAllUsersHandler(req: Request, res: Response, service: UserService): Promise { + try { + const full = req.query.full === 'true'; + + const users: UserDTO[] | string[] = full ? await service.getAllUsers() : await service.getAllUserIds(); + + if (!users) { + res.status(404).json({ error: `Users not found.` }); + return; + } + + res.status(201).json(users); + } catch (error) { + console.error('❌ Error fetching users:', error); + res.status(500).json({ error: 'Internal server error' }); + } +} + +export async function getUserHandler(req: Request, res: Response, service: UserService): Promise { + try { + const username = req.params.username as string; + + if (!username) { + res.status(400).json({ error: 'Missing required field: username' }); + return; + } + + const user = await service.getUserByUsername(username); + + if (!user) { + res.status(404).json({ + error: `User with username '${username}' not found.`, + }); + return; + } + + res.status(201).json(user); + } catch (error) { + console.error('❌ Error fetching users:', error); + res.status(500).json({ error: 'Internal server error' }); + } +} + +export async function createUserHandler(req: Request, res: Response, service: UserService, UserClass: new () => T) { + try { + console.log('req', req); + const userData = req.body as UserDTO; + + if (!userData.username || !userData.firstName || !userData.lastName) { + res.status(400).json({ + error: 'Missing required fields: username, firstName, lastName', + }); + return; + } + + const newUser = await service.createUser(userData, UserClass); + res.status(201).json(newUser); + } catch (error) { + console.error('❌ Error creating user:', error); + res.status(500).json({ error: 'Internal server error' }); + } +} + +export async function deleteUserHandler(req: Request, res: Response, service: UserService) { + try { + const username = req.params.username; + + if (!username) { + res.status(400).json({ error: 'Missing required field: username' }); + return; + } + + const deletedUser = await service.deleteUser(username); + if (!deletedUser) { + res.status(404).json({ + error: `User with username '${username}' not found.`, + }); + return; + } + + res.status(200).json(deletedUser); + } catch (error) { + console.error('❌ Error deleting user:', error); + res.status(500).json({ error: 'Internal server error' }); + } +} diff --git a/backend/src/data/assignments/group-repository.ts b/backend/src/data/assignments/group-repository.ts index df92eaae..eb1b09e2 100644 --- a/backend/src/data/assignments/group-repository.ts +++ b/backend/src/data/assignments/group-repository.ts @@ -1,16 +1,26 @@ import { DwengoEntityRepository } from '../dwengo-entity-repository.js'; import { Group } from '../../entities/assignments/group.entity.js'; import { Assignment } from '../../entities/assignments/assignment.entity.js'; +import { Student } from '../../entities/users/student.entity.js'; export class GroupRepository extends DwengoEntityRepository { public findByAssignmentAndGroupNumber(assignment: Assignment, groupNumber: number): Promise { - return this.findOne({ - assignment: assignment, - groupNumber: groupNumber, - }); + return this.findOne( + { + assignment: assignment, + groupNumber: groupNumber, + }, + { populate: ['members'] } + ); } public findAllGroupsForAssignment(assignment: Assignment): Promise { - return this.findAll({ where: { assignment: assignment } }); + return this.findAll({ + where: { assignment: assignment }, + populate: ['members'], + }); + } + public findAllGroupsWithStudent(student: Student): Promise { + return this.find({ members: student }, { populate: ['members'] }); } public deleteByAssignmentAndGroupNumber(assignment: Assignment, groupNumber: number) { return this.deleteWhere({ diff --git a/backend/src/data/assignments/submission-repository.ts b/backend/src/data/assignments/submission-repository.ts index faa9fef1..251823fa 100644 --- a/backend/src/data/assignments/submission-repository.ts +++ b/backend/src/data/assignments/submission-repository.ts @@ -38,6 +38,14 @@ export class SubmissionRepository extends DwengoEntityRepository { ); } + public findAllSubmissionsForGroup(group: Group): Promise { + return this.find({ onBehalfOf: group }); + } + + public findAllSubmissionsForStudent(student: Student): Promise { + return this.find({ submitter: student }); + } + public deleteSubmissionByLearningObjectAndSubmissionNumber(loId: LearningObjectIdentifier, submissionNumber: number): Promise { return this.deleteWhere({ learningObjectHruid: loId.hruid, diff --git a/backend/src/data/classes/class-repository.ts b/backend/src/data/classes/class-repository.ts index e3b9f959..0ceed98e 100644 --- a/backend/src/data/classes/class-repository.ts +++ b/backend/src/data/classes/class-repository.ts @@ -1,11 +1,23 @@ import { DwengoEntityRepository } from '../dwengo-entity-repository.js'; import { Class } from '../../entities/classes/class.entity.js'; +import { Student } from '../../entities/users/student.entity.js'; +import { Teacher } from '../../entities/users/teacher.entity'; export class ClassRepository extends DwengoEntityRepository { public findById(id: string): Promise { - return this.findOne({ classId: id }); + return this.findOne({ classId: id }, { populate: ['students', 'teachers'] }); } public deleteById(id: string): Promise { return this.deleteWhere({ classId: id }); } + public findByStudent(student: Student): Promise { + return this.find( + { students: student }, + { populate: ['students', 'teachers'] } // Voegt student en teacher objecten toe + ); + } + + public findByTeacher(teacher: Teacher): Promise { + return this.find({ teachers: teacher }, { populate: ['students', 'teachers'] }); + } } diff --git a/backend/src/data/content/learning-object-repository.ts b/backend/src/data/content/learning-object-repository.ts index f9b6bfcb..49b4c536 100644 --- a/backend/src/data/content/learning-object-repository.ts +++ b/backend/src/data/content/learning-object-repository.ts @@ -1,7 +1,8 @@ import { DwengoEntityRepository } from '../dwengo-entity-repository.js'; import { LearningObject } from '../../entities/content/learning-object.entity.js'; import { LearningObjectIdentifier } from '../../entities/content/learning-object-identifier.js'; -import { Language } from '../../entities/content/language'; +import { Language } from '../../entities/content/language.js'; +import { Teacher } from '../../entities/users/teacher.entity.js'; export class LearningObjectRepository extends DwengoEntityRepository { public findByIdentifier(identifier: LearningObjectIdentifier): Promise { @@ -31,4 +32,11 @@ export class LearningObjectRepository extends DwengoEntityRepository { + return this.find( + { admins: teacher }, + { populate: ['admins'] } // Make sure to load admin relations + ); + } } diff --git a/backend/src/data/questions/question-repository.ts b/backend/src/data/questions/question-repository.ts index 4099c528..9207e1dd 100644 --- a/backend/src/data/questions/question-repository.ts +++ b/backend/src/data/questions/question-repository.ts @@ -2,6 +2,7 @@ import { DwengoEntityRepository } from '../dwengo-entity-repository.js'; import { Question } from '../../entities/questions/question.entity.js'; import { LearningObjectIdentifier } from '../../entities/content/learning-object-identifier.js'; import { Student } from '../../entities/users/student.entity.js'; +import { LearningObject } from '../../entities/content/learning-object.entity.js'; export class QuestionRepository extends DwengoEntityRepository { public createQuestion(question: { loId: LearningObjectIdentifier; author: Student; content: string }): Promise { @@ -40,4 +41,17 @@ export class QuestionRepository extends DwengoEntityRepository { sequenceNumber: sequenceNumber, }); } + + public async findAllByLearningObjects(learningObjects: LearningObject[]): Promise { + const objectIdentifiers = learningObjects.map((lo) => ({ + learningObjectHruid: lo.hruid, + learningObjectLanguage: lo.language, + learningObjectVersion: lo.version, + })); + + return this.findAll({ + where: { $or: objectIdentifiers }, + orderBy: { timestamp: 'ASC' }, + }); + } } diff --git a/backend/src/data/repositories.ts b/backend/src/data/repositories.ts index 3daa026d..02385109 100644 --- a/backend/src/data/repositories.ts +++ b/backend/src/data/repositories.ts @@ -54,7 +54,6 @@ function repositoryGetter>(en } /* Users */ -export const getUserRepository = repositoryGetter(User); export const getStudentRepository = repositoryGetter(Student); export const getTeacherRepository = repositoryGetter(Teacher); diff --git a/backend/src/data/users/student-repository.ts b/backend/src/data/users/student-repository.ts index 1c3a6fae..0792678d 100644 --- a/backend/src/data/users/student-repository.ts +++ b/backend/src/data/users/student-repository.ts @@ -1,5 +1,9 @@ -import { DwengoEntityRepository } from '../dwengo-entity-repository.js'; import { Student } from '../../entities/users/student.entity.js'; +import { User } from '../../entities/users/user.entity.js'; +import { DwengoEntityRepository } from '../dwengo-entity-repository.js'; +// Import { UserRepository } from './user-repository.js'; + +// Export class StudentRepository extends UserRepository {} export class StudentRepository extends DwengoEntityRepository { public findByUsername(username: string): Promise { diff --git a/backend/src/data/users/teacher-repository.ts b/backend/src/data/users/teacher-repository.ts index 704ef409..2b2bee75 100644 --- a/backend/src/data/users/teacher-repository.ts +++ b/backend/src/data/users/teacher-repository.ts @@ -1,5 +1,6 @@ -import { DwengoEntityRepository } from '../dwengo-entity-repository.js'; import { Teacher } from '../../entities/users/teacher.entity.js'; +import { DwengoEntityRepository } from '../dwengo-entity-repository.js'; +import { UserRepository } from './user-repository.js'; export class TeacherRepository extends DwengoEntityRepository { public findByUsername(username: string): Promise { diff --git a/backend/src/data/users/user-repository.ts b/backend/src/data/users/user-repository.ts index 7e2a42ad..21497b79 100644 --- a/backend/src/data/users/user-repository.ts +++ b/backend/src/data/users/user-repository.ts @@ -1,11 +1,11 @@ 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 { - return this.findOne({ username: username }); +export class UserRepository extends DwengoEntityRepository { + public findByUsername(username: string): Promise { + return this.findOne({ username } as Partial); } public deleteByUsername(username: string): Promise { - return this.deleteWhere({ username: username }); + 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 cda27d66..692e2112 100644 --- a/backend/src/entities/assignments/assignment.entity.ts +++ b/backend/src/entities/assignments/assignment.entity.ts @@ -1,16 +1,21 @@ -import { Entity, Enum, ManyToOne, OneToMany, PrimaryKey, Property } from '@mikro-orm/core'; +import { Collection, 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 { AssignmentRepository } from '../../data/assignments/assignment-repository.js'; -@Entity({ repository: () => AssignmentRepository }) +@Entity({ + repository: () => AssignmentRepository, +}) export class Assignment { - @ManyToOne({ entity: () => Class, primary: true }) + @ManyToOne({ + entity: () => Class, + primary: true, + }) within!: Class; - @PrimaryKey({ type: 'number' }) - id!: number; + @PrimaryKey({ type: 'number', autoincrement: true }) + id?: number; @Property({ type: 'string' }) title!: string; @@ -21,9 +26,14 @@ export class Assignment { @Property({ type: 'string' }) learningPathHruid!: string; - @Enum({ items: () => Language }) + @Enum({ + items: () => Language, + }) learningPathLanguage!: Language; - @OneToMany({ entity: () => Group, mappedBy: 'assignment' }) + @OneToMany({ + entity: () => Group, + mappedBy: 'assignment', + }) groups!: Group[]; } diff --git a/backend/src/entities/assignments/group.entity.ts b/backend/src/entities/assignments/group.entity.ts index 632ad722..213e0f38 100644 --- a/backend/src/entities/assignments/group.entity.ts +++ b/backend/src/entities/assignments/group.entity.ts @@ -1,9 +1,11 @@ -import { Entity, ManyToMany, ManyToOne, PrimaryKey } from '@mikro-orm/core'; +import { Collection, Entity, ManyToMany, ManyToOne, PrimaryKey } from '@mikro-orm/core'; import { Assignment } from './assignment.entity.js'; import { Student } from '../users/student.entity.js'; import { GroupRepository } from '../../data/assignments/group-repository.js'; -@Entity({ repository: () => GroupRepository }) +@Entity({ + repository: () => GroupRepository, +}) export class Group { @ManyToOne({ entity: () => Assignment, @@ -11,8 +13,8 @@ export class Group { }) assignment!: Assignment; - @PrimaryKey({ type: 'integer' }) - groupNumber!: number; + @PrimaryKey({ type: 'integer', autoincrement: true }) + groupNumber?: number; @ManyToMany({ entity: () => Student, diff --git a/backend/src/entities/assignments/submission.entity.ts b/backend/src/entities/assignments/submission.entity.ts index f6f8b3c7..f008c8c2 100644 --- a/backend/src/entities/assignments/submission.entity.ts +++ b/backend/src/entities/assignments/submission.entity.ts @@ -18,7 +18,7 @@ export class Submission { @PrimaryKey({ type: 'numeric' }) learningObjectVersion: number = 1; - @PrimaryKey({ type: 'integer' }) + @PrimaryKey({ type: 'integer', autoincrement: true }) submissionNumber!: number; @ManyToOne({ diff --git a/backend/src/entities/classes/class-join-request.entity.ts b/backend/src/entities/classes/class-join-request.entity.ts index 62ed37d0..bdef1f52 100644 --- a/backend/src/entities/classes/class-join-request.entity.ts +++ b/backend/src/entities/classes/class-join-request.entity.ts @@ -3,7 +3,9 @@ import { Student } from '../users/student.entity.js'; import { Class } from './class.entity.js'; import { ClassJoinRequestRepository } from '../../data/classes/class-join-request-repository.js'; -@Entity({ repository: () => ClassJoinRequestRepository }) +@Entity({ + repository: () => ClassJoinRequestRepository, +}) export class ClassJoinRequest { @ManyToOne({ entity: () => Student, diff --git a/backend/src/entities/classes/class.entity.ts b/backend/src/entities/classes/class.entity.ts index d4e44bf9..63315304 100644 --- a/backend/src/entities/classes/class.entity.ts +++ b/backend/src/entities/classes/class.entity.ts @@ -4,10 +4,12 @@ import { Teacher } from '../users/teacher.entity.js'; import { Student } from '../users/student.entity.js'; import { ClassRepository } from '../../data/classes/class-repository.js'; -@Entity({ repository: () => ClassRepository }) +@Entity({ + repository: () => ClassRepository, +}) export class Class { @PrimaryKey() - classId = v4(); + classId? = v4(); @Property({ type: 'string' }) displayName!: string; diff --git a/backend/src/entities/classes/teacher-invitation.entity.ts b/backend/src/entities/classes/teacher-invitation.entity.ts index eb57d98a..668a0a1c 100644 --- a/backend/src/entities/classes/teacher-invitation.entity.ts +++ b/backend/src/entities/classes/teacher-invitation.entity.ts @@ -7,9 +7,6 @@ import { TeacherInvitationRepository } from '../../data/classes/teacher-invitati * Invitation of a teacher into a class (in order to teach it). */ @Entity({ repository: () => TeacherInvitationRepository }) -@Entity({ - repository: () => TeacherInvitationRepository, -}) export class TeacherInvitation { @ManyToOne({ entity: () => Teacher, diff --git a/backend/src/entities/content/attachment.entity.ts b/backend/src/entities/content/attachment.entity.ts index 0c0f53c4..80104f28 100644 --- a/backend/src/entities/content/attachment.entity.ts +++ b/backend/src/entities/content/attachment.entity.ts @@ -2,7 +2,9 @@ import { Entity, ManyToOne, PrimaryKey, Property } from '@mikro-orm/core'; import { LearningObject } from './learning-object.entity.js'; import { AttachmentRepository } from '../../data/content/attachment-repository.js'; -@Entity({ repository: () => AttachmentRepository }) +@Entity({ + repository: () => AttachmentRepository, +}) export class Attachment { @ManyToOne({ entity: () => LearningObject, diff --git a/backend/src/entities/content/language.ts b/backend/src/entities/content/language.ts index d7687331..7e7b42d2 100644 --- a/backend/src/entities/content/language.ts +++ b/backend/src/entities/content/language.ts @@ -184,3 +184,10 @@ export enum Language { Zhuang = 'za', Zulu = 'zu', } + +export const languageMap: Record = { + nl: Language.Dutch, + fr: Language.French, + en: Language.English, + de: Language.German, +}; diff --git a/backend/src/interfaces/answer.ts b/backend/src/interfaces/answer.ts new file mode 100644 index 00000000..493fd3c0 --- /dev/null +++ b/backend/src/interfaces/answer.ts @@ -0,0 +1,38 @@ +import { mapToUserDTO, UserDTO } from './user.js'; +import { mapToQuestionDTO, mapToQuestionId, QuestionDTO, QuestionId } from './question.js'; +import { Answer } from '../entities/questions/answer.entity.js'; + +export interface AnswerDTO { + author: UserDTO; + toQuestion: QuestionDTO; + sequenceNumber: number; + timestamp: string; + content: string; +} + +/** + * Convert a Question entity to a DTO format. + */ +export function mapToAnswerDTO(answer: Answer): AnswerDTO { + return { + author: mapToUserDTO(answer.author), + toQuestion: mapToQuestionDTO(answer.toQuestion), + sequenceNumber: answer.sequenceNumber!, + timestamp: answer.timestamp.toISOString(), + content: answer.content, + }; +} + +export interface AnswerId { + author: string; + toQuestion: QuestionId; + sequenceNumber: number; +} + +export function mapToAnswerId(answer: AnswerDTO): AnswerId { + return { + author: answer.author.username, + toQuestion: mapToQuestionId(answer.toQuestion), + sequenceNumber: answer.sequenceNumber, + }; +} diff --git a/backend/src/interfaces/assignment.ts b/backend/src/interfaces/assignment.ts new file mode 100644 index 00000000..8f6120b6 --- /dev/null +++ b/backend/src/interfaces/assignment.ts @@ -0,0 +1,52 @@ +import { FALLBACK_LANG } from '../config.js'; +import { Assignment } from '../entities/assignments/assignment.entity.js'; +import { Class } from '../entities/classes/class.entity.js'; +import { languageMap } from '../entities/content/language.js'; +import { GroupDTO, mapToGroupDTO } from './group.js'; + +export interface AssignmentDTO { + id: number; + class: string; // Id of class 'within' + title: string; + description: string; + learningPath: string; + language: string; + groups?: GroupDTO[] | string[]; // TODO +} + +export function mapToAssignmentDTOId(assignment: Assignment): AssignmentDTO { + return { + id: assignment.id!, + class: assignment.within.classId!, + title: assignment.title, + description: assignment.description, + learningPath: assignment.learningPathHruid, + language: assignment.learningPathLanguage, + // Groups: assignment.groups.map(group => group.groupNumber), + }; +} + +export function mapToAssignmentDTO(assignment: Assignment): AssignmentDTO { + return { + id: assignment.id!, + class: assignment.within.classId!, + title: assignment.title, + description: assignment.description, + learningPath: assignment.learningPathHruid, + language: assignment.learningPathLanguage, + // Groups: assignment.groups.map(mapToGroupDTO), + }; +} + +export function mapToAssignment(assignmentData: AssignmentDTO, cls: Class): Assignment { + const assignment = new Assignment(); + assignment.title = assignmentData.title; + assignment.description = assignmentData.description; + assignment.learningPathHruid = assignmentData.learningPath; + assignment.learningPathLanguage = languageMap[assignmentData.language] || FALLBACK_LANG; + assignment.within = cls; + + console.log(assignment); + + return assignment; +} diff --git a/backend/src/interfaces/class.ts b/backend/src/interfaces/class.ts new file mode 100644 index 00000000..371e3cae --- /dev/null +++ b/backend/src/interfaces/class.ts @@ -0,0 +1,37 @@ +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[]; + endpoints?: { + self: string; + invitations: string; + assignments: string; + students: string; + }; +} + +export function mapToClassDTO(cls: Class): ClassDTO { + return { + id: cls.classId!, + displayName: cls.displayName, + teachers: cls.teachers.map((teacher) => teacher.username), + students: cls.students.map((student) => student.username), + joinRequests: [], // TODO + }; +} + +export function mapToClass(classData: ClassDTO, students: Collection, teachers: Collection): Class { + const cls = new Class(); + cls.displayName = classData.displayName; + cls.students = students; + cls.teachers = teachers; + + return cls; +} diff --git a/backend/src/interfaces/group.ts b/backend/src/interfaces/group.ts new file mode 100644 index 00000000..a25c5b8e --- /dev/null +++ b/backend/src/interfaces/group.ts @@ -0,0 +1,25 @@ +import { Group } from '../entities/assignments/group.entity.js'; +import { AssignmentDTO, mapToAssignmentDTO } from './assignment.js'; +import { mapToStudentDTO, StudentDTO } from './student.js'; + +export interface GroupDTO { + assignment: number | AssignmentDTO; + groupNumber: number; + members: string[] | StudentDTO[]; +} + +export function mapToGroupDTO(group: Group): GroupDTO { + return { + assignment: mapToAssignmentDTO(group.assignment), // ERROR: , group.assignment.within), + groupNumber: group.groupNumber!, + members: group.members.map(mapToStudentDTO), + }; +} + +export function mapToGroupDTOId(group: Group): GroupDTO { + return { + assignment: group.assignment.id!, + groupNumber: group.groupNumber!, + members: group.members.map((member) => member.username), + }; +} diff --git a/backend/src/interfaces/list.ts b/backend/src/interfaces/list.ts new file mode 100644 index 00000000..6892fb9d --- /dev/null +++ b/backend/src/interfaces/list.ts @@ -0,0 +1,5 @@ +// TODO: implement something like this but with named endpoints +export interface List { + items: T[]; + endpoints?: string[]; +} diff --git a/backend/src/interfaces/question.ts b/backend/src/interfaces/question.ts new file mode 100644 index 00000000..8cca42f6 --- /dev/null +++ b/backend/src/interfaces/question.ts @@ -0,0 +1,44 @@ +import { Question } from '../entities/questions/question.entity.js'; +import { UserDTO } from './user.js'; +import { LearningObjectIdentifier } from '../entities/content/learning-object-identifier.js'; +import { mapToStudentDTO, StudentDTO } from './student.js'; +import { TeacherDTO } from './teacher.js'; + +export interface QuestionDTO { + learningObjectIdentifier: LearningObjectIdentifier; + sequenceNumber?: number; + author: StudentDTO; + timestamp?: string; + content: string; +} + +/** + * Convert a Question entity to a DTO format. + */ +export function mapToQuestionDTO(question: Question): QuestionDTO { + const learningObjectIdentifier = { + hruid: question.learningObjectHruid, + language: question.learningObjectLanguage, + version: question.learningObjectVersion, + }; + + return { + learningObjectIdentifier, + sequenceNumber: question.sequenceNumber!, + author: mapToStudentDTO(question.author), + timestamp: question.timestamp.toISOString(), + content: question.content, + }; +} + +export interface QuestionId { + learningObjectIdentifier: LearningObjectIdentifier; + sequenceNumber: number; +} + +export function mapToQuestionId(question: QuestionDTO): QuestionId { + return { + learningObjectIdentifier: question.learningObjectIdentifier, + sequenceNumber: question.sequenceNumber!, + }; +} diff --git a/backend/src/interfaces/student.ts b/backend/src/interfaces/student.ts new file mode 100644 index 00000000..079b355b --- /dev/null +++ b/backend/src/interfaces/student.ts @@ -0,0 +1,29 @@ +import { Student } from '../entities/users/student.entity.js'; + +export interface StudentDTO { + id: string; + username: string; + firstName: string; + lastName: string; + endpoints?: { + classes: string; + questions: string; + invitations: string; + groups: string; + }; +} + +export function mapToStudentDTO(student: Student): StudentDTO { + return { + id: student.username, + username: student.username, + firstName: student.firstName, + lastName: student.lastName, + }; +} + +export function mapToStudent(studentData: StudentDTO): Student { + const student = new Student(studentData.username, studentData.firstName, studentData.lastName); + + return student; +} diff --git a/backend/src/interfaces/submission.ts b/backend/src/interfaces/submission.ts new file mode 100644 index 00000000..fbaf520d --- /dev/null +++ b/backend/src/interfaces/submission.ts @@ -0,0 +1,47 @@ +import { Submission } from '../entities/assignments/submission.entity.js'; +import { Language } from '../entities/content/language.js'; +import { GroupDTO, mapToGroupDTO } from './group.js'; +import { mapToStudent, mapToStudentDTO, StudentDTO } from './student.js'; +import { mapToUser } from './user'; +import { Student } from '../entities/users/student.entity'; + +export interface SubmissionDTO { + learningObjectHruid: string; + learningObjectLanguage: Language; + learningObjectVersion: number; + + submissionNumber?: number; + submitter: StudentDTO; + time?: Date; + group?: GroupDTO; + content: string; +} + +export function mapToSubmissionDTO(submission: Submission): SubmissionDTO { + return { + learningObjectHruid: submission.learningObjectHruid, + learningObjectLanguage: submission.learningObjectLanguage, + learningObjectVersion: submission.learningObjectVersion, + + submissionNumber: submission.submissionNumber, + submitter: mapToStudentDTO(submission.submitter), + time: submission.submissionTime, + group: submission.onBehalfOf ? mapToGroupDTO(submission.onBehalfOf) : undefined, + content: submission.content, + }; +} + +export function mapToSubmission(submissionDTO: SubmissionDTO): Submission { + const submission = new Submission(); + submission.learningObjectHruid = submissionDTO.learningObjectHruid; + submission.learningObjectLanguage = submissionDTO.learningObjectLanguage; + submission.learningObjectVersion = submissionDTO.learningObjectVersion; + // Submission.submissionNumber = submissionDTO.submissionNumber; + submission.submitter = mapToStudent(submissionDTO.submitter); + // Submission.submissionTime = submissionDTO.time; + // Submission.onBehalfOf = submissionDTO.group!; + // TODO fix group + submission.content = submissionDTO.content; + + return submission; +} diff --git a/backend/src/interfaces/teacher-invitation.ts b/backend/src/interfaces/teacher-invitation.ts new file mode 100644 index 00000000..cddef566 --- /dev/null +++ b/backend/src/interfaces/teacher-invitation.ts @@ -0,0 +1,25 @@ +import { TeacherInvitation } from '../entities/classes/teacher-invitation.entity.js'; +import { ClassDTO, mapToClassDTO } from './class.js'; +import { mapToUserDTO, UserDTO } from './user.js'; + +export interface TeacherInvitationDTO { + sender: string | UserDTO; + receiver: string | UserDTO; + class: string | ClassDTO; +} + +export function mapToTeacherInvitationDTO(invitation: TeacherInvitation): TeacherInvitationDTO { + return { + sender: mapToUserDTO(invitation.sender), + receiver: mapToUserDTO(invitation.receiver), + class: mapToClassDTO(invitation.class), + }; +} + +export function mapToTeacherInvitationDTOIds(invitation: TeacherInvitation): TeacherInvitationDTO { + return { + sender: invitation.sender.username, + receiver: invitation.receiver.username, + class: invitation.class.classId!, + }; +} diff --git a/backend/src/interfaces/teacher.ts b/backend/src/interfaces/teacher.ts new file mode 100644 index 00000000..4dd6adb4 --- /dev/null +++ b/backend/src/interfaces/teacher.ts @@ -0,0 +1,29 @@ +import { Teacher } from '../entities/users/teacher.entity.js'; + +export interface TeacherDTO { + id: string; + username: string; + firstName: string; + lastName: string; + endpoints?: { + classes: string; + questions: string; + invitations: string; + groups: string; + }; +} + +export function mapToTeacherDTO(teacher: Teacher): TeacherDTO { + return { + id: teacher.username, + username: teacher.username, + firstName: teacher.firstName, + lastName: teacher.lastName, + }; +} + +export function mapToTeacher(TeacherData: TeacherDTO): Teacher { + const teacher = new Teacher(TeacherData.username, TeacherData.firstName, TeacherData.lastName); + + return teacher; +} diff --git a/backend/src/interfaces/user.ts b/backend/src/interfaces/user.ts new file mode 100644 index 00000000..58f0dd5a --- /dev/null +++ b/backend/src/interfaces/user.ts @@ -0,0 +1,30 @@ +import { User } from '../entities/users/user.entity.js'; + +export interface UserDTO { + id?: string; + username: string; + firstName: string; + lastName: string; + endpoints?: { + self: string; + classes: string; + questions: string; + invitations: string; + }; +} + +export function mapToUserDTO(user: User): UserDTO { + return { + id: user.username, + username: user.username, + firstName: user.firstName, + lastName: user.lastName, + }; +} + +export function mapToUser(userData: UserDTO, userInstance: T): T { + userInstance.username = userData.username; + userInstance.firstName = userData.firstName; + userInstance.lastName = userData.lastName; + return userInstance; +} diff --git a/backend/src/routes/assignment.ts b/backend/src/routes/assignment.ts deleted file mode 100644 index 4ae5756d..00000000 --- a/backend/src/routes/assignment.ts +++ /dev/null @@ -1,45 +0,0 @@ -import express from 'express'; -const router = express.Router(); - -// Root endpoint used to search objects -router.get('/', (req, res) => { - res.json({ - assignments: ['0', '1'], - }); -}); - -// Information about an assignment with id 'id' -router.get('/:id', (req, res) => { - res.json({ - id: req.params.id, - title: 'Dit is een test assignment', - description: 'Een korte beschrijving', - groups: ['0'], - learningPath: '0', - class: '0', - links: { - self: `${req.baseUrl}/${req.params.id}`, - submissions: `${req.baseUrl}/${req.params.id}`, - }, - }); -}); - -router.get('/:id/submissions', (req, res) => { - res.json({ - submissions: ['0'], - }); -}); - -router.get('/:id/groups', (req, res) => { - res.json({ - groups: ['0'], - }); -}); - -router.get('/:id/questions', (req, res) => { - res.json({ - questions: ['0'], - }); -}); - -export default router; diff --git a/backend/src/routes/assignments.ts b/backend/src/routes/assignments.ts new file mode 100644 index 00000000..a733d093 --- /dev/null +++ b/backend/src/routes/assignments.ts @@ -0,0 +1,30 @@ +import express from 'express'; +import { + createAssignmentHandler, + getAllAssignmentsHandler, + getAssignmentHandler, + getAssignmentsSubmissionsHandler, +} from '../controllers/assignments.js'; +import groupRouter from './groups.js'; + +const router = express.Router({ mergeParams: true }); + +// Root endpoint used to search objects +router.get('/', getAllAssignmentsHandler); + +router.post('/', createAssignmentHandler); + +// Information about an assignment with id 'id' +router.get('/:id', getAssignmentHandler); + +router.get('/:id/submissions', getAssignmentsSubmissionsHandler); + +router.get('/:id/questions', (req, res) => { + res.json({ + questions: ['0'], + }); +}); + +router.use('/:assignmentid/groups', groupRouter); + +export default router; diff --git a/backend/src/routes/auth.ts b/backend/src/routes/auth.ts index 942a997a..778e51fd 100644 --- a/backend/src/routes/auth.ts +++ b/backend/src/routes/auth.ts @@ -9,14 +9,17 @@ router.get('/config', (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) => { + /* #swagger.security = [{ "student": [ ] }] */ res.json({ message: 'If you see this, you should be a student!' }); }); 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/class.ts b/backend/src/routes/class.ts deleted file mode 100644 index 6f8f324e..00000000 --- a/backend/src/routes/class.ts +++ /dev/null @@ -1,46 +0,0 @@ -import express from 'express'; -const router = express.Router(); - -// Root endpoint used to search objects -router.get('/', (req, res) => { - res.json({ - classes: ['0', '1'], - }); -}); - -// Information about an class with id 'id' -router.get('/:id', (req, res) => { - res.json({ - id: req.params.id, - displayName: 'Klas 4B', - teachers: ['0'], - students: ['0'], - joinRequests: ['0'], - links: { - self: `${req.baseUrl}/${req.params.id}`, - classes: `${req.baseUrl}/${req.params.id}/invitations`, - questions: `${req.baseUrl}/${req.params.id}/assignments`, - students: `${req.baseUrl}/${req.params.id}/students`, - }, - }); -}); - -router.get('/:id/invitations', (req, res) => { - res.json({ - invitations: ['0'], - }); -}); - -router.get('/:id/assignments', (req, res) => { - res.json({ - assignments: ['0'], - }); -}); - -router.get('/:id/students', (req, res) => { - res.json({ - students: ['0'], - }); -}); - -export default router; diff --git a/backend/src/routes/classes.ts b/backend/src/routes/classes.ts new file mode 100644 index 00000000..e0972988 --- /dev/null +++ b/backend/src/routes/classes.ts @@ -0,0 +1,27 @@ +import express from 'express'; +import { + createClassHandler, + getAllClassesHandler, + getClassHandler, + getClassStudentsHandler, + getTeacherInvitationsHandler, +} from '../controllers/classes.js'; +import assignmentRouter from './assignments.js'; + +const router = express.Router(); + +// Root endpoint used to search objects +router.get('/', getAllClassesHandler); + +router.post('/', createClassHandler); + +// Information about an class with id 'id' +router.get('/:id', getClassHandler); + +router.get('/:id/teacher-invitations', getTeacherInvitationsHandler); + +router.get('/:id/students', getClassStudentsHandler); + +router.use('/:classid/assignments', assignmentRouter); + +export default router; diff --git a/backend/src/routes/group.ts b/backend/src/routes/group.ts deleted file mode 100644 index 303f5215..00000000 --- a/backend/src/routes/group.ts +++ /dev/null @@ -1,31 +0,0 @@ -import express from 'express'; -const router = express.Router(); - -// Root endpoint used to search objects -router.get('/', (req, res) => { - res.json({ - groups: ['0', '1'], - }); -}); - -// Information about a group (members, ... [TODO DOC]) -router.get('/:id', (req, res) => { - res.json({ - id: req.params.id, - assignment: '0', - students: ['0'], - submissions: ['0'], - // Reference to other endpoint - // Should be less hardcoded - questions: `/group/${req.params.id}/question`, - }); -}); - -// The list of questions a group has made -router.get('/:id/question', (req, res) => { - res.json({ - questions: ['0'], - }); -}); - -export default router; diff --git a/backend/src/routes/groups.ts b/backend/src/routes/groups.ts new file mode 100644 index 00000000..0c9692b0 --- /dev/null +++ b/backend/src/routes/groups.ts @@ -0,0 +1,23 @@ +import express from 'express'; +import { createGroupHandler, getAllGroupsHandler, getGroupHandler, getGroupSubmissionsHandler } from '../controllers/groups.js'; + +const router = express.Router({ mergeParams: true }); + +// Root endpoint used to search objects +router.get('/', getAllGroupsHandler); + +router.post('/', createGroupHandler); + +// Information about a group (members, ... [TODO DOC]) +router.get('/:groupid', getGroupHandler); + +router.get('/:groupid', getGroupSubmissionsHandler); + +// The list of questions a group has made +router.get('/:id/questions', (req, res) => { + res.json({ + questions: ['0'], + }); +}); + +export default router; diff --git a/backend/src/routes/learning-objects.ts b/backend/src/routes/learning-objects.ts index b731fe69..7532765b 100644 --- a/backend/src/routes/learning-objects.ts +++ b/backend/src/routes/learning-objects.ts @@ -1,6 +1,9 @@ import express from 'express'; import { getAllLearningObjects, getAttachment, getLearningObject, getLearningObjectHTML } from '../controllers/learning-objects.js'; +import submissionRoutes from './submissions.js'; +import questionRoutes from './questions.js'; + const router = express.Router(); // DWENGO learning objects @@ -21,6 +24,10 @@ router.get('/', getAllLearningObjects); // Example: http://localhost:3000/learningObject/un_ai7 router.get('/:hruid', getLearningObject); +router.use('/:hruid/submissions', submissionRoutes); + +router.use('/:hruid/:version/questions', questionRoutes); + // Parameter: hruid of learning object // Query: language, version (optional) // Route to fetch the HTML rendering of one learning object based on its hruid. diff --git a/backend/src/routes/question.ts b/backend/src/routes/question.ts deleted file mode 100644 index 2e5db624..00000000 --- a/backend/src/routes/question.ts +++ /dev/null @@ -1,33 +0,0 @@ -import express from 'express'; -const router = express.Router(); - -// Root endpoint used to search objects -router.get('/', (req, res) => { - res.json({ - questions: ['0', '1'], - }); -}); - -// Information about an question with id 'id' -router.get('/:id', (req, res) => { - res.json({ - id: req.params.id, - student: '0', - group: '0', - time: new Date(2025, 1, 1), - content: 'Zijn alle gehele getallen groter dan 2 gelijk aan de som van 2 priemgetallen????', - learningObject: '0', - links: { - self: `${req.baseUrl}/${req.params.id}`, - answers: `${req.baseUrl}/${req.params.id}/answers`, - }, - }); -}); - -router.get('/:id/answers', (req, res) => { - res.json({ - answers: ['0'], - }); -}); - -export default router; diff --git a/backend/src/routes/questions.ts b/backend/src/routes/questions.ts new file mode 100644 index 00000000..31a71f3b --- /dev/null +++ b/backend/src/routes/questions.ts @@ -0,0 +1,25 @@ +import express from 'express'; +import { + createQuestionHandler, + deleteQuestionHandler, + getAllQuestionsHandler, + getQuestionAnswersHandler, + getQuestionHandler, +} from '../controllers/questions.js'; +const router = express.Router({ mergeParams: true }); + +// Query language + +// Root endpoint used to search objects +router.get('/', getAllQuestionsHandler); + +router.post('/', createQuestionHandler); + +router.delete('/:seq', deleteQuestionHandler); + +// Information about a question with id +router.get('/:seq', getQuestionHandler); + +router.get('/answers/:seq', getQuestionAnswersHandler); + +export default router; diff --git a/backend/src/routes/router.ts b/backend/src/routes/router.ts new file mode 100644 index 00000000..639857a7 --- /dev/null +++ b/backend/src/routes/router.ts @@ -0,0 +1,35 @@ +import { Response, Router } from 'express'; +import studentRouter from './students.js'; +import groupRouter from './groups.js'; +import assignmentRouter from './assignments.js'; +import submissionRouter from './submissions.js'; +import classRouter from './classes.js'; +import questionRouter from './questions.js'; +import authRouter from './auth.js'; +import themeRoutes from './themes.js'; +import learningPathRoutes from './learning-paths.js'; +import learningObjectRoutes from './learning-objects.js'; +import { getLogger, Logger } from '../logging/initalize.js'; + +const router = Router(); +const logger: Logger = getLogger(); + +router.get('/', (_, res: Response) => { + logger.debug('GET /'); + res.json({ + message: 'Hello Dwengo!🚀', + }); +}); + +router.use('/student', studentRouter /* #swagger.tags = ['Student'] */); +router.use('/group', groupRouter /* #swagger.tags = ['Group'] */); +router.use('/assignment', assignmentRouter /* #swagger.tags = ['Assignment'] */); +router.use('/submission', submissionRouter /* #swagger.tags = ['Submission'] */); +router.use('/class', classRouter /* #swagger.tags = ['Class'] */); +router.use('/question', questionRouter /* #swagger.tags = ['Question'] */); +router.use('/auth', authRouter /* #swagger.tags = ['Auth'] */); +router.use('/theme', themeRoutes /* #swagger.tags = ['Theme'] */); +router.use('/learningPath', learningPathRoutes /* #swagger.tags = ['Learning Path'] */); +router.use('/learningObject', learningObjectRoutes /* #swagger.tags = ['Learning Object'] */); + +export default router; diff --git a/backend/src/routes/student.ts b/backend/src/routes/student.ts deleted file mode 100644 index 9cb0cdee..00000000 --- a/backend/src/routes/student.ts +++ /dev/null @@ -1,55 +0,0 @@ -import express from 'express'; -const router = express.Router(); - -// Root endpoint used to search objects -router.get('/', (req, res) => { - res.json({ - students: ['0', '1'], - }); -}); - -// Information about a student's profile -router.get('/:id', (req, res) => { - res.json({ - id: req.params.id, - firstName: 'Jimmy', - lastName: 'Faster', - username: 'JimmyFaster2', - endpoints: { - classes: `/student/${req.params.id}/classes`, - questions: `/student/${req.params.id}/submissions`, - invitations: `/student/${req.params.id}/assignments`, - groups: `/student/${req.params.id}/groups`, - }, - }); -}); - -// The list of classes a student is in -router.get('/:id/classes', (req, res) => { - res.json({ - classes: ['0'], - }); -}); - -// The list of submissions a student has made -router.get('/:id/submissions', (req, res) => { - res.json({ - submissions: ['0'], - }); -}); - -// The list of assignments a student has -router.get('/:id/assignments', (req, res) => { - res.json({ - assignments: ['0'], - }); -}); - -// The list of groups a student is in -router.get('/:id/groups', (req, res) => { - res.json({ - groups: ['0'], - }); -}); - -export default router; diff --git a/backend/src/routes/students.ts b/backend/src/routes/students.ts new file mode 100644 index 00000000..7ed7a666 --- /dev/null +++ b/backend/src/routes/students.ts @@ -0,0 +1,46 @@ +import express from 'express'; +import { + createStudentHandler, + deleteStudentHandler, + getAllStudentsHandler, + getStudentAssignmentsHandler, + getStudentClassesHandler, + getStudentGroupsHandler, + getStudentHandler, + getStudentSubmissionsHandler, +} from '../controllers/students.js'; +import { getStudentGroups } from '../services/students.js'; +const router = express.Router(); + +// Root endpoint used to search objects +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); + +// The list of submissions a student has made +router.get('/:id/submissions', getStudentSubmissionsHandler); + +// The list of assignments a student has +router.get('/:id/assignments', getStudentAssignmentsHandler); + +// The list of groups a student is in +router.get('/:id/groups', getStudentGroupsHandler); + +// A list of questions a user has created +router.get('/:id/questions', (req, res) => { + res.json({ + questions: ['0'], + }); +}); + +export default router; diff --git a/backend/src/routes/submission.ts b/backend/src/routes/submission.ts deleted file mode 100644 index cb4d3e85..00000000 --- a/backend/src/routes/submission.ts +++ /dev/null @@ -1,23 +0,0 @@ -import express from 'express'; -const router = express.Router(); - -// Root endpoint used to search objects -router.get('/', (req, res) => { - res.json({ - submissions: ['0', '1'], - }); -}); - -// Information about an submission with id 'id' -router.get('/:id', (req, res) => { - res.json({ - id: req.params.id, - student: '0', - group: '0', - time: new Date(2025, 1, 1), - content: 'Wortel 2 is rationeel', - learningObject: '0', - }); -}); - -export default router; diff --git a/backend/src/routes/submissions.ts b/backend/src/routes/submissions.ts new file mode 100644 index 00000000..4db93027 --- /dev/null +++ b/backend/src/routes/submissions.ts @@ -0,0 +1,19 @@ +import express from 'express'; +import { createSubmissionHandler, deleteSubmissionHandler, getSubmissionHandler } from '../controllers/submissions.js'; +const router = express.Router({ mergeParams: true }); + +// Root endpoint used to search objects +router.get('/', (req, res) => { + res.json({ + submissions: ['0', '1'], + }); +}); + +router.post('/:id', createSubmissionHandler); + +// Information about an submission with id 'id' +router.get('/:id', getSubmissionHandler); + +router.delete('/:id', deleteSubmissionHandler); + +export default router; diff --git a/backend/src/routes/teacher.ts b/backend/src/routes/teacher.ts deleted file mode 100644 index a7c60bc9..00000000 --- a/backend/src/routes/teacher.ts +++ /dev/null @@ -1,48 +0,0 @@ -import express from 'express'; -const router = express.Router(); - -// Root endpoint used to search objects -router.get('/', (req, res) => { - res.json({ - teachers: ['0', '1'], - }); -}); - -// Information about a teacher -router.get('/:id', (req, res) => { - res.json({ - id: req.params.id, - firstName: 'John', - lastName: 'Doe', - username: 'JohnDoe1', - links: { - self: `${req.baseUrl}/${req.params.id}`, - classes: `${req.baseUrl}/${req.params.id}/classes`, - questions: `${req.baseUrl}/${req.params.id}/questions`, - invitations: `${req.baseUrl}/${req.params.id}/invitations`, - }, - }); -}); - -// The questions students asked a teacher -router.get('/:id/questions', (req, res) => { - res.json({ - questions: ['0'], - }); -}); - -// Invitations to other classes a teacher received -router.get('/:id/invitations', (req, res) => { - res.json({ - invitations: ['0'], - }); -}); - -// A list with ids of classes a teacher is in -router.get('/:id/classes', (req, res) => { - res.json({ - classes: ['0'], - }); -}); - -export default router; diff --git a/backend/src/routes/teachers.ts b/backend/src/routes/teachers.ts new file mode 100644 index 00000000..c04e1575 --- /dev/null +++ b/backend/src/routes/teachers.ts @@ -0,0 +1,37 @@ +import express from 'express'; +import { + createTeacherHandler, + deleteTeacherHandler, + getAllTeachersHandler, + getTeacherClassHandler, + getTeacherHandler, + getTeacherQuestionHandler, + getTeacherStudentHandler, +} from '../controllers/teachers.js'; +const router = express.Router(); + +// Root endpoint used to search objects +router.get('/', getAllTeachersHandler); + +router.post('/', createTeacherHandler); + +router.delete('/', deleteTeacherHandler); + +router.get('/:username', getTeacherHandler); + +router.delete('/:username', deleteTeacherHandler); + +router.get('/:username/classes', getTeacherClassHandler); + +router.get('/:username/students', getTeacherStudentHandler); + +router.get('/:username/questions', getTeacherQuestionHandler); + +// Invitations to other classes a teacher received +router.get('/:id/invitations', (req, res) => { + res.json({ + invitations: ['0'], + }); +}); + +export default router; diff --git a/backend/src/services/assignments.ts b/backend/src/services/assignments.ts new file mode 100644 index 00000000..be121810 --- /dev/null +++ b/backend/src/services/assignments.ts @@ -0,0 +1,85 @@ +import { getAssignmentRepository, getClassRepository, getGroupRepository, getSubmissionRepository } from '../data/repositories.js'; +import { Assignment } from '../entities/assignments/assignment.entity.js'; +import { AssignmentDTO, mapToAssignment, mapToAssignmentDTO, mapToAssignmentDTOId } from '../interfaces/assignment.js'; +import { mapToSubmissionDTO, SubmissionDTO } from '../interfaces/submission.js'; + +export async function getAllAssignments(classid: string, full: boolean): Promise { + const classRepository = getClassRepository(); + const cls = await classRepository.findById(classid); + + if (!cls) { + return []; + } + + const assignmentRepository = getAssignmentRepository(); + const assignments = await assignmentRepository.findAllAssignmentsInClass(cls); + + if (full) { + return assignments.map(mapToAssignmentDTO); + } + + return assignments.map(mapToAssignmentDTOId); +} + +export async function createAssignment(classid: string, assignmentData: AssignmentDTO): Promise { + const classRepository = getClassRepository(); + const cls = await classRepository.findById(classid); + + if (!cls) { + return null; + } + + const assignment = mapToAssignment(assignmentData, cls); + const assignmentRepository = getAssignmentRepository(); + + try { + const newAssignment = assignmentRepository.create(assignment); + await assignmentRepository.save(newAssignment); + + return newAssignment; + } catch (e) { + return null; + } +} + +export async function getAssignment(classid: string, id: number): Promise { + const classRepository = getClassRepository(); + const cls = await classRepository.findById(classid); + + if (!cls) { + return null; + } + + const assignmentRepository = getAssignmentRepository(); + const assignment = await assignmentRepository.findByClassAndId(cls, id); + + if (!assignment) { + return null; + } + + return mapToAssignmentDTO(assignment); +} + +export async function getAssignmentsSubmissions(classid: string, assignmentNumber: number): Promise { + const classRepository = getClassRepository(); + const cls = await classRepository.findById(classid); + + if (!cls) { + return []; + } + + const assignmentRepository = getAssignmentRepository(); + const assignment = await assignmentRepository.findByClassAndId(cls, assignmentNumber); + + if (!assignment) { + return []; + } + + const groupRepository = getGroupRepository(); + const groups = await groupRepository.findAllGroupsForAssignment(assignment); + + const submissionRepository = getSubmissionRepository(); + const submissions = (await Promise.all(groups.map((group) => submissionRepository.findAllSubmissionsForGroup(group)))).flat(); + + return submissions.map(mapToSubmissionDTO); +} diff --git a/backend/src/services/class.ts b/backend/src/services/class.ts new file mode 100644 index 00000000..9f6e1efe --- /dev/null +++ b/backend/src/services/class.ts @@ -0,0 +1,99 @@ +import { getClassRepository, getStudentRepository, getTeacherInvitationRepository, getTeacherRepository } from '../data/repositories.js'; +import { Class } from '../entities/classes/class.entity.js'; +import { ClassDTO, mapToClassDTO } from '../interfaces/class.js'; +import { mapToStudentDTO, StudentDTO } from '../interfaces/student.js'; +import { mapToTeacherInvitationDTO, mapToTeacherInvitationDTOIds, TeacherInvitationDTO } from '../interfaces/teacher-invitation.js'; +import { getLogger } from '../logging/initalize.js'; + +const logger = getLogger(); + +export async function getAllClasses(full: boolean): Promise { + const classRepository = getClassRepository(); + const classes = await classRepository.find({}, { populate: ['students', 'teachers'] }); + + if (!classes) { + return []; + } + + if (full) { + return classes.map(mapToClassDTO); + } + return classes.map((cls) => cls.classId!); +} + +export async function createClass(classData: ClassDTO): Promise { + const teacherRepository = getTeacherRepository(); + const teacherUsernames = classData.teachers || []; + const teachers = (await Promise.all(teacherUsernames.map((id) => teacherRepository.findByUsername(id)))).filter((teacher) => teacher != null); + + const studentRepository = getStudentRepository(); + const studentUsernames = classData.students || []; + const students = (await Promise.all(studentUsernames.map((id) => studentRepository.findByUsername(id)))).filter((student) => student != null); + + //Const cls = mapToClass(classData, teachers, students); + + const classRepository = getClassRepository(); + + try { + const newClass = classRepository.create({ + displayName: classData.displayName, + teachers: teachers, + students: students, + }); + await classRepository.save(newClass); + + return newClass; + } catch (e) { + logger.error(e); + return null; + } +} + +export async function getClass(classId: string): Promise { + const classRepository = getClassRepository(); + const cls = await classRepository.findById(classId); + + if (!cls) { + return null; + } + + return mapToClassDTO(cls); +} + +async function fetchClassStudents(classId: string): Promise { + const classRepository = getClassRepository(); + const cls = await classRepository.findById(classId); + + if (!cls) { + return []; + } + + return cls.students.map(mapToStudentDTO); +} + +export async function getClassStudents(classId: string): Promise { + return await fetchClassStudents(classId); +} + +export async function getClassStudentsIds(classId: string): Promise { + const students: StudentDTO[] = await fetchClassStudents(classId); + return students.map((student) => student.username); +} + +export async function getClassTeacherInvitations(classId: string, full: boolean): Promise { + const classRepository = getClassRepository(); + const cls = await classRepository.findById(classId); + + if (!cls) { + return []; + } + + const teacherInvitationRepository = getTeacherInvitationRepository(); + const invitations = await teacherInvitationRepository.findAllInvitationsForClass(cls); + + if (full) { + return invitations.map(mapToTeacherInvitationDTO); + } + + return invitations.map(mapToTeacherInvitationDTOIds); +} diff --git a/backend/src/services/groups.ts b/backend/src/services/groups.ts new file mode 100644 index 00000000..91091703 --- /dev/null +++ b/backend/src/services/groups.ts @@ -0,0 +1,132 @@ +import { GroupRepository } from '../data/assignments/group-repository.js'; +import { + getAssignmentRepository, + getClassRepository, + getGroupRepository, + getStudentRepository, + getSubmissionRepository, +} from '../data/repositories.js'; +import { Group } from '../entities/assignments/group.entity.js'; +import { GroupDTO, mapToGroupDTO, mapToGroupDTOId } from '../interfaces/group.js'; +import { mapToSubmissionDTO, SubmissionDTO } from '../interfaces/submission.js'; + +export async function getGroup(classId: string, assignmentNumber: number, groupNumber: number, full: boolean): Promise { + const classRepository = getClassRepository(); + const cls = await classRepository.findById(classId); + + if (!cls) { + return null; + } + + const assignmentRepository = getAssignmentRepository(); + const assignment = await assignmentRepository.findByClassAndId(cls, assignmentNumber); + + if (!assignment) { + return null; + } + + const groupRepository = getGroupRepository(); + const group = await groupRepository.findByAssignmentAndGroupNumber(assignment, groupNumber); + + if (!group) { + return null; + } + + if (full) { + return mapToGroupDTO(group); + } + + return mapToGroupDTOId(group); +} + +export async function createGroup(groupData: GroupDTO, classid: string, assignmentNumber: number): Promise { + 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); + + console.log(members); + + const classRepository = getClassRepository(); + const cls = await classRepository.findById(classid); + + if (!cls) { + return null; + } + + const assignmentRepository = getAssignmentRepository(); + const assignment = await assignmentRepository.findByClassAndId(cls, assignmentNumber); + + if (!assignment) { + return null; + } + + const groupRepository = getGroupRepository(); + try { + const newGroup = groupRepository.create({ + assignment: assignment, + members: members, + }); + await groupRepository.save(newGroup); + + return newGroup; + } catch (e) { + console.log(e); + return null; + } +} + +export async function getAllGroups(classId: string, assignmentNumber: number, full: boolean): Promise { + const classRepository = getClassRepository(); + const cls = await classRepository.findById(classId); + + if (!cls) { + return []; + } + + const assignmentRepository = getAssignmentRepository(); + const assignment = await assignmentRepository.findByClassAndId(cls, assignmentNumber); + + if (!assignment) { + return []; + } + + const groupRepository = getGroupRepository(); + const groups = await groupRepository.findAllGroupsForAssignment(assignment); + + if (full) { + console.log('full'); + console.log(groups); + return groups.map(mapToGroupDTO); + } + + return groups.map(mapToGroupDTOId); +} + +export async function getGroupSubmissions(classId: string, assignmentNumber: number, groupNumber: number): Promise { + const classRepository = getClassRepository(); + const cls = await classRepository.findById(classId); + + if (!cls) { + return []; + } + + const assignmentRepository = getAssignmentRepository(); + const assignment = await assignmentRepository.findByClassAndId(cls, assignmentNumber); + + if (!assignment) { + return []; + } + + const groupRepository = getGroupRepository(); + const group = await groupRepository.findByAssignmentAndGroupNumber(assignment, groupNumber); + + if (!group) { + return []; + } + + const submissionRepository = getSubmissionRepository(); + const submissions = await submissionRepository.findAllSubmissionsForGroup(group); + + return submissions.map(mapToSubmissionDTO); +} diff --git a/backend/src/services/learning-objects.ts b/backend/src/services/learning-objects.ts new file mode 100644 index 00000000..fb579471 --- /dev/null +++ b/backend/src/services/learning-objects.ts @@ -0,0 +1,90 @@ +import { DWENGO_API_BASE } from '../config.js'; +import { fetchWithLogging } from '../util/api-helper.js'; +import { FilteredLearningObject, LearningObjectMetadata, LearningObjectNode, LearningPathResponse } from '../interfaces/learning-content.js'; + +function filterData(data: LearningObjectMetadata, htmlUrl: string): FilteredLearningObject { + return { + key: data.hruid, // Hruid learningObject (not path) + _id: data._id, + uuid: data.uuid, + version: data.version, + title: data.title, + htmlUrl, // Url to fetch html content + language: data.language, + difficulty: data.difficulty, + estimatedTime: data.estimated_time, + available: data.available, + teacherExclusive: data.teacher_exclusive, + educationalGoals: data.educational_goals, // List with learningObjects + keywords: data.keywords, // For search + description: data.description, // For search (not an actual description) + targetAges: data.target_ages, + contentType: data.content_type, // Markdown, image, audio, etc. + contentLocation: data.content_location, // If content type extern + skosConcepts: data.skos_concepts, + returnValue: data.return_value, // Callback response information + }; +} + +/** + * Fetches a single learning object by its HRUID + */ +export async function getLearningObjectById(hruid: string, language: string): Promise { + const metadataUrl = `${DWENGO_API_BASE}/learningObject/getMetadata?hruid=${hruid}&language=${language}`; + const metadata = await fetchWithLogging( + metadataUrl, + `Metadata for Learning Object HRUID "${hruid}" (language ${language})` + ); + + if (!metadata) { + console.error(`⚠️ WARNING: Learning object "${hruid}" not found.`); + return null; + } + + const htmlUrl = `${DWENGO_API_BASE}/learningObject/getRaw?hruid=${hruid}&language=${language}`; + return filterData(metadata, htmlUrl); +} + +/** + * Generic function to fetch learning objects (full data or just HRUIDs) + */ +async function fetchLearningObjects(hruid: string, full: boolean, language: string): Promise { + try { + 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.`); + return []; + } + + const nodes: LearningObjectNode[] = learningPathResponse.data[0].nodes; + + if (!full) { + return nodes.map((node) => node.learningobject_hruid); + } + + return await Promise.all(nodes.map(async (node) => getLearningObjectById(node.learningobject_hruid, language))).then((objects) => + objects.filter((obj): obj is FilteredLearningObject => obj !== null) + ); + } catch (error) { + console.error('❌ Error fetching learning objects:', error); + return []; + } +} + +/** + * Fetch full learning object data (metadata) + */ +export async function getLearningObjectsFromPath(hruid: string, language: string): Promise { + return (await fetchLearningObjects(hruid, true, language)) as FilteredLearningObject[]; +} + +/** + * Fetch only learning object HRUIDs + */ +export async function getLearningObjectIdsFromPath(hruid: string, language: string): Promise { + return (await fetchLearningObjects(hruid, false, language)) as string[]; +} +function fetchLearningPaths(arg0: string[], language: string, arg2: string): LearningPathResponse | PromiseLike { + throw new Error('Function not implemented.'); +} diff --git a/backend/src/services/learning-objects/dwengo-api-learning-object-provider.ts b/backend/src/services/learning-objects/dwengo-api-learning-object-provider.ts index 37e68c07..dfee329d 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,5 @@ import { DWENGO_API_BASE } from '../../config.js'; -import { fetchWithLogging } from '../../util/apiHelper.js'; +import { fetchWithLogging } from '../../util/api-helper.js'; import { FilteredLearningObject, LearningObjectIdentifier, diff --git a/backend/src/services/learning-paths/database-learning-path-provider.ts b/backend/src/services/learning-paths/database-learning-path-provider.ts index fa8f42c6..68986885 100644 --- a/backend/src/services/learning-paths/database-learning-path-provider.ts +++ b/backend/src/services/learning-paths/database-learning-path-provider.ts @@ -29,7 +29,7 @@ async function getLearningObjectsForNodes(nodes: LearningPathNode[]): Promise it === null)) { + if (Array.from(nullableNodesToLearningObjects.values()).some((it) => it === null)) { throw new Error('At least one of the learning objects on this path could not be found.'); } return nullableNodesToLearningObjects as Map; @@ -41,15 +41,9 @@ async function getLearningObjectsForNodes(nodes: LearningPathNode[]): Promise { const nodesToLearningObjects: Map = await getLearningObjectsForNodes(learningPath.nodes); - const targetAges = nodesToLearningObjects - .values() - .flatMap((it) => it.targetAges || []) - .toArray(); + const targetAges = Array.from(nodesToLearningObjects.values()).flatMap((it) => it.targetAges || []); - const keywords = nodesToLearningObjects - .values() - .flatMap((it) => it.keywords || []) - .toArray(); + const keywords = Array.from(nodesToLearningObjects.values()).flatMap((it) => it.keywords || []); const image = learningPath.image ? learningPath.image.toString('base64') : undefined; @@ -83,27 +77,24 @@ async function convertNodes( nodesToLearningObjects: Map, personalizedFor?: PersonalizationTarget ): Promise { - const nodesPromise = nodesToLearningObjects - .entries() - .map(async (entry) => { - const [node, learningObject] = entry; - const lastSubmission = personalizedFor ? await getLastSubmissionForCustomizationTarget(node, personalizedFor) : null; - return { - _id: learningObject.uuid, - language: learningObject.language, - start_node: node.startNode, - created_at: node.createdAt.toISOString(), - updatedAt: node.updatedAt.toISOString(), - learningobject_hruid: node.learningObjectHruid, - version: learningObject.version, - transitions: node.transitions - .filter( - (trans) => !personalizedFor || isTransitionPossible(trans, optionalJsonStringToObject(lastSubmission?.content)) // If we want a personalized learning path, remove all transitions that aren't possible. - ) - .map((trans, i) => convertTransition(trans, i, nodesToLearningObjects)), // Then convert all the transition - }; - }) - .toArray(); + const nodesPromise = Array.from(nodesToLearningObjects.entries()).map(async (entry) => { + const [node, learningObject] = entry; + const lastSubmission = personalizedFor ? await getLastSubmissionForCustomizationTarget(node, personalizedFor) : null; + return { + _id: learningObject.uuid, + language: learningObject.language, + start_node: node.startNode, + created_at: node.createdAt.toISOString(), + updatedAt: node.updatedAt.toISOString(), + learningobject_hruid: node.learningObjectHruid, + version: learningObject.version, + transitions: node.transitions + .filter( + (trans) => !personalizedFor || isTransitionPossible(trans, optionalJsonStringToObject(lastSubmission?.content)) // If we want a personalized learning path, remove all transitions that aren't possible. + ) + .map((trans, i) => convertTransition(trans, i, nodesToLearningObjects)), // Then convert all the transition + }; + }); return await Promise.all(nodesPromise); } diff --git a/backend/src/services/learning-paths/dwengo-api-learning-path-provider.ts b/backend/src/services/learning-paths/dwengo-api-learning-path-provider.ts index 2b1d17a6..a6093bb4 100644 --- a/backend/src/services/learning-paths/dwengo-api-learning-path-provider.ts +++ b/backend/src/services/learning-paths/dwengo-api-learning-path-provider.ts @@ -1,4 +1,4 @@ -import { fetchWithLogging } from '../../util/apiHelper.js'; +import { fetchWithLogging } from '../../util/api-helper.js'; import { DWENGO_API_BASE } from '../../config.js'; import { LearningPath, LearningPathResponse } from '../../interfaces/learning-content.js'; import { LearningPathProvider } from './learning-path-provider.js'; diff --git a/backend/src/services/questions.ts b/backend/src/services/questions.ts new file mode 100644 index 00000000..ee003bcd --- /dev/null +++ b/backend/src/services/questions.ts @@ -0,0 +1,107 @@ +import { getAnswerRepository, getQuestionRepository } from '../data/repositories.js'; +import { mapToQuestionDTO, mapToQuestionId, QuestionDTO, QuestionId } from '../interfaces/question.js'; +import { Question } from '../entities/questions/question.entity.js'; +import { Answer } from '../entities/questions/answer.entity.js'; +import { mapToAnswerDTO, mapToAnswerId } from '../interfaces/answer.js'; +import { QuestionRepository } from '../data/questions/question-repository.js'; +import { LearningObjectIdentifier } from '../entities/content/learning-object-identifier.js'; +import { mapToUser } from '../interfaces/user.js'; +import { Student } from '../entities/users/student.entity.js'; +import { mapToStudent } from '../interfaces/student.js'; + +export async function getAllQuestions(id: LearningObjectIdentifier, full: boolean): Promise { + const questionRepository: QuestionRepository = getQuestionRepository(); + const questions = await questionRepository.findAllQuestionsAboutLearningObject(id); + + if (!questions) { + return []; + } + + const questionsDTO: QuestionDTO[] = questions.map(mapToQuestionDTO); + + if (full) { + return questionsDTO; + } + + return questionsDTO.map(mapToQuestionId); +} + +async function fetchQuestion(questionId: QuestionId): Promise { + const questionRepository = getQuestionRepository(); + + return await questionRepository.findOne({ + learningObjectHruid: questionId.learningObjectIdentifier.hruid, + learningObjectLanguage: questionId.learningObjectIdentifier.language, + learningObjectVersion: questionId.learningObjectIdentifier.version, + sequenceNumber: questionId.sequenceNumber, + }); +} + +export async function getQuestion(questionId: QuestionId): Promise { + const question = await fetchQuestion(questionId); + + if (!question) { + return null; + } + + return mapToQuestionDTO(question); +} + +export async function getAnswersByQuestion(questionId: QuestionId, full: boolean) { + const answerRepository = getAnswerRepository(); + const question = await fetchQuestion(questionId); + + if (!question) { + return []; + } + + const answers: Answer[] = await answerRepository.findAllAnswersToQuestion(question); + + if (!answers) { + return []; + } + + const answersDTO = answers.map(mapToAnswerDTO); + + if (full) { + return answersDTO; + } + + return answersDTO.map(mapToAnswerId); +} + +export async function createQuestion(questionDTO: QuestionDTO) { + const questionRepository = getQuestionRepository(); + + const author = mapToStudent(questionDTO.author); + + try { + await questionRepository.createQuestion({ + loId: questionDTO.learningObjectIdentifier, + author, + content: questionDTO.content, + }); + } catch (e) { + return null; + } + + return questionDTO; +} + +export async function deleteQuestion(questionId: QuestionId) { + const questionRepository = getQuestionRepository(); + + const question = await fetchQuestion(questionId); + + if (!question) { + return null; + } + + try { + await questionRepository.removeQuestionByLearningObjectAndSequenceNumber(questionId.learningObjectIdentifier, questionId.sequenceNumber); + } catch (e) { + return null; + } + + return question; +} diff --git a/backend/src/services/students.ts b/backend/src/services/students.ts new file mode 100644 index 00000000..5099a18d --- /dev/null +++ b/backend/src/services/students.ts @@ -0,0 +1,126 @@ +import { getClassRepository, getGroupRepository, getStudentRepository, getSubmissionRepository } from '../data/repositories.js'; +import { Class } from '../entities/classes/class.entity.js'; +import { Student } from '../entities/users/student.entity.js'; +import { AssignmentDTO } from '../interfaces/assignment.js'; +import { ClassDTO, mapToClassDTO } from '../interfaces/class.js'; +import { GroupDTO, mapToGroupDTO, mapToGroupDTOId } from '../interfaces/group.js'; +import { mapToStudent, mapToStudentDTO, StudentDTO } from '../interfaces/student.js'; +import { mapToSubmissionDTO, SubmissionDTO } from '../interfaces/submission.js'; +import { getAllAssignments } from './assignments.js'; +import { UserService } from './users.js'; + +export async function getAllStudents(): Promise { + const studentRepository = getStudentRepository(); + const users = await studentRepository.findAll(); + return users.map(mapToStudentDTO); +} + +export async function getAllStudentIds(): Promise { + const users = await getAllStudents(); + return users.map((user) => user.username); +} + +export async function getStudent(username: string): Promise { + const studentRepository = getStudentRepository(); + const user = await studentRepository.findByUsername(username); + return user ? mapToStudentDTO(user) : null; +} + +export async function createStudent(userData: StudentDTO): Promise { + const studentRepository = getStudentRepository(); + + try { + const newStudent = studentRepository.create(mapToStudent(userData)); + await studentRepository.save(newStudent); + + return mapToStudentDTO(newStudent); + } catch (e) { + console.log(e); + return null; + } +} + +export async function deleteStudent(username: string): Promise { + const studentRepository = getStudentRepository(); + + const user = await studentRepository.findByUsername(username); + + if (!user) { + return null; + } + + try { + await studentRepository.deleteByUsername(username); + + return mapToStudentDTO(user); + } catch (e) { + console.log(e); + return null; + } +} + +export async function getStudentClasses(username: string, full: boolean): Promise { + const studentRepository = getStudentRepository(); + const student = await studentRepository.findByUsername(username); + + if (!student) { + return []; + } + + const classRepository = getClassRepository(); + const classes = await classRepository.findByStudent(student); + + if (full) { + return classes.map(mapToClassDTO); + } + + return classes.map((cls) => cls.classId!); +} + +export async function getStudentAssignments(username: string, full: boolean): Promise { + const studentRepository = getStudentRepository(); + const student = await studentRepository.findByUsername(username); + + if (!student) { + return []; + } + + const classRepository = getClassRepository(); + const classes = await classRepository.findByStudent(student); + + const assignments = (await Promise.all(classes.map(async (cls) => await getAllAssignments(cls.classId!, full)))).flat(); + + return assignments; +} + +export async function getStudentGroups(username: string, full: boolean): Promise { + const studentRepository = getStudentRepository(); + const student = await studentRepository.findByUsername(username); + + if (!student) { + return []; + } + + const groupRepository = getGroupRepository(); + const groups = await groupRepository.findAllGroupsWithStudent(student); + + if (full) { + return groups.map(mapToGroupDTO); + } + + return groups.map(mapToGroupDTOId); +} + +export async function getStudentSubmissions(username: string): Promise { + const studentRepository = getStudentRepository(); + const student = await studentRepository.findByUsername(username); + + if (!student) { + return []; + } + + const submissionRepository = getSubmissionRepository(); + const submissions = await submissionRepository.findAllSubmissionsForStudent(student); + + return submissions.map(mapToSubmissionDTO); +} diff --git a/backend/src/services/submissions.ts b/backend/src/services/submissions.ts new file mode 100644 index 00000000..a8fa96c7 --- /dev/null +++ b/backend/src/services/submissions.ts @@ -0,0 +1,51 @@ +import { getGroupRepository, getSubmissionRepository } from '../data/repositories.js'; +import { Language } from '../entities/content/language.js'; +import { LearningObjectIdentifier } from '../entities/content/learning-object-identifier.js'; +import { mapToSubmission, mapToSubmissionDTO, SubmissionDTO } from '../interfaces/submission.js'; + +export async function getSubmission( + learningObjectHruid: string, + language: Language, + version: number, + submissionNumber: number +): Promise { + const loId = new LearningObjectIdentifier(learningObjectHruid, language, version); + + const submissionRepository = getSubmissionRepository(); + const submission = await submissionRepository.findSubmissionByLearningObjectAndSubmissionNumber(loId, submissionNumber); + + if (!submission) { + return null; + } + + return mapToSubmissionDTO(submission); +} + +export async function createSubmission(submissionDTO: SubmissionDTO) { + const submissionRepository = getSubmissionRepository(); + const submission = mapToSubmission(submissionDTO); + + try { + const newSubmission = await submissionRepository.create(submission); + await submissionRepository.save(newSubmission); + } catch (e) { + return null; + } + + return submission; +} + +export async function deleteSubmission(learningObjectHruid: string, language: Language, version: number, submissionNumber: number) { + const submissionRepository = getSubmissionRepository(); + + const submission = getSubmission(learningObjectHruid, language, version, submissionNumber); + + if (!submission) { + return null; + } + + const loId = new LearningObjectIdentifier(learningObjectHruid, language, version); + await submissionRepository.deleteSubmissionByLearningObjectAndSubmissionNumber(loId, submissionNumber); + + return submission; +} diff --git a/backend/src/services/teachers.ts b/backend/src/services/teachers.ts new file mode 100644 index 00000000..f4dbedfe --- /dev/null +++ b/backend/src/services/teachers.ts @@ -0,0 +1,129 @@ +import { + getClassRepository, + getLearningObjectRepository, + getQuestionRepository, + getStudentRepository, + getTeacherRepository, +} from '../data/repositories.js'; +import { Teacher } from '../entities/users/teacher.entity.js'; +import { ClassDTO, mapToClassDTO } from '../interfaces/class.js'; +import { getClassStudents } from './class.js'; +import { StudentDTO } from '../interfaces/student.js'; +import { mapToQuestionDTO, mapToQuestionId, QuestionDTO, QuestionId } from '../interfaces/question.js'; +import { UserService } from './users.js'; +import { mapToUser } from '../interfaces/user.js'; +import { mapToTeacher, mapToTeacherDTO, TeacherDTO } from '../interfaces/teacher.js'; + +export async function getAllTeachers(): Promise { + const teacherRepository = getTeacherRepository(); + const users = await teacherRepository.findAll(); + return users.map(mapToTeacherDTO); +} + +export async function getAllTeacherIds(): Promise { + const users = await getAllTeachers(); + return users.map((user) => user.username); +} + +export async function getTeacher(username: string): Promise { + const teacherRepository = getTeacherRepository(); + const user = await teacherRepository.findByUsername(username); + return user ? mapToTeacherDTO(user) : null; +} + +export async function createTeacher(userData: TeacherDTO): Promise { + const teacherRepository = getTeacherRepository(); + + try { + const newTeacher = teacherRepository.create(mapToTeacher(userData)); + await teacherRepository.save(newTeacher); + + return mapToTeacherDTO(newTeacher); + } catch (e) { + console.log(e); + return null; + } +} + +export async function deleteTeacher(username: string): Promise { + const teacherRepository = getTeacherRepository(); + + const user = await teacherRepository.findByUsername(username); + + if (!user) { + return null; + } + + try { + await teacherRepository.deleteByUsername(username); + + return mapToTeacherDTO(user); + } catch (e) { + console.log(e); + return null; + } +} + +export async function fetchClassesByTeacher(username: string): Promise { + const teacherRepository = getTeacherRepository(); + const teacher = await teacherRepository.findByUsername(username); + if (!teacher) { + return []; + } + + const classRepository = getClassRepository(); + const classes = await classRepository.findByTeacher(teacher); + return classes.map(mapToClassDTO); +} + +export async function getClassesByTeacher(username: string): Promise { + return await fetchClassesByTeacher(username); +} + +export async function getClassIdsByTeacher(username: string): Promise { + const classes = await fetchClassesByTeacher(username); + return classes.map((cls) => cls.id); +} + +export async function fetchStudentsByTeacher(username: string) { + const classes = await getClassIdsByTeacher(username); + + return (await Promise.all(classes.map(async (id) => getClassStudents(id)))).flat(); +} + +export async function getStudentsByTeacher(username: string): Promise { + return await fetchStudentsByTeacher(username); +} + +export async function getStudentIdsByTeacher(username: string): Promise { + const students = await fetchStudentsByTeacher(username); + return students.map((student) => student.username); +} + +export async function fetchTeacherQuestions(username: string): Promise { + const teacherRepository = getTeacherRepository(); + const teacher = await teacherRepository.findByUsername(username); + if (!teacher) { + throw new Error(`Teacher with username '${username}' not found.`); + } + + // Find all learning objects that this teacher manages + const learningObjectRepository = getLearningObjectRepository(); + const learningObjects = await learningObjectRepository.findAllByTeacher(teacher); + + // Fetch all questions related to these learning objects + const questionRepository = getQuestionRepository(); + const questions = await questionRepository.findAllByLearningObjects(learningObjects); + + return questions.map(mapToQuestionDTO); +} + +export async function getQuestionsByTeacher(username: string): Promise { + return await fetchTeacherQuestions(username); +} + +export async function getQuestionIdsByTeacher(username: string): Promise { + const questions = await fetchTeacherQuestions(username); + + return questions.map(mapToQuestionId); +} diff --git a/backend/src/services/users.ts b/backend/src/services/users.ts new file mode 100644 index 00000000..65ed5d4c --- /dev/null +++ b/backend/src/services/users.ts @@ -0,0 +1,41 @@ +import { UserRepository } from '../data/users/user-repository.js'; +import { UserDTO, mapToUser, mapToUserDTO } from '../interfaces/user.js'; +import { User } from '../entities/users/user.entity.js'; + +export class UserService { + protected repository: UserRepository; + + constructor(repository: UserRepository) { + this.repository = repository; + } + + async getAllUsers(): Promise { + const users = await this.repository.findAll(); + return users.map(mapToUserDTO); + } + + async getAllUserIds(): Promise { + const users = await this.getAllUsers(); + return users.map((user) => user.username); + } + + async getUserByUsername(username: string): Promise { + const user = await this.repository.findByUsername(username); + return user ? mapToUserDTO(user) : null; + } + + async createUser(userData: UserDTO, UserClass: new () => T): Promise { + const newUser = mapToUser(userData, new UserClass()); + await this.repository.save(newUser); + return newUser; + } + + async deleteUser(username: string): Promise { + const user = await this.getUserByUsername(username); + if (!user) { + return null; + } + await this.repository.deleteByUsername(username); + return mapToUserDTO(user); + } +} diff --git a/backend/src/swagger.ts b/backend/src/swagger.ts new file mode 100644 index 00000000..5845030b --- /dev/null +++ b/backend/src/swagger.ts @@ -0,0 +1,7 @@ +import { RequestHandler } from 'express'; +import swaggerUi from 'swagger-ui-express'; +import swaggerDocument from '../../docs/api/swagger.json' with { type: 'json' }; + +const swaggerMiddleware: RequestHandler = swaggerUi.setup(swaggerDocument); + +export default swaggerMiddleware; diff --git a/backend/src/util/apiHelper.ts b/backend/src/util/api-helper.ts similarity index 100% rename from backend/src/util/apiHelper.ts rename to backend/src/util/api-helper.ts diff --git a/backend/src/util/translationHelper.ts b/backend/src/util/translation-helper.ts similarity index 100% rename from backend/src/util/translationHelper.ts rename to backend/src/util/translation-helper.ts diff --git a/backend/tests/data/assignments/submissions.test.ts b/backend/tests/data/assignments/submissions.test.ts index 8712a710..cd212b77 100644 --- a/backend/tests/data/assignments/submissions.test.ts +++ b/backend/tests/data/assignments/submissions.test.ts @@ -32,7 +32,7 @@ describe('SubmissionRepository', () => { }); it('should find the requested submission', async () => { - const id = new LearningObjectIdentifier('id03', Language.English, '1'); + const id = new LearningObjectIdentifier('id03', Language.English, 1); const submission = await submissionRepository.findSubmissionByLearningObjectAndSubmissionNumber(id, 1); expect(submission).toBeTruthy(); @@ -40,7 +40,7 @@ describe('SubmissionRepository', () => { }); it('should find the most recent submission for a student', async () => { - const id = new LearningObjectIdentifier('id02', Language.English, '1'); + const id = new LearningObjectIdentifier('id02', Language.English, 1); const student = await studentRepository.findByUsername('Noordkaap'); const submission = await submissionRepository.findMostRecentSubmissionForStudent(id, student!); @@ -49,7 +49,7 @@ describe('SubmissionRepository', () => { }); it('should find the most recent submission for a group', async () => { - const id = new LearningObjectIdentifier('id03', Language.English, '1'); + const id = new LearningObjectIdentifier('id03', Language.English, 1); const class_ = await classRepository.findById('id01'); const assignment = await assignmentRepository.findByClassAndId(class_!, 1); const group = await groupRepository.findByAssignmentAndGroupNumber(assignment!, 1); @@ -60,7 +60,7 @@ describe('SubmissionRepository', () => { }); it('should not find a deleted submission', async () => { - const id = new LearningObjectIdentifier('id01', Language.English, '1'); + const id = new LearningObjectIdentifier('id01', Language.English, 1); await submissionRepository.deleteSubmissionByLearningObjectAndSubmissionNumber(id, 1); const submission = await submissionRepository.findSubmissionByLearningObjectAndSubmissionNumber(id, 1); diff --git a/backend/tests/data/content/attachments.test.ts b/backend/tests/data/content/attachments.test.ts index a8bea88a..94e132a9 100644 --- a/backend/tests/data/content/attachments.test.ts +++ b/backend/tests/data/content/attachments.test.ts @@ -17,11 +17,11 @@ describe('AttachmentRepository', () => { }); it('should return the requested attachment', async () => { - const id = new LearningObjectIdentifier('id02', Language.English, '1'); + const id = new LearningObjectIdentifier('id02', Language.English, 1); const learningObject = await learningObjectRepository.findByIdentifier(id); const attachment = await attachmentRepository.findByMostRecentVersionOfLearningObjectAndName( - learningObject!, + learningObject!.hruid, Language.English, 'attachment01' ); diff --git a/backend/tests/data/content/learning-objects.test.ts b/backend/tests/data/content/learning-objects.test.ts index 51f9c98e..712f75c9 100644 --- a/backend/tests/data/content/learning-objects.test.ts +++ b/backend/tests/data/content/learning-objects.test.ts @@ -13,8 +13,8 @@ describe('LearningObjectRepository', () => { learningObjectRepository = getLearningObjectRepository(); }); - const id01 = new LearningObjectIdentifier('id01', Language.English, '1'); - const id02 = new LearningObjectIdentifier('test_id', Language.English, '1'); + const id01 = new LearningObjectIdentifier('id01', Language.English, 1); + const id02 = new LearningObjectIdentifier('test_id', Language.English, 1); it('should return the learning object that matches identifier 1', async () => { const learningObject = await learningObjectRepository.findByIdentifier(id01); diff --git a/backend/tests/data/questions/answers.test.ts b/backend/tests/data/questions/answers.test.ts index f15fed6a..bcc62cf6 100644 --- a/backend/tests/data/questions/answers.test.ts +++ b/backend/tests/data/questions/answers.test.ts @@ -20,7 +20,7 @@ describe('AnswerRepository', () => { }); it('should find all answers to a question', async () => { - const id = new LearningObjectIdentifier('id05', Language.English, '1'); + const id = new LearningObjectIdentifier('id05', Language.English, 1); const questions = await questionRepository.findAllQuestionsAboutLearningObject(id); const question = questions.filter((it) => it.sequenceNumber == 2)[0]; @@ -35,7 +35,7 @@ describe('AnswerRepository', () => { it('should create an answer to a question', async () => { const teacher = await teacherRepository.findByUsername('FooFighters'); - const id = new LearningObjectIdentifier('id05', Language.English, '1'); + const id = new LearningObjectIdentifier('id05', Language.English, 1); const questions = await questionRepository.findAllQuestionsAboutLearningObject(id); const question = questions[0]; @@ -54,7 +54,7 @@ describe('AnswerRepository', () => { }); it('should not find a removed answer', async () => { - const id = new LearningObjectIdentifier('id04', Language.English, '1'); + const id = new LearningObjectIdentifier('id04', Language.English, 1); const questions = await questionRepository.findAllQuestionsAboutLearningObject(id); await answerRepository.removeAnswerByQuestionAndSequenceNumber(questions[0], 1); diff --git a/backend/tests/data/questions/questions.test.ts b/backend/tests/data/questions/questions.test.ts index 1a1cb034..7b408df4 100644 --- a/backend/tests/data/questions/questions.test.ts +++ b/backend/tests/data/questions/questions.test.ts @@ -20,7 +20,7 @@ describe('QuestionRepository', () => { }); it('should return all questions part of the given learning object', async () => { - const id = new LearningObjectIdentifier('id05', Language.English, '1'); + const id = new LearningObjectIdentifier('id05', Language.English, 1); const questions = await questionRepository.findAllQuestionsAboutLearningObject(id); expect(questions).toBeTruthy(); @@ -28,7 +28,7 @@ describe('QuestionRepository', () => { }); it('should create new question', async () => { - const id = new LearningObjectIdentifier('id03', Language.English, '1'); + const id = new LearningObjectIdentifier('id03', Language.English, 1); const student = await studentRepository.findByUsername('Noordkaap'); await questionRepository.createQuestion({ loId: id, @@ -42,7 +42,7 @@ describe('QuestionRepository', () => { }); it('should not find removed question', async () => { - const id = new LearningObjectIdentifier('id04', Language.English, '1'); + const id = new LearningObjectIdentifier('id04', Language.English, 1); await questionRepository.removeQuestionByLearningObjectAndSequenceNumber(id, 1); const question = await questionRepository.findAllQuestionsAboutLearningObject(id); diff --git a/backend/tests/test_assets/assignments/submission.testdata.ts b/backend/tests/test_assets/assignments/submission.testdata.ts index 058af70f..95dd65df 100644 --- a/backend/tests/test_assets/assignments/submission.testdata.ts +++ b/backend/tests/test_assets/assignments/submission.testdata.ts @@ -12,7 +12,7 @@ export function makeTestSubmissions( const submission01 = em.create(Submission, { learningObjectHruid: 'id03', learningObjectLanguage: Language.English, - learningObjectVersion: '1', + learningObjectVersion: 1, submissionNumber: 1, submitter: students[0], submissionTime: new Date(2025, 2, 20), @@ -23,7 +23,7 @@ export function makeTestSubmissions( const submission02 = em.create(Submission, { learningObjectHruid: 'id03', learningObjectLanguage: Language.English, - learningObjectVersion: '1', + learningObjectVersion: 1, submissionNumber: 2, submitter: students[0], submissionTime: new Date(2025, 2, 25), @@ -34,7 +34,7 @@ export function makeTestSubmissions( const submission03 = em.create(Submission, { learningObjectHruid: 'id02', learningObjectLanguage: Language.English, - learningObjectVersion: '1', + learningObjectVersion: 1, submissionNumber: 1, submitter: students[0], submissionTime: new Date(2025, 2, 20), @@ -44,7 +44,7 @@ export function makeTestSubmissions( const submission04 = em.create(Submission, { learningObjectHruid: 'id02', learningObjectLanguage: Language.English, - learningObjectVersion: '1', + learningObjectVersion: 1, submissionNumber: 2, submitter: students[0], submissionTime: new Date(2025, 2, 25), @@ -54,7 +54,7 @@ export function makeTestSubmissions( const submission05 = em.create(Submission, { learningObjectHruid: 'id01', learningObjectLanguage: Language.English, - learningObjectVersion: '1', + learningObjectVersion: 1, submissionNumber: 1, submitter: students[1], submissionTime: new Date(2025, 2, 20), diff --git a/backend/tests/test_assets/content/learning-paths.testdata.ts b/backend/tests/test_assets/content/learning-paths.testdata.ts index d2e65c9e..10de885c 100644 --- a/backend/tests/test_assets/content/learning-paths.testdata.ts +++ b/backend/tests/test_assets/content/learning-paths.testdata.ts @@ -77,7 +77,7 @@ export function makeTestLearningPaths(em: EntityManager>, students: Array): Array { const question01 = em.create(Question, { learningObjectLanguage: Language.English, - learningObjectVersion: '1', + learningObjectVersion: 1, learningObjectHruid: 'id05', sequenceNumber: 1, author: students[0], @@ -16,7 +16,7 @@ export function makeTestQuestions(em: EntityManager> const question02 = em.create(Question, { learningObjectLanguage: Language.English, - learningObjectVersion: '1', + learningObjectVersion: 1, learningObjectHruid: 'id05', sequenceNumber: 2, author: students[2], @@ -26,7 +26,7 @@ export function makeTestQuestions(em: EntityManager> const question03 = em.create(Question, { learningObjectLanguage: Language.English, - learningObjectVersion: '1', + learningObjectVersion: 1, learningObjectHruid: 'id04', sequenceNumber: 1, author: students[0], @@ -36,7 +36,7 @@ export function makeTestQuestions(em: EntityManager> const question04 = em.create(Question, { learningObjectLanguage: Language.English, - learningObjectVersion: '1', + learningObjectVersion: 1, learningObjectHruid: 'id01', sequenceNumber: 1, author: students[1], diff --git a/backend/tsconfig.json b/backend/tsconfig.json index 86267d25..2dd3998d 100644 --- a/backend/tsconfig.json +++ b/backend/tsconfig.json @@ -3,6 +3,7 @@ "include": ["src/**/*.ts"], "compilerOptions": { "rootDir": "./src", - "outDir": "./dist" + "outDir": "./dist", + "resolveJsonModule": true } } diff --git a/compose.override.yml b/compose.override.yml new file mode 100644 index 00000000..5c35441e --- /dev/null +++ b/compose.override.yml @@ -0,0 +1,72 @@ +# +# Use this configuration to test the production configuration locally. +# +# This configuration builds the frontend and backend services as Docker images, +# and uses the paths for the services, instead of ports. +# +services: + web: + build: + context: . + dockerfile: frontend/Dockerfile + ports: + - '8080:8080/tcp' + restart: unless-stopped + labels: + - 'traefik.http.routers.web.rule=PathPrefix(`/`)' + - 'traefik.http.services.web.loadbalancer.server.port=8080' + + api: + build: + context: . + dockerfile: backend/Dockerfile + ports: + - '3000:3000/tcp' + restart: unless-stopped + volumes: + - ./backend/.env:/app/.env + depends_on: + - db + - logging + labels: + - 'traefik.http.routers.api.rule=PathPrefix(`/api`)' + - 'traefik.http.services.api.loadbalancer.server.port=3000' + + idp: + # Also see compose.yml + labels: + - 'traefik.http.routers.idp.rule=PathPrefix(`/idp`)' + - 'traefik.http.services.idp.loadbalancer.server.port=7080' + environment: + PROXY_ADDRESS_FORWARDING: 'true' + KC_HTTP_RELATIVE_PATH: '/idp' + + reverse-proxy: + image: traefik:v3.3 + command: + # Enable web UI + - '--api.insecure=true' + + # Add Docker provider + - '--providers.docker=true' + - '--providers.docker.exposedbydefault=true' + + # Add web entrypoint + - '--entrypoints.web.address=:80/tcp' + ports: + - '9000:8080' + - '80:80/tcp' + restart: unless-stopped + volumes: + - /var/run/docker.sock:/var/run/docker.sock:ro + + dashboards: + image: grafana/grafana:latest + ports: + - '9002:3000' + volumes: + - dwengo_grafana_data:/var/lib/grafana + restart: unless-stopped + +volumes: + dwengo_grafana_data: diff --git a/compose.prod.yml b/compose.prod.yml new file mode 100644 index 00000000..8825796e --- /dev/null +++ b/compose.prod.yml @@ -0,0 +1,112 @@ +# +# This file is used to define the production environment for the project. +# It is used to deploy the project on a server. +# Should not be used for local development. +# +services: + web: + build: + context: . + dockerfile: frontend/Dockerfile + restart: unless-stopped + networks: + - dwengo-1 + labels: + - 'traefik.enable=true' + - 'traefik.http.routers.web.rule=PathPrefix(`/`)' + - 'traefik.http.services.web.loadbalancer.server.port=8080' + + api: + build: + context: . + dockerfile: backend/Dockerfile + restart: unless-stopped + volumes: + # TODO Replace with environment keys + - ./backend/.env:/app/.env + depends_on: + - db + - logging + networks: + - dwengo-1 + labels: + - 'traefik.enable=true' + - 'traefik.http.routers.api.rule=PathPrefix(`/api`)' + - 'traefik.http.services.api.loadbalancer.server.port=3000' + + db: + # Also see compose.yml + networks: + - dwengo-1 + + idp: + # Also see compose.yml + # TODO Replace with proper production command + command: ['start-dev', '--http-port', '7080', '--https-port', '7443', '--import-realm'] + networks: + - dwengo-1 + labels: + - 'traefik.enable=true' + - 'traefik.http.routers.idp.rule=PathPrefix(`/idp`)' + - 'traefik.http.services.idp.loadbalancer.server.port=7080' + env_file: + - ./config/idp/.env + environment: + KC_HOSTNAME: 'sel2-1.ugent.be' + PROXY_ADDRESS_FORWARDING: 'true' + KC_PROXY_HEADERS: 'xforwarded' + KC_HTTP_ENABLED: 'true' + KC_HTTP_RELATIVE_PATH: '/idp' + + reverse-proxy: + image: traefik:v3.3 + ports: + - '80:80/tcp' + - '443:443/tcp' + command: + # Add Docker provider + - '--providers.docker=true' + - '--providers.docker.exposedbydefault=false' + + # Add web entrypoint + - '--entrypoints.web.address=:80/tcp' + - '--entrypoints.web.http.redirections.entryPoint.to=websecure' + - '--entrypoints.web.http.redirections.entryPoint.scheme=https' + + # Add websecure entrypoint + - '--entrypoints.websecure.address=:443/tcp' + - '--entrypoints.websecure.http.tls=true' + - '--entrypoints.websecure.http.tls.certResolver=letsencrypt' + - '--entrypoints.websecure.http.tls.domains[0].main=sel2-1.ugent.be' + + # Certificates + - '--certificatesresolvers.letsencrypt.acme.httpchallenge=true' + - '--certificatesresolvers.letsencrypt.acme.httpchallenge.entrypoint=web' + - '--certificatesresolvers.letsencrypt.acme.email=timo.demeyst@ugent.be' + - '--certificatesresolvers.letsencrypt.acme.storage=/letsencrypt/acme.json' + restart: unless-stopped + volumes: + - /var/run/docker.sock:/var/run/docker.sock:ro + - dwengo_letsencrypt:/letsencrypt + networks: + - dwengo-1 + + logging: + # Also see compose.yml + networks: + - dwengo-1 + + dashboards: + image: grafana/grafana:latest + ports: + - '9002:3000' + restart: unless-stopped + volumes: + - dwengo_grafana_data:/var/lib/grafana + +volumes: + dwengo_grafana_data: + dwengo_letsencrypt: + +networks: + dwengo-1: diff --git a/docker-compose.yml b/compose.yml similarity index 74% rename from docker-compose.yml rename to compose.yml index 4ef03dfb..1276c1af 100644 --- a/docker-compose.yml +++ b/compose.yml @@ -1,38 +1,32 @@ +# +# Use this configuration during development. +# +# This configuration is suitable to access the services using their ports. +# services: db: image: postgres:latest + ports: + - '5431:5432' + restart: unless-stopped + volumes: + - dwengo_postgres_data:/var/lib/postgresql/data environment: POSTGRES_USER: postgres POSTGRES_PASSWORD: postgres POSTGRES_DB: postgres - ports: - - '5431:5432' - volumes: - - dwengo_postgres_data:/var/lib/postgresql/data - - logging: - image: grafana/loki:latest - ports: - - '3102:3102' - - '9095:9095' - volumes: - - ./config/loki/config.yml:/etc/loki/config.yaml - - dwengo_loki_data:/loki - command: -config.file=/etc/loki/config.yaml - restart: unless-stopped - - dashboards: - image: grafana/grafana:latest - ports: - - '3100:3000' - volumes: - - dwengo_grafana_data:/var/lib/grafana - restart: unless-stopped idp: # Based on: https://medium.com/@fingervinicius/easy-running-keycloak-with-docker-compose-b0d7a4ee2358 image: quay.io/keycloak/keycloak:latest + ports: + - '7080:7080' + # - '7443:7443' + command: ['start-dev', '--http-port', '7080', '--https-port', '7443', '--import-realm'] + restart: unless-stopped volumes: - - ./idp:/opt/keycloak/data/import + - ./config/idp:/opt/keycloak/data/import + depends_on: + - db environment: KC_HOSTNAME: localhost KC_HOSTNAME_PORT: 7080 @@ -41,19 +35,18 @@ services: KC_BOOTSTRAP_ADMIN_PASSWORD: admin KC_HEALTH_ENABLED: 'true' KC_LOG_LEVEL: info - healthcheck: - test: ['CMD', 'curl', '-f', 'http://localhost:7080/health/ready'] - interval: 15s - timeout: 2s - retries: 15 - command: ['start-dev', '--http-port', '7080', '--https-port', '7443', '--import-realm'] + + logging: + image: grafana/loki:latest ports: - - '7080:7080' - - '7443:7443' - depends_on: - - db + - '9001:3102' + - '9095:9095' + command: -config.file=/etc/loki/config.yaml + restart: unless-stopped + volumes: + - ./config/loki/config.yml:/etc/loki/config.yaml + - dwengo_loki_data:/loki volumes: - dwengo_postgres_data: dwengo_loki_data: - dwengo_grafana_data: + dwengo_postgres_data: diff --git a/idp/README.md b/config/idp/README.md similarity index 100% rename from idp/README.md rename to config/idp/README.md diff --git a/idp/student-realm.json b/config/idp/student-realm.json similarity index 99% rename from idp/student-realm.json rename to config/idp/student-realm.json index 697fda34..32107e4e 100644 --- a/idp/student-realm.json +++ b/config/idp/student-realm.json @@ -620,7 +620,15 @@ "enabled": true, "alwaysDisplayInConsole": false, "clientAuthenticatorType": "client-jwt", - "redirectUris": ["urn:ietf:wg:oauth:2.0:oob", "http://localhost:5173/*", "http://localhost:5173"], + "redirectUris": [ + "urn:ietf:wg:oauth:2.0:oob", + "http://localhost:5173/*", + "http://localhost:5173", + "http://localhost/*", + "http://localhost", + "https://sel2-1.ugent.be/*", + "https://sel2-1.ugent.be" + ], "webOrigins": ["+"], "notBefore": 0, "bearerOnly": false, diff --git a/idp/teacher-realm.json b/config/idp/teacher-realm.json similarity index 99% rename from idp/teacher-realm.json rename to config/idp/teacher-realm.json index fd965e96..b9d29dcb 100644 --- a/idp/teacher-realm.json +++ b/config/idp/teacher-realm.json @@ -620,7 +620,15 @@ "enabled": true, "alwaysDisplayInConsole": false, "clientAuthenticatorType": "client-secret", - "redirectUris": ["urn:ietf:wg:oauth:2.0:oob", "http://localhost:5173/*", "http://localhost:5173"], + "redirectUris": [ + "urn:ietf:wg:oauth:2.0:oob", + "http://localhost:5173/*", + "http://localhost:5173", + "http://localhost/*", + "http://localhost", + "https://sel2-1.ugent.be/*", + "https://sel2-1.ugent.be" + ], "webOrigins": ["+"], "notBefore": 0, "bearerOnly": false, diff --git a/config/nginx/nginx.conf b/config/nginx/nginx.conf new file mode 100644 index 00000000..dc9317f6 --- /dev/null +++ b/config/nginx/nginx.conf @@ -0,0 +1,32 @@ +worker_processes auto; + + +events { + worker_connections 1024; +} + +http { + include mime.types; + default_type application/octet-stream; + + types { + application/javascript mjs; + text/css; + } + + server { + listen 8080; + + location / { + root /usr/share/nginx/html; + try_files $uri $uri/ /index.html; + } + + location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg)$ { + root /usr/share/nginx/html; + expires 1y; + add_header Cache-Control "public"; + try_files $uri =404; + } + } +} diff --git a/docs/api/generate.ts b/docs/api/generate.ts new file mode 100644 index 00000000..baed224d --- /dev/null +++ b/docs/api/generate.ts @@ -0,0 +1,58 @@ +import swaggerAutogen from 'swagger-autogen'; + +const doc = { + info: { + version: '0.1.0', + title: 'Dwengo-1 Backend API', + description: 'Dwengo-1 Backend API using Express, based on VZW Dwengo', + license: { + name: 'MIT', + url: 'https://github.com/SELab-2/Dwengo-1/blob/336496ab6352ee3f8bf47490c90b5cf81526cef6/LICENSE', + }, + }, + servers: [ + { + url: 'http://localhost:3000/', + description: 'Development server', + }, + { + url: 'https://sel2-1.ugent.be/', + description: 'Production server', + }, + ], + components: { + securitySchemes: { + student: { + type: 'oauth2', + flows: { + implicit: { + authorizationUrl: 'https://sel2-1.ugent.be/idp/realms/student/protocol/openid-connect/auth', + scopes: { + openid: 'openid', + profile: 'profile', + email: 'email', + }, + }, + }, + }, + teacher: { + type: 'oauth2', + flows: { + implicit: { + authorizationUrl: 'https://sel2-1.ugent.be/idp/realms/teacher/protocol/openid-connect/auth', + scopes: { + openid: 'openid', + profile: 'profile', + email: 'email', + }, + }, + }, + }, + }, + }, +}; + +const outputFile = './swagger.json'; +const routes = ['../../backend/src/app.ts']; + +swaggerAutogen({ openapi: '3.1.0' })(outputFile, routes, doc); diff --git a/docs/api/swagger.json b/docs/api/swagger.json new file mode 100644 index 00000000..911839d0 --- /dev/null +++ b/docs/api/swagger.json @@ -0,0 +1,1964 @@ +{ + "openapi": "3.1.0", + "info": { + "version": "0.1.0", + "title": "Dwengo-1 Backend API", + "description": "Dwengo-1 Backend API using Express, based on VZW Dwengo", + "license": { + "name": "MIT", + "url": "https://github.com/SELab-2/Dwengo-1/blob/336496ab6352ee3f8bf47490c90b5cf81526cef6/LICENSE" + } + }, + "servers": [ + { + "url": "http://localhost:3000/", + "description": "Development server" + }, + { + "url": "https://sel2-1.ugent.be/", + "description": "Production server" + } + ], + "paths": { + "/api/": { + "get": { + "description": "", + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/api/student/": { + "get": { + "tags": ["Student"], + "description": "", + "parameters": [ + { + "name": "full", + "in": "query", + "schema": { + "type": "string" + } + } + ], + "responses": { + "201": { + "description": "Created" + }, + "404": { + "description": "Not Found" + } + } + }, + "post": { + "tags": ["Student"], + "description": "", + "responses": { + "201": { + "description": "Created" + }, + "400": { + "description": "Bad Request" + } + }, + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "username": { + "example": "any" + }, + "firstName": { + "example": "any" + }, + "lastName": { + "example": "any" + } + } + } + } + } + } + }, + "delete": { + "tags": ["Student"], + "description": "", + "responses": { + "200": { + "description": "OK" + }, + "400": { + "description": "Bad Request" + }, + "404": { + "description": "Not Found" + } + } + } + }, + "/api/student/{username}": { + "delete": { + "tags": ["Student"], + "description": "", + "parameters": [ + { + "name": "username", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK" + }, + "400": { + "description": "Bad Request" + }, + "404": { + "description": "Not Found" + } + } + }, + "get": { + "tags": ["Student"], + "description": "", + "parameters": [ + { + "name": "username", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "201": { + "description": "Created" + }, + "400": { + "description": "Bad Request" + }, + "404": { + "description": "Not Found" + } + } + } + }, + "/api/student/{id}/classes": { + "get": { + "tags": ["Student"], + "description": "", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "full", + "in": "query", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK" + }, + "500": { + "description": "Internal Server Error" + } + } + } + }, + "/api/student/{id}/submissions": { + "get": { + "tags": ["Student"], + "description": "", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/api/student/{id}/assignments": { + "get": { + "tags": ["Student"], + "description": "", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "full", + "in": "query", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/api/student/{id}/groups": { + "get": { + "tags": ["Student"], + "description": "", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "full", + "in": "query", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/api/student/{id}/questions": { + "get": { + "tags": ["Student"], + "description": "", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/api/group/": { + "get": { + "tags": ["Group"], + "description": "", + "parameters": [ + { + "name": "full", + "in": "query", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK" + }, + "400": { + "description": "Bad Request" + } + } + }, + "post": { + "tags": ["Group"], + "description": "", + "responses": { + "201": { + "description": "Created" + }, + "400": { + "description": "Bad Request" + }, + "500": { + "description": "Internal Server Error" + } + } + } + }, + "/api/group/{groupid}": { + "get": { + "tags": ["Group"], + "description": "", + "parameters": [ + { + "name": "groupid", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK" + }, + "400": { + "description": "Bad Request" + } + } + } + }, + "/api/group/{id}/questions": { + "get": { + "tags": ["Group"], + "description": "", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/api/assignment/": { + "get": { + "tags": ["Assignment"], + "description": "", + "parameters": [ + { + "name": "full", + "in": "query", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + }, + "post": { + "tags": ["Assignment"], + "description": "", + "responses": { + "201": { + "description": "Created" + }, + "400": { + "description": "Bad Request" + }, + "500": { + "description": "Internal Server Error" + } + }, + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "description": { + "example": "any" + }, + "language": { + "example": "any" + }, + "learningPath": { + "example": "any" + }, + "title": { + "example": "any" + } + } + } + } + } + } + } + }, + "/api/assignment/{id}": { + "get": { + "tags": ["Assignment"], + "description": "", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK" + }, + "400": { + "description": "Bad Request" + }, + "404": { + "description": "Not Found" + } + } + } + }, + "/api/assignment/{id}/submissions": { + "get": { + "tags": ["Assignment"], + "description": "", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK" + }, + "400": { + "description": "Bad Request" + } + } + } + }, + "/api/assignment/{id}/questions": { + "get": { + "tags": ["Assignment"], + "description": "", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/api/assignment/{assignmentid}/groups/": { + "get": { + "tags": ["Assignment"], + "description": "", + "parameters": [ + { + "name": "assignmentid", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "full", + "in": "query", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK" + }, + "400": { + "description": "Bad Request" + } + } + }, + "post": { + "tags": ["Assignment"], + "description": "", + "parameters": [ + { + "name": "assignmentid", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "201": { + "description": "Created" + }, + "400": { + "description": "Bad Request" + }, + "500": { + "description": "Internal Server Error" + } + } + } + }, + "/api/assignment/{assignmentid}/groups/{groupid}": { + "get": { + "tags": ["Assignment"], + "description": "", + "parameters": [ + { + "name": "assignmentid", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "groupid", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK" + }, + "400": { + "description": "Bad Request" + } + } + } + }, + "/api/assignment/{assignmentid}/groups/{id}/questions": { + "get": { + "tags": ["Assignment"], + "description": "", + "parameters": [ + { + "name": "assignmentid", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/api/submission/": { + "get": { + "tags": ["Submission"], + "description": "", + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/api/submission/{id}": { + "post": { + "tags": ["Submission"], + "description": "", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK" + }, + "404": { + "description": "Not Found" + } + } + }, + "get": { + "tags": ["Submission"], + "description": "", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "language", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "version", + "in": "query", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK" + }, + "400": { + "description": "Bad Request" + }, + "404": { + "description": "Not Found" + } + } + }, + "delete": { + "tags": ["Submission"], + "description": "", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "language", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "version", + "in": "query", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK" + }, + "404": { + "description": "Not Found" + } + } + } + }, + "/api/class/": { + "get": { + "tags": ["Class"], + "description": "", + "parameters": [ + { + "name": "full", + "in": "query", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + }, + "post": { + "tags": ["Class"], + "description": "", + "responses": { + "201": { + "description": "Created" + }, + "400": { + "description": "Bad Request" + }, + "500": { + "description": "Internal Server Error" + } + }, + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "displayName": { + "example": "any" + } + } + } + } + } + } + } + }, + "/api/class/{id}": { + "get": { + "tags": ["Class"], + "description": "", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK" + }, + "404": { + "description": "Not Found" + }, + "500": { + "description": "Internal Server Error" + } + } + } + }, + "/api/class/{id}/teacher-invitations": { + "get": { + "tags": ["Class"], + "description": "", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "full", + "in": "query", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/api/class/{id}/students": { + "get": { + "tags": ["Class"], + "description": "", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "full", + "in": "query", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/api/class/{classid}/assignments/": { + "get": { + "tags": ["Class"], + "description": "", + "parameters": [ + { + "name": "classid", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "full", + "in": "query", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + }, + "post": { + "tags": ["Class"], + "description": "", + "parameters": [ + { + "name": "classid", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "201": { + "description": "Created" + }, + "400": { + "description": "Bad Request" + }, + "500": { + "description": "Internal Server Error" + } + }, + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "description": { + "example": "any" + }, + "language": { + "example": "any" + }, + "learningPath": { + "example": "any" + }, + "title": { + "example": "any" + } + } + } + } + } + } + } + }, + "/api/class/{classid}/assignments/{id}": { + "get": { + "tags": ["Class"], + "description": "", + "parameters": [ + { + "name": "classid", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK" + }, + "400": { + "description": "Bad Request" + }, + "404": { + "description": "Not Found" + } + } + } + }, + "/api/class/{classid}/assignments/{id}/submissions": { + "get": { + "tags": ["Class"], + "description": "", + "parameters": [ + { + "name": "classid", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK" + }, + "400": { + "description": "Bad Request" + } + } + } + }, + "/api/class/{classid}/assignments/{id}/questions": { + "get": { + "tags": ["Class"], + "description": "", + "parameters": [ + { + "name": "classid", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/api/class/{classid}/assignments/{assignmentid}/groups/": { + "get": { + "tags": ["Class"], + "description": "", + "parameters": [ + { + "name": "classid", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "assignmentid", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "full", + "in": "query", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK" + }, + "400": { + "description": "Bad Request" + } + } + }, + "post": { + "tags": ["Class"], + "description": "", + "parameters": [ + { + "name": "classid", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "assignmentid", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "201": { + "description": "Created" + }, + "400": { + "description": "Bad Request" + }, + "500": { + "description": "Internal Server Error" + } + } + } + }, + "/api/class/{classid}/assignments/{assignmentid}/groups/{groupid}": { + "get": { + "tags": ["Class"], + "description": "", + "parameters": [ + { + "name": "classid", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "assignmentid", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "groupid", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK" + }, + "400": { + "description": "Bad Request" + } + } + } + }, + "/api/class/{classid}/assignments/{assignmentid}/groups/{id}/questions": { + "get": { + "tags": ["Class"], + "description": "", + "parameters": [ + { + "name": "classid", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "assignmentid", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/api/question/": { + "get": { + "tags": ["Question"], + "description": "", + "parameters": [ + { + "name": "full", + "in": "query", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK" + }, + "404": { + "description": "Not Found" + } + } + }, + "post": { + "tags": ["Question"], + "description": "", + "responses": { + "200": { + "description": "OK" + }, + "400": { + "description": "Bad Request" + } + }, + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "learningObjectIdentifier": { + "example": "any" + }, + "author": { + "example": "any" + }, + "content": { + "example": "any" + } + } + } + } + } + } + } + }, + "/api/question/{seq}": { + "delete": { + "tags": ["Question"], + "description": "", + "parameters": [ + { + "name": "seq", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK" + }, + "400": { + "description": "Bad Request" + } + } + }, + "get": { + "tags": ["Question"], + "description": "", + "parameters": [ + { + "name": "seq", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK" + }, + "404": { + "description": "Not Found" + } + } + } + }, + "/api/question/answers/{seq}": { + "get": { + "tags": ["Question"], + "description": "", + "parameters": [ + { + "name": "seq", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "full", + "in": "query", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK" + }, + "404": { + "description": "Not Found" + } + } + } + }, + "/api/auth/config": { + "get": { + "tags": ["Auth"], + "description": "", + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/api/auth/testAuthenticatedOnly": { + "get": { + "tags": ["Auth"], + "description": "", + "responses": { + "200": { + "description": "OK" + } + }, + "security": [ + { + "student": [] + }, + { + "teacher": [] + } + ] + } + }, + "/api/auth/testStudentsOnly": { + "get": { + "tags": ["Auth"], + "description": "", + "responses": { + "200": { + "description": "OK" + } + }, + "security": [ + { + "student": [] + } + ] + } + }, + "/api/auth/testTeachersOnly": { + "get": { + "tags": ["Auth"], + "description": "", + "responses": { + "200": { + "description": "OK" + } + }, + "security": [ + { + "teacher": [] + } + ] + } + }, + "/api/theme/": { + "get": { + "tags": ["Theme"], + "description": "", + "parameters": [ + { + "name": "language", + "in": "query", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/api/theme/{theme}": { + "get": { + "tags": ["Theme"], + "description": "", + "parameters": [ + { + "name": "theme", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK" + }, + "404": { + "description": "Not Found" + } + } + } + }, + "/api/learningPath/": { + "get": { + "tags": ["Learning Path"], + "description": "", + "parameters": [ + { + "name": "hruid", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "theme", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "search", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "language", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "forStudent", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "forGroup", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "assignmentNo", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "classId", + "in": "query", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/api/learningObject/": { + "get": { + "tags": ["Learning Object"], + "description": "", + "parameters": [ + { + "name": "full", + "in": "query", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/api/learningObject/{hruid}": { + "get": { + "tags": ["Learning Object"], + "description": "", + "parameters": [ + { + "name": "hruid", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/api/learningObject/{hruid}/html": { + "get": { + "tags": ["Learning Object"], + "description": "", + "parameters": [ + { + "name": "hruid", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/api/learningObject/{hruid}/html/{attachmentName}": { + "get": { + "tags": ["Learning Object"], + "description": "", + "parameters": [ + { + "name": "hruid", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "attachmentName", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "default": { + "description": "" + } + } + } + }, + "/api/learningObject/{hruid}/submissions/": { + "get": { + "tags": ["Learning Object"], + "description": "", + "parameters": [ + { + "name": "hruid", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/api/learningObject/{hruid}/submissions/{id}": { + "post": { + "tags": ["Learning Object"], + "description": "", + "parameters": [ + { + "name": "hruid", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK" + }, + "404": { + "description": "Not Found" + } + } + }, + "get": { + "tags": ["Learning Object"], + "description": "", + "parameters": [ + { + "name": "hruid", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "language", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "version", + "in": "query", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK" + }, + "400": { + "description": "Bad Request" + }, + "404": { + "description": "Not Found" + } + } + }, + "delete": { + "tags": ["Learning Object"], + "description": "", + "parameters": [ + { + "name": "hruid", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "language", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "version", + "in": "query", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK" + }, + "404": { + "description": "Not Found" + } + } + } + }, + "/api/learningObject/{hruid}/{version}/questions/": { + "get": { + "tags": ["Learning Object"], + "description": "", + "parameters": [ + { + "name": "hruid", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "version", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "full", + "in": "query", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK" + }, + "404": { + "description": "Not Found" + } + } + }, + "post": { + "tags": ["Learning Object"], + "description": "", + "parameters": [ + { + "name": "hruid", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "version", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK" + }, + "400": { + "description": "Bad Request" + } + }, + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "learningObjectIdentifier": { + "example": "any" + }, + "author": { + "example": "any" + }, + "content": { + "example": "any" + } + } + } + } + } + } + } + }, + "/api/learningObject/{hruid}/{version}/questions/{seq}": { + "delete": { + "tags": ["Learning Object"], + "description": "", + "parameters": [ + { + "name": "hruid", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "version", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "seq", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK" + }, + "400": { + "description": "Bad Request" + } + } + }, + "get": { + "tags": ["Learning Object"], + "description": "", + "parameters": [ + { + "name": "hruid", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "version", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "seq", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK" + }, + "404": { + "description": "Not Found" + } + } + } + }, + "/api/learningObject/{hruid}/{version}/questions/answers/{seq}": { + "get": { + "tags": ["Learning Object"], + "description": "", + "parameters": [ + { + "name": "hruid", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "version", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "seq", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "full", + "in": "query", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK" + }, + "404": { + "description": "Not Found" + } + } + } + } + }, + "components": { + "securitySchemes": { + "student": { + "type": "oauth2", + "flows": { + "implicit": { + "authorizationUrl": "https://sel2-1.ugent.be/idp/realms/student/protocol/openid-connect/auth", + "scopes": { + "openid": "openid", + "profile": "profile", + "email": "email" + } + } + } + }, + "teacher": { + "type": "oauth2", + "flows": { + "implicit": { + "authorizationUrl": "https://sel2-1.ugent.be/idp/realms/teacher/protocol/openid-connect/auth", + "scopes": { + "openid": "openid", + "profile": "profile", + "email": "email" + } + } + } + } + } + } +} diff --git a/docs/package.json b/docs/package.json new file mode 100644 index 00000000..3e3cb619 --- /dev/null +++ b/docs/package.json @@ -0,0 +1,14 @@ +{ + "name": "dwengo-1-docs", + "version": "0.0.1", + "description": "Documentation for Dwengo-1", + "private": true, + "scripts": { + "build": "npm run architecture && npm run swagger", + "architecture": "python3 -m venv .venv && source .venv/bin/activate && pip install -r docs/requirements.txt && python docs/architecture/schema.py", + "swagger": "tsx api/generate.ts" + }, + "devDependencies": { + "swagger-autogen": "^2.23.7" + } +} diff --git a/frontend/Dockerfile b/frontend/Dockerfile new file mode 100644 index 00000000..9cbb61ea --- /dev/null +++ b/frontend/Dockerfile @@ -0,0 +1,36 @@ +FROM node:22 AS build-stage + +# install simple http server for serving static content +RUN npm install -g http-server + +WORKDIR /app + +# Install dependencies + +COPY package*.json ./ +COPY ./frontend/package.json ./frontend/ + +RUN npm install --silent + +# Build the frontend + +# Root tsconfig.json +COPY tsconfig.json ./ +COPY assets ./assets/ + +WORKDIR /app/frontend + +COPY frontend ./ + +RUN npx vite build + +FROM nginx:stable AS production-stage + +COPY config/nginx/nginx.conf /etc/nginx/nginx.conf + +COPY --from=build-stage /app/assets /usr/share/nginx/html/assets +COPY --from=build-stage /app/frontend/dist /usr/share/nginx/html + +EXPOSE 8080 + +CMD ["nginx", "-g", "daemon off;"] diff --git a/frontend/package.json b/frontend/package.json index b056c9f3..ac1efc4a 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,6 +1,6 @@ { "name": "dwengo-1-frontend", - "version": "0.0.1", + "version": "0.1.1", "description": "Frontend for Dwengo-1", "private": true, "type": "module", @@ -17,6 +17,7 @@ }, "dependencies": { "vue": "^3.5.13", + "vue-i18n": "^11.1.2", "vue-router": "^4.5.0", "vuetify": "^3.7.12", "oidc-client-ts": "^3.1.0", diff --git a/frontend/src/config.ts b/frontend/src/config.ts index 9feb71b3..53d6f253 100644 --- a/frontend/src/config.ts +++ b/frontend/src/config.ts @@ -1,5 +1,8 @@ export const apiConfig = { - baseUrl: window.location.hostname == "localhost" ? "http://localhost:3000" : window.location.origin, + baseUrl: + window.location.hostname === "localhost" && !(window.location.port === "80" || window.location.port === "") + ? "http://localhost:3000/api" + : window.location.origin + "/api", }; export const loginRoute = "/login"; diff --git a/frontend/src/services/auth/auth-service.ts b/frontend/src/services/auth/auth-service.ts index 61032170..53f8cb61 100644 --- a/frontend/src/services/auth/auth-service.ts +++ b/frontend/src/services/auth/auth-service.ts @@ -12,12 +12,13 @@ import apiClient from "@/services/api-client.ts"; import router from "@/router"; import type { AxiosError } from "axios"; -const authConfig = await loadAuthConfig(); - -const userManagers: UserManagersForRoles = { - student: new UserManager(authConfig.student), - teacher: new UserManager(authConfig.teacher), -}; +async function getUserManagers(): Promise { + const authConfig = await loadAuthConfig(); + return { + student: new UserManager(authConfig.student), + teacher: new UserManager(authConfig.teacher), + }; +} /** * Load the information about who is currently logged in from the IDP. @@ -27,7 +28,7 @@ async function loadUser(): Promise { if (!activeRole) { return null; } - const user = await userManagers[activeRole].getUser(); + const user = await (await getUserManagers())[activeRole].getUser(); authState.user = user; authState.accessToken = user?.access_token || null; authState.activeRole = activeRole || null; @@ -59,7 +60,7 @@ async function initiateLogin() { async function loginAs(role: Role): Promise { // Storing it in local storage so that it won't be lost when redirecting outside of the app. authStorage.setActiveRole(role); - await userManagers[role].signinRedirect(); + await (await getUserManagers())[role].signinRedirect(); } /** @@ -70,7 +71,7 @@ async function handleLoginCallback(): Promise { if (!activeRole) { throw new Error("Login callback received, but the user is not logging in!"); } - authState.user = (await userManagers[activeRole].signinCallback()) || null; + authState.user = (await (await getUserManagers())[activeRole].signinCallback()) || null; } /** @@ -84,7 +85,7 @@ async function renewToken() { return; } try { - return await userManagers[activeRole].signinSilent(); + return await (await getUserManagers())[activeRole].signinSilent(); } catch (error) { console.log("Can't renew the token:"); console.log(error); @@ -98,7 +99,7 @@ async function renewToken() { async function logout(): Promise { const activeRole = authStorage.getActiveRole(); if (activeRole) { - await userManagers[activeRole].signoutRedirect(); + await (await getUserManagers())[activeRole].signoutRedirect(); authStorage.deleteActiveRole(); } } diff --git a/package-lock.json b/package-lock.json index bade5beb..0844e7b1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,8 @@ "license": "MIT", "workspaces": [ "backend", - "frontend" + "frontend", + "docs" ], "devDependencies": { "@eslint/compat": "^1.2.6", @@ -48,6 +49,7 @@ "loki-logger-ts": "^1.0.2", "marked": "^15.0.7", "response-time": "^2.3.3", + "swagger-ui-express": "^5.0.1", "uuid": "^11.1.0", "winston": "^3.17.0", "winston-loki": "^6.1.3" @@ -59,6 +61,7 @@ "@types/js-yaml": "^4.0.9", "@types/node": "^22.13.4", "@types/response-time": "^2.3.8", + "@types/swagger-ui-express": "^4.1.8", "globals": "^15.15.0", "ts-node": "^10.9.2", "tsx": "^4.19.3", @@ -77,6 +80,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "docs": { + "name": "dwengo-1-docs", + "version": "0.0.1", + "devDependencies": { + "swagger-autogen": "^2.23.7" + } + }, "frontend": { "name": "dwengo-1-frontend", "version": "0.0.1", @@ -84,6 +94,7 @@ "axios": "^1.8.2", "oidc-client-ts": "^3.1.0", "vue": "^3.5.13", + "vue-i18n": "^11.1.2", "vue-router": "^4.5.0", "vuetify": "^3.7.12" }, @@ -588,7 +599,8 @@ }, "node_modules/@colors/colors": { "version": "1.6.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.6.0.tgz", + "integrity": "sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==", "engines": { "node": ">=0.1.90" } @@ -706,7 +718,8 @@ }, "node_modules/@dabh/diagnostics": { "version": "2.0.3", - "license": "MIT", + "resolved": "https://registry.npmjs.org/@dabh/diagnostics/-/diagnostics-2.0.3.tgz", + "integrity": "sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA==", "dependencies": { "colorspace": "1.1.x", "enabled": "2.0.x", @@ -945,6 +958,50 @@ "url": "https://github.com/sponsors/nzakas" } }, + "node_modules/@intlify/core-base": { + "version": "11.1.2", + "resolved": "https://registry.npmjs.org/@intlify/core-base/-/core-base-11.1.2.tgz", + "integrity": "sha512-nmG512G8QOABsserleechwHGZxzKSAlggGf9hQX0nltvSwyKNVuB/4o6iFeG2OnjXK253r8p8eSDOZf8PgFdWw==", + "license": "MIT", + "dependencies": { + "@intlify/message-compiler": "11.1.2", + "@intlify/shared": "11.1.2" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/kazupon" + } + }, + "node_modules/@intlify/message-compiler": { + "version": "11.1.2", + "resolved": "https://registry.npmjs.org/@intlify/message-compiler/-/message-compiler-11.1.2.tgz", + "integrity": "sha512-T/xbNDzi+Yv0Qn2Dfz2CWCAJiwNgU5d95EhhAEf4YmOgjCKktpfpiUSmLcBvK1CtLpPQ85AMMQk/2NCcXnNj1g==", + "license": "MIT", + "dependencies": { + "@intlify/shared": "11.1.2", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/kazupon" + } + }, + "node_modules/@intlify/shared": { + "version": "11.1.2", + "resolved": "https://registry.npmjs.org/@intlify/shared/-/shared-11.1.2.tgz", + "integrity": "sha512-dF2iMMy8P9uKVHV/20LA1ulFLL+MKSbfMiixSmn6fpwqzvix38OIc7ebgnFbBqElvghZCW9ACtzKTGKsTGTWGA==", + "license": "MIT", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/kazupon" + } + }, "node_modules/@isaacs/cliui": { "version": "8.0.2", "dev": true, @@ -1048,10 +1105,7 @@ }, "node_modules/@mikro-orm/cli": { "version": "6.4.9", - "resolved": "https://registry.npmjs.org/@mikro-orm/cli/-/cli-6.4.9.tgz", - "integrity": "sha512-LQzVsmar/0DoJkPGyz3OpB8pa9BCQtvYreEC71h0O+RcizppJjgBQNTkj5tJd2Iqvh4hSaMv6qTv0l5UK6F2Vw==", "dev": true, - "license": "MIT", "dependencies": { "@jercle/yargonaut": "1.1.5", "@mikro-orm/core": "6.4.9", @@ -1173,12 +1227,133 @@ "@mikro-orm/core": "^6.0.0" } }, - "node_modules/@napi-rs/snappy-linux-x64-gnu": { + "node_modules/@napi-rs/snappy-android-arm-eabi": { "version": "7.2.2", + "resolved": "https://registry.npmjs.org/@napi-rs/snappy-android-arm-eabi/-/snappy-android-arm-eabi-7.2.2.tgz", + "integrity": "sha512-H7DuVkPCK5BlAr1NfSU8bDEN7gYs+R78pSHhDng83QxRnCLmVIZk33ymmIwurmoA1HrdTxbkbuNl+lMvNqnytw==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/snappy-android-arm64": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/@napi-rs/snappy-android-arm64/-/snappy-android-arm64-7.2.2.tgz", + "integrity": "sha512-2R/A3qok+nGtpVK8oUMcrIi5OMDckGYNoBLFyli3zp8w6IArPRfg1yOfVUcHvpUDTo9T7LOS1fXgMOoC796eQw==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/snappy-darwin-arm64": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/@napi-rs/snappy-darwin-arm64/-/snappy-darwin-arm64-7.2.2.tgz", + "integrity": "sha512-USgArHbfrmdbuq33bD5ssbkPIoT7YCXCRLmZpDS6dMDrx+iM7eD2BecNbOOo7/v1eu6TRmQ0xOzeQ6I/9FIi5g==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/snappy-darwin-x64": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/@napi-rs/snappy-darwin-x64/-/snappy-darwin-x64-7.2.2.tgz", + "integrity": "sha512-0APDu8iO5iT0IJKblk2lH0VpWSl9zOZndZKnBYIc+ei1npw2L5QvuErFOTeTdHBtzvUHASB+9bvgaWnQo4PvTQ==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/snappy-freebsd-x64": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/@napi-rs/snappy-freebsd-x64/-/snappy-freebsd-x64-7.2.2.tgz", + "integrity": "sha512-mRTCJsuzy0o/B0Hnp9CwNB5V6cOJ4wedDTWEthsdKHSsQlO7WU9W1yP7H3Qv3Ccp/ZfMyrmG98Ad7u7lG58WXA==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/snappy-linux-arm-gnueabihf": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/@napi-rs/snappy-linux-arm-gnueabihf/-/snappy-linux-arm-gnueabihf-7.2.2.tgz", + "integrity": "sha512-v1uzm8+6uYjasBPcFkv90VLZ+WhLzr/tnfkZ/iD9mHYiULqkqpRuC8zvc3FZaJy5wLQE9zTDkTJN1IvUcZ+Vcg==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/snappy-linux-arm64-gnu": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/@napi-rs/snappy-linux-arm64-gnu/-/snappy-linux-arm64-gnu-7.2.2.tgz", + "integrity": "sha512-LrEMa5pBScs4GXWOn6ZYXfQ72IzoolZw5txqUHVGs8eK4g1HR9HTHhb2oY5ySNaKakG5sOgMsb1rwaEnjhChmQ==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/snappy-linux-arm64-musl": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/@napi-rs/snappy-linux-arm64-musl/-/snappy-linux-arm64-musl-7.2.2.tgz", + "integrity": "sha512-3orWZo9hUpGQcB+3aTLW7UFDqNCQfbr0+MvV67x8nMNYj5eAeUtMmUE/HxLznHO4eZ1qSqiTwLbVx05/Socdlw==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/snappy-linux-x64-gnu": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/@napi-rs/snappy-linux-x64-gnu/-/snappy-linux-x64-gnu-7.2.2.tgz", + "integrity": "sha512-jZt8Jit/HHDcavt80zxEkDpH+R1Ic0ssiVCoueASzMXa7vwPJeF4ZxZyqUw4qeSy7n8UUExomu8G8ZbP6VKhgw==", "cpu": [ "x64" ], - "license": "MIT", "optional": true, "os": [ "linux" @@ -1189,10 +1364,11 @@ }, "node_modules/@napi-rs/snappy-linux-x64-musl": { "version": "7.2.2", + "resolved": "https://registry.npmjs.org/@napi-rs/snappy-linux-x64-musl/-/snappy-linux-x64-musl-7.2.2.tgz", + "integrity": "sha512-Dh96IXgcZrV39a+Tej/owcd9vr5ihiZ3KRix11rr1v0MWtVb61+H1GXXlz6+Zcx9y8jM1NmOuiIuJwkV4vZ4WA==", "cpu": [ "x64" ], - "license": "MIT", "optional": true, "os": [ "linux" @@ -1201,6 +1377,51 @@ "node": ">= 10" } }, + "node_modules/@napi-rs/snappy-win32-arm64-msvc": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/@napi-rs/snappy-win32-arm64-msvc/-/snappy-win32-arm64-msvc-7.2.2.tgz", + "integrity": "sha512-9No0b3xGbHSWv2wtLEn3MO76Yopn1U2TdemZpCaEgOGccz1V+a/1d16Piz3ofSmnA13HGFz3h9NwZH9EOaIgYA==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/snappy-win32-ia32-msvc": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/@napi-rs/snappy-win32-ia32-msvc/-/snappy-win32-ia32-msvc-7.2.2.tgz", + "integrity": "sha512-QiGe+0G86J74Qz1JcHtBwM3OYdTni1hX1PFyLRo3HhQUSpmi13Bzc1En7APn+6Pvo7gkrcy81dObGLDSxFAkQQ==", + "cpu": [ + "ia32" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/snappy-win32-x64-msvc": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/@napi-rs/snappy-win32-x64-msvc/-/snappy-win32-x64-msvc-7.2.2.tgz", + "integrity": "sha512-a43cyx1nK0daw6BZxVcvDEXxKMFLSBSDTAhsFD0VqSKcC7MGUBMaqyoWUcMiI7LBSz4bxUmxDWKfCYzpEmeb3w==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "license": "MIT", @@ -1297,23 +1518,28 @@ }, "node_modules/@protobufjs/aspromise": { "version": "1.1.2", - "license": "BSD-3-Clause" + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==" }, "node_modules/@protobufjs/base64": { "version": "1.1.2", - "license": "BSD-3-Clause" + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==" }, "node_modules/@protobufjs/codegen": { "version": "2.0.4", - "license": "BSD-3-Clause" + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", + "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==" }, "node_modules/@protobufjs/eventemitter": { "version": "1.1.0", - "license": "BSD-3-Clause" + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", + "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==" }, "node_modules/@protobufjs/fetch": { "version": "1.1.0", - "license": "BSD-3-Clause", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", + "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", "dependencies": { "@protobufjs/aspromise": "^1.1.1", "@protobufjs/inquire": "^1.1.0" @@ -1321,23 +1547,28 @@ }, "node_modules/@protobufjs/float": { "version": "1.0.2", - "license": "BSD-3-Clause" + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==" }, "node_modules/@protobufjs/inquire": { "version": "1.1.0", - "license": "BSD-3-Clause" + "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", + "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==" }, "node_modules/@protobufjs/path": { "version": "1.1.2", - "license": "BSD-3-Clause" + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==" }, "node_modules/@protobufjs/pool": { "version": "1.1.0", - "license": "BSD-3-Clause" + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==" }, "node_modules/@protobufjs/utf8": { "version": "1.1.0", - "license": "BSD-3-Clause" + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", + "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==" }, "node_modules/@rollup/pluginutils": { "version": "5.1.4", @@ -1395,6 +1626,13 @@ "linux" ] }, + "node_modules/@scarf/scarf": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@scarf/scarf/-/scarf-1.4.0.tgz", + "integrity": "sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ==", + "hasInstallScript": true, + "license": "Apache-2.0" + }, "node_modules/@sec-ant/readable-stream": { "version": "0.4.1", "dev": true, @@ -1470,8 +1708,9 @@ }, "node_modules/@types/cors": { "version": "2.8.17", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.17.tgz", + "integrity": "sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==", "dev": true, - "license": "MIT", "dependencies": { "@types/node": "*" } @@ -1534,7 +1773,8 @@ }, "node_modules/@types/jsonwebtoken": { "version": "9.0.9", - "license": "MIT", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.9.tgz", + "integrity": "sha512-uoe+GxEuHbvy12OUQct2X9JenKM3qAscquYymuQN4fMWG9DBQtykrQEFcAbVACF7qaLw9BePSodUL0kquqBJpQ==", "dependencies": { "@types/ms": "*", "@types/node": "*" @@ -1546,7 +1786,8 @@ }, "node_modules/@types/ms": { "version": "2.1.0", - "license": "MIT" + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==" }, "node_modules/@types/node": { "version": "22.13.4", @@ -1565,8 +1806,9 @@ }, "node_modules/@types/response-time": { "version": "2.3.8", + "resolved": "https://registry.npmjs.org/@types/response-time/-/response-time-2.3.8.tgz", + "integrity": "sha512-7qGaNYvdxc0zRab8oHpYx7AW17qj+G0xuag1eCrw3M2VWPJQ/HyKaaghWygiaOUl0y9x7QGQwppDpqLJ5V9pzw==", "dev": true, - "license": "MIT", "dependencies": { "@types/express": "*", "@types/node": "*" @@ -1589,6 +1831,17 @@ "@types/send": "*" } }, + "node_modules/@types/swagger-ui-express": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@types/swagger-ui-express/-/swagger-ui-express-4.1.8.tgz", + "integrity": "sha512-AhZV8/EIreHFmBV5wAs0gzJUNq9JbbSXgJLQubCC0jtIo6prnI9MIRRxnU4MZX9RB9yXxF1V4R7jtLl/Wcj31g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/express": "*", + "@types/serve-static": "*" + } + }, "node_modules/@types/tough-cookie": { "version": "4.0.5", "dev": true, @@ -1596,7 +1849,8 @@ }, "node_modules/@types/triple-beam": { "version": "1.3.5", - "license": "MIT" + "resolved": "https://registry.npmjs.org/@types/triple-beam/-/triple-beam-1.3.5.tgz", + "integrity": "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==" }, "node_modules/@types/trusted-types": { "version": "2.0.7", @@ -2389,11 +2643,13 @@ }, "node_modules/async": { "version": "3.2.6", - "license": "MIT" + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==" }, "node_modules/async-exit-hook": { "version": "2.0.1", - "license": "MIT", + "resolved": "https://registry.npmjs.org/async-exit-hook/-/async-exit-hook-2.0.1.tgz", + "integrity": "sha512-NW2cX8m1Q7KPA7a5M2ULQeZ2wR5qI5PAbw5L0UOMxdioVk9PMZ0h1TmyZEkPYrCvYjDlFICusOu1dlEKAAeXBw==", "engines": { "node": ">=0.12.0" } @@ -2403,7 +2659,9 @@ "license": "MIT" }, "node_modules/axios": { - "version": "1.8.2", + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.8.3.tgz", + "integrity": "sha512-iP4DebzoNlP/YN2dpwCgb8zoCmhtkajzS48JvwmkSkXvPI3DHc7m+XYL5tGnSlJtR6nImXZmdCuN5aP8dh1d8A==", "license": "MIT", "dependencies": { "follow-redirects": "^1.15.6", @@ -2543,7 +2801,8 @@ }, "node_modules/btoa": { "version": "1.2.1", - "license": "(MIT OR Apache-2.0)", + "resolved": "https://registry.npmjs.org/btoa/-/btoa-1.2.1.tgz", + "integrity": "sha512-SB4/MIGlsiVkMcHmT+pSmIPoNDoHg+7cMzmt3Uxt628MTz2487DKSqK/fuhFBrkuqrYv5UCEnACpF4dTFNKc/g==", "bin": { "btoa": "bin/btoa.js" }, @@ -2575,7 +2834,8 @@ }, "node_modules/buffer-equal-constant-time": { "version": "1.0.1", - "license": "BSD-3-Clause" + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==" }, "node_modules/bundle-name": { "version": "4.1.0", @@ -2877,7 +3137,8 @@ }, "node_modules/color": { "version": "3.2.1", - "license": "MIT", + "resolved": "https://registry.npmjs.org/color/-/color-3.2.1.tgz", + "integrity": "sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA==", "dependencies": { "color-convert": "^1.9.3", "color-string": "^1.6.0" @@ -2900,7 +3161,8 @@ }, "node_modules/color-string": { "version": "1.9.1", - "license": "MIT", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", + "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", "dependencies": { "color-name": "^1.0.0", "simple-swizzle": "^0.2.2" @@ -2916,14 +3178,16 @@ }, "node_modules/color/node_modules/color-convert": { "version": "1.9.3", - "license": "MIT", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", "dependencies": { "color-name": "1.1.3" } }, "node_modules/color/node_modules/color-name": { "version": "1.1.3", - "license": "MIT" + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" }, "node_modules/colorette": { "version": "2.0.19", @@ -2931,7 +3195,8 @@ }, "node_modules/colorspace": { "version": "1.1.4", - "license": "MIT", + "resolved": "https://registry.npmjs.org/colorspace/-/colorspace-1.1.4.tgz", + "integrity": "sha512-BgvKJiuVu1igBUF2kEjRCZXol6wiiGbY5ipL/oVPwm0BL9sIpMIzM8IK7vwuxIIzOXMV3Ey5w+vxhm0rR/TN8w==", "dependencies": { "color": "^3.1.3", "text-hex": "1.0.x" @@ -3025,7 +3290,8 @@ }, "node_modules/cors": { "version": "2.8.5", - "license": "MIT", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", "dependencies": { "object-assign": "^4", "vary": "^1" @@ -3047,8 +3313,6 @@ }, "node_modules/cross-env": { "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz", - "integrity": "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==", "license": "MIT", "dependencies": { "cross-spawn": "^7.0.1" @@ -3173,6 +3437,16 @@ "dev": true, "license": "MIT" }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/default-browser": { "version": "5.2.1", "dev": true, @@ -3297,6 +3571,10 @@ "resolved": "backend", "link": true }, + "node_modules/dwengo-1-docs": { + "resolved": "docs", + "link": true + }, "node_modules/dwengo-1-frontend": { "resolved": "frontend", "link": true @@ -3308,7 +3586,8 @@ }, "node_modules/ecdsa-sig-formatter": { "version": "1.0.11", - "license": "Apache-2.0", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", "dependencies": { "safe-buffer": "^5.0.1" } @@ -3360,7 +3639,8 @@ }, "node_modules/enabled": { "version": "2.0.0", - "license": "MIT" + "resolved": "https://registry.npmjs.org/enabled/-/enabled-2.0.0.tgz", + "integrity": "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==" }, "node_modules/encodeurl": { "version": "2.0.0", @@ -3927,7 +4207,8 @@ }, "node_modules/express-jwt": { "version": "8.5.1", - "license": "MIT", + "resolved": "https://registry.npmjs.org/express-jwt/-/express-jwt-8.5.1.tgz", + "integrity": "sha512-Dv6QjDLpR2jmdb8M6XQXiCcpEom7mK8TOqnr0/TngDKsG2DHVkO8+XnVxkJVN7BuS1I3OrGw6N8j5DaaGgkDRQ==", "dependencies": { "@types/jsonwebtoken": "^9", "express-unless": "^2.1.3", @@ -3939,7 +4220,8 @@ }, "node_modules/express-unless": { "version": "2.1.3", - "license": "MIT" + "resolved": "https://registry.npmjs.org/express-unless/-/express-unless-2.1.3.tgz", + "integrity": "sha512-wj4tLMyCVYuIIKHGt0FhCtIViBcwzWejX0EjNxveAa6dG+0XBCQhMbx+PnkLkFCxLC69qoFrxds4pIyL88inaQ==" }, "node_modules/express/node_modules/debug": { "version": "4.3.6", @@ -4013,7 +4295,8 @@ }, "node_modules/fecha": { "version": "4.2.3", - "license": "MIT" + "resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz", + "integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==" }, "node_modules/figlet": { "version": "1.8.0", @@ -4133,7 +4416,8 @@ }, "node_modules/fn.name": { "version": "1.1.0", - "license": "MIT" + "resolved": "https://registry.npmjs.org/fn.name/-/fn.name-1.1.0.tgz", + "integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==" }, "node_modules/follow-redirects": { "version": "1.15.9", @@ -4254,8 +4538,8 @@ }, "node_modules/fs.realpath": { "version": "1.0.0", - "license": "ISC", - "optional": true + "devOptional": true, + "license": "ISC" }, "node_modules/function-bind": { "version": "1.1.2", @@ -4711,8 +4995,8 @@ }, "node_modules/inflight": { "version": "1.0.6", + "devOptional": true, "license": "ISC", - "optional": true, "dependencies": { "once": "^1.3.0", "wrappy": "1" @@ -4754,7 +5038,8 @@ }, "node_modules/is-arrayish": { "version": "0.3.2", - "license": "MIT" + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", + "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==" }, "node_modules/is-core-module": { "version": "2.16.1", @@ -4944,7 +5229,8 @@ }, "node_modules/jose": { "version": "4.15.9", - "license": "MIT", + "resolved": "https://registry.npmjs.org/jose/-/jose-4.15.9.tgz", + "integrity": "sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA==", "funding": { "url": "https://github.com/sponsors/panva" } @@ -5126,7 +5412,8 @@ }, "node_modules/jsonwebtoken": { "version": "9.0.2", - "license": "MIT", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", + "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", "dependencies": { "jws": "^3.2.2", "lodash.includes": "^4.3.0", @@ -5146,7 +5433,8 @@ }, "node_modules/jwa": { "version": "1.4.1", - "license": "MIT", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", + "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", "dependencies": { "buffer-equal-constant-time": "1.0.1", "ecdsa-sig-formatter": "1.0.11", @@ -5155,7 +5443,8 @@ }, "node_modules/jwks-rsa": { "version": "3.1.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/jwks-rsa/-/jwks-rsa-3.1.0.tgz", + "integrity": "sha512-v7nqlfezb9YfHHzYII3ef2a2j1XnGeSE/bK3WfumaYCqONAIstJbrEGapz4kadScZzEt7zYCN7bucj8C0Mv/Rg==", "dependencies": { "@types/express": "^4.17.17", "@types/jsonwebtoken": "^9.0.2", @@ -5170,7 +5459,8 @@ }, "node_modules/jwks-rsa/node_modules/@types/express": { "version": "4.17.21", - "license": "MIT", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.21.tgz", + "integrity": "sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==", "dependencies": { "@types/body-parser": "*", "@types/express-serve-static-core": "^4.17.33", @@ -5180,7 +5470,8 @@ }, "node_modules/jwks-rsa/node_modules/@types/express-serve-static-core": { "version": "4.19.6", - "license": "MIT", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.6.tgz", + "integrity": "sha512-N4LZ2xG7DatVqhCZzOGb1Yi5lMbXSZcmdLDe9EzSndPV2HpWYWzRbaerl2n27irrm94EPpprqa8KpskPT085+A==", "dependencies": { "@types/node": "*", "@types/qs": "*", @@ -5190,7 +5481,8 @@ }, "node_modules/jws": { "version": "3.2.2", - "license": "MIT", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", "dependencies": { "jwa": "^1.4.1", "safe-buffer": "^5.0.1" @@ -5198,7 +5490,8 @@ }, "node_modules/jwt-decode": { "version": "4.0.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-4.0.0.tgz", + "integrity": "sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA==", "engines": { "node": ">=18" } @@ -5293,7 +5586,8 @@ }, "node_modules/kuler": { "version": "2.0.0", - "license": "MIT" + "resolved": "https://registry.npmjs.org/kuler/-/kuler-2.0.0.tgz", + "integrity": "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==" }, "node_modules/levn": { "version": "0.4.1", @@ -5308,7 +5602,9 @@ } }, "node_modules/limiter": { - "version": "1.1.5" + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/limiter/-/limiter-1.1.5.tgz", + "integrity": "sha512-FWWMIEOxz3GwUI4Ts/IvgVy6LPvoMPgjMdQ185nN6psJyBJ4yOpzqm695/h5umdLJg2vW3GR5iG11MAkR2AzJA==" }, "node_modules/locate-path": { "version": "6.0.0", @@ -5330,31 +5626,38 @@ }, "node_modules/lodash.clonedeep": { "version": "4.5.0", - "license": "MIT" + "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", + "integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==" }, "node_modules/lodash.includes": { "version": "4.3.0", - "license": "MIT" + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==" }, "node_modules/lodash.isboolean": { "version": "3.0.3", - "license": "MIT" + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==" }, "node_modules/lodash.isinteger": { "version": "4.0.4", - "license": "MIT" + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==" }, "node_modules/lodash.isnumber": { "version": "3.0.3", - "license": "MIT" + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==" }, "node_modules/lodash.isplainobject": { "version": "4.0.6", - "license": "MIT" + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==" }, "node_modules/lodash.isstring": { "version": "4.0.1", - "license": "MIT" + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==" }, "node_modules/lodash.merge": { "version": "4.6.2", @@ -5363,11 +5666,13 @@ }, "node_modules/lodash.once": { "version": "4.1.1", - "license": "MIT" + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==" }, "node_modules/logform": { "version": "2.7.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/logform/-/logform-2.7.0.tgz", + "integrity": "sha512-TFYA4jnP7PVbmlBIfhlSe+WKxs9dklXMTEGcBCIvLhE/Tn3H6Gk1norupVW7m5Cnd4bLcr08AytbyV/xj7f/kQ==", "dependencies": { "@colors/colors": "1.6.0", "@types/triple-beam": "^1.3.2", @@ -5382,13 +5687,16 @@ }, "node_modules/loki-logger-ts": { "version": "1.0.2", + "resolved": "https://registry.npmjs.org/loki-logger-ts/-/loki-logger-ts-1.0.2.tgz", + "integrity": "sha512-SV/B5o+9jaxiThcU5N3LUxCNTx20IgR9xjCjx/ED/pVc/097mqKSRpmvSjvx9ezFcjJlUF7GBkrBBpR6veNp7Q==", "dependencies": { "axios": "^1.4.0" } }, "node_modules/long": { "version": "5.3.1", - "license": "Apache-2.0" + "resolved": "https://registry.npmjs.org/long/-/long-5.3.1.tgz", + "integrity": "sha512-ka87Jz3gcx/I7Hal94xaN2tZEOPoUOEVftkQqZx2EeQRN7LGdfLlI3FvZ+7WDplm+vK2Urx9ULrvSowtdCieng==" }, "node_modules/loupe": { "version": "3.1.3", @@ -5401,7 +5709,8 @@ }, "node_modules/lru-memoizer": { "version": "2.3.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/lru-memoizer/-/lru-memoizer-2.3.0.tgz", + "integrity": "sha512-GXn7gyHAMhO13WSKrIiNfztwxodVsP8IoZ3XfrJV4yH2x0/OeTO/FIaAHTY5YekdGgW94njfuKmyyt1E0mR6Ug==", "dependencies": { "lodash.clonedeep": "^4.5.0", "lru-cache": "6.0.0" @@ -5409,7 +5718,8 @@ }, "node_modules/lru-memoizer/node_modules/lru-cache": { "version": "6.0.0", - "license": "ISC", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", "dependencies": { "yallist": "^4.0.0" }, @@ -5419,7 +5729,8 @@ }, "node_modules/lru-memoizer/node_modules/yallist": { "version": "4.0.0", - "license": "ISC" + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" }, "node_modules/magic-string": { "version": "0.30.17", @@ -6132,7 +6443,8 @@ }, "node_modules/object-assign": { "version": "4.1.1", - "license": "MIT", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", "engines": { "node": ">=0.10.0" } @@ -6148,7 +6460,9 @@ } }, "node_modules/oidc-client-ts": { - "version": "3.1.0", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/oidc-client-ts/-/oidc-client-ts-3.2.0.tgz", + "integrity": "sha512-wUvVcG3SXzZDKHxi/VGQGaTUk9qguMKfYh26Y1zOVrQsu1zp85JWx/SjZzKSXK5j3NA1RcasgMoaHe6gt1WNtw==", "license": "Apache-2.0", "dependencies": { "jwt-decode": "^4.0.0" @@ -6169,7 +6483,8 @@ }, "node_modules/on-headers": { "version": "1.0.2", - "license": "MIT", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", + "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==", "engines": { "node": ">= 0.8" } @@ -6183,7 +6498,8 @@ }, "node_modules/one-time": { "version": "1.0.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/one-time/-/one-time-1.0.0.tgz", + "integrity": "sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==", "dependencies": { "fn.name": "1.x.x" } @@ -6328,8 +6644,8 @@ }, "node_modules/path-is-absolute": { "version": "1.0.1", + "devOptional": true, "license": "MIT", - "optional": true, "engines": { "node": ">=0.10.0" } @@ -6468,8 +6784,6 @@ }, "node_modules/pg-types": { "version": "2.2.0", - "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz", - "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==", "license": "MIT", "dependencies": { "pg-int8": "1.0.1", @@ -6520,8 +6834,6 @@ }, "node_modules/pgpass": { "version": "1.0.5", - "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz", - "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==", "license": "MIT", "dependencies": { "split2": "^4.1.0" @@ -6746,8 +7058,9 @@ }, "node_modules/protobufjs": { "version": "7.4.0", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.4.0.tgz", + "integrity": "sha512-mRUWCc3KUU4w1jU8sGxICXH/gNS94DvI1gxqDvBzhj1JpcsimQkYiOJfwsPUykUI5ZaspFbSgmBLER8IrQ3tqw==", "hasInstallScript": true, - "license": "BSD-3-Clause", "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", @@ -6959,7 +7272,8 @@ }, "node_modules/response-time": { "version": "2.3.3", - "license": "MIT", + "resolved": "https://registry.npmjs.org/response-time/-/response-time-2.3.3.tgz", + "integrity": "sha512-SsjjOPHl/FfrTQNgmc5oen8Hr1Jxpn6LlHNXxCIFdYMHuK1kMeYMobb9XN3mvxaGQm3dbegqYFMX4+GDORfbWg==", "dependencies": { "depd": "~2.0.0", "on-headers": "~1.0.1" @@ -7147,7 +7461,8 @@ }, "node_modules/safe-stable-stringify": { "version": "2.5.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", + "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", "engines": { "node": ">=10" } @@ -7394,7 +7709,8 @@ }, "node_modules/simple-swizzle": { "version": "0.2.2", - "license": "MIT", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", + "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==", "dependencies": { "is-arrayish": "^0.3.1" } @@ -7430,7 +7746,8 @@ }, "node_modules/snappy": { "version": "7.2.2", - "license": "MIT", + "resolved": "https://registry.npmjs.org/snappy/-/snappy-7.2.2.tgz", + "integrity": "sha512-iADMq1kY0v3vJmGTuKcFWSXt15qYUz7wFkArOrsSg0IFfI3nJqIJvK2/ZbEIndg7erIJLtAVX2nSOqPz7DcwbA==", "optional": true, "engines": { "node": ">= 10" @@ -7586,7 +7903,8 @@ }, "node_modules/stack-trace": { "version": "0.0.10", - "license": "MIT", + "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", + "integrity": "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==", "engines": { "node": "*" } @@ -7769,6 +8087,102 @@ "version": "1.0.0", "dev": true }, + "node_modules/swagger-autogen": { + "version": "2.23.7", + "resolved": "https://registry.npmjs.org/swagger-autogen/-/swagger-autogen-2.23.7.tgz", + "integrity": "sha512-vr7uRmuV0DCxWc0wokLJAwX3GwQFJ0jwN+AWk0hKxre2EZwusnkGSGdVFd82u7fQLgwSTnbWkxUL7HXuz5LTZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^7.4.1", + "deepmerge": "^4.2.2", + "glob": "^7.1.7", + "json5": "^2.2.3" + } + }, + "node_modules/swagger-autogen/node_modules/acorn": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", + "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/swagger-autogen/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/swagger-autogen/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/swagger-autogen/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/swagger-ui-dist": { + "version": "5.20.1", + "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.20.1.tgz", + "integrity": "sha512-qBPCis2w8nP4US7SvUxdJD3OwKcqiWeZmjN2VWhq2v+ESZEXOP/7n4DeiOiiZcGYTKMHAHUUrroHaTsjUWTEGw==", + "license": "Apache-2.0", + "dependencies": { + "@scarf/scarf": "=1.4.0" + } + }, + "node_modules/swagger-ui-express": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/swagger-ui-express/-/swagger-ui-express-5.0.1.tgz", + "integrity": "sha512-SrNU3RiBGTLLmFU8GIJdOdanJTl4TOmT27tt3bWWHppqYmAZ6IDuEuBvMU6nZq0zLEe6b/1rACXCgLZqO6ZfrA==", + "license": "MIT", + "dependencies": { + "swagger-ui-dist": ">=5.0.0" + }, + "engines": { + "node": ">= v0.10.32" + }, + "peerDependencies": { + "express": ">=4.0.0 || >=5.0.0-beta" + } + }, "node_modules/symbol-tree": { "version": "3.2.4", "license": "MIT" @@ -7851,7 +8265,8 @@ }, "node_modules/text-hex": { "version": "1.0.0", - "license": "MIT" + "resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz", + "integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==" }, "node_modules/tildify": { "version": "2.0.0", @@ -7955,7 +8370,8 @@ }, "node_modules/triple-beam": { "version": "1.4.1", - "license": "MIT", + "resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.4.1.tgz", + "integrity": "sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg==", "engines": { "node": ">= 14.0.0" } @@ -8218,7 +8634,8 @@ }, "node_modules/url-polyfill": { "version": "1.1.13", - "license": "MIT" + "resolved": "https://registry.npmjs.org/url-polyfill/-/url-polyfill-1.1.13.tgz", + "integrity": "sha512-tXzkojrv2SujumYthZ/WjF7jaSfNhSXlYMpE5AYdL2I3D7DCeo+mch8KtW2rUuKjDg+3VXODXHVgipt8yGY/eQ==" }, "node_modules/util-deprecate": { "version": "1.0.2", @@ -8630,6 +9047,26 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/vue-i18n": { + "version": "11.1.2", + "resolved": "https://registry.npmjs.org/vue-i18n/-/vue-i18n-11.1.2.tgz", + "integrity": "sha512-MfdkdKGUHN+jkkaMT5Zbl4FpRmN7kfelJIwKoUpJ32ONIxdFhzxZiLTVaAXkAwvH3y9GmWpoiwjDqbPIkPIMFA==", + "license": "MIT", + "dependencies": { + "@intlify/core-base": "11.1.2", + "@intlify/shared": "11.1.2", + "@vue/devtools-api": "^6.5.0" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/kazupon" + }, + "peerDependencies": { + "vue": "^3.0.0" + } + }, "node_modules/vue-router": { "version": "4.5.0", "license": "MIT", @@ -8823,7 +9260,8 @@ }, "node_modules/winston": { "version": "3.17.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/winston/-/winston-3.17.0.tgz", + "integrity": "sha512-DLiFIXYC5fMPxaRg832S6F5mJYvePtmO5G9v9IgUFPhXm9/GkXarH/TUrBAVzhTCzAj9anE/+GjrgXp/54nOgw==", "dependencies": { "@colors/colors": "^1.6.0", "@dabh/diagnostics": "^2.0.2", @@ -8843,7 +9281,8 @@ }, "node_modules/winston-loki": { "version": "6.1.3", - "license": "MIT", + "resolved": "https://registry.npmjs.org/winston-loki/-/winston-loki-6.1.3.tgz", + "integrity": "sha512-DjWtJ230xHyYQWr9mZJa93yhwHttn3JEtSYWP8vXZWJOahiQheUhf+88dSIidbGXB3u0oLweV6G1vkL/ouT62Q==", "dependencies": { "async-exit-hook": "2.0.1", "btoa": "^1.2.1", @@ -8857,7 +9296,8 @@ }, "node_modules/winston-transport": { "version": "4.9.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.9.0.tgz", + "integrity": "sha512-8drMJ4rkgaPo1Me4zD/3WLfI/zPdA9o2IipKODunnGDcuqbHwjsbB79ylv04LCGGzU0xQ6vTznOMpQGaLhhm6A==", "dependencies": { "logform": "^2.7.0", "readable-stream": "^3.6.2", @@ -8869,7 +9309,8 @@ }, "node_modules/winston/node_modules/is-stream": { "version": "2.0.1", - "license": "MIT", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", "engines": { "node": ">=8" }, diff --git a/package.json b/package.json index db7f5ba3..9a69bdb6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "dwengo-1-monorepo", - "version": "0.0.1", + "version": "0.1.1", "description": "Monorepo for Dwengo-1", "private": true, "type": "module", @@ -8,13 +8,13 @@ "build": "npm run build --ws", "format": "npm run format --ws", "format-check": "npm run format-check --ws", - "generate-docs": "python3 -m venv .venv && source .venv/bin/activate && pip install -r docs/requirements.txt && python docs/architecture/schema.py", "lint": "npm run lint --ws", "test:unit": "npm run test:unit --ws" }, "workspaces": [ "backend", - "frontend" + "frontend", + "docs" ], "repository": { "type": "git",