Compare commits

...

4 Commits

Author SHA1 Message Date
Carlos Valente fa7ec623f1 bump version to 3.9.4 2024-12-08 08:52:18 +01:00
Carlos Valente bfb9b51073 fix: secure protocol resolution 2024-12-08 08:52:18 +01:00
Carlos Valente 8bc3f5a56c Fix links (#1366)
* chore: remove unused code

* bump version to 3.9.3

* chore: use constant links

* refactor: build links

* chore: ignore ontime-data
2024-12-06 16:48:35 +01:00
Carlos Valente acf1afb5e9 fix: prevent overflow 2024-12-06 14:54:41 +01:00
19 changed files with 114 additions and 119 deletions
+1
View File
@@ -39,6 +39,7 @@ dist/
# docker utils # docker utils
ontime-db ontime-db
ontime-external/ ontime-external/
ontime-data/
# versioning file # versioning file
**/ONTIME_VERSION.js **/ONTIME_VERSION.js
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@getontime/cli", "name": "@getontime/cli",
"version": "3.9.2", "version": "3.9.4",
"author": "Carlos Valente", "author": "Carlos Valente",
"description": "Time keeping for live events", "description": "Time keeping for live events",
"repository": "https://github.com/cpvalente/ontime", "repository": "https://github.com/cpvalente/ontime",
+2 -2
View File
@@ -1,6 +1,6 @@
{ {
"name": "ontime-ui", "name": "ontime-ui",
"version": "3.9.2", "version": "3.9.4",
"private": true, "private": true,
"type": "module", "type": "module",
"dependencies": { "dependencies": {
@@ -39,7 +39,7 @@
"build": "vite build", "build": "vite build",
"build:local": "cross-env NODE_ENV=local vite build", "build:local": "cross-env NODE_ENV=local vite build",
"build:electron": "cross-env NODE_ENV=local vite build", "build:electron": "cross-env NODE_ENV=local vite build",
"build:docker": "cross-env VITE_IS_CLOUD=true vite build", "build:docker": "cross-env VITE_IS_DOCKER=true vite build",
"build:localdocker": "cross-env NODE_ENV=local vite build", "build:localdocker": "cross-env NODE_ENV=local vite build",
"lint": "eslint . --quiet", "lint": "eslint . --quiet",
"test": "vitest", "test": "vitest",
@@ -17,7 +17,7 @@ import { IoExpand } from '@react-icons/all-files/io5/IoExpand';
import { IoLockClosedOutline } from '@react-icons/all-files/io5/IoLockClosedOutline'; import { IoLockClosedOutline } from '@react-icons/all-files/io5/IoLockClosedOutline';
import { IoSwapVertical } from '@react-icons/all-files/io5/IoSwapVertical'; import { IoSwapVertical } from '@react-icons/all-files/io5/IoSwapVertical';
import { isLocalhost, serverPort } from '../../../externals'; import { isLocalhost } from '../../../externals';
import { navigatorConstants } from '../../../viewerConfig'; import { navigatorConstants } from '../../../viewerConfig';
import useClickOutside from '../../hooks/useClickOutside'; import useClickOutside from '../../hooks/useClickOutside';
import { useElectronEvent } from '../../hooks/useElectronEvent'; import { useElectronEvent } from '../../hooks/useElectronEvent';
@@ -25,7 +25,7 @@ import useInfo from '../../hooks-query/useInfo';
import { useClientStore } from '../../stores/clientStore'; import { useClientStore } from '../../stores/clientStore';
import { useViewOptionsStore } from '../../stores/viewOptions'; import { useViewOptionsStore } from '../../stores/viewOptions';
import { isKeyEnter } from '../../utils/keyEvent'; import { isKeyEnter } from '../../utils/keyEvent';
import { handleLinks, openLink } from '../../utils/linkUtils'; import { handleLinks, linkToOtherHost, openLink } from '../../utils/linkUtils';
import { cx } from '../../utils/styleUtils'; import { cx } from '../../utils/styleUtils';
import { RenameClientModal } from '../client-modal/RenameClientModal'; import { RenameClientModal } from '../client-modal/RenameClientModal';
import CopyTag from '../copy-tag/CopyTag'; import CopyTag from '../copy-tag/CopyTag';
@@ -154,7 +154,8 @@ function OtherAddresses(props: OtherAddressesProps) {
return null; return null;
} }
const address = `http://${nif.address}:${serverPort}${currentLocation}`; const address = linkToOtherHost(nif.address, currentLocation);
return ( return (
<CopyTag <CopyTag
key={nif.name} key={nif.name}
-30
View File
@@ -1,30 +0,0 @@
/**
* Returns hostname
* @type {string}
*/
export const host = window?.location?.host;
/**
* Open an external URLs: specifically for a electron / browser case
* If electron: ask main process to call a new browser window
* If browser: open in new tab
* @param url
*/
export function openLink(url) {
if (window.process?.type === 'renderer') {
window.ipcRenderer.send('send-to-link', url);
} else {
window.open(url);
}
}
/**
* Handles opening external links
* @param event
* @param location
*/
export function handleLinks(event, location) {
// we handle the link manually
event.preventDefault();
openLink(`http://${host}/${location}`);
}
+40
View File
@@ -0,0 +1,40 @@
import type { MouseEvent } from 'react';
import { baseURI, serverURL } from '../../externals';
/**
* Open an external URLs: specifically for a electron / browser case
* If electron: ask main process to call a new browser window
* If browser: open in new tab
* @param url
*/
export function openLink(url: string) {
if (window.process?.type === 'renderer') {
window.ipcRenderer.send('send-to-link', url);
} else {
window.open(url);
}
}
/**
* Handles opening external links
* @param event
* @param location
*/
export function handleLinks(event: MouseEvent, location: string) {
// we handle the link manually
event.preventDefault();
const destination = new URL(serverURL);
destination.pathname = baseURI ? `${baseURI}/${location}` : location;
openLink(destination.toString());
}
export function linkToOtherHost(host: string, path?: string) {
const destination = new URL(serverURL);
destination.hostname = host;
if (path) {
destination.pathname = baseURI ? `${baseURI}/${path}` : path;
}
return destination.toString();
}
+46 -17
View File
@@ -1,8 +1,9 @@
import { version } from '../../../package.json';
/** /**
* This file contains a list of constants that may need to be resolved at runtime * This file contains a list of constants that may need to be resolved at runtime
*/ */
import { version } from '../../../package.json';
export const githubUrl = 'https://www.github.com/cpvalente/ontime'; export const githubUrl = 'https://www.github.com/cpvalente/ontime';
export const apiRepoLatest = 'https://api.github.com/repos/cpvalente/ontime/releases/latest'; export const apiRepoLatest = 'https://api.github.com/repos/cpvalente/ontime/releases/latest';
export const websiteUrl = 'https://www.getontime.no'; export const websiteUrl = 'https://www.getontime.no';
@@ -16,31 +17,59 @@ export const appVersion = version;
export const isProduction = import.meta.env.MODE === 'production'; export const isProduction = import.meta.env.MODE === 'production';
export const isDev = !isProduction; export const isDev = !isProduction;
export const isLocalhost = window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1'; export const isLocalhost = window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1';
export const isOntimeCloud = Boolean(import.meta.env.VITE_IS_CLOUD); export const isDockerImage = Boolean(import.meta.env.VITE_IS_DOCKER);
export const isOntimeCloud = window.location.hostname.includes('cloud.getontime.no');
// resolve protocol // resolve entrypoint URLs
const socketProtocol = window.location.protocol === 'https:' ? 'wss' : 'ws';
// resolve port /**
const STATIC_PORT = 4001; // this is used as a fallback port for development * Resolve base
export const serverPort = isProduction ? window.location.port : STATIC_PORT; * @example '' for electron
* @example '/client-hash' for cloud
*/
export const baseURI = resolveBaseURI(); export const baseURI = resolveBaseURI();
export const serverURL = `${window.location.protocol}//${window.location.hostname}:${serverPort}${baseURI}`; export const serverURL = resolveUrl('http', '');
export const websocketUrl = `${socketProtocol}://${window.location.hostname}:${serverPort}${baseURI}/ws`; export const websocketUrl = resolveUrl('ws', 'ws');
function resolveUrl(protocol: 'http' | 'ws', path: string) {
const url = new URL(window.location.origin);
// generate ws url
if (protocol === 'ws') {
// ensure we remain in a secure context
const isSecure = window.location.protocol === 'https:';
url.protocol = isSecure ? 'wss' : 'ws';
}
// make path name relative to the base URI
url.pathname = baseURI ? `${baseURI}/${path}` : path;
// in development mode, we use the React port for UI, but need the requests to target the server
if (isDev) {
// this is used as a fallback port for development
url.port = '4001';
}
const result = url.toString();
// prevent trailing slash
return result.endsWith('/') ? result.slice(0, -1) : result;
}
/** /**
* Resolves a base URI for a client that is not at the root segment * Resolves a base URI for a client that is not at the root segment
* ie: https://cloud.getontime.com/client-hash/timer * ie: https://cloud.getontime.com/client-hash/timer
* This is necessary for ontime cloud and should otherwise not affect the client * This is necessary for ontime cloud and should otherwise not affect the client
*/ */
function resolveBaseURI() { function resolveBaseURI(): string {
if (!isOntimeCloud) { // in ontime cloud, the base tag is set by the server
return ''; const baseHref = document.querySelector('base')?.getAttribute('href');
} const base = baseHref ?? '';
const [_, base, location] = window.location.pathname.split('/');
if (!location) { // prevent a trailing slash from either an empty base or a base with a trailing slash
return ''; if (base.endsWith('/')) {
return base.slice(0, -1);
} }
return `/${base}`; return base;
} }
@@ -2,8 +2,8 @@ import { IoArrowUp } from '@react-icons/all-files/io5/IoArrowUp';
import CopyTag from '../../../../common/components/copy-tag/CopyTag'; import CopyTag from '../../../../common/components/copy-tag/CopyTag';
import useInfo from '../../../../common/hooks-query/useInfo'; import useInfo from '../../../../common/hooks-query/useInfo';
import { openLink } from '../../../../common/utils/linkUtils'; import { linkToOtherHost, openLink } from '../../../../common/utils/linkUtils';
import { isLocalhost, serverPort } from '../../../../externals'; import { isLocalhost } from '../../../../externals';
import style from './NetworkInterfaces.module.scss'; import style from './NetworkInterfaces.module.scss';
@@ -14,11 +14,11 @@ export default function InfoNif() {
return ( return (
<div className={style.interfaces}> <div className={style.interfaces}>
{data.networkInterfaces?.map((nif) => { {data.networkInterfaces.map((nif) => {
// interfaces outside localhost wont have access // interfaces outside localhost wont have access
if (nif.name === 'localhost' && !isLocalhost) return null; if (nif.name === 'localhost' && !isLocalhost) return null;
const address = linkToOtherHost(nif.address);
const address = `http://${nif.address}:${serverPort}`;
return ( return (
<CopyTag <CopyTag
key={nif.name} key={nif.name}
@@ -3,7 +3,7 @@ import { useEffect } from 'react';
import useScrollIntoView from '../../../../common/hooks/useScrollIntoView'; import useScrollIntoView from '../../../../common/hooks/useScrollIntoView';
import { usePing } from '../../../../common/hooks/useSocket'; import { usePing } from '../../../../common/hooks/useSocket';
import { socketSendJson } from '../../../../common/utils/socket'; import { socketSendJson } from '../../../../common/utils/socket';
import { isOntimeCloud } from '../../../../externals'; import { isDockerImage } from '../../../../externals';
import type { PanelBaseProps } from '../../panel-list/PanelList'; import type { PanelBaseProps } from '../../panel-list/PanelList';
import * as Panel from '../../panel-utils/PanelUtils'; import * as Panel from '../../panel-utils/PanelUtils';
import ClientControlPanel from '../client-control-panel/ClientControlPanel'; import ClientControlPanel from '../client-control-panel/ClientControlPanel';
@@ -19,7 +19,7 @@ export default function NetworkLogPanel({ location }: PanelBaseProps) {
<> <>
<Panel.Header>Network</Panel.Header> <Panel.Header>Network</Panel.Header>
<Panel.Section> <Panel.Section>
{isOntimeCloud && <OntimeCloudStats />} {isDockerImage && <OntimeCloudStats />}
<Panel.Paragraph>Ontime is streaming on the following network interfaces</Panel.Paragraph> <Panel.Paragraph>Ontime is streaming on the following network interfaces</Panel.Paragraph>
</Panel.Section> </Panel.Section>
<InfoNif /> <InfoNif />
@@ -6,6 +6,7 @@ import { useQueryClient } from '@tanstack/react-query';
import { PROJECT_LIST } from '../../../../common/api/constants'; import { PROJECT_LIST } from '../../../../common/api/constants';
import { createProject } from '../../../../common/api/db'; import { createProject } from '../../../../common/api/db';
import { maybeAxiosError } from '../../../../common/api/utils'; import { maybeAxiosError } from '../../../../common/api/utils';
import { documentationUrl, websiteUrl } from '../../../../externals';
import * as Panel from '../../panel-utils/PanelUtils'; import * as Panel from '../../panel-utils/PanelUtils';
import style from './ProjectPanel.module.scss'; import style from './ProjectPanel.module.scss';
@@ -118,7 +119,7 @@ export default function ProjectCreateForm(props: ProjectCreateFromProps) {
<Input <Input
variant='ontime-filled' variant='ontime-filled'
size='sm' size='sm'
placeholder='www.getontime.no' placeholder={websiteUrl}
autoComplete='off' autoComplete='off'
{...register('publicUrl')} {...register('publicUrl')}
/> />
@@ -140,7 +141,7 @@ export default function ProjectCreateForm(props: ProjectCreateFromProps) {
<Input <Input
variant='ontime-filled' variant='ontime-filled'
size='sm' size='sm'
placeholder='http://docs.getontime.no' placeholder={documentationUrl}
autoComplete='off' autoComplete='off'
{...register('backstageUrl')} {...register('backstageUrl')}
/> />
@@ -10,6 +10,7 @@ import { postProjectData, uploadProjectLogo } from '../../../../common/api/proje
import { maybeAxiosError } from '../../../../common/api/utils'; import { maybeAxiosError } from '../../../../common/api/utils';
import useProjectData from '../../../../common/hooks-query/useProjectData'; import useProjectData from '../../../../common/hooks-query/useProjectData';
import { validateLogo } from '../../../../common/utils/uploadUtils'; import { validateLogo } from '../../../../common/utils/uploadUtils';
import { documentationUrl, websiteUrl } from '../../../../externals';
import * as Panel from '../../panel-utils/PanelUtils'; import * as Panel from '../../panel-utils/PanelUtils';
import style from './ProjectPanel.module.scss'; import style from './ProjectPanel.module.scss';
@@ -203,7 +204,7 @@ export default function ProjectData() {
<Input <Input
variant='ontime-filled' variant='ontime-filled'
size='sm' size='sm'
placeholder='www.getontime.no' placeholder={websiteUrl}
autoComplete='off' autoComplete='off'
{...register('publicUrl')} {...register('publicUrl')}
/> />
@@ -225,7 +226,7 @@ export default function ProjectData() {
<Input <Input
variant='ontime-filled' variant='ontime-filled'
size='sm' size='sm'
placeholder='http://docs.getontime.no' placeholder={documentationUrl}
autoComplete='off' autoComplete='off'
{...register('backstageUrl')} {...register('backstageUrl')}
/> />
@@ -149,6 +149,7 @@ $orange-active: #f60;
grid-area: schedule; grid-area: schedule;
font-family: monospace; font-family: monospace;
margin-right: 2vw; margin-right: 2vw;
overflow: hidden;
} }
.onAir { .onAir {
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "ontime", "name": "ontime",
"version": "3.9.2", "version": "3.9.4",
"author": "Carlos Valente", "author": "Carlos Valente",
"description": "Time keeping for live events", "description": "Time keeping for live events",
"repository": "https://github.com/cpvalente/ontime", "repository": "https://github.com/cpvalente/ontime",
+1 -1
View File
@@ -2,7 +2,7 @@
"name": "ontime-server", "name": "ontime-server",
"type": "module", "type": "module",
"main": "src/index.ts", "main": "src/index.ts",
"version": "3.9.2", "version": "3.9.4",
"exports": "./src/index.js", "exports": "./src/index.js",
"dependencies": { "dependencies": {
"@googleapis/sheets": "^5.0.5", "@googleapis/sheets": "^5.0.5",
@@ -1,33 +0,0 @@
import { cleanURL } from '../url.js';
describe('url is correctly formatted', () => {
it('has no leading spaces', () => {
const test = ' http://testing';
const expected = 'http://testing';
expect(cleanURL(test)).toBe(expected);
});
it('has no trailing spaces', () => {
const test = 'http://testing ';
const expected = 'http://testing';
expect(cleanURL(test)).toBe(expected);
});
it('doesnt contain spaces', () => {
const test = 'http://t e s t i n g';
const expected = 'http://t%20e%20s%20t%20i%20n%20g';
expect(cleanURL(test)).toBe(expected);
});
it('only contains allowed characters', () => {
const test = 'http://<>[]{}|^';
const expected = 'http://';
expect(cleanURL(test)).toBe(expected);
});
it('begins with http://', () => {
const test = 'ontime.com';
const expected = 'http://ontime.com';
expect(cleanURL(test)).toBe(expected);
});
});
-20
View File
@@ -1,20 +0,0 @@
/**
* @description Cleans given url
* @param {string} url - URL to be checked
* @returns {string} Sanitized url
*/
export const cleanURL = (url: string): string => {
// trim whitespaces
let sanitised = url.trim();
// clear any whitespaces
sanitised = sanitised.split(' ').join('%20');
// contain only allowed characters
sanitised = sanitised.replace(/([@\s<>[\]{}|\\^])+/g, '');
// starts with http://
if (!sanitised.startsWith('http://')) sanitised = `http://${sanitised}`;
return sanitised;
};
+4
View File
@@ -4,6 +4,10 @@ const fileToUpload = 'e2e/tests/fixtures/test-db.json';
test('project file upload', async ({ page }) => { test('project file upload', async ({ page }) => {
await page.goto('http://localhost:4001/editor'); await page.goto('http://localhost:4001/editor');
// close the welcome modal if it is open
await page.keyboard.down('Escape');
await page.getByRole('button', { name: 'Edit' }).click(); await page.getByRole('button', { name: 'Edit' }).click();
await page.getByRole('button', { name: 'Clear rundown' }).click(); await page.getByRole('button', { name: 'Clear rundown' }).click();
await page.getByRole('button', { name: 'Delete all' }).click(); await page.getByRole('button', { name: 'Delete all' }).click();
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "ontime", "name": "ontime",
"version": "3.9.2", "version": "3.9.4",
"description": "Time keeping for live events", "description": "Time keeping for live events",
"keywords": [ "keywords": [
"ontime", "ontime",