diff --git a/front/Dockerfile b/front/Dockerfile index 80d525a9..96a7d6d7 100644 --- a/front/Dockerfile +++ b/front/Dockerfile @@ -1,23 +1,35 @@ FROM node:14.18.2-buster-slim@sha256:20bedf0c09de887379e59a41c04284974f5fb529cf0e13aab613473ce298da3d as builder -WORKDIR /usr/src + +WORKDIR /usr/src/messages COPY messages . RUN yarn install && yarn ts-proto -# we are rebuilding on each deploy to cope with the PUSHER_URL environment URL -FROM thecodingmachine/nodejs:14-apache +WORKDIR /usr/src/front +COPY front . -COPY --chown=docker:docker front . -COPY --from=builder --chown=docker:docker /usr/src/ts-proto-generated/protos /var/www/html/src/Messages/ts-proto-generated -RUN sed -i 's/import { Observable } from "rxjs";/import type { Observable } from "rxjs";/g' /var/www/html/src/Messages/ts-proto-generated/messages.ts -COPY --from=builder --chown=docker:docker /usr/src/JsonMessages /var/www/html/src/Messages/JsonMessages +# move messages to front +RUN cp -r ../messages/ts-proto-generated/protos/* src/Messages/ts-proto-generated +RUN sed -i 's/import { Observable } from "rxjs";/import type { Observable } from "rxjs";/g' src/Messages/ts-proto-generated/messages.ts +RUN cp -r ../messages/JsonMessages src/Messages/JsonMessages + +RUN yarn install && yarn build # Removing the iframe.html file from the final image as this adds a XSS attack. # iframe.html is only in dev mode to circumvent a limitation RUN rm dist/iframe.html -RUN yarn install +FROM thecodingmachine/nodejs:14-apache + +COPY --from=builder --chown=docker:docker /usr/src/front/dist dist +COPY front/templater.sh . + +USER root +RUN DEBIAN_FRONTEND=noninteractive apt-get update \ + && apt-get install -y \ + gettext-base \ + && rm -rf /var/lib/apt/lists/* +USER docker -ENV NODE_ENV=production ENV STARTUP_COMMAND_0="./templater.sh" -ENV STARTUP_COMMAND_1="yarn run build" +ENV STARTUP_COMMAND_1="envsubst < dist/env-config.template.js > dist/env-config.js" ENV APACHE_DOCUMENT_ROOT=dist/ diff --git a/front/dist/.gitignore b/front/dist/.gitignore index 785f2eb9..4ffce093 100644 --- a/front/dist/.gitignore +++ b/front/dist/.gitignore @@ -1,4 +1,3 @@ index.html -index.tmpl.html.tmp /js/ style.*.css diff --git a/front/dist/index.tmpl.html b/front/dist/index.ejs similarity index 99% rename from front/dist/index.tmpl.html rename to front/dist/index.ejs index 3b43a5ef..29b8e6cb 100644 --- a/front/dist/index.tmpl.html +++ b/front/dist/index.ejs @@ -27,7 +27,7 @@ - + diff --git a/front/src/Enum/EnvironmentVariable.ts b/front/src/Enum/EnvironmentVariable.ts index 76b4c8af..91be4cd8 100644 --- a/front/src/Enum/EnvironmentVariable.ts +++ b/front/src/Enum/EnvironmentVariable.ts @@ -1,30 +1,46 @@ -const DEBUG_MODE: boolean = process.env.DEBUG_MODE == "true"; -const START_ROOM_URL: string = - process.env.START_ROOM_URL || "/_/global/maps.workadventure.localhost/Floor1/floor1.json"; -const PUSHER_URL = process.env.PUSHER_URL || "//pusher.workadventure.localhost"; -export const ADMIN_URL = process.env.ADMIN_URL || "//workadventu.re"; -const UPLOADER_URL = process.env.UPLOADER_URL || "//uploader.workadventure.localhost"; -const ICON_URL = process.env.ICON_URL || "//icon.workadventure.localhost"; -const STUN_SERVER: string = process.env.STUN_SERVER || "stun:stun.l.google.com:19302"; -const TURN_SERVER: string = process.env.TURN_SERVER || ""; -const SKIP_RENDER_OPTIMIZATIONS: boolean = process.env.SKIP_RENDER_OPTIMIZATIONS == "true"; -const DISABLE_NOTIFICATIONS: boolean = process.env.DISABLE_NOTIFICATIONS == "true"; -const TURN_USER: string = process.env.TURN_USER || ""; -const TURN_PASSWORD: string = process.env.TURN_PASSWORD || ""; -const JITSI_URL: string | undefined = process.env.JITSI_URL === "" ? undefined : process.env.JITSI_URL; -const JITSI_PRIVATE_MODE: boolean = process.env.JITSI_PRIVATE_MODE == "true"; +declare global { + interface Window { + env?: Record; + } +} + +const getEnv = (key: string): string | undefined => { + if (!window.env) { + return; + } + const value = window.env[key]; + if (value === "") { + return; + } + return value; +}; + +const DEBUG_MODE: boolean = getEnv("DEBUG_MODE") == "true"; +const START_ROOM_URL: string = getEnv("START_ROOM_URL") || "/_/global/maps.workadventure.localhost/Floor1/floor1.json"; +const PUSHER_URL = getEnv("PUSHER_URL") || "//pusher.workadventure.localhost"; +export const ADMIN_URL = getEnv("ADMIN_URL") || "//workadventu.re"; +const UPLOADER_URL = getEnv("UPLOADER_URL") || "//uploader.workadventure.localhost"; +const ICON_URL = getEnv("ICON_URL") || "//icon.workadventure.localhost"; +const STUN_SERVER: string = getEnv("STUN_SERVER") || "stun:stun.l.google.com:19302"; +const TURN_SERVER: string = getEnv("TURN_SERVER") || ""; +const SKIP_RENDER_OPTIMIZATIONS: boolean = getEnv("SKIP_RENDER_OPTIMIZATIONS") == "true"; +const DISABLE_NOTIFICATIONS: boolean = getEnv("DISABLE_NOTIFICATIONS") == "true"; +const TURN_USER: string = getEnv("TURN_USER") || ""; +const TURN_PASSWORD: string = getEnv("TURN_PASSWORD") || ""; +const JITSI_URL: string | undefined = getEnv("JITSI_URL") === "" ? undefined : getEnv("JITSI_URL"); +const JITSI_PRIVATE_MODE: boolean = getEnv("JITSI_PRIVATE_MODE") == "true"; const POSITION_DELAY = 200; // Wait 200ms between sending position events const MAX_EXTRAPOLATION_TIME = 100; // Extrapolate a maximum of 250ms if no new movement is sent by the player -export const MAX_USERNAME_LENGTH = parseInt(process.env.MAX_USERNAME_LENGTH || "") || 8; -export const MAX_PER_GROUP = parseInt(process.env.MAX_PER_GROUP || "4"); -export const DISPLAY_TERMS_OF_USE = process.env.DISPLAY_TERMS_OF_USE == "true"; -export const NODE_ENV = process.env.NODE_ENV || "development"; -export const CONTACT_URL = process.env.CONTACT_URL || undefined; -export const PROFILE_URL = process.env.PROFILE_URL || undefined; -export const POSTHOG_API_KEY: string = (process.env.POSTHOG_API_KEY as string) || ""; -export const POSTHOG_URL = process.env.POSTHOG_URL || undefined; -export const DISABLE_ANONYMOUS: boolean = process.env.DISABLE_ANONYMOUS === "true"; -export const OPID_LOGIN_SCREEN_PROVIDER = process.env.OPID_LOGIN_SCREEN_PROVIDER; +export const MAX_USERNAME_LENGTH = parseInt(getEnv("MAX_USERNAME_LENGTH") || "") || 8; +export const MAX_PER_GROUP = parseInt(getEnv("MAX_PER_GROUP") || "4"); +export const DISPLAY_TERMS_OF_USE = getEnv("DISPLAY_TERMS_OF_USE") == "true"; +export const NODE_ENV = getEnv("NODE_ENV") || "development"; +export const CONTACT_URL = getEnv("CONTACT_URL") || undefined; +export const PROFILE_URL = getEnv("PROFILE_URL") || undefined; +export const POSTHOG_API_KEY: string = (getEnv("POSTHOG_API_KEY") as string) || ""; +export const POSTHOG_URL = getEnv("POSTHOG_URL") || undefined; +export const DISABLE_ANONYMOUS: boolean = getEnv("DISABLE_ANONYMOUS") === "true"; +export const OPID_LOGIN_SCREEN_PROVIDER = getEnv("OPID_LOGIN_SCREEN_PROVIDER"); export const isMobile = (): boolean => window.innerWidth <= 800 || window.innerHeight <= 600; diff --git a/front/templater.sh b/front/templater.sh index a5a28c9c..e6a78e08 100755 --- a/front/templater.sh +++ b/front/templater.sh @@ -1,8 +1,7 @@ #!/usr/bin/env bash set -x set -o nounset errexit -template_file_index=dist/index.tmpl.html -generated_file_index=dist/index.tmpl.html.tmp +index_file=dist/index.html tmp_trackcodefile=/tmp/trackcode # To inject tracking code, you have two choices: @@ -21,6 +20,6 @@ if [[ "${GA_TRACKING_ID:-}" != "" || "${INSERT_ANALYTICS:-NO}" != "NO" ]]; then sed "s##${GA_TRACKING_ID:-}#g" "${ANALYTICS_CODE_PATH}" > "$tmp_trackcodefile" fi -echo "Templating ${generated_file_index} from ${template_file_index}" -sed "//r ${tmp_trackcodefile}" "${template_file_index}" > "${generated_file_index}" +echo "Templating ${index_file}" +sed --in-place "//r ${tmp_trackcodefile}" "${index_file}" rm "${tmp_trackcodefile}" diff --git a/front/webpack.config.ts b/front/webpack.config.ts index 77ad92bd..87ce8e0d 100644 --- a/front/webpack.config.ts +++ b/front/webpack.config.ts @@ -1,5 +1,6 @@ import type { Configuration } from "webpack"; import type WebpackDevServer from "webpack-dev-server"; +import fs from 'fs/promises'; import path from "path"; import webpack from "webpack"; import HtmlWebpackPlugin from "html-webpack-plugin"; @@ -33,6 +34,35 @@ module.exports = { disableDotRule: true, }, liveReload: process.env.LIVE_RELOAD != "0" && process.env.LIVE_RELOAD != "false", + before: (app) => { + let appConfigContent = ''; + const TEMPLATE_PATH = path.join(__dirname, 'dist', 'env-config.template.js'); + + function renderTemplateWithEnvVars(content: string): string { + let result = content; + const regex = /\$\{([a-zA-Z_]+[a-zA-Z0-9_]*?)\}/g; + + let matched: RegExpExecArray | null; + while ((matched = regex.exec(content))) { + result = result.replace(`\${${matched[1]}}`, process.env[matched[1]] || ''); + } + + return result; + } + + void (async () => { + const content = (await fs.readFile(TEMPLATE_PATH)).toString(); + appConfigContent = renderTemplateWithEnvVars(content); + })(); + + app.get('/env-config.js', (_, response) => { + response.setHeader('Content-Type', 'application/javascript; charset=utf-8'); + response.setHeader('Cache-Control', 'no-cache'); + response.setHeader('Content-Length', Buffer.byteLength(appConfigContent, 'utf8')); + + response.send(appConfigContent); + }); + }, }, module: { rules: [ @@ -168,7 +198,7 @@ module.exports = { }), new MiniCssExtractPlugin({ filename: "[name].[contenthash].css" }), new HtmlWebpackPlugin({ - template: "./dist/index.tmpl.html.tmp", + template: "./dist/index.ejs", minify: { collapseWhitespace: true, keepClosingSlash: true, @@ -184,32 +214,5 @@ module.exports = { Phaser: "phaser", }), new NodePolyfillPlugin(), - new webpack.EnvironmentPlugin({ - API_URL: null, - SKIP_RENDER_OPTIMIZATIONS: false, - DISABLE_NOTIFICATIONS: false, - PUSHER_URL: undefined, - UPLOADER_URL: null, - ADMIN_URL: null, - CONTACT_URL: null, - PROFILE_URL: null, - ICON_URL: null, - DEBUG_MODE: null, - STUN_SERVER: null, - TURN_SERVER: null, - TURN_USER: null, - TURN_PASSWORD: null, - JITSI_URL: null, - JITSI_PRIVATE_MODE: null, - START_ROOM_URL: null, - MAX_USERNAME_LENGTH: 8, - MAX_PER_GROUP: 4, - DISPLAY_TERMS_OF_USE: false, - POSTHOG_API_KEY: null, - POSTHOG_URL: null, - NODE_ENV: mode, - DISABLE_ANONYMOUS: false, - OPID_LOGIN_SCREEN_PROVIDER: null, - }), ], } as Configuration & WebpackDevServer.Configuration;