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; class AdminApi implements AdminInterface { async fetchMapDetails( playUri: string, authToken?: string, locale?: string ): Promise { 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>(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 { /** * @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>(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 { /** * @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 { /** * @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 { /** * @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 { await Axios.get(ADMIN_API_URL + `/oauth/logout?token=${token}`); } } export const adminApi = new AdminApi();