Skip to content

Commit e96015f

Browse files
committed
fix: refactor + perf
1 parent d4d1cdc commit e96015f

File tree

18 files changed

+535
-525
lines changed

18 files changed

+535
-525
lines changed

src/client/pages/auth/login.tsx

Lines changed: 83 additions & 353 deletions
Large diffs are not rendered by default.

src/client/pages/auth/logout.tsx

Lines changed: 0 additions & 35 deletions
This file was deleted.

src/client/routes.tsx

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import DashboardErrorBoundary from './error/DashboardErrorBoundary';
66
import RootErrorBoundary from './error/RootErrorBoundary';
77
import FourOhFour from './pages/404';
88
import Login from './pages/auth/login';
9-
import Logout from './pages/auth/logout';
109
import Root from './Root';
1110

1211
export async function dashboardLoader() {
@@ -38,7 +37,6 @@ export const router = createBrowserRouter([
3837
path: '/auth',
3938
children: [
4039
{ path: 'login', Component: Login },
41-
{ path: 'logout', Component: Logout },
4240
{ path: 'register', lazy: () => import('./pages/auth/register') },
4341
{
4442
path: 'setup',

src/components/Layout.tsx

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ import ConfigProvider from './ConfigProvider';
5151
import VersionBadge from './VersionBadge';
5252
import { Link, useLoaderData } from 'react-router-dom';
5353
import { dashboardLoader } from '../client/routes';
54+
import { useLogout } from '@/lib/hooks/useLogout';
5455

5556
type NavLinks = {
5657
label: string;
@@ -158,6 +159,7 @@ export default function Layout() {
158159
const clipboard = useClipboard();
159160
const setUser = useUserStore((s) => s.setUser);
160161
const location = useLocation();
162+
const logout = useLogout();
161163

162164
const loaderData = useLoaderData<typeof dashboardLoader>();
163165
const config = loaderData.config;
@@ -304,12 +306,7 @@ export default function Layout() {
304306
)}
305307

306308
<Menu.Divider />
307-
<Menu.Item
308-
color='red'
309-
leftSection={<IconLogout size='1rem' />}
310-
component={Link}
311-
to='/auth/logout'
312-
>
309+
<Menu.Item color='red' leftSection={<IconLogout size='1rem' />} onClick={logout}>
313310
Logout
314311
</Menu.Item>
315312
</Menu.Dropdown>

src/components/file/DashboardFile/EditFileDetailsModal.tsx

Lines changed: 61 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
import { File } from '@/lib/db/models/file';
22
import { fetchApi } from '@/lib/fetchApi';
3+
import useObjectState from '@/lib/hooks/useObjectState';
34
import { Button, Divider, Modal, NumberInput, PasswordInput, Stack, TextInput } from '@mantine/core';
45
import { showNotification } from '@mantine/notifications';
56
import { IconEye, IconKey, IconPencil, IconPencilOff, IconTrashFilled } from '@tabler/icons-react';
6-
import { useEffect, useState } from 'react';
7+
import { useEffect } from 'react';
78
import { mutateFiles } from '../actions';
89

910
export default function EditFileDetailsModal({
@@ -15,13 +16,41 @@ export default function EditFileDetailsModal({
1516
file: File | null;
1617
onClose: () => void;
1718
}) {
18-
if (!file) return null;
19+
const [formData, setFormData] = useObjectState<{
20+
name: string;
21+
maxViews: number | null;
22+
password: string | null;
23+
originalName: string | null;
24+
type: string | null;
25+
}>({
26+
name: file?.name ?? '',
27+
maxViews: file?.maxViews ?? null,
28+
password: file?.password ? '' : null,
29+
originalName: file?.originalName ?? null,
30+
type: file?.type ?? null,
31+
});
1932

20-
const [name, setName] = useState<string>(file.name ?? '');
21-
const [maxViews, setMaxViews] = useState<number | null>(file?.maxViews ?? null);
22-
const [password, setPassword] = useState<string | null>('');
23-
const [originalName, setOriginalName] = useState<string | null>(file?.originalName ?? null);
24-
const [type, setType] = useState<string | null>(file?.type ?? null);
33+
useEffect(() => {
34+
if (open) {
35+
setFormData({
36+
name: file?.name ?? '',
37+
maxViews: file?.maxViews ?? null,
38+
password: file?.password ? '' : null,
39+
originalName: file?.originalName ?? null,
40+
type: file?.type ?? null,
41+
});
42+
} else {
43+
setFormData({
44+
name: '',
45+
maxViews: null,
46+
password: null,
47+
originalName: null,
48+
type: null,
49+
});
50+
}
51+
}, [open, file]);
52+
53+
if (!file) return null;
2554

2655
const handleRemovePassword = async () => {
2756
if (!file.password) return;
@@ -58,12 +87,12 @@ export default function EditFileDetailsModal({
5887
name?: string;
5988
} = {};
6089

61-
if (maxViews !== null) data['maxViews'] = maxViews;
62-
if (originalName !== null) data['originalName'] = originalName?.trim();
63-
if (type !== null) data['type'] = type?.trim();
64-
if (name !== file.name) data['name'] = name.trim();
90+
if (formData.maxViews !== null) data['maxViews'] = formData.maxViews;
91+
if (formData.originalName !== null) data['originalName'] = formData.originalName?.trim();
92+
if (formData.type !== null) data['type'] = formData.type?.trim();
93+
if (formData.name !== file.name) data['name'] = formData.name.trim();
6594

66-
const passwordTrimmed = password?.trim();
95+
const passwordTrimmed = formData.password?.trim();
6796
if (passwordTrimmed !== '') data['password'] = passwordTrimmed;
6897

6998
const { error } = await fetchApi(`/api/user/files/${file.id}`, 'PATCH', data);
@@ -85,47 +114,40 @@ export default function EditFileDetailsModal({
85114

86115
onClose();
87116

88-
setPassword(null);
117+
setFormData('password', null);
89118
mutateFiles();
90119
}
91120
};
92121

93-
useEffect(() => {
94-
if (open) {
95-
setName(file.name ?? '');
96-
setMaxViews(file.maxViews ?? null);
97-
setPassword(file.password ? '' : null);
98-
setOriginalName(file.originalName ?? null);
99-
setType(file.type ?? null);
100-
}
101-
}, [open, file]);
102-
103122
return (
104123
<Modal zIndex={300} title={`Editing "${file.name}"`} onClose={onClose} opened={open}>
105124
<Stack gap='xs' my='sm'>
106125
<TextInput
107126
label='Name'
108127
description='Rename the file.'
109-
value={name}
110-
onChange={(event) => setName(event.currentTarget.value.trim())}
128+
value={formData.name}
129+
onChange={(event) => setFormData('name', event.currentTarget.value.trim())}
111130
/>
112131

113132
<NumberInput
114133
label='Max Views'
115134
placeholder='Unlimited'
116135
description='The maximum number of views this file can have before it is deleted. Leave blank to allow as many views as you want.'
117136
min={0}
118-
value={maxViews || ''}
119-
onChange={(value) => setMaxViews(value === '' ? null : Number(value))}
137+
value={formData.maxViews || ''}
138+
onChange={(value) => setFormData('maxViews', value === '' ? null : Number(value))}
120139
leftSection={<IconEye size='1rem' />}
121140
/>
122141

123142
<TextInput
124143
label='Original Name'
125144
description='Add an original name. When downloading this file, instead of using the generated file name (if chosen), it will download with this "original name" instead.'
126-
value={originalName ?? ''}
145+
value={formData.originalName ?? ''}
127146
onChange={(event) =>
128-
setOriginalName(event.currentTarget.value.trim() === '' ? null : event.currentTarget.value.trim())
147+
setFormData(
148+
'originalName',
149+
event.currentTarget.value.trim() === '' ? null : event.currentTarget.value.trim(),
150+
)
129151
}
130152
/>
131153

@@ -137,9 +159,12 @@ export default function EditFileDetailsModal({
137159
doing, this can mess with how Zipline renders specific file types.
138160
</>
139161
}
140-
value={type ?? ''}
162+
value={formData.type ?? ''}
141163
onChange={(event) =>
142-
setType(event.currentTarget.value.trim() === '' ? null : event.currentTarget.value.trim())
164+
setFormData(
165+
'type',
166+
event.currentTarget.value.trim() === '' ? null : event.currentTarget.value.trim(),
167+
)
143168
}
144169
c='red'
145170
/>
@@ -159,10 +184,13 @@ export default function EditFileDetailsModal({
159184
<PasswordInput
160185
label='Password'
161186
description='Set a password for this file. Leave blank to disable password protection.'
162-
value={password ?? ''}
187+
value={formData.password ?? ''}
163188
autoComplete='off'
164189
onChange={(event) =>
165-
setPassword(event.currentTarget.value.trim() === '' ? null : event.currentTarget.value.trim())
190+
setFormData(
191+
'password',
192+
event.currentTarget.value.trim() === '' ? null : event.currentTarget.value.trim(),
193+
)
166194
}
167195
leftSection={<IconKey size='1rem' />}
168196
/>

src/components/pages/files/views/FileTable.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
import FolderComboboxOptions from '@/components/folders/FolderComboboxOptions';
21
import RelativeDate from '@/components/RelativeDate';
32
import { addMultipleToFolder, copyFile, deleteFile, downloadFile } from '@/components/file/actions';
3+
import FolderComboboxOptions from '@/components/folders/FolderComboboxOptions';
44
import { Response } from '@/lib/api/response';
55
import { bytes } from '@/lib/bytes';
66
import { type File } from '@/lib/db/models/file';
@@ -111,7 +111,7 @@ function TagsFilter({
111111
const combobox = useCombobox();
112112
const { data: tags } = useSWR<Extract<Response['/api/user/tags'], Tag[]>>('/api/user/tags');
113113

114-
const [value, setValue] = useState(searchQuery.tags.split(','));
114+
const [value, setValue] = useState(() => searchQuery.tags.split(','));
115115
const handleValueSelect = (val: string) => {
116116
setValue((current) => (current.includes(val) ? current.filter((v) => v !== val) : [...current, val]));
117117
};
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import { Stack, TextInput, PasswordInput, Button } from '@mantine/core';
2+
import { UseFormReturnType } from '@mantine/form';
3+
4+
export default function LocalLogin({
5+
form,
6+
onSubmit,
7+
loading,
8+
hasBackground,
9+
}: {
10+
form: UseFormReturnType<any>;
11+
onSubmit: (values: any) => void;
12+
loading: boolean;
13+
hasBackground: boolean;
14+
}) {
15+
return (
16+
<form onSubmit={form.onSubmit((v) => onSubmit(v))}>
17+
<Stack my='sm'>
18+
<TextInput
19+
size='md'
20+
placeholder='Enter your username...'
21+
autoComplete='username'
22+
styles={{
23+
input: { backgroundColor: hasBackground ? 'transparent' : undefined },
24+
}}
25+
{...form.getInputProps('username')}
26+
/>
27+
28+
<PasswordInput
29+
size='md'
30+
placeholder='Enter your password...'
31+
autoComplete='current-password'
32+
styles={{
33+
input: { backgroundColor: hasBackground ? 'transparent' : undefined },
34+
}}
35+
{...form.getInputProps('password')}
36+
/>
37+
38+
<Button
39+
size='md'
40+
fullWidth
41+
type='submit'
42+
loading={loading}
43+
variant={hasBackground ? 'outline' : 'filled'}
44+
>
45+
Login
46+
</Button>
47+
</Stack>
48+
</form>
49+
);
50+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import { Button } from '@mantine/core';
2+
import { IconKey } from '@tabler/icons-react';
3+
import { useState } from 'react';
4+
import { startAuthentication } from '@simplewebauthn/browser';
5+
import { fetchApi } from '@/lib/fetchApi';
6+
import { notifications } from '@mantine/notifications';
7+
import { getWebClient } from '@/lib/api/detect';
8+
9+
export default function PasskeyAuthButton({ onAuthSuccess }: { onAuthSuccess: (data: any) => void }) {
10+
const [loading, setLoading] = useState(false);
11+
const [errored, setErrored] = useState(false);
12+
13+
const handleLogin = async () => {
14+
setLoading(true);
15+
try {
16+
const { data: options } = await fetchApi<any>('/api/auth/webauthn/options', 'GET');
17+
const res = await startAuthentication({ optionsJSON: options.options });
18+
19+
const { data, error } = await fetchApi<any>(
20+
'/api/auth/webauthn',
21+
'POST',
22+
{ response: res },
23+
{ 'x-zipline-client': JSON.stringify(getWebClient()) },
24+
);
25+
26+
if (error) throw new Error(error.error);
27+
onAuthSuccess(data);
28+
} catch (e: any) {
29+
setErrored(true);
30+
setTimeout(() => setErrored(false), 3000);
31+
notifications.show({ title: 'Auth Failed', message: e.message, color: 'red' });
32+
} finally {
33+
setLoading(false);
34+
}
35+
};
36+
37+
return (
38+
<Button
39+
onClick={handleLogin}
40+
size='md'
41+
fullWidth
42+
variant='outline'
43+
leftSection={<IconKey size='1rem' />}
44+
color={errored ? 'red' : undefined}
45+
loading={loading}
46+
>
47+
Login with passkey
48+
</Button>
49+
);
50+
}

0 commit comments

Comments
 (0)