mirror of
https://github.com/tlsnotary/explorer.git
synced 2026-01-09 14:58:09 -05:00
feat: Upload/Get Proofs from IPFS (#8)
* feature: adding cid to proof store * refactor: stringify cid on server side * feature: route for /:cid to display proof details of shared proofs * chore: removing clogs * refactor: update imports and added useNotaryKey() hook * refactor: adding store action to update selectedProof.IpfsCID * refactor: proof is only uploaded to IPFS after user accepts now * refactor: updated uploadFileToIpfs thunk * refactor: refactored modal component to use createPortal * feature: updated to alpha 4 * refactor: integrated modal component from tlsn-extension * chore: changing port * refactor: more styling * refactor: fixed error on proof share if initial notary key doesn't verify proof --------- Co-authored-by: John Shaw <codetrauma@Johns-MacBook-Pro.local>
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -2,3 +2,4 @@
|
||||
**/.DS_Store
|
||||
.idea
|
||||
build
|
||||
.env
|
||||
|
||||
62
package-lock.json
generated
62
package-lock.json
generated
@@ -32,7 +32,7 @@
|
||||
"redux-thunk": "^2.4.2",
|
||||
"stream": "^0.0.2",
|
||||
"tailwindcss": "^3.3.3",
|
||||
"tlsn-js": "^0.1.0-alpha.3"
|
||||
"tlsn-js": "^0.1.0-alpha.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.20.12",
|
||||
@@ -41,6 +41,7 @@
|
||||
"@babel/preset-react": "^7.18.6",
|
||||
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.10",
|
||||
"@types/chrome": "^0.0.202",
|
||||
"@types/express": "^4.17.21",
|
||||
"@types/express-fileupload": "^1.4.4",
|
||||
"@types/node": "^20.4.10",
|
||||
"@types/react": "^18.0.26",
|
||||
@@ -84,7 +85,12 @@
|
||||
"typescript": "^4.9.4",
|
||||
"webpack": "^5.75.0",
|
||||
"webpack-cli": "^4.10.0",
|
||||
"webpack-dev-server": "^4.11.1"
|
||||
"webpack-dev-server": "^4.11.1",
|
||||
"webpack-node-externals": "^3.0.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"bufferutil": "^4.0.8",
|
||||
"utf-8-validate": "^5.0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/@aashutoshrathi/word-wrap": {
|
||||
@@ -8034,6 +8040,19 @@
|
||||
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
|
||||
"integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="
|
||||
},
|
||||
"node_modules/bufferutil": {
|
||||
"version": "4.0.8",
|
||||
"resolved": "https://registry.npmjs.org/bufferutil/-/bufferutil-4.0.8.tgz",
|
||||
"integrity": "sha512-4T53u4PdgsXqKaIctwF8ifXlRTTmEPJ8iEPWFdGZvcf7sbwYo6FKFEX9eNNAnzFZ7EzJAQ3CJeOtCRA4rDp7Pw==",
|
||||
"hasInstallScript": true,
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"node-gyp-build": "^4.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.14.2"
|
||||
}
|
||||
},
|
||||
"node_modules/busboy": {
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz",
|
||||
@@ -15116,6 +15135,17 @@
|
||||
"node": ">= 6.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/node-gyp-build": {
|
||||
"version": "4.8.0",
|
||||
"resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.0.tgz",
|
||||
"integrity": "sha512-u6fs2AEUljNho3EYTJNBfImO5QTo/J/1Etd+NVdCj7qWKUSN/bSLkZwhDv7I+w/MSC6qJ4cknepkAYykDdK8og==",
|
||||
"optional": true,
|
||||
"bin": {
|
||||
"node-gyp-build": "bin.js",
|
||||
"node-gyp-build-optional": "optional.js",
|
||||
"node-gyp-build-test": "build-test.js"
|
||||
}
|
||||
},
|
||||
"node_modules/node-int64": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz",
|
||||
@@ -19496,9 +19526,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/tlsn-js": {
|
||||
"version": "0.1.0-alpha.3",
|
||||
"resolved": "https://registry.npmjs.org/tlsn-js/-/tlsn-js-0.1.0-alpha.3.tgz",
|
||||
"integrity": "sha512-P26JOq50UOeQgjznH/M5E4B2lHBuSHd36jvHly/c2mNt5N3TsH0Dmhk16Ll7t/Dt62YdVcYOEvg/GVA3Xdkh0A==",
|
||||
"version": "0.1.0-alpha.4.1",
|
||||
"resolved": "https://registry.npmjs.org/tlsn-js/-/tlsn-js-0.1.0-alpha.4.1.tgz",
|
||||
"integrity": "sha512-vQdauBqRkB9i6EnHbuCP4JJyZ+/YT/r2WbGVJVqMbYT0BLYTOx7zI+zv9RkmMZ0a8urhPZT/bJPVWTrymyleWg==",
|
||||
"dependencies": {
|
||||
"comlink": "^4.4.1"
|
||||
},
|
||||
@@ -20035,6 +20065,19 @@
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/utf-8-validate": {
|
||||
"version": "5.0.10",
|
||||
"resolved": "https://registry.npmjs.org/utf-8-validate/-/utf-8-validate-5.0.10.tgz",
|
||||
"integrity": "sha512-Z6czzLq4u8fPOyx7TU6X3dvUZVvoJmxSQ+IcrlmagKhilxlhZgxPK6C5Jqbkw1IDUmFTM+cz9QDnnLTwDz/2gQ==",
|
||||
"hasInstallScript": true,
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"node-gyp-build": "^4.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.14.2"
|
||||
}
|
||||
},
|
||||
"node_modules/utf8-byte-length": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/utf8-byte-length/-/utf8-byte-length-1.0.4.tgz",
|
||||
@@ -20469,6 +20512,15 @@
|
||||
"node": ">=10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/webpack-node-externals": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/webpack-node-externals/-/webpack-node-externals-3.0.0.tgz",
|
||||
"integrity": "sha512-LnL6Z3GGDPht/AigwRh2dvL9PQPFQ8skEpVrWZXLWBYmqcaojHNN0onvHzie6rq7EWKrrBfPYqNEzTJgiwEQDQ==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/webpack-sources": {
|
||||
"version": "3.2.3",
|
||||
"resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz",
|
||||
|
||||
12
package.json
12
package.json
@@ -48,7 +48,7 @@
|
||||
"redux-thunk": "^2.4.2",
|
||||
"stream": "^0.0.2",
|
||||
"tailwindcss": "^3.3.3",
|
||||
"tlsn-js": "^0.1.0-alpha.3"
|
||||
"tlsn-js": "^0.1.0-alpha.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.20.12",
|
||||
@@ -57,6 +57,7 @@
|
||||
"@babel/preset-react": "^7.18.6",
|
||||
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.10",
|
||||
"@types/chrome": "^0.0.202",
|
||||
"@types/express": "^4.17.21",
|
||||
"@types/express-fileupload": "^1.4.4",
|
||||
"@types/node": "^20.4.10",
|
||||
"@types/react": "^18.0.26",
|
||||
@@ -100,7 +101,12 @@
|
||||
"typescript": "^4.9.4",
|
||||
"webpack": "^5.75.0",
|
||||
"webpack-cli": "^4.10.0",
|
||||
"webpack-dev-server": "^4.11.1"
|
||||
"webpack-dev-server": "^4.11.1",
|
||||
"webpack-node-externals": "^3.0.0"
|
||||
},
|
||||
"homepage": "https://github.com/tlsnotary/explorer#readme"
|
||||
"homepage": "https://github.com/tlsnotary/explorer#readme",
|
||||
"optionalDependencies": {
|
||||
"bufferutil": "^4.0.8",
|
||||
"utf-8-validate": "^5.0.10"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import express from 'express';
|
||||
import fileUpload from 'express-fileupload';
|
||||
import stream from 'stream';
|
||||
import path from 'path';
|
||||
|
||||
import { addBytes, getCID } from './services/ipfs';
|
||||
|
||||
@@ -25,13 +26,14 @@ app.use(fileUpload({
|
||||
limits: { fileSize: 1024 * 1024 }, // 1mb file limit
|
||||
}));
|
||||
|
||||
|
||||
|
||||
app.post('/upload', async (req, res) => {
|
||||
for (const file of Object.values(req.files!)) {
|
||||
// @ts-ignore
|
||||
const data = file.data;
|
||||
const cid = await addBytes(data);
|
||||
res.send(cid.toString());
|
||||
|
||||
res.send(JSON.stringify(cid.toString()));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -48,6 +50,10 @@ app.get('/ipfs/:cid', async (req, res) => {
|
||||
readStream.pipe(res);
|
||||
});
|
||||
|
||||
app.get('*', (req, res) => {
|
||||
res.sendFile(path.join(__dirname, '../ui', 'index.html'));
|
||||
});
|
||||
|
||||
app.listen(port, () => {
|
||||
console.log(`explorer server listening on port ${port}`);
|
||||
});
|
||||
|
||||
@@ -1,20 +1,22 @@
|
||||
import React, { ReactElement, useCallback, useState } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { uploadFile } from '../../store/proofupload';
|
||||
import { verify } from 'tlsn-js'
|
||||
import { readFileAsync } from '../../utils';
|
||||
import NotaryKey from '../NotaryKey';
|
||||
import ProofDetails from '../ProofDetails';
|
||||
import type { Proof } from '../types/types';
|
||||
import { useNotaryKey } from '../../store/notaryKey';
|
||||
import Icon from '../Icon';
|
||||
|
||||
export default function FileDrop(): ReactElement {
|
||||
const dispatch = useDispatch();
|
||||
const notaryKey = useNotaryKey();
|
||||
|
||||
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [verifiedProof, setVerifiedProof] = useState<any>(null);
|
||||
|
||||
const notaryKey = useSelector((state: any) => state.notaryKey.key);
|
||||
|
||||
const [file, setFile] = useState<File | null>(null);
|
||||
|
||||
const handleFileUpload = useCallback(async (file: any): Promise<void> => {
|
||||
if (file.type !== 'application/json') {
|
||||
@@ -26,6 +28,7 @@ export default function FileDrop(): ReactElement {
|
||||
setError('File size exceeds the maximum limit (1MB).');
|
||||
return;
|
||||
}
|
||||
setFile(file);
|
||||
setError(null);
|
||||
let verifiedProof: Proof;
|
||||
const proofContent = await readFileAsync(file);
|
||||
@@ -37,8 +40,7 @@ export default function FileDrop(): ReactElement {
|
||||
return;
|
||||
}
|
||||
dispatch(uploadFile(file.name, verifiedProof));
|
||||
|
||||
}, [dispatch])
|
||||
}, [dispatch, notaryKey])
|
||||
|
||||
const handleFileDrop = useCallback((e: React.DragEvent<HTMLDivElement>) => {
|
||||
e.preventDefault();
|
||||
@@ -68,7 +70,7 @@ return (
|
||||
onDrop={handleFileDrop}
|
||||
onDragOver={(e) => e.preventDefault()}
|
||||
>
|
||||
<i className="text-white fa-solid fa-upload text-6xl"></i>
|
||||
<Icon className="text-white" fa="fa-solid fa-upload" size={6} />
|
||||
<br></br>
|
||||
<p className="font-bold font-medium text-white">Drop your "proof.json" file here or click to select</p>
|
||||
{error && <p className="text-red-500 font-bold">{error}</p>}
|
||||
@@ -84,7 +86,7 @@ return (
|
||||
<NotaryKey />
|
||||
<br></br>
|
||||
<br></br>
|
||||
{!error && <ProofDetails proof={verifiedProof} /> }
|
||||
{!error && <ProofDetails proof={verifiedProof} file={file} />}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ export default function Header(): ReactElement {
|
||||
|
||||
return (
|
||||
<header className="flex flex-row items-center justify-between h-16 px-4 bg-gray-800 text-white">
|
||||
<div className="text-xl font-bold">TLSN Explorer</div>
|
||||
<a href="/" className="text-xl font-bold">TLSN Explorer</a>
|
||||
<div className="flex flex-row items-center gap-4">
|
||||
<a href="https://tlsnotary.org/"
|
||||
className="flex flex-row items-center justify-center w-40 h-10 rounded button"
|
||||
|
||||
94
web/components/Modal/index.tsx
Normal file
94
web/components/Modal/index.tsx
Normal file
@@ -0,0 +1,94 @@
|
||||
import React, { MouseEventHandler, ReactElement, ReactNode } from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import './modal.scss';
|
||||
import Icon from '../Icon';
|
||||
import classNames from 'classnames';
|
||||
|
||||
type Props = {
|
||||
className?: string;
|
||||
onClose: MouseEventHandler;
|
||||
children: ReactNode | ReactNode[];
|
||||
};
|
||||
|
||||
export default function Modal(props: Props): ReactElement {
|
||||
const { className, onClose, children } = props;
|
||||
|
||||
const modalRoot = document.querySelector('#modal-root');
|
||||
|
||||
if (!modalRoot) return <></>;
|
||||
|
||||
return ReactDOM.createPortal(
|
||||
<div
|
||||
className={classNames('bg-black bg-opacity-80', 'modal__overlay')}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onClose && onClose(e);
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className={classNames(`modal__wrapper bg-white`, className)}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</div>,
|
||||
modalRoot,
|
||||
);
|
||||
}
|
||||
|
||||
type HeaderProps = {
|
||||
onClose?: () => void;
|
||||
children: ReactNode;
|
||||
};
|
||||
|
||||
export function ModalHeader(props: HeaderProps): ReactElement {
|
||||
return (
|
||||
<div className={classNames('border-b modal__header border-gray-100')}>
|
||||
<div className="modal__header__title">{props.children}</div>
|
||||
<div className="modal__header__content">
|
||||
{props.onClose && (
|
||||
<div
|
||||
className={classNames(
|
||||
'flex flex-row items-center justify-center',
|
||||
'p-2 rounded-full opacity-50',
|
||||
'hover:opacity-100 text-black',
|
||||
)}
|
||||
>
|
||||
<Icon fa="fas fa-times" size={1} onClick={props.onClose} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
type ContentProps = {
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export function ModalContent(props: ContentProps): ReactElement {
|
||||
return (
|
||||
<div className={classNames('modal__content', props.className)}>
|
||||
{props.children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
type FooterProps = {
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export function ModalFooter(props: FooterProps): ReactElement {
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
'border-t modal__footer border-gray-100',
|
||||
props.className,
|
||||
)}
|
||||
>
|
||||
{props.children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
72
web/components/Modal/modal.scss
Normal file
72
web/components/Modal/modal.scss
Normal file
@@ -0,0 +1,72 @@
|
||||
.modal {
|
||||
display: flex;
|
||||
flex-flow: column nowrap;
|
||||
|
||||
&__overlay {
|
||||
position: fixed;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 9999;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
&__wrapper {
|
||||
margin: 3rem auto;
|
||||
border-radius: 0.5rem;
|
||||
z-index: 200;
|
||||
overflow: hidden;
|
||||
|
||||
@media only screen and (max-width: 768px) {
|
||||
width: 100vw !important;
|
||||
}
|
||||
}
|
||||
|
||||
&__header {
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
flex: 0 0 auto;
|
||||
align-items: center;
|
||||
padding: 0.5rem 1rem;
|
||||
|
||||
&__title {
|
||||
font-weight: 500;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
&__content {
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
flex: 1 1 auto;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
}
|
||||
|
||||
&__content {
|
||||
flex: 1 1 auto;
|
||||
max-height: calc(100vh - 20rem);
|
||||
overflow-y: auto;
|
||||
|
||||
p:nth-of-type(1) {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.error-message {
|
||||
font-size: 0.8125rem;
|
||||
text-align: center;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
&__footer {
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
align-content: center;
|
||||
justify-content: flex-end;
|
||||
flex: 0 0 auto;
|
||||
padding: 1rem 1.25rem;
|
||||
}
|
||||
}
|
||||
@@ -9,32 +9,26 @@ export default function NotaryKey(): ReactElement {
|
||||
const defaultKey: string = keys.defaultKey
|
||||
const notaryPseKey: string = keys.notaryPseKey
|
||||
|
||||
const [notaryKey, setNotaryKey] = useState<string>(defaultKey);
|
||||
const [notaryKey, setNotaryKey] = useState<string>(notaryPseKey);
|
||||
const [errors, setError] = useState<string | null>(null);
|
||||
|
||||
|
||||
const isValidPEMKey = (key: string): boolean => {
|
||||
try {
|
||||
|
||||
const trimmedKey = key.trim();
|
||||
|
||||
|
||||
if (!trimmedKey.startsWith('-----BEGIN PUBLIC KEY-----') || !trimmedKey.endsWith('-----END PUBLIC KEY-----')) {
|
||||
setError('Invalid PEM format: header or footer missing');
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
const keyContent = trimmedKey
|
||||
.replace('-----BEGIN PUBLIC KEY-----', '')
|
||||
.replace('-----END PUBLIC KEY-----', '')
|
||||
.trim();
|
||||
|
||||
try {
|
||||
const decodedKeyContent = atob(keyContent);
|
||||
atob(keyContent);
|
||||
|
||||
} catch (err) {
|
||||
console.log('here');
|
||||
setError('Invalid Base64 encoding');
|
||||
return false;
|
||||
}
|
||||
@@ -60,19 +54,30 @@ export default function NotaryKey(): ReactElement {
|
||||
}
|
||||
|
||||
return (
|
||||
<details className="w-3/4 m-auto">
|
||||
<details className="w-3/4 mx-auto">
|
||||
<summary className="text-2xl font-bold cursor-pointer">
|
||||
Change Notary Public Key:
|
||||
</summary>
|
||||
<textarea className="w-full h-40 rounded bg-gray-800 text-white resize-none" value={notaryKey} onChange={(e) => handleInput(e)}>
|
||||
</textarea>
|
||||
{errors && <p className="text-red-500">{errors}</p>}
|
||||
<button className="button" onClick={(e) => handleInput(e, notaryPseKey)}>
|
||||
notary.pse.dev
|
||||
</button>
|
||||
<button className="button" onClick={(e) => handleInput(e, defaultKey)}>
|
||||
Default
|
||||
</button>
|
||||
<textarea
|
||||
className="w-full h-40 rounded bg-gray-800 text-white resize-none mt-4 p-4"
|
||||
value={notaryKey}
|
||||
onChange={(e) => handleInput(e)}
|
||||
/>
|
||||
{errors && <p className="text-red-500 mt-2">{errors}</p>}
|
||||
<div className="flex mt-4">
|
||||
<button
|
||||
className="button"
|
||||
onClick={(e) => handleInput(e, notaryPseKey)}
|
||||
>
|
||||
notary.pse.dev
|
||||
</button>
|
||||
<button
|
||||
className="button"
|
||||
onClick={(e) => handleInput(e, defaultKey)}
|
||||
>
|
||||
Default
|
||||
</button>
|
||||
</div>
|
||||
</details>
|
||||
)
|
||||
}
|
||||
|
||||
45
web/components/ProofDetails/index.scss
Normal file
45
web/components/ProofDetails/index.scss
Normal file
@@ -0,0 +1,45 @@
|
||||
.button {
|
||||
@apply bg-slate-100;
|
||||
@apply text-slate-500;
|
||||
@apply font-bold;
|
||||
@apply px-2 py-0.5;
|
||||
user-select: none;
|
||||
|
||||
&:hover {
|
||||
@apply text-slate-600;
|
||||
@apply bg-slate-200;
|
||||
}
|
||||
|
||||
&:active {
|
||||
@apply text-slate-700;
|
||||
@apply bg-slate-300;
|
||||
}
|
||||
|
||||
&--primary {
|
||||
@apply bg-primary/[0.8];
|
||||
@apply text-white;
|
||||
|
||||
&:hover {
|
||||
@apply bg-primary/[0.9];
|
||||
@apply text-white;
|
||||
}
|
||||
|
||||
&:active {
|
||||
@apply bg-primary;
|
||||
@apply text-white;
|
||||
}
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
@apply opacity-50;
|
||||
@apply select-none;
|
||||
|
||||
&:hover {
|
||||
@apply text-slate-400;
|
||||
}
|
||||
|
||||
&:active {
|
||||
@apply text-slate-400;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,21 +1,115 @@
|
||||
import React, { ReactElement } from 'react';
|
||||
import React, { ReactElement, useState, useCallback, useEffect } from 'react';
|
||||
import { formatStrings, formatTime, extractHTML } from '../../utils';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
import ProofSelect from '../ProofSelect';
|
||||
import Modal, { ModalContent, ModalHeader, ModalFooter } from '../Modal';
|
||||
import { copyText } from '../../utils';
|
||||
import { useSelectedProof } from '../../store/proofupload';
|
||||
import { uploadFileToIpfs } from '../../store/upload';
|
||||
import Icon from '../Icon';
|
||||
|
||||
interface ProofDetailsProps {
|
||||
proof: any;
|
||||
cid?: string;
|
||||
file?: File | null;
|
||||
}
|
||||
|
||||
const ProofDetails: React.FC<ProofDetailsProps> = ({proof, cid, file}): ReactElement => {
|
||||
const dispatch = useDispatch();
|
||||
const selectedProof = useSelectedProof();
|
||||
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [accepted, setAccepted] = useState(false);
|
||||
const [fileToUpload, setFileToUpload] = useState<File | null>(null);
|
||||
|
||||
const proofs = useSelector((state: any) => state.proofUpload.proofs);
|
||||
|
||||
useEffect(() => {
|
||||
if (file) {
|
||||
setFileToUpload(file)
|
||||
}
|
||||
}, [file])
|
||||
|
||||
|
||||
export default function ProofDetails(proof: any): ReactElement {
|
||||
const closeModal = useCallback(() => {
|
||||
setIsOpen(false)
|
||||
}, []);
|
||||
|
||||
const selectedProof = useSelector((state: any) => state.proofUpload.selectedProof);
|
||||
const openModal = useCallback(() => {
|
||||
setIsOpen(true)
|
||||
}, []);
|
||||
|
||||
const proofToDisplay = selectedProof?.proof || proof?.proof;
|
||||
const handleAccept = useCallback(async () => {
|
||||
try {
|
||||
if (!fileToUpload) {
|
||||
console.error('No file to upload, state might be out of sync');
|
||||
return;
|
||||
}
|
||||
const uploadedFile = fileToUpload;
|
||||
await dispatch(uploadFileToIpfs(uploadedFile));
|
||||
setAccepted(true);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}, [fileToUpload]);
|
||||
|
||||
|
||||
const handleCopyClick: React.MouseEventHandler<HTMLButtonElement> = async (e) => {
|
||||
e.preventDefault();
|
||||
await copyText(inputValue);
|
||||
};
|
||||
|
||||
const proofToDisplay = selectedProof?.proof || proof;
|
||||
const inputValue = process.env.NODE_ENV === "development" ? `http://localhost:3000/${selectedProof?.ipfsCID ? selectedProof?.ipfsCID : cid}` : `www.tlsnexplorer.com/${selectedProof?.ipfsCID ? selectedProof?.ipfsCID : cid}`;
|
||||
|
||||
// TODO - Format proof details for redacted data
|
||||
|
||||
return (
|
||||
<div>
|
||||
{proofToDisplay && (
|
||||
<div className="flex flex-col gap-3 text-left items-center">
|
||||
<ProofSelect />
|
||||
<div className='flex flex-row gap-3'>
|
||||
{proofs.length > 1 && <ProofSelect />}
|
||||
<button onClick={openModal} className="bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600">
|
||||
Share
|
||||
</button>
|
||||
{isOpen && (
|
||||
<Modal
|
||||
className="flex flex-col items-center justify-center w-11/12 md:w-3/4 lg:w-2/4 h-auto md:h-auto lg:h-auto bg-white rounded-lg p-4 gap-4"
|
||||
onClose={closeModal}>
|
||||
<ModalHeader onClose={closeModal}>
|
||||
Share Proof
|
||||
</ModalHeader>
|
||||
<ModalContent className="flex flex-col items-center text-center gap-4">
|
||||
<p className='text-red-500 font-bold'>This will make your proof publicly accessible by anyone with the CID</p>
|
||||
{!accepted ? (
|
||||
<button className="m-0 w-32 bg-red-200 text-red-500 hover:bg-red-200 hover:text-red-500 hover:font-bold" onClick={handleAccept}>
|
||||
I understand
|
||||
</button>
|
||||
) : (
|
||||
<div className="relative w-11/12">
|
||||
<input
|
||||
readOnly
|
||||
value={inputValue}
|
||||
className="w-full h-12 bg-gray-800 text-white rounded px-3 pr-10" // Added pr-10 for padding on the right
|
||||
/>
|
||||
<button
|
||||
className="absolute top-0 right-0 w-10 h-12 bg-gray-700 text-white flex items-center justify-center hover:bg-gray-500 rounded-r focus:outline-none focus:ring focus:border-blue-500"
|
||||
onClick={handleCopyClick}
|
||||
>
|
||||
<Icon className="fas fa-copy" size={1}/>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</ModalContent>
|
||||
<ModalFooter>
|
||||
<button className="m-0 w-24 bg-slate-600 text-slate-200 hover:bg-slate-500 hover:text-slate-100 hover:font-bold" onClick={closeModal}>
|
||||
Close
|
||||
</button>
|
||||
</ModalFooter>
|
||||
</Modal>
|
||||
)}
|
||||
</div>
|
||||
<span className="font-bold text-2xl">Server Domain:</span>
|
||||
<div className="flex items-center h-12 w-4/5 bg-gray-800 text-white rounded">{proofToDisplay.serverName}</div>
|
||||
<span className="font-bold text-2xl">Notarization Time:</span>
|
||||
@@ -45,3 +139,5 @@ export default function ProofDetails(proof: any): ReactElement {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ProofDetails
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import React, { ReactElement } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { readFileAsync } from '../../utils';
|
||||
import { selectProof } from '../../store/proofupload';
|
||||
|
||||
export default function ProofSelect(): ReactElement {
|
||||
|
||||
52
web/components/SharedProof/index.tsx
Normal file
52
web/components/SharedProof/index.tsx
Normal file
@@ -0,0 +1,52 @@
|
||||
import React, { ReactElement, useEffect, useState } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { verify } from 'tlsn-js';
|
||||
import ProofDetails from '../ProofDetails';
|
||||
import type { Proof } from '../types/types';
|
||||
import { useNotaryKey } from '../../store/notaryKey';
|
||||
import NotaryKey from '../NotaryKey';
|
||||
|
||||
export default function SharedProof(): ReactElement {
|
||||
const { cid } = useParams();
|
||||
|
||||
const [verifiedProof, setVerifiedProof] = useState<Proof | null>(null);
|
||||
const [errors, setErrors] = useState<string | null>(null);
|
||||
|
||||
const notaryKey = useNotaryKey();
|
||||
|
||||
useEffect(() => {
|
||||
async function fetchFile() {
|
||||
if (!cid) {
|
||||
setErrors('No CID provided');
|
||||
return;
|
||||
}
|
||||
const response = await fetch(`/ipfs/${cid}`);
|
||||
if (!response.ok) {
|
||||
setErrors('Failed to fetch file from IPFS');
|
||||
throw new Error('Failed to fetch file from IPFS');
|
||||
}
|
||||
const data = await response.json();
|
||||
try {
|
||||
const proof = await verify(data, notaryKey);
|
||||
setVerifiedProof(proof);
|
||||
|
||||
} catch (e) {
|
||||
setErrors('Provide a valid public key')
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
fetchFile();
|
||||
|
||||
}, [cid, notaryKey]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
{<NotaryKey />}
|
||||
<div className="flex flex-col items-center">
|
||||
{!verifiedProof && errors && <div className="text-red-500 font-bold">{errors}</div>}
|
||||
</div>
|
||||
{verifiedProof && <ProofDetails proof={verifiedProof} cid={cid} />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -3,6 +3,7 @@ import * as React from 'react';
|
||||
import * as ReactDOM from 'react-dom';
|
||||
import { Provider } from 'react-redux';
|
||||
import { BrowserRouter } from 'react-router-dom';
|
||||
import type {} from 'redux-thunk/extend-redux';
|
||||
import store from './store';
|
||||
import App from './pages/App';
|
||||
|
||||
@@ -19,4 +20,4 @@ import App from './pages/App';
|
||||
|
||||
if ((module as any).hot) {
|
||||
(module as any).hot.accept();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import React, { ReactElement } from 'react';
|
||||
import './index.scss';
|
||||
import Header from '../../components/Header';
|
||||
import FileDrop from '../../components/FileUpload';
|
||||
import { Routes, Route } from 'react-router-dom';
|
||||
import ProofSelect from '../../components/ProofSelect';
|
||||
import FileDrop from '../../components/FileUpload';
|
||||
import SharedProof from '../../components/SharedProof';
|
||||
|
||||
export default function App(): ReactElement {
|
||||
|
||||
@@ -13,6 +13,7 @@ export default function App(): ReactElement {
|
||||
<Header />
|
||||
<Routes>
|
||||
<Route path="/" element={<FileDrop />} />
|
||||
<Route path="/:cid" element={<SharedProof />} />
|
||||
</Routes>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { AppRootState } from '.';
|
||||
import { useSelector } from 'react-redux';
|
||||
import keys from '../utils/keys.json';
|
||||
|
||||
export enum ActionType {
|
||||
@@ -34,6 +36,9 @@ function handleKey(state: State, action: Action): State {
|
||||
}
|
||||
}
|
||||
|
||||
export const useNotaryKey = () => {
|
||||
return useSelector((state: AppRootState) => state.notaryKey.key);
|
||||
}
|
||||
|
||||
export default function notaryKey(state = initState, action: Action): State {
|
||||
switch (action.type) {
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
import { AppRootState } from '.';
|
||||
import type { Proof } from '../components/types/types'
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
|
||||
|
||||
export enum ActionType {
|
||||
AddFile = 'proofupload/addFile',
|
||||
SelectProof = 'proofupload/selectProof'
|
||||
SelectProof = 'proofupload/selectProof',
|
||||
UploadFileSuccess = 'proofupload/uploadFileSuccess',
|
||||
}
|
||||
|
||||
export const uploadFile = (fileName: string, proof: Proof) => ({
|
||||
@@ -15,6 +20,11 @@ export const selectProof = (proof: string) => ({
|
||||
payload: proof
|
||||
})
|
||||
|
||||
export const uploadFileSuccess = (cid: string) => ({
|
||||
type: ActionType.UploadFileSuccess,
|
||||
payload: cid
|
||||
})
|
||||
|
||||
export type Action<payload = any> = {
|
||||
type: ActionType;
|
||||
payload: payload;
|
||||
@@ -24,7 +34,7 @@ export type Action<payload = any> = {
|
||||
|
||||
type State = {
|
||||
proofs: { fileName: string, proof: Proof }[];
|
||||
selectedProof?: {fileName: string, proof: Proof} | null;
|
||||
selectedProof?: { fileName: string, proof: Proof, ipfsCID?: string } | null;
|
||||
}
|
||||
|
||||
const initState: State = {
|
||||
@@ -34,7 +44,8 @@ const initState: State = {
|
||||
function handleFile(state: State, action: Action): State {
|
||||
return {
|
||||
...state,
|
||||
proofs: [...state.proofs, action.payload]
|
||||
proofs: [...state.proofs, action.payload],
|
||||
selectedProof: action.payload
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,7 +54,21 @@ function handleProofSelect(state: State, action: Action): State {
|
||||
...state,
|
||||
selectedProof: action.payload
|
||||
}
|
||||
}
|
||||
|
||||
function handleProofUpload(state: State, action: Action): State {
|
||||
return {
|
||||
...state,
|
||||
// @ts-ignore
|
||||
selectedProof: {
|
||||
...state.selectedProof,
|
||||
ipfsCID: action.payload
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const useSelectedProof = () => {
|
||||
return useSelector((state: AppRootState) => state.proofUpload.selectedProof);
|
||||
}
|
||||
|
||||
export default function proofUpload(state = initState, action: Action): State {
|
||||
@@ -52,6 +77,8 @@ export default function proofUpload(state = initState, action: Action): State {
|
||||
return handleFile(state, action);
|
||||
case ActionType.SelectProof:
|
||||
return handleProofSelect(state, action);
|
||||
case ActionType.UploadFileSuccess:
|
||||
return handleProofUpload(state, action);
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
|
||||
28
web/store/upload.ts
Normal file
28
web/store/upload.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { ThunkDispatch } from 'redux-thunk';
|
||||
import { Action, uploadFileSuccess } from './proofupload';
|
||||
import { AppRootState } from './index';
|
||||
import { ActionType
|
||||
} from './proofupload';
|
||||
|
||||
|
||||
export const uploadFileToIpfs = (file: File) => {
|
||||
return async (dispatch: ThunkDispatch<AppRootState, ActionType, Action>) => {
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
|
||||
try {
|
||||
const response = await fetch('/upload', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to upload file to IPFS');
|
||||
}
|
||||
const data = await response.json();
|
||||
dispatch(uploadFileSuccess(data))
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error('Error uploading file to IPFS:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { ReactElement } from 'react';
|
||||
import React, { ReactElement, useRef } from 'react';
|
||||
|
||||
|
||||
export const readFileAsync = (file: File): Promise<string> => {
|
||||
@@ -49,3 +49,12 @@ export const extractHTML = (receivedData: string): ReactElement => {
|
||||
return <iframe className="w-full h-auto" srcDoc={html}></iframe>
|
||||
|
||||
};
|
||||
|
||||
|
||||
export const copyText = async (text: string): Promise<void> => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(text);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user