Compare commits

..

7 Commits

Author SHA1 Message Date
Carlos Valente 3c41b40c5f refactor: create fast tracks for cookie parsing 2025-08-18 22:00:16 +02:00
arc-alex 3b38756dda fix: use new token with scope 2025-08-18 22:00:16 +02:00
Carlos Valente 5a4494ba85 refactor: improve timers visibility 2025-08-18 22:00:16 +02:00
Carlos Valente 02ad852dd4 fix: external demo connects to hash 2025-08-18 22:00:16 +02:00
Carlos Valente 7ce1097826 fix: prevent reflow between entry editors 2025-08-18 22:00:16 +02:00
Carlos Valente 7ee0de5341 docs: improve development documentation 2025-08-18 22:00:16 +02:00
Carlos Valente 0db249871b bump version to 4.0.0-alpha.4 2025-08-18 22:00:16 +02:00
15 changed files with 138 additions and 51 deletions
+13 -19
View File
@@ -20,19 +20,20 @@ development.
Locally, we would need to run both the React client and the node.js server in development mode
From the project root, run the following commands
- __Install the project dependencies__ by running `pnpm i`
- __Run dev mode__ by running `turbo dev`
- __Run dev mode__ by running `pnpm turbo dev`
### Debugging backend
To debug backend code in Node.js:
The previous command will start the development servers for both the client, server and electron applications.
Typically in dev mode we prefer to start these in separate terminals to help with error tracking and debugging.
- Open two separate terminals and navigate to the `apps/client` and `apps/server` directories.
- In each terminal, run the command `pnpm dev` to start the development servers for both the client and server
applications.
- If you need to set breakpoints and inspect the code execution, enable Node.js inspect mode by
running `pnpm dev:inspect`.
We do that by creating two terminals an running
- __Run the React UI__ by running `pnpm turbo dev --filter=ontime-ui`
- __Run the nodejs server__ by running `pnpm turbo dev --filter=ontime-server`
- If you need to set breakpoints and inspect the code execution, enable Node.js inspect mode by running `pnpm turbo dev:inspect --filter=ontime-server`.
## TESTING
@@ -45,7 +46,7 @@ Generally we have 2 types of tests.
Unit tests are contained in mostly all the apps and packages (client, server and utils)
You can run unit tests by running `turbo run test:pipeline` from the project root.
You can run unit tests by running `pnpm turbo test:pipeline` from the project root.
This will run all tests and close test runner.
Alternatively you can navigate to an app or project and run `pnpm test` to run those tests in watch mode
@@ -76,13 +77,13 @@ You can generate a distribution for your OS by running the following steps.
From the project root, run the following commands
- __Install the project dependencies__ by running `pnpm i`
- __Build the UI and server__ by running `turbo run build:electron`
- __Create the package__ by running `turbo run dist-win`, `turbo run dist-mac` or `turbo run dist-linux`
- __Build the UI and server__ by running `pnpm turbo run build:electron`
- __Create the package__ by running `pnpm turbo run dist-win`, `pnpm turbo run dist-mac` or `pnpm turbo run dist-linux`
The build distribution assets will be at `.apps/electron/dist`
Note: The MacOS build will only work in CI, locally it will fail due to notarisation issues.
Use the `turbo run dist-mac:local` command to build a MacOS distribution locally.
Use the `pnpm turbo run dist-mac:local` command to build a MacOS distribution locally and skip the notary process.
## DOCKER
@@ -98,10 +99,3 @@ Other useful commands
- __List running processes__ by running `docker ps`
- __Kill running process__ by running `docker kill <process-id>`
## General Info
# APP Building
We build the app from app.js for almost all applications. The output file will still be named index.cjs. This is because of Electron.
Building the app from index.ts only applies for applications that don't use electron. index.ts will take over the initialization of the server and UI when electron isn't present.
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@getontime/cli",
"version": "4.0.0-alpha.3",
"version": "4.0.0-alpha.4",
"author": "Carlos Valente",
"description": "Time keeping for live events",
"repository": "https://github.com/cpvalente/ontime",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "ontime-ui",
"version": "4.0.0-alpha.3",
"version": "4.0.0-alpha.4",
"private": true,
"type": "module",
"dependencies": {
@@ -1,12 +1,13 @@
.list {
display: flex;
flex-wrap: wrap;
flex-wrap: nowrap;
gap: 0.5rem;
}
.swatch {
width: 2rem;
height: 2rem;
aspect-ratio: 1;
border-radius: 99px;
border: 2px solid $gray-1200;
color: $ui-white;
@@ -14,7 +14,8 @@
display: flex;
flex-direction: column;
row-gap: 1rem;
padding: 0.5em;
padding-top: 0.75rem;
padding-left: 0.5em;
position: fixed;
left: 0;
@@ -7,8 +7,8 @@
}
.timers {
padding-block: 0.75rem 0.5rem;
padding-inline: 1rem 0;
padding-block: 1rem 0.5rem;
padding-inline: 1rem 0.5rem;
display: grid;
grid-template-columns: 1fr 1fr 1fr;
@@ -18,11 +18,21 @@
.runningTimer {
grid-area: timers;
justify-self: center;
:nth-child(2) {
font-size: 2.5rem;
line-height: 1.2;
}
}
.timeNow {
grid-area: clock;
justify-self: right;
:nth-child(2) {
font-size: 2.5rem;
line-height: 1.2;
}
}
.progressOverride {
@@ -144,11 +144,15 @@ function FlagTimes() {
<span className={title ? style.labelTitle : style.label}>{`${title ? title : 'Flag'} `}</span>
<div className={style.labelledElement}>
<Tooltip text='Time to next flag planned start' render={<TbFlagPin className={style.icon} />} />
<span data-testid='flag-plannedStart' className={cx([style.time, !entry && style.muted])}>{plannedTimeUntilDisplay}</span>
<span data-testid='flag-plannedStart' className={cx([style.time, !entry && style.muted])}>
{plannedTimeUntilDisplay}
</span>
</div>
<div className={style.labelledElement}>
<Tooltip text='Time to next flag expected start' render={<TbFlagStar className={style.icon} />} />
<span data-testid='flag-expectedStart' className={cx([style.time, expectedTimeUntil === null && style.muted])}>{expectedTimeUntilDisplay}</span>
<span data-testid='flag-expectedStart' className={cx([style.time, expectedTimeUntil === null && style.muted])}>
{expectedTimeUntilDisplay}
</span>
</div>
</div>
);
@@ -43,6 +43,5 @@
border-radius: 0 8px 8px 0;
flex: 1 1 auto; /* flex-grow: 1, flex-shrink: 1, flex-basis: auto */
min-width: 30rem;
max-width: 45rem;
}
@@ -13,7 +13,7 @@
display: flex;
flex-direction: column;
gap: 1.5rem;
overflow-y: auto;
overflow-y: scroll;
}
.timeSettings {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "ontime-electron",
"version": "4.0.0-alpha.3",
"version": "4.0.0-alpha.4",
"author": "Carlos Valente",
"description": "Time keeping for live events",
"repository": "https://github.com/cpvalente/ontime",
+1 -1
View File
@@ -2,7 +2,7 @@
"name": "ontime-server",
"type": "module",
"main": "src/index.ts",
"version": "4.0.0-alpha.3",
"version": "4.0.0-alpha.4",
"exports": "./src/index.js",
"dependencies": {
"@googleapis/sheets": "^5.0.5",
+31 -1
View File
@@ -6,7 +6,7 @@
// Data that the user needs to provide depending on the Ontime URL
const isSecure = window.location.protocol === 'https:';
const userProvidedSocketUrl = `${isSecure ? 'wss' : 'ws'}://${window.location.hostname}:${window.location.port}/ws`;
const userProvidedSocketUrl = `${isSecure ? 'wss' : 'ws'}://${window.location.hostname}${getStageHash()}${getUserPort()}/ws`;
connectSocket();
@@ -136,3 +136,33 @@ function formatTimer(number) {
function formatObject(data) {
return JSON.stringify(data, null, 2);
}
/**
* Utility to handle a demo deployed in an ontime stage
* You can likely ignore this in your app
*
* an url looks like
* https://cloud.getontime.no/stage-hash/external/demo/
* @returns {string} - The stage hash if the app is running in an ontime stage
*/
function getStageHash() {
const href = window.location.href;
if (!href.includes('getontime.no')) {
return '';
}
const hash = href.split('/');
const stageHash = hash.at(3);
return stageHash || '';
}
/**
* Utility to optionally use a URL port
* You can likely hard code this in your app
*
* @returns {string} - The port Ontime server is available at
*/
function getUserPort() {
const port = window.location.port;
return port ? `:${port}` : '';
}
+62 -16
View File
@@ -1,5 +1,3 @@
import { LogOrigin } from 'ontime-types';
import express, { type Request, type Response, type NextFunction } from 'express';
import type { IncomingMessage } from 'node:http';
import type { WebSocket } from 'ws';
@@ -7,7 +5,6 @@ import { parse as parseCookie } from 'cookie';
import { hashPassword } from '../utils/hash.js';
import { srcFiles } from '../setup/index.js';
import { logger } from '../classes/Logger.js';
import { hashedPassword, hasPassword } from '../api-data/session/session.service.js';
import { noopMiddleware } from './noop.js';
@@ -53,10 +50,21 @@ export function makeAuthenticateMiddleware(prefix: string) {
return { authenticate: noopMiddleware, authenticateAndRedirect: noopMiddleware };
}
// pre-compute the login redirect base URL to avoid string concatenation on every request
const loginRedirectBase = `${prefix}/login?redirect=`;
function authenticate(req: Request, res: Response, next: NextFunction) {
const token = req.query.token || req.cookies?.token;
if (token && token === hashedPassword) {
return next();
if (req.query.token) {
if (req.query.token === hashedPassword) {
return next();
}
}
if (req.cookies?.token) {
const tokenFromCookie = getTokenFromCookie(req.cookies.token);
if (tokenFromCookie === hashedPassword) {
return next();
}
}
res.status(401).send('Unauthorized');
@@ -74,8 +82,11 @@ export function makeAuthenticateMiddleware(prefix: string) {
}
// we expect the token to be in the cookies
if (req.cookies?.token === hashedPassword) {
return next();
if (req.cookies?.token) {
const tokenFromCookie = getTokenFromCookie(req.cookies.token);
if (tokenFromCookie === hashedPassword) {
return next();
}
}
// we use query params for generating authenticated URLs and for clients like the companion module
@@ -87,7 +98,7 @@ export function makeAuthenticateMiddleware(prefix: string) {
return next();
}
res.redirect(`${prefix}/login?redirect=${req.originalUrl}`);
res.redirect(loginRedirectBase + req.originalUrl);
}
return { authenticate, authenticateAndRedirect };
@@ -105,19 +116,31 @@ export function authenticateSocket(_ws: WebSocket, req: IncomingMessage, next: (
const cookieString = req.headers.cookie;
if (typeof cookieString === 'string') {
const cookies = parseCookie(cookieString);
if (cookies.token === hashedPassword) {
return next();
if (cookies.token) {
const token = getTokenFromCookie(cookies.token);
if (token === hashedPassword) {
return next();
}
}
}
// check if token is in the params
const url = new URL(req.url || '', `http://${req.headers.host}`);
const token = url.searchParams.get('token');
if (token === hashedPassword) {
// check if token is in the params - simple string check first
const urlString = req.url || '';
if (urlString.includes(`token=${hashedPassword}`)) {
return next();
}
logger.warning(LogOrigin.Client, 'Unauthorized WebSocket connection attempt');
// fallback to full URL parsing for other formats
try {
const url = new URL(urlString, `http://${req.headers.host}`);
const token = url.searchParams.get('token');
if (token === hashedPassword) {
return next();
}
} catch (_) {
// ignore URL parsing errors
}
return next(new Error('Unauthorized'));
}
@@ -133,3 +156,26 @@ function setSessionCookie(res: Response, token: string) {
sameSite: 'none', // allow cookies to be sent in cross-origin requests (e.g., iframes)
});
}
/**
* When calling this function we already know a cookie called 'token' exists
* And want to extract its value
*/
function getTokenFromCookie(cookieContents: string): string | undefined {
// Fast path: check if the hashed password is directly in the cookie string
// This avoids JSON parsing for the common case
const cookieTokenString = '"token":"' + hashedPassword + '}"';
if (cookieTokenString && cookieContents.includes(cookieTokenString)) {
return hashedPassword;
}
// Fallback to JSON parsing for other cases or validation
try {
const cookie = JSON.parse(cookieContents);
if (cookie && typeof cookie.token === 'string') {
return cookie.token;
}
} catch (_) {
// no error handling to do here
}
}
+1 -3
View File
@@ -1,6 +1,6 @@
{
"name": "ontime",
"version": "4.0.0-alpha.3",
"version": "4.0.0-alpha.4",
"description": "Time keeping for live events",
"keywords": [
"ontime",
@@ -22,8 +22,6 @@
"type": "module",
"scripts": {
"dev": "turbo run dev",
"dev:electron": "turbo run dev --filter=ontime",
"dev:server": "turbo run dev --filter=ontime-server",
"lint": "turbo run lint",
"build": "turbo run build",
"build:local": "turbo run build:local",
+4
View File
@@ -10,6 +10,10 @@
"cache": false,
"persistent": true
},
"dev:inspect": {
"cache": false,
"persistent": true
},
"dev:test": {
"cache": false
},