mirror of
https://github.com/diced/zipline.git
synced 2025-05-11 18:36:02 +02:00
Merge 6e57d3140d
into 8b9303ed80
This commit is contained in:
commit
b5cabfb55f
25 changed files with 1970 additions and 882 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -45,4 +45,4 @@ next-env.d.ts
|
|||
# zipline
|
||||
uploads*/
|
||||
*.crt
|
||||
*.key
|
||||
*.key
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { Response } from '@/lib/api/response';
|
||||
import { Group, SimpleGrid, Skeleton, Stack, Title } from '@mantine/core';
|
||||
import { Group, SimpleGrid, Skeleton, Stack, Title, Tooltip } from '@mantine/core';
|
||||
import useSWR from 'swr';
|
||||
import dynamic from 'next/dynamic';
|
||||
|
||||
|
@ -93,3 +93,42 @@ export default function DashboardSettings() {
|
|||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export function EnvTooltip(
|
||||
props: React.PropsWithChildren<{
|
||||
envVar: string;
|
||||
data: any;
|
||||
varKey: string;
|
||||
}>,
|
||||
) {
|
||||
const state = checkPropSafe(props);
|
||||
const enabled = state !== false;
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
label={
|
||||
enabled
|
||||
? `WARNING: The ${props.envVar} environment variable takes priority over this value. Currently "${state}"`
|
||||
: ''
|
||||
}
|
||||
color='red'
|
||||
events={{
|
||||
hover: enabled,
|
||||
focus: false,
|
||||
touch: false,
|
||||
}}
|
||||
>
|
||||
<div>{props.children}</div>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
function checkPropSafe(props: any): boolean | string {
|
||||
const data = props.data;
|
||||
if (data === undefined) return false;
|
||||
const locked = data.locked;
|
||||
if (locked === undefined) return false;
|
||||
const val = locked[props.varKey];
|
||||
if (val === undefined) return false;
|
||||
return val;
|
||||
}
|
||||
|
|
|
@ -5,6 +5,7 @@ import { IconDeviceFloppy } from '@tabler/icons-react';
|
|||
import { useRouter } from 'next/router';
|
||||
import { useEffect } from 'react';
|
||||
import { settingsOnSubmit } from '../settingsOnSubmit';
|
||||
import { EnvTooltip } from '..';
|
||||
|
||||
export default function ServerSettingsChunks({
|
||||
swr: { data, isLoading },
|
||||
|
@ -39,29 +40,35 @@ export default function ServerSettingsChunks({
|
|||
<Title order={2}>Chunks</Title>
|
||||
|
||||
<form onSubmit={form.onSubmit(onSubmit)}>
|
||||
<Switch
|
||||
mt='md'
|
||||
label='Enable Chunks'
|
||||
description='Enable chunked uploads.'
|
||||
{...form.getInputProps('chunksEnabled', { type: 'checkbox' })}
|
||||
/>
|
||||
<EnvTooltip envVar='CHUNKS_ENABLED' data={data} varKey='chunksEnabled'>
|
||||
<Switch
|
||||
mt='md'
|
||||
label='Enable Chunks'
|
||||
description='Enable chunked uploads.'
|
||||
{...form.getInputProps('chunksEnabled', { type: 'checkbox' })}
|
||||
/>
|
||||
</EnvTooltip>
|
||||
|
||||
<SimpleGrid mt='md' cols={{ base: 1, md: 2 }} spacing='lg'>
|
||||
<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')}
|
||||
/>
|
||||
<EnvTooltip envVar='CHUNKS_MAX' data={data} varKey='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')}
|
||||
/>
|
||||
</EnvTooltip>
|
||||
|
||||
<TextInput
|
||||
label='Chunk Size'
|
||||
description='Size of each chunk.'
|
||||
placeholder='25mb'
|
||||
disabled={!form.values.chunksEnabled}
|
||||
{...form.getInputProps('chunksSize')}
|
||||
/>
|
||||
<EnvTooltip envVar='CHUNKS_SIZE' data={data} varKey='chunksSize'>
|
||||
<TextInput
|
||||
label='Chunk Size'
|
||||
description='Size of each chunk.'
|
||||
placeholder='25mb'
|
||||
disabled={!form.values.chunksEnabled}
|
||||
{...form.getInputProps('chunksSize')}
|
||||
/>
|
||||
</EnvTooltip>
|
||||
</SimpleGrid>
|
||||
|
||||
<Button type='submit' mt='md' loading={isLoading} leftSection={<IconDeviceFloppy size='1rem' />}>
|
||||
|
|
|
@ -5,6 +5,7 @@ import { IconDeviceFloppy } from '@tabler/icons-react';
|
|||
import { useRouter } from 'next/router';
|
||||
import { useEffect } from 'react';
|
||||
import { settingsOnSubmit } from '../settingsOnSubmit';
|
||||
import { EnvTooltip } from '..';
|
||||
|
||||
export default function ServerSettingsCore({
|
||||
swr: { data, isLoading },
|
||||
|
@ -49,27 +50,33 @@ export default function ServerSettingsCore({
|
|||
<Title order={2}>Core</Title>
|
||||
|
||||
<form onSubmit={form.onSubmit(onSubmit)}>
|
||||
<Switch
|
||||
mt='md'
|
||||
label='Return HTTPS URLs'
|
||||
description='Return URLs with HTTPS protocol.'
|
||||
{...form.getInputProps('coreReturnHttpsUrls', { type: 'checkbox' })}
|
||||
/>
|
||||
<EnvTooltip envVar='CORE_RETURN_HTTPS_URLS' data={data} varKey='coreReturnHttpsUrls'>
|
||||
<Switch
|
||||
mt='md'
|
||||
label='Return HTTPS URLs'
|
||||
description='Return URLs with HTTPS protocol.'
|
||||
{...form.getInputProps('coreReturnHttpsUrls', { type: 'checkbox' })}
|
||||
/>
|
||||
</EnvTooltip>
|
||||
|
||||
<SimpleGrid mt='md' cols={{ base: 1, md: 2 }} spacing='lg'>
|
||||
<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')}
|
||||
/>
|
||||
<EnvTooltip envVar='CORE_DEFAULT_DOMAIN' data={data} varKey='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')}
|
||||
/>
|
||||
</EnvTooltip>
|
||||
|
||||
<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')}
|
||||
/>
|
||||
<EnvTooltip envVar='CORE_TEMP_DIRECTORY' data={data} varKey='coreTempDirectory'>
|
||||
<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')}
|
||||
/>
|
||||
</EnvTooltip>
|
||||
</SimpleGrid>
|
||||
|
||||
<Button type='submit' mt='md' loading={isLoading} leftSection={<IconDeviceFloppy size='1rem' />}>
|
||||
|
|
|
@ -16,6 +16,7 @@ import { IconDeviceFloppy } from '@tabler/icons-react';
|
|||
import { useRouter } from 'next/router';
|
||||
import { useEffect } from 'react';
|
||||
import { settingsOnSubmit } from '../settingsOnSubmit';
|
||||
import { EnvTooltip } from '..';
|
||||
|
||||
type DiscordEmbed = Record<string, any>;
|
||||
|
||||
|
@ -169,27 +170,33 @@ export default function ServerSettingsDiscord({
|
|||
<Title order={2}>Discord Webhook</Title>
|
||||
|
||||
<form onSubmit={formMain.onSubmit(onSubmitMain)}>
|
||||
<TextInput
|
||||
mt='md'
|
||||
label='Webhook URL'
|
||||
description='The Discord webhook URL to send notifications to'
|
||||
placeholder='https://discord.com/api/webhooks/...'
|
||||
{...formMain.getInputProps('discordWebhookUrl')}
|
||||
/>
|
||||
<EnvTooltip envVar='DISCORD_WEBHOOK_URL' data={data} varKey='discordWebhookUrl'>
|
||||
<TextInput
|
||||
mt='md'
|
||||
label='Webhook URL'
|
||||
description='The Discord webhook URL to send notifications to'
|
||||
placeholder='https://discord.com/api/webhooks/...'
|
||||
{...formMain.getInputProps('discordWebhookUrl')}
|
||||
/>
|
||||
</EnvTooltip>
|
||||
|
||||
<SimpleGrid mt='md' cols={{ base: 1, md: 2 }} spacing='lg'>
|
||||
<TextInput
|
||||
label='Username'
|
||||
description='The username to send notifications as'
|
||||
{...formMain.getInputProps('discordUsername')}
|
||||
/>
|
||||
<EnvTooltip envVar='DISCORD_USERNAME' data={data} varKey='discordUsername'>
|
||||
<TextInput
|
||||
label='Username'
|
||||
description='The username to send notifications as'
|
||||
{...formMain.getInputProps('discordUsername')}
|
||||
/>
|
||||
</EnvTooltip>
|
||||
|
||||
<TextInput
|
||||
label='Avatar URL'
|
||||
description='The avatar for the webhook'
|
||||
placeholder='https://example.com/avatar.png'
|
||||
{...formMain.getInputProps('discordAvatarUrl')}
|
||||
/>
|
||||
<EnvTooltip envVar='DISCORD_AVATAR_URL' data={data} varKey='discordAvatarUrl'>
|
||||
<TextInput
|
||||
label='Avatar URL'
|
||||
description='The avatar for the webhook'
|
||||
placeholder='https://example.com/avatar.png'
|
||||
{...formMain.getInputProps('discordAvatarUrl')}
|
||||
/>
|
||||
</EnvTooltip>
|
||||
</SimpleGrid>
|
||||
|
||||
<Button type='submit' mt='md' loading={isLoading} leftSection={<IconDeviceFloppy size='1rem' />}>
|
||||
|
@ -202,98 +209,110 @@ export default function ServerSettingsDiscord({
|
|||
<Title order={3}>On Upload</Title>
|
||||
|
||||
<form onSubmit={formOnUpload.onSubmit(onSubmitNotif('upload'))}>
|
||||
<TextInput
|
||||
mt='md'
|
||||
label='Webhook URL'
|
||||
description='The Discord webhook URL to send notifications to. If this is left blank, the main webhook url will be used'
|
||||
placeholder='https://discord.com/api/webhooks/...'
|
||||
{...formOnUpload.getInputProps('discordOnUploadWebhookUrl')}
|
||||
/>
|
||||
<EnvTooltip envVar='DISCORD_ON_UPLOAD_WEBHOOK_URL' data={data} varKey='discordOnUploadWebhookUrl'>
|
||||
<TextInput
|
||||
mt='md'
|
||||
label='Webhook URL'
|
||||
description='The Discord webhook URL to send notifications to. If this is left blank, the main webhook url will be used'
|
||||
placeholder='https://discord.com/api/webhooks/...'
|
||||
{...formOnUpload.getInputProps('discordOnUploadWebhookUrl')}
|
||||
/>
|
||||
</EnvTooltip>
|
||||
|
||||
<SimpleGrid mt='md' cols={{ base: 1, md: 2 }} spacing='lg'>
|
||||
<TextInput
|
||||
label='Username'
|
||||
description='The username to send notifications as. If this is left blank, the main username will be used'
|
||||
{...formOnUpload.getInputProps('discordOnUploadUsername')}
|
||||
/>
|
||||
<EnvTooltip envVar='DISCORD_ON_UPLOAD_USERNAME' data={data} varKey='discordOnUploadUsername'>
|
||||
<TextInput
|
||||
label='Username'
|
||||
description='The username to send notifications as. If this is left blank, the main username will be used'
|
||||
{...formOnUpload.getInputProps('discordOnUploadUsername')}
|
||||
/>
|
||||
</EnvTooltip>
|
||||
|
||||
<TextInput
|
||||
label='Avatar URL'
|
||||
description='The avatar for the webhook. If this is left blank, the main avatar will be used'
|
||||
placeholder='https://example.com/avatar.png'
|
||||
{...formOnUpload.getInputProps('discordOnUploadAvatarUrl')}
|
||||
/>
|
||||
<EnvTooltip envVar='DISCORD_ON_UPLOAD_AVATAR_URL' data={data} varKey='discordOnUploadAvatarUrl'>
|
||||
<TextInput
|
||||
label='Avatar URL'
|
||||
description='The avatar for the webhook. If this is left blank, the main avatar will be used'
|
||||
placeholder='https://example.com/avatar.png'
|
||||
{...formOnUpload.getInputProps('discordOnUploadAvatarUrl')}
|
||||
/>
|
||||
</EnvTooltip>
|
||||
</SimpleGrid>
|
||||
|
||||
<Textarea
|
||||
mt='md'
|
||||
label='Content'
|
||||
description='The content of the notification. This can be blank, but at least one of the content or embed fields must be filled out'
|
||||
minRows={1}
|
||||
maxRows={7}
|
||||
{...formOnUpload.getInputProps('discordOnUploadContent')}
|
||||
/>
|
||||
<EnvTooltip envVar='DISCORD_ON_UPLOAD_CONTENT' data={data} varKey='discordOnUploadContent'>
|
||||
<Textarea
|
||||
mt='md'
|
||||
label='Content'
|
||||
description='The content of the notification. This can be blank, but at least one of the content or embed fields must be filled out'
|
||||
minRows={1}
|
||||
maxRows={7}
|
||||
{...formOnUpload.getInputProps('discordOnUploadContent')}
|
||||
/>
|
||||
</EnvTooltip>
|
||||
|
||||
<Switch
|
||||
mt='md'
|
||||
label='Embed'
|
||||
description='Send the notification as an embed. This will allow for more customization below.'
|
||||
{...formOnUpload.getInputProps('discordOnUploadEmbed', { type: 'checkbox' })}
|
||||
/>
|
||||
<EnvTooltip envVar='DISCORD_ON_UPLOAD_EMBED' data={data} varKey='discordOnUploadEmbed'>
|
||||
<Switch
|
||||
mt='md'
|
||||
label='Embed'
|
||||
description='Send the notification as an embed. This will allow for more customization below.'
|
||||
{...formOnUpload.getInputProps('discordOnUploadEmbed', { type: 'checkbox' })}
|
||||
/>
|
||||
|
||||
<Collapse in={formOnUpload.values.discordOnUploadEmbed}>
|
||||
<Paper withBorder p='sm' mt='md'>
|
||||
<SimpleGrid cols={{ base: 1, md: 2 }} spacing='lg'>
|
||||
<TextInput
|
||||
label='Title'
|
||||
description='The title of the embed'
|
||||
{...formOnUpload.getInputProps('discordOnUploadEmbedTitle')}
|
||||
/>
|
||||
<Collapse in={formOnUpload.values.discordOnUploadEmbed}>
|
||||
<Paper withBorder p='sm' mt='md'>
|
||||
<SimpleGrid cols={{ base: 1, md: 2 }} spacing='lg'>
|
||||
<TextInput
|
||||
label='Title'
|
||||
description='The title of the embed'
|
||||
{...formOnUpload.getInputProps('discordOnUploadEmbedTitle')}
|
||||
/>
|
||||
|
||||
<TextInput
|
||||
label='Description'
|
||||
description='The description of the embed'
|
||||
{...formOnUpload.getInputProps('discordOnUploadEmbedDescription')}
|
||||
/>
|
||||
<TextInput
|
||||
label='Description'
|
||||
description='The description of the embed'
|
||||
{...formOnUpload.getInputProps('discordOnUploadEmbedDescription')}
|
||||
/>
|
||||
|
||||
<TextInput
|
||||
label='Footer'
|
||||
description='The footer of the embed'
|
||||
{...formOnUpload.getInputProps('discordOnUploadEmbedFooter')}
|
||||
/>
|
||||
<TextInput
|
||||
label='Footer'
|
||||
description='The footer of the embed'
|
||||
{...formOnUpload.getInputProps('discordOnUploadEmbedFooter')}
|
||||
/>
|
||||
|
||||
<ColorInput
|
||||
label='Color'
|
||||
description='The color of the embed'
|
||||
{...formOnUpload.getInputProps('discordOnUploadEmbedColor')}
|
||||
/>
|
||||
<ColorInput
|
||||
label='Color'
|
||||
description='The color of the embed'
|
||||
{...formOnUpload.getInputProps('discordOnUploadEmbedColor')}
|
||||
/>
|
||||
|
||||
<Switch
|
||||
label='Thumbnail'
|
||||
description="Show the thumbnail (it will show the file if it's an image) in the embed"
|
||||
{...formOnUpload.getInputProps('discordOnUploadEmbedThumbnail', { type: 'checkbox' })}
|
||||
/>
|
||||
<Switch
|
||||
label='Thumbnail'
|
||||
description="Show the thumbnail (it will show the file if it's an image) in the embed"
|
||||
{...formOnUpload.getInputProps('discordOnUploadEmbedThumbnail', { type: 'checkbox' })}
|
||||
/>
|
||||
|
||||
<Switch
|
||||
label='Image/Video'
|
||||
description='Show the image or video in the embed'
|
||||
{...formOnUpload.getInputProps('discordOnUploadEmbedImageOrVideo', { type: 'checkbox' })}
|
||||
/>
|
||||
<Switch
|
||||
label='Image/Video'
|
||||
description='Show the image or video in the embed'
|
||||
{...formOnUpload.getInputProps('discordOnUploadEmbedImageOrVideo', {
|
||||
type: 'checkbox',
|
||||
})}
|
||||
/>
|
||||
|
||||
<Switch
|
||||
label='Timestamp'
|
||||
description='Show the timestamp in the embed'
|
||||
{...formOnUpload.getInputProps('discordOnUploadEmbedTimestamp', { type: 'checkbox' })}
|
||||
/>
|
||||
<Switch
|
||||
label='Timestamp'
|
||||
description='Show the timestamp in the embed'
|
||||
{...formOnUpload.getInputProps('discordOnUploadEmbedTimestamp', { type: 'checkbox' })}
|
||||
/>
|
||||
|
||||
<Switch
|
||||
label='URL'
|
||||
description='Makes the title clickable and links to the URL of the file'
|
||||
{...formOnUpload.getInputProps('discordOnUploadEmbedUrl', { type: 'checkbox' })}
|
||||
/>
|
||||
</SimpleGrid>
|
||||
</Paper>
|
||||
</Collapse>
|
||||
<Switch
|
||||
label='URL'
|
||||
description='Makes the title clickable and links to the URL of the file'
|
||||
{...formOnUpload.getInputProps('discordOnUploadEmbedUrl', { type: 'checkbox' })}
|
||||
/>
|
||||
</SimpleGrid>
|
||||
</Paper>
|
||||
</Collapse>
|
||||
</EnvTooltip>
|
||||
|
||||
<Button type='submit' mt='md' loading={isLoading} leftSection={<IconDeviceFloppy size='1rem' />}>
|
||||
Save
|
||||
|
@ -305,86 +324,104 @@ export default function ServerSettingsDiscord({
|
|||
<Title order={3}>On Shorten</Title>
|
||||
|
||||
<form onSubmit={formOnShorten.onSubmit(onSubmitNotif('shorten'))}>
|
||||
<TextInput
|
||||
mt='md'
|
||||
label='Webhook URL'
|
||||
description='The Discord webhook URL to send notifications to. If this is left blank, the main webhook url will be used'
|
||||
placeholder='https://discord.com/api/webhooks/...'
|
||||
{...formOnShorten.getInputProps('discordOnShortenWebhookUrl')}
|
||||
/>
|
||||
<EnvTooltip
|
||||
envVar='DISCORD_ON_SHORTEN_WEBHOOK_URL'
|
||||
data={data}
|
||||
varKey='discordOnShortenWebhookUrl'
|
||||
>
|
||||
<TextInput
|
||||
mt='md'
|
||||
label='Webhook URL'
|
||||
description='The Discord webhook URL to send notifications to. If this is left blank, the main webhook url will be used'
|
||||
placeholder='https://discord.com/api/webhooks/...'
|
||||
{...formOnShorten.getInputProps('discordOnShortenWebhookUrl')}
|
||||
/>
|
||||
</EnvTooltip>
|
||||
|
||||
<SimpleGrid mt='md' cols={{ base: 1, md: 2 }} spacing='lg'>
|
||||
<TextInput
|
||||
label='Username'
|
||||
description='The username to send notifications as. If this is left blank, the main username will be used'
|
||||
{...formOnShorten.getInputProps('discordOnShortenUsername')}
|
||||
/>
|
||||
<EnvTooltip envVar='DISCORD_ON_SHORTEN_USERNAME' data={data} varKey='discordOnShortenUsername'>
|
||||
<TextInput
|
||||
label='Username'
|
||||
description='The username to send notifications as. If this is left blank, the main username will be used'
|
||||
{...formOnShorten.getInputProps('discordOnShortenUsername')}
|
||||
/>
|
||||
</EnvTooltip>
|
||||
|
||||
<TextInput
|
||||
label='Avatar URL'
|
||||
description='The avatar for the webhook. If this is left blank, the main avatar will be used'
|
||||
placeholder='https://example.com/avatar.png'
|
||||
{...formOnShorten.getInputProps('discordOnShortenAvatarUrl')}
|
||||
/>
|
||||
<EnvTooltip
|
||||
envVar='DISCORD_ON_SHORTEN_AVATAR_URL'
|
||||
data={data}
|
||||
varKey='discordOnShortenAvatarUrl'
|
||||
>
|
||||
<TextInput
|
||||
label='Avatar URL'
|
||||
description='The avatar for the webhook. If this is left blank, the main avatar will be used'
|
||||
placeholder='https://example.com/avatar.png'
|
||||
{...formOnShorten.getInputProps('discordOnShortenAvatarUrl')}
|
||||
/>
|
||||
</EnvTooltip>
|
||||
</SimpleGrid>
|
||||
|
||||
<Textarea
|
||||
mt='md'
|
||||
label='Content'
|
||||
description='The content of the notification. This can be blank, but at least one of the content or embed fields must be filled out'
|
||||
minRows={1}
|
||||
maxRows={7}
|
||||
{...formOnShorten.getInputProps('discordOnShortenContent')}
|
||||
/>
|
||||
<EnvTooltip envVar='DISCORD_ON_SHORTEN_CONTENT' data={data} varKey='discordOnShortenContent'>
|
||||
<Textarea
|
||||
mt='md'
|
||||
label='Content'
|
||||
description='The content of the notification. This can be blank, but at least one of the content or embed fields must be filled out'
|
||||
minRows={1}
|
||||
maxRows={7}
|
||||
{...formOnShorten.getInputProps('discordOnShortenContent')}
|
||||
/>
|
||||
</EnvTooltip>
|
||||
|
||||
<Switch
|
||||
mt='md'
|
||||
label='Embed'
|
||||
description='Send the notification as an embed. This will allow for more customization below.'
|
||||
{...formOnShorten.getInputProps('discordOnShortenEmbed', { type: 'checkbox' })}
|
||||
/>
|
||||
<EnvTooltip envVar='DISCORD_ON_SHORTEN_EMBED' data={data} varKey='discordOnShortenEmbed'>
|
||||
<Switch
|
||||
mt='md'
|
||||
label='Embed'
|
||||
description='Send the notification as an embed. This will allow for more customization below.'
|
||||
{...formOnShorten.getInputProps('discordOnShortenEmbed', { type: 'checkbox' })}
|
||||
/>
|
||||
|
||||
<Collapse in={formOnShorten.values.discordOnShortenEmbed}>
|
||||
<Paper withBorder p='sm' mt='md'>
|
||||
<SimpleGrid cols={{ base: 1, md: 2 }} spacing='lg'>
|
||||
<TextInput
|
||||
label='Title'
|
||||
description='The title of the embed'
|
||||
{...formOnShorten.getInputProps('discordOnShortenEmbedTitle')}
|
||||
/>
|
||||
<Collapse in={formOnShorten.values.discordOnShortenEmbed}>
|
||||
<Paper withBorder p='sm' mt='md'>
|
||||
<SimpleGrid cols={{ base: 1, md: 2 }} spacing='lg'>
|
||||
<TextInput
|
||||
label='Title'
|
||||
description='The title of the embed'
|
||||
{...formOnShorten.getInputProps('discordOnShortenEmbedTitle')}
|
||||
/>
|
||||
|
||||
<TextInput
|
||||
label='Description'
|
||||
description='The description of the embed'
|
||||
{...formOnShorten.getInputProps('discordOnShortenEmbedDescription')}
|
||||
/>
|
||||
<TextInput
|
||||
label='Description'
|
||||
description='The description of the embed'
|
||||
{...formOnShorten.getInputProps('discordOnShortenEmbedDescription')}
|
||||
/>
|
||||
|
||||
<TextInput
|
||||
label='Footer'
|
||||
description='The footer of the embed'
|
||||
{...formOnShorten.getInputProps('discordOnShortenEmbedFooter')}
|
||||
/>
|
||||
<TextInput
|
||||
label='Footer'
|
||||
description='The footer of the embed'
|
||||
{...formOnShorten.getInputProps('discordOnShortenEmbedFooter')}
|
||||
/>
|
||||
|
||||
<ColorInput
|
||||
label='Color'
|
||||
description='The color of the embed'
|
||||
{...formOnShorten.getInputProps('discordOnShortenEmbedColor')}
|
||||
/>
|
||||
<ColorInput
|
||||
label='Color'
|
||||
description='The color of the embed'
|
||||
{...formOnShorten.getInputProps('discordOnShortenEmbedColor')}
|
||||
/>
|
||||
|
||||
<Switch
|
||||
label='Timestamp'
|
||||
description='Show the timestamp in the embed'
|
||||
{...formOnShorten.getInputProps('discordOnShortenEmbedTimestamp', { type: 'checkbox' })}
|
||||
/>
|
||||
<Switch
|
||||
label='Timestamp'
|
||||
description='Show the timestamp in the embed'
|
||||
{...formOnShorten.getInputProps('discordOnShortenEmbedTimestamp', { type: 'checkbox' })}
|
||||
/>
|
||||
|
||||
<Switch
|
||||
label='URL'
|
||||
description='Makes the title clickable and links to the URL of the file'
|
||||
{...formOnShorten.getInputProps('discordOnShortenEmbedUrl', { type: 'checkbox' })}
|
||||
/>
|
||||
</SimpleGrid>
|
||||
</Paper>
|
||||
</Collapse>
|
||||
<Switch
|
||||
label='URL'
|
||||
description='Makes the title clickable and links to the URL of the file'
|
||||
{...formOnShorten.getInputProps('discordOnShortenEmbedUrl', { type: 'checkbox' })}
|
||||
/>
|
||||
</SimpleGrid>
|
||||
</Paper>
|
||||
</Collapse>
|
||||
</EnvTooltip>
|
||||
|
||||
<Button type='submit' mt='md' loading={isLoading} leftSection={<IconDeviceFloppy size='1rem' />}>
|
||||
Save
|
||||
|
|
|
@ -5,6 +5,7 @@ import { IconDeviceFloppy } from '@tabler/icons-react';
|
|||
import { useRouter } from 'next/router';
|
||||
import { useEffect } from 'react';
|
||||
import { settingsOnSubmit } from '../settingsOnSubmit';
|
||||
import { EnvTooltip } from '..';
|
||||
|
||||
export default function ServerSettingsFeatures({
|
||||
swr: { data, isLoading },
|
||||
|
@ -54,74 +55,104 @@ export default function ServerSettingsFeatures({
|
|||
|
||||
<form onSubmit={form.onSubmit(onSubmit)}>
|
||||
<SimpleGrid mt='md' 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' })}
|
||||
/>
|
||||
<EnvTooltip envVar='FEATURES_IMAGE_COMPRESSION' data={data} varKey='featuresImageCompression'>
|
||||
<Switch
|
||||
label='Image Compression'
|
||||
description='Allows the ability for users to compress images.'
|
||||
{...form.getInputProps('featuresImageCompression', { type: 'checkbox' })}
|
||||
/>
|
||||
</EnvTooltip>
|
||||
|
||||
<Switch
|
||||
label='/robots.txt'
|
||||
description='Enables a robots.txt file for search engine optimization. Requires a server restart.'
|
||||
{...form.getInputProps('featuresRobotsTxt', { type: 'checkbox' })}
|
||||
/>
|
||||
<EnvTooltip envVar='FEATURES_ROBOTS_TXT' data={data} varKey='featuresRobotsTxt'>
|
||||
<Switch
|
||||
label='/robots.txt'
|
||||
description='Enables a robots.txt file for search engine optimization. Requires a server restart.'
|
||||
{...form.getInputProps('featuresRobotsTxt', { type: 'checkbox' })}
|
||||
/>
|
||||
</EnvTooltip>
|
||||
|
||||
<Switch
|
||||
label='Healthcheck'
|
||||
description='Enables a healthcheck route for uptime monitoring. Requires a server restart.'
|
||||
{...form.getInputProps('featuresHealthcheck', { type: 'checkbox' })}
|
||||
/>
|
||||
<EnvTooltip envVar='FEATURES_HEALTHCHECK' data={data} varKey='featuresHealthcheck'>
|
||||
<Switch
|
||||
label='Healthcheck'
|
||||
description='Enables a healthcheck route for uptime monitoring. Requires a server restart.'
|
||||
{...form.getInputProps('featuresHealthcheck', { type: 'checkbox' })}
|
||||
/>
|
||||
</EnvTooltip>
|
||||
|
||||
<Switch
|
||||
label='User Registration'
|
||||
description='Allows users to register an account on the server.'
|
||||
{...form.getInputProps('featuresUserRegistration', { type: 'checkbox' })}
|
||||
/>
|
||||
<EnvTooltip envVar='FEATURES_USER_REGISTRATION' data={data} varKey='featuresUserRegistration'>
|
||||
<Switch
|
||||
label='User Registration'
|
||||
description='Allows users to register an account on the server.'
|
||||
{...form.getInputProps('featuresUserRegistration', { type: 'checkbox' })}
|
||||
/>
|
||||
</EnvTooltip>
|
||||
|
||||
<Switch
|
||||
label='OAuth Registration'
|
||||
description='Allows users to register an account using OAuth providers.'
|
||||
{...form.getInputProps('featuresOauthRegistration', { type: 'checkbox' })}
|
||||
/>
|
||||
<EnvTooltip envVar='FEATURES_OAUTH_REGISTRATION' data={data} varKey='featuresOauthRegistration'>
|
||||
<Switch
|
||||
label='OAuth Registration'
|
||||
description='Allows users to register an account using OAuth providers.'
|
||||
{...form.getInputProps('featuresOauthRegistration', { type: 'checkbox' })}
|
||||
/>
|
||||
</EnvTooltip>
|
||||
|
||||
<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' })}
|
||||
/>
|
||||
<EnvTooltip envVar='FEATURES_DELETE_ON_MAX_VIEWS' data={data} varKey='featuresDeleteOnMaxViews'>
|
||||
<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' })}
|
||||
/>
|
||||
</EnvTooltip>
|
||||
|
||||
<Switch
|
||||
label='Enable Metrics'
|
||||
description='Enables metrics for the server. Requires a server restart.'
|
||||
{...form.getInputProps('featuresMetricsEnabled', { type: 'checkbox' })}
|
||||
/>
|
||||
<EnvTooltip envVar='FEATURES_METRICS_ENABLED' data={data} varKey='featuresMetricsEnabled'>
|
||||
<Switch
|
||||
label='Enable Metrics'
|
||||
description='Enables metrics for the server. Requires a server restart.'
|
||||
{...form.getInputProps('featuresMetricsEnabled', { type: 'checkbox' })}
|
||||
/>
|
||||
</EnvTooltip>
|
||||
|
||||
<Switch
|
||||
label='Admin Only Metrics'
|
||||
description='Requires an administrator to view metrics.'
|
||||
{...form.getInputProps('featuresMetricsAdminOnly', { type: 'checkbox' })}
|
||||
/>
|
||||
<EnvTooltip envVar='FEATURES_METRICS_ADMIN_ONLY' data={data} varKey='featuresMetricsAdminOnly'>
|
||||
<Switch
|
||||
label='Admin Only Metrics'
|
||||
description='Requires an administrator to view metrics.'
|
||||
{...form.getInputProps('featuresMetricsAdminOnly', { type: 'checkbox' })}
|
||||
/>
|
||||
</EnvTooltip>
|
||||
|
||||
<Switch
|
||||
label='Show User Specific Metrics'
|
||||
description='Shows metrics specific to each user, for all users.'
|
||||
{...form.getInputProps('featuresMetricsShowUserSpecific', { type: 'checkbox' })}
|
||||
/>
|
||||
<EnvTooltip
|
||||
envVar='FEATURES_METRICS_SHOW_USER_SPECIFIC'
|
||||
data={data}
|
||||
varKey='featuresMetricsShowUserSpecific'
|
||||
>
|
||||
<Switch
|
||||
label='Show User Specific Metrics'
|
||||
description='Shows metrics specific to each user, for all users.'
|
||||
{...form.getInputProps('featuresMetricsShowUserSpecific', { type: 'checkbox' })}
|
||||
/>
|
||||
</EnvTooltip>
|
||||
|
||||
<Switch
|
||||
label='Enable Thumbnails'
|
||||
description='Enables thumbnail generation for images. Requires a server restart.'
|
||||
{...form.getInputProps('featuresThumbnailsEnabled', { type: 'checkbox' })}
|
||||
/>
|
||||
<EnvTooltip envVar='FEATURES_THUMBNAILS_ENABLED' data={data} varKey='featuresThumbnailsEnabled'>
|
||||
<Switch
|
||||
label='Enable Thumbnails'
|
||||
description='Enables thumbnail generation for images. Requires a server restart.'
|
||||
{...form.getInputProps('featuresThumbnailsEnabled', { type: 'checkbox' })}
|
||||
/>
|
||||
</EnvTooltip>
|
||||
|
||||
<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')}
|
||||
/>
|
||||
<EnvTooltip
|
||||
envVar='FEATURES_THUMBNAILS_NUMBER_THREADS'
|
||||
data={data}
|
||||
varKey='featuresThumbnailsNumberThreads'
|
||||
>
|
||||
<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')}
|
||||
/>
|
||||
</EnvTooltip>
|
||||
</SimpleGrid>
|
||||
|
||||
<Button type='submit' mt='md' loading={isLoading} leftSection={<IconDeviceFloppy size='1rem' />}>
|
||||
|
|
|
@ -15,6 +15,7 @@ import { IconDeviceFloppy } from '@tabler/icons-react';
|
|||
import { useRouter } from 'next/router';
|
||||
import { useEffect } from 'react';
|
||||
import { settingsOnSubmit } from '../settingsOnSubmit';
|
||||
import { EnvTooltip } from '..';
|
||||
|
||||
export default function ServerSettingsFiles({
|
||||
swr: { data, isLoading },
|
||||
|
@ -103,83 +104,109 @@ export default function ServerSettingsFiles({
|
|||
|
||||
<form onSubmit={form.onSubmit(onSubmit)}>
|
||||
<SimpleGrid mt='md' cols={{ base: 1, md: 2 }} spacing='lg'>
|
||||
<TextInput
|
||||
label='Route'
|
||||
description='The route to use for file uploads. Requires a server restart.'
|
||||
placeholder='/u'
|
||||
{...form.getInputProps('filesRoute')}
|
||||
/>
|
||||
<EnvTooltip envVar='FILES_ROUTE' data={data} varKey='filesRoute'>
|
||||
<TextInput
|
||||
label='Route'
|
||||
description='The route to use for file uploads. Requires a server restart.'
|
||||
placeholder='/u'
|
||||
{...form.getInputProps('filesRoute')}
|
||||
/>
|
||||
</EnvTooltip>
|
||||
|
||||
<NumberInput
|
||||
label='Length'
|
||||
description='The length of the file name (for randomly generated names).'
|
||||
min={1}
|
||||
max={64}
|
||||
{...form.getInputProps('filesLength')}
|
||||
/>
|
||||
<EnvTooltip envVar='FILES_LENGTH' data={data} varKey='filesLength'>
|
||||
<NumberInput
|
||||
label='Length'
|
||||
description='The length of the file name (for randomly generated names).'
|
||||
min={1}
|
||||
max={64}
|
||||
{...form.getInputProps('filesLength')}
|
||||
/>
|
||||
</EnvTooltip>
|
||||
|
||||
<Switch
|
||||
label='Assume Mimetypes'
|
||||
description='Assume the mimetype of a file for its extension.'
|
||||
{...form.getInputProps('filesAssumeMimetypes', { type: 'checkbox' })}
|
||||
/>
|
||||
<EnvTooltip envVar='FILES_ASSUME_MIMETYPES' data={data} varKey='filesAssumeMimetypes'>
|
||||
<Switch
|
||||
label='Assume Mimetypes'
|
||||
description='Assume the mimetype of a file for its extension.'
|
||||
{...form.getInputProps('filesAssumeMimetypes', { type: 'checkbox' })}
|
||||
/>
|
||||
</EnvTooltip>
|
||||
|
||||
<Switch
|
||||
label='Remove GPS Metadata'
|
||||
description='Remove GPS metadata from files.'
|
||||
{...form.getInputProps('filesRemoveGpsMetadata', { type: 'checkbox' })}
|
||||
/>
|
||||
<EnvTooltip envVar='FILES_REMOVE_GPS_METADATA' data={data} varKey='filesRemoveGpsMetadata'>
|
||||
<Switch
|
||||
label='Remove GPS Metadata'
|
||||
description='Remove GPS metadata from files.'
|
||||
{...form.getInputProps('filesRemoveGpsMetadata', { type: 'checkbox' })}
|
||||
/>
|
||||
</EnvTooltip>
|
||||
|
||||
<Select
|
||||
label='Default Format'
|
||||
description='The default format to use for file names.'
|
||||
placeholder='random'
|
||||
data={['random', 'date', 'uuid', 'name', 'gfycat']}
|
||||
{...form.getInputProps('filesDefaultFormat')}
|
||||
/>
|
||||
<EnvTooltip envVar='FILES_DEFAULT_FORMAT' data={data} varKey='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')}
|
||||
/>
|
||||
</EnvTooltip>
|
||||
|
||||
<TextInput
|
||||
label='Disabled Extensions'
|
||||
description='Extensions to disable, separated by commas.'
|
||||
placeholder='exe, bat, sh'
|
||||
{...form.getInputProps('filesDisabledExtensions')}
|
||||
/>
|
||||
<EnvTooltip envVar='FILES_DISABLED_EXTENSIONS' data={data} varKey='filesDisabledExtensions'>
|
||||
<TextInput
|
||||
label='Disabled Extensions'
|
||||
description='Extensions to disable, separated by commas.'
|
||||
placeholder='exe, bat, sh'
|
||||
{...form.getInputProps('filesDisabledExtensions')}
|
||||
/>
|
||||
</EnvTooltip>
|
||||
|
||||
<TextInput
|
||||
label='Max File Size'
|
||||
description='The maximum file size allowed.'
|
||||
placeholder='100mb'
|
||||
{...form.getInputProps('filesMaxFileSize')}
|
||||
/>
|
||||
<EnvTooltip envVar='FILES_MAX_FILE_SIZE' data={data} varKey='filesMaxFileSize'>
|
||||
<TextInput
|
||||
label='Max File Size'
|
||||
description='The maximum file size allowed.'
|
||||
placeholder='100mb'
|
||||
{...form.getInputProps('filesMaxFileSize')}
|
||||
/>
|
||||
</EnvTooltip>
|
||||
|
||||
<TextInput
|
||||
label='Default Expiration'
|
||||
description='The default expiration time for files.'
|
||||
placeholder='30d'
|
||||
{...form.getInputProps('filesDefaultExpiration')}
|
||||
/>
|
||||
<EnvTooltip envVar='FILES_DEFAULT_EXPIRATION' data={data} varKey='filesDefaultExpiration'>
|
||||
<TextInput
|
||||
label='Default Expiration'
|
||||
description='The default expiration time for files.'
|
||||
placeholder='30d'
|
||||
{...form.getInputProps('filesDefaultExpiration')}
|
||||
/>
|
||||
</EnvTooltip>
|
||||
|
||||
<TextInput
|
||||
label='Default Date Format'
|
||||
description='The default date format to use.'
|
||||
placeholder='YYYY-MM-DD_HH:mm:ss'
|
||||
{...form.getInputProps('filesDefaultDateFormat')}
|
||||
/>
|
||||
<EnvTooltip envVar='FILES_DEFAULT_DATE_FORMAT' data={data} varKey='filesDefaultDateFormat'>
|
||||
<TextInput
|
||||
label='Default Date Format'
|
||||
description='The default date format to use.'
|
||||
placeholder='YYYY-MM-DD_HH:mm:ss'
|
||||
{...form.getInputProps('filesDefaultDateFormat')}
|
||||
/>
|
||||
</EnvTooltip>
|
||||
|
||||
<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')}
|
||||
/>
|
||||
<EnvTooltip
|
||||
envVar='FILES_RANDOM_WORDS_NUM_ADJECTIVES'
|
||||
data={data}
|
||||
varKey='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')}
|
||||
/>
|
||||
</EnvTooltip>
|
||||
|
||||
<TextInput
|
||||
label='Random Words Separator'
|
||||
description='The separator to use for the random-words/gfycat format.'
|
||||
placeholder='-'
|
||||
{...form.getInputProps('filesRandomWordsSeparator')}
|
||||
/>
|
||||
<EnvTooltip envVar='FILES_RANDOM_WORDS_SEPARATOR' data={data} varKey='filesRandomWordsSeparator'>
|
||||
<TextInput
|
||||
label='Random Words Separator'
|
||||
description='The separator to use for the random-words/gfycat format.'
|
||||
placeholder='-'
|
||||
{...form.getInputProps('filesRandomWordsSeparator')}
|
||||
/>
|
||||
</EnvTooltip>
|
||||
</SimpleGrid>
|
||||
|
||||
<Button type='submit' mt='md' loading={isLoading} leftSection={<IconDeviceFloppy size='1rem' />}>
|
||||
|
|
|
@ -5,6 +5,7 @@ import { IconDeviceFloppy } from '@tabler/icons-react';
|
|||
import { useRouter } from 'next/router';
|
||||
import { useEffect } from 'react';
|
||||
import { settingsOnSubmit } from '../settingsOnSubmit';
|
||||
import { EnvTooltip } from '..';
|
||||
|
||||
export default function ServerSettingsHttpWebhook({
|
||||
swr: { data, isLoading },
|
||||
|
@ -50,19 +51,23 @@ export default function ServerSettingsHttpWebhook({
|
|||
|
||||
<form onSubmit={form.onSubmit(onSubmit)}>
|
||||
<SimpleGrid mt='md' cols={{ base: 1, md: 2 }} spacing='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')}
|
||||
/>
|
||||
<EnvTooltip envVar='HTTP_WEBHOOK_ON_UPLOAD' data={data} varKey='httpWebhookOnUpload'>
|
||||
<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')}
|
||||
/>
|
||||
</EnvTooltip>
|
||||
|
||||
<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')}
|
||||
/>
|
||||
<EnvTooltip envVar='HTTP_WEBHOOK_ON_SHORTEN' data={data} varKey='httpWebhookOnShorten'>
|
||||
<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')}
|
||||
/>
|
||||
</EnvTooltip>
|
||||
</SimpleGrid>
|
||||
|
||||
<Button type='submit' mt='md' loading={isLoading} leftSection={<IconDeviceFloppy size='1rem' />}>
|
||||
|
|
|
@ -5,6 +5,7 @@ import { IconDeviceFloppy } from '@tabler/icons-react';
|
|||
import { useRouter } from 'next/router';
|
||||
import { useEffect } from 'react';
|
||||
import { settingsOnSubmit } from '../settingsOnSubmit';
|
||||
import { EnvTooltip } from '..';
|
||||
|
||||
export default function ServerSettingsInvites({
|
||||
swr: { data, isLoading },
|
||||
|
@ -38,21 +39,25 @@ export default function ServerSettingsInvites({
|
|||
|
||||
<form onSubmit={form.onSubmit(onSubmit)}>
|
||||
<SimpleGrid mt='md' cols={{ base: 1, md: 2 }} spacing='lg'>
|
||||
<Switch
|
||||
label='Enable Invites'
|
||||
description='Enable the use of invite links to register new users.'
|
||||
{...form.getInputProps('invitesEnabled', { type: 'checkbox' })}
|
||||
/>
|
||||
<EnvTooltip envVar='INVITES_ENABLED' data={data} varKey='invitesEnabled'>
|
||||
<Switch
|
||||
label='Enable Invites'
|
||||
description='Enable the use of invite links to register new users.'
|
||||
{...form.getInputProps('invitesEnabled', { type: 'checkbox' })}
|
||||
/>
|
||||
</EnvTooltip>
|
||||
|
||||
<NumberInput
|
||||
label='Length'
|
||||
description='The length of the invite code.'
|
||||
placeholder='6'
|
||||
min={1}
|
||||
max={64}
|
||||
disabled={!form.values.invitesEnabled}
|
||||
{...form.getInputProps('invitesLength')}
|
||||
/>
|
||||
<EnvTooltip envVar='INVITES_LENGTH' data={data} varKey='invitesLength'>
|
||||
<NumberInput
|
||||
label='Length'
|
||||
description='The length of the invite code.'
|
||||
placeholder='6'
|
||||
min={1}
|
||||
max={64}
|
||||
disabled={!form.values.invitesEnabled}
|
||||
{...form.getInputProps('invitesLength')}
|
||||
/>
|
||||
</EnvTooltip>
|
||||
</SimpleGrid>
|
||||
|
||||
<Button type='submit' mt='md' loading={isLoading} leftSection={<IconDeviceFloppy size='1rem' />}>
|
||||
|
|
|
@ -5,6 +5,7 @@ import { IconDeviceFloppy } from '@tabler/icons-react';
|
|||
import { useRouter } from 'next/router';
|
||||
import { useEffect } from 'react';
|
||||
import { settingsOnSubmit } from '../settingsOnSubmit';
|
||||
import { EnvTooltip } from '..';
|
||||
|
||||
export default function ServerSettingsMfa({
|
||||
swr: { data, isLoading },
|
||||
|
@ -40,23 +41,30 @@ export default function ServerSettingsMfa({
|
|||
|
||||
<form onSubmit={form.onSubmit(onSubmit)}>
|
||||
<SimpleGrid mt='md' cols={{ base: 1, md: 2 }} spacing='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('mfaPasskeys', { type: 'checkbox' })}
|
||||
/>
|
||||
<EnvTooltip envVar='MFA_TOTP_ENABLED' data={data} varKey='mfaTotpEnabled'>
|
||||
<Switch
|
||||
label='Passkeys'
|
||||
description='Enable the use of passwordless login with the use of WebAuthn passkeys like your phone, security keys, etc.'
|
||||
{...form.getInputProps('mfaPasskeys', { type: 'checkbox' })}
|
||||
/>
|
||||
</EnvTooltip>
|
||||
|
||||
<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')}
|
||||
/>
|
||||
<EnvTooltip envVar='MFA_TOTP_ENABLED' data={data} varKey='mfaTotpEnabled'>
|
||||
<Switch
|
||||
label='Enable TOTP'
|
||||
description='Enable Time-based One-Time Passwords with the use of an authenticator app.'
|
||||
{...form.getInputProps('mfaTotpEnabled', { type: 'checkbox' })}
|
||||
/>
|
||||
</EnvTooltip>
|
||||
|
||||
<EnvTooltip envVar='MFA_TOTP_ISSUER' data={data} varKey='mfaTotpIssuer'>
|
||||
<TextInput
|
||||
label='Issuer'
|
||||
description='The issuer to use for the TOTP token.'
|
||||
placeholder='Zipline'
|
||||
{...form.getInputProps('mfaTotpIssuer')}
|
||||
/>
|
||||
</EnvTooltip>
|
||||
</SimpleGrid>
|
||||
|
||||
<Button type='submit' mt='md' loading={isLoading} leftSection={<IconDeviceFloppy size='1rem' />}>
|
||||
|
|
|
@ -15,6 +15,7 @@ import { IconDeviceFloppy } from '@tabler/icons-react';
|
|||
import { useRouter } from 'next/router';
|
||||
import { useEffect } from 'react';
|
||||
import { settingsOnSubmit } from '../settingsOnSubmit';
|
||||
import { EnvTooltip } from '..';
|
||||
|
||||
export default function ServerSettingsOauth({
|
||||
swr: { data, isLoading },
|
||||
|
@ -107,17 +108,21 @@ export default function ServerSettingsOauth({
|
|||
|
||||
<form onSubmit={form.onSubmit(onSubmit)}>
|
||||
<SimpleGrid mt='md' cols={{ base: 1, md: 2 }} spacing='lg'>
|
||||
<Switch
|
||||
label='Bypass Local Login'
|
||||
description='Skips the local login page and redirects to the OAuth provider, this only works with one provider enabled.'
|
||||
{...form.getInputProps('oauthBypassLocalLogin', { type: 'checkbox' })}
|
||||
/>
|
||||
<EnvTooltip envVar='OAUTH_BYPASS_LOCAL_LOGIN' data={data} varKey='oauthBypassLocalLogin'>
|
||||
<Switch
|
||||
label='Bypass Local Login'
|
||||
description='Skips the local login page and redirects to the OAuth provider, this only works with one provider enabled.'
|
||||
{...form.getInputProps('oauthBypassLocalLogin', { type: 'checkbox' })}
|
||||
/>
|
||||
</EnvTooltip>
|
||||
|
||||
<Switch
|
||||
label='Login Only'
|
||||
description='Disables registration and only allows login with OAuth, existing users can link providers for example.'
|
||||
{...form.getInputProps('oauthLoginOnly', { type: 'checkbox' })}
|
||||
/>
|
||||
<EnvTooltip envVar='OAUTH_LOGIN_ONLY' data={data} varKey='oauthLoginOnly'>
|
||||
<Switch
|
||||
label='Login Only'
|
||||
description='Disables registration and only allows login with OAuth, existing users can link providers for example.'
|
||||
{...form.getInputProps('oauthLoginOnly', { type: 'checkbox' })}
|
||||
/>
|
||||
</EnvTooltip>
|
||||
</SimpleGrid>
|
||||
<SimpleGrid mt='md' cols={{ base: 1, md: 2 }} spacing='lg'>
|
||||
<Paper withBorder p='sm'>
|
||||
|
@ -127,13 +132,21 @@ export default function ServerSettingsOauth({
|
|||
</Title>
|
||||
</Anchor>
|
||||
|
||||
<TextInput label='Discord Client ID' {...form.getInputProps('oauthDiscordClientId')} />
|
||||
<TextInput label='Discord Client Secret' {...form.getInputProps('oauthDiscordClientSecret')} />
|
||||
<TextInput
|
||||
label='Discord Redirect URL'
|
||||
description='The redirect URL to use instead of the host when logging in. This is not required if the URL generated by Zipline works as intended.'
|
||||
{...form.getInputProps('oauthDiscordRedirectUri')}
|
||||
/>
|
||||
<EnvTooltip envVar='OAUTH_DISCORD_CLIENT_ID' data={data} varKey='oauthDiscordClientId'>
|
||||
<TextInput label='Discord Client ID' {...form.getInputProps('oauthDiscordClientId')} />
|
||||
</EnvTooltip>
|
||||
|
||||
<EnvTooltip envVar='OAUTH_DISCORD_CLIENT_SECRET' data={data} varKey='oauthDiscordClientSecret'>
|
||||
<TextInput label='Discord Client Secret' {...form.getInputProps('oauthDiscordClientSecret')} />
|
||||
</EnvTooltip>
|
||||
|
||||
<EnvTooltip envVar='OAUTH_DISCORD_REDIRECT_URI' data={data} varKey='oauthDiscordRedirectUri'>
|
||||
<TextInput
|
||||
label='Discord Redirect URL'
|
||||
description='The redirect URL to use instead of the host when logging in. This is not required if the URL generated by Zipline works as intended.'
|
||||
{...form.getInputProps('oauthDiscordRedirectUri')}
|
||||
/>
|
||||
</EnvTooltip>
|
||||
</Paper>
|
||||
<Paper withBorder p='sm'>
|
||||
<Anchor href='https://console.developers.google.com/' target='_blank'>
|
||||
|
@ -142,13 +155,21 @@ export default function ServerSettingsOauth({
|
|||
</Title>
|
||||
</Anchor>
|
||||
|
||||
<TextInput label='Google Client ID' {...form.getInputProps('oauthGoogleClientId')} />
|
||||
<TextInput label='Google Client Secret' {...form.getInputProps('oauthGoogleClientSecret')} />
|
||||
<TextInput
|
||||
label='Google Redirect URL'
|
||||
description='The redirect URL to use instead of the host when logging in. This is not required if the URL generated by Zipline works as intended.'
|
||||
{...form.getInputProps('oauthGoogleRedirectUri')}
|
||||
/>
|
||||
<EnvTooltip envVar='OAUTH_GOOGLE_CLIENT_ID' data={data} varKey='oauthGoogleClientId'>
|
||||
<TextInput label='Google Client ID' {...form.getInputProps('oauthGoogleClientId')} />
|
||||
</EnvTooltip>
|
||||
|
||||
<EnvTooltip envVar='OAUTH_GOOGLE_CLIENT_SECRET' data={data} varKey='oauthGoogleClientSecret'>
|
||||
<TextInput label='Google Client Secret' {...form.getInputProps('oauthGoogleClientSecret')} />
|
||||
</EnvTooltip>
|
||||
|
||||
<EnvTooltip envVar='OAUTH_GOOGLE_REDIRECT_URI' data={data} varKey='oauthGoogleRedirectUri'>
|
||||
<TextInput
|
||||
label='Google Redirect URL'
|
||||
description='The redirect URL to use instead of the host when logging in. This is not required if the URL generated by Zipline works as intended.'
|
||||
{...form.getInputProps('oauthGoogleRedirectUri')}
|
||||
/>
|
||||
</EnvTooltip>
|
||||
</Paper>
|
||||
</SimpleGrid>
|
||||
|
||||
|
@ -160,13 +181,21 @@ export default function ServerSettingsOauth({
|
|||
</Anchor>
|
||||
|
||||
<SimpleGrid mt='md' cols={{ base: 1, md: 2 }} spacing='lg'>
|
||||
<TextInput label='GitHub Client ID' {...form.getInputProps('oauthGithubClientId')} />
|
||||
<TextInput label='GitHub Client Secret' {...form.getInputProps('oauthGithubClientSecret')} />
|
||||
<TextInput
|
||||
label='GitHub Redirect URL'
|
||||
description='The redirect URL to use instead of the host when logging in. This is not required if the URL generated by Zipline works as intended.'
|
||||
{...form.getInputProps('oauthGithubRedirectUri')}
|
||||
/>
|
||||
<EnvTooltip envVar='OAUTH_GITHUB_CLIENT_ID' data={data} varKey='oauthGithubClientId'>
|
||||
<TextInput label='GitHub Client ID' {...form.getInputProps('oauthGithubClientId')} />
|
||||
</EnvTooltip>
|
||||
|
||||
<EnvTooltip envVar='OAUTH_GITHUB_CLIENT_SECRET' data={data} varKey='oauthGithubClientSecret'>
|
||||
<TextInput label='GitHub Client Secret' {...form.getInputProps('oauthGithubClientSecret')} />
|
||||
</EnvTooltip>
|
||||
|
||||
<EnvTooltip envVar='OAUTH_GITHUB_REDIRECT_URI' data={data} varKey='oauthGithubRedirectUri'>
|
||||
<TextInput
|
||||
label='GitHub Redirect URL'
|
||||
description='The redirect URL to use instead of the host when logging in. This is not required if the URL generated by Zipline works as intended.'
|
||||
{...form.getInputProps('oauthGithubRedirectUri')}
|
||||
/>
|
||||
</EnvTooltip>
|
||||
</SimpleGrid>
|
||||
</Paper>
|
||||
|
||||
|
@ -174,16 +203,33 @@ export default function ServerSettingsOauth({
|
|||
<Title order={4}>OpenID Connect</Title>
|
||||
|
||||
<SimpleGrid mt='md' cols={{ base: 1, md: 2 }} spacing='lg'>
|
||||
<TextInput label='OIDC Client ID' {...form.getInputProps('oauthOidcClientId')} />
|
||||
<TextInput label='OIDC Client Secret' {...form.getInputProps('oauthOidcClientSecret')} />
|
||||
<TextInput label='OIDC Authorize URL' {...form.getInputProps('oauthOidcAuthorizeUrl')} />
|
||||
<TextInput label='OIDC Token URL' {...form.getInputProps('oauthOidcTokenUrl')} />
|
||||
<TextInput label='OIDC Userinfo URL' {...form.getInputProps('oauthOidcUserinfoUrl')} />
|
||||
<TextInput
|
||||
label='OIDC Redirect URL'
|
||||
description='The redirect URL to use instead of the host when logging in. This is not required if the URL generated by Zipline works as intended.'
|
||||
{...form.getInputProps('oauthOidcRedirectUri')}
|
||||
/>
|
||||
<EnvTooltip envVar='OAUTH_OIDC_CLIENT_ID' data={data} varKey='oauthOidcClientId'>
|
||||
<TextInput label='OIDC Client ID' {...form.getInputProps('oauthOidcClientId')} />
|
||||
</EnvTooltip>
|
||||
|
||||
<EnvTooltip envVar='OAUTH_OIDC_CLIENT_SECRET' data={data} varKey='oauthOidcClientSecret'>
|
||||
<TextInput label='OIDC Client Secret' {...form.getInputProps('oauthOidcClientSecret')} />
|
||||
</EnvTooltip>
|
||||
|
||||
<EnvTooltip envVar='OAUTH_OIDC_AUTHORIZE_URL' data={data} varKey='oauthOidcAuthorizeUrl'>
|
||||
<TextInput label='OIDC Authorize URL' {...form.getInputProps('oauthOidcAuthorizeUrl')} />
|
||||
</EnvTooltip>
|
||||
|
||||
<EnvTooltip envVar='OAUTH_OIDC_TOKEN_URL' data={data} varKey='oauthOidcTokenUrl'>
|
||||
<TextInput label='OIDC Token URL' {...form.getInputProps('oauthOidcTokenUrl')} />
|
||||
</EnvTooltip>
|
||||
|
||||
<EnvTooltip envVar='OAUTH_OIDC_USERINFO_URL' data={data} varKey='oauthOidcUserinfoUrl'>
|
||||
<TextInput label='OIDC Userinfo URL' {...form.getInputProps('oauthOidcUserinfoUrl')} />
|
||||
</EnvTooltip>
|
||||
|
||||
<EnvTooltip envVar='OAUTH_OIDC_REDIRECT_URI' data={data} varKey='oauthOidcRedirectUri'>
|
||||
<TextInput
|
||||
label='OIDC Redirect URL'
|
||||
description='The redirect URL to use instead of the host when logging in. This is not required if the URL generated by Zipline works as intended.'
|
||||
{...form.getInputProps('oauthOidcRedirectUri')}
|
||||
/>
|
||||
</EnvTooltip>
|
||||
</SimpleGrid>
|
||||
</Paper>
|
||||
|
||||
|
|
|
@ -16,6 +16,7 @@ import { IconDeviceFloppy, IconRefresh } from '@tabler/icons-react';
|
|||
import { useRouter } from 'next/router';
|
||||
import { useEffect } from 'react';
|
||||
import { settingsOnSubmit } from '../settingsOnSubmit';
|
||||
import { EnvTooltip } from '..';
|
||||
|
||||
export default function ServerSettingsPWA({
|
||||
swr: { data, isLoading },
|
||||
|
@ -74,53 +75,65 @@ export default function ServerSettingsPWA({
|
|||
</Text>
|
||||
|
||||
<form onSubmit={form.onSubmit(onSubmit)}>
|
||||
<Switch
|
||||
mt='md'
|
||||
label='PWA Enabled'
|
||||
description='Allow users to install the Zipline PWA on their devices.'
|
||||
{...form.getInputProps('pwaEnabled', { type: 'checkbox' })}
|
||||
/>
|
||||
<EnvTooltip envVar='PWA_ENABLED' data={data} varKey='pwaEnabled'>
|
||||
<Switch
|
||||
mt='md'
|
||||
label='PWA Enabled'
|
||||
description='Allow users to install the Zipline PWA on their devices.'
|
||||
{...form.getInputProps('pwaEnabled', { type: 'checkbox' })}
|
||||
/>
|
||||
</EnvTooltip>
|
||||
|
||||
<SimpleGrid mt='md' cols={{ base: 1, md: 2 }} spacing='lg'>
|
||||
<TextInput
|
||||
label='Title'
|
||||
description='The title for the PWA'
|
||||
placeholder='Zipline'
|
||||
disabled={!form.values.pwaEnabled}
|
||||
{...form.getInputProps('pwaTitle')}
|
||||
/>
|
||||
<EnvTooltip envVar='PWA_TITLE' data={data} varKey='pwaTitle'>
|
||||
<TextInput
|
||||
label='Title'
|
||||
description='The title for the PWA'
|
||||
placeholder='Zipline'
|
||||
disabled={!form.values.pwaEnabled}
|
||||
{...form.getInputProps('pwaTitle')}
|
||||
/>
|
||||
</EnvTooltip>
|
||||
|
||||
<TextInput
|
||||
label='Short Name'
|
||||
description='The short name for the PWA'
|
||||
placeholder='Zipline'
|
||||
disabled={!form.values.pwaEnabled}
|
||||
{...form.getInputProps('pwaShortName')}
|
||||
/>
|
||||
<EnvTooltip envVar='PWA_SHORT_NAME' data={data} varKey='pwaShortName'>
|
||||
<TextInput
|
||||
label='Short Name'
|
||||
description='The short name for the PWA'
|
||||
placeholder='Zipline'
|
||||
disabled={!form.values.pwaEnabled}
|
||||
{...form.getInputProps('pwaShortName')}
|
||||
/>
|
||||
</EnvTooltip>
|
||||
|
||||
<TextInput
|
||||
label='Description'
|
||||
description='The description for the PWA'
|
||||
placeholder='Zipline'
|
||||
disabled={!form.values.pwaEnabled}
|
||||
{...form.getInputProps('pwaDescription')}
|
||||
/>
|
||||
<EnvTooltip envVar='PWA_DESCRIPTION' data={data} varKey='pwaDescription'>
|
||||
<TextInput
|
||||
label='Description'
|
||||
description='The description for the PWA'
|
||||
placeholder='Zipline'
|
||||
disabled={!form.values.pwaEnabled}
|
||||
{...form.getInputProps('pwaDescription')}
|
||||
/>
|
||||
</EnvTooltip>
|
||||
|
||||
<ColorInput
|
||||
label='Theme Color'
|
||||
description='The theme color for the PWA'
|
||||
placeholder='#000000'
|
||||
disabled={!form.values.pwaEnabled}
|
||||
{...form.getInputProps('pwaThemeColor')}
|
||||
/>
|
||||
<EnvTooltip envVar='PWA_THEME_COLOR' data={data} varKey='pwaThemeColor'>
|
||||
<ColorInput
|
||||
label='Theme Color'
|
||||
description='The theme color for the PWA'
|
||||
placeholder='#000000'
|
||||
disabled={!form.values.pwaEnabled}
|
||||
{...form.getInputProps('pwaThemeColor')}
|
||||
/>
|
||||
</EnvTooltip>
|
||||
|
||||
<ColorInput
|
||||
label='Background Color'
|
||||
description='The background color for the PWA'
|
||||
placeholder='#ffffff'
|
||||
disabled={!form.values.pwaEnabled}
|
||||
{...form.getInputProps('pwaBackgroundColor')}
|
||||
/>
|
||||
<EnvTooltip envVar='PWA_BACKGROUND_COLOR' data={data} varKey='pwaBackgroundColor'>
|
||||
<ColorInput
|
||||
label='Background Color'
|
||||
description='The background color for the PWA'
|
||||
placeholder='#ffffff'
|
||||
disabled={!form.values.pwaEnabled}
|
||||
{...form.getInputProps('pwaBackgroundColor')}
|
||||
/>
|
||||
</EnvTooltip>
|
||||
</SimpleGrid>
|
||||
|
||||
<Group mt='md'>
|
||||
|
|
|
@ -15,6 +15,7 @@ import { IconDeviceFloppy } from '@tabler/icons-react';
|
|||
import { useRouter } from 'next/router';
|
||||
import { useEffect } from 'react';
|
||||
import { settingsOnSubmit } from '../settingsOnSubmit';
|
||||
import { EnvTooltip } from '..';
|
||||
|
||||
export default function ServerSettingsRatelimit({
|
||||
swr: { data, isLoading },
|
||||
|
@ -82,47 +83,69 @@ export default function ServerSettingsRatelimit({
|
|||
|
||||
<form onSubmit={form.onSubmit(onSubmit)}>
|
||||
<SimpleGrid mt='md' cols={{ base: 1, md: 2 }} spacing='lg'>
|
||||
<Switch
|
||||
label='Enable Ratelimit'
|
||||
description='Enable ratelimiting for the server.'
|
||||
{...form.getInputProps('ratelimitEnabled', { type: 'checkbox' })}
|
||||
/>
|
||||
<EnvTooltip envVar='RATELIMIT_ENABLED' data={data} varKey='ratelimitEnabled'>
|
||||
<Switch
|
||||
label='Enable Ratelimit'
|
||||
description='Enable ratelimiting for the server.'
|
||||
{...form.getInputProps('ratelimitEnabled', { type: 'checkbox' })}
|
||||
/>
|
||||
</EnvTooltip>
|
||||
|
||||
<Switch
|
||||
label='Admin Bypass'
|
||||
description='Allow admins to bypass the ratelimit.'
|
||||
disabled={!form.values.ratelimitEnabled}
|
||||
{...form.getInputProps('ratelimitAdminBypass', { type: 'checkbox' })}
|
||||
/>
|
||||
<EnvTooltip envVar='RATELIMIT_ADMIN_BYPASS' data={data} varKey='ratelimitAdminBypass'>
|
||||
<Switch
|
||||
label='Admin Bypass'
|
||||
description='Allow admins to bypass the ratelimit.'
|
||||
disabled={!form.values.ratelimitEnabled}
|
||||
{...form.getInputProps('ratelimitAdminBypass', { type: 'checkbox' })}
|
||||
/>
|
||||
</EnvTooltip>
|
||||
|
||||
<NumberInput
|
||||
label='Max Requests'
|
||||
description='The maximum number of requests allowed within the window. If no window is set, this is the maximum number of requests until it reaches the limit.'
|
||||
placeholder='10'
|
||||
min={1}
|
||||
disabled={!form.values.ratelimitEnabled}
|
||||
{...form.getInputProps('ratelimitMax')}
|
||||
/>
|
||||
<EnvTooltip envVar='RATELIMIT_MAX' data={data} varKey='ratelimitMax'>
|
||||
<NumberInput
|
||||
label='Max Requests'
|
||||
description='The maximum number of requests allowed within the window. If no window is set, this is the maximum number of requests until it reaches the limit.'
|
||||
placeholder='10'
|
||||
min={1}
|
||||
disabled={!form.values.ratelimitEnabled}
|
||||
{...form.getInputProps('ratelimitMax')}
|
||||
/>
|
||||
</EnvTooltip>
|
||||
|
||||
<NumberInput
|
||||
label='Window'
|
||||
description='The window in seconds to allow the max requests.'
|
||||
placeholder='60'
|
||||
min={1}
|
||||
disabled={!form.values.ratelimitEnabled}
|
||||
{...form.getInputProps('ratelimitWindow')}
|
||||
/>
|
||||
<EnvTooltip envVar='RATELIMIT_WINDOW' data={data} varKey='ratelimitWindow'>
|
||||
<NumberInput
|
||||
label='Window'
|
||||
description='The window in seconds to allow the max requests.'
|
||||
placeholder='60'
|
||||
min={1}
|
||||
disabled={!form.values.ratelimitEnabled}
|
||||
{...form.getInputProps('ratelimitWindow')}
|
||||
/>
|
||||
</EnvTooltip>
|
||||
|
||||
<TextInput
|
||||
label='Allow List'
|
||||
description='A comma-separated list of IP addresses to bypass the ratelimit.'
|
||||
placeholder='1.1.1.1, 8.8.8.8'
|
||||
disabled={!form.values.ratelimitEnabled}
|
||||
{...form.getInputProps('ratelimitAllowList')}
|
||||
/>
|
||||
<EnvTooltip envVar='RATELIMIT_ALLOW_LIST' data={data} varKey='ratelimitAllowList'>
|
||||
<TextInput
|
||||
label='Allow List'
|
||||
description='A comma-separated list of IP addresses to bypass the ratelimit.'
|
||||
placeholder='1.1.1.1, 8.8.8.8'
|
||||
disabled={!form.values.ratelimitEnabled}
|
||||
{...form.getInputProps('ratelimitAllowList')}
|
||||
/>
|
||||
</EnvTooltip>
|
||||
</SimpleGrid>
|
||||
|
||||
<Button type='submit' mt='md' loading={isLoading} leftSection={<IconDeviceFloppy size='1rem' />}>
|
||||
<Button
|
||||
type='submit'
|
||||
mt='md'
|
||||
loading={isLoading}
|
||||
disabled={
|
||||
data?.locked['ratelimitEnabled'] &&
|
||||
data?.locked['ratelimitMax'] &&
|
||||
data?.locked['ratelimitWindow'] &&
|
||||
data?.locked['ratelimitAdminBypass'] &&
|
||||
data?.locked['ratelimitAllowList']
|
||||
}
|
||||
leftSection={<IconDeviceFloppy size='1rem' />}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
</form>
|
||||
|
|
|
@ -5,6 +5,7 @@ import { IconDeviceFloppy } from '@tabler/icons-react';
|
|||
import { useRouter } from 'next/router';
|
||||
import { useEffect } from 'react';
|
||||
import { settingsOnSubmit } from '../settingsOnSubmit';
|
||||
import { EnvTooltip } from '..';
|
||||
|
||||
export default function ServerSettingsTasks({
|
||||
swr: { data, isLoading },
|
||||
|
@ -48,33 +49,41 @@ export default function ServerSettingsTasks({
|
|||
|
||||
<form onSubmit={form.onSubmit(onSubmit)}>
|
||||
<SimpleGrid mt='md' cols={{ base: 1, md: 2 }} spacing='lg'>
|
||||
<TextInput
|
||||
label='Delete Files Interval'
|
||||
description='How often to check and delete expired files.'
|
||||
placeholder='30m'
|
||||
{...form.getInputProps('tasksDeleteInterval')}
|
||||
/>
|
||||
<EnvTooltip envVar='TASKS_DELETE_INTERVAL' data={data} varKey='tasksDeleteInterval'>
|
||||
<TextInput
|
||||
label='Delete Files Interval'
|
||||
description='How often to check and delete expired files.'
|
||||
placeholder='30m'
|
||||
{...form.getInputProps('tasksDeleteInterval')}
|
||||
/>
|
||||
</EnvTooltip>
|
||||
|
||||
<TextInput
|
||||
label='Clear Invites Interval'
|
||||
description='How often to check and clear expired/used invites.'
|
||||
placeholder='30m'
|
||||
{...form.getInputProps('tasksClearInvitesInterval')}
|
||||
/>
|
||||
<EnvTooltip envVar='TASKS_METRICS_INTERVAL' data={data} varKey='tasksMetricsInterval'>
|
||||
<TextInput
|
||||
label='Clear Invites Interval'
|
||||
description='How often to check and clear expired/used invites.'
|
||||
placeholder='30m'
|
||||
{...form.getInputProps('tasksClearInvitesInterval')}
|
||||
/>
|
||||
</EnvTooltip>
|
||||
|
||||
<TextInput
|
||||
label='Max Views Interval'
|
||||
description='How often to check and delete files that have reached max views.'
|
||||
placeholder='30m'
|
||||
{...form.getInputProps('tasksMaxViewsInterval')}
|
||||
/>
|
||||
<EnvTooltip envVar='TASKS_MAX_VIEWS_INTERVAL' data={data} varKey='tasksMaxViewsInterval'>
|
||||
<TextInput
|
||||
label='Max Views Interval'
|
||||
description='How often to check and delete files that have reached max views.'
|
||||
placeholder='30m'
|
||||
{...form.getInputProps('tasksMaxViewsInterval')}
|
||||
/>
|
||||
</EnvTooltip>
|
||||
|
||||
<TextInput
|
||||
label='Thumbnails Interval'
|
||||
description='How often to check and generate thumbnails for video files.'
|
||||
placeholder='30m'
|
||||
{...form.getInputProps('tasksThumbnailsInterval')}
|
||||
/>
|
||||
<EnvTooltip envVar='TASKS_THUMBNAILS_INTERVAL' data={data} varKey='tasksThumbnailsInterval'>
|
||||
<TextInput
|
||||
label='Thumbnails Interval'
|
||||
description='How often to check and generate thumbnails for video files.'
|
||||
placeholder='30m'
|
||||
{...form.getInputProps('tasksThumbnailsInterval')}
|
||||
/>
|
||||
</EnvTooltip>
|
||||
</SimpleGrid>
|
||||
|
||||
<Button type='submit' mt='md' loading={isLoading} leftSection={<IconDeviceFloppy size='1rem' />}>
|
||||
|
|
|
@ -5,6 +5,7 @@ import { IconDeviceFloppy } from '@tabler/icons-react';
|
|||
import { useRouter } from 'next/router';
|
||||
import { useEffect } from 'react';
|
||||
import { settingsOnSubmit } from '../settingsOnSubmit';
|
||||
import { EnvTooltip } from '..';
|
||||
|
||||
export default function ServerSettingsUrls({
|
||||
swr: { data, isLoading },
|
||||
|
@ -38,21 +39,25 @@ export default function ServerSettingsUrls({
|
|||
|
||||
<form onSubmit={form.onSubmit(onSubmit)}>
|
||||
<SimpleGrid mt='md' cols={{ base: 1, md: 2 }} spacing='lg'>
|
||||
<TextInput
|
||||
label='Route'
|
||||
description='The route to use for short URLs. Requires a server restart.'
|
||||
placeholder='/go'
|
||||
{...form.getInputProps('urlsRoute')}
|
||||
/>
|
||||
<EnvTooltip envVar='URLS_ROUTE' data={data} varKey='urlsRoute'>
|
||||
<TextInput
|
||||
label='Route'
|
||||
description='The route to use for short URLs. Requires a server restart.'
|
||||
placeholder='/go'
|
||||
{...form.getInputProps('urlsRoute')}
|
||||
/>
|
||||
</EnvTooltip>
|
||||
|
||||
<NumberInput
|
||||
label='Length'
|
||||
description='The length of the short URL (for randomly generated names).'
|
||||
placeholder='6'
|
||||
min={1}
|
||||
max={64}
|
||||
{...form.getInputProps('urlsLength')}
|
||||
/>
|
||||
<EnvTooltip envVar='URLS_LENGTH' data={data} varKey='urlsLength'>
|
||||
<NumberInput
|
||||
label='Length'
|
||||
description='The length of the short URL (for randomly generated names).'
|
||||
placeholder='6'
|
||||
min={1}
|
||||
max={64}
|
||||
{...form.getInputProps('urlsLength')}
|
||||
/>
|
||||
</EnvTooltip>
|
||||
</SimpleGrid>
|
||||
|
||||
<Button type='submit' mt='md' loading={isLoading} leftSection={<IconDeviceFloppy size='1rem' />}>
|
||||
|
|
|
@ -5,6 +5,7 @@ import { IconDeviceFloppy } from '@tabler/icons-react';
|
|||
import { useRouter } from 'next/router';
|
||||
import { useEffect } from 'react';
|
||||
import { settingsOnSubmit } from '../settingsOnSubmit';
|
||||
import { EnvTooltip } from '..';
|
||||
|
||||
const defaultExternalLinks = [
|
||||
{
|
||||
|
@ -97,98 +98,122 @@ export default function ServerSettingsWebsite({
|
|||
{/* <SimpleGrid mt='md' cols={{ base: 1, md: 2 }} spacing='lg'> */}
|
||||
<Grid>
|
||||
<Grid.Col span={{ base: 12, md: 6 }}>
|
||||
<TextInput
|
||||
label='Title'
|
||||
description='The title of the website in browser tabs and at the top.'
|
||||
placeholder='Zipline'
|
||||
{...form.getInputProps('websiteTitle')}
|
||||
/>
|
||||
<EnvTooltip envVar='WEBSITE_TITLE' data={data} varKey='websiteTitle'>
|
||||
<TextInput
|
||||
label='Title'
|
||||
description='The title of the website in browser tabs and at the top.'
|
||||
placeholder='Zipline'
|
||||
{...form.getInputProps('websiteTitle')}
|
||||
/>
|
||||
</EnvTooltip>
|
||||
</Grid.Col>
|
||||
|
||||
<Grid.Col span={{ base: 12, md: 6 }}>
|
||||
<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')}
|
||||
/>
|
||||
<EnvTooltip envVar='WEBSITE_TITLE_LOGO' data={data} varKey='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')}
|
||||
/>
|
||||
</EnvTooltip>
|
||||
</Grid.Col>
|
||||
|
||||
<Grid.Col span={12}>
|
||||
<JsonInput
|
||||
label='External Links'
|
||||
description='The external links to show in the footer. This must be valid JSON.'
|
||||
formatOnBlur
|
||||
minRows={1}
|
||||
maxRows={7}
|
||||
autosize
|
||||
placeholder={JSON.stringify(defaultExternalLinks, null, 2)}
|
||||
{...form.getInputProps('websiteExternalLinks')}
|
||||
/>
|
||||
<EnvTooltip envVar='WEBSITE_EXTERNAL_LINKS' data={data} varKey='websiteExternalLinks'>
|
||||
<JsonInput
|
||||
label='External Links'
|
||||
description='The external links to show in the footer. This must be valid JSON.'
|
||||
formatOnBlur
|
||||
minRows={1}
|
||||
maxRows={7}
|
||||
autosize
|
||||
placeholder={JSON.stringify(defaultExternalLinks, null, 2)}
|
||||
{...form.getInputProps('websiteExternalLinks')}
|
||||
/>
|
||||
</EnvTooltip>
|
||||
</Grid.Col>
|
||||
|
||||
<Grid.Col span={{ base: 12, md: 6 }}>
|
||||
<TextInput
|
||||
label='Login Background'
|
||||
description='The URL to use for the login background.'
|
||||
placeholder='https://example.com/background.png'
|
||||
{...form.getInputProps('websiteLoginBackground')}
|
||||
/>
|
||||
<EnvTooltip envVar='WEBSITE_LOGIN_BACKGROUND' data={data} varKey='websiteLoginBackground'>
|
||||
<TextInput
|
||||
label='Login Background'
|
||||
description='The URL to use for the login background.'
|
||||
placeholder='https://example.com/background.png'
|
||||
{...form.getInputProps('websiteLoginBackground')}
|
||||
/>
|
||||
</EnvTooltip>
|
||||
</Grid.Col>
|
||||
|
||||
<Grid.Col span={{ base: 12, md: 6 }}>
|
||||
<Switch
|
||||
label='Login Background Blur'
|
||||
description='Whether to blur the login background.'
|
||||
{...form.getInputProps('websiteLoginBackgroundBlur', { type: 'checkbox' })}
|
||||
/>
|
||||
<EnvTooltip
|
||||
envVar='WEBSITE_LOGIN_BACKGROUND_BLUR'
|
||||
data={data}
|
||||
varKey='websiteLoginBackgroundBlur'
|
||||
>
|
||||
<Switch
|
||||
label='Login Background Blur'
|
||||
description='Whether to blur the login background.'
|
||||
{...form.getInputProps('websiteLoginBackgroundBlur', { type: 'checkbox' })}
|
||||
/>
|
||||
</EnvTooltip>
|
||||
</Grid.Col>
|
||||
|
||||
<Grid.Col span={{ base: 12, md: 6 }}>
|
||||
<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')}
|
||||
/>
|
||||
<EnvTooltip envVar='WEBSITE_DEFAULT_AVATAR' data={data} varKey='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')}
|
||||
/>
|
||||
</EnvTooltip>
|
||||
</Grid.Col>
|
||||
|
||||
<Grid.Col span={{ base: 12, md: 6 }}>
|
||||
<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')}
|
||||
/>
|
||||
<EnvTooltip envVar='WEBSITE_TOS' data={data} varKey='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')}
|
||||
/>
|
||||
</EnvTooltip>
|
||||
</Grid.Col>
|
||||
|
||||
<Grid.Col span={12}>
|
||||
<TextInput
|
||||
label='Default Theme'
|
||||
description='The default theme to use for the website.'
|
||||
placeholder='system'
|
||||
{...form.getInputProps('websiteThemeDefault')}
|
||||
/>
|
||||
<EnvTooltip envVar='WEBSITE_THEME_DEFAULT' data={data} varKey='websiteThemeDefault'>
|
||||
<TextInput
|
||||
label='Default Theme'
|
||||
description='The default theme to use for the website.'
|
||||
placeholder='system'
|
||||
{...form.getInputProps('websiteThemeDefault')}
|
||||
/>
|
||||
</EnvTooltip>
|
||||
</Grid.Col>
|
||||
|
||||
<Grid.Col span={{ base: 12, md: 6 }}>
|
||||
<TextInput
|
||||
label='Dark Theme'
|
||||
description='The dark theme to use for the website when the default theme is "system".'
|
||||
placeholder='builtin:dark_gray'
|
||||
disabled={form.values.websiteThemeDefault !== 'system'}
|
||||
{...form.getInputProps('websiteThemeDark')}
|
||||
/>
|
||||
<EnvTooltip envVar='WEBSITE_THEME_DARK' data={data} varKey='websiteThemeDark'>
|
||||
<TextInput
|
||||
label='Dark Theme'
|
||||
description='The dark theme to use for the website when the default theme is "system".'
|
||||
placeholder='builtin:dark_gray'
|
||||
disabled={form.values.websiteThemeDefault !== 'system'}
|
||||
{...form.getInputProps('websiteThemeDark')}
|
||||
/>
|
||||
</EnvTooltip>
|
||||
</Grid.Col>
|
||||
|
||||
<Grid.Col span={{ base: 12, md: 6 }}>
|
||||
<TextInput
|
||||
label='Light Theme'
|
||||
description='The light theme to use for the website when the default theme is "system".'
|
||||
placeholder='builtin:light_gray'
|
||||
disabled={form.values.websiteThemeDefault !== 'system'}
|
||||
{...form.getInputProps('websiteThemeLight')}
|
||||
/>
|
||||
<EnvTooltip envVar='WEBSITE_THEME_LIGHT' data={data} varKey='websiteThemeLight'>
|
||||
<TextInput
|
||||
label='Light Theme'
|
||||
description='The light theme to use for the website when the default theme is "system".'
|
||||
placeholder='builtin:light_gray'
|
||||
disabled={form.values.websiteThemeDefault !== 'system'}
|
||||
{...form.getInputProps('websiteThemeLight')}
|
||||
/>
|
||||
</EnvTooltip>
|
||||
</Grid.Col>
|
||||
</Grid>
|
||||
|
||||
|
|
|
@ -9,7 +9,7 @@ declare global {
|
|||
}
|
||||
|
||||
const reloadSettings = async () => {
|
||||
config = global.__config__ = validateConfigObject((await read()) as any);
|
||||
config = global.__config__ = await validateConfigObject((await read()) as any);
|
||||
};
|
||||
|
||||
config = global.__config__;
|
||||
|
|
|
@ -41,7 +41,7 @@ export const rawConfig: any = {
|
|||
defaultDateFormat: undefined,
|
||||
removeGpsMetadata: undefined,
|
||||
randomWordsNumAdjectives: undefined,
|
||||
randomWordsSeperator: undefined,
|
||||
randomWordsSeparator: undefined,
|
||||
},
|
||||
urls: {
|
||||
route: undefined,
|
||||
|
@ -164,6 +164,113 @@ export const PROP_TO_ENV = {
|
|||
|
||||
'ssl.key': 'SSL_KEY',
|
||||
'ssl.cert': 'SSL_CERT',
|
||||
|
||||
'core.returnHttpsUrls': 'CORE_RETURN_HTTPS_URLS',
|
||||
'core.defaultDomain': 'CORE_DEFAULT_DOMAIN',
|
||||
'core.tempDirectory': 'CORE_TEMP_DIRECTORY',
|
||||
|
||||
'chunks.max': 'CHUNKS_MAX',
|
||||
'chunks.size': 'CHUNKS_SIZE',
|
||||
'chunks.enabled': 'CHUNKS_ENABLED',
|
||||
|
||||
'tasks.deleteInterval': 'TASKS_DELETE_INTERVAL',
|
||||
'tasks.clearInvitesInterval': 'TASKS_CLEAR_INVITES_INTERVAL',
|
||||
'tasks.maxViewsInterval': 'TASKS_MAX_VIEWS_INTERVAL',
|
||||
'tasks.thumbnailsInterval': 'TASKS_THUMBNAILS_INTERVAL',
|
||||
'tasks.metricsInterval': 'TASKS_METRICS_INTERVAL',
|
||||
|
||||
'files.route': 'FILES_ROUTE',
|
||||
'files.length': 'FILES_LENGTH',
|
||||
'files.defaultFormat': 'FILES_DEFAULT_FORMAT',
|
||||
'files.disabledExtensions': 'FILES_DISABLED_EXTENSIONS',
|
||||
'files.maxFileSize': 'FILES_MAX_FILE_SIZE',
|
||||
'files.defaultExpiration': 'FILES_DEFAULT_EXPIRATION',
|
||||
'files.assumeMimetypes': 'FILES_ASSUME_MIMETYPES',
|
||||
'files.defaultDateFormat': 'FILES_DEFAULT_DATE_FORMAT',
|
||||
'files.removeGpsMetadata': 'FILES_REMOVE_GPS_METADATA',
|
||||
'files.randomWordsNumAdjectives': 'FILES_RANDOM_WORDS_NUM_ADJECTIVES',
|
||||
'files.randomWordsSeparator': 'FILES_RANDOM_WORDS_SEPARATOR',
|
||||
|
||||
'urls.route': 'URLS_ROUTE',
|
||||
'urls.length': 'URLS_LENGTH',
|
||||
|
||||
'features.imageCompression': 'FEATURES_IMAGE_COMPRESSION',
|
||||
'features.robotsTxt': 'FEATURES_ROBOTS_TXT',
|
||||
'features.healthcheck': 'FEATURES_HEALTHCHECK',
|
||||
'features.userRegistration': 'FEATURES_USER_REGISTRATION',
|
||||
'features.oauthRegistration': 'FEATURES_OAUTH_REGISTRATION',
|
||||
'features.deleteOnMaxViews': 'FEATURES_DELETE_ON_MAX_VIEWS',
|
||||
'features.thumbnails.enabled': 'FEATURES_THUMBNAILS_ENABLED',
|
||||
'features.thumbnails.num_threads': 'FEATURES_THUMBNAILS_NUMBER_THREADS',
|
||||
'features.metrics.enabled': 'FEATURES_METRICS_ENABLED',
|
||||
'features.metrics.adminOnly': 'FEATURES_METRICS_ADMIN_ONLY',
|
||||
'features.metrics.showUserSpecific': 'FEATURES_METRICS_SHOW_USER_SPECIFIC',
|
||||
|
||||
'invites.enabled': 'INVITES_ENABLED',
|
||||
'invites.length': 'INVITES_LENGTH',
|
||||
|
||||
'website.title': 'WEBSITE_TITLE',
|
||||
'website.titleLogo': 'WEBSITE_TITLE_LOGO',
|
||||
'website.externalLinks': 'WEBSITE_EXTERNAL_LINKS',
|
||||
'website.loginBackground': 'WEBSITE_LOGIN_BACKGROUND',
|
||||
'website.loginBackgroundBlur': 'WEBSITE_LOGIN_BACKGROUND_BLUR',
|
||||
'website.defaultAvatar': 'WEBSITE_DEFAULT_AVATAR',
|
||||
'website.tos': 'WEBSITE_TOS',
|
||||
'website.theme.default': 'WEBSITE_THEME_DEFAULT',
|
||||
'website.theme.dark': 'WEBSITE_THEME_DARK',
|
||||
'website.theme.light': 'WEBSITE_THEME_LIGHT',
|
||||
|
||||
'oauth.bypassLocalLogin': 'OAUTH_BYPASS_LOCAL_LOGIN',
|
||||
'oauth.loginOnly': 'OAUTH_LOGIN_ONLY',
|
||||
'oauth.discord.clientId': 'OAUTH_DISCORD_CLIENT_ID',
|
||||
'oauth.discord.clientSecret': 'OAUTH_DISCORD_CLIENT_SECRET',
|
||||
'oauth.discord.redirectUri': 'OAUTH_DISCORD_REDIRECT_URI',
|
||||
'oauth.google.clientId': 'OAUTH_GOOGLE_CLIENT_ID',
|
||||
'oauth.google.clientSecret': 'OAUTH_GOOGLE_CLIENT_SECRET',
|
||||
'oauth.google.redirectUri': 'OAUTH_GOOGLE_REDIRECT_URI',
|
||||
'oauth.github.clientId': 'OAUTH_GITHUB_CLIENT_ID',
|
||||
'oauth.github.clientSecret': 'OAUTH_GITHUB_CLIENT_SECRET',
|
||||
'oauth.github.redirectUri': 'OAUTH_GITHUB_REDIRECT_URI',
|
||||
'oauth.oidc.clientId': 'OAUTH_OIDC_CLIENT_ID',
|
||||
'oauth.oidc.clientSecret': 'OAUTH_OIDC_CLIENT_SECRET',
|
||||
'oauth.oidc.authorizeUrl': 'OAUTH_OIDC_AUTHORIZE_URL',
|
||||
'oauth.oidc.userinfoUrl': 'OAUTH_OIDC_USERINFO_URL',
|
||||
'oauth.oidc.tokenUrl': 'OAUTH_OIDC_TOKEN_URL',
|
||||
'oauth.oidc.redirectUri': 'OAUTH_OIDC_REDIRECT_URI',
|
||||
|
||||
'mfa.totp.enabled': 'MFA_TOTP_ENABLED',
|
||||
'mfa.totp.issuer': 'MFA_TOTP_ISSUER',
|
||||
'mfa.passkeys': 'MFA_PASSKEYS',
|
||||
|
||||
'ratelimit.enabled': 'RATELIMIT_ENABLED',
|
||||
'ratelimit.max': 'RATELIMIT_MAX',
|
||||
'ratelimit.window': 'RATELIMIT_WINDOW',
|
||||
'ratelimit.adminBypass': 'RATELIMIT_ADMIN_BYPASS',
|
||||
'ratelimit.allowList': 'RATELIMIT_ALLOW_LIST',
|
||||
|
||||
'httpWebhook.onUpload': 'HTTPWEBHOOK_ON_UPLOAD',
|
||||
'httpWebhook.onShorten': 'HTTPWEBHOOK_ON_SHORTEN',
|
||||
|
||||
'discord.webhookUrl': 'DISCORD_WEBHOOK_URL',
|
||||
'discord.username': 'DISCORD_USERNAME',
|
||||
'discord.avatarUrl': 'DISCORD_AVATAR_URL',
|
||||
'discord.onUpload.webhookUrl': 'DISCORD_ON_UPLOAD_WEBHOOK_URL',
|
||||
'discord.onUpload.username': 'DISCORD_ON_UPLOAD_USERNAME',
|
||||
'discord.onUpload.avatarUrl': 'DISCORD_ON_UPLOAD_AVATAR_URL',
|
||||
'discord.onUpload.content': 'DISCORD_ON_UPLOAD_CONTENT',
|
||||
'discord.onUpload.embed': 'DISCORD_ON_UPLOAD_EMBED',
|
||||
'discord.onShorten.webhookUrl': 'DISCORD_ON_SHORTEN_WEBHOOK_URL',
|
||||
'discord.onShorten.username': 'DISCORD_ON_SHORTEN_USERNAME',
|
||||
'discord.onShorten.avatarUrl': 'DISCORD_ON_SHORTEN_AVATAR_URL',
|
||||
'discord.onShorten.content': 'DISCORD_ON_SHORTEN_CONTENT',
|
||||
'discord.onShorten.embed': 'DISCORD_ON_SHORTEN_EMBED',
|
||||
|
||||
'pwa.enabled': 'PWA_ENABLED',
|
||||
'pwa.title': 'PWA_TITLE',
|
||||
'pwa.shortName': 'PWA_SHORT_NAME',
|
||||
'pwa.description': 'PWA_DESCRIPTION',
|
||||
'pwa.themeColor': 'PWA_THEME_COLOR',
|
||||
'pwa.backgroundColor': 'PWA_BACKGROUND_COLOR',
|
||||
};
|
||||
|
||||
export const DATABASE_TO_PROP = {
|
||||
|
@ -191,7 +298,7 @@ export const DATABASE_TO_PROP = {
|
|||
filesDefaultDateFormat: 'files.defaultDateFormat',
|
||||
filesRemoveGpsMetadata: 'files.removeGpsMetadata',
|
||||
filesRandomWordsNumAdjectives: 'files.randomWordsNumAdjectives',
|
||||
filesRandomWordsSeperator: 'files.randomWordsSeperator',
|
||||
filesRandomWordsSeparator: 'files.randomWordsSeparator',
|
||||
|
||||
urlsRoute: 'urls.route',
|
||||
urlsLength: 'urls.length',
|
||||
|
@ -333,6 +440,113 @@ export function readEnv() {
|
|||
|
||||
env('ssl.key', 'string'),
|
||||
env('ssl.cert', 'string'),
|
||||
|
||||
env('core.returnHttpsUrls', 'boolean'),
|
||||
env('core.defaultDomain', 'string'),
|
||||
env('core.tempDirectory', 'string'),
|
||||
|
||||
env('chunks.max', 'string'),
|
||||
env('chunks.size', 'string'),
|
||||
env('chunks.enabled', 'boolean'),
|
||||
|
||||
env('tasks.deleteInterval', 'string'),
|
||||
env('tasks.clearInvitesInterval', 'string'),
|
||||
env('tasks.maxViewsInterval', 'string'),
|
||||
env('tasks.thumbnailsInterval', 'string'),
|
||||
env('tasks.metricsInterval', 'string'),
|
||||
|
||||
env('files.route', 'string'),
|
||||
env('files.length', 'number'),
|
||||
env('files.defaultFormat', 'string'),
|
||||
env('files.disabledExtensions', 'string[]'),
|
||||
env('files.maxFileSize', 'string'),
|
||||
env('files.defaultExpiration', 'string'),
|
||||
env('files.assumeMimetypes', 'boolean'),
|
||||
env('files.defaultDateFormat', 'string'),
|
||||
env('files.removeGpsMetadata', 'boolean'),
|
||||
env('files.randomWordsNumAdjectives', 'number'),
|
||||
env('files.randomWordsSeparator', 'string'),
|
||||
|
||||
env('urls.route', 'string'),
|
||||
env('urls.length', 'number'),
|
||||
|
||||
env('features.imageCompression', 'boolean'),
|
||||
env('features.robotsTxt', 'boolean'),
|
||||
env('features.healthcheck', 'boolean'),
|
||||
env('features.userRegistration', 'boolean'),
|
||||
env('features.oauthRegistration', 'boolean'),
|
||||
env('features.deleteOnMaxViews', 'boolean'),
|
||||
env('features.thumbnails.enabled', 'boolean'),
|
||||
env('features.thumbnails.num_threads', 'number'),
|
||||
env('features.metrics.enabled', 'boolean'),
|
||||
env('features.metrics.adminOnly', 'boolean'),
|
||||
env('features.metrics.showUserSpecific', 'boolean'),
|
||||
|
||||
env('invites.enabled', 'boolean'),
|
||||
env('invites.length', 'number'),
|
||||
|
||||
env('website.title', 'string'),
|
||||
env('website.titleLogo', 'string'),
|
||||
env('website.externalLinks', 'json[]'),
|
||||
env('website.loginBackground', 'string'),
|
||||
env('website.loginBackgroundBlur', 'boolean'),
|
||||
env('website.defaultAvatar', 'string'),
|
||||
env('website.tos', 'string'),
|
||||
env('website.theme.default', 'string'),
|
||||
env('website.theme.dark', 'string'),
|
||||
env('website.theme.light', 'string'),
|
||||
|
||||
env('oauth.bypassLocalLogin', 'boolean'),
|
||||
env('oauth.loginOnly', 'boolean'),
|
||||
env('oauth.discord.clientId', 'string'),
|
||||
env('oauth.discord.clientSecret', 'string'),
|
||||
env('oauth.discord.redirectUri', 'string'),
|
||||
env('oauth.google.clientId', 'string'),
|
||||
env('oauth.google.clientSecret', 'string'),
|
||||
env('oauth.google.redirectUri', 'string'),
|
||||
env('oauth.github.clientId', 'string'),
|
||||
env('oauth.github.clientSecret', 'string'),
|
||||
env('oauth.github.redirectUri', 'string'),
|
||||
env('oauth.oidc.clientId', 'string'),
|
||||
env('oauth.oidc.clientSecret', 'string'),
|
||||
env('oauth.oidc.authorizeUrl', 'string'),
|
||||
env('oauth.oidc.userinfoUrl', 'string'),
|
||||
env('oauth.oidc.tokenUrl', 'string'),
|
||||
env('oauth.oidc.redirectUri', 'string'),
|
||||
|
||||
env('mfa.totp.enabled', 'boolean'),
|
||||
env('mfa.totp.issuer', 'string'),
|
||||
env('mfa.passkeys', 'boolean'),
|
||||
|
||||
env('ratelimit.enabled', 'boolean'),
|
||||
env('ratelimit.max', 'number'),
|
||||
env('ratelimit.window', 'number'),
|
||||
env('ratelimit.adminBypass', 'boolean'),
|
||||
env('ratelimit.allowList', 'string[]'),
|
||||
|
||||
env('httpWebhook.onUpload', 'string'),
|
||||
env('httpWebhook.onShorten', 'string'),
|
||||
|
||||
env('discord.webhookUrl', 'string'),
|
||||
env('discord.username', 'string'),
|
||||
env('discord.avatarUrl', 'string'),
|
||||
env('discord.onUpload.webhookUrl', 'string'),
|
||||
env('discord.onUpload.username', 'string'),
|
||||
env('discord.onUpload.avatarUrl', 'string'),
|
||||
env('discord.onUpload.content', 'string'),
|
||||
env('discord.onUpload.embed', 'json[]'),
|
||||
env('discord.onShorten.webhookUrl', 'string'),
|
||||
env('discord.onShorten.username', 'string'),
|
||||
env('discord.onShorten.avatarUrl', 'string'),
|
||||
env('discord.onShorten.content', 'string'),
|
||||
env('discord.onShorten.embed', 'json[]'),
|
||||
|
||||
env('pwa.enabled', 'boolean'),
|
||||
env('pwa.title', 'string'),
|
||||
env('pwa.shortName', 'string'),
|
||||
env('pwa.description', 'string'),
|
||||
env('pwa.themeColor', 'string'),
|
||||
env('pwa.backgroundColor', 'string'),
|
||||
];
|
||||
|
||||
const raw: Record<keyof typeof rawConfig, any> = {};
|
||||
|
@ -397,6 +611,44 @@ export async function read() {
|
|||
return raw;
|
||||
}
|
||||
|
||||
export function replaceDatabaseValueWithEnv<T>(
|
||||
Key: keyof typeof DATABASE_TO_PROP,
|
||||
databaseValue: T,
|
||||
typeString: EnvType,
|
||||
): T {
|
||||
const envKeys = databaseToEnv(Key);
|
||||
|
||||
for (let i = 0; i !== envKeys.length; ++i) {
|
||||
const value = process.env[envKeys[i]];
|
||||
if (value === undefined) continue;
|
||||
|
||||
const parsed = parse(value, typeString);
|
||||
if (parsed === undefined) continue;
|
||||
|
||||
return parsed;
|
||||
}
|
||||
|
||||
return databaseValue;
|
||||
}
|
||||
|
||||
export function valueIsFromEnv(Key: keyof typeof DATABASE_TO_PROP): string | undefined {
|
||||
const envKeys = databaseToEnv(Key);
|
||||
|
||||
for (let i = 0; i !== envKeys.length; ++i) {
|
||||
const value = process.env[envKeys[i]];
|
||||
if (value !== undefined) return value;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function databaseToEnv(key: keyof typeof DATABASE_TO_PROP): string[] {
|
||||
const prop = PROP_TO_ENV[DATABASE_TO_PROP[key] as keyof typeof PROP_TO_ENV];
|
||||
if (!prop) return [];
|
||||
if (typeof prop === 'string') return [prop];
|
||||
return prop;
|
||||
}
|
||||
|
||||
function isObject(value: any) {
|
||||
return typeof value === 'object' && value !== null;
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@ import { join, resolve } from 'path';
|
|||
import { type ZodIssue, z } from 'zod';
|
||||
import { log } from '../logger';
|
||||
import { PROP_TO_ENV, ParsedConfig } from './read';
|
||||
import { parseSettings } from '@/server/routes/api/server/settings';
|
||||
|
||||
declare global {
|
||||
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||
|
@ -95,7 +96,7 @@ export const schema = z.object({
|
|||
defaultDateFormat: z.string().default('YYYY-MM-DD_HH:mm:ss'),
|
||||
removeGpsMetadata: z.boolean().default(false),
|
||||
randomWordsNumAdjectives: z.number().default(3),
|
||||
randomWordsSeperator: z.string().default('-'),
|
||||
randomWordsSeparator: z.string().default('-'),
|
||||
}),
|
||||
urls: z.object({
|
||||
route: z.string().startsWith('/').min(1).trim().toLowerCase().default('/go'),
|
||||
|
@ -323,7 +324,7 @@ export type Config = z.infer<typeof schema>;
|
|||
|
||||
const logger = log('config').c('validate');
|
||||
|
||||
export function validateConfigObject(env: ParsedConfig): Config {
|
||||
export async function validateConfigObject(env: ParsedConfig): Promise<Config> {
|
||||
const building = !!process.env.ZIPLINE_BUILD;
|
||||
|
||||
if (building) {
|
||||
|
@ -343,6 +344,113 @@ export function validateConfigObject(env: ParsedConfig): Config {
|
|||
process.exit(1);
|
||||
}
|
||||
|
||||
// flatten object for use with settingsValidator
|
||||
const flat = {
|
||||
coreReturnHttpsUrls: validated.data.core.returnHttpsUrls,
|
||||
corePort: validated.data.core.port,
|
||||
coreHostname: validated.data.core.hostname,
|
||||
coreSecret: validated.data.core.secret,
|
||||
coreDatabaseUrl: validated.data.core.databaseUrl,
|
||||
coreDefaultDomain: validated.data.core.defaultDomain,
|
||||
coreTempDirectory: validated.data.core.tempDirectory,
|
||||
chunksMax: validated.data.chunks.max,
|
||||
chunksSize: validated.data.chunks.size,
|
||||
chunksEnabled: validated.data.chunks.enabled,
|
||||
tasksDeleteInterval: validated.data.tasks.deleteInterval,
|
||||
tasksClearInvitesInterval: validated.data.tasks.clearInvitesInterval,
|
||||
tasksMaxViewsInterval: validated.data.tasks.maxViewsInterval,
|
||||
tasksThumbnailsInterval: validated.data.tasks.thumbnailsInterval,
|
||||
tasksMetricsInterval: validated.data.tasks.metricsInterval,
|
||||
filesRoute: validated.data.files.route,
|
||||
filesLength: validated.data.files.length,
|
||||
filesDefaultFormat: validated.data.files.defaultFormat,
|
||||
filesDisabledExtensions: validated.data.files.disabledExtensions,
|
||||
filesMaxFileSize: validated.data.files.maxFileSize,
|
||||
filesDefaultExpiration: validated.data.files.defaultExpiration,
|
||||
filesAssumeMimetypes: validated.data.files.assumeMimetypes,
|
||||
filesDefaultDateFormat: validated.data.files.defaultDateFormat,
|
||||
filesRemoveGpsMetadata: validated.data.files.removeGpsMetadata,
|
||||
filesRandomWordsNumAdjectives: validated.data.files.randomWordsNumAdjectives,
|
||||
filesRandomWordsSeparator: validated.data.files.randomWordsSeparator,
|
||||
urlsRoute: validated.data.urls.route,
|
||||
urlsLength: validated.data.urls.length,
|
||||
datasourceType: validated.data.datasource.type,
|
||||
datasourceS3AccessKeyId: validated.data.datasource.s3?.accessKeyId,
|
||||
datasourceS3SecretAccessKey: validated.data.datasource.s3?.secretAccessKey,
|
||||
datasourceS3Region: validated.data.datasource.s3?.region,
|
||||
datasourceS3Bucket: validated.data.datasource.s3?.bucket,
|
||||
datasourceS3Endpoint: validated.data.datasource.s3?.endpoint,
|
||||
datasourceS3ForcePathStyle: validated.data.datasource.s3?.forcePathStyle,
|
||||
datasourceLocalDirectory: validated.data.datasource.local.directory,
|
||||
featuresImageCompression: validated.data.features.imageCompression,
|
||||
featuresRobotsTxt: validated.data.features.robotsTxt,
|
||||
featuresHealthcheck: validated.data.features.healthcheck,
|
||||
featuresUserRegistration: validated.data.features.userRegistration,
|
||||
featuresOauthRegistration: validated.data.features.oauthRegistration,
|
||||
featuresDeleteOnMaxViews: validated.data.features.deleteOnMaxViews,
|
||||
featuresThumbnailsEnabled: validated.data.features.thumbnails.enabled,
|
||||
featuresThumbnailsNumThreads: validated.data.features.thumbnails.num_threads,
|
||||
featuresMetricsEnabled: validated.data.features.metrics.enabled,
|
||||
featuresMetricsAdminOnly: validated.data.features.metrics.adminOnly,
|
||||
featuresMetricsShowUserSpecific: validated.data.features.metrics.showUserSpecific,
|
||||
invitesEnabled: validated.data.invites.enabled,
|
||||
invitesLength: validated.data.invites.length,
|
||||
websiteTitle: validated.data.website.title,
|
||||
websiteTitleLogo: validated.data.website.titleLogo,
|
||||
websiteExternalLinks: validated.data.website.externalLinks,
|
||||
websiteLoginBackground: validated.data.website.loginBackground,
|
||||
websiteLoginBackgroundBlur: validated.data.website.loginBackgroundBlur,
|
||||
websiteDefaultAvatar: validated.data.website.defaultAvatar,
|
||||
websiteThemeDefault: validated.data.website.theme.default,
|
||||
websiteThemeDark: validated.data.website.theme.dark,
|
||||
websiteThemeLight: validated.data.website.theme.light,
|
||||
websiteTos: validated.data.website.tos,
|
||||
mfaTotpEnabled: validated.data.mfa.totp.enabled,
|
||||
mfaTotpIssuer: validated.data.mfa.totp.issuer,
|
||||
mfaPasskeys: validated.data.mfa.passkeys,
|
||||
oauthBypassLocalLogin: validated.data.oauth.bypassLocalLogin,
|
||||
oauthLoginOnly: validated.data.oauth.loginOnly,
|
||||
oauthDiscordClientId: validated.data.oauth.discord.clientId,
|
||||
oauthDiscordClientSecret: validated.data.oauth.discord.clientSecret,
|
||||
oauthDiscordRedirectUri: validated.data.oauth.discord.redirectUri,
|
||||
oauthGithubClientId: validated.data.oauth.github.clientId,
|
||||
oauthGithubClientSecret: validated.data.oauth.github.clientSecret,
|
||||
oauthGithubRedirectUri: validated.data.oauth.github.redirectUri,
|
||||
oauthGoogleClientId: validated.data.oauth.google.clientId,
|
||||
oauthGoogleClientSecret: validated.data.oauth.google.clientSecret,
|
||||
oauthGoogleRedirectUri: validated.data.oauth.google.redirectUri,
|
||||
oauthOidcClientId: validated.data.oauth.oidc.clientId,
|
||||
oauthOidcClientSecret: validated.data.oauth.oidc.clientSecret,
|
||||
oauthOidcAuthorizeUrl: validated.data.oauth.oidc.authorizeUrl,
|
||||
oauthOidcUserinfoUrl: validated.data.oauth.oidc.userinfoUrl,
|
||||
oauthOidcTokenUrl: validated.data.oauth.oidc.tokenUrl,
|
||||
oauthOidcRedirectUri: validated.data.oauth.oidc.redirectUri,
|
||||
discordWebhookUrl: validated.data.discord?.webhookUrl,
|
||||
discordUsername: validated.data.discord?.username,
|
||||
discordAvatarUrl: validated.data.discord?.avatarUrl,
|
||||
discordOnUpload: validated.data.discord?.onUpload,
|
||||
discordOnShorten: validated.data.discord?.onShorten,
|
||||
ratelimitEnabled: validated.data.ratelimit.enabled,
|
||||
ratelimitMax: validated.data.ratelimit.max,
|
||||
ratelimitWindow: validated.data.ratelimit.window,
|
||||
ratelimitAdminBypass: validated.data.ratelimit.adminBypass,
|
||||
ratelimitAllowList: validated.data.ratelimit.allowList,
|
||||
httpWebhookOnUpload: validated.data.httpWebhook.onUpload,
|
||||
httpWebhookOnShorten: validated.data.httpWebhook.onShorten,
|
||||
};
|
||||
|
||||
const result = await parseSettings(flat);
|
||||
|
||||
if (!result.success) {
|
||||
logger.error('There was an error while validating the environment.');
|
||||
|
||||
for (const error of result.error?.issues ?? []) {
|
||||
handleError(error);
|
||||
}
|
||||
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
logger.debug('reloaded config');
|
||||
|
||||
return validated.data;
|
||||
|
|
|
@ -3,6 +3,7 @@ import { Prisma, PrismaClient } from '@prisma/client';
|
|||
import { userViewSchema } from './models/user';
|
||||
import { metricDataSchema } from './models/metric';
|
||||
import { metadataSchema } from './models/incompleteFile';
|
||||
import { SettingsExtension } from './settingsExtension';
|
||||
|
||||
const building = !!process.env.ZIPLINE_BUILD;
|
||||
|
||||
|
@ -38,42 +39,44 @@ function getClient() {
|
|||
|
||||
const client = new PrismaClient({
|
||||
log: process.env.ZIPLINE_DB_LOG ? parseDbLog(process.env.ZIPLINE_DB_LOG) : undefined,
|
||||
}).$extends({
|
||||
result: {
|
||||
file: {
|
||||
size: {
|
||||
needs: { size: true },
|
||||
compute({ size }: { size: bigint }) {
|
||||
return Number(size);
|
||||
})
|
||||
.$extends({
|
||||
result: {
|
||||
file: {
|
||||
size: {
|
||||
needs: { size: true },
|
||||
compute({ size }: { size: bigint }) {
|
||||
return Number(size);
|
||||
},
|
||||
},
|
||||
},
|
||||
user: {
|
||||
view: {
|
||||
needs: { view: true },
|
||||
compute({ view }: { view: Prisma.JsonValue }) {
|
||||
return userViewSchema.parse(view);
|
||||
},
|
||||
},
|
||||
},
|
||||
metric: {
|
||||
data: {
|
||||
needs: { data: true },
|
||||
compute({ data }: { data: Prisma.JsonValue }) {
|
||||
return metricDataSchema.parse(data);
|
||||
},
|
||||
},
|
||||
},
|
||||
incompleteFile: {
|
||||
metadata: {
|
||||
needs: { metadata: true },
|
||||
compute({ metadata }: { metadata: Prisma.JsonValue }) {
|
||||
return metadataSchema.parse(metadata);
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
user: {
|
||||
view: {
|
||||
needs: { view: true },
|
||||
compute({ view }: { view: Prisma.JsonValue }) {
|
||||
return userViewSchema.parse(view);
|
||||
},
|
||||
},
|
||||
},
|
||||
metric: {
|
||||
data: {
|
||||
needs: { data: true },
|
||||
compute({ data }: { data: Prisma.JsonValue }) {
|
||||
return metricDataSchema.parse(data);
|
||||
},
|
||||
},
|
||||
},
|
||||
incompleteFile: {
|
||||
metadata: {
|
||||
needs: { metadata: true },
|
||||
compute({ metadata }: { metadata: Prisma.JsonValue }) {
|
||||
return metadataSchema.parse(metadata);
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
})
|
||||
.$extends(SettingsExtension);
|
||||
client.$connect();
|
||||
|
||||
return client;
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { prisma } from '..';
|
||||
|
||||
export async function getZipline() {
|
||||
const zipline = await prisma.zipline.findFirst();
|
||||
const zipline = await prisma.zipline.getSettings();
|
||||
if (!zipline) {
|
||||
return prisma.zipline.create({
|
||||
data: {
|
||||
|
|
401
src/lib/db/settingsExtension.tsx
Normal file
401
src/lib/db/settingsExtension.tsx
Normal file
|
@ -0,0 +1,401 @@
|
|||
import { Prisma } from '@prisma/client';
|
||||
import { prisma } from '.';
|
||||
import { replaceDatabaseValueWithEnv } from '../config/read';
|
||||
|
||||
export const SettingsExtension = Prisma.defineExtension({
|
||||
name: 'settings',
|
||||
model: {
|
||||
zipline: {
|
||||
async getSettings(...args: any[]) {
|
||||
let settings = await prisma.zipline.findFirst(...args);
|
||||
if (!settings) {
|
||||
return settings;
|
||||
}
|
||||
// I was going to do a loop, but i kept getting typescript errors. please forgive me
|
||||
settings = {
|
||||
id: settings.id,
|
||||
createdAt: settings.createdAt,
|
||||
updatedAt: settings.updatedAt,
|
||||
firstSetup: settings.firstSetup,
|
||||
coreReturnHttpsUrls: replaceDatabaseValueWithEnv(
|
||||
'coreReturnHttpsUrls',
|
||||
settings.coreReturnHttpsUrls,
|
||||
'boolean',
|
||||
),
|
||||
coreDefaultDomain: replaceDatabaseValueWithEnv(
|
||||
'coreDefaultDomain',
|
||||
settings.coreDefaultDomain,
|
||||
'string',
|
||||
),
|
||||
coreTempDirectory: replaceDatabaseValueWithEnv(
|
||||
'coreTempDirectory',
|
||||
settings.coreTempDirectory,
|
||||
'string',
|
||||
),
|
||||
chunksMax: replaceDatabaseValueWithEnv('chunksMax', settings.chunksMax, 'string'),
|
||||
chunksSize: replaceDatabaseValueWithEnv('chunksSize', settings.chunksSize, 'string'),
|
||||
chunksEnabled: replaceDatabaseValueWithEnv('chunksEnabled', settings.chunksEnabled, 'boolean'),
|
||||
tasksDeleteInterval: replaceDatabaseValueWithEnv(
|
||||
'tasksDeleteInterval',
|
||||
settings.tasksDeleteInterval,
|
||||
'ms',
|
||||
),
|
||||
tasksClearInvitesInterval: replaceDatabaseValueWithEnv(
|
||||
'tasksClearInvitesInterval',
|
||||
settings.tasksClearInvitesInterval,
|
||||
'ms',
|
||||
),
|
||||
tasksMaxViewsInterval: replaceDatabaseValueWithEnv(
|
||||
'tasksMaxViewsInterval',
|
||||
settings.tasksMaxViewsInterval,
|
||||
'ms',
|
||||
),
|
||||
tasksThumbnailsInterval: replaceDatabaseValueWithEnv(
|
||||
'tasksThumbnailsInterval',
|
||||
settings.tasksThumbnailsInterval,
|
||||
'ms',
|
||||
),
|
||||
tasksMetricsInterval: replaceDatabaseValueWithEnv(
|
||||
'tasksMetricsInterval',
|
||||
settings.tasksMetricsInterval,
|
||||
'ms',
|
||||
),
|
||||
filesRoute: replaceDatabaseValueWithEnv('filesRoute', settings.filesRoute, 'string'),
|
||||
filesLength: replaceDatabaseValueWithEnv('filesLength', settings.filesLength, 'number'),
|
||||
filesDefaultFormat: replaceDatabaseValueWithEnv(
|
||||
'filesDefaultFormat',
|
||||
settings.filesDefaultFormat,
|
||||
'string',
|
||||
),
|
||||
filesDisabledExtensions: replaceDatabaseValueWithEnv(
|
||||
'filesDisabledExtensions',
|
||||
settings.filesDisabledExtensions,
|
||||
'string[]',
|
||||
),
|
||||
filesMaxFileSize: replaceDatabaseValueWithEnv(
|
||||
'filesMaxFileSize',
|
||||
settings.filesMaxFileSize,
|
||||
'byte',
|
||||
),
|
||||
filesDefaultExpiration: replaceDatabaseValueWithEnv(
|
||||
'filesDefaultExpiration',
|
||||
settings.filesDefaultExpiration,
|
||||
'string',
|
||||
),
|
||||
filesAssumeMimetypes: replaceDatabaseValueWithEnv(
|
||||
'filesAssumeMimetypes',
|
||||
settings.filesAssumeMimetypes,
|
||||
'boolean',
|
||||
),
|
||||
filesDefaultDateFormat: replaceDatabaseValueWithEnv(
|
||||
'filesDefaultDateFormat',
|
||||
settings.filesDefaultDateFormat,
|
||||
'string',
|
||||
),
|
||||
filesRemoveGpsMetadata: replaceDatabaseValueWithEnv(
|
||||
'filesRemoveGpsMetadata',
|
||||
settings.filesRemoveGpsMetadata,
|
||||
'boolean',
|
||||
),
|
||||
filesRandomWordsNumAdjectives: replaceDatabaseValueWithEnv(
|
||||
'filesRandomWordsNumAdjectives',
|
||||
settings.filesRandomWordsNumAdjectives,
|
||||
'number',
|
||||
),
|
||||
filesRandomWordsSeparator: replaceDatabaseValueWithEnv(
|
||||
'filesRandomWordsSeparator',
|
||||
settings.filesRandomWordsSeparator,
|
||||
'string',
|
||||
),
|
||||
urlsRoute: replaceDatabaseValueWithEnv('urlsRoute', settings.urlsRoute, 'string'),
|
||||
urlsLength: replaceDatabaseValueWithEnv('urlsLength', settings.urlsLength, 'number'),
|
||||
featuresImageCompression: replaceDatabaseValueWithEnv(
|
||||
'featuresImageCompression',
|
||||
settings.featuresImageCompression,
|
||||
'boolean',
|
||||
),
|
||||
featuresRobotsTxt: replaceDatabaseValueWithEnv(
|
||||
'featuresRobotsTxt',
|
||||
settings.featuresRobotsTxt,
|
||||
'boolean',
|
||||
),
|
||||
featuresHealthcheck: replaceDatabaseValueWithEnv(
|
||||
'featuresHealthcheck',
|
||||
settings.featuresHealthcheck,
|
||||
'boolean',
|
||||
),
|
||||
featuresUserRegistration: replaceDatabaseValueWithEnv(
|
||||
'featuresUserRegistration',
|
||||
settings.featuresUserRegistration,
|
||||
'boolean',
|
||||
),
|
||||
featuresOauthRegistration: replaceDatabaseValueWithEnv(
|
||||
'featuresOauthRegistration',
|
||||
settings.featuresOauthRegistration,
|
||||
'boolean',
|
||||
),
|
||||
featuresDeleteOnMaxViews: replaceDatabaseValueWithEnv(
|
||||
'featuresDeleteOnMaxViews',
|
||||
settings.featuresDeleteOnMaxViews,
|
||||
'boolean',
|
||||
),
|
||||
featuresThumbnailsEnabled: replaceDatabaseValueWithEnv(
|
||||
'featuresThumbnailsEnabled',
|
||||
settings.featuresThumbnailsEnabled,
|
||||
'boolean',
|
||||
),
|
||||
featuresThumbnailsNumberThreads: replaceDatabaseValueWithEnv(
|
||||
'featuresThumbnailsNumberThreads',
|
||||
settings.featuresThumbnailsNumberThreads,
|
||||
'number',
|
||||
),
|
||||
featuresMetricsEnabled: replaceDatabaseValueWithEnv(
|
||||
'featuresMetricsEnabled',
|
||||
settings.featuresMetricsEnabled,
|
||||
'boolean',
|
||||
),
|
||||
featuresMetricsAdminOnly: replaceDatabaseValueWithEnv(
|
||||
'featuresMetricsAdminOnly',
|
||||
settings.featuresMetricsAdminOnly,
|
||||
'boolean',
|
||||
),
|
||||
featuresMetricsShowUserSpecific: replaceDatabaseValueWithEnv(
|
||||
'featuresMetricsShowUserSpecific',
|
||||
settings.featuresMetricsShowUserSpecific,
|
||||
'boolean',
|
||||
),
|
||||
invitesEnabled: replaceDatabaseValueWithEnv('invitesEnabled', settings.invitesEnabled, 'boolean'),
|
||||
invitesLength: replaceDatabaseValueWithEnv('invitesLength', settings.invitesLength, 'number'),
|
||||
websiteTitle: replaceDatabaseValueWithEnv('websiteTitle', settings.websiteTitle, 'string'),
|
||||
websiteTitleLogo: replaceDatabaseValueWithEnv(
|
||||
'websiteTitleLogo',
|
||||
settings.websiteTitleLogo,
|
||||
'string',
|
||||
),
|
||||
websiteExternalLinks: replaceDatabaseValueWithEnv(
|
||||
'websiteExternalLinks',
|
||||
settings.websiteExternalLinks,
|
||||
'json[]',
|
||||
),
|
||||
websiteLoginBackground: replaceDatabaseValueWithEnv(
|
||||
'websiteLoginBackground',
|
||||
settings.websiteLoginBackground,
|
||||
'string',
|
||||
),
|
||||
websiteLoginBackgroundBlur: replaceDatabaseValueWithEnv(
|
||||
'websiteLoginBackgroundBlur',
|
||||
settings.websiteLoginBackgroundBlur,
|
||||
'boolean',
|
||||
),
|
||||
websiteDefaultAvatar: replaceDatabaseValueWithEnv(
|
||||
'websiteDefaultAvatar',
|
||||
settings.websiteDefaultAvatar,
|
||||
'string',
|
||||
),
|
||||
websiteTos: replaceDatabaseValueWithEnv('websiteTos', settings.websiteTos, 'string'),
|
||||
websiteThemeDefault: replaceDatabaseValueWithEnv(
|
||||
'websiteThemeDefault',
|
||||
settings.websiteThemeDefault,
|
||||
'string',
|
||||
),
|
||||
websiteThemeDark: replaceDatabaseValueWithEnv(
|
||||
'websiteThemeDark',
|
||||
settings.websiteThemeDark,
|
||||
'string',
|
||||
),
|
||||
websiteThemeLight: replaceDatabaseValueWithEnv(
|
||||
'websiteThemeLight',
|
||||
settings.websiteThemeLight,
|
||||
'string',
|
||||
),
|
||||
oauthBypassLocalLogin: replaceDatabaseValueWithEnv(
|
||||
'oauthBypassLocalLogin',
|
||||
settings.oauthBypassLocalLogin,
|
||||
'boolean',
|
||||
),
|
||||
oauthLoginOnly: replaceDatabaseValueWithEnv('oauthLoginOnly', settings.oauthLoginOnly, 'boolean'),
|
||||
oauthDiscordClientId: replaceDatabaseValueWithEnv(
|
||||
'oauthDiscordClientId',
|
||||
settings.oauthDiscordClientId,
|
||||
'string',
|
||||
),
|
||||
oauthDiscordClientSecret: replaceDatabaseValueWithEnv(
|
||||
'oauthDiscordClientSecret',
|
||||
settings.oauthDiscordClientSecret,
|
||||
'string',
|
||||
),
|
||||
oauthDiscordRedirectUri: replaceDatabaseValueWithEnv(
|
||||
'oauthDiscordRedirectUri',
|
||||
settings.oauthDiscordRedirectUri,
|
||||
'string',
|
||||
),
|
||||
oauthGoogleClientId: replaceDatabaseValueWithEnv(
|
||||
'oauthGoogleClientId',
|
||||
settings.oauthGoogleClientId,
|
||||
'string',
|
||||
),
|
||||
oauthGoogleClientSecret: replaceDatabaseValueWithEnv(
|
||||
'oauthGoogleClientSecret',
|
||||
settings.oauthGoogleClientSecret,
|
||||
'string',
|
||||
),
|
||||
oauthGoogleRedirectUri: replaceDatabaseValueWithEnv(
|
||||
'oauthGoogleRedirectUri',
|
||||
settings.oauthGoogleRedirectUri,
|
||||
'string',
|
||||
),
|
||||
oauthGithubClientId: replaceDatabaseValueWithEnv(
|
||||
'oauthGithubClientId',
|
||||
settings.oauthGithubClientId,
|
||||
'string',
|
||||
),
|
||||
oauthGithubClientSecret: replaceDatabaseValueWithEnv(
|
||||
'oauthGithubClientSecret',
|
||||
settings.oauthGithubClientSecret,
|
||||
'string',
|
||||
),
|
||||
oauthGithubRedirectUri: replaceDatabaseValueWithEnv(
|
||||
'oauthGithubRedirectUri',
|
||||
settings.oauthGithubRedirectUri,
|
||||
'string',
|
||||
),
|
||||
oauthOidcClientId: replaceDatabaseValueWithEnv(
|
||||
'oauthOidcClientId',
|
||||
settings.oauthOidcClientId,
|
||||
'string',
|
||||
),
|
||||
oauthOidcClientSecret: replaceDatabaseValueWithEnv(
|
||||
'oauthOidcClientSecret',
|
||||
settings.oauthOidcClientSecret,
|
||||
'string',
|
||||
),
|
||||
oauthOidcAuthorizeUrl: replaceDatabaseValueWithEnv(
|
||||
'oauthOidcAuthorizeUrl',
|
||||
settings.oauthOidcAuthorizeUrl,
|
||||
'string',
|
||||
),
|
||||
oauthOidcUserinfoUrl: replaceDatabaseValueWithEnv(
|
||||
'oauthOidcUserinfoUrl',
|
||||
settings.oauthOidcUserinfoUrl,
|
||||
'string',
|
||||
),
|
||||
oauthOidcTokenUrl: replaceDatabaseValueWithEnv(
|
||||
'oauthOidcTokenUrl',
|
||||
settings.oauthOidcTokenUrl,
|
||||
'string',
|
||||
),
|
||||
oauthOidcRedirectUri: replaceDatabaseValueWithEnv(
|
||||
'oauthOidcRedirectUri',
|
||||
settings.oauthOidcRedirectUri,
|
||||
'string',
|
||||
),
|
||||
mfaTotpEnabled: replaceDatabaseValueWithEnv('mfaTotpEnabled', settings.mfaTotpEnabled, 'boolean'),
|
||||
mfaTotpIssuer: replaceDatabaseValueWithEnv('mfaTotpIssuer', settings.mfaTotpIssuer, 'string'),
|
||||
mfaPasskeys: replaceDatabaseValueWithEnv('mfaPasskeys', settings.mfaPasskeys, 'boolean'),
|
||||
ratelimitEnabled: replaceDatabaseValueWithEnv(
|
||||
'ratelimitEnabled',
|
||||
settings.ratelimitEnabled,
|
||||
'boolean',
|
||||
),
|
||||
ratelimitMax: replaceDatabaseValueWithEnv('ratelimitMax', settings.ratelimitMax, 'number'),
|
||||
ratelimitWindow: replaceDatabaseValueWithEnv('ratelimitWindow', settings.ratelimitWindow, 'number'),
|
||||
ratelimitAdminBypass: replaceDatabaseValueWithEnv(
|
||||
'ratelimitAdminBypass',
|
||||
settings.ratelimitAdminBypass,
|
||||
'boolean',
|
||||
),
|
||||
ratelimitAllowList: replaceDatabaseValueWithEnv(
|
||||
'ratelimitAllowList',
|
||||
settings.ratelimitAllowList,
|
||||
'string[]',
|
||||
),
|
||||
httpWebhookOnUpload: replaceDatabaseValueWithEnv(
|
||||
'httpWebhookOnUpload',
|
||||
settings.httpWebhookOnUpload,
|
||||
'string',
|
||||
),
|
||||
httpWebhookOnShorten: replaceDatabaseValueWithEnv(
|
||||
'httpWebhookOnShorten',
|
||||
settings.httpWebhookOnShorten,
|
||||
'string',
|
||||
),
|
||||
discordWebhookUrl: replaceDatabaseValueWithEnv(
|
||||
'discordWebhookUrl',
|
||||
settings.discordWebhookUrl,
|
||||
'string',
|
||||
),
|
||||
discordUsername: replaceDatabaseValueWithEnv('discordUsername', settings.discordUsername, 'string'),
|
||||
discordAvatarUrl: replaceDatabaseValueWithEnv(
|
||||
'discordAvatarUrl',
|
||||
settings.discordAvatarUrl,
|
||||
'string',
|
||||
),
|
||||
discordOnUploadWebhookUrl: replaceDatabaseValueWithEnv(
|
||||
'discordOnUploadWebhookUrl',
|
||||
settings.discordOnUploadWebhookUrl,
|
||||
'string',
|
||||
),
|
||||
discordOnUploadUsername: replaceDatabaseValueWithEnv(
|
||||
'discordOnUploadUsername',
|
||||
settings.discordOnUploadUsername,
|
||||
'string',
|
||||
),
|
||||
discordOnUploadAvatarUrl: replaceDatabaseValueWithEnv(
|
||||
'discordOnUploadAvatarUrl',
|
||||
settings.discordOnUploadAvatarUrl,
|
||||
'string',
|
||||
),
|
||||
discordOnUploadContent: replaceDatabaseValueWithEnv(
|
||||
'discordOnUploadContent',
|
||||
settings.discordOnUploadContent,
|
||||
'string',
|
||||
),
|
||||
discordOnUploadEmbed: replaceDatabaseValueWithEnv(
|
||||
'discordOnUploadEmbed',
|
||||
settings.discordOnUploadEmbed,
|
||||
'json[]',
|
||||
),
|
||||
discordOnShortenWebhookUrl: replaceDatabaseValueWithEnv(
|
||||
'discordOnShortenWebhookUrl',
|
||||
settings.discordOnShortenWebhookUrl,
|
||||
'string',
|
||||
),
|
||||
discordOnShortenUsername: replaceDatabaseValueWithEnv(
|
||||
'discordOnShortenUsername',
|
||||
settings.discordOnShortenUsername,
|
||||
'string',
|
||||
),
|
||||
discordOnShortenAvatarUrl: replaceDatabaseValueWithEnv(
|
||||
'discordOnShortenAvatarUrl',
|
||||
settings.discordOnShortenAvatarUrl,
|
||||
'string',
|
||||
),
|
||||
discordOnShortenContent: replaceDatabaseValueWithEnv(
|
||||
'discordOnShortenContent',
|
||||
settings.discordOnShortenContent,
|
||||
'string',
|
||||
),
|
||||
discordOnShortenEmbed: replaceDatabaseValueWithEnv(
|
||||
'discordOnShortenEmbed',
|
||||
settings.discordOnShortenEmbed,
|
||||
'json[]',
|
||||
),
|
||||
pwaEnabled: replaceDatabaseValueWithEnv('pwaEnabled', settings.pwaEnabled, 'boolean'),
|
||||
pwaTitle: replaceDatabaseValueWithEnv('pwaTitle', settings.pwaTitle, 'string'),
|
||||
pwaShortName: replaceDatabaseValueWithEnv('pwaShortName', settings.pwaShortName, 'string'),
|
||||
pwaDescription: replaceDatabaseValueWithEnv('pwaDescription', settings.pwaDescription, 'string'),
|
||||
pwaThemeColor: replaceDatabaseValueWithEnv('pwaThemeColor', settings.pwaThemeColor, 'string'),
|
||||
pwaBackgroundColor: replaceDatabaseValueWithEnv(
|
||||
'pwaBackgroundColor',
|
||||
settings.pwaBackgroundColor,
|
||||
'string',
|
||||
),
|
||||
};
|
||||
return settings;
|
||||
},
|
||||
async getSettingsRaw(...args: any[]) {
|
||||
return await prisma.zipline.findFirst(...args);
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
|
@ -20,7 +20,7 @@ export function formatFileName(nameFormat: Config['files']['defaultFormat'], ori
|
|||
return name;
|
||||
case 'random-words':
|
||||
case 'gfycat':
|
||||
return randomWords(config.files.randomWordsNumAdjectives, config.files.randomWordsSeperator);
|
||||
return randomWords(config.files.randomWordsNumAdjectives, config.files.randomWordsSeparator);
|
||||
default:
|
||||
return randomCharacters(config.files.length);
|
||||
}
|
||||
|
|
|
@ -28,13 +28,13 @@ function importWords(): {
|
|||
}
|
||||
}
|
||||
|
||||
export function randomWords(numAdjectives: number = 2, seperator: string = '-') {
|
||||
export function randomWords(numAdjectives: number = 2, separator: string = '-') {
|
||||
const { adjectives, animals } = importWords();
|
||||
|
||||
let words = '';
|
||||
|
||||
for (let i = 0; i !== numAdjectives; ++i) {
|
||||
words += adjectives[randomIndex(adjectives.length)] + seperator;
|
||||
words += adjectives[randomIndex(adjectives.length)] + separator;
|
||||
}
|
||||
|
||||
words += animals[randomIndex(animals.length)];
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { bytes } from '@/lib/bytes';
|
||||
import { reloadSettings } from '@/lib/config';
|
||||
import { readDatabaseSettings } from '@/lib/config/read';
|
||||
import { DATABASE_TO_PROP, readDatabaseSettings, valueIsFromEnv as valueFromEnv } from '@/lib/config/read';
|
||||
import { prisma } from '@/lib/db';
|
||||
import { log } from '@/lib/logger';
|
||||
import { readThemes } from '@/lib/theme/file';
|
||||
|
@ -15,7 +15,11 @@ import { z } from 'zod';
|
|||
|
||||
type Settings = Awaited<ReturnType<typeof readDatabaseSettings>>;
|
||||
|
||||
export type ApiServerSettingsResponse = Settings;
|
||||
type LockedSettings = {
|
||||
locked: any;
|
||||
};
|
||||
|
||||
export type ApiServerSettingsResponse = Settings & LockedSettings;
|
||||
type Body = Partial<Settings>;
|
||||
|
||||
const reservedRoutes = ['/dashboard', '/api', '/raw', '/robots.txt', '/manifest.json', '/favicon.ico'];
|
||||
|
@ -60,7 +64,7 @@ export default fastifyPlugin(
|
|||
preHandler: [userMiddleware, administratorMiddleware],
|
||||
},
|
||||
async (_, res) => {
|
||||
const settings = await prisma.zipline.findFirst({
|
||||
const settings = await prisma.zipline.getSettingsRaw({
|
||||
omit: {
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
|
@ -70,8 +74,20 @@ export default fastifyPlugin(
|
|||
});
|
||||
|
||||
if (!settings) return res.notFound('no settings table found');
|
||||
const settingsResponse: ApiServerSettingsResponse = {
|
||||
...settings,
|
||||
locked: {},
|
||||
};
|
||||
|
||||
return res.send(settings);
|
||||
for (const key in DATABASE_TO_PROP) {
|
||||
const val = valueFromEnv(key as keyof typeof DATABASE_TO_PROP);
|
||||
if (val === undefined) {
|
||||
continue;
|
||||
}
|
||||
settingsResponse.locked[key] = val;
|
||||
}
|
||||
|
||||
return res.send(settingsResponse);
|
||||
},
|
||||
);
|
||||
|
||||
|
@ -81,253 +97,11 @@ export default fastifyPlugin(
|
|||
preHandler: [userMiddleware, administratorMiddleware],
|
||||
},
|
||||
async (req, res) => {
|
||||
const settings = await prisma.zipline.findFirst();
|
||||
const settings = await prisma.zipline.getSettingsRaw();
|
||||
if (!settings) return res.notFound('no settings table found');
|
||||
|
||||
const themes = (await readThemes()).map((x) => x.id);
|
||||
const result: any = await parseSettings(req.body);
|
||||
|
||||
const settingsBodySchema = z
|
||||
.object({
|
||||
coreTempDirectory: z.string().refine((dir) => {
|
||||
try {
|
||||
return !dir || statSync(dir).isDirectory();
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}, 'Directory does not exist'),
|
||||
coreDefaultDomain: z
|
||||
.string()
|
||||
.nullable()
|
||||
.refine((value) => !value || /^[a-z0-9-.]+$/.test(value), 'Invalid domain format'),
|
||||
coreReturnHttpsUrls: z.boolean(),
|
||||
|
||||
chunksEnabled: z.boolean(),
|
||||
chunksMax: zBytes,
|
||||
chunksSize: zBytes,
|
||||
|
||||
tasksDeleteInterval: zMs,
|
||||
tasksClearInvitesInterval: zMs,
|
||||
tasksMaxViewsInterval: zMs,
|
||||
tasksThumbnailsInterval: zMs,
|
||||
tasksMetricsInterval: zMs,
|
||||
|
||||
filesRoute: z
|
||||
.string()
|
||||
.startsWith('/')
|
||||
.refine(
|
||||
(value) => !reservedRoutes.some((route) => value.startsWith(route)),
|
||||
'Provided route is reserved',
|
||||
),
|
||||
filesLength: z.number().min(1).max(64),
|
||||
filesDefaultFormat: z.enum(['random', 'date', 'uuid', 'name', 'gfycat']),
|
||||
filesDisabledExtensions: z
|
||||
.union([
|
||||
z.array(z.string().refine((s) => !s.startsWith('.'), 'extension can\'t include "."')),
|
||||
z.string(),
|
||||
])
|
||||
.transform((value) =>
|
||||
typeof value === 'string' ? value.split(',').map((ext) => ext.trim()) : value,
|
||||
),
|
||||
filesMaxFileSize: zBytes,
|
||||
|
||||
filesDefaultExpiration: zMs.nullable(),
|
||||
filesAssumeMimetypes: z.boolean(),
|
||||
filesDefaultDateFormat: z.string(),
|
||||
filesRemoveGpsMetadata: z.boolean(),
|
||||
filesRandomWordsNumAdjectives: z.number().min(1).max(20),
|
||||
filesRandomWordsSeparator: z.string(),
|
||||
|
||||
urlsRoute: z
|
||||
.string()
|
||||
.startsWith('/')
|
||||
.refine(
|
||||
(value) => !reservedRoutes.some((route) => value.startsWith(route)),
|
||||
'Provided route is reserved',
|
||||
),
|
||||
urlsLength: z.number().min(1).max(64),
|
||||
|
||||
featuresImageCompression: z.boolean(),
|
||||
featuresRobotsTxt: z.boolean(),
|
||||
featuresHealthcheck: z.boolean(),
|
||||
featuresUserRegistration: z.boolean(),
|
||||
featuresOauthRegistration: z.boolean(),
|
||||
featuresDeleteOnMaxViews: z.boolean(),
|
||||
|
||||
featuresThumbnailsEnabled: z.boolean(),
|
||||
featuresThumbnailsNumberThreads: z
|
||||
.number()
|
||||
.min(1)
|
||||
.max(
|
||||
cpus().length,
|
||||
'Number of threads must be less than or equal to the number of CPUs: ' + cpus().length,
|
||||
),
|
||||
|
||||
featuresMetricsEnabled: z.boolean(),
|
||||
featuresMetricsAdminOnly: z.boolean(),
|
||||
featuresMetricsShowUserSpecific: z.boolean(),
|
||||
|
||||
invitesEnabled: z.boolean(),
|
||||
invitesLength: z.number().min(1).max(64),
|
||||
|
||||
websiteTitle: z.string(),
|
||||
websiteTitleLogo: z.string().url().nullable(),
|
||||
websiteExternalLinks: z
|
||||
.union([
|
||||
z.array(
|
||||
z.object({
|
||||
name: z.string(),
|
||||
url: z.string().url(),
|
||||
}),
|
||||
),
|
||||
z.string(),
|
||||
])
|
||||
.transform((value) => (typeof value === 'string' ? JSON.parse(value) : value)),
|
||||
websiteLoginBackground: z.string().url().nullable(),
|
||||
websiteLoginBackgroundBlur: z.boolean(),
|
||||
websiteDefaultAvatar: z
|
||||
.string()
|
||||
.nullable()
|
||||
.transform((s) => (s ? resolve(s) : null))
|
||||
.refine((input) => {
|
||||
try {
|
||||
return !input || statSync(input).isFile();
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}, 'File does not exist'),
|
||||
websiteTos: z
|
||||
.string()
|
||||
.nullable()
|
||||
.refine((input) => !input || input.endsWith('.md'), 'File is not a markdown file')
|
||||
.refine((input) => {
|
||||
try {
|
||||
return !input || statSync(input).isFile();
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}, 'File does not exist'),
|
||||
|
||||
websiteThemeDefault: z.enum(['system', ...themes]),
|
||||
websiteThemeDark: z.enum(themes as unknown as readonly [string, ...string[]]),
|
||||
websiteThemeLight: z.enum(themes as unknown as readonly [string, ...string[]]),
|
||||
|
||||
oauthBypassLocalLogin: z.boolean(),
|
||||
oauthLoginOnly: z.boolean(),
|
||||
|
||||
oauthDiscordClientId: z.string().nullable(),
|
||||
oauthDiscordClientSecret: z.string().nullable(),
|
||||
oauthDiscordRedirectUri: z.string().url().endsWith('/api/auth/oauth/discord').nullable(),
|
||||
|
||||
oauthGoogleClientId: z.string().nullable(),
|
||||
oauthGoogleClientSecret: z.string().nullable(),
|
||||
oauthGoogleRedirectUri: z.string().url().endsWith('/api/auth/oauth/google').nullable(),
|
||||
|
||||
oauthGithubClientId: z.string().nullable(),
|
||||
oauthGithubClientSecret: z.string().nullable(),
|
||||
oauthGithubRedirectUri: z.string().url().endsWith('/api/auth/oauth/github').nullable(),
|
||||
|
||||
oauthOidcClientId: z.string().nullable(),
|
||||
oauthOidcClientSecret: z.string().nullable(),
|
||||
oauthOidcAuthorizeUrl: z.string().url().nullable(),
|
||||
oauthOidcTokenUrl: z.string().url().nullable(),
|
||||
oauthOidcUserinfoUrl: z.string().url().nullable(),
|
||||
oauthOidcRedirectUri: z.string().url().endsWith('/api/auth/oauth/oidc').nullable(),
|
||||
|
||||
mfaTotpEnabled: z.boolean(),
|
||||
mfaTotpIssuer: z.string(),
|
||||
mfaPasskeys: z.boolean(),
|
||||
|
||||
ratelimitEnabled: z.boolean(),
|
||||
ratelimitMax: z.number().refine((value) => value > 0, 'Value must be greater than 0'),
|
||||
ratelimitWindow: z.number().nullable(),
|
||||
ratelimitAdminBypass: z.boolean(),
|
||||
ratelimitAllowList: z
|
||||
.union([z.array(z.string()), z.string()])
|
||||
.transform((value) => (typeof value === 'string' ? value.split(',') : value)),
|
||||
|
||||
httpWebhookOnUpload: z.string().url().nullable(),
|
||||
httpWebhookOnShorten: z.string().url().nullable(),
|
||||
|
||||
discordWebhookUrl: z.string().url().nullable(),
|
||||
discordUsername: z.string().nullable(),
|
||||
discordAvatarUrl: z.string().url().nullable(),
|
||||
|
||||
discordOnUploadWebhookUrl: z.string().url().nullable(),
|
||||
discordOnUploadUsername: z.string().nullable(),
|
||||
discordOnUploadAvatarUrl: z.string().url().nullable(),
|
||||
discordOnUploadContent: z.string().nullable(),
|
||||
discordOnUploadEmbed: discordEmbed,
|
||||
|
||||
discordOnShortenWebhookUrl: z.string().url().nullable(),
|
||||
discordOnShortenUsername: z.string().nullable(),
|
||||
discordOnShortenAvatarUrl: z.string().url().nullable(),
|
||||
discordOnShortenContent: z.string().nullable(),
|
||||
discordOnShortenEmbed: discordEmbed,
|
||||
|
||||
pwaEnabled: z.boolean(),
|
||||
pwaTitle: z.string(),
|
||||
pwaShortName: z.string(),
|
||||
pwaDescription: z.string(),
|
||||
pwaThemeColor: z.string().regex(/^#?([a-f0-9]{6}|[a-f0-9]{3})$/),
|
||||
pwaBackgroundColor: z.string().regex(/^#?([a-f0-9]{6}|[a-f0-9]{3})/),
|
||||
})
|
||||
.partial()
|
||||
.refine(
|
||||
(data) =>
|
||||
(!data.oauthDiscordClientId || data.oauthDiscordClientSecret) &&
|
||||
(!data.oauthDiscordClientSecret || data.oauthDiscordClientId),
|
||||
{
|
||||
message: 'discord oauth fields are incomplete',
|
||||
path: ['oauthDiscordClientId', 'oauthDiscordClientSecret'],
|
||||
},
|
||||
)
|
||||
.refine(
|
||||
(data) =>
|
||||
(!data.oauthGoogleClientId || data.oauthGoogleClientSecret) &&
|
||||
(!data.oauthGoogleClientSecret || data.oauthGoogleClientId),
|
||||
{
|
||||
message: 'google oauth fields are incomplete',
|
||||
path: ['oauthGoogleClientId', 'oauthGoogleClientSecret'],
|
||||
},
|
||||
)
|
||||
.refine(
|
||||
(data) =>
|
||||
(!data.oauthGithubClientId || data.oauthGithubClientSecret) &&
|
||||
(!data.oauthGithubClientSecret || data.oauthGithubClientId),
|
||||
{
|
||||
message: 'github oauth fields are incomplete',
|
||||
path: ['oauthGithubClientId', 'oauthGithubClientSecret'],
|
||||
},
|
||||
)
|
||||
.refine(
|
||||
(data) =>
|
||||
(!data.oauthOidcClientId &&
|
||||
!data.oauthOidcClientSecret &&
|
||||
!data.oauthOidcAuthorizeUrl &&
|
||||
!data.oauthOidcTokenUrl &&
|
||||
!data.oauthOidcUserinfoUrl) ||
|
||||
(data.oauthOidcClientId &&
|
||||
data.oauthOidcClientSecret &&
|
||||
data.oauthOidcAuthorizeUrl &&
|
||||
data.oauthOidcTokenUrl &&
|
||||
data.oauthOidcUserinfoUrl),
|
||||
{
|
||||
message: 'oidc oauth fields are incomplete',
|
||||
path: [
|
||||
'oauthOidcClientId',
|
||||
'oauthOidcClientSecret',
|
||||
'oauthOidcAuthorizeUrl',
|
||||
'oauthOidcTokenUrl',
|
||||
'oauthOidcUserinfoUrl',
|
||||
],
|
||||
},
|
||||
)
|
||||
.refine((data) => !data.ratelimitWindow || (data.ratelimitMax && data.ratelimitMax > 0), {
|
||||
message: 'ratelimitMax must be set if ratelimitWindow is set',
|
||||
path: ['ratelimitMax'],
|
||||
});
|
||||
|
||||
const result = settingsBodySchema.safeParse(req.body);
|
||||
if (!result.success) {
|
||||
logger.warn('invalid settings update', {
|
||||
issues: result.error.issues,
|
||||
|
@ -362,7 +136,22 @@ export default fastifyPlugin(
|
|||
by: req.user.username,
|
||||
});
|
||||
|
||||
return res.send(newSettings);
|
||||
const settingsResponse: ApiServerSettingsResponse = {
|
||||
...newSettings,
|
||||
locked: {},
|
||||
};
|
||||
|
||||
for (const key in DATABASE_TO_PROP) {
|
||||
if (DATABASE_TO_PROP.hasOwnProperty(key)) {
|
||||
const val = valueFromEnv(key as keyof typeof DATABASE_TO_PROP);
|
||||
if (val === undefined) {
|
||||
continue;
|
||||
}
|
||||
settingsResponse.locked[key] = val;
|
||||
}
|
||||
}
|
||||
|
||||
return res.send(settingsResponse);
|
||||
},
|
||||
);
|
||||
|
||||
|
@ -370,3 +159,251 @@ export default fastifyPlugin(
|
|||
},
|
||||
{ name: PATH },
|
||||
);
|
||||
|
||||
export async function parseSettings(body: object) {
|
||||
const themes = (await readThemes()).map((x) => x.id);
|
||||
const settingsBodySchema = z
|
||||
.object({
|
||||
coreTempDirectory: z.string().refine((dir) => {
|
||||
try {
|
||||
return !dir || statSync(dir).isDirectory();
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}, 'Directory does not exist'),
|
||||
coreDefaultDomain: z
|
||||
.string()
|
||||
.nullable()
|
||||
.refine((value) => !value || /^[a-z0-9-.]+$/.test(value), 'Invalid domain format'),
|
||||
coreReturnHttpsUrls: z.boolean(),
|
||||
|
||||
chunksEnabled: z.boolean(),
|
||||
chunksMax: zBytes,
|
||||
chunksSize: zBytes,
|
||||
|
||||
tasksDeleteInterval: zMs,
|
||||
tasksClearInvitesInterval: zMs,
|
||||
tasksMaxViewsInterval: zMs,
|
||||
tasksThumbnailsInterval: zMs,
|
||||
tasksMetricsInterval: zMs,
|
||||
|
||||
filesRoute: z
|
||||
.string()
|
||||
.startsWith('/')
|
||||
.refine(
|
||||
(value) => !reservedRoutes.some((route) => value.startsWith(route)),
|
||||
'Provided route is reserved',
|
||||
),
|
||||
filesLength: z.number().min(1).max(64),
|
||||
filesDefaultFormat: z.enum(['random', 'date', 'uuid', 'name', 'gfycat']),
|
||||
filesDisabledExtensions: z
|
||||
.union([
|
||||
z.array(z.string().refine((s) => !s.startsWith('.'), 'extension can\'t include "."')),
|
||||
z.string(),
|
||||
])
|
||||
.transform((value) =>
|
||||
typeof value === 'string' ? value.split(',').map((ext) => ext.trim()) : value,
|
||||
),
|
||||
filesMaxFileSize: zBytes,
|
||||
|
||||
filesDefaultExpiration: zMs.nullable(),
|
||||
filesAssumeMimetypes: z.boolean(),
|
||||
filesDefaultDateFormat: z.string(),
|
||||
filesRemoveGpsMetadata: z.boolean(),
|
||||
filesRandomWordsNumAdjectives: z.number().min(1).max(20),
|
||||
filesRandomWordsSeparator: z.string(),
|
||||
|
||||
urlsRoute: z
|
||||
.string()
|
||||
.startsWith('/')
|
||||
.refine(
|
||||
(value) => !reservedRoutes.some((route) => value.startsWith(route)),
|
||||
'Provided route is reserved',
|
||||
),
|
||||
urlsLength: z.number().min(1).max(64),
|
||||
|
||||
featuresImageCompression: z.boolean(),
|
||||
featuresRobotsTxt: z.boolean(),
|
||||
featuresHealthcheck: z.boolean(),
|
||||
featuresUserRegistration: z.boolean(),
|
||||
featuresOauthRegistration: z.boolean(),
|
||||
featuresDeleteOnMaxViews: z.boolean(),
|
||||
|
||||
featuresThumbnailsEnabled: z.boolean(),
|
||||
featuresThumbnailsNumberThreads: z
|
||||
.number()
|
||||
.min(1)
|
||||
.max(
|
||||
cpus().length,
|
||||
'Number of threads must be less than or equal to the number of CPUs: ' + cpus().length,
|
||||
),
|
||||
|
||||
featuresMetricsEnabled: z.boolean(),
|
||||
featuresMetricsAdminOnly: z.boolean(),
|
||||
featuresMetricsShowUserSpecific: z.boolean(),
|
||||
|
||||
invitesEnabled: z.boolean(),
|
||||
invitesLength: z.number().min(1).max(64),
|
||||
|
||||
websiteTitle: z.string(),
|
||||
websiteTitleLogo: z.string().url().nullable(),
|
||||
websiteExternalLinks: z
|
||||
.union([
|
||||
z.array(
|
||||
z.object({
|
||||
name: z.string(),
|
||||
url: z.string().url(),
|
||||
}),
|
||||
),
|
||||
z.string(),
|
||||
])
|
||||
.transform((value) => (typeof value === 'string' ? JSON.parse(value) : value)),
|
||||
websiteLoginBackground: z.string().url().nullable(),
|
||||
websiteLoginBackgroundBlur: z.boolean(),
|
||||
websiteDefaultAvatar: z
|
||||
.string()
|
||||
.nullable()
|
||||
.transform((s) => (s ? resolve(s) : null))
|
||||
.refine((input) => {
|
||||
try {
|
||||
return !input || statSync(input).isFile();
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}, 'File does not exist'),
|
||||
websiteTos: z
|
||||
.string()
|
||||
.nullable()
|
||||
.refine((input) => !input || input.endsWith('.md'), 'File is not a markdown file')
|
||||
.refine((input) => {
|
||||
try {
|
||||
return !input || statSync(input).isFile();
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}, 'File does not exist'),
|
||||
|
||||
websiteThemeDefault: z.enum(['system', ...themes]),
|
||||
websiteThemeDark: z.enum(themes as unknown as readonly [string, ...string[]]),
|
||||
websiteThemeLight: z.enum(themes as unknown as readonly [string, ...string[]]),
|
||||
|
||||
oauthBypassLocalLogin: z.boolean(),
|
||||
oauthLoginOnly: z.boolean(),
|
||||
|
||||
oauthDiscordClientId: z.string().nullable(),
|
||||
oauthDiscordClientSecret: z.string().nullable(),
|
||||
oauthDiscordRedirectUri: z.string().url().endsWith('/api/auth/oauth/discord').nullable(),
|
||||
|
||||
oauthGoogleClientId: z.string().nullable(),
|
||||
oauthGoogleClientSecret: z.string().nullable(),
|
||||
oauthGoogleRedirectUri: z.string().url().endsWith('/api/auth/oauth/google').nullable(),
|
||||
|
||||
oauthGithubClientId: z.string().nullable(),
|
||||
oauthGithubClientSecret: z.string().nullable(),
|
||||
oauthGithubRedirectUri: z.string().url().endsWith('/api/auth/oauth/github').nullable(),
|
||||
|
||||
oauthOidcClientId: z.string().nullable(),
|
||||
oauthOidcClientSecret: z.string().nullable(),
|
||||
oauthOidcAuthorizeUrl: z.string().url().nullable(),
|
||||
oauthOidcTokenUrl: z.string().url().nullable(),
|
||||
oauthOidcUserinfoUrl: z.string().url().nullable(),
|
||||
oauthOidcRedirectUri: z.string().url().endsWith('/api/auth/oauth/oidc').nullable(),
|
||||
|
||||
mfaTotpEnabled: z.boolean(),
|
||||
mfaTotpIssuer: z.string(),
|
||||
mfaPasskeys: z.boolean(),
|
||||
|
||||
ratelimitEnabled: z.boolean(),
|
||||
ratelimitMax: z.number().refine((value) => value > 0, 'Value must be greater than 0'),
|
||||
ratelimitWindow: z.number().nullable(),
|
||||
ratelimitAdminBypass: z.boolean(),
|
||||
ratelimitAllowList: z
|
||||
.union([z.array(z.string()), z.string()])
|
||||
.transform((value) => (typeof value === 'string' ? value.split(',') : value)),
|
||||
|
||||
httpWebhookOnUpload: z.string().url().nullable(),
|
||||
httpWebhookOnShorten: z.string().url().nullable(),
|
||||
|
||||
discordWebhookUrl: z.string().url().nullable(),
|
||||
discordUsername: z.string().nullable(),
|
||||
discordAvatarUrl: z.string().url().nullable(),
|
||||
|
||||
discordOnUploadWebhookUrl: z.string().url().nullable(),
|
||||
discordOnUploadUsername: z.string().nullable(),
|
||||
discordOnUploadAvatarUrl: z.string().url().nullable(),
|
||||
discordOnUploadContent: z.string().nullable(),
|
||||
discordOnUploadEmbed: discordEmbed,
|
||||
|
||||
discordOnShortenWebhookUrl: z.string().url().nullable(),
|
||||
discordOnShortenUsername: z.string().nullable(),
|
||||
discordOnShortenAvatarUrl: z.string().url().nullable(),
|
||||
discordOnShortenContent: z.string().nullable(),
|
||||
discordOnShortenEmbed: discordEmbed,
|
||||
|
||||
pwaEnabled: z.boolean(),
|
||||
pwaTitle: z.string(),
|
||||
pwaShortName: z.string(),
|
||||
pwaDescription: z.string(),
|
||||
pwaThemeColor: z.string().regex(/^#?([a-f0-9]{6}|[a-f0-9]{3})$/),
|
||||
pwaBackgroundColor: z.string().regex(/^#?([a-f0-9]{6}|[a-f0-9]{3})/),
|
||||
})
|
||||
.partial()
|
||||
.refine(
|
||||
(data) =>
|
||||
(!data.oauthDiscordClientId || data.oauthDiscordClientSecret) &&
|
||||
(!data.oauthDiscordClientSecret || data.oauthDiscordClientId),
|
||||
{
|
||||
message: 'discord oauth fields are incomplete',
|
||||
path: ['oauthDiscordClientId', 'oauthDiscordClientSecret'],
|
||||
},
|
||||
)
|
||||
.refine(
|
||||
(data) =>
|
||||
(!data.oauthGoogleClientId || data.oauthGoogleClientSecret) &&
|
||||
(!data.oauthGoogleClientSecret || data.oauthGoogleClientId),
|
||||
{
|
||||
message: 'google oauth fields are incomplete',
|
||||
path: ['oauthGoogleClientId', 'oauthGoogleClientSecret'],
|
||||
},
|
||||
)
|
||||
.refine(
|
||||
(data) =>
|
||||
(!data.oauthGithubClientId || data.oauthGithubClientSecret) &&
|
||||
(!data.oauthGithubClientSecret || data.oauthGithubClientId),
|
||||
{
|
||||
message: 'github oauth fields are incomplete',
|
||||
path: ['oauthGithubClientId', 'oauthGithubClientSecret'],
|
||||
},
|
||||
)
|
||||
.refine(
|
||||
(data) =>
|
||||
(!data.oauthOidcClientId &&
|
||||
!data.oauthOidcClientSecret &&
|
||||
!data.oauthOidcAuthorizeUrl &&
|
||||
!data.oauthOidcTokenUrl &&
|
||||
!data.oauthOidcUserinfoUrl) ||
|
||||
(data.oauthOidcClientId &&
|
||||
data.oauthOidcClientSecret &&
|
||||
data.oauthOidcAuthorizeUrl &&
|
||||
data.oauthOidcTokenUrl &&
|
||||
data.oauthOidcUserinfoUrl),
|
||||
{
|
||||
message: 'oidc oauth fields are incomplete',
|
||||
path: [
|
||||
'oauthOidcClientId',
|
||||
'oauthOidcClientSecret',
|
||||
'oauthOidcAuthorizeUrl',
|
||||
'oauthOidcTokenUrl',
|
||||
'oauthOidcUserinfoUrl',
|
||||
],
|
||||
},
|
||||
)
|
||||
.refine((data) => !data.ratelimitWindow || (data.ratelimitMax && data.ratelimitMax > 0), {
|
||||
message: 'ratelimitMax must be set if ratelimitWindow is set',
|
||||
path: ['ratelimitMax'],
|
||||
});
|
||||
|
||||
const result = settingsBodySchema.safeParse(body);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue