mirror of
https://github.com/diced/zipline.git
synced 2026-04-28 02:33:07 -07:00
fix: perf improvements
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import { Response } from '@/lib/api/response';
|
||||
import { fetchApi } from '@/lib/fetchApi';
|
||||
import useUser from '@/lib/client/hooks/useUser';
|
||||
import { useTitle } from '@/lib/client/hooks/useTitle';
|
||||
import {
|
||||
Button,
|
||||
@@ -18,8 +19,8 @@ import {
|
||||
import { useForm } from '@mantine/form';
|
||||
import { notifications, showNotification } from '@mantine/notifications';
|
||||
import { IconLogin, IconPlus, IconUserPlus, IconX } from '@tabler/icons-react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Link, useLocation, useNavigate } from 'react-router-dom';
|
||||
import { useEffect } from 'react';
|
||||
import { Link, Navigate, useLocation, useNavigate } from 'react-router-dom';
|
||||
import useSWR, { mutate } from 'swr';
|
||||
import GenericError from '../../error/GenericError';
|
||||
import { getWebClient } from '@/lib/api/detect';
|
||||
@@ -31,8 +32,6 @@ export function Component() {
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
const {
|
||||
data: config,
|
||||
error: configError,
|
||||
@@ -59,6 +58,8 @@ export function Component() {
|
||||
},
|
||||
);
|
||||
|
||||
const { user, loading: userLoading } = useUser();
|
||||
|
||||
const form = useForm({
|
||||
initialValues: {
|
||||
username: '',
|
||||
@@ -74,17 +75,6 @@ export function Component() {
|
||||
}),
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
const res = await fetch('/api/user');
|
||||
if (res.ok) {
|
||||
navigate('/dashboard');
|
||||
} else {
|
||||
setLoading(false);
|
||||
}
|
||||
})();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!config) return;
|
||||
|
||||
@@ -138,7 +128,11 @@ export function Component() {
|
||||
}
|
||||
};
|
||||
|
||||
if (loading || configLoading) return <LoadingOverlay visible />;
|
||||
if (userLoading || configLoading) return <LoadingOverlay visible />;
|
||||
|
||||
if (user) {
|
||||
return <Navigate to='/dashboard' replace />;
|
||||
}
|
||||
|
||||
if (!config || configError) {
|
||||
return (
|
||||
|
||||
@@ -1,10 +1,32 @@
|
||||
import type { File as DbFile } from '@/lib/db/models/file';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import useSWR from 'swr';
|
||||
import { isDbFile } from './useFileUrls';
|
||||
|
||||
const MAX_BYTES = 1 * 1024 * 1024;
|
||||
const FILE_BIG = '\n...\nThe file is too big to display click the download icon to view/download it.';
|
||||
|
||||
async function readBlobText(file: File) {
|
||||
const raw = await new Promise<string>((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.onerror = () => reject(new Error('Failed to read file'));
|
||||
reader.onload = () => resolve((reader.result ?? '') as string);
|
||||
reader.readAsText(file);
|
||||
});
|
||||
|
||||
return raw.length > MAX_BYTES ? raw.slice(0, MAX_BYTES) + FILE_BIG : raw;
|
||||
}
|
||||
|
||||
async function readText(fileUrl: string) {
|
||||
const res = await fetch(fileUrl, {
|
||||
headers: {
|
||||
Range: `bytes=0-${MAX_BYTES}`,
|
||||
},
|
||||
});
|
||||
|
||||
if (!res.ok) throw new Error('Failed to fetch file');
|
||||
return await res.text();
|
||||
}
|
||||
|
||||
export default function useFileContent({
|
||||
enabled,
|
||||
file,
|
||||
@@ -14,41 +36,32 @@ export default function useFileContent({
|
||||
file: DbFile | File;
|
||||
fileUrl: string;
|
||||
}) {
|
||||
const [content, setContent] = useState('');
|
||||
const { data, error } = useSWR<string>(
|
||||
() => {
|
||||
if (!enabled) return null;
|
||||
|
||||
const loadText = useCallback(async () => {
|
||||
try {
|
||||
if (!isDbFile(file)) {
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => {
|
||||
const raw = reader.result as string;
|
||||
setContent(raw.length > MAX_BYTES ? raw.slice(0, MAX_BYTES) + FILE_BIG : raw);
|
||||
};
|
||||
reader.readAsText(file as File);
|
||||
return;
|
||||
}
|
||||
if (isDbFile(file)) return ['dbfile', file.id] as const;
|
||||
|
||||
const f = file as File;
|
||||
return ['blobfile', f.name] as const;
|
||||
},
|
||||
async () => {
|
||||
if (!isDbFile(file)) return readBlobText(file as File);
|
||||
|
||||
if (file.size > MAX_BYTES) {
|
||||
const res = await fetch(fileUrl, { headers: { Range: `bytes=0-${MAX_BYTES}` } });
|
||||
if (!res.ok) throw new Error('Failed to fetch file');
|
||||
const text = await res.text();
|
||||
setContent(text + FILE_BIG);
|
||||
return;
|
||||
const text = await readText(fileUrl);
|
||||
return text + FILE_BIG;
|
||||
}
|
||||
|
||||
const res = await fetch(fileUrl);
|
||||
if (!res.ok) throw new Error('Failed to fetch file');
|
||||
const text = await res.text();
|
||||
setContent(text);
|
||||
} catch {
|
||||
setContent('Error loading file.');
|
||||
}
|
||||
}, [file, fileUrl]);
|
||||
return readText(fileUrl);
|
||||
},
|
||||
{
|
||||
revalidateOnFocus: false,
|
||||
shouldRetryOnError: false,
|
||||
},
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!enabled) return;
|
||||
loadText();
|
||||
}, [enabled, loadText]);
|
||||
if (error) return 'Error loading file.';
|
||||
|
||||
return content;
|
||||
return data ?? '';
|
||||
}
|
||||
|
||||
@@ -189,7 +189,7 @@ export default function DashboardServerSettings() {
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const { data, isLoading } = useSWR<Response['/api/server/settings']>('/api/server/settings');
|
||||
const { data } = useSWR<Response['/api/server/settings']>('/api/server/settings');
|
||||
const [opened, { toggle }] = useDisclosure(false);
|
||||
|
||||
const toSettingSection = useCallback((settingKey: string) => {
|
||||
@@ -328,7 +328,7 @@ export default function DashboardServerSettings() {
|
||||
</Box>
|
||||
}
|
||||
>
|
||||
<SettingsComponent swr={{ data, isLoading }} />
|
||||
<SettingsComponent />
|
||||
</Suspense>
|
||||
</Box>
|
||||
) : (
|
||||
|
||||
@@ -1,27 +1,34 @@
|
||||
import { Response } from '@/lib/api/response';
|
||||
import type { Response } from '@/lib/api/response';
|
||||
import { Button, LoadingOverlay, Stack, Switch, TextInput } from '@mantine/core';
|
||||
import { useForm } from '@mantine/form';
|
||||
import { IconDeviceFloppy } from '@tabler/icons-react';
|
||||
import { useEffect } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { settingsOnSubmit } from '../settingsOnSubmit';
|
||||
import useServerSettings from '../useServerSettings';
|
||||
|
||||
export default function Chunks({
|
||||
swr: { data, isLoading },
|
||||
}: {
|
||||
swr: { data: Response['/api/server/settings'] | undefined; isLoading: boolean };
|
||||
}) {
|
||||
export default function Chunks() {
|
||||
const { data, isLoading } = useServerSettings();
|
||||
|
||||
return (
|
||||
<>
|
||||
<LoadingOverlay visible={isLoading} bdrs='md' />
|
||||
{data ? <Form data={data} isLoading={isLoading} /> : null}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function Form({ data, isLoading }: { data: Response['/api/server/settings']; isLoading: boolean }) {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const form = useForm({
|
||||
initialValues: {
|
||||
chunksEnabled: true,
|
||||
chunksMax: '95mb',
|
||||
chunksSize: '25mb',
|
||||
chunksEnabled: data.settings.chunksEnabled,
|
||||
chunksMax: data.settings.chunksMax,
|
||||
chunksSize: data.settings.chunksSize,
|
||||
},
|
||||
enhanceGetInputProps: (payload: any): object => ({
|
||||
disabled:
|
||||
data?.tampered?.includes(payload.field) ||
|
||||
data.tampered.includes(payload.field) ||
|
||||
(payload.field !== 'chunksEnabled' && !form.values.chunksEnabled) ||
|
||||
false,
|
||||
}),
|
||||
@@ -29,49 +36,35 @@ export default function Chunks({
|
||||
|
||||
const onSubmit = settingsOnSubmit(navigate, form);
|
||||
|
||||
useEffect(() => {
|
||||
if (!data) return;
|
||||
|
||||
form.setValues({
|
||||
chunksEnabled: data.settings.chunksEnabled ?? true,
|
||||
chunksMax: data.settings.chunksMax ?? '',
|
||||
chunksSize: data.settings.chunksSize ?? '',
|
||||
});
|
||||
}, [data]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<LoadingOverlay visible={isLoading} bdrs='md' />
|
||||
<form onSubmit={form.onSubmit(onSubmit)}>
|
||||
<Stack gap='lg'>
|
||||
<Switch
|
||||
label='Enable Chunks'
|
||||
description='Enable chunked uploads.'
|
||||
{...form.getInputProps('chunksEnabled', { type: 'checkbox' })}
|
||||
/>
|
||||
|
||||
<form onSubmit={form.onSubmit(onSubmit)}>
|
||||
<Stack gap='lg'>
|
||||
<Switch
|
||||
label='Enable Chunks'
|
||||
description='Enable chunked uploads.'
|
||||
{...form.getInputProps('chunksEnabled', { type: 'checkbox' })}
|
||||
/>
|
||||
<TextInput
|
||||
label='Max Chunk Size'
|
||||
description='Maximum size of an upload before it is split into chunks.'
|
||||
placeholder='95mb'
|
||||
disabled={!form.values.chunksEnabled}
|
||||
{...form.getInputProps('chunksMax')}
|
||||
/>
|
||||
|
||||
<TextInput
|
||||
label='Max Chunk Size'
|
||||
description='Maximum size of an upload before it is split into chunks.'
|
||||
placeholder='95mb'
|
||||
disabled={!form.values.chunksEnabled}
|
||||
{...form.getInputProps('chunksMax')}
|
||||
/>
|
||||
<TextInput
|
||||
label='Chunk Size'
|
||||
description='Size of each chunk.'
|
||||
placeholder='25mb'
|
||||
disabled={!form.values.chunksEnabled}
|
||||
{...form.getInputProps('chunksSize')}
|
||||
/>
|
||||
</Stack>
|
||||
|
||||
<TextInput
|
||||
label='Chunk Size'
|
||||
description='Size of each chunk.'
|
||||
placeholder='25mb'
|
||||
disabled={!form.values.chunksEnabled}
|
||||
{...form.getInputProps('chunksSize')}
|
||||
/>
|
||||
</Stack>
|
||||
|
||||
<Button type='submit' mt='md' loading={isLoading} leftSection={<IconDeviceFloppy size='1rem' />}>
|
||||
Save
|
||||
</Button>
|
||||
</form>
|
||||
</>
|
||||
<Button type='submit' mt='md' loading={isLoading} leftSection={<IconDeviceFloppy size='1rem' />}>
|
||||
Save
|
||||
</Button>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,32 +1,34 @@
|
||||
import { Response } from '@/lib/api/response';
|
||||
import type { Response } from '@/lib/api/response';
|
||||
import { Button, LoadingOverlay, Stack, Switch, TextInput } from '@mantine/core';
|
||||
import { useForm } from '@mantine/form';
|
||||
import { IconDeviceFloppy } from '@tabler/icons-react';
|
||||
import { useEffect } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { settingsOnSubmit } from '../settingsOnSubmit';
|
||||
import useServerSettings from '../useServerSettings';
|
||||
|
||||
export default function Core({
|
||||
swr: { data, isLoading },
|
||||
}: {
|
||||
swr: { data: Response['/api/server/settings'] | undefined; isLoading: boolean };
|
||||
}) {
|
||||
export default function Core() {
|
||||
const { data, isLoading } = useServerSettings();
|
||||
|
||||
return (
|
||||
<>
|
||||
<LoadingOverlay visible={isLoading} />
|
||||
{data ? <Form data={data} isLoading={isLoading} /> : null}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function Form({ data, isLoading }: { data: Response['/api/server/settings']; isLoading: boolean }) {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const form = useForm<{
|
||||
coreReturnHttpsUrls: boolean;
|
||||
coreDefaultDomain: string | null | undefined;
|
||||
coreTempDirectory: string;
|
||||
coreTrustProxy: boolean;
|
||||
}>({
|
||||
const form = useForm({
|
||||
initialValues: {
|
||||
coreReturnHttpsUrls: false,
|
||||
coreDefaultDomain: '',
|
||||
coreTempDirectory: '/tmp/zipline',
|
||||
coreTrustProxy: false,
|
||||
coreReturnHttpsUrls: data.settings.coreReturnHttpsUrls,
|
||||
coreDefaultDomain: data.settings.coreDefaultDomain,
|
||||
coreTempDirectory: data.settings.coreTempDirectory,
|
||||
coreTrustProxy: data.settings.coreTrustProxy,
|
||||
},
|
||||
enhanceGetInputProps: (payload) => ({
|
||||
disabled: data?.tampered?.includes(payload.field) || false,
|
||||
disabled: data.tampered.includes(payload.field) || false,
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -40,55 +42,40 @@ export default function Core({
|
||||
return settingsOnSubmit(navigate, form)(values);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!data) return;
|
||||
|
||||
form.setValues({
|
||||
coreReturnHttpsUrls: data.settings.coreReturnHttpsUrls ?? false,
|
||||
coreDefaultDomain: data.settings.coreDefaultDomain ?? '',
|
||||
coreTempDirectory: data.settings.coreTempDirectory ?? '/tmp/zipline',
|
||||
coreTrustProxy: data.settings.coreTrustProxy ?? false,
|
||||
});
|
||||
}, [data]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<LoadingOverlay visible={isLoading} />
|
||||
<form onSubmit={form.onSubmit(onSubmit)}>
|
||||
<Stack gap='lg'>
|
||||
<Switch
|
||||
mt='md'
|
||||
label='Return HTTPS URLs'
|
||||
description='Return URLs with HTTPS protocol.'
|
||||
{...form.getInputProps('coreReturnHttpsUrls', { type: 'checkbox' })}
|
||||
/>
|
||||
|
||||
<form onSubmit={form.onSubmit(onSubmit)}>
|
||||
<Stack gap='lg'>
|
||||
<Switch
|
||||
mt='md'
|
||||
label='Return HTTPS URLs'
|
||||
description='Return URLs with HTTPS protocol.'
|
||||
{...form.getInputProps('coreReturnHttpsUrls', { type: 'checkbox' })}
|
||||
/>
|
||||
<Switch
|
||||
label='Trust Proxies'
|
||||
description='Trust the X-Forwarded-* headers set by proxies. Only enable this if you are behind a trusted proxy (nginx, caddy, etc.). Requires a server restart.'
|
||||
{...form.getInputProps('coreTrustProxy', { type: 'checkbox' })}
|
||||
/>
|
||||
|
||||
<Switch
|
||||
label='Trust Proxies'
|
||||
description='Trust the X-Forwarded-* headers set by proxies. Only enable this if you are behind a trusted proxy (nginx, caddy, etc.). Requires a server restart.'
|
||||
{...form.getInputProps('coreTrustProxy', { type: 'checkbox' })}
|
||||
/>
|
||||
<TextInput
|
||||
label='Default Domain'
|
||||
description='The domain to use when generating URLs. This value should not include the protocol.'
|
||||
placeholder='example.com'
|
||||
{...form.getInputProps('coreDefaultDomain')}
|
||||
/>
|
||||
|
||||
<TextInput
|
||||
label='Default Domain'
|
||||
description='The domain to use when generating URLs. This value should not include the protocol.'
|
||||
placeholder='example.com'
|
||||
{...form.getInputProps('coreDefaultDomain')}
|
||||
/>
|
||||
<TextInput
|
||||
label='Temporary Directory'
|
||||
description='The directory to store temporary files. If the path is invalid, certain functions may break. Requires a server restart.'
|
||||
placeholder='/tmp/zipline'
|
||||
{...form.getInputProps('coreTempDirectory')}
|
||||
/>
|
||||
</Stack>
|
||||
|
||||
<TextInput
|
||||
label='Temporary Directory'
|
||||
description='The directory to store temporary files. If the path is invalid, certain functions may break. Requires a server restart.'
|
||||
placeholder='/tmp/zipline'
|
||||
{...form.getInputProps('coreTempDirectory')}
|
||||
/>
|
||||
</Stack>
|
||||
|
||||
<Button type='submit' mt='md' loading={isLoading} leftSection={<IconDeviceFloppy size='1rem' />}>
|
||||
Save
|
||||
</Button>
|
||||
</form>
|
||||
</>
|
||||
<Button type='submit' mt='md' loading={isLoading} leftSection={<IconDeviceFloppy size='1rem' />}>
|
||||
Save
|
||||
</Button>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Response } from '@/lib/api/response';
|
||||
import type { Response } from '@/lib/api/response';
|
||||
import {
|
||||
Button,
|
||||
Collapse,
|
||||
@@ -13,24 +13,31 @@ import {
|
||||
} from '@mantine/core';
|
||||
import { useForm } from '@mantine/form';
|
||||
import { IconDeviceFloppy } from '@tabler/icons-react';
|
||||
import { useEffect } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { settingsOnSubmit } from '../settingsOnSubmit';
|
||||
import useServerSettings from '../useServerSettings';
|
||||
|
||||
type DiscordEmbed = Record<string, any>;
|
||||
|
||||
export default function Discord({
|
||||
swr: { data, isLoading },
|
||||
}: {
|
||||
swr: { data: Response['/api/server/settings'] | undefined; isLoading: boolean };
|
||||
}) {
|
||||
export default function Discord() {
|
||||
const { data, isLoading } = useServerSettings();
|
||||
|
||||
return (
|
||||
<>
|
||||
<LoadingOverlay visible={isLoading} />
|
||||
{data ? <Form data={data} isLoading={isLoading} /> : null}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function Form({ data, isLoading }: { data: Response['/api/server/settings']; isLoading: boolean }) {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const formMain = useForm({
|
||||
initialValues: {
|
||||
discordWebhookUrl: '',
|
||||
discordUsername: '',
|
||||
discordAvatarUrl: '',
|
||||
discordWebhookUrl: data.settings.discordWebhookUrl,
|
||||
discordUsername: data.settings.discordUsername,
|
||||
discordAvatarUrl: data.settings.discordAvatarUrl,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -49,42 +56,46 @@ export default function Discord({
|
||||
|
||||
const formOnUpload = useForm({
|
||||
initialValues: {
|
||||
discordOnUploadWebhookUrl: '',
|
||||
discordOnUploadUsername: '',
|
||||
discordOnUploadAvatarUrl: '',
|
||||
discordOnUploadWebhookUrl: data.settings.discordOnUploadWebhookUrl,
|
||||
discordOnUploadUsername: data.settings.discordOnUploadUsername,
|
||||
discordOnUploadAvatarUrl: data.settings.discordOnUploadAvatarUrl,
|
||||
|
||||
discordOnUploadContent: '',
|
||||
discordOnUploadContent: data.settings.discordOnUploadContent,
|
||||
|
||||
discordOnUploadEmbed: false,
|
||||
discordOnUploadEmbedTitle: '',
|
||||
discordOnUploadEmbedDescription: '',
|
||||
discordOnUploadEmbedFooter: '',
|
||||
discordOnUploadEmbedColor: '',
|
||||
discordOnUploadEmbedThumbnail: false,
|
||||
discordOnUploadEmbedImageOrVideo: false,
|
||||
discordOnUploadEmbedTimestamp: false,
|
||||
discordOnUploadEmbedUrl: false,
|
||||
discordOnUploadEmbed: Boolean(data.settings.discordOnUploadEmbed),
|
||||
discordOnUploadEmbedTitle: (data.settings.discordOnUploadEmbed as DiscordEmbed | null)?.title || '',
|
||||
discordOnUploadEmbedDescription:
|
||||
(data.settings.discordOnUploadEmbed as DiscordEmbed | null)?.description || '',
|
||||
discordOnUploadEmbedFooter: (data.settings.discordOnUploadEmbed as DiscordEmbed | null)?.footer || '',
|
||||
discordOnUploadEmbedColor: (data.settings.discordOnUploadEmbed as DiscordEmbed | null)?.color || '',
|
||||
discordOnUploadEmbedThumbnail: !!(data.settings.discordOnUploadEmbed as DiscordEmbed | null)?.thumbnail,
|
||||
discordOnUploadEmbedImageOrVideo: !!(data.settings.discordOnUploadEmbed as DiscordEmbed | null)
|
||||
?.imageOrVideo,
|
||||
discordOnUploadEmbedTimestamp: !!(data.settings.discordOnUploadEmbed as DiscordEmbed | null)?.timestamp,
|
||||
discordOnUploadEmbedUrl: !!(data.settings.discordOnUploadEmbed as DiscordEmbed | null)?.url,
|
||||
},
|
||||
enhanceGetInputProps: (payload) => ({
|
||||
disabled: data?.tampered?.includes(payload.field) || false,
|
||||
disabled: data.tampered.includes(payload.field) || false,
|
||||
}),
|
||||
});
|
||||
|
||||
const formOnShorten = useForm({
|
||||
initialValues: {
|
||||
discordOnShortenWebhookUrl: '',
|
||||
discordOnShortenUsername: '',
|
||||
discordOnShortenAvatarUrl: '',
|
||||
discordOnShortenWebhookUrl: data.settings.discordOnShortenWebhookUrl,
|
||||
discordOnShortenUsername: data.settings.discordOnShortenUsername,
|
||||
discordOnShortenAvatarUrl: data.settings.discordOnShortenAvatarUrl,
|
||||
|
||||
discordOnShortenContent: '',
|
||||
discordOnShortenContent: data.settings.discordOnShortenContent,
|
||||
|
||||
discordOnShortenEmbed: false,
|
||||
discordOnShortenEmbedTitle: '',
|
||||
discordOnShortenEmbedDescription: '',
|
||||
discordOnShortenEmbedFooter: '',
|
||||
discordOnShortenEmbedColor: '',
|
||||
discordOnShortenEmbedTimestamp: false,
|
||||
discordOnShortenEmbedUrl: false,
|
||||
discordOnShortenEmbed: Boolean(data.settings.discordOnShortenEmbed),
|
||||
discordOnShortenEmbedTitle: (data.settings.discordOnShortenEmbed as DiscordEmbed | null)?.title || '',
|
||||
discordOnShortenEmbedDescription:
|
||||
(data.settings.discordOnShortenEmbed as DiscordEmbed | null)?.description || '',
|
||||
discordOnShortenEmbedFooter: (data.settings.discordOnShortenEmbed as DiscordEmbed | null)?.footer || '',
|
||||
discordOnShortenEmbedColor: (data.settings.discordOnShortenEmbed as DiscordEmbed | null)?.color || '',
|
||||
discordOnShortenEmbedTimestamp: !!(data.settings.discordOnShortenEmbed as DiscordEmbed | null)
|
||||
?.timestamp,
|
||||
discordOnShortenEmbedUrl: !!(data.settings.discordOnShortenEmbed as DiscordEmbed | null)?.url,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -123,56 +134,8 @@ export default function Discord({
|
||||
return settingsOnSubmit(navigate, type === 'upload' ? formOnUpload : formOnShorten)(sendValues);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!data) return;
|
||||
|
||||
formMain.setValues({
|
||||
discordWebhookUrl: data.settings.discordWebhookUrl ?? '',
|
||||
discordUsername: data.settings.discordUsername ?? '',
|
||||
discordAvatarUrl: data.settings.discordAvatarUrl ?? '',
|
||||
});
|
||||
|
||||
formOnUpload.setValues({
|
||||
discordOnUploadWebhookUrl: data.settings.discordOnUploadWebhookUrl ?? '',
|
||||
discordOnUploadUsername: data.settings.discordOnUploadUsername ?? '',
|
||||
discordOnUploadAvatarUrl: data.settings.discordOnUploadAvatarUrl ?? '',
|
||||
|
||||
discordOnUploadContent: data.settings.discordOnUploadContent ?? '',
|
||||
discordOnUploadEmbed: data.settings.discordOnUploadEmbed ? true : false,
|
||||
discordOnUploadEmbedTitle: (data.settings.discordOnUploadEmbed as DiscordEmbed)?.title ?? '',
|
||||
discordOnUploadEmbedDescription:
|
||||
(data.settings.discordOnUploadEmbed as DiscordEmbed)?.description ?? '',
|
||||
discordOnUploadEmbedFooter: (data.settings.discordOnUploadEmbed as DiscordEmbed)?.footer ?? '',
|
||||
discordOnUploadEmbedColor: (data.settings.discordOnUploadEmbed as DiscordEmbed)?.color ?? '',
|
||||
discordOnUploadEmbedThumbnail: (data.settings.discordOnUploadEmbed as DiscordEmbed)?.thumbnail ?? false,
|
||||
discordOnUploadEmbedImageOrVideo:
|
||||
(data.settings.discordOnUploadEmbed as DiscordEmbed)?.imageOrVideo ?? false,
|
||||
discordOnUploadEmbedTimestamp: (data.settings.discordOnUploadEmbed as DiscordEmbed)?.timestamp ?? false,
|
||||
discordOnUploadEmbedUrl: (data.settings.discordOnUploadEmbed as DiscordEmbed)?.url ?? false,
|
||||
});
|
||||
|
||||
formOnShorten.setValues({
|
||||
discordOnShortenWebhookUrl: data.settings.discordOnShortenWebhookUrl ?? '',
|
||||
discordOnShortenUsername: data.settings.discordOnShortenUsername ?? '',
|
||||
discordOnShortenAvatarUrl: data.settings.discordOnShortenAvatarUrl ?? '',
|
||||
|
||||
discordOnShortenContent: data.settings.discordOnShortenContent ?? '',
|
||||
discordOnShortenEmbed: data.settings.discordOnShortenEmbed ? true : false,
|
||||
discordOnShortenEmbedTitle: (data.settings.discordOnShortenEmbed as DiscordEmbed)?.title ?? '',
|
||||
discordOnShortenEmbedDescription:
|
||||
(data.settings.discordOnShortenEmbed as DiscordEmbed)?.description ?? '',
|
||||
discordOnShortenEmbedFooter: (data.settings.discordOnShortenEmbed as DiscordEmbed)?.footer ?? '',
|
||||
discordOnShortenEmbedColor: (data.settings.discordOnShortenEmbed as DiscordEmbed)?.color ?? '',
|
||||
discordOnShortenEmbedTimestamp:
|
||||
(data.settings.discordOnShortenEmbed as DiscordEmbed)?.timestamp ?? false,
|
||||
discordOnShortenEmbedUrl: (data.settings.discordOnShortenEmbed as DiscordEmbed)?.url ?? false,
|
||||
});
|
||||
}, [data]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<LoadingOverlay visible={isLoading} />
|
||||
|
||||
<form onSubmit={formMain.onSubmit(onSubmitMain)}>
|
||||
<TextInput
|
||||
label='Webhook URL'
|
||||
|
||||
@@ -1,19 +1,24 @@
|
||||
import { Response } from '@/lib/api/response';
|
||||
import type { Response } from '@/lib/api/response';
|
||||
import { ActionIcon, LoadingOverlay, Paper, Table, Text, TextInput } from '@mantine/core';
|
||||
import { useForm } from '@mantine/form';
|
||||
import { IconPlus, IconTrash } from '@tabler/icons-react';
|
||||
import { useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { settingsOnSubmit } from '../settingsOnSubmit';
|
||||
import useServerSettings from '../useServerSettings';
|
||||
|
||||
export default function Domains({
|
||||
swr: { data, isLoading },
|
||||
}: {
|
||||
swr: {
|
||||
data: Response['/api/server/settings'] | undefined;
|
||||
isLoading: boolean;
|
||||
};
|
||||
}) {
|
||||
export default function Domains() {
|
||||
const { data, isLoading } = useServerSettings();
|
||||
|
||||
return (
|
||||
<>
|
||||
<LoadingOverlay visible={isLoading} />
|
||||
{data ? <Form data={data} /> : null}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function Form({ data }: { data: Response['/api/server/settings'] }) {
|
||||
const navigate = useNavigate();
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
|
||||
@@ -24,7 +29,7 @@ export default function Domains({
|
||||
|
||||
const submitSettings = settingsOnSubmit(navigate, form);
|
||||
|
||||
const domains = Array.isArray(data?.settings.domains) ? data!.settings.domains.map(String) : [];
|
||||
const domains = data.settings.domains.map(String);
|
||||
|
||||
async function updateDomains(nextDomains: string[]) {
|
||||
setSubmitting(true);
|
||||
@@ -56,7 +61,7 @@ export default function Domains({
|
||||
|
||||
return (
|
||||
<>
|
||||
<LoadingOverlay visible={isLoading || submitting} />
|
||||
<LoadingOverlay visible={submitting} />
|
||||
|
||||
<form onSubmit={addDomain}>
|
||||
<TextInput
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Response } from '@/lib/api/response';
|
||||
import type { Response } from '@/lib/api/response';
|
||||
import {
|
||||
Anchor,
|
||||
Button,
|
||||
@@ -13,191 +13,175 @@ import {
|
||||
} from '@mantine/core';
|
||||
import { useForm } from '@mantine/form';
|
||||
import { IconDeviceFloppy } from '@tabler/icons-react';
|
||||
import { useEffect } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { settingsOnSubmit } from '../settingsOnSubmit';
|
||||
import useServerSettings from '../useServerSettings';
|
||||
|
||||
export default function Features({
|
||||
swr: { data, isLoading },
|
||||
}: {
|
||||
swr: { data: Response['/api/server/settings'] | undefined; isLoading: boolean };
|
||||
}) {
|
||||
export default function Features() {
|
||||
const { data, isLoading } = useServerSettings();
|
||||
|
||||
return (
|
||||
<>
|
||||
<LoadingOverlay visible={isLoading} />
|
||||
{data ? <Form data={data} isLoading={isLoading} /> : null}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function Form({ data, isLoading }: { data: Response['/api/server/settings']; isLoading: boolean }) {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const form = useForm({
|
||||
initialValues: {
|
||||
featuresImageCompression: true,
|
||||
featuresRobotsTxt: true,
|
||||
featuresHealthcheck: true,
|
||||
featuresUserRegistration: false,
|
||||
featuresOauthRegistration: true,
|
||||
featuresDeleteOnMaxViews: true,
|
||||
featuresThumbnailsEnabled: true,
|
||||
featuresThumbnailsNumberThreads: 4,
|
||||
featuresThumbnailsFormat: 'jpg',
|
||||
featuresThumbnailsInstantaneous: false,
|
||||
featuresMetricsEnabled: true,
|
||||
featuresMetricsAdminOnly: false,
|
||||
featuresMetricsShowUserSpecific: true,
|
||||
featuresVersionChecking: true,
|
||||
featuresVersionAPI: 'https://zipline-version.diced.sh/',
|
||||
featuresImageCompression: data.settings.featuresImageCompression,
|
||||
featuresRobotsTxt: data.settings.featuresRobotsTxt,
|
||||
featuresHealthcheck: data.settings.featuresHealthcheck,
|
||||
featuresUserRegistration: data.settings.featuresUserRegistration,
|
||||
featuresOauthRegistration: data.settings.featuresOauthRegistration,
|
||||
featuresDeleteOnMaxViews: data.settings.featuresDeleteOnMaxViews,
|
||||
|
||||
featuresThumbnailsEnabled: data.settings.featuresThumbnailsEnabled,
|
||||
featuresThumbnailsNumberThreads: data.settings.featuresThumbnailsNumberThreads,
|
||||
featuresThumbnailsFormat: data.settings.featuresThumbnailsFormat,
|
||||
featuresThumbnailsInstantaneous: data.settings.featuresThumbnailsInstantaneous,
|
||||
|
||||
featuresMetricsEnabled: data.settings.featuresMetricsEnabled,
|
||||
featuresMetricsAdminOnly: data.settings.featuresMetricsAdminOnly,
|
||||
featuresMetricsShowUserSpecific: data.settings.featuresMetricsShowUserSpecific,
|
||||
|
||||
featuresVersionChecking: data.settings.featuresVersionChecking,
|
||||
featuresVersionAPI: data.settings.featuresVersionAPI,
|
||||
},
|
||||
enhanceGetInputProps: (payload) => ({
|
||||
disabled: data?.tampered?.includes(payload.field) || false,
|
||||
disabled: data.tampered.includes(payload.field) || false,
|
||||
}),
|
||||
});
|
||||
|
||||
const onSubmit = settingsOnSubmit(navigate, form);
|
||||
|
||||
useEffect(() => {
|
||||
if (!data) return;
|
||||
|
||||
form.setValues({
|
||||
featuresImageCompression: data.settings.featuresImageCompression ?? true,
|
||||
featuresRobotsTxt: data.settings.featuresRobotsTxt ?? true,
|
||||
featuresHealthcheck: data.settings.featuresHealthcheck ?? true,
|
||||
featuresUserRegistration: data.settings.featuresUserRegistration ?? false,
|
||||
featuresOauthRegistration: data.settings.featuresOauthRegistration ?? true,
|
||||
featuresDeleteOnMaxViews: data.settings.featuresDeleteOnMaxViews ?? true,
|
||||
featuresThumbnailsEnabled: data.settings.featuresThumbnailsEnabled ?? true,
|
||||
featuresThumbnailsNumberThreads: data.settings.featuresThumbnailsNumberThreads ?? 4,
|
||||
featuresThumbnailsFormat: data.settings.featuresThumbnailsFormat ?? 'jpg',
|
||||
featuresThumbnailsInstantaneous: data.settings.featuresThumbnailsInstantaneous ?? false,
|
||||
featuresMetricsEnabled: data.settings.featuresMetricsEnabled ?? true,
|
||||
featuresMetricsAdminOnly: data.settings.featuresMetricsAdminOnly ?? false,
|
||||
featuresMetricsShowUserSpecific: data.settings.featuresMetricsShowUserSpecific ?? true,
|
||||
featuresVersionChecking: data.settings.featuresVersionChecking ?? true,
|
||||
featuresVersionAPI: data.settings.featuresVersionAPI ?? 'https://zipline-version.diced.sh/',
|
||||
});
|
||||
}, [data]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<LoadingOverlay visible={isLoading} />
|
||||
<form onSubmit={form.onSubmit(onSubmit)}>
|
||||
<Stack gap='lg'>
|
||||
<Switch
|
||||
label='Image Compression'
|
||||
description='Allows the ability for users to compress images.'
|
||||
{...form.getInputProps('featuresImageCompression', { type: 'checkbox' })}
|
||||
/>
|
||||
|
||||
<form onSubmit={form.onSubmit(onSubmit)}>
|
||||
<Stack gap='lg'>
|
||||
<Switch
|
||||
label='/robots.txt'
|
||||
description='Enables a /robots.txt to stop search crawlers. Requires a server restart.'
|
||||
{...form.getInputProps('featuresRobotsTxt', { type: 'checkbox' })}
|
||||
/>
|
||||
|
||||
<Switch
|
||||
label='Healthcheck'
|
||||
description='Enables a healthcheck route for uptime monitoring. Requires a server restart.'
|
||||
{...form.getInputProps('featuresHealthcheck', { type: 'checkbox' })}
|
||||
/>
|
||||
|
||||
<Switch
|
||||
label='User Registration'
|
||||
description='Allows users to register an account on the server.'
|
||||
{...form.getInputProps('featuresUserRegistration', { type: 'checkbox' })}
|
||||
/>
|
||||
|
||||
<Switch
|
||||
label='OAuth Registration'
|
||||
description='Allows users to register an account using OAuth providers.'
|
||||
{...form.getInputProps('featuresOauthRegistration', { type: 'checkbox' })}
|
||||
/>
|
||||
|
||||
<Switch
|
||||
label='Delete on Max Views'
|
||||
description='Automatically deletes files/urls after they reach the maximum view count. Requires a server restart.'
|
||||
{...form.getInputProps('featuresDeleteOnMaxViews', { type: 'checkbox' })}
|
||||
/>
|
||||
|
||||
<Switch
|
||||
label='Enable Metrics'
|
||||
description='Enables metrics for the server. Requires a server restart.'
|
||||
{...form.getInputProps('featuresMetricsEnabled', { type: 'checkbox' })}
|
||||
/>
|
||||
|
||||
<Switch
|
||||
label='Admin Only Metrics'
|
||||
description='Requires an administrator to view metrics.'
|
||||
{...form.getInputProps('featuresMetricsAdminOnly', { type: 'checkbox' })}
|
||||
/>
|
||||
|
||||
<Switch
|
||||
label='Show User Specific Metrics'
|
||||
description='Shows metrics specific to each user, for all users.'
|
||||
{...form.getInputProps('featuresMetricsShowUserSpecific', { type: 'checkbox' })}
|
||||
/>
|
||||
|
||||
<Divider label='Thumbnails' />
|
||||
|
||||
<SimpleGrid cols={{ base: 1, md: 2 }} spacing='lg'>
|
||||
<Switch
|
||||
label='Image Compression'
|
||||
description='Allows the ability for users to compress images.'
|
||||
{...form.getInputProps('featuresImageCompression', { type: 'checkbox' })}
|
||||
label='Enable Thumbnails'
|
||||
description='Enables thumbnail generation for images. Requires a server restart.'
|
||||
{...form.getInputProps('featuresThumbnailsEnabled', { type: 'checkbox' })}
|
||||
/>
|
||||
|
||||
<Switch
|
||||
label='/robots.txt'
|
||||
description='Enables a /robots.txt to stop search crawlers. Requires a server restart.'
|
||||
{...form.getInputProps('featuresRobotsTxt', { type: 'checkbox' })}
|
||||
label='Instantaneous Thumbnails'
|
||||
description='Generates thumbnails immediately after a file is uploaded, instead of waiting for the task to run.'
|
||||
{...form.getInputProps('featuresThumbnailsInstantaneous', { type: 'checkbox' })}
|
||||
/>
|
||||
</SimpleGrid>
|
||||
|
||||
<Switch
|
||||
label='Healthcheck'
|
||||
description='Enables a healthcheck route for uptime monitoring. Requires a server restart.'
|
||||
{...form.getInputProps('featuresHealthcheck', { type: 'checkbox' })}
|
||||
/>
|
||||
<NumberInput
|
||||
label='Thumbnails Number Threads'
|
||||
description='Number of threads to use for thumbnail generation, usually the number of CPU threads. Requires a server restart.'
|
||||
placeholder='Enter a number...'
|
||||
min={1}
|
||||
max={16}
|
||||
{...form.getInputProps('featuresThumbnailsNumberThreads')}
|
||||
/>
|
||||
|
||||
<Switch
|
||||
label='User Registration'
|
||||
description='Allows users to register an account on the server.'
|
||||
{...form.getInputProps('featuresUserRegistration', { type: 'checkbox' })}
|
||||
/>
|
||||
<Select
|
||||
label='Thumbnails Format'
|
||||
description='The output format for thumbnails. Requires a server restart.'
|
||||
data={[
|
||||
{ value: 'jpg', label: '.jpg' },
|
||||
{ value: 'png', label: '.png' },
|
||||
{ value: 'webp', label: '.webp' },
|
||||
]}
|
||||
{...form.getInputProps('featuresThumbnailsFormat')}
|
||||
/>
|
||||
|
||||
<Switch
|
||||
label='OAuth Registration'
|
||||
description='Allows users to register an account using OAuth providers.'
|
||||
{...form.getInputProps('featuresOauthRegistration', { type: 'checkbox' })}
|
||||
/>
|
||||
<Divider label='Version Checking' />
|
||||
|
||||
<Switch
|
||||
label='Delete on Max Views'
|
||||
description='Automatically deletes files/urls after they reach the maximum view count. Requires a server restart.'
|
||||
{...form.getInputProps('featuresDeleteOnMaxViews', { type: 'checkbox' })}
|
||||
/>
|
||||
<Switch
|
||||
label='Version Checking'
|
||||
description='Enable version checking for the server. This will check for updates and display the status on the sidebar to all users.'
|
||||
{...form.getInputProps('featuresVersionChecking', { type: 'checkbox' })}
|
||||
/>
|
||||
|
||||
<Switch
|
||||
label='Enable Metrics'
|
||||
description='Enables metrics for the server. Requires a server restart.'
|
||||
{...form.getInputProps('featuresMetricsEnabled', { type: 'checkbox' })}
|
||||
/>
|
||||
<TextInput
|
||||
label='Version API URL'
|
||||
description={
|
||||
<>
|
||||
The URL of the version checking server. The default is{' '}
|
||||
<Anchor size='xs' href='https://zipline-version.diced.sh' target='_blank'>
|
||||
https://zipline-version.diced.sh
|
||||
</Anchor>
|
||||
. Visit the{' '}
|
||||
<Anchor size='xs' href='https://github.com/diced/zipline-version-worker' target='_blank'>
|
||||
GitHub
|
||||
</Anchor>{' '}
|
||||
to host your own version checking server.
|
||||
</>
|
||||
}
|
||||
placeholder='https://zipline-version.diced.sh/'
|
||||
{...form.getInputProps('featuresVersionAPI')}
|
||||
/>
|
||||
</Stack>
|
||||
|
||||
<Switch
|
||||
label='Admin Only Metrics'
|
||||
description='Requires an administrator to view metrics.'
|
||||
{...form.getInputProps('featuresMetricsAdminOnly', { type: 'checkbox' })}
|
||||
/>
|
||||
|
||||
<Switch
|
||||
label='Show User Specific Metrics'
|
||||
description='Shows metrics specific to each user, for all users.'
|
||||
{...form.getInputProps('featuresMetricsShowUserSpecific', { type: 'checkbox' })}
|
||||
/>
|
||||
|
||||
<Divider label='Thumbnails' />
|
||||
|
||||
<SimpleGrid cols={{ base: 1, md: 2 }} spacing='lg'>
|
||||
<Switch
|
||||
label='Enable Thumbnails'
|
||||
description='Enables thumbnail generation for images. Requires a server restart.'
|
||||
{...form.getInputProps('featuresThumbnailsEnabled', { type: 'checkbox' })}
|
||||
/>
|
||||
<Switch
|
||||
label='Instantaneous Thumbnails'
|
||||
description='Generates thumbnails immediately after a file is uploaded, instead of waiting for the task to run.'
|
||||
{...form.getInputProps('featuresThumbnailsInstantaneous', { type: 'checkbox' })}
|
||||
/>
|
||||
</SimpleGrid>
|
||||
|
||||
<NumberInput
|
||||
label='Thumbnails Number Threads'
|
||||
description='Number of threads to use for thumbnail generation, usually the number of CPU threads. Requires a server restart.'
|
||||
placeholder='Enter a number...'
|
||||
min={1}
|
||||
max={16}
|
||||
{...form.getInputProps('featuresThumbnailsNumberThreads')}
|
||||
/>
|
||||
|
||||
<Select
|
||||
label='Thumbnails Format'
|
||||
description='The output format for thumbnails. Requires a server restart.'
|
||||
data={[
|
||||
{ value: 'jpg', label: '.jpg' },
|
||||
{ value: 'png', label: '.png' },
|
||||
{ value: 'webp', label: '.webp' },
|
||||
]}
|
||||
{...form.getInputProps('featuresThumbnailsFormat')}
|
||||
/>
|
||||
|
||||
<Divider label='Version Checking' />
|
||||
|
||||
<Switch
|
||||
label='Version Checking'
|
||||
description='Enable version checking for the server. This will check for updates and display the status on the sidebar to all users.'
|
||||
{...form.getInputProps('featuresVersionChecking', { type: 'checkbox' })}
|
||||
/>
|
||||
|
||||
<TextInput
|
||||
label='Version API URL'
|
||||
description={
|
||||
<>
|
||||
The URL of the version checking server. The default is{' '}
|
||||
<Anchor size='xs' href='https://zipline-version.diced.sh' target='_blank'>
|
||||
https://zipline-version.diced.sh
|
||||
</Anchor>
|
||||
. Visit the{' '}
|
||||
<Anchor size='xs' href='https://github.com/diced/zipline-version-worker' target='_blank'>
|
||||
GitHub
|
||||
</Anchor>{' '}
|
||||
to host your own version checking server.
|
||||
</>
|
||||
}
|
||||
placeholder='https://zipline-version.diced.sh/'
|
||||
{...form.getInputProps('featuresVersionAPI')}
|
||||
/>
|
||||
</Stack>
|
||||
|
||||
<Button type='submit' mt='md' loading={isLoading} leftSection={<IconDeviceFloppy size='1rem' />}>
|
||||
Save
|
||||
</Button>
|
||||
</form>
|
||||
</>
|
||||
<Button type='submit' mt='md' loading={isLoading} leftSection={<IconDeviceFloppy size='1rem' />}>
|
||||
Save
|
||||
</Button>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,52 +1,44 @@
|
||||
import { Response } from '@/lib/api/response';
|
||||
import type { Response } from '@/lib/api/response';
|
||||
import { Button, LoadingOverlay, NumberInput, Select, Stack, Switch, TextInput } from '@mantine/core';
|
||||
import { useForm } from '@mantine/form';
|
||||
import { IconDeviceFloppy } from '@tabler/icons-react';
|
||||
import { useEffect } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { settingsOnSubmit } from '../settingsOnSubmit';
|
||||
import useServerSettings from '../useServerSettings';
|
||||
|
||||
export default function Files({
|
||||
swr: { data, isLoading },
|
||||
}: {
|
||||
swr: { data: Response['/api/server/settings'] | undefined; isLoading: boolean };
|
||||
}) {
|
||||
export default function Files() {
|
||||
const { data, isLoading } = useServerSettings();
|
||||
|
||||
return (
|
||||
<>
|
||||
<LoadingOverlay visible={isLoading} />
|
||||
{data ? <Form data={data} isLoading={isLoading} /> : null}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function Form({ data, isLoading }: { data: Response['/api/server/settings']; isLoading: boolean }) {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const form = useForm<{
|
||||
filesRoute: string;
|
||||
filesLength: number;
|
||||
filesDefaultFormat: string;
|
||||
filesDisabledExtensions: string;
|
||||
filesMaxFileSize: string;
|
||||
filesDefaultExpiration: string | null;
|
||||
filesMaxExpiration: string | null;
|
||||
filesAssumeMimetypes: boolean;
|
||||
filesDefaultDateFormat: string;
|
||||
filesRemoveGpsMetadata: boolean;
|
||||
filesRandomWordsNumAdjectives: number;
|
||||
filesRandomWordsSeparator: string;
|
||||
filesDefaultCompressionFormat: string;
|
||||
filesMaxFilesPerUpload: number;
|
||||
}>({
|
||||
const form = useForm({
|
||||
initialValues: {
|
||||
filesRoute: '/u',
|
||||
filesLength: 6,
|
||||
filesDefaultFormat: 'random',
|
||||
filesDisabledExtensions: '',
|
||||
filesMaxFileSize: '100mb',
|
||||
filesDefaultExpiration: '',
|
||||
filesMaxExpiration: '',
|
||||
filesAssumeMimetypes: false,
|
||||
filesDefaultDateFormat: 'YYYY-MM-DD_HH:mm:ss',
|
||||
filesRemoveGpsMetadata: false,
|
||||
filesRandomWordsNumAdjectives: 3,
|
||||
filesRandomWordsSeparator: '-',
|
||||
filesDefaultCompressionFormat: 'jpg',
|
||||
filesMaxFilesPerUpload: 1000,
|
||||
filesRoute: data.settings.filesRoute,
|
||||
filesLength: data.settings.filesLength,
|
||||
filesDefaultFormat: data.settings.filesDefaultFormat,
|
||||
filesDisabledExtensions: data.settings.filesDisabledExtensions.join(', '),
|
||||
filesMaxFileSize: data.settings.filesMaxFileSize,
|
||||
filesDefaultExpiration: data.settings.filesDefaultExpiration,
|
||||
filesMaxExpiration: data.settings.filesMaxExpiration,
|
||||
filesAssumeMimetypes: data.settings.filesAssumeMimetypes,
|
||||
filesDefaultDateFormat: data.settings.filesDefaultDateFormat,
|
||||
filesRemoveGpsMetadata: data.settings.filesRemoveGpsMetadata,
|
||||
filesRandomWordsNumAdjectives: data.settings.filesRandomWordsNumAdjectives,
|
||||
filesRandomWordsSeparator: data.settings.filesRandomWordsSeparator,
|
||||
filesDefaultCompressionFormat: data.settings.filesDefaultCompressionFormat,
|
||||
filesMaxFilesPerUpload: data.settings.filesMaxFilesPerUpload,
|
||||
},
|
||||
enhanceGetInputProps: (payload) => ({
|
||||
disabled: data?.tampered?.includes(payload.field) || false,
|
||||
disabled: data.tampered.includes(payload.field) || false,
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -85,143 +77,118 @@ export default function Files({
|
||||
return settingsOnSubmit(navigate, form)(values);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!data) return;
|
||||
|
||||
form.setValues({
|
||||
filesRoute: data.settings.filesRoute ?? '/u',
|
||||
filesLength: data.settings.filesLength ?? 6,
|
||||
filesDefaultFormat: data.settings.filesDefaultFormat ?? 'random',
|
||||
filesDisabledExtensions: data.settings.filesDisabledExtensions.join(', ') ?? '',
|
||||
filesMaxFileSize: data.settings.filesMaxFileSize ?? '100mb',
|
||||
filesDefaultExpiration: data.settings.filesDefaultExpiration ?? '',
|
||||
filesMaxExpiration: data.settings.filesMaxExpiration ?? '',
|
||||
filesAssumeMimetypes: data.settings.filesAssumeMimetypes ?? false,
|
||||
filesDefaultDateFormat: data.settings.filesDefaultDateFormat ?? 'YYYY-MM-DD_HH:mm:ss',
|
||||
filesRemoveGpsMetadata: data.settings.filesRemoveGpsMetadata ?? false,
|
||||
filesRandomWordsNumAdjectives: data.settings.filesRandomWordsNumAdjectives ?? 3,
|
||||
filesRandomWordsSeparator: data.settings.filesRandomWordsSeparator ?? '-',
|
||||
filesDefaultCompressionFormat: data.settings.filesDefaultCompressionFormat ?? 'jpg',
|
||||
filesMaxFilesPerUpload: data.settings.filesMaxFilesPerUpload ?? 1000,
|
||||
});
|
||||
}, [data]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<LoadingOverlay visible={isLoading} />
|
||||
<form onSubmit={form.onSubmit(onSubmit)}>
|
||||
<Stack gap='lg'>
|
||||
<Switch
|
||||
label='Assume Mimetypes'
|
||||
description='Assume the mimetype of a file for its extension.'
|
||||
{...form.getInputProps('filesAssumeMimetypes', { type: 'checkbox' })}
|
||||
/>
|
||||
|
||||
<form onSubmit={form.onSubmit(onSubmit)}>
|
||||
<Stack gap='lg'>
|
||||
<Switch
|
||||
label='Assume Mimetypes'
|
||||
description='Assume the mimetype of a file for its extension.'
|
||||
{...form.getInputProps('filesAssumeMimetypes', { type: 'checkbox' })}
|
||||
/>
|
||||
<Switch
|
||||
label='Remove GPS Metadata'
|
||||
description='Remove GPS metadata from files.'
|
||||
{...form.getInputProps('filesRemoveGpsMetadata', { type: 'checkbox' })}
|
||||
/>
|
||||
|
||||
<Switch
|
||||
label='Remove GPS Metadata'
|
||||
description='Remove GPS metadata from files.'
|
||||
{...form.getInputProps('filesRemoveGpsMetadata', { type: 'checkbox' })}
|
||||
/>
|
||||
<TextInput
|
||||
label='Route'
|
||||
description='The route to use for file uploads. Requires a server restart.'
|
||||
placeholder='/u'
|
||||
{...form.getInputProps('filesRoute')}
|
||||
/>
|
||||
|
||||
<TextInput
|
||||
label='Route'
|
||||
description='The route to use for file uploads. Requires a server restart.'
|
||||
placeholder='/u'
|
||||
{...form.getInputProps('filesRoute')}
|
||||
/>
|
||||
<NumberInput
|
||||
label='Length'
|
||||
description='The length of the file name (for randomly generated names).'
|
||||
min={1}
|
||||
max={64}
|
||||
{...form.getInputProps('filesLength')}
|
||||
/>
|
||||
|
||||
<NumberInput
|
||||
label='Length'
|
||||
description='The length of the file name (for randomly generated names).'
|
||||
min={1}
|
||||
max={64}
|
||||
{...form.getInputProps('filesLength')}
|
||||
/>
|
||||
<Select
|
||||
label='Default Format'
|
||||
description='The default format to use for file names.'
|
||||
placeholder='random'
|
||||
data={['random', 'date', 'uuid', 'name', 'gfycat']}
|
||||
{...form.getInputProps('filesDefaultFormat')}
|
||||
/>
|
||||
|
||||
<Select
|
||||
label='Default Format'
|
||||
description='The default format to use for file names.'
|
||||
placeholder='random'
|
||||
data={['random', 'date', 'uuid', 'name', 'gfycat']}
|
||||
{...form.getInputProps('filesDefaultFormat')}
|
||||
/>
|
||||
<TextInput
|
||||
label='Disabled Extensions'
|
||||
description='Extensions to disable, separated by commas.'
|
||||
placeholder='exe, bat, sh'
|
||||
{...form.getInputProps('filesDisabledExtensions')}
|
||||
/>
|
||||
|
||||
<TextInput
|
||||
label='Disabled Extensions'
|
||||
description='Extensions to disable, separated by commas.'
|
||||
placeholder='exe, bat, sh'
|
||||
{...form.getInputProps('filesDisabledExtensions')}
|
||||
/>
|
||||
<TextInput
|
||||
label='Max File Size'
|
||||
description='The maximum file size allowed.'
|
||||
placeholder='100mb'
|
||||
{...form.getInputProps('filesMaxFileSize')}
|
||||
/>
|
||||
|
||||
<TextInput
|
||||
label='Max File Size'
|
||||
description='The maximum file size allowed.'
|
||||
placeholder='100mb'
|
||||
{...form.getInputProps('filesMaxFileSize')}
|
||||
/>
|
||||
<TextInput
|
||||
label='Default Date Format'
|
||||
description='The default date format to use.'
|
||||
placeholder='YYYY-MM-DD_HH:mm:ss'
|
||||
{...form.getInputProps('filesDefaultDateFormat')}
|
||||
/>
|
||||
|
||||
<TextInput
|
||||
label='Default Date Format'
|
||||
description='The default date format to use.'
|
||||
placeholder='YYYY-MM-DD_HH:mm:ss'
|
||||
{...form.getInputProps('filesDefaultDateFormat')}
|
||||
/>
|
||||
<TextInput
|
||||
label='Default Expiration'
|
||||
description='The default expiration time for files.'
|
||||
placeholder='30d'
|
||||
{...form.getInputProps('filesDefaultExpiration')}
|
||||
/>
|
||||
|
||||
<TextInput
|
||||
label='Default Expiration'
|
||||
description='The default expiration time for files.'
|
||||
placeholder='30d'
|
||||
{...form.getInputProps('filesDefaultExpiration')}
|
||||
/>
|
||||
<TextInput
|
||||
label='Max Expiration'
|
||||
description='The maximum expiration time allowed for files.'
|
||||
placeholder='365d'
|
||||
{...form.getInputProps('filesMaxExpiration')}
|
||||
/>
|
||||
|
||||
<TextInput
|
||||
label='Max Expiration'
|
||||
description='The maximum expiration time allowed for files.'
|
||||
placeholder='365d'
|
||||
{...form.getInputProps('filesMaxExpiration')}
|
||||
/>
|
||||
<NumberInput
|
||||
label='Random Words Num Adjectives'
|
||||
description='The number of adjectives to use for the random-words/gfycat format.'
|
||||
min={1}
|
||||
max={10}
|
||||
{...form.getInputProps('filesRandomWordsNumAdjectives')}
|
||||
/>
|
||||
|
||||
<NumberInput
|
||||
label='Random Words Num Adjectives'
|
||||
description='The number of adjectives to use for the random-words/gfycat format.'
|
||||
min={1}
|
||||
max={10}
|
||||
{...form.getInputProps('filesRandomWordsNumAdjectives')}
|
||||
/>
|
||||
<TextInput
|
||||
label='Random Words Separator'
|
||||
description='The separator to use for the random-words/gfycat format.'
|
||||
placeholder='-'
|
||||
{...form.getInputProps('filesRandomWordsSeparator')}
|
||||
/>
|
||||
|
||||
<TextInput
|
||||
label='Random Words Separator'
|
||||
description='The separator to use for the random-words/gfycat format.'
|
||||
placeholder='-'
|
||||
{...form.getInputProps('filesRandomWordsSeparator')}
|
||||
/>
|
||||
<Select
|
||||
label='Default Compression Format'
|
||||
description='The default image compression format to use when only a compression percent is specified.'
|
||||
placeholder='jpg'
|
||||
data={[
|
||||
{ value: 'jpg', label: '.jpg' },
|
||||
{ value: 'png', label: '.png' },
|
||||
{ value: 'webp', label: '.webp' },
|
||||
{ value: 'jxl', label: '.jxl' },
|
||||
]}
|
||||
{...form.getInputProps('filesDefaultCompressionFormat')}
|
||||
/>
|
||||
|
||||
<Select
|
||||
label='Default Compression Format'
|
||||
description='The default image compression format to use when only a compression percent is specified.'
|
||||
placeholder='jpg'
|
||||
data={[
|
||||
{ value: 'jpg', label: '.jpg' },
|
||||
{ value: 'png', label: '.png' },
|
||||
{ value: 'webp', label: '.webp' },
|
||||
{ value: 'jxl', label: '.jxl' },
|
||||
]}
|
||||
{...form.getInputProps('filesDefaultCompressionFormat')}
|
||||
/>
|
||||
<NumberInput
|
||||
label='Max Files Per Upload'
|
||||
description='The maximum number of files allowed per upload. Requires a server restart.'
|
||||
min={1}
|
||||
{...form.getInputProps('filesMaxFilesPerUpload')}
|
||||
/>
|
||||
</Stack>
|
||||
|
||||
<NumberInput
|
||||
label='Max Files Per Upload'
|
||||
description='The maximum number of files allowed per upload. Requires a server restart.'
|
||||
min={1}
|
||||
{...form.getInputProps('filesMaxFilesPerUpload')}
|
||||
/>
|
||||
</Stack>
|
||||
|
||||
<Button type='submit' mt='md' loading={isLoading} leftSection={<IconDeviceFloppy size='1rem' />}>
|
||||
Save
|
||||
</Button>
|
||||
</form>
|
||||
</>
|
||||
<Button type='submit' mt='md' loading={isLoading} leftSection={<IconDeviceFloppy size='1rem' />}>
|
||||
Save
|
||||
</Button>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,25 +1,32 @@
|
||||
import { Response } from '@/lib/api/response';
|
||||
import type { Response } from '@/lib/api/response';
|
||||
import { Button, LoadingOverlay, Stack, TextInput } from '@mantine/core';
|
||||
import { useForm } from '@mantine/form';
|
||||
import { IconDeviceFloppy } from '@tabler/icons-react';
|
||||
import { useEffect } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { settingsOnSubmit } from '../settingsOnSubmit';
|
||||
import useServerSettings from '../useServerSettings';
|
||||
|
||||
export default function HttpWebhook({
|
||||
swr: { data, isLoading },
|
||||
}: {
|
||||
swr: { data: Response['/api/server/settings'] | undefined; isLoading: boolean };
|
||||
}) {
|
||||
export default function HttpWebhook() {
|
||||
const { data, isLoading } = useServerSettings();
|
||||
|
||||
return (
|
||||
<>
|
||||
<LoadingOverlay visible={isLoading} />
|
||||
{data ? <Form data={data} isLoading={isLoading} /> : null}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function Form({ data, isLoading }: { data: Response['/api/server/settings']; isLoading: boolean }) {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const form = useForm({
|
||||
initialValues: {
|
||||
httpWebhookOnUpload: '',
|
||||
httpWebhookOnShorten: '',
|
||||
httpWebhookOnUpload: data.settings.httpWebhookOnUpload,
|
||||
httpWebhookOnShorten: data.settings.httpWebhookOnShorten,
|
||||
},
|
||||
enhanceGetInputProps: (payload) => ({
|
||||
disabled: data?.tampered?.includes(payload.field) || false,
|
||||
disabled: data.tampered.includes(payload.field) || false,
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -37,40 +44,27 @@ export default function HttpWebhook({
|
||||
return settingsOnSubmit(navigate, form)(values);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!data) return;
|
||||
|
||||
form.setValues({
|
||||
httpWebhookOnUpload: data.settings.httpWebhookOnUpload ?? '',
|
||||
httpWebhookOnShorten: data.settings.httpWebhookOnShorten ?? '',
|
||||
});
|
||||
}, [data]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<LoadingOverlay visible={isLoading} />
|
||||
<form onSubmit={form.onSubmit(onSubmit)}>
|
||||
<Stack gap='lg'>
|
||||
<TextInput
|
||||
label='On Upload'
|
||||
description='The URL to send a POST request to when a file is uploaded.'
|
||||
placeholder='https://example.com/upload'
|
||||
{...form.getInputProps('httpWebhookOnUpload')}
|
||||
/>
|
||||
|
||||
<form onSubmit={form.onSubmit(onSubmit)}>
|
||||
<Stack gap='lg'>
|
||||
<TextInput
|
||||
label='On Upload'
|
||||
description='The URL to send a POST request to when a file is uploaded.'
|
||||
placeholder='https://example.com/upload'
|
||||
{...form.getInputProps('httpWebhookOnUpload')}
|
||||
/>
|
||||
<TextInput
|
||||
label='On Shorten'
|
||||
description='The URL to send a POST request to when a URL is shortened.'
|
||||
placeholder='https://example.com/shorten'
|
||||
{...form.getInputProps('httpWebhookOnShorten')}
|
||||
/>
|
||||
</Stack>
|
||||
|
||||
<TextInput
|
||||
label='On Shorten'
|
||||
description='The URL to send a POST request to when a URL is shortened.'
|
||||
placeholder='https://example.com/shorten'
|
||||
{...form.getInputProps('httpWebhookOnShorten')}
|
||||
/>
|
||||
</Stack>
|
||||
|
||||
<Button type='submit' mt='md' loading={isLoading} leftSection={<IconDeviceFloppy size='1rem' />}>
|
||||
Save
|
||||
</Button>
|
||||
</form>
|
||||
</>
|
||||
<Button type='submit' mt='md' loading={isLoading} leftSection={<IconDeviceFloppy size='1rem' />}>
|
||||
Save
|
||||
</Button>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,26 +1,33 @@
|
||||
import { Response } from '@/lib/api/response';
|
||||
import type { Response } from '@/lib/api/response';
|
||||
import { Button, LoadingOverlay, NumberInput, Stack, Switch } from '@mantine/core';
|
||||
import { useForm } from '@mantine/form';
|
||||
import { IconDeviceFloppy } from '@tabler/icons-react';
|
||||
import { useEffect } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { settingsOnSubmit } from '../settingsOnSubmit';
|
||||
import useServerSettings from '../useServerSettings';
|
||||
|
||||
export default function Invites({
|
||||
swr: { data, isLoading },
|
||||
}: {
|
||||
swr: { data: Response['/api/server/settings'] | undefined; isLoading: boolean };
|
||||
}) {
|
||||
export default function Invites() {
|
||||
const { data, isLoading } = useServerSettings();
|
||||
|
||||
return (
|
||||
<>
|
||||
<LoadingOverlay visible={isLoading} />
|
||||
{data ? <Form data={data} isLoading={isLoading} /> : null}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function Form({ data, isLoading }: { data: Response['/api/server/settings']; isLoading: boolean }) {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const form = useForm({
|
||||
initialValues: {
|
||||
invitesEnabled: true,
|
||||
invitesLength: 6,
|
||||
invitesEnabled: data.settings.invitesEnabled,
|
||||
invitesLength: data.settings.invitesLength,
|
||||
},
|
||||
enhanceGetInputProps: (payload: any): object => ({
|
||||
disabled:
|
||||
data?.tampered?.includes(payload.field) ||
|
||||
data.tampered.includes(payload.field) ||
|
||||
(payload.field !== 'invitesEnabled' && !form.values.invitesEnabled) ||
|
||||
false,
|
||||
}),
|
||||
@@ -28,41 +35,28 @@ export default function Invites({
|
||||
|
||||
const onSubmit = settingsOnSubmit(navigate, form);
|
||||
|
||||
useEffect(() => {
|
||||
if (!data) return;
|
||||
|
||||
form.setValues({
|
||||
invitesEnabled: data.settings.invitesEnabled ?? true,
|
||||
invitesLength: data.settings.invitesLength ?? 6,
|
||||
});
|
||||
}, [data]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<LoadingOverlay visible={isLoading} />
|
||||
<form onSubmit={form.onSubmit(onSubmit)}>
|
||||
<Stack gap='lg'>
|
||||
<Switch
|
||||
label='Enable Invites'
|
||||
description='Enable the use of invite links to register new users.'
|
||||
{...form.getInputProps('invitesEnabled', { type: 'checkbox' })}
|
||||
/>
|
||||
|
||||
<form onSubmit={form.onSubmit(onSubmit)}>
|
||||
<Stack gap='lg'>
|
||||
<Switch
|
||||
label='Enable Invites'
|
||||
description='Enable the use of invite links to register new users.'
|
||||
{...form.getInputProps('invitesEnabled', { type: 'checkbox' })}
|
||||
/>
|
||||
<NumberInput
|
||||
label='Length'
|
||||
description='The length of the invite code.'
|
||||
placeholder='6'
|
||||
min={1}
|
||||
max={64}
|
||||
{...form.getInputProps('invitesLength')}
|
||||
/>
|
||||
</Stack>
|
||||
|
||||
<NumberInput
|
||||
label='Length'
|
||||
description='The length of the invite code.'
|
||||
placeholder='6'
|
||||
min={1}
|
||||
max={64}
|
||||
{...form.getInputProps('invitesLength')}
|
||||
/>
|
||||
</Stack>
|
||||
|
||||
<Button type='submit' mt='md' loading={isLoading} leftSection={<IconDeviceFloppy size='1rem' />}>
|
||||
Save
|
||||
</Button>
|
||||
</form>
|
||||
</>
|
||||
<Button type='submit' mt='md' loading={isLoading} leftSection={<IconDeviceFloppy size='1rem' />}>
|
||||
Save
|
||||
</Button>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,90 +1,81 @@
|
||||
import { Response } from '@/lib/api/response';
|
||||
import type { Response } from '@/lib/api/response';
|
||||
import { Button, Divider, LoadingOverlay, Stack, Switch, TextInput } from '@mantine/core';
|
||||
import { useForm } from '@mantine/form';
|
||||
import { IconDeviceFloppy } from '@tabler/icons-react';
|
||||
import { useEffect } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { settingsOnSubmit } from '../settingsOnSubmit';
|
||||
import useServerSettings from '../useServerSettings';
|
||||
|
||||
export default function Mfa({
|
||||
swr: { data, isLoading },
|
||||
}: {
|
||||
swr: { data: Response['/api/server/settings'] | undefined; isLoading: boolean };
|
||||
}) {
|
||||
export default function Mfa() {
|
||||
const { data, isLoading } = useServerSettings();
|
||||
|
||||
return (
|
||||
<>
|
||||
<LoadingOverlay visible={isLoading} />
|
||||
{data ? <Form data={data} isLoading={isLoading} /> : null}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function Form({ data, isLoading }: { data: Response['/api/server/settings']; isLoading: boolean }) {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const form = useForm({
|
||||
initialValues: {
|
||||
mfaTotpEnabled: false,
|
||||
mfaTotpIssuer: 'Zipline',
|
||||
mfaPasskeysEnabled: false,
|
||||
mfaPasskeysRpID: '',
|
||||
mfaPasskeysOrigin: '',
|
||||
mfaTotpEnabled: data.settings.mfaTotpEnabled,
|
||||
mfaTotpIssuer: data.settings.mfaTotpIssuer,
|
||||
mfaPasskeysEnabled: data.settings.mfaPasskeysEnabled,
|
||||
mfaPasskeysRpID: data.settings.mfaPasskeysRpID,
|
||||
mfaPasskeysOrigin: data.settings.mfaPasskeysOrigin,
|
||||
},
|
||||
enhanceGetInputProps: (payload) => ({
|
||||
disabled: data?.tampered?.includes(payload.field) || false,
|
||||
disabled: data.tampered.includes(payload.field) || false,
|
||||
}),
|
||||
});
|
||||
|
||||
const onSubmit = settingsOnSubmit(navigate, form);
|
||||
|
||||
useEffect(() => {
|
||||
if (!data) return;
|
||||
|
||||
form.setValues({
|
||||
mfaTotpEnabled: data.settings.mfaTotpEnabled ?? false,
|
||||
mfaTotpIssuer: data.settings.mfaTotpIssuer ?? 'Zipline',
|
||||
mfaPasskeysEnabled: data.settings.mfaPasskeysEnabled ?? false,
|
||||
mfaPasskeysRpID: data.settings.mfaPasskeysRpID ?? '',
|
||||
mfaPasskeysOrigin: data.settings.mfaPasskeysOrigin ?? '',
|
||||
});
|
||||
}, [data]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<LoadingOverlay visible={isLoading} />
|
||||
<form onSubmit={form.onSubmit(onSubmit)}>
|
||||
<Stack gap='lg'>
|
||||
<Switch
|
||||
label='Passkeys'
|
||||
description='Enable the use of passwordless login with the use of WebAuthn passkeys like your phone, security keys, etc.'
|
||||
{...form.getInputProps('mfaPasskeysEnabled', { type: 'checkbox' })}
|
||||
/>
|
||||
|
||||
<form onSubmit={form.onSubmit(onSubmit)}>
|
||||
<Stack gap='lg'>
|
||||
<Switch
|
||||
label='Passkeys'
|
||||
description='Enable the use of passwordless login with the use of WebAuthn passkeys like your phone, security keys, etc.'
|
||||
{...form.getInputProps('mfaPasskeysEnabled', { type: 'checkbox' })}
|
||||
/>
|
||||
<TextInput
|
||||
label='Relying Party ID'
|
||||
description='The Relying Party ID (RP ID) to use for WebAuthn passkeys.'
|
||||
placeholder='example.com'
|
||||
{...form.getInputProps('mfaPasskeysRpID')}
|
||||
/>
|
||||
|
||||
<TextInput
|
||||
label='Relying Party ID'
|
||||
description='The Relying Party ID (RP ID) to use for WebAuthn passkeys.'
|
||||
placeholder='example.com'
|
||||
{...form.getInputProps('mfaPasskeysRpID')}
|
||||
/>
|
||||
<TextInput
|
||||
label='Origin'
|
||||
description='The Origin to use for WebAuthn passkeys.'
|
||||
placeholder='https://example.com'
|
||||
{...form.getInputProps('mfaPasskeysOrigin')}
|
||||
/>
|
||||
|
||||
<TextInput
|
||||
label='Origin'
|
||||
description='The Origin to use for WebAuthn passkeys.'
|
||||
placeholder='https://example.com'
|
||||
{...form.getInputProps('mfaPasskeysOrigin')}
|
||||
/>
|
||||
<Divider />
|
||||
|
||||
<Divider />
|
||||
<Switch
|
||||
label='Enable TOTP'
|
||||
description='Enable Time-based One-Time Passwords with the use of an authenticator app.'
|
||||
{...form.getInputProps('mfaTotpEnabled', { type: 'checkbox' })}
|
||||
/>
|
||||
<TextInput
|
||||
label='Issuer'
|
||||
description='The issuer to use for the TOTP token.'
|
||||
placeholder='Zipline'
|
||||
{...form.getInputProps('mfaTotpIssuer')}
|
||||
/>
|
||||
</Stack>
|
||||
|
||||
<Switch
|
||||
label='Enable TOTP'
|
||||
description='Enable Time-based One-Time Passwords with the use of an authenticator app.'
|
||||
{...form.getInputProps('mfaTotpEnabled', { type: 'checkbox' })}
|
||||
/>
|
||||
<TextInput
|
||||
label='Issuer'
|
||||
description='The issuer to use for the TOTP token.'
|
||||
placeholder='Zipline'
|
||||
{...form.getInputProps('mfaTotpIssuer')}
|
||||
/>
|
||||
</Stack>
|
||||
|
||||
<Button type='submit' mt='md' loading={isLoading} leftSection={<IconDeviceFloppy size='1rem' />}>
|
||||
Save
|
||||
</Button>
|
||||
</form>
|
||||
</>
|
||||
<Button type='submit' mt='md' loading={isLoading} leftSection={<IconDeviceFloppy size='1rem' />}>
|
||||
Save
|
||||
</Button>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Response } from '@/lib/api/response';
|
||||
import type { Response } from '@/lib/api/response';
|
||||
import {
|
||||
Anchor,
|
||||
Button,
|
||||
@@ -13,45 +13,52 @@ import {
|
||||
} from '@mantine/core';
|
||||
import { useForm } from '@mantine/form';
|
||||
import { IconDeviceFloppy } from '@tabler/icons-react';
|
||||
import { useEffect } from 'react';
|
||||
import { Link, useNavigate } from 'react-router-dom';
|
||||
import { settingsOnSubmit } from '../settingsOnSubmit';
|
||||
import useServerSettings from '../useServerSettings';
|
||||
|
||||
export default function Oauth({
|
||||
swr: { data, isLoading },
|
||||
}: {
|
||||
swr: { data: Response['/api/server/settings'] | undefined; isLoading: boolean };
|
||||
}) {
|
||||
export default function Oauth() {
|
||||
const { data, isLoading } = useServerSettings();
|
||||
|
||||
return (
|
||||
<>
|
||||
<LoadingOverlay visible={isLoading} />
|
||||
{data ? <Form data={data} isLoading={isLoading} /> : null}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function Form({ data, isLoading }: { data: Response['/api/server/settings']; isLoading: boolean }) {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const form = useForm({
|
||||
initialValues: {
|
||||
oauthBypassLocalLogin: false,
|
||||
oauthLoginOnly: false,
|
||||
oauthBypassLocalLogin: data.settings.oauthBypassLocalLogin,
|
||||
oauthLoginOnly: data.settings.oauthLoginOnly,
|
||||
|
||||
oauthDiscordClientId: '',
|
||||
oauthDiscordClientSecret: '',
|
||||
oauthDiscordRedirectUri: '',
|
||||
oauthDiscordAllowedIds: '',
|
||||
oauthDiscordDeniedIds: '',
|
||||
oauthDiscordClientId: data.settings.oauthDiscordClientId,
|
||||
oauthDiscordClientSecret: data.settings.oauthDiscordClientSecret,
|
||||
oauthDiscordRedirectUri: data.settings.oauthDiscordRedirectUri,
|
||||
oauthDiscordAllowedIds: data.settings.oauthDiscordAllowedIds.join(', '),
|
||||
oauthDiscordDeniedIds: data.settings.oauthDiscordDeniedIds.join(', '),
|
||||
|
||||
oauthGoogleClientId: '',
|
||||
oauthGoogleClientSecret: '',
|
||||
oauthGoogleRedirectUri: '',
|
||||
oauthGoogleClientId: data.settings.oauthGoogleClientId,
|
||||
oauthGoogleClientSecret: data.settings.oauthGoogleClientSecret,
|
||||
oauthGoogleRedirectUri: data.settings.oauthGoogleRedirectUri,
|
||||
|
||||
oauthGithubClientId: '',
|
||||
oauthGithubClientSecret: '',
|
||||
oauthGithubRedirectUri: '',
|
||||
oauthGithubClientId: data.settings.oauthGithubClientId,
|
||||
oauthGithubClientSecret: data.settings.oauthGithubClientSecret,
|
||||
oauthGithubRedirectUri: data.settings.oauthGithubRedirectUri,
|
||||
|
||||
oauthOidcClientId: '',
|
||||
oauthOidcClientSecret: '',
|
||||
oauthOidcAuthorizeUrl: '',
|
||||
oauthOidcTokenUrl: '',
|
||||
oauthOidcUserinfoUrl: '',
|
||||
oauthOidcRedirectUri: '',
|
||||
oauthOidcClientId: data.settings.oauthOidcClientId,
|
||||
oauthOidcClientSecret: data.settings.oauthOidcClientSecret,
|
||||
oauthOidcAuthorizeUrl: data.settings.oauthOidcAuthorizeUrl,
|
||||
oauthOidcTokenUrl: data.settings.oauthOidcTokenUrl,
|
||||
oauthOidcUserinfoUrl: data.settings.oauthOidcUserinfoUrl,
|
||||
oauthOidcRedirectUri: data.settings.oauthOidcRedirectUri,
|
||||
},
|
||||
enhanceGetInputProps: (payload) => ({
|
||||
disabled: data?.tampered?.includes(payload.field) || false,
|
||||
disabled: data.tampered.includes(payload.field) || false,
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -90,44 +97,8 @@ export default function Oauth({
|
||||
return settingsOnSubmit(navigate, form)(values);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!data) return;
|
||||
|
||||
form.setValues({
|
||||
oauthBypassLocalLogin: data.settings.oauthBypassLocalLogin ?? false,
|
||||
oauthLoginOnly: data.settings.oauthLoginOnly ?? false,
|
||||
|
||||
oauthDiscordClientId: data.settings.oauthDiscordClientId ?? '',
|
||||
oauthDiscordClientSecret: data.settings.oauthDiscordClientSecret ?? '',
|
||||
oauthDiscordRedirectUri: data.settings.oauthDiscordRedirectUri ?? '',
|
||||
oauthDiscordAllowedIds: data.settings.oauthDiscordAllowedIds
|
||||
? data.settings.oauthDiscordAllowedIds.join(', ')
|
||||
: '',
|
||||
oauthDiscordDeniedIds: data.settings.oauthDiscordDeniedIds
|
||||
? data.settings.oauthDiscordDeniedIds.join(', ')
|
||||
: '',
|
||||
|
||||
oauthGoogleClientId: data.settings.oauthGoogleClientId ?? '',
|
||||
oauthGoogleClientSecret: data.settings.oauthGoogleClientSecret ?? '',
|
||||
oauthGoogleRedirectUri: data.settings.oauthGoogleRedirectUri ?? '',
|
||||
|
||||
oauthGithubClientId: data.settings.oauthGithubClientId ?? '',
|
||||
oauthGithubClientSecret: data.settings.oauthGithubClientSecret ?? '',
|
||||
oauthGithubRedirectUri: data.settings.oauthGithubRedirectUri ?? '',
|
||||
|
||||
oauthOidcClientId: data.settings.oauthOidcClientId ?? '',
|
||||
oauthOidcClientSecret: data.settings.oauthOidcClientSecret ?? '',
|
||||
oauthOidcAuthorizeUrl: data.settings.oauthOidcAuthorizeUrl ?? '',
|
||||
oauthOidcTokenUrl: data.settings.oauthOidcTokenUrl ?? '',
|
||||
oauthOidcUserinfoUrl: data.settings.oauthOidcUserinfoUrl ?? '',
|
||||
oauthOidcRedirectUri: data.settings.oauthOidcRedirectUri ?? '',
|
||||
});
|
||||
}, [data]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<LoadingOverlay visible={isLoading} />
|
||||
|
||||
<Text size='sm' c='dimmed' mb='md'>
|
||||
For OAuth to work, the "OAuth Registration" setting must be enabled in the{' '}
|
||||
<Anchor component={Link} to='/dashboard/admin/settings/features'>
|
||||
|
||||
@@ -1,30 +1,37 @@
|
||||
import { Response } from '@/lib/api/response';
|
||||
import type { Response } from '@/lib/api/response';
|
||||
import { Button, ColorInput, Group, LoadingOverlay, Stack, Switch, Text, TextInput } from '@mantine/core';
|
||||
import { useForm } from '@mantine/form';
|
||||
import { IconDeviceFloppy, IconRefresh } from '@tabler/icons-react';
|
||||
import { useEffect } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { settingsOnSubmit } from '../settingsOnSubmit';
|
||||
import useServerSettings from '../useServerSettings';
|
||||
|
||||
export default function PWA({
|
||||
swr: { data, isLoading },
|
||||
}: {
|
||||
swr: { data: Response['/api/server/settings'] | undefined; isLoading: boolean };
|
||||
}) {
|
||||
export default function PWA() {
|
||||
const { data, isLoading } = useServerSettings();
|
||||
|
||||
return (
|
||||
<>
|
||||
<LoadingOverlay visible={isLoading} />
|
||||
{data ? <Form data={data} isLoading={isLoading} /> : null}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function Form({ data, isLoading }: { data: Response['/api/server/settings']; isLoading: boolean }) {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const form = useForm({
|
||||
initialValues: {
|
||||
pwaEnabled: false,
|
||||
pwaTitle: '',
|
||||
pwaShortName: '',
|
||||
pwaDescription: '',
|
||||
pwaThemeColor: '',
|
||||
pwaBackgroundColor: '',
|
||||
pwaEnabled: data.settings.pwaEnabled,
|
||||
pwaTitle: data.settings.pwaTitle,
|
||||
pwaShortName: data.settings.pwaShortName,
|
||||
pwaDescription: data.settings.pwaDescription,
|
||||
pwaThemeColor: data.settings.pwaThemeColor,
|
||||
pwaBackgroundColor: data.settings.pwaBackgroundColor,
|
||||
},
|
||||
enhanceGetInputProps: (payload: any): object => ({
|
||||
disabled:
|
||||
data?.tampered?.includes(payload.field) ||
|
||||
data.tampered.includes(payload.field) ||
|
||||
(payload.field !== 'pwaEnabled' && !form.values.pwaEnabled) ||
|
||||
false,
|
||||
}),
|
||||
@@ -48,23 +55,8 @@ export default function PWA({
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!data) return;
|
||||
|
||||
form.setValues({
|
||||
pwaEnabled: data.settings.pwaEnabled ?? false,
|
||||
pwaTitle: data.settings.pwaTitle ?? '',
|
||||
pwaShortName: data.settings.pwaShortName ?? '',
|
||||
pwaDescription: data.settings.pwaDescription ?? '',
|
||||
pwaThemeColor: data.settings.pwaThemeColor ?? '',
|
||||
pwaBackgroundColor: data.settings.pwaBackgroundColor ?? '',
|
||||
});
|
||||
}, [data]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<LoadingOverlay visible={isLoading} />
|
||||
|
||||
<Text size='sm' c='dimmed' mb='md'>
|
||||
Refresh the page after enabling PWA to see any changes.
|
||||
</Text>
|
||||
|
||||
@@ -1,35 +1,42 @@
|
||||
import { Response } from '@/lib/api/response';
|
||||
import type { Response } from '@/lib/api/response';
|
||||
import { Button, LoadingOverlay, NumberInput, Stack, Switch, Text, TextInput } from '@mantine/core';
|
||||
import { useForm } from '@mantine/form';
|
||||
import { IconDeviceFloppy } from '@tabler/icons-react';
|
||||
import { useEffect } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { settingsOnSubmit } from '../settingsOnSubmit';
|
||||
import useServerSettings from '../useServerSettings';
|
||||
|
||||
export default function Ratelimit({
|
||||
swr: { data, isLoading },
|
||||
}: {
|
||||
swr: { data: Response['/api/server/settings'] | undefined; isLoading: boolean };
|
||||
}) {
|
||||
export default function Ratelimit() {
|
||||
const { data, isLoading } = useServerSettings();
|
||||
|
||||
return (
|
||||
<>
|
||||
<LoadingOverlay visible={isLoading} />
|
||||
{data ? <Form data={data} isLoading={isLoading} /> : null}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function Form({ data, isLoading }: { data: Response['/api/server/settings']; isLoading: boolean }) {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const form = useForm<{
|
||||
ratelimitEnabled: boolean;
|
||||
ratelimitMax: number;
|
||||
ratelimitWindow: number | '';
|
||||
ratelimitWindow: number | '' | null;
|
||||
ratelimitAdminBypass: boolean;
|
||||
ratelimitAllowList: string;
|
||||
}>({
|
||||
initialValues: {
|
||||
ratelimitEnabled: true,
|
||||
ratelimitMax: 10,
|
||||
ratelimitWindow: '',
|
||||
ratelimitAdminBypass: false,
|
||||
ratelimitAllowList: '',
|
||||
ratelimitEnabled: data.settings.ratelimitEnabled,
|
||||
ratelimitMax: data.settings.ratelimitMax,
|
||||
ratelimitWindow: data.settings.ratelimitWindow,
|
||||
ratelimitAdminBypass: data.settings.ratelimitAdminBypass,
|
||||
ratelimitAllowList: data.settings.ratelimitAllowList.join(', '),
|
||||
},
|
||||
enhanceGetInputProps: (payload: any): object => ({
|
||||
disabled:
|
||||
data?.tampered?.includes(payload.field) ||
|
||||
data.tampered.includes(payload.field) ||
|
||||
(payload.field !== 'ratelimitEnabled' && !form.values.ratelimitEnabled) ||
|
||||
false,
|
||||
}),
|
||||
@@ -55,22 +62,8 @@ export default function Ratelimit({
|
||||
return settingsOnSubmit(navigate, form)(values);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!data) return;
|
||||
|
||||
form.setValues({
|
||||
ratelimitEnabled: data.settings.ratelimitEnabled ?? true,
|
||||
ratelimitMax: data.settings.ratelimitMax ?? 10,
|
||||
ratelimitWindow: data.settings.ratelimitWindow ?? '',
|
||||
ratelimitAdminBypass: data.settings.ratelimitAdminBypass ?? false,
|
||||
ratelimitAllowList: data.settings.ratelimitAllowList.join(', ') ?? '',
|
||||
});
|
||||
}, [data]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<LoadingOverlay visible={isLoading} />
|
||||
|
||||
<Text size='sm' c='dimmed' mb='md'>
|
||||
All options require a restart to take effect.
|
||||
</Text>
|
||||
|
||||
@@ -1,51 +1,43 @@
|
||||
import { Response } from '@/lib/api/response';
|
||||
import type { Response } from '@/lib/api/response';
|
||||
import { Button, Code, LoadingOverlay, Stack, Text, TextInput } from '@mantine/core';
|
||||
import { useForm } from '@mantine/form';
|
||||
import { IconDeviceFloppy } from '@tabler/icons-react';
|
||||
import { useEffect } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { settingsOnSubmit } from '../settingsOnSubmit';
|
||||
import useServerSettings from '../useServerSettings';
|
||||
|
||||
export default function Tasks({
|
||||
swr: { data, isLoading },
|
||||
}: {
|
||||
swr: { data: Response['/api/server/settings'] | undefined; isLoading: boolean };
|
||||
}) {
|
||||
export default function Tasks() {
|
||||
const { data, isLoading } = useServerSettings();
|
||||
|
||||
return (
|
||||
<>
|
||||
<LoadingOverlay visible={isLoading} />
|
||||
{data ? <Form data={data} isLoading={isLoading} /> : null}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function Form({ data, isLoading }: { data: Response['/api/server/settings']; isLoading: boolean }) {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const form = useForm({
|
||||
initialValues: {
|
||||
tasksDeleteInterval: '30m',
|
||||
tasksClearInvitesInterval: '30m',
|
||||
tasksMaxViewsInterval: '30m',
|
||||
tasksThumbnailsInterval: '30m',
|
||||
tasksMetricsInterval: '30m',
|
||||
tasksCleanThumbnailsInterval: '1d',
|
||||
tasksDeleteInterval: data.settings.tasksDeleteInterval,
|
||||
tasksClearInvitesInterval: data.settings.tasksClearInvitesInterval,
|
||||
tasksMaxViewsInterval: data.settings.tasksMaxViewsInterval,
|
||||
tasksThumbnailsInterval: data.settings.tasksThumbnailsInterval,
|
||||
tasksMetricsInterval: data.settings.tasksMetricsInterval,
|
||||
tasksCleanThumbnailsInterval: data.settings.tasksCleanThumbnailsInterval,
|
||||
},
|
||||
enhanceGetInputProps: (payload) => ({
|
||||
disabled: data?.tampered?.includes(payload.field) || false,
|
||||
disabled: data.tampered.includes(payload.field) || false,
|
||||
}),
|
||||
});
|
||||
|
||||
const onSubmit = settingsOnSubmit(navigate, form);
|
||||
|
||||
useEffect(() => {
|
||||
if (!data) return;
|
||||
|
||||
form.setValues({
|
||||
tasksDeleteInterval: data.settings.tasksDeleteInterval ?? '30m',
|
||||
tasksClearInvitesInterval: data.settings.tasksClearInvitesInterval ?? '30m',
|
||||
tasksMaxViewsInterval: data.settings.tasksMaxViewsInterval ?? '30m',
|
||||
tasksThumbnailsInterval: data.settings.tasksThumbnailsInterval ?? '30m',
|
||||
tasksMetricsInterval: data.settings.tasksMetricsInterval ?? '30m',
|
||||
tasksCleanThumbnailsInterval: data.settings.tasksCleanThumbnailsInterval ?? '1d',
|
||||
});
|
||||
}, [data]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<LoadingOverlay visible={isLoading} />
|
||||
|
||||
<Text size='sm' c='dimmed' mb='md'>
|
||||
All options require a restart to take effect. Setting a value of <Code>0</Code> will disable the task.
|
||||
</Text>
|
||||
|
||||
@@ -1,66 +1,60 @@
|
||||
import { Response } from '@/lib/api/response';
|
||||
import type { Response } from '@/lib/api/response';
|
||||
import { Button, LoadingOverlay, NumberInput, Stack, TextInput } from '@mantine/core';
|
||||
import { useForm } from '@mantine/form';
|
||||
import { IconDeviceFloppy } from '@tabler/icons-react';
|
||||
import { useEffect } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { settingsOnSubmit } from '../settingsOnSubmit';
|
||||
import useServerSettings from '../useServerSettings';
|
||||
|
||||
export default function Urls({
|
||||
swr: { data, isLoading },
|
||||
}: {
|
||||
swr: { data: Response['/api/server/settings'] | undefined; isLoading: boolean };
|
||||
}) {
|
||||
export default function Urls() {
|
||||
const { data, isLoading } = useServerSettings();
|
||||
|
||||
return (
|
||||
<>
|
||||
<LoadingOverlay visible={isLoading} />
|
||||
{data ? <Form data={data} isLoading={isLoading} /> : null}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function Form({ data, isLoading }: { data: Response['/api/server/settings']; isLoading: boolean }) {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const form = useForm({
|
||||
initialValues: {
|
||||
urlsRoute: '/go',
|
||||
urlsLength: 6,
|
||||
urlsRoute: data.settings.urlsRoute,
|
||||
urlsLength: data.settings.urlsLength,
|
||||
},
|
||||
enhanceGetInputProps: (payload) => ({
|
||||
disabled: data?.tampered?.includes(payload.field) || false,
|
||||
disabled: data.tampered.includes(payload.field) || false,
|
||||
}),
|
||||
});
|
||||
|
||||
const onSubmit = settingsOnSubmit(navigate, form);
|
||||
|
||||
useEffect(() => {
|
||||
if (!data) return;
|
||||
|
||||
form.setValues({
|
||||
urlsRoute: data.settings.urlsRoute ?? '/go',
|
||||
urlsLength: data.settings.urlsLength ?? 6,
|
||||
});
|
||||
}, [data]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<LoadingOverlay visible={isLoading} />
|
||||
<form onSubmit={form.onSubmit(onSubmit)}>
|
||||
<Stack gap='lg'>
|
||||
<TextInput
|
||||
label='Route'
|
||||
description='The route to use for short URLs. Requires a server restart.'
|
||||
placeholder='/go'
|
||||
{...form.getInputProps('urlsRoute')}
|
||||
/>
|
||||
|
||||
<form onSubmit={form.onSubmit(onSubmit)}>
|
||||
<Stack gap='lg'>
|
||||
<TextInput
|
||||
label='Route'
|
||||
description='The route to use for short URLs. Requires a server restart.'
|
||||
placeholder='/go'
|
||||
{...form.getInputProps('urlsRoute')}
|
||||
/>
|
||||
<NumberInput
|
||||
label='Length'
|
||||
description='The length of the short URL (for randomly generated names).'
|
||||
placeholder='6'
|
||||
min={1}
|
||||
max={64}
|
||||
{...form.getInputProps('urlsLength')}
|
||||
/>
|
||||
</Stack>
|
||||
|
||||
<NumberInput
|
||||
label='Length'
|
||||
description='The length of the short URL (for randomly generated names).'
|
||||
placeholder='6'
|
||||
min={1}
|
||||
max={64}
|
||||
{...form.getInputProps('urlsLength')}
|
||||
/>
|
||||
</Stack>
|
||||
|
||||
<Button type='submit' mt='md' loading={isLoading} leftSection={<IconDeviceFloppy size='1rem' />}>
|
||||
Save
|
||||
</Button>
|
||||
</form>
|
||||
</>
|
||||
<Button type='submit' mt='md' loading={isLoading} leftSection={<IconDeviceFloppy size='1rem' />}>
|
||||
Save
|
||||
</Button>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,45 +1,41 @@
|
||||
import { Response } from '@/lib/api/response';
|
||||
import type { Response } from '@/lib/api/response';
|
||||
import { Button, JsonInput, LoadingOverlay, Stack, Switch, TextInput } from '@mantine/core';
|
||||
import { useForm } from '@mantine/form';
|
||||
import { IconDeviceFloppy } from '@tabler/icons-react';
|
||||
import { useEffect } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { settingsOnSubmit } from '../settingsOnSubmit';
|
||||
import useServerSettings from '../useServerSettings';
|
||||
|
||||
const defaultExternalLinks = [
|
||||
{
|
||||
name: 'GitHub',
|
||||
url: 'https://github.com/diced/zipline',
|
||||
},
|
||||
{
|
||||
name: 'Documentation',
|
||||
url: 'https://zipline.diced.sh',
|
||||
},
|
||||
];
|
||||
export default function Website() {
|
||||
const { data, isLoading } = useServerSettings();
|
||||
|
||||
export default function Website({
|
||||
swr: { data, isLoading },
|
||||
}: {
|
||||
swr: { data: Response['/api/server/settings'] | undefined; isLoading: boolean };
|
||||
}) {
|
||||
return (
|
||||
<>
|
||||
<LoadingOverlay visible={isLoading} />
|
||||
{data ? <Form data={data} isLoading={isLoading} /> : null}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function Form({ data, isLoading }: { data: Response['/api/server/settings']; isLoading: boolean }) {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const form = useForm({
|
||||
initialValues: {
|
||||
websiteTitle: 'Zipline',
|
||||
websiteTitleLogo: '',
|
||||
websiteExternalLinks: JSON.stringify(defaultExternalLinks),
|
||||
websiteLoginBackground: '',
|
||||
websiteLoginBackgroundBlur: true,
|
||||
websiteDefaultAvatar: '',
|
||||
websiteTos: '',
|
||||
websiteTitle: data.settings.websiteTitle,
|
||||
websiteTitleLogo: data.settings.websiteTitleLogo,
|
||||
websiteExternalLinks: JSON.stringify(data.settings.websiteExternalLinks, null, 2),
|
||||
websiteLoginBackground: data.settings.websiteLoginBackground,
|
||||
websiteLoginBackgroundBlur: data.settings.websiteLoginBackgroundBlur,
|
||||
websiteDefaultAvatar: data.settings.websiteDefaultAvatar,
|
||||
websiteTos: data.settings.websiteTos,
|
||||
|
||||
websiteThemeDefault: 'system',
|
||||
websiteThemeDark: 'builtin:dark_gray',
|
||||
websiteThemeLight: 'builtin:light_gray',
|
||||
websiteThemeDefault: data.settings.websiteThemeDefault,
|
||||
websiteThemeDark: data.settings.websiteThemeDark,
|
||||
websiteThemeLight: data.settings.websiteThemeLight,
|
||||
},
|
||||
enhanceGetInputProps: (payload) => ({
|
||||
disabled: data?.tampered?.includes(payload.field) || false,
|
||||
disabled: data.tampered.includes(payload.field) || false,
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -59,12 +55,19 @@ export default function Website({
|
||||
}
|
||||
|
||||
sendValues.websiteTitleLogo =
|
||||
values.websiteTitleLogo.trim() === '' ? null : values.websiteTitleLogo.trim();
|
||||
values.websiteTitleLogo?.trim() === '' || !values.websiteTitleLogo?.trim()
|
||||
? null
|
||||
: values.websiteTitleLogo.trim();
|
||||
sendValues.websiteLoginBackground =
|
||||
values.websiteLoginBackground.trim() === '' ? null : values.websiteLoginBackground.trim();
|
||||
values.websiteLoginBackground?.trim() === '' || !values.websiteLoginBackground?.trim()
|
||||
? null
|
||||
: values.websiteLoginBackground.trim();
|
||||
sendValues.websiteDefaultAvatar =
|
||||
values.websiteDefaultAvatar.trim() === '' ? null : values.websiteDefaultAvatar.trim();
|
||||
sendValues.websiteTos = values.websiteTos.trim() === '' ? null : values.websiteTos.trim();
|
||||
values.websiteDefaultAvatar?.trim() === '' || !values.websiteDefaultAvatar?.trim()
|
||||
? null
|
||||
: values.websiteDefaultAvatar.trim();
|
||||
sendValues.websiteTos =
|
||||
values.websiteTos?.trim() === '' || !values.websiteTos?.trim() ? null : values.websiteTos.trim();
|
||||
|
||||
sendValues.websiteThemeDefault = values.websiteThemeDefault.trim();
|
||||
sendValues.websiteThemeDark = values.websiteThemeDark.trim();
|
||||
@@ -76,110 +79,92 @@ export default function Website({
|
||||
return settingsOnSubmit(navigate, form)(sendValues);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!data) return;
|
||||
|
||||
form.setValues({
|
||||
websiteTitle: data.settings.websiteTitle ?? 'Zipline',
|
||||
websiteTitleLogo: data.settings.websiteTitleLogo ?? '',
|
||||
websiteExternalLinks: JSON.stringify(
|
||||
data.settings.websiteExternalLinks ?? defaultExternalLinks,
|
||||
null,
|
||||
2,
|
||||
),
|
||||
websiteLoginBackground: data.settings.websiteLoginBackground ?? '',
|
||||
websiteLoginBackgroundBlur: data.settings.websiteLoginBackgroundBlur ?? true,
|
||||
websiteDefaultAvatar: data.settings.websiteDefaultAvatar ?? '',
|
||||
websiteTos: data.settings.websiteTos ?? '',
|
||||
websiteThemeDefault: data.settings.websiteThemeDefault ?? 'system',
|
||||
websiteThemeDark: data.settings.websiteThemeDark ?? 'builtin:dark_gray',
|
||||
websiteThemeLight: data.settings.websiteThemeLight ?? 'builtin:light_gray',
|
||||
});
|
||||
}, [data]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<LoadingOverlay visible={isLoading} />
|
||||
<form onSubmit={form.onSubmit(onSubmit)}>
|
||||
<Stack gap='lg'>
|
||||
<TextInput
|
||||
label='Title'
|
||||
description='The title of the website in browser tabs and at the top.'
|
||||
placeholder='Zipline'
|
||||
{...form.getInputProps('websiteTitle')}
|
||||
/>
|
||||
|
||||
<form onSubmit={form.onSubmit(onSubmit)}>
|
||||
<Stack gap='lg'>
|
||||
<TextInput
|
||||
label='Title'
|
||||
description='The title of the website in browser tabs and at the top.'
|
||||
placeholder='Zipline'
|
||||
{...form.getInputProps('websiteTitle')}
|
||||
/>
|
||||
<TextInput
|
||||
label='Title Logo'
|
||||
description='The URL to use for the title logo. This is placed to the left of the title.'
|
||||
placeholder='https://example.com/logo.png'
|
||||
{...form.getInputProps('websiteTitleLogo')}
|
||||
/>
|
||||
|
||||
<TextInput
|
||||
label='Title Logo'
|
||||
description='The URL to use for the title logo. This is placed to the left of the title.'
|
||||
placeholder='https://example.com/logo.png'
|
||||
{...form.getInputProps('websiteTitleLogo')}
|
||||
/>
|
||||
<JsonInput
|
||||
label='External Links'
|
||||
description='The external links to show in the footer. This must be valid JSON in the format of an array of objects with "name" and "url" properties. For example: [{"name": "GitHub", "url": "https://github.com/diced/zipline"}]'
|
||||
formatOnBlur
|
||||
minRows={1}
|
||||
maxRows={7}
|
||||
autosize
|
||||
placeholder={JSON.stringify(
|
||||
[
|
||||
{ name: 'GitHub', url: 'https://github.com/diced/zipline' },
|
||||
{ name: 'Documentation', url: 'https://zipline.diced.sh' },
|
||||
],
|
||||
null,
|
||||
2,
|
||||
)}
|
||||
{...form.getInputProps('websiteExternalLinks')}
|
||||
/>
|
||||
|
||||
<JsonInput
|
||||
label='External Links'
|
||||
description='The external links to show in the footer. This must be valid JSON in the format of an array of objects with "name" and "url" properties. For example: [{"name": "GitHub", "url": "https://github.com/diced/zipline"}]'
|
||||
formatOnBlur
|
||||
minRows={1}
|
||||
maxRows={7}
|
||||
autosize
|
||||
placeholder={JSON.stringify(defaultExternalLinks, null, 2)}
|
||||
{...form.getInputProps('websiteExternalLinks')}
|
||||
/>
|
||||
<TextInput
|
||||
label='Login Background'
|
||||
description='The URL to use for the login background.'
|
||||
placeholder='https://example.com/background.png'
|
||||
{...form.getInputProps('websiteLoginBackground')}
|
||||
/>
|
||||
|
||||
<TextInput
|
||||
label='Login Background'
|
||||
description='The URL to use for the login background.'
|
||||
placeholder='https://example.com/background.png'
|
||||
{...form.getInputProps('websiteLoginBackground')}
|
||||
/>
|
||||
<Switch
|
||||
label='Login Background Blur'
|
||||
description='Whether to blur the login background.'
|
||||
{...form.getInputProps('websiteLoginBackgroundBlur', { type: 'checkbox' })}
|
||||
/>
|
||||
|
||||
<Switch
|
||||
label='Login Background Blur'
|
||||
description='Whether to blur the login background.'
|
||||
{...form.getInputProps('websiteLoginBackgroundBlur', { type: 'checkbox' })}
|
||||
/>
|
||||
<TextInput
|
||||
label='Default Avatar'
|
||||
description='The path to use for the default avatar. This must be a path to an image, not a URL.'
|
||||
placeholder='/zipline/avatar.png'
|
||||
{...form.getInputProps('websiteDefaultAvatar')}
|
||||
/>
|
||||
|
||||
<TextInput
|
||||
label='Default Avatar'
|
||||
description='The path to use for the default avatar. This must be a path to an image, not a URL.'
|
||||
placeholder='/zipline/avatar.png'
|
||||
{...form.getInputProps('websiteDefaultAvatar')}
|
||||
/>
|
||||
<TextInput
|
||||
label='Terms of Service'
|
||||
description='Path to a Markdown (.md) file to use for the terms of service.'
|
||||
placeholder='/zipline/TOS.md'
|
||||
{...form.getInputProps('websiteTos')}
|
||||
/>
|
||||
|
||||
<TextInput
|
||||
label='Terms of Service'
|
||||
description='Path to a Markdown (.md) file to use for the terms of service.'
|
||||
placeholder='/zipline/TOS.md'
|
||||
{...form.getInputProps('websiteTos')}
|
||||
/>
|
||||
<TextInput
|
||||
label='Default Theme'
|
||||
description='The default theme to use for the website.'
|
||||
placeholder='system'
|
||||
{...form.getInputProps('websiteThemeDefault')}
|
||||
/>
|
||||
|
||||
<TextInput
|
||||
label='Default Theme'
|
||||
description='The default theme to use for the website.'
|
||||
placeholder='system'
|
||||
{...form.getInputProps('websiteThemeDefault')}
|
||||
/>
|
||||
<TextInput
|
||||
label='Dark Theme'
|
||||
description='The dark theme to use for the website when the default theme is "system".'
|
||||
placeholder='builtin:dark_gray'
|
||||
{...form.getInputProps('websiteThemeDark')}
|
||||
/>
|
||||
|
||||
<TextInput
|
||||
label='Dark Theme'
|
||||
description='The dark theme to use for the website when the default theme is "system".'
|
||||
placeholder='builtin:dark_gray'
|
||||
{...form.getInputProps('websiteThemeDark')}
|
||||
/>
|
||||
|
||||
<TextInput
|
||||
label='Light Theme'
|
||||
description='The light theme to use for the website when the default theme is "system".'
|
||||
placeholder='builtin:light_gray'
|
||||
{...form.getInputProps('websiteThemeLight')}
|
||||
/>
|
||||
</Stack>
|
||||
<Button type='submit' mt='md' loading={isLoading} leftSection={<IconDeviceFloppy size='1rem' />}>
|
||||
Save
|
||||
</Button>
|
||||
</form>
|
||||
</>
|
||||
<TextInput
|
||||
label='Light Theme'
|
||||
description='The light theme to use for the website when the default theme is "system".'
|
||||
placeholder='builtin:light_gray'
|
||||
{...form.getInputProps('websiteThemeLight')}
|
||||
/>
|
||||
</Stack>
|
||||
<Button type='submit' mt='md' loading={isLoading} leftSection={<IconDeviceFloppy size='1rem' />}>
|
||||
Save
|
||||
</Button>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
6
src/components/pages/serverSettings/useServerSettings.ts
Normal file
6
src/components/pages/serverSettings/useServerSettings.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import type { Response } from '@/lib/api/response';
|
||||
import useSWR from 'swr';
|
||||
|
||||
export default function useServerSettings() {
|
||||
return useSWR<Response['/api/server/settings']>('/api/server/settings');
|
||||
}
|
||||
@@ -25,7 +25,7 @@ import {
|
||||
IconSettingsFilled,
|
||||
IconX,
|
||||
} from '@tabler/icons-react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useState } from 'react';
|
||||
|
||||
export default function SettingsAvatar() {
|
||||
const user = useUserStore((state) => state.user);
|
||||
@@ -36,14 +36,16 @@ export default function SettingsAvatar() {
|
||||
const [avatar, setAvatar] = useState<File | null>(null);
|
||||
const [avatarSrc, setAvatarSrc] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
if (!avatar) return;
|
||||
const onAvatarChange = async (file: File | null) => {
|
||||
setAvatar(file);
|
||||
|
||||
const base64url = await readToDataURL(avatar);
|
||||
setAvatarSrc(base64url);
|
||||
})();
|
||||
}, [avatar]);
|
||||
if (!file) {
|
||||
setAvatarSrc(null);
|
||||
return;
|
||||
}
|
||||
|
||||
setAvatarSrc(await readToDataURL(file));
|
||||
};
|
||||
|
||||
const saveAvatar = async () => {
|
||||
if (!avatar) return;
|
||||
@@ -111,7 +113,7 @@ export default function SettingsAvatar() {
|
||||
accept='image/*'
|
||||
placeholder='Upload new avatar...'
|
||||
value={avatar}
|
||||
onChange={(file) => setAvatar(file)}
|
||||
onChange={onAvatarChange}
|
||||
leftSection={<IconPhotoUp size='1rem' />}
|
||||
/>
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { User } from '@/lib/db/models/user';
|
||||
import { Response } from '@/lib/api/response';
|
||||
import { fetchApi } from '@/lib/fetchApi';
|
||||
import { useUserStore } from '@/lib/client/store/user';
|
||||
@@ -27,7 +28,6 @@ import {
|
||||
IconDeviceFloppy,
|
||||
IconFileX,
|
||||
} from '@tabler/icons-react';
|
||||
import { useEffect } from 'react';
|
||||
import { mutate } from 'swr';
|
||||
import { useShallow } from 'zustand/shallow';
|
||||
|
||||
@@ -40,19 +40,34 @@ const alignIcons: Record<string, React.ReactNode> = {
|
||||
export default function SettingsFileView() {
|
||||
const [user, setUser] = useUserStore(useShallow((state) => [state.user, state.setUser]));
|
||||
|
||||
if (!user) {
|
||||
return (
|
||||
<Paper withBorder p='sm'>
|
||||
<Title order={2}>Viewing Files</Title>
|
||||
<Text c='dimmed' mt='xs'>
|
||||
Loading…
|
||||
</Text>
|
||||
</Paper>
|
||||
);
|
||||
}
|
||||
|
||||
return <Form user={user} setUser={setUser} />;
|
||||
}
|
||||
|
||||
function Form({ user, setUser }: { user: User; setUser: (u: User) => void }) {
|
||||
const form = useForm({
|
||||
initialValues: {
|
||||
enabled: user?.view.enabled ?? false,
|
||||
content: user?.view.content ?? '',
|
||||
embed: user?.view.embed ?? false,
|
||||
embedTitle: user?.view.embedTitle ?? '',
|
||||
embedDescription: user?.view.embedDescription ?? '',
|
||||
embedSiteName: user?.view.embedSiteName ?? '',
|
||||
embedColor: user?.view.embedColor ?? '',
|
||||
align: user?.view.align ?? 'left',
|
||||
showMimetype: user?.view.showMimetype ?? false,
|
||||
showTags: user?.view.showTags ?? false,
|
||||
showFolder: user?.view.showFolder ?? false,
|
||||
enabled: user.view.enabled || false,
|
||||
content: user.view.content || '',
|
||||
embed: user.view.embed || false,
|
||||
embedTitle: user.view.embedTitle || '',
|
||||
embedDescription: user.view.embedDescription || '',
|
||||
embedSiteName: user.view.embedSiteName || '',
|
||||
embedColor: user.view.embedColor || '',
|
||||
align: user.view.align || 'left',
|
||||
showMimetype: user.view.showMimetype || false,
|
||||
showTags: user.view.showTags || false,
|
||||
showFolder: user.view.showFolder || false,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -95,24 +110,6 @@ export default function SettingsFileView() {
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (user) {
|
||||
form.setValues({
|
||||
enabled: user.view.enabled || false,
|
||||
content: user.view.content || '',
|
||||
embed: user.view.embed || false,
|
||||
embedTitle: user.view.embedTitle || '',
|
||||
embedDescription: user.view.embedDescription || '',
|
||||
embedSiteName: user.view.embedSiteName || '',
|
||||
embedColor: user.view.embedColor || '',
|
||||
align: user.view.align || 'left',
|
||||
showMimetype: user.view.showMimetype || false,
|
||||
showTags: user.view.showTags || false,
|
||||
showFolder: user.view.showFolder || false,
|
||||
});
|
||||
}
|
||||
}, [user]);
|
||||
|
||||
return (
|
||||
<Paper withBorder p='sm'>
|
||||
<Title order={2}>Viewing Files</Title>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { User } from '@/lib/db/models/user';
|
||||
import { ApiError } from '@/lib/api/errors';
|
||||
import { Response } from '@/lib/api/response';
|
||||
import { fetchApi } from '@/lib/fetchApi';
|
||||
@@ -25,29 +26,36 @@ import {
|
||||
IconUser,
|
||||
IconUserCancel,
|
||||
} from '@tabler/icons-react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useState } from 'react';
|
||||
import { mutate } from 'swr';
|
||||
import useSWR from 'swr';
|
||||
import { useShallow } from 'zustand/shallow';
|
||||
|
||||
export default function SettingsUser() {
|
||||
const [user, setUser] = useUserStore(useShallow((state) => [state.user, state.setUser]));
|
||||
|
||||
const { data: tokenPayload } = useSWR<Response['/api/user/token']>('/api/user/token');
|
||||
|
||||
if (!user) {
|
||||
return (
|
||||
<Paper withBorder p='sm'>
|
||||
<Title order={2}>User</Title>
|
||||
<Text c='dimmed' size='sm' mt='sm'>
|
||||
Loading…
|
||||
</Text>
|
||||
</Paper>
|
||||
);
|
||||
}
|
||||
|
||||
return <Form user={user} setUser={setUser} token={tokenPayload?.token ?? ''} />;
|
||||
}
|
||||
|
||||
function Form({ user, setUser, token }: { user: User; setUser: (u: User) => void; token: string }) {
|
||||
const [tokenShown, setTokenShown] = useState(false);
|
||||
const [token, setToken] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
const { data } = await fetchApi<Response['/api/user/token']>('/api/user/token');
|
||||
|
||||
if (data) {
|
||||
setToken(data.token || '');
|
||||
}
|
||||
})();
|
||||
}, []);
|
||||
|
||||
const form = useForm({
|
||||
initialValues: {
|
||||
username: user?.username ?? '',
|
||||
username: user.username,
|
||||
password: '',
|
||||
},
|
||||
validate: {
|
||||
@@ -61,7 +69,7 @@ export default function SettingsUser() {
|
||||
password?: string;
|
||||
} = {};
|
||||
|
||||
if (values.username !== user?.username) send['username'] = values.username.trim();
|
||||
if (values.username !== user.username) send['username'] = values.username.trim();
|
||||
if (values.password) send['password'] = values.password.trim();
|
||||
|
||||
const { data, error } = await fetchApi<Response['/api/user']>('/api/user', 'PATCH', send);
|
||||
@@ -84,6 +92,7 @@ export default function SettingsUser() {
|
||||
if (!data?.user) return;
|
||||
|
||||
mutate('/api/user');
|
||||
mutate('/api/user/token');
|
||||
setUser(data.user);
|
||||
notifications.show({
|
||||
message: 'User updated',
|
||||
@@ -96,7 +105,7 @@ export default function SettingsUser() {
|
||||
<Paper withBorder p='sm'>
|
||||
<Title order={2}>User</Title>
|
||||
<Text c='dimmed' size='sm' mb='sm'>
|
||||
{user?.id}
|
||||
{user.id}
|
||||
</Text>
|
||||
|
||||
<form onSubmit={form.onSubmit(onSubmit)}>
|
||||
@@ -134,7 +143,7 @@ export default function SettingsUser() {
|
||||
leftSection={<IconAsteriskSimple size='1rem' />}
|
||||
/>
|
||||
|
||||
<Button type='submit' mt='md' loading={!user} leftSection={<IconDeviceFloppy size='1rem' />}>
|
||||
<Button type='submit' mt='md' leftSection={<IconDeviceFloppy size='1rem' />}>
|
||||
Save
|
||||
</Button>
|
||||
</form>
|
||||
|
||||
24
src/lib/client/hooks/useUser.ts
Normal file
24
src/lib/client/hooks/useUser.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import type { Response } from '@/lib/api/response';
|
||||
import useSWR from 'swr';
|
||||
|
||||
async function fetcher(url: string): Promise<Response['/api/user'] | null> {
|
||||
const res = await fetch(url);
|
||||
if (!res.ok) return null;
|
||||
|
||||
return res.json();
|
||||
}
|
||||
|
||||
export default function useUser(): {
|
||||
user: Response['/api/user']['user'] | undefined;
|
||||
loading: boolean;
|
||||
} {
|
||||
const { data, isLoading } = useSWR<Response['/api/user'] | null>('/api/user', fetcher, {
|
||||
revalidateOnFocus: false,
|
||||
revalidateOnReconnect: false,
|
||||
refreshWhenHidden: false,
|
||||
revalidateIfStale: false,
|
||||
shouldRetryOnError: false,
|
||||
});
|
||||
|
||||
return { user: data?.user, loading: isLoading };
|
||||
}
|
||||
@@ -1,15 +1,16 @@
|
||||
import useSWR from 'swr';
|
||||
import { Response } from '../../api/response';
|
||||
const f = async () => {
|
||||
|
||||
async function fetcher() {
|
||||
const res = await fetch('/api/version');
|
||||
if (!res.ok) throw new Error('Failed to fetch version');
|
||||
|
||||
const r = await res.json();
|
||||
return r;
|
||||
};
|
||||
}
|
||||
|
||||
export default function useVersion() {
|
||||
const { isLoading, data } = useSWR<Response['/api/version'], Error>('/api/version', f, {
|
||||
const { isLoading, data } = useSWR<Response['/api/version'], Error>('/api/version', fetcher, {
|
||||
refreshInterval: undefined,
|
||||
revalidateOnFocus: false,
|
||||
revalidateIfStale: false,
|
||||
|
||||
Reference in New Issue
Block a user