5016c85ca4
* Dissociate admin API from external API * Add adminAPI doc for self-hosted
480 lines
18 KiB
TypeScript
480 lines
18 KiB
TypeScript
import { ADMIN_API_TOKEN, ADMIN_API_URL, OPID_PROFILE_SCREEN_PROVIDER } from "../Enum/EnvironmentVariable";
|
|
import Axios, { AxiosResponse } from "axios";
|
|
import { isMapDetailsData, MapDetailsData } from "../Messages/JsonMessages/MapDetailsData";
|
|
import { isRoomRedirect, RoomRedirect } from "../Messages/JsonMessages/RoomRedirect";
|
|
import { AdminApiData, isAdminApiData } from "../Messages/JsonMessages/AdminApiData";
|
|
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";
|
|
import { extendApi } from "@anatine/zod-openapi";
|
|
|
|
export interface AdminBannedData {
|
|
is_banned: boolean;
|
|
message: string;
|
|
}
|
|
|
|
export const isFetchMemberDataByUuidResponse = z.object({
|
|
// @ts-ignore
|
|
email: extendApi(z.string(), {
|
|
description: "The email of the fetched user, it can be an email, an uuid or undefined.",
|
|
example: "example@workadventu.re",
|
|
}),
|
|
userUuid: extendApi(z.string(), {
|
|
description: "The uuid of the fetched user, it can be an email, an uuid or undefined.",
|
|
example: "998ce839-3dea-4698-8b41-ebbdf7688ad9",
|
|
}),
|
|
tags: extendApi(z.array(z.string()), {
|
|
description: "List of tags related to the user fetched.",
|
|
example: ["editor"],
|
|
}),
|
|
visitCardUrl: extendApi(z.nullable(z.string()), {
|
|
description: "URL of the visitCard of the user fetched.",
|
|
example: "https://mycompany.com/contact/me",
|
|
}),
|
|
textures: extendApi(z.array(isWokaDetail), {
|
|
description: "This data represents the textures (WOKA) that will be available to users.",
|
|
$ref: "#/definitions/WokaDetail",
|
|
}),
|
|
messages: extendApi(z.array(z.unknown()), {
|
|
description:
|
|
"Sets messages that will be displayed when the user logs in to the WA room. These messages are used for ban or ban warning.",
|
|
}),
|
|
|
|
anonymous: extendApi(z.optional(z.boolean()), {
|
|
description: "Defines whether it is possible to login as anonymous on a WorkAdventure room.",
|
|
example: false,
|
|
}),
|
|
userRoomToken: extendApi(z.optional(z.string()), { description: "", example: "" }),
|
|
});
|
|
|
|
export type FetchMemberDataByUuidResponse = z.infer<typeof isFetchMemberDataByUuidResponse>;
|
|
|
|
class AdminApi implements AdminInterface {
|
|
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 } = {
|
|
playUri,
|
|
userId,
|
|
};
|
|
|
|
/**
|
|
* @openapi
|
|
* /api/map:
|
|
* get:
|
|
* tags: ["AdminAPI"]
|
|
* description: Returns a map mapping map name to file name of the map
|
|
* security:
|
|
* - Bearer: []
|
|
* produces:
|
|
* - "application/json"
|
|
* parameters:
|
|
* - name: "playUri"
|
|
* in: "query"
|
|
* description: "The full URL of WorkAdventure"
|
|
* required: true
|
|
* type: "string"
|
|
* example: "http://play.workadventure.localhost/@/teamSlug/worldSLug/roomSlug"
|
|
* - name: "userId"
|
|
* in: "query"
|
|
* description: "The identifier of the current user \n It can be undefined or an uuid or an email"
|
|
* type: "string"
|
|
* example: "998ce839-3dea-4698-8b41-ebbdf7688ad9"
|
|
* responses:
|
|
* 200:
|
|
* description: The details of the map
|
|
* schema:
|
|
* $ref: "#/definitions/MapDetailsData"
|
|
* 401:
|
|
* description: Error while retrieving the data because you are not authorized
|
|
* schema:
|
|
* $ref: '#/definitions/ErrorApiRedirectData'
|
|
* 403:
|
|
* description: Error while retrieving the data because you are not authorized
|
|
* schema:
|
|
* $ref: '#/definitions/ErrorApiUnauthorizedData'
|
|
* 404:
|
|
* description: Error while retrieving the data
|
|
* schema:
|
|
* $ref: '#/definitions/ErrorApiErrorData'
|
|
*
|
|
*/
|
|
const res = await Axios.get<unknown, AxiosResponse<unknown>>(ADMIN_API_URL + "/api/map", {
|
|
headers: { Authorization: `${ADMIN_API_TOKEN}`, "Accept-Language": locale ?? "en" },
|
|
params,
|
|
});
|
|
|
|
const mapDetailData = isMapDetailsData.safeParse(res.data);
|
|
const roomRedirect = isRoomRedirect.safeParse(res.data);
|
|
|
|
if (mapDetailData.success) {
|
|
return mapDetailData.data;
|
|
}
|
|
|
|
if (roomRedirect.success) {
|
|
return roomRedirect.data;
|
|
}
|
|
|
|
console.error(mapDetailData.error.issues);
|
|
console.error(roomRedirect.error.issues);
|
|
throw new Error(
|
|
"Invalid answer received from the admin for the /api/map endpoint. Received: " + JSON.stringify(res.data)
|
|
);
|
|
}
|
|
|
|
async fetchMemberDataByUuid(
|
|
userIdentifier: string,
|
|
playUri: string,
|
|
ipAddress: string,
|
|
characterLayers: string[],
|
|
locale?: string
|
|
): Promise<FetchMemberDataByUuidResponse> {
|
|
/**
|
|
* @openapi
|
|
* /api/room/access:
|
|
* get:
|
|
* tags: ["AdminAPI"]
|
|
* description: Returns the member's information if he can access this room
|
|
* security:
|
|
* - Bearer: []
|
|
* produces:
|
|
* - "application/json"
|
|
* parameters:
|
|
* - name: "userIdentifier"
|
|
* in: "query"
|
|
* description: "The identifier of the current user \n It can be undefined or an uuid or an email"
|
|
* type: "string"
|
|
* example: "998ce839-3dea-4698-8b41-ebbdf7688ad9"
|
|
* - name: "playUri"
|
|
* in: "query"
|
|
* description: "The full URL of WorkAdventure"
|
|
* required: true
|
|
* type: "string"
|
|
* example: "http://play.workadventure.localhost/@/teamSlug/worldSLug/roomSlug"
|
|
* - name: "ipAddress"
|
|
* in: "query"
|
|
* description: "IP Address of the user logged in, allows you to check whether a user has been banned or not"
|
|
* required: true
|
|
* type: "string"
|
|
* example: "127.0.0.1"
|
|
* - name: "characterLayers"
|
|
* in: "query"
|
|
* type: "array"
|
|
* items:
|
|
* type: string
|
|
* example: ["male1"]
|
|
* responses:
|
|
* 200:
|
|
* description: The details of the member
|
|
* schema:
|
|
* $ref: "#/definitions/FetchMemberDataByUuidResponse"
|
|
* 401:
|
|
* description: Error while retrieving the data because you are not authorized
|
|
* schema:
|
|
* $ref: '#/definitions/ErrorApiRedirectData'
|
|
* 403:
|
|
* description: Error while retrieving the data because you are not authorized
|
|
* schema:
|
|
* $ref: '#/definitions/ErrorApiUnauthorizedData'
|
|
* 404:
|
|
* description: Error while retrieving the data
|
|
* schema:
|
|
* $ref: '#/definitions/ErrorApiErrorData'
|
|
*
|
|
*/
|
|
const res = await Axios.get<unknown, AxiosResponse<unknown>>(ADMIN_API_URL + "/api/room/access", {
|
|
params: {
|
|
userIdentifier,
|
|
playUri,
|
|
ipAddress,
|
|
characterLayers,
|
|
},
|
|
headers: { Authorization: `${ADMIN_API_TOKEN}`, "Accept-Language": locale ?? "en" },
|
|
paramsSerializer: (p) => {
|
|
return qs.stringify(p, { arrayFormat: "brackets" });
|
|
},
|
|
});
|
|
|
|
const fetchMemberDataByUuidResponse = isFetchMemberDataByUuidResponse.safeParse(res.data);
|
|
|
|
if (fetchMemberDataByUuidResponse.success) {
|
|
return fetchMemberDataByUuidResponse.data;
|
|
}
|
|
|
|
console.error(fetchMemberDataByUuidResponse.error.issues);
|
|
throw new Error(
|
|
"Invalid answer received from the admin for the /api/room/access endpoint. Received: " +
|
|
JSON.stringify(res.data)
|
|
);
|
|
}
|
|
|
|
async fetchMemberDataByToken(
|
|
organizationMemberToken: string,
|
|
playUri: string | null,
|
|
locale?: string
|
|
): Promise<AdminApiData> {
|
|
/**
|
|
* @openapi
|
|
* /api/login-url/{organizationMemberToken}:
|
|
* get:
|
|
* tags: ["AdminAPI"]
|
|
* description: Returns a member from the token
|
|
* security:
|
|
* - Bearer: []
|
|
* produces:
|
|
* - "application/json"
|
|
* parameters:
|
|
* - name: "organizationMemberToken"
|
|
* in: "path"
|
|
* description: "The token of member in the organization"
|
|
* type: "string"
|
|
* - name: "playUri"
|
|
* in: "query"
|
|
* description: "The full URL of WorkAdventure"
|
|
* required: true
|
|
* type: "string"
|
|
* example: "http://play.workadventure.localhost/@/teamSlug/worldSLug/roomSlug"
|
|
* responses:
|
|
* 200:
|
|
* description: The details of the member
|
|
* schema:
|
|
* $ref: "#/definitions/AdminApiData"
|
|
* 401:
|
|
* description: Error while retrieving the data because you are not authorized
|
|
* schema:
|
|
* $ref: '#/definitions/ErrorApiRedirectData'
|
|
* 404:
|
|
* description: Error while retrieving the data
|
|
* schema:
|
|
* $ref: '#/definitions/ErrorApiErrorData'
|
|
*
|
|
*/
|
|
//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}`, "Accept-Language": locale ?? "en" },
|
|
});
|
|
|
|
const adminApiData = isAdminApiData.safeParse(res.data);
|
|
|
|
if (adminApiData.success) {
|
|
return adminApiData.data;
|
|
}
|
|
|
|
console.error(adminApiData.error.issues);
|
|
console.error("Message received from /api/login-url is not in the expected format. Message: ", res.data);
|
|
throw new Error("Message received from /api/login-url is not in the expected format.");
|
|
}
|
|
|
|
reportPlayer(
|
|
reportedUserUuid: string,
|
|
reportedUserComment: string,
|
|
reporterUserUuid: string,
|
|
reportWorldSlug: string,
|
|
locale?: string
|
|
) {
|
|
/**
|
|
* @openapi
|
|
* /api/report:
|
|
* post:
|
|
* tags: ["AdminAPI"]
|
|
* description: Report one user with a comment
|
|
* security:
|
|
* - Bearer: []
|
|
* produces:
|
|
* - "application/json"
|
|
* parameters:
|
|
* - name: "reportedUserUuid"
|
|
* in: "query"
|
|
* description: "The identifier of the reported user \n It can be an uuid or an email"
|
|
* type: "string"
|
|
* example: "998ce839-3dea-4698-8b41-ebbdf7688ad9"
|
|
* - name: "reportedUserComment"
|
|
* in: "query"
|
|
* description: "The comment of the report"
|
|
* required: true
|
|
* type: "string"
|
|
* - name: "reporterUserUuid"
|
|
* in: "query"
|
|
* description: "The identifier of the reporter user \n It can be an uuid or an email"
|
|
* type: "string"
|
|
* example: "998ce839-3dea-4698-8b41-ebbdf7688ad8"
|
|
* - name: "reportWorldSlug"
|
|
* in: "query"
|
|
* description: "The slug of the world where the report is made"
|
|
* type: "string"
|
|
* example: "/@/teamSlug/worldSlug/roomSlug"
|
|
* responses:
|
|
* 200:
|
|
* description: The report has been successfully saved
|
|
*/
|
|
return Axios.post(
|
|
`${ADMIN_API_URL}/api/report`,
|
|
{
|
|
reportedUserUuid,
|
|
reportedUserComment,
|
|
reporterUserUuid,
|
|
reportWorldSlug,
|
|
},
|
|
{
|
|
headers: { Authorization: `${ADMIN_API_TOKEN}`, "Accept-Language": locale ?? "en" },
|
|
}
|
|
);
|
|
}
|
|
|
|
async verifyBanUser(
|
|
userUuid: string,
|
|
ipAddress: string,
|
|
roomUrl: string,
|
|
locale?: string
|
|
): Promise<AdminBannedData> {
|
|
/**
|
|
* @openapi
|
|
* /api/ban:
|
|
* get:
|
|
* tags: ["AdminAPI"]
|
|
* description: Check if user is banned or not
|
|
* security:
|
|
* - Bearer: []
|
|
* produces:
|
|
* - "application/json"
|
|
* parameters:
|
|
* - name: "ipAddress"
|
|
* in: "query"
|
|
* type: "string"
|
|
* required: true
|
|
* example: "127.0.0.1"
|
|
* - name: "token"
|
|
* in: "query"
|
|
* description: "The uuid of the user \n It can be an uuid or an email"
|
|
* type: "string"
|
|
* required: true
|
|
* example: "998ce839-3dea-4698-8b41-ebbdf7688ad8"
|
|
* - name: "roomUrl"
|
|
* in: "query"
|
|
* description: "The slug of the world where to check if the user is banned"
|
|
* type: "string"
|
|
* required: true
|
|
* example: "/@/teamSlug/worldSlug/roomSlug"
|
|
* responses:
|
|
* 200:
|
|
* description: The user is banned or not
|
|
* content:
|
|
* application/json:
|
|
* schema:
|
|
* type: array
|
|
* required:
|
|
* - is_banned
|
|
* properties:
|
|
* is_banned:
|
|
* type: boolean
|
|
* description: Whether the user is banned or not
|
|
* example: true
|
|
* 404:
|
|
* description: Error while retrieving the data
|
|
* schema:
|
|
* $ref: '#/definitions/ErrorApiErrorData'
|
|
*/
|
|
//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 +
|
|
"/api/ban" +
|
|
"?ipAddress=" +
|
|
encodeURIComponent(ipAddress) +
|
|
"&token=" +
|
|
encodeURIComponent(userUuid) +
|
|
"&roomUrl=" +
|
|
encodeURIComponent(roomUrl),
|
|
{ headers: { Authorization: `${ADMIN_API_TOKEN}`, "Accept-Language": locale ?? "en" } }
|
|
).then((data) => {
|
|
return data.data;
|
|
});
|
|
}
|
|
|
|
async getUrlRoomsFromSameWorld(roomUrl: string, locale?: string): Promise<string[]> {
|
|
/**
|
|
* @openapi
|
|
* /api/room/sameWorld:
|
|
* get:
|
|
* tags: ["AdminAPI"]
|
|
* description: Get all URLs of the rooms from the world specified
|
|
* security:
|
|
* - Bearer: []
|
|
* produces:
|
|
* - "application/json"
|
|
* parameters:
|
|
* - name: "roomUrl"
|
|
* in: "query"
|
|
* description: "The slug of the room"
|
|
* type: "string"
|
|
* required: true
|
|
* example: "/@/teamSlug/worldSlug/roomSlug"
|
|
* responses:
|
|
* 200:
|
|
* description: The list of URL of the rooms from the same world
|
|
* schema:
|
|
* type: array
|
|
* items:
|
|
* type: string
|
|
* description: URL of a room
|
|
* example: "http://example.com/@/teamSlug/worldSlug/room2Slug"
|
|
* 404:
|
|
* description: Error while retrieving the data
|
|
* schema:
|
|
* $ref: '#/definitions/ErrorApiErrorData'
|
|
*/
|
|
return Axios.get(ADMIN_API_URL + "/api/room/sameWorld" + "?roomUrl=" + encodeURIComponent(roomUrl), {
|
|
headers: { Authorization: `${ADMIN_API_TOKEN}`, "Accept-Language": locale ?? "en" },
|
|
}).then((data) => {
|
|
return data.data;
|
|
});
|
|
}
|
|
|
|
getProfileUrl(accessToken: string, playUri: string): string {
|
|
if (!OPID_PROFILE_SCREEN_PROVIDER) {
|
|
throw new Error("No admin backoffice set!");
|
|
}
|
|
return `${OPID_PROFILE_SCREEN_PROVIDER}?accessToken=${accessToken}&playUri=${playUri}`;
|
|
}
|
|
|
|
async logoutOauth(token: string): Promise<void> {
|
|
await Axios.get(ADMIN_API_URL + `/oauth/logout?token=${token}`);
|
|
}
|
|
}
|
|
|
|
export const adminApi = new AdminApi();
|