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

This commit is contained in:
David Négrier 2021-11-24 15:36:35 +01:00
commit a1107bd20e
71 changed files with 10373 additions and 520 deletions

View File

@ -22,6 +22,10 @@ MAX_USERNAME_LENGTH=8
OPID_CLIENT_ID=
OPID_CLIENT_SECRET=
OPID_CLIENT_ISSUER=
OPID_CLIENT_REDIRECT_URL=
OPID_LOGIN_SCREEN_PROVIDER=http://pusher.workadventure.localhost/login-screen
OPID_PROFILE_SCREEN_PROVIDER=
DISABLE_ANONYMOUS=
# If you want to have a contact page in your menu, you MUST set CONTACT_URL to the URL of the page that you want
CONTACT_URL=

51
.github/workflows/end_to_end_tests.yml vendored Normal file
View File

@ -0,0 +1,51 @@
# https://help.github.com/en/categories/automating-your-workflow-with-github-actions
name: "End to end tests"
on:
push:
branches:
- master
- develop
pull_request:
jobs:
end-to-end-tests:
name: "End-to-end testcafe tests"
runs-on: "ubuntu-latest"
steps:
- name: "Checkout"
uses: "actions/checkout@v2.0.0"
- name: "Setup .env file"
run: cp .env.template .env
- name: "Edit ownership of file for test cases"
run: sudo chown 1000:1000 -R .
- name: "Start environment"
run: docker-compose up -d
- name: "Wait for environment to build (and downloading testcafe image)"
run: (docker-compose -f docker-compose.testcafe.yml pull &) && docker-compose logs -f --tail=0 front | grep -m 1 "Compiled successfully"
- name: "Run tests"
run: docker-compose -f docker-compose.testcafe.yml up --exit-code-from testcafe
- name: Upload failed tests
if: ${{ failure() }}
uses: actions/upload-artifact@v2
with:
name: my-artifact
path: './tests/screenshots/'
- name: Display state
if: ${{ failure() }}
run: docker-compose ps
- name: Display logs
if: ${{ failure() }}
run: docker-compose logs

View File

@ -14,6 +14,7 @@ export const MAX_PER_GROUP = parseInt(process.env.MAX_PER_GROUP || "4");
export const REDIS_HOST = process.env.REDIS_HOST || undefined;
export const REDIS_PORT = parseInt(process.env.REDIS_PORT || "6379") || 6379;
export const REDIS_PASSWORD = process.env.REDIS_PASSWORD || undefined;
export const STORE_VARIABLES_FOR_LOCAL_MAPS = process.env.STORE_VARIABLES_FOR_LOCAL_MAPS === "true";
export {
MINIMUM_DISTANCE,

View File

@ -26,6 +26,7 @@ import { VariablesManager } from "../Services/VariablesManager";
import { ADMIN_API_URL } from "../Enum/EnvironmentVariable";
import { LocalUrlError } from "../Services/LocalUrlError";
import { emitErrorOnRoomSocket } from "../Services/MessageHelpers";
import { VariableError } from "../Services/VariableError";
import log from "../Services/Logger";
export type ConnectCallback = (user: User, group: Group) => void;
@ -337,6 +338,7 @@ export class GameRoom {
// First, let's check if "user" is allowed to modify the variable.
const variableManager = await this.getVariableManager();
try {
const readableBy = variableManager.setVariable(name, value, user);
// If the variable was not changed, let's not dispatch anything.
@ -362,6 +364,37 @@ export class GameRoom {
for (const socket of this.roomListeners) {
socket.write(batchMessage);
}
} catch (e) {
if (e instanceof VariableError) {
// Ok, we have an error setting a variable. Either the user is trying to hack the map... or the map
// is not up to date. So let's try to reload the map from scratch.
if (this.variableManagerLastLoad === undefined) {
throw e;
}
const lastLoaded = new Date().getTime() - this.variableManagerLastLoad.getTime();
if (lastLoaded < 10000) {
console.log(
'An error occurred while setting the "' +
name +
"\" variable. But we tried to reload the map less than 10 seconds ago, so let's fail."
);
// Do not try to reload if we tried to reload less than 10 seconds ago.
throw e;
}
// Reset the variable manager
this.variableManagerPromise = undefined;
this.mapPromise = undefined;
console.log(
'An error occurred while setting the "' + name + "\" variable. Let's reload the map and try again"
);
// Try to set the variable again!
await this.setVariable(name, value, user);
} else {
throw e;
}
}
}
public addZoneListener(call: ZoneSocket, x: number, y: number): Set<Movable> {
@ -450,9 +483,11 @@ export class GameRoom {
}
private variableManagerPromise: Promise<VariablesManager> | undefined;
private variableManagerLastLoad: Date | undefined;
private getVariableManager(): Promise<VariablesManager> {
if (!this.variableManagerPromise) {
this.variableManagerLastLoad = new Date();
this.variableManagerPromise = this.getMap()
.then((map) => {
const variablesManager = new VariablesManager(this.roomUrl, map);

View File

@ -5,13 +5,14 @@ import { promisify } from "util";
import { LocalUrlError } from "./LocalUrlError";
import { ITiledMap } from "@workadventure/tiled-map-type-guard";
import { isTiledMap } from "@workadventure/tiled-map-type-guard/dist";
import { STORE_VARIABLES_FOR_LOCAL_MAPS } from "../Enum/EnvironmentVariable";
import log from "./Logger";
class MapFetcher {
async fetchMap(mapUrl: string): Promise<ITiledMap> {
// Before trying to make the query, let's verify the map is actually on the open internet (and not a local test map)
if (await this.isLocalUrl(mapUrl)) {
if ((await this.isLocalUrl(mapUrl)) && !STORE_VARIABLES_FOR_LOCAL_MAPS) {
throw new LocalUrlError('URL for map "' + mapUrl + '" targets a local map');
}

View File

@ -0,0 +1,9 @@
/**
* Errors related to variable handling.
*/
export class VariableError extends Error {
constructor(message: string) {
super(message);
Object.setPrototypeOf(this, VariableError.prototype);
}
}

View File

@ -10,6 +10,7 @@ import {
import { User } from "_Model/User";
import { variablesRepository } from "./Repository/VariablesRepository";
import { redisClient } from "./RedisClient";
import { VariableError } from "./VariableError";
import log from "./Logger";
interface Variable {
@ -175,11 +176,13 @@ export class VariablesManager {
if (this.variableObjects) {
variableObject = this.variableObjects.get(name);
if (variableObject === undefined) {
throw new Error('Trying to set a variable "' + name + '" that is not defined as an object in the map.');
throw new VariableError(
'Trying to set a variable "' + name + '" that is not defined as an object in the map.'
);
}
if (variableObject.writableBy && !user.tags.includes(variableObject.writableBy)) {
throw new Error(
throw new VariableError(
'Trying to set a variable "' +
name +
'". User "' +

View File

@ -38,6 +38,7 @@ services:
JITSI_URL: $JITSI_URL
JITSI_PRIVATE_MODE: "$JITSI_PRIVATE_MODE"
PUSHER_URL: //pusher.${DOMAIN}
ICON_URL: //icon.${DOMAIN}
TURN_SERVER: "${TURN_SERVER}"
TURN_USER: "${TURN_USER}"
TURN_PASSWORD: "${TURN_PASSWORD}"
@ -98,3 +99,15 @@ services:
- "traefik.http.routers.back-ssl.service=back"
- "traefik.http.routers.back-ssl.tls.certresolver=myresolver"
restart: unless-stopped
icon:
image: matthiasluedtke/iconserver:v3.13.0
labels:
- "traefik.http.routers.icon.rule=Host(`icon.${DOMAIN}`)"
- "traefik.http.routers.icon.entryPoints=web,traefik"
- "traefik.http.services.icon.loadbalancer.server.port=8080"
- "traefik.http.routers.icon-ssl.rule=Host(`icon.${DOMAIN}`)"
- "traefik.http.routers.icon-ssl.entryPoints=websecure"
- "traefik.http.routers.icon-ssl.tls=true"
- "traefik.http.routers.icon-ssl.service=icon"
- "traefik.http.routers.icon-ssl.tls.certresolver=myresolver"

View File

@ -62,6 +62,7 @@
} + (if adminUrl != null then {
"ADMIN_API_URL": adminUrl,
"ADMIN_API_TOKEN": env.ADMIN_API_TOKEN,
"ADMIN_SOCKETS_TOKEN": env.ADMIN_SOCKETS_TOKEN,
} else {})
},
"front": {

View File

@ -30,6 +30,7 @@ services:
UPLOADER_URL: /uploader
ADMIN_URL: /admin
MAPS_URL: /maps
ICON_URL: /icon
STARTUP_COMMAND_1: ./templater.sh
STARTUP_COMMAND_2: yarn install
TURN_SERVER: "turn:localhost:3478,turns:localhost:5349"
@ -40,6 +41,7 @@ services:
TURN_USER: ""
TURN_PASSWORD: ""
START_ROOM_URL: "$START_ROOM_URL"
DISABLE_ANONYMOUS: "$DISABLE_ANONYMOUS"
command: yarn run start
volumes:
- ./front:/usr/src/app
@ -60,6 +62,8 @@ services:
environment:
DEBUG: "*"
STARTUP_COMMAND_1: yarn install
# wait for files generated by "messages" container to exists
STARTUP_COMMAND_2: while [ ! -f /usr/src/app/src/Messages/generated/messages_pb.js ]; do sleep 1; done
SECRET_JITSI_KEY: "$SECRET_JITSI_KEY"
SECRET_KEY: yourSecretKey
ADMIN_API_TOKEN: "$ADMIN_API_TOKEN"
@ -70,6 +74,9 @@ services:
OPID_CLIENT_ID: $OPID_CLIENT_ID
OPID_CLIENT_SECRET: $OPID_CLIENT_SECRET
OPID_CLIENT_ISSUER: $OPID_CLIENT_ISSUER
OPID_CLIENT_REDIRECT_URL: $OPID_CLIENT_REDIRECT_URL
OPID_PROFILE_SCREEN_PROVIDER: $OPID_PROFILE_SCREEN_PROVIDER
DISABLE_ANONYMOUS: $DISABLE_ANONYMOUS
volumes:
- ./pusher:/usr/src/app
labels:
@ -117,6 +124,8 @@ services:
environment:
DEBUG: "*"
STARTUP_COMMAND_1: yarn install
# wait for files generated by "messages" container to exists
STARTUP_COMMAND_2: while [ ! -f /usr/src/app/src/Messages/generated/messages_pb.js ]; do sleep 1; done
SECRET_KEY: yourSecretKey
SECRET_JITSI_KEY: "$SECRET_JITSI_KEY"
ALLOW_ARTILLERY: "true"
@ -177,6 +186,20 @@ services:
redis:
image: redis:6
icon:
image: matthiasluedtke/iconserver:v3.13.0
labels:
- "traefik.http.middlewares.strip-icon-prefix.stripprefix.prefixes=/icon"
- "traefik.http.routers.icon.rule=PathPrefix(`/icon`)"
- "traefik.http.routers.icon.middlewares=strip-icon-prefix@docker"
- "traefik.http.routers.icon.entryPoints=web"
- "traefik.http.services.icon.loadbalancer.server.port=8080"
- "traefik.http.routers.icon-ssl.rule=PathPrefix(`/icon`)"
- "traefik.http.routers.icon-ssl.middlewares=strip-icon-prefix@docker"
- "traefik.http.routers.icon-ssl.entryPoints=websecure"
- "traefik.http.routers.icon-ssl.tls=true"
- "traefik.http.routers.icon-ssl.service=icon"
# coturn:
# image: coturn/coturn:4.5.2
# command:

View File

@ -0,0 +1,12 @@
version: "3"
services:
testcafe:
image: testcafe/testcafe:1.17.1
working_dir: /tests
environment:
BROWSER: "chromium --use-fake-device-for-media-stream"
volumes:
- ./tests:/tests
- ./maps:/maps
# security_opt:
# - seccomp:unconfined

View File

@ -17,6 +17,12 @@ services:
- front
volumes:
- /var/run/docker.sock:/var/run/docker.sock
networks:
default:
aliases:
- 'play.workadventure.localhost'
- 'pusher.workadventure.localhost'
- 'maps.workadventure.localhost'
front:
image: thecodingmachine/nodejs:14
@ -29,6 +35,7 @@ services:
PUSHER_URL: //pusher.workadventure.localhost
UPLOADER_URL: //uploader.workadventure.localhost
ADMIN_URL: //workadventure.localhost
ICON_URL: //icon.workadventure.localhost
STARTUP_COMMAND_1: ./templater.sh
STARTUP_COMMAND_2: yarn install
STUN_SERVER: "stun:stun.l.google.com:19302"
@ -42,6 +49,8 @@ services:
START_ROOM_URL: "$START_ROOM_URL"
MAX_PER_GROUP: "$MAX_PER_GROUP"
MAX_USERNAME_LENGTH: "$MAX_USERNAME_LENGTH"
DISABLE_ANONYMOUS: "$DISABLE_ANONYMOUS"
OPID_LOGIN_SCREEN_PROVIDER: "$OPID_LOGIN_SCREEN_PROVIDER"
command: yarn run start
volumes:
- ./front:/usr/src/app
@ -60,6 +69,8 @@ services:
environment:
DEBUG: "socket:*"
STARTUP_COMMAND_1: yarn install
# wait for files generated by "messages" container to exists
STARTUP_COMMAND_2: while [ ! -f /usr/src/app/src/Messages/generated/messages_pb.js ]; do sleep 1; done
SECRET_JITSI_KEY: "$SECRET_JITSI_KEY"
SECRET_KEY: yourSecretKey
ADMIN_API_TOKEN: "$ADMIN_API_TOKEN"
@ -70,6 +81,9 @@ services:
OPID_CLIENT_ID: $OPID_CLIENT_ID
OPID_CLIENT_SECRET: $OPID_CLIENT_SECRET
OPID_CLIENT_ISSUER: $OPID_CLIENT_ISSUER
OPID_CLIENT_REDIRECT_URL: $OPID_CLIENT_REDIRECT_URL
OPID_PROFILE_SCREEN_PROVIDER: $OPID_PROFILE_SCREEN_PROVIDER
DISABLE_ANONYMOUS: $DISABLE_ANONYMOUS
volumes:
- ./pusher:/usr/src/app
labels:
@ -111,6 +125,8 @@ services:
environment:
DEBUG: "*"
STARTUP_COMMAND_1: yarn install
# wait for files generated by "messages" container to exists
STARTUP_COMMAND_2: while [ ! -f /usr/src/app/src/Messages/generated/messages_pb.js ]; do sleep 1; done
SECRET_KEY: yourSecretKey
SECRET_JITSI_KEY: "$SECRET_JITSI_KEY"
ALLOW_ARTILLERY: "true"
@ -121,6 +137,7 @@ services:
MAX_PER_GROUP: "MAX_PER_GROUP"
REDIS_HOST: redis
NODE_ENV: development
STORE_VARIABLES_FOR_LOCAL_MAPS: "true"
volumes:
- ./back:/usr/src/app
labels:
@ -177,6 +194,17 @@ services:
- "traefik.http.routers.redisinsight-ssl.tls=true"
- "traefik.http.routers.redisinsight-ssl.service=redisinsight"
icon:
image: matthiasluedtke/iconserver:v3.13.0
labels:
- "traefik.http.routers.icon.rule=Host(`icon.workadventure.localhost`)"
- "traefik.http.routers.icon.entryPoints=web"
- "traefik.http.services.icon.loadbalancer.server.port=8080"
- "traefik.http.routers.icon-ssl.rule=Host(`icon.workadventure.localhost`)"
- "traefik.http.routers.icon-ssl.entryPoints=websecure"
- "traefik.http.routers.icon-ssl.tls=true"
- "traefik.http.routers.icon-ssl.service=icon"
# coturn:
# image: coturn/coturn:4.5.2
# command:

View File

@ -15,7 +15,7 @@ When controls are disabled, the user cannot move anymore using keyboard input. T
Example:
```javascript
WA.room.onEnterZone('myZone', () => {
WA.room.onEnterLayer('myZone').subscribe(() => {
WA.controls.disablePlayerControls();
WA.ui.openPopup("popupRectangle", 'This is an imporant message!', [{
label: "Got it!",
@ -25,5 +25,5 @@ WA.room.onEnterZone('myZone', () => {
popup.close();
}
}]);
});
})
```

View File

@ -49,7 +49,7 @@ Example:
let helloWorldPopup;
// Open the popup when we enter a given zone
helloWorldPopup = WA.room.onEnterZone('myZone', () => {
helloWorldPopup = WA.room.onEnterLayer("myZone").subscribe(() => {
WA.ui.openPopup("popupRectangle", 'Hello world!', [{
label: "Close",
className: "primary",
@ -57,13 +57,13 @@ helloWorldPopup = WA.room.onEnterZone('myZone', () => {
// Close the popup when the "Close" button is pressed.
popup.close();
}
});
}]);
});
// Close the popup when we leave the zone.
WA.room.onLeaveZone('myZone', () => {
WA.room.onLeaveLayer("myZone").subscribe(() => {
helloWorldPopup.close();
});
})
```
### Add custom menu

View File

@ -4,7 +4,7 @@ declare let window: any;
class AnalyticsClient {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
private posthogPromise: Promise<any>;
private posthogPromise: Promise<any> | undefined;
constructor() {
if (POSTHOG_API_KEY && POSTHOG_URL) {
@ -14,65 +14,68 @@ class AnalyticsClient {
window.posthog = posthog;
return posthog;
});
} else {
this.posthogPromise = Promise.reject();
}
}
identifyUser(uuid: string, email: string | null) {
this.posthogPromise
.then((posthog) => {
this.posthogPromise?.then((posthog) => {
posthog.identify(uuid, { uuid, email, wa: true });
})
.catch();
});
}
loggedWithSso() {
this.posthogPromise
.then((posthog) => {
this.posthogPromise?.then((posthog) => {
posthog.capture("wa-logged-sso");
})
.catch();
});
}
loggedWithToken() {
this.posthogPromise
.then((posthog) => {
this.posthogPromise?.then((posthog) => {
posthog.capture("wa-logged-token");
})
.catch();
});
}
enteredRoom(roomId: string, roomGroup: string | null) {
this.posthogPromise
.then((posthog) => {
this.posthogPromise?.then((posthog) => {
posthog.capture("$pageView", { roomId, roomGroup });
})
.catch();
posthog.capture("enteredRoom");
});
}
openedMenu() {
this.posthogPromise
.then((posthog) => {
this.posthogPromise?.then((posthog) => {
posthog.capture("wa-opened-menu");
})
.catch();
});
}
launchEmote(emote: string) {
this.posthogPromise
.then((posthog) => {
this.posthogPromise?.then((posthog) => {
posthog.capture("wa-emote-launch", { emote });
})
.catch();
});
}
enteredJitsi(roomName: string, roomId: string) {
this.posthogPromise
.then((posthog) => {
this.posthogPromise?.then((posthog) => {
posthog.capture("wa-entered-jitsi", { roomName, roomId });
})
.catch();
});
}
validationName() {
this.posthogPromise?.then((posthog) => {
posthog.capture("wa-name-validation");
});
}
validationWoka(scene: string) {
this.posthogPromise?.then((posthog) => {
posthog.capture("wa-woka-validation", { scene });
});
}
validationVideo() {
this.posthogPromise?.then((posthog) => {
posthog.capture("wa-video-validation");
});
}
}
export const analyticsClient = new AnalyticsClient();

View File

@ -49,7 +49,7 @@ class IframeListener {
public readonly openTabStream = this._openTabStream.asObservable();
private readonly _loadPageStream: Subject<string> = new Subject();
public readonly loadPageStream = this._loadPageStream.asObservable()
public readonly loadPageStream = this._loadPageStream.asObservable();
private readonly _disablePlayerControlStream: Subject<void> = new Subject();
public readonly disablePlayerControlStream = this._disablePlayerControlStream.asObservable();
@ -269,7 +269,7 @@ class IframeListener {
registerScript(scriptUrl: string): Promise<void> {
return new Promise<void>((resolve, reject) => {
console.log("Loading map related script at ", scriptUrl);
console.info("Loading map related script at ", scriptUrl);
if (!process.env.NODE_ENV || process.env.NODE_ENV === "development") {
// Using external iframe mode (

View File

@ -18,13 +18,18 @@
'--font': 'Press Start 2P'
},
emojisPerRow: isMobile() ? 6 : 8,
autoFocusSearch: false
autoFocusSearch: false,
style: 'twemoji',
});
//the timeout is here to prevent the menu from flashing
setTimeout(() => picker.showPicker(emojiContainer), 100);
picker.on("emoji", (selection) => {
emoteStore.set(selection.emoji);
emoteStore.set({
unicode: selection.emoji,
url: selection.url,
name: selection.name
});
});
picker.on("hidden", () => {

View File

@ -4,7 +4,6 @@
let gameScene = gameManager.getCurrentGameScene();
let HTMLShareLink: HTMLInputElement;
let expandedMapCopyright = 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>
<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>
<section class="container-overflow">
<h3>{mapName}</h3>
@ -93,24 +66,6 @@
div.about-room-main {
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 {
width: 100%;
text-align: center;
@ -131,16 +86,6 @@
@media only screen and (max-width: 800px), only screen and (max-height: 800px) {
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 {
height: calc(100% - 120px);
}

View File

@ -1,10 +1,60 @@
<script lang="ts">
function goToGettingStarted() {
const sparkHost = "https://workadventu.re/getting-started";
window.open(sparkHost, "_blank");
}
function goToBuildingMap() {
const sparkHost = "https://workadventu.re/map-building/";
window.open(sparkHost, "_blank");
}
import {contactPageStore} from "../../Stores/MenuStore";
</script>
<iframe title="contact" src="{$contactPageStore}" allow="clipboard-read; clipboard-write self {$contactPageStore}" allowfullscreen></iframe>
<div class="create-map-main">
<section class="container-overflow">
<section>
<h3>Getting started</h3>
<p>
WorkAdventure allows you to create an online space to communicate spontaneously with others.
And it all starts with creating your own space. Choose from a large selection of prefabricated maps by our team.
</p>
<button type="button" class="nes-btn is-primary" on:click={goToGettingStarted}>Getting started</button>
</section>
<section>
<h3>Create your map</h3>
<p>You can also create your own custom map by following the step of the documentation.</p>
<button type="button" class="nes-btn" on:click={goToBuildingMap}>Create your map</button>
</section>
<iframe title="contact"
src="{$contactPageStore}"
allow="clipboard-read; clipboard-write self {$contactPageStore}"
allowfullscreen></iframe>
</section>
</div>
<style lang="scss">
div.create-map-main {
height: calc(100% - 56px);
text-align: center;
section {
margin-bottom: 50px;
}
section.container-overflow {
height: 100%;
margin: 0;
padding: 0;
overflow: auto;
}
}
iframe {
border: none;
height: calc(100% - 56px);

View File

@ -1,51 +0,0 @@
<script lang="ts">
function goToGettingStarted() {
const sparkHost = "https://workadventu.re/getting-started";
window.open(sparkHost, "_blank");
}
function goToBuildingMap() {
const sparkHost = "https://workadventu.re/map-building/";
window.open(sparkHost, "_blank");
}
</script>
<div class="create-map-main">
<section class="container-overflow">
<section>
<h3>Getting started</h3>
<p>
WorkAdventure allows you to create an online space to communicate spontaneously with others.
And it all starts with creating your own space. Choose from a large selection of prefabricated maps by our team.
</p>
<button type="button" class="nes-btn is-primary" on:click={goToGettingStarted}>Getting started</button>
</section>
<section>
<h3>Create your map</h3>
<p>You can also create your own custom map by following the step of the documentation.</p>
<button type="button" class="nes-btn" on:click={goToBuildingMap}>Create your map</button>
</section>
</section>
</div>
<style lang="scss">
div.create-map-main {
height: calc(100% - 56px);
text-align: center;
section {
margin-bottom: 50px;
}
section.container-overflow {
height: 100%;
margin: 0;
padding: 0;
overflow: auto;
}
}
</style>

View File

@ -0,0 +1,75 @@
<script lang="ts">
function copyLink() {
const input: HTMLInputElement = document.getElementById('input-share-link') as HTMLInputElement;
input.focus();
input.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>
<div class="guest-main">
<section class="container-overflow">
<section class="share-url not-mobile">
<h3>Share the link of the room !</h3>
<input type="text" readonly id="input-share-link" 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 id="input-share-link" value={location.toString()}>
<button type="button" class="nes-btn is-primary" on:click={shareLink}>Share</button>
</section>
</section>
</div>
<style lang="scss">
div.guest-main {
height: calc(100% - 56px);
text-align: center;
section {
margin-bottom: 50px;
}
section.container-overflow {
height: 100%;
margin: 0;
padding: 0;
overflow: auto;
}
section.is-mobile {
display: none;
}
}
@media only screen and (max-width: 900px), only screen and (max-height: 600px) {
div.guest-main {
section.share-url.not-mobile {
display: none;
}
section.is-mobile {
display: block;
text-align: center;
margin-bottom: 20px;
}
section.container-overflow {
height: calc(100% - 120px);
}
}
}
</style>

View File

@ -2,11 +2,11 @@
import {fly} from "svelte/transition";
import SettingsSubMenu from "./SettingsSubMenu.svelte";
import ProfileSubMenu from "./ProfileSubMenu.svelte";
import CreateMapSubMenu from "./CreateMapSubMenu.svelte";
import AboutRoomSubMenu from "./AboutRoomSubMenu.svelte";
import GlobalMessageSubMenu from "./GlobalMessagesSubMenu.svelte";
import ContactSubMenu from "./ContactSubMenu.svelte";
import CustomSubMenu from "./CustomSubMenu.svelte"
import GuestSubMenu from "./GuestSubMenu.svelte";
import {
checkSubMenuToShow,
customMenuIframe,
@ -19,21 +19,21 @@
import type {Unsubscriber} from "svelte/store";
import {sendMenuClickedEvent} from "../../Api/iframe/Ui/MenuItem";
let activeSubMenu: string = SubMenusInterface.settings;
let activeComponent: typeof SettingsSubMenu | typeof CustomSubMenu = SettingsSubMenu;
let activeSubMenu: string = SubMenusInterface.profile;
let activeComponent: typeof ProfileSubMenu | typeof CustomSubMenu = ProfileSubMenu;
let props: { url: string, allowApi: boolean };
let unsubscriberSubMenuStore: Unsubscriber;
onMount(() => {
unsubscriberSubMenuStore = subMenusStore.subscribe(() => {
if(!get(subMenusStore).includes(activeSubMenu)) {
switchMenu(SubMenusInterface.settings);
switchMenu(SubMenusInterface.profile);
}
})
checkSubMenuToShow();
switchMenu(SubMenusInterface.settings);
switchMenu(SubMenusInterface.profile);
})
onDestroy(() => {
@ -52,8 +52,8 @@
case SubMenusInterface.profile:
activeComponent = ProfileSubMenu;
break;
case SubMenusInterface.createMap:
activeComponent = CreateMapSubMenu;
case SubMenusInterface.invite:
activeComponent = GuestSubMenu;
break;
case SubMenusInterface.aboutRoom:
activeComponent = AboutRoomSubMenu;

View File

@ -11,15 +11,9 @@
function showChat(){
chatVisibilityStore.set(true);
}
function onKeyDown(e: KeyboardEvent) {
if (e.key === "Tab") {
showMenu();
}
}
</script>
<svelte:window on:keydown={onKeyDown}/>
<svelte:window/>
<main class="menuIcon">
<img src={logoWA} alt="open menu" class="nes-pointer" on:click|preventDefault={showMenu}>

View File

@ -12,6 +12,10 @@
import {localUserStore} from "../../Connexion/LocalUserStore";
import {EnableCameraScene, EnableCameraSceneName} from "../../Phaser/Login/EnableCameraScene";
import {enableCameraSceneVisibilityStore} from "../../Stores/MediaStore";
import btnProfileSubMenuCamera from "../images/btn-menu-profile-camera.svg";
import btnProfileSubMenuIdentity from "../images/btn-menu-profile-identity.svg";
import btnProfileSubMenuCompanion from "../images/btn-menu-profile-companion.svg";
import btnProfileSubMenuWoka from "../images/btn-menu-profile-woka.svg";
function disableMenuStores(){
@ -55,6 +59,28 @@
</script>
<div class="customize-main">
<div class="submenu">
<section>
<button type="button" class="nes-btn" on:click|preventDefault={openEditNameScene}>
<img src={btnProfileSubMenuIdentity} alt="Edit your name">
<span class="btn-hover">Edit your name</span>
</button>
<button type="button" class="nes-btn" on:click|preventDefault={openEditSkinScene}>
<img src={btnProfileSubMenuWoka} alt="Edit your WOKA">
<span class="btn-hover">Edit your WOKA</span>
</button>
<button type="button" class="nes-btn" on:click|preventDefault={openEditCompanionScene}>
<img src={btnProfileSubMenuCompanion} alt="Edit your companion">
<span class="btn-hover">Edit your companion</span>
</button>
<button type="button" class="nes-btn" on:click|preventDefault={openEnableCameraScene}>
<img src={btnProfileSubMenuCamera} alt="Edit your camera">
<span class="btn-hover">Edit your camera</span>
</button>
</section>
</div>
<div class="content">
{#if $userIsConnected}
<section>
{#if PROFILE_URL != undefined}
@ -69,18 +95,48 @@
<a type="button" class="nes-btn" href="/login">Sign in</a>
</section>
{/if}
<section>
<button type="button" class="nes-btn" on:click|preventDefault={openEditNameScene}>Edit Name</button>
<button type="button" class="nes-btn" on:click|preventDefault={openEditSkinScene}>Edit Skin</button>
<button type="button" class="nes-btn" on:click|preventDefault={openEditCompanionScene}>Edit Companion</button>
</section>
<section>
<button type="button" class="nes-btn" on:click|preventDefault={openEnableCameraScene}>Setup camera</button>
</section>
</div>
</div>
<style lang="scss">
div.customize-main{
width: 100%;
display: inline-flex;
div.submenu{
height: 100%;
width: 50px;
button {
transition: all .5s ease;
text-align: left;
white-space: nowrap;
margin-bottom: 10px;
max-height: 44px;
img {
height: 26px;
width: 26px;
cursor: pointer;
}
span.btn-hover{
display: none;
font-family: "Press Start 2P";
}
&:hover{
width: auto;
span.btn-hover {
display: initial;
}
}
}
}
div.content {
width: 100%;
section {
display: flex;
justify-content: center;
@ -100,9 +156,10 @@
}
}
}
}
@media only screen and (max-width: 800px) {
div.customize-main section button {
div.customize-main.content section button {
width: 130px;
}
}

View File

@ -0,0 +1 @@
<svg id="Calque_1" data-name="Calque 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 50 50"><g id="btn_setup_camera" data-name="btn setup camera"><circle cx="24.71" cy="13.11" r="1.46"/><path d="M8.65,23.34H40.78V2.89H31.16L28.24,0h-7L18.27,2.89H8.65ZM32,8.73h2.92v2.92H32Zm-7.31,0a4.39,4.39,0,1,1-4.38,4.38A4.39,4.39,0,0,1,24.71,8.73Z"/><path d="M2.81,44H5.73v5.84h8.76V44h5.84V46.9h2.92V44h5.84V46.9H32V44h5.84V46.9h2.92V44h5.84V41.06h-33l-3.52-3.53L6.58,41.06H2.81Z"/><path d="M2.81,32.3H8.65v2.92h2.92V32.3h5.84v2.92h2.92V32.3h5.84v2.92h2.92V32.3h5.85v5.84H43.7V32.3h2.92V29.37H42.84l-3.52-3.52-3.53,3.52h-33Z"/></g></svg>

After

Width:  |  Height:  |  Size: 629 B

View File

@ -0,0 +1 @@
<svg id="Calque_1" data-name="Calque 1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 50 50"><g id="btn_setup_companion" data-name="btn setup companion"><g id="iNhyGC.tif"><image id="Layer_0" data-name="Layer 0" width="15" height="30" transform="translate(13.21 0.25) scale(1.65)" xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAfCAYAAADupU20AAAACXBIWXMAAAbEAAAGxAGo1xHEAAAB20lEQVQ4T51UO04DMRScRYmEhGWlApRsBcUap4VUXICSjpqaY3ACSgpOgERQKnpoNjXRhjrkANZGikRhiuDH8w8BI7l5n/H4fVxYaxFC66FnnM1eiyjoC53QoPXQjkYnkS1HUnAFWg+tUhXatsVi8Q4AUKqClBJ1PU0q8RTMZq+Fk++CU4o4oiekbjHGhCbCVtbzS/xIwGuSQ/GbNgL5VkYEWg9tWQ4ghCCb60qKJGqjS5ZSkt0YkyWhGjjZYTIASClJUfg8r4hKVWiaOYwx0WmaOZSqEKIDfEu/PDvGHYCmmUeBSlW4PDvGTdt6o00Krq8uAIDkl+UASlUoy4Fnd3EOHXc7AOzu7QN4o/l3ie79G/+G3Kkojo50svI5hB3pAOnK5xDG0TLV9ZSMqe3jft4NKuLt/Qu6qzXG40m0fcYYjMcTdFdr3N6/eL4tYDOqhwc9AMDT4wNScPbDg563XFTEfr8P+3C6MZ4/RwTct1wuqYiFtTa5A3U9RXe1xsfONtUktRNUxMXi3Rua0egExhgi5MkcRFCWA3I6IgD0Xu7jJEQghKDE8BaX6OI4vE9VCEE38r3nqx7CW2c+9yFy00oE/x1lInDTlyPi3eCgP5F/VanPM+dPfut/wScEgxZt/KHdLgAAAABJRU5ErkJggg=="/></g></g></svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -0,0 +1 @@
<svg id="Calque_1" data-name="Calque 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 50 50"><g id="btn_setup_identity" data-name="btn setup identity"><path d="M31.94,6l-4.35,6.39a3.63,3.63,0,1,1-3.07-1.7,3.71,3.71,0,0,1,.67.06l4-5.89a16,16,0,0,0-9.37,0L17,.76a1.45,1.45,0,0,0-2.4,1.64L17.09,6A16,16,0,0,0,8.56,20.15V33.69a16,16,0,0,0,31.91,0V20.15A16,16,0,0,0,31.94,6Zm-.65,32.49H17.75a1.45,1.45,0,1,1,0-2.9H31.29a1.45,1.45,0,0,1,0,2.9Zm0-5.8H17.75a1.45,1.45,0,1,1,0-2.9H31.29a1.45,1.45,0,1,1,0,2.9Zm0-5.8H17.75a1.45,1.45,0,1,1,0-2.9H31.29a1.45,1.45,0,1,1,0,2.9Z"/><path d="M34.42,2.4A1.45,1.45,0,1,0,32,.76L29.21,4.89A16.38,16.38,0,0,1,31.94,6Z"/></g></svg>

After

Width:  |  Height:  |  Size: 661 B

View File

@ -0,0 +1 @@
<svg id="Calque_1" data-name="Calque 1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 50 50"><g id="btn_setup_woka" data-name="btn setup woka"><g id="NP8bMB.tif"><image id="Layer_0" data-name="Layer 0" width="23" height="29" transform="translate(4.61 0.1) scale(1.71)" xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABcAAAAdCAYAAABBsffGAAAACXBIWXMAAAZ1AAAGdQGEn07tAAACCUlEQVRIS61WS4rDMAx9abLoxuqi4MIYSk+Q+59iBnqCUlBhDNnYmy4aMosgj+3YyRTmQReVpSdZP6eZpglrMOajqsD8aGpnANDUyIW0bTsAwPF4DGfDMAAAxvEFoO6kSG7Mx9S2XSBUSi10vPcAZkfj+Co6WJDHxCXSHN77qoOuZJATE9FCxzkH4PdW1n4vdHbxH4k6Rom4JG/bblH8hBxIo64RC+RcKZUUXLAgF2wRC9b0AvlaP7+DmGcnAq1PdYs3oPUpOKim5T+wA+YJk1aS6XsXYmftd+j3EPnWnvgrYp4kLcyPZhxf8N6HIdmCcw7e+79PaDxta60mxKXpBLLdEk/oOL6wtmPinRLbxNEHciHu+34zohhan6CUAhHher0mDpK0xMQSrTEGAJIaSKqYGUqpsH77vsfX12fQ62aCOeo8YmMMDodDUHbOgYgSGTMnN5QFxvxoQrdcLhcA633+fD6rZ2InPADQSdREBGYOT1fuxFqL/X4Pa22QMXPy5HnvYYwJ0e+AOVc5oXSJcw7OOWitQUTQWgdZvmrFXvhCWpxzSaWHYQiFk6GSnxSQiAKhDGBc+I750cgWK60AIsLtdlukSbooRs5V/bQ4n8+TpCbveeltGaT7/V7cS9WVK4WVFADpzUQueiWs7nOlFIwxi/GvyXOskm+9o1vn1ZwD6XsYp6Qmz/ED0qpw9h1b2uQAAAAASUVORK5CYII="/></g></g></svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -41,7 +41,6 @@ class ConnectionManager {
const nonce = localUserStore.generateNonce();
localUserStore.setAuthToken(null);
//TODO fix me to redirect this URL by pusher
if (!this._currentRoom || !this._currentRoom.iframeAuthentication) {
loginSceneVisibleIframeStore.set(false);
return null;
@ -79,6 +78,16 @@ class ConnectionManager {
const connexionType = urlManager.getGameConnexionType();
this.connexionType = connexionType;
this._currentRoom = null;
const urlParams = new URLSearchParams(window.location.search);
const token = urlParams.get("token");
if (token) {
this.authToken = token;
localUserStore.setAuthToken(token);
//token was saved, clear url
urlParams.delete("token");
}
if (connexionType === GameConnexionTypes.login) {
this._currentRoom = await Room.createRoom(new URL(localUserStore.getLastRoomUrl()));
if (this.loadOpenIDScreen() !== null) {
@ -87,6 +96,8 @@ class ConnectionManager {
urlManager.pushRoomIdToUrl(this._currentRoom);
} else if (connexionType === GameConnexionTypes.jwt) {
const urlParams = new URLSearchParams(window.location.search);
if (!token) {
const code = urlParams.get("code");
const state = urlParams.get("state");
if (!state || !localUserStore.verifyState(state)) {
@ -96,6 +107,8 @@ class ConnectionManager {
throw "No Auth code provided";
}
localUserStore.setCode(code);
}
this._currentRoom = await Room.createRoom(new URL(localUserStore.getLastRoomUrl()));
try {
await this.checkAuthUserConnexion();
@ -164,14 +177,20 @@ class ConnectionManager {
//before set token of user we must load room and all information. For example the mandatory authentication could be require on current room
this._currentRoom = await Room.createRoom(new URL(roomPath));
//defined last room url this room path
localUserStore.setLastRoomUrl(this._currentRoom.key);
//todo: add here some kind of warning if authToken has expired.
if (!this.authToken && !this._currentRoom.authenticationMandatory) {
await this.anonymousLogin();
} else {
try {
await this.checkAuthUserConnexion();
analyticsClient.loggedWithSso();
} catch (err) {
console.error(err);
this.loadOpenIDScreen();
return Promise.reject(new Error("You will be redirect on login page"));
}
}
this.localUser = localUserStore.getLocalUser() as LocalUser; //if authToken exist in localStorage then localUser cannot be null
@ -199,13 +218,15 @@ class ConnectionManager {
analyticsClient.identifyUser(this.localUser.uuid, this.localUser.email);
}
//clean history with new URL
window.history.pushState({}, document.title, window.location.pathname);
this.serviceWorker = new _ServiceWorker();
return Promise.resolve(this._currentRoom);
}
public async anonymousLogin(isBenchmark: boolean = false): Promise<void> {
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;
if (!isBenchmark) {
// In benchmark, we don't have a local storage.
@ -279,20 +300,25 @@ class ConnectionManager {
//set connected store for menu at false
userIsConnected.set(false);
const token = localUserStore.getAuthToken();
const state = localUserStore.getState();
const code = localUserStore.getCode();
const nonce = localUserStore.getNonce();
if (!token) {
if (!state || !localUserStore.verifyState(state)) {
throw "Could not validate state!";
}
if (!code) {
throw "No Auth code provided";
}
const nonce = localUserStore.getNonce();
const token = localUserStore.getAuthToken();
const { authToken } = await Axios.get(`${PUSHER_URL}/login-callback`, { params: { code, nonce, token } }).then(
(res) => res.data
);
}
const { authToken, userUuid, textures, email } = await Axios.get(`${PUSHER_URL}/login-callback`, {
params: { code, nonce, token, playUri: this.currentRoom?.key },
}).then((res) => res.data);
localUserStore.setAuthToken(authToken);
this.localUser = new LocalUser(userUuid, textures, email);
localUserStore.saveUser(this.localUser);
this.authToken = authToken;
//user connected, set connected store for menu at true

View File

@ -122,17 +122,22 @@ class LocalUserStore {
setLastRoomUrl(roomUrl: string): void {
localStorage.setItem(lastRoomUrl, roomUrl.toString());
if ("caches" in window) {
caches.open(cacheAPIIndex).then((cache) => {
const stringResponse = new Response(JSON.stringify({ roomUrl }));
cache.put(`/${lastRoomUrl}`, stringResponse);
});
}
}
getLastRoomUrl(): string {
return (
localStorage.getItem(lastRoomUrl) ?? window.location.protocol + "//" + window.location.host + START_ROOM_URL
);
}
getLastRoomUrlCacheApi(): Promise<string | undefined> {
if (!("caches" in window)) {
return Promise.resolve(undefined);
}
return caches.open(cacheAPIIndex).then((cache) => {
return cache.match(`/${lastRoomUrl}`).then((res) => {
return res?.json().then((data) => {
@ -165,8 +170,15 @@ class LocalUserStore {
verifyState(value: string): boolean {
const oldValue = localStorage.getItem(state);
if (!oldValue) {
localStorage.setItem(state, value);
return true;
}
return oldValue === value;
}
setState(value: string) {
localStorage.setItem(state, value);
}
getState(): string | null {
return localStorage.getItem(state);
}

View File

@ -1,5 +1,5 @@
import Axios from "axios";
import { CONTACT_URL, PUSHER_URL } from "../Enum/EnvironmentVariable";
import { CONTACT_URL, PUSHER_URL, DISABLE_ANONYMOUS, OPID_LOGIN_SCREEN_PROVIDER } from "../Enum/EnvironmentVariable";
import type { CharacterTexture } from "./LocalUser";
import { localUserStore } from "./LocalUserStore";
@ -14,8 +14,8 @@ export interface RoomRedirect {
export class Room {
public readonly id: string;
public readonly isPublic: boolean;
private _authenticationMandatory: boolean = false;
private _iframeAuthentication?: string;
private _authenticationMandatory: boolean = DISABLE_ANONYMOUS;
private _iframeAuthentication?: string = OPID_LOGIN_SCREEN_PROVIDER;
private _mapUrl: string | undefined;
private _textures: CharacterTexture[] | undefined;
private instance: string | undefined;
@ -89,6 +89,7 @@ export class Room {
}
private async getMapDetail(): Promise<MapDetail | RoomRedirect> {
try {
const result = await Axios.get(`${PUSHER_URL}/map`, {
params: {
playUri: this.roomUrl.toString(),
@ -106,10 +107,19 @@ export class Room {
this._mapUrl = data.mapUrl;
this._textures = data.textures;
this._group = data.group;
this._authenticationMandatory = data.authenticationMandatory || false;
this._iframeAuthentication = data.iframeAuthentication;
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;
}
}
/**

View File

@ -4,6 +4,7 @@ const START_ROOM_URL: string =
const PUSHER_URL = process.env.PUSHER_URL || "//pusher.workadventure.localhost";
export const ADMIN_URL = process.env.ADMIN_URL || "//workadventu.re";
const UPLOADER_URL = process.env.UPLOADER_URL || "//uploader.workadventure.localhost";
const ICON_URL = process.env.ICON_URL || "//icon.workadventure.localhost";
const STUN_SERVER: string = process.env.STUN_SERVER || "stun:stun.l.google.com:19302";
const TURN_SERVER: string = process.env.TURN_SERVER || "";
const SKIP_RENDER_OPTIMIZATIONS: boolean = process.env.SKIP_RENDER_OPTIMIZATIONS == "true";
@ -22,6 +23,8 @@ export const CONTACT_URL = process.env.CONTACT_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_URL = process.env.POSTHOG_URL || undefined;
export const DISABLE_ANONYMOUS: boolean = process.env.DISABLE_ANONYMOUS === "true";
export const OPID_LOGIN_SCREEN_PROVIDER = process.env.OPID_LOGIN_SCREEN_PROVIDER;
export const isMobile = (): boolean => window.innerWidth <= 800 || window.innerHeight <= 600;
@ -32,6 +35,7 @@ export {
DISABLE_NOTIFICATIONS,
PUSHER_URL,
UPLOADER_URL,
ICON_URL,
POSITION_DELAY,
MAX_EXTRAPOLATION_TIME,
STUN_SERVER,

View File

@ -6,73 +6,124 @@ const TextName: string = "Loading...";
const LogoResource: string = "static/images/logo.png";
const LogoFrame: ImageFrameConfig = { frameWidth: 310, frameHeight: 60 };
export const addLoader = (scene: Phaser.Scene): void => {
// If there is nothing to load, do not display the loader.
if (scene.load.list.entries.length === 0) {
return;
}
let loadingText: Phaser.GameObjects.Text | null = null;
const loadingBarWidth: number = Math.floor(scene.game.renderer.width / 3);
const loadingBarHeight: number = 16;
const padding: number = 5;
export class Loader {
private progressContainer!: Phaser.GameObjects.Graphics;
private progress!: Phaser.GameObjects.Graphics;
private progressAmount: number = 0;
private logo: Phaser.GameObjects.Image | undefined;
private loadingText: Phaser.GameObjects.Text | null = null;
public constructor(private scene: Phaser.Scene) {}
public addLoader(): void {
// If there is nothing to load, do not display the loader.
if (this.scene.load.list.entries.length === 0) {
return;
}
const loadingBarWidth: number = Math.floor(this.scene.game.renderer.width / 3);
const promiseLoadLogoTexture = new Promise<Phaser.GameObjects.Image>((res) => {
if (scene.load.textureManager.exists(LogoNameIndex)) {
if (this.scene.load.textureManager.exists(LogoNameIndex)) {
return res(
scene.add.image(scene.game.renderer.width / 2, scene.game.renderer.height / 2 - 150, LogoNameIndex)
(this.logo = this.scene.add.image(
this.scene.game.renderer.width / 2,
this.scene.game.renderer.height / 2 - 150,
LogoNameIndex
))
);
} else {
//add loading if logo image is not ready
loadingText = scene.add.text(scene.game.renderer.width / 2, scene.game.renderer.height / 2 - 50, TextName);
this.loadingText = this.scene.add.text(
this.scene.game.renderer.width / 2,
this.scene.game.renderer.height / 2 - 50,
TextName
);
}
scene.load.spritesheet(LogoNameIndex, LogoResource, LogoFrame);
scene.load.once(`filecomplete-spritesheet-${LogoNameIndex}`, () => {
if (loadingText) {
loadingText.destroy();
this.scene.load.spritesheet(LogoNameIndex, LogoResource, LogoFrame);
this.scene.load.once(`filecomplete-spritesheet-${LogoNameIndex}`, () => {
if (this.loadingText) {
this.loadingText.destroy();
}
return res(
scene.add.image(scene.game.renderer.width / 2, scene.game.renderer.height / 2 - 150, LogoNameIndex)
(this.logo = this.scene.add.image(
this.scene.game.renderer.width / 2,
this.scene.game.renderer.height / 2 - 150,
LogoNameIndex
))
);
});
});
const progressContainer = scene.add.graphics();
const progress = scene.add.graphics();
progressContainer.fillStyle(0x444444, 0.8);
progressContainer.fillRect(
(scene.game.renderer.width - loadingBarWidth) / 2 - padding,
scene.game.renderer.height / 2 + 50 - padding,
loadingBarWidth + padding * 2,
loadingBarHeight + padding * 2
);
this.progressContainer = this.scene.add.graphics();
this.progress = this.scene.add.graphics();
this.progressContainer.fillStyle(0x444444, 0.8);
scene.load.on("progress", (value: number) => {
progress.clear();
progress.fillStyle(0xbbbbbb, 1);
progress.fillRect(
(scene.game.renderer.width - loadingBarWidth) / 2,
scene.game.renderer.height / 2 + 50,
loadingBarWidth * value,
loadingBarHeight
);
this.resize();
this.scene.load.on("progress", (value: number) => {
this.progressAmount = value;
this.drawProgress();
});
scene.load.on("complete", () => {
if (loadingText) {
loadingText.destroy();
this.scene.load.on("complete", () => {
if (this.loadingText) {
this.loadingText.destroy();
}
promiseLoadLogoTexture.then((resLoadingImage: Phaser.GameObjects.Image) => {
resLoadingImage.destroy();
});
progress.destroy();
progressContainer.destroy();
if (scene instanceof DirtyScene) {
scene.markDirty();
this.progress.destroy();
this.progressContainer.destroy();
if (this.scene instanceof DirtyScene) {
this.scene.markDirty();
}
});
};
export const removeLoader = (scene: Phaser.Scene): void => {
if (scene.load.textureManager.exists(LogoNameIndex)) {
scene.load.textureManager.remove(LogoNameIndex);
}
};
public removeLoader(): void {
if (this.scene.load.textureManager.exists(LogoNameIndex)) {
this.scene.load.textureManager.remove(LogoNameIndex);
}
}
public resize(): void {
const loadingBarWidth: number = Math.floor(this.scene.game.renderer.width / 3);
this.progressContainer.clear();
this.progressContainer.fillStyle(0x444444, 0.8);
this.progressContainer.fillRect(
(this.scene.game.renderer.width - loadingBarWidth) / 2 - padding,
this.scene.game.renderer.height / 2 + 50 - padding,
loadingBarWidth + padding * 2,
loadingBarHeight + padding * 2
);
this.drawProgress();
if (this.loadingText) {
this.loadingText.x = this.scene.game.renderer.width / 2;
this.loadingText.y = this.scene.game.renderer.height / 2 - 50;
}
if (this.logo) {
this.logo.x = this.scene.game.renderer.width / 2;
this.logo.y = this.scene.game.renderer.height / 2 - 150;
}
}
private drawProgress() {
const loadingBarWidth: number = Math.floor(this.scene.game.renderer.width / 3);
this.progress.clear();
this.progress.fillStyle(0xbbbbbb, 1);
this.progress.fillRect(
(this.scene.game.renderer.width - loadingBarWidth) / 2,
this.scene.game.renderer.height / 2 + 50,
loadingBarWidth * this.progressAmount,
loadingBarHeight
);
}
}

View File

@ -3,6 +3,7 @@ import { SpeechBubble } from "./SpeechBubble";
import Text = Phaser.GameObjects.Text;
import Container = Phaser.GameObjects.Container;
import Sprite = Phaser.GameObjects.Sprite;
import DOMElement = Phaser.GameObjects.DOMElement;
import { TextureError } from "../../Exception/TextureError";
import { Companion } from "../Companion/Companion";
import type { GameScene } from "../Game/GameScene";
@ -33,7 +34,7 @@ export abstract class Character extends Container {
//private teleportation: Sprite;
private invisible: boolean;
public companion?: Companion;
private emote: Phaser.GameObjects.Text | null = null;
private emote: Phaser.GameObjects.DOMElement | null = null;
private emoteTween: Phaser.Tweens.Tween | null = null;
scene: GameScene;
@ -300,8 +301,9 @@ export abstract class Character extends Container {
playEmote(emote: string) {
this.cancelPreviousEmote();
const emoteY = -45;
this.playerName.setVisible(false);
this.emote = new Text(this.scene, -10, 0, emote, { fontSize: "20px" });
const image = new Image(16, 16);
image.src = emote;
this.emote = new DOMElement(this.scene, -1, 0, image, "z-index:10;");
this.emote.setAlpha(0);
this.add(this.emote);
this.createStartTransition(emoteY);

View File

@ -162,6 +162,7 @@ export class EmbeddedWebsiteManager {
const iframe = document.createElement("iframe");
iframe.src = absoluteUrl;
iframe.tabIndex = -1;
iframe.style.width = embeddedWebsiteEvent.position.width + "px";
iframe.style.height = embeddedWebsiteEvent.position.height + "px";
iframe.style.margin = "0";

View File

@ -15,10 +15,7 @@ import type {
import { DEBUG_MODE, JITSI_PRIVATE_MODE, MAX_PER_GROUP, POSITION_DELAY } from "../../Enum/EnvironmentVariable";
import { Queue } from "queue-typescript";
import {
Box,
ON_ACTION_TRIGGER_BUTTON,
} from "../../WebRtc/LayoutManager";
import { Box, ON_ACTION_TRIGGER_BUTTON } from "../../WebRtc/LayoutManager";
import { CoWebsite, coWebsiteManager } from "../../WebRtc/CoWebsiteManager";
import type { UserMovedMessage } from "../../Messages/generated/messages_pb";
import { ProtobufClientUtils } from "../../Network/ProtobufClientUtils";
@ -31,7 +28,7 @@ import { localUserStore } from "../../Connexion/LocalUserStore";
import { HtmlUtils } from "../../WebRtc/HtmlUtils";
import { mediaManager } from "../../WebRtc/MediaManager";
import { SimplePeer } from "../../WebRtc/SimplePeer";
import { addLoader, removeLoader } from "../Components/Loader";
import { Loader } from "../Components/Loader";
import { lazyLoadPlayerCharacterTextures, loadCustomTexture } from "../Entity/PlayerTexturesLoadingManager";
import { RemotePlayer } from "../Entity/RemotePlayer";
import type { ActionableItem } from "../Items/ActionableItem";
@ -91,6 +88,7 @@ import { analyticsClient } from "../../Administration/AnalyticsClient";
import { get } from "svelte/store";
import { contactPageStore } from "../../Stores/MenuStore";
import { GameMapProperties } from "./GameMapProperties";
import SpriteSheetFile = Phaser.Loader.FileTypes.SpriteSheetFile;
export interface GameSceneInitInterface {
initPosition: PointInterface | null;
@ -205,6 +203,7 @@ export class GameScene extends DirtyScene {
private sharedVariablesManager!: SharedVariablesManager;
private objectsByType = new Map<string, ITiledMapObject[]>();
private embeddedWebsiteManager!: EmbeddedWebsiteManager;
private loader: Loader;
constructor(private room: Room, MapUrlFile: string, customKey?: string | undefined) {
super({
@ -223,6 +222,7 @@ export class GameScene extends DirtyScene {
this.connectionAnswerPromise = new Promise<RoomJoinedMessageInterface>((resolve, reject): void => {
this.connectionAnswerPromiseResolve = resolve;
});
this.loader = new Loader(this);
}
//hook preload scene
@ -296,9 +296,10 @@ 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.
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
removeLoader(this);
this.loader.removeLoader();
//display an error scene
this.scene.start(ErrorSceneName, {
@ -332,7 +333,7 @@ export class GameScene extends DirtyScene {
});
//this function must stay at the end of preload function
addLoader(this);
this.loader.addLoader();
}
// FIXME: we need to put a "unknown" instead of a "any" and validate the structure of the JSON we are receiving.
@ -497,7 +498,10 @@ export class GameScene extends DirtyScene {
object.properties,
'in the "' + object.name + '" object of type "website"'
);
const allowApi = PropertyUtils.findBooleanProperty(GameMapProperties.ALLOW_API, object.properties);
const allowApi = PropertyUtils.findBooleanProperty(
GameMapProperties.ALLOW_API,
object.properties
);
// TODO: add a "allow" property to iframe
this.embeddedWebsiteManager.createEmbeddedWebsite(
@ -614,10 +618,10 @@ export class GameScene extends DirtyScene {
oldPeerNumber = newPeerNumber;
});
this.emoteUnsubscribe = emoteStore.subscribe((emoteKey) => {
if (emoteKey) {
this.CurrentPlayer?.playEmote(emoteKey);
this.connection?.emitEmoteEvent(emoteKey);
this.emoteUnsubscribe = emoteStore.subscribe((emote) => {
if (emote) {
this.CurrentPlayer?.playEmote(emote.url);
this.connection?.emitEmoteEvent(emote.url);
emoteStore.set(null);
}
});
@ -763,14 +767,14 @@ export class GameScene extends DirtyScene {
this.gameMap.setPosition(this.CurrentPlayer.x, this.CurrentPlayer.y);
// Init layer change listener
this.gameMap.onEnterLayer(layers => {
layers.forEach(layer => {
this.gameMap.onEnterLayer((layers) => {
layers.forEach((layer) => {
iframeListener.sendEnterLayerEvent(layer.name);
});
});
this.gameMap.onLeaveLayer(layers => {
layers.forEach(layer => {
this.gameMap.onLeaveLayer((layers) => {
layers.forEach((layer) => {
iframeListener.sendLeaveLayerEvent(layer.name);
});
});
@ -1830,6 +1834,8 @@ ${escapedMessage}
right: camera.scrollX + camera.width,
bottom: camera.scrollY + camera.height,
});
this.loader.resize();
}
private getObjectLayerData(objectName: string): ITiledMapObject | undefined {
for (const layer of this.mapFile.layers) {
@ -1868,7 +1874,8 @@ ${escapedMessage}
public startJitsi(roomName: string, jwt?: string): void {
const allProps = this.gameMap.getCurrentProperties();
const jitsiConfig = this.safeParseJSONstring(
allProps.get(GameMapProperties.JITSI_CONFIG) as string | undefined, GameMapProperties.JITSI_CONFIG
allProps.get(GameMapProperties.JITSI_CONFIG) as string | undefined,
GameMapProperties.JITSI_CONFIG
);
const jitsiInterfaceConfig = this.safeParseJSONstring(
allProps.get(GameMapProperties.JITSI_INTERFACE_CONFIG) as string | undefined,

View File

@ -4,7 +4,7 @@ import { loadAllLayers } from "../Entity/PlayerTexturesLoadingManager";
import Sprite = Phaser.GameObjects.Sprite;
import { gameManager } from "../Game/GameManager";
import { localUserStore } from "../../Connexion/LocalUserStore";
import { addLoader } from "../Components/Loader";
import { Loader } from "../Components/Loader";
import type { BodyResourceDescriptionInterface } from "../Entity/PlayerTextures";
import { AbstractCharacterScene } from "./AbstractCharacterScene";
import { areCharacterLayersValid } from "../../Connexion/LocalUser";
@ -14,6 +14,7 @@ import { waScaleManager } from "../Services/WaScaleManager";
import { isMobile } from "../../Enum/EnvironmentVariable";
import { CustomizedCharacter } from "../Entity/CustomizedCharacter";
import { get } from "svelte/store";
import { analyticsClient } from "../../Administration/AnalyticsClient";
export const CustomizeSceneName = "CustomizeScene";
@ -29,10 +30,13 @@ export class CustomizeScene extends AbstractCharacterScene {
private moveHorizontally: number = 0;
private moveVertically: number = 0;
private loader: Loader;
constructor() {
super({
key: CustomizeSceneName,
});
this.loader = new Loader(this);
}
preload() {
@ -54,7 +58,7 @@ export class CustomizeScene extends AbstractCharacterScene {
this.lazyloadingAttempt = false;
//this function must stay at the end of preload function
addLoader(this);
this.loader.addLoader();
}
create() {
@ -278,6 +282,8 @@ export class CustomizeScene extends AbstractCharacterScene {
return;
}
analyticsClient.validationWoka("CustomizeWoka");
gameManager.setCharacterLayers(layers);
this.scene.sleep(CustomizeSceneName);
waScaleManager.restoreZoom();

View File

@ -2,6 +2,7 @@ import { gameManager } from "../Game/GameManager";
import { ResizableScene } from "./ResizableScene";
import { enableCameraSceneVisibilityStore } from "../../Stores/MediaStore";
import { localUserStore } from "../../Connexion/LocalUserStore";
import { analyticsClient } from "../../Administration/AnalyticsClient";
export const EnableCameraSceneName = "EnableCameraScene";
@ -27,6 +28,8 @@ export class EnableCameraScene extends ResizableScene {
update(time: number, delta: number): void {}
public login(): void {
analyticsClient.validationVideo();
enableCameraSceneVisibilityStore.hideEnableCameraScene();
this.scene.sleep(EnableCameraSceneName);

View File

@ -4,6 +4,7 @@ import { loginSceneVisibleIframeStore, loginSceneVisibleStore } from "../../Stor
import { localUserStore } from "../../Connexion/LocalUserStore";
import { connectionManager } from "../../Connexion/ConnectionManager";
import { gameManager } from "../Game/GameManager";
import { analyticsClient } from "../../Administration/AnalyticsClient";
export const LoginSceneName = "LoginScene";
@ -34,6 +35,8 @@ export class LoginScene extends ResizableScene {
}
public login(name: string): void {
analyticsClient.validationName();
name = name.trim();
gameManager.setPlayerName(name);

View File

@ -4,7 +4,7 @@ import { EnableCameraSceneName } from "./EnableCameraScene";
import { CustomizeSceneName } from "./CustomizeScene";
import { localUserStore } from "../../Connexion/LocalUserStore";
import { loadAllDefaultModels } from "../Entity/PlayerTexturesLoadingManager";
import { addLoader } from "../Components/Loader";
import { Loader } from "../Components/Loader";
import type { BodyResourceDescriptionInterface } from "../Entity/PlayerTextures";
import { AbstractCharacterScene } from "./AbstractCharacterScene";
import { areCharacterLayersValid } from "../../Connexion/LocalUser";
@ -13,6 +13,7 @@ import { PinchManager } from "../UserInput/PinchManager";
import { selectCharacterSceneVisibleStore } from "../../Stores/SelectCharacterStore";
import { waScaleManager } from "../Services/WaScaleManager";
import { isMobile } from "../../Enum/EnvironmentVariable";
import { analyticsClient } from "../../Administration/AnalyticsClient";
//todo: put this constants in a dedicated file
export const SelectCharacterSceneName = "SelectCharacterScene";
@ -30,11 +31,13 @@ export class SelectCharacterScene extends AbstractCharacterScene {
protected pointerTimer: number = 0;
protected lazyloadingAttempt = true; //permit to update texture loaded after renderer
private loader: Loader;
constructor() {
super({
key: SelectCharacterSceneName,
});
this.loader = new Loader(this);
}
preload() {
@ -48,7 +51,7 @@ export class SelectCharacterScene extends AbstractCharacterScene {
this.lazyloadingAttempt = false;
//this function must stay at the end of preload function
addLoader(this);
this.loader.addLoader();
}
create() {
@ -98,6 +101,9 @@ export class SelectCharacterScene extends AbstractCharacterScene {
if (!this.selectedPlayer) {
return;
}
analyticsClient.validationWoka("SelectWoka");
this.scene.stop(SelectCharacterSceneName);
waScaleManager.restoreZoom();
gameManager.setCharacterLayers([this.selectedPlayer.texture.key]);

View File

@ -1,4 +1,4 @@
import { addLoader } from "../Components/Loader";
import { Loader } from "../Components/Loader";
import { gameManager } from "../Game/GameManager";
import { ResizableScene } from "./ResizableScene";
import { EnableCameraSceneName } from "./EnableCameraScene";
@ -22,11 +22,13 @@ export class SelectCompanionScene extends ResizableScene {
private currentCompanion = 0;
private pointerClicked: boolean = false;
private pointerTimer: number = 0;
private loader: Loader;
constructor() {
super({
key: SelectCompanionSceneName,
});
this.loader = new Loader(this);
}
preload() {
@ -35,7 +37,7 @@ export class SelectCompanionScene extends ResizableScene {
});
//this function must stay at the end of preload function
addLoader(this);
this.loader.addLoader();
}
create() {

View File

@ -3,8 +3,6 @@ import type { GameScene } from "../Game/GameScene";
import { UserInputEvent, UserInputManager } from "../UserInput/UserInputManager";
import { Character } from "../Entity/Character";
import { userMovingStore } from "../../Stores/GameStore";
import { get } from "svelte/store";
import { emoteMenuStore } from "../../Stores/EmoteStore";
export const hasMovedEventName = "hasMoved";
export const requestEmoteEventName = "requestEmote";
@ -68,10 +66,24 @@ export class Player extends Character {
} else if (this.wasMoving && moving) {
// slow joystick movement
this.move(0, 0);
this.emit(hasMovedEventName, { moving, direction: this.previousDirection, x: this.x, y: this.y, oldX: x, oldY: y });
this.emit(hasMovedEventName, {
moving,
direction: this.previousDirection,
x: this.x,
y: this.y,
oldX: x,
oldY: y,
});
} else if (this.wasMoving && !moving) {
this.stop();
this.emit(hasMovedEventName, { moving, direction: this.previousDirection, x: this.x, y: this.y, oldX: x, oldY: y });
this.emit(hasMovedEventName, {
moving,
direction: this.previousDirection,
x: this.x,
y: this.y,
oldX: x,
oldY: y,
});
}
if (direction !== null) {

View File

@ -67,6 +67,9 @@ function createChatMessagesStore() {
});
},
addPersonnalMessage(text: string) {
//post message iframe listener
iframeListener.sendUserInputChat(text);
newChatMessageStore.set(text);
update((list) => {
const lastMessage = list[list.length - 1];

View File

@ -1,5 +1,11 @@
import { writable } from "svelte/store";
export interface Emoji {
unicode: string;
url: string;
name: string;
}
function createEmoteMenuStore() {
const { subscribe, set } = writable(false);
@ -14,5 +20,5 @@ function createEmoteMenuStore() {
};
}
export const emoteStore = writable<string | null>(null);
export const emoteStore = writable<Emoji | null>(null);
export const emoteMenuStore = createEmoteMenuStore();

View File

@ -34,20 +34,20 @@ export const warningContainerStore = createWarningContainerStore();
export enum SubMenusInterface {
settings = "Settings",
profile = "Profile",
createMap = "Create a Map",
aboutRoom = "About the Room",
invite = "Invite",
aboutRoom = "Credit",
globalMessages = "Global Messages",
contact = "Contact",
}
function createSubMenusStore() {
const { subscribe, update } = writable<string[]>([
SubMenusInterface.settings,
SubMenusInterface.profile,
SubMenusInterface.createMap,
SubMenusInterface.aboutRoom,
SubMenusInterface.globalMessages,
SubMenusInterface.contact,
SubMenusInterface.settings,
SubMenusInterface.invite,
SubMenusInterface.aboutRoom,
]);
return {

View File

@ -3,6 +3,7 @@ import { Subject } from "rxjs";
import { iframeListener } from "../Api/IframeListener";
import { touchScreenManager } from "../Touch/TouchScreenManager";
import { waScaleManager } from "../Phaser/Services/WaScaleManager";
import { ICON_URL } from "../Enum/EnvironmentVariable";
enum iframeStates {
closed = 1,
@ -28,15 +29,15 @@ interface TouchMoveCoordinates {
}
export type CoWebsite = {
iframe: HTMLIFrameElement,
icon: HTMLDivElement,
position: number
}
iframe: HTMLIFrameElement;
icon: HTMLDivElement;
position: number;
};
type CoWebsiteSlot = {
container: HTMLElement,
position: number
}
container: HTMLElement;
position: number;
};
class CoWebsiteManager {
private openedMain: iframeStates = iframeStates.closed;
@ -61,7 +62,7 @@ class CoWebsiteManager {
private slots: CoWebsiteSlot[];
private resizeObserver = new ResizeObserver(entries => {
private resizeObserver = new ResizeObserver((entries) => {
this.resizeAllIframes();
});
@ -108,23 +109,23 @@ class CoWebsiteManager {
this.slots = [
{
container: this.cowebsiteMainDom,
position: 0
position: 0,
},
{
container: HtmlUtils.getElementByIdOrFail<HTMLDivElement>('cowebsite-slot-1'),
position: 1
container: HtmlUtils.getElementByIdOrFail<HTMLDivElement>("cowebsite-slot-1"),
position: 1,
},
{
container: HtmlUtils.getElementByIdOrFail<HTMLDivElement>('cowebsite-slot-2'),
position: 2
container: HtmlUtils.getElementByIdOrFail<HTMLDivElement>("cowebsite-slot-2"),
position: 2,
},
{
container: HtmlUtils.getElementByIdOrFail<HTMLDivElement>('cowebsite-slot-3'),
position: 3
container: HtmlUtils.getElementByIdOrFail<HTMLDivElement>("cowebsite-slot-3"),
position: 3,
},
{
container: HtmlUtils.getElementByIdOrFail<HTMLDivElement>('cowebsite-slot-4'),
position: 4
container: HtmlUtils.getElementByIdOrFail<HTMLDivElement>("cowebsite-slot-4"),
position: 4,
},
];
@ -163,8 +164,10 @@ class CoWebsiteManager {
}
private isSmallScreen(): boolean {
return window.matchMedia("(max-aspect-ratio: 1/1)").matches ||
window.matchMedia("(max-width:960px) and (max-height:768px)").matches;
return (
window.matchMedia("(max-aspect-ratio: 1/1)").matches ||
window.matchMedia("(max-width:960px) and (max-height:768px)").matches
);
}
private initResizeListeners(touchMode: boolean) {
@ -235,12 +238,12 @@ class CoWebsiteManager {
private initActionsListeners() {
this.slots.forEach((slot: CoWebsiteSlot) => {
const expandButton = slot.container.querySelector('.expand');
const highlightButton = slot.container.querySelector('.hightlight');
const closeButton = slot.container.querySelector('.close');
const expandButton = slot.container.querySelector(".expand");
const highlightButton = slot.container.querySelector(".hightlight");
const closeButton = slot.container.querySelector(".close");
if (expandButton) {
expandButton.addEventListener('click', (event) => {
expandButton.addEventListener("click", (event) => {
event.preventDefault();
const coWebsite = this.getCoWebsiteByPosition(slot.position);
@ -253,7 +256,7 @@ class CoWebsiteManager {
}
if (highlightButton) {
highlightButton.addEventListener('click', (event) => {
highlightButton.addEventListener("click", (event) => {
event.preventDefault();
const coWebsite = this.getCoWebsiteByPosition(slot.position);
@ -266,7 +269,7 @@ class CoWebsiteManager {
}
if (closeButton) {
closeButton.addEventListener('click', (event) => {
closeButton.addEventListener("click", (event) => {
event.preventDefault();
const coWebsite = this.getCoWebsiteByPosition(slot.position);
@ -299,16 +302,16 @@ class CoWebsiteManager {
private setIframeOffset(coWebsite: CoWebsite, slot: CoWebsiteSlot) {
const bounding = slot.container.getBoundingClientRect();
if (coWebsite.iframe.classList.contains('thumbnail')) {
coWebsite.iframe.style.width = ((bounding.right - bounding.left) * 2) + 'px';
coWebsite.iframe.style.height = ((bounding.bottom - bounding.top) * 2) + 'px';
coWebsite.iframe.style.top = (bounding.top - (Math.floor(bounding.height * 0.5))) + 'px';
coWebsite.iframe.style.left = (bounding.left - (Math.floor(bounding.width * 0.5))) + 'px';
if (coWebsite.iframe.classList.contains("thumbnail")) {
coWebsite.iframe.style.width = (bounding.right - bounding.left) * 2 + "px";
coWebsite.iframe.style.height = (bounding.bottom - bounding.top) * 2 + "px";
coWebsite.iframe.style.top = bounding.top - Math.floor(bounding.height * 0.5) + "px";
coWebsite.iframe.style.left = bounding.left - Math.floor(bounding.width * 0.5) + "px";
} else {
coWebsite.iframe.style.top = bounding.top + 'px';
coWebsite.iframe.style.left = bounding.left + 'px';
coWebsite.iframe.style.width = (bounding.right - bounding.left) + 'px';
coWebsite.iframe.style.height = (bounding.bottom - bounding.top) + 'px';
coWebsite.iframe.style.top = bounding.top + "px";
coWebsite.iframe.style.left = bounding.left + "px";
coWebsite.iframe.style.width = bounding.right - bounding.left + "px";
coWebsite.iframe.style.height = bounding.bottom - bounding.top + "px";
}
}
@ -333,38 +336,48 @@ class CoWebsiteManager {
coWebsite.iframe.scrolling = newPosition === 0 || newPosition === 1 ? "yes" : "no";
if (newPosition === 0) {
coWebsite.iframe.classList.add('main');
coWebsite.iframe.classList.add("main");
coWebsite.icon.style.display = "none";
} else {
coWebsite.iframe.classList.remove('main');
coWebsite.iframe.classList.remove("main");
coWebsite.icon.style.display = "flex";
}
if (newPosition === 1) {
coWebsite.iframe.classList.add('sub-main');
coWebsite.iframe.classList.add("sub-main");
} else {
coWebsite.iframe.classList.remove('sub-main');
coWebsite.iframe.classList.remove("sub-main");
}
if (newPosition >= 2) {
coWebsite.iframe.classList.add('thumbnail');
coWebsite.iframe.classList.add("thumbnail");
} else {
coWebsite.iframe.classList.remove('thumbnail');
coWebsite.iframe.classList.remove("thumbnail");
}
coWebsite.position = newPosition;
if (oldSlot && !this.getCoWebsiteByPosition(oldSlot.position)) {
oldSlot.container.style.display = 'none';
oldSlot.container.style.display = "none";
}
newSlot.container.style.display = 'block';
this.displayCowebsiteContainer();
coWebsite.iframe.classList.remove('pixel');
newSlot.container.style.display = "block";
coWebsite.iframe.classList.remove("pixel");
this.resizeAllIframes();
}
private displayCowebsiteContainer() {
if (this.coWebsites.find((cowebsite) => cowebsite.position > 0)) {
this.cowebsiteContainerDom.style.display = "block";
} else {
this.cowebsiteContainerDom.style.display = "none";
}
}
private moveLeftPreviousCoWebsite(coWebsite: CoWebsite, newPosition: number) {
const nextCoWebsite = this.getCoWebsiteByPosition(coWebsite.position + 1);
@ -384,11 +397,7 @@ class CoWebsiteManager {
this.moveCoWebsite(coWebsite, newPosition);
if (newPosition === 4 ||
!currentCoWebsite ||
currentCoWebsite.iframe.id === coWebsite.iframe.id
)
{
if (newPosition === 4 || !currentCoWebsite || currentCoWebsite.iframe.id === coWebsite.iframe.id) {
return;
}
@ -411,26 +420,26 @@ class CoWebsiteManager {
if (coWebsite.position > 0) {
const slot = this.getSlotByPosition(coWebsite.position);
if (slot) {
slot.container.style.display = 'none';
slot.container.style.display = "none";
}
}
const previousCoWebsite = this.coWebsites.find((coWebsiteToCheck: CoWebsite) =>
coWebsite.position + 1 === coWebsiteToCheck.position
const previousCoWebsite = this.coWebsites.find(
(coWebsiteToCheck: CoWebsite) => coWebsite.position + 1 === coWebsiteToCheck.position
);
if (previousCoWebsite) {
this.moveLeftPreviousCoWebsite(previousCoWebsite, coWebsite.position);
}
this.displayCowebsiteContainer();
coWebsite.icon.remove();
coWebsite.iframe.remove();
}
public searchJitsi(): CoWebsite | undefined {
return this.coWebsites.find((coWebsite : CoWebsite) =>
coWebsite.iframe.id.toLowerCase().includes('jitsi')
);
return this.coWebsites.find((coWebsite: CoWebsite) => coWebsite.iframe.id.toLowerCase().includes("jitsi"));
}
private generateCoWebsiteIcon(iframe: HTMLIFrameElement): HTMLDivElement {
@ -439,7 +448,7 @@ class CoWebsiteManager {
icon.style.display = "none";
const iconImage = document.createElement("img");
iconImage.src = `https://www.google.com/s2/favicons?sz=64&domain_url=${iframe.src}`;
iconImage.src = `${ICON_URL}/icon?url=${iframe.src}&size=16..30..256`;
const url = new URL(iframe.src);
iconImage.alt = url.hostname;
@ -456,10 +465,10 @@ class CoWebsiteManager {
widthPercent?: number,
position?: number
): Promise<CoWebsite> {
return this.addCoWebsite((iframeBuffer) => {
return this.addCoWebsite(
(iframeBuffer) => {
const iframe = document.createElement("iframe");
iframe.src = new URL(url, base).toString()
iframe.src = new URL(url, base).toString();
if (allowPolicy) {
iframe.allow = allowPolicy;
@ -472,7 +481,10 @@ class CoWebsiteManager {
iframeBuffer.appendChild(iframe);
return iframe;
}, widthPercent, position);
},
widthPercent,
position
);
}
public async addCoWebsite(
@ -484,10 +496,10 @@ class CoWebsiteManager {
if (this.coWebsites.length < 1) {
this.loadMain();
} else if (this.coWebsites.length === 5) {
throw new Error('Too many we')
throw new Error("Too many we");
}
Promise.resolve(callback(this.cowebsiteBufferDom)).then(iframe =>{
Promise.resolve(callback(this.cowebsiteBufferDom)).then((iframe) => {
iframe?.classList.add("pixel");
if (!iframe.id) {
@ -533,14 +545,14 @@ class CoWebsiteManager {
setTimeout(() => {
this.fire();
position !== undefined ?
this.moveRightPreviousCoWebsite(coWebsite, coWebsite.position) :
this.moveCoWebsite(coWebsite, coWebsite.position);
position !== undefined
? this.moveRightPreviousCoWebsite(coWebsite, coWebsite.position)
: this.moveCoWebsite(coWebsite, coWebsite.position);
}, animationTime);
} else {
position !== undefined ?
this.moveRightPreviousCoWebsite(coWebsite, coWebsite.position) :
this.moveCoWebsite(coWebsite, coWebsite.position);
position !== undefined
? this.moveRightPreviousCoWebsite(coWebsite, coWebsite.position)
: this.moveCoWebsite(coWebsite, coWebsite.position);
}
return resolve(coWebsite);
@ -583,8 +595,7 @@ class CoWebsiteManager {
}
public closeCoWebsites(): Promise<void> {
this.currentOperationPromise = this.currentOperationPromise
.then(() => {
this.currentOperationPromise = this.currentOperationPromise.then(() => {
this.coWebsites.forEach((coWebsite: CoWebsite) => {
this.closeCoWebsite(coWebsite);
});

View File

@ -65,6 +65,7 @@
&-container {
position: absolute;
display: none;
height: 100%;
width: 100%;

View File

@ -7,7 +7,6 @@ import MiniCssExtractPlugin from "mini-css-extract-plugin";
import sveltePreprocess from "svelte-preprocess";
import ForkTsCheckerWebpackPlugin from "fork-ts-checker-webpack-plugin";
import NodePolyfillPlugin from "node-polyfill-webpack-plugin";
import { POSTHOG_API_KEY, PROFILE_URL } from "./src/Enum/EnvironmentVariable";
const mode = process.env.NODE_ENV ?? "development";
const buildNpmTypingsForApi = !!process.env.BUILD_TYPINGS;
@ -193,6 +192,7 @@ module.exports = {
ADMIN_URL: undefined,
CONTACT_URL: null,
PROFILE_URL: null,
ICON_URL: null,
DEBUG_MODE: null,
STUN_SERVER: null,
TURN_SERVER: null,
@ -207,6 +207,8 @@ module.exports = {
POSTHOG_API_KEY: null,
POSTHOG_URL: null,
NODE_ENV: mode,
DISABLE_ANONYMOUS: false,
OPID_LOGIN_SCREEN_PROVIDER: null,
}),
],
} as Configuration & WebpackDevServer.Configuration;

View File

@ -1,16 +1,18 @@
WA.onInit().then(() => {
let message;
WA.room.onEnterZone("carpet", () => {
WA.room.onEnterLayer("carpet").subscribe(() => {
message = WA.ui.displayActionMessage({
message: "This is a test message. Press space to display a chat message. Walk out to hide the message.",
message:
"This is a test message. Press space to display a chat message. Walk out to hide the message.",
callback: () => {
WA.chat.sendChatMessage("Hello world!", "The bot");
}
},
});
});
WA.room.onLeaveZone("carpet", () => {
WA.room.onLeaveLayer("carpet").subscribe(() => {
message && message.remove();
});
});

1
maps/tests/Variables/Cache/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
variables_tmp.json

View File

@ -0,0 +1,5 @@
WA.onInit().then(async () => {
console.log('Trying to set variable "myvar". This should work, even if the cache was busted.');
await WA.state.saveVariable('myvar', {'foo': 'bar'});
console.log('SUCCESS!');
});

View File

@ -0,0 +1,82 @@
{ "compressionlevel":-1,
"height":10,
"infinite":false,
"layers":[
{
"data":[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
"height":10,
"id":1,
"name":"floor",
"opacity":1,
"type":"tilelayer",
"visible":true,
"width":10,
"x":0,
"y":0
},
{
"data":[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
"height":10,
"id":2,
"name":"start",
"opacity":1,
"type":"tilelayer",
"visible":true,
"width":10,
"x":0,
"y":0
},
{
"draworder":"topdown",
"id":3,
"name":"floorLayer",
"objects":[
{
"height":67,
"id":3,
"name":"",
"rotation":0,
"text":
{
"fontfamily":"Sans Serif",
"pixelsize":11,
"text":"Test:\nThis test is to be run automatically using testcafe",
"wrap":true
},
"type":"",
"visible":true,
"width":252.4375,
"x":2.78125,
"y":2.5
}],
"opacity":1,
"type":"objectgroup",
"visible":true,
"x":0,
"y":0
}],
"nextlayerid":8,
"nextobjectid":11,
"orientation":"orthogonal",
"renderorder":"right-down",
"tiledversion":"2021.03.23",
"tileheight":32,
"tilesets":[
{
"columns":11,
"firstgid":1,
"image":"..\/..\/tileset1.png",
"imageheight":352,
"imagewidth":352,
"margin":0,
"name":"tileset1",
"spacing":0,
"tilecount":121,
"tileheight":32,
"tilewidth":32
}],
"tilewidth":32,
"type":"map",
"version":1.5,
"width":10
}

View File

@ -0,0 +1,126 @@
{ "compressionlevel":-1,
"height":10,
"infinite":false,
"layers":[
{
"data":[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
"height":10,
"id":1,
"name":"floor",
"opacity":1,
"type":"tilelayer",
"visible":true,
"width":10,
"x":0,
"y":0
},
{
"data":[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
"height":10,
"id":2,
"name":"start",
"opacity":1,
"type":"tilelayer",
"visible":true,
"width":10,
"x":0,
"y":0
},
{
"draworder":"topdown",
"id":3,
"name":"floorLayer",
"objects":[
{
"height":67,
"id":3,
"name":"",
"rotation":0,
"text":
{
"fontfamily":"Sans Serif",
"pixelsize":11,
"text":"Test:\nThis test is to be run automatically using testcafe\n\n(2nd file)",
"wrap":true
},
"type":"",
"visible":true,
"width":252.4375,
"x":2.78125,
"y":2.5
},
{
"height":0,
"id":9,
"name":"myvar",
"point":true,
"properties":[
{
"name":"default",
"type":"string",
"value":"{}"
},
{
"name":"jsonSchema",
"type":"string",
"value":"{}"
},
{
"name":"persist",
"type":"bool",
"value":true
},
{
"name":"readableBy",
"type":"string",
"value":""
},
{
"name":"writableBy",
"type":"string",
"value":""
}],
"rotation":0,
"type":"variable",
"visible":true,
"width":0,
"x":88.8149900876127,
"y":147.75212636695
}],
"opacity":1,
"type":"objectgroup",
"visible":true,
"x":0,
"y":0
}],
"nextlayerid":8,
"nextobjectid":11,
"orientation":"orthogonal",
"properties":[
{
"name":"script",
"type":"string",
"value":"script.js"
}],
"renderorder":"right-down",
"tiledversion":"2021.03.23",
"tileheight":32,
"tilesets":[
{
"columns":11,
"firstgid":1,
"image":"..\/..\/tileset1.png",
"imageheight":352,
"imagewidth":352,
"margin":0,
"name":"tileset1",
"spacing":0,
"tilecount":121,
"tileheight":32,
"tilewidth":32
}],
"tilewidth":32,
"type":"map",
"version":1.5,
"width":10
}

View File

@ -6,6 +6,7 @@ import { PrometheusController } from "./Controller/PrometheusController";
import { DebugController } from "./Controller/DebugController";
import { App as uwsApp } from "./Server/sifrr.server";
import { AdminController } from "./Controller/AdminController";
import { OpenIdProfileController } from "./Controller/OpenIdProfileController";
class App {
public app: uwsApp;
@ -15,6 +16,7 @@ class App {
public prometheusController: PrometheusController;
private debugController: DebugController;
private adminController: AdminController;
private openIdProfileController: OpenIdProfileController;
constructor() {
this.app = new uwsApp();
@ -26,6 +28,7 @@ class App {
this.prometheusController = new PrometheusController(this.app);
this.debugController = new DebugController(this.app);
this.adminController = new AdminController(this.app);
this.openIdProfileController = new OpenIdProfileController(this.app);
}
}

View File

@ -1,10 +1,11 @@
import { v4 } from "uuid";
import { HttpRequest, HttpResponse, TemplatedApp } from "uWebSockets.js";
import { BaseController } from "./BaseController";
import { adminApi } from "../Services/AdminApi";
import { adminApi, FetchMemberDataByUuidResponse } from "../Services/AdminApi";
import { AuthTokenData, jwtTokenManager } from "../Services/JWTTokenManager";
import { parse } from "query-string";
import { openIDClient } from "../Services/OpenIDClient";
import { DISABLE_ANONYMOUS } from "../Enum/EnvironmentVariable";
import log from "../Services/Logger";
export interface TokenInterface {
@ -56,19 +57,37 @@ export class AuthenticateController extends BaseController {
res.onAborted(() => {
log.warn("/message request was aborted");
});
const { code, nonce, token } = parse(req.getQuery());
const IPAddress = req.getHeader("x-forwarded-for");
const { code, nonce, token, playUri } = parse(req.getQuery());
try {
//verify connected by token
if (token != undefined) {
try {
const authTokenData: AuthTokenData = jwtTokenManager.verifyJWTToken(token as string, false);
if (authTokenData.hydraAccessToken == undefined) {
throw Error("Token cannot to be check on Hydra");
}
await openIDClient.checkTokenAuth(authTokenData.hydraAccessToken);
//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 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({ authToken: token }));
return res.end(JSON.stringify({ ...resUserData, authToken: token }));
}
throw Error("Token cannot to be check on Hydra");
}
const resCheckTokenAuth = await openIDClient.checkTokenAuth(authTokenData.accessToken);
res.writeStatus("200");
this.addCorsHeaders(res);
return res.end(JSON.stringify({ ...resCheckTokenAuth, ...resUserData, authToken: token }));
} catch (err) {
log.info("User was not connected", err);
}
@ -80,10 +99,15 @@ export class AuthenticateController extends BaseController {
if (!email) {
throw new Error("No email in the response");
}
const authToken = jwtTokenManager.createAuthToken(email, userInfo.access_token);
const authToken = jwtTokenManager.createAuthToken(email, userInfo?.access_token);
//Get user data from Admin Back Office
//This is very important to create User Local in LocalStorage in WorkAdventure
const data = await this.getUserByUserIdentifier(email, playUri as string, IPAddress);
res.writeStatus("200");
this.addCorsHeaders(res);
return res.end(JSON.stringify({ authToken }));
return res.end(JSON.stringify({ ...data, authToken }));
} catch (e) {
log.error("openIDCallback => ERROR", e);
return this.errorToResponse(e, res);
@ -100,10 +124,10 @@ export class AuthenticateController extends BaseController {
try {
const authTokenData: AuthTokenData = jwtTokenManager.verifyJWTToken(token as string, false);
if (authTokenData.hydraAccessToken == undefined) {
if (authTokenData.accessToken == undefined) {
throw Error("Token cannot to be logout on Hydra");
}
await openIDClient.logoutUser(authTokenData.hydraAccessToken);
await openIDClient.logoutUser(authTokenData.accessToken);
} catch (error) {
log.error("openIDCallback => logout-callback", error);
} finally {
@ -176,6 +200,10 @@ export class AuthenticateController extends BaseController {
log.warn("Login request was aborted");
});
if (DISABLE_ANONYMOUS) {
res.writeStatus("403 FORBIDDEN");
res.end();
} else {
const userUuid = v4();
const authToken = jwtTokenManager.createAuthToken(userUuid);
res.writeStatus("200 OK");
@ -186,6 +214,7 @@ export class AuthenticateController extends BaseController {
userUuid,
})
);
}
});
}
@ -197,20 +226,20 @@ export class AuthenticateController extends BaseController {
res.onAborted(() => {
log.warn("/message request was aborted");
});
const { userIdentify, token } = parse(req.getQuery());
const { token } = parse(req.getQuery());
try {
//verify connected by token
if (token != undefined) {
try {
const authTokenData: AuthTokenData = jwtTokenManager.verifyJWTToken(token as string, false);
if (authTokenData.hydraAccessToken == undefined) {
if (authTokenData.accessToken == undefined) {
throw Error("Token cannot to be check on Hydra");
}
await openIDClient.checkTokenAuth(authTokenData.hydraAccessToken);
await openIDClient.checkTokenAuth(authTokenData.accessToken);
//get login profile
res.writeStatus("302");
res.writeHeader("Location", adminApi.getProfileUrl(authTokenData.hydraAccessToken));
res.writeHeader("Location", adminApi.getProfileUrl(authTokenData.accessToken));
this.addCorsHeaders(res);
// eslint-disable-next-line no-unsafe-finally
return res.end();
@ -224,4 +253,33 @@ export class AuthenticateController extends BaseController {
}
});
}
/**
*
* @param email
* @param playUri
* @param IPAddress
* @return FetchMemberDataByUuidResponse|object
* @private
*/
private async getUserByUserIdentifier(
email: string,
playUri: string,
IPAddress: string
): Promise<FetchMemberDataByUuidResponse | object> {
let data: FetchMemberDataByUuidResponse = {
email: email,
userUuid: email,
tags: [],
messages: [],
visitCardUrl: null,
textures: [],
};
try {
data = await adminApi.fetchMemberDataByUuid(email, playUri, IPAddress);
} catch (err) {
console.error("openIDCallback => fetchMemberDataByUuid", err);
}
return data;
}
}

View File

@ -1,11 +1,12 @@
import { HttpResponse } from "uWebSockets.js";
import { FRONT_URL } from "../Enum/EnvironmentVariable";
import log from "../Services/Logger";
export class BaseController {
protected addCorsHeaders(res: HttpResponse): void {
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-origin", "*");
res.writeHeader("access-control-allow-origin", FRONT_URL);
}
/**

View File

@ -26,10 +26,9 @@ import { jwtTokenManager, tokenInvalidException } from "../Services/JWTTokenMana
import { adminApi, FetchMemberDataByUuidResponse } from "../Services/AdminApi";
import { SocketManager, socketManager } from "../Services/SocketManager";
import { emitInBatch } from "../Services/IoSocketHelpers";
import { ADMIN_API_TOKEN, ADMIN_API_URL, SOCKET_IDLE_TIMER } from "../Enum/EnvironmentVariable";
import { ADMIN_SOCKETS_TOKEN, ADMIN_API_URL, DISABLE_ANONYMOUS, SOCKET_IDLE_TIMER } from "../Enum/EnvironmentVariable";
import { Zone } from "_Model/Zone";
import { ExAdminSocketInterface } from "_Model/Websocket/ExAdminSocketInterface";
import { v4 } from "uuid";
import { CharacterTexture } from "../Services/AdminApi/CharacterTexture";
import log from "../Services/Logger";
@ -49,15 +48,19 @@ export class IoSocketController {
const websocketProtocol = req.getHeader("sec-websocket-protocol");
const websocketExtensions = req.getHeader("sec-websocket-extensions");
const token = query.token;
if (token !== ADMIN_API_TOKEN) {
let authorizedRoomIds: string[];
try {
const data = jwtTokenManager.verifyAdminSocketToken(token as string);
authorizedRoomIds = data.authorizedRoomIds;
} catch (e) {
log.info("Admin access refused for token: " + token);
res.writeStatus("401 Unauthorized").end("Incorrect token");
return;
}
const roomId = query.roomId;
if (typeof roomId !== "string") {
log.error("Received");
res.writeStatus("400 Bad Request").end("Missing room id");
if (typeof roomId !== "string" || !authorizedRoomIds.includes(roomId)) {
log.error("Invalid room id");
res.writeStatus("403 Bad Request").end("Invalid room id");
return;
}
@ -71,8 +74,6 @@ export class IoSocketController {
},
message: (ws, arrayBuffer, isBinary): void => {
try {
const roomId = ws.roomId as string;
//TODO refactor message type and data
const message: { event: string; message: { type: string; message: unknown; userUuid: string } } =
JSON.parse(new TextDecoder("utf-8").decode(new Uint8Array(arrayBuffer)));
@ -176,6 +177,11 @@ export class IoSocketController {
const tokenData =
token && typeof token === "string" ? jwtTokenManager.verifyJWTToken(token) : null;
if (DISABLE_ANONYMOUS && !tokenData) {
throw new Error("Expecting token");
}
const userIdentifier = tokenData ? tokenData.identifier : "";
let memberTags: string[] = [];
@ -184,6 +190,7 @@ export class IoSocketController {
let memberTextures: CharacterTexture[] = [];
const room = await socketManager.getOrCreateRoom(roomId);
let userData: FetchMemberDataByUuidResponse = {
email: userIdentifier,
userUuid: userIdentifier,
tags: [],
visitCardUrl: null,

View File

@ -2,9 +2,9 @@ import { HttpRequest, HttpResponse, TemplatedApp } from "uWebSockets.js";
import { BaseController } from "./BaseController";
import { parse } from "query-string";
import { adminApi } from "../Services/AdminApi";
import { ADMIN_API_URL } from "../Enum/EnvironmentVariable";
import { ADMIN_API_URL, DISABLE_ANONYMOUS, FRONT_URL } from "../Enum/EnvironmentVariable";
import { GameRoomPolicyTypes } from "../Model/PusherRoom";
import { MapDetailsData } from "../Services/AdminApi/MapDetailsData";
import { isMapDetailsData, MapDetailsData } from "../Services/AdminApi/MapDetailsData";
import { socketManager } from "../Services/SocketManager";
import { AuthTokenData, jwtTokenManager } from "../Services/JWTTokenManager";
import { v4 } from "uuid";
@ -21,7 +21,6 @@ export class MapController extends BaseController {
getMapUrl() {
this.App.options("/map", (res: HttpResponse, req: HttpRequest) => {
this.addCorsHeaders(res);
res.end();
});
@ -65,6 +64,7 @@ export class MapController extends BaseController {
tags: [],
textures: [],
contactPage: undefined,
authenticationMandatory: DISABLE_ANONYMOUS,
} as MapDetailsData)
);
@ -80,14 +80,26 @@ export class MapController extends BaseController {
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;
log.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);
if (isMapDetailsData(mapDetails) && DISABLE_ANONYMOUS) {
mapDetails.authenticationMandatory = true;
}
res.writeStatus("200 OK");
this.addCorsHeaders(res);
res.end(JSON.stringify(mapDetails));

View File

@ -0,0 +1,80 @@
import { BaseController } from "./BaseController";
import { HttpRequest, HttpResponse, TemplatedApp } from "uWebSockets.js";
import { parse } from "query-string";
import { openIDClient } from "../Services/OpenIDClient";
import { AuthTokenData, jwtTokenManager } from "../Services/JWTTokenManager";
import { adminApi } from "../Services/AdminApi";
import { OPID_CLIENT_ISSUER } from "../Enum/EnvironmentVariable";
import { IntrospectionResponse } from "openid-client";
export class OpenIdProfileController extends BaseController {
constructor(private App: TemplatedApp) {
super();
this.profileOpenId();
}
profileOpenId() {
//eslint-disable-next-line @typescript-eslint/no-misused-promises
this.App.get("/profile", async (res: HttpResponse, req: HttpRequest) => {
res.onAborted(() => {
console.warn("/message request was aborted");
});
const { accessToken } = parse(req.getQuery());
if (!accessToken) {
throw Error("Access token expected cannot to be check on Hydra");
}
try {
const resCheckTokenAuth = await openIDClient.checkTokenAuth(accessToken as string);
if (!resCheckTokenAuth.email) {
throw "Email was not found";
}
res.end(
this.buildHtml(
OPID_CLIENT_ISSUER,
resCheckTokenAuth.email as string,
resCheckTokenAuth.picture as string | undefined
)
);
} catch (error) {
console.error("profileCallback => ERROR", error);
this.errorToResponse(error, res);
}
});
}
buildHtml(domain: string, email: string, pictureUrl?: string) {
return `
<!DOCTYPE>
<html>
<head>
<style>
*{
font-family: PixelFont-7, monospace;
}
body{
text-align: center;
color: white;
}
section{
margin: 20px;
}
</style>
</head>
<body>
<div class="container">
<section>
<img src="${pictureUrl ? pictureUrl : "/images/profile"}">
</section>
<section>
Profile validated by domain: <span style="font-weight: bold">${domain}</span>
</section>
<section>
Your email: <span style="font-weight: bold">${email}</span>
</section>
</div>
</body>
</html>
`;
}
}

View File

@ -4,6 +4,7 @@ const API_URL = process.env.API_URL || "";
const ADMIN_API_URL = process.env.ADMIN_API_URL || "";
const ADMIN_URL = process.env.ADMIN_URL || "";
const ADMIN_API_TOKEN = process.env.ADMIN_API_TOKEN || "myapitoken";
export const ADMIN_SOCKETS_TOKEN = process.env.ADMIN_SOCKETS_TOKEN || "myapitoken";
const CPU_OVERHEAT_THRESHOLD = Number(process.env.CPU_OVERHEAT_THRESHOLD) || 80;
const JITSI_URL: string | undefined = process.env.JITSI_URL === "" ? undefined : process.env.JITSI_URL;
const JITSI_ISS = process.env.JITSI_ISS || "";
@ -15,6 +16,9 @@ export const FRONT_URL = process.env.FRONT_URL || "http://localhost";
export const OPID_CLIENT_ID = process.env.OPID_CLIENT_ID || "";
export const OPID_CLIENT_SECRET = process.env.OPID_CLIENT_SECRET || "";
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_PROFILE_SCREEN_PROVIDER = process.env.OPID_PROFILE_SCREEN_PROVIDER || ADMIN_URL + "/profile";
export const DISABLE_ANONYMOUS: boolean = process.env.DISABLE_ANONYMOUS === "true";
export {
SECRET_KEY,

View File

@ -1,4 +1,4 @@
import { ADMIN_API_TOKEN, ADMIN_API_URL, ADMIN_URL } from "../Enum/EnvironmentVariable";
import { ADMIN_API_TOKEN, ADMIN_API_URL, ADMIN_URL, OPID_PROFILE_SCREEN_PROVIDER } from "../Enum/EnvironmentVariable";
import Axios from "axios";
import { GameRoomPolicyTypes } from "_Model/PusherRoom";
import { CharacterTexture } from "./AdminApi/CharacterTexture";
@ -22,6 +22,7 @@ export interface AdminBannedData {
}
export interface FetchMemberDataByUuidResponse {
email: string;
userUuid: string;
tags: string[];
visitCardUrl: string | null;
@ -142,13 +143,19 @@ class AdminApi {
});
}
/*TODO add constant to use profile companny*/
/**
*
* @param accessToken
*/
getProfileUrl(accessToken: string): string {
if (!ADMIN_URL) {
if (!OPID_PROFILE_SCREEN_PROVIDER) {
throw new Error("No admin backoffice set!");
}
return `${OPID_PROFILE_SCREEN_PROVIDER}?accessToken=${accessToken}`;
}
return ADMIN_URL + `/profile?token=${accessToken}`;
async logoutOauth(token: string) {
await Axios.get(ADMIN_API_URL + `/oauth/logout?token=${token}`);
}
}

View File

@ -16,6 +16,7 @@ export const isMapDetailsData = new tg.IsInterface()
tags: tg.isArray(tg.isString),
textures: tg.isArray(isCharacterTexture),
contactPage: tg.isUnion(tg.isString, tg.isUndefined),
authenticationMandatory: tg.isUnion(tg.isBoolean, tg.isUndefined),
})
.get();

View File

@ -1,4 +1,4 @@
import { ADMIN_API_URL, ALLOW_ARTILLERY, SECRET_KEY } from "../Enum/EnvironmentVariable";
import { ADMIN_API_URL, ADMIN_SOCKETS_TOKEN, ALLOW_ARTILLERY, SECRET_KEY } from "../Enum/EnvironmentVariable";
import { uuid } from "uuidv4";
import Jwt, { verify } from "jsonwebtoken";
import { TokenInterface } from "../Controller/AuthenticateController";
@ -6,13 +6,20 @@ import { adminApi, AdminBannedData } from "../Services/AdminApi";
export interface AuthTokenData {
identifier: string; //will be a email if logged in or an uuid if anonymous
hydraAccessToken?: string;
accessToken?: string;
}
export interface AdminSocketTokenData {
authorizedRoomIds: string[]; //the list of rooms the client is authorized to read from.
}
export const tokenInvalidException = "tokenInvalid";
class JWTTokenManager {
public createAuthToken(identifier: string, hydraAccessToken?: string) {
return Jwt.sign({ identifier, hydraAccessToken }, SECRET_KEY, { expiresIn: "30d" });
public verifyAdminSocketToken(token: string): AdminSocketTokenData {
return Jwt.verify(token, ADMIN_SOCKETS_TOKEN) as AdminSocketTokenData;
}
public createAuthToken(identifier: string, accessToken?: string) {
return Jwt.sign({ identifier, accessToken }, SECRET_KEY, { expiresIn: "30d" });
}
public verifyJWTToken(token: string, ignoreExpiration: boolean = false): AuthTokenData {

View File

@ -1,7 +1,10 @@
import { Issuer, Client, IntrospectionResponse } from "openid-client";
import { OPID_CLIENT_ID, OPID_CLIENT_SECRET, OPID_CLIENT_ISSUER, FRONT_URL } from "../Enum/EnvironmentVariable";
const opidRedirectUri = FRONT_URL + "/jwt";
import {
OPID_CLIENT_ID,
OPID_CLIENT_SECRET,
OPID_CLIENT_ISSUER,
OPID_CLIENT_REDIRECT_URL,
} from "../Enum/EnvironmentVariable";
class OpenIDClient {
private issuerPromise: Promise<Client> | null = null;
@ -12,7 +15,7 @@ class OpenIDClient {
return new issuer.Client({
client_id: OPID_CLIENT_ID,
client_secret: OPID_CLIENT_SECRET,
redirect_uris: [opidRedirectUri],
redirect_uris: [OPID_CLIENT_REDIRECT_URL],
response_types: ["code"],
});
});
@ -35,7 +38,7 @@ class OpenIDClient {
public getUserInfo(code: string, nonce: string): Promise<{ email: string; sub: string; access_token: string }> {
return this.initClient().then((client) => {
return client.callback(opidRedirectUri, { code }, { nonce }).then((tokenSet) => {
return client.callback(OPID_CLIENT_REDIRECT_URL, { code }, { nonce }).then((tokenSet) => {
return client.userinfo(tokenSet).then((res) => {
return {
...res,

1
tests/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/node_modules

13
tests/.testcaferc.js Normal file
View File

@ -0,0 +1,13 @@
const BROWSER = process.env.BROWSER || "chrome --use-fake-ui-for-media-stream --use-fake-device-for-media-stream";
module.exports = {
"browsers": BROWSER,
"hostname": "localhost",
//"skipJsErrors": true,
"src": "tests/",
"screenshots": {
"path": "screenshots/",
"takeOnFails": true,
"thumbnails": false,
}
}

18
tests/README.md Normal file
View File

@ -0,0 +1,18 @@
End-to-end tests
This directory contains automated end to end tests.
To run them locally:
```console
$ npm install
$ npm test
```
Alternatively, you can use docker-compose to run the tests:
```console
$ docker-compose -f docker-compose.testcafe.yml up --exit-code-from testcafe
```
Note: by default, tests are running in Chrome locally and in Chromium in the Docker image.

8915
tests/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

8
tests/package.json Normal file
View File

@ -0,0 +1,8 @@
{
"devDependencies": {
"testcafe": "^1.17.1"
},
"scripts": {
"test": "testcafe"
}
}

1
tests/screenshots/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
*

67
tests/tests/test.ts Normal file
View File

@ -0,0 +1,67 @@
const fs = require('fs')
fixture `Variables`
.page `http://play.workadventure.localhost/_/global/maps.workadventure.localhost/tests/Variables/Cache/variables_tmp.json`;
test("Test that variables cache in the back don't prevent setting a variable in case the map changes", async t => {
// Let's start by visiting a map that DOES not have the variable.
fs.copyFileSync('../maps/tests/Variables/Cache/variables_cache_1.json', '../maps/tests/Variables/Cache/variables_tmp.json');
await t
.typeText('input[name="loginSceneName"]', 'foo')
.click('button.loginSceneFormSubmit')
.click('button.selectCharacterButtonRight')
.click('button.selectCharacterButtonRight')
.click('button.selectCharacterSceneFormSubmit')
.click('button.letsgo');
//.takeScreenshot('before_switch.png');
// Let's REPLACE the map by a map that has a new variable
// At this point, the back server contains a cache of the old map (with no variables)
fs.copyFileSync('../maps/tests/Variables/Cache/variables_cache_2.json', '../maps/tests/Variables/Cache/variables_tmp.json');
await t.openWindow('http://play.workadventure.localhost/_/global/maps.workadventure.localhost/tests/Variables/Cache/variables_tmp.json');
await t.resizeWindow(960, 800);
await t
.typeText('input[name="loginSceneName"]', 'foo')
.click('button.loginSceneFormSubmit')
.click('button.selectCharacterButtonRight')
.click('button.selectCharacterButtonRight')
.click('button.selectCharacterSceneFormSubmit')
.click('button.letsgo');
//.takeScreenshot('after_switch.png');
const messages = await t.getBrowserConsoleMessages();
const logs = messages['log'];
// Let's check we successfully manage to save the variable value.
await assertLogMessage(t, 'SUCCESS!');
t.ctx.passed = true;
}).after(async t => {
if (!t.ctx.passed) {
console.log("Test failed. Browser logs:")
console.log(await t.getBrowserConsoleMessages());
}
});
/**
* Tries to find a given log message in the logs (for 10 seconds)
*/
async function assertLogMessage(t, message: string): Promise<void> {
let i = 0;
let logs: string[]|undefined;
do {
const messages = await t.getBrowserConsoleMessages();
logs = messages['log'];
if (logs.find((str) => str === message)) {
break;
}
await t.wait(1000);
i++;
} while (i < 10);
await t.expect(logs).contains(message);
}