chore: quality of life improvements (#449)

This commit is contained in:
2022-04-13 22:16:34 +02:00
committed by GitHub
parent e916177569
commit c10817716e
34 changed files with 14038 additions and 11804 deletions

View File

@@ -1,5 +1,4 @@
import React from 'react';
import { hot } from 'react-hot-loader/root';
import { Layout } from 'antd';
import { HashRouter as Router } from 'react-router-dom';
import { GithubProvider } from './contexts/Github';
@@ -12,9 +11,9 @@ const App: React.FC = () => {
<GithubProvider>
<EncryptionProvider>
<DecryptionProvider>
<Layout style={{minHeight:"100vh"}}>
<Layout style={{ minHeight: '100vh' }}>
<Router>
<AppRouter/>
<AppRouter />
</Router>
</Layout>
</DecryptionProvider>
@@ -23,4 +22,4 @@ const App: React.FC = () => {
);
};
export default hot(App);
export default App;

View File

@@ -1,10 +1,6 @@
import React from 'react';
import {
Routes,
Route,
useNavigate,
} from 'react-router-dom';
import { HomeFilled } from '@ant-design/icons';
import { Routes, Route, useNavigate } from 'react-router-dom';
import { HomeFilled } from '@ant-design/icons';
import { Layout, Button, Space } from 'antd';
import Intro from './screens/Intro';
@@ -19,14 +15,18 @@ const AppRouter: React.FC = () => {
return (
<>
<Space>
<Button
onClick={() => navigate('/')}
icon={<HomeFilled />}
>
<Button onClick={() => navigate('/')} icon={<HomeFilled />}>
Home
</Button>
</Space>
<Layout.Content style={{ padding: '25px', maxWidth: '800px', width: '100%', margin: 'auto' }}>
<Layout.Content
style={{
padding: '25px',
maxWidth: '800px',
width: '100%',
margin: 'auto',
}}
>
<Routes>
<Route path="/debug" element={<Debug />} />
<Route path="/welcome" element={<Welcome />} />
@@ -38,6 +38,6 @@ const AppRouter: React.FC = () => {
</Layout.Content>
</>
);
}
};
export default AppRouter;

View File

@@ -1,9 +1,5 @@
import React from 'react';
import {
List,
Button,
Popconfirm,
} from 'antd';
import { List, Button, Popconfirm } from 'antd';
import {
DeleteOutlined,
SyncOutlined,
@@ -25,24 +21,17 @@ const iconStyle = {
},
};
const icons: {[name: string]: any} = {
const icons: { [name: string]: any } = {
processing: <SyncOutlined spin {...iconStyle} />,
failed: <IssuesCloseOutlined {...iconStyle} />,
success: <LockOutlined {...iconStyle} />,
};
const IconText = ({ icon, text, ...props }) => (
<Button
{...props}
shape="round"
icon={React.createElement(icon)}
/>
<Button {...props} shape="round" icon={React.createElement(icon)} />
);
const FileView: React.FC<Props> = ({
file,
remove,
}) => {
const FileView: React.FC<Props> = ({ file, remove }) => {
const icon = icons[file.status];
const actions = [];
@@ -54,11 +43,7 @@ const FileView: React.FC<Props> = ({
okText="Yes"
cancelText="No"
>
<IconText
icon={DeleteOutlined}
danger
text="Delete"
/>
<IconText icon={DeleteOutlined} danger text="Delete" />
</Popconfirm>
);
}
@@ -76,14 +61,8 @@ const FileView: React.FC<Props> = ({
}
return (
<List.Item
actions={actions}
className="msg-item"
>
<List.Item.Meta
avatar={icon}
title={file.name}
/>
<List.Item actions={actions} className="msg-item">
<List.Item.Meta avatar={icon} title={file.name} />
</List.Item>
);
};

View File

@@ -6,29 +6,22 @@ import File from './File';
import FileType from '../types/File';
interface Props {
files: {[id: string]: FileType};
files: { [id: string]: FileType };
deleteFile: (id: string) => void;
}
const Encrypt: React.FC<Props> = ({
files,
deleteFile,
}) => {
const Encrypt: React.FC<Props> = ({ files, deleteFile }) => {
const { status, downloadAll } = useDownloadAll();
if (Object.keys(files).length === 0) {
return <Empty />
return <Empty />;
}
return (
<Space direction="vertical" style={{width: '100%' }}>
<Space direction="vertical" style={{ width: '100%' }}>
<List>
{Object.entries(files).map(([id, file]) => (
<File
key={id}
file={file}
remove={() => deleteFile(id)}
/>
<File key={id} file={file} remove={() => deleteFile(id)} />
))}
</List>
{downloadAll && (

View File

@@ -20,10 +20,13 @@ const DropWrapper = styled(Layout)`
const AddFile: React.FC = () => {
const { addFile } = useContext(DecryptionContext);
const onDrop = useCallback(acceptedFiles => {
acceptedFiles.forEach(addFile);
}, [addFile])
const {getRootProps, getInputProps} = useDropzone({ onDrop });
const onDrop = useCallback(
(acceptedFiles) => {
acceptedFiles.forEach(addFile);
},
[addFile]
);
const { getRootProps, getInputProps } = useDropzone({ onDrop });
return (
<DropWrapper {...getRootProps()}>
<input {...getInputProps()} />

View File

@@ -12,9 +12,16 @@ const Add: React.FC = () => {
return (
<>
<Divider>
<Radio.Group onChange={evt => setType(evt.target.value)} defaultValue={DEFAULT_VALUE}>
<Radio.Button className="add-text-tab" value="text"><FileTextOutlined /> Text</Radio.Button>
<Radio.Button className="add-file-tab" value="file"><FileOutlined /> File</Radio.Button>
<Radio.Group
onChange={(evt) => setType(evt.target.value)}
defaultValue={DEFAULT_VALUE}
>
<Radio.Button className="add-text-tab" value="text">
<FileTextOutlined /> Text
</Radio.Button>
<Radio.Button className="add-file-tab" value="file">
<FileOutlined /> File
</Radio.Button>
</Radio.Group>
</Divider>
{type === 'text' && <AddText />}

View File

@@ -20,10 +20,13 @@ const DropWrapper = styled(Layout)`
const AddFile: React.FC = () => {
const { addFile } = useContext(EncryptionContext);
const onDrop = useCallback(acceptedFiles => {
acceptedFiles.forEach(addFile);
}, [addFile])
const {getRootProps, getInputProps} = useDropzone({ onDrop });
const onDrop = useCallback(
(acceptedFiles) => {
acceptedFiles.forEach(addFile);
},
[addFile]
);
const { getRootProps, getInputProps } = useDropzone({ onDrop });
return (
<DropWrapper {...getRootProps()}>
<input {...getInputProps()} />

View File

@@ -3,7 +3,7 @@ import { Input, Form, Button } from 'antd';
import { PlusOutlined } from '@ant-design/icons';
import EncryptionContext from '../../contexts/Encryption';
const AddText : React.FC = () => {
const AddText: React.FC = () => {
const { addText } = useContext(EncryptionContext);
const [name, setName] = useState('');
const [text, setText] = useState('');
@@ -21,16 +21,16 @@ const AddText : React.FC = () => {
placeholder="Title (Not encrypted)"
className="msg-title"
value={name}
onChange={evt => setName(evt.target.value)}
onChange={(evt) => setName(evt.target.value)}
/>
</Form.Item>
<Form.Item>
<Input.TextArea
className="msg-body"
placeholder="Your message here..."
value={text}
value={text}
rows={6}
onChange={evt => setText(evt.target.value)}
onChange={(evt) => setText(evt.target.value)}
/>
</Form.Item>
<Form.Item>
@@ -49,4 +49,3 @@ const AddText : React.FC = () => {
};
export default AddText;

View File

@@ -1,5 +1,19 @@
import React, { useState, useCallback, useContext, createContext, useEffect } from 'react';
import { readMessage, readKey, decrypt as pgpDecrypt, readPrivateKeys, readPrivateKey, generateKey } from 'openpgp';
import React, {
useState,
useCallback,
useContext,
createContext,
useEffect,
ReactNode,
} from 'react';
import {
readMessage,
readKey,
decrypt as pgpDecrypt,
readPrivateKeys,
readPrivateKey,
generateKey,
} from 'openpgp';
import GithubContext from './Github';
import FileType from '../types/File';
import { createFile } from '../helpers/files';
@@ -9,11 +23,15 @@ interface DecryptionContextType {
privateKey: string | undefined;
createKey: (name: string, email: string) => void;
deleteKey: () => void;
files: {[id: string]: FileType};
files: { [id: string]: FileType };
addFile: (file: File) => Promise<void>;
deleteFile: (id: string) => void;
}
type DecryptionProviderProps = {
children: ReactNode;
};
const removeExtension = (name: string) => {
const parts = name.split('.');
parts.pop();
@@ -24,21 +42,32 @@ const DecryptionContext = createContext<DecryptionContextType>({
publicKey: undefined,
privateKey: undefined,
files: {},
createKey: async () => { throw new Error('Not using provider'); },
deleteKey: async () => { throw new Error('Not using provider'); },
addFile: async () => { throw new Error('Not using provider'); },
deleteFile: async () => { throw new Error('Not using provider'); },
createKey: async () => {
throw new Error('Not using provider');
},
deleteKey: async () => {
throw new Error('Not using provider');
},
addFile: async () => {
throw new Error('Not using provider');
},
deleteFile: async () => {
throw new Error('Not using provider');
},
});
const decrypt = async (privateKey: string, keys: string[], content: string) => {
const armoredKeys = await Promise.all(
keys.map(key => readKey({ armoredKey: key })),
keys.map((key) => readKey({ armoredKey: key }))
);
const message = await readMessage({ armoredMessage: content });
const encrypted = await pgpDecrypt({
message,
decryptionKeys: await readPrivateKeys({ armoredKeys: privateKey }),
verificationKeys: armoredKeys.reduce<any>((output, key: any) => [...output, ...key], []),
verificationKeys: armoredKeys.reduce<any>(
(output, key: any) => [...output, ...key],
[]
),
});
const { data } = encrypted;
const blob = new Blob([data as any], {
@@ -47,7 +76,7 @@ const decrypt = async (privateKey: string, keys: string[], content: string) => {
return blob;
};
const DecryptionProvider: React.FC = ({
const DecryptionProvider: React.FC<DecryptionProviderProps> = ({
children,
}) => {
const { keys } = useContext(GithubContext);
@@ -55,12 +84,15 @@ const DecryptionProvider: React.FC = ({
const [publicKey, setPublicKey] = useState<string | undefined>(undefined);
const [files, setFiles] = useState<DecryptionContextType['files']>({});
const deleteFile = useCallback((id: string) => {
delete files[id];
setFiles({
...files,
});
}, [files]);
const deleteFile = useCallback(
(id: string) => {
delete files[id];
setFiles({
...files,
});
},
[files]
);
useEffect(() => {
const run = async () => {
@@ -83,29 +115,34 @@ const DecryptionProvider: React.FC = ({
const createKey = async () => {
const key = await generateKey({
userIDs: [{ name: 'unknown unknown', email: 'unknown@unknown.foo'}],
userIDs: [{ name: 'unknown unknown', email: 'unknown@unknown.foo' }],
curve: 'ed25519',
});
setPrivateKey(key.privateKey);
setPublicKey(key.publicKey);
localStorage.setItem('key', key.privateKey);
}
};
const addFile = useCallback(async (file: File) => {
if (!keys || !privateKey) return;
const addedFile = createFile(setFiles, removeExtension(file.name));
const reader = new FileReader()
const addFile = useCallback(
async (file: File) => {
if (!keys || !privateKey) {
return;
}
const addedFile = createFile(setFiles, removeExtension(file.name));
const reader = new FileReader();
reader.onabort = addedFile.setFailed,
reader.onerror = addedFile.setFailed,
reader.onload = () => {
addedFile.setContent(
decrypt(privateKey, keys, reader.result as string),
);
}
reader.readAsText(file);
}, [keys, privateKey]);
(reader.onabort = addedFile.setFailed),
(reader.onerror = addedFile.setFailed),
(reader.onload = () => {
addedFile.setContent(
decrypt(privateKey, keys, reader.result as string)
);
});
reader.readAsText(file);
},
[keys, privateKey]
);
return (
<DecryptionContext.Provider
@@ -124,8 +161,6 @@ const DecryptionProvider: React.FC = ({
);
};
export {
DecryptionProvider,
};
export { DecryptionProvider };
export default DecryptionContext;;
export default DecryptionContext;

View File

@@ -1,31 +1,50 @@
import React, { useState, useCallback, useContext, createContext } from 'react';
import React, {
useState,
useCallback,
useContext,
createContext,
ReactNode,
} from 'react';
import * as openpgp from 'openpgp';
import GithubContext from './Github';
import { createFile } from '../helpers/files';
import FileType from '../types/File';
interface EncryptionContextType {
files: {[id: string]: FileType};
files: { [id: string]: FileType };
addFile: (file: File) => Promise<void>;
addText: (text: string, name: string) => Promise<void>;
deleteFile: (id: string) => void;
}
type EncryptionProviderProps = {
children: ReactNode;
};
const EncryptionContext = createContext<EncryptionContextType>({
files: {},
addFile: async () => { throw new Error('Not using provider'); },
addText: async () => { throw new Error('Not using provider'); },
deleteFile: async () => { throw new Error('Not using provider'); },
addFile: async () => {
throw new Error('Not using provider');
},
addText: async () => {
throw new Error('Not using provider');
},
deleteFile: async () => {
throw new Error('Not using provider');
},
});
const encrypt = async (keys: string[], content: string) => {
const armoredKeys = await Promise.all(
keys.map(key => openpgp.readKeys({ armoredKeys: key })),
keys.map((key) => openpgp.readKeys({ armoredKeys: key }))
);
const message = await openpgp.createMessage({ text: content });
const encrypted = await openpgp.encrypt({
message,
encryptionKeys: armoredKeys.reduce<any>((output, key: any) => [...output, ...key], []),
encryptionKeys: armoredKeys.reduce<any>(
(output, key: any) => [...output, ...key],
[]
),
});
const data = encrypted;
const blob = new Blob([data as any], {
@@ -34,41 +53,50 @@ const encrypt = async (keys: string[], content: string) => {
return blob;
};
const EncryptionProvider: React.FC = ({
const EncryptionProvider: React.FC<EncryptionProviderProps> = ({
children,
}) => {
const { username, keys } = useContext(GithubContext);
const [files, setFiles] = useState<EncryptionContextType['files']>({});
const deleteFile = useCallback((id: string) => {
delete files[id];
setFiles({
...files,
});
}, [files]);
const deleteFile = useCallback(
(id: string) => {
delete files[id];
setFiles({
...files,
});
},
[files]
);
const addFile = useCallback(async (file: File) => {
if (!keys) return;
const addedFile = createFile(setFiles, `${file.name}.acs`);
const reader = new FileReader()
const addFile = useCallback(
async (file: File) => {
if (!keys) {
return;
}
const addedFile = createFile(setFiles, `${file.name}.acs`);
const reader = new FileReader();
reader.onabort = addedFile.setFailed,
reader.onerror = addedFile.setFailed,
reader.onload = () => {
addedFile.setContent(
encrypt(keys, reader.result as string),
);
}
reader.readAsText(file)
}, [keys, username]);
(reader.onabort = addedFile.setFailed),
(reader.onerror = addedFile.setFailed),
(reader.onload = () => {
addedFile.setContent(encrypt(keys, reader.result as string));
});
reader.readAsText(file);
},
[keys, username]
);
const addText = useCallback(async (text: string, name: string) => {
if (!keys) return;
const file = createFile(setFiles, `${name}.txt.asc`);
file.setContent(
encrypt(keys, text),
);
}, [keys, username]);
const addText = useCallback(
async (text: string, name: string) => {
if (!keys) {
return;
}
const file = createFile(setFiles, `${name}.txt.asc`);
file.setContent(encrypt(keys, text));
},
[keys, username]
);
return (
<EncryptionContext.Provider
@@ -84,9 +112,6 @@ const EncryptionProvider: React.FC = ({
);
};
export {
EncryptionProvider,
};
export { EncryptionProvider };
export default EncryptionContext;

View File

@@ -1,4 +1,4 @@
import React, { createContext } from 'react';
import React, { createContext, ReactNode } from 'react';
declare var data: any;
@@ -7,19 +7,17 @@ interface GithubContextType {
keys?: string[];
}
type GithubProviderProps = {
children: ReactNode;
};
const GithubContext = createContext<GithubContextType>(data);
const GithubProvider: React.FC = ({
children,
}) => (
<GithubContext.Provider
value={{ ...data }}
>
const GithubProvider: React.FC<GithubProviderProps> = ({ children }) => (
<GithubContext.Provider value={{ ...data }}>
{children}
</GithubContext.Provider>
);
export {
GithubProvider,
};
export { GithubProvider };
export default GithubContext;

View File

@@ -11,7 +11,7 @@ export const downloadLink = (name: string, blob: Blob) => {
document.body.removeChild(downloadLink);
};
//type SetFilesType = (fn: (files: {[id: string]: File}) => {[id: string]: File}) => any;
//type SetFilesType = (fn: (files: {[id: string]: File}) => {[id: string]: File}) => any;
type SetFilesType = any;
export const createFile = (setFiles: SetFilesType, name: string) => {
@@ -38,7 +38,7 @@ export const createFile = (setFiles: SetFilesType, name: string) => {
}));
})
.catch(setFailed);
}
};
const setFailed = (err: any) => {
console.error(err);

View File

@@ -8,7 +8,8 @@ type Statuses = 'packing' | 'ready';
const useDownloadAll = () => {
const [status, setStatus] = useState<Statuses>('ready');
const { files } = useContext(EncryptionContext);
const allFilesReady = Object.values(files).filter(f => f.status === 'success').length > 1;
const allFilesReady =
Object.values(files).filter((f) => f.status === 'success').length > 1;
const downloadAll = useCallback(() => {
setStatus('packing');

View File

@@ -1,6 +1,6 @@
import React from 'react';
import 'antd/dist/antd.css';
import { render } from 'react-dom';
import { render } from 'react-dom';
import App from './App';
if ('serviceWorker' in navigator) {

View File

@@ -1,24 +1,25 @@
import React, { useMemo } from 'react';
import {
Table,
} from 'antd';
import { Table } from 'antd';
import config from '../config';
const columns = [{
title: 'Name',
dataIndex: 'name',
key: 'name',
}, {
title: 'Value',
dataIndex: 'value',
key: 'value',
}];
const columns = [
{
title: 'Name',
dataIndex: 'name',
key: 'name',
},
{
title: 'Value',
dataIndex: 'value',
key: 'value',
},
];
const Debug: React.FC = () => {
const data = useMemo(() => {
const vals = {
'Repository': config.repo,
'User': config.user,
Repository: config.repo,
User: config.user,
'Is Production': config.isProd,
};
@@ -29,9 +30,7 @@ const Debug: React.FC = () => {
}));
}, []);
return (
<Table dataSource={data} columns={columns} />
);
return <Table dataSource={data} columns={columns} />;
};
export default Debug;

View File

@@ -13,10 +13,7 @@ const Decrypt: React.FC = () => {
{Object.keys(files).length > 0 && (
<>
<Divider>Files</Divider>
<FileList
files={files}
deleteFile={deleteFile}
/>
<FileList files={files} deleteFile={deleteFile} />
</>
)}
</>

View File

@@ -13,13 +13,18 @@ const Encrypt: React.FC = () => {
{Object.keys(files).length > 0 && (
<>
<Divider>Files</Divider>
<FileList
files={files}
deleteFile={deleteFile}
/>
<FileList files={files} deleteFile={deleteFile} />
<Divider />
<i style={{ textAlign: 'center', paddingTop: '10px', display: 'block', fontSize: 12 }}>
Note: files are not send to me, you still have to download the encrypted files and send it to me.
<i
style={{
textAlign: 'center',
paddingTop: '10px',
display: 'block',
fontSize: 12,
}}
>
Note: files are not send to me, you still have to download the
encrypted files and send it to me.
</i>
</>
)}

View File

@@ -1,23 +1,14 @@
import React from 'react';
import { useNavigate } from 'react-router';
import Welcome from './Welcome';
import {
Button,
Space,
} from 'antd';
import { Button, Space } from 'antd';
import {
UploadOutlined,
DownloadOutlined,
KeyOutlined,
} from '@ant-design/icons';
const Thumb: React.FC = ({
title,
Icon,
link,
className,
}) => {
const Thumb: React.FC = ({ title, Icon, link, className }) => {
const navigate = useNavigate();
return (
<Button
@@ -43,7 +34,7 @@ const Intro = () => {
link="/send"
className="send-btn"
Icon={UploadOutlined}
/>
/>
<Thumb
link="/key"
className="want-to-receive-btn"

View File

@@ -10,11 +10,7 @@ import {
} from '@ant-design/icons';
const SetupKey: React.FC = () => {
const {
createKey,
deleteKey,
publicKey,
} = useContext(DecryptionContext);
const { createKey, deleteKey, publicKey } = useContext(DecryptionContext);
const [name, setName] = useState('');
const [email, setEmail] = useState('');
@@ -33,15 +29,15 @@ const SetupKey: React.FC = () => {
if (!publicKey) {
return (
<>
<Space direction="vertical" style={{ textAlign: 'center' }} >
<Space direction="vertical" style={{ textAlign: 'center' }}>
<LockTwoTone style={{ fontSize: 150 }} />
<Typography.Title>Create your sharing key</Typography.Title>
<p>
Before I can send protected information to you I need a "sharing" key, which is a key that gets stored this device, allowing this device (and this device only) to read the informations I am sending.
</p>
<p>
After creating it you need to send it to me
Before I can send protected information to you I need a "sharing"
key, which is a key that gets stored this device, allowing this
device (and this device only) to read the informations I am sending.
</p>
<p>After creating it you need to send it to me</p>
</Space>
<Form>
<Form.Item>
@@ -50,7 +46,7 @@ const SetupKey: React.FC = () => {
size="large"
prefix={<UserOutlined />}
value={name}
onChange={evt => setName(evt.target.value)}
onChange={(evt) => setName(evt.target.value)}
/>
</Form.Item>
<Form.Item>
@@ -59,10 +55,10 @@ const SetupKey: React.FC = () => {
size="large"
prefix={<MailOutlined />}
value={email}
onChange={evt => setEmail(evt.target.value)}
/>
onChange={(evt) => setEmail(evt.target.value)}
/>
</Form.Item>
<Form.Item style={{ textAlign: 'center' }} >
<Form.Item style={{ textAlign: 'center' }}>
<Button
disabled={!name || !email}
type="primary"
@@ -81,13 +77,12 @@ const SetupKey: React.FC = () => {
<div style={{ textAlign: 'center' }}>
<RocketTwoTone style={{ fontSize: 150 }} />
<Typography.Title>Okay, you are all set.</Typography.Title>
<p>Just send me your sharing key, and I will send files using it.</p>
<p>
Just send me your sharing key, and I will send files using it.
Remember that you need to go to this website on this device to decrypt
the files after receiving them
</p>
<p>
Remember that you need to go to this website on this device to decrypt the files after receiving them
</p>
<Space direction="vertical" size="large">
<Space direction="vertical" size="large">
<Button
onClick={downloadPublicKey}
type="primary"
@@ -96,15 +91,8 @@ const SetupKey: React.FC = () => {
>
Download sharing key
</Button>
<Popconfirm
title="Are you sure?"
onConfirm={deleteKey}
>
<Button
danger
size="small"
type="link"
>
<Popconfirm title="Are you sure?" onConfirm={deleteKey}>
<Button danger size="small" type="link">
Delete sharing key
</Button>
</Popconfirm>

View File

@@ -8,15 +8,16 @@ const Welcome: React.FC = () => {
<Space direction="vertical">
<EyeInvisibleTwoTone style={{ fontSize: 200 }} />
<Typography.Title level={1}>Protect before sending</Typography.Title>
<p>The internet can seem like a scary place...</p>
<p>
The internet can seem like a scary place...
Especially because a lot of the tools we use everyday (such as e-mail)
wasn't build for the internet that we have today. This is why it is
important to have an additional layer of security when sending
sensitive information.
</p>
<p>
Especially because a lot of the tools we use everyday (such as e-mail) wasn't build for the internet that we have today.
This is why it is important to have an additional layer of security when sending sensitive information.
</p>
<p>
This is a tool that will help you have that extra layer of security when sharing files with me.
This is a tool that will help you have that extra layer of security
when sharing files with me.
</p>
</Space>
</Layout>

View File

@@ -10,7 +10,7 @@ interface FileProcessing extends FileBase {
}
interface FileFailed extends FileBase {
status: 'failed',
status: 'failed';
error: any;
}