diff --git a/back/src/Controller/AuthenticateController.ts b/back/src/Controller/AuthenticateController.ts index 84cfd97f..4d950815 100644 --- a/back/src/Controller/AuthenticateController.ts +++ b/back/src/Controller/AuthenticateController.ts @@ -1,38 +1,30 @@ -import Jwt from "jsonwebtoken"; -import {ADMIN_API_TOKEN, ADMIN_API_URL, SECRET_KEY, 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 Axios from "axios"; +import {adminApi} from "../Services/AdminApi"; +import {jwtTokenManager} from "../Services/JWTTokenManager"; export interface TokenInterface { userUuid: string } -interface AdminApiData { - organizationSlug: string - worldSlug: string - roomSlug: string - mapUrlStart: string - userUuid: string -} - 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); @@ -44,36 +36,25 @@ export class AuthenticateController extends BaseController { //todo: what to do if the organizationMemberToken is already used? const organizationMemberToken:string|null = param.organizationMemberToken; - + 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) { - if (!ADMIN_API_URL) { - return res.status(401).send('No admin backoffice set!'); - } - //todo: this call can fail if the corresponding world is not activated or if the token is invalid. Handle that case. - const data = await Axios.get(ADMIN_API_URL+'/api/login-url/'+organizationMemberToken, - { headers: {"Authorization" : `${ADMIN_API_TOKEN}`} } - ).then((res): AdminApiData => res.data); + const userUuid = data.userUuid; + const organizationSlug = data.organizationSlug; + const worldSlug = data.worldSlug; + const roomSlug = data.roomSlug; + const mapUrlStart = data.mapUrlStart; - userUuid = data.userUuid; - mapUrlStart = data.mapUrlStart; - newUrl = this.getNewUrlOnAdminAuth(data) - } else { - userUuid = v4(); - mapUrlStart = host.replace('api.', 'maps.') + URL_ROOM_STARTED; - newUrl = null; - } - - const authToken = Jwt.sign({userUuid: userUuid}, SECRET_KEY, {expiresIn: '24h'}); + const authToken = jwtTokenManager.createJWTToken(userUuid); res.writeStatus("200 OK").end(JSON.stringify({ authToken, userUuid, + organizationSlug, + worldSlug, + roomSlug, mapUrlStart, - newUrl, })); } catch (e) { @@ -84,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 2ef8b422..2a14ade1 100644 --- a/back/src/Controller/IoSocketController.ts +++ b/back/src/Controller/IoSocketController.ts @@ -1,30 +1,11 @@ -import * as http from "http"; -import {MessageUserPosition, Point} from "../Model/Websocket/MessageUserPosition"; //TODO fix import by "_Model/.." import {ExSocketInterface} from "../Model/Websocket/ExSocketInterface"; //TODO fix import by "_Model/.." -import Jwt, {JsonWebTokenError} from "jsonwebtoken"; -import { - SECRET_KEY, - MINIMUM_DISTANCE, - GROUP_RADIUS, - ALLOW_ARTILLERY, - ADMIN_API_URL, - ADMIN_API_TOKEN -} from "../Enum/EnvironmentVariable"; //TODO fix import by "_Enum/..." -import {World} from "../Model/World"; +import {MINIMUM_DISTANCE, GROUP_RADIUS} from "../Enum/EnvironmentVariable"; //TODO fix import by "_Enum/..." +import {GameRoom} from "../Model/GameRoom"; import {Group} from "../Model/Group"; import {User} from "../Model/User"; import {isSetPlayerDetailsMessage,} from "../Model/Websocket/SetPlayerDetailsMessage"; -import {MessageUserJoined} from "../Model/Websocket/MessageUserJoined"; -import si from "systeminformation"; import {Gauge} from "prom-client"; -import {TokenInterface} from "../Controller/AuthenticateController"; -import {isJoinRoomMessageInterface} from "../Model/Websocket/JoinRoomMessage"; import {PointInterface} from "../Model/Websocket/PointInterface"; -import {isWebRtcSignalMessageInterface} from "../Model/Websocket/WebRtcSignalMessage"; -import {UserInGroupInterface} from "../Model/Websocket/UserInGroupInterface"; -import {isItemEventMessageInterface} from "../Model/Websocket/ItemEventMessage"; -import { v4 as uuidv4 } from 'uuid'; -import {GroupUpdateInterface} from "_Model/Websocket/GroupUpdateInterface"; import {Movable} from "../Model/Movable"; import { PositionMessage, @@ -40,23 +21,28 @@ import { ItemEventMessage, ViewportMessage, ClientToServerMessage, - JoinRoomMessage, ErrorMessage, RoomJoinedMessage, ItemStateMessage, ServerToClientMessage, - SetUserIdMessage, SilentMessage, WebRtcSignalToClientMessage, WebRtcSignalToServerMessage, - WebRtcStartMessage, WebRtcDisconnectMessage, PlayGlobalMessage, ReportPlayerMessage, TeleportMessageMessage + WebRtcStartMessage, + WebRtcDisconnectMessage, + PlayGlobalMessage, + TeleportMessageMessage } from "../Messages/generated/messages_pb"; import {UserMovesMessage} from "../Messages/generated/messages_pb"; import Direction = PositionMessage.Direction; import {ProtobufUtils} from "../Model/Websocket/ProtobufUtils"; -import {App, HttpRequest, TemplatedApp, WebSocket} from "uWebSockets.js" +import {TemplatedApp} from "uWebSockets.js" import {parse} from "query-string"; 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"; import Axios from "axios"; function emitInBatch(socket: ExSocketInterface, payload: SubMessage): void { @@ -79,7 +65,7 @@ function emitInBatch(socket: ExSocketInterface, payload: SubMessage): void { } export class IoSocketController { - private Worlds: Map = new Map(); + private Worlds: Map = new Map(); private sockets: Map = new Map(); private nbClientsGauge: Gauge; private nbClientsPerRoomGauge: Gauge; @@ -101,76 +87,8 @@ export class IoSocketController { this.ioConnection(); } - private isValidToken(token: object): token is TokenInterface { - if (typeof((token as TokenInterface).userUuid) !== 'string') { - return false; - } - return true; - } - - private async authenticate(req: HttpRequest): Promise<{ token: string, userUuid: string }> { - //console.log(socket.handshake.query.token); - - const query = parse(req.getQuery()); - - if (!query.token) { - throw new Error('An authentication error happened, a user tried to connect without a token.'); - } - - const token = query.token; - if (typeof(token) !== "string") { - throw new Error('Token is expected to be a string'); - } - - - if(token === 'test') { - if (ALLOW_ARTILLERY) { - return { - token, - userUuid: uuidv4() - } - } else { - throw new Error("In order to perform a load-testing test on this environment, you must set the ALLOW_ARTILLERY environment variable to 'true'"); - } - } - - /*if(this.searchClientByToken(socket.handshake.query.token)){ - console.error('An authentication error happened, a user tried to connect while its token is already connected.'); - return next(new Error('Authentication error')); - }*/ - - const promise = new Promise<{ token: string, userUuid: string }>((resolve, reject) => { - Jwt.verify(token, SECRET_KEY, {},(err, tokenDecoded) => { - if (err) { - console.error('An authentication error happened, invalid JsonWebToken.', err); - reject(new Error('An authentication error happened, invalid JsonWebToken. '+err.message)); - return; - } - if (tokenDecoded === undefined) { - console.error('Empty token found.'); - reject(new Error('Empty token found.')); - return; - } - const tokenInterface = tokenDecoded as TokenInterface; - - if (!this.isValidToken(tokenInterface)) { - reject(new Error('Authentication error, invalid token structure.')); - return; - } - - resolve({ - token, - userUuid: tokenInterface.userUuid - }); - }); - }); - - return promise; - } - ioConnection() { - this.app.ws('/*', { - + this.app.ws('/room', { /* Options */ //compression: uWS.SHARED_COMPRESSOR, maxPayloadLength: 16 * 1024 * 1024, @@ -179,7 +97,6 @@ export class IoSocketController { upgrade: (res, req, context) => { //console.log('An Http connection wants to become WebSocket, URL: ' + req.getUrl() + '!'); (async () => { - /* Keep track of abortions */ const upgradeAborted = {aborted: false}; @@ -189,7 +106,54 @@ export class IoSocketController { }); try { - const result = await this.authenticate(req); + const url = req.getUrl(); + const query = parse(req.getQuery()); + const websocketKey = req.getHeader('sec-websocket-key'); + const websocketProtocol = req.getHeader('sec-websocket-protocol'); + const websocketExtensions = req.getHeader('sec-websocket-extensions'); + + 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); + const y = Number(query.y); + const top = Number(query.top); + const bottom = Number(query.bottom); + const left = Number(query.left); + const right = Number(query.right); + const name = query.name; + if (typeof name !== 'string') { + throw new Error('Expecting name'); + } + if (name === '') { + throw new Error('No empty name'); + } + let characterLayers = query.characterLayers; + if (characterLayers === null) { + throw new Error('Expecting skin'); + } + if (typeof characterLayers === 'string') { + characterLayers = [ characterLayers ]; + } + + + const userUuid = await jwtTokenManager.getUserUuidFromToken(token); + console.log('uuid', userUuid); + + 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) { console.log("Ouch! Client disconnected before we could upgrade it!"); @@ -200,22 +164,37 @@ export class IoSocketController { /* This immediately calls open handler, you must not use res after this call */ res.upgrade({ // Data passed here is accessible on the "websocket" socket object. - url: req.getUrl(), - token: result.token, - userUuid: result.userUuid + url, + token, + userUuid, + roomId, + name, + characterLayers, + position: { + x: x, + y: y, + direction: 'down', + moving: false + } as PointInterface, + viewport: { + top, + right, + bottom, + left + } }, /* Spell these correctly */ - req.getHeader('sec-websocket-key'), - req.getHeader('sec-websocket-protocol'), - req.getHeader('sec-websocket-extensions'), + websocketKey, + websocketProtocol, + websocketExtensions, context); } catch (e) { if (e instanceof Error) { - console.warn(e.message); + console.log(e.message); res.writeStatus("401 Unauthorized").end(e.message); } else { - console.warn(e); + console.log(e); res.writeStatus("500 Internal Server Error").end('An error occurred'); } return; @@ -235,20 +214,25 @@ export class IoSocketController { emitInBatch(client, payload); } client.disconnecting = false; + + client.name = ws.name; + client.characterLayers = ws.characterLayers; + client.roomId = ws.roomId; + this.sockets.set(client.userId, client); // Let's log server load when a user joins this.nbClientsGauge.inc(); console.log(new Date().toISOString() + ' A user joined (', this.sockets.size, ' connected users)'); + // Let's join the room + this.handleJoinRoom(client, client.roomId, client.position, client.viewport, client.name, client.characterLayers); }, - message: (ws, arrayBuffer, isBinary) => { + message: (ws, arrayBuffer, isBinary): void => { const client = ws as ExSocketInterface; const message = ClientToServerMessage.deserializeBinary(new Uint8Array(arrayBuffer)); - if (message.hasJoinroommessage()) { - this.handleJoinRoom(client, message.getJoinroommessage() as JoinRoomMessage); - } else if (message.hasViewportmessage()) { + if (message.hasViewportmessage()) { this.handleViewport(client, message.getViewportmessage() as ViewportMessage); } else if (message.hasUsermovesmessage()) { this.handleUserMovesMessage(client, message.getUsermovesmessage() as UserMovesMessage); @@ -327,26 +311,12 @@ export class IoSocketController { console.warn(message); } - private handleJoinRoom(Client: ExSocketInterface, message: JoinRoomMessage): void { + private handleJoinRoom(client: ExSocketInterface, roomId: string, position: PointInterface, viewport: ViewportInterface, name: string, characterLayers: string[]): void { try { - /*if (!isJoinRoomMessageInterface(message.toObject())) { - console.log(message.toObject()) - this.emitError(Client, 'Invalid JOIN_ROOM message received: ' + message.toObject().toString()); - return; - }*/ - const roomId = message.getRoomid(); - - if (Client.roomId === roomId) { - return; - } - - //leave previous room - //this.leaveRoom(Client); // Useless now, there is only one room per connection - //join new previous room - const world = this.joinRoom(Client, roomId, ProtobufUtils.toPointInterface(message.getPosition() as PositionMessage)); + const gameRoom = this.joinRoom(client, roomId, position); - const things = world.setViewport(Client, (message.getViewport() as ViewportMessage).toObject()); + const things = gameRoom.setViewport(client, viewport); const roomJoinedMessage = new RoomJoinedMessage(); @@ -376,7 +346,7 @@ export class IoSocketController { } } - for (const [itemId, item] of world.getItemsState().entries()) { + for (const [itemId, item] of gameRoom.getItemsState().entries()) { const itemStateMessage = new ItemStateMessage(); itemStateMessage.setItemid(itemId); itemStateMessage.setStatejson(JSON.stringify(item)); @@ -384,11 +354,13 @@ export class IoSocketController { roomJoinedMessage.addItem(itemStateMessage); } + roomJoinedMessage.setCurrentuserid(client.userId); + const serverToClientMessage = new ServerToClientMessage(); serverToClientMessage.setRoomjoinedmessage(roomJoinedMessage); - if (!Client.disconnecting) { - Client.send(serverToClientMessage.serializeBinary().buffer, true); + if (!client.disconnecting) { + client.send(serverToClientMessage.serializeBinary().buffer, true); } } catch (e) { console.error('An error occurred on "join_room" event'); @@ -474,6 +446,7 @@ export class IoSocketController { } } + // Useless now, will be useful again if we allow editing details in game private handleSetPlayerDetails(client: ExSocketInterface, playerDetailsMessage: SetPlayerDetailsMessage) { const playerDetails = { name: playerDetailsMessage.getName(), @@ -487,16 +460,6 @@ export class IoSocketController { client.name = playerDetails.name; client.characterLayers = playerDetails.characterLayers; - - const setUserIdMessage = new SetUserIdMessage(); - setUserIdMessage.setUserid(client.userId); - - const serverToClientMessage = new ServerToClientMessage(); - serverToClientMessage.setSetuseridmessage(setUserIdMessage); - - if (!client.disconnecting) { - client.send(serverToClientMessage.serializeBinary().buffer, true); - } } private handleSilentMessage(client: ExSocketInterface, silentMessage: SilentMessage) { @@ -617,7 +580,7 @@ export class IoSocketController { if(Client.roomId){ try { //user leave previous world - const world: World | undefined = this.Worlds.get(Client.roomId); + const world: GameRoom | undefined = this.Worlds.get(Client.roomId); if (world) { world.leave(Client); if (world.isEmpty()) { @@ -633,17 +596,17 @@ export class IoSocketController { } } - private joinRoom(Client : ExSocketInterface, roomId: string, position: PointInterface): World { + private joinRoom(client : ExSocketInterface, roomId: string, position: PointInterface): GameRoom { + //join user in room - //Client.join(roomId); this.nbClientsPerRoomGauge.inc({ room: roomId }); - Client.roomId = roomId; - Client.position = position; + client.roomId = roomId; + client.position = position; //check and create new world for a room let world = this.Worlds.get(roomId) if(world === undefined){ - world = new World((user1: User, group: Group) => { + world = new GameRoom((user1: User, group: Group) => { this.joinWebRtcRoom(user1, group); }, (user1: User, group: Group) => { this.disConnectedUser(user1, group); @@ -706,10 +669,10 @@ export class IoSocketController { // Dispatch groups position to newly connected user world.getGroups().forEach((group: Group) => { - this.emitCreateUpdateGroupEvent(Client, group); + this.emitCreateUpdateGroupEvent(client, group); }); //join world - world.join(Client, Client.position); + world.join(client, client.position); return world; } @@ -899,7 +862,7 @@ export class IoSocketController { } - public getWorlds(): Map { + public getWorlds(): Map { return this.Worlds; } 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/World.ts b/back/src/Model/GameRoom.ts similarity index 68% rename from back/src/Model/World.ts rename to back/src/Model/GameRoom.ts index c276d04e..1f438e61 100644 --- a/back/src/Model/World.ts +++ b/back/src/Model/GameRoom.ts @@ -1,12 +1,10 @@ -import {MessageUserPosition, Point} from "./Websocket/MessageUserPosition"; import {PointInterface} from "./Websocket/PointInterface"; import {Group} from "./Group"; -import {Distance} from "./Distance"; import {User} from "./User"; import {ExSocketInterface} from "_Model/Websocket/ExSocketInterface"; import {PositionInterface} from "_Model/PositionInterface"; import {Identificable} from "_Model/Websocket/Identificable"; -import {EntersCallback, LeavesCallback, MovesCallback, Zone} from "_Model/Zone"; +import {EntersCallback, LeavesCallback, MovesCallback} from "_Model/Zone"; import {PositionNotifier} from "./PositionNotifier"; import {ViewportInterface} from "_Model/Websocket/ViewportMessage"; import {Movable} from "_Model/Movable"; @@ -14,7 +12,7 @@ import {Movable} from "_Model/Movable"; export type ConnectCallback = (user: User, group: Group) => void; export type DisconnectCallback = (user: User, group: Group) => void; -export class World { +export class GameRoom { private readonly minDistance: number; private readonly groupRadius: number; @@ -123,7 +121,7 @@ export class World { } else { // If the user is part of a group: // should he leave the group? - const distance = World.computeDistanceBetweenPositions(user.getPosition(), user.group.getPosition()); + const distance = GameRoom.computeDistanceBetweenPositions(user.getPosition(), user.group.getPosition()); if (distance > this.groupRadius) { this.leaveGroup(user); } @@ -199,53 +197,19 @@ export class World { return; } - const distance = World.computeDistance(user, currentUser); // compute distance between peers. + const distance = GameRoom.computeDistance(user, currentUser); // compute distance between peers. if(distance <= minimumDistanceFound && distance <= this.minDistance) { minimumDistanceFound = distance; matchingItem = currentUser; } - /*if (typeof currentUser.group === 'undefined' || !currentUser.group.isFull()) { - // We found a user we can bind to. - return; - }*/ - /* - if(context.groups.length > 0) { - - context.groups.forEach(group => { - if(group.isPartOfGroup(userPosition)) { // Is the user in a group ? - if(group.isStillIn(userPosition)) { // Is the user leaving the group ? (is the user at more than max distance of each player) - - // Should we split the group? (is each player reachable from the current player?) - // This is needed if - // A <==> B <==> C <===> D - // becomes A <==> B <=====> C <> D - // If C moves right, the distance between B and C is too great and we must form 2 groups - - } - } else { - // If the user is in no group - // Is there someone in a group close enough and with room in the group ? - } - }); - - } else { - // Aucun groupe n'existe donc je stock les users assez proches de moi - let dist: Distance = { - distance: distance, - first: userPosition, - second: user // TODO: convertir en messageUserPosition - } - usersToBeGroupedWith.push(dist); - } - */ }); this.groups.forEach((group: Group) => { if (group.isFull()) { return; } - const distance = World.computeDistanceBetweenPositions(user.getPosition(), group.getPosition()); + const distance = GameRoom.computeDistanceBetweenPositions(user.getPosition(), group.getPosition()); if(distance <= minimumDistanceFound && distance <= this.groupRadius) { minimumDistanceFound = distance; matchingItem = group; @@ -275,66 +239,7 @@ export class World { return this.itemsState; } - /*getDistancesBetweenGroupUsers(group: Group): Distance[] - { - let i = 0; - let users = group.getUsers(); - let distances: Distance[] = []; - users.forEach(function(user1, key1) { - users.forEach(function(user2, key2) { - if(key1 < key2) { - distances[i] = { - distance: World.computeDistance(user1, user2), - first: user1, - second: user2 - }; - i++; - } - }); - }); - - distances.sort(World.compareDistances); - - return distances; - } - - filterGroup(distances: Distance[], group: Group): void - { - let users = group.getUsers(); - let usersToRemove = false; - let groupTmp: MessageUserPosition[] = []; - distances.forEach(dist => { - if(dist.distance <= World.MIN_DISTANCE) { - let users = [dist.first]; - let usersbis = [dist.second] - groupTmp.push(dist.first); - groupTmp.push(dist.second); - } else { - usersToRemove = true; - } - }); - - if(usersToRemove) { - // Detecte le ou les users qui se sont fait sortir du groupe - let difference = users.filter(x => !groupTmp.includes(x)); - - // TODO : Notify users un difference that they have left the group - } - - let newgroup = new Group(groupTmp); - this.groups.push(newgroup); - } - - private static compareDistances(distA: Distance, distB: Distance): number - { - if (distA.distance < distB.distance) { - return -1; - } - if (distA.distance > distB.distance) { - return 1; - } - return 0; - }*/ + setViewport(socket : Identificable, viewport: ViewportInterface): Movable[] { const user = this.users.get(socket.userId); if(typeof user === 'undefined') { diff --git a/back/src/Model/Group.ts b/back/src/Model/Group.ts index a67383f0..9afa9764 100644 --- a/back/src/Model/Group.ts +++ b/back/src/Model/Group.ts @@ -1,4 +1,4 @@ -import { ConnectCallback, DisconnectCallback } from "./World"; +import { ConnectCallback, DisconnectCallback } from "./GameRoom"; import { User } from "./User"; import {PositionInterface} from "_Model/PositionInterface"; import {Movable} from "_Model/Movable"; 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 new file mode 100644 index 00000000..9e64296c --- /dev/null +++ b/back/src/Services/AdminApi.ts @@ -0,0 +1,66 @@ +import {ADMIN_API_TOKEN, ADMIN_API_URL} from "../Enum/EnvironmentVariable"; +import Axios from "axios"; +import {RoomIdentifier} from "../Model/RoomIdentifier"; + +export interface AdminApiData { + organizationSlug: string + worldSlug: string + roomSlug: string + mapUrlStart: string + userUuid: string +} + +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!'); + } + //todo: this call can fail if the corresponding world is not activated or if the token is invalid. Handle that case. + const res = await Axios.get(ADMIN_API_URL+'/api/login-url/'+organizationMemberToken, + { headers: {"Authorization" : `${ADMIN_API_TOKEN}`} } + ) + return res.data; + } + + 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: roomIdentifier.id} } + ) + return !!res.data; + } catch (e) { + console.log(e.message) + return false; + } + } +} + +export const adminApi = new AdminApi(); diff --git a/back/src/Services/JWTTokenManager.ts b/back/src/Services/JWTTokenManager.ts new file mode 100644 index 00000000..905e0ac6 --- /dev/null +++ b/back/src/Services/JWTTokenManager.ts @@ -0,0 +1,60 @@ +import {ALLOW_ARTILLERY, SECRET_KEY} from "../Enum/EnvironmentVariable"; +import {uuid} from "uuidv4"; +import Jwt from "jsonwebtoken"; +import {TokenInterface} from "../Controller/AuthenticateController"; + +class JWTTokenManager { + + public createJWTToken(userUuid: string) { + return Jwt.sign({userUuid: userUuid}, SECRET_KEY, {expiresIn: '24h'}); + } + + public async getUserUuidFromToken(token: unknown): Promise { + + if (!token) { + throw new Error('An authentication error happened, a user tried to connect without a token.'); + } + if (typeof(token) !== "string") { + throw new Error('Token is expected to be a string'); + } + + + if(token === 'test') { + if (ALLOW_ARTILLERY) { + return uuid(); + } else { + throw new Error("In order to perform a load-testing test on this environment, you must set the ALLOW_ARTILLERY environment variable to 'true'"); + } + } + + return new Promise((resolve, reject) => { + Jwt.verify(token, SECRET_KEY, {},(err, tokenDecoded) => { + const tokenInterface = tokenDecoded as TokenInterface; + if (err) { + console.error('An authentication error happened, invalid JsonWebToken.', err); + reject(new Error('An authentication error happened, invalid JsonWebToken. '+err.message)); + return; + } + if (tokenDecoded === undefined) { + console.error('Empty token found.'); + reject(new Error('Empty token found.')); + return; + } + + if (!this.isValidToken(tokenInterface)) { + reject(new Error('Authentication error, invalid token structure.')); + return; + } + + resolve(tokenInterface.userUuid); + }); + }); + } + + private isValidToken(token: object): token is TokenInterface { + return !(typeof((token as TokenInterface).userUuid) !== 'string'); + } + +} + +export const jwtTokenManager = new JWTTokenManager(); \ No newline at end of file diff --git a/back/tests/PositionNotifierTest.ts b/back/tests/PositionNotifierTest.ts index 253283af..0f556866 100644 --- a/back/tests/PositionNotifierTest.ts +++ b/back/tests/PositionNotifierTest.ts @@ -1,5 +1,5 @@ import "jasmine"; -import {World, ConnectCallback, DisconnectCallback } from "../src/Model/World"; +import {GameRoom, ConnectCallback, DisconnectCallback } from "_Model/GameRoom"; import {Point} from "../src/Model/Websocket/MessageUserPosition"; import { Group } from "../src/Model/Group"; import {PositionNotifier} from "../src/Model/PositionNotifier"; diff --git a/back/tests/WorldTest.ts b/back/tests/WorldTest.ts index 8d3b1a2d..5e06414c 100644 --- a/back/tests/WorldTest.ts +++ b/back/tests/WorldTest.ts @@ -1,5 +1,5 @@ import "jasmine"; -import {World, ConnectCallback, DisconnectCallback } from "../src/Model/World"; +import {GameRoom, ConnectCallback, DisconnectCallback } from "../src/Model/GameRoom"; import {Point} from "../src/Model/Websocket/MessageUserPosition"; import { Group } from "../src/Model/Group"; import {ExSocketInterface} from "_Model/Websocket/ExSocketInterface"; @@ -21,7 +21,7 @@ describe("World", () => { } - const world = new World(connect, disconnect, 160, 160, () => {}, () => {}, () => {}); + const world = new GameRoom(connect, disconnect, 160, 160, () => {}, () => {}, () => {}); world.join(createMockUser(1), new Point(100, 100)); @@ -48,7 +48,7 @@ describe("World", () => { } - const world = new World(connect, disconnect, 160, 160, () => {}, () => {}, () => {}); + const world = new GameRoom(connect, disconnect, 160, 160, () => {}, () => {}, () => {}); world.join(createMockUser(1), new Point(100, 100)); @@ -77,7 +77,7 @@ describe("World", () => { disconnectCallNumber++; } - const world = new World(connect, disconnect, 160, 160, () => {}, () => {}, () => {}); + const world = new GameRoom(connect, disconnect, 160, 160, () => {}, () => {}, () => {}); world.join(createMockUser(1), new Point(100, 100)); 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"> - - - - - -