mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-21 06:59:09 +00:00
feat: generate authenticated url (#1471)
* refactor: extract info component * feat: generate authenticated url * refactor: add companion select * refactor: ensure behaviour across environments
This commit is contained in:
@@ -12,3 +12,16 @@ export async function getInfo(): Promise<GetInfo> {
|
||||
const res = await axios.get(`${sessionPath}/info`);
|
||||
return res.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* HTTP request to get a pre-authenticated URL
|
||||
*/
|
||||
export async function generateUrl(
|
||||
baseUrl: string,
|
||||
path: string,
|
||||
lock: boolean,
|
||||
authenticate: boolean,
|
||||
): Promise<string> {
|
||||
const res = await axios.post(`${sessionPath}/url`, { baseUrl, path, lock, authenticate });
|
||||
return res.data.url;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
.infoLabel {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: $element-spacing;
|
||||
padding: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
|
||||
background-color: $gray-1100;
|
||||
border-radius: 2px;
|
||||
font-size: $inner-section-text-size;
|
||||
|
||||
svg {
|
||||
font-size: 1.5rem;
|
||||
color: $info-blue;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { PropsWithChildren } from 'react';
|
||||
import { IoAlertCircle } from '@react-icons/all-files/io5/IoAlertCircle';
|
||||
|
||||
import style from './Info.module.scss';
|
||||
|
||||
export default function Info({ children }: PropsWithChildren) {
|
||||
return (
|
||||
<div className={style.infoLabel}>
|
||||
<IoAlertCircle />
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -8,23 +8,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
.infoLabel {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: $element-spacing;
|
||||
padding: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
|
||||
background-color: $gray-1100;
|
||||
border-radius: 2px;
|
||||
font-size: $inner-section-text-size;
|
||||
|
||||
svg {
|
||||
font-size: 1.5rem;
|
||||
color: $info-blue;
|
||||
}
|
||||
}
|
||||
|
||||
.sectionList {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@@ -11,9 +11,9 @@ import {
|
||||
DrawerOverlay,
|
||||
useDisclosure,
|
||||
} from '@chakra-ui/react';
|
||||
import { IoAlertCircle } from '@react-icons/all-files/io5/IoAlertCircle';
|
||||
|
||||
import useViewSettings from '../../../common/hooks-query/useViewSettings';
|
||||
import Info from '../info/Info';
|
||||
|
||||
import { ViewOption } from './types';
|
||||
import ViewParamsSection from './ViewParamsSection';
|
||||
@@ -132,12 +132,7 @@ export default function ViewParamsEditor({ viewOptions }: EditFormDrawerProps) {
|
||||
</DrawerHeader>
|
||||
|
||||
<DrawerBody>
|
||||
{viewSettings.overrideStyles && (
|
||||
<div className={style.infoLabel}>
|
||||
<IoAlertCircle />
|
||||
This view style is being modified by a custom CSS file. <br />
|
||||
</div>
|
||||
)}
|
||||
{viewSettings.overrideStyles && <Info>This view style is being modified by a custom CSS file.</Info>}
|
||||
<form id='edit-params-form' onSubmit={onParamsFormSubmit} className={style.sectionList}>
|
||||
{viewOptions.map((section) => (
|
||||
<ViewParamsSection
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import { linkToOtherHost } from '../linkUtils';
|
||||
|
||||
describe('linkToOTherHost', () => {
|
||||
it('should handle electron links', () => {
|
||||
const serverUrl = 'http://localhost:4001';
|
||||
const baseUri = '';
|
||||
const destination = linkToOtherHost('192.168.10.166', 'path', serverUrl, baseUri);
|
||||
expect(destination).toBe('http://192.168.10.166:4001/path');
|
||||
});
|
||||
|
||||
it('should handle ontime cloud links', () => {
|
||||
const serverUrl = 'https://cloud.getontime.no/user-hash';
|
||||
const baseUri = 'user-hash';
|
||||
const destination = linkToOtherHost('cloud.getontime.no', 'path', serverUrl, baseUri);
|
||||
expect(destination).toBe('https://cloud.getontime.no/user-hash/path');
|
||||
});
|
||||
});
|
||||
@@ -18,23 +18,32 @@ export function openLink(url: string) {
|
||||
|
||||
/**
|
||||
* Handles opening external links
|
||||
* @param event
|
||||
* @param location
|
||||
* serverUrl and baseURI are used for testing
|
||||
*/
|
||||
export function handleLinks(event: MouseEvent, location: string) {
|
||||
export function handleLinks(
|
||||
event: MouseEvent,
|
||||
location: string,
|
||||
externalServerUrl: string = serverURL,
|
||||
externalBaseURI: string = baseURI,
|
||||
) {
|
||||
// we handle the link manually
|
||||
event.preventDefault();
|
||||
|
||||
const destination = new URL(serverURL);
|
||||
destination.pathname = baseURI ? `${baseURI}/${location}` : location;
|
||||
const destination = new URL(externalServerUrl);
|
||||
destination.pathname = externalBaseURI ? `${externalBaseURI}/${location}` : location;
|
||||
openLink(destination.toString());
|
||||
}
|
||||
|
||||
export function linkToOtherHost(host: string, path?: string) {
|
||||
const destination = new URL(serverURL);
|
||||
export function linkToOtherHost(
|
||||
host: string,
|
||||
path?: string,
|
||||
externalServerUrl: string = serverURL,
|
||||
externalBaseURI: string = baseURI,
|
||||
) {
|
||||
const destination = new URL(externalServerUrl);
|
||||
destination.hostname = host;
|
||||
if (path) {
|
||||
destination.pathname = baseURI ? `${baseURI}/${path}` : path;
|
||||
destination.pathname = externalBaseURI ? `${externalBaseURI}/${path}` : path;
|
||||
}
|
||||
return destination.toString();
|
||||
}
|
||||
|
||||
@@ -75,4 +75,4 @@ function resolveBaseURI(): string {
|
||||
}
|
||||
|
||||
return base;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
.qrCode {
|
||||
padding: 0.5rem;
|
||||
background: $ui-white;
|
||||
border-radius: 3px;
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
import { useState } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import QRCode from 'react-qr-code';
|
||||
import { Button, Select, Switch } from '@chakra-ui/react';
|
||||
|
||||
import { generateUrl } from '../../../../common/api/session';
|
||||
import { maybeAxiosError } from '../../../../common/api/utils';
|
||||
import ExternalLink from '../../../../common/components/external-link/ExternalLink';
|
||||
import Info from '../../../../common/components/info/Info';
|
||||
import useInfo from '../../../../common/hooks-query/useInfo';
|
||||
import useUrlPresets from '../../../../common/hooks-query/useUrlPresets';
|
||||
import copyToClipboard from '../../../../common/utils/copyToClipboard';
|
||||
import { preventEscape } from '../../../../common/utils/keyEvent';
|
||||
import { linkToOtherHost } from '../../../../common/utils/linkUtils';
|
||||
import { serverURL } from '../../../../externals';
|
||||
import * as Panel from '../../panel-utils/PanelUtils';
|
||||
|
||||
import style from './GenerateLinkForm.module.scss';
|
||||
|
||||
interface GenerateLinkFormOptions {
|
||||
baseUrl: string;
|
||||
path: string;
|
||||
lock: boolean;
|
||||
authenticate: boolean;
|
||||
}
|
||||
|
||||
type GenerateLinkState = 'pending' | 'loading' | 'success' | 'error';
|
||||
|
||||
export default function GenerateLinkForm() {
|
||||
const { data: infoData } = useInfo();
|
||||
const { data: urlPresetData } = useUrlPresets();
|
||||
const [formState, setFormState] = useState<GenerateLinkState>('pending');
|
||||
const [url, setUrl] = useState(serverURL);
|
||||
|
||||
const {
|
||||
handleSubmit,
|
||||
register,
|
||||
setError,
|
||||
formState: { errors },
|
||||
} = useForm<GenerateLinkFormOptions>({
|
||||
mode: 'onChange',
|
||||
resetOptions: {
|
||||
keepDirtyValues: true,
|
||||
},
|
||||
});
|
||||
|
||||
const onSubmit = async (options: GenerateLinkFormOptions) => {
|
||||
try {
|
||||
setFormState('loading');
|
||||
const baseUrl = linkToOtherHost(options.baseUrl);
|
||||
const url = await generateUrl(baseUrl, options.path, options.lock, options.authenticate);
|
||||
await copyToClipboard(url);
|
||||
setUrl(url);
|
||||
setFormState('success');
|
||||
setTimeout(() => {
|
||||
setFormState('pending');
|
||||
}, 4000);
|
||||
} catch (error) {
|
||||
const message = maybeAxiosError(error);
|
||||
setError('root', { message });
|
||||
setFormState('error');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Panel.Section as='form' onSubmit={handleSubmit(onSubmit)} onKeyDown={(event) => preventEscape(event)}>
|
||||
{errors.root && <Panel.Error>{errors.root.message}</Panel.Error>}
|
||||
<Info>
|
||||
<Panel.Paragraph>
|
||||
You can generate a link to share with your team or to use in automation (such as companion).
|
||||
</Panel.Paragraph>
|
||||
</Info>
|
||||
<Panel.ListGroup>
|
||||
<Panel.ListItem>
|
||||
<Panel.Field title='Host IP' description='Which IP address will be used' />
|
||||
<Select variant='ontime' size='sm' {...register('baseUrl')}>
|
||||
{infoData.networkInterfaces.map((nif) => {
|
||||
return (
|
||||
<option key={nif.name} value={nif.address}>
|
||||
{`${nif.name} - ${nif.address}`}
|
||||
</option>
|
||||
);
|
||||
})}
|
||||
</Select>
|
||||
</Panel.ListItem>
|
||||
<Panel.ListItem>
|
||||
<Panel.Field
|
||||
title='URL Preset'
|
||||
description='Which preset will the link point to (will default to /timer if none is given)'
|
||||
/>
|
||||
<Select variant='ontime' size='sm' {...register('path')}>
|
||||
<option key='timer' value='timer'>
|
||||
Timer
|
||||
</option>
|
||||
<option key='companion' value=''>
|
||||
Companion
|
||||
</option>
|
||||
{urlPresetData.map((preset) => {
|
||||
return (
|
||||
<option key={preset.alias} value={preset.alias}>
|
||||
{`Preset: ${preset.alias}`}
|
||||
</option>
|
||||
);
|
||||
})}
|
||||
</Select>
|
||||
</Panel.ListItem>
|
||||
<Panel.ListItem>
|
||||
<Panel.Field
|
||||
title='Lock navigation'
|
||||
description='Prevent showing navigation (will only work for non production URLs)'
|
||||
/>
|
||||
<Switch variant='ontime' size='lg' {...register('lock')} />
|
||||
</Panel.ListItem>
|
||||
<Panel.ListItem>
|
||||
<Panel.Field title='Authenticate' description='Whether the URL should be pre-authenticated' />
|
||||
<Switch variant='ontime' size='lg' {...register('authenticate')} />
|
||||
</Panel.ListItem>
|
||||
</Panel.ListGroup>
|
||||
<Panel.ListGroup>
|
||||
<Panel.ListItem>
|
||||
<Panel.Field title='Generate link' description='Fill form and generate link and QR code' />
|
||||
<Button
|
||||
variant='ontime-filled'
|
||||
size='sm'
|
||||
isLoading={formState === 'loading'}
|
||||
type='submit'
|
||||
style={{ alignSelf: 'end' }}
|
||||
>
|
||||
{formState === 'success' ? 'Link copied to clipboard!' : 'Update share link'}
|
||||
</Button>
|
||||
<div style={{ marginTop: '1rem', display: 'flex', flexDirection: 'column', gap: '0.25rem' }}>
|
||||
<QRCode size={172} value={url} className={style.qrCode} />
|
||||
<ExternalLink href={url}>{url}</ExternalLink>
|
||||
</div>
|
||||
</Panel.ListItem>
|
||||
</Panel.ListGroup>
|
||||
</Panel.Section>
|
||||
);
|
||||
}
|
||||
@@ -8,21 +8,34 @@ import type { PanelBaseProps } from '../../panel-list/PanelList';
|
||||
import * as Panel from '../../panel-utils/PanelUtils';
|
||||
import ClientControlPanel from '../client-control-panel/ClientControlPanel';
|
||||
|
||||
import GenerateLinkForm from './GenerateLinkForm';
|
||||
import InfoNif from './NetworkInterfaces';
|
||||
import LogExport from './NetworkLogExport';
|
||||
|
||||
export default function NetworkLogPanel({ location }: PanelBaseProps) {
|
||||
const linkRef = useScrollIntoView<HTMLDivElement>('link', location);
|
||||
const clientsRef = useScrollIntoView<HTMLDivElement>('clients', location);
|
||||
const logRef = useScrollIntoView<HTMLDivElement>('log', location);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Panel.Header>Network</Panel.Header>
|
||||
<Panel.Section>
|
||||
{isDockerImage && <OntimeCloudStats />}
|
||||
<Panel.Paragraph>Ontime is streaming on the following network interfaces</Panel.Paragraph>
|
||||
</Panel.Section>
|
||||
<InfoNif />
|
||||
{isDockerImage && (
|
||||
<Panel.Section>
|
||||
<OntimeCloudStats />
|
||||
</Panel.Section>
|
||||
)}
|
||||
<div ref={linkRef}>
|
||||
<Panel.Section>
|
||||
<Panel.Card>
|
||||
<Panel.SubHeader>Share Ontime Link</Panel.SubHeader>
|
||||
<Panel.Divider />
|
||||
<Panel.Paragraph>Ontime is streaming on the following network interfaces</Panel.Paragraph>
|
||||
<InfoNif />
|
||||
<GenerateLinkForm />
|
||||
</Panel.Card>
|
||||
</Panel.Section>
|
||||
</div>
|
||||
<div ref={logRef}>
|
||||
<LogExport />
|
||||
</div>
|
||||
|
||||
@@ -60,6 +60,10 @@ const staticOptions = [
|
||||
label: 'Network',
|
||||
split: true,
|
||||
secondary: [
|
||||
{
|
||||
id: 'network__link',
|
||||
label: 'Share link',
|
||||
},
|
||||
{
|
||||
id: 'network__log',
|
||||
label: 'Event log',
|
||||
|
||||
Reference in New Issue
Block a user