mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-04 15:08:01 +00:00
Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3c41b40c5f | |||
| 3b38756dda | |||
| 5a4494ba85 | |||
| 02ad852dd4 | |||
| 7ce1097826 | |||
| 7ee0de5341 | |||
| 0db249871b |
+13
-19
@@ -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,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,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;
|
||||
|
||||
+2
-1
@@ -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,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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
Vendored
+31
-1
@@ -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}` : '';
|
||||
}
|
||||
|
||||
@@ -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
@@ -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",
|
||||
|
||||
@@ -10,6 +10,10 @@
|
||||
"cache": false,
|
||||
"persistent": true
|
||||
},
|
||||
"dev:inspect": {
|
||||
"cache": false,
|
||||
"persistent": true
|
||||
},
|
||||
"dev:test": {
|
||||
"cache": false
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user