mirror of
https://github.com/spacedriveapp/spacedrive
synced 2024-07-08 04:52:49 +00:00
[ENG-313] Key auto-generation and viewing (#478)
* add generic dialog for keys settings * revert artifact failed key viewing attempt * move `Select` key list component * rename dialog * remove unused imports and add new select option for *all* keys * add WIP but broken key viewer dialog * cleanup code and fix key viewer dialog * add clipboard icon and copy functionality * generalise the `AlertDialog` and refactor `BackupRestoreDialog` to use it * use new alert dialog in place of JS/tauri alerts * use generic alerts everywhere and bring generic alert props/default state * make `SelectOptionKeyList` generic for mounted/unmounted keys (with the use of `map` for the latter) * add clipboard to generic alert dialog + clean up * fix accent colour button for backup restoration * remove unneeded props from components * add slider+automount button * tweak password gen function * add password autogeneration * clippy * tweak password generation * use `crypto-random-string` and drop rust password generation * add default TEMPORARY keymanager pass/secret key to library creation screen * make key automounting functional * clean up key viewer * change dialog name * remove slider as that wasn't even being used? * make requested changes and hide key viewer if no keys are in the key manager * prevent automount and library sync from being enabled simultaneously * include `memoryOnly` in key * mark keys as memoryOnly
This commit is contained in:
parent
8ee2d18053
commit
3ce8c74a0d
|
@ -21,6 +21,7 @@ pub struct KeyAddArgs {
|
|||
hashing_algorithm: HashingAlgorithm,
|
||||
key: String,
|
||||
library_sync: bool,
|
||||
automount: bool,
|
||||
}
|
||||
|
||||
#[derive(Type, Deserialize)]
|
||||
|
@ -283,7 +284,21 @@ pub(crate) fn mount() -> RouterBuilder {
|
|||
|
||||
let stored_key = library.key_manager.access_keystore(uuid)?;
|
||||
|
||||
write_storedkey_to_db(library.db.clone(), &stored_key).await?;
|
||||
if args.library_sync {
|
||||
write_storedkey_to_db(library.db.clone(), &stored_key).await?;
|
||||
|
||||
if args.automount {
|
||||
library
|
||||
.db
|
||||
.key()
|
||||
.update(
|
||||
key::uuid::equals(uuid.to_string()),
|
||||
vec![key::SetParam::SetAutomount(true)],
|
||||
)
|
||||
.exec()
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
|
||||
// mount the key
|
||||
library.key_manager.mount(uuid)?;
|
||||
|
|
|
@ -108,7 +108,7 @@ export interface JobReport { id: string, name: string, data: Array<number> | nul
|
|||
|
||||
export type JobStatus = "Queued" | "Running" | "Completed" | "Canceled" | "Failed" | "Paused"
|
||||
|
||||
export interface KeyAddArgs { algorithm: Algorithm, hashing_algorithm: HashingAlgorithm, key: string, library_sync: boolean }
|
||||
export interface KeyAddArgs { algorithm: Algorithm, hashing_algorithm: HashingAlgorithm, key: string, library_sync: boolean, automount: boolean }
|
||||
|
||||
export interface KeyNameUpdateArgs { uuid: string, name: string }
|
||||
|
||||
|
|
|
@ -39,6 +39,7 @@
|
|||
"autoprefixer": "^10.4.12",
|
||||
"byte-size": "^8.1.0",
|
||||
"clsx": "^1.2.1",
|
||||
"crypto-random-string": "^5.0.0",
|
||||
"dayjs": "^1.11.5",
|
||||
"phosphor-react": "^1.4.1",
|
||||
"react": "^18.2.0",
|
||||
|
|
63
packages/interface/src/components/dialog/AlertDialog.tsx
Normal file
63
packages/interface/src/components/dialog/AlertDialog.tsx
Normal file
|
@ -0,0 +1,63 @@
|
|||
import { Button, Dialog, Input } from '@sd/ui';
|
||||
import { writeText } from '@tauri-apps/api/clipboard';
|
||||
import { Clipboard } from 'phosphor-react';
|
||||
|
||||
export const GenericAlertDialogState = {
|
||||
open: false,
|
||||
title: '',
|
||||
description: '',
|
||||
value: '',
|
||||
inputBox: false
|
||||
};
|
||||
|
||||
export interface GenericAlertDialogProps {
|
||||
open: boolean;
|
||||
title: string;
|
||||
description: string;
|
||||
value: string;
|
||||
inputBox: boolean;
|
||||
}
|
||||
|
||||
export interface AlertDialogProps {
|
||||
open: boolean;
|
||||
setOpen: (isShowing: boolean) => void;
|
||||
title: string; // dialog title
|
||||
description?: string; // description of the dialog
|
||||
value: string; // value to be displayed as text or in an input box
|
||||
label?: string; // button label
|
||||
inputBox: boolean; // whether the dialog should display the `value` in a disabled input box or as text
|
||||
}
|
||||
|
||||
export const AlertDialog = (props: AlertDialogProps) => {
|
||||
// maybe a copy-to-clipboard button would be beneficial too
|
||||
return (
|
||||
<Dialog
|
||||
open={props.open}
|
||||
setOpen={props.setOpen}
|
||||
title={props.title}
|
||||
description={props.description}
|
||||
ctaAction={() => {
|
||||
props.setOpen(false);
|
||||
}}
|
||||
ctaLabel={props.label !== undefined ? props.label : 'Done'}
|
||||
>
|
||||
{props.inputBox && (
|
||||
<div className="relative flex flex-grow mt-3">
|
||||
<Input value={props.value} disabled className="flex-grow !py-0.5" />
|
||||
<Button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
writeText(props.value);
|
||||
}}
|
||||
size="icon"
|
||||
className="border-none absolute right-[5px] top-[5px]"
|
||||
>
|
||||
<Clipboard className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!props.inputBox && <div className="text-sm">{props.value}</div>}
|
||||
</Dialog>
|
||||
);
|
||||
};
|
|
@ -5,56 +5,69 @@ import { Eye, EyeSlash } from 'phosphor-react';
|
|||
import { ReactNode, useState } from 'react';
|
||||
import { SubmitHandler, useForm } from 'react-hook-form';
|
||||
|
||||
import { GenericAlertDialogProps } from './AlertDialog';
|
||||
|
||||
type FormValues = {
|
||||
masterPassword: string;
|
||||
secretKey: string;
|
||||
filePath: string;
|
||||
};
|
||||
|
||||
export const BackupRestoreDialog = (props: { trigger: ReactNode }) => {
|
||||
const { trigger } = props;
|
||||
export interface BackupRestorationDialogProps {
|
||||
trigger: ReactNode;
|
||||
setDialogData: (data: GenericAlertDialogProps) => void;
|
||||
}
|
||||
|
||||
export const BackupRestoreDialog = (props: BackupRestorationDialogProps) => {
|
||||
const { register, handleSubmit, getValues, setValue } = useForm<FormValues>({
|
||||
defaultValues: {
|
||||
masterPassword: '',
|
||||
secretKey: '',
|
||||
filePath: ''
|
||||
secretKey: ''
|
||||
}
|
||||
});
|
||||
|
||||
const onSubmit: SubmitHandler<FormValues> = (data) => {
|
||||
if (data.filePath !== '') {
|
||||
setValue('masterPassword', '');
|
||||
setValue('secretKey', '');
|
||||
setValue('filePath', '');
|
||||
if (filePath !== '') {
|
||||
restoreKeystoreMutation.mutate(
|
||||
{
|
||||
password: data.masterPassword,
|
||||
secret_key: data.secretKey,
|
||||
path: data.filePath
|
||||
path: filePath
|
||||
},
|
||||
{
|
||||
onSuccess: (total) => {
|
||||
setTotalKeysImported(total);
|
||||
setShowBackupRestoreDialog(false);
|
||||
setShowRestorationFinalizationDialog(true);
|
||||
props.setDialogData({
|
||||
open: true,
|
||||
title: 'Import Successful',
|
||||
description: '',
|
||||
value: `${total} ${total !== 1 ? 'keys were imported.' : 'key was imported.'}`,
|
||||
inputBox: false
|
||||
});
|
||||
},
|
||||
onError: () => {
|
||||
alert('There was an error while restoring your backup.');
|
||||
setShowBackupRestoreDialog(false);
|
||||
props.setDialogData({
|
||||
open: true,
|
||||
title: 'Import Error',
|
||||
description: '',
|
||||
value: 'There was an error while restoring your backup.',
|
||||
inputBox: false
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
setValue('masterPassword', '');
|
||||
setValue('secretKey', '');
|
||||
setFilePath('');
|
||||
}
|
||||
};
|
||||
|
||||
const [showBackupRestoreDialog, setShowBackupRestoreDialog] = useState(false);
|
||||
const [showRestorationFinalizationDialog, setShowRestorationFinalizationDialog] = useState(false);
|
||||
const restoreKeystoreMutation = useLibraryMutation('keys.restoreKeystore');
|
||||
|
||||
const [showMasterPassword, setShowMasterPassword] = useState(false);
|
||||
const [showSecretKey, setShowSecretKey] = useState(false);
|
||||
|
||||
const [totalKeysImported, setTotalKeysImported] = useState(0);
|
||||
const [filePath, setFilePath] = useState('');
|
||||
|
||||
const MPCurrentEyeIcon = showMasterPassword ? EyeSlash : Eye;
|
||||
const SKCurrentEyeIcon = showSecretKey ? EyeSlash : Eye;
|
||||
|
@ -69,7 +82,7 @@ export const BackupRestoreDialog = (props: { trigger: ReactNode }) => {
|
|||
description="Restore keys from a backup."
|
||||
loading={restoreKeystoreMutation.isLoading}
|
||||
ctaLabel="Restore"
|
||||
trigger={trigger}
|
||||
trigger={props.trigger}
|
||||
>
|
||||
<div className="relative flex flex-grow mt-3 mb-2">
|
||||
<Input
|
||||
|
@ -108,11 +121,11 @@ export const BackupRestoreDialog = (props: { trigger: ReactNode }) => {
|
|||
<div className="relative flex flex-grow mb-2">
|
||||
<Button
|
||||
size="sm"
|
||||
variant={getValues('filePath') !== '' ? 'accent' : 'gray'}
|
||||
variant={filePath !== '' ? 'accent' : 'gray'}
|
||||
type="button"
|
||||
onClick={() => {
|
||||
open()?.then((result) => {
|
||||
if (result) setValue('filePath', result as string);
|
||||
if (result) setFilePath(result as string);
|
||||
});
|
||||
}}
|
||||
>
|
||||
|
@ -121,23 +134,6 @@ export const BackupRestoreDialog = (props: { trigger: ReactNode }) => {
|
|||
</div>
|
||||
</Dialog>
|
||||
</form>
|
||||
|
||||
<Dialog
|
||||
open={showRestorationFinalizationDialog}
|
||||
setOpen={setShowRestorationFinalizationDialog}
|
||||
title="Import Successful"
|
||||
description=""
|
||||
ctaAction={() => {
|
||||
setShowRestorationFinalizationDialog(false);
|
||||
}}
|
||||
ctaLabel="Done"
|
||||
trigger={<></>}
|
||||
>
|
||||
<div className="text-sm">
|
||||
{totalKeysImported}{' '}
|
||||
{totalKeysImported !== 1 ? 'keys were imported.' : 'key was imported.'}
|
||||
</div>
|
||||
</Dialog>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -3,13 +3,14 @@ import { Button, Dialog } from '@sd/ui';
|
|||
import { save } from '@tauri-apps/api/dialog';
|
||||
import { useState } from 'react';
|
||||
|
||||
import { GenericAlertDialogProps } from './AlertDialog';
|
||||
|
||||
interface DecryptDialogProps {
|
||||
open: boolean;
|
||||
setOpen: (isShowing: boolean) => void;
|
||||
location_id: number | null;
|
||||
object_id: number | null;
|
||||
setShowAlertDialog: (isShowing: boolean) => void;
|
||||
setAlertDialogData: (data: { title: string; text: string }) => void;
|
||||
setAlertDialogData: (data: GenericAlertDialogProps) => void;
|
||||
}
|
||||
|
||||
export const DecryptFileDialog = (props: DecryptDialogProps) => {
|
||||
|
@ -41,20 +42,25 @@ export const DecryptFileDialog = (props: DecryptDialogProps) => {
|
|||
{
|
||||
onSuccess: () => {
|
||||
props.setAlertDialogData({
|
||||
open: true,
|
||||
title: 'Info',
|
||||
text: 'The decryption job has started successfully. You may track the progress in the job overview panel.'
|
||||
value:
|
||||
'The decryption job has started successfully. You may track the progress in the job overview panel.',
|
||||
inputBox: false,
|
||||
description: ''
|
||||
});
|
||||
},
|
||||
onError: () => {
|
||||
props.setAlertDialogData({
|
||||
open: true,
|
||||
title: 'Error',
|
||||
text: 'The decryption job failed to start.'
|
||||
value: 'The decryption job failed to start.',
|
||||
inputBox: false,
|
||||
description: ''
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
props.setShowAlertDialog(true);
|
||||
}}
|
||||
>
|
||||
<div className="grid w-full grid-cols-2 gap-4 mt-4 mb-3">
|
||||
|
|
|
@ -1,40 +1,22 @@
|
|||
import { StoredKey, useLibraryMutation, useLibraryQuery } from '@sd/client';
|
||||
import { useLibraryMutation, useLibraryQuery } from '@sd/client';
|
||||
import { Button, Dialog, Select, SelectOption } from '@sd/ui';
|
||||
import { save } from '@tauri-apps/api/dialog';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { useState } from 'react';
|
||||
|
||||
import {
|
||||
getCryptoSettings,
|
||||
getHashingAlgorithmString
|
||||
} from '../../screens/settings/library/KeysSetting';
|
||||
import { SelectOptionKeyList } from '../key/KeyList';
|
||||
import { Checkbox } from '../primitive/Checkbox';
|
||||
|
||||
export const ListOfMountedKeys = (props: { keys: StoredKey[]; mountedUuids: string[] }) => {
|
||||
const { keys, mountedUuids } = props;
|
||||
|
||||
const [mountedKeys] = useMemo(
|
||||
() => [keys.filter((key) => mountedUuids.includes(key.uuid)) ?? []],
|
||||
[keys, mountedUuids]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{[...mountedKeys]?.map((key) => {
|
||||
return (
|
||||
<SelectOption value={key.uuid}>Key {key.uuid.substring(0, 8).toUpperCase()}</SelectOption>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
};
|
||||
import { GenericAlertDialogProps } from './AlertDialog';
|
||||
|
||||
interface EncryptDialogProps {
|
||||
open: boolean;
|
||||
setOpen: (isShowing: boolean) => void;
|
||||
location_id: number | null;
|
||||
object_id: number | null;
|
||||
setShowAlertDialog: (isShowing: boolean) => void;
|
||||
setAlertDialogData: (data: { title: string; text: string }) => void;
|
||||
setAlertDialogData: (data: GenericAlertDialogProps) => void;
|
||||
}
|
||||
|
||||
export const EncryptFileDialog = (props: EncryptDialogProps) => {
|
||||
|
@ -99,20 +81,25 @@ export const EncryptFileDialog = (props: EncryptDialogProps) => {
|
|||
{
|
||||
onSuccess: () => {
|
||||
props.setAlertDialogData({
|
||||
open: true,
|
||||
title: 'Success',
|
||||
text: 'The encryption job has started successfully. You may track the progress in the job overview panel.'
|
||||
value:
|
||||
'The encryption job has started successfully. You may track the progress in the job overview panel.',
|
||||
inputBox: false,
|
||||
description: ''
|
||||
});
|
||||
},
|
||||
onError: () => {
|
||||
props.setAlertDialogData({
|
||||
open: true,
|
||||
title: 'Error',
|
||||
text: 'The encryption job failed to start.'
|
||||
value: 'The encryption job failed to start.',
|
||||
inputBox: false,
|
||||
description: ''
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
props.setShowAlertDialog(true);
|
||||
}}
|
||||
>
|
||||
<div className="grid w-full grid-cols-2 gap-4 mt-4 mb-3">
|
||||
|
@ -126,9 +113,7 @@ export const EncryptFileDialog = (props: EncryptDialogProps) => {
|
|||
}}
|
||||
>
|
||||
{/* this only returns MOUNTED keys. we could include unmounted keys, but then we'd have to prompt the user to mount them too */}
|
||||
{keys.data && mountedUuids.data && (
|
||||
<ListOfMountedKeys keys={keys.data} mountedUuids={mountedUuids.data} />
|
||||
)}
|
||||
{mountedUuids.data && <SelectOptionKeyList keys={mountedUuids.data} />}
|
||||
</Select>
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
|
|
|
@ -1,27 +0,0 @@
|
|||
import { Dialog } from '@sd/ui';
|
||||
|
||||
export const ExplorerAlertDialog = (props: {
|
||||
open: boolean;
|
||||
setOpen: (isShowing: boolean) => void;
|
||||
title: string;
|
||||
description?: string;
|
||||
text: string;
|
||||
}) => {
|
||||
const { open, setOpen, title, description, text } = props;
|
||||
return (
|
||||
<>
|
||||
<Dialog
|
||||
open={open}
|
||||
setOpen={setOpen}
|
||||
title={title}
|
||||
description={description}
|
||||
ctaLabel="Done"
|
||||
ctaAction={() => {
|
||||
setOpen(false);
|
||||
}}
|
||||
>
|
||||
<div className="text-sm">{text}</div>
|
||||
</Dialog>
|
||||
</>
|
||||
);
|
||||
};
|
85
packages/interface/src/components/dialog/KeyViewerDialog.tsx
Normal file
85
packages/interface/src/components/dialog/KeyViewerDialog.tsx
Normal file
|
@ -0,0 +1,85 @@
|
|||
import { useLibraryQuery } from '@sd/client';
|
||||
import { Button, Dialog, Input, Select } from '@sd/ui';
|
||||
import { writeText } from '@tauri-apps/api/clipboard';
|
||||
import { Clipboard } from 'phosphor-react';
|
||||
import { ReactNode, useEffect, useState } from 'react';
|
||||
|
||||
import { SelectOptionKeyList } from '../key/KeyList';
|
||||
|
||||
interface KeyViewerDialogProps {
|
||||
trigger: ReactNode;
|
||||
}
|
||||
|
||||
export const KeyTextBox = (props: { uuid: string; setKey: (value: string) => void }) => {
|
||||
useLibraryQuery(['keys.getKey', props.uuid], {
|
||||
onSuccess: (data) => {
|
||||
props.setKey(data);
|
||||
}
|
||||
});
|
||||
|
||||
return <></>;
|
||||
};
|
||||
|
||||
export const KeyViewerDialog = (props: KeyViewerDialogProps) => {
|
||||
const keys = useLibraryQuery(['keys.list'], {
|
||||
onSuccess: (data) => {
|
||||
if (key === '' && data.length !== 0) {
|
||||
setKey(data[0].uuid);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const [showKeyViewerDialog, setShowKeyViewerDialog] = useState(false);
|
||||
const [key, setKey] = useState('');
|
||||
const [keyValue, setKeyValue] = useState('');
|
||||
|
||||
return (
|
||||
<>
|
||||
<Dialog
|
||||
open={showKeyViewerDialog}
|
||||
setOpen={setShowKeyViewerDialog}
|
||||
trigger={props.trigger}
|
||||
title="View Key Values"
|
||||
description="Here you can view the values of your keys."
|
||||
ctaLabel="Done"
|
||||
ctaAction={() => {
|
||||
setShowKeyViewerDialog(false);
|
||||
}}
|
||||
>
|
||||
<div className="grid w-full gap-4 mt-4 mb-3">
|
||||
<div className="flex flex-col">
|
||||
<span className="text-xs font-bold">Key</span>
|
||||
<Select
|
||||
className="mt-2 flex-grow"
|
||||
value={key}
|
||||
onChange={(e) => {
|
||||
setKey(e);
|
||||
}}
|
||||
>
|
||||
{keys.data && <SelectOptionKeyList keys={keys.data.map((key) => key.uuid)} />}
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid w-full gap-4 mt-4 mb-3">
|
||||
<div className="flex flex-col">
|
||||
<span className="text-xs font-bold">Value</span>
|
||||
<div className="relative flex flex-grow">
|
||||
<Input value={keyValue} disabled className="flex-grow !py-0.5" />
|
||||
<Button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
writeText(keyValue);
|
||||
}}
|
||||
size="icon"
|
||||
className="border-none absolute right-[5px] top-[5px]"
|
||||
>
|
||||
<Clipboard className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
<KeyTextBox uuid={key} setKey={setKeyValue} />
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -9,16 +9,19 @@ import { ReactNode, useState } from 'react';
|
|||
import { SubmitHandler, useForm } from 'react-hook-form';
|
||||
|
||||
import { getCryptoSettings } from '../../screens/settings/library/KeysSetting';
|
||||
import { GenericAlertDialogProps } from './AlertDialog';
|
||||
|
||||
export const PasswordChangeDialog = (props: { trigger: ReactNode }) => {
|
||||
export interface MasterPasswordChangeDialogProps {
|
||||
trigger: ReactNode;
|
||||
setDialogData: (data: GenericAlertDialogProps) => void;
|
||||
}
|
||||
export const MasterPasswordChangeDialog = (props: MasterPasswordChangeDialogProps) => {
|
||||
type FormValues = {
|
||||
masterPassword: string;
|
||||
masterPassword2: string;
|
||||
};
|
||||
|
||||
const [secretKey, setSecretKey] = useState('');
|
||||
|
||||
const { register, handleSubmit, getValues, setValue } = useForm<FormValues>({
|
||||
const { register, handleSubmit, reset } = useForm<FormValues>({
|
||||
defaultValues: {
|
||||
masterPassword: '',
|
||||
masterPassword2: ''
|
||||
|
@ -27,7 +30,13 @@ export const PasswordChangeDialog = (props: { trigger: ReactNode }) => {
|
|||
|
||||
const onSubmit: SubmitHandler<FormValues> = (data) => {
|
||||
if (data.masterPassword !== data.masterPassword2) {
|
||||
alert('Passwords are not the same.');
|
||||
props.setDialogData({
|
||||
open: true,
|
||||
title: 'Error',
|
||||
description: '',
|
||||
value: 'Passwords are not the same, please try again.',
|
||||
inputBox: false
|
||||
});
|
||||
} else {
|
||||
const [algorithm, hashing_algorithm] = getCryptoSettings(encryptionAlgo, hashingAlgo);
|
||||
|
||||
|
@ -35,17 +44,31 @@ export const PasswordChangeDialog = (props: { trigger: ReactNode }) => {
|
|||
{ algorithm, hashing_algorithm, password: data.masterPassword },
|
||||
{
|
||||
onSuccess: (sk) => {
|
||||
setSecretKey(sk);
|
||||
|
||||
setShowMasterPasswordDialog(false);
|
||||
setShowSecretKeyDialog(true);
|
||||
props.setDialogData({
|
||||
open: true,
|
||||
title: 'Secret Key',
|
||||
description:
|
||||
'Please store this secret key securely as it is needed to access your key manager.',
|
||||
value: sk,
|
||||
inputBox: true
|
||||
});
|
||||
},
|
||||
onError: () => {
|
||||
// this should never really happen
|
||||
alert('There was an error while changing your master password.');
|
||||
setShowMasterPasswordDialog(false);
|
||||
props.setDialogData({
|
||||
open: true,
|
||||
title: 'Master Password Change Error',
|
||||
description: '',
|
||||
value: 'There was an error while changing your master password.',
|
||||
inputBox: false
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
reset();
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -53,7 +76,7 @@ export const PasswordChangeDialog = (props: { trigger: ReactNode }) => {
|
|||
const [hashingAlgo, setHashingAlgo] = useState('Argon2id-s');
|
||||
const [passwordMeterMasterPw, setPasswordMeterMasterPw] = useState(''); // this is needed as the password meter won't update purely with react-hook-for
|
||||
const [showMasterPasswordDialog, setShowMasterPasswordDialog] = useState(false);
|
||||
const [showSecretKeyDialog, setShowSecretKeyDialog] = useState(false);
|
||||
// const [showSecretKeyDialog, setShowSecretKeyDialog] = useState(false);
|
||||
const changeMasterPassword = useLibraryMutation('keys.changeMasterPassword');
|
||||
const [showMasterPassword1, setShowMasterPassword1] = useState(false);
|
||||
const [showMasterPassword2, setShowMasterPassword2] = useState(false);
|
||||
|
@ -136,24 +159,6 @@ export const PasswordChangeDialog = (props: { trigger: ReactNode }) => {
|
|||
</div>
|
||||
</Dialog>
|
||||
</form>
|
||||
<Dialog
|
||||
open={showSecretKeyDialog}
|
||||
setOpen={setShowSecretKeyDialog}
|
||||
title="Secret Key"
|
||||
description="Please store this secret key securely as it is needed to access your key manager."
|
||||
ctaAction={() => {
|
||||
setShowSecretKeyDialog(false);
|
||||
}}
|
||||
ctaLabel="Done"
|
||||
trigger={<></>}
|
||||
>
|
||||
<Input
|
||||
className="flex-grow w-full mt-3"
|
||||
value={secretKey}
|
||||
placeholder="Secret Key"
|
||||
disabled={true}
|
||||
/>
|
||||
</Dialog>
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -2,9 +2,9 @@ import { ExplorerData, rspc, useCurrentLibrary } from '@sd/client';
|
|||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { useExplorerStore } from '../../util/explorerStore';
|
||||
import { AlertDialog, GenericAlertDialogState } from '../dialog/AlertDialog';
|
||||
import { DecryptFileDialog } from '../dialog/DecryptFileDialog';
|
||||
import { EncryptFileDialog } from '../dialog/EncryptFileDialog';
|
||||
import { ExplorerAlertDialog } from '../dialog/ExplorerAlertDialog';
|
||||
import { Inspector } from '../explorer/Inspector';
|
||||
import ExplorerContextMenu from './ExplorerContextMenu';
|
||||
import { TopBar } from './ExplorerTopBar';
|
||||
|
@ -23,11 +23,11 @@ export default function Explorer(props: Props) {
|
|||
|
||||
const [showEncryptDialog, setShowEncryptDialog] = useState(false);
|
||||
const [showDecryptDialog, setShowDecryptDialog] = useState(false);
|
||||
const [showAlertDialog, setShowAlertDialog] = useState(false);
|
||||
const [alertDialogData, setAlertDialogData] = useState({
|
||||
title: '',
|
||||
text: ''
|
||||
});
|
||||
|
||||
const [alertDialogData, setAlertDialogData] = useState(GenericAlertDialogState);
|
||||
const setShowAlertDialog = (state: boolean) => {
|
||||
setAlertDialogData({ ...alertDialogData, open: state });
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setSeparateTopBar((oldValue) => {
|
||||
|
@ -50,7 +50,6 @@ export default function Explorer(props: Props) {
|
|||
<ExplorerContextMenu
|
||||
setShowEncryptDialog={setShowEncryptDialog}
|
||||
setShowDecryptDialog={setShowDecryptDialog}
|
||||
setShowAlertDialog={setShowAlertDialog}
|
||||
setAlertDialogData={setAlertDialogData}
|
||||
>
|
||||
<div className="relative flex flex-col w-full">
|
||||
|
@ -93,18 +92,18 @@ export default function Explorer(props: Props) {
|
|||
</div>
|
||||
</ExplorerContextMenu>
|
||||
</div>
|
||||
<ExplorerAlertDialog
|
||||
open={showAlertDialog}
|
||||
<AlertDialog
|
||||
open={alertDialogData.open}
|
||||
setOpen={setShowAlertDialog}
|
||||
title={alertDialogData.title}
|
||||
text={alertDialogData.text}
|
||||
value={alertDialogData.value}
|
||||
inputBox={alertDialogData.inputBox}
|
||||
/>
|
||||
<EncryptFileDialog
|
||||
location_id={expStore.locationId}
|
||||
object_id={expStore.contextMenuObjectId}
|
||||
open={showEncryptDialog}
|
||||
setOpen={setShowEncryptDialog}
|
||||
setShowAlertDialog={setShowAlertDialog}
|
||||
setAlertDialogData={setAlertDialogData}
|
||||
/>
|
||||
<DecryptFileDialog
|
||||
|
@ -112,7 +111,6 @@ export default function Explorer(props: Props) {
|
|||
object_id={expStore.contextMenuObjectId}
|
||||
open={showDecryptDialog}
|
||||
setOpen={setShowDecryptDialog}
|
||||
setShowAlertDialog={setShowAlertDialog}
|
||||
setAlertDialogData={setAlertDialogData}
|
||||
/>
|
||||
</>
|
||||
|
|
|
@ -16,6 +16,7 @@ import { PropsWithChildren, useMemo } from 'react';
|
|||
import { useOperatingSystem } from '../../hooks/useOperatingSystem';
|
||||
import { usePlatform } from '../../util/Platform';
|
||||
import { getExplorerStore } from '../../util/explorerStore';
|
||||
import { GenericAlertDialogProps } from '../dialog/AlertDialog';
|
||||
import { EncryptFileDialog } from '../dialog/EncryptFileDialog';
|
||||
|
||||
const AssignTagMenuItems = (props: { objectId: number }) => {
|
||||
|
@ -61,8 +62,7 @@ const AssignTagMenuItems = (props: { objectId: number }) => {
|
|||
export interface ExplorerContextMenuProps extends PropsWithChildren {
|
||||
setShowEncryptDialog: (isShowing: boolean) => void;
|
||||
setShowDecryptDialog: (isShowing: boolean) => void;
|
||||
setShowAlertDialog: (isShowing: boolean) => void;
|
||||
setAlertDialogData: (data: { title: string; text: string }) => void;
|
||||
setAlertDialogData: (data: GenericAlertDialogProps) => void;
|
||||
}
|
||||
|
||||
export default function ExplorerContextMenu(props: ExplorerContextMenuProps) {
|
||||
|
@ -150,16 +150,20 @@ export default function ExplorerContextMenu(props: ExplorerContextMenuProps) {
|
|||
props.setShowEncryptDialog(true);
|
||||
} else if (!hasMasterPassword) {
|
||||
props.setAlertDialogData({
|
||||
open: true,
|
||||
title: 'Key manager locked',
|
||||
text: 'The key manager is currently locked. Please unlock it and try again.'
|
||||
value: 'The key manager is currently locked. Please unlock it and try again.',
|
||||
inputBox: false,
|
||||
description: ''
|
||||
});
|
||||
props.setShowAlertDialog(true);
|
||||
} else if (!hasMountedKeys) {
|
||||
props.setAlertDialogData({
|
||||
open: true,
|
||||
title: 'No mounted keys',
|
||||
text: 'No mounted keys were found. Please mount a key and try again.'
|
||||
description: '',
|
||||
value: 'No mounted keys were found. Please mount a key and try again.',
|
||||
inputBox: false
|
||||
});
|
||||
props.setShowAlertDialog(true);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
@ -173,16 +177,20 @@ export default function ExplorerContextMenu(props: ExplorerContextMenuProps) {
|
|||
props.setShowDecryptDialog(true);
|
||||
} else if (!hasMasterPassword) {
|
||||
props.setAlertDialogData({
|
||||
open: true,
|
||||
title: 'Key manager locked',
|
||||
text: 'The key manager is currently locked. Please unlock it and try again.'
|
||||
value: 'The key manager is currently locked. Please unlock it and try again.',
|
||||
inputBox: false,
|
||||
description: ''
|
||||
});
|
||||
props.setShowAlertDialog(true);
|
||||
} else if (!hasMountedKeys) {
|
||||
props.setAlertDialogData({
|
||||
open: true,
|
||||
title: 'No mounted keys',
|
||||
text: 'No mounted keys were found. Please mount a key and try again.'
|
||||
value: 'No mounted keys were found. Please mount a key and try again.',
|
||||
inputBox: false,
|
||||
description: ''
|
||||
});
|
||||
props.setShowAlertDialog(true);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
|
|
@ -21,7 +21,8 @@ export interface Key {
|
|||
objectCount?: number;
|
||||
containerCount?: number;
|
||||
};
|
||||
default?: boolean; // need to make use of this within the UI
|
||||
default?: boolean;
|
||||
memoryOnly?: boolean;
|
||||
// Nodes this key is mounted on
|
||||
nodes?: string[]; // will be node object
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { useLibraryMutation, useLibraryQuery } from '@sd/client';
|
||||
import { Button, CategoryHeading } from '@sd/ui';
|
||||
import { StoredKey, useLibraryMutation, useLibraryQuery } from '@sd/client';
|
||||
import { Button, CategoryHeading, SelectOption } from '@sd/ui';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { DefaultProps } from '../primitive/types';
|
||||
|
@ -7,12 +7,21 @@ import { DummyKey, Key } from './Key';
|
|||
|
||||
export type KeyListProps = DefaultProps;
|
||||
|
||||
// ideal for going within a select box
|
||||
// can use mounted or unmounted keys, just provide different inputs
|
||||
export const SelectOptionKeyList = (props: { keys: string[] }) => {
|
||||
return (
|
||||
<>
|
||||
{props.keys.map((key) => {
|
||||
return <SelectOption value={key}>Key {key.substring(0, 8).toUpperCase()}</SelectOption>;
|
||||
})}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const ListOfKeys = () => {
|
||||
const keys = useLibraryQuery(['keys.list']);
|
||||
const mountedUuids = useLibraryQuery(['keys.listMounted']);
|
||||
|
||||
// use a separate route so we get the default key from the key manager, not the database
|
||||
// sometimes the key won't be stored in the database
|
||||
const defaultKey = useLibraryQuery(['keys.getDefault']);
|
||||
|
||||
const [mountedKeys, unmountedKeys] = useMemo(
|
||||
|
@ -37,7 +46,8 @@ export const ListOfKeys = () => {
|
|||
id: key.uuid,
|
||||
name: `Key ${key.uuid.substring(0, 8).toUpperCase()}`,
|
||||
mounted: mountedKeys.includes(key),
|
||||
default: defaultKey.data === key.uuid
|
||||
default: defaultKey.data === key.uuid,
|
||||
memoryOnly: key.memory_only
|
||||
// key stats need including here at some point
|
||||
}}
|
||||
/>
|
||||
|
|
|
@ -1,25 +1,29 @@
|
|||
import { useLibraryMutation, useLibraryQuery } from '@sd/client';
|
||||
import { Algorithm, HashingAlgorithm, Params } from '@sd/client';
|
||||
import { Button, CategoryHeading, Input, Select, SelectOption, Switch, cva, tw } from '@sd/ui';
|
||||
import cryptoRandomString from 'crypto-random-string';
|
||||
import { Eye, EyeSlash, Info } from 'phosphor-react';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
|
||||
import { getCryptoSettings } from '../../screens/settings/library/KeysSetting';
|
||||
import Slider from '../primitive/Slider';
|
||||
import { Tooltip } from '../tooltip/Tooltip';
|
||||
|
||||
const KeyHeading = tw(CategoryHeading)`mb-1`;
|
||||
|
||||
const PasswordCharset =
|
||||
'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*()_+-={}[]:"\';<>?,./\\|`~';
|
||||
|
||||
const GeneratePassword = (length: number) => {
|
||||
return cryptoRandomString({ length, characters: PasswordCharset });
|
||||
};
|
||||
|
||||
export function KeyMounter() {
|
||||
const ref = useRef<HTMLInputElement>(null);
|
||||
|
||||
// we need to call these at least once somewhere
|
||||
// if we don't, if a user mounts a key before first viewing the key list, no key will show in the list
|
||||
// either call it in here or in the keymanager itself
|
||||
const keys = useLibraryQuery(['keys.list']);
|
||||
const mounted_uuids = useLibraryQuery(['keys.listMounted']);
|
||||
|
||||
const [showKey, setShowKey] = useState(false);
|
||||
const [librarySync, setLibrarySync] = useState(true);
|
||||
const [autoMount, setAutoMount] = useState(false);
|
||||
|
||||
const [sliderValue, setSliderValue] = useState([64]);
|
||||
|
||||
const [key, setKey] = useState('');
|
||||
const [encryptionAlgo, setEncryptionAlgo] = useState('XChaCha20Poly1305');
|
||||
|
@ -59,19 +63,55 @@ export function KeyMounter() {
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-row space-x-2">
|
||||
<div className="relative flex flex-grow mt-2 mb-2">
|
||||
<Slider
|
||||
value={sliderValue}
|
||||
max={128}
|
||||
min={8}
|
||||
step={4}
|
||||
defaultValue={[64]}
|
||||
onValueChange={(e) => {
|
||||
setSliderValue(e);
|
||||
setKey(GeneratePassword(e[0]));
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<span className="text-sm mt-2.5 font-medium">{sliderValue}</span>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-row items-center mt-3 mb-1">
|
||||
<div className="space-x-2">
|
||||
<Switch
|
||||
className="bg-app-selected"
|
||||
size="sm"
|
||||
checked={librarySync}
|
||||
onCheckedChange={setLibrarySync}
|
||||
onCheckedChange={(e) => {
|
||||
if (autoMount && e) setAutoMount(false);
|
||||
setLibrarySync(e);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<span className="ml-3 text-xs font-medium">Sync with Library</span>
|
||||
<Tooltip label="This key will be registered with all devices running your Library">
|
||||
<Info className="w-4 h-4 ml-1.5 text-ink-faint" />
|
||||
</Tooltip>
|
||||
<div className="flex-grow" />
|
||||
<div className="space-x-2">
|
||||
<Switch
|
||||
className="bg-app-selected"
|
||||
size="sm"
|
||||
checked={autoMount}
|
||||
onCheckedChange={(e) => {
|
||||
if (librarySync && e) setLibrarySync(false);
|
||||
setAutoMount(e);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<span className="ml-3 text-xs font-medium">Automount</span>
|
||||
<Tooltip label="This key will be automatically mounted every time you unlock the key manager">
|
||||
<Info className="w-4 h-4 ml-1.5 text-ink-faint" />
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
<div className="grid w-full grid-cols-2 gap-4 mt-4 mb-3">
|
||||
|
@ -99,13 +139,17 @@ export function KeyMounter() {
|
|||
variant="accent"
|
||||
disabled={key === ''}
|
||||
onClick={() => {
|
||||
if (key !== '') {
|
||||
setKey('');
|
||||
setKey('');
|
||||
|
||||
const [algorithm, hashing_algorithm] = getCryptoSettings(encryptionAlgo, hashingAlgo);
|
||||
const [algorithm, hashing_algorithm] = getCryptoSettings(encryptionAlgo, hashingAlgo);
|
||||
|
||||
createKey.mutate({ algorithm, hashing_algorithm, key, library_sync: librarySync });
|
||||
}
|
||||
createKey.mutate({
|
||||
algorithm,
|
||||
hashing_algorithm,
|
||||
key,
|
||||
library_sync: librarySync,
|
||||
automount: autoMount
|
||||
});
|
||||
}}
|
||||
>
|
||||
Mount Key
|
||||
|
|
|
@ -2,7 +2,7 @@ import clsx from 'clsx';
|
|||
import { useState } from 'react';
|
||||
import { useNavigate } from 'react-router';
|
||||
|
||||
import { Button } from '../../../../ui/src';
|
||||
import { Button, Input } from '../../../../ui/src';
|
||||
import { useOperatingSystem } from '../../hooks/useOperatingSystem';
|
||||
import CreateLibraryDialog from '../dialog/CreateLibraryDialog';
|
||||
|
||||
|
@ -20,6 +20,27 @@ export default function OnboardingPage() {
|
|||
)}
|
||||
>
|
||||
<h1 className="text-red-500">Welcome to Spacedrive</h1>
|
||||
<div className="text-white mt-2 mb-4">
|
||||
<p className="text-sm mb-1">
|
||||
The default keymanager details are below. This is only for development, and will be
|
||||
completely random once onboarding has completed. The secret key is just 16x zeroes encoded
|
||||
in hex.
|
||||
</p>
|
||||
<div className="flex space-x-2">
|
||||
<div className="relative flex">
|
||||
<p className="mr-2 text-sm mt-2">Password:</p>
|
||||
<Input value="password" className="flex-grow !py-0.5" disabled />
|
||||
</div>
|
||||
<div className="relative flex w-[375px]">
|
||||
<p className="mr-2 text-sm mt-2">Secret Key:</p>
|
||||
<Input
|
||||
value="30303030-30303030-30303030-30303030"
|
||||
className="flex-grow !py-0.5"
|
||||
disabled
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<CreateLibraryDialog open={open} setOpen={setOpen} onSubmit={() => navigate('/overview')}>
|
||||
<Button variant="accent" size="sm">
|
||||
|
|
|
@ -13,8 +13,10 @@ import { Eye, EyeSlash, Lock, Plus } from 'phosphor-react';
|
|||
import { PropsWithChildren, useState } from 'react';
|
||||
import { animated, useTransition } from 'react-spring';
|
||||
|
||||
import { AlertDialog, GenericAlertDialogState } from '../../../components/dialog/AlertDialog';
|
||||
import { BackupRestoreDialog } from '../../../components/dialog/BackupRestoreDialog';
|
||||
import { PasswordChangeDialog } from '../../../components/dialog/PasswordChangeDialog';
|
||||
import { KeyViewerDialog } from '../../../components/dialog/KeyViewerDialog';
|
||||
import { MasterPasswordChangeDialog } from '../../../components/dialog/MasterPasswordChangeDialog';
|
||||
import { ListOfKeys } from '../../../components/key/KeyList';
|
||||
import { KeyMounter } from '../../../components/key/KeyMounter';
|
||||
import { SettingsContainer } from '../../../components/settings/SettingsContainer';
|
||||
|
@ -91,141 +93,184 @@ export default function KeysSettings() {
|
|||
const [showSecretKey, setShowSecretKey] = useState(false);
|
||||
const [masterPassword, setMasterPassword] = useState('');
|
||||
const [secretKey, setSecretKey] = useState('');
|
||||
|
||||
const keys = useLibraryQuery(['keys.list']);
|
||||
|
||||
const [alertDialogData, setAlertDialogData] = useState(GenericAlertDialogState);
|
||||
const setShowAlertDialog = (state: boolean) => {
|
||||
setAlertDialogData({ ...alertDialogData, open: state });
|
||||
};
|
||||
|
||||
const MPCurrentEyeIcon = showMasterPassword ? EyeSlash : Eye;
|
||||
const SKCurrentEyeIcon = showSecretKey ? EyeSlash : Eye;
|
||||
|
||||
if (!hasMasterPw?.data) {
|
||||
return (
|
||||
<div className="p-2 mr-20 ml-20 mt-10">
|
||||
<div className="relative flex flex-grow mb-2">
|
||||
<Input
|
||||
value={masterPassword}
|
||||
onChange={(e) => setMasterPassword(e.target.value)}
|
||||
autoFocus
|
||||
type={showMasterPassword ? 'text' : 'password'}
|
||||
className="flex-grow !py-0.5"
|
||||
placeholder="Master Password"
|
||||
/>
|
||||
<Button
|
||||
onClick={() => setShowMasterPassword(!showMasterPassword)}
|
||||
size="icon"
|
||||
className="border-none absolute right-[5px] top-[5px]"
|
||||
>
|
||||
<MPCurrentEyeIcon className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
<>
|
||||
<div className="p-2 mr-20 ml-20 mt-10">
|
||||
<div className="relative flex flex-grow mb-2">
|
||||
<Input
|
||||
value={masterPassword}
|
||||
onChange={(e) => setMasterPassword(e.target.value)}
|
||||
autoFocus
|
||||
type={showMasterPassword ? 'text' : 'password'}
|
||||
className="flex-grow !py-0.5"
|
||||
placeholder="Master Password"
|
||||
/>
|
||||
<Button
|
||||
onClick={() => setShowMasterPassword(!showMasterPassword)}
|
||||
size="icon"
|
||||
className="border-none absolute right-[5px] top-[5px]"
|
||||
>
|
||||
<MPCurrentEyeIcon className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="relative flex flex-grow mb-2">
|
||||
<Input
|
||||
value={secretKey}
|
||||
onChange={(e) => setSecretKey(e.target.value)}
|
||||
type={showSecretKey ? 'text' : 'password'}
|
||||
className="flex-grow !py-0.5"
|
||||
placeholder="Secret Key"
|
||||
/>
|
||||
<Button
|
||||
onClick={() => setShowSecretKey(!showSecretKey)}
|
||||
size="icon"
|
||||
className="border-none absolute right-[5px] top-[5px]"
|
||||
>
|
||||
<SKCurrentEyeIcon className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
<div className="relative flex flex-grow mb-2">
|
||||
<Input
|
||||
value={secretKey}
|
||||
onChange={(e) => setSecretKey(e.target.value)}
|
||||
type={showSecretKey ? 'text' : 'password'}
|
||||
className="flex-grow !py-0.5"
|
||||
placeholder="Secret Key"
|
||||
/>
|
||||
<Button
|
||||
onClick={() => setShowSecretKey(!showSecretKey)}
|
||||
size="icon"
|
||||
className="border-none absolute right-[5px] top-[5px]"
|
||||
>
|
||||
<SKCurrentEyeIcon className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
className="w-full"
|
||||
variant="accent"
|
||||
disabled={setMasterPasswordMutation.isLoading}
|
||||
onClick={() => {
|
||||
if (masterPassword !== '' && secretKey !== '') {
|
||||
setMasterPassword('');
|
||||
setSecretKey('');
|
||||
setMasterPasswordMutation.mutate(
|
||||
{ password: masterPassword, secret_key: secretKey },
|
||||
{
|
||||
onError: () => {
|
||||
alert('Incorrect information provided.');
|
||||
<Button
|
||||
className="w-full"
|
||||
variant="accent"
|
||||
disabled={setMasterPasswordMutation.isLoading}
|
||||
onClick={() => {
|
||||
if (masterPassword !== '' && secretKey !== '') {
|
||||
setMasterPassword('');
|
||||
setSecretKey('');
|
||||
setMasterPasswordMutation.mutate(
|
||||
{ password: masterPassword, secret_key: secretKey },
|
||||
{
|
||||
onError: () => {
|
||||
setAlertDialogData({
|
||||
open: true,
|
||||
title: 'Unlock Error',
|
||||
description: '',
|
||||
value: 'The information provided to the key manager was incorrect',
|
||||
inputBox: false
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
}}
|
||||
>
|
||||
Unlock
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}}
|
||||
>
|
||||
Unlock
|
||||
</Button>
|
||||
</div>
|
||||
<AlertDialog
|
||||
open={alertDialogData.open}
|
||||
setOpen={setShowAlertDialog}
|
||||
title={alertDialogData.title}
|
||||
description={alertDialogData.description}
|
||||
value={alertDialogData.value}
|
||||
inputBox={alertDialogData.inputBox}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<SettingsContainer>
|
||||
<SettingsHeader
|
||||
title="Keys"
|
||||
description="Manage your keys."
|
||||
rightArea={
|
||||
<div className="flex flex-row items-center">
|
||||
<Button
|
||||
size="icon"
|
||||
onClick={() => {
|
||||
unmountAll.mutate(null);
|
||||
clearMasterPassword.mutate(null);
|
||||
}}
|
||||
variant="outline"
|
||||
className="text-ink-faint"
|
||||
>
|
||||
<Lock className="w-4 h-4 text-ink-faint" />
|
||||
</Button>
|
||||
<KeyMounterDropdown
|
||||
trigger={
|
||||
<Button size="icon" variant="outline" className="text-ink-faint">
|
||||
<Plus className="w-4 h-4 text-ink-faint" />
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
<KeyMounter />
|
||||
</KeyMounterDropdown>
|
||||
</div>
|
||||
}
|
||||
<>
|
||||
<SettingsContainer>
|
||||
<SettingsHeader
|
||||
title="Keys"
|
||||
description="Manage your keys."
|
||||
rightArea={
|
||||
<div className="flex flex-row items-center">
|
||||
<Button
|
||||
size="icon"
|
||||
onClick={() => {
|
||||
unmountAll.mutate(null);
|
||||
clearMasterPassword.mutate(null);
|
||||
}}
|
||||
variant="outline"
|
||||
className="text-ink-faint"
|
||||
>
|
||||
<Lock className="w-4 h-4 text-ink-faint" />
|
||||
</Button>
|
||||
<KeyMounterDropdown
|
||||
trigger={
|
||||
<Button size="icon" variant="outline" className="text-ink-faint">
|
||||
<Plus className="w-4 h-4 text-ink-faint" />
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
<KeyMounter />
|
||||
</KeyMounterDropdown>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
<div className="grid space-y-2">
|
||||
<ListOfKeys />
|
||||
</div>
|
||||
|
||||
<SettingsSubHeader title="Password Options" />
|
||||
<div className="flex flex-row">
|
||||
<MasterPasswordChangeDialog
|
||||
setDialogData={setAlertDialogData}
|
||||
trigger={
|
||||
<Button size="sm" variant="gray" className="mr-2">
|
||||
Change Master Password
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
<KeyViewerDialog
|
||||
trigger={
|
||||
<Button size="sm" variant="gray" className="mr-2" hidden={keys.data?.length === 0}>
|
||||
View Key Values
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<SettingsSubHeader title="Data Recovery" />
|
||||
<div className="flex flex-row">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="gray"
|
||||
className="mr-2"
|
||||
type="button"
|
||||
onClick={() => {
|
||||
// not platform-safe, probably will break on web but `platform` doesn't have a save dialog option
|
||||
save()?.then((result) => {
|
||||
if (result) backupKeystore.mutate(result as string);
|
||||
});
|
||||
}}
|
||||
>
|
||||
Backup
|
||||
</Button>
|
||||
<BackupRestoreDialog
|
||||
setDialogData={setAlertDialogData}
|
||||
trigger={
|
||||
<Button size="sm" variant="gray" className="mr-2">
|
||||
Restore
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</SettingsContainer>
|
||||
<AlertDialog
|
||||
open={alertDialogData.open}
|
||||
setOpen={setShowAlertDialog}
|
||||
title={alertDialogData.title}
|
||||
description={alertDialogData.description}
|
||||
value={alertDialogData.value}
|
||||
inputBox={alertDialogData.inputBox}
|
||||
/>
|
||||
<div className="grid space-y-2">
|
||||
<ListOfKeys />
|
||||
</div>
|
||||
|
||||
<SettingsSubHeader title="Password Options" />
|
||||
<div className="flex flex-row">
|
||||
<PasswordChangeDialog
|
||||
trigger={
|
||||
<Button size="sm" variant="gray" className="mr-2">
|
||||
Change Master Password
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<SettingsSubHeader title="Data Recovery" />
|
||||
<div className="flex flex-row">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="gray"
|
||||
className="mr-2"
|
||||
type="button"
|
||||
onClick={() => {
|
||||
// not platform-safe, probably will break on web but `platform` doesn't have a save dialog option
|
||||
save()?.then((result) => {
|
||||
if (result) backupKeystore.mutate(result as string);
|
||||
});
|
||||
}}
|
||||
>
|
||||
Backup
|
||||
</Button>
|
||||
<BackupRestoreDialog
|
||||
trigger={
|
||||
<Button size="sm" variant="gray" className="mr-2">
|
||||
Restore
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</SettingsContainer>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -423,6 +423,7 @@ importers:
|
|||
autoprefixer: ^10.4.12
|
||||
byte-size: ^8.1.0
|
||||
clsx: ^1.2.1
|
||||
crypto-random-string: ^5.0.0
|
||||
dayjs: ^1.11.5
|
||||
phosphor-react: ^1.4.1
|
||||
prettier: ^2.7.1
|
||||
|
@ -468,6 +469,7 @@ importers:
|
|||
autoprefixer: 10.4.13
|
||||
byte-size: 8.1.0
|
||||
clsx: 1.2.1
|
||||
crypto-random-string: 5.0.0
|
||||
dayjs: 1.11.6
|
||||
phosphor-react: 1.4.1_react@18.2.0
|
||||
react: 18.2.0
|
||||
|
@ -8474,7 +8476,7 @@ packages:
|
|||
'@babel/plugin-transform-react-jsx-source': 7.19.6_@babel+core@7.20.5
|
||||
magic-string: 0.26.7
|
||||
react-refresh: 0.14.0
|
||||
vite: 3.2.4_sass@1.56.1
|
||||
vite: 3.2.4_@types+node@16.18.4
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
|
@ -11027,6 +11029,13 @@ packages:
|
|||
resolution: {integrity: sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
/crypto-random-string/5.0.0:
|
||||
resolution: {integrity: sha512-KWjTXWwxFd6a94m5CdRGW/t82Tr8DoBc9dNnPCAbFI1EBweN6v1tv8y4Y1m7ndkp/nkIBRxUxAzpaBnR2k3bcQ==}
|
||||
engines: {node: '>=14.16'}
|
||||
dependencies:
|
||||
type-fest: 2.19.0
|
||||
dev: false
|
||||
|
||||
/cspell-dictionary/6.15.1:
|
||||
resolution: {integrity: sha512-VCx8URiNgOCYZkG6ThQKYMJ6jXyv4RC7C5H8yD8r3tBU81snYV4KWNqEdQCU9ClM+uHjz8FEANrF9hggB+KzuA==}
|
||||
engines: {node: '>=14'}
|
||||
|
@ -21567,6 +21576,11 @@ packages:
|
|||
engines: {node: '>=8'}
|
||||
dev: true
|
||||
|
||||
/type-fest/2.19.0:
|
||||
resolution: {integrity: sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==}
|
||||
engines: {node: '>=12.20'}
|
||||
dev: false
|
||||
|
||||
/type-is/1.6.18:
|
||||
resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==}
|
||||
engines: {node: '>= 0.6'}
|
||||
|
@ -22300,7 +22314,7 @@ packages:
|
|||
dependencies:
|
||||
'@rollup/pluginutils': 5.0.2
|
||||
'@svgr/core': 6.5.1
|
||||
vite: 3.2.4_sass@1.56.1
|
||||
vite: 3.2.4_@types+node@16.18.4
|
||||
transitivePeerDependencies:
|
||||
- rollup
|
||||
- supports-color
|
||||
|
@ -22452,6 +22466,7 @@ packages:
|
|||
sass: 1.56.1
|
||||
optionalDependencies:
|
||||
fsevents: 2.3.2
|
||||
dev: true
|
||||
|
||||
/vlq/1.0.1:
|
||||
resolution: {integrity: sha512-gQpnTgkubC6hQgdIcRdYGDSDc+SaujOdyesZQMv6JlfQee/9Mp0Qhnys6WxDWvQnL5WZdT7o2Ul187aSt0Rq+w==}
|
||||
|
|
Loading…
Reference in a new issue