Skip to content

Commit

Permalink
- feature: added feature to export encrypted credentials
Browse files Browse the repository at this point in the history
  • Loading branch information
agallardol committed Oct 31, 2023
1 parent de6fb21 commit 49cacb2
Show file tree
Hide file tree
Showing 8 changed files with 262 additions and 14 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
import { zodResolver } from '@hookform/resolvers/zod';
import { Download, FileKey } from 'lucide-react';
import { QRCodeCanvas } from 'qrcode.react';
import { useEffect, useRef, useState } from 'react';
import { useForm } from 'react-hook-form';
import { FormattedMessage, useIntl } from 'react-intl';
import { z } from 'zod';

import shinkaiLogo from '../../assets/icons/shinkai-min.svg';
import { srcUrlResolver } from '../../helpers/src-url-resolver';
import { useAuth } from '../../store/auth/auth';
import { Button } from '../ui/button';
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '../ui/form';
import { Input } from '../ui/input';

export const ExportConnection = () => {
const intl = useIntl();
const formSchema = z
.object({
passphrase: z.string().min(8),
confirmPassphrase: z.string().min(8),
})
.superRefine(({ passphrase, confirmPassphrase }, ctx) => {
if (passphrase !== confirmPassphrase) {
ctx.addIssue({
code: 'custom',
message: intl.formatMessage({ id: 'passphrases-dont-match'}),
path: ['confirmPassphrase'],
});
}
});
type FormSchemaType = z.infer<typeof formSchema>;
const auth = useAuth((state) => state.auth);
const form = useForm<FormSchemaType>({
resolver: zodResolver(formSchema),
defaultValues: {
passphrase: '',
confirmPassphrase: '',
},
});
const qrCanvasContainerRef = useRef<HTMLDivElement>(null);
const passphrase = form.watch('passphrase');
const confirmPassphrase = form.watch('confirmPassphrase');
const [encryptedSetupData, setEncryptedSetupData] = useState<string>('');
useEffect(() => {
setEncryptedSetupData('');
}, [passphrase, confirmPassphrase, setEncryptedSetupData]);
const exportConnection = (values: FormSchemaType): void => {
// TODO: Convert to a common format
const parsedSetupData = JSON.stringify(auth);
const encryptedSetupData = parsedSetupData; // TODO: call shinkai-typescript
setEncryptedSetupData(encryptedSetupData);
};
const qrPropsCanvas = {
level: 'L',
size: 150,
imageSettings: {
src: srcUrlResolver(shinkaiLogo),
x: undefined,
y: undefined,
height: 24,
width: 24,
excavate: true,
includeMargin: false,
},
};
const downloadQR = (): void => {
const canvas: HTMLCanvasElement | undefined = qrCanvasContainerRef?.current?.children[0] as HTMLCanvasElement;
if (!qrCanvasContainerRef.current || !canvas) {
return;
}
const imageRef = canvas.toDataURL('image/jpg');
const dummyAnchor = document.createElement('a');
dummyAnchor.href = imageRef;
dummyAnchor.download = `shinkai-${auth?.registration_name}-backup.jpg`;
document.body.appendChild(dummyAnchor);
dummyAnchor.click();
document.body.removeChild(dummyAnchor);
};
return (
<div className="h-full flex flex-col space-y-3">
<div className="flex flex-row space-x-1 items-center">
<FileKey className="h-4 w-4" />
<h1 className="font-semibold">
<FormattedMessage id="export-connection"></FormattedMessage>
</h1>
</div>
<div className="grow flex flex-col space-y-2">
<Form {...form}>
<form
className="flex flex-col space-y-3 justify-between"
onSubmit={form.handleSubmit(exportConnection)}
>
<div className="grow flex flex-col space-y-2">
<FormField
control={form.control}
name="passphrase"
render={({ field }) => (
<FormItem>
<FormLabel>
<FormattedMessage id="passphrase" />
</FormLabel>
<FormControl>
<Input {...field} type="password" />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="confirmPassphrase"
render={({ field }) => (
<FormItem>
<FormLabel>
<FormattedMessage id="confirm-passphrase" />
</FormLabel>
<FormControl>
<Input {...field} type="password" />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
<Button className="w-full" type="submit">
<FileKey className="mr-2 h-4 w-4" />
<FormattedMessage id="generate-connection-file" />
</Button>
</form>
</Form>

{encryptedSetupData && (
<div className=" grow flex flex-col items-center justify-center space-y-3">
<span>
<FormattedMessage id="download-connection-file-description" />
</span>
<div className="w-full flex flex-col space-y-2 justify-center items-center">
<div ref={qrCanvasContainerRef}>
<QRCodeCanvas
{...qrPropsCanvas}
value={encryptedSetupData}
/>
</div>
<Button className="w-[150px]" onClick={() => downloadQR()}>
<Download className="mr-2 h-4 w-4" />
<FormattedMessage id="download" />
</Button>
</div>
</div>
)}
</div>
</div>
);
};
45 changes: 35 additions & 10 deletions apps/shinkai-visor/src/components/nav/nav.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,14 @@
import { ArrowLeft, Bot, Inbox, LogOut, Menu, MessageCircle, Workflow, X } from 'lucide-react';
import {
ArrowLeft,
Bot,
Inbox,
LogOut,
Menu,
MessageCircle,
Settings,
Workflow,
X,
} from 'lucide-react';
import { useState } from 'react';
import { FormattedMessage } from 'react-intl';
import { useHistory, useLocation } from 'react-router-dom';
Expand All @@ -24,6 +34,7 @@ enum MenuOption {
Agents = 'agents',
AddAgent = 'add-agent',
CreateJob = 'create-job',
Settings = 'settings',
Logout = 'logout',
}

Expand All @@ -34,17 +45,16 @@ export default function NavBar() {
const uiContainer = useUIContainer((state) => state.uiContainer);

const [isMenuOpened, setMenuOpened] = useState(false);
const isRootPage = ['/inboxes', '/agents'].includes(
location.pathname
);
const isRootPage = ['/inboxes', '/agents', '/settings'].includes(location.pathname);
const goBack = () => {
history.goBack();
}
};
const logout = (): void => {
setLogout();
};

const onClickMenuOption = (key: MenuOption) => {
console.log('menu option', key, MenuOption.Settings);
switch (key) {
case MenuOption.Inbox:
history.push('/inboxes');
Expand All @@ -61,6 +71,9 @@ export default function NavBar() {
case MenuOption.AddAgent:
history.push('/agents/add');
break;
case MenuOption.Settings:
history.push('/settings');
break;
case MenuOption.Logout:
logout();
break;
Expand All @@ -71,11 +84,15 @@ export default function NavBar() {
return (
<nav className="">
<div className="flex items-center justify-between">
<div className={`flex-none ${isRootPage || history.length <= 1 ? 'invisible' : ''}`}>
<Button onClick={() => goBack()} size="icon" variant="ghost">
<ArrowLeft className="h-4 w-4" />
</Button>
</div>
<div
className={`flex-none ${
isRootPage || history.length <= 1 ? 'invisible' : ''
}`}
>
<Button onClick={() => goBack()} size="icon" variant="ghost">
<ArrowLeft className="h-4 w-4" />
</Button>
</div>
<img
alt="shinkai-app-logo"
className="h-5"
Expand Down Expand Up @@ -149,6 +166,14 @@ export default function NavBar() {
<DropdownMenuLabel>
<FormattedMessage id="account.one"></FormattedMessage>
</DropdownMenuLabel>
<DropdownMenuItem
onClick={() => onClickMenuOption(MenuOption.Settings)}
>
<Settings className="mr-2 h-4 w-4" />
<span>
<FormattedMessage id="setting.other" />
</span>
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => onClickMenuOption(MenuOption.Logout)}
>
Expand Down
18 changes: 17 additions & 1 deletion apps/shinkai-visor/src/components/popup/popup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,10 @@ import { Agents } from '../agents/agents';
import { AnimatedRoute } from '../animated-route/animated-routed';
import { CreateInbox } from '../create-inbox/create-inbox';
import { CreateJob } from '../create-job/create-job';
import { ExportConnection } from '../export-connection/export-connection';
import { Inbox } from '../inbox/inbox';
import { Inboxes } from '../inboxes/inboxes';
import { Settings } from '../settings/settings';
import { SplashScreen } from '../splash-screen/splash-screen';
import Welcome from '../welcome/welcome';
import { WithNav } from '../with-nav/with-nav';
Expand All @@ -31,9 +33,10 @@ export const Popup = () => {
const auth = useAuth((state) => state.auth);
const location = useLocation();
const [popupVisibility] = useGlobalPopupChromeMessage();

useEffect(() => {
const isAuthenticated = !!auth;
console.log('isAuth', isAuthenticated, auth);
if (isAuthenticated) {
ApiConfig.getInstance().setEndpoint(auth.node_address);
history.replace('/inboxes');
Expand All @@ -42,6 +45,9 @@ export const Popup = () => {
history.replace('/welcome');
}
}, [history, auth]);
useEffect(() => {
console.log('location', location.pathname);
}, [location]);
return (
<AnimatePresence>
{popupVisibility && (
Expand Down Expand Up @@ -101,6 +107,16 @@ export const Popup = () => {
</Route>
</Switch>
</Route>
<Route path="/settings">
<Switch>
<Route path="/settings/export-connection">
<ExportConnection></ExportConnection>
</Route>
<Route path="/">
<Settings></Settings>
</Route>
</Switch>
</Route>
</WithNav>
</AnimatedRoute>
</Route>
Expand Down
28 changes: 28 additions & 0 deletions apps/shinkai-visor/src/components/settings/settings.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { FileKey, SettingsIcon } from 'lucide-react';
import { FormattedMessage } from 'react-intl';
import { useHistory } from 'react-router';

import { Button } from '../ui/button';

export const Settings = () => {
const history = useHistory();
const exportConnection = () => {
history.push('settings/export-connection');
};
return (
<div className="flex flex-col space-y-3">
<div className="flex flex-row space-x-1 items-center">
<SettingsIcon className="h-4 w-4" />
<h1 className="font-semibold">
<FormattedMessage id="setting.other"></FormattedMessage>
</h1>
</div>
<div>
<Button className="w-full" onClick={() => exportConnection()}>
<FileKey className="mr-2 h-4 w-4" />
<FormattedMessage id="export-connection" />
</Button>
</div>
</div>
);
};
11 changes: 9 additions & 2 deletions apps/shinkai-visor/src/lang/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"inbox.one": "Inbox",
"inbox.other": "Inboxes",
"setting.one": "Setting",
"settings.other": "Settings",
"setting.other": "Settings",
"about": "About Shinkai",
"add-node": "Add Node",
"registration-code": "Registration Code",
Expand Down Expand Up @@ -52,5 +52,12 @@
"empty-agents-message": "Connect your first agent to start asking Shinkai AI. Try connecting OpenAI",
"today": "Today",
"yesterday": "Yesterday",
"installed-sucessfully": "Shinkai Visor was installed sucessfully, navigate to a website and use the action button to start asking Shinkai AI"
"installed-sucessfully": "Shinkai Visor was installed sucessfully, navigate to a website and use the action button to start asking Shinkai AI",
"export-connection": "Export connection",
"generate-connection-file": "Generate connection file",
"passphrase": "Passphrase",
"confirm-passphrase": "Confirm passphrase",
"download": "Download",
"download-connection-file-description": "Download and keep this QR code in a safe place. Use it with your passphrase to connect to your node again.",
"passphrases-dont-match": "Passphrases don't match"
}
2 changes: 1 addition & 1 deletion apps/shinkai-visor/src/lang/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"inbox.one": "Bandeja de entrada",
"inbox.other": "Bandejas de entrada",
"setting.one": "Configuración",
"settings.other": "Configuraciones",
"setting.other": "Configuraciones",
"about": "Acerca de Shinkai",
"add-node": "Conectar nodo",
"registration-code": "Código de registración",
Expand Down
9 changes: 9 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,7 @@
"jspdf": "^2.5.1",
"lucide-react": "^0.263.1",
"node-forge": ">=1.0.0",
"qrcode.react": "^3.1.0",
"react": "18.2.0",
"react-dom": "18.2.0",
"react-hook-form": "^7.47.0",
Expand Down

0 comments on commit 49cacb2

Please sign in to comment.