diff --git a/.env.template b/.env.template index a83bd171..a54df82e 100644 --- a/.env.template +++ b/.env.template @@ -10,3 +10,6 @@ START_ROOM_URL=/_/global/maps.workadventure.localhost/Floor0/floor0.json # If you are using Coturn, this is the value of the "static-auth-secret" parameter in your coturn config file. # Keep empty if you are sharing hard coded / clear text credentials. TURN_STATIC_AUTH_SECRET= + +# The email address used by Let's encrypt to send renewal warnings (compulsory) +ACME_EMAIL= diff --git a/README.md b/README.md index a8c186b6..26f1e816 100644 --- a/README.md +++ b/README.md @@ -25,13 +25,14 @@ docker-compose up The environment will start. -You should now be able to browse to http://workadventure.localhost/ and see the application. +You should now be able to browse to http://play.workadventure.localhost/ and see the application. +You can view the dashboard at http://workadventure.localhost:8080/ Note: on some OSes, you will need to add this line to your `/etc/hosts` file: **/etc/hosts** ``` -workadventure.localhost 127.0.0.1 +127.0.0.1 workadventure.localhost ``` ### MacOS developers, your environment with Vagrant diff --git a/back/src/Controller/BaseController.ts b/back/src/Controller/BaseController.ts index 0b744082..93c17ab4 100644 --- a/back/src/Controller/BaseController.ts +++ b/back/src/Controller/BaseController.ts @@ -1,5 +1,4 @@ -import {HttpRequest, HttpResponse} from "uWebSockets.js"; -import {ADMIN_API_TOKEN} from "../Enum/EnvironmentVariable"; +import {HttpResponse} from "uWebSockets.js"; export class BaseController { diff --git a/back/src/Controller/DebugController.ts b/back/src/Controller/DebugController.ts index a94cc616..509d8b2f 100644 --- a/back/src/Controller/DebugController.ts +++ b/back/src/Controller/DebugController.ts @@ -4,7 +4,6 @@ import {HttpRequest, HttpResponse} from "uWebSockets.js"; import { parse } from 'query-string'; import {App} from "../Server/sifrr.server"; import {socketManager} from "../Services/SocketManager"; -import {ServerWritableStream} from "grpc"; export class DebugController { constructor(private App : App) { diff --git a/back/src/Enum/EnvironmentVariable.ts b/back/src/Enum/EnvironmentVariable.ts index b12f0542..8736a856 100644 --- a/back/src/Enum/EnvironmentVariable.ts +++ b/back/src/Enum/EnvironmentVariable.ts @@ -3,7 +3,6 @@ const GROUP_RADIUS = process.env.GROUP_RADIUS ? Number(process.env.GROUP_RADIUS) const ALLOW_ARTILLERY = process.env.ALLOW_ARTILLERY ? process.env.ALLOW_ARTILLERY == 'true' : false; const ADMIN_API_URL = process.env.ADMIN_API_URL || ''; const ADMIN_API_TOKEN = process.env.ADMIN_API_TOKEN || 'myapitoken'; -const MAX_USERS_PER_ROOM = parseInt(process.env.MAX_USERS_PER_ROOM || '') || 600; const CPU_OVERHEAT_THRESHOLD = Number(process.env.CPU_OVERHEAT_THRESHOLD) || 80; const JITSI_URL : string|undefined = (process.env.JITSI_URL === '') ? undefined : process.env.JITSI_URL; const JITSI_ISS = process.env.JITSI_ISS || ''; @@ -19,7 +18,6 @@ export { ADMIN_API_TOKEN, HTTP_PORT, GRPC_PORT, - MAX_USERS_PER_ROOM, GROUP_RADIUS, ALLOW_ARTILLERY, CPU_OVERHEAT_THRESHOLD, diff --git a/back/src/Model/GameRoom.ts b/back/src/Model/GameRoom.ts index 41c215ad..6a592ed0 100644 --- a/back/src/Model/GameRoom.ts +++ b/back/src/Model/GameRoom.ts @@ -7,7 +7,6 @@ import {PositionNotifier} from "./PositionNotifier"; import {Movable} from "_Model/Movable"; import {extractDataFromPrivateRoomId, extractRoomSlugPublicRoomId, isRoomAnonymous} from "./RoomIdentifier"; import {arrayIntersect} from "../Services/ArrayHelper"; -import {MAX_USERS_PER_ROOM} from "../Enum/EnvironmentVariable"; import {JoinRoomMessage} from "../Messages/generated/messages_pb"; import {ProtobufUtils} from "../Model/Websocket/ProtobufUtils"; import {ZoneSocket} from "src/RoomManager"; @@ -116,8 +115,6 @@ export class GameRoom { this.nextUserId++; this.users.set(user.id, user); this.usersByUuid.set(user.uuid, user); - // Let's call update position to trigger the join / leave room - //this.updatePosition(socket, userPosition); this.updateUserGroup(user); // Notify admins @@ -149,10 +146,6 @@ export class GameRoom { } } - get isFull(): boolean { - return this.users.size >= MAX_USERS_PER_ROOM; - } - public isEmpty(): boolean { return this.users.size === 0 && this.admins.size === 0; } diff --git a/back/src/RoomManager.ts b/back/src/RoomManager.ts index bc3e5313..256c904e 100644 --- a/back/src/RoomManager.ts +++ b/back/src/RoomManager.ts @@ -2,7 +2,8 @@ import {IRoomManagerServer} from "./Messages/generated/messages_grpc_pb"; import { AdminGlobalMessage, AdminMessage, - AdminPusherToBackMessage, + AdminPusherToBackMessage, + AdminRoomMessage, BanMessage, EmptyMessage, ItemEventMessage, @@ -51,12 +52,8 @@ const roomManager: IRoomManagerServer = { } else { if (message.hasJoinroommessage()) { throw new Error('Cannot call JoinRoomMessage twice!'); - /*} else if (message.hasViewportmessage()) { - socketManager.handleViewport(client, message.getViewportmessage() as ViewportMessage);*/ } else if (message.hasUsermovesmessage()) { socketManager.handleUserMovesMessage(room, user, message.getUsermovesmessage() as UserMovesMessage); - /*} else if (message.hasSetplayerdetailsmessage()) { - socketManager.handleSetPlayerDetails(client, message.getSetplayerdetailsmessage() as SetPlayerDetailsMessage);*/ } else if (message.hasSilentmessage()) { socketManager.handleSilentMessage(room, user, message.getSilentmessage() as SilentMessage); } else if (message.hasItemeventmessage()) { @@ -67,8 +64,6 @@ const roomManager: IRoomManagerServer = { socketManager.emitScreenSharing(room, user, message.getWebrtcscreensharingsignaltoservermessage() as WebRtcSignalToServerMessage); } else if (message.hasPlayglobalmessage()) { socketManager.emitPlayGlobalMessage(room, message.getPlayglobalmessage() as PlayGlobalMessage); - /*} else if (message.hasReportplayermessage()){ - socketManager.handleReportMessage(client, message.getReportplayermessage() as ReportPlayerMessage);*/ } else if (message.hasQueryjitsijwtmessage()){ socketManager.handleQueryJitsiJwtMessage(user, message.getQueryjitsijwtmessage() as QueryJitsiJwtMessage); }else if (message.hasSendusermessage()) { @@ -119,10 +114,7 @@ const roomManager: IRoomManagerServer = { socketManager.removeZoneListener(call, zoneMessage.getRoomid(), zoneMessage.getX(), zoneMessage.getY()); call.end(); }) - - /*call.on('finish', () => { - debug('listenZone finish'); - })*/ + call.on('close', () => { debug('listenZone connection closed'); socketManager.removeZoneListener(call, zoneMessage.getRoomid(), zoneMessage.getX(), zoneMessage.getY()); @@ -150,26 +142,6 @@ const roomManager: IRoomManagerServer = { } else { throw new Error('The first message sent MUST be of type JoinRoomMessage'); } - } else { - /*if (message.hasJoinroommessage()) { - throw new Error('Cannot call JoinRoomMessage twice!'); - } else if (message.hasUsermovesmessage()) { - socketManager.handleUserMovesMessage(room, user, message.getUsermovesmessage() as UserMovesMessage); - } else if (message.hasSilentmessage()) { - socketManager.handleSilentMessage(room, user, message.getSilentmessage() as SilentMessage); - } else if (message.hasItemeventmessage()) { - socketManager.handleItemEvent(room, user, message.getItemeventmessage() as ItemEventMessage); - } else if (message.hasWebrtcsignaltoservermessage()) { - socketManager.emitVideo(room, user, message.getWebrtcsignaltoservermessage() as WebRtcSignalToServerMessage); - } else if (message.hasWebrtcscreensharingsignaltoservermessage()) { - socketManager.emitScreenSharing(room, user, message.getWebrtcscreensharingsignaltoservermessage() as WebRtcSignalToServerMessage); - } else if (message.hasPlayglobalmessage()) { - socketManager.emitPlayGlobalMessage(room, message.getPlayglobalmessage() as PlayGlobalMessage); - } else if (message.hasQueryjitsijwtmessage()){ - socketManager.handleQueryJitsiJwtMessage(user, message.getQueryjitsijwtmessage() as QueryJitsiJwtMessage); - } else { - throw new Error('Unhandled message type'); - }*/ } } catch (e) { emitError(call, e); @@ -208,6 +180,10 @@ const roomManager: IRoomManagerServer = { callback(null, new EmptyMessage()); }, + sendAdminMessageToRoom(call: ServerUnaryCall, callback: sendUnaryData): void { + socketManager.sendAdminRoomMessage(call.request.getRoomid(), call.request.getMessage()); + callback(null, new EmptyMessage()); + }, }; export {roomManager}; diff --git a/back/src/Services/AdminApi.ts b/back/src/Services/AdminApi.ts index 3e2dd3e0..ef969a76 100644 --- a/back/src/Services/AdminApi.ts +++ b/back/src/Services/AdminApi.ts @@ -1,6 +1,5 @@ import {ADMIN_API_TOKEN, ADMIN_API_URL} from "../Enum/EnvironmentVariable"; import Axios from "axios"; -import {v4} from "uuid"; export interface AdminApiData { organizationSlug: string @@ -21,13 +20,6 @@ export interface CharacterTexture { rights: string } -export interface FetchMemberDataByUuidResponse { - uuid: string; - tags: string[]; - textures: CharacterTexture[]; - messages: unknown[]; -} - class AdminApi { async fetchMapDetails(organizationSlug: string, worldSlug: string, roomSlug: string|undefined): Promise { @@ -52,65 +44,6 @@ class AdminApi { ) return res.data; } - - async fetchMemberDataByUuid(uuid: string): Promise { - if (!ADMIN_API_URL) { - return Promise.reject('No admin backoffice set!'); - } - try { - const res = await Axios.get(ADMIN_API_URL+'/api/membership/'+uuid, - { headers: {"Authorization" : `${ADMIN_API_TOKEN}`} } - ) - return res.data; - } catch (e) { - if (e?.response?.status == 404) { - // If we get an HTTP 404, the token is invalid. Let's perform an anonymous login! - console.warn('Cannot find user with uuid "'+uuid+'". Performing an anonymous login instead.'); - return { - uuid: v4(), - tags: [], - textures: [], - messages: [], - } - } else { - throw e; - } - } - } - - async fetchMemberDataByToken(organizationMemberToken: string): Promise { - if (!ADMIN_API_URL) { - return Promise.reject('No admin backoffice set!'); - } - //todo: this call can fail if the corresponding world is not activated or if the token is invalid. Handle that case. - const res = await Axios.get(ADMIN_API_URL+'/api/login-url/'+organizationMemberToken, - { headers: {"Authorization" : `${ADMIN_API_TOKEN}`} } - ) - return res.data; - } - - async fetchCheckUserByToken(organizationMemberToken: string): Promise { - if (!ADMIN_API_URL) { - return Promise.reject('No admin backoffice set!'); - } - //todo: this call can fail if the corresponding world is not activated or if the token is invalid. Handle that case. - const res = await Axios.get(ADMIN_API_URL+'/api/check-user/'+organizationMemberToken, - { headers: {"Authorization" : `${ADMIN_API_TOKEN}`} } - ) - return res.data; - } - - reportPlayer(reportedUserUuid: string, reportedUserComment: string, reporterUserUuid: string, reportWorldSlug: string) { - return Axios.post(`${ADMIN_API_URL}/api/report`, { - reportedUserUuid, - reportedUserComment, - reporterUserUuid, - reportWorldSlug, - }, - { - headers: {"Authorization": `${ADMIN_API_TOKEN}`} - }); - } } export const adminApi = new AdminApi(); diff --git a/back/src/Services/SocketManager.ts b/back/src/Services/SocketManager.ts index 302900ac..6317a46d 100644 --- a/back/src/Services/SocketManager.ts +++ b/back/src/Services/SocketManager.ts @@ -1,5 +1,4 @@ import {GameRoom} from "../Model/GameRoom"; -import {CharacterLayer} from "_Model/Websocket/CharacterLayer"; import { ItemEventMessage, ItemStateMessage, @@ -22,7 +21,11 @@ import { Zone as ProtoZone, BatchToPusherMessage, SubToPusherMessage, - UserJoinedZoneMessage, GroupUpdateZoneMessage, GroupLeftZoneMessage, UserLeftZoneMessage, BanUserMessage + UserJoinedZoneMessage, + GroupUpdateZoneMessage, + GroupLeftZoneMessage, + UserLeftZoneMessage, + BanUserMessage, } from "../Messages/generated/messages_pb"; import {User, UserSocket} from "../Model/User"; import {ProtobufUtils} from "../Model/Websocket/ProtobufUtils"; @@ -51,18 +54,6 @@ import crypto from "crypto"; const debug = Debug('sockermanager'); -interface AdminSocketRoomsList { - [index: string]: number; -} -interface AdminSocketUsersList { - [index: string]: boolean; -} - -export interface AdminSocketData { - rooms: AdminSocketRoomsList, - users: AdminSocketUsersList, -} - function emitZoneMessage(subMessage: SubToPusherMessage, socket: ZoneSocket): void { // TODO: should we batch those every 100ms? const batchMessage = new BatchToPusherMessage(); @@ -83,68 +74,13 @@ export class SocketManager { }); } - /*getAdminSocketDataFor(roomId:string): AdminSocketData { - const data:AdminSocketData = { - rooms: {}, - users: {}, - } - const room = this.rooms.get(roomId); - if (room === undefined) { - return data; - } - const users = room.getUsers(); - data.rooms[roomId] = users.size; - users.forEach(user => { - data.users[user.uuid] = true - }) - return data; - }*/ - public async handleJoinRoom(socket: UserSocket, joinRoomMessage: JoinRoomMessage): Promise<{ room: GameRoom; user: User }> { - /*const positionMessage = joinRoomMessage.getPositionmessage(); - if (positionMessage === undefined) { - // TODO: send error message? - throw new Error('Empty pointMessage found in JoinRoomMessage'); - }*/ - - //const position = ProtobufUtils.toPointInterface(positionMessage); - //const viewport = client.viewport; - - //this.sockets.set(client.userId, client); //todo: should this be at the end of the function? //join new previous room const {room, user} = await this.joinRoom(socket, joinRoomMessage); - //const things = room.setViewport(client, viewport); - const roomJoinedMessage = new RoomJoinedMessage(); roomJoinedMessage.setTagList(joinRoomMessage.getTagList()); - /*for (const thing of things) { - if (thing instanceof User) { - const player: ExSocketInterface|undefined = this.sockets.get(thing.id); - if (player === undefined) { - console.warn('Something went wrong. The World contains a user "'+thing.id+"' but this user does not exist in the sockets list!"); - continue; - } - - const userJoinedMessage = new UserJoinedMessage(); - userJoinedMessage.setUserid(thing.id); - userJoinedMessage.setName(player.name); - userJoinedMessage.setCharacterlayersList(ProtobufUtils.toCharacterLayerMessages(player.characterLayers)); - userJoinedMessage.setPosition(ProtobufUtils.toPositionMessage(player.position)); - - roomJoinedMessage.addUser(userJoinedMessage); - roomJoinedMessage.setTagList(joinRoomMessage.getTagList()); - } else if (thing instanceof Group) { - const groupUpdateMessage = new GroupUpdateMessage(); - groupUpdateMessage.setGroupid(thing.getId()); - groupUpdateMessage.setPosition(ProtobufUtils.toPointMessage(thing.getPosition())); - - roomJoinedMessage.addGroup(groupUpdateMessage); - } else { - console.error("Unexpected type for Movable returned by setViewport"); - } - }*/ for (const [itemId, item] of room.getItemsState().entries()) { const itemStateMessage = new ItemStateMessage(); @@ -158,8 +94,6 @@ export class SocketManager { const serverToClientMessage = new ServerToClientMessage(); serverToClientMessage.setRoomjoinedmessage(roomJoinedMessage); - - //user.socket.write(serverToClientMessage); console.log('SENDING MESSAGE roomJoinedMessage'); socket.write(serverToClientMessage); @@ -168,13 +102,6 @@ export class SocketManager { user }; - /*const serverToClientMessage = new ServerToClientMessage(); - serverToClientMessage.setRoomjoinedmessage(roomJoinedMessage); - - if (!client.disconnecting) { - client.send(serverToClientMessage.serializeBinary().buffer, true); - }*/ - } handleUserMovesMessage(room: GameRoom, user: User, userMovesMessage: UserMovesMessage) { @@ -693,33 +620,6 @@ export class SocketManager { }, 10000); } - /** - * Merges the characterLayers received from the front (as an array of string) with the custom textures from the back. - */ - static mergeCharacterLayersAndCustomTextures(characterLayers: string[], memberTextures: CharacterTexture[]): CharacterLayer[] { - const characterLayerObjs: CharacterLayer[] = []; - for (const characterLayer of characterLayers) { - if (characterLayer.startsWith('customCharacterTexture')) { - const customCharacterLayerId: number = +characterLayer.substr(22); - for (const memberTexture of memberTextures) { - if (memberTexture.id == customCharacterLayerId) { - characterLayerObjs.push({ - name: characterLayer, - url: memberTexture.url - }) - break; - } - } - } else { - characterLayerObjs.push({ - name: characterLayer, - url: undefined - }) - } - } - return characterLayerObjs; - } - public addZoneListener(call: ZoneSocket, roomId: string, x: number, y: number): void { const room = this.rooms.get(roomId); if (!room) { @@ -773,11 +673,6 @@ export class SocketManager { public async handleJoinAdminRoom(admin: Admin, roomId: string): Promise { const room = await socketManager.getOrCreateRoom(roomId); - // Dispatch groups position to newly connected user - /*world.getGroups().forEach((group: Group) => { - this.emitCreateUpdateGroupEvent(socket, group); - });*/ - room.adminJoin(admin); return room; @@ -807,7 +702,7 @@ export class SocketManager { const sendUserMessage = new SendUserMessage(); sendUserMessage.setMessage(message); - sendUserMessage.setType('ban'); + sendUserMessage.setType('ban'); //todo: is the type correct? const serverToClientMessage = new ServerToClientMessage(); serverToClientMessage.setSendusermessage(sendUserMessage); @@ -842,6 +737,27 @@ export class SocketManager { recipient.socket.write(serverToClientMessage); recipient.socket.end(); } + + + sendAdminRoomMessage(roomId: string, message: string) { + const room = this.rooms.get(roomId); + if (!room) { + //todo: this should cause the http call to return a 500 + console.error("In sendAdminRoomMessage, could not find room with id '" + roomId + "'. Maybe the room was closed a few milliseconds ago and there was a race condition?"); + return; + } + + room.getUsers().forEach((recipient) => { + const sendUserMessage = new SendUserMessage(); + sendUserMessage.setMessage(message); + sendUserMessage.setType('message'); + + const clientMessage = new ServerToClientMessage(); + clientMessage.setSendusermessage(sendUserMessage); + + recipient.socket.write(clientMessage); + }); + } } export const socketManager = new SocketManager(); diff --git a/contrib/docker/docker-compose.prod.yaml b/contrib/docker/docker-compose.prod.yaml index 22860748..c726ba84 100644 --- a/contrib/docker/docker-compose.prod.yaml +++ b/contrib/docker/docker-compose.prod.yaml @@ -10,7 +10,7 @@ services: - --entrypoints.web.http.redirections.entryPoint.to=websecure - --entrypoints.web.http.redirections.entryPoint.scheme=https - --entryPoints.websecure.address=:443 - - --certificatesresolvers.myresolver.acme.email=d.negrier@thecodingmachine.com + - --certificatesresolvers.myresolver.acme.email=${ACME_EMAIL} - --certificatesresolvers.myresolver.acme.storage=/acme.json # used during the challenge - --certificatesresolvers.myresolver.acme.httpchallenge.entrypoint=web diff --git a/deeployer.libsonnet b/deeployer.libsonnet index 5093c86a..07f5f491 100644 --- a/deeployer.libsonnet +++ b/deeployer.libsonnet @@ -4,7 +4,7 @@ local tag = namespace, local url = if namespace == "master" then "workadventu.re" else namespace+".workadventure.test.thecodingmachine.com", // develop branch does not use admin because of issue with SSL certificate of admin as of now. - local adminUrl = if namespace == "master" /*|| namespace == "develop"*/ || std.startsWith(namespace, "admin") then "https://"+url else null, + local adminUrl = if namespace == "master" || namespace == "develop" || std.startsWith(namespace, "admin") then "https://"+url else null, "$schema": "https://raw.githubusercontent.com/thecodingmachine/deeployer/master/deeployer.schema.json", "version": "1.0", "containers": { @@ -23,9 +23,12 @@ "JITSI_URL": env.JITSI_URL, "SECRET_JITSI_KEY": env.SECRET_JITSI_KEY, "TURN_STATIC_AUTH_SECRET": env.TURN_STATIC_AUTH_SECRET, - } + if adminUrl != null then { + } + (if adminUrl != null then { "ADMIN_API_URL": adminUrl, - } else {} + } else {}) + if namespace != "master" then { + // Absolutely ugly WorkAround to circumvent broken certificates on the K8S test cluster. Don't do this in production kids! + "NODE_TLS_REJECT_UNAUTHORIZED": "0" + } }, "back2": { "image": "thecodingmachine/workadventure-back:"+tag, @@ -42,9 +45,12 @@ "JITSI_URL": env.JITSI_URL, "SECRET_JITSI_KEY": env.SECRET_JITSI_KEY, "TURN_STATIC_AUTH_SECRET": env.TURN_STATIC_AUTH_SECRET, - } + if adminUrl != null then { + } + (if adminUrl != null then { "ADMIN_API_URL": adminUrl, - } else {} + } else {}) + if namespace != "master" then { + // Absolutely ugly WorkAround to circumvent broken certificates on the K8S test cluster. Don't do this in production kids! + "NODE_TLS_REJECT_UNAUTHORIZED": "0" + } }, "pusher": { "replicas": 2, @@ -61,9 +67,12 @@ "JITSI_URL": env.JITSI_URL, "API_URL": "back1:50051,back2:50051", "SECRET_JITSI_KEY": env.SECRET_JITSI_KEY, - } + if adminUrl != null then { + } + (if adminUrl != null then { "ADMIN_API_URL": adminUrl, - } else {} + } else {}) + if namespace != "master" then { + // Absolutely ugly WorkAround to circumvent broken certificates on the K8S test cluster. Don't do this in production kids! + "NODE_TLS_REJECT_UNAUTHORIZED": "0" + } }, "front": { "image": "thecodingmachine/workadventure-front:"+tag, diff --git a/front/dist/.gitignore b/front/dist/.gitignore index 64233a9e..05c474ec 100644 --- a/front/dist/.gitignore +++ b/front/dist/.gitignore @@ -1 +1,3 @@ -index.html \ No newline at end of file +index.html +index.tmpl.html.tmp +style.*.css \ No newline at end of file diff --git a/front/dist/index.tmpl.html b/front/dist/index.tmpl.html index a2b44788..eeb8bb5b 100644 --- a/front/dist/index.tmpl.html +++ b/front/dist/index.tmpl.html @@ -29,7 +29,6 @@ - WorkAdventure @@ -66,98 +65,54 @@ + +
- +
- -
- -
- -
- - + +