mirror of
https://github.com/morten-olsen/parcel.git
synced 2026-02-08 01:36:24 +01:00
chore: quality of life improvements (#449)
This commit is contained in:
7
.eslintrc
Normal file
7
.eslintrc
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"extends": "@react-native-community",
|
||||
"rules": {
|
||||
"quotes": [2, "single"],
|
||||
"prettier/prettier": ["error", { "singleQuote": true }]
|
||||
}
|
||||
}
|
||||
9
.github/workflows/publish.yml
vendored
9
.github/workflows/publish.yml
vendored
@@ -9,12 +9,13 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- 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:
|
||||
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: |
|
||||
corepack enable
|
||||
yarn install
|
||||
NODE_ENV=production yarn build
|
||||
|
||||
@@ -22,5 +23,5 @@ jobs:
|
||||
uses: JamesIves/github-pages-deploy-action@4.0.0
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
branch: gh-pages # The branch the action should deploy to.
|
||||
folder: dist # The folder the action should deploy.
|
||||
branch: gh-pages
|
||||
folder: dist
|
||||
|
||||
5
.github/workflows/qa.yml
vendored
5
.github/workflows/qa.yml
vendored
@@ -7,14 +7,15 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- 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:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Install 🔧
|
||||
run: |
|
||||
corepack enable
|
||||
yarn install
|
||||
|
||||
- name: Test
|
||||
run: |
|
||||
yarn test
|
||||
NODE_ENV=production yarn test
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -3,4 +3,5 @@
|
||||
/.cache
|
||||
/.env
|
||||
/.tmp
|
||||
/.yarn
|
||||
*.log
|
||||
|
||||
3
.prettierrc
Normal file
3
.prettierrc
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"singleQuote": true
|
||||
}
|
||||
1
.yarnrc.yml
Normal file
1
.yarnrc.yml
Normal file
@@ -0,0 +1 @@
|
||||
nodeLinker: node-modules
|
||||
@@ -1,3 +1,5 @@
|
||||
const isDevelopment = process.env.NODE_ENV !== 'production';
|
||||
|
||||
const config = (api) => {
|
||||
api.cache(false);
|
||||
return {
|
||||
@@ -12,8 +14,8 @@ const config = (api) => {
|
||||
'GITHUB_REPOSITORY',
|
||||
],
|
||||
}],
|
||||
[require.resolve('react-hot-loader/babel')],
|
||||
],
|
||||
isDevelopment && require.resolve('react-refresh/babel'),
|
||||
].filter(Boolean),
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -2,6 +2,5 @@ const path = require('path');
|
||||
|
||||
module.exports = {
|
||||
preset: 'ts-jest',
|
||||
//testEnvironment: 'node',
|
||||
testEnvironment: path.join(__dirname, 'tests', 'env-ts.js'),
|
||||
testEnvironment: path.join(__dirname, 'tests', 'env.js'),
|
||||
};
|
||||
|
||||
28
package.json
28
package.json
@@ -1,47 +1,49 @@
|
||||
{
|
||||
"name": "dropbox",
|
||||
"name": "@morten-olsen/parcel",
|
||||
"version": "1.0.0",
|
||||
"main": "index.js",
|
||||
"license": "MIT",
|
||||
"packageManager": "yarn@3.1.0",
|
||||
"scripts": {
|
||||
"dev": "webpack-dev-server",
|
||||
"build": "webpack",
|
||||
"test": "jest"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@ant-design/icons": "^4.7.0",
|
||||
"@babel/core": "^7.17.9",
|
||||
"@babel/preset-env": "^7.16.11",
|
||||
"@babel/preset-react": "^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/fs-extra": "^9.0.13",
|
||||
"@types/get-port": "^4.2.0",
|
||||
"@types/html-webpack-plugin": "^3.2.6",
|
||||
"@types/jest": "^27.4.1",
|
||||
"@types/jszip": "^3.4.1",
|
||||
"@types/react": "^18.0.3",
|
||||
"@types/react": "^18.0.4",
|
||||
"@types/react-dom": "^18.0.0",
|
||||
"@types/react-router": "^5.1.18",
|
||||
"@types/react-router-dom": "^5.3.3",
|
||||
"@types/styled-components": "^5.1.25",
|
||||
"@types/webpack-subresource-integrity": "^5.0.0",
|
||||
"@types/workbox-webpack-plugin": "^5.1.8",
|
||||
"antd": "^4.19.5",
|
||||
"@typescript-eslint/eslint-plugin": "^5.19.0",
|
||||
"axios": "^0.26.1",
|
||||
"babel-loader": "^8.2.4",
|
||||
"babel-plugin-transform-inline-environment-variables": "^0.4.3",
|
||||
"css-loader": "^6.7.1",
|
||||
"dotenv": "^16.0.0",
|
||||
"eslint": "^8.13.0",
|
||||
"express": "^4.17.3",
|
||||
"fs-extra": "^10.0.1",
|
||||
"get-port": "^5",
|
||||
"get-port": "^6.1.2",
|
||||
"html-webpack-plugin": "^5.5.0",
|
||||
"husky": "^7.0.4",
|
||||
"jest": "^27.5.1",
|
||||
"offline-plugin": "^5.0.7",
|
||||
"parcel-bundler": "^1.12.5",
|
||||
"prettier": "^2.6.2",
|
||||
"puppeteer": "^13.5.2",
|
||||
"react-hot-loader": "^4.13.0",
|
||||
"react-refresh": "^0.12.0",
|
||||
"style-loader": "^3.3.1",
|
||||
"ts-jest": "^27.1.4",
|
||||
"ts-node": "^10.7.0",
|
||||
@@ -54,6 +56,8 @@
|
||||
"workbox-webpack-plugin": "^6.5.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ant-design/icons": "^4.7.0",
|
||||
"antd": "^4.19.5",
|
||||
"jszip": "^3.9.1",
|
||||
"nanoid": "^3.3.2",
|
||||
"openpgp": "^5.2.1",
|
||||
|
||||
@@ -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,7 +11,7 @@ const App: React.FC = () => {
|
||||
<GithubProvider>
|
||||
<EncryptionProvider>
|
||||
<DecryptionProvider>
|
||||
<Layout style={{minHeight:"100vh"}}>
|
||||
<Layout style={{ minHeight: '100vh' }}>
|
||||
<Router>
|
||||
<AppRouter />
|
||||
</Router>
|
||||
@@ -23,4 +22,4 @@ const App: React.FC = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export default hot(App);
|
||||
export default App;
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
Routes,
|
||||
Route,
|
||||
useNavigate,
|
||||
} from 'react-router-dom';
|
||||
import { Routes, Route, useNavigate } from 'react-router-dom';
|
||||
import { HomeFilled } from '@ant-design/icons';
|
||||
import { Layout, Button, Space } from 'antd';
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
List,
|
||||
Button,
|
||||
Popconfirm,
|
||||
} from 'antd';
|
||||
import { List, Button, Popconfirm } from 'antd';
|
||||
import {
|
||||
DeleteOutlined,
|
||||
SyncOutlined,
|
||||
@@ -32,17 +28,10 @@ const icons: {[name: string]: any} = {
|
||||
};
|
||||
|
||||
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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -10,25 +10,18 @@ interface Props {
|
||||
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%' }}>
|
||||
<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 && (
|
||||
|
||||
@@ -20,9 +20,12 @@ const DropWrapper = styled(Layout)`
|
||||
|
||||
const AddFile: React.FC = () => {
|
||||
const { addFile } = useContext(DecryptionContext);
|
||||
const onDrop = useCallback(acceptedFiles => {
|
||||
const onDrop = useCallback(
|
||||
(acceptedFiles) => {
|
||||
acceptedFiles.forEach(addFile);
|
||||
}, [addFile])
|
||||
},
|
||||
[addFile]
|
||||
);
|
||||
const { getRootProps, getInputProps } = useDropzone({ onDrop });
|
||||
return (
|
||||
<DropWrapper {...getRootProps()}>
|
||||
|
||||
@@ -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 />}
|
||||
|
||||
@@ -20,9 +20,12 @@ const DropWrapper = styled(Layout)`
|
||||
|
||||
const AddFile: React.FC = () => {
|
||||
const { addFile } = useContext(EncryptionContext);
|
||||
const onDrop = useCallback(acceptedFiles => {
|
||||
const onDrop = useCallback(
|
||||
(acceptedFiles) => {
|
||||
acceptedFiles.forEach(addFile);
|
||||
}, [addFile])
|
||||
},
|
||||
[addFile]
|
||||
);
|
||||
const { getRootProps, getInputProps } = useDropzone({ onDrop });
|
||||
return (
|
||||
<DropWrapper {...getRootProps()}>
|
||||
|
||||
@@ -21,7 +21,7 @@ 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>
|
||||
@@ -30,7 +30,7 @@ const AddText : React.FC = () => {
|
||||
placeholder="Your message here..."
|
||||
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;
|
||||
|
||||
|
||||
@@ -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';
|
||||
@@ -14,6 +28,10 @@ interface DecryptionContextType {
|
||||
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) => {
|
||||
const deleteFile = useCallback(
|
||||
(id: string) => {
|
||||
delete files[id];
|
||||
setFiles({
|
||||
...files,
|
||||
});
|
||||
}, [files]);
|
||||
},
|
||||
[files]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const run = async () => {
|
||||
@@ -90,22 +122,27 @@ const DecryptionProvider: React.FC = ({
|
||||
setPrivateKey(key.privateKey);
|
||||
setPublicKey(key.publicKey);
|
||||
localStorage.setItem('key', key.privateKey);
|
||||
}
|
||||
};
|
||||
|
||||
const addFile = useCallback(async (file: File) => {
|
||||
if (!keys || !privateKey) return;
|
||||
const addFile = useCallback(
|
||||
async (file: File) => {
|
||||
if (!keys || !privateKey) {
|
||||
return;
|
||||
}
|
||||
const addedFile = createFile(setFiles, removeExtension(file.name));
|
||||
const reader = new FileReader()
|
||||
const reader = new FileReader();
|
||||
|
||||
reader.onabort = addedFile.setFailed,
|
||||
reader.onerror = addedFile.setFailed,
|
||||
reader.onload = () => {
|
||||
(reader.onabort = addedFile.setFailed),
|
||||
(reader.onerror = addedFile.setFailed),
|
||||
(reader.onload = () => {
|
||||
addedFile.setContent(
|
||||
decrypt(privateKey, keys, reader.result as string),
|
||||
decrypt(privateKey, keys, reader.result as string)
|
||||
);
|
||||
}
|
||||
});
|
||||
reader.readAsText(file);
|
||||
}, [keys, privateKey]);
|
||||
},
|
||||
[keys, privateKey]
|
||||
);
|
||||
|
||||
return (
|
||||
<DecryptionContext.Provider
|
||||
@@ -124,8 +161,6 @@ const DecryptionProvider: React.FC = ({
|
||||
);
|
||||
};
|
||||
|
||||
export {
|
||||
DecryptionProvider,
|
||||
};
|
||||
export { DecryptionProvider };
|
||||
|
||||
export default DecryptionContext;;
|
||||
export default DecryptionContext;
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
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';
|
||||
@@ -11,21 +17,34 @@ interface EncryptionContextType {
|
||||
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) => {
|
||||
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()
|
||||
|
||||
reader.onabort = addedFile.setFailed,
|
||||
reader.onerror = addedFile.setFailed,
|
||||
reader.onload = () => {
|
||||
addedFile.setContent(
|
||||
encrypt(keys, reader.result as string),
|
||||
},
|
||||
[files]
|
||||
);
|
||||
|
||||
const addFile = useCallback(
|
||||
async (file: File) => {
|
||||
if (!keys) {
|
||||
return;
|
||||
}
|
||||
reader.readAsText(file)
|
||||
}, [keys, username]);
|
||||
const addedFile = createFile(setFiles, `${file.name}.acs`);
|
||||
const reader = new FileReader();
|
||||
|
||||
const addText = useCallback(async (text: string, name: string) => {
|
||||
if (!keys) return;
|
||||
const file = createFile(setFiles, `${name}.txt.asc`);
|
||||
file.setContent(
|
||||
encrypt(keys, text),
|
||||
(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]
|
||||
);
|
||||
}, [keys, username]);
|
||||
|
||||
return (
|
||||
<EncryptionContext.Provider
|
||||
@@ -84,9 +112,6 @@ const EncryptionProvider: React.FC = ({
|
||||
);
|
||||
};
|
||||
|
||||
export {
|
||||
EncryptionProvider,
|
||||
};
|
||||
export { EncryptionProvider };
|
||||
|
||||
export default EncryptionContext;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -38,7 +38,7 @@ export const createFile = (setFiles: SetFilesType, name: string) => {
|
||||
}));
|
||||
})
|
||||
.catch(setFailed);
|
||||
}
|
||||
};
|
||||
|
||||
const setFailed = (err: any) => {
|
||||
console.error(err);
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -1,24 +1,25 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import {
|
||||
Table,
|
||||
} from 'antd';
|
||||
import { Table } from 'antd';
|
||||
import config from '../config';
|
||||
|
||||
const columns = [{
|
||||
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;
|
||||
|
||||
@@ -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} />
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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('');
|
||||
@@ -37,11 +33,11 @@ const SetupKey: React.FC = () => {
|
||||
<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,7 +55,7 @@ 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' }}>
|
||||
@@ -81,11 +77,10 @@ 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.
|
||||
</p>
|
||||
<p>
|
||||
Remember that you need to go to this website on this device to decrypt the files after receiving them
|
||||
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">
|
||||
<Button
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -10,7 +10,7 @@ interface FileProcessing extends FileBase {
|
||||
}
|
||||
|
||||
interface FileFailed extends FileBase {
|
||||
status: 'failed',
|
||||
status: 'failed';
|
||||
error: any;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
53
tests/env.js
53
tests/env.js
@@ -1,4 +1,51 @@
|
||||
require('ts-node/register');
|
||||
const Env = require('./env-ts');
|
||||
const NodeEnvironment = require('jest-environment-node');
|
||||
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;
|
||||
|
||||
@@ -4,6 +4,7 @@ import axios from 'axios';
|
||||
import fs from 'fs';
|
||||
import HtmlWebpackPlugin from 'html-webpack-plugin';
|
||||
import WorkboxWebpackPlugin from 'workbox-webpack-plugin';
|
||||
import ReactRefreshWebpackPlugin from '@pmmmwh/react-refresh-webpack-plugin';
|
||||
import path from 'path';
|
||||
|
||||
interface Options {
|
||||
@@ -19,7 +20,9 @@ const [username] = repo.split('/');
|
||||
const __DEV__ = process.env.NODE_ENV !== 'production';
|
||||
|
||||
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) {
|
||||
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 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 {
|
||||
username: 'foobar',
|
||||
keys: [
|
||||
pubKey,
|
||||
],
|
||||
keys: [pubKey],
|
||||
};
|
||||
};
|
||||
|
||||
const createConfig = async (options: Options = {
|
||||
const createConfig = async (
|
||||
options: Options = {
|
||||
test: false,
|
||||
}):Promise<Configuration> => {
|
||||
}
|
||||
): Promise<Configuration> => {
|
||||
const data = await (options.test ? getTestData() : getData());
|
||||
const config: Configuration = {
|
||||
mode: __DEV__ ? 'development' : 'production',
|
||||
entry: {
|
||||
app: [
|
||||
...(__DEV__ ? ['react-hot-loader/patch'] : []),
|
||||
path.join(__dirname, 'src', 'index.tsx'),
|
||||
],
|
||||
app: [path.join(__dirname, 'src', 'index.tsx')],
|
||||
},
|
||||
output: {
|
||||
path: path.join(__dirname, 'dist'),
|
||||
@@ -59,9 +62,6 @@ const createConfig = async (options: Options = {
|
||||
},
|
||||
resolve: {
|
||||
extensions: ['.tsx', '.ts', '.js'],
|
||||
alias: {
|
||||
'react-dom': '@hot-loader/react-dom',
|
||||
},
|
||||
},
|
||||
plugins: [
|
||||
new webpack.DefinePlugin({
|
||||
@@ -73,20 +73,27 @@ const createConfig = async (options: Options = {
|
||||
minify: true,
|
||||
template: path.join(__dirname, 'html.html'),
|
||||
}),
|
||||
...(__DEV__
|
||||
? [new ReactRefreshWebpackPlugin()]
|
||||
: [
|
||||
new WorkboxWebpackPlugin.GenerateSW({
|
||||
swDest: 'sw.js',
|
||||
clientsClaim: true,
|
||||
skipWaiting: true,
|
||||
}),
|
||||
]),
|
||||
],
|
||||
module: {
|
||||
rules: [{
|
||||
rules: [
|
||||
{
|
||||
test: /\.tsx?$/,
|
||||
use: ['babel-loader'],
|
||||
}, {
|
||||
},
|
||||
{
|
||||
test: /\.css$/,
|
||||
use: ['style-loader', 'css-loader'],
|
||||
}],
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user