mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-03-18 07:23:51 +01:00
Refactor desktop app into separate client and proxy apps
This commit is contained in:
312
apps/yaak-client/components/WorkspaceEncryptionSetting.tsx
Normal file
312
apps/yaak-client/components/WorkspaceEncryptionSetting.tsx
Normal file
@@ -0,0 +1,312 @@
|
||||
import {
|
||||
disableEncryption,
|
||||
enableEncryption,
|
||||
revealWorkspaceKey,
|
||||
setWorkspaceKey,
|
||||
} from '@yaakapp-internal/crypto';
|
||||
import type { WorkspaceMeta } from '@yaakapp-internal/models';
|
||||
import classNames from 'classnames';
|
||||
import { useAtomValue } from 'jotai';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { activeWorkspaceAtom, activeWorkspaceMetaAtom } from '../hooks/useActiveWorkspace';
|
||||
import { createFastMutation } from '../hooks/useFastMutation';
|
||||
import { useStateWithDeps } from '../hooks/useStateWithDeps';
|
||||
import { showConfirm } from '../lib/confirm';
|
||||
import { CopyIconButton } from './CopyIconButton';
|
||||
import { Banner } from './core/Banner';
|
||||
import type { ButtonProps } from './core/Button';
|
||||
import { Button } from './core/Button';
|
||||
import { IconButton } from './core/IconButton';
|
||||
import { IconTooltip } from './core/IconTooltip';
|
||||
import { Label } from './core/Label';
|
||||
import { PlainInput } from './core/PlainInput';
|
||||
import { HStack, VStack } from './core/Stacks';
|
||||
import { EncryptionHelp } from './EncryptionHelp';
|
||||
|
||||
interface Props {
|
||||
size?: ButtonProps['size'];
|
||||
expanded?: boolean;
|
||||
onDone?: () => void;
|
||||
onEnabledEncryption?: () => void;
|
||||
}
|
||||
|
||||
export function WorkspaceEncryptionSetting({ size, expanded, onDone, onEnabledEncryption }: Props) {
|
||||
const [justEnabledEncryption, setJustEnabledEncryption] = useState<boolean>(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const workspace = useAtomValue(activeWorkspaceAtom);
|
||||
const workspaceMeta = useAtomValue(activeWorkspaceMetaAtom);
|
||||
const [key, setKey] = useState<{ key: string | null; error: string | null } | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (workspaceMeta == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (workspaceMeta?.encryptionKey == null) {
|
||||
setKey({ key: null, error: null });
|
||||
return;
|
||||
}
|
||||
|
||||
revealWorkspaceKey(workspaceMeta.workspaceId).then(
|
||||
(key) => {
|
||||
setKey({ key, error: null });
|
||||
},
|
||||
(err) => {
|
||||
setKey({ key: null, error: `${err}` });
|
||||
},
|
||||
);
|
||||
}, [workspaceMeta, workspaceMeta?.encryptionKey]);
|
||||
|
||||
if (key == null || workspace == null || workspaceMeta == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Prompt for key if it doesn't exist or could not be decrypted
|
||||
if (
|
||||
key.error != null ||
|
||||
(workspace.encryptionKeyChallenge && workspaceMeta.encryptionKey == null)
|
||||
) {
|
||||
return (
|
||||
<EnterWorkspaceKey
|
||||
workspaceMeta={workspaceMeta}
|
||||
error={key.error}
|
||||
onEnabled={() => {
|
||||
onDone?.();
|
||||
onEnabledEncryption?.();
|
||||
}}
|
||||
onDisabled={() => {
|
||||
onDone?.();
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// Show the key if it exists
|
||||
if (workspaceMeta.encryptionKey && key.key != null) {
|
||||
const keyRevealer = (
|
||||
<KeyRevealer
|
||||
disableLabel={justEnabledEncryption}
|
||||
defaultShow={justEnabledEncryption}
|
||||
encryptionKey={key.key}
|
||||
/>
|
||||
);
|
||||
return (
|
||||
<VStack space={2} className="w-full">
|
||||
{justEnabledEncryption && (
|
||||
<Banner color="success" className="flex flex-col gap-2">
|
||||
{helpAfterEncryption}
|
||||
</Banner>
|
||||
)}
|
||||
{keyRevealer}
|
||||
{onDone && (
|
||||
<Button
|
||||
color="secondary"
|
||||
onClick={() => {
|
||||
onDone();
|
||||
onEnabledEncryption?.();
|
||||
}}
|
||||
>
|
||||
Done
|
||||
</Button>
|
||||
)}
|
||||
</VStack>
|
||||
);
|
||||
}
|
||||
|
||||
// Show button to enable encryption
|
||||
return (
|
||||
<div className="mb-auto flex flex-col-reverse">
|
||||
<Button
|
||||
className="mt-3"
|
||||
color={expanded ? 'info' : 'secondary'}
|
||||
size={size}
|
||||
onClick={async () => {
|
||||
setError(null);
|
||||
try {
|
||||
await enableEncryption(workspaceMeta.workspaceId);
|
||||
setJustEnabledEncryption(true);
|
||||
} catch (err) {
|
||||
setError(`Failed to enable encryption: ${err}`);
|
||||
}
|
||||
}}
|
||||
>
|
||||
Enable Encryption
|
||||
</Button>
|
||||
{error && (
|
||||
<Banner color="danger" className="mb-2">
|
||||
{error}
|
||||
</Banner>
|
||||
)}
|
||||
{expanded ? (
|
||||
<Banner color="info" className="mb-6">
|
||||
<EncryptionHelp />
|
||||
</Banner>
|
||||
) : (
|
||||
<Label htmlFor={null} help={<EncryptionHelp />}>
|
||||
Workspace encryption
|
||||
</Label>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const setWorkspaceKeyMut = createFastMutation({
|
||||
mutationKey: ['set-workspace-key'],
|
||||
mutationFn: setWorkspaceKey,
|
||||
});
|
||||
|
||||
function EnterWorkspaceKey({
|
||||
workspaceMeta,
|
||||
onEnabled,
|
||||
onDisabled,
|
||||
error,
|
||||
}: {
|
||||
workspaceMeta: WorkspaceMeta;
|
||||
onEnabled?: () => void;
|
||||
onDisabled?: () => void;
|
||||
error?: string | null;
|
||||
}) {
|
||||
const [key, setKey] = useState<string>('');
|
||||
|
||||
const handleForgotKey = async () => {
|
||||
const confirmed = await showConfirm({
|
||||
id: 'disable-encryption',
|
||||
title: 'Disable Encryption',
|
||||
color: 'danger',
|
||||
confirmText: 'Disable Encryption',
|
||||
description: (
|
||||
<>
|
||||
This will disable encryption for this workspace. Any previously encrypted values will fail
|
||||
to decrypt and will need to be re-entered manually.
|
||||
<br />
|
||||
<br />
|
||||
This action cannot be undone.
|
||||
</>
|
||||
),
|
||||
});
|
||||
|
||||
if (confirmed) {
|
||||
await disableEncryption(workspaceMeta.workspaceId);
|
||||
onDisabled?.();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<VStack space={4} className="w-full">
|
||||
{error ? (
|
||||
<Banner color="danger">{error}</Banner>
|
||||
) : (
|
||||
<Banner color="info">
|
||||
This workspace contains encrypted values but no key is configured. Please enter the
|
||||
workspace key to access the encrypted data.
|
||||
</Banner>
|
||||
)}
|
||||
<HStack
|
||||
as="form"
|
||||
alignItems="end"
|
||||
className="w-full"
|
||||
space={1.5}
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
setWorkspaceKeyMut.mutate(
|
||||
{
|
||||
workspaceId: workspaceMeta.workspaceId,
|
||||
key: key.trim(),
|
||||
},
|
||||
{ onSuccess: onEnabled },
|
||||
);
|
||||
}}
|
||||
>
|
||||
<PlainInput
|
||||
required
|
||||
onChange={setKey}
|
||||
label="Workspace encryption key"
|
||||
placeholder="YK0000-111111-222222-333333-444444-AAAAAA-BBBBBB-CCCCCC-DDDDDD"
|
||||
/>
|
||||
<Button variant="border" type="submit" color="secondary">
|
||||
Submit
|
||||
</Button>
|
||||
</HStack>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleForgotKey}
|
||||
className="text-text-subtlest text-sm hover:text-text-subtle"
|
||||
>
|
||||
Forgot your key?
|
||||
</button>
|
||||
</VStack>
|
||||
);
|
||||
}
|
||||
|
||||
function KeyRevealer({
|
||||
defaultShow = false,
|
||||
disableLabel = false,
|
||||
encryptionKey,
|
||||
}: {
|
||||
defaultShow?: boolean;
|
||||
disableLabel?: boolean;
|
||||
encryptionKey: string;
|
||||
}) {
|
||||
const [show, setShow] = useStateWithDeps<boolean>(defaultShow, [defaultShow]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
'w-full border border-border rounded-md pl-3 py-2 p-1',
|
||||
'grid gap-1 grid-cols-[minmax(0,1fr)_auto] items-center',
|
||||
)}
|
||||
>
|
||||
<VStack space={0.5}>
|
||||
{!disableLabel && (
|
||||
<span className="text-sm text-primary flex items-center gap-1">
|
||||
Workspace encryption key{' '}
|
||||
<IconTooltip iconSize="sm" size="lg" content={helpAfterEncryption} />
|
||||
</span>
|
||||
)}
|
||||
{encryptionKey && <HighlightedKey keyText={encryptionKey} show={show} />}
|
||||
</VStack>
|
||||
<HStack>
|
||||
{encryptionKey && <CopyIconButton text={encryptionKey} title="Copy workspace key" />}
|
||||
<IconButton
|
||||
title={show ? 'Hide' : 'Reveal' + 'workspace key'}
|
||||
icon={show ? 'eye_closed' : 'eye'}
|
||||
onClick={() => setShow((v) => !v)}
|
||||
/>
|
||||
</HStack>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function HighlightedKey({ keyText, show }: { keyText: string; show: boolean }) {
|
||||
return (
|
||||
<span className="text-xs font-mono [&_*]:cursor-auto [&_*]:select-text">
|
||||
{show ? (
|
||||
keyText.split('').map((c, i) => {
|
||||
return (
|
||||
<span
|
||||
// biome-ignore lint/suspicious/noArrayIndexKey: it's fine
|
||||
key={i}
|
||||
className={classNames(
|
||||
c.match(/[0-9]/) && 'text-info',
|
||||
c === '-' && 'text-text-subtle',
|
||||
)}
|
||||
>
|
||||
{c}
|
||||
</span>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<div className="text-text-subtle">•••••••••••••••••••••</div>
|
||||
)}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
const helpAfterEncryption = (
|
||||
<p>
|
||||
The following key is used for encryption operations within this workspace. It is stored securely
|
||||
using your OS keychain, but it is recommended to back it up. If you share this workspace with
|
||||
others, you'll need to send them this key to access any encrypted values.
|
||||
</p>
|
||||
);
|
||||
Reference in New Issue
Block a user