Merge branch 'develop' of github.com:thecodingmachine/workadventure into feature/global-message

+ migrating to protobuf messages

# Conflicts:
#	back/src/App.ts
#	back/src/Controller/IoSocketController.ts
#	back/yarn.lock
#	front/src/Connection.ts
#	front/src/Phaser/Game/GameScene.ts
#	front/src/Phaser/Login/EnableCameraScene.ts
#	front/src/WebRtc/SimplePeer.ts
This commit is contained in:
David Négrier 2020-10-01 14:11:34 +02:00
commit d3fa901691
87 changed files with 7429 additions and 1891 deletions

View file

@ -1,36 +0,0 @@
import {Application, Request, Response} from "express";
import {OK} from "http-status-codes";
import {ADMIN_API_TOKEN, ADMIN_API_URL} from "../Enum/EnvironmentVariable";
import Axios from "axios";
export class AdminController {
App : Application;
constructor(App : Application) {
this.App = App;
this.getLoginUrlByToken();
}
getLoginUrlByToken(){
this.App.get("/register/:token", async (req: Request, res: Response) => {
if (!ADMIN_API_URL) {
return res.status(500).send('No admin backoffice set!');
}
const token:string = req.params.token;
let response = null
try {
response = await Axios.get(ADMIN_API_URL+'/api/login-url/'+token, { headers: {"Authorization" : `${ADMIN_API_TOKEN}`} })
} catch (e) {
console.log(e.message)
return res.status(e.status || 500).send('An error happened');
}
const organizationSlug = response.data.organizationSlug;
const worldSlug = response.data.worldSlug;
const roomSlug = response.data.roomSlug;
return res.status(OK).send({organizationSlug, worldSlug, roomSlug});
});
}
}

View file

@ -1,40 +1,95 @@
import {Application, Request, Response} from "express";
import Jwt from "jsonwebtoken";
import {BAD_REQUEST, OK} from "http-status-codes";
import {SECRET_KEY, URL_ROOM_STARTED} from "../Enum/EnvironmentVariable"; //TODO fix import by "_Enum/..."
import {ADMIN_API_TOKEN, ADMIN_API_URL, SECRET_KEY, URL_ROOM_STARTED} from "../Enum/EnvironmentVariable"; //TODO fix import by "_Enum/..."
import { uuid } from 'uuidv4';
import {HttpRequest, HttpResponse, TemplatedApp} from "uWebSockets.js";
import {BaseController} from "./BaseController";
import Axios from "axios";
export interface TokenInterface {
name: string,
userId: string
userUuid: string
}
export class AuthenticateController {
App : Application;
interface AdminApiData {
organizationSlug: string
worldSlug: string
roomSlug: string
mapUrlStart: string
userUuid: string
}
constructor(App : Application) {
this.App = App;
export class AuthenticateController extends BaseController {
constructor(private App : TemplatedApp) {
super();
this.login();
}
//permit to login on application. Return token to connect on Websocket IO.
login(){
// For now, let's completely forget the /login route.
this.App.post("/login", (req: Request, res: Response) => {
const param = req.body;
/*if(!param.name){
return res.status(BAD_REQUEST).send({
message: "email parameter is empty"
});
}*/
//TODO check user email for The Coding Machine game
const userId = uuid();
const token = Jwt.sign({name: param.name, userId: userId} as TokenInterface, SECRET_KEY, {expiresIn: '24h'});
return res.status(OK).send({
token: token,
mapUrlStart: URL_ROOM_STARTED,
userId: userId,
});
this.App.options("/login", (res: HttpResponse, req: HttpRequest) => {
this.addCorsHeaders(res);
res.end();
});
this.App.post("/login", (res: HttpResponse, req: HttpRequest) => {
(async () => {
this.addCorsHeaders(res);
res.onAborted(() => {
console.warn('Login request was aborted');
})
const host = req.getHeader('host');
const param = await res.json();
//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 (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);
userUuid = data.userUuid;
mapUrlStart = data.mapUrlStart;
newUrl = this.getNewUrlOnAdminAuth(data)
} else {
userUuid = uuid();
mapUrlStart = host.replace('api.', 'maps.') + URL_ROOM_STARTED;
newUrl = null;
}
const authToken = Jwt.sign({userUuid: userUuid}, SECRET_KEY, {expiresIn: '24h'});
res.writeStatus("200 OK").end(JSON.stringify({
authToken,
userUuid,
mapUrlStart,
newUrl,
}));
} catch (e) {
console.log("An error happened", e)
res.writeStatus(e.status || "500 Internal Server Error").end('An error happened');
}
})();
});
}
private getNewUrlOnAdminAuth(data:AdminApiData): string {
const organizationSlug = data.organizationSlug;
const worldSlug = data.worldSlug;
const roomSlug = data.roomSlug;
return '/@/'+organizationSlug+'/'+worldSlug+'/'+roomSlug;
}
}

View file

@ -0,0 +1,10 @@
import {HttpResponse} from "uWebSockets.js";
export class BaseController {
protected addCorsHeaders(res: HttpResponse): void {
res.writeHeader('access-control-allow-headers', 'Origin, X-Requested-With, Content-Type, Accept');
res.writeHeader('access-control-allow-methods', 'GET, POST, OPTIONS, PUT, PATCH, DELETE');
res.writeHeader('access-control-allow-origin', '*');
}
}

View file

@ -0,0 +1,44 @@
import {ADMIN_API_TOKEN} from "../Enum/EnvironmentVariable";
import {IoSocketController} from "_Controller/IoSocketController";
import {stringify} from "circular-json";
import {HttpRequest, HttpResponse} from "uWebSockets.js";
import { parse } from 'query-string';
import {App} from "../Server/sifrr.server";
export class DebugController {
constructor(private App : App, private ioSocketController: IoSocketController) {
this.getDump();
}
getDump(){
this.App.get("/dump", (res: HttpResponse, req: HttpRequest) => {
const query = parse(req.getQuery());
if (query.token !== ADMIN_API_TOKEN) {
return res.status(401).send('Invalid token sent!');
}
return res.writeStatus('200 OK').writeHeader('Content-Type', 'application/json').end(stringify(
this.ioSocketController.getWorlds(),
(key: unknown, value: unknown) => {
if(value instanceof Map) {
const obj: any = {}; // eslint-disable-line @typescript-eslint/no-explicit-any
for (const [mapKey, mapValue] of value.entries()) {
obj[mapKey] = mapValue;
}
return obj;
} else if(value instanceof Set) {
const obj: Array<unknown> = [];
for (const [setKey, setValue] of value.entries()) {
obj.push(setValue);
}
return obj;
} else {
return value;
}
}
));
});
}
}

View file

@ -1,59 +1,161 @@
const multer = require('multer');
import {Application, Request, RequestHandler, Response} from "express";
import {OK} from "http-status-codes";
import {URL_ROOM_STARTED} from "_Enum/EnvironmentVariable";
import {App} from "../Server/sifrr.server";
import {uuid} from "uuidv4";
import fs from "fs";
import {HttpRequest, HttpResponse} from "uWebSockets.js";
import {BaseController} from "./BaseController";
import { Readable } from 'stream'
const upload = multer({ dest: 'dist/files/' });
class FileUpload{
path: string
constructor(path : string) {
this.path = path;
}
interface UploadedFileBuffer {
buffer: Buffer,
expireDate: Date
}
interface RequestFileHandlerInterface extends Request{
file: FileUpload
}
export class FileController extends BaseController {
private uploadedFileBuffers: Map<string, UploadedFileBuffer> = new Map<string, UploadedFileBuffer>();
export class FileController {
App : Application;
constructor(App : Application) {
constructor(private App : App) {
super();
this.App = App;
this.uploadAudioMessage();
this.downloadAudioMessage();
// Cleanup every 1 minute
setInterval(this.cleanup.bind(this), 60000);
}
/**
* Clean memory from old files
*/
cleanup(): void {
const now = new Date();
for (const [id, file] of this.uploadedFileBuffers) {
if (file.expireDate < now) {
this.uploadedFileBuffers.delete(id);
}
}
}
uploadAudioMessage(){
this.App.post("/upload-audio-message", (upload.single('file') as RequestHandler), (req: Request, res: Response) => {
//TODO check user connected and admin role
//TODO upload audio message
const audioMessageId = uuid();
this.App.options("/upload-audio-message", (res: HttpResponse, req: HttpRequest) => {
this.addCorsHeaders(res);
fs.copyFileSync((req as RequestFileHandlerInterface).file.path, `dist/files/${audioMessageId}`);
fs.unlinkSync((req as RequestFileHandlerInterface).file.path);
res.end();
});
return res.status(OK).send({
id: audioMessageId,
path: `/download-audio-message/${audioMessageId}`
});
this.App.post("/upload-audio-message", (res: HttpResponse, req: HttpRequest) => {
(async () => {
this.addCorsHeaders(res);
res.onAborted(() => {
console.warn('upload-audio-message request was aborted');
})
try {
const audioMessageId = uuid();
const params = await res.formData({
onFile: (fieldname: string,
file: NodeJS.ReadableStream,
filename: string,
encoding: string,
mimetype: string) => {
(async () => {
console.log('READING FILE', fieldname)
const chunks: Buffer[] = []
for await (let chunk of file) {
if (!(chunk instanceof Buffer)) {
throw new Error('Unexpected chunk');
}
chunks.push(chunk)
}
// Let's expire in 1 minute.
const expireDate = new Date();
expireDate.setMinutes(expireDate.getMinutes() + 1);
this.uploadedFileBuffers.set(audioMessageId, {
buffer: Buffer.concat(chunks),
expireDate
});
})();
}
});
res.writeStatus("200 OK").end(JSON.stringify({
id: audioMessageId,
path: `/download-audio-message/${audioMessageId}`
}));
} catch (e) {
console.log("An error happened", e)
res.writeStatus(e.status || "500 Internal Server Error").end('An error happened');
}
})();
});
}
downloadAudioMessage(){
this.App.get("/download-audio-message/:id", (req: Request, res: Response) => {
//TODO check user connected and admin role
//TODO upload audio message
const audiMessageId = req.params.id;
this.App.options("/download-audio-message/*", (res: HttpResponse, req: HttpRequest) => {
this.addCorsHeaders(res);
const fs = require('fs');
const path = `dist/files/${audiMessageId}`;
const file = fs.createReadStream(path);
res.writeHead(200);
file.pipe(res);
res.end();
});
this.App.get("/download-audio-message/:id", (res: HttpResponse, req: HttpRequest) => {
(async () => {
this.addCorsHeaders(res);
res.onAborted(() => {
console.warn('upload-audio-message request was aborted');
})
const id = req.getParameter(0);
const file = this.uploadedFileBuffers.get(id);
if (file === undefined) {
res.writeStatus("404 Not found").end("Cannot find file");
return;
}
const readable = new Readable()
readable._read = () => {} // _read is required but you can noop it
readable.push(file.buffer);
readable.push(null);
const size = file.buffer.byteLength;
res.writeStatus("200 OK");
readable.on('data', buffer => {
const chunk = buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength),
lastOffset = res.getWriteOffset();
// First try
const [ok, done] = res.tryEnd(chunk, size);
if (done) {
readable.destroy();
} else if (!ok) {
// pause because backpressure
readable.pause();
// Save unsent chunk for later
res.ab = chunk;
res.abOffset = lastOffset;
// Register async handlers for drainage
res.onWritable(offset => {
const [ok, done] = res.tryEnd(res.ab.slice(offset - res.abOffset), size);
if (done) {
readable.destroy();
} else if (ok) {
readable.resume();
}
return ok;
});
}
});
})();
});
}
}
}

File diff suppressed because it is too large Load diff

View file

@ -1,29 +1,34 @@
import express from "express";
import {Application, Request, Response} from "express";
import {OK} from "http-status-codes";
import {URL_ROOM_STARTED} from "../Enum/EnvironmentVariable";
import {HttpRequest, HttpResponse, TemplatedApp} from "uWebSockets.js";
import {BaseController} from "./BaseController";
export class MapController {
App: Application;
//todo: delete this
export class MapController extends BaseController{
constructor(App: Application) {
constructor(private App : TemplatedApp) {
super();
this.App = App;
this.getStartMap();
this.assetMaps();
}
assetMaps() {
this.App.use('/map/files', express.static('src/Assets/Maps'));
}
// Returns a map mapping map name to file name of the map
getStartMap() {
this.App.get("/start-map", (req: Request, res: Response) => {
const url = req.headers.host?.replace('api.', 'maps.') + URL_ROOM_STARTED;
res.status(OK).send({
this.App.options("/start-map", (res: HttpResponse, req: HttpRequest) => {
this.addCorsHeaders(res);
res.end();
});
this.App.get("/start-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"
});
}));
});
}
}

View file

@ -1,10 +1,11 @@
import {Application, Request, Response} from "express";
import {App} from "../Server/sifrr.server";
import {IoSocketController} from "_Controller/IoSocketController";
import {HttpRequest, HttpResponse} from "uWebSockets.js";
const register = require('prom-client').register;
const collectDefaultMetrics = require('prom-client').collectDefaultMetrics;
export class PrometheusController {
constructor(private App: Application, private ioSocketController: IoSocketController) {
constructor(private App: App, private ioSocketController: IoSocketController) {
collectDefaultMetrics({
timeout: 10000,
gcDurationBuckets: [0.001, 0.01, 0.1, 1, 2, 5], // These are the default buckets.
@ -13,8 +14,8 @@ export class PrometheusController {
this.App.get("/metrics", this.metrics.bind(this));
}
private metrics(req: Request, res: Response): void {
res.set('Content-Type', register.contentType);
private metrics(res: HttpResponse, req: HttpRequest): void {
res.writeHeader('Content-Type', register.contentType);
res.end(register.metrics());
}
}