diff --git a/backend/package.json b/backend/package.json index 478b26e6..b4dc9c1e 100644 --- a/backend/package.json +++ b/backend/package.json @@ -25,6 +25,7 @@ "js-yaml": "^4.1.0", "loki-logger-ts": "^1.0.2", "response-time": "^2.3.3", + "swagger-ui-express": "^5.0.1", "uuid": "^11.1.0", "winston": "^3.17.0", "winston-loki": "^6.1.3" @@ -34,6 +35,7 @@ "@types/express": "^5.0.0", "@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 5769e360..f76d72d6 100644 --- a/backend/src/app.ts +++ b/backend/src/app.ts @@ -16,6 +16,8 @@ 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 swaggerMiddleware from "./swagger"; +import swaggerUi from "swagger-ui-express"; const logger: Logger = getLogger(); @@ -23,7 +25,6 @@ const app: Express = express(); const port: string | number = getNumericEnvVar(EnvVars.Port); app.use(express.json()); -app.use(responseTime(responseTimeLogger)); // TODO Replace with Express routes app.get('/', (_, res: Response) => { @@ -33,17 +34,24 @@ app.get('/', (_, res: Response) => { }); }); -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('/login', loginRouter); +// Routes +app.use('/student', studentRouter /* #swagger.tags = ['Student'] */); +app.use('/group', groupRouter /* #swagger.tags = ['Group'] */); +app.use('/assignment', assignmentRouter /* #swagger.tags = ['Assignment'] */); +app.use('/submission', submissionRouter /* #swagger.tags = ['Submission'] */); +app.use('/class', classRouter /* #swagger.tags = ['Class'] */); +app.use('/question', questionRouter /* #swagger.tags = ['Question'] */); +app.use('/login', loginRouter /* #swagger.tags = ['Login'] */); -app.use('/theme', themeRoutes); -app.use('/learningPath', learningPathRoutes); -app.use('/learningObject', learningObjectRoutes); +app.use('/theme', themeRoutes /* #swagger.tags = ['Theme'] */); +app.use('/learningPath', learningPathRoutes /* #swagger.tags = ['Learning Path'] */); +app.use('/learningObject', learningObjectRoutes /* #swagger.tags = ['Learning Object'] */); + +// Add response time loggin +app.use(responseTime(responseTimeLogger)); + +// Swagger UI for API documentation +app.use('/api-docs', swaggerUi.serve, swaggerMiddleware); async function startServer() { await initORM(); diff --git a/backend/src/swagger.ts b/backend/src/swagger.ts new file mode 100644 index 00000000..97b08496 --- /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'; + +const swaggerMiddleware: RequestHandler = swaggerUi.setup(swaggerDocument); + +export default swaggerMiddleware; 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/docs/api/generate.ts b/docs/api/generate.ts new file mode 100644 index 00000000..737619bb --- /dev/null +++ b/docs/api/generate.ts @@ -0,0 +1,28 @@ +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/', + }, + { + url: 'https://sel2-1.ugent.be/api' + } + ] +}; + +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..4f231cbd --- /dev/null +++ b/docs/api/swagger.json @@ -0,0 +1,712 @@ +{ + "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/" + }, + { + "url": "https://sel2-1.ugent.be/api" + } + ], + "paths": { + "/": { + "get": { + "description": "", + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/student/": { + "get": { + "tags": [ + "Student" + ], + "description": "", + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/student/{id}": { + "get": { + "tags": [ + "Student" + ], + "description": "", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/student/{id}/classes": { + "get": { + "tags": [ + "Student" + ], + "description": "", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/student/{id}/submissions": { + "get": { + "tags": [ + "Student" + ], + "description": "", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/student/{id}/assignments": { + "get": { + "tags": [ + "Student" + ], + "description": "", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/student/{id}/groups": { + "get": { + "tags": [ + "Student" + ], + "description": "", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/group/": { + "get": { + "tags": [ + "Group" + ], + "description": "", + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/group/{id}": { + "get": { + "tags": [ + "Group" + ], + "description": "", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/group/{id}/question": { + "get": { + "tags": [ + "Group" + ], + "description": "", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/assignment/": { + "get": { + "tags": [ + "Assignment" + ], + "description": "", + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/assignment/{id}": { + "get": { + "tags": [ + "Assignment" + ], + "description": "", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/assignment/{id}/submissions": { + "get": { + "tags": [ + "Assignment" + ], + "description": "", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/assignment/{id}/groups": { + "get": { + "tags": [ + "Assignment" + ], + "description": "", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/assignment/{id}/questions": { + "get": { + "tags": [ + "Assignment" + ], + "description": "", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/submission/": { + "get": { + "tags": [ + "Submission" + ], + "description": "", + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/submission/{id}": { + "get": { + "tags": [ + "Submission" + ], + "description": "", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/class/": { + "get": { + "tags": [ + "Class" + ], + "description": "", + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/class/{id}": { + "get": { + "tags": [ + "Class" + ], + "description": "", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/class/{id}/invitations": { + "get": { + "tags": [ + "Class" + ], + "description": "", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/class/{id}/assignments": { + "get": { + "tags": [ + "Class" + ], + "description": "", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/class/{id}/students": { + "get": { + "tags": [ + "Class" + ], + "description": "", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/question/": { + "get": { + "tags": [ + "Question" + ], + "description": "", + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/question/{id}": { + "get": { + "tags": [ + "Question" + ], + "description": "", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/question/{id}/answers": { + "get": { + "tags": [ + "Question" + ], + "description": "", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/login/": { + "get": { + "tags": [ + "Login" + ], + "description": "", + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/theme/": { + "get": { + "tags": [ + "Theme" + ], + "description": "", + "parameters": [ + { + "name": "language", + "in": "query", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/theme/{theme}": { + "get": { + "tags": [ + "Theme" + ], + "description": "", + "parameters": [ + { + "name": "theme", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK" + }, + "404": { + "description": "Not Found" + } + } + } + }, + "/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" + } + } + ], + "responses": { + "200": { + "description": "OK" + }, + "404": { + "description": "Not Found" + }, + "500": { + "description": "Internal Server Error" + } + } + } + }, + "/learningObject/": { + "get": { + "tags": [ + "Learning Object" + ], + "description": "", + "parameters": [ + { + "name": "hruid", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "full", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "language", + "in": "query", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK" + }, + "400": { + "description": "Bad Request" + }, + "500": { + "description": "Internal Server Error" + } + } + } + }, + "/learningObject/{hruid}": { + "get": { + "tags": [ + "Learning Object" + ], + "description": "", + "parameters": [ + { + "name": "hruid", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "language", + "in": "query", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK" + }, + "400": { + "description": "Bad Request" + }, + "500": { + "description": "Internal Server Error" + } + } + } + } + } +} \ No newline at end of file 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/package-lock.json b/package-lock.json index 92bf5a8e..3a99b9ad 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", @@ -39,6 +40,7 @@ "js-yaml": "^4.1.0", "loki-logger-ts": "^1.0.2", "response-time": "^2.3.3", + "swagger-ui-express": "^5.0.1", "uuid": "^11.1.0", "winston": "^3.17.0", "winston-loki": "^6.1.3" @@ -48,6 +50,7 @@ "@types/express": "^5.0.0", "@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", @@ -82,6 +85,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", @@ -1556,6 +1566,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, @@ -1739,6 +1756,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, @@ -3300,6 +3328,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, @@ -3415,6 +3453,10 @@ "resolved": "backend", "link": true }, + "node_modules/dwengo-1-docs": { + "resolved": "docs", + "link": true + }, "node_modules/dwengo-1-frontend": { "resolved": "frontend", "link": true @@ -4355,8 +4397,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", @@ -4806,8 +4848,8 @@ }, "node_modules/inflight": { "version": "1.0.6", + "devOptional": true, "license": "ISC", - "optional": true, "dependencies": { "once": "^1.3.0", "wrappy": "1" @@ -6230,8 +6272,8 @@ }, "node_modules/path-is-absolute": { "version": "1.0.1", + "devOptional": true, "license": "MIT", - "optional": true, "engines": { "node": ">=0.10.0" } @@ -7649,6 +7691,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.0", + "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.20.0.tgz", + "integrity": "sha512-V5pozVTZxivdoQq/SQWxj3A4cOu5opk9MEbcZANX3Pj8X8xCrD1QCtBT7744Pz9msOvt0nnmy9JvM/9PGonCdg==", + "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", "dev": true, diff --git a/package.json b/package.json index db7f5ba3..09703f20 100644 --- a/package.json +++ b/package.json @@ -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",