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:
commit
d3fa901691
87 changed files with 7429 additions and 1891 deletions
|
@ -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});
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
10
back/src/Controller/BaseController.ts
Normal file
10
back/src/Controller/BaseController.ts
Normal 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', '*');
|
||||
}
|
||||
}
|
44
back/src/Controller/DebugController.ts
Normal file
44
back/src/Controller/DebugController.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
));
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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
|
@ -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"
|
||||
});
|
||||
}));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue