From 0670cbb586d8276f4cb9ea4d9ae3de8ec96fe8d5 Mon Sep 17 00:00:00 2001 From: diced Date: Thu, 20 Jul 2023 12:35:08 -0700 Subject: [PATCH] featL settings, view routes, embeds, etc. --- package.json | 9 +- prisma/schema.prisma | 11 +- src/components/Layout.tsx | 10 +- src/components/file/DashboardFileType.tsx | 27 +- .../pages/files/useApiPagination.tsx | 2 +- src/components/pages/settings/index.tsx | 31 ++ .../pages/settings/parts/SettingsAvatar.tsx | 148 ++++++ .../settings/parts/SettingsDashboard.tsx | 30 ++ .../pages/settings/parts/SettingsFileView.tsx | 173 +++++++ .../pages/settings/parts/SettingsOAuth.tsx | 79 +++ .../pages/settings/parts/SettingsUser.tsx | 125 +++++ src/lib/config/safe.ts | 9 +- src/lib/db/index.ts | 22 +- src/lib/db/models/user.ts | 24 + src/lib/hooks/useAvatar.ts | 14 + src/lib/oauth/enabled.ts | 38 ++ src/lib/oauth/providerUtil.ts | 9 + src/lib/parser.ts | 165 +++++++ src/lib/store/settings.ts | 33 ++ src/lib/url.ts | 4 +- src/pages/api/user/avatar.ts | 7 +- src/pages/api/user/index.ts | 43 +- src/pages/auth/login.tsx | 62 +-- src/pages/dashboard/settings.tsx | 19 + src/pages/view/[id].tsx | 262 ++++++++-- yarn.lock | 458 +++++++++++++++--- 26 files changed, 1604 insertions(+), 210 deletions(-) create mode 100644 src/components/pages/settings/index.tsx create mode 100644 src/components/pages/settings/parts/SettingsAvatar.tsx create mode 100644 src/components/pages/settings/parts/SettingsDashboard.tsx create mode 100644 src/components/pages/settings/parts/SettingsFileView.tsx create mode 100644 src/components/pages/settings/parts/SettingsOAuth.tsx create mode 100644 src/components/pages/settings/parts/SettingsUser.tsx create mode 100644 src/lib/hooks/useAvatar.ts create mode 100644 src/lib/oauth/enabled.ts create mode 100644 src/lib/oauth/providerUtil.ts create mode 100644 src/lib/parser.ts create mode 100644 src/lib/store/settings.ts create mode 100644 src/pages/dashboard/settings.tsx diff --git a/package.json b/package.json index e4949246..788166f6 100644 --- a/package.json +++ b/package.json @@ -28,9 +28,9 @@ "@mantine/next": "^6.0.14", "@mantine/notifications": "^6.0.14", "@mantine/nprogress": "^6.0.14", - "@prisma/client": "4.16.1", - "@prisma/internals": "^4.16.1", - "@prisma/migrate": "^4.16.1", + "@prisma/client": "^5.0.0", + "@prisma/internals": "^5.0.0", + "@prisma/migrate": "^5.0.0", "@tabler/icons-react": "^2.23.0", "argon2": "^0.30.3", "bytes": "^3.1.2", @@ -38,6 +38,7 @@ "dayjs": "^1.11.8", "express": "^4.18.2", "highlight.js": "^11.8.0", + "isomorphic-dompurify": "^1.8.0", "katex": "^0.16.8", "mantine-datatable": "^2.8.2", "ms": "^2.1.3", @@ -67,7 +68,7 @@ "dotenv": "^16.1.3", "eslint": "^8.41.0", "npm-run-all": "^4.1.5", - "prisma": "^4.16.1", + "prisma": "^5.0.0", "tsup": "^7.0.0", "typescript": "^5.1.3" }, diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 26ba0871..c7c0115c 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -20,11 +20,12 @@ model User { createdAt DateTime @default(now()) updatedAt DateTime @updatedAt - username String @unique - password String? - avatar String? - token String @unique - role Role @default(USER) + username String @unique + password String? + avatar String? + token String @unique + role Role @default(USER) + view Json @default("{}") files File[] urls Url[] diff --git a/src/components/Layout.tsx b/src/components/Layout.tsx index 49402641..6d091110 100644 --- a/src/components/Layout.tsx +++ b/src/components/Layout.tsx @@ -44,6 +44,7 @@ import { useRouter } from 'next/router'; import { useState } from 'react'; import ConfigProvider from './ConfigProvider'; import { isAdministrator } from '@/lib/role'; +import useAvatar from '@/lib/hooks/useAvatar'; type NavLinks = { label: string; @@ -117,6 +118,7 @@ export default function Layout({ children, config }: { children: React.ReactNode const [setUser, setToken] = useUserStore((s) => [s.setUser, s.setToken]); const { user, mutate } = useLogin(); + const { avatar } = useAvatar(); const copyToken = () => { modals.openConfirmModal({ @@ -279,9 +281,11 @@ export default function Layout({ children, config }: { children: React.ReactNode variant='subtle' color='gray' leftIcon={ - - - + avatar ? ( + + ) : ( + + ) } rightIcon={} size='sm' diff --git a/src/components/file/DashboardFileType.tsx b/src/components/file/DashboardFileType.tsx index 1ed229e1..108497f0 100644 --- a/src/components/file/DashboardFileType.tsx +++ b/src/components/file/DashboardFileType.tsx @@ -46,27 +46,36 @@ export default function DashboardFileType({ show, password, disableMediaPreview, + code, }: { file: DbFile | File; show?: boolean; password?: string; disableMediaPreview?: boolean; + code?: boolean; }) { - const type = file.type.split('/')[0]; const dbFile = 'id' in file; const renderIn = renderMode(file.name.split('.').pop() || ''); const [fileContent, setFileContent] = useState(''); + const [type, setType] = useState(file.type.split('/')[0]); + + const gettext = async () => { + const res = await fetch(`/raw/${file.name}${password ? `?pw=${password}` : ''}`); + const text = await res.text(); + + setFileContent(text); + }; useEffect(() => { - if (type !== 'text') return; - - (async () => { - const res = await fetch(`/raw/${file.name}${password ? `?pw=${password}` : ''}`); - const text = await res.text(); - - setFileContent(text); - })(); + if (code) { + setType('text'); + gettext(); + } else if (type === 'text') { + gettext(); + } else { + return; + } }, []); if (disableMediaPreview) diff --git a/src/components/pages/files/useApiPagination.tsx b/src/components/pages/files/useApiPagination.tsx index 7eee51b9..bc6c6dd2 100644 --- a/src/components/pages/files/useApiPagination.tsx +++ b/src/components/pages/files/useApiPagination.tsx @@ -45,7 +45,7 @@ export function useApiPagination( page: 1, } ) { - const { data, error, isLoading, mutate } = useSWR( + const { data, error, isLoading, mutate } = useSWR( { key: `/api/user/files`, options }, fetcher ); diff --git a/src/components/pages/settings/index.tsx b/src/components/pages/settings/index.tsx new file mode 100644 index 00000000..feb017f3 --- /dev/null +++ b/src/components/pages/settings/index.tsx @@ -0,0 +1,31 @@ +import { Group, SimpleGrid, Title } from '@mantine/core'; +import SettingsAvatar from './parts/SettingsAvatar'; +import SettingsDashboard from './parts/SettingsDashboard'; +import SettingsUser from './parts/SettingsUser'; +import SettingsFileView from './parts/SettingsFileView'; +import { useConfig } from '@/components/ConfigProvider'; +import SettingsOAuth from './parts/SettingsOAuth'; + +export default function DashboardSettings() { + const config = useConfig(); + + return ( + <> + + Settings + + + + + + + + + + + + {config.features.oauthRegistration && } + + + ); +} diff --git a/src/components/pages/settings/parts/SettingsAvatar.tsx b/src/components/pages/settings/parts/SettingsAvatar.tsx new file mode 100644 index 00000000..f2434c47 --- /dev/null +++ b/src/components/pages/settings/parts/SettingsAvatar.tsx @@ -0,0 +1,148 @@ +import { Response } from '@/lib/api/response'; +import { fetchApi } from '@/lib/fetchApi'; +import useAvatar from '@/lib/hooks/useAvatar'; +import { readToDataURL } from '@/lib/readToDataURL'; +import { useUserStore } from '@/lib/store/user'; +import { Avatar, Button, Card, FileInput, Group, Paper, Stack, Text, Title } from '@mantine/core'; +import { notifications } from '@mantine/notifications'; +import { IconChevronDown, IconPhoto, IconPhotoCancel, IconSettingsFilled } from '@tabler/icons-react'; +import { useRouter } from 'next/router'; +import { useEffect, useState } from 'react'; +import useSWR from 'swr'; + +export default function SettingsAvatar() { + const router = useRouter(); + const user = useUserStore((state) => state.user); + + const { avatar: currentAvatar, mutate } = useAvatar(); + const [avatar, setAvatar] = useState(null); + const [avatarSrc, setAvatarSrc] = useState(null); + + useEffect(() => { + (async () => { + if (!avatar) return; + + const base64url = await readToDataURL(avatar); + setAvatarSrc(base64url); + })(); + }, [avatar]); + + const saveAvatar = async () => { + if (!avatar) return; + + const base64url = await readToDataURL(avatar); + const { data, error } = await fetchApi(`/api/user`, 'PATCH', { + avatar: base64url, + }); + + if (!data && error) { + notifications.show({ + title: 'Error while updating avatar', + message: error.message, + color: 'red', + icon: , + }); + + return; + } + + notifications.show({ + message: 'Avatar updated', + color: 'green', + icon: , + }); + + setAvatar(null); + setAvatarSrc(null); + mutate(base64url); + }; + + const clearAvatar = async () => { + const { data, error } = await fetchApi(`/api/user`, 'PATCH', { + avatar: null, + }); + + if (!data && error) { + notifications.show({ + title: 'Error while updating avatar', + message: error.message, + color: 'red', + icon: , + }); + + return; + } + + notifications.show({ + message: 'Avatar updated', + color: 'green', + icon: , + }); + + setAvatar(null); + setAvatarSrc(null); + mutate(undefined); + }; + + return ( + + Avatar + + + setAvatar(file)} + /> + + + + Preview of {avatar ? 'new' : 'current'} avatar + + + + + + + {avatarSrc && ( + + )} + {currentAvatar && ( + + )} + + + + + ); +} diff --git a/src/components/pages/settings/parts/SettingsDashboard.tsx b/src/components/pages/settings/parts/SettingsDashboard.tsx new file mode 100644 index 00000000..6475c595 --- /dev/null +++ b/src/components/pages/settings/parts/SettingsDashboard.tsx @@ -0,0 +1,30 @@ +import { useSettingsStore } from '@/lib/store/settings'; +import { Paper, Stack, Switch, Text, Title } from '@mantine/core'; + +export default function SettingsDashboard() { + const [settings, update] = useSettingsStore((state) => [state.settings, state.update]); + + return ( + + Dashboard Settings + + These settings are saved in your browser. + + + + update('disableMediaPreview', event.currentTarget.checked)} + /> + update('warnDeletion', event.currentTarget.checked)} + /> + + + ); +} diff --git a/src/components/pages/settings/parts/SettingsFileView.tsx b/src/components/pages/settings/parts/SettingsFileView.tsx new file mode 100644 index 00000000..01691558 --- /dev/null +++ b/src/components/pages/settings/parts/SettingsFileView.tsx @@ -0,0 +1,173 @@ +import { Response } from '@/lib/api/response'; +import { fetchApi } from '@/lib/fetchApi'; +import { useUserStore } from '@/lib/store/user'; +import { + ActionIcon, + Button, + ColorInput, + CopyButton, + Divider, + Group, + Paper, + PasswordInput, + ScrollArea, + Select, + SimpleGrid, + Stack, + Switch, + Text, + TextInput, + Textarea, + Title, + Tooltip, +} from '@mantine/core'; +import { hasLength, useForm } from '@mantine/form'; +import { notifications } from '@mantine/notifications'; +import { IconCheck, IconCopy, IconFileX, IconUserCancel } from '@tabler/icons-react'; +import { forwardRef, useEffect, useState } from 'react'; +import { mutate } from 'swr'; + +export default function SettingsFileView() { + const [user, setUser] = useUserStore((state) => [state.user, state.setUser]); + + const form = useForm({ + initialValues: { + enabled: user?.view.enabled ?? false, + content: user?.view.content ?? '', + embed: user?.view.embed ?? false, + embedTitle: user?.view.embedTitle ?? '', + embedDescription: user?.view.embedDescription ?? '', + embedSiteName: user?.view.embedSiteName ?? '', + embedColor: user?.view.embedColor ?? '', + align: user?.view.align ?? 'left', + showMimetype: user?.view.showMimetype ?? false, + }, + }); + + const onSubmit = async (values: typeof form.values) => { + const valuesTrimmed = { + enabled: values.enabled, + embed: values.embed, + content: values.content.trim() || null, + embedTitle: values.embedTitle.trim() || null, + embedDescription: values.embedDescription.trim() || null, + embedSiteName: values.embedSiteName.trim() || null, + embedColor: values.embedColor.trim() || null, + align: values.align, + showMimetype: values.showMimetype, + }; + + const { data, error } = await fetchApi(`/api/user`, 'PATCH', { + view: valuesTrimmed, + }); + + if (!data && error) { + notifications.show({ + title: 'Error while updating view settings', + message: error.message, + color: 'red', + icon: , + }); + } + + if (!data?.user) return; + + mutate('/api/user'); + setUser(data.user); + notifications.show({ + message: 'View settings updated', + color: 'green', + icon: , + }); + }; + + return ( + + Viewing Files + + All text fields support using variables. + + +
+ + + + + + +