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:
commit
9c44d37020
28 changed files with 1187 additions and 969 deletions
|
@ -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,
|
||||
}));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
})();
|
||||
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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') {
|
|
@ -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";
|
||||
|
|
14
back/src/Model/RoomIdentifier.ts
Normal file
14
back/src/Model/RoomIdentifier.ts
Normal 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);
|
||||
}
|
||||
}
|
66
back/src/Services/AdminApi.ts
Normal file
66
back/src/Services/AdminApi.ts
Normal 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();
|
60
back/src/Services/JWTTokenManager.ts
Normal file
60
back/src/Services/JWTTokenManager.ts
Normal 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();
|
Loading…
Add table
Add a link
Reference in a new issue