mirror of
https://github.com/diced/zipline.git
synced 2025-12-05 20:40:12 -08:00
feat: predefined domains (#822)
* feat(domains): add domains to server settings * fix(domains): fix linting errors * fix(domains): remove unused imports * fix(urls): fix typo * feat(domains): remove expiration date from domains * feat(domains): changed domains from JSONB to TEXT[] * fix(domains): linter errors --------- Co-authored-by: dicedtomato <35403473+diced@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "Zipline" ADD COLUMN "domains" TEXT[] DEFAULT ARRAY[]::TEXT[];
|
||||
@@ -135,6 +135,8 @@ model Zipline {
|
||||
pwaDescription String @default("Zipline")
|
||||
pwaThemeColor String @default("#000000")
|
||||
pwaBackgroundColor String @default("#000000")
|
||||
|
||||
domains String[] @default([])
|
||||
}
|
||||
|
||||
model User {
|
||||
|
||||
@@ -3,6 +3,7 @@ import { Alert, Anchor, Collapse, Group, SimpleGrid, Skeleton, Stack, Title } fr
|
||||
import useSWR from 'swr';
|
||||
import dynamic from 'next/dynamic';
|
||||
import { useDisclosure } from '@mantine/hooks';
|
||||
import Domains from './parts/Domains';
|
||||
|
||||
function SettingsSkeleton() {
|
||||
return <Skeleton height={280} animate />;
|
||||
@@ -105,6 +106,8 @@ export default function DashboardSettings() {
|
||||
<PWA swr={{ data, isLoading }} />
|
||||
|
||||
<HttpWebhook swr={{ data, isLoading }} />
|
||||
|
||||
<Domains swr={{ data, isLoading }} />
|
||||
</>
|
||||
)}
|
||||
</SimpleGrid>
|
||||
|
||||
94
src/components/pages/serverSettings/parts/Domains.tsx
Normal file
94
src/components/pages/serverSettings/parts/Domains.tsx
Normal file
@@ -0,0 +1,94 @@
|
||||
import { Response } from '@/lib/api/response';
|
||||
import { Button, Group, LoadingOverlay, Paper, SimpleGrid, TextInput, Title } from '@mantine/core';
|
||||
import { useForm } from '@mantine/form';
|
||||
import { IconPlus, IconTrash } from '@tabler/icons-react';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { settingsOnSubmit } from '../settingsOnSubmit';
|
||||
|
||||
export default function Domains({
|
||||
swr: { data, isLoading },
|
||||
}: {
|
||||
swr: { data: Response['/api/server/settings'] | undefined; isLoading: boolean };
|
||||
}) {
|
||||
const router = useRouter();
|
||||
const [domains, setDomains] = useState<string[]>([]);
|
||||
const form = useForm({
|
||||
initialValues: {
|
||||
newDomain: '',
|
||||
},
|
||||
});
|
||||
|
||||
const onSubmit = settingsOnSubmit(router, form);
|
||||
|
||||
useEffect(() => {
|
||||
if (!data) return;
|
||||
const domainsData = Array.isArray(data.settings.domains)
|
||||
? data.settings.domains.map((d) => String(d))
|
||||
: [];
|
||||
setDomains(domainsData);
|
||||
}, [data]);
|
||||
|
||||
const addDomain = () => {
|
||||
const { newDomain } = form.values;
|
||||
if (!newDomain) return;
|
||||
|
||||
const updatedDomains = [...domains, newDomain.trim()];
|
||||
setDomains(updatedDomains);
|
||||
form.setValues({ newDomain: '' });
|
||||
onSubmit({ domains: updatedDomains });
|
||||
};
|
||||
|
||||
const removeDomain = (index: number) => {
|
||||
const updatedDomains = domains.filter((_, i) => i !== index);
|
||||
setDomains(updatedDomains);
|
||||
onSubmit({ domains: updatedDomains });
|
||||
};
|
||||
|
||||
return (
|
||||
<Paper withBorder p='sm' pos='relative'>
|
||||
<LoadingOverlay visible={isLoading} />
|
||||
|
||||
<Title order={2}>Domains</Title>
|
||||
|
||||
<Group mt='md' align='flex-end'>
|
||||
<TextInput
|
||||
label='Domain'
|
||||
description='Enter a domain name (e.g. example.com)'
|
||||
placeholder='example.com'
|
||||
{...form.getInputProps('newDomain')}
|
||||
/>
|
||||
<Button onClick={addDomain} leftSection={<IconPlus size='1rem' />}>
|
||||
Add Domain
|
||||
</Button>
|
||||
</Group>
|
||||
|
||||
<SimpleGrid mt='md' cols={{ base: 1, sm: 2, md: 3 }} spacing='xs'>
|
||||
{domains.map((domain, index) => (
|
||||
<Paper key={index} withBorder p='xs'>
|
||||
<Group justify='space-between'>
|
||||
<div>
|
||||
<strong>{domain}</strong>
|
||||
</div>
|
||||
<Button
|
||||
variant='subtle'
|
||||
color='red'
|
||||
size='xs'
|
||||
onClick={() => removeDomain(index)}
|
||||
px={8}
|
||||
style={{
|
||||
aspectRatio: '1/1',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
>
|
||||
<IconTrash size='1rem' />
|
||||
</Button>
|
||||
</Group>
|
||||
</Paper>
|
||||
))}
|
||||
</SimpleGrid>
|
||||
</Paper>
|
||||
);
|
||||
}
|
||||
@@ -11,7 +11,6 @@ import {
|
||||
Stack,
|
||||
Switch,
|
||||
Text,
|
||||
TextInput,
|
||||
} from '@mantine/core';
|
||||
import { IconDownload, IconEyeFilled, IconGlobe, IconPercentage, IconWriting } from '@tabler/icons-react';
|
||||
import Link from 'next/link';
|
||||
@@ -105,10 +104,22 @@ export default function GeneratorButton({
|
||||
);
|
||||
|
||||
const { data: tokenData, isLoading, error } = useSWR<Response['/api/user/token']>('/api/user/token');
|
||||
const { data: settingsData } = useSWR<Response['/api/server/settings']>('/api/server/settings');
|
||||
|
||||
const isUnixLike = name === 'Flameshot' || name === 'Shell Script';
|
||||
const onlyFile = generatorType === 'file';
|
||||
|
||||
const domains = Array.isArray(settingsData?.settings.domains)
|
||||
? settingsData?.settings.domains.map((d) => String(d))
|
||||
: [];
|
||||
const domainOptions = [
|
||||
{ value: '', label: 'Default Domain' },
|
||||
...domains.map((domain) => ({
|
||||
value: domain,
|
||||
label: domain,
|
||||
})),
|
||||
] as { value: string; label: string; disabled?: boolean }[];
|
||||
|
||||
return (
|
||||
<>
|
||||
<Modal opened={opened} onClose={() => setOpen(false)} title={`Generate ${name} Uploader`}>
|
||||
@@ -187,14 +198,21 @@ export default function GeneratorButton({
|
||||
onChange={(value) => setOption({ maxViews: value === '' ? null : Number(value) })}
|
||||
/>
|
||||
|
||||
<TextInput
|
||||
<Select
|
||||
data={domainOptions}
|
||||
label='Override Domain'
|
||||
description='Override the domain with this value. This will change the domain returned in your uploads. Leave blank to use the default domain.'
|
||||
leftSection={<IconGlobe size='1rem' />}
|
||||
value={options.overrides_returnDomain ?? ''}
|
||||
onChange={(event) =>
|
||||
setOption({ overrides_returnDomain: event.currentTarget.value.trim() || null })
|
||||
}
|
||||
onChange={(value) => setOption({ overrides_returnDomain: value || null })}
|
||||
comboboxProps={{
|
||||
withinPortal: true,
|
||||
portalProps: {
|
||||
style: {
|
||||
zIndex: 100000000,
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
<Text c='dimmed' size='sm'>
|
||||
|
||||
@@ -62,9 +62,20 @@ export default function UploadOptionsButton({ folder, numFiles }: { folder?: str
|
||||
const { data: folders } = useSWR<Extract<Response['/api/user/folders'], Folder[]>>(
|
||||
'/api/user/folders?noincl=true',
|
||||
);
|
||||
const { data: settingsData } = useSWR<Response['/api/server/settings']>('/api/server/settings');
|
||||
|
||||
const combobox = useCombobox();
|
||||
const [folderSearch, setFolderSearch] = useState('');
|
||||
|
||||
const domains = Array.isArray(settingsData?.settings.domains) ? settingsData.settings.domains : [];
|
||||
const domainOptions = [
|
||||
{ value: '', label: 'Default Domain' },
|
||||
...domains.map((domain) => ({
|
||||
value: domain,
|
||||
label: domain,
|
||||
})),
|
||||
];
|
||||
|
||||
useEffect(() => {
|
||||
if (folder) return;
|
||||
|
||||
@@ -264,9 +275,7 @@ export default function UploadOptionsButton({ folder, numFiles }: { folder?: str
|
||||
|
||||
<Combobox.Dropdown>
|
||||
<Combobox.Options>
|
||||
<Combobox.Option defaultChecked={true} value='no folder'>
|
||||
No Folder
|
||||
</Combobox.Option>
|
||||
<Combobox.Option value='no folder'>No Folder</Combobox.Option>
|
||||
|
||||
{folders
|
||||
?.filter((f) => f.name.toLowerCase().includes(folderSearch.toLowerCase().trim()))
|
||||
@@ -279,7 +288,8 @@ export default function UploadOptionsButton({ folder, numFiles }: { folder?: str
|
||||
</Combobox.Dropdown>
|
||||
</Combobox>
|
||||
|
||||
<TextInput
|
||||
<Select
|
||||
data={domainOptions}
|
||||
label={
|
||||
<>
|
||||
Override Domain{' '}
|
||||
@@ -293,12 +303,15 @@ export default function UploadOptionsButton({ folder, numFiles }: { folder?: str
|
||||
description='Override the domain with this value. This will change the domain returned in your uploads. Leave blank to use the default domain.'
|
||||
leftSection={<IconGlobe size='1rem' />}
|
||||
value={options.overrides_returnDomain ?? ''}
|
||||
onChange={(event) =>
|
||||
setOption(
|
||||
'overrides_returnDomain',
|
||||
event.currentTarget.value.trim() === '' ? null : event.currentTarget.value.trim(),
|
||||
)
|
||||
}
|
||||
onChange={(value) => setOption('overrides_returnDomain', value || null)}
|
||||
comboboxProps={{
|
||||
withinPortal: true,
|
||||
portalProps: {
|
||||
style: {
|
||||
zIndex: 100000000,
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
<TextInput
|
||||
|
||||
@@ -51,6 +51,7 @@ export const DATABASE_TO_PROP = {
|
||||
|
||||
invitesEnabled: 'invites.enabled',
|
||||
invitesLength: 'invites.length',
|
||||
domains: 'domains',
|
||||
|
||||
websiteTitle: 'website.title',
|
||||
websiteTitleLogo: 'website.titleLogo',
|
||||
|
||||
@@ -295,6 +295,11 @@ export default fastifyPlugin(
|
||||
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})/),
|
||||
|
||||
domains: z.union([
|
||||
z.array(z.string()),
|
||||
z.string().transform((value) => value.split(',').map((s) => s.trim())),
|
||||
]),
|
||||
})
|
||||
.partial()
|
||||
.refine(
|
||||
|
||||
@@ -59,7 +59,7 @@ export default fastifyPlugin(
|
||||
});
|
||||
if (req.user.quota && req.user.quota.maxUrls && countUrls + 1 > req.user.quota.maxUrls)
|
||||
return res.forbidden(
|
||||
`shortenning this url would exceed your quota of ${req.user.quota.maxUrls} urls`,
|
||||
`Shortening this URL would exceed your quota of ${req.user.quota.maxUrls} URLs.`,
|
||||
);
|
||||
|
||||
let maxViews: number | undefined;
|
||||
|
||||
Reference in New Issue
Block a user