Merge branch 'develop' into changeRegisterAccess
Signed-off-by: Gregoire Parant <g.parant@thecodingmachine.com> # Conflicts: # pusher/src/Services/AdminApi.ts
This commit is contained in:
+25
-18
@@ -4,31 +4,38 @@ import { AuthenticateController } from "./Controller/AuthenticateController"; //
|
||||
import { MapController } from "./Controller/MapController";
|
||||
import { PrometheusController } from "./Controller/PrometheusController";
|
||||
import { DebugController } from "./Controller/DebugController";
|
||||
import { App as uwsApp } from "./Server/sifrr.server";
|
||||
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: uwsApp;
|
||||
public ioSocketController: IoSocketController;
|
||||
public authenticateController: AuthenticateController;
|
||||
public mapController: MapController;
|
||||
public prometheusController: PrometheusController;
|
||||
private debugController: DebugController;
|
||||
private adminController: AdminController;
|
||||
private openIdProfileController: OpenIdProfileController;
|
||||
public app: HyperExpress.compressors.TemplatedApp;
|
||||
|
||||
constructor() {
|
||||
this.app = new uwsApp();
|
||||
const webserver = new HyperExpress.Server();
|
||||
this.app = webserver.uws_instance;
|
||||
|
||||
//create socket controllers
|
||||
this.ioSocketController = new IoSocketController(this.app);
|
||||
this.authenticateController = new AuthenticateController(this.app);
|
||||
this.mapController = new MapController(this.app);
|
||||
this.prometheusController = new PrometheusController(this.app);
|
||||
this.debugController = new DebugController(this.app);
|
||||
this.adminController = new AdminController(this.app);
|
||||
this.openIdProfileController = new OpenIdProfileController(this.app);
|
||||
// Global middlewares
|
||||
webserver.use(cors);
|
||||
|
||||
// Socket controllers
|
||||
new IoSocketController(this.app);
|
||||
|
||||
// Http controllers
|
||||
new AuthenticateController(webserver);
|
||||
new MapController(webserver);
|
||||
new PrometheusController(webserver);
|
||||
new DebugController(webserver);
|
||||
new AdminController(webserver);
|
||||
new OpenIdProfileController(webserver);
|
||||
new WokaListController(webserver);
|
||||
if (ENABLE_OPENAPI_ENDPOINT) {
|
||||
new SwaggerController(webserver);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,45 +1,43 @@
|
||||
import { BaseController } from "./BaseController";
|
||||
import { HttpRequest, HttpResponse, TemplatedApp } from "uWebSockets.js";
|
||||
import { ADMIN_API_TOKEN } from "../Enum/EnvironmentVariable";
|
||||
import { apiClientRepository } from "../Services/ApiClientRepository";
|
||||
import {
|
||||
AdminRoomMessage,
|
||||
WorldFullWarningToRoomMessage,
|
||||
RefreshRoomPromptMessage,
|
||||
} from "../Messages/generated/messages_pb";
|
||||
import { adminToken } from "../Middleware/AdminToken";
|
||||
import { BaseHttpController } from "./BaseHttpController";
|
||||
|
||||
export class AdminController extends BaseController {
|
||||
constructor(private App: TemplatedApp) {
|
||||
super();
|
||||
this.App = App;
|
||||
export class AdminController extends BaseHttpController {
|
||||
routes() {
|
||||
this.receiveGlobalMessagePrompt();
|
||||
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() {
|
||||
this.App.options("/room/refresh", (res: HttpResponse, req: HttpRequest) => {
|
||||
this.addCorsHeaders(res);
|
||||
res.end();
|
||||
});
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
||||
this.App.post("/room/refresh", async (res: HttpResponse, req: HttpRequest) => {
|
||||
res.onAborted(() => {
|
||||
console.warn("/message request was aborted");
|
||||
});
|
||||
|
||||
const token = req.getHeader("admin-token");
|
||||
const body = await res.json();
|
||||
|
||||
if (ADMIN_API_TOKEN === "") {
|
||||
res.writeStatus("401 Unauthorized").end("No token configured!");
|
||||
return;
|
||||
}
|
||||
if (token !== ADMIN_API_TOKEN) {
|
||||
console.error("Admin access refused for token: " + token);
|
||||
res.writeStatus("401 Unauthorized").end("Incorrect token");
|
||||
return;
|
||||
}
|
||||
this.app.post("/room/refresh", { middlewares: [adminToken] }, async (req, res) => {
|
||||
const body = await req.json();
|
||||
|
||||
try {
|
||||
if (typeof body.roomId !== "string") {
|
||||
@@ -58,39 +56,53 @@ export class AdminController extends BaseController {
|
||||
});
|
||||
});
|
||||
} catch (err) {
|
||||
this.errorToResponse(err, res);
|
||||
this.castErrorToResponse(err, res);
|
||||
return;
|
||||
}
|
||||
|
||||
res.writeStatus("200");
|
||||
res.end("ok");
|
||||
res.send("ok");
|
||||
return;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @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() {
|
||||
this.App.options("/message", (res: HttpResponse, req: HttpRequest) => {
|
||||
this.addCorsHeaders(res);
|
||||
res.end();
|
||||
});
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
||||
this.App.post("/message", async (res: HttpResponse, req: HttpRequest) => {
|
||||
res.onAborted(() => {
|
||||
console.warn("/message request was aborted");
|
||||
});
|
||||
|
||||
const token = req.getHeader("admin-token");
|
||||
const body = await res.json();
|
||||
|
||||
if (ADMIN_API_TOKEN === "") {
|
||||
res.writeStatus("401 Unauthorized").end("No token configured!");
|
||||
return;
|
||||
}
|
||||
if (token !== ADMIN_API_TOKEN) {
|
||||
console.error("Admin access refused for token: " + token);
|
||||
res.writeStatus("401 Unauthorized").end("Incorrect token");
|
||||
return;
|
||||
}
|
||||
this.app.post("/message", { middlewares: [adminToken] }, async (req, res) => {
|
||||
const body = await req.json();
|
||||
|
||||
try {
|
||||
if (typeof body.text !== "string") {
|
||||
@@ -131,13 +143,11 @@ export class AdminController extends BaseController {
|
||||
})
|
||||
);
|
||||
} catch (err) {
|
||||
this.errorToResponse(err, res);
|
||||
this.castErrorToResponse(err, res);
|
||||
return;
|
||||
}
|
||||
|
||||
res.writeStatus("200");
|
||||
this.addCorsHeaders(res);
|
||||
res.end("ok");
|
||||
res.send("ok");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,98 @@ 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: "code"
|
||||
* in: "query"
|
||||
* description: "todo"
|
||||
* required: false
|
||||
* type: "string"
|
||||
* - name: "nonce"
|
||||
* in: "query"
|
||||
* description: "todo"
|
||||
* required: false
|
||||
* type: "string"
|
||||
* - name: "token"
|
||||
* in: "query"
|
||||
* description: "todo"
|
||||
* required: false
|
||||
* type: "string"
|
||||
* - name: "playUri"
|
||||
* in: "query"
|
||||
* description: "todo"
|
||||
* required: true
|
||||
* type: "string"
|
||||
* responses:
|
||||
* 200:
|
||||
* description: NOTE - THERE ARE ADDITIONAL PROPERTIES NOT DISPLAYED HERE. THEY COME FROM THE CALL TO openIDClient.checkTokenAuth
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* authToken:
|
||||
* type: string
|
||||
* description: A new JWT token (if no token was passed in parameter), or returns the token that was passed in parameter if one was supplied
|
||||
* username:
|
||||
* type: string|undefined
|
||||
* description: Contains the username stored in the JWT token passed in parameter. If no token was passed, contains the data from OpenID.
|
||||
* example: John Doe
|
||||
* locale:
|
||||
* type: string|undefined
|
||||
* description: Contains the locale stored in the JWT token passed in parameter. If no token was passed, contains the data from OpenID.
|
||||
* example: fr_FR
|
||||
* email:
|
||||
* type: string
|
||||
* description: TODO
|
||||
* example: TODO
|
||||
* userUuid:
|
||||
* type: string
|
||||
* description: TODO
|
||||
* example: TODO
|
||||
* visitCardUrl:
|
||||
* type: string|null
|
||||
* description: TODO
|
||||
* example: TODO
|
||||
* tags:
|
||||
* type: array
|
||||
* description: The list of tags of the user
|
||||
* items:
|
||||
* type: string
|
||||
* example: speaker
|
||||
* textures:
|
||||
* type: array
|
||||
* description: The list of textures of the user
|
||||
* items:
|
||||
* type: TODO
|
||||
* example: TODO
|
||||
* messages:
|
||||
* type: array
|
||||
* description: The list of messages to be displayed to the user
|
||||
* items:
|
||||
* type: TODO
|
||||
* example: 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,21 +176,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);
|
||||
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);
|
||||
return res.end(JSON.stringify({ ...resCheckTokenAuth, ...resUserData, authToken: token }));
|
||||
return res.json({
|
||||
...resCheckTokenAuth,
|
||||
...resUserData,
|
||||
authToken: token,
|
||||
username: authTokenData?.username,
|
||||
locale: authTokenData?.locale,
|
||||
});
|
||||
} catch (err) {
|
||||
console.info("User was not connected", err);
|
||||
}
|
||||
@@ -104,37 +204,51 @@ 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;
|
||||
if (!email) {
|
||||
throw new Error("No email in the response");
|
||||
}
|
||||
const authToken = jwtTokenManager.createAuthToken(email, userInfo?.access_token);
|
||||
const authToken = jwtTokenManager.createAuthToken(
|
||||
email,
|
||||
userInfo?.access_token,
|
||||
userInfo?.username,
|
||||
userInfo?.locale
|
||||
);
|
||||
|
||||
//Get user data from Admin Back Office
|
||||
//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);
|
||||
return res.end(JSON.stringify({ ...data, authToken }));
|
||||
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);
|
||||
@@ -144,29 +258,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;
|
||||
@@ -179,69 +329,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.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.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) {
|
||||
@@ -253,18 +415,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);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -292,7 +454,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);
|
||||
}
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
import { HttpResponse } from "uWebSockets.js";
|
||||
import { FRONT_URL } from "../Enum/EnvironmentVariable";
|
||||
|
||||
export class BaseController {
|
||||
protected addCorsHeaders(res: HttpResponse): void {
|
||||
res.writeHeader("access-control-allow-headers", "Origin, X-Requested-With, Content-Type, Accept");
|
||||
res.writeHeader("access-control-allow-methods", "GET, POST, OPTIONS, PUT, PATCH, DELETE");
|
||||
res.writeHeader("access-control-allow-origin", FRONT_URL);
|
||||
}
|
||||
|
||||
/**
|
||||
* Turns any exception into a HTTP response (and logs the error)
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
protected errorToResponse(e: any, res: HttpResponse): void {
|
||||
if (e && e.message) {
|
||||
let url = e?.config?.url;
|
||||
if (url !== undefined) {
|
||||
url = " for URL: " + url;
|
||||
} else {
|
||||
url = "";
|
||||
}
|
||||
console.error("ERROR: " + e.message + url);
|
||||
} else if (typeof e === "string") {
|
||||
console.error(e);
|
||||
}
|
||||
if (e.stack) {
|
||||
console.error(e.stack);
|
||||
}
|
||||
if (e.response) {
|
||||
res.writeStatus(e.response.status + " " + e.response.statusText);
|
||||
this.addCorsHeaders(res);
|
||||
res.end(
|
||||
"An error occurred: " +
|
||||
e.response.status +
|
||||
" " +
|
||||
(e.response.data && e.response.data.message ? e.response.data.message : e.response.statusText)
|
||||
);
|
||||
} else {
|
||||
res.writeStatus("500 Internal Server Error");
|
||||
this.addCorsHeaders(res);
|
||||
res.end("An error occurred");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
import { Server } from "hyper-express";
|
||||
import Response from "hyper-express/types/components/http/Response";
|
||||
import axios from "axios";
|
||||
|
||||
export class BaseHttpController {
|
||||
constructor(protected app: Server) {
|
||||
this.routes();
|
||||
}
|
||||
|
||||
protected routes() {
|
||||
/* Define routes on children */
|
||||
}
|
||||
|
||||
protected castErrorToResponse(e: unknown, res: Response): void {
|
||||
if (e instanceof Error) {
|
||||
let url: string | undefined;
|
||||
if (axios.isAxiosError(e)) {
|
||||
url = e.config.url;
|
||||
if (url !== undefined) {
|
||||
url = " for URL: " + url;
|
||||
} else {
|
||||
url = "";
|
||||
}
|
||||
}
|
||||
|
||||
console.error("ERROR: " + e.message + url);
|
||||
console.error(e.stack);
|
||||
} else if (typeof e === "string") {
|
||||
console.error(e);
|
||||
}
|
||||
|
||||
if (axios.isAxiosError(e) && e.response) {
|
||||
res.status(e.response.status);
|
||||
res.send(
|
||||
"An error occurred: " +
|
||||
e.response.status +
|
||||
" " +
|
||||
(e.response.data && e.response.data.message ? e.response.data.message : e.response.statusText)
|
||||
);
|
||||
return;
|
||||
} else {
|
||||
res.status(500);
|
||||
res.send("An error occurred");
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,51 +1,42 @@
|
||||
import { ADMIN_API_TOKEN } from "../Enum/EnvironmentVariable";
|
||||
import { IoSocketController } from "_Controller/IoSocketController";
|
||||
import { stringify } from "circular-json";
|
||||
import { HttpRequest, HttpResponse } from "uWebSockets.js";
|
||||
import { parse } from "query-string";
|
||||
import { App } from "../Server/sifrr.server";
|
||||
import { socketManager } from "../Services/SocketManager";
|
||||
import { BaseHttpController } from "./BaseHttpController";
|
||||
|
||||
export class DebugController {
|
||||
constructor(private App: App) {
|
||||
this.getDump();
|
||||
}
|
||||
|
||||
getDump() {
|
||||
this.App.get("/dump", (res: HttpResponse, req: HttpRequest) => {
|
||||
const query = parse(req.getQuery());
|
||||
export class DebugController extends BaseHttpController {
|
||||
routes() {
|
||||
this.app.get("/dump", (req, res) => {
|
||||
const query = parse(req.path_query);
|
||||
|
||||
if (ADMIN_API_TOKEN === "") {
|
||||
return res.writeStatus("401 Unauthorized").end("No token configured!");
|
||||
return res.status(401).send("No token configured!");
|
||||
}
|
||||
if (query.token !== ADMIN_API_TOKEN) {
|
||||
return res.writeStatus("401 Unauthorized").end("Invalid token sent!");
|
||||
return res.status(401).send("Invalid token sent!");
|
||||
}
|
||||
|
||||
const worlds = Object.fromEntries(socketManager.getWorlds().entries());
|
||||
|
||||
return res
|
||||
.writeStatus("200 OK")
|
||||
.writeHeader("Content-Type", "application/json")
|
||||
.end(
|
||||
stringify(worlds, (key: unknown, value: unknown) => {
|
||||
if (value instanceof Map) {
|
||||
const obj: any = {}; // eslint-disable-line @typescript-eslint/no-explicit-any
|
||||
for (const [mapKey, mapValue] of value.entries()) {
|
||||
obj[mapKey] = mapValue;
|
||||
}
|
||||
return obj;
|
||||
} else if (value instanceof Set) {
|
||||
const obj: Array<unknown> = [];
|
||||
for (const [setKey, setValue] of value.entries()) {
|
||||
obj.push(setValue);
|
||||
}
|
||||
return obj;
|
||||
} else {
|
||||
return value;
|
||||
return res.json(
|
||||
stringify(worlds, (key: unknown, value: unknown) => {
|
||||
if (value instanceof Map) {
|
||||
const obj: any = {}; // eslint-disable-line @typescript-eslint/no-explicit-any
|
||||
for (const [mapKey, mapValue] of value.entries()) {
|
||||
obj[mapKey] = mapValue;
|
||||
}
|
||||
})
|
||||
);
|
||||
return obj;
|
||||
} else if (value instanceof Set) {
|
||||
const obj: Array<unknown> = [];
|
||||
for (const [setKey, setValue] of value.entries()) {
|
||||
obj.push(setValue);
|
||||
}
|
||||
return obj;
|
||||
} else {
|
||||
return value;
|
||||
}
|
||||
})
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { CharacterLayer, ExSocketInterface } from "../Model/Websocket/ExSocketInterface"; //TODO fix import by "_Model/.."
|
||||
import { GameRoomPolicyTypes, PusherRoom } from "../Model/PusherRoom";
|
||||
import { ExSocketInterface } from "../Model/Websocket/ExSocketInterface"; //TODO fix import by "_Model/.."
|
||||
import { GameRoomPolicyTypes } from "../Model/PusherRoom";
|
||||
import { PointInterface } from "../Model/Websocket/PointInterface";
|
||||
import {
|
||||
SetPlayerDetailsMessage,
|
||||
@@ -23,7 +23,6 @@ import {
|
||||
VariableMessage,
|
||||
} from "../Messages/generated/messages_pb";
|
||||
import { UserMovesMessage } from "../Messages/generated/messages_pb";
|
||||
import { TemplatedApp } from "uWebSockets.js";
|
||||
import { parse } from "query-string";
|
||||
import { AdminSocketTokenData, jwtTokenManager, tokenInvalidException } from "../Services/JWTTokenManager";
|
||||
import { adminApi, FetchMemberDataByUuidResponse } from "../Services/AdminApi";
|
||||
@@ -32,15 +31,51 @@ import { emitInBatch } from "../Services/IoSocketHelpers";
|
||||
import { ADMIN_API_URL, ADMIN_SOCKETS_TOKEN, DISABLE_ANONYMOUS, SOCKET_IDLE_TIMER } from "../Enum/EnvironmentVariable";
|
||||
import { Zone } from "_Model/Zone";
|
||||
import { ExAdminSocketInterface } from "_Model/Websocket/ExAdminSocketInterface";
|
||||
import { CharacterTexture } from "../Messages/JsonMessages/CharacterTexture";
|
||||
import { isAdminMessageInterface } from "../Model/Websocket/Admin/AdminMessages";
|
||||
import Axios from "axios";
|
||||
import { InvalidTokenError } from "../Controller/InvalidTokenError";
|
||||
import HyperExpress from "hyper-express";
|
||||
import { localWokaService } from "../Services/LocalWokaService";
|
||||
import { WebSocket } from "uWebSockets.js";
|
||||
import { WokaDetail } from "../Enum/PlayerTextures";
|
||||
|
||||
/**
|
||||
* The object passed between the "open" and the "upgrade" methods when opening a websocket
|
||||
*/
|
||||
interface UpgradeData {
|
||||
// Data passed here is accessible on the "websocket" socket object.
|
||||
rejected: false;
|
||||
token: string;
|
||||
userUuid: string;
|
||||
IPAddress: string;
|
||||
roomId: string;
|
||||
name: string;
|
||||
companion: CompanionMessage | undefined;
|
||||
characterLayers: WokaDetail[];
|
||||
messages: unknown[];
|
||||
tags: string[];
|
||||
visitCardUrl: string | null;
|
||||
userRoomToken: string | undefined;
|
||||
position: PointInterface;
|
||||
viewport: {
|
||||
top: number;
|
||||
right: number;
|
||||
bottom: number;
|
||||
left: number;
|
||||
};
|
||||
}
|
||||
|
||||
interface UpgradeFailedData {
|
||||
rejected: true;
|
||||
reason: "tokenInvalid" | "textureInvalid" | null;
|
||||
message: string;
|
||||
roomId: string;
|
||||
}
|
||||
|
||||
export class IoSocketController {
|
||||
private nextUserId: number = 1;
|
||||
|
||||
constructor(private readonly app: TemplatedApp) {
|
||||
constructor(private readonly app: HyperExpress.compressors.TemplatedApp) {
|
||||
this.ioConnection();
|
||||
if (ADMIN_SOCKETS_TOKEN) {
|
||||
this.adminRoomSocket();
|
||||
@@ -244,7 +279,7 @@ export class IoSocketController {
|
||||
let memberVisitCardUrl: string | null = null;
|
||||
let memberMessages: unknown;
|
||||
let memberUserRoomToken: string | undefined;
|
||||
let memberTextures: CharacterTexture[] = [];
|
||||
let memberTextures: WokaDetail[] = [];
|
||||
const room = await socketManager.getOrCreateRoom(roomId);
|
||||
let userData: FetchMemberDataByUuidResponse = {
|
||||
email: userIdentifier,
|
||||
@@ -256,10 +291,18 @@ export class IoSocketController {
|
||||
anonymous: true,
|
||||
userRoomToken: undefined,
|
||||
};
|
||||
|
||||
let characterLayerObjs: WokaDetail[];
|
||||
|
||||
if (ADMIN_API_URL) {
|
||||
try {
|
||||
try {
|
||||
userData = await adminApi.fetchMemberDataByUuid(userIdentifier, roomId, IPAddress);
|
||||
userData = await adminApi.fetchMemberDataByUuid(
|
||||
userIdentifier,
|
||||
roomId,
|
||||
IPAddress,
|
||||
characterLayers
|
||||
);
|
||||
} catch (err) {
|
||||
if (Axios.isAxiosError(err)) {
|
||||
if (err?.response?.status == 404) {
|
||||
@@ -308,6 +351,8 @@ export class IoSocketController {
|
||||
) {
|
||||
throw new Error("Use the login URL to connect");
|
||||
}
|
||||
|
||||
characterLayerObjs = memberTextures;
|
||||
} catch (e) {
|
||||
console.log(
|
||||
"access not granted for user " +
|
||||
@@ -318,11 +363,31 @@ export class IoSocketController {
|
||||
console.error(e);
|
||||
throw new Error("User cannot access this world");
|
||||
}
|
||||
} else {
|
||||
const fetchedTextures = await localWokaService.fetchWokaDetails(characterLayers);
|
||||
if (fetchedTextures === undefined) {
|
||||
// The textures we want to use do not exist!
|
||||
// We need to go in error.
|
||||
res.upgrade(
|
||||
{
|
||||
rejected: true,
|
||||
reason: "textureInvalid",
|
||||
message: "",
|
||||
roomId,
|
||||
} as UpgradeFailedData,
|
||||
websocketKey,
|
||||
websocketProtocol,
|
||||
websocketExtensions,
|
||||
context
|
||||
);
|
||||
return;
|
||||
}
|
||||
characterLayerObjs = fetchedTextures;
|
||||
}
|
||||
|
||||
// Generate characterLayers objects from characterLayers string[]
|
||||
const characterLayerObjs: CharacterLayer[] =
|
||||
SocketManager.mergeCharacterLayersAndCustomTextures(characterLayers, memberTextures);
|
||||
/*const characterLayerObjs: CharacterLayer[] =
|
||||
SocketManager.mergeCharacterLayersAndCustomTextures(characterLayers, memberTextures);*/
|
||||
|
||||
if (upgradeAborted.aborted) {
|
||||
console.log("Ouch! Client disconnected before we could upgrade it!");
|
||||
@@ -334,7 +399,7 @@ export class IoSocketController {
|
||||
res.upgrade(
|
||||
{
|
||||
// Data passed here is accessible on the "websocket" socket object.
|
||||
url,
|
||||
rejected: false,
|
||||
token,
|
||||
userUuid: userData.userUuid,
|
||||
IPAddress,
|
||||
@@ -346,7 +411,6 @@ export class IoSocketController {
|
||||
tags: memberTags,
|
||||
visitCardUrl: memberVisitCardUrl,
|
||||
userRoomToken: memberUserRoomToken,
|
||||
textures: memberTextures,
|
||||
position: {
|
||||
x: x,
|
||||
y: y,
|
||||
@@ -359,7 +423,7 @@ export class IoSocketController {
|
||||
bottom,
|
||||
left,
|
||||
},
|
||||
},
|
||||
} as UpgradeData,
|
||||
/* Spell these correctly */
|
||||
websocketKey,
|
||||
websocketProtocol,
|
||||
@@ -374,7 +438,7 @@ export class IoSocketController {
|
||||
reason: e instanceof InvalidTokenError ? tokenInvalidException : null,
|
||||
message: e.message,
|
||||
roomId,
|
||||
},
|
||||
} as UpgradeFailedData,
|
||||
websocketKey,
|
||||
websocketProtocol,
|
||||
websocketExtensions,
|
||||
@@ -387,7 +451,7 @@ export class IoSocketController {
|
||||
reason: null,
|
||||
message: "500 Internal Server Error",
|
||||
roomId,
|
||||
},
|
||||
} as UpgradeFailedData,
|
||||
websocketKey,
|
||||
websocketProtocol,
|
||||
websocketExtensions,
|
||||
@@ -398,20 +462,23 @@ export class IoSocketController {
|
||||
})();
|
||||
},
|
||||
/* Handlers */
|
||||
open: (ws) => {
|
||||
open: (_ws: WebSocket) => {
|
||||
const ws = _ws as WebSocket & (UpgradeData | UpgradeFailedData);
|
||||
if (ws.rejected === true) {
|
||||
// If there is a room in the error, let's check if we need to clean it.
|
||||
if (ws.roomId) {
|
||||
socketManager.deleteRoomIfEmptyFromId(ws.roomId as string);
|
||||
socketManager.deleteRoomIfEmptyFromId(ws.roomId);
|
||||
}
|
||||
|
||||
//FIX ME to use status code
|
||||
if (ws.reason === tokenInvalidException) {
|
||||
socketManager.emitTokenExpiredMessage(ws);
|
||||
} else if (ws.reason === "textureInvalid") {
|
||||
socketManager.emitInvalidTextureMessage(ws);
|
||||
} else if (ws.message === "World is full") {
|
||||
socketManager.emitWorldFullMessage(ws);
|
||||
} else {
|
||||
socketManager.emitConnexionErrorMessage(ws, ws.message as string);
|
||||
socketManager.emitConnexionErrorMessage(ws, ws.message);
|
||||
}
|
||||
setTimeout(() => ws.close(), 0);
|
||||
return;
|
||||
@@ -535,7 +602,6 @@ export class IoSocketController {
|
||||
client.name = ws.name;
|
||||
client.tags = ws.tags;
|
||||
client.visitCardUrl = ws.visitCardUrl;
|
||||
client.textures = ws.textures;
|
||||
client.characterLayers = ws.characterLayers;
|
||||
client.companion = ws.companion;
|
||||
client.roomId = ws.roomId;
|
||||
|
||||
@@ -1,41 +1,101 @@
|
||||
import { HttpRequest, HttpResponse, TemplatedApp } from "uWebSockets.js";
|
||||
import { BaseController } from "./BaseController";
|
||||
import { parse } from "query-string";
|
||||
import { adminApi } from "../Services/AdminApi";
|
||||
import { ADMIN_API_URL, DISABLE_ANONYMOUS, FRONT_URL } from "../Enum/EnvironmentVariable";
|
||||
import { ADMIN_API_URL, DISABLE_ANONYMOUS } from "../Enum/EnvironmentVariable";
|
||||
import { GameRoomPolicyTypes } from "../Model/PusherRoom";
|
||||
import { isMapDetailsData, MapDetailsData } from "../Messages/JsonMessages/MapDetailsData";
|
||||
import { socketManager } from "../Services/SocketManager";
|
||||
import { AuthTokenData, jwtTokenManager } from "../Services/JWTTokenManager";
|
||||
import { v4 } from "uuid";
|
||||
import { InvalidTokenError } from "./InvalidTokenError";
|
||||
import { parse } from "query-string";
|
||||
import { BaseHttpController } from "./BaseHttpController";
|
||||
|
||||
export class MapController extends BaseController {
|
||||
constructor(private App: TemplatedApp) {
|
||||
super();
|
||||
this.App = App;
|
||||
this.getMapUrl();
|
||||
}
|
||||
|
||||
export class MapController extends BaseHttpController {
|
||||
// Returns a map mapping map name to file name of the map
|
||||
getMapUrl() {
|
||||
this.App.options("/map", (res: HttpResponse, req: HttpRequest) => {
|
||||
this.addCorsHeaders(res);
|
||||
res.end();
|
||||
});
|
||||
|
||||
this.App.get("/map", (res: HttpResponse, req: HttpRequest) => {
|
||||
res.onAborted(() => {
|
||||
console.warn("/map request was aborted");
|
||||
});
|
||||
|
||||
const query = parse(req.getQuery());
|
||||
|
||||
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
|
||||
* 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") {
|
||||
console.error("Expected playUri parameter in /map endpoint");
|
||||
res.writeStatus("400 Bad request");
|
||||
this.addCorsHeaders(res);
|
||||
res.end("Expected playUri parameter");
|
||||
res.status(400);
|
||||
res.send("Expected playUri parameter");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -45,28 +105,22 @@ export class MapController extends BaseController {
|
||||
|
||||
const match = /\/_\/[^/]+\/(.+)/.exec(roomUrl.pathname);
|
||||
if (!match) {
|
||||
res.writeStatus("404 Not Found");
|
||||
this.addCorsHeaders(res);
|
||||
res.end(JSON.stringify({}));
|
||||
res.status(404);
|
||||
res.json({});
|
||||
return;
|
||||
}
|
||||
|
||||
const mapUrl = roomUrl.protocol + "//" + match[1];
|
||||
|
||||
res.writeStatus("200 OK");
|
||||
this.addCorsHeaders(res);
|
||||
res.end(
|
||||
JSON.stringify({
|
||||
mapUrl,
|
||||
policy_type: GameRoomPolicyTypes.ANONYMOUS_POLICY,
|
||||
roomSlug: null, // Deprecated
|
||||
group: null,
|
||||
tags: [],
|
||||
textures: [],
|
||||
contactPage: null,
|
||||
authenticationMandatory: DISABLE_ANONYMOUS,
|
||||
} as MapDetailsData)
|
||||
);
|
||||
res.json({
|
||||
mapUrl,
|
||||
policy_type: GameRoomPolicyTypes.ANONYMOUS_POLICY,
|
||||
roomSlug: null, // Deprecated
|
||||
group: null,
|
||||
tags: [],
|
||||
contactPage: null,
|
||||
authenticationMandatory: DISABLE_ANONYMOUS,
|
||||
} as MapDetailsData);
|
||||
|
||||
return;
|
||||
}
|
||||
@@ -88,12 +142,12 @@ export class MapController extends BaseController {
|
||||
} catch (e) {
|
||||
if (e instanceof InvalidTokenError) {
|
||||
// The token was not good, redirect user on login page
|
||||
res.writeStatus("401 Unauthorized");
|
||||
res.writeHeader("Access-Control-Allow-Origin", FRONT_URL);
|
||||
res.end("Token decrypted error");
|
||||
res.status(401);
|
||||
res.send("Token decrypted error");
|
||||
return;
|
||||
} else {
|
||||
return this.errorToResponse(e, res);
|
||||
this.castErrorToResponse(e, res);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -104,11 +158,10 @@ export class MapController extends BaseController {
|
||||
mapDetails.authenticationMandatory = true;
|
||||
}
|
||||
|
||||
res.writeStatus("200 OK");
|
||||
this.addCorsHeaders(res);
|
||||
res.end(JSON.stringify(mapDetails));
|
||||
res.json(mapDetails);
|
||||
return;
|
||||
} catch (e) {
|
||||
this.errorToResponse(e, res);
|
||||
this.castErrorToResponse(e, res);
|
||||
}
|
||||
})();
|
||||
});
|
||||
|
||||
@@ -1,26 +1,13 @@
|
||||
import { BaseController } from "./BaseController";
|
||||
import { HttpRequest, HttpResponse, TemplatedApp } from "uWebSockets.js";
|
||||
import { parse } from "query-string";
|
||||
import { openIDClient } from "../Services/OpenIDClient";
|
||||
import { AuthTokenData, jwtTokenManager } from "../Services/JWTTokenManager";
|
||||
import { adminApi } from "../Services/AdminApi";
|
||||
import { OPID_CLIENT_ISSUER } from "../Enum/EnvironmentVariable";
|
||||
import { IntrospectionResponse } from "openid-client";
|
||||
import { BaseHttpController } from "./BaseHttpController";
|
||||
|
||||
export class OpenIdProfileController extends BaseController {
|
||||
constructor(private App: TemplatedApp) {
|
||||
super();
|
||||
this.profileOpenId();
|
||||
}
|
||||
|
||||
profileOpenId() {
|
||||
export class OpenIdProfileController extends BaseHttpController {
|
||||
routes() {
|
||||
//eslint-disable-next-line @typescript-eslint/no-misused-promises
|
||||
this.App.get("/profile", async (res: HttpResponse, req: HttpRequest) => {
|
||||
res.onAborted(() => {
|
||||
console.warn("/message request was aborted");
|
||||
});
|
||||
|
||||
const { accessToken } = parse(req.getQuery());
|
||||
this.app.get("/profile", async (req, res) => {
|
||||
const { accessToken } = parse(req.path_query);
|
||||
if (!accessToken) {
|
||||
throw Error("Access token expected cannot to be check on Hydra");
|
||||
}
|
||||
@@ -29,16 +16,17 @@ export class OpenIdProfileController extends BaseController {
|
||||
if (!resCheckTokenAuth.email) {
|
||||
throw new Error("Email was not found");
|
||||
}
|
||||
res.end(
|
||||
res.send(
|
||||
this.buildHtml(
|
||||
OPID_CLIENT_ISSUER,
|
||||
resCheckTokenAuth.email as string,
|
||||
resCheckTokenAuth.picture as string | undefined
|
||||
)
|
||||
);
|
||||
return;
|
||||
} catch (error) {
|
||||
console.error("profileCallback => ERROR", error);
|
||||
this.errorToResponse(error, res);
|
||||
this.castErrorToResponse(error, res);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -64,13 +52,13 @@ export class OpenIdProfileController extends BaseController {
|
||||
<body>
|
||||
<div class="container">
|
||||
<section>
|
||||
<img src="${pictureUrl ? pictureUrl : "/images/profile"}">
|
||||
<img src="${pictureUrl ? pictureUrl : "/images/profile"}">
|
||||
</section>
|
||||
<section>
|
||||
Profile validated by domain: <span style="font-weight: bold">${domain}</span>
|
||||
</section>
|
||||
</section>
|
||||
<section>
|
||||
Your email: <span style="font-weight: bold">${email}</span>
|
||||
Your email: <span style="font-weight: bold">${email}</span>
|
||||
</section>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
@@ -1,18 +1,23 @@
|
||||
import { App } from "../Server/sifrr.server";
|
||||
import { HttpRequest, HttpResponse } from "uWebSockets.js";
|
||||
import { register, collectDefaultMetrics } from "prom-client";
|
||||
import { Server } from "hyper-express";
|
||||
import { BaseHttpController } from "./BaseHttpController";
|
||||
import Request from "hyper-express/types/components/http/Request";
|
||||
import Response from "hyper-express/types/components/http/Response";
|
||||
|
||||
export class PrometheusController {
|
||||
constructor(private App: App) {
|
||||
export class PrometheusController extends BaseHttpController {
|
||||
constructor(app: Server) {
|
||||
super(app);
|
||||
collectDefaultMetrics({
|
||||
gcDurationBuckets: [0.001, 0.01, 0.1, 1, 2, 5], // These are the default buckets.
|
||||
});
|
||||
|
||||
this.App.get("/metrics", this.metrics.bind(this));
|
||||
}
|
||||
|
||||
private metrics(res: HttpResponse, req: HttpRequest): void {
|
||||
res.writeHeader("Content-Type", register.contentType);
|
||||
routes() {
|
||||
this.app.get("/metrics", this.metrics.bind(this));
|
||||
}
|
||||
|
||||
private metrics(req: Request, res: Response): void {
|
||||
res.setHeader("Content-Type", register.contentType);
|
||||
res.end(register.metrics());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
import { BaseHttpController } from "./BaseHttpController";
|
||||
import * as fs from "fs";
|
||||
|
||||
export class SwaggerController extends BaseHttpController {
|
||||
routes() {
|
||||
this.app.get("/openapi", (req, res) => {
|
||||
// Let's load the module dynamically (it may not exist in prod because part of the -dev packages)
|
||||
const swaggerJsdoc = require("swagger-jsdoc");
|
||||
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
|
||||
// @ts-ignore
|
||||
const LiveDirectory = require("live-directory");
|
||||
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);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
import { BaseHttpController } from "./BaseHttpController";
|
||||
import { parse } from "query-string";
|
||||
import { wokaService } from "../Services/WokaService";
|
||||
import { jwtTokenManager } from "../Services/JWTTokenManager";
|
||||
|
||||
export class WokaListController extends BaseHttpController {
|
||||
routes() {
|
||||
this.app.options("/woka/list", {}, (req, res) => {
|
||||
res.status(200).send("");
|
||||
return;
|
||||
});
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
||||
this.app.get("/woka/list", {}, async (req, res) => {
|
||||
const token = req.header("Authorization");
|
||||
|
||||
if (!token) {
|
||||
res.status(401).send("Undefined authorization header");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const jwtData = jwtTokenManager.verifyJWTToken(token);
|
||||
// Let's set the "uuid" param
|
||||
req.params["uuid"] = jwtData.identifier;
|
||||
} catch (e) {
|
||||
console.error("Connection refused for token: " + token, e);
|
||||
res.status(401).send("Invalid token sent");
|
||||
return;
|
||||
}
|
||||
|
||||
let { roomUrl } = parse(req.path_query);
|
||||
|
||||
if (typeof roomUrl !== "string") {
|
||||
return res.status(400).send("missing roomUrl URL parameter");
|
||||
}
|
||||
|
||||
roomUrl = decodeURIComponent(roomUrl);
|
||||
const wokaList = await wokaService.getWokaList(roomUrl, req.params["uuid"]);
|
||||
|
||||
if (!wokaList) {
|
||||
return res.status(500).send("Error on getting woka list");
|
||||
}
|
||||
|
||||
return res.status(200).json(wokaList);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -18,8 +18,14 @@ export const OPID_CLIENT_SECRET = process.env.OPID_CLIENT_SECRET || "";
|
||||
export const OPID_CLIENT_ISSUER = process.env.OPID_CLIENT_ISSUER || "";
|
||||
export const OPID_CLIENT_REDIRECT_URL = process.env.OPID_CLIENT_REDIRECT_URL || FRONT_URL + "/jwt";
|
||||
export const OPID_PROFILE_SCREEN_PROVIDER = process.env.OPID_PROFILE_SCREEN_PROVIDER || ADMIN_URL + "/profile";
|
||||
export const OPID_SCOPE = process.env.OPID_SCOPE || "openid email";
|
||||
export const OPID_USERNAME_CLAIM = process.env.OPID_USERNAME_CLAIM || "username";
|
||||
export const OPID_LOCALE_CLAIM = process.env.OPID_LOCALE_CLAIM || "locale";
|
||||
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,
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
import * as tg from "generic-type-guard";
|
||||
import { z } from "zod";
|
||||
|
||||
//The list of all the player textures, both the default models and the partial textures used for customization
|
||||
|
||||
const wokaTexture = z.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
url: z.string(),
|
||||
tags: z.array(z.string()).optional(),
|
||||
tintable: z.boolean().optional(),
|
||||
});
|
||||
|
||||
export type WokaTexture = z.infer<typeof wokaTexture>;
|
||||
|
||||
const wokaTextureCollection = z.object({
|
||||
name: z.string(),
|
||||
textures: z.array(wokaTexture),
|
||||
});
|
||||
|
||||
export type WokaTextureCollection = z.infer<typeof wokaTextureCollection>;
|
||||
|
||||
const wokaPartType = z.object({
|
||||
collections: z.array(wokaTextureCollection),
|
||||
required: z.boolean().optional(),
|
||||
});
|
||||
|
||||
export type WokaPartType = z.infer<typeof wokaPartType>;
|
||||
|
||||
export const wokaList = z.record(wokaPartType);
|
||||
|
||||
export type WokaList = z.infer<typeof wokaList>;
|
||||
|
||||
export const wokaPartNames = ["woka", "body", "eyes", "hair", "clothes", "hat", "accessory"];
|
||||
|
||||
export const isWokaDetail = new tg.IsInterface()
|
||||
.withProperties({
|
||||
id: tg.isString,
|
||||
})
|
||||
.withOptionalProperties({
|
||||
url: tg.isString,
|
||||
layer: tg.isString,
|
||||
})
|
||||
.get();
|
||||
|
||||
export type WokaDetail = tg.GuardedType<typeof isWokaDetail>;
|
||||
|
||||
export type WokaDetailsResult = WokaDetail[];
|
||||
@@ -0,0 +1,22 @@
|
||||
import Request from "hyper-express/types/components/http/Request";
|
||||
import Response from "hyper-express/types/components/http/Response";
|
||||
import { MiddlewareNext, MiddlewarePromise } from "hyper-express/types/components/router/Router";
|
||||
import { ADMIN_API_TOKEN } from "../Enum/EnvironmentVariable";
|
||||
|
||||
export function adminToken(req: Request, res: Response, next?: MiddlewareNext): MiddlewarePromise {
|
||||
const token = req.header("admin-token");
|
||||
|
||||
if (ADMIN_API_TOKEN === "") {
|
||||
res.status(401).end("No token configured!");
|
||||
return;
|
||||
}
|
||||
if (token !== ADMIN_API_TOKEN) {
|
||||
console.error("Admin access refused for token: " + token);
|
||||
res.status(401).end("Incorrect token");
|
||||
return;
|
||||
}
|
||||
|
||||
if (next) {
|
||||
next();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import Request from "hyper-express/types/components/http/Request";
|
||||
import Response from "hyper-express/types/components/http/Response";
|
||||
import { MiddlewareNext, MiddlewarePromise } from "hyper-express/types/components/router/Router";
|
||||
import { FRONT_URL } from "../Enum/EnvironmentVariable";
|
||||
|
||||
export function cors(req: Request, res: Response, next?: MiddlewareNext): MiddlewarePromise {
|
||||
res.setHeader(
|
||||
"access-control-allow-headers",
|
||||
"Origin, X-Requested-With, Content-Type, Accept, Authorization, Pragma, Cache-Control"
|
||||
);
|
||||
res.setHeader("access-control-allow-methods", "GET, POST, OPTIONS, PUT, PATCH, DELETE");
|
||||
res.setHeader("access-control-allow-origin", FRONT_URL);
|
||||
res.setHeader("access-control-allow-credentials", "true");
|
||||
|
||||
if (next) {
|
||||
next();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import Request from "hyper-express/types/components/http/Request";
|
||||
import Response from "hyper-express/types/components/http/Response";
|
||||
import { MiddlewareNext, MiddlewarePromise } from "hyper-express/types/components/router/Router";
|
||||
|
||||
export function hasToken(req: Request, res: Response, next?: MiddlewareNext): MiddlewarePromise {
|
||||
const authorizationHeader = req.header("Authorization");
|
||||
|
||||
if (!authorizationHeader) {
|
||||
res.status(401).send("Undefined authorization header");
|
||||
return;
|
||||
}
|
||||
|
||||
if (next) {
|
||||
next();
|
||||
}
|
||||
}
|
||||
@@ -9,13 +9,13 @@ import {
|
||||
ServerToClientMessage,
|
||||
SubMessage,
|
||||
} from "../../Messages/generated/messages_pb";
|
||||
import { WebSocket } from "uWebSockets.js";
|
||||
import { compressors } from "hyper-express";
|
||||
import { ClientDuplexStream } from "grpc";
|
||||
import { Zone } from "_Model/Zone";
|
||||
|
||||
export type AdminConnection = ClientDuplexStream<AdminPusherToBackMessage, ServerToAdminClientMessage>;
|
||||
|
||||
export interface ExAdminSocketInterface extends WebSocket {
|
||||
export interface ExAdminSocketInterface extends compressors.WebSocket {
|
||||
adminConnection: AdminConnection;
|
||||
disconnecting: boolean;
|
||||
}
|
||||
|
||||
@@ -8,26 +8,21 @@ import {
|
||||
ServerToClientMessage,
|
||||
SubMessage,
|
||||
} from "../../Messages/generated/messages_pb";
|
||||
import { WebSocket } from "uWebSockets.js";
|
||||
import { ClientDuplexStream } from "grpc";
|
||||
import { Zone } from "_Model/Zone";
|
||||
import { CharacterTexture } from "../../Messages/JsonMessages/CharacterTexture";
|
||||
import { compressors } from "hyper-express";
|
||||
import { WokaDetail } from "_Enum/PlayerTextures";
|
||||
|
||||
export type BackConnection = ClientDuplexStream<PusherToBackMessage, ServerToClientMessage>;
|
||||
|
||||
export interface CharacterLayer {
|
||||
name: string;
|
||||
url: string | undefined;
|
||||
}
|
||||
|
||||
export interface ExSocketInterface extends WebSocket, Identificable {
|
||||
export interface ExSocketInterface extends compressors.WebSocket, Identificable {
|
||||
token: string;
|
||||
roomId: string;
|
||||
//userId: number; // A temporary (autoincremented) identifier for this user
|
||||
userUuid: string; // A unique identifier for this user
|
||||
IPAddress: string; // IP address
|
||||
name: string;
|
||||
characterLayers: CharacterLayer[];
|
||||
characterLayers: WokaDetail[];
|
||||
position: PointInterface;
|
||||
viewport: ViewportInterface;
|
||||
companion?: CompanionMessage;
|
||||
@@ -41,7 +36,6 @@ export interface ExSocketInterface extends WebSocket, Identificable {
|
||||
messages: unknown;
|
||||
tags: string[];
|
||||
visitCardUrl: string | null;
|
||||
textures: CharacterTexture[];
|
||||
backConnection: BackConnection;
|
||||
listenedZones: Set<Zone>;
|
||||
userRoomToken: string | undefined;
|
||||
|
||||
@@ -5,10 +5,11 @@ import {
|
||||
PointMessage,
|
||||
PositionMessage,
|
||||
} from "../../Messages/generated/messages_pb";
|
||||
import { CharacterLayer, ExSocketInterface } from "_Model/Websocket/ExSocketInterface";
|
||||
import { ExSocketInterface } from "_Model/Websocket/ExSocketInterface";
|
||||
import Direction = PositionMessage.Direction;
|
||||
import { ItemEventMessageInterface } from "_Model/Websocket/ItemEventMessage";
|
||||
import { PositionInterface } from "_Model/PositionInterface";
|
||||
import { WokaDetail } from "_Enum/PlayerTextures";
|
||||
|
||||
export class ProtobufUtils {
|
||||
public static toPositionMessage(point: PointInterface): PositionMessage {
|
||||
@@ -94,13 +95,16 @@ export class ProtobufUtils {
|
||||
return itemEventMessage;
|
||||
}
|
||||
|
||||
public static toCharacterLayerMessages(characterLayers: CharacterLayer[]): CharacterLayerMessage[] {
|
||||
public static toCharacterLayerMessages(characterLayers: WokaDetail[]): CharacterLayerMessage[] {
|
||||
return characterLayers.map(function (characterLayer): CharacterLayerMessage {
|
||||
const message = new CharacterLayerMessage();
|
||||
message.setName(characterLayer.name);
|
||||
message.setName(characterLayer.id);
|
||||
if (characterLayer.url) {
|
||||
message.setUrl(characterLayer.url);
|
||||
}
|
||||
if (characterLayer.layer) {
|
||||
message.setLayer(characterLayer.layer);
|
||||
}
|
||||
return message;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
import { App as _App, AppOptions } from "uWebSockets.js";
|
||||
import BaseApp from "./baseapp";
|
||||
import { extend } from "./utils";
|
||||
import { UwsApp } from "./types";
|
||||
|
||||
class App extends (<UwsApp>_App) {
|
||||
constructor(options: AppOptions = {}) {
|
||||
super(options); // eslint-disable-line constructor-super
|
||||
extend(this, new BaseApp());
|
||||
}
|
||||
}
|
||||
|
||||
export default App;
|
||||
@@ -1,109 +0,0 @@
|
||||
import { Readable } from "stream";
|
||||
import { us_listen_socket_close, TemplatedApp, HttpResponse, HttpRequest } from "uWebSockets.js";
|
||||
|
||||
import formData from "./formdata";
|
||||
import { stob } from "./utils";
|
||||
import { Handler } from "./types";
|
||||
import { join } from "path";
|
||||
|
||||
const contTypes = ["application/x-www-form-urlencoded", "multipart/form-data"];
|
||||
const noOp = () => true;
|
||||
|
||||
const handleBody = (res: HttpResponse, req: HttpRequest) => {
|
||||
const contType = req.getHeader("content-type");
|
||||
|
||||
res.bodyStream = function () {
|
||||
const stream = new Readable();
|
||||
stream._read = noOp; // eslint-disable-line @typescript-eslint/unbound-method
|
||||
|
||||
this.onData((ab: ArrayBuffer, isLast: boolean) => {
|
||||
// uint and then slicing is bit faster than slice and then uint
|
||||
stream.push(new Uint8Array(ab.slice((ab as any).byteOffset, ab.byteLength))); // eslint-disable-line @typescript-eslint/no-explicit-any
|
||||
if (isLast) {
|
||||
stream.push(null);
|
||||
}
|
||||
});
|
||||
|
||||
return stream;
|
||||
};
|
||||
|
||||
res.body = () => stob(res.bodyStream());
|
||||
|
||||
if (contType.includes("application/json")) res.json = async () => JSON.parse(await res.body());
|
||||
if (contTypes.map((t) => contType.includes(t)).includes(true)) res.formData = formData.bind(res, contType);
|
||||
};
|
||||
|
||||
class BaseApp {
|
||||
_sockets = new Map();
|
||||
ws!: TemplatedApp["ws"];
|
||||
get!: TemplatedApp["get"];
|
||||
_post!: TemplatedApp["post"];
|
||||
_put!: TemplatedApp["put"];
|
||||
_patch!: TemplatedApp["patch"];
|
||||
_listen!: TemplatedApp["listen"];
|
||||
|
||||
post(pattern: string, handler: Handler) {
|
||||
if (typeof handler !== "function") throw Error(`handler should be a function, given ${typeof handler}.`);
|
||||
this._post(pattern, (res, req) => {
|
||||
handleBody(res, req);
|
||||
handler(res, req);
|
||||
});
|
||||
return this;
|
||||
}
|
||||
|
||||
put(pattern: string, handler: Handler) {
|
||||
if (typeof handler !== "function") throw Error(`handler should be a function, given ${typeof handler}.`);
|
||||
this._put(pattern, (res, req) => {
|
||||
handleBody(res, req);
|
||||
|
||||
handler(res, req);
|
||||
});
|
||||
return this;
|
||||
}
|
||||
|
||||
patch(pattern: string, handler: Handler) {
|
||||
if (typeof handler !== "function") throw Error(`handler should be a function, given ${typeof handler}.`);
|
||||
this._patch(pattern, (res, req) => {
|
||||
handleBody(res, req);
|
||||
|
||||
handler(res, req);
|
||||
});
|
||||
return this;
|
||||
}
|
||||
|
||||
listen(h: string | number, p: Function | number = noOp, cb?: Function) {
|
||||
if (typeof p === "number" && typeof h === "string") {
|
||||
this._listen(h, p, (socket) => {
|
||||
this._sockets.set(p, socket);
|
||||
if (cb === undefined) {
|
||||
throw new Error("cb undefined");
|
||||
}
|
||||
cb(socket);
|
||||
});
|
||||
} else if (typeof h === "number" && typeof p === "function") {
|
||||
this._listen(h, (socket) => {
|
||||
this._sockets.set(h, socket);
|
||||
p(socket);
|
||||
});
|
||||
} else {
|
||||
throw Error("Argument types: (host: string, port: number, cb?: Function) | (port: number, cb?: Function)");
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
close(port: null | number = null) {
|
||||
if (port) {
|
||||
this._sockets.has(port) && us_listen_socket_close(this._sockets.get(port));
|
||||
this._sockets.delete(port);
|
||||
} else {
|
||||
this._sockets.forEach((app) => {
|
||||
us_listen_socket_close(app);
|
||||
});
|
||||
this._sockets.clear();
|
||||
}
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
export default BaseApp;
|
||||
@@ -1,99 +0,0 @@
|
||||
import { createWriteStream } from "fs";
|
||||
import { join, dirname } from "path";
|
||||
import Busboy from "busboy";
|
||||
import mkdirp from "mkdirp";
|
||||
|
||||
function formData(
|
||||
contType: string,
|
||||
options: busboy.BusboyConfig & {
|
||||
abortOnLimit?: boolean;
|
||||
tmpDir?: string;
|
||||
onFile?: (
|
||||
fieldname: string,
|
||||
file: NodeJS.ReadableStream,
|
||||
filename: string,
|
||||
encoding: string,
|
||||
mimetype: string
|
||||
) => string;
|
||||
onField?: (fieldname: string, value: any) => void; // eslint-disable-line @typescript-eslint/no-explicit-any
|
||||
filename?: (oldName: string) => string;
|
||||
} = {}
|
||||
) {
|
||||
console.log("Enter form data");
|
||||
options.headers = {
|
||||
"content-type": contType,
|
||||
};
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const busb = new Busboy(options);
|
||||
const ret = {};
|
||||
|
||||
this.bodyStream().pipe(busb);
|
||||
|
||||
busb.on("limit", () => {
|
||||
if (options.abortOnLimit) {
|
||||
reject(Error("limit"));
|
||||
}
|
||||
});
|
||||
|
||||
busb.on("file", function (fieldname, file, filename, encoding, mimetype) {
|
||||
const value: { filePath: string | undefined; filename: string; encoding: string; mimetype: string } = {
|
||||
filename,
|
||||
encoding,
|
||||
mimetype,
|
||||
filePath: undefined,
|
||||
};
|
||||
|
||||
if (typeof options.tmpDir === "string") {
|
||||
if (typeof options.filename === "function") filename = options.filename(filename);
|
||||
const fileToSave = join(options.tmpDir, filename);
|
||||
mkdirp(dirname(fileToSave));
|
||||
|
||||
file.pipe(createWriteStream(fileToSave));
|
||||
value.filePath = fileToSave;
|
||||
}
|
||||
if (typeof options.onFile === "function") {
|
||||
value.filePath = options.onFile(fieldname, file, filename, encoding, mimetype) || value.filePath;
|
||||
}
|
||||
|
||||
setRetValue(ret, fieldname, value);
|
||||
});
|
||||
|
||||
busb.on("field", function (fieldname, value) {
|
||||
if (typeof options.onField === "function") options.onField(fieldname, value);
|
||||
|
||||
setRetValue(ret, fieldname, value);
|
||||
});
|
||||
|
||||
busb.on("finish", function () {
|
||||
resolve(ret);
|
||||
});
|
||||
|
||||
busb.on("error", reject);
|
||||
});
|
||||
}
|
||||
|
||||
function setRetValue(
|
||||
ret: { [x: string]: any }, // eslint-disable-line @typescript-eslint/no-explicit-any
|
||||
fieldname: string,
|
||||
value: { filename: string; encoding: string; mimetype: string; filePath?: string } | any // eslint-disable-line @typescript-eslint/no-explicit-any
|
||||
) {
|
||||
if (fieldname.endsWith("[]")) {
|
||||
fieldname = fieldname.slice(0, fieldname.length - 2);
|
||||
if (Array.isArray(ret[fieldname])) {
|
||||
ret[fieldname].push(value);
|
||||
} else {
|
||||
ret[fieldname] = [value];
|
||||
}
|
||||
} else {
|
||||
if (Array.isArray(ret[fieldname])) {
|
||||
ret[fieldname].push(value);
|
||||
} else if (ret[fieldname]) {
|
||||
ret[fieldname] = [ret[fieldname], value];
|
||||
} else {
|
||||
ret[fieldname] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default formData;
|
||||
@@ -1,13 +0,0 @@
|
||||
import { SSLApp as _SSLApp, AppOptions } from "uWebSockets.js";
|
||||
import BaseApp from "./baseapp";
|
||||
import { extend } from "./utils";
|
||||
import { UwsApp } from "./types";
|
||||
|
||||
class SSLApp extends (<UwsApp>_SSLApp) {
|
||||
constructor(options: AppOptions) {
|
||||
super(options); // eslint-disable-line constructor-super
|
||||
extend(this, new BaseApp());
|
||||
}
|
||||
}
|
||||
|
||||
export default SSLApp;
|
||||
@@ -1,11 +0,0 @@
|
||||
import { AppOptions, TemplatedApp, HttpResponse, HttpRequest } from "uWebSockets.js";
|
||||
|
||||
export type UwsApp = {
|
||||
(options: AppOptions): TemplatedApp;
|
||||
new (options: AppOptions): TemplatedApp;
|
||||
prototype: TemplatedApp;
|
||||
};
|
||||
|
||||
export type Handler = (res: HttpResponse, req: HttpRequest) => void;
|
||||
|
||||
export {};
|
||||
@@ -1,36 +0,0 @@
|
||||
import { ReadStream } from "fs";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
function extend(who: any, from: any, overwrite = true) {
|
||||
const ownProps = Object.getOwnPropertyNames(Object.getPrototypeOf(from)).concat(Object.keys(from));
|
||||
ownProps.forEach((prop) => {
|
||||
if (prop === "constructor" || from[prop] === undefined) return;
|
||||
if (who[prop] && overwrite) {
|
||||
who[`_${prop}`] = who[prop];
|
||||
}
|
||||
if (typeof from[prop] === "function") who[prop] = from[prop].bind(who);
|
||||
else who[prop] = from[prop];
|
||||
});
|
||||
}
|
||||
|
||||
function stob(stream: ReadStream): Promise<Buffer> {
|
||||
return new Promise((resolve) => {
|
||||
const buffers: Buffer[] = [];
|
||||
stream.on("data", buffers.push.bind(buffers));
|
||||
|
||||
stream.on("end", () => {
|
||||
switch (buffers.length) {
|
||||
case 0:
|
||||
resolve(Buffer.allocUnsafe(0));
|
||||
break;
|
||||
case 1:
|
||||
resolve(buffers[0]);
|
||||
break;
|
||||
default:
|
||||
resolve(Buffer.concat(buffers));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export { extend, stob };
|
||||
@@ -1,19 +0,0 @@
|
||||
import { parse } from "query-string";
|
||||
import { HttpRequest } from "uWebSockets.js";
|
||||
import App from "./server/app";
|
||||
import SSLApp from "./server/sslapp";
|
||||
import * as types from "./server/types";
|
||||
|
||||
const getQuery = (req: HttpRequest) => {
|
||||
return parse(req.getQuery());
|
||||
};
|
||||
|
||||
export { App, SSLApp, getQuery };
|
||||
export * from "./server/types";
|
||||
|
||||
export default {
|
||||
App,
|
||||
SSLApp,
|
||||
getQuery,
|
||||
...types,
|
||||
};
|
||||
@@ -1,26 +1,34 @@
|
||||
import { ADMIN_API_TOKEN, ADMIN_API_URL, ADMIN_URL, OPID_PROFILE_SCREEN_PROVIDER } from "../Enum/EnvironmentVariable";
|
||||
import Axios from "axios";
|
||||
import { GameRoomPolicyTypes } from "_Model/PusherRoom";
|
||||
import { CharacterTexture } from "../Messages/JsonMessages/CharacterTexture";
|
||||
import Axios, { AxiosResponse } from "axios";
|
||||
import { MapDetailsData } from "../Messages/JsonMessages/MapDetailsData";
|
||||
import { RoomRedirect } from "../Messages/JsonMessages/RoomRedirect";
|
||||
import { AdminApiData, isAdminApiData } from "../Messages/JsonMessages/AdminApiData";
|
||||
import * as tg from "generic-type-guard";
|
||||
import { isNumber } from "generic-type-guard";
|
||||
import { isWokaDetail } from "../Enum/PlayerTextures";
|
||||
import qs from "qs";
|
||||
|
||||
export interface AdminBannedData {
|
||||
is_banned: boolean;
|
||||
message: string;
|
||||
}
|
||||
|
||||
export interface FetchMemberDataByUuidResponse {
|
||||
email: string;
|
||||
userUuid: string;
|
||||
tags: string[];
|
||||
visitCardUrl: string | null;
|
||||
textures: CharacterTexture[];
|
||||
messages: unknown[];
|
||||
anonymous?: boolean;
|
||||
userRoomToken: string | undefined;
|
||||
}
|
||||
const isFetchMemberDataByUuidResponse = new tg.IsInterface()
|
||||
.withProperties({
|
||||
email: tg.isString,
|
||||
userUuid: tg.isString,
|
||||
tags: tg.isArray(tg.isString),
|
||||
visitCardUrl: tg.isNullable(tg.isString),
|
||||
textures: tg.isArray(isWokaDetail),
|
||||
messages: tg.isArray(tg.isUnknown),
|
||||
})
|
||||
.withOptionalProperties({
|
||||
anonymous: tg.isBoolean,
|
||||
userRoomToken: tg.isString,
|
||||
})
|
||||
.get();
|
||||
|
||||
export type FetchMemberDataByUuidResponse = tg.GuardedType<typeof isFetchMemberDataByUuidResponse>;
|
||||
|
||||
class AdminApi {
|
||||
/**
|
||||
@@ -48,20 +56,30 @@ class AdminApi {
|
||||
async fetchMemberDataByUuid(
|
||||
userIdentifier: string | null,
|
||||
playUri: string,
|
||||
ipAddress: string
|
||||
ipAddress: string,
|
||||
characterLayers: string[]
|
||||
): Promise<FetchMemberDataByUuidResponse> {
|
||||
if (!ADMIN_API_URL) {
|
||||
return Promise.reject(new Error("No admin backoffice set!"));
|
||||
}
|
||||
const res = await Axios.get(ADMIN_API_URL + "/api/room/access", {
|
||||
const res = await Axios.get<unknown, AxiosResponse<unknown>>(ADMIN_API_URL + "/api/room/access", {
|
||||
params: {
|
||||
userIdentifier,
|
||||
roomId: playUri /* @deprecated */,
|
||||
playUri,
|
||||
roomId: playUri,
|
||||
ipAddress,
|
||||
characterLayers,
|
||||
},
|
||||
headers: { Authorization: `${ADMIN_API_TOKEN}` },
|
||||
paramsSerializer: (p) => {
|
||||
return qs.stringify(p, { arrayFormat: "brackets" });
|
||||
},
|
||||
});
|
||||
if (!isFetchMemberDataByUuidResponse(res.data)) {
|
||||
throw new Error(
|
||||
"Invalid answer received from the admin for the /api/room/access endpoint. Received: " +
|
||||
JSON.stringify(res.data)
|
||||
);
|
||||
}
|
||||
return res.data;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
import axios, { AxiosResponse } from "axios";
|
||||
import { ADMIN_API_TOKEN, ADMIN_API_URL } from "../Enum/EnvironmentVariable";
|
||||
import { wokaList, WokaList } from "../Enum/PlayerTextures";
|
||||
import { WokaServiceInterface } from "./WokaServiceInterface";
|
||||
|
||||
class AdminWokaService implements WokaServiceInterface {
|
||||
/**
|
||||
* Returns the list of all available Wokas for the current user.
|
||||
*/
|
||||
getWokaList(roomUrl: string, token: string): Promise<WokaList | undefined> {
|
||||
return axios
|
||||
.get<unknown, AxiosResponse<unknown>>(`${ADMIN_API_URL}/api/woka/list`, {
|
||||
headers: { Authorization: `${ADMIN_API_TOKEN}` },
|
||||
params: {
|
||||
roomUrl,
|
||||
uuid: token,
|
||||
},
|
||||
})
|
||||
.then((res) => {
|
||||
return wokaList.parse(res.data);
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(`Cannot get woka list from admin API with token: ${token}`, err);
|
||||
return undefined;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export const adminWokaService = new AdminWokaService();
|
||||
@@ -5,6 +5,8 @@ import { InvalidTokenError } from "../Controller/InvalidTokenError";
|
||||
export interface AuthTokenData {
|
||||
identifier: string; //will be a email if logged in or an uuid if anonymous
|
||||
accessToken?: string;
|
||||
username?: string;
|
||||
locale?: string;
|
||||
}
|
||||
export interface AdminSocketTokenData {
|
||||
authorizedRoomIds: string[]; //the list of rooms the client is authorized to read from.
|
||||
@@ -16,8 +18,8 @@ class JWTTokenManager {
|
||||
return Jwt.verify(token, ADMIN_SOCKETS_TOKEN) as AdminSocketTokenData;
|
||||
}
|
||||
|
||||
public createAuthToken(identifier: string, accessToken?: string) {
|
||||
return Jwt.sign({ identifier, accessToken }, SECRET_KEY, { expiresIn: "30d" });
|
||||
public createAuthToken(identifier: string, accessToken?: string, username?: string, locale?: string) {
|
||||
return Jwt.sign({ identifier, accessToken, username, locale }, SECRET_KEY, { expiresIn: "30d" });
|
||||
}
|
||||
|
||||
public verifyJWTToken(token: string, ignoreExpiration: boolean = false): AuthTokenData {
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
import { WokaDetail, WokaDetailsResult, WokaList, wokaPartNames } from "../Enum/PlayerTextures";
|
||||
import { WokaServiceInterface } from "./WokaServiceInterface";
|
||||
|
||||
class LocalWokaService implements WokaServiceInterface {
|
||||
/**
|
||||
* Returns the list of all available Wokas & Woka Parts for the current user.
|
||||
*/
|
||||
async getWokaList(roomId: string, token: string): Promise<WokaList | undefined> {
|
||||
const wokaData: WokaList = await require("../../data/woka.json");
|
||||
if (!wokaData) {
|
||||
return undefined;
|
||||
}
|
||||
return wokaData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the URL of all the images for the given texture ids.
|
||||
*
|
||||
* Key: texture id
|
||||
* Value: URL
|
||||
*
|
||||
* If one of the textures cannot be found, undefined is returned (and the user should be redirected to Woka choice page!)
|
||||
*/
|
||||
async fetchWokaDetails(textureIds: string[]): Promise<WokaDetailsResult | undefined> {
|
||||
const wokaData: WokaList = await require("../../data/woka.json");
|
||||
const textures = new Map<
|
||||
string,
|
||||
{
|
||||
url: string;
|
||||
layer: string;
|
||||
}
|
||||
>();
|
||||
const searchIds = new Set(textureIds);
|
||||
|
||||
for (const part of wokaPartNames) {
|
||||
const wokaPartType = wokaData[part];
|
||||
if (!wokaPartType) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const collection of wokaPartType.collections) {
|
||||
for (const id of searchIds) {
|
||||
const texture = collection.textures.find((texture) => texture.id === id);
|
||||
|
||||
if (texture) {
|
||||
textures.set(id, {
|
||||
url: texture.url,
|
||||
layer: part,
|
||||
});
|
||||
searchIds.delete(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (textureIds.length !== textures.size) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const details: WokaDetail[] = [];
|
||||
|
||||
textures.forEach((value, key) => {
|
||||
details.push({
|
||||
id: key,
|
||||
url: value.url,
|
||||
layer: value.layer,
|
||||
});
|
||||
});
|
||||
|
||||
return details;
|
||||
}
|
||||
}
|
||||
|
||||
export const localWokaService = new LocalWokaService();
|
||||
@@ -4,6 +4,9 @@ import {
|
||||
OPID_CLIENT_SECRET,
|
||||
OPID_CLIENT_ISSUER,
|
||||
OPID_CLIENT_REDIRECT_URL,
|
||||
OPID_USERNAME_CLAIM,
|
||||
OPID_LOCALE_CLAIM,
|
||||
OPID_SCOPE,
|
||||
} from "../Enum/EnvironmentVariable";
|
||||
|
||||
class OpenIDClient {
|
||||
@@ -25,8 +28,11 @@ class OpenIDClient {
|
||||
|
||||
public authorizationUrl(state: string, nonce: string, playUri?: string, redirect?: string) {
|
||||
return this.initClient().then((client) => {
|
||||
if (!OPID_SCOPE.includes("email") || !OPID_SCOPE.includes("openid")) {
|
||||
throw new Error("Invalid scope, 'email' and 'openid' are required in OPID_SCOPE.");
|
||||
}
|
||||
return client.authorizationUrl({
|
||||
scope: "openid email",
|
||||
scope: OPID_SCOPE,
|
||||
prompt: "login",
|
||||
state: state,
|
||||
nonce: nonce,
|
||||
@@ -36,7 +42,10 @@ class OpenIDClient {
|
||||
});
|
||||
}
|
||||
|
||||
public getUserInfo(code: string, nonce: string): Promise<{ email: string; sub: string; access_token: string }> {
|
||||
public getUserInfo(
|
||||
code: string,
|
||||
nonce: string
|
||||
): Promise<{ email: string; sub: string; access_token: string; username: string; locale: string }> {
|
||||
return this.initClient().then((client) => {
|
||||
return client.callback(OPID_CLIENT_REDIRECT_URL, { code }, { nonce }).then((tokenSet) => {
|
||||
return client.userinfo(tokenSet).then((res) => {
|
||||
@@ -45,6 +54,8 @@ class OpenIDClient {
|
||||
email: res.email as string,
|
||||
sub: res.sub,
|
||||
access_token: tokenSet.access_token as string,
|
||||
username: res[OPID_USERNAME_CLAIM] as string,
|
||||
locale: res[OPID_LOCALE_CLAIM] as string,
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { PusherRoom } from "../Model/PusherRoom";
|
||||
import { CharacterLayer, ExSocketInterface } from "../Model/Websocket/ExSocketInterface";
|
||||
import { ExSocketInterface } from "../Model/Websocket/ExSocketInterface";
|
||||
import {
|
||||
AdminMessage,
|
||||
AdminPusherToBackMessage,
|
||||
@@ -38,6 +38,7 @@ import {
|
||||
ErrorMessage,
|
||||
WorldFullMessage,
|
||||
PlayerDetailsUpdatedMessage,
|
||||
InvalidTextureMessage,
|
||||
} from "../Messages/generated/messages_pb";
|
||||
import { ProtobufUtils } from "../Model/Websocket/ProtobufUtils";
|
||||
import { ADMIN_API_URL, JITSI_ISS, JITSI_URL, SECRET_JITSI_KEY } from "../Enum/EnvironmentVariable";
|
||||
@@ -52,7 +53,8 @@ import Debug from "debug";
|
||||
import { ExAdminSocketInterface } from "_Model/Websocket/ExAdminSocketInterface";
|
||||
import { WebSocket } from "uWebSockets.js";
|
||||
import { isRoomRedirect } from "../Messages/JsonMessages/RoomRedirect";
|
||||
import { CharacterTexture } from "../Messages/JsonMessages/CharacterTexture";
|
||||
//import { CharacterTexture } from "../Messages/JsonMessages/CharacterTexture";
|
||||
import { compressors } from "hyper-express";
|
||||
|
||||
const debug = Debug("socket");
|
||||
|
||||
@@ -174,10 +176,13 @@ export class SocketManager implements ZoneEventListener {
|
||||
|
||||
for (const characterLayer of client.characterLayers) {
|
||||
const characterLayerMessage = new CharacterLayerMessage();
|
||||
characterLayerMessage.setName(characterLayer.name);
|
||||
characterLayerMessage.setName(characterLayer.id);
|
||||
if (characterLayer.url !== undefined) {
|
||||
characterLayerMessage.setUrl(characterLayer.url);
|
||||
}
|
||||
if (characterLayer.layer !== undefined) {
|
||||
characterLayerMessage.setLayer(characterLayer.layer);
|
||||
}
|
||||
|
||||
joinRoomMessage.addCharacterlayer(characterLayerMessage);
|
||||
}
|
||||
@@ -544,36 +549,6 @@ export class SocketManager implements ZoneEventListener {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Merges the characterLayers received from the front (as an array of string) with the custom textures from the back.
|
||||
*/
|
||||
static mergeCharacterLayersAndCustomTextures(
|
||||
characterLayers: string[],
|
||||
memberTextures: CharacterTexture[]
|
||||
): CharacterLayer[] {
|
||||
const characterLayerObjs: CharacterLayer[] = [];
|
||||
for (const characterLayer of characterLayers) {
|
||||
if (characterLayer.startsWith("customCharacterTexture")) {
|
||||
const customCharacterLayerId: number = +characterLayer.substr(22);
|
||||
for (const memberTexture of memberTextures) {
|
||||
if (memberTexture.id == customCharacterLayerId) {
|
||||
characterLayerObjs.push({
|
||||
name: characterLayer,
|
||||
url: memberTexture.url,
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
characterLayerObjs.push({
|
||||
name: characterLayer,
|
||||
url: undefined,
|
||||
});
|
||||
}
|
||||
}
|
||||
return characterLayerObjs;
|
||||
}
|
||||
|
||||
public onUserEnters(user: UserDescriptor, listener: ExSocketInterface): void {
|
||||
const subMessage = new SubMessage();
|
||||
subMessage.setUserjoinedmessage(user.toUserJoinedMessage());
|
||||
@@ -619,7 +594,7 @@ export class SocketManager implements ZoneEventListener {
|
||||
emitInBatch(listener, subMessage);
|
||||
}
|
||||
|
||||
public emitWorldFullMessage(client: WebSocket) {
|
||||
public emitWorldFullMessage(client: compressors.WebSocket) {
|
||||
const errorMessage = new WorldFullMessage();
|
||||
|
||||
const serverToClientMessage = new ServerToClientMessage();
|
||||
@@ -630,7 +605,7 @@ export class SocketManager implements ZoneEventListener {
|
||||
}
|
||||
}
|
||||
|
||||
public emitTokenExpiredMessage(client: WebSocket) {
|
||||
public emitTokenExpiredMessage(client: compressors.WebSocket) {
|
||||
const errorMessage = new TokenExpiredMessage();
|
||||
|
||||
const serverToClientMessage = new ServerToClientMessage();
|
||||
@@ -641,7 +616,18 @@ export class SocketManager implements ZoneEventListener {
|
||||
}
|
||||
}
|
||||
|
||||
public emitConnexionErrorMessage(client: WebSocket, message: string) {
|
||||
public emitInvalidTextureMessage(client: compressors.WebSocket) {
|
||||
const errorMessage = new InvalidTextureMessage();
|
||||
|
||||
const serverToClientMessage = new ServerToClientMessage();
|
||||
serverToClientMessage.setInvalidtexturemessage(errorMessage);
|
||||
|
||||
if (!client.disconnecting) {
|
||||
client.send(serverToClientMessage.serializeBinary().buffer, true);
|
||||
}
|
||||
}
|
||||
|
||||
public emitConnexionErrorMessage(client: compressors.WebSocket, message: string) {
|
||||
const errorMessage = new WorldConnexionMessage();
|
||||
errorMessage.setMessage(message);
|
||||
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
import { ADMIN_API_URL } from "../Enum/EnvironmentVariable";
|
||||
import { adminWokaService } from "./AdminWokaService";
|
||||
import { localWokaService } from "./LocalWokaService";
|
||||
|
||||
export const wokaService = ADMIN_API_URL ? adminWokaService : localWokaService;
|
||||
@@ -0,0 +1,8 @@
|
||||
import { WokaDetailsResult, WokaList } from "../Enum/PlayerTextures";
|
||||
|
||||
export interface WokaServiceInterface {
|
||||
/**
|
||||
* Returns the list of all available Wokas for the current user.
|
||||
*/
|
||||
getWokaList(roomId: string, token: string): Promise<WokaList | undefined>;
|
||||
}
|
||||
Reference in New Issue
Block a user