diff --git a/apps/client/src/externals.ts b/apps/client/src/externals.ts
index ec771ba29..1fd3e4618 100644
--- a/apps/client/src/externals.ts
+++ b/apps/client/src/externals.ts
@@ -22,7 +22,25 @@ export const isOntimeCloud = Boolean(import.meta.env.VITE_IS_CLOUD);
const socketProtocol = window.location.protocol === 'https:' ? 'wss' : 'ws';
// resolve port
-const STATIC_PORT = 4001;
+const STATIC_PORT = 4001; // this is used as a fallback port for development
export const serverPort = isProduction ? window.location.port : STATIC_PORT;
-export const serverURL = `${window.location.protocol}//${location.hostname}:${serverPort}`;
-export const websocketUrl = `${socketProtocol}://${location.hostname}:${serverPort}/ws`;
+export const baseURI = resolveBaseURI();
+export const serverURL = `${window.location.protocol}//${window.location.hostname}:${serverPort}${baseURI}`;
+export const websocketUrl = `${socketProtocol}://${window.location.hostname}:${serverPort}${baseURI}/ws`;
+
+/**
+ * Resolves a base URI for a client that is not at the root segment
+ * ie: https://cloud.getontime.com/client-hash/timer
+ * This is necessary for ontime cloud and should otherwise not affect the client
+ */
+function resolveBaseURI() {
+ if (!isOntimeCloud) {
+ return '';
+ }
+ const [_, base, location] = window.location.pathname.split('/');
+ if (!location) {
+ return '';
+ }
+
+ return `/${base}`;
+}
diff --git a/apps/client/vite.config.js b/apps/client/vite.config.js
index d8e53fa85..25b2d5dc4 100644
--- a/apps/client/vite.config.js
+++ b/apps/client/vite.config.js
@@ -11,6 +11,7 @@ const sentryAuthToken = process.env.SENTRY_AUTH_TOKEN;
const isDev = process.env.NODE_ENV === 'local' || process.env.NODE_ENV === 'development';
export default defineConfig({
+ base: './', // Ontime cloud: we use relative paths to allow them to reference a dynamic base set at runtime
plugins: [
react(),
svgrPlugin(),
@@ -33,7 +34,7 @@ export default defineConfig({
}),
compression({
algorithm: 'brotliCompress',
- exclude: /\.(html)$/, // Exclude HTML files from compression so we can change the base property at runtime
+ exclude: /\.(html)$/, // Ontime cloud: Exclude HTML files from compression so we can change the base property at runtime
}),
],
server: {
diff --git a/apps/server/src/adapters/WebsocketAdapter.ts b/apps/server/src/adapters/WebsocketAdapter.ts
index 6fd5e7274..cc87be0de 100644
--- a/apps/server/src/adapters/WebsocketAdapter.ts
+++ b/apps/server/src/adapters/WebsocketAdapter.ts
@@ -47,8 +47,8 @@ export class SocketServer implements IAdapter {
this.wss = null;
}
- init(server: Server) {
- this.wss = new WebSocketServer({ path: '/ws', server, maxPayload: this.MAX_PAYLOAD });
+ init(server: Server, prefix?: string) {
+ this.wss = new WebSocketServer({ path: `${prefix}/ws`, server, maxPayload: this.MAX_PAYLOAD });
this.wss.on('connection', (ws) => {
const clientId = generateId();
diff --git a/apps/server/src/app.ts b/apps/server/src/app.ts
index 751808032..6f6cf9ac5 100644
--- a/apps/server/src/app.ts
+++ b/apps/server/src/app.ts
@@ -6,10 +6,10 @@ import expressStaticGzip from 'express-static-gzip';
import http, { type Server } from 'http';
import cors from 'cors';
import serverTiming from 'server-timing';
-import { extname, resolve } from 'path';
+import { extname } from 'node:path';
// import utils
-import { publicDir, srcDir } from './setup/index.js';
+import { publicDir, srcDir, srcFiles } from './setup/index.js';
import { environment, isProduction, updateRouterPrefix } from './externals.js';
import { ONTIME_VERSION } from './ONTIME_VERSION.js';
import { consoleSuccess, consoleHighlight, consoleError } from './utils/console.js';
@@ -53,8 +53,14 @@ if (!canLog) {
console.log(`Ontime public directory at ${publicDir.root} `);
}
-// calls an update to the client router prefix
-updateRouterPrefix();
+/**
+ * When running in Ontime cloud, the client is not at the root segment
+ * ie: https://cloud.getontime.com/client-hash/timer
+ * This means:
+ * - changing the base path in the index.html file
+ * - prepending all express routes with the given prefix
+ */
+const prefix = updateRouterPrefix();
// Create express APP
const app = express();
@@ -75,20 +81,20 @@ app.use(express.urlencoded({ extended: true }));
app.use(express.json({ limit: '1mb' }));
// Implement route endpoints
-app.use('/data', appRouter); // router for application data
-app.use('/api', integrationRouter); // router for integrations
+app.use(`${prefix}/data`, appRouter); // router for application data
+app.use(`${prefix}/api`, integrationRouter); // router for integrations
// serve static external files
-app.use('/external', express.static(publicDir.externalDir));
-app.use('/user', express.static(publicDir.userDir));
-
-// if the user reaches to the root, we show a 404
-app.use('/external', (req, res) => {
+app.use(`${prefix}/external`, express.static(publicDir.externalDir));
+app.use(`${prefix}/external`, (req, res) => {
+ // if the user reaches to the root, we show a 404
res.status(404).send(`${req.originalUrl} not found`);
});
+app.use(`${prefix}/user`, express.static(publicDir.userDir));
// serve static - react, in dev/test mode we fetch the React app from module
app.use(
+ prefix,
expressStaticGzip(srcDir.clientDir, {
enableBrotli: true,
orderPreference: ['br'],
@@ -111,8 +117,8 @@ app.use(
}),
);
-app.get('*', (_req, res) => {
- res.sendFile(resolve(srcDir.clientDir, 'index.html'));
+app.get(`${prefix}/*`, (_req, res) => {
+ res.sendFile(srcFiles.clientIndexHtml);
});
// Implement catch all
@@ -176,7 +182,7 @@ export const startServer = async (
const { serverPort } = getDataProvider().getSettings();
expressServer = http.createServer(app);
- socket.init(expressServer);
+ socket.init(expressServer, prefix);
/**
* Module initialises the services and provides initial payload for the store
@@ -221,10 +227,10 @@ export const startServer = async (
expressServer.listen(serverPort, '0.0.0.0', () => {
const nif = getNetworkInterfaces();
- consoleSuccess(`Local: http://localhost:${serverPort}/editor`);
+ consoleSuccess(`Local: http://localhost:${serverPort}${prefix}/editor`);
for (const key in nif) {
const address = nif[key].address;
- consoleSuccess(`Network: http://${address}:${serverPort}/editor`);
+ consoleSuccess(`Network: http://${address}:${serverPort}${prefix}/editor`);
}
});
diff --git a/apps/server/src/externals.ts b/apps/server/src/externals.ts
index 83f02ccb3..6bbbab2dd 100644
--- a/apps/server/src/externals.ts
+++ b/apps/server/src/externals.ts
@@ -3,7 +3,8 @@
*/
import { readFileSync, writeFileSync } from 'node:fs';
-import { resolve } from 'node:path';
+
+import { srcFiles } from './setup/index.js';
// =================================================
// resolve running environment
@@ -19,19 +20,20 @@ export const isProduction = isDocker || (env === 'production' && !isTest);
* This is only needed in the cloud environment where the client is not at the root segment
* ie: https://cloud.getontime.com/client-hash/timer
*/
-export function updateRouterPrefix(prefix: string | undefined = process.env.ROUTER_PREFIX) {
+export function updateRouterPrefix(prefix: string | undefined = process.env.ROUTER_PREFIX): string {
if (!prefix) {
- return;
+ return '';
}
- const indexFile = resolve('.', 'client', 'index.html');
try {
- const data = readFileSync(indexFile, { encoding: 'utf-8', flag: 'r' }).replace(
- //g,
- ``,
+ const data = readFileSync(srcFiles.clientIndexHtml, { encoding: 'utf-8', flag: 'r' }).replace(
+ '',
+ ``,
);
- writeFileSync(indexFile, data, { encoding: 'utf-8', flag: 'w' });
+ writeFileSync(srcFiles.clientIndexHtml, data, { encoding: 'utf-8', flag: 'w' });
} catch (_error) {
/** unhandled */
}
+
+ return `/${prefix}`;
}
diff --git a/apps/server/src/setup/index.ts b/apps/server/src/setup/index.ts
index 3a1c7c96e..0990d69c0 100644
--- a/apps/server/src/setup/index.ts
+++ b/apps/server/src/setup/index.ts
@@ -77,6 +77,8 @@ export const srcDir = {
} as const;
export const srcFiles = {
+ /** Path to start index.html */
+ clientIndexHtml: join(srcDir.clientDir, 'index.html'),
/** Path to bundled CSS */
cssOverride: join(srcDir.root, config.user, config.styles.directory, config.styles.filename),
/** Path to bundled external readme */