Refactoring Woka management (#1810)
* Wrap websockets with HyperExpress * Add endpoints on pusher to resolve wokas * getting textures urls from pusher * Adding OpenAPI documentation for the pusher. The pusher now exposes a "/openapi" endpoint and a "/swagger-ui/" endpoint. * revert FRONT_URL * playerTextures metadata is being loaded via Phaser.Loader * fetch textures every time character or customize scene is open * Heavy changes: refactoring the pusher to always send the textures (and the front to accept them) * Sending character layer details to admin * Cleaning commented code * Fixing regex * Fix woka endpoints on pusher * Change error wording on pusher * Working on integration of the woka-list with the new admin endpoint. * Switching from "name" to "id" in texture object + using zod for woka/list validation * Add position on default woka data * Remove async on pusher option method * Fix woka list url * add options for /register * Fxiing loading the Woka list * Actually returning something in logout-callback * Copying messages to back too * remove customize button if no body parts are available (#1952) * remove customize button if no body parts are available * remove unused position field from PlayerTexturesCollection interface * removed unused label field * fix LocalUser test * little PlayerTextures class refactor * Fixing linting * Fixing missing Openapi packages in prod * Fixing back build Co-authored-by: Hanusiak Piotr <piotr@ltmp.co> Co-authored-by: David Négrier <d.negrier@thecodingmachine.com> * Add returns on pusher endpoints Co-authored-by: Alexis Faizeau <a.faizeau@workadventu.re> Co-authored-by: Hanusiak Piotr <piotr@ltmp.co> Co-authored-by: Piotr Hanusiak <wacneg@gmail.com>
This commit is contained in:
@@ -1,20 +1,18 @@
|
||||
import { v4 } from "uuid";
|
||||
import { HttpRequest, HttpResponse, TemplatedApp } from "uWebSockets.js";
|
||||
import { BaseController } from "./BaseController";
|
||||
import { BaseHttpController } from "./BaseHttpController";
|
||||
import { adminApi, FetchMemberDataByUuidResponse } from "../Services/AdminApi";
|
||||
import { AuthTokenData, jwtTokenManager } from "../Services/JWTTokenManager";
|
||||
import { parse } from "query-string";
|
||||
import { openIDClient } from "../Services/OpenIDClient";
|
||||
import { DISABLE_ANONYMOUS, FRONT_URL } from "../Enum/EnvironmentVariable";
|
||||
import { DISABLE_ANONYMOUS } from "../Enum/EnvironmentVariable";
|
||||
import { RegisterData } from "../Messages/JsonMessages/RegisterData";
|
||||
|
||||
export interface TokenInterface {
|
||||
userUuid: string;
|
||||
}
|
||||
|
||||
export class AuthenticateController extends BaseController {
|
||||
constructor(private App: TemplatedApp) {
|
||||
super();
|
||||
export class AuthenticateController extends BaseHttpController {
|
||||
routes() {
|
||||
this.openIDLogin();
|
||||
this.openIDCallback();
|
||||
this.register();
|
||||
@@ -23,14 +21,41 @@ export class AuthenticateController extends BaseController {
|
||||
}
|
||||
|
||||
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 (res: HttpResponse, req: HttpRequest) => {
|
||||
res.onAborted(() => {
|
||||
console.warn("/message request was aborted");
|
||||
});
|
||||
|
||||
this.app.get("/login-screen", async (req, res) => {
|
||||
try {
|
||||
const { nonce, state, playUri, redirect } = parse(req.getQuery());
|
||||
const { nonce, state, playUri, redirect } = parse(req.path_query);
|
||||
if (!state || !nonce) {
|
||||
throw new Error("missing state and nonce URL parameters");
|
||||
}
|
||||
@@ -41,24 +66,53 @@ export class AuthenticateController extends BaseController {
|
||||
playUri as string | undefined,
|
||||
redirect as string | undefined
|
||||
);
|
||||
res.writeStatus("302");
|
||||
res.writeHeader("Location", loginUri);
|
||||
return res.end();
|
||||
res.status(302);
|
||||
res.setHeader("Location", loginUri);
|
||||
return res.send("");
|
||||
} catch (e) {
|
||||
console.error("openIDLogin => e", e);
|
||||
return this.errorToResponse(e, res);
|
||||
this.castErrorToResponse(e, res);
|
||||
return;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
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 (res: HttpResponse, req: HttpRequest) => {
|
||||
res.onAborted(() => {
|
||||
console.warn("/message request was aborted");
|
||||
});
|
||||
const IPAddress = req.getHeader("x-forwarded-for");
|
||||
const { code, nonce, token, playUri } = parse(req.getQuery());
|
||||
this.app.get("/login-callback", async (req, res) => {
|
||||
const IPAddress = req.header("x-forwarded-for");
|
||||
const { code, nonce, token, playUri } = parse(req.path_query);
|
||||
try {
|
||||
//verify connected by token
|
||||
if (token != undefined) {
|
||||
@@ -77,31 +131,22 @@ export class AuthenticateController extends BaseController {
|
||||
//if not nonce and code, user connected in anonymous
|
||||
//get data with identifier and return token
|
||||
if (!code && !nonce) {
|
||||
res.writeStatus("200");
|
||||
this.addCorsHeaders(res);
|
||||
res.writeHeader("Content-Type", "application/json");
|
||||
return res.end(JSON.stringify({ ...resUserData, authToken: token }));
|
||||
return res.json({ ...resUserData, authToken: token });
|
||||
}
|
||||
console.error("Token cannot to be check on OpenId provider");
|
||||
res.writeStatus("500");
|
||||
res.writeHeader("Access-Control-Allow-Origin", FRONT_URL);
|
||||
res.end("User cannot to be connected on openid provider");
|
||||
res.status(500);
|
||||
res.send("User cannot to be connected on openid provider");
|
||||
return;
|
||||
}
|
||||
|
||||
const resCheckTokenAuth = await openIDClient.checkTokenAuth(authTokenData.accessToken);
|
||||
res.writeStatus("200");
|
||||
this.addCorsHeaders(res);
|
||||
res.writeHeader("Content-Type", "application/json");
|
||||
return res.end(
|
||||
JSON.stringify({
|
||||
...resCheckTokenAuth,
|
||||
...resUserData,
|
||||
authToken: token,
|
||||
username: authTokenData?.username,
|
||||
locale: authTokenData?.locale,
|
||||
})
|
||||
);
|
||||
return res.json({
|
||||
...resCheckTokenAuth,
|
||||
...resUserData,
|
||||
authToken: token,
|
||||
username: authTokenData?.username,
|
||||
locale: authTokenData?.locale,
|
||||
});
|
||||
} catch (err) {
|
||||
console.info("User was not connected", err);
|
||||
}
|
||||
@@ -114,9 +159,8 @@ export class AuthenticateController extends BaseController {
|
||||
} catch (err) {
|
||||
//if no access on openid provider, return error
|
||||
console.error("User cannot to be connected on OpenId provider => ", err);
|
||||
res.writeStatus("500");
|
||||
res.writeHeader("Access-Control-Allow-Origin", FRONT_URL);
|
||||
res.end("User cannot to be connected on openid provider");
|
||||
res.status(500);
|
||||
res.send("User cannot to be connected on openid provider");
|
||||
return;
|
||||
}
|
||||
const email = userInfo.email || userInfo.sub;
|
||||
@@ -134,25 +178,32 @@ export class AuthenticateController extends BaseController {
|
||||
//This is very important to create User Local in LocalStorage in WorkAdventure
|
||||
const data = await this.getUserByUserIdentifier(email, playUri as string, IPAddress);
|
||||
|
||||
res.writeStatus("200");
|
||||
this.addCorsHeaders(res);
|
||||
res.writeHeader("Content-Type", "application/json");
|
||||
return res.end(
|
||||
JSON.stringify({ ...data, authToken, username: userInfo?.username, locale: userInfo?.locale })
|
||||
);
|
||||
return res.json({ ...data, authToken, username: userInfo?.username, locale: userInfo?.locale });
|
||||
} catch (e) {
|
||||
console.error("openIDCallback => ERROR", e);
|
||||
return this.errorToResponse(e, res);
|
||||
return this.castErrorToResponse(e, res);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* @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 (res: HttpResponse, req: HttpRequest) => {
|
||||
res.onAborted(() => {
|
||||
console.warn("/message request was aborted");
|
||||
});
|
||||
|
||||
const { token } = parse(req.getQuery());
|
||||
this.app.get("/logout-callback", async (req, res) => {
|
||||
const { token } = parse(req.path_query);
|
||||
|
||||
try {
|
||||
const authTokenData: AuthTokenData = jwtTokenManager.verifyJWTToken(token as string, false);
|
||||
@@ -162,29 +213,65 @@ export class AuthenticateController extends BaseController {
|
||||
await openIDClient.logoutUser(authTokenData.accessToken);
|
||||
} catch (error) {
|
||||
console.error("openIDCallback => logout-callback", error);
|
||||
} finally {
|
||||
res.writeStatus("200");
|
||||
this.addCorsHeaders(res);
|
||||
// eslint-disable-next-line no-unsafe-finally
|
||||
return res.end();
|
||||
}
|
||||
|
||||
return res.status(200).send("");
|
||||
});
|
||||
}
|
||||
|
||||
//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: ???
|
||||
* messages:
|
||||
* type: array
|
||||
* description: The list of messages to be displayed when the user logs?
|
||||
* example: ???
|
||||
*/
|
||||
private register() {
|
||||
this.App.options("/register", (res: HttpResponse, req: HttpRequest) => {
|
||||
this.addCorsHeaders(res);
|
||||
|
||||
res.end();
|
||||
this.app.options("/register", {}, (req, res) => {
|
||||
res.status(200).send("");
|
||||
});
|
||||
|
||||
this.App.post("/register", (res: HttpResponse, req: HttpRequest) => {
|
||||
this.app.post("/register", (req, res) => {
|
||||
(async () => {
|
||||
res.onAborted(() => {
|
||||
console.warn("Login request was aborted");
|
||||
});
|
||||
const param = await res.json();
|
||||
const param = await req.json();
|
||||
|
||||
//todo: what to do if the organizationMemberToken is already used?
|
||||
const organizationMemberToken: string | null = param.organizationMemberToken;
|
||||
@@ -196,71 +283,81 @@ export class AuthenticateController extends BaseController {
|
||||
const email = data.email;
|
||||
const roomUrl = data.roomUrl;
|
||||
const mapUrlStart = data.mapUrlStart;
|
||||
const textures = data.textures;
|
||||
|
||||
const authToken = jwtTokenManager.createAuthToken(email || userUuid);
|
||||
res.writeStatus("200 OK");
|
||||
this.addCorsHeaders(res);
|
||||
res.writeHeader("Content-Type", "application/json");
|
||||
res.end(
|
||||
JSON.stringify({
|
||||
authToken,
|
||||
userUuid,
|
||||
email,
|
||||
roomUrl,
|
||||
mapUrlStart,
|
||||
organizationMemberToken,
|
||||
textures,
|
||||
} as RegisterData)
|
||||
);
|
||||
res.json({
|
||||
authToken,
|
||||
userUuid,
|
||||
email,
|
||||
roomUrl,
|
||||
mapUrlStart,
|
||||
organizationMemberToken,
|
||||
} as RegisterData);
|
||||
} catch (e) {
|
||||
console.error("register => ERROR", e);
|
||||
this.errorToResponse(e, res);
|
||||
this.castErrorToResponse(e, res);
|
||||
}
|
||||
})();
|
||||
});
|
||||
}
|
||||
|
||||
//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.options("/anonymLogin", (res: HttpResponse, req: HttpRequest) => {
|
||||
this.addCorsHeaders(res);
|
||||
res.end();
|
||||
});
|
||||
|
||||
this.App.post("/anonymLogin", (res: HttpResponse, req: HttpRequest) => {
|
||||
res.onAborted(() => {
|
||||
console.warn("Login request was aborted");
|
||||
});
|
||||
|
||||
this.app.post("/anonymLogin", (req, res) => {
|
||||
if (DISABLE_ANONYMOUS) {
|
||||
res.writeStatus("403 FORBIDDEN");
|
||||
res.end();
|
||||
res.status(403);
|
||||
return res;
|
||||
} else {
|
||||
const userUuid = v4();
|
||||
const authToken = jwtTokenManager.createAuthToken(userUuid);
|
||||
res.writeStatus("200 OK");
|
||||
this.addCorsHeaders(res);
|
||||
res.writeHeader("Content-Type", "application/json");
|
||||
res.end(
|
||||
JSON.stringify({
|
||||
authToken,
|
||||
userUuid,
|
||||
})
|
||||
);
|
||||
return res.json({
|
||||
authToken,
|
||||
userUuid,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @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
|
||||
// @ts-ignore
|
||||
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
||||
this.App.get("/profile-callback", async (res: HttpResponse, req: HttpRequest) => {
|
||||
res.onAborted(() => {
|
||||
console.warn("/message request was aborted");
|
||||
});
|
||||
const { token } = parse(req.getQuery());
|
||||
this.app.get("/profile-callback", async (req, res) => {
|
||||
const { token } = parse(req.path_query);
|
||||
try {
|
||||
//verify connected by token
|
||||
if (token != undefined) {
|
||||
@@ -272,18 +369,18 @@ export class AuthenticateController extends BaseController {
|
||||
await openIDClient.checkTokenAuth(authTokenData.accessToken);
|
||||
|
||||
//get login profile
|
||||
res.writeStatus("302");
|
||||
res.writeHeader("Location", adminApi.getProfileUrl(authTokenData.accessToken));
|
||||
this.addCorsHeaders(res);
|
||||
// eslint-disable-next-line no-unsafe-finally
|
||||
return res.end();
|
||||
res.status(302);
|
||||
res.setHeader("Location", adminApi.getProfileUrl(authTokenData.accessToken));
|
||||
res.send("");
|
||||
return;
|
||||
} catch (error) {
|
||||
return this.errorToResponse(error, res);
|
||||
this.castErrorToResponse(error, res);
|
||||
return;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("profileCallback => ERROR", error);
|
||||
this.errorToResponse(error, res);
|
||||
this.castErrorToResponse(error, res);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -311,7 +408,7 @@ export class AuthenticateController extends BaseController {
|
||||
userRoomToken: undefined,
|
||||
};
|
||||
try {
|
||||
data = await adminApi.fetchMemberDataByUuid(email, playUri, IPAddress);
|
||||
data = await adminApi.fetchMemberDataByUuid(email, playUri, IPAddress, []);
|
||||
} catch (err) {
|
||||
console.error("openIDCallback => fetchMemberDataByUuid", err);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user