Merge branch 'develop' of github.com:thecodingmachine/workadventure into develop

This commit is contained in:
_Bastler
2022-04-25 14:57:17 +02:00
75 changed files with 1753 additions and 1288 deletions
+2 -2
View File
@@ -27,8 +27,8 @@
"no-unused-vars": "off",
"@typescript-eslint/no-explicit-any": "error",
"@typescript-eslint/no-unused-vars": [
"error"
"error", { "args": "none", "caughtErrors": "all", "varsIgnorePattern": "_exhaustiveCheck" }
],
"no-throw-literal": "error"
}
}
}
+2
View File
@@ -40,6 +40,7 @@
},
"homepage": "https://github.com/thecodingmachine/workadventure#readme",
"dependencies": {
"@anatine/zod-openapi": "^1.3.0",
"axios": "^0.21.2",
"circular-json": "^0.5.9",
"debug": "^4.3.1",
@@ -48,6 +49,7 @@
"hyper-express": "^5.8.1",
"jsonwebtoken": "^8.5.1",
"mkdirp": "^1.0.4",
"openapi3-ts": "^2.0.2",
"openid-client": "^4.7.4",
"prom-client": "^12.0.0",
"qs": "^6.10.3",
@@ -1,6 +1,6 @@
import { v4 } from "uuid";
import { BaseHttpController } from "./BaseHttpController";
import { adminApi, FetchMemberDataByUuidResponse } from "../Services/AdminApi";
import { FetchMemberDataByUuidResponse } from "../Services/AdminApi";
import { AuthTokenData, jwtTokenManager } from "../Services/JWTTokenManager";
import { parse } from "query-string";
import { openIDClient } from "../Services/OpenIDClient";
@@ -172,7 +172,8 @@ export class AuthenticateController extends BaseHttpController {
authTokenData.identifier,
playUri as string,
IPAddress,
[]
[],
req.header("accept-language")
);
if (authTokenData.accessToken == undefined) {
@@ -224,7 +225,13 @@ export class AuthenticateController extends BaseHttpController {
//Get user data from Admin Back Office
//This is very important to create User Local in LocalStorage in WorkAdventure
const data = await adminService.fetchMemberDataByUuid(sub, playUri as string, IPAddress, []);
const data = await adminService.fetchMemberDataByUuid(
sub,
playUri as string,
IPAddress,
[],
req.header("accept-language")
);
return res.json({ ...data, authToken, username: userInfo?.username, locale: userInfo?.locale, userUuid : sub });
} catch (e) {
@@ -327,13 +334,18 @@ export class AuthenticateController extends BaseHttpController {
try {
if (typeof organizationMemberToken != "string") throw new Error("No organization token");
const data = await adminApi.fetchMemberDataByToken(organizationMemberToken, playUri);
const data = await adminService.fetchMemberDataByToken(
organizationMemberToken,
playUri,
req.header("accept-language")
);
const userUuid = data.userUuid;
const email = data.email;
const roomUrl = data.roomUrl;
const mapUrlStart = data.mapUrlStart;
const authToken = jwtTokenManager.createAuthToken(email || userUuid);
res.json({
authToken,
userUuid,
@@ -419,7 +431,7 @@ export class AuthenticateController extends BaseHttpController {
//get login profile
res.status(302);
res.setHeader("Location", adminApi.getProfileUrl(authTokenData.accessToken));
res.setHeader("Location", adminService.getProfileUrl(authTokenData.accessToken));
res.send("");
return;
} catch (error) {
@@ -485,7 +497,8 @@ export class AuthenticateController extends BaseHttpController {
* @param email
* @param playUri
* @param IPAddress
* @return FetchMemberDataByUuidResponse|object
* @return
|object
* @private
*/
private async getUserByUserIdentifier(
@@ -503,7 +516,7 @@ export class AuthenticateController extends BaseHttpController {
userRoomToken: undefined,
};
try {
data = await adminApi.fetchMemberDataByUuid(email, playUri, IPAddress, []);
data = await adminService.fetchMemberDataByUuid(email, playUri, IPAddress, []);
} catch (err) {
console.error("openIDCallback => fetchMemberDataByUuid", err);
}
+10 -6
View File
@@ -1,6 +1,7 @@
import { Server } from "hyper-express";
import Response from "hyper-express/types/components/http/Response";
import axios from "axios";
import { isErrorApiData } from "../Messages/JsonMessages/ErrorApiData";
export class BaseHttpController {
constructor(protected app: Server) {
@@ -31,12 +32,15 @@ export class BaseHttpController {
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)
);
const errorType = isErrorApiData.safeParse(e?.response?.data);
if (!errorType.success) {
res.send(
"An error occurred: " +
e.response.status +
" " +
(e.response.data && e.response.data.message ? e.response.data.message : e.response.statusText)
);
} else res.json(errorType.data);
return;
} else {
res.status(500);
+38 -38
View File
@@ -1,5 +1,4 @@
import { ExSocketInterface } from "../Model/Websocket/ExSocketInterface";
import { GameRoomPolicyTypes } from "../Model/PusherRoom";
import { PointInterface } from "../Model/Websocket/PointInterface";
import {
SetPlayerDetailsMessage,
@@ -25,7 +24,7 @@ import {
import { UserMovesMessage } from "../Messages/generated/messages_pb";
import { parse } from "query-string";
import { AdminSocketTokenData, jwtTokenManager, tokenInvalidException } from "../Services/JWTTokenManager";
import { adminApi, FetchMemberDataByUuidResponse } from "../Services/AdminApi";
import { FetchMemberDataByUuidResponse } from "../Services/AdminApi";
import { socketManager } from "../Services/SocketManager";
import { emitInBatch } from "../Services/IoSocketHelpers";
import { ADMIN_API_URL, ADMIN_SOCKETS_TOKEN, DISABLE_ANONYMOUS, SOCKET_IDLE_TIMER } from "../Enum/EnvironmentVariable";
@@ -39,6 +38,8 @@ import { localWokaService } from "../Services/LocalWokaService";
import { WebSocket } from "uWebSockets.js";
import { WokaDetail } from "../Messages/JsonMessages/PlayerTextures";
import { z } from "zod";
import { adminService } from "../Services/AdminService";
import { ErrorApiData, isErrorApiData } from "../Messages/JsonMessages/ErrorApiData";
/**
* The object passed between the "open" and the "upgrade" methods when opening a websocket
@@ -66,13 +67,21 @@ interface UpgradeData {
};
}
interface UpgradeFailedData {
interface UpgradeFailedInvalidData {
rejected: true;
reason: "tokenInvalid" | "textureInvalid" | null;
message: string;
roomId: string;
}
interface UpgradeFailedErrorData {
rejected: true;
reason: "error";
error: ErrorApiData;
}
type UpgradeFailedData = UpgradeFailedErrorData | UpgradeFailedInvalidData;
export class IoSocketController {
private nextUserId: number = 1;
@@ -234,6 +243,7 @@ export class IoSocketController {
const websocketProtocol = req.getHeader("sec-websocket-protocol");
const websocketExtensions = req.getHeader("sec-websocket-extensions");
const IPAddress = req.getHeader("x-forwarded-for");
const locale = req.getHeader("accept-language");
const roomId = query.roomId;
try {
@@ -285,7 +295,6 @@ export class IoSocketController {
let memberMessages: unknown;
let memberUserRoomToken: string | undefined;
let memberTextures: WokaDetail[] = [];
const room = await socketManager.getOrCreateRoom(roomId);
let userData: FetchMemberDataByUuidResponse = {
email: userIdentifier,
userUuid: userIdentifier,
@@ -302,61 +311,52 @@ export class IoSocketController {
if (ADMIN_API_URL) {
try {
try {
userData = await adminApi.fetchMemberDataByUuid(
userData = await adminService.fetchMemberDataByUuid(
userIdentifier,
roomId,
IPAddress,
characterLayers
characterLayers,
locale
);
} catch (err) {
if (Axios.isAxiosError(err)) {
if (err?.response?.status == 404) {
// If we get an HTTP 404, the token is invalid. Let's perform an anonymous login!
console.warn(
'Cannot find user with email "' +
(userIdentifier || "anonymous") +
'". Performing an anonymous login instead.'
);
} else if (err?.response?.status == 403) {
// If we get an HTTP 403, the world is full. We need to broadcast a special error to the client.
// we finish immediately the upgrade then we will close the socket as soon as it starts opening.
const errorType = isErrorApiData.safeParse(err?.response?.data);
if (errorType.success) {
return res.upgrade(
{
rejected: true,
message: err?.response?.data.message,
reason: "error",
status: err?.response?.status,
roomId,
},
error: errorType.data,
} as UpgradeFailedData,
websocketKey,
websocketProtocol,
websocketExtensions,
context
);
} else {
return res.upgrade(
{
rejected: true,
reason: null,
status: 500,
message: err?.response?.data,
roomId: roomId,
} as UpgradeFailedData,
websocketKey,
websocketProtocol,
websocketExtensions,
context
);
}
} else {
throw err;
}
throw err;
}
memberMessages = userData.messages;
memberTags = userData.tags;
memberVisitCardUrl = userData.visitCardUrl;
memberTextures = userData.textures;
memberUserRoomToken = userData.userRoomToken;
if (
room.policyType === GameRoomPolicyTypes.USE_TAGS_POLICY &&
(userData.anonymous === true || !room.canAccess(memberTags))
) {
throw new Error("Insufficient privileges to access this room");
}
if (
room.policyType === GameRoomPolicyTypes.MEMBERS_ONLY_POLICY &&
userData.anonymous === true
) {
throw new Error("Use the login URL to connect");
}
characterLayerObjs = memberTextures;
} catch (e) {
console.log(
@@ -480,8 +480,8 @@ export class IoSocketController {
socketManager.emitTokenExpiredMessage(ws);
} else if (ws.reason === "textureInvalid") {
socketManager.emitInvalidTextureMessage(ws);
} else if (ws.message === "World is full") {
socketManager.emitWorldFullMessage(ws);
} else if (ws.reason === "error") {
socketManager.emitErrorScreenMessage(ws, ws.error);
} else {
socketManager.emitConnexionErrorMessage(ws, ws.message);
}
+9 -61
View File
@@ -1,11 +1,8 @@
import { adminApi } from "../Services/AdminApi";
import { ADMIN_API_URL, DISABLE_ANONYMOUS } from "../Enum/EnvironmentVariable";
import { GameRoomPolicyTypes } from "../Model/PusherRoom";
import { isMapDetailsData, MapDetailsData } from "../Messages/JsonMessages/MapDetailsData";
import { AuthTokenData, jwtTokenManager } from "../Services/JWTTokenManager";
import { InvalidTokenError } from "./InvalidTokenError";
import { DISABLE_ANONYMOUS } from "../Enum/EnvironmentVariable";
import { isMapDetailsData } from "../Messages/JsonMessages/MapDetailsData";
import { parse } from "query-string";
import { BaseHttpController } from "./BaseHttpController";
import { adminService } from "../Services/AdminService";
export class MapController extends BaseHttpController {
// Returns a map mapping map name to file name of the map
@@ -107,66 +104,17 @@ export class MapController extends BaseHttpController {
return;
}
// If no admin URL is set, let's react on '/_/[instance]/[map url]' URLs
if (!ADMIN_API_URL) {
const roomUrl = new URL(query.playUri);
const match = /\/_\/[^/]+\/(.+)/.exec(roomUrl.pathname);
if (!match) {
res.status(404);
res.json({});
return;
}
const mapUrl = roomUrl.protocol + "//" + match[1];
res.json({
mapUrl,
policy_type: GameRoomPolicyTypes.ANONYMOUS_POLICY,
roomSlug: null, // Deprecated
group: null,
tags: [],
contactPage: null,
authenticationMandatory: DISABLE_ANONYMOUS,
} as MapDetailsData);
return;
}
(async () => {
try {
let userId: string | undefined = undefined;
if (query.authToken != undefined) {
let authTokenData: AuthTokenData;
try {
authTokenData = jwtTokenManager.verifyJWTToken(query.authToken as string);
userId = authTokenData.identifier;
} catch (e) {
try {
// Decode token, in this case we don't need to create new token.
authTokenData = jwtTokenManager.verifyJWTToken(query.authToken as string, true);
userId = authTokenData.identifier;
console.info("JWT expire, but decoded", userId);
} catch (e) {
if (e instanceof InvalidTokenError) {
// The token was not good, redirect user on login page
res.status(401);
res.send("Token decrypted error");
return;
} else {
this.castErrorToResponse(e, res);
return;
}
}
}
}
const mapDetails = isMapDetailsData.parse(
await adminApi.fetchMapDetails(query.playUri as string, userId)
await adminService.fetchMapDetails(
query.playUri as string,
query.authToken as string,
req.header("accept-language")
)
);
if (DISABLE_ANONYMOUS) {
mapDetails.authenticationMandatory = true;
}
if (DISABLE_ANONYMOUS) mapDetails.authenticationMandatory = true;
res.json(mapDetails);
return;
-10
View File
@@ -1,7 +1,6 @@
import { ExSocketInterface } from "../Model/Websocket/ExSocketInterface";
import { PositionDispatcher } from "./PositionDispatcher";
import { ViewportInterface } from "../Model/Websocket/ViewportMessage";
import { arrayIntersect } from "../Services/ArrayHelper";
import { ZoneEventListener } from "../Model/Zone";
import { apiClientRepository } from "../Services/ApiClientRepository";
import {
@@ -24,8 +23,6 @@ export enum GameRoomPolicyTypes {
export class PusherRoom {
private readonly positionNotifier: PositionDispatcher;
public tags: string[];
public policyType: GameRoomPolicyTypes;
private versionNumber: number = 1;
private backConnection!: ClientReadableStream<BatchToPusherRoomMessage>;
private isClosing: boolean = false;
@@ -33,9 +30,6 @@ export class PusherRoom {
//public readonly variables = new Map<string, string>();
constructor(public readonly roomUrl: string, private socketListener: ZoneEventListener) {
this.tags = [];
this.policyType = GameRoomPolicyTypes.ANONYMOUS_POLICY;
// A zone is 10 sprites wide.
this.positionNotifier = new PositionDispatcher(this.roomUrl, 320, 320, this.socketListener);
}
@@ -53,10 +47,6 @@ export class PusherRoom {
this.listeners.delete(socket);
}
public canAccess(userTags: string[]): boolean {
return arrayIntersect(userTags, this.tags);
}
public isEmpty(): boolean {
return this.positionNotifier.isEmpty();
}
+58 -40
View File
@@ -7,6 +7,8 @@ import { z } from "zod";
import { isWokaDetail } from "../Messages/JsonMessages/PlayerTextures";
import qs from "qs";
import { AdminInterface } from "./AdminInterface";
import { AuthTokenData, jwtTokenManager } from "./JWTTokenManager";
import { InvalidTokenError } from "../Controller/InvalidTokenError";
export interface AdminBannedData {
is_banned: boolean;
@@ -20,6 +22,7 @@ export const isFetchMemberDataByUuidResponse = z.object({
visitCardUrl: z.nullable(z.string()),
textures: z.array(isWokaDetail),
messages: z.array(z.unknown()),
anonymous: z.optional(z.boolean()),
userRoomToken: z.optional(z.string()),
});
@@ -27,14 +30,38 @@ export const isFetchMemberDataByUuidResponse = z.object({
export type FetchMemberDataByUuidResponse = z.infer<typeof isFetchMemberDataByUuidResponse>;
class AdminApi implements AdminInterface {
/**
* @var playUri: is url of the room
* @var userId: can to be undefined or email or uuid
* @return MapDetailsData|RoomRedirect
*/
async fetchMapDetails(playUri: string, userId?: string): Promise<MapDetailsData | RoomRedirect> {
if (!ADMIN_API_URL) {
return Promise.reject(new Error("No admin backoffice set!"));
async fetchMapDetails(
playUri: string,
authToken?: string,
locale?: string
): Promise<MapDetailsData | RoomRedirect> {
let userId: string | undefined = undefined;
if (authToken != undefined) {
let authTokenData: AuthTokenData;
try {
authTokenData = jwtTokenManager.verifyJWTToken(authToken);
userId = authTokenData.identifier;
//eslint-disable-next-line @typescript-eslint/no-unused-vars
} catch (e) {
try {
// Decode token, in this case we don't need to create new token.
authTokenData = jwtTokenManager.verifyJWTToken(authToken, true);
userId = authTokenData.identifier;
console.info("JWT expire, but decoded:", userId);
} catch (e) {
if (e instanceof InvalidTokenError) {
throw new Error("Token decrypted error");
// The token was not good, redirect user on login page
//res.status(401);
//res.send("Token decrypted error");
//return;
} else {
throw new Error("Error on decryption of token :" + e);
//this.castErrorToResponse(e, res);
//return;
}
}
}
}
const params: { playUri: string; userId?: string } = {
@@ -43,7 +70,7 @@ class AdminApi implements AdminInterface {
};
const res = await Axios.get<unknown, AxiosResponse<unknown>>(ADMIN_API_URL + "/api/map", {
headers: { Authorization: `${ADMIN_API_TOKEN}` },
headers: { Authorization: `${ADMIN_API_TOKEN}`, "Accept-Language": locale ?? "en" },
params,
});
@@ -69,11 +96,9 @@ class AdminApi implements AdminInterface {
userIdentifier: string,
playUri: string,
ipAddress: string,
characterLayers: string[]
characterLayers: string[],
locale?: string
): Promise<FetchMemberDataByUuidResponse> {
if (!ADMIN_API_URL) {
return Promise.reject(new Error("No admin backoffice set!"));
}
const res = await Axios.get<unknown, AxiosResponse<unknown>>(ADMIN_API_URL + "/api/room/access", {
params: {
userIdentifier,
@@ -81,7 +106,7 @@ class AdminApi implements AdminInterface {
ipAddress,
characterLayers,
},
headers: { Authorization: `${ADMIN_API_TOKEN}` },
headers: { Authorization: `${ADMIN_API_TOKEN}`, "Accept-Language": locale ?? "en" },
paramsSerializer: (p) => {
return qs.stringify(p, { arrayFormat: "brackets" });
},
@@ -100,14 +125,15 @@ class AdminApi implements AdminInterface {
);
}
async fetchMemberDataByToken(organizationMemberToken: string, playUri: string | null): Promise<AdminApiData> {
if (!ADMIN_API_URL) {
return Promise.reject(new Error("No admin backoffice set!"));
}
async fetchMemberDataByToken(
organizationMemberToken: string,
playUri: string | null,
locale?: string
): Promise<AdminApiData> {
//todo: this call can fail if the corresponding world is not activated or if the token is invalid. Handle that case.
const res = await Axios.get(ADMIN_API_URL + "/api/login-url/" + organizationMemberToken, {
params: { playUri },
headers: { Authorization: `${ADMIN_API_TOKEN}` },
headers: { Authorization: `${ADMIN_API_TOKEN}`, "Accept-Language": locale ?? "en" },
});
const adminApiData = isAdminApiData.safeParse(res.data);
@@ -125,11 +151,9 @@ class AdminApi implements AdminInterface {
reportedUserUuid: string,
reportedUserComment: string,
reporterUserUuid: string,
reportWorldSlug: string
reportWorldSlug: string,
locale?: string
) {
if (!ADMIN_API_URL) {
return Promise.reject(new Error("No admin backoffice set!"));
}
return Axios.post(
`${ADMIN_API_URL}/api/report`,
{
@@ -139,15 +163,17 @@ class AdminApi implements AdminInterface {
reportWorldSlug,
},
{
headers: { Authorization: `${ADMIN_API_TOKEN}` },
headers: { Authorization: `${ADMIN_API_TOKEN}`, "Accept-Language": locale ?? "en" },
}
);
}
async verifyBanUser(userUuid: string, ipAddress: string, roomUrl: string): Promise<AdminBannedData> {
if (!ADMIN_API_URL) {
return Promise.reject(new Error("No admin backoffice set!"));
}
async verifyBanUser(
userUuid: string,
ipAddress: string,
roomUrl: string,
locale?: string
): Promise<AdminBannedData> {
//todo: this call can fail if the corresponding world is not activated or if the token is invalid. Handle that case.
return Axios.get(
ADMIN_API_URL +
@@ -158,28 +184,20 @@ class AdminApi implements AdminInterface {
encodeURIComponent(userUuid) +
"&roomUrl=" +
encodeURIComponent(roomUrl),
{ headers: { Authorization: `${ADMIN_API_TOKEN}` } }
{ headers: { Authorization: `${ADMIN_API_TOKEN}`, "Accept-Language": locale ?? "en" } }
).then((data) => {
return data.data;
});
}
async getUrlRoomsFromSameWorld(roomUrl: string): Promise<string[]> {
if (!ADMIN_API_URL) {
return Promise.reject(new Error("No admin backoffice set!"));
}
async getUrlRoomsFromSameWorld(roomUrl: string, locale?: string): Promise<string[]> {
return Axios.get(ADMIN_API_URL + "/api/room/sameWorld" + "?roomUrl=" + encodeURIComponent(roomUrl), {
headers: { Authorization: `${ADMIN_API_TOKEN}` },
headers: { Authorization: `${ADMIN_API_TOKEN}`, "Accept-Language": locale ?? "en" },
}).then((data) => {
return data.data;
});
}
/**
*
* @param accessToken
*/
getProfileUrl(accessToken: string): string {
if (!OPID_PROFILE_SCREEN_PROVIDER) {
throw new Error("No admin backoffice set!");
@@ -187,7 +205,7 @@ class AdminApi implements AdminInterface {
return `${OPID_PROFILE_SCREEN_PROVIDER}?accessToken=${accessToken}`;
}
async logoutOauth(token: string) {
async logoutOauth(token: string): Promise<void> {
await Axios.get(ADMIN_API_URL + `/oauth/logout?token=${token}`);
}
}
+74 -2
View File
@@ -1,10 +1,82 @@
import { FetchMemberDataByUuidResponse } from "./AdminApi";
import { AdminBannedData, FetchMemberDataByUuidResponse } from "./AdminApi";
import { MapDetailsData } from "../Messages/JsonMessages/MapDetailsData";
import { RoomRedirect } from "../Messages/JsonMessages/RoomRedirect";
import { AdminApiData } from "../Messages/JsonMessages/AdminApiData";
export interface AdminInterface {
/**
* @var playUri: is url of the room
* @var userIdentifier: can to be undefined or email or uuid
* @var ipAddress
* @var characterLayers
* @return MapDetailsData|RoomRedirect
*/
fetchMemberDataByUuid(
userIdentifier: string,
playUri: string,
ipAddress: string,
characterLayers: string[]
characterLayers: string[],
locale?: string
): Promise<FetchMemberDataByUuidResponse>;
/**
* @var playUri: is url of the room
* @var userId: can to be undefined or email or uuid
* @return MapDetailsData|RoomRedirect
*/
fetchMapDetails(playUri: string, authToken?: string, locale?: string): Promise<MapDetailsData | RoomRedirect>;
/**
* @param locale
* @param organizationMemberToken
* @param playUri
* @return AdminApiData
*/
fetchMemberDataByToken(
organizationMemberToken: string,
playUri: string | null,
locale?: string
): Promise<AdminApiData>;
/**
* @param locale
* @param reportedUserUuid
* @param reportedUserComment
* @param reporterUserUuid
* @param reportWorldSlug
*/
reportPlayer(
reportedUserUuid: string,
reportedUserComment: string,
reporterUserUuid: string,
reportWorldSlug: string,
locale?: string
): Promise<unknown>;
/**
* @param locale
* @param userUuid
* @param ipAddress
* @param roomUrl
* @return AdminBannedData
*/
verifyBanUser(userUuid: string, ipAddress: string, roomUrl: string, locale?: string): Promise<AdminBannedData>;
/**
* @param locale
* @param roomUrl
* @return string[]
*/
getUrlRoomsFromSameWorld(roomUrl: string, locale?: string): Promise<string[]>;
/**
* @param accessToken
* @return string
*/
getProfileUrl(accessToken: string): string;
/**
* @param token
*/
logoutOauth(token: string): Promise<void>;
}
+72 -5
View File
@@ -1,5 +1,10 @@
import { FetchMemberDataByUuidResponse } from "./AdminApi";
import { AdminBannedData, FetchMemberDataByUuidResponse } from "./AdminApi";
import { AdminInterface } from "./AdminInterface";
import { MapDetailsData } from "../Messages/JsonMessages/MapDetailsData";
import { RoomRedirect } from "../Messages/JsonMessages/RoomRedirect";
import { GameRoomPolicyTypes } from "../Model/PusherRoom";
import { DISABLE_ANONYMOUS } from "../Enum/EnvironmentVariable";
import { AdminApiData } from "../Messages/JsonMessages/AdminApiData";
/**
* A local class mocking a real admin if no admin is configured.
@@ -7,12 +12,10 @@ import { AdminInterface } from "./AdminInterface";
class LocalAdmin implements AdminInterface {
fetchMemberDataByUuid(
userIdentifier: string,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
playUri: string,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
ipAddress: string,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
characterLayers: string[]
characterLayers: string[],
locale?: string
): Promise<FetchMemberDataByUuidResponse> {
return Promise.resolve({
email: userIdentifier,
@@ -24,6 +27,70 @@ class LocalAdmin implements AdminInterface {
userRoomToken: undefined,
});
}
fetchMapDetails(playUri: string, authToken?: string, locale?: string): Promise<MapDetailsData | RoomRedirect> {
const roomUrl = new URL(playUri);
const match = /\/_\/[^/]+\/(.+)/.exec(roomUrl.pathname);
if (!match) {
throw new Error("URL format is not good");
}
const mapUrl = roomUrl.protocol + "//" + match[1];
return Promise.resolve({
mapUrl,
policy_type: GameRoomPolicyTypes.ANONYMOUS_POLICY,
tags: [],
authenticationMandatory: DISABLE_ANONYMOUS,
roomSlug: null,
contactPage: null,
group: null,
iframeAuthentication: null,
loadingLogo: null,
loginSceneLogo: null,
});
}
async fetchMemberDataByToken(
organizationMemberToken: string,
playUri: string | null,
locale?: string
): Promise<AdminApiData> {
return Promise.reject(new Error("No admin backoffice set!"));
}
reportPlayer(
reportedUserUuid: string,
reportedUserComment: string,
reporterUserUuid: string,
reportWorldSlug: string,
locale?: string
) {
return Promise.reject(new Error("No admin backoffice set!"));
}
async verifyBanUser(
userUuid: string,
ipAddress: string,
roomUrl: string,
locale?: string
): Promise<AdminBannedData> {
return Promise.reject(new Error("No admin backoffice set!"));
}
async getUrlRoomsFromSameWorld(roomUrl: string, locale?: string): Promise<string[]> {
return Promise.reject(new Error("No admin backoffice set!"));
}
getProfileUrl(accessToken: string): string {
new Error("No admin backoffice set!");
return "";
}
async logoutOauth(token: string): Promise<void> {
return Promise.reject(new Error("No admin backoffice set!"));
}
}
export const localAdmin = new LocalAdmin();
-1
View File
@@ -5,7 +5,6 @@ class LocalWokaService implements WokaServiceInterface {
/**
* Returns the list of all available Wokas & Woka Parts for the current user.
*/
// eslint-disable-next-line @typescript-eslint/no-unused-vars
async getWokaList(roomId: string, token: string): Promise<WokaList | undefined> {
const wokaData: WokaList = await require("../../data/woka.json");
if (!wokaData) {
+37 -25
View File
@@ -39,10 +39,10 @@ import {
PlayerDetailsUpdatedMessage,
LockGroupPromptMessage,
InvalidTextureMessage,
ErrorScreenMessage,
} from "../Messages/generated/messages_pb";
import { ProtobufUtils } from "../Model/Websocket/ProtobufUtils";
import { ADMIN_API_URL, JITSI_ISS, JITSI_URL, SECRET_JITSI_KEY, PUSHER_FORCE_ROOM_UPDATE } from "../Enum/EnvironmentVariable";
import { adminApi } from "./AdminApi";
import { JITSI_ISS, JITSI_URL, SECRET_JITSI_KEY } from "../Enum/EnvironmentVariable";
import { emitInBatch } from "./IoSocketHelpers";
import Jwt from "jsonwebtoken";
import { clientEventsEmitter } from "./ClientEventsEmitter";
@@ -52,7 +52,9 @@ import { GroupDescriptor, UserDescriptor, ZoneEventListener } from "../Model/Zon
import Debug from "debug";
import { ExAdminSocketInterface } from "../Model/Websocket/ExAdminSocketInterface";
import { compressors } from "hyper-express";
import { isMapDetailsData } from "../Messages/JsonMessages/MapDetailsData";
import { adminService } from "./AdminService";
import { ErrorApiData } from "../Messages/JsonMessages/ErrorApiData";
import { BoolValue, Int32Value, StringValue } from "google-protobuf/google/protobuf/wrappers_pb";
const debug = Debug("socket");
@@ -382,7 +384,8 @@ export class SocketManager implements ZoneEventListener {
async handleReportMessage(client: ExSocketInterface, reportPlayerMessage: ReportPlayerMessage) {
try {
await adminApi.reportPlayer(
await adminService.reportPlayer(
"en",
reportPlayerMessage.getReporteduseruuid(),
reportPlayerMessage.getReportcomment(),
client.userUuid,
@@ -458,30 +461,12 @@ export class SocketManager implements ZoneEventListener {
let room = this.rooms.get(roomUrl);
if (room === undefined) {
room = new PusherRoom(roomUrl, this);
if (ADMIN_API_URL) {
await this.updateRoomWithAdminData(room);
}
await room.init();
this.rooms.set(roomUrl, room);
} else if (PUSHER_FORCE_ROOM_UPDATE && ADMIN_API_URL) {
await this.updateRoomWithAdminData(room);
}
return room;
}
public async updateRoomWithAdminData(room: PusherRoom): Promise<void> {
const data = await adminApi.fetchMapDetails(room.roomUrl);
const mapDetailsData = isMapDetailsData.safeParse(data);
if (mapDetailsData.success) {
room.tags = mapDetailsData.data.tags;
room.policyType = Number(mapDetailsData.data.policy_type);
} else {
// TODO: if the updated room data is actually a redirect, we need to take everybody on the map
// and redirect everybody to the new location (so we need to close the connection for everybody)
}
}
public getWorlds(): Map<string, PusherRoom> {
return this.rooms;
}
@@ -670,12 +655,39 @@ export class SocketManager implements ZoneEventListener {
client.send(serverToClientMessage.serializeBinary().buffer, true);
}
public emitErrorScreenMessage(client: compressors.WebSocket, errorApi: ErrorApiData) {
const errorMessage = new ErrorScreenMessage();
errorMessage.setType(errorApi.type);
if (errorApi.type == "retry" || errorApi.type == "error") {
errorMessage.setCode(new StringValue().setValue(errorApi.code));
errorMessage.setTitle(new StringValue().setValue(errorApi.title));
errorMessage.setSubtitle(new StringValue().setValue(errorApi.subtitle));
errorMessage.setDetails(new StringValue().setValue(errorApi.details));
errorMessage.setImage(new StringValue().setValue(errorApi.image));
}
if (errorApi.type == "retry") {
if (errorApi.buttonTitle) errorMessage.setButtontitle(new StringValue().setValue(errorApi.buttonTitle));
if (errorApi.canRetryManual !== undefined)
errorMessage.setCanretrymanual(new BoolValue().setValue(errorApi.canRetryManual));
if (errorApi.timeToRetry)
errorMessage.setTimetoretry(new Int32Value().setValue(Number(errorApi.timeToRetry)));
}
if (errorApi.type == "redirect" && errorApi.urlToRedirect)
errorMessage.setUrltoredirect(new StringValue().setValue(errorApi.urlToRedirect));
const serverToClientMessage = new ServerToClientMessage();
serverToClientMessage.setErrorscreenmessage(errorMessage);
//if (!client.disconnecting) {
client.send(serverToClientMessage.serializeBinary().buffer, true);
//}
}
private refreshRoomData(roomId: string, versionNumber: number): void {
const room = this.rooms.get(roomId);
//this function is run for every users connected to the room, so we need to make sure the room wasn't already refreshed.
if (!room || !room.needsUpdate(versionNumber)) return;
this.updateRoomWithAdminData(room);
//TODO check right of user in admin
}
handleEmotePromptMessage(client: ExSocketInterface, emoteEventmessage: EmotePromptMessage) {
@@ -697,7 +709,7 @@ export class SocketManager implements ZoneEventListener {
let tabUrlRooms: string[];
if (playGlobalMessageEvent.getBroadcasttoworld()) {
tabUrlRooms = await adminApi.getUrlRoomsFromSameWorld(clientRoomUrl);
tabUrlRooms = await adminService.getUrlRoomsFromSameWorld("en", clientRoomUrl);
} else {
tabUrlRooms = [clientRoomUrl];
}
+22 -2
View File
@@ -2,6 +2,14 @@
# yarn lockfile v1
"@anatine/zod-openapi@^1.3.0":
version "1.3.0"
resolved "https://registry.yarnpkg.com/@anatine/zod-openapi/-/zod-openapi-1.3.0.tgz#b5b38c3d821b79674226aa7b327c88c371860d0d"
integrity sha512-l54DypUdDsIq1Uwjv4ib9IBkTXMKZQLUj7qvdFL51EExC5LdSSqOlTOyaVVZZGYgWPKM7ZjGklhdoknLz4EC+w==
dependencies:
ts-deepmerge "^1.1.0"
validator "^13.7.0"
"@apidevtools/json-schema-ref-parser@^9.0.6":
version "9.0.9"
resolved "https://registry.yarnpkg.com/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-9.0.9.tgz#d720f9256e3609621280584f2b47ae165359268b"
@@ -1926,6 +1934,13 @@ onetime@^5.1.0, onetime@^5.1.2:
dependencies:
mimic-fn "^2.1.0"
openapi3-ts@^2.0.2:
version "2.0.2"
resolved "https://registry.yarnpkg.com/openapi3-ts/-/openapi3-ts-2.0.2.tgz#a200dd838bf24c9086c8eedcfeb380b7eb31e82a"
integrity sha512-TxhYBMoqx9frXyOgnRHufjQfPXomTIHYKhSKJ6jHfj13kS8OEIhvmE8CTuQyKtjjWttAjX5DPxM1vmalEpo8Qw==
dependencies:
yaml "^1.10.2"
openid-client@^4.7.4:
version "4.9.1"
resolved "https://registry.yarnpkg.com/openid-client/-/openid-client-4.9.1.tgz#4f00a9d1566c0fa08f0dd5986cf0e6b1e5d14186"
@@ -2563,6 +2578,11 @@ tree-kill@^1.2.2:
resolved "https://registry.yarnpkg.com/tree-kill/-/tree-kill-1.2.2.tgz#4ca09a9092c88b73a7cdc5e8a01b507b0790a0cc"
integrity sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==
ts-deepmerge@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/ts-deepmerge/-/ts-deepmerge-1.1.0.tgz#4236ae102199affe2e77690dcf198a420160eef2"
integrity sha512-VvwaV/6RyYMwT9d8dClmfHIsG2PCdm6WY430QKOIbPRR50Y/1Q2ilp4i2XEZeHFcNqfaYnAQzpyUC6XA0AqqBg==
ts-node-dev@^1.1.8:
version "1.1.8"
resolved "https://registry.yarnpkg.com/ts-node-dev/-/ts-node-dev-1.1.8.tgz#95520d8ab9d45fffa854d6668e2f8f9286241066"
@@ -2689,7 +2709,7 @@ v8-compile-cache@^2.0.3:
resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz#2de19618c66dc247dcfb6f99338035d8245a2cee"
integrity sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==
validator@^13.6.0:
validator@^13.6.0, validator@^13.7.0:
version "13.7.0"
resolved "https://registry.yarnpkg.com/validator/-/validator-13.7.0.tgz#4f9658ba13ba8f3d82ee881d3516489ea85c0857"
integrity sha512-nYXQLCBkpJ8X6ltALua9dRrZDHVYxjJ1wgskNt1lH9fzGjs3tgojGSCBjmEPwkWS1y29+DrizMTW19Pr9uB2nw==
@@ -2796,7 +2816,7 @@ yaml@2.0.0-1:
resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.0.0-1.tgz#8c3029b3ee2028306d5bcf396980623115ff8d18"
integrity sha512-W7h5dEhywMKenDJh2iX/LABkbFnBxasD27oyXWDS/feDsxiw0dD5ncXdYXgkvAsXIY2MpW/ZKkr9IU30DBdMNQ==
yaml@^1.10.0:
yaml@^1.10.0, yaml@^1.10.2:
version "1.10.2"
resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.2.tgz#2301c5ffbf12b467de8da2333a459e29e7920e4b"
integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==