Merge remote-tracking branch 'github.com/develop' into player-report

# Conflicts:
#	back/src/Controller/IoSocketController.ts
#	front/src/Phaser/Game/GameScene.ts
#	front/src/index.ts
#	messages/messages.proto
This commit is contained in:
Gregoire Parant 2020-10-13 21:14:46 +02:00
commit 9c44d37020
28 changed files with 1187 additions and 969 deletions

View file

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

View file

@ -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,29 @@ import {
ItemEventMessage,
ViewportMessage,
ClientToServerMessage,
JoinRoomMessage,
ErrorMessage,
RoomJoinedMessage,
ItemStateMessage,
ServerToClientMessage,
SetUserIdMessage,
SilentMessage,
WebRtcSignalToClientMessage,
WebRtcSignalToServerMessage,
WebRtcStartMessage, WebRtcDisconnectMessage, PlayGlobalMessage, ReportPlayerMessage, TeleportMessageMessage
WebRtcStartMessage,
WebRtcDisconnectMessage,
PlayGlobalMessage,
ReportPlayerMessage,
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 +66,7 @@ function emitInBatch(socket: ExSocketInterface, payload: SubMessage): void {
}
export class IoSocketController {
private Worlds: Map<string, World> = new Map<string, World>();
private Worlds: Map<string, GameRoom> = new Map<string, GameRoom>();
private sockets: Map<number, ExSocketInterface> = new Map<number, ExSocketInterface>();
private nbClientsGauge: Gauge<string>;
private nbClientsPerRoomGauge: Gauge<string>;
@ -101,76 +88,10 @@ 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 +100,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 +109,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 +167,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 +217,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 +314,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 +349,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 +357,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 +449,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 +463,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 +583,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 +599,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 +672,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 +865,7 @@ export class IoSocketController {
}
public getWorlds(): Map<string, World> {
public getWorlds(): Map<string, GameRoom> {
return this.Worlds;
}

View file

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

View file

@ -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') {

View file

@ -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";

View file

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

View file

@ -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<AdminApiData> {
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<AdminApiData> {
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<boolean> {
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();

View file

@ -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<string> {
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<string>((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();