Adding OpenAPI documentation for the pusher.

The pusher now exposes a "/openapi" endpoint and a "/swagger-ui/" endpoint.
This commit is contained in:
David Négrier
2022-02-21 19:05:56 +01:00
parent 80761804a7
commit 3b4f06d659
10 changed files with 547 additions and 6 deletions
+5
View File
@@ -7,8 +7,10 @@ import { DebugController } from "./Controller/DebugController";
import { AdminController } from "./Controller/AdminController";
import { OpenIdProfileController } from "./Controller/OpenIdProfileController";
import { WokaListController } from "./Controller/WokaListController";
import { SwaggerController } from "./Controller/SwaggerController";
import HyperExpress from "hyper-express";
import { cors } from "./Middleware/Cors";
import { ENABLE_OPENAPI_ENDPOINT } from "./Enum/EnvironmentVariable";
class App {
public app: HyperExpress.compressors.TemplatedApp;
@@ -31,6 +33,9 @@ class App {
new AdminController(webserver);
new OpenIdProfileController(webserver);
new WokaListController(webserver);
if (ENABLE_OPENAPI_ENDPOINT) {
new SwaggerController(webserver);
}
}
}
+55
View File
@@ -13,6 +13,27 @@ export class AdminController extends BaseHttpController {
this.receiveRoomEditionPrompt();
}
/**
* @openapi
* /room/refresh:
* post:
* description: Forces anyone out of the room. The request must be authenticated with the "admin-token" header.
* parameters:
* - name: "admin-token"
* in: "header"
* required: true
* type: "string"
* description: TODO - move this to a classic "Authorization" header!
* - name: "roomId"
* in: "body"
* description: "The ID (full URL) to the room"
* required: true
* type: "string"
* responses:
* 200:
* description: Will always return "ok".
* example: "ok"
*/
receiveRoomEditionPrompt() {
// eslint-disable-next-line @typescript-eslint/no-misused-promises
this.app.post("/room/refresh", { middlewares: [adminToken] }, async (req, res) => {
@@ -43,6 +64,40 @@ export class AdminController extends BaseHttpController {
});
}
/**
* @openapi
* /message:
* post:
* description: Sends a message (or a world full message) to a number of rooms.
* parameters:
* - name: "admin-token"
* in: "header"
* required: true
* type: "string"
* description: TODO - move this to a classic "Authorization" header!
* - name: "text"
* in: "body"
* description: "The text of the message"
* required: true
* type: "string"
* - name: "type"
* in: "body"
* description: Either "capacity" or "message
* required: true
* type: "string"
* - name: "targets"
* in: "body"
* description: The list of room IDs to target
* required: true
* type: array
* items:
* type: string
* example: "https://play.workadventu.re/@/foo/bar/baz"
* responses:
* 200:
* description: Will always return "ok".
* example: "ok"
*/
receiveGlobalMessagePrompt() {
// eslint-disable-next-line @typescript-eslint/no-misused-promises
this.app.post("/message", { middlewares: [adminToken] }, async (req, res) => {
+165 -2
View File
@@ -21,6 +21,37 @@ export class AuthenticateController extends BaseHttpController {
}
openIDLogin() {
/**
* @openapi
* /login-screen:
* get:
* description: Redirects the user to the OpenID login screen
* parameters:
* - name: "nonce"
* in: "query"
* description: "todo"
* required: true
* type: "string"
* - name: "state"
* in: "query"
* description: "todo"
* required: true
* type: "string"
* - name: "playUri"
* in: "query"
* description: "todo"
* required: false
* type: "string"
* - name: "redirect"
* in: "query"
* description: "todo"
* required: false
* type: "string"
* responses:
* 302:
* description: Redirects the user to the OpenID login screen
*
*/
//eslint-disable-next-line @typescript-eslint/no-misused-promises
this.app.get("/login-screen", async (req, res) => {
try {
@@ -47,6 +78,37 @@ export class AuthenticateController extends BaseHttpController {
}
openIDCallback() {
/**
* @openapi
* /login-callback:
* get:
* description: TODO
* parameters:
* - name: "nonce"
* in: "query"
* description: "todo"
* required: true
* type: "string"
* - name: "state"
* in: "query"
* description: "todo"
* required: true
* type: "string"
* - name: "playUri"
* in: "query"
* description: "todo"
* required: false
* type: "string"
* - name: "redirect"
* in: "query"
* description: "todo"
* required: false
* type: "string"
* responses:
* 200:
* description: TODO
*
*/
//eslint-disable-next-line @typescript-eslint/no-misused-promises
this.app.get("/login-callback", async (req, res) => {
const IPAddress = req.header("x-forwarded-for");
@@ -112,6 +174,22 @@ export class AuthenticateController extends BaseHttpController {
}
});
/**
* @openapi
* /logout-callback:
* get:
* description: TODO
* parameters:
* - name: "token"
* in: "query"
* description: "todo"
* required: false
* type: "string"
* responses:
* 200:
* description: TODO
*
*/
// eslint-disable-next-line @typescript-eslint/no-misused-promises
this.app.get("/logout-callback", async (req, res) => {
const { token } = parse(req.path_query);
@@ -130,7 +208,56 @@ export class AuthenticateController extends BaseHttpController {
});
}
//Try to login with an admin token
/**
* @openapi
* /register:
* post:
* description: Try to login with an admin token
* parameters:
* - name: "organizationMemberToken"
* in: "body"
* description: "A token allowing a user to connect to a given world"
* required: true
* type: "string"
* responses:
* 200:
* description: The details of the logged user
* content:
* application/json:
* schema:
* type: object
* properties:
* authToken:
* type: string
* description: A unique identification JWT token
* userUuid:
* type: string
* description: Unique user ID
* email:
* type: string|null
* description: The email of the user
* example: john.doe@example.com
* roomUrl:
* type: string
* description: The room URL to connect to
* example: https://play.workadventu.re/@/foo/bar/baz
* organizationMemberToken:
* type: string|null
* description: TODO- unclear. It seems to be sent back from the request?
* example: ???
* mapUrlStart:
* type: string
* description: TODO- unclear. I cannot find any use of this
* example: ???
* textures:
* type: string
* description: TODO - document this is still needed
* example: ???
* messages:
* type: array
* description: The list of messages to be displayed when the user logs?
* example: ???
*/
private register() {
this.app.post("/register", (req, res) => {
(async () => {
@@ -166,7 +293,28 @@ export class AuthenticateController extends BaseHttpController {
});
}
//permit to login on application. Return token to connect on Websocket IO.
/**
* @openapi
* /anonymLogin:
* post:
* description: Generates an "anonymous" JWT token allowing to connect to WorkAdventure anonymously.
* responses:
* 200:
* description: The details of the logged user
* content:
* application/json:
* schema:
* type: object
* properties:
* authToken:
* type: string
* description: A unique identification JWT token
* userUuid:
* type: string
* description: Unique user ID
* 403:
* description: Anonymous login is disabled at the configuration level (environment variable DISABLE_ANONYMOUS = true)
*/
private anonymLogin() {
this.app.post("/anonymLogin", (req, res) => {
if (DISABLE_ANONYMOUS) {
@@ -183,6 +331,21 @@ export class AuthenticateController extends BaseHttpController {
});
}
/**
* @openapi
* /profile-callback:
* get:
* description: ???
* parameters:
* - name: "token"
* in: "query"
* description: "A JWT authentication token ???"
* required: true
* type: "string"
* responses:
* 302:
* description: Redirects the user to the profile screen of the admin
*/
profileCallback() {
// eslint-disable-next-line @typescript-eslint/no-misused-promises
this.app.get("/profile-callback", async (req, res) => {
+88
View File
@@ -10,6 +10,94 @@ import { BaseHttpController } from "./BaseHttpController";
export class MapController extends BaseHttpController {
// Returns a map mapping map name to file name of the map
routes() {
/**
* @openapi
* /map:
* get:
* description: Returns a map mapping map name to file name of the map
* produces:
* - "application/json"
* parameters:
* - name: "playUri"
* in: "query"
* description: "The full URL of WorkAdventure to load this map"
* required: true
* type: "string"
* - name: "authToken"
* in: "query"
* description: "The authentication token"
* required: true
* type: "string"
* responses:
* 200:
* description: The details of the map
* content:
* application/json:
* schema:
* type: object
* required:
* - mapUrl
* - policy_type
* - tags
* - textures
* - authenticationMandatory
* - roomSlug
* - contactPage
* - group
* properties:
* mapUrl:
* type: string
* description: The full URL to the JSON map file
* example: https://myuser.github.io/myrepo/map.json
* policy_type:
* type: integer
* description: ANONYMOUS_POLICY = 1, MEMBERS_ONLY_POLICY = 2, USE_TAGS_POLICY= 3
* example: 1
* tags:
* type: array
* description: The list of tags required to enter this room
* items:
* type: string
* example: speaker
* textures:
* type: array
* description: The list of public textures for this map (TODO remove this)
* items:
* type: object
* properties:
* todo:
* type: string
* authenticationMandatory:
* type: boolean|null
* description: Whether the authentication is mandatory or not for this map.
* example: true
* roomSlug:
* type: string
* description: The slug of the room
* deprecated: true
* example: foo
* contactPage:
* type: string|null
* description: The URL to the contact page
* example: https://mycompany.com/contact-us
* group:
* type: string|null
* description: The group this room is part of (maps the notion of "world" in WorkAdventure SAAS)
* example: myorg/myworld
* iframeAuthentication:
* type: string|null
* description: The URL of the authentication Iframe
* example: https://mycompany.com/authc
* expireOn:
* type: string|undefined
* description: The date (in ISO 8601 format) at which the room will expire
* example: 2022-11-05T08:15:30-05:00
* canReport:
* type: boolean|undefined
* description: Whether the "report" feature is enabled or not on this room
* example: true
*
*/
this.app.get("/map", (req, res) => {
const query = parse(req.path_query);
if (typeof query.playUri !== "string") {
@@ -0,0 +1,63 @@
import swaggerJsdoc from "swagger-jsdoc";
import { BaseHttpController } from "./BaseHttpController";
// @ts-ignore
import LiveDirectory from "live-directory";
import * as fs from "fs";
export class SwaggerController extends BaseHttpController {
routes() {
this.app.get("/openapi", (req, res) => {
const options = {
swaggerDefinition: {
openapi: "3.0.0",
info: {
title: "WorkAdventure Pusher",
version: "1.0.0",
},
},
apis: ["./src/Controller/*.ts"],
};
res.json(swaggerJsdoc(options));
});
// Create a LiveDirectory instance to virtualize directory with our assets
const LiveAssets = new LiveDirectory({
path: __dirname + "/../../node_modules/swagger-ui-dist", // We want to provide the system path to the folder. Avoid using relative paths.
keep: {
extensions: [".css", ".js", ".json", ".png", ".jpg", ".jpeg", ".html"], // We only want to serve files with these extensions
},
ignore: (path: string) => {
return path.startsWith("."); // We want to ignore dotfiles for safety
},
});
// Create static serve route to serve index.html
this.app.get("/swagger-ui/", (request, response) => {
fs.readFile(__dirname + "/../../node_modules/swagger-ui-dist/index.html", "utf8", function (err, data) {
if (err) {
return response.status(500).send(err.message);
}
const result = data.replace(/https:\/\/petstore.swagger.io\/v2\/swagger.json/g, "/openapi");
response.send(result);
return;
});
});
// Create static serve route to serve frontend assets
this.app.get("/swagger-ui/*", (request, response) => {
// Strip away '/assets' from the request path to get asset relative path
// Lookup LiveFile instance from our LiveDirectory instance.
const path = request.path.replace("/swagger-ui", "");
const file = LiveAssets.get(path);
// Return a 404 if no asset/file exists on the derived path
if (file === undefined) return response.status(404).send("");
// Set appropriate mime-type and serve file buffer as response body
return response.type(file.extension).send(file.buffer);
});
}
}
+3
View File
@@ -20,6 +20,9 @@ export const OPID_CLIENT_REDIRECT_URL = process.env.OPID_CLIENT_REDIRECT_URL ||
export const OPID_PROFILE_SCREEN_PROVIDER = process.env.OPID_PROFILE_SCREEN_PROVIDER || ADMIN_URL + "/profile";
export const DISABLE_ANONYMOUS: boolean = process.env.DISABLE_ANONYMOUS === "true";
// If set to the string "true", the /openapi route will return the OpenAPI definition and the swagger-ui/ route will display the documentation
export const ENABLE_OPENAPI_ENDPOINT = process.env.ENABLE_OPENAPI_ENDPOINT === "true";
export {
SECRET_KEY,
API_URL,