From 38a90787d0a61ea889cd044dc22db2307a5504a8 Mon Sep 17 00:00:00 2001 From: curet <76007534+curet-dev@users.noreply.github.com> Date: Wed, 2 Jul 2025 19:52:33 +0200 Subject: [PATCH] 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> --- .../20250613161158_add_domains/migration.sql | 2 + prisma/schema.prisma | 2 + src/components/pages/serverSettings/index.tsx | 3 + .../pages/serverSettings/parts/Domains.tsx | 94 +++++++++++++++++++ .../SettingsGenerators/GeneratorButton.tsx | 28 +++++- .../pages/upload/UploadOptionsButton.tsx | 33 +++++-- src/lib/config/read/db.ts | 1 + src/server/routes/api/server/settings.ts | 5 + src/server/routes/api/user/urls/index.ts | 2 +- 9 files changed, 154 insertions(+), 16 deletions(-) create mode 100644 prisma/migrations/20250613161158_add_domains/migration.sql create mode 100644 src/components/pages/serverSettings/parts/Domains.tsx diff --git a/prisma/migrations/20250613161158_add_domains/migration.sql b/prisma/migrations/20250613161158_add_domains/migration.sql new file mode 100644 index 00000000..ab3c76c5 --- /dev/null +++ b/prisma/migrations/20250613161158_add_domains/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "Zipline" ADD COLUMN "domains" TEXT[] DEFAULT ARRAY[]::TEXT[]; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index dd919952..e9e54d7d 100755 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -135,6 +135,8 @@ model Zipline { pwaDescription String @default("Zipline") pwaThemeColor String @default("#000000") pwaBackgroundColor String @default("#000000") + + domains String[] @default([]) } model User { diff --git a/src/components/pages/serverSettings/index.tsx b/src/components/pages/serverSettings/index.tsx index 898a92e6..9dc8c61c 100644 --- a/src/components/pages/serverSettings/index.tsx +++ b/src/components/pages/serverSettings/index.tsx @@ -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 ; @@ -105,6 +106,8 @@ export default function DashboardSettings() { + + )} diff --git a/src/components/pages/serverSettings/parts/Domains.tsx b/src/components/pages/serverSettings/parts/Domains.tsx new file mode 100644 index 00000000..e554dc1e --- /dev/null +++ b/src/components/pages/serverSettings/parts/Domains.tsx @@ -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([]); + 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 ( + + + + Domains + + + + + + + + {domains.map((domain, index) => ( + + +
+ {domain} +
+ +
+
+ ))} +
+
+ ); +} diff --git a/src/components/pages/settings/parts/SettingsGenerators/GeneratorButton.tsx b/src/components/pages/settings/parts/SettingsGenerators/GeneratorButton.tsx index 46cec2d4..0a366a3f 100755 --- a/src/components/pages/settings/parts/SettingsGenerators/GeneratorButton.tsx +++ b/src/components/pages/settings/parts/SettingsGenerators/GeneratorButton.tsx @@ -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('/api/user/token'); + const { data: settingsData } = useSWR('/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 ( <> setOpen(false)} title={`Generate ${name} Uploader`}> @@ -187,14 +198,21 @@ export default function GeneratorButton({ onChange={(value) => setOption({ maxViews: value === '' ? null : Number(value) })} /> - } 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, + }, + }, + }} /> diff --git a/src/components/pages/upload/UploadOptionsButton.tsx b/src/components/pages/upload/UploadOptionsButton.tsx index 4ea81b3f..171f41a4 100755 --- a/src/components/pages/upload/UploadOptionsButton.tsx +++ b/src/components/pages/upload/UploadOptionsButton.tsx @@ -62,9 +62,20 @@ export default function UploadOptionsButton({ folder, numFiles }: { folder?: str const { data: folders } = useSWR>( '/api/user/folders?noincl=true', ); + const { data: settingsData } = useSWR('/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 - - No Folder - + No Folder {folders ?.filter((f) => f.name.toLowerCase().includes(folderSearch.toLowerCase().trim())) @@ -279,7 +288,8 @@ export default function UploadOptionsButton({ folder, numFiles }: { folder?: str - 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={} 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, + }, + }, + }} /> value.split(',').map((s) => s.trim())), + ]), }) .partial() .refine( diff --git a/src/server/routes/api/user/urls/index.ts b/src/server/routes/api/user/urls/index.ts index c18ea619..cd7bbf47 100755 --- a/src/server/routes/api/user/urls/index.ts +++ b/src/server/routes/api/user/urls/index.ts @@ -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;