From 02c193a262422d2ac90442f00f390d13be5b4788 Mon Sep 17 00:00:00 2001 From: arp Date: Mon, 12 Oct 2020 16:23:07 +0200 Subject: [PATCH 1/8] rewrote the authorisation flow: give more responsability to gameManager and less to gameScene --- back/src/Controller/AuthenticateController.ts | 76 ++++++++------- back/src/Controller/IoSocketController.ts | 24 +++-- back/src/Model/RoomIdentifier.ts | 14 +++ back/src/Services/AdminApi.ts | 8 +- front/src/Connexion/ConnectionManager.ts | 94 ++++++++++--------- front/src/Connexion/LocalUser.ts | 9 ++ front/src/Connexion/LocalUserStore.ts | 16 ++++ front/src/Connexion/Room.ts | 10 ++ front/src/Connexion/RoomConnection.ts | 3 +- front/src/Phaser/Game/GameManager.ts | 77 ++++----------- front/src/Phaser/Game/GameScene.ts | 23 +---- front/src/Phaser/Login/EnableCameraScene.ts | 3 +- front/src/Url/UrlManager.ts | 52 ++++++++++ front/src/index.ts | 5 +- 14 files changed, 244 insertions(+), 170 deletions(-) create mode 100644 back/src/Model/RoomIdentifier.ts create mode 100644 front/src/Connexion/LocalUser.ts create mode 100644 front/src/Connexion/LocalUserStore.ts create mode 100644 front/src/Connexion/Room.ts create mode 100644 front/src/Url/UrlManager.ts diff --git a/back/src/Controller/AuthenticateController.ts b/back/src/Controller/AuthenticateController.ts index 984d7445..55036a0e 100644 --- a/back/src/Controller/AuthenticateController.ts +++ b/back/src/Controller/AuthenticateController.ts @@ -1,8 +1,7 @@ -import {URL_ROOM_STARTED} from "../Enum/EnvironmentVariable"; //TODO fix import by "_Enum/..." import { v4 } from 'uuid'; import {HttpRequest, HttpResponse, TemplatedApp} from "uWebSockets.js"; import {BaseController} from "./BaseController"; -import {adminApi, AdminApiData} from "../Services/AdminApi"; +import {adminApi} from "../Services/AdminApi"; import {jwtTokenManager} from "../Services/JWTTokenManager"; export interface TokenInterface { @@ -13,18 +12,19 @@ export class AuthenticateController extends BaseController { constructor(private App : TemplatedApp) { super(); - this.login(); + this.register(); + this.anonymLogin(); } - //permit to login on application. Return token to connect on Websocket IO. - login(){ - this.App.options("/login", (res: HttpResponse, req: HttpRequest) => { + //Try to login with an admin token + register(){ + this.App.options("/register", (res: HttpResponse, req: HttpRequest) => { this.addCorsHeaders(res); res.end(); }); - this.App.post("/login", (res: HttpResponse, req: HttpRequest) => { + this.App.post("/register", (res: HttpResponse, req: HttpRequest) => { (async () => { this.addCorsHeaders(res); @@ -36,35 +36,25 @@ export class AuthenticateController extends BaseController { //todo: what to do if the organizationMemberToken is already used? const organizationMemberToken:string|null = param.organizationMemberToken; - const mapSlug:string|null = param.mapSlug; - + try { - let userUuid; - let mapUrlStart; - let newUrl: string|null = null; + if (typeof organizationMemberToken != 'string') throw new Error('No organization token'); + const data = await adminApi.fetchMemberDataByToken(organizationMemberToken); - if (organizationMemberToken) { - const data = await adminApi.fetchMemberDataByToken(organizationMemberToken); - - userUuid = data.userUuid; - mapUrlStart = data.mapUrlStart; - newUrl = this.getNewUrlOnAdminAuth(data) - } else if (mapSlug !== null) { - userUuid = v4(); - mapUrlStart = mapSlug; - newUrl = null; - } else { - userUuid = v4(); - mapUrlStart = host.replace('api.', 'maps.') + URL_ROOM_STARTED; - newUrl = '_/global/'+mapUrlStart; - } + const userUuid = data.userUuid; + const organizationSlug = data.organizationSlug; + const worldSlug = data.worldSlug; + const roomSlug = data.roomSlug; + const mapUrlStart = data.mapUrlStart; const authToken = jwtTokenManager.createJWTToken(userUuid); res.writeStatus("200 OK").end(JSON.stringify({ authToken, userUuid, + organizationSlug, + worldSlug, + roomSlug, mapUrlStart, - newUrl, })); } catch (e) { @@ -75,12 +65,32 @@ export class AuthenticateController extends BaseController { })(); }); + } - private getNewUrlOnAdminAuth(data:AdminApiData): string { - const organizationSlug = data.organizationSlug; - const worldSlug = data.worldSlug; - const roomSlug = data.roomSlug; - return '/@/'+organizationSlug+'/'+worldSlug+'/'+roomSlug; + //permit to login on application. Return token to connect on Websocket IO. + anonymLogin(){ + this.App.options("/anonymLogin", (res: HttpResponse, req: HttpRequest) => { + this.addCorsHeaders(res); + + res.end(); + }); + + this.App.post("/anonymLogin", (res: HttpResponse, req: HttpRequest) => { + (async () => { + this.addCorsHeaders(res); + + res.onAborted(() => { + console.warn('Login request was aborted'); + }) + + const userUuid = v4(); + const authToken = jwtTokenManager.createJWTToken(userUuid); + res.writeStatus("200 OK").end(JSON.stringify({ + authToken, + userUuid, + })); + })(); + }); } } diff --git a/back/src/Controller/IoSocketController.ts b/back/src/Controller/IoSocketController.ts index 7ef0d811..306b874e 100644 --- a/back/src/Controller/IoSocketController.ts +++ b/back/src/Controller/IoSocketController.ts @@ -42,6 +42,7 @@ import {cpuTracker} from "../Services/CpuTracker"; import {ViewportInterface} from "../Model/Websocket/ViewportMessage"; import {jwtTokenManager} from "../Services/JWTTokenManager"; import {adminApi} from "../Services/AdminApi"; +import {RoomIdentifier} from "../Model/RoomIdentifier"; function emitInBatch(socket: ExSocketInterface, payload: SubMessage): void { socket.batchedMessages.addPayload(payload); @@ -88,7 +89,7 @@ export class IoSocketController { ioConnection() { - this.app.ws('/room/*', { + this.app.ws('/room', { /* Options */ //compression: uWS.SHARED_COMPRESSOR, maxPayloadLength: 16 * 1024 * 1024, @@ -112,7 +113,12 @@ export class IoSocketController { const websocketProtocol = req.getHeader('sec-websocket-protocol'); const websocketExtensions = req.getHeader('sec-websocket-extensions'); - const roomId = req.getUrl().substr(6); + const roomId = query.roomId; + //todo: better validation: /\/_\/.*\/.*/ or /\/@\/.*\/.*\/.*/ + if (typeof roomId !== 'string') { + throw new Error('Undefined room ID: '); + } + const roomIdentifier = new RoomIdentifier(roomId); const token = query.token; const x = Number(query.x); @@ -140,12 +146,14 @@ export class IoSocketController { const userUuid = await jwtTokenManager.getUserUuidFromToken(token); console.log('uuid', userUuid); - const isGranted = await adminApi.memberIsGrantedAccessToRoom(userUuid, roomId); - if (!isGranted) { - console.log('access not granted for user '+userUuid+' and room '+roomId); - throw new Error('Client cannot acces this ressource.') - } else { - console.log('access granted for user '+userUuid+' and room '+roomId); + if (roomIdentifier.anonymous === false) { + const isGranted = await adminApi.memberIsGrantedAccessToRoom(userUuid, roomIdentifier); + if (!isGranted) { + console.log('access not granted for user '+userUuid+' and room '+roomId); + throw new Error('Client cannot acces this ressource.') + } else { + console.log('access granted for user '+userUuid+' and room '+roomId); + } } if (upgradeAborted.aborted) { diff --git a/back/src/Model/RoomIdentifier.ts b/back/src/Model/RoomIdentifier.ts new file mode 100644 index 00000000..9bb58bb9 --- /dev/null +++ b/back/src/Model/RoomIdentifier.ts @@ -0,0 +1,14 @@ +export class RoomIdentifier { + public anonymous: boolean; + public id:string + constructor(roomID: string) { + if (roomID.indexOf('_/') === 0) { + this.anonymous = true; + } else if(roomID.indexOf('@/') === 0) { + this.anonymous = false; + } else { + throw new Error('Incorrect room ID: '+roomID); + } + this.id = roomID; //todo: extract more data from the id (like room slug, organization name, etc); + } +} \ No newline at end of file diff --git a/back/src/Services/AdminApi.ts b/back/src/Services/AdminApi.ts index 79a68810..2d03ee9d 100644 --- a/back/src/Services/AdminApi.ts +++ b/back/src/Services/AdminApi.ts @@ -1,5 +1,6 @@ import {ADMIN_API_TOKEN, ADMIN_API_URL} from "../Enum/EnvironmentVariable"; -import Axios, {AxiosError} from "axios"; +import Axios from "axios"; +import {RoomIdentifier} from "../Model/RoomIdentifier"; export interface AdminApiData { organizationSlug: string @@ -22,13 +23,14 @@ class AdminApi { return res.data; } - async memberIsGrantedAccessToRoom(memberId: string, roomId: string): Promise { + async memberIsGrantedAccessToRoom(memberId: string, roomIdentifier: RoomIdentifier): Promise { if (!ADMIN_API_URL) { return Promise.reject('No admin backoffice set!'); } try { + //todo: send more specialized data instead of the whole id const res = await Axios.get(ADMIN_API_URL+'/api/member/is-granted-access', - { headers: {"Authorization" : `${ADMIN_API_TOKEN}`}, params: {memberId, roomIdentifier: roomId} } + { headers: {"Authorization" : `${ADMIN_API_TOKEN}`}, params: {memberId, roomIdentifier: roomIdentifier.id} } ) return !!res.data; } catch (e) { diff --git a/front/src/Connexion/ConnectionManager.ts b/front/src/Connexion/ConnectionManager.ts index 91a42882..0fea50b5 100644 --- a/front/src/Connexion/ConnectionManager.ts +++ b/front/src/Connexion/ConnectionManager.ts @@ -2,54 +2,67 @@ import Axios from "axios"; import {API_URL} from "../Enum/EnvironmentVariable"; import {RoomConnection} from "./RoomConnection"; import {PositionInterface, ViewportInterface} from "./ConnexionModels"; - -interface LoginApiData { - authToken: string - userUuid: string - mapUrlStart: string - newUrl: string -} +import {GameConnexionTypes, urlManager} from "../Url/UrlManager"; +import {localUserStore} from "./LocalUserStore"; +import {LocalUser} from "./LocalUser"; +import {Room} from "./Room"; class ConnectionManager { - private initPromise!: Promise; - private mapUrlStart: string|null = null; + private localUser!:LocalUser; - private authToken:string|null = null; - private userUuid: string|null = null; + /** + * Tries to login to the node server and return the starting map url to be loaded + */ + public async initGameConnexion(): Promise { - //todo: get map infos from url in anonym case - public async init(): Promise { - let organizationMemberToken = null; - let teamSlug = null; - let mapSlug = null; - const match = /\/register\/(.+)/.exec(window.location.toString()); - if (match) { - organizationMemberToken = match[1]; - } else { - const match = /\/_\/(.+)\/(.+)/.exec(window.location.toString()); - teamSlug = match ? match[1] : null; - mapSlug = match ? match[2] : null; - } - this.initPromise = Axios.post(`${API_URL}/login`, {organizationMemberToken, teamSlug, mapSlug}).then(res => res.data); - const data = await this.initPromise - this.authToken = data.authToken; - this.userUuid = data.userUuid; - this.mapUrlStart = data.mapUrlStart; - const newUrl = data.newUrl; - console.log('u', this.userUuid) - - if (newUrl) { - history.pushState({}, '', newUrl); + const connexionType = urlManager.getGameConnexionType(); + if(connexionType === GameConnexionTypes.register) { + const organizationMemberToken = urlManager.getOrganizationToken(); + const data:any = await Axios.post(`${API_URL}/register`, {organizationMemberToken}).then(res => res.data); + this.localUser = new LocalUser(data.userUuid, data.authToken); + localUserStore.saveUser(this.localUser); + + const organizationSlug = data.organizationSlug; + const worldSlug = data.worldSlug; + const roomSlug = data.roomSlug; + urlManager.editUrlForRoom(roomSlug, organizationSlug, worldSlug); + + const room = new Room(window.location.pathname, data.mapUrlStart) + return Promise.resolve(room); + } else if (connexionType === GameConnexionTypes.anonymous) { + const localUser = localUserStore.getLocalUser(); + + if (localUser) { + this.localUser = localUser + } else { + const data:any = await Axios.post(`${API_URL}/anonymLogin`).then(res => res.data); + this.localUser = new LocalUser(data.userUuid, data.authToken); + localUserStore.saveUser(this.localUser); + } + const room = new Room(window.location.pathname, urlManager.getAnonymousMapUrlStart()) + return Promise.resolve(room); + } else if (connexionType == GameConnexionTypes.organization) { + const localUser = localUserStore.getLocalUser(); + + if (localUser) { + this.localUser = localUser + //todo: ask the node api for the correct starting map Url from its slug + return Promise.reject('Case not handled: need to get the map\'s url from its slug'); + } else { + //todo: find some kind of fallback? + return Promise.reject('Could not find a user in localstorage'); + } } + return Promise.reject('ConnexionManager initialization failed'); } public initBenchmark(): void { - this.authToken = 'test'; + this.localUser = new LocalUser('', 'test'); } public connectToRoomSocket(roomId: string, name: string, characterLayers: string[], position: PositionInterface, viewport: ViewportInterface): Promise { return new Promise((resolve, reject) => { - const connection = new RoomConnection(this.authToken, roomId, name, characterLayers, position, viewport); + const connection = new RoomConnection(this.localUser.jwtToken, roomId, name, characterLayers, position, viewport); connection.onConnectError((error: object) => { console.log('An error occurred while connecting to socket server. Retrying'); reject(error); @@ -67,15 +80,6 @@ class ConnectionManager { }); }); } - - public getMapUrlStart(): Promise { - return this.initPromise.then(() => { - if (!this.mapUrlStart) { - throw new Error('No map url set!'); - } - return this.mapUrlStart; - }) - } } export const connectionManager = new ConnectionManager(); diff --git a/front/src/Connexion/LocalUser.ts b/front/src/Connexion/LocalUser.ts new file mode 100644 index 00000000..1411f66c --- /dev/null +++ b/front/src/Connexion/LocalUser.ts @@ -0,0 +1,9 @@ +export class LocalUser { + public uuid: string; + public jwtToken: string; + + constructor(uuid:string, jwtToken: string) { + this.uuid = uuid; + this.jwtToken = jwtToken; + } +} \ No newline at end of file diff --git a/front/src/Connexion/LocalUserStore.ts b/front/src/Connexion/LocalUserStore.ts new file mode 100644 index 00000000..0976b5c9 --- /dev/null +++ b/front/src/Connexion/LocalUserStore.ts @@ -0,0 +1,16 @@ +import {LocalUser} from "./LocalUser"; + +class LocalUserStore { + + saveUser(localUser: LocalUser) { + localStorage.setItem('localUser', JSON.stringify(localUser)); + } + + getLocalUser(): LocalUser|null { + const data = localStorage.getItem('localUser'); + return data ? JSON.parse(data) : null; + } + +} + +export const localUserStore = new LocalUserStore(); \ No newline at end of file diff --git a/front/src/Connexion/Room.ts b/front/src/Connexion/Room.ts new file mode 100644 index 00000000..36a8072b --- /dev/null +++ b/front/src/Connexion/Room.ts @@ -0,0 +1,10 @@ +export class Room { + public ID: string; + public url: string + + constructor(ID: string, url: string) { + this.ID = ID; + this.url = url; + } + +} \ No newline at end of file diff --git a/front/src/Connexion/RoomConnection.ts b/front/src/Connexion/RoomConnection.ts index ed669fed..fd9410c4 100644 --- a/front/src/Connexion/RoomConnection.ts +++ b/front/src/Connexion/RoomConnection.ts @@ -56,7 +56,8 @@ export class RoomConnection implements RoomConnection { */ public constructor(token: string|null, roomId: string, name: string, characterLayers: string[], position: PositionInterface, viewport: ViewportInterface) { let url = API_URL.replace('http://', 'ws://').replace('https://', 'wss://'); - url += '/room/'+roomId + url += '/room'; + url += '?roomId='+(roomId ?encodeURIComponent(roomId):''); url += '?token='+(token ?encodeURIComponent(token):''); url += '&name='+encodeURIComponent(name); for (const layer of characterLayers) { diff --git a/front/src/Phaser/Game/GameManager.ts b/front/src/Phaser/Game/GameManager.ts index 5188d2fe..10277e20 100644 --- a/front/src/Phaser/Game/GameManager.ts +++ b/front/src/Phaser/Game/GameManager.ts @@ -1,10 +1,6 @@ -import {GameScene, GameSceneInitInterface} from "./GameScene"; -import { - StartMapInterface -} from "../../Connexion/ConnexionModels"; -import Axios from "axios"; -import {API_URL} from "../../Enum/EnvironmentVariable"; +import {GameScene} from "./GameScene"; import {connectionManager} from "../../Connexion/ConnectionManager"; +import {Room} from "../../Connexion/Room"; export interface HasMovedEvent { direction: string; @@ -13,14 +9,17 @@ export interface HasMovedEvent { y: number; } -export interface loadMapResponseInterface { - key: string, - startLayerName: string; -} - export class GameManager { private playerName!: string; private characterLayers!: string[]; + private startRoom!:Room; + private sceneManager!: Phaser.Scenes.SceneManager; + + public async init(sceneManager: Phaser.Scenes.SceneManager) { + this.sceneManager = sceneManager; + this.startRoom = await connectionManager.initGameConnexion(); + this.loadMap(this.startRoom.url, this.startRoom.ID); + } public setPlayerName(name: string): void { this.playerName = name; @@ -41,55 +40,15 @@ export class GameManager { getCharacterSelected(): string[] { return this.characterLayers; } - - /** - * Returns the map URL and the instance from the current URL - */ - private findMapUrl(): [string, string]|null { - const path = window.location.pathname; - if (!path.startsWith('/_/')) { - return null; - } - const instanceAndMap = path.substr(3); - const firstSlash = instanceAndMap.indexOf('/'); - if (firstSlash === -1) { - return null; - } - const instance = instanceAndMap.substr(0, firstSlash); - return [window.location.protocol+'//'+instanceAndMap.substr(firstSlash+1), instance]; - } - public loadStartingMap(scene: Phaser.Scenes.ScenePlugin): Promise { - // Do we have a start URL in the address bar? If so, let's redirect to this address - const instanceAndMapUrl = this.findMapUrl(); - if (instanceAndMapUrl !== null) { - const [mapUrl, instance] = instanceAndMapUrl; - const key = gameManager.loadMap(mapUrl, scene, instance); - const startLayerName = window.location.hash ? window.location.hash.substr(1) : ''; - return Promise.resolve({key, startLayerName}); - - } else { - // If we do not have a map address in the URL, let's ask the server for a start map. - return connectionManager.getMapUrlStart().then((mapUrlStart: string) => { - const key = gameManager.loadMap(window.location.protocol + "//" + mapUrlStart, scene, 'global'); - return {key, startLayerName: ''} - }).catch((err) => { - console.error(err); - throw err; - }); - } - - } - - public loadMap(mapUrl: string, scene: Phaser.Scenes.ScenePlugin, instance: string): string { - const sceneKey = this.getMapKeyByUrl(mapUrl); - - const gameIndex = scene.getIndex(sceneKey); + + public loadMap(mapUrl: string, roomID: string): void { + console.log('Loading map '+roomID+' at url '+mapUrl); + const gameIndex = this.sceneManager.getIndex(roomID); if(gameIndex === -1){ - const game : Phaser.Scene = GameScene.createFromUrl(mapUrl, instance); - scene.add(sceneKey, game, false); + const game : Phaser.Scene = GameScene.createFromUrl(mapUrl, roomID); + this.sceneManager.add(roomID, game, false); } - return sceneKey; } public getMapKeyByUrl(mapUrlStart: string) : string { @@ -98,6 +57,10 @@ export class GameManager { const endPos = mapUrlStart.indexOf(".json"); return mapUrlStart.substring(startPos, endPos); } + + public async goToStartingMap() { + this.sceneManager.start(this.startRoom.ID, {startLayerName: 'global'}); + } } export const gameManager = new GameManager(); diff --git a/front/src/Phaser/Game/GameScene.ts b/front/src/Phaser/Game/GameScene.ts index ba1d75fa..602f697e 100644 --- a/front/src/Phaser/Game/GameScene.ts +++ b/front/src/Phaser/Game/GameScene.ts @@ -418,15 +418,7 @@ export class GameScene extends ResizableScene implements CenterListener { context.strokeStyle = '#ffffff'; context.stroke(); this.circleTexture.refresh(); - - // Let's alter browser history - const url = new URL(this.MapUrlFile); - let path = '/_/'+this.instance+'/'+url.host+url.pathname; - if (this.startLayerName) { - path += '#'+this.startLayerName; - } - window.history.pushState({}, 'WorkAdventure', path); - + // Let's pause the scene if the connection is not established yet if (this.connection === undefined) { // Let's wait 0.5 seconds before printing the "connecting" screen to avoid blinking @@ -686,6 +678,7 @@ export class GameScene extends ResizableScene implements CenterListener { * @param tileWidth * @param tileHeight */ + //todo: push that into the gameManager private loadNextGame(layer: ITiledMapLayer, mapWidth: number, tileWidth: number, tileHeight: number){ const exitSceneUrl = this.getExitSceneUrl(layer); if (exitSceneUrl === undefined) { @@ -698,7 +691,8 @@ export class GameScene extends ResizableScene implements CenterListener { // TODO: eventually compute a relative URL const absoluteExitSceneUrl = new URL(exitSceneUrl, this.MapUrlFile).href; - const exitSceneKey = gameManager.loadMap(absoluteExitSceneUrl, this.scene, instance); + gameManager.loadMap(absoluteExitSceneUrl, instance); + const exitSceneKey = instance; const tiles : number[] = layer.data as number[]; for (let key=0; key < tiles.length; key++) { @@ -785,14 +779,6 @@ export class GameScene extends ResizableScene implements CenterListener { }); } - createCollisionObject(){ - /*this.Objects.forEach((Object : Phaser.Physics.Arcade.Sprite) => { - this.physics.add.collider(this.CurrentPlayer, Object, (object1, object2) => { - this.CurrentPlayer.say("Collision with object : " + (object2 as Phaser.Physics.Arcade.Sprite).texture.key) - }); - })*/ - } - createCurrentPlayer(){ //initialise player //TODO create animation moving between exit and start @@ -809,7 +795,6 @@ export class GameScene extends ResizableScene implements CenterListener { //create collision this.createCollisionWithPlayer(); - this.createCollisionObject(); } pushPlayerPosition(event: HasMovedEvent) { diff --git a/front/src/Phaser/Login/EnableCameraScene.ts b/front/src/Phaser/Login/EnableCameraScene.ts index 5d5339d9..8695464b 100644 --- a/front/src/Phaser/Login/EnableCameraScene.ts +++ b/front/src/Phaser/Login/EnableCameraScene.ts @@ -266,8 +266,7 @@ export class EnableCameraScene extends Phaser.Scene { mediaManager.stopCamera(); mediaManager.stopMicrophone(); - const {key, startLayerName} = await gameManager.loadStartingMap(this.scene); - this.scene.start(key, {startLayerName}); + gameManager.goToStartingMap(); } private async getDevices() { diff --git a/front/src/Url/UrlManager.ts b/front/src/Url/UrlManager.ts new file mode 100644 index 00000000..39f5667d --- /dev/null +++ b/front/src/Url/UrlManager.ts @@ -0,0 +1,52 @@ + +export enum GameConnexionTypes { + anonymous=1, + organization, + register, + unknown, +} + +//this class is responsible with analysing and editing the game's url +class UrlManager { + + //todo: use that to detect if we can find a token in localstorage + public getGameConnexionType(): GameConnexionTypes { + const url = window.location.pathname.toString(); + if (url.indexOf('_/') > -1) { + return GameConnexionTypes.anonymous; + } else if (url.indexOf('@/') > -1) { + return GameConnexionTypes.organization; + } else if(url.indexOf('register/')) { + return GameConnexionTypes.register + } else { + return GameConnexionTypes.unknown + } + } + + public getAnonymousMapUrlStart():string { + const match = /\/_\/global\/(.+)/.exec(window.location.pathname.toString()) + if (!match) throw new Error('Could not extract startmap url from'+window.location.pathname); + return match[1]; + + } + + public getOrganizationToken(): string|null { + const match = /\/register\/(.+)/.exec(window.location.pathname.toString()); + return match ? match [1] : null; + } + + + public editUrlForRoom(roomSlug: string, organizationSlug: string|null, worldSlug: string |null): string { + let newUrl:string; + if (organizationSlug) { + newUrl = '/@/'+organizationSlug+'/'+worldSlug+'/'+roomSlug; + } else { + newUrl = '/_/global/'+roomSlug; + } + history.pushState({}, 'WorkAdventure', newUrl); + return newUrl; + } + +} + +export const urlManager = new UrlManager(); \ No newline at end of file diff --git a/front/src/index.ts b/front/src/index.ts index 177c56c0..f57474d7 100644 --- a/front/src/index.ts +++ b/front/src/index.ts @@ -11,11 +11,10 @@ import WebGLRenderer = Phaser.Renderer.WebGL.WebGLRenderer; import {OutlinePipeline} from "./Phaser/Shaders/OutlinePipeline"; import {CustomizeScene} from "./Phaser/Login/CustomizeScene"; import {CoWebsiteManager} from "./WebRtc/CoWebsiteManager"; -import {connectionManager} from "./Connexion/ConnectionManager"; +import {gameManager} from "./Phaser/Game/GameManager"; import {ResizableScene} from "./Phaser/Login/ResizableScene"; //CoWebsiteManager.loadCoWebsite('https://thecodingmachine.com'); -connectionManager.init(); // Load Jitsi if the environment variable is set. if (JITSI_URL) { @@ -52,6 +51,8 @@ cypressAsserter.gameStarted(); const game = new Phaser.Game(config); +gameManager.init(game.scene); + window.addEventListener('resize', function (event) { const {width, height} = CoWebsiteManager.getGameSize(); From 2852f204f506caa38b438caeac7c92338c584840 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20N=C3=A9grier?= Date: Mon, 12 Oct 2020 17:42:37 +0200 Subject: [PATCH 2/8] Improving error handling upon unknown URL --- front/src/Connexion/ConnectionManager.ts | 2 +- front/src/Phaser/Game/GameManager.ts | 17 ++++++++++---- .../src/Phaser/Reconnecting/FourOFourScene.ts | 23 +++++++++++++++---- front/src/Url/UrlManager.ts | 2 +- 4 files changed, 32 insertions(+), 12 deletions(-) diff --git a/front/src/Connexion/ConnectionManager.ts b/front/src/Connexion/ConnectionManager.ts index 0fea50b5..615f75b9 100644 --- a/front/src/Connexion/ConnectionManager.ts +++ b/front/src/Connexion/ConnectionManager.ts @@ -53,7 +53,7 @@ class ConnectionManager { return Promise.reject('Could not find a user in localstorage'); } } - return Promise.reject('ConnexionManager initialization failed'); + return Promise.reject('ConnexionManager initialization failed: invalid URL'); } public initBenchmark(): void { diff --git a/front/src/Phaser/Game/GameManager.ts b/front/src/Phaser/Game/GameManager.ts index 10277e20..bed098ae 100644 --- a/front/src/Phaser/Game/GameManager.ts +++ b/front/src/Phaser/Game/GameManager.ts @@ -1,6 +1,7 @@ import {GameScene} from "./GameScene"; import {connectionManager} from "../../Connexion/ConnectionManager"; import {Room} from "../../Connexion/Room"; +import {FourOFourSceneName} from "../Reconnecting/FourOFourScene"; export interface HasMovedEvent { direction: string; @@ -14,10 +15,16 @@ export class GameManager { private characterLayers!: string[]; private startRoom!:Room; private sceneManager!: Phaser.Scenes.SceneManager; - + public async init(sceneManager: Phaser.Scenes.SceneManager) { this.sceneManager = sceneManager; - this.startRoom = await connectionManager.initGameConnexion(); + try { + this.startRoom = await connectionManager.initGameConnexion(); + } catch (e) { + this.sceneManager.start(FourOFourSceneName, { + url: window.location.pathname.toString() + }); + } this.loadMap(this.startRoom.url, this.startRoom.ID); } @@ -40,8 +47,8 @@ export class GameManager { getCharacterSelected(): string[] { return this.characterLayers; } - - + + public loadMap(mapUrl: string, roomID: string): void { console.log('Loading map '+roomID+' at url '+mapUrl); const gameIndex = this.sceneManager.getIndex(roomID); @@ -57,7 +64,7 @@ export class GameManager { const endPos = mapUrlStart.indexOf(".json"); return mapUrlStart.substring(startPos, endPos); } - + public async goToStartingMap() { this.sceneManager.start(this.startRoom.ID, {startLayerName: 'global'}); } diff --git a/front/src/Phaser/Reconnecting/FourOFourScene.ts b/front/src/Phaser/Reconnecting/FourOFourScene.ts index 0c91a5bc..3e84b7e9 100644 --- a/front/src/Phaser/Reconnecting/FourOFourScene.ts +++ b/front/src/Phaser/Reconnecting/FourOFourScene.ts @@ -15,7 +15,8 @@ export class FourOFourScene extends Phaser.Scene { private fileNameField!: Text; private logo!: Image; private cat!: Sprite; - private file!: string; + private file: string|undefined; + private url: string|undefined; constructor() { super({ @@ -23,8 +24,9 @@ export class FourOFourScene extends Phaser.Scene { }); } - init({ file }: { file: string }) { + init({ file, url }: { file?: string, url?: string }) { this.file = file; + this.url = url; } preload() { @@ -45,11 +47,22 @@ export class FourOFourScene extends Phaser.Scene { this.mapNotFoundField = new TextField(this, this.game.renderer.width / 2, this.game.renderer.height / 2, "404 - File not found"); this.mapNotFoundField.setOrigin(0.5, 0.5).setCenterAlign(); - this.couldNotFindField = new TextField(this, this.game.renderer.width / 2, this.game.renderer.height / 2 + 24, "Could not load file"); + let text: string = ''; + if (this.file !== undefined) { + text = "Could not load map" + } + if (this.url !== undefined) { + text = "Invalid URL" + } + + this.couldNotFindField = new TextField(this, this.game.renderer.width / 2, this.game.renderer.height / 2 + 24, text); this.couldNotFindField.setOrigin(0.5, 0.5).setCenterAlign(); - this.fileNameField = this.add.text(this.game.renderer.width / 2, this.game.renderer.height / 2 + 38, this.file, { fontFamily: 'Georgia, "Goudy Bookletter 1911", Times, serif', fontSize: '10px' }); - this.fileNameField.setOrigin(0.5, 0.5); + const url = this.file ? this.file : this.url; + if (url !== undefined) { + this.fileNameField = this.add.text(this.game.renderer.width / 2, this.game.renderer.height / 2 + 38, url, { fontFamily: 'Georgia, "Goudy Bookletter 1911", Times, serif', fontSize: '10px' }); + this.fileNameField.setOrigin(0.5, 0.5); + } this.cat = this.physics.add.sprite(this.game.renderer.width / 2, this.game.renderer.height / 2 - 32, 'cat', 6); this.cat.flipY=true; diff --git a/front/src/Url/UrlManager.ts b/front/src/Url/UrlManager.ts index 39f5667d..876e258e 100644 --- a/front/src/Url/UrlManager.ts +++ b/front/src/Url/UrlManager.ts @@ -16,7 +16,7 @@ class UrlManager { return GameConnexionTypes.anonymous; } else if (url.indexOf('@/') > -1) { return GameConnexionTypes.organization; - } else if(url.indexOf('register/')) { + } else if(url.indexOf('register/') > -1) { return GameConnexionTypes.register } else { return GameConnexionTypes.unknown From 0731bd39e500b181f527cceaae2f6ddaf0a295a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20N=C3=A9grier?= Date: Mon, 12 Oct 2020 18:59:49 +0200 Subject: [PATCH 3/8] Moving back to using ScenePlugin and adding EntryScene --- front/src/Phaser/Game/GameManager.ts | 26 ++++++-------- front/src/Phaser/Game/GameScene.ts | 10 +++--- front/src/Phaser/Login/EnableCameraScene.ts | 2 +- front/src/Phaser/Login/EntryScene.ts | 40 +++++++++++++++++++++ front/src/Url/UrlManager.ts | 14 ++++---- front/src/index.ts | 5 ++- 6 files changed, 65 insertions(+), 32 deletions(-) create mode 100644 front/src/Phaser/Login/EntryScene.ts diff --git a/front/src/Phaser/Game/GameManager.ts b/front/src/Phaser/Game/GameManager.ts index bed098ae..22123d1c 100644 --- a/front/src/Phaser/Game/GameManager.ts +++ b/front/src/Phaser/Game/GameManager.ts @@ -14,18 +14,10 @@ export class GameManager { private playerName!: string; private characterLayers!: string[]; private startRoom!:Room; - private sceneManager!: Phaser.Scenes.SceneManager; - public async init(sceneManager: Phaser.Scenes.SceneManager) { - this.sceneManager = sceneManager; - try { - this.startRoom = await connectionManager.initGameConnexion(); - } catch (e) { - this.sceneManager.start(FourOFourSceneName, { - url: window.location.pathname.toString() - }); - } - this.loadMap(this.startRoom.url, this.startRoom.ID); + public async init(scenePlugin: Phaser.Scenes.ScenePlugin) { + this.startRoom = await connectionManager.initGameConnexion(); + this.loadMap(this.startRoom.url, this.startRoom.ID, scenePlugin); } public setPlayerName(name: string): void { @@ -49,12 +41,13 @@ export class GameManager { } - public loadMap(mapUrl: string, roomID: string): void { + public loadMap(mapUrl: string, roomID: string, scenePlugin: Phaser.Scenes.ScenePlugin): void { console.log('Loading map '+roomID+' at url '+mapUrl); - const gameIndex = this.sceneManager.getIndex(roomID); + const gameIndex = scenePlugin.getIndex(mapUrl); if(gameIndex === -1){ const game : Phaser.Scene = GameScene.createFromUrl(mapUrl, roomID); - this.sceneManager.add(roomID, game, false); + console.log('Adding scene '+mapUrl); + scenePlugin.add(mapUrl, game, false); } } @@ -65,8 +58,9 @@ export class GameManager { return mapUrlStart.substring(startPos, endPos); } - public async goToStartingMap() { - this.sceneManager.start(this.startRoom.ID, {startLayerName: 'global'}); + public async goToStartingMap(scenePlugin: Phaser.Scenes.ScenePlugin) { + console.log('Starting scene '+this.startRoom.url); + scenePlugin.start(this.startRoom.url, {startLayerName: 'global'}); } } diff --git a/front/src/Phaser/Game/GameScene.ts b/front/src/Phaser/Game/GameScene.ts index 602f697e..a22d973a 100644 --- a/front/src/Phaser/Game/GameScene.ts +++ b/front/src/Phaser/Game/GameScene.ts @@ -139,11 +139,11 @@ export class GameScene extends ResizableScene implements CenterListener { private userInputManager!: UserInputManager; static createFromUrl(mapUrlFile: string, instance: string, gameSceneKey: string|null = null): GameScene { - const mapKey = gameManager.getMapKeyByUrl(mapUrlFile); + // We use the map URL as a key if (gameSceneKey === null) { - gameSceneKey = mapKey; + gameSceneKey = mapUrlFile; } - return new GameScene(mapKey, mapUrlFile, instance, gameSceneKey); + return new GameScene(mapUrlFile, mapUrlFile, instance, gameSceneKey); } constructor(MapKey : string, MapUrlFile: string, instance: string, gameSceneKey: string) { @@ -418,7 +418,7 @@ export class GameScene extends ResizableScene implements CenterListener { context.strokeStyle = '#ffffff'; context.stroke(); this.circleTexture.refresh(); - + // Let's pause the scene if the connection is not established yet if (this.connection === undefined) { // Let's wait 0.5 seconds before printing the "connecting" screen to avoid blinking @@ -691,7 +691,7 @@ export class GameScene extends ResizableScene implements CenterListener { // TODO: eventually compute a relative URL const absoluteExitSceneUrl = new URL(exitSceneUrl, this.MapUrlFile).href; - gameManager.loadMap(absoluteExitSceneUrl, instance); + gameManager.loadMap(absoluteExitSceneUrl, instance, this.scene); const exitSceneKey = instance; const tiles : number[] = layer.data as number[]; diff --git a/front/src/Phaser/Login/EnableCameraScene.ts b/front/src/Phaser/Login/EnableCameraScene.ts index 8695464b..3916587a 100644 --- a/front/src/Phaser/Login/EnableCameraScene.ts +++ b/front/src/Phaser/Login/EnableCameraScene.ts @@ -266,7 +266,7 @@ export class EnableCameraScene extends Phaser.Scene { mediaManager.stopCamera(); mediaManager.stopMicrophone(); - gameManager.goToStartingMap(); + gameManager.goToStartingMap(this.scene); } private async getDevices() { diff --git a/front/src/Phaser/Login/EntryScene.ts b/front/src/Phaser/Login/EntryScene.ts new file mode 100644 index 00000000..fec4e880 --- /dev/null +++ b/front/src/Phaser/Login/EntryScene.ts @@ -0,0 +1,40 @@ +import {gameManager} from "../Game/GameManager"; +import {TextField} from "../Components/TextField"; +import {TextInput} from "../Components/TextInput"; +import {ClickButton} from "../Components/ClickButton"; +import Image = Phaser.GameObjects.Image; +import Rectangle = Phaser.GameObjects.Rectangle; +import {PLAYER_RESOURCES, PlayerResourceDescriptionInterface} from "../Entity/Character"; +import {cypressAsserter} from "../../Cypress/CypressAsserter"; +import {SelectCharacterSceneName} from "./SelectCharacterScene"; +import {ResizableScene} from "./ResizableScene"; +import {Scene} from "phaser"; +import {LoginSceneName} from "./LoginScene"; +import {FourOFourSceneName} from "../Reconnecting/FourOFourScene"; + +export const EntrySceneName = "EntryScene"; + +/** + * The EntryScene is not a real scene. It is the first scene loaded and is only used to initialize the gameManager + * and to route to the next correct scene. + */ +export class EntryScene extends Scene { + constructor() { + super({ + key: EntrySceneName + }); + } + + preload() { + } + + create() { + gameManager.init(this.scene).then(() => { + this.scene.start(LoginSceneName); + }).catch(() => { + this.scene.start(FourOFourSceneName, { + url: window.location.pathname.toString() + }); + }); + } +} diff --git a/front/src/Url/UrlManager.ts b/front/src/Url/UrlManager.ts index 876e258e..ae8725bc 100644 --- a/front/src/Url/UrlManager.ts +++ b/front/src/Url/UrlManager.ts @@ -8,7 +8,7 @@ export enum GameConnexionTypes { //this class is responsible with analysing and editing the game's url class UrlManager { - + //todo: use that to detect if we can find a token in localstorage public getGameConnexionType(): GameConnexionTypes { const url = window.location.pathname.toString(); @@ -22,14 +22,14 @@ class UrlManager { return GameConnexionTypes.unknown } } - + public getAnonymousMapUrlStart():string { const match = /\/_\/global\/(.+)/.exec(window.location.pathname.toString()) if (!match) throw new Error('Could not extract startmap url from'+window.location.pathname); - return match[1]; - + return window.location.protocol+'//'+match[1]; + } - + public getOrganizationToken(): string|null { const match = /\/register\/(.+)/.exec(window.location.pathname.toString()); return match ? match [1] : null; @@ -46,7 +46,7 @@ class UrlManager { history.pushState({}, 'WorkAdventure', newUrl); return newUrl; } - + } -export const urlManager = new UrlManager(); \ No newline at end of file +export const urlManager = new UrlManager(); diff --git a/front/src/index.ts b/front/src/index.ts index f57474d7..e12d8707 100644 --- a/front/src/index.ts +++ b/front/src/index.ts @@ -13,6 +13,7 @@ import {CustomizeScene} from "./Phaser/Login/CustomizeScene"; import {CoWebsiteManager} from "./WebRtc/CoWebsiteManager"; import {gameManager} from "./Phaser/Game/GameManager"; import {ResizableScene} from "./Phaser/Login/ResizableScene"; +import {EntryScene} from "./Phaser/Login/EntryScene"; //CoWebsiteManager.loadCoWebsite('https://thecodingmachine.com'); @@ -30,7 +31,7 @@ const config: GameConfig = { width: width / RESOLUTION, height: height / RESOLUTION, parent: "game", - scene: [LoginScene, SelectCharacterScene, EnableCameraScene, ReconnectingScene, FourOFourScene, CustomizeScene], + scene: [EntryScene, LoginScene, SelectCharacterScene, EnableCameraScene, ReconnectingScene, FourOFourScene, CustomizeScene], zoom: RESOLUTION, physics: { default: "arcade", @@ -51,8 +52,6 @@ cypressAsserter.gameStarted(); const game = new Phaser.Game(config); -gameManager.init(game.scene); - window.addEventListener('resize', function (event) { const {width, height} = CoWebsiteManager.getGameSize(); From 9a04836215ad27239c73ccf3166844cb6c7b6c91 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20N=C3=A9grier?= Date: Tue, 13 Oct 2020 10:26:27 +0200 Subject: [PATCH 4/8] Dynamically import Quill We load Quill only if it is needed (after all, only admins need Quill) --- front/dist/index.html | 6 - front/package.json | 3 +- .../ConsoleGlobalMessageManager.ts | 57 +- front/yarn.lock | 578 +++++++++--------- 4 files changed, 324 insertions(+), 320 deletions(-) diff --git a/front/dist/index.html b/front/dist/index.html index 9d883ffe..5984af7b 100644 --- a/front/dist/index.html +++ b/front/dist/index.html @@ -6,12 +6,6 @@ content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0"> - - - - - -