From 5dabd0b2f81f9c6137a7fb88175b6c8d802411bb Mon Sep 17 00:00:00 2001 From: Lukas Hass Date: Fri, 21 Jan 2022 21:01:34 +0100 Subject: [PATCH 1/8] Pre-compile frontend and add environment config script --- front/Dockerfile | 32 +++++++---- front/dist/.gitignore | 1 - front/dist/{index.tmpl.html => index.ejs} | 2 +- front/src/Enum/EnvironmentVariable.ts | 66 ++++++++++++++--------- front/templater.sh | 7 ++- front/webpack.config.ts | 59 ++++++++++---------- 6 files changed, 98 insertions(+), 69 deletions(-) rename front/dist/{index.tmpl.html => index.ejs} (99%) 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; From 8c1f9e1ac73d426cd64145473e3bf60ac3f9766a Mon Sep 17 00:00:00 2001 From: Lukas Hass Date: Sun, 23 Jan 2022 15:17:53 +0100 Subject: [PATCH 2/8] Fix cp path --- front/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/front/Dockerfile b/front/Dockerfile index 96a7d6d7..4469bdaf 100644 --- a/front/Dockerfile +++ b/front/Dockerfile @@ -10,7 +10,7 @@ COPY front . # 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 cp -r ../messages/JsonMessages/* src/Messages/JsonMessages RUN yarn install && yarn build From 605765a86f69307e383f2463650ca3673d49cb2a Mon Sep 17 00:00:00 2001 From: Lukas Hass Date: Mon, 24 Jan 2022 11:26:49 +0100 Subject: [PATCH 3/8] Check in env config template --- front/dist/.gitignore | 1 + front/dist/env-config.template.js | 26 ++++++++++++++++++++++++++ 2 files changed, 27 insertions(+) create mode 100644 front/dist/env-config.template.js diff --git a/front/dist/.gitignore b/front/dist/.gitignore index 4ffce093..0561f7a5 100644 --- a/front/dist/.gitignore +++ b/front/dist/.gitignore @@ -1,3 +1,4 @@ index.html /js/ style.*.css +!env-config.template.js diff --git a/front/dist/env-config.template.js b/front/dist/env-config.template.js new file mode 100644 index 00000000..4a549c18 --- /dev/null +++ b/front/dist/env-config.template.js @@ -0,0 +1,26 @@ +window.env = { + SKIP_RENDER_OPTIMIZATIONS: '${SKIP_RENDER_OPTIMIZATIONS}', + DISABLE_NOTIFICATIONS: '${DISABLE_NOTIFICATIONS}', + PUSHER_URL: '${PUSHER_URL}', + UPLOADER_URL: '${UPLOADER_URL}', + ADMIN_URL: '${ADMIN_URL}', + CONTACT_URL: '${CONTACT_URL}', + PROFILE_URL: '${PROFILE_URL}', + ICON_URL: '${ICON_URL}', + DEBUG_MODE: '${DEBUG_MODE}', + STUN_SERVER: '${STUN_SERVER}', + TURN_SERVER: '${TURN_SERVER}', + TURN_USER: '${TURN_USER}', + TURN_PASSWORD: '${TURN_PASSWORD}', + JITSI_URL: '${JITSI_URL}', + JITSI_PRIVATE_MODE: '${JITSI_PRIVATE_MODE}', + START_ROOM_URL: '${START_ROOM_URL}', + MAX_USERNAME_LENGTH: '${MAX_USERNAME_LENGTH}', + MAX_PER_GROUP: '${MAX_PER_GROUP}', + DISPLAY_TERMS_OF_USE: '${DISPLAY_TERMS_OF_USE}', + POSTHOG_API_KEY: '${POSTHOG_API_KEY}', + POSTHOG_URL: '${POSTHOG_URL}', + NODE_ENV: '${NODE_ENV}', + DISABLE_ANONYMOUS: '${DISABLE_ANONYMOUS}', + OPID_LOGIN_SCREEN_PROVIDER: '${OPID_LOGIN_SCREEN_PROVIDER}', +}; From 00464f71449fe7ed237c441bbfc930f38c7d3855 Mon Sep 17 00:00:00 2001 From: Lukas Hass Date: Mon, 24 Jan 2022 11:29:26 +0100 Subject: [PATCH 4/8] Test for window existence on global object This fixes running tests in Jasmine where window not defined --- front/src/Enum/EnvironmentVariable.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/front/src/Enum/EnvironmentVariable.ts b/front/src/Enum/EnvironmentVariable.ts index 91be4cd8..df8ec7e6 100644 --- a/front/src/Enum/EnvironmentVariable.ts +++ b/front/src/Enum/EnvironmentVariable.ts @@ -5,10 +5,10 @@ declare global { } const getEnv = (key: string): string | undefined => { - if (!window.env) { + if (!global.window || !global.window.env) { return; } - const value = window.env[key]; + const value = global.window.env[key]; if (value === "") { return; } From 7863774dcacfa5f2546f22b44374348c29916486 Mon Sep 17 00:00:00 2001 From: Lukas Hass Date: Mon, 24 Jan 2022 11:53:01 +0100 Subject: [PATCH 5/8] Return value from process.env if it exists --- front/src/Enum/EnvironmentVariable.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/front/src/Enum/EnvironmentVariable.ts b/front/src/Enum/EnvironmentVariable.ts index df8ec7e6..83a820b3 100644 --- a/front/src/Enum/EnvironmentVariable.ts +++ b/front/src/Enum/EnvironmentVariable.ts @@ -5,8 +5,8 @@ declare global { } const getEnv = (key: string): string | undefined => { - if (!global.window || !global.window.env) { - return; + if (global.process?.env) { + return global.process.env[key]; } const value = global.window.env[key]; if (value === "") { From 6f247808745cb833591f701bf917696a57421811 Mon Sep 17 00:00:00 2001 From: Lukas Hass Date: Mon, 24 Jan 2022 11:54:00 +0100 Subject: [PATCH 6/8] Allow returning empty strings --- front/src/Enum/EnvironmentVariable.ts | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/front/src/Enum/EnvironmentVariable.ts b/front/src/Enum/EnvironmentVariable.ts index 83a820b3..e9b3bfc4 100644 --- a/front/src/Enum/EnvironmentVariable.ts +++ b/front/src/Enum/EnvironmentVariable.ts @@ -5,14 +5,13 @@ declare global { } const getEnv = (key: string): string | undefined => { + if (global.window?.env) { + return global.window.env[key]; + } if (global.process?.env) { return global.process.env[key]; } - const value = global.window.env[key]; - if (value === "") { - return; - } - return value; + return; }; const DEBUG_MODE: boolean = getEnv("DEBUG_MODE") == "true"; From 78a020576fdfbea60b53954d28caee9ccd9d6068 Mon Sep 17 00:00:00 2001 From: Lukas Hass Date: Tue, 25 Jan 2022 18:04:59 +0100 Subject: [PATCH 7/8] Add FALLBACK_LOCALE to config template --- front/dist/env-config.template.js | 1 + 1 file changed, 1 insertion(+) diff --git a/front/dist/env-config.template.js b/front/dist/env-config.template.js index 4a549c18..e672d7aa 100644 --- a/front/dist/env-config.template.js +++ b/front/dist/env-config.template.js @@ -23,4 +23,5 @@ window.env = { NODE_ENV: '${NODE_ENV}', DISABLE_ANONYMOUS: '${DISABLE_ANONYMOUS}', OPID_LOGIN_SCREEN_PROVIDER: '${OPID_LOGIN_SCREEN_PROVIDER}', + FALLBACK_LOCALE: '${FALLBACK_LOCALE}', }; From d18e9162b96003970a40a41eeefbbcd46ae7d91e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20N=C3=A9grier?= Date: Thu, 27 Jan 2022 15:51:23 +0100 Subject: [PATCH 8/8] Generating i18n files before build --- front/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/front/Dockerfile b/front/Dockerfile index 4469bdaf..14914ebf 100644 --- a/front/Dockerfile +++ b/front/Dockerfile @@ -12,7 +12,7 @@ RUN cp -r ../messages/ts-proto-generated/protos/* src/Messages/ts-proto-generate 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 +RUN yarn install && yarn run typesafe-i18n && 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