mirror of
https://github.com/diced/zipline.git
synced 2025-12-12 07:40:45 -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")
|
pwaDescription String @default("Zipline")
|
||||||
pwaThemeColor String @default("#000000")
|
pwaThemeColor String @default("#000000")
|
||||||
pwaBackgroundColor String @default("#000000")
|
pwaBackgroundColor String @default("#000000")
|
||||||
|
|
||||||
|
domains String[] @default([])
|
||||||
}
|
}
|
||||||
|
|
||||||
model User {
|
model User {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { Alert, Anchor, Collapse, Group, SimpleGrid, Skeleton, Stack, Title } fr
|
|||||||
import useSWR from 'swr';
|
import useSWR from 'swr';
|
||||||
import dynamic from 'next/dynamic';
|
import dynamic from 'next/dynamic';
|
||||||
import { useDisclosure } from '@mantine/hooks';
|
import { useDisclosure } from '@mantine/hooks';
|
||||||
|
import Domains from './parts/Domains';
|
||||||
|
|
||||||
function SettingsSkeleton() {
|
function SettingsSkeleton() {
|
||||||
return <Skeleton height={280} animate />;
|
return <Skeleton height={280} animate />;
|
||||||
@@ -105,6 +106,8 @@ export default function DashboardSettings() {
|
|||||||
<PWA swr={{ data, isLoading }} />
|
<PWA swr={{ data, isLoading }} />
|
||||||
|
|
||||||
<HttpWebhook swr={{ data, isLoading }} />
|
<HttpWebhook swr={{ data, isLoading }} />
|
||||||
|
|
||||||
|
<Domains swr={{ data, isLoading }} />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</SimpleGrid>
|
</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,
|
Stack,
|
||||||
Switch,
|
Switch,
|
||||||
Text,
|
Text,
|
||||||
TextInput,
|
|
||||||
} from '@mantine/core';
|
} from '@mantine/core';
|
||||||
import { IconDownload, IconEyeFilled, IconGlobe, IconPercentage, IconWriting } from '@tabler/icons-react';
|
import { IconDownload, IconEyeFilled, IconGlobe, IconPercentage, IconWriting } from '@tabler/icons-react';
|
||||||
import Link from 'next/link';
|
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: 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 isUnixLike = name === 'Flameshot' || name === 'Shell Script';
|
||||||
const onlyFile = generatorType === 'file';
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<Modal opened={opened} onClose={() => setOpen(false)} title={`Generate ${name} Uploader`}>
|
<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) })}
|
onChange={(value) => setOption({ maxViews: value === '' ? null : Number(value) })}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<TextInput
|
<Select
|
||||||
|
data={domainOptions}
|
||||||
label='Override Domain'
|
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.'
|
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' />}
|
leftSection={<IconGlobe size='1rem' />}
|
||||||
value={options.overrides_returnDomain ?? ''}
|
value={options.overrides_returnDomain ?? ''}
|
||||||
onChange={(event) =>
|
onChange={(value) => setOption({ overrides_returnDomain: value || null })}
|
||||||
setOption({ overrides_returnDomain: event.currentTarget.value.trim() || null })
|
comboboxProps={{
|
||||||
}
|
withinPortal: true,
|
||||||
|
portalProps: {
|
||||||
|
style: {
|
||||||
|
zIndex: 100000000,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Text c='dimmed' size='sm'>
|
<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[]>>(
|
const { data: folders } = useSWR<Extract<Response['/api/user/folders'], Folder[]>>(
|
||||||
'/api/user/folders?noincl=true',
|
'/api/user/folders?noincl=true',
|
||||||
);
|
);
|
||||||
|
const { data: settingsData } = useSWR<Response['/api/server/settings']>('/api/server/settings');
|
||||||
|
|
||||||
const combobox = useCombobox();
|
const combobox = useCombobox();
|
||||||
const [folderSearch, setFolderSearch] = useState('');
|
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(() => {
|
useEffect(() => {
|
||||||
if (folder) return;
|
if (folder) return;
|
||||||
|
|
||||||
@@ -264,9 +275,7 @@ export default function UploadOptionsButton({ folder, numFiles }: { folder?: str
|
|||||||
|
|
||||||
<Combobox.Dropdown>
|
<Combobox.Dropdown>
|
||||||
<Combobox.Options>
|
<Combobox.Options>
|
||||||
<Combobox.Option defaultChecked={true} value='no folder'>
|
<Combobox.Option value='no folder'>No Folder</Combobox.Option>
|
||||||
No Folder
|
|
||||||
</Combobox.Option>
|
|
||||||
|
|
||||||
{folders
|
{folders
|
||||||
?.filter((f) => f.name.toLowerCase().includes(folderSearch.toLowerCase().trim()))
|
?.filter((f) => f.name.toLowerCase().includes(folderSearch.toLowerCase().trim()))
|
||||||
@@ -279,7 +288,8 @@ export default function UploadOptionsButton({ folder, numFiles }: { folder?: str
|
|||||||
</Combobox.Dropdown>
|
</Combobox.Dropdown>
|
||||||
</Combobox>
|
</Combobox>
|
||||||
|
|
||||||
<TextInput
|
<Select
|
||||||
|
data={domainOptions}
|
||||||
label={
|
label={
|
||||||
<>
|
<>
|
||||||
Override Domain{' '}
|
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.'
|
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' />}
|
leftSection={<IconGlobe size='1rem' />}
|
||||||
value={options.overrides_returnDomain ?? ''}
|
value={options.overrides_returnDomain ?? ''}
|
||||||
onChange={(event) =>
|
onChange={(value) => setOption('overrides_returnDomain', value || null)}
|
||||||
setOption(
|
comboboxProps={{
|
||||||
'overrides_returnDomain',
|
withinPortal: true,
|
||||||
event.currentTarget.value.trim() === '' ? null : event.currentTarget.value.trim(),
|
portalProps: {
|
||||||
)
|
style: {
|
||||||
}
|
zIndex: 100000000,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<TextInput
|
<TextInput
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ export const DATABASE_TO_PROP = {
|
|||||||
|
|
||||||
invitesEnabled: 'invites.enabled',
|
invitesEnabled: 'invites.enabled',
|
||||||
invitesLength: 'invites.length',
|
invitesLength: 'invites.length',
|
||||||
|
domains: 'domains',
|
||||||
|
|
||||||
websiteTitle: 'website.title',
|
websiteTitle: 'website.title',
|
||||||
websiteTitleLogo: 'website.titleLogo',
|
websiteTitleLogo: 'website.titleLogo',
|
||||||
|
|||||||
@@ -295,6 +295,11 @@ export default fastifyPlugin(
|
|||||||
pwaDescription: z.string(),
|
pwaDescription: z.string(),
|
||||||
pwaThemeColor: z.string().regex(/^#?([a-f0-9]{6}|[a-f0-9]{3})$/),
|
pwaThemeColor: z.string().regex(/^#?([a-f0-9]{6}|[a-f0-9]{3})$/),
|
||||||
pwaBackgroundColor: 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()
|
.partial()
|
||||||
.refine(
|
.refine(
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ export default fastifyPlugin(
|
|||||||
});
|
});
|
||||||
if (req.user.quota && req.user.quota.maxUrls && countUrls + 1 > req.user.quota.maxUrls)
|
if (req.user.quota && req.user.quota.maxUrls && countUrls + 1 > req.user.quota.maxUrls)
|
||||||
return res.forbidden(
|
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;
|
let maxViews: number | undefined;
|
||||||
|
|||||||
Reference in New Issue
Block a user