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

7
.eslintrc Normal file
View File

@@ -0,0 +1,7 @@
{
"extends": "@react-native-community",
"rules": {
"quotes": [2, "single"],
"prettier/prettier": ["error", { "singleQuote": true }]
}
}

View File

@@ -9,12 +9,13 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout 🛎️ - name: Checkout 🛎️
uses: actions/checkout@v2.3.1 # If you're using actions/checkout@v2 you must set persist-credentials to false in most cases for the deployment to work correctly. uses: actions/checkout@v2.3.1
with: with:
persist-credentials: false persist-credentials: false
- name: Install and Build 🔧 # This example project is built using npm and outputs the result to the 'build' folder. Replace with the commands required to build your project, or remove this step entirely if your site is pre-built. - name: Install and Build 🔧
run: | run: |
corepack enable
yarn install yarn install
NODE_ENV=production yarn build NODE_ENV=production yarn build
@@ -22,5 +23,5 @@ jobs:
uses: JamesIves/github-pages-deploy-action@4.0.0 uses: JamesIves/github-pages-deploy-action@4.0.0
with: with:
token: ${{ secrets.GITHUB_TOKEN }} token: ${{ secrets.GITHUB_TOKEN }}
branch: gh-pages # The branch the action should deploy to. branch: gh-pages
folder: dist # The folder the action should deploy. folder: dist

View File

@@ -7,14 +7,15 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout 🛎️ - name: Checkout 🛎️
uses: actions/checkout@v2.3.1 # If you're using actions/checkout@v2 you must set persist-credentials to false in most cases for the deployment to work correctly. uses: actions/checkout@v2.3.1
with: with:
persist-credentials: false persist-credentials: false
- name: Install 🔧 - name: Install 🔧
run: | run: |
corepack enable
yarn install yarn install
- name: Test - name: Test
run: | run: |
yarn test NODE_ENV=production yarn test

1
.gitignore vendored
View File

@@ -3,4 +3,5 @@
/.cache /.cache
/.env /.env
/.tmp /.tmp
/.yarn
*.log *.log

3
.prettierrc Normal file
View File

@@ -0,0 +1,3 @@
{
"singleQuote": true
}

1
.yarnrc.yml Normal file
View File

@@ -0,0 +1 @@
nodeLinker: node-modules

View File

@@ -1,3 +1,5 @@
const isDevelopment = process.env.NODE_ENV !== 'production';
const config = (api) => { const config = (api) => {
api.cache(false); api.cache(false);
return { return {
@@ -12,8 +14,8 @@ const config = (api) => {
'GITHUB_REPOSITORY', 'GITHUB_REPOSITORY',
], ],
}], }],
[require.resolve('react-hot-loader/babel')], isDevelopment && require.resolve('react-refresh/babel'),
], ].filter(Boolean),
}; };
}; };

View File

@@ -2,6 +2,5 @@ const path = require('path');
module.exports = { module.exports = {
preset: 'ts-jest', preset: 'ts-jest',
//testEnvironment: 'node', testEnvironment: path.join(__dirname, 'tests', 'env.js'),
testEnvironment: path.join(__dirname, 'tests', 'env-ts.js'),
}; };

View File

@@ -1,47 +1,49 @@
{ {
"name": "dropbox", "name": "@morten-olsen/parcel",
"version": "1.0.0", "version": "1.0.0",
"main": "index.js",
"license": "MIT", "license": "MIT",
"packageManager": "yarn@3.1.0",
"scripts": { "scripts": {
"dev": "webpack-dev-server",
"build": "webpack", "build": "webpack",
"test": "jest" "test": "jest"
}, },
"devDependencies": { "devDependencies": {
"@ant-design/icons": "^4.7.0",
"@babel/core": "^7.17.9", "@babel/core": "^7.17.9",
"@babel/preset-env": "^7.16.11", "@babel/preset-env": "^7.16.11",
"@babel/preset-react": "^7.16.7", "@babel/preset-react": "^7.16.7",
"@babel/preset-typescript": "^7.16.7", "@babel/preset-typescript": "^7.16.7",
"@hot-loader/react-dom": "^17.0.2", "@commitlint/cli": "^16.2.3",
"@commitlint/config-conventional": "^16.2.1",
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.5",
"@react-native-community/eslint-config": "^3.0.1",
"@types/express": "^4.17.13", "@types/express": "^4.17.13",
"@types/fs-extra": "^9.0.13", "@types/fs-extra": "^9.0.13",
"@types/get-port": "^4.2.0",
"@types/html-webpack-plugin": "^3.2.6", "@types/html-webpack-plugin": "^3.2.6",
"@types/jest": "^27.4.1", "@types/jest": "^27.4.1",
"@types/jszip": "^3.4.1", "@types/react": "^18.0.4",
"@types/react": "^18.0.3",
"@types/react-dom": "^18.0.0", "@types/react-dom": "^18.0.0",
"@types/react-router": "^5.1.18", "@types/react-router": "^5.1.18",
"@types/react-router-dom": "^5.3.3", "@types/react-router-dom": "^5.3.3",
"@types/styled-components": "^5.1.25", "@types/styled-components": "^5.1.25",
"@types/webpack-subresource-integrity": "^5.0.0",
"@types/workbox-webpack-plugin": "^5.1.8", "@types/workbox-webpack-plugin": "^5.1.8",
"antd": "^4.19.5", "@typescript-eslint/eslint-plugin": "^5.19.0",
"axios": "^0.26.1", "axios": "^0.26.1",
"babel-loader": "^8.2.4", "babel-loader": "^8.2.4",
"babel-plugin-transform-inline-environment-variables": "^0.4.3", "babel-plugin-transform-inline-environment-variables": "^0.4.3",
"css-loader": "^6.7.1", "css-loader": "^6.7.1",
"dotenv": "^16.0.0", "dotenv": "^16.0.0",
"eslint": "^8.13.0",
"express": "^4.17.3", "express": "^4.17.3",
"fs-extra": "^10.0.1", "fs-extra": "^10.0.1",
"get-port": "^5", "get-port": "^6.1.2",
"html-webpack-plugin": "^5.5.0", "html-webpack-plugin": "^5.5.0",
"husky": "^7.0.4",
"jest": "^27.5.1", "jest": "^27.5.1",
"offline-plugin": "^5.0.7", "offline-plugin": "^5.0.7",
"parcel-bundler": "^1.12.5", "prettier": "^2.6.2",
"puppeteer": "^13.5.2", "puppeteer": "^13.5.2",
"react-hot-loader": "^4.13.0", "react-refresh": "^0.12.0",
"style-loader": "^3.3.1", "style-loader": "^3.3.1",
"ts-jest": "^27.1.4", "ts-jest": "^27.1.4",
"ts-node": "^10.7.0", "ts-node": "^10.7.0",
@@ -54,6 +56,8 @@
"workbox-webpack-plugin": "^6.5.3" "workbox-webpack-plugin": "^6.5.3"
}, },
"dependencies": { "dependencies": {
"@ant-design/icons": "^4.7.0",
"antd": "^4.19.5",
"jszip": "^3.9.1", "jszip": "^3.9.1",
"nanoid": "^3.3.2", "nanoid": "^3.3.2",
"openpgp": "^5.2.1", "openpgp": "^5.2.1",

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -38,7 +38,7 @@ export const createFile = (setFiles: SetFilesType, name: string) => {
})); }));
}) })
.catch(setFailed); .catch(setFailed);
} };
const setFailed = (err: any) => { const setFailed = (err: any) => {
console.error(err); console.error(err);

View File

@@ -8,7 +8,8 @@ type Statuses = 'packing' | 'ready';
const useDownloadAll = () => { const useDownloadAll = () => {
const [status, setStatus] = useState<Statuses>('ready'); const [status, setStatus] = useState<Statuses>('ready');
const { files } = useContext(EncryptionContext); 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(() => { const downloadAll = useCallback(() => {
setStatus('packing'); setStatus('packing');

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -8,15 +8,16 @@ const Welcome: React.FC = () => {
<Space direction="vertical"> <Space direction="vertical">
<EyeInvisibleTwoTone style={{ fontSize: 200 }} /> <EyeInvisibleTwoTone style={{ fontSize: 200 }} />
<Typography.Title level={1}>Protect before sending</Typography.Title> <Typography.Title level={1}>Protect before sending</Typography.Title>
<p>The internet can seem like a scary place...</p>
<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>
<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 a tool that will help you have that extra layer of security
This is why it is important to have an additional layer of security when sending sensitive information. when sharing files with me.
</p>
<p>
This is a tool that will help you have that extra layer of security when sharing files with me.
</p> </p>
</Space> </Space>
</Layout> </Layout>

View File

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

View File

@@ -1,51 +0,0 @@
const NodeEnvironment = require('jest-environment-node');
const { Server, createServer } = require('http');
const getPort = require('get-port');
const webpack = require('webpack');
const path = require('path');
const express = require('express');
const { default: createConfig } = require('../webpack.config');
const build = () => new Promise(async (resolve, reject) => {
const config = await createConfig({
test: true,
});
const port = await getPort();
const bundler = webpack(config);
bundler.run((err, stats) => {
if (err || !stats) {
return reject(err);
} else if (stats.hasErrors()) {
return reject(new Error('Webpack errors'));
}
const app = express();
app.use(express.static(path.join(__dirname, '..', 'dist')));
const server = createServer(app);
const listener = server.listen(port, '127.0.0.1', () => {
resolve(listener);
});
});
});
class CustomEnvironment extends NodeEnvironment {
constructor(config) {
super(config);
}
async setup() {
await super.setup();
this._server = await build();
const address = this._server.address();
this.global.testUrl = `http://${address.address}:${address.port}`
}
async teardown() {
await super.teardown();
if (!this._server) {
return;
}
this._server.close();
}
}
module.exports = CustomEnvironment;

View File

@@ -1,4 +1,51 @@
require('ts-node/register'); const NodeEnvironment = require('jest-environment-node');
const Env = require('./env-ts'); const { Server, createServer } = require('http');
const webpack = require('webpack');
const path = require('path');
const express = require('express');
const { default: createConfig } = require('../webpack.config');
module.exports = Env; const build = () => new Promise(async (resolve, reject) => {
const { default: getPort } = await import('get-port');
const config = await createConfig({
test: true,
});
const port = await getPort();
const bundler = webpack(config);
bundler.run((err, stats) => {
if (err || !stats) {
return reject(err);
} else if (stats.hasErrors()) {
return reject(new Error('Webpack errors'));
}
const app = express();
app.use(express.static(path.join(__dirname, '..', 'dist')));
const server = createServer(app);
const listener = server.listen(port, '127.0.0.1', () => {
resolve(listener);
});
});
});
class CustomEnvironment extends NodeEnvironment {
constructor(config) {
super(config);
}
async setup() {
await super.setup();
this._server = await build();
const address = this._server.address();
this.global.testUrl = `http://${address.address}:${address.port}`
}
async teardown() {
await super.teardown();
if (!this._server) {
return;
}
this._server.close();
}
}
module.exports = CustomEnvironment;

View File

@@ -4,6 +4,7 @@ import axios from 'axios';
import fs from 'fs'; import fs from 'fs';
import HtmlWebpackPlugin from 'html-webpack-plugin'; import HtmlWebpackPlugin from 'html-webpack-plugin';
import WorkboxWebpackPlugin from 'workbox-webpack-plugin'; import WorkboxWebpackPlugin from 'workbox-webpack-plugin';
import ReactRefreshWebpackPlugin from '@pmmmwh/react-refresh-webpack-plugin';
import path from 'path'; import path from 'path';
interface Options { interface Options {
@@ -19,7 +20,9 @@ const [username] = repo.split('/');
const __DEV__ = process.env.NODE_ENV !== 'production'; const __DEV__ = process.env.NODE_ENV !== 'production';
const getData = async () => { const getData = async () => {
const { data: keyList } = await axios.get(`https://api.github.com/users/${username}/gpg_keys`); const { data: keyList } = await axios.get(
`https://api.github.com/users/${username}/gpg_keys`
);
if (keyList.length === 0) { if (keyList.length === 0) {
throw new Error(`The user ${username} does not have any GPG keys`); throw new Error(`The user ${username} does not have any GPG keys`);
} }
@@ -32,26 +35,26 @@ const getData = async () => {
}; };
const getTestData: typeof getData = async () => { const getTestData: typeof getData = async () => {
const pubKey = fs.readFileSync(path.join(__dirname, 'test-assets', 'key.pub'), 'utf-8'); const pubKey = fs.readFileSync(
path.join(__dirname, 'test-assets', 'key.pub'),
'utf-8'
);
return { return {
username: 'foobar', username: 'foobar',
keys: [ keys: [pubKey],
pubKey,
],
}; };
}; };
const createConfig = async (options: Options = { const createConfig = async (
options: Options = {
test: false, test: false,
}):Promise<Configuration> => { }
): Promise<Configuration> => {
const data = await (options.test ? getTestData() : getData()); const data = await (options.test ? getTestData() : getData());
const config: Configuration = { const config: Configuration = {
mode: __DEV__ ? 'development' : 'production', mode: __DEV__ ? 'development' : 'production',
entry: { entry: {
app: [ app: [path.join(__dirname, 'src', 'index.tsx')],
...(__DEV__ ? ['react-hot-loader/patch'] : []),
path.join(__dirname, 'src', 'index.tsx'),
],
}, },
output: { output: {
path: path.join(__dirname, 'dist'), path: path.join(__dirname, 'dist'),
@@ -59,9 +62,6 @@ const createConfig = async (options: Options = {
}, },
resolve: { resolve: {
extensions: ['.tsx', '.ts', '.js'], extensions: ['.tsx', '.ts', '.js'],
alias: {
'react-dom': '@hot-loader/react-dom',
},
}, },
plugins: [ plugins: [
new webpack.DefinePlugin({ new webpack.DefinePlugin({
@@ -73,20 +73,27 @@ const createConfig = async (options: Options = {
minify: true, minify: true,
template: path.join(__dirname, 'html.html'), template: path.join(__dirname, 'html.html'),
}), }),
...(__DEV__
? [new ReactRefreshWebpackPlugin()]
: [
new WorkboxWebpackPlugin.GenerateSW({ new WorkboxWebpackPlugin.GenerateSW({
swDest: 'sw.js', swDest: 'sw.js',
clientsClaim: true, clientsClaim: true,
skipWaiting: true, skipWaiting: true,
}), }),
]),
], ],
module: { module: {
rules: [{ rules: [
{
test: /\.tsx?$/, test: /\.tsx?$/,
use: ['babel-loader'], use: ['babel-loader'],
}, { },
{
test: /\.css$/, test: /\.css$/,
use: ['style-loader', 'css-loader'], use: ['style-loader', 'css-loader'],
}], },
],
}, },
}; };

25125
yarn.lock

File diff suppressed because it is too large Load Diff