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

This commit is contained in:
_Bastler 2021-11-17 09:28:11 +01:00
commit a6891b704c
14 changed files with 90 additions and 98 deletions

View File

@ -110,4 +110,4 @@ Vagrant destroy
> The production environment of Partey is based on a single-domain deployment with some changed, minimized *Dockerfile*s based on the fork [workadventure-xce](https://github.com/workadventure-xce/workadventure-xce). An example can be found in [contrib/docker/docker-compose.single-domain.prod.yaml](/contrib/docker/docker-compose.single-domain.prod.yaml). To create a non-single-domain based environment, please adopt settings from the below *Work Adventure* provided file yourself. The changed *Dockerfile*s won't work directly with the file below! > The production environment of Partey is based on a single-domain deployment with some changed, minimized *Dockerfile*s based on the fork [workadventure-xce](https://github.com/workadventure-xce/workadventure-xce). An example can be found in [contrib/docker/docker-compose.single-domain.prod.yaml](/contrib/docker/docker-compose.single-domain.prod.yaml). To create a non-single-domain based environment, please adopt settings from the below *Work Adventure* provided file yourself. The changed *Dockerfile*s won't work directly with the file below!
The way you set up your production environment will highly depend on your servers. The way you set up your production environment will highly depend on your servers.
We provide a production ready `docker-compose` file that you can use as a good starting point in the [contrib/docker](https://github.com/thecodingmachine/workadventure/tree/master/contrib/docker) director We provide a production ready `docker-compose` file that you can use as a good starting point in the [contrib/docker](https://github.com/thecodingmachine/workadventure/tree/master/contrib/docker) directory.

View File

@ -4,7 +4,6 @@
let gameScene = gameManager.getCurrentGameScene(); let gameScene = gameManager.getCurrentGameScene();
let HTMLShareLink: HTMLInputElement;
let expandedMapCopyright = false; let expandedMapCopyright = false;
let expandedTilesetCopyright = false; let expandedTilesetCopyright = false;
@ -38,35 +37,9 @@
} }
} }
}) })
function copyLink() {
HTMLShareLink.select();
document.execCommand('copy');
}
async function shareLink() {
const shareData = {url: location.toString()};
try {
await navigator.share(shareData);
} catch (err) {
console.error('Error: ' + err);
copyLink();
}
}
</script> </script>
<div class="about-room-main"> <div class="about-room-main">
<section class="share-url not-mobile">
<h3>Share the link of the room !</h3>
<input type="text" readonly bind:this={HTMLShareLink} value={location.toString()}>
<button type="button" class="nes-btn is-primary" on:click={copyLink}>Copy</button>
</section>
<section class="is-mobile">
<h3>Share the link of the room !</h3>
<input type="hidden" readonly bind:this={HTMLShareLink} value={location.toString()}>
<button type="button" class="nes-btn is-primary" on:click={shareLink}>Share</button>
</section>
<h2>Information on the map</h2> <h2>Information on the map</h2>
<section class="container-overflow"> <section class="container-overflow">
<h3>{mapName}</h3> <h3>{mapName}</h3>
@ -93,24 +66,6 @@
div.about-room-main { div.about-room-main {
height: calc(100% - 56px); height: calc(100% - 56px);
section.share-url {
text-align: center;
margin-bottom: 20px;
input {
width: 85%;
border-radius: 32px;
padding: 3px;
}
input::selection {
background-color: #209cee;
}
}
section.is-mobile {
display: none;
}
h2, h3 { h2, h3 {
width: 100%; width: 100%;
text-align: center; text-align: center;
@ -126,21 +81,11 @@
margin: 0; margin: 0;
padding: 0; padding: 0;
overflow-y: auto; overflow-y: auto;
} }
} }
@media only screen and (max-width: 800px), only screen and (max-height: 800px) { @media only screen and (max-width: 800px), only screen and (max-height: 800px) {
div.about-room-main { div.about-room-main {
section.share-url.not-mobile {
display: none;
}
section.is-mobile {
display: block;
text-align: center;
margin-bottom: 20px;
}
section.container-overflow { section.container-overflow {
height: calc(100% - 120px); height: calc(100% - 120px);
} }

View File

@ -1,8 +1,8 @@
<script lang="ts"> <script lang="ts">
let HTMLShareLink: HTMLInputElement;
function copyLink() { function copyLink() {
HTMLShareLink.select(); const input: HTMLInputElement = document.getElementById('input-share-link') as HTMLInputElement;
input.focus();
input.select();
document.execCommand('copy'); document.execCommand('copy');
} }
@ -22,12 +22,12 @@
<section class="container-overflow"> <section class="container-overflow">
<section class="share-url not-mobile"> <section class="share-url not-mobile">
<h3>Share the link of the room !</h3> <h3>Share the link of the room !</h3>
<input type="text" readonly bind:this={HTMLShareLink} value={location.toString()}> <input type="text" readonly id="input-share-link" value={location.toString()}>
<button type="button" class="nes-btn is-primary" on:click={copyLink}>Copy</button> <button type="button" class="nes-btn is-primary" on:click={copyLink}>Copy</button>
</section> </section>
<section class="is-mobile"> <section class="is-mobile">
<h3>Share the link of the room !</h3> <h3>Share the link of the room !</h3>
<input type="hidden" readonly bind:this={HTMLShareLink} value={location.toString()}> <input type="hidden" readonly id="input-share-link" value={location.toString()}>
<button type="button" class="nes-btn is-primary" on:click={shareLink}>Share</button> <button type="button" class="nes-btn is-primary" on:click={shareLink}>Share</button>
</section> </section>
</section> </section>

View File

@ -227,7 +227,7 @@ class ConnectionManager {
public async anonymousLogin(isBenchmark: boolean = false): Promise<void> { public async anonymousLogin(isBenchmark: boolean = false): Promise<void> {
const data = await Axios.post(`${PUSHER_URL}/anonymLogin`).then((res) => res.data); const data = await Axios.post(`${PUSHER_URL}/anonymLogin`).then((res) => res.data);
this.localUser = new LocalUser(data.userUuid, []); this.localUser = new LocalUser(data.userUuid, [], data.email);
this.authToken = data.authToken; this.authToken = data.authToken;
if (!isBenchmark) { if (!isBenchmark) {
// In benchmark, we don't have a local storage. // In benchmark, we don't have a local storage.
@ -315,7 +315,7 @@ class ConnectionManager {
} }
} }
const { authToken, userUuid, textures, email, username } = await Axios.get(`${PUSHER_URL}/login-callback`, { const { authToken, userUuid, textures, email, username } = await Axios.get(`${PUSHER_URL}/login-callback`, {
params: { code, nonce, token }, params: { code, nonce, token, playUri: this.currentRoom?.key },
}).then((res) => res.data); }).then((res) => res.data);
localUserStore.setAuthToken(authToken); localUserStore.setAuthToken(authToken);
this.localUser = new LocalUser(userUuid, textures, email); this.localUser = new LocalUser(userUuid, textures, email);

View File

@ -14,7 +14,7 @@ export interface RoomRedirect {
export class Room { export class Room {
public readonly id: string; public readonly id: string;
public readonly isPublic: boolean; public readonly isPublic: boolean;
private _authenticationMandatory: boolean = DISABLE_ANONYMOUS as boolean; private _authenticationMandatory: boolean = DISABLE_ANONYMOUS;
private _iframeAuthentication?: string = OPID_LOGIN_SCREEN_PROVIDER; private _iframeAuthentication?: string = OPID_LOGIN_SCREEN_PROVIDER;
private _mapUrl: string | undefined; private _mapUrl: string | undefined;
private _textures: CharacterTexture[] | undefined; private _textures: CharacterTexture[] | undefined;
@ -89,27 +89,37 @@ export class Room {
} }
private async getMapDetail(): Promise<MapDetail | RoomRedirect> { private async getMapDetail(): Promise<MapDetail | RoomRedirect> {
const result = await Axios.get(`${PUSHER_URL}/map`, { try {
params: { const result = await Axios.get(`${PUSHER_URL}/map`, {
playUri: this.roomUrl.toString(), params: {
authToken: localUserStore.getAuthToken(), playUri: this.roomUrl.toString(),
}, authToken: localUserStore.getAuthToken(),
}); },
});
const data = result.data; const data = result.data;
if (data.redirectUrl) { if (data.redirectUrl) {
return { return {
redirectUrl: data.redirectUrl as string, redirectUrl: data.redirectUrl as string,
}; };
}
console.log("Map ", this.id, " resolves to URL ", data.mapUrl);
this._mapUrl = data.mapUrl;
this._textures = data.textures;
this._group = data.group;
this._authenticationMandatory = data.authenticationMandatory || DISABLE_ANONYMOUS;
this._iframeAuthentication = data.iframeAuthentication || OPID_LOGIN_SCREEN_PROVIDER;
this._contactPage = data.contactPage || CONTACT_URL;
return new MapDetail(data.mapUrl, data.textures);
} catch (e) {
console.error("Error => getMapDetail", e, e.response);
//TODO fix me and manage Error class
if (e.response?.data === "Token decrypted error") {
localUserStore.setAuthToken(null);
window.location.assign("/login");
}
throw e;
} }
console.log("Map ", this.id, " resolves to URL ", data.mapUrl);
this._mapUrl = data.mapUrl;
this._textures = data.textures;
this._group = data.group;
this._authenticationMandatory = data.authenticationMandatory || (DISABLE_ANONYMOUS as boolean);
this._iframeAuthentication = data.iframeAuthentication || OPID_LOGIN_SCREEN_PROVIDER;
this._contactPage = data.contactPage || CONTACT_URL;
return new MapDetail(data.mapUrl, data.textures);
} }
/** /**

View File

@ -23,7 +23,7 @@ export const CONTACT_URL = process.env.CONTACT_URL || undefined;
export const PROFILE_URL = process.env.PROFILE_URL || undefined; export const PROFILE_URL = process.env.PROFILE_URL || undefined;
export const POSTHOG_API_KEY: string = (process.env.POSTHOG_API_KEY as string) || ""; export const POSTHOG_API_KEY: string = (process.env.POSTHOG_API_KEY as string) || "";
export const POSTHOG_URL = process.env.POSTHOG_URL || undefined; export const POSTHOG_URL = process.env.POSTHOG_URL || undefined;
export const DISABLE_ANONYMOUS = process.env.DISABLE_ANONYMOUS || false; export const DISABLE_ANONYMOUS: boolean = process.env.DISABLE_ANONYMOUS === "true";
export const OPID_LOGIN_SCREEN_PROVIDER = process.env.OPID_LOGIN_SCREEN_PROVIDER; export const OPID_LOGIN_SCREEN_PROVIDER = process.env.OPID_LOGIN_SCREEN_PROVIDER;
export const isMobile = (): boolean => window.innerWidth <= 800 || window.innerHeight <= 600; export const isMobile = (): boolean => window.innerWidth <= 800 || window.innerHeight <= 600;

View File

@ -88,6 +88,7 @@ import { analyticsClient } from "../../Administration/AnalyticsClient";
import { get } from "svelte/store"; import { get } from "svelte/store";
import { contactPageStore } from "../../Stores/MenuStore"; import { contactPageStore } from "../../Stores/MenuStore";
import { GameMapProperties } from "./GameMapProperties"; import { GameMapProperties } from "./GameMapProperties";
import SpriteSheetFile = Phaser.Loader.FileTypes.SpriteSheetFile;
export interface GameSceneInitInterface { export interface GameSceneInitInterface {
initPosition: PointInterface | null; initPosition: PointInterface | null;
@ -295,7 +296,8 @@ export class GameScene extends DirtyScene {
} }
//once preloading is over, we don't want loading errors to crash the game, so we need to disable this behavior after preloading. //once preloading is over, we don't want loading errors to crash the game, so we need to disable this behavior after preloading.
if (this.preloading) { //if SpriteSheetFile (WOKA file) don't display error and give an access for user
if (this.preloading && !(file instanceof SpriteSheetFile)) {
//remove loader in progress //remove loader in progress
removeLoader(this); removeLoader(this);

View File

@ -91,6 +91,7 @@ export function checkSubMenuToShow() {
} }
subMenusStore.removeMenu(SubMenusInterface.aboutRoom); subMenusStore.removeMenu(SubMenusInterface.aboutRoom);
subMenusStore.removeMenu(SubMenusInterface.invite);
} }
export const customMenuIframe = new Map<string, { url: string; allowApi: boolean }>(); export const customMenuIframe = new Map<string, { url: string; allowApi: boolean }>();

View File

@ -63,13 +63,30 @@ export class AuthenticateController extends BaseController {
if (token != undefined) { if (token != undefined) {
try { try {
const authTokenData: AuthTokenData = jwtTokenManager.verifyJWTToken(token as string, false); const authTokenData: AuthTokenData = jwtTokenManager.verifyJWTToken(token as string, false);
//Get user data from Admin Back Office
//This is very important to create User Local in LocalStorage in WorkAdventure
const resUserData = await this.getUserByUserIdentifier(
authTokenData.identifier,
playUri as string,
IPAddress
);
if (authTokenData.accessToken == undefined) { if (authTokenData.accessToken == undefined) {
//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 }));
}
throw Error("Token cannot to be check on Hydra"); throw Error("Token cannot to be check on Hydra");
} }
const resCheckTokenAuth = await openIDClient.checkTokenAuth(authTokenData.accessToken); const resCheckTokenAuth = await openIDClient.checkTokenAuth(authTokenData.accessToken);
res.writeStatus("200"); res.writeStatus("200");
this.addCorsHeaders(res); this.addCorsHeaders(res);
return res.end(JSON.stringify({ ...resCheckTokenAuth, username: authTokenData.username, authToken: token })); return res.end(JSON.stringify({ ...resCheckTokenAuth, ...resUserData, username: authTokenData.username, authToken: token }));
} catch (err) { } catch (err) {
console.info("User was not connected", err); console.info("User was not connected", err);
} }
@ -81,7 +98,7 @@ export class AuthenticateController extends BaseController {
if (!sub) { if (!sub) {
throw new Error("No sub in the response"); throw new Error("No sub in the response");
} }
const authToken = jwtTokenManager.createAuthToken(sub, userInfo.access_token, userInfo.username); const authToken = jwtTokenManager.createAuthToken(sub, userInfo?.access_token, userInfo?.username);
//Get user data from Admin Back Office //Get user data from Admin Back Office
//This is very important to create User Local in LocalStorage in WorkAdventure //This is very important to create User Local in LocalStorage in WorkAdventure
@ -249,7 +266,14 @@ export class AuthenticateController extends BaseController {
playUri: string, playUri: string,
IPAddress: string IPAddress: string
): Promise<FetchMemberDataByUuidResponse | object> { ): Promise<FetchMemberDataByUuidResponse | object> {
let data: FetchMemberDataByUuidResponse | object = {}; let data: FetchMemberDataByUuidResponse = {
email: email,
userUuid: email,
tags: [],
messages: [],
visitCardUrl: null,
textures: [],
};
try { try {
data = await adminApi.fetchMemberDataByUuid(email, playUri, IPAddress); data = await adminApi.fetchMemberDataByUuid(email, playUri, IPAddress);
} catch (err) { } catch (err) {

View File

@ -1,10 +1,11 @@
import { HttpResponse } from "uWebSockets.js"; import { HttpResponse } from "uWebSockets.js";
import { FRONT_URL } from "../Enum/EnvironmentVariable";
export class BaseController { export class BaseController {
protected addCorsHeaders(res: HttpResponse): void { protected addCorsHeaders(res: HttpResponse): void {
res.writeHeader("access-control-allow-headers", "Origin, X-Requested-With, Content-Type, Accept"); 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-methods", "GET, POST, OPTIONS, PUT, PATCH, DELETE");
res.writeHeader("access-control-allow-origin", "*"); res.writeHeader("access-control-allow-origin", FRONT_URL);
} }
/** /**

View File

@ -189,6 +189,7 @@ export class IoSocketController {
let memberTextures: CharacterTexture[] = []; let memberTextures: CharacterTexture[] = [];
const room = await socketManager.getOrCreateRoom(roomId); const room = await socketManager.getOrCreateRoom(roomId);
let userData: FetchMemberDataByUuidResponse = { let userData: FetchMemberDataByUuidResponse = {
email: userIdentifier,
userUuid: userIdentifier, userUuid: userIdentifier,
tags: [], tags: [],
visitCardUrl: null, visitCardUrl: null,

View File

@ -2,7 +2,7 @@ import { HttpRequest, HttpResponse, TemplatedApp } from "uWebSockets.js";
import { BaseController } from "./BaseController"; import { BaseController } from "./BaseController";
import { parse } from "query-string"; import { parse } from "query-string";
import { adminApi } from "../Services/AdminApi"; import { adminApi } from "../Services/AdminApi";
import { ADMIN_API_URL, DISABLE_ANONYMOUS } from "../Enum/EnvironmentVariable"; import { ADMIN_API_URL, DISABLE_ANONYMOUS, FRONT_URL } from "../Enum/EnvironmentVariable";
import { GameRoomPolicyTypes } from "../Model/PusherRoom"; import { GameRoomPolicyTypes } from "../Model/PusherRoom";
import { isMapDetailsData, MapDetailsData } from "../Services/AdminApi/MapDetailsData"; import { isMapDetailsData, MapDetailsData } from "../Services/AdminApi/MapDetailsData";
import { socketManager } from "../Services/SocketManager"; import { socketManager } from "../Services/SocketManager";
@ -21,7 +21,6 @@ export class MapController extends BaseController {
getMapUrl() { getMapUrl() {
this.App.options("/map", (res: HttpResponse, req: HttpRequest) => { this.App.options("/map", (res: HttpResponse, req: HttpRequest) => {
this.addCorsHeaders(res); this.addCorsHeaders(res);
res.end(); res.end();
}); });
@ -81,10 +80,18 @@ export class MapController extends BaseController {
authTokenData = jwtTokenManager.verifyJWTToken(query.authToken as string); authTokenData = jwtTokenManager.verifyJWTToken(query.authToken as string);
userId = authTokenData.identifier; userId = authTokenData.identifier;
} catch (e) { } catch (e) {
// Decode token, in this case we don't need to create new token. try {
authTokenData = jwtTokenManager.verifyJWTToken(query.authToken as string, true); // Decode token, in this case we don't need to create new token.
userId = authTokenData.identifier; authTokenData = jwtTokenManager.verifyJWTToken(query.authToken as string, true);
console.info("JWT expire, but decoded", userId); userId = authTokenData.identifier;
console.info("JWT expire, but decoded", userId);
} catch (e) {
// The token was not good, redirect user on login page
res.writeStatus("500");
res.writeHeader("Access-Control-Allow-Origin", FRONT_URL);
res.end("Token decrypted error");
return;
}
} }
} }
const mapDetails = await adminApi.fetchMapDetails(query.playUri as string, userId); const mapDetails = await adminApi.fetchMapDetails(query.playUri as string, userId);

View File

@ -12,14 +12,14 @@ const SECRET_JITSI_KEY = process.env.SECRET_JITSI_KEY || "";
const PUSHER_HTTP_PORT = parseInt(process.env.PUSHER_HTTP_PORT || "8080") || 8080; const PUSHER_HTTP_PORT = parseInt(process.env.PUSHER_HTTP_PORT || "8080") || 8080;
export const SOCKET_IDLE_TIMER = parseInt(process.env.SOCKET_IDLE_TIMER as string) || 120; // maximum time (in second) without activity before a socket is closed. Should be greater than 60 seconds in order to cope for Chrome intensive throttling (https://developer.chrome.com/blog/timer-throttling-in-chrome-88/#intensive-throttling) export const SOCKET_IDLE_TIMER = parseInt(process.env.SOCKET_IDLE_TIMER as string) || 120; // maximum time (in second) without activity before a socket is closed. Should be greater than 60 seconds in order to cope for Chrome intensive throttling (https://developer.chrome.com/blog/timer-throttling-in-chrome-88/#intensive-throttling)
export const PUSHER_FORCE_ROOM_UPDATE = process.env.PUSHER_FORCE_ROOM_UPDATE ? process.env.PUSHER_FORCE_ROOM_UPDATE == "true" : false;
export const FRONT_URL = process.env.FRONT_URL || "http://localhost"; export const FRONT_URL = process.env.FRONT_URL || "http://localhost";
export const OPID_CLIENT_ID = process.env.OPID_CLIENT_ID || ""; export const OPID_CLIENT_ID = process.env.OPID_CLIENT_ID || "";
export const OPID_CLIENT_SECRET = process.env.OPID_CLIENT_SECRET || ""; export const OPID_CLIENT_SECRET = process.env.OPID_CLIENT_SECRET || "";
export const OPID_CLIENT_ISSUER = process.env.OPID_CLIENT_ISSUER || ""; 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_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_PROFILE_SCREEN_PROVIDER = process.env.OPID_PROFILE_SCREEN_PROVIDER || ADMIN_URL + "/profile";
export const DISABLE_ANONYMOUS = process.env.DISABLE_ANONYMOUS || false; export const DISABLE_ANONYMOUS: boolean = process.env.DISABLE_ANONYMOUS === "true";
export const PUSHER_FORCE_ROOM_UPDATE = process.env.PUSHER_FORCE_ROOM_UPDATE ? process.env.PUSHER_FORCE_ROOM_UPDATE == "true" : false;
export { export {
SECRET_KEY, SECRET_KEY,

View File

@ -22,6 +22,7 @@ export interface AdminBannedData {
} }
export interface FetchMemberDataByUuidResponse { export interface FetchMemberDataByUuidResponse {
email: string;
userUuid: string; userUuid: string;
tags: string[]; tags: string[];
visitCardUrl: string | null; visitCardUrl: string | null;