diff --git a/back/src/Controller/AuthenticateController.ts b/back/src/Controller/AuthenticateController.ts index 984d7445..4d950815 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,30 @@ 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) => { + 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/Controller/MapController.ts b/back/src/Controller/MapController.ts index 50e47a9d..86e3790f 100644 --- a/back/src/Controller/MapController.ts +++ b/back/src/Controller/MapController.ts @@ -2,6 +2,8 @@ import {OK} from "http-status-codes"; import {URL_ROOM_STARTED} from "../Enum/EnvironmentVariable"; import {HttpRequest, HttpResponse, TemplatedApp} from "uWebSockets.js"; import {BaseController} from "./BaseController"; +import {parse} from "query-string"; +import {adminApi} from "../Services/AdminApi"; //todo: delete this export class MapController extends BaseController{ @@ -9,26 +11,51 @@ export class MapController extends BaseController{ constructor(private App : TemplatedApp) { super(); this.App = App; - this.getStartMap(); + this.getMapUrl(); } // Returns a map mapping map name to file name of the map - getStartMap() { - this.App.options("/start-map", (res: HttpResponse, req: HttpRequest) => { + getMapUrl() { + this.App.options("/map", (res: HttpResponse, req: HttpRequest) => { this.addCorsHeaders(res); res.end(); }); - this.App.get("/start-map", (res: HttpResponse, req: HttpRequest) => { + this.App.get("/map", (res: HttpResponse, req: HttpRequest) => { this.addCorsHeaders(res); - const url = req.getHeader('host').replace('api.', 'maps.') + URL_ROOM_STARTED; - res.writeStatus("200 OK").end(JSON.stringify({ - mapUrlStart: url, - startInstance: "global" - })); + res.onAborted(() => { + console.warn('/map request was aborted'); + }) + + const query = parse(req.getQuery()); + + if (typeof query.organizationSlug !== 'string') { + console.error('Expected organizationSlug parameter'); + res.writeStatus("400 Bad request").end("Expected organizationSlug parameter"); + } + if (typeof query.worldSlug !== 'string') { + console.error('Expected worldSlug parameter'); + res.writeStatus("400 Bad request").end("Expected worldSlug parameter"); + } + if (typeof query.roomSlug !== 'string' && query.roomSlug !== undefined) { + console.error('Expected only one roomSlug parameter'); + res.writeStatus("400 Bad request").end("Expected only one roomSlug parameter"); + } + + (async () => { + try { + const mapDetails = await adminApi.fetchMapDetails(query.organizationSlug as string, query.worldSlug as string, query.roomSlug as string|undefined); + + res.writeStatus("200 OK").end(JSON.stringify(mapDetails)); + } catch (e) { + console.error(e); + res.writeStatus("500 Internal Server Error").end("An error occurred"); + } + })(); + }); } } diff --git a/back/src/Model/RoomIdentifier.ts b/back/src/Model/RoomIdentifier.ts new file mode 100644 index 00000000..2cba16fd --- /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.startsWith('_/')) { + this.anonymous = true; + } else if(roomID.startsWith('@/')) { + 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..9e64296c 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 @@ -10,7 +11,30 @@ export interface AdminApiData { } class AdminApi { - + + async fetchMapDetails(organizationSlug: string, worldSlug: string, roomSlug: string|undefined): Promise { + if (!ADMIN_API_URL) { + return Promise.reject('No admin backoffice set!'); + } + + const params: { organizationSlug: string, worldSlug: string, roomSlug?: string } = { + organizationSlug, + worldSlug + }; + + if (roomSlug) { + params.roomSlug = roomSlug; + } + + const res = await Axios.get(ADMIN_API_URL+'/api/map', + { + headers: {"Authorization" : `${ADMIN_API_TOKEN}`}, + params + } + ) + return res.data; + } + async fetchMemberDataByToken(organizationMemberToken: string): Promise { if (!ADMIN_API_URL) { return Promise.reject('No admin backoffice set!'); @@ -22,13 +46,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) { @@ -38,4 +63,4 @@ class AdminApi { } } -export const adminApi = new AdminApi(); \ No newline at end of file +export const adminApi = new AdminApi(); 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"> - - - - - -