This commit is contained in:
2020-08-19 22:55:44 +02:00
parent 312c8754bc
commit b84aa3db45
18 changed files with 1184 additions and 146 deletions

View File

@@ -12,6 +12,7 @@ const config = (api) => {
'GITHUB_REPOSITORY', 'GITHUB_REPOSITORY',
], ],
}], }],
[require.resolve('react-hot-loader/babel')],
], ],
}; };
}; };

View File

@@ -7,19 +7,27 @@
"build": "webpack" "build": "webpack"
}, },
"devDependencies": { "devDependencies": {
"@ant-design/icons": "^4.2.2",
"@babel/core": "^7.11.1", "@babel/core": "^7.11.1",
"@babel/preset-env": "^7.11.0", "@babel/preset-env": "^7.11.0",
"@babel/preset-react": "^7.10.4", "@babel/preset-react": "^7.10.4",
"@babel/preset-typescript": "^7.10.4", "@babel/preset-typescript": "^7.10.4",
"@hot-loader/react-dom": "^16.13.0",
"@types/html-webpack-plugin": "^3.2.3", "@types/html-webpack-plugin": "^3.2.3",
"@types/openpgp": "^4.4.12", "@types/openpgp": "^4.4.12",
"@types/react": "^16.9.46", "@types/react": "^16.9.46",
"@types/react-dom": "^16.9.8", "@types/react-dom": "^16.9.8",
"@types/react-router": "^5.1.8",
"@types/react-router-dom": "^5.1.5",
"@types/styled-components": "^5.1.2", "@types/styled-components": "^5.1.2",
"antd": "^4.5.4",
"babel-loader": "^8.1.0", "babel-loader": "^8.1.0",
"babel-plugin-transform-inline-environment-variables": "^0.4.3", "babel-plugin-transform-inline-environment-variables": "^0.4.3",
"css-loader": "^4.2.1",
"html-webpack-plugin": "^4.3.0", "html-webpack-plugin": "^4.3.0",
"parcel-bundler": "^1.12.4", "parcel-bundler": "^1.12.4",
"react-hot-loader": "^4.12.21",
"style-loader": "^1.2.1",
"ts-node": "^8.10.2", "ts-node": "^8.10.2",
"typescript": "^3.9.7", "typescript": "^3.9.7",
"webpack": "^4.44.1", "webpack": "^4.44.1",
@@ -33,6 +41,8 @@
"react-dom": "^16.13.1", "react-dom": "^16.13.1",
"react-dropzone": "^11.0.3", "react-dropzone": "^11.0.3",
"react-feather": "^2.0.8", "react-feather": "^2.0.8",
"react-router": "^5.2.0",
"react-router-dom": "^5.2.0",
"styled-components": "^5.1.1" "styled-components": "^5.1.1"
}, },
"browserslist": [ "browserslist": [

View File

@@ -1,19 +1,20 @@
import React from 'react'; import React from 'react';
import { ThemeProvider } from 'styled-components'; import { hot } from 'react-hot-loader/root';
import { Layout } from 'antd';
import { GithubProvider } from './contexts/Github'; import { GithubProvider } from './contexts/Github';
import { EncryptionProvider } from './contexts/Encryption'; import { EncryptionProvider } from './contexts/Encryption';
import Encrypt from './screens/Encrypt'; import AppRouter from './Router';
import theme from './theme';
const App: React.FC = () => ( const App: React.FC = () => (
<ThemeProvider theme={theme}> <GithubProvider username="morten-olsen">
<GithubProvider username="morten-olsen"> <EncryptionProvider>
<div>Test</div> <Layout>
<EncryptionProvider> <Layout.Content style={{ padding: '25px' }}>
<Encrypt /> <AppRouter/>
</EncryptionProvider> </Layout.Content>
</GithubProvider> </Layout>
</ThemeProvider> </EncryptionProvider>
</GithubProvider>
); );
export default App; export default hot(App);

24
src/Router.tsx Normal file
View File

@@ -0,0 +1,24 @@
import React from 'react';
import {
HashRouter as Router,
Switch,
Route,
} from 'react-router-dom';
import Encrypt from './screens/Encrypt';
import Debug from './screens/Debug';
const AppRouter: React.FC = () => (
<Router>
<Switch>
<Route path="/debug">
<Debug />
</Route>
<Route path="/">
<Encrypt />
</Route>
</Switch>
</Router>
);
export default AppRouter;

View File

@@ -1,49 +1,49 @@
import React, { useState } from 'react'; import React, { Fragment, useState } from 'react';
import styled from 'styled-components'; import { Menu, Dropdown, Form } from 'antd';
import { Theme } from '../theme'; import { DownOutlined, FileOutlined, FileTextOutlined } from '@ant-design/icons';
import AddText from './AddText'; import AddText from './AddText';
import AddFile from './AddFile'; import AddFile from './AddFile';
const Wrapper = styled.div` const layout = {
`; labelCol: { span: 2 },
};
const Top = styled.div`
display: flex;
`;
const Button = styled.button<{
active: boolean;
theme: Theme;
}>`
background: ${({ active }) => active ? '#2c3e50' : 'transparent'};
padding: ${({ theme }) => theme.margin.medium}px;
border: none;
font: inherit;
color: inherit;
`;
const Panel = styled.div<{
theme: Theme;
}>`
background: #2c3e50;
color: #fff;
padding: ${({ theme }) => theme.margin.medium}px;
`;
const Add: React.FC = () => { const Add: React.FC = () => {
const [type, setType] = useState<'file' | 'text'>('text'); const [type, setType] = useState<'file' | 'text'>('text');
const menu = (
<Menu>
<Menu.Item
onClick={() => setType('file')}
active={type === 'file'}
icon={<FileOutlined />}
>
File
</Menu.Item>
<Menu.Item
onClick={() => setType('text')}
active={type === 'text'}
icon={<FileTextOutlined />}
>
Text
</Menu.Item>
</Menu>
);
return ( return (
<Wrapper> <>
<Top> <Form {...layout}>
<Button active={type==='text'} onClick={() => setType('text')}>Text</Button> <Form.Item
<Button active={type==='file'} onClick={() => setType('file')}>File</Button> label="I want to encrypt a"
</Top> >
<Panel> <Dropdown overlay={menu}>
{type === 'file' && <AddFile />} <a>{type} <DownOutlined /></a>
{type === 'text' && <AddText />} </Dropdown>
</Panel> </Form.Item>
</Wrapper> </Form>
{type === 'text' && <AddText />}
{type === 'file' && <AddFile />}
</>
); );
}; };

View File

@@ -1,10 +1,15 @@
import React, { useContext, useCallback } from 'react'; import React, { useContext, useCallback } from 'react';
import styled from 'styled-components'; import styled from 'styled-components';
import { Layout } from 'antd';
import { UploadOutlined } from '@ant-design/icons';
import { useDropzone } from 'react-dropzone'; import { useDropzone } from 'react-dropzone';
import EncryptionContext from '../contexts/Encryption'; import EncryptionContext from '../contexts/Encryption';
import { Upload } from 'react-feather';
const DropWrapper = styled.div` const Icon = styled(UploadOutlined)`
font-size: 100px;
`;
const DropWrapper = styled(Layout)`
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
@@ -19,13 +24,11 @@ const AddFile: React.FC = () => {
}, []) }, [])
const {getRootProps, getInputProps} = useDropzone({ onDrop }); const {getRootProps, getInputProps} = useDropzone({ onDrop });
return ( return (
<div>
<DropWrapper {...getRootProps()}> <DropWrapper {...getRootProps()}>
<input {...getInputProps()} /> <input {...getInputProps()} />
<Upload size={200} /> <Icon />
<p>Drag 'n' drop some files here, or click to select files</p> <p>Drag 'n' drop some files here, or click to select files</p>
</DropWrapper> </DropWrapper>
</div>
); );
}; };

View File

@@ -1,39 +1,57 @@
import React, { useState, useCallback, useContext } from 'react'; import React, { useState, useCallback, useContext } from 'react';
import styled from 'styled-components'; import { Input, Form, Button } from 'antd';
import { PlusOutlined } from '@ant-design/icons';
import EncryptionContext from '../contexts/Encryption'; import EncryptionContext from '../contexts/Encryption';
import { Theme } from '../theme';
const Wrapper = styled.div` const layout = {
display: flex; labelCol: { span: 2 },
flex-direction: column; };
flex: 1;
`;
const Text = styled.textarea<{ const tailLayout = {
theme: Theme; wrapperCol: { offset: 2 },
}>` };
border: none;
height: 200px;
background: transparent;
color: inherit;
padding: ${({ theme }) => theme.margin.medium}px;
font: inherit;
`;
const AddText : React.FC = () => { const AddText : React.FC = () => {
const { addText } = useContext(EncryptionContext); const { addText } = useContext(EncryptionContext);
const [name, setName] = useState('');
const [text, setText] = useState(''); const [text, setText] = useState('');
const add = useCallback(() => { const add = useCallback(() => {
addText(text); addText(text, name || 'untitled');
setText(''); setText('');
setName('');
}, [text, addText]); }, [text, addText]);
return ( return (
<Wrapper> <Form {...layout}>
<Text placeholder="Enter you message..." value={text} onChange={evt => setText(evt.target.value)} /> <Form.Item
<button onClick={add}>Save</button> label="Name"
</Wrapper> >
<Input
value={name}
onChange={evt => setName(evt.target.value)}
/>
</Form.Item>
<Form.Item
label="Message"
>
<Input.TextArea
value={text}
rows={10}
onChange={evt => setText(evt.target.value)}
/>
</Form.Item>
<Form.Item {...tailLayout}>
<Button
onClick={add}
type="primary"
icon={<PlusOutlined />}
disabled={!text}
>
Add
</Button>
</Form.Item>
</Form>
); );
}; };

View File

@@ -1,12 +1,20 @@
import React from 'react'; import React from 'react';
import styled from 'styled-components'; import {
import { File } from '../contexts/Encryption'; List,
Button,
} from 'antd';
import {
DeleteOutlined,
SyncOutlined,
IssuesCloseOutlined,
LockOutlined,
} from '@ant-design/icons';
import { FileType } from '../contexts/Encryption';
import { CheckCircle, XCircle, Download, Trash, Loader } from 'react-feather'; import { CheckCircle, XCircle, Download, Trash, Loader } from 'react-feather';
import Row, { Cell } from './Row';
interface Props { interface Props {
remove: () => void; remove: () => void;
file: File; file: FileType;
} }
const downloadLink = (name: string, url: string) => { const downloadLink = (name: string, url: string) => {
@@ -18,42 +26,51 @@ const downloadLink = (name: string, url: string) => {
document.body.removeChild(downloadLink); document.body.removeChild(downloadLink);
}; };
const Button = styled.button` const icons: {[name: string]: any} = {
background: none; encrypting: <SyncOutlined spin />,
border: none; failed: <IssuesCloseOutlined />,
color: green; encrypted: <LockOutlined />,
`;
const icons: {[name: string]: typeof CheckCircle} = {
encrypting: Loader,
failed: XCircle,
encrypted: CheckCircle,
}; };
const IconText = ({ icon, text, ...props }) => (
<Button
{...props}
icon={React.createElement(icon)}
>
{text}
</Button>
);
const FileView: React.FC<Props> = ({ const FileView: React.FC<Props> = ({
file, file,
remove, remove,
}) => { }) => {
const Icon = icons[file.status]; const icon = icons[file.status];
return ( return (
<Row <List.Item
left={( actions={file.link ? [(
<Cell><Icon /></Cell> <IconText
)} icon={DeleteOutlined}
title={file.name} danger
body={`encrypted for ${file.reciever}`} text="Delete"
right={!!file.link && ( onClick={remove}
<> />
<Cell> ), (
<Button onClick={() => downloadLink(file.name, file.link)}><Download /></Button> <IconText
</Cell> icon={DeleteOutlined}
<Cell> type="primary"
<Button onClick={remove}><Trash /></Button> text="Download"
</Cell> onClick={() => downloadLink(file.name, file.link!)}
</> />
)} )]: []}
/> >
<List.Item.Meta
avatar={icon}
title={file.name}
description={`Encrypted for ${file.reciever}`}
/>
</List.Item>
); );
}; };

View File

@@ -1,12 +1,17 @@
import React, { useContext } from 'react'; import React, { useContext } from 'react';
import { List, Empty } from 'antd';
import EncryptionContext from '../contexts/Encryption'; import EncryptionContext from '../contexts/Encryption';
import File from './File'; import File from './File';
const Encrypt: React.FC = () => { const Encrypt: React.FC = () => {
const { files, deleteFile } = useContext(EncryptionContext); const { files, deleteFile } = useContext(EncryptionContext);
if (Object.keys(files).length === 0) {
return <Empty />
}
return ( return (
<div> <List>
{Object.entries(files).map(([id, file]) => ( {Object.entries(files).map(([id, file]) => (
<File <File
key={id} key={id}
@@ -14,7 +19,7 @@ const Encrypt: React.FC = () => {
remove={() => deleteFile(id)} remove={() => deleteFile(id)}
/> />
))} ))}
</div> </List>
); );
}; };

View File

@@ -0,0 +1,71 @@
import React, { useContext, useState } from 'react';
import {
Card,
Avatar,
Button,
Modal,
} from 'antd';
import { KeyOutlined, GithubOutlined } from '@ant-design/icons'
import GithubContext from '../contexts/Github';
const IconText = ({ icon, text, ...props }) => (
<Button
{...props}
icon={React.createElement(icon)}
>
{text}
</Button>
);
const Profile: React.FC = () => {
const { user, keys } = useContext(GithubContext);
const [showKeys, setShowKeys] = useState(false);
if (!user) {
return null;
}
return (
<>
<Modal
visible={showKeys}
onOk={() => setShowKeys(false)}
onCancel={() => setShowKeys(false)}
title="Keys"
>
<pre>
{keys!.join('\n\n')}
</pre>
</Modal>
<Card
style={{ width: 300, marginTop: 16, alignSelf: 'center' }}
actions={[(
<IconText
key="showkeys"
text="Show keys"
icon={KeyOutlined}
onClick={() => setShowKeys(true)}
/>
), (
<a target="_blank" href={`https://github.com/${user.login}`}>
<IconText
key="gotogithub"
text="Go to Github"
icon={GithubOutlined}
/>
</a>
)]}
>
<Card.Meta
title={user.name}
avatar={(
<Avatar src={user.avatar_url} size={80} />
)}
description={user.location}
/>
</Card>
</>
);
};
export default Profile;

View File

@@ -10,6 +10,10 @@ class Config {
const [user] = this.repo.split('/'); const [user] = this.repo.split('/');
return user; return user;
} }
get isProd() {
return process.env.NODE_ENV === 'production';
}
} }
export default new Config(); export default new Config();

View File

@@ -1,6 +1,7 @@
import React, { useState, useCallback, useContext, createContext } from 'react'; import React, { useState, useCallback, useContext, createContext } from 'react';
import * as openpgp from 'openpgp'; import * as openpgp from 'openpgp';
import { nanoid } from 'nanoid'; import { nanoid } from 'nanoid';
import { message } from 'antd';
import GithubContext from './Github'; import GithubContext from './Github';
export interface FileType { export interface FileType {
@@ -14,7 +15,7 @@ export interface FileType {
interface EncryptionContextType { interface EncryptionContextType {
files: {[id: string]: FileType}; files: {[id: string]: FileType};
addFile: (file: File) => Promise<void>; addFile: (file: File) => Promise<void>;
addText: (text: string) => Promise<void>; addText: (text: string, name: string) => Promise<void>;
deleteFile: (id: string) => void; deleteFile: (id: string) => void;
} }
@@ -32,7 +33,7 @@ const encrypt = async (keys: string[], content: string) => {
const encrypted = await openpgp.encrypt({ const encrypted = await openpgp.encrypt({
message, message,
armor: true, armor: true,
publicKeys: armoredKeys.reduce((output, key: any) => [...output, ...key.keys], []), publicKeys: armoredKeys.reduce<any>((output, key: any) => [...output, ...key.keys], []),
}); });
const { data } = encrypted; const { data } = encrypted;
const blob = new Blob([data], { const blob = new Blob([data], {
@@ -55,7 +56,9 @@ const EncryptionProvider: React.FC = ({
}); });
}, [files]); }, [files]);
const add = (name: string = nanoid()) => { const add = (name: string) => {
const id = nanoid();
message.info(`Beginning to encrypt ${name}`);
const file: FileType = { const file: FileType = {
name: `${name}.asc`, name: `${name}.asc`,
reciever: username, reciever: username,
@@ -63,19 +66,20 @@ const EncryptionProvider: React.FC = ({
}; };
setFiles(files => ({ setFiles(files => ({
...files, ...files,
[name]: file, [id]: file,
})); }));
const setError = (err: any) => { const setError = (err: any) => {
console.error(err); console.error(err);
setFiles(files => ({ setFiles(files => ({
...files, ...files,
[name]: { [id]: {
...files[name], ...files[id],
status: 'failed', status: 'failed',
error: err, error: err,
}, },
})); }));
message.error(`Failed to encrypt ${name}`);
}; };
const setContent = (text: string, keys: string[]) => { const setContent = (text: string, keys: string[]) => {
@@ -84,12 +88,13 @@ const EncryptionProvider: React.FC = ({
const encrypted = await encrypt(keys, text); const encrypted = await encrypt(keys, text);
setFiles(files => ({ setFiles(files => ({
...files, ...files,
[name]: { [id]: {
...files[name], ...files[id],
link: encrypted, link: encrypted,
status: 'encrypted' status: 'encrypted'
}, },
})); }));
message.success(`Done encrypting ${name}`);
} catch (err) { } catch (err) {
setError(err); setError(err);
} }
@@ -104,7 +109,6 @@ const EncryptionProvider: React.FC = ({
} }
const addFile = useCallback(async (file: File) => { const addFile = useCallback(async (file: File) => {
console.log('a', keys, file);
if (!keys) return; if (!keys) return;
const addedFile = add(file.name); const addedFile = add(file.name);
const reader = new FileReader() const reader = new FileReader()
@@ -112,15 +116,14 @@ const EncryptionProvider: React.FC = ({
reader.onabort = addedFile.setError, reader.onabort = addedFile.setError,
reader.onerror = addedFile.setError, reader.onerror = addedFile.setError,
reader.onload = () => { reader.onload = () => {
console.log('foo', file);
addedFile.setContent(reader.result as string, keys); addedFile.setContent(reader.result as string, keys);
} }
reader.readAsText(file) reader.readAsText(file)
}, [keys, username]); }, [keys, username]);
const addText = useCallback(async (text: string) => { const addText = useCallback(async (text: string, name: string) => {
if (!keys) return; if (!keys) return;
const file = add(); const file = add(`${name}.txt`);
file.setContent(text, keys); file.setContent(text, keys);
}, [keys, username]); }, [keys, username]);

View File

@@ -1,4 +1,5 @@
import React, { useState, useEffect, createContext } from 'react'; import React, { useState, useEffect, createContext } from 'react';
import { Layout, Spin } from 'antd';
interface GithubContextType { interface GithubContextType {
username: string; username: string;
@@ -13,6 +14,20 @@ interface Props {
children: React.ReactNode; children: React.ReactNode;
} }
const Loader = () => (
<Layout
style={{
position: 'fixed',
alignItems: 'center',
justifyContent: 'center',
height: '100%',
width: '100%',
}}
>
<Spin size="large" />
</Layout>
);
const GithubContext = createContext<GithubContextType>({ const GithubContext = createContext<GithubContextType>({
username: 'unknown', username: 'unknown',
state: 'failed', state: 'failed',
@@ -46,6 +61,10 @@ const GithubProvider: React.FC<Props> = ({
run(); run();
}, [username]); }, [username]);
if (state === 'loading') {
return <Loader />;
}
return ( return (
<GithubContext.Provider <GithubContext.Provider
value={{ value={{

View File

@@ -1,4 +1,5 @@
import React from 'react'; import React from 'react';
import 'antd/dist/antd.css';
import { render } from 'react-dom'; import { render } from 'react-dom';
import App from './App'; import App from './App';

37
src/screens/Debug.tsx Normal file
View File

@@ -0,0 +1,37 @@
import React, { useMemo } from 'react';
import {
Table,
} from 'antd';
import config from '../config';
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,
'Is Production': config.isProd,
};
return Object.entries(vals).map(([name, value]) => ({
key: name,
name,
value: value.toString(),
}));
}, []);
return (
<Table dataSource={data} columns={columns} />
);
};
export default Debug;

View File

@@ -1,16 +1,33 @@
import React, { useContext } from 'react'; import React, { useContext } from 'react';
import { Collapse, Badge } from 'antd';
import Profile from '../components/Profile';
import Add from '../components/Add'; import Add from '../components/Add';
import FileList from '../components/FileList'; import FileList from '../components/FileList';
import GithubContext from '../contexts/Github'; import EncryptionContext from '../contexts/Encryption';
const Encrypt: React.FC = () => { const Encrypt: React.FC = () => {
const { username } = useContext(GithubContext); const { files } = useContext(EncryptionContext);
return ( return (
<div> <>
<div>To: {username}</div> <Collapse ghost defaultActiveKey={[2, 3]}>
<Add /> <Collapse.Panel key={2} header="Encrypt">
<FileList /> <Add />
</div> </Collapse.Panel>
<Collapse.Panel
key={3}
header={(
<Badge count={Object.keys(files).length} offset={[20, 7]}>
Files
</Badge>
)}
>
<FileList />
</Collapse.Panel>
<Collapse.Panel key={1} header="Profile">
<Profile />
</Collapse.Panel>
</Collapse>
</>
); );
}; };

View File

@@ -1,4 +1,4 @@
import { Configuration } from 'webpack'; import webpack, { Configuration } from 'webpack';
import HtmlWebpackPlugin from 'html-webpack-plugin'; import HtmlWebpackPlugin from 'html-webpack-plugin';
import path from 'path'; import path from 'path';
@@ -6,6 +6,7 @@ const config: Configuration = {
mode: 'development', mode: 'development',
entry: { entry: {
app: [ app: [
'react-hot-loader/patch',
path.join(__dirname, 'src', 'index.tsx'), path.join(__dirname, 'src', 'index.tsx'),
], ],
}, },
@@ -14,16 +15,27 @@ const config: Configuration = {
}, },
resolve: { resolve: {
extensions: ['.tsx', '.ts', '.js'], extensions: ['.tsx', '.ts', '.js'],
alias: {
'react-dom': '@hot-loader/react-dom',
},
}, },
plugins: [ plugins: [
new HtmlWebpackPlugin(), new HtmlWebpackPlugin(),
], ],
module: { module: {
rules: [{ rules: [{
test: /\.tsx?/, test: /\.tsx?$/,
use: ['babel-loader'], use: ['babel-loader'],
}, {
test: /\.css$/,
use: ['style-loader', 'css-loader'],
}], }],
}, },
}; };
(config as any).devServer = {
hot: true,
contentBase: './dist',
};
export default config; export default config;

823
yarn.lock

File diff suppressed because it is too large Load Diff