featL settings, view routes, embeds, etc.

This commit is contained in:
diced
2023-07-20 12:35:08 -07:00
parent 5f295924b9
commit 0670cbb586
26 changed files with 1604 additions and 210 deletions

View File

@@ -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"
},

View File

@@ -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[]

View File

@@ -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 size='sm' src='/api/user/avatar' radius='sm'>
<IconSettingsFilled size='1.4rem' />
</Avatar>
avatar ? (
<Avatar src={avatar} radius='sm' size='sm' alt={user?.username ?? 'User avatar'} />
) : (
<IconSettingsFilled size='1rem' />
)
}
rightIcon={<IconChevronDown size='0.7rem' />}
size='sm'

View File

@@ -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)

View File

@@ -45,7 +45,7 @@ export function useApiPagination(
page: 1,
}
) {
const { data, error, isLoading, mutate } = useSWR<Response['/api/user/files']>(
const { data, error, isLoading, mutate } = useSWR<Response['/api/user/files']>(
{ key: `/api/user/files`, options },
fetcher
);

View File

@@ -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 (
<>
<Group spacing='sm'>
<Title order={1}>Settings</Title>
</Group>
<SimpleGrid mt='md' cols={2} spacing='lg' breakpoints={[{ maxWidth: 'sm', cols: 1 }]}>
<SettingsUser />
<SettingsDashboard />
<SettingsAvatar />
<SettingsFileView />
{config.features.oauthRegistration && <SettingsOAuth />}
</SimpleGrid>
</>
);
}

View File

@@ -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<File | null>(null);
const [avatarSrc, setAvatarSrc] = useState<string | null>(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<Response['/api/user']>(`/api/user`, 'PATCH', {
avatar: base64url,
});
if (!data && error) {
notifications.show({
title: 'Error while updating avatar',
message: error.message,
color: 'red',
icon: <IconPhotoCancel size='1rem' />,
});
return;
}
notifications.show({
message: 'Avatar updated',
color: 'green',
icon: <IconPhoto size='1rem' />,
});
setAvatar(null);
setAvatarSrc(null);
mutate(base64url);
};
const clearAvatar = async () => {
const { data, error } = await fetchApi<Response['/api/user']>(`/api/user`, 'PATCH', {
avatar: null,
});
if (!data && error) {
notifications.show({
title: 'Error while updating avatar',
message: error.message,
color: 'red',
icon: <IconPhotoCancel size='1rem' />,
});
return;
}
notifications.show({
message: 'Avatar updated',
color: 'green',
icon: <IconPhoto size='1rem' />,
});
setAvatar(null);
setAvatarSrc(null);
mutate(undefined);
};
return (
<Paper withBorder p='sm'>
<Title order={2}>Avatar</Title>
<Stack spacing='sm'>
<FileInput
accept='image/*'
placeholder='Upload new avatar...'
value={avatar}
onChange={(file) => setAvatar(file)}
/>
<Card withBorder shadow='sm'>
<Text size='sm' color='dimmed'>
Preview of {avatar ? 'new' : 'current'} avatar
</Text>
<Button
variant='subtle'
color='gray'
leftIcon={
avatarSrc ? (
<Avatar src={avatarSrc} radius='sm' size='sm' alt={user?.username ?? 'Proposed avatar'} />
) : currentAvatar ? (
<Avatar src={currentAvatar} radius='sm' size='sm' alt={user?.username ?? 'User avatar'} />
) : (
<IconSettingsFilled size='1rem' />
)
}
rightIcon={<IconChevronDown size='0.7rem' />}
size='sm'
>
{user?.username}
</Button>
</Card>
<Group position='left'>
{avatarSrc && (
<Button
variant='outline'
color='red'
onClick={() => {
setAvatar(null);
setAvatarSrc(null);
}}
>
Cancel
</Button>
)}
{currentAvatar && (
<Button variant='outline' color='red' onClick={clearAvatar}>
Remove Avatar
</Button>
)}
<Button variant='outline' color='gray' disabled={!avatar} onClick={saveAvatar}>
Save
</Button>
</Group>
</Stack>
</Paper>
);
}

View File

@@ -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 (
<Paper withBorder p='sm'>
<Title order={2}>Dashboard Settings</Title>
<Text size='sm' color='dimmed' mt={3}>
These settings are saved in your browser.
</Text>
<Stack spacing='sm' my='xs'>
<Switch
label='Disable Media Preview'
description='Disable previews of files in the dashboard. This is useful to save data as Zipline, by default, will load previews of files.'
checked={settings.disableMediaPreview}
onChange={(event) => update('disableMediaPreview', event.currentTarget.checked)}
/>
<Switch
label='Warn on deletion'
description='Show a warning when deleting files. This is useful to prevent accidental deletion of files.'
checked={settings.warnDeletion}
onChange={(event) => update('warnDeletion', event.currentTarget.checked)}
/>
</Stack>
</Paper>
);
}

View File

@@ -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<Response['/api/user']>(`/api/user`, 'PATCH', {
view: valuesTrimmed,
});
if (!data && error) {
notifications.show({
title: 'Error while updating view settings',
message: error.message,
color: 'red',
icon: <IconFileX size='1rem' />,
});
}
if (!data?.user) return;
mutate('/api/user');
setUser(data.user);
notifications.show({
message: 'View settings updated',
color: 'green',
icon: <IconCheck size='1rem' />,
});
};
return (
<Paper withBorder p='sm'>
<Title order={2}>Viewing Files</Title>
<Text color='dimmed' mt='xs'>
All text fields support using variables.
</Text>
<Stack spacing='sm' mt='xs'>
<form onSubmit={form.onSubmit(onSubmit)}>
<SimpleGrid cols={2} spacing='sm' breakpoints={[{ maxWidth: 'sm', cols: 1 }]} mb='xs'>
<Switch
label='Enable View Routes'
description='Enable viewing files through customizable view-routes'
{...form.getInputProps('enabled', { type: 'checkbox' })}
/>
<Switch
label='Show mimetype'
description='Show the mimetype of the file in the view-route'
disabled={!form.values.enabled}
{...form.getInputProps('showMimetype', { type: 'checkbox' })}
/>
</SimpleGrid>
<Textarea
label='View Content'
description='Change the content within view-routes'
disabled={!form.values.enabled}
mb='xs'
{...form.getInputProps('content')}
/>
<Select
label='View Content Alignment'
description='Change the alignment of the content within view-routes'
data={[
{ value: 'left', label: 'Left' },
{ value: 'center', label: 'Center' },
{ value: 'right', label: 'Right' },
]}
itemComponent={({ label, value, ...props }) => (
<Group position={value} {...props}>
{label}
</Group>
)}
disabled={!form.values.enabled}
{...form.getInputProps('align')}
/>
<Divider my='sm' />
<Switch
label='Enable Embed'
description='Enable the following embed properties. These properties take advantage of OpenGraph tags.'
my='xs'
{...form.getInputProps('embed', { type: 'checkbox' })}
/>
<SimpleGrid cols={2} spacing='sm' breakpoints={[{ maxWidth: 'sm', cols: 1 }]}>
<TextInput
label='Embed Title'
disabled={!form.values.embed}
{...form.getInputProps('embedTitle')}
/>
<TextInput
label='Embed Description'
disabled={!form.values.embed}
{...form.getInputProps('embedDescription')}
/>
<TextInput
label='Embed Site Name'
disabled={!form.values.embed}
{...form.getInputProps('embedSiteName')}
/>
<ColorInput
label='Embed Color'
disabled={!form.values.embed}
{...form.getInputProps('embedColor')}
/>
</SimpleGrid>
<Group position='left' mt='sm'>
<Button variant='outline' color='gray' type='submit'>
Save
</Button>
</Group>
</form>
</Stack>
</Paper>
);
}

View File

@@ -0,0 +1,79 @@
import { useConfig } from '@/components/ConfigProvider';
import { findProvider } from '@/lib/oauth/providerUtil';
import { useSettingsStore } from '@/lib/store/settings';
import { useUserStore } from '@/lib/store/user';
import { Button, Group, Paper, Stack, Switch, Text, Title } from '@mantine/core';
import type { OAuthProviderType } from '@prisma/client';
import {
IconBrandDiscordFilled,
IconBrandGithubFilled,
IconBrandGoogle,
IconCircleKeyFilled,
} from '@tabler/icons-react';
import Link from 'next/link';
const icons = {
DISCORD: <IconBrandDiscordFilled />,
GITHUB: <IconBrandGithubFilled />,
GOOGLE: <IconBrandGoogle stroke={4} />,
AUTHENTIK: <IconCircleKeyFilled />,
};
const names = {
DISCORD: 'Discord',
GITHUB: 'GitHub',
GOOGLE: 'Google',
AUTHENTIK: 'Authentik',
};
function OAuthButton({ provider, linked }: { provider: OAuthProviderType; linked: boolean }) {
const unlink = async () => {};
const baseProps = {
size: 'sm',
leftIcon: icons[provider],
color: linked ? 'red' : `${provider.toLowerCase()}.0`,
sx: (t: any) => ({
'&:hover': {
...(!linked && { backgroundColor: t.fn.darken(t.colors[provider.toLowerCase()][0], 0.2) }),
},
}),
};
return linked ? (
<Button {...baseProps} onClick={unlink}>
Unlink {names[provider]} account
</Button>
) : (
<Button {...baseProps} component={Link} href={`/api/auth/oauth/${provider.toLowerCase()}?link=true`}>
Link {names[provider]} account
</Button>
);
}
export default function SettingsOAuth() {
const config = useConfig();
const [user, setUser] = useUserStore((state) => [state.user, state.setUser]);
const discordLinked = findProvider('DISCORD', user?.oauthProviders ?? []);
const githubLinked = findProvider('GITHUB', user?.oauthProviders ?? []);
const googleLinked = findProvider('GOOGLE', user?.oauthProviders ?? []);
const authentikLinked = findProvider('AUTHENTIK', user?.oauthProviders ?? []);
return (
<Paper withBorder p='sm'>
<Title order={2}>OAuth</Title>
<Text size='sm' color='dimmed' mt={3}>
Manage your connected OAuth providers.
</Text>
<Group mt='xs'>
{config.oauthEnabled.discord && <OAuthButton provider='DISCORD' linked={!!discordLinked} />}
{config.oauthEnabled.github && <OAuthButton provider='GITHUB' linked={!!githubLinked} />}
{config.oauthEnabled.google && <OAuthButton provider='GOOGLE' linked={!!googleLinked} />}
{config.oauthEnabled.authentik && <OAuthButton provider='AUTHENTIK' linked={!!authentikLinked} />}
</Group>
</Paper>
);
}

View File

@@ -0,0 +1,125 @@
import { Response } from '@/lib/api/response';
import { fetchApi } from '@/lib/fetchApi';
import { useUserStore } from '@/lib/store/user';
import {
ActionIcon,
Button,
CopyButton,
Group,
Paper,
PasswordInput,
ScrollArea,
Stack,
TextInput,
Title,
Tooltip,
} from '@mantine/core';
import { hasLength, useForm } from '@mantine/form';
import { notifications } from '@mantine/notifications';
import { IconCheck, IconCopy, IconUserCancel } from '@tabler/icons-react';
import { useEffect, useState } from 'react';
import { mutate } from 'swr';
export default function SettingsUser() {
const [user, setUser] = useUserStore((state) => [state.user, state.setUser]);
const [tokenShown, setTokenShown] = useState(false);
const [token, setToken] = useState('');
useEffect(() => {
(async () => {
const { data, error } = await fetchApi<Response['/api/user/token']>('/api/user/token');
if (data) {
setToken(data.token || '');
}
})();
}, []);
const form = useForm({
initialValues: {
username: user?.username ?? '',
password: '',
},
validate: {
username: hasLength({ min: 1 }, 'Username is required'),
},
});
const onSubmit = async (values: typeof form.values) => {
const send: {
username?: string;
password?: string;
} = {};
if (values.username !== user?.username) send['username'] = values.username.trim();
if (values.password) send['password'] = values.password.trim();
const { data, error } = await fetchApi<Response['/api/user']>(`/api/user`, 'PATCH', send);
if (!data && error) {
if (error.field === 'username') {
form.setFieldError('username', error.message);
} else {
notifications.show({
title: 'Error while updating user',
message: error.message,
color: 'red',
icon: <IconUserCancel size='1rem' />,
});
}
return;
}
if (!data?.user) return;
mutate('/api/user');
setUser(data.user);
notifications.show({
message: 'User updated',
color: 'green',
icon: <IconCheck size='1rem' />,
});
};
return (
<Paper withBorder p='sm'>
<Title order={2}>User info</Title>
<form onSubmit={form.onSubmit(onSubmit)}>
<TextInput
rightSection={
<CopyButton value={token} timeout={1000}>
{({ copied, copy }) => (
<Tooltip label='Click to copy token'>
<ActionIcon onClick={copy}>
{copied ? <IconCheck color='green' size='1rem' /> : <IconCopy size='1rem' />}
</ActionIcon>
</Tooltip>
)}
</CopyButton>
}
// @ts-ignore this works trust
component='span'
label='Token'
onClick={() => setTokenShown(true)}
>
<ScrollArea scrollbarSize={5}>{tokenShown ? token : '[click to reveal]'}</ScrollArea>
</TextInput>
<TextInput label='Username' {...form.getInputProps('username')} />
<PasswordInput
label='Password'
description='Leave blank to keep the same password'
{...form.getInputProps('password')}
/>
<Group position='left' mt='sm'>
<Button variant='outline' color='gray' type='submit'>
Save
</Button>
</Group>
</form>
</Paper>
);
}

View File

@@ -1,10 +1,15 @@
import { config } from '.';
import enabled from '../oauth/enabled';
import { Config } from './validate';
export type SafeConfig = Omit<Config, 'oauth' | 'datasource' | 'core'>;
export type SafeConfig = Omit<Config, 'oauth' | 'datasource' | 'core'> & {
oauthEnabled: ReturnType<typeof enabled>;
};
export function safeConfig(): SafeConfig {
const { datasource, core, oauth, ...rest } = config;
return rest;
(rest as SafeConfig).oauthEnabled = enabled(config);
return rest as SafeConfig;
}

View File

@@ -1,10 +1,11 @@
import { PrismaClient } from '@prisma/client';
import { log } from '@/lib/logger';
import { Prisma, PrismaClient } from '@prisma/client';
import { userViewSchema } from './models/user';
let prisma: PrismaClient;
let prisma: ExtendedPrismaClient;
declare global {
var __db__: PrismaClient;
var __db__: ExtendedPrismaClient;
}
if (process.env.NODE_ENV === 'production') {
@@ -16,12 +17,25 @@ if (process.env.NODE_ENV === 'production') {
prisma = global.__db__;
}
type ExtendedPrismaClient = ReturnType<typeof getClient>;
function getClient() {
const logger = log('db');
logger.info('connecting to database ' + process.env.DATABASE_URL);
const client = new PrismaClient();
const client = new PrismaClient().$extends({
result: {
user: {
view: {
needs: { view: true },
compute({ view }: { view: Prisma.JsonValue }) {
return userViewSchema.parse(view);
},
},
},
},
});
client.$connect();
return client;

View File

@@ -1,9 +1,16 @@
import { OAuthProvider } from '@prisma/client';
import { z } from 'zod';
export type User = {
id: string;
username: string;
createdAt: Date;
updatedAt: Date;
role: 'USER' | 'ADMIN' | 'SUPERADMIN';
view: UserViewSettings;
oauthProviders: OAuthProvider[];
avatar?: string | null;
password?: string | null;
token?: string | null;
@@ -15,4 +22,21 @@ export const userSelect = {
createdAt: true,
updatedAt: true,
role: true,
view: true,
oauthProviders: true,
};
export type UserViewSettings = z.infer<typeof userViewSchema>;
export const userViewSchema = z
.object({
enabled: z.boolean().nullish(),
align: z.enum(['left', 'center', 'right']).nullish(),
showMimetype: z.boolean().nullish(),
content: z.string().nullish(),
embed: z.boolean().nullish(),
embedTitle: z.string().nullish(),
embedDescription: z.string().nullish(),
embedColor: z.string().nullish(),
embedSiteName: z.string().nullish(),
})
.partial();

View File

@@ -0,0 +1,14 @@
import useSWR from 'swr';
const f = async () => {
const res = await fetch('/api/user/avatar');
if (!res.ok) return null;
const r = await res.text();
return r;
};
export default function useAvatar() {
const { data, mutate } = useSWR('/api/user/avatar', f);
return { avatar: data, mutate };
}

38
src/lib/oauth/enabled.ts Normal file
View File

@@ -0,0 +1,38 @@
import { Config } from '../config/validate';
import { isTruthy } from '../primitive';
export default function enabled(config: Config) {
const discordEnabled = isTruthy(
config.oauth?.discord?.clientId,
config.oauth?.discord?.clientSecret,
config.features.oauthRegistration
);
const githubEnabled = isTruthy(
config.oauth?.github?.clientId,
config.oauth?.github?.clientSecret,
config.features.oauthRegistration
);
const googleEnabled = isTruthy(
config.oauth?.google?.clientId,
config.oauth?.google?.clientSecret,
config.features.oauthRegistration
);
const authentikEnabled = isTruthy(
config.oauth?.authentik?.clientId,
config.oauth?.authentik?.clientSecret,
config.oauth?.authentik?.authorizeUrl,
config.oauth?.authentik?.tokenUrl,
config.oauth?.authentik?.userinfoUrl,
config.features.oauthRegistration
);
return {
discord: discordEnabled,
github: githubEnabled,
google: googleEnabled,
authentik: authentikEnabled,
};
}

View File

@@ -0,0 +1,9 @@
import type { OAuthProviderType } from '@prisma/client';
import { User } from '../db/models/user';
export function findProvider(
provider: OAuthProviderType,
providers: User['oauthProviders']
): User['oauthProviders'][0] | undefined {
return providers.find((p) => p.provider === provider);
}

165
src/lib/parser.ts Normal file
View File

@@ -0,0 +1,165 @@
import bytes from 'bytes';
import { File } from './db/models/file';
import { User } from './db/models/user';
export type ParseValue = {
file?: File;
user?: User | Omit<User, 'oauthProviders'>;
link?: string;
raw_link?: string;
};
export function parseString(str: string, value: ParseValue) {
if (!str) return null;
str = str
.replace(/\{link\}/gi, value.link ?? '{unknown_link}')
.replace(/\{raw_link\}/gi, value.raw_link ?? '{unknown_raw_link}')
.replace(/\\n/g, '\n');
const re = /\{(?<type>file|url|user)\.(?<prop>\w+)(::(?<mod>\w+))?\}/gi;
let matches: RegExpMatchArray | null;
while ((matches = re.exec(str))) {
if (!matches.groups) continue;
const index = matches.index as number;
const getV = value[matches.groups.type as keyof ParseValue];
if (!getV) {
str = replaceCharsFromString(str, '{unknown_type}', index, re.lastIndex);
re.lastIndex = index;
continue;
}
if (['password', 'avatar', 'uuid'].includes(matches.groups.prop)) {
str = replaceCharsFromString(str, '{unknown_property}', index, re.lastIndex);
re.lastIndex = index;
continue;
}
if (['originalName', 'name'].includes(matches.groups.prop)) {
str = replaceCharsFromString(
str,
decodeURIComponent(escape(getV[matches.groups.prop as keyof ParseValue['file']])),
index,
re.lastIndex
);
re.lastIndex = index;
continue;
}
const v = getV[matches.groups.prop as keyof ParseValue['file'] | keyof ParseValue['user']];
if (v === undefined) {
str = replaceCharsFromString(str, '{unknown_property}', index, re.lastIndex);
re.lastIndex = index;
continue;
}
if (matches.groups.mod) {
str = replaceCharsFromString(str, modifier(matches.groups.mod, v), index, re.lastIndex);
re.lastIndex = index;
continue;
}
str = replaceCharsFromString(str, v, index, re.lastIndex);
re.lastIndex = index;
}
return str;
}
function modifier(mod: string, value: unknown): string {
mod = mod.toLowerCase();
if (value instanceof Date) {
switch (mod) {
case 'locale':
return value.toLocaleString();
case 'time':
return value.toLocaleTimeString();
case 'date':
return value.toLocaleDateString();
case 'unix':
return Math.floor(value.getTime() / 1000).toString();
case 'iso':
return value.toISOString();
case 'utc':
return value.toUTCString();
case 'year':
return value.getFullYear().toString();
case 'month':
return (value.getMonth() + 1).toString();
case 'day':
return value.getDate().toString();
case 'hour':
return value.getHours().toString();
case 'minute':
return value.getMinutes().toString();
case 'second':
return value.getSeconds().toString();
default:
return `{unknown_date_modifier(${mod})}`;
}
} else if (typeof value === 'string') {
switch (mod) {
case 'upper':
return value.toUpperCase();
case 'lower':
return value.toLowerCase();
case 'title':
return value.charAt(0).toUpperCase() + value.slice(1);
case 'length':
return value.length.toString();
case 'reverse':
return value.split('').reverse().join('');
case 'base64':
return btoa(value);
case 'hex':
return toHex(value);
default:
return `{unknown_str_modifier(${mod})}`;
}
} else if (typeof value === 'number') {
switch (mod) {
case 'comma':
return value.toLocaleString();
case 'hex':
return value.toString(16);
case 'octal':
return value.toString(8);
case 'binary':
return value.toString(2);
case 'bytes':
return bytes(value, { unitSeparator: ' ' });
default:
return `{unknown_int_modifier(${mod})}`;
}
} else if (typeof value === 'boolean') {
switch (mod) {
case 'yesno':
return value ? 'Yes' : 'No';
case 'onoff':
return value ? 'On' : 'Off';
case 'truefalse':
return value ? 'True' : 'False';
default:
return `{unknown_bool_modifier(${mod})}`;
}
}
return `{unknown_modifier(${mod})}`;
}
function replaceCharsFromString(str: string, replace: string, start: number, end: number): string {
return str.slice(0, start) + replace + str.slice(end);
}
function toHex(str: string): string {
let hex = '';
for (let i = 0; i < str.length; i++) {
hex += '' + str.charCodeAt(i).toString(16);
}
return hex;
}

33
src/lib/store/settings.ts Normal file
View File

@@ -0,0 +1,33 @@
import { create } from 'zustand';
import { persist } from 'zustand/middleware';
export type SettingsStore = {
settings: {
disableMediaPreview: boolean;
warnDeletion: boolean;
};
update: <K extends keyof SettingsStore['settings']>(key: K, value: SettingsStore['settings'][K]) => void;
};
export const useSettingsStore = create<SettingsStore>()(
persist(
(set) => ({
settings: {
disableMediaPreview: false,
warnDeletion: true,
},
update: (key, value) =>
set((state) => ({
settings: {
...state.settings,
[key]: value,
},
})),
}),
{
name: 'zipline-settings',
}
)
);

View File

@@ -1,3 +1,3 @@
export function formatRootUrl(route: string, src: string) {
return `${route === '/' || route === '' ? '' : route}${encodeURI(src)}`
}
return `${route === '/' ? '' : route}/${encodeURI(src)}`;
}

View File

@@ -25,12 +25,7 @@ export async function handler(req: NextApiReq, res: NextApiRes<ApiUserTokenRespo
if (!u.avatar) return res.notFound();
u.avatar = u.avatar.replace(/^data:image\/\w+;base64,/, '');
const buf = Buffer.from(u.avatar, 'base64');
res.setHeader('Content-Length', buf.length);
return res.send(buf);
return res.status(200).send(u.avatar);
}
export default combine([method(['GET']), ziplineAuth()], handler);

View File

@@ -15,12 +15,33 @@ type EditBody = {
username?: string;
password?: string;
avatar?: string;
view?: {
content?: string;
embed?: boolean;
embedTitle?: string;
embedDescription?: string;
embedColor?: string;
embedSiteName?: string;
enabled?: boolean;
align?: 'left' | 'center' | 'right';
showMimetype?: boolean;
};
};
export async function handler(req: NextApiReq<EditBody>, res: NextApiRes<ApiUserResponse>) {
if (req.method === 'GET') {
return res.ok({ user: req.user, token: req.cookies.zipline_token });
} else if (req.method === 'PATCH') {
if (req.body.username) {
const existing = await prisma.user.findUnique({
where: {
username: req.body.username,
},
});
if (existing) return res.badRequest('Username already taken', { field: 'username' });
}
const user = await prisma.user.update({
where: {
id: req.user.id,
@@ -28,7 +49,27 @@ export async function handler(req: NextApiReq<EditBody>, res: NextApiRes<ApiUser
data: {
...(req.body.username && { username: req.body.username }),
...(req.body.password && { password: await hashPassword(req.body.password) }),
...(req.body.avatar && { avatar: req.body.avatar }),
...(req.body.avatar !== undefined && { avatar: req.body.avatar || null }),
...(req.body.view && {
view: {
...req.user.view,
...(req.body.view.enabled !== undefined && { enabled: req.body.view.enabled || false }),
...(req.body.view.content !== undefined && { content: req.body.view.content || null }),
...(req.body.view.embed !== undefined && { embed: req.body.view.embed || false }),
...(req.body.view.embedTitle !== undefined && { embedTitle: req.body.view.embedTitle || null }),
...(req.body.view.embedDescription !== undefined && {
embedDescription: req.body.view.embedDescription || null,
}),
...(req.body.view.embedColor !== undefined && { embedColor: req.body.view.embedColor || null }),
...(req.body.view.embedSiteName !== undefined && {
embedSiteName: req.body.view.embedSiteName || null,
}),
...(req.body.view.align !== undefined && { align: req.body.view.align || 'center' }),
...(req.body.view.showMimetype !== undefined && {
showMimetype: req.body.view.showMimetype || false,
}),
},
}),
},
select: {
...userSelect,

View File

@@ -4,21 +4,13 @@ import { getZipline } from '@/lib/db/models/zipline';
import { fetchApi } from '@/lib/fetchApi';
import { withSafeConfig } from '@/lib/middleware/next/withSafeConfig';
import { eitherTrue, isTruthy } from '@/lib/primitive';
import {
Button,
Center,
PasswordInput,
Stack,
Text,
TextInput,
Title
} from '@mantine/core';
import { Button, Center, PasswordInput, Stack, Text, TextInput, Title } from '@mantine/core';
import { hasLength, useForm } from '@mantine/form';
import {
IconBrandDiscordFilled,
IconBrandGithubFilled,
IconBrandGoogle,
IconCircleKeyFilled
IconCircleKeyFilled,
} from '@tabler/icons-react';
import { InferGetServerSidePropsType } from 'next';
import Link from 'next/link';
@@ -26,13 +18,7 @@ import { useRouter } from 'next/router';
import { useEffect } from 'react';
import useSWR from 'swr';
export default function Login({
config,
authentikEnabled,
discordEnabled,
githubEnabled,
googleEnabled,
}: InferGetServerSidePropsType<typeof getServerSideProps>) {
export default function Login({ config }: InferGetServerSidePropsType<typeof getServerSideProps>) {
const router = useRouter();
const { data, isLoading, mutate } = useSWR<Response['/api/user']>('/api/user');
@@ -110,7 +96,7 @@ export default function Login({
</Button>
)}
{discordEnabled && (
{config.oauthEnabled.discord && (
<Button
size='lg'
fullWidth
@@ -129,7 +115,7 @@ export default function Login({
</Button>
)}
{githubEnabled && (
{config.oauthEnabled.github && (
<Button
size='lg'
fullWidth
@@ -147,7 +133,7 @@ export default function Login({
</Button>
)}
{googleEnabled && (
{config.oauthEnabled.google && (
<Button
size='lg'
fullWidth
@@ -165,7 +151,7 @@ export default function Login({
</Button>
)}
{authentikEnabled && (
{config.oauthEnabled.authentik && (
<Button
size='lg'
fullWidth
@@ -200,37 +186,5 @@ export const getServerSideProps = withSafeConfig(async () => {
},
};
const discordEnabled = isTruthy(
config.oauth?.discord?.clientId,
config.oauth?.discord?.clientSecret,
config.features.oauthRegistration
);
const githubEnabled = isTruthy(
config.oauth?.github?.clientId,
config.oauth?.github?.clientSecret,
config.features.oauthRegistration
);
const googleEnabled = isTruthy(
config.oauth?.google?.clientId,
config.oauth?.google?.clientSecret,
config.features.oauthRegistration
);
const authentikEnabled = isTruthy(
config.oauth?.authentik?.clientId,
config.oauth?.authentik?.clientSecret,
config.oauth?.authentik?.authorizeUrl,
config.oauth?.authentik?.tokenUrl,
config.oauth?.authentik?.userinfoUrl,
config.features.oauthRegistration
);
return {
discordEnabled,
githubEnabled,
googleEnabled,
authentikEnabled,
};
return {};
});

View File

@@ -0,0 +1,19 @@
import Layout from '@/components/Layout';
import DashboardSettings from '@/components/pages/settings';
import useLogin from '@/lib/hooks/useLogin';
import { withSafeConfig } from '@/lib/middleware/next/withSafeConfig';
import { LoadingOverlay } from '@mantine/core';
import { InferGetServerSidePropsType } from 'next';
export default function DashboardIndex({ config }: InferGetServerSidePropsType<typeof getServerSideProps>) {
const { loading } = useLogin();
if (loading) return <LoadingOverlay visible />;
return (
<Layout config={config}>
<DashboardSettings />
</Layout>
);
}
export const getServerSideProps = withSafeConfig();

View File

@@ -1,9 +1,13 @@
import DashboardFileType from '@/components/file/DashboardFileType';
import { isCode } from '@/lib/code';
import { SafeConfig, safeConfig } from '@/lib/config/safe';
import { verifyPassword } from '@/lib/crypto';
import { prisma } from '@/lib/db';
import { fileSelect, type File } from '@/lib/db/models/file';
import { User, UserViewSettings, userSelect } from '@/lib/db/models/user';
import { fetchApi } from '@/lib/fetchApi';
import { parseString } from '@/lib/parser';
import { formatRootUrl } from '@/lib/url';
import {
Box,
Button,
@@ -16,24 +20,35 @@ import {
Space,
Text,
Title,
TypographyStylesProvider,
} from '@mantine/core';
import { IconFileDownload } from '@tabler/icons-react';
import bytes from 'bytes';
import { GetServerSideProps, InferGetServerSidePropsType } from 'next';
import Link from 'next/link';
import { useRouter } from 'next/router';
import { useState } from 'react';
import { useEffect, useState } from 'react';
import { sanitize } from 'isomorphic-dompurify';
import Head from 'next/head';
export default function ViewFile({
file,
password,
pw,
code,
user,
config,
host,
}: InferGetServerSidePropsType<typeof getServerSideProps>) {
file.createdAt = new Date(file.createdAt);
file.updatedAt = new Date(file.updatedAt);
file.deletesAt = file.deletesAt ? new Date(file.deletesAt) : null;
if (user) {
user.createdAt = new Date(user.createdAt);
user.updatedAt = new Date(user.updatedAt);
}
const router = useRouter();
const [passwordValue, setPassword] = useState<string>('');
@@ -53,6 +68,94 @@ export default function ViewFile({
}
};
const meta = (
<Head>
{user?.view.embedTitle && user?.view.embed && (
<meta property='og:title' content={parseString(user.view.embedTitle, { file: file, user }) ?? ''} />
)}
{user?.view.embedDescription && user?.view.embed && (
<meta
property='og:description'
content={parseString(user.view.embedDescription, { file: file, user }) ?? ''}
/>
)}
{user?.view.embedSiteName && user?.view.embed && (
<meta
property='og:site_name'
content={parseString(user.view.embedSiteName, { file: file, user }) ?? ''}
/>
)}
{user?.view.embedColor && user?.view.embed && (
<meta
property='theme-color'
content={parseString(user.view.embedColor, { file: file, user }) ?? ''}
/>
)}
{file.type.startsWith('image') && (
<>
<meta property='og:type' content='image' />
<meta property='og:image' itemProp='image' content={`${host}/raw/${file.name}`} />
<meta property='og:url' content={`${host}/raw/${file.name}`} />
<meta property='twitter:card' content='summary_large_image' />
<meta property='twitter:image' content={`${host}/raw/${file.name}`} />
<meta property='twitter:title' content={file.name} />
</>
)}
{file.type.startsWith('video') && (
<>
<meta name='twitter:card' content='player' />
<meta name='twitter:player' content={`${host}/raw/${file.name}`} />
<meta name='twitter:player:stream' content={`${host}/raw/${file.name}`} />
<meta name='twitter:player:width' content='720' />
<meta name='twitter:player:height' content='480' />
<meta name='twitter:player:stream:content_type' content={file.type} />
<meta name='twitter:title' content={file.name} />
{/*
{file.thumbnail && (
<>
<meta name='twitter:image' content={`${host}/raw/${file.thumbnail.name}`} />
<meta property='og:image' content={`${host}/raw/${file.thumbnail.name}`} />
</>
)} */}
<meta property='og:url' content={`${host}/raw/${file.name}`} />
<meta property='og:video' content={`${host}/raw/${file.name}`} />
<meta property='og:video:url' content={`${host}/raw/${file.name}`} />
<meta property='og:video:secure_url' content={`${host}/raw/${file.name}`} />
<meta property='og:video:type' content={file.type} />
<meta property='og:video:width' content='720' />
<meta property='og:video:height' content='480' />
</>
)}
{file.type.startsWith('audio') && (
<>
<meta name='twitter:card' content='player' />
<meta name='twitter:player' content={`${host}/raw/${file.name}`} />
<meta name='twitter:player:stream' content={`${host}/raw/${file.name}`} />
<meta name='twitter:player:stream:content_type' content={file.type} />
<meta name='twitter:title' content={file.name} />
<meta name='twitter:player:width' content='720' />
<meta name='twitter:player:height' content='480' />
<meta property='og:type' content='music.song' />
<meta property='og:url' content={`${host}/raw/${file.name}`} />
<meta property='og:audio' content={`${host}/raw/${file.name}`} />
<meta property='og:audio:secure_url' content={`${host}/raw/${file.name}`} />
<meta property='og:audio:type' content={file.type} />
</>
)}
{!file.type.startsWith('video') && !file.type.startsWith('image') && (
<meta property='og:url' content={`${host}/raw/${file.name}`} />
)}
<title>{file.name}</title>
</Head>
);
return password && !pw ? (
<Modal
onClose={() => {}}
@@ -83,6 +186,7 @@ export default function ViewFile({
</Modal>
) : code ? (
<>
{meta}
<Paper withBorder>
<Group position='apart' py={5} px='xs'>
<Text color='dimmed'>{file.name}</Text>
@@ -95,61 +199,88 @@ export default function ViewFile({
<Collapse in={detailsOpen}>
<Paper m='md' p='md' withBorder>
<Text size='sm' color='dimmed'>
<b>Created at:</b> {file.createdAt.toLocaleString()}
<Space />
<b>Updated at:</b> {file.updatedAt.toLocaleString()}
<Space />
<b>Size:</b> {bytes(file.size)}
<Space />
<b>Views:</b> {file.views.toLocaleString()}
</Text>
{user?.view.content && (
<TypographyStylesProvider>
<Text
align={user?.view.align ?? 'left'}
dangerouslySetInnerHTML={{
__html: sanitize(
parseString(user.view.content, {
file,
link: `${host}${formatRootUrl(config?.files?.route ?? '/u', file.name)}`,
raw_link: `${host}/raw/${file.name}`,
}) ?? '',
{
USE_PROFILES: { html: true },
FORBID_TAGS: ['style', 'script'],
}
),
}}
/>
</TypographyStylesProvider>
)}
</Paper>
</Collapse>
<Paper m='md' p='md' withBorder>
<DashboardFileType file={file} password={pw} show />
<DashboardFileType file={file} password={pw} show code={code} />
</Paper>
</>
) : (
<Center h='100%'>
<Paper m='md' p='md' shadow='md' radius='md' withBorder>
<Group position='apart' mb='sm'>
<Text size='lg' weight={700} sx={{ display: 'flex' }}>
{file.name}
<>
{meta}
<Center h='100%'>
<Paper m='md' p='md' shadow='md' radius='md' withBorder>
<Group position='apart' mb='sm'>
<Text size='lg' weight={700} sx={{ display: 'flex' }}>
{file.name}
<Text size='sm' color='dimmed' ml='sm' sx={{ alignSelf: 'center' }}>
{file.type}
{user?.view.showMimetype && (
<Text size='sm' color='dimmed' ml='sm' sx={{ alignSelf: 'center' }}>
{file.type}
</Text>
)}
</Text>
</Text>
<Button
ml='sm'
variant='outline'
component={Link}
href={`/raw/${file.name}?download=true${pw ? `&pw=${pw}` : ''}`}
target='_blank'
compact
color='gray'
leftIcon={<IconFileDownload size='1rem' />}
>
Download
</Button>
</Group>
<Button
ml='sm'
variant='outline'
component={Link}
href={`/raw/${file.name}?download=true${pw ? `&pw=${pw}` : ''}`}
target='_blank'
compact
color='gray'
leftIcon={<IconFileDownload size='1rem' />}
>
Download
</Button>
</Group>
<DashboardFileType file={file} password={pw} show />
<DashboardFileType file={file} password={pw} show />
<Text size='sm' color='dimmed' mt='sm'>
<b>Created at:</b> {file.createdAt.toLocaleString()}
<Space />
<b>Updated at:</b> {file.updatedAt.toLocaleString()}
<Space />
<b>Size:</b> {bytes(file.size)}
<Space />
<b>Views:</b> {file.views.toLocaleString()}
</Text>
</Paper>
</Center>
{user?.view.content && (
<TypographyStylesProvider>
<Text
align={user?.view.align ?? 'left'}
dangerouslySetInnerHTML={{
__html: sanitize(
parseString(user?.view.content, {
file,
link: `${host}${formatRootUrl(config?.files?.route ?? '/u', file.name)}`,
raw_link: `${host}/raw/${file.name}`,
}) ?? '',
{
USE_PROFILES: { html: true },
FORBID_TAGS: ['style', 'script'],
}
),
}}
/>
</TypographyStylesProvider>
)}
</Paper>
</Center>
</>
);
}
@@ -158,6 +289,9 @@ export const getServerSideProps: GetServerSideProps<{
password?: boolean;
pw?: string;
code: boolean;
user?: Omit<User, 'oauthProviders'>;
config?: SafeConfig;
host: string;
}> = async (context) => {
const { id, pw } = context.query;
if (!id) return { notFound: true };
@@ -169,32 +303,68 @@ export const getServerSideProps: GetServerSideProps<{
select: {
...fileSelect,
password: true,
userId: true,
},
});
if (!file) return { notFound: true };
if (!file || !file.userId) return { notFound: true };
const user = await prisma.user.findFirst({
where: {
id: file.userId,
},
select: {
...userSelect,
oauthProviders: false,
},
});
if (!user) return { notFound: true };
let host = context.req.headers.host;
const proto = context.req.headers['x-forwarded-proto'];
try {
if (
JSON.parse(context.req.headers['cf-visitor'] as string).scheme === 'https' ||
proto === 'https' ||
false // return+Htt[s]
)
host = `https://${host}`;
else host = `http://${host}`;
} catch (e) {
if (proto === 'https' || false /* return https */) host = `https://${host}`;
else host = `http://${host}`;
}
// convert date to string dumb nextjs :@
(file as any).createdAt = file.createdAt.toISOString();
(file as any).updatedAt = file.updatedAt.toISOString();
(file as any).deletesAt = file.deletesAt?.toISOString() || null;
(user as any).createdAt = user.createdAt.toISOString();
(user as any).updatedAt = user.updatedAt.toISOString();
const code = await isCode(file.name);
if (pw) {
const verified = await verifyPassword(pw as string, file.password!);
delete (file as any).password;
if (verified) return { props: { file, pw: pw as string, code } };
if (verified) return { props: { file, pw: pw as string, code, host } };
}
const password = !!file.password;
delete (file as any).password;
const config = safeConfig();
return {
props: {
file,
password,
code,
user,
config,
host,
},
};
};

458
yarn.lock
View File

@@ -3719,51 +3719,51 @@ __metadata:
languageName: node
linkType: hard
"@prisma/client@npm:4.16.1":
version: 4.16.1
resolution: "@prisma/client@npm:4.16.1"
"@prisma/client@npm:^5.0.0":
version: 5.0.0
resolution: "@prisma/client@npm:5.0.0"
dependencies:
"@prisma/engines-version": 4.16.0-66.b20ead4d3ab9e78ac112966e242ded703f4a052c
"@prisma/engines-version": 4.17.0-26.6b0aef69b7cdfc787f822ecd7cdc76d5f1991584
peerDependencies:
prisma: "*"
peerDependenciesMeta:
prisma:
optional: true
checksum: da715b6e930f56493cb7c6232b179250a4e93bbc2b5fe42a84cb9511d127b6fd89e1271f83e1d531bd4fdd7e87f6ac0701b30174cf89212ae7da88097de31026
checksum: 332c2af44e880ffc9dd1223992bf6f45910094c7a3a540cbbfdda45d6caf3e82998376338abdf85e34a12f1082ae932f2385d6396c62fe4bba7ec6b84de54466
languageName: node
linkType: hard
"@prisma/debug@npm:4.16.1":
version: 4.16.1
resolution: "@prisma/debug@npm:4.16.1"
"@prisma/debug@npm:5.0.0":
version: 5.0.0
resolution: "@prisma/debug@npm:5.0.0"
dependencies:
"@types/debug": 4.1.8
debug: 4.3.4
strip-ansi: 6.0.1
checksum: 11a1fe1ac41c2729295774fe4361023c6c2140d0e3d5ffaee38bf89ca1e73ccf27f933d068c0d4dc8911470272add4ae55c742dec297d7629ac46c74ff043c4c
checksum: 17d720717fab190a94762b90f93cfde799f419bfd2fee05c145566d6fa31d59d86dede80417234094afc5fff6d684ce350c17d40c302b26610ee60fe56c704e9
languageName: node
linkType: hard
"@prisma/engines-version@npm:4.16.0-66.b20ead4d3ab9e78ac112966e242ded703f4a052c":
version: 4.16.0-66.b20ead4d3ab9e78ac112966e242ded703f4a052c
resolution: "@prisma/engines-version@npm:4.16.0-66.b20ead4d3ab9e78ac112966e242ded703f4a052c"
checksum: 23183cab33f95e0711cd6cee7642f4beeeb77b8b0aa54b419f55097bfb96863a11a28f6509ab27fef0ea794522e188044ce850993a25c341333f8305711a255f
"@prisma/engines-version@npm:4.17.0-26.6b0aef69b7cdfc787f822ecd7cdc76d5f1991584":
version: 4.17.0-26.6b0aef69b7cdfc787f822ecd7cdc76d5f1991584
resolution: "@prisma/engines-version@npm:4.17.0-26.6b0aef69b7cdfc787f822ecd7cdc76d5f1991584"
checksum: 8fcbceef3b554ee7fa404bead50be5286412a097b21723272aff298b289caf2802b01b84bb85c4c9f3b592eeac114c8d6e79db083f271dc8c54f453b4a515233
languageName: node
linkType: hard
"@prisma/engines@npm:4.16.1":
version: 4.16.1
resolution: "@prisma/engines@npm:4.16.1"
checksum: 7b8c1a9e20434edaa919fb24585ebff573c4ace787ea5a135dfefde3571ae31d3f0b0347397774c38be6d2e3fc0087d4e21a2720fe9f7a66ffd870e34377040f
"@prisma/engines@npm:5.0.0":
version: 5.0.0
resolution: "@prisma/engines@npm:5.0.0"
checksum: 31271d85c29709059f91051d4cef7acf874014ba0128b674ca2f842e5fac61d3011e9db246dfa67ba4803081d36dbc9e31492716bab677128588343c92117b2b
languageName: node
linkType: hard
"@prisma/fetch-engine@npm:4.16.1":
version: 4.16.1
resolution: "@prisma/fetch-engine@npm:4.16.1"
"@prisma/fetch-engine@npm:5.0.0":
version: 5.0.0
resolution: "@prisma/fetch-engine@npm:5.0.0"
dependencies:
"@prisma/debug": 4.16.1
"@prisma/get-platform": 4.16.1
"@prisma/debug": 5.0.0
"@prisma/get-platform": 5.0.0
execa: 5.1.1
find-cache-dir: 3.3.2
fs-extra: 11.1.1
@@ -3771,7 +3771,7 @@ __metadata:
http-proxy-agent: 7.0.0
https-proxy-agent: 7.0.0
kleur: 4.1.5
node-fetch: 2.6.11
node-fetch: 2.6.12
p-filter: 2.1.0
p-map: 4.0.0
p-retry: 4.6.2
@@ -3779,27 +3779,27 @@ __metadata:
rimraf: 3.0.2
temp-dir: 2.0.0
tempy: 1.0.1
checksum: 2678c35d8ef3f5909eceec783908fdd52818ab5dc8520f69ff0ca39eaab9a2ae3a5652b85dd422f39f59014a4a9f9a6634965265901ca6a8b0915453e4e681d4
checksum: f60f78ded05803db1455e4beae6040c6cc86fcaef2e398b8ed0df2d8aca2352a8fbb2104b81531051aa3174cf93dd2bb7070017c6de67bc661255cebda371213
languageName: node
linkType: hard
"@prisma/generator-helper@npm:4.16.1":
version: 4.16.1
resolution: "@prisma/generator-helper@npm:4.16.1"
"@prisma/generator-helper@npm:5.0.0":
version: 5.0.0
resolution: "@prisma/generator-helper@npm:5.0.0"
dependencies:
"@prisma/debug": 4.16.1
"@prisma/debug": 5.0.0
"@types/cross-spawn": 6.0.2
cross-spawn: 7.0.3
kleur: 4.1.5
checksum: fa4ce40b39e2adbb5526c5c972d5c13222ef0708ef30c2aa282591f884fb6c4d06b7793f699010ffafa7f5934b9925fe669ef38413d30931dc7c15a911b99461
checksum: 3506c9f23d2a7afcf4b99ad264f15efc7873f5e2ff9a7246a770c9ea7e7df627226a2e736890556c495cae3b0d1feaa3fd087417974500feaea14fb6da9dd0b0
languageName: node
linkType: hard
"@prisma/get-platform@npm:4.16.1":
version: 4.16.1
resolution: "@prisma/get-platform@npm:4.16.1"
"@prisma/get-platform@npm:5.0.0":
version: 5.0.0
resolution: "@prisma/get-platform@npm:5.0.0"
dependencies:
"@prisma/debug": 4.16.1
"@prisma/debug": 5.0.0
escape-string-regexp: 4.0.0
execa: 5.1.1
fs-jetpack: 5.1.0
@@ -3809,22 +3809,22 @@ __metadata:
tempy: 1.0.1
terminal-link: 2.1.1
ts-pattern: 4.3.0
checksum: 93e08e4d52fc35550ce643c0f54f9d9bb027619448359adac6cd89817617d51791cf24a8873e703f1441afa386c975fc1212e37d46f8d6059ed29b16270b8527
checksum: a5917c29198ff11bac52772496afe6f55755b715b83ab94655b616e33eded80a9e82a987b8b8fde1a48ae09b055703ae2d3e51a11471d1f61341072b2af8dbae
languageName: node
linkType: hard
"@prisma/internals@npm:^4.16.1":
version: 4.16.1
resolution: "@prisma/internals@npm:4.16.1"
"@prisma/internals@npm:^5.0.0":
version: 5.0.0
resolution: "@prisma/internals@npm:5.0.0"
dependencies:
"@antfu/ni": 0.21.4
"@opentelemetry/api": 1.4.1
"@prisma/debug": 4.16.1
"@prisma/engines": 4.16.1
"@prisma/fetch-engine": 4.16.1
"@prisma/generator-helper": 4.16.1
"@prisma/get-platform": 4.16.1
"@prisma/prisma-fmt-wasm": 4.16.0-66.b20ead4d3ab9e78ac112966e242ded703f4a052c
"@prisma/debug": 5.0.0
"@prisma/engines": 5.0.0
"@prisma/fetch-engine": 5.0.0
"@prisma/generator-helper": 5.0.0
"@prisma/get-platform": 5.0.0
"@prisma/prisma-schema-wasm": 4.17.0-26.6b0aef69b7cdfc787f822ecd7cdc76d5f1991584
archiver: 5.3.1
arg: 5.0.2
checkpoint-client: 1.1.24
@@ -3843,7 +3843,7 @@ __metadata:
is-wsl: 2.2.0
kleur: 4.1.5
new-github-issue-url: 0.2.1
node-fetch: 2.6.11
node-fetch: 2.6.12
npm-packlist: 5.1.3
open: 7.4.2
p-map: 4.0.0
@@ -3860,16 +3860,16 @@ __metadata:
terminal-link: 2.1.1
tmp: 0.2.1
ts-pattern: 4.3.0
checksum: 15b697834b9feae4749c7a815cbbd0b69a9d371eb89d49146145b0c34c175d0beb0832215fcc5c4bd825e2595f479e1b53732c6f152160fbb45e919d5c145f79
checksum: 8d567b3cad3eaf28bb0248adf5e9f3250a21677470e8552ffbd30f8219f8af6ba7721eaf39980329a1181f5aea97daeb68dfb0af8a2d31dce8cc2a4f7f99d288
languageName: node
linkType: hard
"@prisma/migrate@npm:^4.16.1":
version: 4.16.1
resolution: "@prisma/migrate@npm:4.16.1"
"@prisma/migrate@npm:^5.0.0":
version: 5.0.0
resolution: "@prisma/migrate@npm:5.0.0"
dependencies:
"@prisma/debug": 4.16.1
"@prisma/get-platform": 4.16.1
"@prisma/debug": 5.0.0
"@prisma/get-platform": 5.0.0
"@sindresorhus/slugify": 1.1.2
arg: 5.0.2
execa: 5.1.1
@@ -3879,7 +3879,7 @@ __metadata:
indent-string: 4.0.0
kleur: 4.1.5
log-update: 4.0.0
mariadb: 3.1.2
mariadb: 3.2.0
mongoose: 6.11.2
mssql: 9.1.1
ora: 5.4.1
@@ -3892,14 +3892,14 @@ __metadata:
peerDependencies:
"@prisma/generator-helper": "*"
"@prisma/internals": "*"
checksum: eaf8ea05fbf29f4faedfa599057bec0f35be8f725b4017626a4bb9e27fe8621a617aae74ea5ac26328fbd65fa8345caad648cab3271c33c2f4aba3478051835e
checksum: 05f99a25da550130d698d1f15e5a0b1e366894b0ed2134e3bf34211b44fe572b0e86ce407692ed2445bd02776423e34d0d0ca3851bb8e4265df0a43f777793bc
languageName: node
linkType: hard
"@prisma/prisma-fmt-wasm@npm:4.16.0-66.b20ead4d3ab9e78ac112966e242ded703f4a052c":
version: 4.16.0-66.b20ead4d3ab9e78ac112966e242ded703f4a052c
resolution: "@prisma/prisma-fmt-wasm@npm:4.16.0-66.b20ead4d3ab9e78ac112966e242ded703f4a052c"
checksum: 7db5b834f49603cfaaedda897a41fc668e1aa4caf32ee9ca5deb6c6e63dfe059780f8c2324ab415c5fbc7d0f6e8ba2dd347a076d37f1df373be5279a80c6beee
"@prisma/prisma-schema-wasm@npm:4.17.0-26.6b0aef69b7cdfc787f822ecd7cdc76d5f1991584":
version: 4.17.0-26.6b0aef69b7cdfc787f822ecd7cdc76d5f1991584
resolution: "@prisma/prisma-schema-wasm@npm:4.17.0-26.6b0aef69b7cdfc787f822ecd7cdc76d5f1991584"
checksum: c0dffdde7b9e59cecea1c581829b1ebb55a934b8b5e7abf30d1e3d498cf3fcc6da39a3c320e1c52f9edb5e5cb08ff7f9bdd24e3ea97546ba4803d03bda5de066
languageName: node
linkType: hard
@@ -4380,6 +4380,15 @@ __metadata:
languageName: node
linkType: hard
"@types/dompurify@npm:^3.0.2":
version: 3.0.2
resolution: "@types/dompurify@npm:3.0.2"
dependencies:
"@types/trusted-types": "*"
checksum: dc017e16a46bcc77086af7d4dc5d2bf4102f16bf1a11b2937e90380da50e77716a2a608ff52990b1293250fefc2ad7593a1378fe07e3a7e21f200d702f0a7878
languageName: node
linkType: hard
"@types/estree-jsx@npm:^0.0.1":
version: 0.0.1
resolution: "@types/estree-jsx@npm:0.0.1"
@@ -4681,6 +4690,13 @@ __metadata:
languageName: node
linkType: hard
"@types/trusted-types@npm:*":
version: 2.0.3
resolution: "@types/trusted-types@npm:2.0.3"
checksum: 4794804bc4a4a173d589841b6d26cf455ff5dc4f3e704e847de7d65d215f2e7043d8757e4741ce3a823af3f08260a8d04a1a6e9c5ec9b20b7b04586956a6b005
languageName: node
linkType: hard
"@types/unist@npm:*, @types/unist@npm:^2.0.0":
version: 2.0.6
resolution: "@types/unist@npm:2.0.6"
@@ -4889,6 +4905,13 @@ __metadata:
languageName: node
linkType: hard
"abab@npm:^2.0.6":
version: 2.0.6
resolution: "abab@npm:2.0.6"
checksum: 6ffc1af4ff315066c62600123990d87551ceb0aafa01e6539da77b0f5987ac7019466780bf480f1787576d4385e3690c81ccc37cfda12819bf510b8ab47e5a3e
languageName: node
linkType: hard
"abbrev@npm:1, abbrev@npm:^1.0.0":
version: 1.1.1
resolution: "abbrev@npm:1.1.1"
@@ -6221,6 +6244,15 @@ __metadata:
languageName: node
linkType: hard
"cssstyle@npm:^3.0.0":
version: 3.0.0
resolution: "cssstyle@npm:3.0.0"
dependencies:
rrweb-cssom: ^0.6.0
checksum: 31f694dfed9998ed93570fe539610837b878193dd8487c33cb12db8004333c53c2a3904166288bbec68388c72fb01014d46d3243ddfb02fe845989d852c06f27
languageName: node
linkType: hard
"csstype@npm:3.0.9":
version: 3.0.9
resolution: "csstype@npm:3.0.9"
@@ -6249,6 +6281,17 @@ __metadata:
languageName: node
linkType: hard
"data-urls@npm:^4.0.0":
version: 4.0.0
resolution: "data-urls@npm:4.0.0"
dependencies:
abab: ^2.0.6
whatwg-mimetype: ^3.0.0
whatwg-url: ^12.0.0
checksum: 006e869b5bf079647949a3e9b1dd69d84b2d5d26e6b01c265485699bc96e83817d4b5aae758b2910a4c58c0601913f3a0034121c1ca2da268e9a244c57515b15
languageName: node
linkType: hard
"dayjs@npm:^1.11.8":
version: 1.11.8
resolution: "dayjs@npm:1.11.8"
@@ -6296,6 +6339,13 @@ __metadata:
languageName: node
linkType: hard
"decimal.js@npm:^10.4.3":
version: 10.4.3
resolution: "decimal.js@npm:10.4.3"
checksum: 796404dcfa9d1dbfdc48870229d57f788b48c21c603c3f6554a1c17c10195fc1024de338b0cf9e1efe0c7c167eeb18f04548979bcc5fdfabebb7cc0ae3287bae
languageName: node
linkType: hard
"decode-named-character-reference@npm:^1.0.0":
version: 1.0.2
resolution: "decode-named-character-reference@npm:1.0.2"
@@ -6590,6 +6640,15 @@ __metadata:
languageName: node
linkType: hard
"domexception@npm:^4.0.0":
version: 4.0.0
resolution: "domexception@npm:4.0.0"
dependencies:
webidl-conversions: ^7.0.0
checksum: ddbc1268edf33a8ba02ccc596735ede80375ee0cf124b30d2f05df5b464ba78ef4f49889b6391df4a04954e63d42d5631c7fcf8b1c4f12bc531252977a5f13d5
languageName: node
linkType: hard
"domhandler@npm:4.3.1, domhandler@npm:^4.2.0, domhandler@npm:^4.2.2":
version: 4.3.1
resolution: "domhandler@npm:4.3.1"
@@ -6599,6 +6658,13 @@ __metadata:
languageName: node
linkType: hard
"dompurify@npm:^3.0.5":
version: 3.0.5
resolution: "dompurify@npm:3.0.5"
checksum: 2d9421570c833ce26ce7022955241749b646d41e8bf453f42ede9f22d0e98af482cedb7dfbf8129419eb48b351c1d677a08fc9f1cd91836ce7f6c1807a0676b2
languageName: node
linkType: hard
"domutils@npm:^2.8.0":
version: 2.8.0
resolution: "domutils@npm:2.8.0"
@@ -6745,6 +6811,13 @@ __metadata:
languageName: node
linkType: hard
"entities@npm:^4.4.0":
version: 4.5.0
resolution: "entities@npm:4.5.0"
checksum: 853f8ebd5b425d350bffa97dd6958143179a5938352ccae092c62d1267c4e392a039be1bae7d51b6e4ffad25f51f9617531fedf5237f15df302ccfb452cbf2d7
languageName: node
linkType: hard
"env-paths@npm:2.2.1, env-paths@npm:^2.2.0":
version: 2.2.1
resolution: "env-paths@npm:2.2.1"
@@ -8637,6 +8710,15 @@ __metadata:
languageName: node
linkType: hard
"html-encoding-sniffer@npm:^3.0.0":
version: 3.0.0
resolution: "html-encoding-sniffer@npm:3.0.0"
dependencies:
whatwg-encoding: ^2.0.0
checksum: 8d806aa00487e279e5ccb573366a951a9f68f65c90298eac9c3a2b440a7ffe46615aff2995a2f61c6746c639234e6179a97e18ca5ccbbf93d3725ef2099a4502
languageName: node
linkType: hard
"html-react-parser@npm:1.4.12":
version: 1.4.12
resolution: "html-react-parser@npm:1.4.12"
@@ -8740,7 +8822,7 @@ __metadata:
languageName: node
linkType: hard
"https-proxy-agent@npm:5, https-proxy-agent@npm:^5.0.0":
"https-proxy-agent@npm:5, https-proxy-agent@npm:^5.0.0, https-proxy-agent@npm:^5.0.1":
version: 5.0.1
resolution: "https-proxy-agent@npm:5.0.1"
dependencies:
@@ -8792,7 +8874,7 @@ __metadata:
languageName: node
linkType: hard
"iconv-lite@npm:^0.6.2, iconv-lite@npm:^0.6.3":
"iconv-lite@npm:0.6.3, iconv-lite@npm:^0.6.2, iconv-lite@npm:^0.6.3":
version: 0.6.3
resolution: "iconv-lite@npm:0.6.3"
dependencies:
@@ -9223,6 +9305,13 @@ __metadata:
languageName: node
linkType: hard
"is-potential-custom-element-name@npm:^1.0.1":
version: 1.0.1
resolution: "is-potential-custom-element-name@npm:1.0.1"
checksum: ced7bbbb6433a5b684af581872afe0e1767e2d1146b2207ca0068a648fb5cab9d898495d1ac0583524faaf24ca98176a7d9876363097c2d14fee6dd324f3a1ab
languageName: node
linkType: hard
"is-reference@npm:^3.0.0":
version: 3.0.1
resolution: "is-reference@npm:3.0.1"
@@ -9380,6 +9469,17 @@ __metadata:
languageName: node
linkType: hard
"isomorphic-dompurify@npm:^1.8.0":
version: 1.8.0
resolution: "isomorphic-dompurify@npm:1.8.0"
dependencies:
"@types/dompurify": ^3.0.2
dompurify: ^3.0.5
jsdom: ^22.1.0
checksum: 5ac39dd13bf9923fdc2932e18cf5f7c4e777fa8c2ffd018ee755207b53cdb739d7e6f2e36103b7bd0a1d2eceb5172954ccd40873cea8c772b42c341a19597897
languageName: node
linkType: hard
"jackspeak@npm:^2.0.3":
version: 2.2.1
resolution: "jackspeak@npm:2.2.1"
@@ -9439,6 +9539,42 @@ __metadata:
languageName: node
linkType: hard
"jsdom@npm:^22.1.0":
version: 22.1.0
resolution: "jsdom@npm:22.1.0"
dependencies:
abab: ^2.0.6
cssstyle: ^3.0.0
data-urls: ^4.0.0
decimal.js: ^10.4.3
domexception: ^4.0.0
form-data: ^4.0.0
html-encoding-sniffer: ^3.0.0
http-proxy-agent: ^5.0.0
https-proxy-agent: ^5.0.1
is-potential-custom-element-name: ^1.0.1
nwsapi: ^2.2.4
parse5: ^7.1.2
rrweb-cssom: ^0.6.0
saxes: ^6.0.0
symbol-tree: ^3.2.4
tough-cookie: ^4.1.2
w3c-xmlserializer: ^4.0.0
webidl-conversions: ^7.0.0
whatwg-encoding: ^2.0.0
whatwg-mimetype: ^3.0.0
whatwg-url: ^12.0.1
ws: ^8.13.0
xml-name-validator: ^4.0.0
peerDependencies:
canvas: ^2.5.0
peerDependenciesMeta:
canvas:
optional: true
checksum: d955ab83a6dad3e6af444098d30647c719bbb4cf97de053aa5751c03c8d6f3283d8c4d7fc2774c181f1d432fb0250e7332bc159e6b466424f4e337d73adcbf30
languageName: node
linkType: hard
"jsesc@npm:3.0.2":
version: 3.0.2
resolution: "jsesc@npm:3.0.2"
@@ -10004,16 +10140,16 @@ __metadata:
languageName: node
linkType: hard
"mariadb@npm:3.1.2":
version: 3.1.2
resolution: "mariadb@npm:3.1.2"
"mariadb@npm:3.2.0":
version: 3.2.0
resolution: "mariadb@npm:3.2.0"
dependencies:
"@types/geojson": ^7946.0.10
"@types/node": ^17.0.45
denque: ^2.1.0
iconv-lite: ^0.6.3
lru-cache: ^7.14.0
checksum: 70079e09ecc2ae4113b317912419053645aabbcc3e584c7c1dba89306cb0af1a47438ccbf03f1c97f8fee07b952c33952edbc100847d55a30aa9d897b04c729f
checksum: 971a84cf42e026ddfd209450cbf98b2af00a6cf9a49fe245b2662e767196a7a8f26ab728cdc6b715aafbb40631f57754e19251f9e38e679106c7d190106d4eee
languageName: node
linkType: hard
@@ -11332,6 +11468,20 @@ __metadata:
languageName: node
linkType: hard
"node-fetch@npm:2.6.12":
version: 2.6.12
resolution: "node-fetch@npm:2.6.12"
dependencies:
whatwg-url: ^5.0.0
peerDependencies:
encoding: ^0.1.0
peerDependenciesMeta:
encoding:
optional: true
checksum: 3bc1655203d47ee8e313c0d96664b9673a3d4dd8002740318e9d27d14ef306693a4b2ef8d6525775056fd912a19e23f3ac0d7111ad8925877b7567b29a625592
languageName: node
linkType: hard
"node-gyp@npm:latest":
version: 9.4.0
resolution: "node-gyp@npm:9.4.0"
@@ -11501,6 +11651,13 @@ __metadata:
languageName: node
linkType: hard
"nwsapi@npm:^2.2.4":
version: 2.2.7
resolution: "nwsapi@npm:2.2.7"
checksum: cab25f7983acec7e23490fec3ef7be608041b460504229770e3bfcf9977c41d6fe58f518994d3bd9aa3a101f501089a3d4a63536f4ff8ae4b8c4ca23bdbfda4e
languageName: node
linkType: hard
"object-assign@npm:^4.0.1, object-assign@npm:^4.1.0, object-assign@npm:^4.1.1":
version: 4.1.1
resolution: "object-assign@npm:4.1.1"
@@ -11912,6 +12069,15 @@ __metadata:
languageName: node
linkType: hard
"parse5@npm:^7.1.2":
version: 7.1.2
resolution: "parse5@npm:7.1.2"
dependencies:
entities: ^4.4.0
checksum: 59465dd05eb4c5ec87b76173d1c596e152a10e290b7abcda1aecf0f33be49646ea74840c69af975d7887543ea45564801736356c568d6b5e71792fd0f4055713
languageName: node
linkType: hard
"parseurl@npm:~1.3.3":
version: 1.3.3
resolution: "parseurl@npm:1.3.3"
@@ -12369,15 +12535,14 @@ __metadata:
languageName: node
linkType: hard
"prisma@npm:^4.16.1":
version: 4.16.1
resolution: "prisma@npm:4.16.1"
"prisma@npm:^5.0.0":
version: 5.0.0
resolution: "prisma@npm:5.0.0"
dependencies:
"@prisma/engines": 4.16.1
"@prisma/engines": 5.0.0
bin:
prisma: build/index.js
prisma2: build/index.js
checksum: eb0f43970464fbaa6ba190a714f23d00acd39dcc14fd8a7183dffc51a548cc32040cc5360415787578749862af7501a7bedcf9f2a0e64a3f82e3af402e9439d7
checksum: fdc62377853d25b4db664c736fd0b08d2b0c6db5752e6f6c6ec3bda77634cfb79e6f49d52d4b8f54ddb8ec9c28fc3fb0c13f95caf61085447d0929e258af9284
languageName: node
linkType: hard
@@ -12473,6 +12638,13 @@ __metadata:
languageName: node
linkType: hard
"psl@npm:^1.1.33":
version: 1.9.0
resolution: "psl@npm:1.9.0"
checksum: 20c4277f640c93d393130673f392618e9a8044c6c7bf61c53917a0fddb4952790f5f362c6c730a9c32b124813e173733f9895add8d26f566ed0ea0654b2e711d
languageName: node
linkType: hard
"pump@npm:^2.0.0":
version: 2.0.1
resolution: "pump@npm:2.0.1"
@@ -12504,7 +12676,7 @@ __metadata:
languageName: node
linkType: hard
"punycode@npm:^2.1.0, punycode@npm:^2.1.1":
"punycode@npm:^2.1.0, punycode@npm:^2.1.1, punycode@npm:^2.3.0":
version: 2.3.0
resolution: "punycode@npm:2.3.0"
checksum: 39f760e09a2a3bbfe8f5287cf733ecdad69d6af2fe6f97ca95f24b8921858b91e9ea3c9eeec6e08cede96181b3bb33f95c6ffd8c77e63986508aa2e8159fa200
@@ -12520,6 +12692,13 @@ __metadata:
languageName: node
linkType: hard
"querystringify@npm:^2.1.1":
version: 2.2.0
resolution: "querystringify@npm:2.2.0"
checksum: 5641ea231bad7ef6d64d9998faca95611ed4b11c2591a8cae741e178a974f6a8e0ebde008475259abe1621cb15e692404e6b6626e927f7b849d5c09392604b15
languageName: node
linkType: hard
"queue-microtask@npm:^1.2.2":
version: 1.2.3
resolution: "queue-microtask@npm:1.2.3"
@@ -13021,6 +13200,13 @@ __metadata:
languageName: node
linkType: hard
"requires-port@npm:^1.0.0":
version: 1.0.0
resolution: "requires-port@npm:1.0.0"
checksum: eee0e303adffb69be55d1a214e415cf42b7441ae858c76dfc5353148644f6fd6e698926fc4643f510d5c126d12a705e7c8ed7e38061113bdf37547ab356797ff
languageName: node
linkType: hard
"resolve-alpn@npm:^1.0.0":
version: 1.2.1
resolution: "resolve-alpn@npm:1.2.1"
@@ -13199,6 +13385,13 @@ __metadata:
languageName: node
linkType: hard
"rrweb-cssom@npm:^0.6.0":
version: 0.6.0
resolution: "rrweb-cssom@npm:0.6.0"
checksum: 182312f6e4f41d18230ccc34f14263bc8e8a6b9d30ee3ec0d2d8e643c6f27964cd7a8d638d4a00e988d93e8dc55369f4ab5a473ccfeff7a8bab95b36d2b5499c
languageName: node
linkType: hard
"run-applescript@npm:^5.0.0":
version: 5.0.0
resolution: "run-applescript@npm:5.0.0"
@@ -13283,6 +13476,15 @@ __metadata:
languageName: node
linkType: hard
"saxes@npm:^6.0.0":
version: 6.0.0
resolution: "saxes@npm:6.0.0"
dependencies:
xmlchars: ^2.2.0
checksum: d3fa3e2aaf6c65ed52ee993aff1891fc47d5e47d515164b5449cbf5da2cbdc396137e55590472e64c5c436c14ae64a8a03c29b9e7389fc6f14035cf4e982ef3b
languageName: node
linkType: hard
"scheduler@npm:^0.23.0":
version: 0.23.0
resolution: "scheduler@npm:0.23.0"
@@ -14017,6 +14219,13 @@ __metadata:
languageName: node
linkType: hard
"symbol-tree@npm:^3.2.4":
version: 3.2.4
resolution: "symbol-tree@npm:3.2.4"
checksum: 6e8fc7e1486b8b54bea91199d9535bb72f10842e40c79e882fc94fb7b14b89866adf2fd79efa5ebb5b658bc07fb459ccce5ac0e99ef3d72f474e74aaf284029d
languageName: node
linkType: hard
"synckit@npm:^0.8.5":
version: 0.8.5
resolution: "synckit@npm:0.8.5"
@@ -14264,6 +14473,18 @@ __metadata:
languageName: node
linkType: hard
"tough-cookie@npm:^4.1.2":
version: 4.1.3
resolution: "tough-cookie@npm:4.1.3"
dependencies:
psl: ^1.1.33
punycode: ^2.1.1
universalify: ^0.2.0
url-parse: ^1.5.3
checksum: c9226afff36492a52118432611af083d1d8493a53ff41ec4ea48e5b583aec744b989e4280bcf476c910ec1525a89a4a0f1cae81c08b18fb2ec3a9b3a72b91dcc
languageName: node
linkType: hard
"tr46@npm:^1.0.1":
version: 1.0.1
resolution: "tr46@npm:1.0.1"
@@ -14282,6 +14503,15 @@ __metadata:
languageName: node
linkType: hard
"tr46@npm:^4.1.1":
version: 4.1.1
resolution: "tr46@npm:4.1.1"
dependencies:
punycode: ^2.3.0
checksum: aeeb821ac2cd792e63ec84888b4fd6598ac6ed75d861579e21a5cf9d4ee78b2c6b94e7d45036f2ca2088bc85b9b46560ad23c4482979421063b24137349dbd96
languageName: node
linkType: hard
"tr46@npm:~0.0.3":
version: 0.0.3
resolution: "tr46@npm:0.0.3"
@@ -14711,6 +14941,13 @@ __metadata:
languageName: node
linkType: hard
"universalify@npm:^0.2.0":
version: 0.2.0
resolution: "universalify@npm:0.2.0"
checksum: e86134cb12919d177c2353196a4cc09981524ee87abf621f7bc8d249dbbbebaec5e7d1314b96061497981350df786e4c5128dbf442eba104d6e765bc260678b5
languageName: node
linkType: hard
"universalify@npm:^2.0.0":
version: 2.0.0
resolution: "universalify@npm:2.0.0"
@@ -14755,6 +14992,16 @@ __metadata:
languageName: node
linkType: hard
"url-parse@npm:^1.5.3":
version: 1.5.10
resolution: "url-parse@npm:1.5.10"
dependencies:
querystringify: ^2.1.1
requires-port: ^1.0.0
checksum: fbdba6b1d83336aca2216bbdc38ba658d9cfb8fc7f665eb8b17852de638ff7d1a162c198a8e4ed66001ddbf6c9888d41e4798912c62b4fd777a31657989f7bdf
languageName: node
linkType: hard
"use-callback-ref@npm:^1.3.0":
version: 1.3.0
resolution: "use-callback-ref@npm:1.3.0"
@@ -14991,6 +15238,15 @@ __metadata:
languageName: node
linkType: hard
"w3c-xmlserializer@npm:^4.0.0":
version: 4.0.0
resolution: "w3c-xmlserializer@npm:4.0.0"
dependencies:
xml-name-validator: ^4.0.0
checksum: eba070e78deb408ae8defa4d36b429f084b2b47a4741c4a9be3f27a0a3d1845e277e3072b04391a138f7e43776842627d1334e448ff13ff90ad9fb1214ee7091
languageName: node
linkType: hard
"watchpack@npm:2.4.0":
version: 2.4.0
resolution: "watchpack@npm:2.4.0"
@@ -15031,6 +15287,22 @@ __metadata:
languageName: node
linkType: hard
"whatwg-encoding@npm:^2.0.0":
version: 2.0.0
resolution: "whatwg-encoding@npm:2.0.0"
dependencies:
iconv-lite: 0.6.3
checksum: 7087810c410aa9b689cbd6af8773341a53cdc1f3aae2a882c163bd5522ec8ca4cdfc269aef417a5792f411807d5d77d50df4c24e3abb00bb60192858a40cc675
languageName: node
linkType: hard
"whatwg-mimetype@npm:^3.0.0":
version: 3.0.0
resolution: "whatwg-mimetype@npm:3.0.0"
checksum: ce08bbb36b6aaf64f3a84da89707e3e6a31e5ab1c1a2379fd68df79ba712a4ab090904f0b50e6693b0dafc8e6343a6157e40bf18fdffd26e513cf95ee2a59824
languageName: node
linkType: hard
"whatwg-url@npm:^11.0.0":
version: 11.0.0
resolution: "whatwg-url@npm:11.0.0"
@@ -15041,6 +15313,16 @@ __metadata:
languageName: node
linkType: hard
"whatwg-url@npm:^12.0.0, whatwg-url@npm:^12.0.1":
version: 12.0.1
resolution: "whatwg-url@npm:12.0.1"
dependencies:
tr46: ^4.1.1
webidl-conversions: ^7.0.0
checksum: 8698993b763c1e7eda5ed16c31dab24bca6489626aca7caf8b5a2b64684dda6578194786f10ec42ceb1c175feea16d0a915096e6419e08d154ce551c43176972
languageName: node
linkType: hard
"whatwg-url@npm:^5.0.0":
version: 5.0.0
resolution: "whatwg-url@npm:5.0.0"
@@ -15194,6 +15476,21 @@ __metadata:
languageName: node
linkType: hard
"ws@npm:^8.13.0":
version: 8.13.0
resolution: "ws@npm:8.13.0"
peerDependencies:
bufferutil: ^4.0.1
utf-8-validate: ">=5.0.2"
peerDependenciesMeta:
bufferutil:
optional: true
utf-8-validate:
optional: true
checksum: 53e991bbf928faf5dc6efac9b8eb9ab6497c69feeb94f963d648b7a3530a720b19ec2e0ec037344257e05a4f35bd9ad04d9de6f289615ffb133282031b18c61c
languageName: node
linkType: hard
"xdm@npm:^2.0.0":
version: 2.1.0
resolution: "xdm@npm:2.1.0"
@@ -15227,6 +15524,20 @@ __metadata:
languageName: node
linkType: hard
"xml-name-validator@npm:^4.0.0":
version: 4.0.0
resolution: "xml-name-validator@npm:4.0.0"
checksum: af100b79c29804f05fa35aa3683e29a321db9b9685d5e5febda3fa1e40f13f85abc40f45a6b2bf7bee33f68a1dc5e8eaef4cec100a304a9db565e6061d4cb5ad
languageName: node
linkType: hard
"xmlchars@npm:^2.2.0":
version: 2.2.0
resolution: "xmlchars@npm:2.2.0"
checksum: 8c70ac94070ccca03f47a81fcce3b271bd1f37a591bf5424e787ae313fcb9c212f5f6786e1fa82076a2c632c0141552babcd85698c437506dfa6ae2d58723062
languageName: node
linkType: hard
"xregexp@npm:2.0.0":
version: 2.0.0
resolution: "xregexp@npm:2.0.0"
@@ -15311,9 +15622,9 @@ __metadata:
"@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
"@remix-run/dev": ^1.16.1
"@remix-run/eslint-config": ^1.16.1
"@tabler/icons-react": ^2.23.0
@@ -15334,13 +15645,14 @@ __metadata:
eslint: ^8.41.0
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
multer: ^1.4.5-lts.1
next: ^13.4.7
npm-run-all: ^4.1.5
prisma: ^4.16.1
prisma: ^5.0.0
react: ^18.2.0
react-dom: ^18.2.0
react-markdown: ^8.0.7