feat: cuesheet sharing

feat: locked presets

refactor: simplify locked param

refactor: create share links
This commit is contained in:
Carlos Valente
2025-07-22 05:45:20 +02:00
committed by Carlos Valente
parent 1695b4dc68
commit 6c6f5c2c0f
62 changed files with 1661 additions and 478 deletions
@@ -1,50 +1,150 @@
import { generateAuthenticatedUrl } from '../session.service.js';
import { generateShareUrl } from '../session.service.js';
describe('generateAuthenticatedUrl()', () => {
describe('for local IP addresses', () => {
it('generates a link without locking or authentication', () => {
const localhostNotLocked = generateAuthenticatedUrl('http://localhost:3000', 'timer', false, false);
const localhostNotLocked = generateShareUrl('http://localhost:3000', 'timer', {
lockConfig: false,
lockNav: false,
authenticate: false,
});
expect(localhostNotLocked.toString()).toBe('http://localhost:3000/timer');
});
it('generates a link with IP locking enabled', () => {
const ipLocked = generateAuthenticatedUrl('http://192.168.10.173:4001', 'timer', true, false);
expect(ipLocked.toString()).toBe('http://192.168.10.173:4001/timer?locked=true');
it('generates a link with navigation locking enabled', () => {
const ipLocked = generateShareUrl('http://192.168.10.173:4001', 'timer', {
lockConfig: false,
lockNav: true,
authenticate: false,
});
expect(ipLocked.toString()).toBe('http://192.168.10.173:4001/timer?n=1');
});
it('generates a link with authentication token and IP locking', () => {
const withAuth = generateAuthenticatedUrl('http://192.168.10.173:4001', 'timer', true, true, undefined, '1234');
expect(withAuth.toString()).toBe('http://192.168.10.173:4001/timer?token=1234&locked=true');
it('generates a link with authentication token and navigation locking', () => {
const withAuth = generateShareUrl('http://192.168.10.173:4001', 'timer', {
lockConfig: false,
lockNav: true,
authenticate: true,
hash: '1234',
});
expect(withAuth.toString()).toBe('http://192.168.10.173:4001/timer?token=1234&n=1');
});
it('generates a link to an unlocked preset', () => {
const withAuth = generateShareUrl('http://192.168.10.173:4001', 'timer', {
lockConfig: false,
lockNav: false,
authenticate: false,
preset: 'minimal',
});
expect(withAuth.toString()).toBe('http://192.168.10.173:4001/minimal');
});
it('generates a link to an unlocked preset without navigation', () => {
const withAuth = generateShareUrl('http://192.168.10.173:4001', 'timer', {
lockConfig: false,
lockNav: true,
authenticate: false,
preset: 'minimal',
});
expect(withAuth.toString()).toBe('http://192.168.10.173:4001/minimal?n=1');
});
it('generates a link to a locked preset', () => {
const withAuth = generateShareUrl('http://192.168.10.173:4001', 'timer', {
lockConfig: true,
lockNav: false,
authenticate: false,
preset: 'minimal',
});
expect(withAuth.toString()).toBe('http://192.168.10.173:4001/preset/minimal');
});
it('generates a link to a locked preset', () => {
const withAuth = generateShareUrl('http://192.168.10.173:4001', 'cuesheet', {
lockConfig: false,
lockNav: false,
authenticate: false,
preset: 'some-cuesheet-preset',
});
expect(withAuth.toString()).toBe('http://192.168.10.173:4001/preset/some-cuesheet-preset');
});
});
describe('for ontime-cloud URLs', () => {
it('generates a link without locking or authentication', () => {
const cloudNotLocked = generateAuthenticatedUrl(
'https://cloud.getontime.no/userhash',
'timer',
false,
false,
'prefix',
);
const cloudNotLocked = generateShareUrl('https://cloud.getontime.no/userhash', 'timer', {
lockConfig: false,
lockNav: false,
authenticate: false,
prefix: 'prefix',
});
expect(cloudNotLocked.toString()).toBe('https://cloud.getontime.no/prefix/timer');
});
it('generates a link with IP locking enabled', () => {
const ipLocked = generateAuthenticatedUrl('https://cloud.getontime.no/prefix', 'timer', true, false, 'prefix');
expect(ipLocked.toString()).toBe('https://cloud.getontime.no/prefix/timer?locked=true');
it('generates a link with navigation locking enabled', () => {
const ipLocked = generateShareUrl('https://cloud.getontime.no/prefix', 'timer', {
lockConfig: false,
lockNav: true,
authenticate: false,
prefix: 'prefix',
});
expect(ipLocked.toString()).toBe('https://cloud.getontime.no/prefix/timer?n=1');
});
it('generates a link with authentication token and IP locking', () => {
const withAuth = generateAuthenticatedUrl(
'https://cloud.getontime.no/prefix',
'timer',
true,
true,
'prefix',
'1234',
);
expect(withAuth.toString()).toBe('https://cloud.getontime.no/prefix/timer?token=1234&locked=true');
it('generates a link with authentication token and navigation locking', () => {
const withAuth = generateShareUrl('https://cloud.getontime.no/prefix', 'timer', {
lockConfig: false,
lockNav: true,
authenticate: true,
prefix: 'prefix',
hash: '1234',
});
expect(withAuth.toString()).toBe('https://cloud.getontime.no/prefix/timer?token=1234&n=1');
});
it('generates a link to an unlocked preset', () => {
const withAuth = generateShareUrl('https://cloud.getontime.no/prefix', 'timer', {
lockConfig: false,
lockNav: false,
authenticate: false,
preset: 'minimal',
prefix: 'prefix',
});
expect(withAuth.toString()).toBe('https://cloud.getontime.no/prefix/minimal');
});
it('generates a link to an unlocked preset without navigation', () => {
const withAuth = generateShareUrl('https://cloud.getontime.no/prefix', 'timer', {
lockConfig: false,
lockNav: true,
authenticate: false,
preset: 'minimal',
prefix: 'prefix',
});
expect(withAuth.toString()).toBe('https://cloud.getontime.no/prefix/minimal?n=1');
});
it('generates a link to a locked preset', () => {
const withAuth = generateShareUrl('https://cloud.getontime.no/prefix', 'timer', {
lockConfig: true,
lockNav: false,
authenticate: false,
prefix: 'prefix',
preset: 'minimal',
});
expect(withAuth.toString()).toBe('https://cloud.getontime.no/prefix/preset/minimal');
});
it('generates a link to a locked preset', () => {
const withAuth = generateShareUrl('https://cloud.getontime.no/prefix', 'cuesheet', {
lockConfig: false,
lockNav: false,
authenticate: false,
prefix: 'prefix',
preset: 'some-cuesheet-preset',
});
expect(withAuth.toString()).toBe('https://cloud.getontime.no/prefix/preset/some-cuesheet-preset');
});
});
});
@@ -29,12 +29,12 @@ router.get('/info', async (_req: Request, res: Response<GetInfo | ErrorResponse>
router.post('/url', validateGenerateUrl, (req: Request, res: Response<GetUrl | ErrorResponse>) => {
try {
const url = sessionService.generateAuthenticatedUrl(
req.body.baseUrl,
req.body.path,
req.body.lock,
req.body.authenticate,
);
const url = sessionService.generateShareUrl(req.body.baseUrl, req.body.path, {
authenticate: req.body.authenticate,
lockConfig: req.body.lockConfig,
lockNav: req.body.lockNav,
preset: req.body.preset,
});
res.status(200).send({ url: url.toString() });
} catch (error) {
const message = getErrorMessage(error);
@@ -1,4 +1,4 @@
import { GetInfo, SessionStats } from 'ontime-types';
import { GetInfo, LinkOptions, OntimeView, SessionStats } from 'ontime-types';
import { getDataProvider } from '../../classes/data-provider/DataProvider.js';
import { publicDir } from '../../setup/index.js';
@@ -57,22 +57,25 @@ export const hashedPassword = hasPassword ? hashPassword(password as string) : u
/**
* Generates a pre-authenticated URL by injecting a token in the URL params
*/
export function generateAuthenticatedUrl(
export function generateShareUrl(
baseUrl: string,
path: string,
lock: boolean,
authenticate: boolean,
prefix = routerPrefix,
hash = hashedPassword,
canonicalPath: string,
{ authenticate, lockConfig, lockNav, preset, prefix = routerPrefix, hash = hashedPassword }: LinkOptions,
): URL {
const url = new URL(baseUrl);
url.pathname = prefix ? `${prefix}/${path}` : path;
// if the config is locked and we are in a preset, we hide the canonical path
const shouldMaskPath = Boolean(preset) && (canonicalPath === OntimeView.Cuesheet || lockConfig);
const maybePresetPath = shouldMaskPath ? `preset/${preset}` : preset || canonicalPath;
url.pathname = prefix ? `${prefix}/${maybePresetPath}` : maybePresetPath;
if (authenticate && hash) {
url.searchParams.append('token', hash);
}
if (lock) {
url.searchParams.append('locked', 'true');
if (lockNav) {
url.searchParams.append('n', '1');
}
return url;
}
@@ -3,9 +3,14 @@ import { requestValidationFunction } from '../validation-utils/validationFunctio
export const validateGenerateUrl = [
body('baseUrl').isString().trim().notEmpty(),
body('path').isString().trim(),
body('lock').isBoolean(),
body('path').isString().trim().notEmpty(),
body('authenticate').isBoolean(),
body('lockConfig').isBoolean(),
body('lockNav').isBoolean(),
body('preset').optional().isString().trim().notEmpty(),
body('prefix').optional().isString().trim().notEmpty(),
body('hash').optional().isString().trim().notEmpty(),
requestValidationFunction,
];