Show error when enabling encryption fails

This commit is contained in:
Gregory Schier
2025-11-03 14:34:43 -08:00
parent bf97ea1659
commit 7e1eb90d29
3 changed files with 110 additions and 70 deletions

View File

@@ -3,7 +3,7 @@ use crate::window_menu::app_menu;
use log::{info, warn}; use log::{info, warn};
use rand::random; use rand::random;
use tauri::{ use tauri::{
AppHandle, Emitter, LogicalSize, Manager, Runtime, WebviewUrl, WebviewWindow, WindowEvent, AppHandle, Emitter, LogicalSize, Manager, PhysicalSize, Runtime, WebviewUrl, WebviewWindow, WindowEvent
}; };
use tauri_plugin_opener::OpenerExt; use tauri_plugin_opener::OpenerExt;
use tokio::sync::mpsc; use tokio::sync::mpsc;
@@ -160,6 +160,11 @@ pub(crate) fn create_window<R: Runtime>(
"dev.reset_size" => webview_window "dev.reset_size" => webview_window
.set_size(LogicalSize::new(DEFAULT_WINDOW_WIDTH, DEFAULT_WINDOW_HEIGHT)) .set_size(LogicalSize::new(DEFAULT_WINDOW_WIDTH, DEFAULT_WINDOW_HEIGHT))
.unwrap(), .unwrap(),
"dev.reset_size_record" => {
let width = webview_window.outer_size().unwrap().width;
let height = width * 9 / 16;
webview_window.set_size(PhysicalSize::new(width, height)).unwrap()
}
"dev.refresh" => webview_window.eval("location.reload()").unwrap(), "dev.refresh" => webview_window.eval("location.reload()").unwrap(),
"dev.generate_theme_css" => { "dev.generate_theme_css" => {
w.emit("generate_theme_css", true).unwrap(); w.emit("generate_theme_css", true).unwrap();

View File

@@ -143,6 +143,8 @@ pub fn app_menu<R: Runtime>(app_handle: &AppHandle<R>) -> tauri::Result<Menu<R>>
.build(app_handle)?, .build(app_handle)?,
&MenuItemBuilder::with_id("dev.reset_size".to_string(), "Reset Size") &MenuItemBuilder::with_id("dev.reset_size".to_string(), "Reset Size")
.build(app_handle)?, .build(app_handle)?,
&MenuItemBuilder::with_id("dev.reset_size_record".to_string(), "Reset Size 16x9")
.build(app_handle)?,
&MenuItemBuilder::with_id( &MenuItemBuilder::with_id(
"dev.generate_theme_css".to_string(), "dev.generate_theme_css".to_string(),
"Generate Theme CSS", "Generate Theme CSS",

View File

@@ -1,35 +1,49 @@
import { enableEncryption, revealWorkspaceKey, setWorkspaceKey } from '@yaakapp-internal/crypto'; import {
import type { WorkspaceMeta } from '@yaakapp-internal/models'; enableEncryption,
import classNames from 'classnames'; revealWorkspaceKey,
import { useAtomValue } from 'jotai'; setWorkspaceKey,
import { useEffect, useState } from 'react'; } from "@yaakapp-internal/crypto";
import { activeWorkspaceAtom, activeWorkspaceMetaAtom } from '../hooks/useActiveWorkspace'; import type { WorkspaceMeta } from "@yaakapp-internal/models";
import { createFastMutation } from '../hooks/useFastMutation'; import classNames from "classnames";
import { useStateWithDeps } from '../hooks/useStateWithDeps'; import { useAtomValue } from "jotai";
import { CopyIconButton } from './CopyIconButton'; import { useEffect, useState } from "react";
import { Banner } from './core/Banner'; import {
import type { ButtonProps } from './core/Button'; activeWorkspaceAtom,
import { Button } from './core/Button'; activeWorkspaceMetaAtom,
import { IconButton } from './core/IconButton'; } from "../hooks/useActiveWorkspace";
import { IconTooltip } from './core/IconTooltip'; import { createFastMutation } from "../hooks/useFastMutation";
import { Label } from './core/Label'; import { useStateWithDeps } from "../hooks/useStateWithDeps";
import { PlainInput } from './core/PlainInput'; import { CopyIconButton } from "./CopyIconButton";
import { HStack, VStack } from './core/Stacks'; import { Banner } from "./core/Banner";
import { EncryptionHelp } from './EncryptionHelp'; 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 { interface Props {
size?: ButtonProps['size']; size?: ButtonProps["size"];
expanded?: boolean; expanded?: boolean;
onDone?: () => void; onDone?: () => void;
onEnabledEncryption?: () => void; onEnabledEncryption?: () => void;
} }
export function WorkspaceEncryptionSetting({ size, expanded, onDone, onEnabledEncryption }: Props) { export function WorkspaceEncryptionSetting(
const [justEnabledEncryption, setJustEnabledEncryption] = useState<boolean>(false); { size, expanded, onDone, onEnabledEncryption }: Props,
) {
const [justEnabledEncryption, setJustEnabledEncryption] = useState<boolean>(
false,
);
const [error, setError] = useState<string | null>(null);
const workspace = useAtomValue(activeWorkspaceAtom); const workspace = useAtomValue(activeWorkspaceAtom);
const workspaceMeta = useAtomValue(activeWorkspaceMetaAtom); const workspaceMeta = useAtomValue(activeWorkspaceMetaAtom);
const [key, setKey] = useState<{ key: string | null; error: string | null } | null>(null); const [key, setKey] = useState<
{ key: string | null; error: string | null } | null
>(null);
useEffect(() => { useEffect(() => {
if (workspaceMeta == null) { if (workspaceMeta == null) {
@@ -108,30 +122,39 @@ export function WorkspaceEncryptionSetting({ size, expanded, onDone, onEnabledEn
return ( return (
<div className="mb-auto flex flex-col-reverse"> <div className="mb-auto flex flex-col-reverse">
<Button <Button
color={expanded ? 'info' : 'secondary'} color={expanded ? "info" : "secondary"}
size={size} size={size}
onClick={async () => { onClick={async () => {
setJustEnabledEncryption(true); setError(null);
await enableEncryption(workspaceMeta.workspaceId); try {
throw new Error("Platform secure storage error");
await enableEncryption(workspaceMeta.workspaceId);
setJustEnabledEncryption(true);
} catch (err) {
setError("Failed to enable encryption: " + err);
}
}} }}
> >
Enable Encryption Enable Encryption
</Button> </Button>
{expanded ? ( {error && <Banner color="danger" className="mb-2">{error}</Banner>}
<Banner color="info" className="mb-6"> {expanded
<EncryptionHelp /> ? (
</Banner> <Banner color="info" className="mb-6">
) : ( <EncryptionHelp />
<Label htmlFor={null} help={<EncryptionHelp />}> </Banner>
Workspace encryption )
</Label> : (
)} <Label htmlFor={null} help={<EncryptionHelp />}>
Workspace encryption
</Label>
)}
</div> </div>
); );
} }
const setWorkspaceKeyMut = createFastMutation({ const setWorkspaceKeyMut = createFastMutation({
mutationKey: ['set-workspace-key'], mutationKey: ["set-workspace-key"],
mutationFn: setWorkspaceKey, mutationFn: setWorkspaceKey,
}); });
@@ -144,15 +167,13 @@ function EnterWorkspaceKey({
onEnabled?: () => void; onEnabled?: () => void;
error?: string | null; error?: string | null;
}) { }) {
const [key, setKey] = useState<string>(''); const [key, setKey] = useState<string>("");
return ( return (
<VStack space={4} className="w-full"> <VStack space={4} className="w-full">
{error ? ( {error ? <Banner color="danger">{error}</Banner> : (
<Banner color="danger">{error}</Banner>
) : (
<Banner color="info"> <Banner color="info">
This workspace contains encrypted values but no key is configured. Please enter the This workspace contains encrypted values but no key is configured.
workspace key to access the encrypted data. Please enter the workspace key to access the encrypted data.
</Banner> </Banner>
)} )}
<HStack <HStack
@@ -199,24 +220,35 @@ function KeyRevealer({
return ( return (
<div <div
className={classNames( className={classNames(
'w-full border border-border rounded-md pl-3 py-2 p-1', "w-full border border-border rounded-md pl-3 py-2 p-1",
'grid gap-1 grid-cols-[minmax(0,1fr)_auto] items-center', "grid gap-1 grid-cols-[minmax(0,1fr)_auto] items-center",
)} )}
> >
<VStack space={0.5}> <VStack space={0.5}>
{!disableLabel && ( {!disableLabel && (
<span className="text-sm text-primary flex items-center gap-1"> <span className="text-sm text-primary flex items-center gap-1">
Workspace encryption key{' '} Workspace encryption key{" "}
<IconTooltip iconSize="sm" size="lg" content={helpAfterEncryption} /> <IconTooltip
iconSize="sm"
size="lg"
content={helpAfterEncryption}
/>
</span> </span>
)} )}
{encryptionKey && <HighlightedKey keyText={encryptionKey} show={show} />} {encryptionKey && (
<HighlightedKey
keyText={encryptionKey}
show={show}
/>
)}
</VStack> </VStack>
<HStack> <HStack>
{encryptionKey && <CopyIconButton text={encryptionKey} title="Copy workspace key" />} {encryptionKey && (
<CopyIconButton text={encryptionKey} title="Copy workspace key" />
)}
<IconButton <IconButton
title={show ? 'Hide' : 'Reveal' + 'workspace key'} title={show ? "Hide" : "Reveal" + "workspace key"}
icon={show ? 'eye_closed' : 'eye'} icon={show ? "eye_closed" : "eye"}
onClick={() => setShow((v) => !v)} onClick={() => setShow((v) => !v)}
/> />
</HStack> </HStack>
@@ -227,31 +259,32 @@ function KeyRevealer({
function HighlightedKey({ keyText, show }: { keyText: string; show: boolean }) { function HighlightedKey({ keyText, show }: { keyText: string; show: boolean }) {
return ( return (
<span className="text-xs font-mono [&_*]:cursor-auto [&_*]:select-text"> <span className="text-xs font-mono [&_*]:cursor-auto [&_*]:select-text">
{show ? ( {show
keyText.split('').map((c, i) => { ? (
return ( keyText.split("").map((c, i) => {
<span return (
key={i} <span
className={classNames( key={i}
c.match(/[0-9]/) && 'text-info', className={classNames(
c == '-' && 'text-text-subtle', c.match(/[0-9]/) && "text-info",
)} c == "-" && "text-text-subtle",
> )}
{c} >
</span> {c}
); </span>
}) );
) : ( })
<div className="text-text-subtle"></div> )
)} : <div className="text-text-subtle"></div>}
</span> </span>
); );
} }
const helpAfterEncryption = ( const helpAfterEncryption = (
<p> <p>
The following key is used for encryption operations within this workspace. It is stored securely The following key is used for encryption operations within this workspace.
using your OS keychain, but it is recommended to back it up. If you share this workspace with It is stored securely using your OS keychain, but it is recommended to back
others, you&apos;ll need to send them this key to access any encrypted values. it up. If you share this workspace with others, you&apos;ll need to send
them this key to access any encrypted values.
</p> </p>
); );