Skip to content

Commit 243818a

Browse files
authored
Merge pull request #1234 from joshunrau/legacy-login-mode
add legacy login mode to playground for old odc instances and use indexdb for multi-gigabyte storage capacity (see new storage page)
2 parents f7c73d8 + 349e7a2 commit 243818a

File tree

6 files changed

+192
-65
lines changed

6 files changed

+192
-65
lines changed

apps/playground/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
"@opendatacapture/schemas": "workspace:*",
2424
"axios": "catalog:",
2525
"esbuild-wasm": "catalog:",
26+
"idb-keyval": "^6.2.2",
2627
"immer": "^10.1.1",
2728
"jszip": "^3.10.1",
2829
"jwt-decode": "^4.0.0",

apps/playground/src/components/Header/ActionsDropdown/ActionsDropdown.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { useAppStore } from '@/store';
88
import { DeleteInstrumentDialog } from './DeleteInstrumentDialog';
99
import { LoginDialog } from './LoginDialog';
1010
import { RestoreDefaultsDialog } from './RestoreDefaultsDialog';
11+
import { StorageUsageDialog } from './StorageUsageDialog';
1112
import { UploadBundleDialog } from './UploadBundleDialog';
1213
import { UserSettingsDialog } from './UserSettingsDialog';
1314

@@ -17,6 +18,7 @@ export const ActionsDropdown = () => {
1718
const [showRestoreDefaultsDialog, setShowRestoreDefaultsDialog] = useState(false);
1819
const [showUploadBundleDialog, setShowUploadBundleDialog] = useState(false);
1920
const [showLoginDialog, setShowLoginDialog] = useState(false);
21+
const [showStorageUsageDialog, setShowStorageUsageDialog] = useState(false);
2022

2123
const selectedInstrument = useAppStore((store) => store.selectedInstrument);
2224

@@ -54,6 +56,11 @@ export const ActionsDropdown = () => {
5456
User Settings
5557
</button>
5658
</DropdownMenu.Item>
59+
<DropdownMenu.Item asChild onSelect={() => setShowStorageUsageDialog(true)}>
60+
<button className="w-full cursor-pointer disabled:cursor-not-allowed disabled:opacity-50" type="button">
61+
Storage Usage
62+
</button>
63+
</DropdownMenu.Item>
5764
<DropdownMenu.Separator />
5865
<DropdownMenu.Item asChild onSelect={() => setShowDeleteInstrumentDialog(true)}>
5966
<button
@@ -86,6 +93,7 @@ export const ActionsDropdown = () => {
8693
}}
8794
/>
8895
<LoginDialog isOpen={showLoginDialog} setIsOpen={setShowLoginDialog} />
96+
<StorageUsageDialog isOpen={showStorageUsageDialog} setIsOpen={setShowStorageUsageDialog} />
8997
</React.Fragment>
9098
);
9199
};

apps/playground/src/components/Header/ActionsDropdown/LoginDialog.tsx

Lines changed: 36 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@ type $LoginData = z.infer<typeof $LoginData>;
1818
const $LoginData = z.object({
1919
apiBaseUrl: z.url(),
2020
username: z.string().min(1),
21-
password: z.string().min(1)
21+
password: z.string().min(1),
22+
legacyLogin: z.boolean()
2223
});
2324

2425
export type LoginDialogProps = {
@@ -39,10 +40,13 @@ export const LoginDialog = ({ isOpen, setIsOpen }: LoginDialogProps) => {
3940
revalidateToken();
4041
}, [isOpen]);
4142

42-
const getAdminToken = (credentials: $LoginCredentials): ResultAsync<{ accessToken: string }, string> => {
43+
const getAdminToken = (
44+
credentials: $LoginCredentials,
45+
baseUrl: string
46+
): ResultAsync<{ accessToken: string }, string> => {
4347
return asyncResultify(async () => {
4448
try {
45-
const response = await axios.post(`${apiBaseUrl}/v1/auth/login`, credentials, {
49+
const response = await axios.post(`${baseUrl}/v1/auth/login`, credentials, {
4650
headers: {
4751
Accept: 'application/json'
4852
},
@@ -59,10 +63,10 @@ export const LoginDialog = ({ isOpen, setIsOpen }: LoginDialogProps) => {
5963
});
6064
};
6165

62-
const getLimitedToken = (adminToken: string): ResultAsync<{ accessToken: string }, string> => {
66+
const getLimitedToken = (adminToken: string, baseUrl: string): ResultAsync<{ accessToken: string }, string> => {
6367
return asyncResultify(async () => {
6468
try {
65-
const response = await axios.get(`${apiBaseUrl}/v1/auth/create-instrument-token`, {
69+
const response = await axios.get(`${baseUrl}/v1/auth/create-instrument-token`, {
6670
headers: {
6771
Accept: 'application/json',
6872
Authorization: `Bearer ${adminToken}`
@@ -80,20 +84,24 @@ export const LoginDialog = ({ isOpen, setIsOpen }: LoginDialogProps) => {
8084
});
8185
};
8286

83-
const handleSubmit = async ({ apiBaseUrl, ...credentials }: $LoginData) => {
87+
const handleSubmit = async ({ apiBaseUrl, legacyLogin, ...credentials }: $LoginData) => {
8488
updateSettings({ apiBaseUrl });
85-
const adminTokenResult = await getAdminToken(credentials);
89+
const adminTokenResult = await getAdminToken(credentials, apiBaseUrl);
8690
if (adminTokenResult.isErr()) {
8791
addNotification({ type: 'error', title: 'Login Failed', message: adminTokenResult.error });
8892
return;
8993
}
90-
const limitedTokenResult = await getLimitedToken(adminTokenResult.value.accessToken);
91-
if (limitedTokenResult.isErr()) {
92-
addNotification({ type: 'error', title: 'Failed to Get Limited Token', message: limitedTokenResult.error });
93-
return;
94-
}
9594

96-
login(limitedTokenResult.value.accessToken);
95+
if (legacyLogin) {
96+
login(adminTokenResult.value.accessToken);
97+
} else {
98+
const limitedTokenResult = await getLimitedToken(adminTokenResult.value.accessToken, apiBaseUrl);
99+
if (limitedTokenResult.isErr()) {
100+
addNotification({ type: 'error', title: 'Failed to Get Limited Token', message: limitedTokenResult.error });
101+
return;
102+
}
103+
login(limitedTokenResult.value.accessToken);
104+
}
97105

98106
addNotification({ type: 'success' });
99107
};
@@ -137,6 +145,20 @@ export const LoginDialog = ({ isOpen, setIsOpen }: LoginDialogProps) => {
137145
placeholder: 'e.g., https://demo.opendatacapture.org/api',
138146
label: 'API Base URL',
139147
variant: 'input'
148+
},
149+
legacyLogin: {
150+
description: [
151+
"Use the user's full access token instead of a granular access token.",
152+
'Note that this can introduce security risks and should not be used on shared machines.',
153+
'It is required only for ODC versions prior to v1.12.0.'
154+
].join(''),
155+
kind: 'boolean',
156+
label: 'Legacy Login Mode',
157+
variant: 'radio',
158+
options: {
159+
false: 'No (Recommended)',
160+
true: 'Yes'
161+
}
140162
}
141163
}
142164
},
@@ -156,7 +178,7 @@ export const LoginDialog = ({ isOpen, setIsOpen }: LoginDialogProps) => {
156178
}
157179
}
158180
]}
159-
initialValues={{ apiBaseUrl }}
181+
initialValues={{ apiBaseUrl, legacyLogin: false }}
160182
validationSchema={$LoginData}
161183
onSubmit={async (data) => {
162184
await handleSubmit(data);
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import { useEffect, useState } from 'react';
2+
3+
import { formatByteSize } from '@douglasneuroinformatics/libjs';
4+
import { Button, Dialog } from '@douglasneuroinformatics/libui/components';
5+
6+
import { useAppStore } from '@/store';
7+
export type StorageUsageDialogProps = {
8+
isOpen: boolean;
9+
setIsOpen: (value: boolean) => void;
10+
};
11+
12+
export const StorageUsageDialog = ({ isOpen, setIsOpen }: StorageUsageDialogProps) => {
13+
const [storageEstimate, setStorageEstimate] = useState<null | StorageEstimate>(null);
14+
const [updateKey, setUpdateKey] = useState(0);
15+
const [message, setMessage] = useState<null | string>('Loading...');
16+
17+
const updateStorage = async () => {
18+
setMessage('Loading...');
19+
const [updated] = await Promise.all([
20+
await navigator.storage.estimate(),
21+
new Promise((resolve) => setTimeout(resolve, 500))
22+
]);
23+
setStorageEstimate(updated);
24+
setMessage(null);
25+
};
26+
27+
useEffect(() => {
28+
void updateStorage();
29+
}, [isOpen, updateKey]);
30+
31+
return (
32+
<Dialog open={isOpen} onOpenChange={setIsOpen}>
33+
<Dialog.Content>
34+
<Dialog.Header>
35+
<Dialog.Title>Storage Usage</Dialog.Title>
36+
<Dialog.Description>
37+
Check the details below to see how much storage your browser is using for instruments and how much space is
38+
still available.
39+
</Dialog.Description>
40+
</Dialog.Header>
41+
<Dialog.Body>
42+
{message ? (
43+
<p>{message}</p>
44+
) : (
45+
<>
46+
<p>Usage: {storageEstimate?.usage ? formatByteSize(storageEstimate.usage, true) : 'N/A'}</p>
47+
<p>Quota: {storageEstimate?.quota ? formatByteSize(storageEstimate.quota, true) : 'N/A'} </p>
48+
</>
49+
)}
50+
</Dialog.Body>
51+
<Dialog.Footer>
52+
<Button
53+
variant="danger"
54+
onClick={() => {
55+
useAppStore.persist.clearStorage();
56+
setMessage('Deleting...');
57+
void new Promise((resolve) => setTimeout(resolve, 2000)).then(() => {
58+
setUpdateKey(updateKey + 1);
59+
});
60+
}}
61+
>
62+
Drop Database (Irreversible)
63+
</Button>
64+
<Button type="button" variant="outline" onClick={() => setIsOpen(false)}>
65+
Close
66+
</Button>
67+
</Dialog.Footer>
68+
</Dialog.Content>
69+
</Dialog>
70+
);
71+
};

apps/playground/src/store/index.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
/* eslint-disable import/exports-last */
2+
import { del, get, set } from 'idb-keyval';
23
import { jwtDecode } from 'jwt-decode';
34
import { pick } from 'lodash-es';
45
import { create } from 'zustand';
56
import { createJSONStorage, devtools, persist, subscribeWithSelector } from 'zustand/middleware';
7+
import type { StateStorage } from 'zustand/middleware';
68
import { immer } from 'zustand/middleware/immer';
79

810
import { resolveIndexFilename } from '@/utils/file';
@@ -16,6 +18,18 @@ import { createViewerSlice } from './slices/viewer.slice';
1618

1719
import type { AppStore } from './types';
1820

21+
const storage: StateStorage = {
22+
getItem: async (name: string): Promise<null | string> => {
23+
return (await get(name)) ?? null;
24+
},
25+
removeItem: async (name: string): Promise<void> => {
26+
await del(name);
27+
},
28+
setItem: async (name: string, value: string): Promise<void> => {
29+
await set(name, value);
30+
}
31+
};
32+
1933
export const useAppStore = create(
2034
devtools(
2135
persist(
@@ -64,7 +78,7 @@ export const useAppStore = create(
6478
_accessToken: state.auth?.accessToken
6579
};
6680
},
67-
storage: createJSONStorage(() => localStorage),
81+
storage: createJSONStorage(() => storage),
6882
version: 1
6983
}
7084
)

0 commit comments

Comments
 (0)