diff --git a/ansible/roles/tldraw-client-core/templates/tldraw-client-configmap.yml.j2 b/ansible/roles/tldraw-client-core/templates/tldraw-client-configmap.yml.j2 index c654cd81..03361093 100644 --- a/ansible/roles/tldraw-client-core/templates/tldraw-client-configmap.yml.j2 +++ b/ansible/roles/tldraw-client-core/templates/tldraw-client-configmap.yml.j2 @@ -9,4 +9,4 @@ data: NODE_ENV: "production" TZ: "Europe/Berlin" PORT: "3046" - TLDRAW_SERVER_URL: "wss://{{ DOMAIN }}/tldraw-server" + CONFIG_PATH: "{{ '/api/tldraw/config/public' if WITH_TLDRAW2 else '/api/v3/config/public' }}" diff --git a/nginx.conf.template b/nginx.conf.template index 77590fd8..d80be6dc 100644 --- a/nginx.conf.template +++ b/nginx.conf.template @@ -5,7 +5,7 @@ server { set $csp "default-src 'self'; connect-src 'self' data:; base-uri 'self'; script-src 'nonce-$request_id' 'strict-dynamic' https:; object-src 'none'; font-src 'self' data:; img-src 'self' data:; style-src 'self' 'unsafe-inline';"; location /tldraw-client-runtime.config.json { - return 200 '{ "tldrawServerURL" : "${TLDRAW_SERVER_URL}" }'; + return 200 '{ "CONFIG_PATH": "${CONFIG_PATH}" }'; add_header Content-Type application/json; } diff --git a/src/configuration/api/api.configuration.ts b/src/configuration/api/api.configuration.ts index 43500aba..b47ffcac 100644 --- a/src/configuration/api/api.configuration.ts +++ b/src/configuration/api/api.configuration.ts @@ -1,8 +1,49 @@ +// remove this code by BC-7906 +// set CONFIG_PATH to /api/tldraw/config/public +// remove line 7 and 8 in ngnix.conf.template +import { HttpStatusCode } from "../../types/StatusCodeEnums"; +import { setErrorData } from "../../utils/errorData"; +import { redirectToErrorPage } from "../../utils/redirectUtils"; + +const getConfigOptions = async (): Promise<{ + CONFIG_PATH: string; +}> => { + const connectionOptions = { + CONFIG_PATH: configApiUrl(), + }; + + if (import.meta.env.PROD) { + try { + const response = await fetch("/tldraw-client-runtime.config.json"); + + if (!response.ok) { + throw new Error(`${response.status} - ${response.statusText}`); + } + + const data: { CONFIG_PATH: string } = await response.json(); + connectionOptions.CONFIG_PATH = data.CONFIG_PATH; + } catch (error) { + setErrorData(HttpStatusCode.InternalServerError, "error.500"); + redirectToErrorPage(); + } + } + + return connectionOptions; +}; + +const configApiUrl = () => { + const configApiUrl = import.meta.env.VITE_SERVER_TLDRAW_2_ENABLED + ? `/api/tldraw/config/public` + : `/api/v3/config/public`; + + return configApiUrl; +}; + export const API = { FILE_UPLOAD: "/api/v3/file/upload/school/SCHOOLID/boardnodes/CONTEXTID", FILE_DELETE: "/api/v3/file/delete/FILERECORD_ID", FILE_RESTORE: "/api/v3/file/restore/FILERECORD_ID", LOGIN_REDIRECT: "/login?redirect=/tldraw?parentId=PARENTID", USER_DATA: `/api/v3/user/me`, - ENV_CONFIG: `/api/v3/config/public`, + CONFIG_PATH: await getConfigOptions().then((options) => options.CONFIG_PATH), }; diff --git a/src/hooks/useMultiplayerState.test.ts b/src/hooks/useMultiplayerState.test.ts index 720ac2e9..5dfac1b4 100644 --- a/src/hooks/useMultiplayerState.test.ts +++ b/src/hooks/useMultiplayerState.test.ts @@ -1,16 +1,16 @@ -import { renderHook, act } from "@testing-library/react"; +import { act, renderHook } from "@testing-library/react"; +import * as Tldraw from "@tldraw/tldraw"; import { TDAsset, TDBinding, TDShape, TDUser, - TldrawApp, TDUserStatus, + TldrawApp, } from "@tldraw/tldraw"; -import * as Tldraw from "@tldraw/tldraw"; -import { useMultiplayerState } from "./useMultiplayerState"; import { doc, room, undoManager } from "../stores/setup"; import { deleteAsset, handleAssets } from "../utils/handleAssets"; +import { useMultiplayerState } from "./useMultiplayerState"; vi.mock("@tldraw/tldraw", async () => { const tldraw = await vi.importActual("@tldraw/tldraw"); @@ -74,7 +74,7 @@ vi.mock("../stores/setup", () => ({ }, envs: { TLDRAW__ASSETS_ENABLED: true, - TLDRAW__ASSETS_MAX_SIZE: 1000000, + TLDRAW__ASSETS_MAX_SIZE_BYTES: 1000000, TLDRAW__ASSETS_ALLOWED_MIME_TYPES_LIST: ["image/png", "image/jpeg"], }, })); diff --git a/src/hooks/useMultiplayerState.ts b/src/hooks/useMultiplayerState.ts index 63e70ba6..531f593b 100644 --- a/src/hooks/useMultiplayerState.ts +++ b/src/hooks/useMultiplayerState.ts @@ -1,4 +1,4 @@ -import lodash from "lodash"; +import { Utils } from "@tldraw/core"; import { TDAsset, TDAssetType, @@ -11,36 +11,36 @@ import { TldrawApp, TldrawPatch, } from "@tldraw/tldraw"; +import { Vec } from "@tldraw/vec"; import { User } from "@y-presence/client"; +import lodash from "lodash"; import { useCallback, useEffect, useState } from "react"; import { toast } from "react-toastify"; import { doc, - room, + envs, + pauseSync, provider, + resumeSync, + room, undoManager, + user, yAssets, yBindings, yShapes, - user, - envs, - pauseSync, - resumeSync, } from "../stores/setup"; -import { STORAGE_SETTINGS_KEY } from "../utils/userSettings"; import { UserPresence } from "../types/UserPresence"; -import { - importAssetsToS3, - openFromFileSystem, -} from "../utils/boardImportUtils"; import { fileToBase64, fileToText, saveToFileSystem, } from "../utils/boardExportUtils"; +import { + importAssetsToS3, + openFromFileSystem, +} from "../utils/boardImportUtils"; import { uploadFileToStorage } from "../utils/fileUpload"; -import { getImageBlob } from "../utils/tldrawImageExportUtils"; -import { Utils } from "@tldraw/core"; +import { deleteAsset, handleAssets } from "../utils/handleAssets"; import { getImageSizeFromSrc, getVideoSizeFromSrc, @@ -49,8 +49,8 @@ import { openAssetsFromFileSystem, VIDEO_EXTENSIONS, } from "../utils/tldrawFileUploadUtils"; -import { Vec } from "@tldraw/vec"; -import { deleteAsset, handleAssets } from "../utils/handleAssets"; +import { getImageBlob } from "../utils/tldrawImageExportUtils"; +import { STORAGE_SETTINGS_KEY } from "../utils/userSettings"; declare const window: Window & { app: TldrawApp }; @@ -350,21 +350,21 @@ export function useMultiplayerState({ file: File, id: string, ): Promise => { - if (!envs!.TLDRAW__ASSETS_ENABLED) { + if (!envs.TLDRAW__ASSETS_ENABLED) { toast.info("Asset uploading is disabled"); return false; } - if (file.size > envs!.TLDRAW__ASSETS_MAX_SIZE) { + if (file.size > envs.TLDRAW__ASSETS_MAX_SIZE_BYTES) { const bytesInMb = 1048576; - const sizeInMb = envs!.TLDRAW__ASSETS_MAX_SIZE / bytesInMb; + const sizeInMb = envs.TLDRAW__ASSETS_MAX_SIZE_BYTES / bytesInMb; toast.info(`Asset is too big - max. ${sizeInMb}MB`); return false; } const isMimeTypeDisallowed = - envs!.TLDRAW__ASSETS_ALLOWED_MIME_TYPES_LIST && - !envs!.TLDRAW__ASSETS_ALLOWED_MIME_TYPES_LIST.includes(file.type); + envs.TLDRAW__ASSETS_ALLOWED_MIME_TYPES_LIST && + !envs.TLDRAW__ASSETS_ALLOWED_MIME_TYPES_LIST.includes(file.type); if (isMimeTypeDisallowed) { toast.info("Asset of this type is not allowed"); diff --git a/src/mapper/configuration.mapper.ts b/src/mapper/configuration.mapper.ts index 23af98ed..9f604704 100644 --- a/src/mapper/configuration.mapper.ts +++ b/src/mapper/configuration.mapper.ts @@ -1,17 +1,17 @@ -import { Envs } from "../types/Envs"; import { TypeGuard } from "../guards/type.guard"; +import { Envs } from "../types/Envs"; const checkEnvType = (obj: Record): void => { + TypeGuard.checkKeyAndValueExists(obj, "TLDRAW__WEBSOCKET_URL"); TypeGuard.checkKeyAndValueExists(obj, "FEATURE_TLDRAW_ENABLED"); TypeGuard.checkKeyAndValueExists(obj, "TLDRAW__ASSETS_ENABLED"); - TypeGuard.checkKeyAndValueExists(obj, "TLDRAW__ASSETS_MAX_SIZE"); + TypeGuard.checkKeyAndValueExists(obj, "TLDRAW__ASSETS_MAX_SIZE_BYTES"); TypeGuard.checkKeyAndValueExists( obj, "TLDRAW__ASSETS_ALLOWED_MIME_TYPES_LIST", ); TypeGuard.checkBoolean(obj.FEATURE_TLDRAW_ENABLED); - TypeGuard.checkBoolean(obj.FEATURE_TLDRAW_ENABLED); - TypeGuard.checkNumber(obj.TLDRAW__ASSETS_MAX_SIZE); + TypeGuard.checkNumber(obj.TLDRAW__ASSETS_MAX_SIZE_BYTES); TypeGuard.checkArray(obj.TLDRAW__ASSETS_ALLOWED_MIME_TYPES_LIST); }; @@ -27,9 +27,11 @@ export class ConfigurationMapper { const configuration = castToEnv(obj); const mappedConfiguration: Envs = { + TLDRAW__WEBSOCKET_URL: configuration.TLDRAW__WEBSOCKET_URL, FEATURE_TLDRAW_ENABLED: configuration.FEATURE_TLDRAW_ENABLED, TLDRAW__ASSETS_ENABLED: configuration.TLDRAW__ASSETS_ENABLED, - TLDRAW__ASSETS_MAX_SIZE: configuration.TLDRAW__ASSETS_MAX_SIZE, + TLDRAW__ASSETS_MAX_SIZE_BYTES: + configuration.TLDRAW__ASSETS_MAX_SIZE_BYTES, TLDRAW__ASSETS_ALLOWED_MIME_TYPES_LIST: configuration.TLDRAW__ASSETS_ALLOWED_MIME_TYPES_LIST, }; diff --git a/src/stores/setup.ts b/src/stores/setup.ts index a5ea58b0..f52ba485 100644 --- a/src/stores/setup.ts +++ b/src/stores/setup.ts @@ -1,25 +1,21 @@ import { TDAsset, TDBinding, TDShape } from "@tldraw/tldraw"; -import { Doc, Map, UndoManager } from "yjs"; -import { WebsocketProvider } from "y-websocket"; import { Room } from "@y-presence/client"; +import { WebsocketProvider } from "y-websocket"; +import { Doc, Map, UndoManager } from "yjs"; import { UserPresence } from "../types/UserPresence"; -import { getConnectionOptions, getParentId } from "../utils/connectionOptions"; import { getEnvs } from "../utils/envConfig"; -import { getUserData } from "../utils/userData"; +import { clearErrorData } from "../utils/errorData"; import { + getParentId, handleRedirectIfNotValid, redirectToNotFoundErrorPage, } from "../utils/redirectUtils"; -import { clearErrorData } from "../utils/errorData"; +import { getUserData } from "../utils/userData"; import { setDefaultState } from "../utils/userSettings"; clearErrorData(); -const [connectionOptions, envs, userResult] = await Promise.all([ - getConnectionOptions(), - getEnvs(), - getUserData(), -]); +const [envs, userResult] = await Promise.all([getEnvs(), getUserData()]); handleRedirectIfNotValid(userResult, envs); @@ -29,7 +25,7 @@ const user = userResult.user; const parentId = getParentId(); const doc = new Doc(); const provider = new WebsocketProvider( - connectionOptions.websocketUrl, + envs?.TLDRAW__WEBSOCKET_URL, parentId, doc, { @@ -67,16 +63,16 @@ const resumeSync = () => { }; export { + doc, envs, - user, parentId, - doc, + pauseSync, provider, + resumeSync, room, - yShapes, - yBindings, - yAssets, undoManager, - pauseSync, - resumeSync, + user, + yAssets, + yBindings, + yShapes, }; diff --git a/src/types/Envs.ts b/src/types/Envs.ts index fcb09ebe..84eb1c79 100644 --- a/src/types/Envs.ts +++ b/src/types/Envs.ts @@ -1,6 +1,7 @@ export type Envs = { FEATURE_TLDRAW_ENABLED: boolean; TLDRAW__ASSETS_ENABLED: boolean; - TLDRAW__ASSETS_MAX_SIZE: number; + TLDRAW__ASSETS_MAX_SIZE_BYTES: number; TLDRAW__ASSETS_ALLOWED_MIME_TYPES_LIST: string[]; + TLDRAW__WEBSOCKET_URL: string; }; diff --git a/src/utils/connectionOptions.ts b/src/utils/connectionOptions.ts deleted file mode 100644 index 4254c7f0..00000000 --- a/src/utils/connectionOptions.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { setErrorData } from "./errorData"; -import { redirectToErrorPage } from "./redirectUtils"; -import { HttpStatusCode } from "../types/StatusCodeEnums"; -import { validateId } from "./validator"; - -const getConnectionOptions = async (): Promise<{ - websocketUrl: string; -}> => { - const connectionOptions = { - websocketUrl: "ws://localhost:3345", - }; - - if (import.meta.env.PROD) { - try { - const response = await fetch("/tldraw-client-runtime.config.json"); - - if (!response.ok) { - throw new Error(`${response.status} - ${response.statusText}`); - } - - const data: { tldrawServerURL: string } = await response.json(); - connectionOptions.websocketUrl = data.tldrawServerURL; - } catch (error) { - console.error("Error fetching tldrawServerURL:", error); - setErrorData(HttpStatusCode.InternalServerError, "error.500"); - redirectToErrorPage(); - } - } - - return connectionOptions; -}; - -const getParentId = () => { - const urlParams = new URLSearchParams(window.location.search); - const parentId = urlParams.get("parentId") ?? ""; - - validateId(parentId); - - return parentId; -}; - -export { getConnectionOptions, getParentId }; diff --git a/src/utils/envConfig.ts b/src/utils/envConfig.ts index b7905fa6..b57808e7 100644 --- a/src/utils/envConfig.ts +++ b/src/utils/envConfig.ts @@ -1,15 +1,18 @@ -import { Envs } from "../types/Envs"; import { API } from "../configuration/api/api.configuration"; -import { ConfigurationMapper } from "../mapper/configuration.mapper"; import { HttpGuard } from "../guards/http.guard"; +import { ConfigurationMapper } from "../mapper/configuration.mapper"; +import { Envs } from "../types/Envs"; +import { HttpStatusCode } from "../types/StatusCodeEnums"; +import { setErrorData } from "./errorData"; +import { redirectToErrorPage } from "./redirectUtils"; // the try catch should not part of getEnvs, the place that use it must handle the errors // should be part of a store // Without loading the config the Promise.all should not be finished and proceed. -export const getEnvs = async (): Promise => { +export const getEnvs = async (): Promise => { try { // TODO: check order.. - const response = await fetch(API.ENV_CONFIG); + const response = await fetch(API.CONFIG_PATH); HttpGuard.checkStatusOk(response); const responseData = await response.json(); @@ -18,8 +21,25 @@ export const getEnvs = async (): Promise => { return configuration; } catch (error) { - // It should exists one place that execute the console.error in the application. A errorHandler. - // If we want to collect this informations to send it back to us, then we have currently no possibility to implement it. - console.error("Error fetching env config:", error); + if (import.meta.env.PROD) { + setErrorData(HttpStatusCode.InternalServerError, "error.500"); + redirectToErrorPage(); + throw error; + } else { + const configuration: Envs = { + TLDRAW__WEBSOCKET_URL: "ws://localhost:3345", + TLDRAW__ASSETS_ENABLED: true, + TLDRAW__ASSETS_MAX_SIZE_BYTES: 10485760, + TLDRAW__ASSETS_ALLOWED_MIME_TYPES_LIST: [ + "image/png", + "image/jpeg", + "image/gif", + "image/svg+xml", + ], + FEATURE_TLDRAW_ENABLED: true, + }; + + return configuration; + } } }; diff --git a/src/utils/redirectUtils.ts b/src/utils/redirectUtils.ts index b9d3b0c7..1b3c2400 100644 --- a/src/utils/redirectUtils.ts +++ b/src/utils/redirectUtils.ts @@ -1,9 +1,18 @@ -import { getParentId } from "./connectionOptions"; -import { UserResult } from "../types/User"; +import { API } from "../configuration/api/api.configuration"; import { Envs } from "../types/Envs"; -import { setErrorData } from "./errorData"; import { HttpStatusCode } from "../types/StatusCodeEnums"; -import { API } from "../configuration/api/api.configuration"; +import { UserResult } from "../types/User"; +import { setErrorData } from "./errorData"; +import { validateId } from "./validator"; + +const getParentId = () => { + const urlParams = new URLSearchParams(window.location.search); + const parentId = urlParams.get("parentId") ?? ""; + + validateId(parentId); + + return parentId; +}; const redirectToLoginPage = () => { const parentId = getParentId(); @@ -42,7 +51,7 @@ const handleRedirectIfNotValid = (userResult: UserResult, envs?: Envs) => { return; } - if (!envs!.FEATURE_TLDRAW_ENABLED) { + if (!envs.FEATURE_TLDRAW_ENABLED) { setErrorData(HttpStatusCode.Forbidden, "error.403"); redirectToErrorPage(); return; @@ -50,8 +59,9 @@ const handleRedirectIfNotValid = (userResult: UserResult, envs?: Envs) => { }; export { - redirectToLoginPage, + getParentId, + handleRedirectIfNotValid, redirectToErrorPage, + redirectToLoginPage, redirectToNotFoundErrorPage, - handleRedirectIfNotValid, }; diff --git a/vite.config.ts b/vite.config.ts index aa256f36..004b210e 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,6 +1,6 @@ /// -import { defineConfig, PluginOption } from "vite"; import react from "@vitejs/plugin-react"; +import { defineConfig, PluginOption } from "vite"; import topLevelAwait from "vite-plugin-top-level-await"; const noncePlugin = (placeholderName = "**CSP_NONCE**"): PluginOption => ({ @@ -35,6 +35,11 @@ export default defineConfig({ changeOrigin: true, secure: false, }, + "/api/tldraw": { + target: "http://localhost:3349", + changeOrigin: true, + secure: false, + }, }, }, test: {