Files
partey_workadventure/pusher/src/Services/AdminApi.ts
T
César Cardinale 5016c85ca4 Add swagger doc for the link pusher -> admin (#2146)
* Dissociate admin API from external API
* Add adminAPI doc for self-hosted
2022-05-11 17:43:25 +02:00

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();