mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-17 05:13:32 +00:00
Sheet use limited input device auth flow (#782)
* refactor: limited-input-device auth * refactor: resolve sheet directory from setup * refactor: extract sheet logic in backend * refactor: simplify sheet integration --------- Co-authored-by: cv <34649812+cpvalente@users.noreply.github.com> Co-authored-by: Carlos Valente <carlosvalente@pm.me>
This commit is contained in:
committed by
GitHub
parent
fc5338903b
commit
474f1e2177
@@ -10,6 +10,7 @@ export const RUNDOWN = ['rundown'];
|
||||
export const RUNTIME = ['runtimeStore'];
|
||||
export const SHEET_STATE = ['sheetState'];
|
||||
export const USERFIELDS = ['userFields'];
|
||||
export const CUSTOM_FIELDS = ['customFields'];
|
||||
export const VIEW_SETTINGS = ['viewSettings'];
|
||||
|
||||
const location = window.location;
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import axios, { AxiosResponse } from 'axios';
|
||||
import {
|
||||
Alias,
|
||||
AuthenticationStatus,
|
||||
CustomField,
|
||||
CustomFieldLabel,
|
||||
CustomFields,
|
||||
DatabaseModel,
|
||||
GetInfo,
|
||||
HttpSettings,
|
||||
@@ -18,7 +22,7 @@ import { ExcelImportMap } from 'ontime-utils';
|
||||
import { apiRepoLatest } from '../../externals';
|
||||
import fileDownload from '../utils/fileDownload';
|
||||
|
||||
import { ontimeURL } from './apiConstants';
|
||||
import { ontimeURL, projectDataURL } from './apiConstants';
|
||||
|
||||
/**
|
||||
* @description HTTP request to retrieve application settings
|
||||
@@ -257,79 +261,65 @@ export async function loadProject(filename: string): Promise<MessageResponse> {
|
||||
}
|
||||
|
||||
/**
|
||||
* @description STEP 1
|
||||
* @description HTTP request to initiate the authentication service with google
|
||||
*/
|
||||
export const uploadSheetClientFile = async (file: File) => {
|
||||
export const requestConnection = async (
|
||||
file: File,
|
||||
sheetId: string,
|
||||
): Promise<{
|
||||
verification_url: string;
|
||||
user_code: string;
|
||||
}> => {
|
||||
const formData = new FormData();
|
||||
formData.append('userFile', file);
|
||||
const res = await axios
|
||||
.post(`${ontimeURL}/sheet/clientsecret`, formData, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
},
|
||||
})
|
||||
.then((response) => response.data.id);
|
||||
return res;
|
||||
};
|
||||
formData.append('client_secret', file);
|
||||
|
||||
const response = await axios.post(`${ontimeURL}/sheet/${sheetId}/connect`, formData, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* @description STEP 1 test
|
||||
*/
|
||||
// TODO: do we still need this?
|
||||
export const getClientSecret = async () => {
|
||||
const response = await axios.get(`${ontimeURL}/sheet/clientsecret`);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
/**
|
||||
* @description STEP 2
|
||||
* @description HTTP request to verify whether we are authenticated with Google Sheet service
|
||||
*/
|
||||
export const getSheetsAuthUrl = async () => {
|
||||
const response = await axios.get(`${ontimeURL}/sheet/authentication/url`);
|
||||
export const verifyAuthenticationStatus = async (): Promise<{ authenticated: AuthenticationStatus }> => {
|
||||
const response = await axios.get(`${ontimeURL}/sheet/connect`);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
/**
|
||||
* @description STEP 2 test
|
||||
* @description HTTP request to revoke authentication to google sheet
|
||||
*/
|
||||
export const getAuthentication = async () => {
|
||||
const response = await axios.get(`${ontimeURL}/sheet/authentication`);
|
||||
export const revokeAuthentication = async (): Promise<{ authenticated: AuthenticationStatus }> => {
|
||||
const response = await axios.post(`${ontimeURL}/sheet/revoke`);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
/**
|
||||
* @description STEP 3
|
||||
* @returns worksheetOptions
|
||||
* @description HTTP request to upload preview the contents of a google sheet as rundown
|
||||
*/
|
||||
export const postId = async (sheetId: string) => {
|
||||
const response = await axios.post(`${ontimeURL}/sheet/sheetId`, { sheetId });
|
||||
export const previewRundown = async (
|
||||
sheetId: string,
|
||||
options: ExcelImportMap,
|
||||
): Promise<{
|
||||
rundown: OntimeRundown;
|
||||
userFields: UserFields;
|
||||
}> => {
|
||||
const response = await axios.post(`${ontimeURL}/sheet/${sheetId}/read`, { options });
|
||||
return response.data;
|
||||
};
|
||||
|
||||
/**
|
||||
* @description STEP 4
|
||||
* @description HTTP request to upload the rundown to a google sheet
|
||||
*/
|
||||
export const postWorksheet = async (sheetId: string, worksheet: string) => {
|
||||
const response = await axios.post(`${ontimeURL}/sheet/worksheet`, { sheetId, worksheet });
|
||||
export const uploadRundown = async (sheetId: string, options: ExcelImportMap): Promise<void> => {
|
||||
const response = await axios.post(`${ontimeURL}/sheet/${sheetId}/write`, { options });
|
||||
return response.data;
|
||||
};
|
||||
|
||||
/**
|
||||
* @description STEP 5
|
||||
*/
|
||||
export const postPreviewSheet = async (sheetId: string, options: ExcelImportMap) => {
|
||||
const response = await axios.post(`${ontimeURL}/sheet-pull`, { sheetId, options });
|
||||
return response.data.data;
|
||||
};
|
||||
|
||||
/**
|
||||
* @description STEP 5
|
||||
*/
|
||||
export const postPushSheet = async (sheetId: string, options: ExcelImportMap) => {
|
||||
const response = await axios.post(`${ontimeURL}/sheet-push`, { sheetId, options });
|
||||
return response.data.data;
|
||||
};
|
||||
|
||||
/**
|
||||
* @description HTTP request to rename a project file
|
||||
*/
|
||||
@@ -374,8 +364,33 @@ export async function createProject(
|
||||
}
|
||||
>,
|
||||
): Promise<MessageResponse> {
|
||||
// TODO: is this URL correct?
|
||||
const url = `${ontimeURL}/project`;
|
||||
const decodedUrl = decodeURIComponent(url);
|
||||
const res = await axios.post(decodedUrl, project);
|
||||
return res.data;
|
||||
}
|
||||
|
||||
export async function getCustomFields(): Promise<CustomFields> {
|
||||
const res = await axios.get(`${projectDataURL}/custom-field`);
|
||||
return res.data;
|
||||
}
|
||||
|
||||
export async function postCustomField(newField: CustomField): Promise<CustomFields> {
|
||||
const res = await axios.post(`${projectDataURL}/custom-field`, {
|
||||
...newField,
|
||||
});
|
||||
return res.data;
|
||||
}
|
||||
|
||||
export async function editCustomField(label: CustomFieldLabel, newField: CustomField): Promise<CustomFields> {
|
||||
const res = await axios.put(`${projectDataURL}/custom-field/${label}`, {
|
||||
...newField,
|
||||
});
|
||||
return res.data;
|
||||
}
|
||||
|
||||
export async function deleteCustomField(label: CustomFieldLabel): Promise<CustomFields> {
|
||||
const res = await axios.delete(`${projectDataURL}/custom-field/${label}`);
|
||||
return res.data;
|
||||
}
|
||||
|
||||
@@ -10,20 +10,28 @@ interface CopyTagProps {
|
||||
label: string;
|
||||
className?: string;
|
||||
size?: Size;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
export default function CopyTag(props: PropsWithChildren<CopyTagProps>) {
|
||||
const { label, className, size = 'xs', children } = props;
|
||||
const { label, className, size = 'xs', disabled, children } = props;
|
||||
|
||||
const handleClick = () => copyToClipboard(children as string);
|
||||
|
||||
return (
|
||||
<Tooltip label={label} openDelay={tooltipDelayFast}>
|
||||
<ButtonGroup size={size} isAttached className={className}>
|
||||
<Button variant='ontime-subtle' tabIndex={-1}>
|
||||
<Button variant='ontime-subtle' tabIndex={-1} isDisabled={disabled}>
|
||||
{children}
|
||||
</Button>
|
||||
<IconButton aria-label={label} icon={<IoCopy />} variant='ontime-filled' tabIndex={-1} onClick={handleClick} />
|
||||
<IconButton
|
||||
aria-label={label}
|
||||
icon={<IoCopy />}
|
||||
variant='ontime-filled'
|
||||
tabIndex={-1}
|
||||
onClick={handleClick}
|
||||
isDisabled={disabled}
|
||||
/>
|
||||
</ButtonGroup>
|
||||
</Tooltip>
|
||||
);
|
||||
|
||||
@@ -6,21 +6,24 @@ import style from './SwatchSelect.module.scss';
|
||||
|
||||
interface SwatchProps {
|
||||
color: string;
|
||||
onClick: (color: string) => void;
|
||||
onClick?: (color: string) => void;
|
||||
isSelected?: boolean;
|
||||
}
|
||||
|
||||
export default function Swatch(props: SwatchProps) {
|
||||
const { color, isSelected, onClick } = props;
|
||||
|
||||
const classes = cx([style.swatch, isSelected ? style.selected : null]);
|
||||
const handleClick = () => {
|
||||
onClick?.(color);
|
||||
};
|
||||
const classes = cx([style.swatch, isSelected ? style.selected : null, onClick ? style.selectable : null]);
|
||||
|
||||
if (!color) {
|
||||
return (
|
||||
<div className={`${classes} ${style.center}`} onClick={() => onClick('')}>
|
||||
<div className={`${classes} ${style.center}`} onClick={handleClick}>
|
||||
<IoBan />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return <div className={classes} style={{ backgroundColor: `${color}` }} onClick={() => onClick(color)} />;
|
||||
return <div className={classes} style={{ backgroundColor: `${color}` }} onClick={handleClick} />;
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
}
|
||||
|
||||
.swatch {
|
||||
cursor: pointer;
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
aspect-ratio: 1;
|
||||
@@ -15,6 +14,10 @@
|
||||
&.selected {
|
||||
border: 2px solid $blue-500;
|
||||
}
|
||||
|
||||
&.selectable {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.center {
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { CustomFields } from 'ontime-types';
|
||||
|
||||
import { queryRefetchInterval } from '../../ontimeConfig';
|
||||
import { CUSTOM_FIELDS } from '../api/apiConstants';
|
||||
import { getCustomFields } from '../api/ontimeApi';
|
||||
|
||||
const placeholder: CustomFields = {};
|
||||
|
||||
export default function useCustomFields() {
|
||||
const { data, status, isFetching, isError, refetch } = useQuery({
|
||||
queryKey: CUSTOM_FIELDS,
|
||||
queryFn: getCustomFields,
|
||||
placeholderData: placeholder,
|
||||
retry: 5,
|
||||
retryDelay: (attempt) => attempt * 2500,
|
||||
refetchInterval: queryRefetchInterval,
|
||||
networkMode: 'always',
|
||||
});
|
||||
|
||||
return { data: data ?? placeholder, status, isFetching, isError, refetch };
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { isIPAddress, isOnlyNumbers, startsWithHttp, startsWithSlash } from '../regex';
|
||||
import { isAlphanumeric, isIPAddress, isNotEmpty, isOnlyNumbers, startsWithHttp, startsWithSlash } from '../regex';
|
||||
|
||||
describe('simple tests for regex', () => {
|
||||
test('isOnlyNumbers', () => {
|
||||
@@ -48,4 +48,28 @@ describe('simple tests for regex', () => {
|
||||
expect(startsWithSlash.test(t)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
test('isAlphanumeric', () => {
|
||||
const right = ['dsafdsafa9f9sdafdsSADFHASDF', '1231', '1', 'a', 'asdas1asdas', '11as', '1'];
|
||||
const wrong = ['with space', 'with @', '#'];
|
||||
|
||||
right.forEach((t) => {
|
||||
expect(isAlphanumeric.test(t)).toBe(true);
|
||||
});
|
||||
wrong.forEach((t) => {
|
||||
expect(isAlphanumeric.test(t)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
test('isNotEmpty', () => {
|
||||
const right = ['notempty'];
|
||||
const wrong = ['', ' '];
|
||||
|
||||
right.forEach((t) => {
|
||||
expect(isNotEmpty.test(t)).toBe(true);
|
||||
});
|
||||
wrong.forEach((t) => {
|
||||
expect(isNotEmpty.test(t)).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -8,7 +8,19 @@ import { OntimeEvent, SupportedEvent } from 'ontime-types';
|
||||
*/
|
||||
type ClonedEvent = Omit<
|
||||
OntimeEvent,
|
||||
'id' | 'cue' | 'user0' | 'user1' | 'user2' | 'user3' | 'user4' | 'user5' | 'user6' | 'user7' | 'user8' | 'user9'
|
||||
| 'id'
|
||||
| 'cue'
|
||||
| 'user0'
|
||||
| 'user1'
|
||||
| 'user2'
|
||||
| 'user3'
|
||||
| 'user4'
|
||||
| 'user5'
|
||||
| 'user6'
|
||||
| 'user7'
|
||||
| 'user8'
|
||||
| 'user9'
|
||||
| 'custom'
|
||||
>;
|
||||
export const cloneEvent = (event: OntimeEvent, after?: string): ClonedEvent => {
|
||||
return {
|
||||
|
||||
@@ -7,3 +7,5 @@ export const isOnlyNumbers = /^\d+$/;
|
||||
export const isIPAddress = /^((25[0-5]|(2[0-4]|1\d|[1-9]|)\d)\.?\b){4}$/;
|
||||
export const startsWithHttp = /^http:\/\//;
|
||||
export const startsWithSlash = /^\//;
|
||||
export const isAlphanumeric = /^[a-z0-9]+$/i;
|
||||
export const isNotEmpty = /\S/;
|
||||
|
||||
Reference in New Issue
Block a user