Skip to content
Snippets Groups Projects
Commit a1ab6547 authored by michael.minelli's avatar michael.minelli
Browse files

OpenAPI => First try (aborted): Use a library for generate openapi file from code and comment

parent 9a1088da
No related branches found
No related tags found
No related merge requests found
......@@ -4,6 +4,9 @@ workspace.xml
Wiki/.idea
ExpressAPI/src/config/Version.ts
openapi.json
redoc.html
############################ MacOS
# General
......
......@@ -9,5 +9,5 @@
"verbose": true,
"ext" : ".ts,.js",
"ignore" : [],
"exec" : "npm run lint; ts-node --files ./src/app.ts"
"exec" : "npm run lint; npm run build:openapi; ts-node --files ./src/app.ts"
}
This diff is collapsed.
......@@ -10,7 +10,9 @@
"dotenv:build" : "npx dotenv-vault local build",
"lint" : "npx eslint .",
"genversion" : "npx genversion -s -e src/config/Version.ts",
"build" : "npm run genversion; npx prisma generate && npx tsc --project ./ && cp -R assets dist/assets",
"build:openapi" : "npx ts-node src/openapi.ts; npx @redocly/cli build-docs assets/openapi.json --output=assets/redoc.html",
"build:project" : "npm run genversion; npx prisma generate && npx tsc --project ./ && cp -R assets dist/assets",
"build" : "npm run build:openapi; npm run build:project",
"database:migrate" : "npx prisma migrate deploy",
"database:seed" : "npm run genversion; npx prisma db seed",
"database:deploy" : "npm run database:migrate && npm run database:seed",
......@@ -43,11 +45,13 @@
"node" : "^20.5.0",
"parse-link-header" : "^2.0.0",
"semver" : "^7.5.4",
"swagger-ui-express": "^5.0.0",
"tar-stream" : "^3.1.6",
"uuid" : "^9.0.0",
"winston" : "^3.8.2"
},
"devDependencies": {
"@redocly/cli" : "^1.4.1",
"@types/compression" : "^1.7.2",
"@types/cors" : "^2.8.13",
"@types/express" : "^4.17.17",
......@@ -57,6 +61,7 @@
"@types/node" : "^20.4.7",
"@types/parse-link-header" : "^2.0.1",
"@types/semver" : "^7.5.3",
"@types/swagger-ui-express" : "^4.1.6",
"@types/tar-stream" : "^2.2.2",
"@types/uuid" : "^9.0.2",
"@typescript-eslint/eslint-plugin": "^6.10.0",
......@@ -64,9 +69,10 @@
"dotenv-vault" : "^1.25.0",
"genversion" : "^3.1.1",
"nodemon" : "^3.0.1",
"npm" : "^9.8.1",
"prisma" : "^5.1.1",
"swagger-autogen" : "^2.23.7",
"ts-node" : "^10.9.1",
"typescript" : "^5.1.6",
"npm" : "^9.8.1"
"typescript" : "^5.1.6"
}
}
......@@ -14,6 +14,8 @@ import ParamsCallbackManager from '../middlewares/ParamsCallbackManager';
import ApiRoutesManager from '../routes/ApiRoutesManager';
import compression from 'compression';
import ClientVersionCheckerMiddleware from '../middlewares/ClientVersionCheckerMiddleware';
import swaggerUi from 'swagger-ui-express';
import path from 'path';
class API implements WorkerTask {
......@@ -23,6 +25,7 @@ class API implements WorkerTask {
constructor() {
this.backend = express();
this.initSwagger();
this.initBaseMiddlewares();
this.backend.use(ClientVersionCheckerMiddleware.register());
......@@ -41,13 +44,35 @@ class API implements WorkerTask {
this.backend.use(cors()); //Allow CORS requests
this.backend.use(compression()); //Compress responses
}
this.backend.use(ClientVersionMiddleware.register());
ParamsCallbackManager.register(this.backend);
this.backend.use(SessionMiddleware.register());
private initSwagger() {
const options = {
swaggerOptions: {
url: '/docs/openapi.json'
}
};
this.backend.get('/docs/openapi.json', (req, res) => res.sendFile(path.resolve(__dirname + '/../../assets/openapi.json')));
this.backend.use('/docs/swagger', swaggerUi.serveFiles(undefined, options), swaggerUi.setup(undefined, options));
this.backend.get('/docs/redoc.html', (req, res) => res.sendFile(path.resolve(__dirname + '/../../assets/redoc.html')));
ApiRoutesManager.registerOnBackend(this.backend);
this.backend.get('/docs/', (req, res) => {
res.send(`
<!DOCTYPE html>
<html lang="en">
<body>
<ul>
<li><a href="/docs/openapi.json">OpenAPI</a></li>
<li>GUI
<ul>
<li><a href="/docs/swagger">Swagger</a></li>
<li><a href="/docs/redoc.html">Redoc</a></li>
</ul>
</li>
</ul>
</body>
</html>
`);
});
}
run() {
......
require('./InitialImports'); // ATTENTION : These lines MUST be the first of this file
import swaggerAutogen from 'swagger-autogen';
import { version } from './config/Version';
import Config from './config/Config';
const doc = {
info : {
title : 'Dojo API',
version : version,
description: '**Backend API of the Dojo project.**\n\nSee more information about the projet on [Gitlab](https://githepia.hesge.ch/dojo_project/dojo).',
license : {
name: 'AGPLv3',
url : 'https://githepia.hesge.ch/dojo_project/projects/backend/dojobackendapi/-/blob/main/LICENSE'
},
contact : {
name : 'Michaël Minelli',
email: 'dojo@minelli.me'
}
},
servers : [ {
url : `http://localhost:${ Config.api.port }/`,
description: 'Development'
}, {
url : `http://dojo-test.edu.hesge.ch/dojo/api/`,
description: 'Test (only from HES-GE network)'
}, {
url : `https://rdps.hesge.ch/dojo/api/`,
description: 'Production'
} ],
tags : [ {
name : 'General',
description: ''
}, {
name : 'Session',
description: 'Routes that are used to manage the user\'s session'
}, {
name : 'Gitlab',
description: 'Routes that are used to provide Gitlab informations'
}, {
name : 'Assignment',
description: 'Routes that are used to manage assignments'
}, {
name : 'Exercise',
description: 'Routes that are used to manage exercises'
} ],
consumes : [ 'multipart/form-data' ],
produces : [ 'application/json' ],
components: {
securitySchemes: {
'Clients token' : {
type : 'http',
scheme : 'bearer',
bearerFormat: 'JWT'
},
'ExerciseChecker secret': {
type: 'apiKey',
in : 'header',
name: 'ExerciseSecret'
}
},
schemas : {
DojoBackendResponse: {
$timestamp : '717876000000',
$code : 200,
$description : 'OK',
$sessionToken: 'JWT token (for content, see schema named \'SessionTokenJWT\')',
$data : {}
},
User : {
$id : 142,
name : 'michael.minelli',
mail : 'dojo@minelli.me',
$role : {
'@enum': [ 'STUDENT', 'TEACHING_STAFF', 'ADMIN' ]
},
$gitlabUsername : 'michael.minelli',
gitlabLastInfo : {},
$isTeachingStaff: true,
$isAdmin : true,
$deleted : false,
assignments : [ {
$ref: '#/components/schemas/Assignment'
} ],
exercises : [ {
$ref: '#/components/schemas/Exercise'
} ]
},
Assignment : {
$name : 'C_Hello_World',
$gitlabId : 30992,
$gitlabLink : 'https://githepia.hesge.ch/dojo_project',
$gitlabCreationInfo: {},
$gitlabLastInfo : {},
$gitlabLastInfoDate: '1992-09-30 19:00:00.000',
$published : true,
$staff : [ {
$ref: '#/components/schemas/User'
} ],
$exercises : [ {
$ref: '#/components/schemas/Exercise'
} ]
},
Exercise : {
$id : 'eb5f2182-f5b1-42a9-80fc-cad384571053',
$assignmentName : 'C_Hello_World',
$name : 'DojoEx - C_Hello_World - michael.minelli',
$gitlabId : 93092,
$gitlabLink : 'https://githepia.hesge.ch/dojo_project/dojo',
$gitlabCreationInfo: {},
$gitlabLastInfo : {},
$gitlabLastInfoDate: '1992-09-30 19:00:00.000'
},
SessionTokenJWT : {
$profile: {
$ref: '#/components/schemas/User'
},
$iat : '1700749215'
}
}
}
};
const options = {
openapi : '3.1.0',
autoHeader: false,
autoBody : false
};
const outputFile = '../assets/openapi.json';
const routes = [ './routes/*.ts' ];
swaggerAutogen(options)(outputFile, routes, doc);
\ No newline at end of file
......@@ -55,6 +55,11 @@ class AssignmentRoutes implements RoutesManager {
// Get an assignment by its name or gitlab url
private async getAssignment(req: express.Request, res: express.Response) {
/*
#swagger.tags = ['Assignment']
#swagger.description = 'Endpoint to get the specific user.'
*/
const assignment: Assignment | undefined = req.boundParams.assignment;
if ( assignment && !assignment.published && !await AssignmentManager.isUserAllowedToAccessAssignment(assignment, req.session.profile) ) {
......@@ -85,6 +90,11 @@ class AssignmentRoutes implements RoutesManager {
}
private async createAssignment(req: express.Request, res: express.Response) {
/*
#swagger.tags = ['Assignment']
#swagger.description = 'Endpoint to get the specific user.'
*/
const params: {
name: string, members: Array<GitlabUser>, template: string
} = req.body;
......@@ -168,10 +178,20 @@ class AssignmentRoutes implements RoutesManager {
}
private async publishAssignment(req: express.Request, res: express.Response) {
/*
#swagger.tags = ['Assignment']
#swagger.description = 'Endpoint to get the specific user.'
*/
return this.changeAssignmentPublishedStatus(true)(req, res);
}
private async unpublishAssignment(req: express.Request, res: express.Response) {
/*
#swagger.tags = ['Assignment']
#swagger.description = 'Endpoint to get the specific user.'
*/
return this.changeAssignmentPublishedStatus(false)(req, res);
}
......
......@@ -11,10 +11,29 @@ class BaseRoutes implements RoutesManager {
}
private async homepage(req: express.Request, res: express.Response) {
/*
#swagger.ignore = true
*/
return req.session.sendResponse(res, StatusCodes.OK);
}
private async healthCheck(req: express.Request, res: express.Response) {
/*
#swagger.tags = ['General']
#swagger.summary = 'Health check'
#swagger.description = 'This route can be used to check if the server is up and running.'
#swagger.responses[200] = {
content: {
"application/json": {
schema:{
$ref: "#/components/schemas/DojoBackendResponse"
}
}
}
}
*/
return req.session.sendResponse(res, StatusCodes.OK);
}
}
......
......@@ -85,6 +85,11 @@ class ExerciseRoutes implements RoutesManager {
}
private async createExercise(req: express.Request, res: express.Response) {
/*
#swagger.tags = ['Exercise']
#swagger.description = 'Endpoint to get the specific user.'
*/
const params: { members: Array<GitlabUser> } = req.body;
params.members = [ await req.session.profile.gitlabProfile!.value, ...params.members ].removeObjectDuplicates(gitlabUser => gitlabUser.id);
const assignment: Assignment = req.boundParams.assignment!;
......@@ -185,6 +190,34 @@ class ExerciseRoutes implements RoutesManager {
}
private async getAssignment(req: express.Request, res: express.Response) {
/*
#swagger.tags = ['Exercise', 'Assignment']
#swagger.description = 'Endpoint to get the specific user.'
#swagger.responses[200] = {
description: "Some description...",
content: {
"application/json": {
schema:{
allOf: [
{ $ref: "#/components/schemas/DojoBackendResponse" },
{
type : 'object',
properties : {
data: {
type: 'object',
properties: {
assignment: { $ref: "#/components/schemas/Assignment" }
}
}
}
}
]
}
}
}
}
*/
const repoTree: Array<GitlabTreeFile> = await GitlabManager.getRepositoryTree(req.boundParams.exercise!.assignment.gitlabId);
let assignmentHjsonFile!: GitlabFile;
......@@ -221,6 +254,11 @@ class ExerciseRoutes implements RoutesManager {
}
private async createResult(req: express.Request, res: express.Response) {
/*
#swagger.tags = ['Exercise']
#swagger.description = 'Endpoint to get the specific user.'
*/
const params: { exitCode: number, commit: Record<string, string>, results: ExerciseResultsFile, files: Array<IFileDirStat>, archiveBase64: string } = req.body;
const exercise: Exercise = req.boundParams.exercise!;
......
......@@ -12,6 +12,11 @@ class GitlabRoutes implements RoutesManager {
}
private async checkTemplateAccess(req: express.Request, res: express.Response) {
/*
#swagger.tags = ['Gitlab']
#swagger.description = 'Endpoint to get the specific user.'
*/
const idOrNamespace: string = req.params.idOrNamespace;
return res.status(await GitlabManager.checkTemplateAccess(idOrNamespace, req)).send();
......
......@@ -38,6 +38,51 @@ class SessionRoutes implements RoutesManager {
}
private async login(req: express.Request, res: express.Response) {
/*
#swagger.tags = ['Session']
#swagger.summary = 'Login to Dojo app'
#swagger.description = 'This route can be used to connect the user to the backend and retrieve informations about his access rights.'
#swagger.requestBody = {
content: {
"multipart/form-data": {
schema: {
type: 'object',
properties: {
accessToken: {
type: 'string',
format: 'Gitlab access token'
},
refreshToken: {
type: 'string',
format: 'Gitlab refresh token'
}
},
required: ['accessToken', 'refreshToken']
}
}
}
}
#swagger.responses[200] = {
content: {
"application/json": {
schema:{
$ref: "#/components/schemas/DojoBackendResponse"
}
}
}
}
#swagger.responses[404] = {
description: "Can't retrieve user informations from Gitlab with the provided access token.",
content: {
"application/json": {
schema:{
$ref: "#/components/schemas/DojoBackendResponse"
}
}
}
}
*/
try {
const params: {
accessToken: string, refreshToken: string
......@@ -59,6 +104,90 @@ class SessionRoutes implements RoutesManager {
}
private async refreshTokens(req: express.Request, res: express.Response) {
/*
#swagger.tags = ['Session']
#swagger.summary = 'Refresh tokens'
#swagger.description = 'This route can be used to refresh the session. Gitlab tokens will be refreshed and a new Dojo backend JWT token will be provided.'
#swagger.requestBody = {
content: {
"multipart/form-data": {
schema: {
type: 'object',
properties: {
refreshToken: {
type: 'string',
format: 'Gitlab refresh token'
}
},
required: ['refreshToken']
}
}
}
}
#swagger.responses[200] = {
description: 'The new Gitlab tokens as returned by Gitlab API.',
content: {
"application/json": {
schema:{
allOf: [
{ $ref: "#/components/schemas/DojoBackendResponse" },
{
type : 'object',
properties : {
data: {
type: 'object',
properties: {
"access_token": {
"type": "string",
"example": "de6780bc506a0446309bd9362820ba8aed28aa506c71eedbe1c5c4f9dd350e54"
},
"token_type": {
"type": "string",
"example": "bearer"
},
"expires_in": {
"type": "number",
"example": 7200
},
"refresh_token": {
"type": "string",
"example": "8257e65c97202ed1726cf9571600918f3bffb2544b26e00a61df9897668c33a1"
},
"scope": {
"type": "array",
"example": [
"api",
"create_runner",
"read_repository",
"write_repository"
],
"items": {
"type": "string"
}
},
"created_at": {
"type": "number",
"example": 1607635748
}
},
"required": [
"access_token",
"token_type",
"expires_in",
"refresh_token",
"scope",
"created_at"
]
}
}
}
]
}
}
}
}
*/
try {
const params: {
refreshToken: string
......@@ -73,6 +202,21 @@ class SessionRoutes implements RoutesManager {
}
private async testSession(req: express.Request, res: express.Response) {
/*
#swagger.tags = ['Session']
#swagger.summary = 'Test of the session'
#swagger.description = 'This route can be used to test the validity of the session token.'
#swagger.responses[200] = {
content: {
"application/json": {
schema:{
$ref: "#/components/schemas/DojoBackendResponse"
}
}
}
}
*/
req.session.sendResponse(res, StatusCodes.OK);
}
}
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment