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

This commit is contained in:
_Bastler 2021-11-16 10:56:33 +01:00
commit e43e1d8463
49 changed files with 755 additions and 489 deletions

View File

@ -19,9 +19,13 @@ ACME_EMAIL=
MAX_PER_GROUP=4
MAX_USERNAME_LENGTH=8
OIDC_CLIENT_ID=
OIDC_CLIENT_SECRET=
OIDC_CLIENT_ISSUER=
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=

View File

@ -51,9 +51,9 @@ services:
JITSI_URL: ${JITSI_URL}
JITSI_ISS: ${JITSI_ISS}
FRONT_URL : ${FRONT_URL}
OIDC_CLIENT_ID: ${OIDC_CLIENT_ID}
OIDC_CLIENT_SECRET: ${OIDC_CLIENT_SECRET}
OIDC_CLIENT_ISSUER: ${OIDC_CLIENT_ISSUER}
OPID_CLIENT_ID: ${OPID_CLIENT_ID}
OPID_CLIENT_SECRET: ${OPID_CLIENT_SECRET}
OPID_CLIENT_ISSUER: ${OPID_CLIENT_ISSUER}
labels:
- "traefik.http.middlewares.strip-pusher-prefix.stripprefix.prefixes=/pusher"
- "traefik.http.routers.pusher.rule=PathPrefix(`/pusher`)"

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

@ -40,6 +40,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
@ -67,9 +68,12 @@ services:
JITSI_URL: $JITSI_URL
JITSI_ISS: $JITSI_ISS
FRONT_URL: http://localhost
OIDC_CLIENT_ID: $OIDC_CLIENT_ID
OIDC_CLIENT_SECRET: $OIDC_CLIENT_SECRET
OIDC_CLIENT_ISSUER: $OIDC_CLIENT_ISSUER
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:

View File

@ -43,6 +43,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
@ -68,9 +70,12 @@ services:
JITSI_URL: $JITSI_URL
JITSI_ISS: $JITSI_ISS
FRONT_URL: http://play.workadventure.localhost
OIDC_CLIENT_ID: $OIDC_CLIENT_ID
OIDC_CLIENT_SECRET: $OIDC_CLIENT_SECRET
OIDC_CLIENT_ISSUER: $OIDC_CLIENT_ISSUER
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:

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

@ -273,7 +273,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

@ -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,38 +0,0 @@
<script lang="ts">
function goToBuildingMap() {
const sparkHost = "https://wiki.bstly.de/services/partey/map";
window.open(sparkHost, "_blank");
}
</script>
<div class="create-map-main">
<section class="container-overflow">
<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">
let HTMLShareLink: HTMLInputElement;
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="guest-main">
<section class="container-overflow">
<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>
</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

@ -3,11 +3,11 @@
import SettingsSubMenu from "./SettingsSubMenu.svelte";
import ProfileSubMenu from "./ProfileSubMenu.svelte";
import WorldsSubMenu from "./WorldsSubMenu.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,
@ -20,21 +20,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(() => {
@ -56,8 +56,8 @@
case SubMenusInterface.worlds:
activeComponent = WorldsSubMenu;
break;
case SubMenusInterface.createMap:
activeComponent = CreateMapSubMenu;
case SubMenusInterface.invite:
activeComponent = GuestSubMenu;
break;
case SubMenusInterface.aboutRoom:
activeComponent = AboutRoomSubMenu;

View File

@ -30,7 +30,6 @@
z-index: 90;
position: relative;
margin: 25px;
img {
pointer-events: auto;
width: 24px;
@ -38,7 +37,6 @@
margin: 3px
}
}
.menuIcon img:hover{
transform: scale(1.2);
}

View File

@ -3,6 +3,7 @@
import {SelectCompanionScene, SelectCompanionSceneName} from "../../Phaser/Login/SelectCompanionScene";
import {menuIconVisiblilityStore, menuVisiblilityStore, userIsConnected} from "../../Stores/MenuStore";
import {selectCompanionSceneVisibleStore} from "../../Stores/SelectCompanionStore";
import {LoginScene, LoginSceneName} from "../../Phaser/Login/LoginScene";
import {loginSceneVisibleStore} from "../../Stores/LoginSceneStore";
import {selectCharacterSceneVisibleStore} from "../../Stores/SelectCharacterStore";
import {SelectCharacterScene, SelectCharacterSceneName} from "../../Phaser/Login/SelectCharacterScene";
@ -11,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(){
@ -24,6 +29,12 @@
gameManager.leaveGame(SelectCompanionSceneName,new SelectCompanionScene());
}
function openEditNameScene(){
disableMenuStores();
loginSceneVisibleStore.set(true);
gameManager.leaveGame(LoginSceneName,new LoginScene());
}
function openEditSkinScene(){
disableMenuStores();
selectCharacterSceneVisibleStore.set(true);
@ -48,13 +59,30 @@
</script>
<div class="customize-main">
<div class="submenu">
<section>
<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>
<!--
<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 Avatar">
<span class="btn-hover">Edit your Avatar</span>
</button>
<button type="button" class="nes-btn" on:click|preventDefault={openEditCompanionScene}>
<img src={btnProfileSubMenuCompanion} alt="Change your companion">
<span class="btn-hover">Change 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}
@ -70,12 +98,47 @@
</section>
{/if}
</div>
</div>
<style lang="scss">
div.customize-main{
overflow-y: auto;
height: 90%;
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;
@ -95,9 +158,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

@ -42,20 +42,11 @@ class ConnectionManager {
const nonce = localUserStore.generateNonce();
localUserStore.setAuthToken(null);
//TODO fix me to redirect this URL by pusher
if (!this._currentRoom) {
if (!this._currentRoom || !this._currentRoom.iframeAuthentication) {
loginSceneVisibleIframeStore.set(false);
return null;
}
// also allow OIDC login without admin API by using pusher
let redirectUrl : URL;
if (this._currentRoom.iframeAuthentication) {
redirectUrl = new URL(`${this._currentRoom.iframeAuthentication}`);
} else {
// need origin if PUSHER_URL is relative (in Single-Domain-Deployment)
redirectUrl = new URL(`${PUSHER_URL}/login-screen`, (!PUSHER_URL.startsWith('http:') || !PUSHER_URL.startsWith('https:')) ? window.location.origin : undefined);
}
const redirectUrl = new URL(`${this._currentRoom.iframeAuthentication}`);
redirectUrl.searchParams.append("state", state);
redirectUrl.searchParams.append("nonce", nonce);
redirectUrl.searchParams.append("playUri", this._currentRoom.key);
@ -88,6 +79,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) {
@ -96,6 +97,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)) {
@ -105,6 +108,8 @@ class ConnectionManager {
throw "No Auth code provided";
}
localUserStore.setCode(code);
}
this._currentRoom = await Room.createRoom(new URL(localUserStore.getLastRoomUrl()));
try {
await this.checkAuthUserConnexion();
@ -173,14 +178,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
@ -208,13 +219,13 @@ 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> {
localUserStore.setAuthToken(null);
try {
const data = await Axios.post(`${PUSHER_URL}/anonymLogin`).then((res) => res.data);
this.localUser = new LocalUser(data.userUuid, []);
this.authToken = data.authToken;
@ -223,9 +234,6 @@ class ConnectionManager {
localUserStore.saveUser(this.localUser);
localUserStore.setAuthToken(this.authToken);
}
} catch (error) {
this.loadOpenIDScreen();
}
}
public initBenchmark(): void {
@ -293,33 +301,30 @@ 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, username, userUuid } = await Axios.get(`${PUSHER_URL}/login-callback`, { params: { code, nonce, token } }).then(
(res) => res.data
);
this.localUser = new LocalUser(userUuid, []);
this.authToken = authToken;
}
const { authToken, userUuid, textures, email, username } = await Axios.get(`${PUSHER_URL}/login-callback`, {
params: { code, nonce, token },
}).then((res) => res.data);
localUserStore.setAuthToken(authToken);
this.localUser = new LocalUser(userUuid, textures, email);
localUserStore.saveUser(this.localUser);
localUserStore.setAuthToken(this.authToken);
this.authToken = authToken;
gameManager.setPlayerName(username);
//user connected, set connected store for menu at true
userIsConnected.set(true);
if (this.connexionType === GameConnexionTypes.anonymous) {
this.connexionType = GameConnexionTypes.empty
}
}
async getWorlds() {

View File

@ -120,19 +120,12 @@ class LocalUserStore {
return localStorage.getItem(fullscreenKey) === "true";
}
setLastRoomUrl(roomUrl: string | null): void {
if (roomUrl) {
setLastRoomUrl(roomUrl: string): void {
localStorage.setItem(lastRoomUrl, roomUrl.toString());
caches.open(cacheAPIIndex).then((cache) => {
const stringResponse = new Response(JSON.stringify({ roomUrl }));
cache.put(`/${lastRoomUrl}`, stringResponse);
});
} else {
localStorage.removeItem(lastRoomUrl);
caches.open(cacheAPIIndex).then((cache) => {
cache.delete(`/${lastRoomUrl}`);
});
}
}
getLastRoomUrl(): string {
return (
@ -172,8 +165,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 as boolean;
private _iframeAuthentication?: string = OPID_LOGIN_SCREEN_PROVIDER;
private _mapUrl: string | undefined;
private _textures: CharacterTexture[] | undefined;
private instance: string | undefined;
@ -106,8 +106,8 @@ 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 as boolean);
this._iframeAuthentication = data.iframeAuthentication || OPID_LOGIN_SCREEN_PROVIDER;
this._contactPage = data.contactPage || CONTACT_URL;
return new MapDetail(data.mapUrl, data.textures);
}

View File

@ -23,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 = process.env.DISABLE_ANONYMOUS || false;
export const OPID_LOGIN_SCREEN_PROVIDER = process.env.OPID_LOGIN_SCREEN_PROVIDER;
export const isMobile = (): boolean => window.innerWidth <= 800 || window.innerHeight <= 600;

View File

@ -77,7 +77,7 @@ export abstract class Character extends Container {
fontFamily: '"Press Start 2P"',
fontSize: "8px",
strokeThickness: 2,
stroke: "#000",
stroke: "gray",
});
this.playerName.setOrigin(0.5).setDepth(DEPTH_INGAME_TEXT_INDEX);
this.add(this.playerName);

View File

@ -434,10 +434,7 @@ export const ACCESSORIES_RESOURCES: BodyResourceDescriptionListInterface = {
name: "accessory_mate_bottle",
img: "resources/customisation/character_accessories/mate_bottle1.png",
},
accessory_mask: {
name: "accessory_mask",
img: "resources/customisation/character_accessories/mask.png",
},
accessory_mask: { name: "accessory_mask", img: "resources/customisation/character_accessories/mask.png" },
wheelchair: {
name: "accessory_wheelchair",
img: "resources/customisation/character_accessories/wheelchair.png",

View File

@ -109,6 +109,11 @@ export class GameMapPropertiesListener {
return;
}
this.coWebsitesOpenByLayer.set(layer, {
coWebsite: undefined,
state: OpenCoWebsiteState.LOADING,
});
const openWebsiteFunction = () => {
coWebsiteManager.loadCoWebsite(
openWebsiteProperty as string,

View File

@ -89,8 +89,6 @@ import { get } from "svelte/store";
import { contactPageStore } from "../../Stores/MenuStore";
import { GameMapProperties } from "./GameMapProperties";
export interface GameSceneInitInterface {
initPosition: PointInterface | null;
reconnecting: boolean;
@ -943,7 +941,8 @@ export class GameScene extends DirtyScene {
html += `<input id="popupinput-${openPopupEvent.popupId}" class="nes-input" />`
}
html += `</div>`;
const buttonContainer = `<div class="buttonContainer"</div>`;
const buttonContainer = '<div class="buttonContainer"</div>';
html += buttonContainer;
let id = 0;
for (const button of openPopupEvent.buttons) {

View File

@ -28,9 +28,9 @@ export class LoginScene extends ResizableScene {
gameManager.currentStartedRoom.authenticationMandatory
) {
connectionManager.loadOpenIDScreen();
} else {
loginSceneVisibleStore.set(true);
loginSceneVisibleIframeStore.set(true);
}
loginSceneVisibleStore.set(true);
}
public login(name: string): void {

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

@ -35,21 +35,21 @@ export enum SubMenusInterface {
settings = "Settings",
profile = "Profile",
worlds = "Worlds",
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.worlds,
SubMenusInterface.createMap,
SubMenusInterface.aboutRoom,
SubMenusInterface.globalMessages,
SubMenusInterface.contact,
SubMenusInterface.settings,
SubMenusInterface.invite,
SubMenusInterface.aboutRoom,
]);
return {

View File

@ -517,6 +517,7 @@ class CoWebsiteManager {
};
this.coWebsites.push(coWebsite);
// this.cowebsiteSubIconsDom.appendChild(icon);
const onTimeoutPromise = new Promise<void>((resolve) => {
setTimeout(() => resolve(), 2000);
@ -564,6 +565,10 @@ class CoWebsiteManager {
this.fire();
}
if (coWebsite) {
iframeListener.unregisterIframe(coWebsite.iframe);
}
this.removeCoWebsiteFromStack(coWebsite);
resolve();
})

View File

@ -147,7 +147,7 @@ const wa = {
* @deprecated Use WA.ui.openPopup instead
*/
openPopup(targetObject: string, message: string, buttons: ButtonDescriptor[], popupClass : string, input : boolean): Popup {
console.warn('Method WA.openPopup is deprecated. Please use WA.ui.openPopup instead');
console.warn("Method WA.openPopup is deprecated. Please use WA.ui.openPopup instead");
return ui.openPopup(targetObject, message, buttons, popupClass, input);
},
/**

View File

@ -21,7 +21,7 @@
}
aside {
background: #212529;
background: gray;
align-items: center;
display: flex;
flex-direction: column;

View File

@ -1,8 +1,9 @@
@import "~nes.css/css/nes.min.css";
@import "nes.custom.css";
@import "quill.snow.css";
@import "cowebsite.scss";
@import "cowebsite-mobile.scss";
@import "style";
@import "mobile-style.scss";
@import "fonts.scss";
@import "quill.snow.css";
@import "TextGlobalMessageSvelte-Style";

View File

@ -8,7 +8,6 @@ body img:focus,
body input:focus {
outline: -webkit-focus-ring-color auto 0;
}
body .message-info{
width: 20%;
height: auto;
@ -19,15 +18,12 @@ body .message-info {
padding-top: 10px;
text-align: center;
}
body .message-info.error{
background: red;
}
body .message-info.success{
background: green;
}
body .message-info.info{
background: dodgerblue;
}
@ -167,11 +163,9 @@ video.myCamVideo{
right: 14px;
top: calc(50% - 5px);
}
.sound-progress.active{
display: table-column;
}
.sound-progress span{
position: absolute;
color: black;
@ -180,27 +174,21 @@ video.myCamVideo{
height: 5px;
border-radius: 50%;
}
.sound-progress span.active{
background-color: #00c3ff66
}
.sound-progress span:nth-child(1){
top: calc(50% + 20px);
}
.sound-progress span:nth-child(2){
top: calc(50% + 10px);
}
.sound-progress span:nth-child(3){
top: calc(50% - 0px);
}
.sound-progress span:nth-child(4){
top: calc(50% - 10px);
}
.sound-progress span:nth-child(5){
top: calc(50% - 20px);
}
@ -232,39 +220,32 @@ video.myCamVideo{
transition-timing-function: ease-in-out;
margin: 0 4%;
}
.btn-cam-action div.disabled {
background: #d75555;
}
.btn-cam-action div.enabled {
background: #73c973;
}
.btn-cam-action:hover div{
transform: translateY(0);
}
.btn-cam-action div:hover{
background: #407cf7;
box-shadow: 4px 4px 48px #666;
transition: 120ms;
}
.btn-micro{
pointer-events: none;
pointer-events: auto;
transition: all .3s;
/*right: 44px;*/
}
.btn-video{
pointer-events: none;
pointer-events: auto;
transition: all .25s;
/*right: 134px;*/
}
.btn-monitor{
pointer-events: none;
pointer-events: auto;
transition: all .2s;
/*right: 224px;*/
}
@ -290,7 +271,6 @@ video.myCamVideo{
right: 44px;
opacity: 1;
}
.btn-cam-action div img{
height: 22px;
width: 30px;
@ -298,18 +278,17 @@ video.myCamVideo{
pointer-events: all;
}
/* Spinner */
.connecting-spinner {
/*display: inline-block;*/
position: absolute;
left: calc(50% - 62px);
top: calc(50% - 62px);
width: 130px;
height: 130px;
}
.connecting-spinner:after {
content: " ";
display: block;
@ -321,7 +300,6 @@ video.myCamVideo{
border-color: #fff transparent #fff transparent;
animation: connecting-spinner 1.2s linear infinite;
}
@keyframes connecting-spinner {
0% {
transform: rotate(0deg);
@ -331,14 +309,16 @@ video.myCamVideo{
}
}
.rtc-error {
position: absolute;
left: calc(50% - 68px);
top: calc(50% - 68px);
width: 130px;
height: 130px;
}
.rtc-error:after {
content: " ";
display: block;
@ -357,13 +337,11 @@ video.myCamVideo{
}
/* New layout */
body {
margin: 0;
height: 100%;
width: 100%;
}
.main-container {
height: 100%;
width: 100%;
@ -374,16 +352,20 @@ body {
.game-overlay {
flex-direction: row;
}
.sidebar {
flex-direction: column;
}
.sidebar > div {
max-height: 21%;
height: 21%;
}
.sidebar > div:hover {
max-height: 25%;
}
}
#game {
@ -398,7 +380,6 @@ body {
* Style Input Range
* https://www.cssportal.com/style-input-range/
*/
input[type=range] {
height: 28px;
-webkit-appearance: none;
@ -406,11 +387,9 @@ input[type=range] {
width: 100%;
background-color: transparent;
}
input[type=range]:focus {
outline: none;
}
input[type=range]::-webkit-slider-runnable-track {
width: 100%;
height: 5px;
@ -420,7 +399,6 @@ input[type=range]::-webkit-slider-runnable-track {
border-radius: 5px;
border: 1px solid #000000;
}
input[type=range]::-webkit-slider-thumb {
box-shadow: 1px 1px 1px #000000;
border: 1px solid #000000;
@ -431,11 +409,9 @@ input[type=range]::-webkit-slider-thumb {
-webkit-appearance: none;
margin-top: -8.5px;
}
input[type=range]:focus::-webkit-slider-runnable-track {
background: #FFFFFF;
}
input[type=range]::-moz-range-track {
width: 100%;
height: 5px;
@ -445,7 +421,6 @@ input[type=range]::-moz-range-track {
border-radius: 5px;
border: 1px solid #000000;
}
input[type=range]::-moz-range-thumb {
box-shadow: 1px 1px 1px #000000;
border: 1px solid #000000;
@ -454,7 +429,6 @@ input[type=range]::-moz-range-thumb {
border-radius: 5px;
background: #FFFFFF;
}
input[type=range]::-ms-track {
width: 100%;
height: 5px;
@ -463,21 +437,18 @@ input[type=range]::-ms-track {
border-color: transparent;
color: transparent;
}
input[type=range]::-ms-fill-lower {
background: #FFFFFF;
border: 1px solid #000000;
border-radius: 10px;
box-shadow: 1px 1px 1px #000000;
}
input[type=range]::-ms-fill-upper {
background: #FFFFFF;
border: 1px solid #000000;
border-radius: 10px;
box-shadow: 1px 1px 1px #000000;
}
input[type=range]::-ms-thumb {
margin-top: 1px;
box-shadow: 1px 1px 1px #000000;
@ -487,15 +458,14 @@ input[type=range]::-ms-thumb {
border-radius: 5px;
background: #FFFFFF;
}
input[type=range]:focus::-ms-fill-lower {
background: #FFFFFF;
}
input[type=range]:focus::-ms-fill-upper {
background: #FFFFFF;
}
.game-overlay {
display: none;
position: absolute;
@ -561,8 +531,8 @@ input[type=range]:focus::-ms-fill-upper {
margin: 0%;
}
/* Let's make sure videos are vertically centered if they need to be cropped */
/* Let's make sure videos are vertically centered if they need to be cropped */
.media-container {
display: flex;
flex-direction: column;
@ -572,7 +542,9 @@ input[type=range]:focus::-ms-fill-upper {
.chat-mode {
display: grid;
width: 100%;
align-items: flex-start;
padding: 1%;
}
@ -585,7 +557,6 @@ input[type=range]:focus::-ms-fill-upper {
.chat-mode > div:hover {
margin: 0%;
}
.chat-mode.one-col {
grid-template-columns: repeat(1, 1fr);
}
@ -682,11 +653,9 @@ input[type=range]:focus::-ms-fill-upper {
/* VIDEO QUALITY */
.main-console div.setting h1{
color: white;
}
.main-console div.setting select{
background: black;
color: white;
@ -696,19 +665,16 @@ input[type=range]:focus::-ms-fill-upper {
padding: 10px;
border-radius: 15px;
}
.main-console div.setting select:focus{
border: solid 1px white;
outline: none;
}
.main-console div.setting.active section{
display: block;
}
/*REPORT input*/
div.modal-report-user{
position: absolute;
width: 800px;
@ -787,28 +753,23 @@ div.modal-report-user {
width: 100%;
text-align: left;
padding: 30px;
max-width: calc(800px - 60px);
/* size of modal - padding*/
max-width: calc(800px - 60px); /* size of modal - padding*/
}
/*MESSAGE*/
.discussion{
position: fixed;
left: -300px;
top: 0px;
width: 280px;
width: 220px;
height: 100%;
background-color: #333333;
padding: 20px;
transition: all 0.5s ease;
}
.discussion.active{
left: 0;
}
.discussion .active-btn{
display: none;
height: 50px;
@ -821,15 +782,12 @@ div.modal-report-user {
border: none;
transition: all 0.5s ease;
}
.discussion .active-btn.active{
display: block;
}
.discussion .active-btn:hover {
transform: scale(1.1) rotateY(3.142rad);
}
.discussion .active-btn img{
width: 26px;
height: 26px;
@ -843,7 +801,6 @@ div.modal-report-user {
background: none;
border: none;
}
.discussion .close-btn img{
height: 15px;
right: 15px;
@ -872,7 +829,6 @@ div.modal-report-user {
.discussion .participants .participant:hover{
background-color: #ffffff;
}
.discussion .participants .participant:hover p{
color: black;
}
@ -957,7 +913,6 @@ div.modal-report-user {
margin-top: 5px;
margin-bottom: 12px;
}
.discussion .messages .message p a{
color: white;
}
@ -992,14 +947,12 @@ div.modal-report-user {
height: 20px;
background-color: #ffffff69;
}
.discussion .send-message img:hover{
background-color: #ffffff;
}
/** Action button **/
div.action{
position: absolute;
height: auto;
@ -1011,7 +964,6 @@ div.action {
animation-iteration-count: infinite;
animation-timing-function: ease-in-out;
}
div.action.info,
div.action.warning,
div.action.danger{
@ -1028,17 +980,14 @@ div.action p.action-body{
width: 350px;
max-width: 350px;
}
div.action.warning p.action-body{
background-color: #ff9800eb;
color: #000;
}
div.action.danger p.action-body{
background-color: #da0000e3;
color: #000;
}
.popUpElement{
font-family: 'Press Start 2P';
text-align: left;
@ -1064,8 +1013,10 @@ div.action.danger p.action-body {
.popUpElement .buttonContainer {
float: right;
background-color: inherit;
}
@keyframes mymove {
0% {bottom: 40px;}
50% {bottom: 30px;}
@ -1115,9 +1066,6 @@ div.is-silent.hide {
right: 15px;
}
.emote-menu .emoji-picker .emoji-picker__emoji {
font-family: "Twemoji Mozilla" !important;
}
.popUpElement.overlay-text {

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;
@ -208,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();
});
});

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,11 +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 { DISABLE_ANONYMOUS } from "../Enum/EnvironmentVariable";
export interface TokenInterface {
userUuid: string;
@ -56,19 +56,20 @@ export class AuthenticateController extends BaseController {
res.onAborted(() => {
console.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) {
if (authTokenData.accessToken == undefined) {
throw Error("Token cannot to be check on Hydra");
}
await openIDClient.checkTokenAuth(authTokenData.hydraAccessToken);
const resCheckTokenAuth = await openIDClient.checkTokenAuth(authTokenData.accessToken);
res.writeStatus("200");
this.addCorsHeaders(res);
return res.end(JSON.stringify({ authToken: token, username: authTokenData.username, userUuid : authTokenData.identifier }));
return res.end(JSON.stringify({ ...resCheckTokenAuth, username: authTokenData.username, authToken: token }));
} catch (err) {
console.info("User was not connected", err);
}
@ -81,9 +82,14 @@ export class AuthenticateController extends BaseController {
throw new Error("No sub in the response");
}
const authToken = jwtTokenManager.createAuthToken(sub, userInfo.access_token, userInfo.username);
//Get user data from Admin Back Office
//This is very important to create User Local in LocalStorage in WorkAdventure
const data = await this.getUserByUserIdentifier(sub, playUri as string, IPAddress);
res.writeStatus("200");
this.addCorsHeaders(res);
return res.end(JSON.stringify({ authToken: authToken, username: userInfo.username, userUuid : sub }));
return res.end(JSON.stringify({ ...data, authToken, username: userInfo.username, userUuid : sub }));
} catch (e) {
console.error("openIDCallback => ERROR", e);
return this.errorToResponse(e, res);
@ -100,10 +106,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) {
console.error("openIDCallback => logout-callback", error);
} finally {
@ -202,20 +208,20 @@ export class AuthenticateController extends BaseController {
res.onAborted(() => {
console.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();
@ -229,4 +235,26 @@ 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 | object = {};
try {
data = await adminApi.fetchMemberDataByUuid(email, playUri, IPAddress);
} catch (err) {
console.error("openIDCallback => fetchMemberDataByUuid", err);
}
return data;
}
}

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, DISABLE_ANONYMOUS } 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";
export class IoSocketController {
@ -48,15 +47,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) {
console.log("Admin access refused for token: " + token);
let authorizedRoomIds: string[];
try {
const data = jwtTokenManager.verifyAdminSocketToken(token as string);
authorizedRoomIds = data.authorizedRoomIds;
} catch (e) {
console.error("Admin access refused for token: " + token);
res.writeStatus("401 Unauthorized").end("Incorrect token");
return;
}
const roomId = query.roomId;
if (typeof roomId !== "string") {
console.error("Received");
res.writeStatus("400 Bad Request").end("Missing room id");
if (typeof roomId !== "string" || !authorizedRoomIds.includes(roomId)) {
console.error("Invalid room id");
res.writeStatus("403 Bad Request").end("Invalid room id");
return;
}
@ -70,8 +73,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)));
@ -250,7 +251,7 @@ export class IoSocketController {
roomId
);
console.error(e);
throw new Error("User cannot access this room");
throw new Error("User cannot access this world");
}
}

View File

@ -90,7 +90,7 @@ export class MapController extends BaseController {
const mapDetails = await adminApi.fetchMapDetails(query.playUri as string, userId);
if (isMapDetailsData(mapDetails) && DISABLE_ANONYMOUS) {
(mapDetails as MapDetailsData).authenticationMandatory = true;
mapDetails.authenticationMandatory = true;
}
res.writeStatus("200 OK");

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 || "";
@ -12,11 +13,13 @@ const PUSHER_HTTP_PORT = parseInt(process.env.PUSHER_HTTP_PORT || "8080") || 808
export const SOCKET_IDLE_TIMER = parseInt(process.env.SOCKET_IDLE_TIMER as string) || 120; // maximum time (in second) without activity before a socket is closed. Should be greater than 60 seconds in order to cope for Chrome intensive throttling (https://developer.chrome.com/blog/timer-throttling-in-chrome-88/#intensive-throttling)
export const FRONT_URL = process.env.FRONT_URL || "http://localhost";
export const DISABLE_ANONYMOUS = process.env.DISABLE_ANONYMOUS ? process.env.DISABLE_ANONYMOUS == "true" : false;
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 = process.env.DISABLE_ANONYMOUS || false;
export const PUSHER_FORCE_ROOM_UPDATE = process.env.PUSHER_FORCE_ROOM_UPDATE ? process.env.PUSHER_FORCE_ROOM_UPDATE == "true" : false;
export const OIDC_CLIENT_ID = process.env.OIDC_CLIENT_ID || "";
export const OIDC_CLIENT_SECRET = process.env.OIDC_CLIENT_SECRET || "";
export const OIDC_CLIENT_ISSUER = process.env.OIDC_CLIENT_ISSUER || "";
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";
@ -142,13 +142,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

@ -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,14 +6,21 @@ import { adminApi, AdminBannedData } from "../Services/AdminApi";
export interface AuthTokenData {
identifier: string; //will be a sub (id) if logged in or an uuid if anonymous
hydraAccessToken?: string;
accessToken?: string;
username?: 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, username?: string) {
return Jwt.sign({ identifier, hydraAccessToken, username }, SECRET_KEY, { expiresIn: "30d" });
public verifyAdminSocketToken(token: string): AdminSocketTokenData {
return Jwt.verify(token, ADMIN_SOCKETS_TOKEN) as AdminSocketTokenData;
}
public createAuthToken(identifier: string, accessToken?: string, username?: string) {
return Jwt.sign({ identifier, accessToken, username }, SECRET_KEY, { expiresIn: "30d" });
}
public verifyJWTToken(token: string, ignoreExpiration: boolean = false): AuthTokenData {

View File

@ -1,18 +1,21 @@
import { Issuer, Client, IntrospectionResponse } from "openid-client";
import { OIDC_CLIENT_ID, OIDC_CLIENT_SECRET, OIDC_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;
private initClient(): Promise<Client> {
if (!this.issuerPromise) {
this.issuerPromise = Issuer.discover(OIDC_CLIENT_ISSUER).then((issuer) => {
this.issuerPromise = Issuer.discover(OPID_CLIENT_ISSUER).then((issuer) => {
return new issuer.Client({
client_id: OIDC_CLIENT_ID,
client_secret: OIDC_CLIENT_SECRET,
redirect_uris: [opidRedirectUri],
client_id: OPID_CLIENT_ID,
client_secret: OPID_CLIENT_SECRET,
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; username: 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,