Compare commits
26 Commits
issue-215-
...
staging
Author | SHA1 | Date | |
---|---|---|---|
af036b1bb7 | |||
c0b903929d | |||
b97afdecfd | |||
feea3197d0 | |||
c45e3912a2 | |||
f72fa1a886 | |||
|
a4310675c1 | ||
|
5f3d92d62f | ||
|
04f1d692a4 | ||
|
99d562a3ed | ||
|
e60c4cbc31 | ||
|
3f01ab8fca | ||
|
b7410c7d33 | ||
|
b6a84dedbe | ||
|
8b5abe02e2 | ||
|
7b2537e355 | ||
|
bcd57138ca | ||
2d472470be | |||
c69d55c3a8 | |||
3be0fd7bbb | |||
|
5079b68bdf | ||
70e7e5305e | |||
b361ab3d99 | |||
|
dbcc96aca2 | ||
|
9a1d3d98bf | ||
5ed3d2f389 |
@ -26,9 +26,27 @@ jobs:
|
||||
- name: Create Build
|
||||
run: npm run build
|
||||
|
||||
- name: Release Build
|
||||
- name: Deploy Build
|
||||
run: |
|
||||
npm -g install cloudron-surfer
|
||||
surfer config --token ${{ secrets.CLOUDRON_SURFER_TOKEN }} --server sigit.io
|
||||
surfer config --token ${{ secrets.CLOUDRON_SURFER_TOKEN }} --server sigit.io
|
||||
surfer put dist/* / --all -d
|
||||
surfer put dist/.well-known / --all
|
||||
surfer put dist/.well-known / --all
|
||||
|
||||
- name: Create Empty Release (assets are posted later)
|
||||
run: |
|
||||
npm i
|
||||
npm i -g semantic-release
|
||||
# We do a semantic-release DRY RUN to make the job fail if there are no changes to release
|
||||
GITEA_TOKEN=${{ secrets.RELEASE_TOKEN }} GITEA_URL=https://git.nostrdev.com/sigit/sigit.io semantic-release --dry-run | grep -q "There are no relevant changes, so no new version is released." && exit 1
|
||||
GITEA_TOKEN=${{ secrets.RELEASE_TOKEN }} GITEA_URL=https://git.nostrdev.com/sigit/sigit.io semantic-release
|
||||
|
||||
- name: Upload assets to release
|
||||
run: |
|
||||
RELEASE_ID=`curl -k 'https://git.nostrdev.com/sigit/sigit.io/api/v1/repos/sigit/sigit.io/releases/latest?access_token=${{ secrets.RELEASE_TOKEN }}' | jq -r '.id'`
|
||||
RELEASE_BODY=`curl -k 'https://git.nostrdev.com/sigit/sigit.io/api/v1/repos/sigit/sigit.io/releases/latest?access_token=${{ secrets.RELEASE_TOKEN }}' | jq -r '.body'`
|
||||
# Update body
|
||||
curl --data '{"draft": false,"body":"'"$RELEASE_BODY\n\nFor installation instructions, please visit https://docs.sigit.io/#/"'"}' -X PATCH --header 'Content-Type: application/json' -k https://git.nostrdev.com/sigit/sigit.io/api/v1/repos/sigit/sigit.io/releases/$RELEASE_ID?access_token=${{ secrets.RELEASE_TOKEN }}
|
||||
# Upload assets
|
||||
URL="https://git.nostrdev.com/sigit/sigit.io/api/v1/repos/sigit/sigit.io/releases/$RELEASE_ID/assets?access_token=${{ secrets.RELEASE_TOKEN }}"
|
||||
curl -k $URL -F attachment=@dist.zip
|
||||
|
2
.gitignore
vendored
2
.gitignore
vendored
@ -8,7 +8,7 @@ pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-zip
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
|
31
.releaserc
Normal file
31
.releaserc
Normal file
@ -0,0 +1,31 @@
|
||||
{
|
||||
"branches": [
|
||||
"main"
|
||||
],
|
||||
"plugins": [
|
||||
"@semantic-release/commit-analyzer",
|
||||
"@semantic-release/release-notes-generator",
|
||||
"@semantic-release/changelog",
|
||||
"@semantic-release/npm",
|
||||
[
|
||||
"@semantic-release/git",
|
||||
{
|
||||
"assets": [
|
||||
"CHANGELOG.md",
|
||||
"package.json"
|
||||
]
|
||||
}
|
||||
],
|
||||
[
|
||||
"@saithodev/semantic-release-gitea",
|
||||
{
|
||||
"giteaUrl": "https://git.nostrdev.com/sigit/sigit.io",
|
||||
"assets": [
|
||||
{
|
||||
"path": "dist-zip/dist.zip"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
8714
package-lock.json
generated
8714
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
11
package.json
11
package.json
@ -17,7 +17,8 @@
|
||||
"preview": "vite preview",
|
||||
"preinstall": "git config core.hooksPath .git-hooks",
|
||||
"license-checker": "node licenseChecker.cjs",
|
||||
"lint-staged": "lint-staged"
|
||||
"lint-staged": "lint-staged",
|
||||
"release": "commit-and-tag-version"
|
||||
},
|
||||
"dependencies": {
|
||||
"@emotion/react": "11.11.4",
|
||||
@ -65,6 +66,12 @@
|
||||
"use-immer": "^0.11.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@saithodev/semantic-release-gitea": "^2.1.0",
|
||||
"@semantic-release/changelog": "^6.0.3",
|
||||
"@semantic-release/commit-analyzer": "^10.0.1",
|
||||
"@semantic-release/git": "^10.0.1",
|
||||
"@semantic-release/npm": "11.0.0",
|
||||
"@semantic-release/release-notes-generator": "^11.0.4",
|
||||
"@types/crypto-js": "^4.2.2",
|
||||
"@types/file-saver": "2.0.7",
|
||||
"@types/lodash": "4.14.202",
|
||||
@ -75,6 +82,7 @@
|
||||
"@typescript-eslint/eslint-plugin": "^7.0.2",
|
||||
"@typescript-eslint/parser": "^7.0.2",
|
||||
"@vitejs/plugin-react": "^4.2.1",
|
||||
"commit-and-tag-version": "^11.2.2",
|
||||
"eslint": "^8.56.0",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.5",
|
||||
@ -85,6 +93,7 @@
|
||||
"typescript": "^5.2.2",
|
||||
"vite": "^5.1.4",
|
||||
"vite-plugin-node-polyfills": "^0.22.0",
|
||||
"vite-plugin-zip-pack": "^1.2.4",
|
||||
"vite-tsconfig-paths": "4.3.2"
|
||||
},
|
||||
"lint-staged": {
|
||||
|
24
src/components/ButtonUnderline/index.tsx
Normal file
24
src/components/ButtonUnderline/index.tsx
Normal file
@ -0,0 +1,24 @@
|
||||
import { PropsWithChildren } from 'react'
|
||||
import styles from './style.module.scss'
|
||||
|
||||
interface ButtonUnderlineProps {
|
||||
onClick: (event: React.MouseEvent<HTMLButtonElement>) => void
|
||||
disabled?: boolean | undefined
|
||||
}
|
||||
|
||||
export const ButtonUnderline = ({
|
||||
onClick,
|
||||
disabled = false,
|
||||
children
|
||||
}: PropsWithChildren<ButtonUnderlineProps>) => {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
className={styles.button}
|
||||
disabled={disabled}
|
||||
onClick={onClick}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
)
|
||||
}
|
25
src/components/ButtonUnderline/style.module.scss
Normal file
25
src/components/ButtonUnderline/style.module.scss
Normal file
@ -0,0 +1,25 @@
|
||||
@import '../../styles/colors.scss';
|
||||
|
||||
.button {
|
||||
color: $primary-main !important;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 12px;
|
||||
|
||||
width: max-content;
|
||||
margin: 0 auto;
|
||||
|
||||
// Override default styling
|
||||
border: none !important;
|
||||
outline: none !important;
|
||||
|
||||
// Override leaky css in sign page
|
||||
background: transparent !important;
|
||||
|
||||
&:focus,
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
text-decoration-color: inherit;
|
||||
}
|
||||
}
|
@ -1,16 +1,20 @@
|
||||
import React from 'react'
|
||||
import { Button, Menu, MenuItem } from '@mui/material'
|
||||
import PopupState, { bindTrigger, bindMenu } from 'material-ui-popup-state'
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||
import {
|
||||
faCheck,
|
||||
faLock,
|
||||
faTriangleExclamation
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
import { CurrentUserFile } from '../../types/file.ts'
|
||||
import styles from './style.module.scss'
|
||||
import { Button, Menu, MenuItem } from '@mui/material'
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||
import { faCheck } from '@fortawesome/free-solid-svg-icons'
|
||||
import PopupState, { bindTrigger, bindMenu } from 'material-ui-popup-state'
|
||||
import React from 'react'
|
||||
|
||||
interface FileListProps {
|
||||
files: CurrentUserFile[]
|
||||
currentFile: CurrentUserFile
|
||||
setCurrentFile: (file: CurrentUserFile) => void
|
||||
handleExport: () => void
|
||||
handleExport?: () => void
|
||||
handleEncryptedExport?: () => void
|
||||
}
|
||||
|
||||
@ -45,34 +49,48 @@ const FileList = ({
|
||||
))}
|
||||
</ul>
|
||||
|
||||
<PopupState variant="popover" popupId="download-popup-menu">
|
||||
{(popupState) => (
|
||||
<React.Fragment>
|
||||
<Button variant="contained" {...bindTrigger(popupState)}>
|
||||
Export files
|
||||
</Button>
|
||||
<Menu {...bindMenu(popupState)}>
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
popupState.close
|
||||
handleExport()
|
||||
}}
|
||||
>
|
||||
Export Files
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
popupState.close
|
||||
typeof handleEncryptedExport === 'function' &&
|
||||
handleEncryptedExport()
|
||||
}}
|
||||
>
|
||||
Export Encrypted Files
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
</React.Fragment>
|
||||
)}
|
||||
</PopupState>
|
||||
{(typeof handleExport === 'function' ||
|
||||
typeof handleEncryptedExport === 'function') && (
|
||||
<PopupState variant="popover" popupId="download-popup-menu">
|
||||
{(popupState) => (
|
||||
<React.Fragment>
|
||||
<Button variant="contained" {...bindTrigger(popupState)}>
|
||||
Export files
|
||||
</Button>
|
||||
<Menu {...bindMenu(popupState)}>
|
||||
{typeof handleEncryptedExport === 'function' && (
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
popupState.close
|
||||
handleEncryptedExport()
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
color={'var(--mui-palette-primary-main)'}
|
||||
icon={faLock}
|
||||
/>
|
||||
ENCRYPTED
|
||||
</MenuItem>
|
||||
)}
|
||||
{typeof handleExport === 'function' && (
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
popupState.close
|
||||
handleExport()
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
color={'var(--mui-palette-primary-main)'}
|
||||
icon={faTriangleExclamation}
|
||||
/>
|
||||
UNENCRYPTED
|
||||
</MenuItem>
|
||||
)}
|
||||
</Menu>
|
||||
</React.Fragment>
|
||||
)}
|
||||
</PopupState>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
@ -8,15 +8,19 @@ import {
|
||||
import React, { useState } from 'react'
|
||||
import { MarkInput } from '../MarkTypeStrategy/MarkInput.tsx'
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||
import { faCheck } from '@fortawesome/free-solid-svg-icons'
|
||||
import { faCheck, faDownload } from '@fortawesome/free-solid-svg-icons'
|
||||
import { Button } from '@mui/material'
|
||||
import styles from './style.module.scss'
|
||||
import { ButtonUnderline } from '../ButtonUnderline/index.tsx'
|
||||
|
||||
interface MarkFormFieldProps {
|
||||
currentUserMarks: CurrentUserMark[]
|
||||
handleCurrentUserMarkChange: (mark: CurrentUserMark) => void
|
||||
handleSelectedMarkValueChange: (value: string) => void
|
||||
handleSubmit: (event: React.MouseEvent<HTMLButtonElement>) => void
|
||||
handleSubmit: (
|
||||
event: React.MouseEvent<HTMLButtonElement>,
|
||||
type: 'online' | 'offline'
|
||||
) => void
|
||||
selectedMark: CurrentUserMark | null
|
||||
selectedMarkValue: string
|
||||
}
|
||||
@ -73,11 +77,11 @@ const MarkFormField = ({
|
||||
setComplete(true)
|
||||
}
|
||||
|
||||
const handleSignAndComplete = (
|
||||
event: React.MouseEvent<HTMLButtonElement>
|
||||
) => {
|
||||
handleSubmit(event)
|
||||
}
|
||||
const handleSignAndComplete =
|
||||
(type: 'online' | 'offline') =>
|
||||
(event: React.MouseEvent<HTMLButtonElement>) => {
|
||||
handleSubmit(event, type)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
@ -129,18 +133,28 @@ const MarkFormField = ({
|
||||
</div>
|
||||
</form>
|
||||
) : (
|
||||
<div className={styles.actionsBottom}>
|
||||
<Button
|
||||
onClick={handleSignAndComplete}
|
||||
className={[styles.submitButton, styles.completeButton].join(
|
||||
' '
|
||||
)}
|
||||
<>
|
||||
<div className={styles.actionsBottom}>
|
||||
<Button
|
||||
onClick={handleSignAndComplete('online')}
|
||||
className={[
|
||||
styles.submitButton,
|
||||
styles.completeButton
|
||||
].join(' ')}
|
||||
disabled={!isReadyToSign()}
|
||||
autoFocus
|
||||
>
|
||||
SIGN AND BROADCAST
|
||||
</Button>
|
||||
</div>
|
||||
<ButtonUnderline
|
||||
onClick={handleSignAndComplete('offline')}
|
||||
disabled={!isReadyToSign()}
|
||||
autoFocus
|
||||
>
|
||||
SIGN AND COMPLETE
|
||||
</Button>
|
||||
</div>
|
||||
<FontAwesomeIcon icon={faDownload} />
|
||||
Sign and export locally instead
|
||||
</ButtonUnderline>
|
||||
</>
|
||||
)}
|
||||
|
||||
<div className={styles.footerContainer}>
|
||||
|
@ -3,7 +3,6 @@ import {
|
||||
decryptArrayBuffer,
|
||||
encryptArrayBuffer,
|
||||
getHash,
|
||||
isOnline,
|
||||
uploadToFileStorage
|
||||
} from '../../../utils'
|
||||
import { MarkStrategy } from '../MarkStrategy'
|
||||
@ -37,21 +36,17 @@ export const SignatureStrategy: MarkStrategy = {
|
||||
// Create the encrypted json file from array buffer and hash
|
||||
const file = new File([encryptedArrayBuffer], `${hash}.json`)
|
||||
|
||||
if (await isOnline()) {
|
||||
try {
|
||||
const url = await uploadToFileStorage(file)
|
||||
console.info(`${file.name} uploaded to file storage`)
|
||||
return url
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
console.error(
|
||||
`Error occurred in uploading file ${file.name}`,
|
||||
error.message
|
||||
)
|
||||
}
|
||||
try {
|
||||
const url = await uploadToFileStorage(file)
|
||||
console.info(`${file.name} uploaded to file storage`)
|
||||
return url
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
console.error(
|
||||
`Error occurred in uploading file ${file.name}`,
|
||||
error.message
|
||||
)
|
||||
}
|
||||
} else {
|
||||
// TOOD: offline
|
||||
}
|
||||
|
||||
return value
|
||||
@ -89,7 +84,6 @@ export const SignatureStrategy: MarkStrategy = {
|
||||
return json
|
||||
}
|
||||
|
||||
// TOOD: offline
|
||||
return value
|
||||
}
|
||||
}
|
||||
|
@ -24,9 +24,8 @@ import {
|
||||
interface PdfMarkingProps {
|
||||
currentUserMarks: CurrentUserMark[]
|
||||
files: CurrentUserFile[]
|
||||
handleExport: () => void
|
||||
handleEncryptedExport: () => void
|
||||
handleSign: () => void
|
||||
handleSignOffline: () => void
|
||||
meta: Meta | null
|
||||
otherUserMarks: Mark[]
|
||||
setCurrentUserMarks: (currentUserMarks: CurrentUserMark[]) => void
|
||||
@ -39,18 +38,16 @@ interface PdfMarkingProps {
|
||||
* @param props
|
||||
* @constructor
|
||||
*/
|
||||
const PdfMarking = (props: PdfMarkingProps) => {
|
||||
const {
|
||||
files,
|
||||
currentUserMarks,
|
||||
setCurrentUserMarks,
|
||||
setUpdatedMarks,
|
||||
handleExport,
|
||||
handleEncryptedExport,
|
||||
handleSign,
|
||||
meta,
|
||||
otherUserMarks
|
||||
} = props
|
||||
const PdfMarking = ({
|
||||
files,
|
||||
currentUserMarks,
|
||||
setCurrentUserMarks,
|
||||
setUpdatedMarks,
|
||||
handleSign,
|
||||
handleSignOffline,
|
||||
meta,
|
||||
otherUserMarks
|
||||
}: PdfMarkingProps) => {
|
||||
const [selectedMark, setSelectedMark] = useState<CurrentUserMark | null>(null)
|
||||
const [selectedMarkValue, setSelectedMarkValue] = useState<string>('')
|
||||
const [currentFile, setCurrentFile] = useState<CurrentUserFile | null>(null)
|
||||
@ -99,7 +96,10 @@ const PdfMarking = (props: PdfMarkingProps) => {
|
||||
/**
|
||||
* Sign and Complete
|
||||
*/
|
||||
const handleSubmit = (event: React.MouseEvent<HTMLButtonElement>) => {
|
||||
const handleSubmit = (
|
||||
event: React.MouseEvent<HTMLButtonElement>,
|
||||
type: 'online' | 'offline'
|
||||
) => {
|
||||
event.preventDefault()
|
||||
if (selectedMarkValue && selectedMark) {
|
||||
const updatedMark: CurrentUserMark = getUpdatedMark(
|
||||
@ -117,16 +117,10 @@ const PdfMarking = (props: PdfMarkingProps) => {
|
||||
setUpdatedMarks(updatedMark.mark)
|
||||
}
|
||||
|
||||
handleSign()
|
||||
if (type === 'online') handleSign()
|
||||
else if (type === 'offline') handleSignOffline()
|
||||
}
|
||||
|
||||
// const updateCurrentUserMarkValues = () => {
|
||||
// const updatedMark: CurrentUserMark = getUpdatedMark(selectedMark!, selectedMarkValue)
|
||||
// const updatedCurrentUserMarks = updateCurrentUserMarks(currentUserMarks, updatedMark)
|
||||
// setSelectedMarkValue(EMPTY)
|
||||
// setCurrentUserMarks(updatedCurrentUserMarks)
|
||||
// }
|
||||
|
||||
const handleChange = (value: string) => {
|
||||
setSelectedMarkValue(value)
|
||||
}
|
||||
@ -142,8 +136,6 @@ const PdfMarking = (props: PdfMarkingProps) => {
|
||||
files={files}
|
||||
currentFile={currentFile}
|
||||
setCurrentFile={setCurrentFile}
|
||||
handleExport={handleExport}
|
||||
handleEncryptedExport={handleEncryptedExport}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
@ -38,12 +38,16 @@ const PdfView = ({
|
||||
currentUserMarks: CurrentUserMark[],
|
||||
hash: string
|
||||
): CurrentUserMark[] => {
|
||||
return currentUserMarks.filter(
|
||||
(currentUserMark) => currentUserMark.mark.pdfFileHash === hash
|
||||
return currentUserMarks.filter((currentUserMark) =>
|
||||
currentUserMark.mark.pdfFileHash
|
||||
? currentUserMark.mark.pdfFileHash === hash
|
||||
: currentUserMark.mark.fileHash === hash
|
||||
)
|
||||
}
|
||||
const filterMarksByFile = (marks: Mark[], hash: string): Mark[] => {
|
||||
return marks.filter((mark) => mark.pdfFileHash === hash)
|
||||
return marks.filter((mark) =>
|
||||
mark.pdfFileHash ? mark.pdfFileHash === hash : mark.fileHash === hash
|
||||
)
|
||||
}
|
||||
return (
|
||||
<div className="files-wrapper">
|
||||
|
@ -19,7 +19,7 @@
|
||||
"page": 1
|
||||
},
|
||||
"npub": "npub1x77qywdllzetv9ncnhlfpv62kshlgtt0uqlsq3v22uzzkk2xvvrsn6uyfy",
|
||||
"pdfFileHash": "da5f857e77d3aa59c461efad804116931c059b36e6b4da0b5d9452753ec70c05"
|
||||
"fileHash": "da5f857e77d3aa59c461efad804116931c059b36e6b4da0b5d9452753ec70c05"
|
||||
}
|
||||
],
|
||||
"da5f857e77d3aa59c461efad804116931c059b36e6b4da0b5d9452753ec70c05/2.png": [
|
||||
@ -34,7 +34,7 @@
|
||||
"page": 2
|
||||
},
|
||||
"npub": "npub1x77qywdllzetv9ncnhlfpv62kshlgtt0uqlsq3v22uzzkk2xvvrsn6uyfy",
|
||||
"pdfFileHash": "da5f857e77d3aa59c461efad804116931c059b36e6b4da0b5d9452753ec70c05"
|
||||
"fileHash": "da5f857e77d3aa59c461efad804116931c059b36e6b4da0b5d9452753ec70c05"
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -54,7 +54,7 @@
|
||||
"page": 1
|
||||
},
|
||||
"npub": "npub1x77qywdllzetv9ncnhlfpv62kshlgtt0uqlsq3v22uzzkk2xvvrsn6uyfy",
|
||||
"pdfFileHash": "da5f857e77d3aa59c461efad804116931c059b36e6b4da0b5d9452753ec70c05",
|
||||
"fileHash": "da5f857e77d3aa59c461efad804116931c059b36e6b4da0b5d9452753ec70c05",
|
||||
"value": "Pera Peric"
|
||||
},
|
||||
{
|
||||
@ -68,7 +68,7 @@
|
||||
"page": 2
|
||||
},
|
||||
"npub": "npub1x77qywdllzetv9ncnhlfpv62kshlgtt0uqlsq3v22uzzkk2xvvrsn6uyfy",
|
||||
"pdfFileHash": "da5f857e77d3aa59c461efad804116931c059b36e6b4da0b5d9452753ec70c05",
|
||||
"fileHash": "da5f857e77d3aa59c461efad804116931c059b36e6b4da0b5d9452753ec70c05",
|
||||
"value": "Pera Peric"
|
||||
}
|
||||
]
|
||||
|
@ -1,11 +1,5 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import {
|
||||
CreateSignatureEventContent,
|
||||
DocSignatureEvent,
|
||||
Meta,
|
||||
SignedEventContent,
|
||||
OpenTimestamp
|
||||
} from '../types'
|
||||
import { DocSignatureEvent, Meta, SignedEventContent, FlatMeta } from '../types'
|
||||
import { Mark } from '../types/mark'
|
||||
import {
|
||||
fromUnixTimestamp,
|
||||
@ -17,53 +11,11 @@ import {
|
||||
} from '../utils'
|
||||
import { toast } from 'react-toastify'
|
||||
import { verifyEvent } from 'nostr-tools'
|
||||
import { Event } from 'nostr-tools'
|
||||
import store from '../store/store'
|
||||
import { NostrController } from '../controllers'
|
||||
import { MetaParseError } from '../types/errors/MetaParseError'
|
||||
import { MARK_TYPE_CONFIG } from '../components/MarkTypeStrategy/MarkStrategy'
|
||||
|
||||
/**
|
||||
* Flattened interface that combines properties `Meta`, `CreateSignatureEventContent`,
|
||||
* and `Event` (event's fields are made optional and pubkey and created_at replaced with our versions)
|
||||
*/
|
||||
export interface FlatMeta
|
||||
extends Meta,
|
||||
CreateSignatureEventContent,
|
||||
Partial<Omit<Event, 'pubkey' | 'created_at'>> {
|
||||
// Remove pubkey and use submittedBy as `npub1${string}`
|
||||
submittedBy?: `npub1${string}`
|
||||
|
||||
// Optional field only present on exported sigits
|
||||
// Exporting adds user's pubkey
|
||||
exportedBy?: `npub1${string}`
|
||||
|
||||
// Remove created_at and replace with createdAt
|
||||
createdAt?: number
|
||||
|
||||
// Validated create signature event
|
||||
isValid: boolean
|
||||
|
||||
// Decryption
|
||||
encryptionKey: string | undefined
|
||||
|
||||
// Parsed Document Signatures
|
||||
parsedSignatureEvents: {
|
||||
[signer: `npub1${string}`]: DocSignatureEvent
|
||||
}
|
||||
|
||||
// Calculated completion time
|
||||
completedAt?: number
|
||||
|
||||
// Calculated status fields
|
||||
signedStatus: SigitStatus
|
||||
signersStatus: {
|
||||
[signer: `npub1${string}`]: SignStatus
|
||||
}
|
||||
|
||||
timestamps?: OpenTimestamp[]
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom use hook for parsing the Sigit Meta
|
||||
* @param meta Sigit Meta
|
||||
@ -74,8 +26,8 @@ export const useSigitMeta = (meta: Meta): FlatMeta => {
|
||||
const [kind, setKind] = useState<number>()
|
||||
const [tags, setTags] = useState<string[][]>()
|
||||
const [createdAt, setCreatedAt] = useState<number>()
|
||||
const [submittedBy, setSubmittedBy] = useState<`npub1${string}`>() // submittedBy, pubkey from nostr event
|
||||
const [exportedBy, setExportedBy] = useState<`npub1${string}`>() // pubkey from export signature nostr event
|
||||
const [submittedBy, setSubmittedBy] = useState<string>() // submittedBy, pubkey from nostr event (hex)
|
||||
const [exportedBy, setExportedBy] = useState<string>() // pubkey from export signature nostr event (hex)
|
||||
const [id, setId] = useState<string>()
|
||||
const [sig, setSig] = useState<string>()
|
||||
|
||||
@ -108,18 +60,16 @@ export const useSigitMeta = (meta: Meta): FlatMeta => {
|
||||
;(async function () {
|
||||
try {
|
||||
if (meta.exportSignature) {
|
||||
const exportSignatureEvent = await parseNostrEvent(
|
||||
meta.exportSignature
|
||||
)
|
||||
const exportSignatureEvent = parseNostrEvent(meta.exportSignature)
|
||||
if (
|
||||
verifyEvent(exportSignatureEvent) &&
|
||||
exportSignatureEvent.pubkey
|
||||
) {
|
||||
setExportedBy(exportSignatureEvent.pubkey as `npub1${string}`)
|
||||
setExportedBy(exportSignatureEvent.pubkey)
|
||||
}
|
||||
}
|
||||
|
||||
const createSignatureEvent = await parseNostrEvent(meta.createSignature)
|
||||
const createSignatureEvent = parseNostrEvent(meta.createSignature)
|
||||
|
||||
const { kind, tags, created_at, pubkey, id, sig, content } =
|
||||
createSignatureEvent
|
||||
@ -129,12 +79,12 @@ export const useSigitMeta = (meta: Meta): FlatMeta => {
|
||||
setTags(tags)
|
||||
// created_at in nostr events are stored in seconds
|
||||
setCreatedAt(fromUnixTimestamp(created_at))
|
||||
setSubmittedBy(pubkey as `npub1${string}`)
|
||||
setSubmittedBy(pubkey)
|
||||
setId(id)
|
||||
setSig(sig)
|
||||
|
||||
const { title, signers, viewers, fileHashes, markConfig, zipUrl } =
|
||||
await parseCreateSignatureEventContent(content)
|
||||
parseCreateSignatureEventContent(content)
|
||||
|
||||
setTitle(title)
|
||||
setSigners(signers)
|
||||
|
@ -8,7 +8,6 @@ import {
|
||||
Tooltip
|
||||
} from '@mui/material'
|
||||
import type { Identifier, XYCoord } from 'dnd-core'
|
||||
import saveAs from 'file-saver'
|
||||
import JSZip from 'jszip'
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { DndProvider, useDrag, useDrop } from 'react-dnd'
|
||||
@ -39,7 +38,6 @@ import {
|
||||
generateKeysFile,
|
||||
getHash,
|
||||
hexToNpub,
|
||||
isOnline,
|
||||
unixNow,
|
||||
npubToHex,
|
||||
queryNip05,
|
||||
@ -57,6 +55,7 @@ import { Mark } from '../../types/mark.ts'
|
||||
import { StickySideColumns } from '../../layouts/StickySideColumns.tsx'
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||
import {
|
||||
faDownload,
|
||||
faEllipsis,
|
||||
faEye,
|
||||
faFile,
|
||||
@ -79,6 +78,7 @@ import { NDKUserProfile, NostrEvent } from '@nostr-dev-kit/ndk'
|
||||
import { useNDKContext } from '../../hooks/useNDKContext.ts'
|
||||
import { useNDK } from '../../hooks/useNDK.ts'
|
||||
import { useImmer } from 'use-immer'
|
||||
import { ButtonUnderline } from '../../components/ButtonUnderline/index.tsx'
|
||||
|
||||
type FoundUser = NostrEvent & { npub: string }
|
||||
|
||||
@ -636,8 +636,8 @@ export const CreatePage = () => {
|
||||
width: drawnField.width
|
||||
},
|
||||
npub: drawnField.counterpart,
|
||||
pdfFileHash: fileHash,
|
||||
fileName: file.name
|
||||
fileName: file.name,
|
||||
fileHash
|
||||
}
|
||||
})
|
||||
}) || []
|
||||
@ -692,10 +692,18 @@ export const CreatePage = () => {
|
||||
type: 'application/sigit'
|
||||
})
|
||||
|
||||
const firstSigner = users.filter((user) => user.role === UserRole.signer)[0]
|
||||
|
||||
const userSet = new Set<string>()
|
||||
const nostrController = NostrController.getInstance()
|
||||
const pubkey = await nostrController.capturePublicKey()
|
||||
userSet.add(pubkey)
|
||||
signers.forEach((signer) => {
|
||||
userSet.add(signer.pubkey)
|
||||
})
|
||||
viewers.forEach((viewer) => {
|
||||
userSet.add(viewer.pubkey)
|
||||
})
|
||||
const keysFileContent = await generateKeysFile(
|
||||
[firstSigner.pubkey],
|
||||
Array.from(userSet),
|
||||
encryptionKey
|
||||
)
|
||||
if (!keysFileContent) return null
|
||||
@ -747,30 +755,6 @@ export const CreatePage = () => {
|
||||
.catch(handleUploadError)
|
||||
}
|
||||
|
||||
// Manage offline scenarios for signing or viewing the file
|
||||
const handleOfflineFlow = async (
|
||||
encryptedArrayBuffer: ArrayBuffer,
|
||||
encryptionKey: string
|
||||
) => {
|
||||
const finalZipFile = await createFinalZipFile(
|
||||
encryptedArrayBuffer,
|
||||
encryptionKey
|
||||
)
|
||||
|
||||
if (!finalZipFile) {
|
||||
setIsLoading(false)
|
||||
return
|
||||
}
|
||||
|
||||
saveAs(finalZipFile, `request-${unixNow()}.sigit.zip`)
|
||||
|
||||
// If user is the next signer, we can navigate directly to sign page
|
||||
if (signers[0].pubkey === usersPubkey) {
|
||||
navigate(appPrivateRoutes.sign, { state: { uploadedZip: finalZipFile } })
|
||||
}
|
||||
setIsLoading(false)
|
||||
}
|
||||
|
||||
const generateFilesZip = async (): Promise<ArrayBuffer | null> => {
|
||||
const zip = new JSZip()
|
||||
selectedFiles.forEach((file) => {
|
||||
@ -836,7 +820,7 @@ export const CreatePage = () => {
|
||||
return e.id
|
||||
}
|
||||
|
||||
const handleCreate = async () => {
|
||||
const initCreation = async () => {
|
||||
try {
|
||||
if (!validateInputs()) return
|
||||
|
||||
@ -848,132 +832,183 @@ export const CreatePage = () => {
|
||||
setLoadingSpinnerDesc('Generating encryption key')
|
||||
const encryptionKey = await generateEncryptionKey()
|
||||
|
||||
if (await isOnline()) {
|
||||
setLoadingSpinnerDesc('generating files.zip')
|
||||
const arrayBuffer = await generateFilesZip()
|
||||
if (!arrayBuffer) return
|
||||
setLoadingSpinnerDesc('Creating marks')
|
||||
const markConfig = createMarks(fileHashes)
|
||||
|
||||
setLoadingSpinnerDesc('Encrypting files.zip')
|
||||
const encryptedArrayBuffer = await encryptZipFile(
|
||||
arrayBuffer,
|
||||
encryptionKey
|
||||
)
|
||||
return {
|
||||
encryptionKey,
|
||||
markConfig,
|
||||
fileHashes
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
toast.error(error.message)
|
||||
}
|
||||
console.error(error)
|
||||
setIsLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const markConfig = createMarks(fileHashes)
|
||||
const handleCreate = async () => {
|
||||
try {
|
||||
const result = await initCreation()
|
||||
if (!result) return
|
||||
|
||||
setLoadingSpinnerDesc('Uploading files.zip to file storage')
|
||||
const fileUrl = await uploadFile(encryptedArrayBuffer)
|
||||
if (!fileUrl) return
|
||||
const { encryptionKey, markConfig, fileHashes } = result
|
||||
|
||||
setLoadingSpinnerDesc('Generating create signature')
|
||||
const createSignature = await generateCreateSignature(
|
||||
markConfig,
|
||||
fileHashes,
|
||||
fileUrl
|
||||
)
|
||||
if (!createSignature) return
|
||||
setLoadingSpinnerDesc('generating files.zip')
|
||||
const arrayBuffer = await generateFilesZip()
|
||||
if (!arrayBuffer) return
|
||||
|
||||
setLoadingSpinnerDesc('Generating keys for decryption')
|
||||
setLoadingSpinnerDesc('Encrypting files.zip')
|
||||
const encryptedArrayBuffer = await encryptZipFile(
|
||||
arrayBuffer,
|
||||
encryptionKey
|
||||
)
|
||||
|
||||
// generate key pairs for decryption
|
||||
const pubkeys = users.map((user) => user.pubkey)
|
||||
// also add creator in the list
|
||||
if (pubkeys.includes(usersPubkey!)) {
|
||||
pubkeys.push(usersPubkey!)
|
||||
}
|
||||
setLoadingSpinnerDesc('Uploading files.zip to file storage')
|
||||
const fileUrl = await uploadFile(encryptedArrayBuffer)
|
||||
if (!fileUrl) return
|
||||
|
||||
const keys = await generateKeys(pubkeys, encryptionKey)
|
||||
if (!keys) return
|
||||
setLoadingSpinnerDesc('Generating create signature')
|
||||
const createSignature = await generateCreateSignature(
|
||||
markConfig,
|
||||
fileHashes,
|
||||
fileUrl
|
||||
)
|
||||
if (!createSignature) return
|
||||
|
||||
setLoadingSpinnerDesc('Generating an open timestamp.')
|
||||
setLoadingSpinnerDesc('Generating keys for decryption')
|
||||
|
||||
const timestamp = await generateTimestamp(
|
||||
extractNostrId(createSignature)
|
||||
)
|
||||
// generate key pairs for decryption
|
||||
const pubkeys = users.map((user) => user.pubkey)
|
||||
// also add creator in the list
|
||||
if (pubkeys.includes(usersPubkey!)) {
|
||||
pubkeys.push(usersPubkey!)
|
||||
}
|
||||
|
||||
const meta: Meta = {
|
||||
createSignature,
|
||||
keys,
|
||||
modifiedAt: unixNow(),
|
||||
docSignatures: {}
|
||||
}
|
||||
const keys = await generateKeys(pubkeys, encryptionKey)
|
||||
if (!keys) return
|
||||
|
||||
if (timestamp) {
|
||||
meta.timestamps = [timestamp]
|
||||
}
|
||||
setLoadingSpinnerDesc('Generating an open timestamp.')
|
||||
|
||||
setLoadingSpinnerDesc('Updating user app data')
|
||||
const timestamp = await generateTimestamp(extractNostrId(createSignature))
|
||||
|
||||
const event = await updateUsersAppData([meta])
|
||||
if (!event) return
|
||||
const meta: Meta = {
|
||||
createSignature,
|
||||
keys,
|
||||
modifiedAt: unixNow(),
|
||||
docSignatures: {}
|
||||
}
|
||||
|
||||
const metaUrl = await uploadMetaToFileStorage(meta, encryptionKey)
|
||||
if (timestamp) {
|
||||
meta.timestamps = [timestamp]
|
||||
}
|
||||
|
||||
setLoadingSpinnerDesc('Sending notifications to counterparties')
|
||||
const promises = sendNotifications({
|
||||
metaUrl,
|
||||
keys: meta.keys
|
||||
setLoadingSpinnerDesc('Updating user app data')
|
||||
|
||||
const event = await updateUsersAppData([meta])
|
||||
if (!event) return
|
||||
|
||||
const metaUrl = await uploadMetaToFileStorage(meta, encryptionKey)
|
||||
|
||||
setLoadingSpinnerDesc('Sending notifications to counterparties')
|
||||
const promises = sendNotifications({
|
||||
metaUrl,
|
||||
keys: meta.keys
|
||||
})
|
||||
|
||||
await Promise.all(promises)
|
||||
.then(() => {
|
||||
toast.success('Notifications sent successfully')
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error('Failed to publish notifications')
|
||||
})
|
||||
|
||||
await Promise.all(promises)
|
||||
.then(() => {
|
||||
toast.success('Notifications sent successfully')
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error('Failed to publish notifications')
|
||||
})
|
||||
|
||||
const isFirstSigner = signers[0].pubkey === usersPubkey
|
||||
|
||||
if (isFirstSigner) {
|
||||
navigate(appPrivateRoutes.sign, { state: { meta } })
|
||||
} else {
|
||||
const createSignatureJson = JSON.parse(createSignature)
|
||||
navigate(`${appPublicRoutes.verify}/${createSignatureJson.id}`)
|
||||
}
|
||||
const isFirstSigner = signers[0].pubkey === usersPubkey
|
||||
if (isFirstSigner) {
|
||||
navigate(appPrivateRoutes.sign, { state: { meta } })
|
||||
} else {
|
||||
const zip = new JSZip()
|
||||
const createSignatureJson = JSON.parse(createSignature)
|
||||
navigate(`${appPublicRoutes.verify}/${createSignatureJson.id}`)
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
toast.error(error.message)
|
||||
}
|
||||
console.error(error)
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
selectedFiles.forEach((file) => {
|
||||
zip.file(`files/${file.name}`, file)
|
||||
const handleCreateOffline = async () => {
|
||||
try {
|
||||
const result = await initCreation()
|
||||
if (!result) return
|
||||
|
||||
const { encryptionKey, markConfig, fileHashes } = result
|
||||
|
||||
const zip = new JSZip()
|
||||
|
||||
selectedFiles.forEach((file) => {
|
||||
zip.file(`files/${file.name}`, file)
|
||||
})
|
||||
|
||||
setLoadingSpinnerDesc('Generating create signature')
|
||||
const createSignature = await generateCreateSignature(
|
||||
markConfig,
|
||||
fileHashes,
|
||||
''
|
||||
)
|
||||
if (!createSignature) return
|
||||
|
||||
const meta: Meta = {
|
||||
createSignature,
|
||||
modifiedAt: unixNow(),
|
||||
docSignatures: {}
|
||||
}
|
||||
|
||||
// add meta to zip
|
||||
try {
|
||||
const stringifiedMeta = JSON.stringify(meta, null, 2)
|
||||
zip.file('meta.json', stringifiedMeta)
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
toast.error('An error occurred in converting meta json to string')
|
||||
return null
|
||||
}
|
||||
|
||||
const arrayBuffer = await generateZipFile(zip)
|
||||
if (!arrayBuffer) return
|
||||
|
||||
setLoadingSpinnerDesc('Encrypting zip file')
|
||||
const encryptedArrayBuffer = await encryptZipFile(
|
||||
arrayBuffer,
|
||||
encryptionKey
|
||||
)
|
||||
|
||||
const finalZipFile = await createFinalZipFile(
|
||||
encryptedArrayBuffer,
|
||||
encryptionKey
|
||||
)
|
||||
|
||||
if (!finalZipFile) {
|
||||
setIsLoading(false)
|
||||
return
|
||||
}
|
||||
|
||||
// If user is the next signer, we can navigate directly to sign page
|
||||
const isFirstSigner = signers[0].pubkey === usersPubkey
|
||||
if (isFirstSigner) {
|
||||
navigate(appPrivateRoutes.sign, {
|
||||
state: { arrayBuffer }
|
||||
})
|
||||
} else {
|
||||
navigate(appPublicRoutes.verify, {
|
||||
state: { uploadedZip: arrayBuffer }
|
||||
})
|
||||
|
||||
const markConfig = createMarks(fileHashes)
|
||||
|
||||
setLoadingSpinnerDesc('Generating create signature')
|
||||
const createSignature = await generateCreateSignature(
|
||||
markConfig,
|
||||
fileHashes,
|
||||
''
|
||||
)
|
||||
if (!createSignature) return
|
||||
|
||||
const meta: Meta = {
|
||||
createSignature,
|
||||
modifiedAt: unixNow(),
|
||||
docSignatures: {}
|
||||
}
|
||||
|
||||
// add meta to zip
|
||||
try {
|
||||
const stringifiedMeta = JSON.stringify(meta, null, 2)
|
||||
zip.file('meta.json', stringifiedMeta)
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
toast.error('An error occurred in converting meta json to string')
|
||||
return null
|
||||
}
|
||||
|
||||
const arrayBuffer = await generateZipFile(zip)
|
||||
if (!arrayBuffer) return
|
||||
|
||||
setLoadingSpinnerDesc('Encrypting zip file')
|
||||
const encryptedArrayBuffer = await encryptZipFile(
|
||||
arrayBuffer,
|
||||
encryptionKey
|
||||
)
|
||||
|
||||
await handleOfflineFlow(encryptedArrayBuffer, encryptionKey)
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
@ -1231,6 +1266,11 @@ export const CreatePage = () => {
|
||||
Publish
|
||||
</Button>
|
||||
|
||||
<ButtonUnderline onClick={handleCreateOffline}>
|
||||
<FontAwesomeIcon icon={faDownload} />
|
||||
Create and export locally
|
||||
</ButtonUnderline>
|
||||
|
||||
{!!error && (
|
||||
<FormHelperText error={!!error}>{error}</FormHelperText>
|
||||
)}
|
||||
|
@ -1,10 +1,9 @@
|
||||
import { Button, TextField } from '@mui/material'
|
||||
import JSZip from 'jszip'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { useNavigate, useSearchParams } from 'react-router-dom'
|
||||
import { toast } from 'react-toastify'
|
||||
import { useAppSelector } from '../../hooks'
|
||||
import { appPrivateRoutes, appPublicRoutes } from '../../routes'
|
||||
import { appPrivateRoutes } from '../../routes'
|
||||
import { Meta } from '../../types'
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||
import { faSearch } from '@fortawesome/free-solid-svg-icons'
|
||||
@ -15,6 +14,7 @@ import { Container } from '../../components/Container'
|
||||
import styles from './style.module.scss'
|
||||
import {
|
||||
extractSigitCardDisplayInfo,
|
||||
navigateFromZip,
|
||||
SigitCardDisplayInfo,
|
||||
SigitStatus
|
||||
} from '../../utils'
|
||||
@ -56,6 +56,7 @@ export const HomePage = () => {
|
||||
[key: string]: SigitCardDisplayInfo
|
||||
}>({})
|
||||
const usersAppData = useAppSelector((state) => state.userAppData)
|
||||
const usersPubkey = useAppSelector((state) => state.auth.usersPubkey)
|
||||
|
||||
useEffect(() => {
|
||||
if (usersAppData?.sigits) {
|
||||
@ -63,7 +64,7 @@ export const HomePage = () => {
|
||||
const parsedSigits: { [key: string]: SigitCardDisplayInfo } = {}
|
||||
for (const key in usersAppData.sigits) {
|
||||
if (Object.prototype.hasOwnProperty.call(usersAppData.sigits, key)) {
|
||||
const sigitInfo = await extractSigitCardDisplayInfo(
|
||||
const sigitInfo = extractSigitCardDisplayInfo(
|
||||
usersAppData.sigits[key]
|
||||
)
|
||||
if (sigitInfo) {
|
||||
@ -92,27 +93,12 @@ export const HomePage = () => {
|
||||
const fileName = file.name
|
||||
const fileExtension = fileName.slice(-10) // ".sigit.zip" has 10 characters
|
||||
if (fileExtension === '.sigit.zip') {
|
||||
const zip = await JSZip.loadAsync(file).catch((err) => {
|
||||
console.log('err in loading zip file :>> ', err)
|
||||
toast.error(err.message || 'An error occurred in loading zip file.')
|
||||
return null
|
||||
})
|
||||
const nav = await navigateFromZip(
|
||||
file,
|
||||
usersPubkey as `npub1${string}`
|
||||
)
|
||||
|
||||
if (!zip) return
|
||||
|
||||
// navigate to sign page if zip contains keys.json
|
||||
if ('keys.json' in zip.files) {
|
||||
return navigate(appPrivateRoutes.sign, {
|
||||
state: { uploadedZip: file }
|
||||
})
|
||||
}
|
||||
|
||||
// navigate to verify page if zip contains meta.json
|
||||
if ('meta.json' in zip.files) {
|
||||
return navigate(appPublicRoutes.verify, {
|
||||
state: { uploadedZip: file }
|
||||
})
|
||||
}
|
||||
if (nav) return navigate(nav.to, nav.options)
|
||||
|
||||
toast.error('Invalid SiGit zip file')
|
||||
return
|
||||
@ -124,7 +110,7 @@ export const HomePage = () => {
|
||||
state: { uploadedFiles: acceptedFiles }
|
||||
})
|
||||
},
|
||||
[navigate]
|
||||
[navigate, usersPubkey]
|
||||
)
|
||||
|
||||
const { getRootProps, getInputProps, isDragActive, open } = useDropzone({
|
||||
|
@ -54,7 +54,10 @@ export const RelaysPage = () => {
|
||||
const relayMap = useAppSelector((state) => state.relays?.map)
|
||||
const relaysInfo = useAppSelector((state) => state.relays?.info)
|
||||
|
||||
const webSocketPrefix = 'wss://'
|
||||
const webSocketPrefix =
|
||||
newRelayURI?.startsWith('wss://') || newRelayURI?.startsWith('ws://')
|
||||
? ''
|
||||
: 'wss://'
|
||||
|
||||
// fetch relay list from relays
|
||||
useEffect(() => {
|
||||
@ -197,7 +200,7 @@ export const RelaysPage = () => {
|
||||
// Check if new relay URI is a valid string
|
||||
if (
|
||||
relayURI &&
|
||||
!/^wss:\/\/[-a-zA-Z0-9@:%._\\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}/.test(
|
||||
!/^wss?:\/\/[-a-zA-Z0-9@:%._\\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}/.test(
|
||||
relayURI
|
||||
)
|
||||
) {
|
||||
@ -260,7 +263,13 @@ export const RelaysPage = () => {
|
||||
}}
|
||||
className={styles.relayURItextfield}
|
||||
/>
|
||||
<Button variant="contained" onClick={() => handleAddNewRelay()}>
|
||||
<Button
|
||||
sx={{
|
||||
height: '56px'
|
||||
}}
|
||||
variant="contained"
|
||||
onClick={() => handleAddNewRelay()}
|
||||
>
|
||||
Add
|
||||
</Button>
|
||||
</Box>
|
||||
|
@ -12,6 +12,7 @@
|
||||
flex-direction: row;
|
||||
gap: 10px;
|
||||
width: 100%;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.sectionIcon {
|
||||
|
@ -1,54 +1,41 @@
|
||||
import axios from 'axios'
|
||||
import saveAs from 'file-saver'
|
||||
import JSZip from 'jszip'
|
||||
import _ from 'lodash'
|
||||
import { Event, verifyEvent } from 'nostr-tools'
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import { useAppSelector } from '../../hooks'
|
||||
import { useLocation, useNavigate, useParams } from 'react-router-dom'
|
||||
import { toast } from 'react-toastify'
|
||||
import { LoadingSpinner } from '../../components/LoadingSpinner'
|
||||
import { NostrController } from '../../controllers'
|
||||
import { appPrivateRoutes, appPublicRoutes } from '../../routes'
|
||||
import { appPublicRoutes } from '../../routes'
|
||||
import { CreateSignatureEventContent, Meta, SignedEvent } from '../../types'
|
||||
import {
|
||||
ARRAY_BUFFER,
|
||||
decryptArrayBuffer,
|
||||
DEFLATE,
|
||||
encryptArrayBuffer,
|
||||
extractMarksFromSignedMeta,
|
||||
extractZipUrlAndEncryptionKey,
|
||||
filterMarksByPubkey,
|
||||
findOtherUserMarks,
|
||||
generateEncryptionKey,
|
||||
generateKeysFile,
|
||||
getCurrentUserFiles,
|
||||
getCurrentUserMarks,
|
||||
getHash,
|
||||
hexToNpub,
|
||||
isOnline,
|
||||
loadZip,
|
||||
npubToHex,
|
||||
parseJson,
|
||||
processMarks,
|
||||
encryptAndUploadMarks,
|
||||
readContentOfZipEntry,
|
||||
signEventForMetaFile,
|
||||
timeout,
|
||||
unixNow,
|
||||
updateMarks,
|
||||
uploadMetaToFileStorage
|
||||
} from '../../utils'
|
||||
import { CurrentUserMark, Mark } from '../../types/mark.ts'
|
||||
import PdfMarking from '../../components/PDFView/PdfMarking.tsx'
|
||||
import {
|
||||
convertToSigitFile,
|
||||
getZipWithFiles,
|
||||
SigitFile
|
||||
} from '../../utils/file.ts'
|
||||
import { convertToSigitFile, SigitFile } from '../../utils/file.ts'
|
||||
import { generateTimestamp } from '../../utils/opentimestamps.ts'
|
||||
import { MARK_TYPE_CONFIG } from '../../components/MarkTypeStrategy/MarkStrategy.tsx'
|
||||
import { useNDK } from '../../hooks/useNDK.ts'
|
||||
import { getLastSignersSig } from '../../utils/sign.ts'
|
||||
|
||||
export const SignPage = () => {
|
||||
const navigate = useNavigate()
|
||||
@ -81,12 +68,10 @@ export const SignPage = () => {
|
||||
/**
|
||||
* Received from `location.state`
|
||||
*
|
||||
* uploadedZip will be received from home page when a user uploads a sigit zip wrapper that contains keys.json
|
||||
* arrayBuffer (decryptedArrayBuffer) will be received in navigation from create page in offline mode
|
||||
*/
|
||||
const { arrayBuffer: decryptedArrayBuffer, uploadedZip } = location.state || {
|
||||
decryptedArrayBuffer: undefined,
|
||||
uploadedZip: undefined
|
||||
decryptedArrayBuffer: undefined
|
||||
}
|
||||
|
||||
const [files, setFiles] = useState<{ [filename: string]: SigitFile }>({})
|
||||
@ -218,63 +203,6 @@ export const SignPage = () => {
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [meta, usersPubkey])
|
||||
|
||||
const decrypt = useCallback(
|
||||
async (file: File) => {
|
||||
setLoadingSpinnerDesc('Decrypting file')
|
||||
|
||||
const zip = await loadZip(file)
|
||||
if (!zip) return
|
||||
|
||||
const parsedKeysJson = await parseKeysJson(zip)
|
||||
if (!parsedKeysJson) return
|
||||
|
||||
const encryptedArrayBuffer = await readContentOfZipEntry(
|
||||
zip,
|
||||
'compressed.sigit',
|
||||
'arraybuffer'
|
||||
)
|
||||
|
||||
if (!encryptedArrayBuffer) return
|
||||
|
||||
const { keys, sender } = parsedKeysJson
|
||||
|
||||
for (const key of keys) {
|
||||
// decrypt the encryptionKey, with timeout (duration = 60 seconds)
|
||||
const encryptionKey = await Promise.race([
|
||||
nostrController.nip04Decrypt(sender, key),
|
||||
timeout(60000)
|
||||
])
|
||||
.then((res) => {
|
||||
return res
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log('err :>> ', err)
|
||||
return null
|
||||
})
|
||||
|
||||
// Return if encryption failed
|
||||
if (!encryptionKey) continue
|
||||
|
||||
const arrayBuffer = await decryptArrayBuffer(
|
||||
encryptedArrayBuffer,
|
||||
encryptionKey
|
||||
)
|
||||
.catch((err) => {
|
||||
console.log('err in decryption:>> ', err)
|
||||
return null
|
||||
})
|
||||
.finally(() => {
|
||||
setIsLoading(false)
|
||||
})
|
||||
|
||||
if (arrayBuffer) return arrayBuffer
|
||||
}
|
||||
|
||||
return null
|
||||
},
|
||||
[nostrController]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (metaInNavState) {
|
||||
const processSigit = async () => {
|
||||
@ -316,28 +244,14 @@ export const SignPage = () => {
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
// online mode - from create and home page views
|
||||
|
||||
if (decryptedArrayBuffer) {
|
||||
handleDecryptedArrayBuffer(decryptedArrayBuffer).finally(() =>
|
||||
setIsLoading(false)
|
||||
if (decryptedArrayBuffer || uploadedZip) {
|
||||
handleDecryptedArrayBuffer(decryptedArrayBuffer || uploadedZip).finally(
|
||||
() => setIsLoading(false)
|
||||
)
|
||||
} else if (uploadedZip) {
|
||||
decrypt(uploadedZip)
|
||||
.then((arrayBuffer) => {
|
||||
if (arrayBuffer) handleDecryptedArrayBuffer(arrayBuffer)
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(`error occurred in decryption`, err)
|
||||
toast.error(err.message || `error occurred in decryption`)
|
||||
})
|
||||
.finally(() => {
|
||||
setIsLoading(false)
|
||||
})
|
||||
} else {
|
||||
setIsLoading(false)
|
||||
}
|
||||
}, [decryptedArrayBuffer, uploadedZip, decrypt])
|
||||
}, [decryptedArrayBuffer, uploadedZip])
|
||||
|
||||
const handleArrayBufferFromBlossom = async (
|
||||
arrayBuffer: ArrayBuffer,
|
||||
@ -396,30 +310,12 @@ export const SignPage = () => {
|
||||
setMarks(updatedMarks)
|
||||
}
|
||||
|
||||
const parseKeysJson = async (zip: JSZip) => {
|
||||
const keysFileContent = await readContentOfZipEntry(
|
||||
zip,
|
||||
'keys.json',
|
||||
'string'
|
||||
)
|
||||
|
||||
if (!keysFileContent) return null
|
||||
|
||||
return await parseJson<{ sender: string; keys: string[] }>(
|
||||
keysFileContent
|
||||
).catch((err) => {
|
||||
console.log(`Error parsing content of keys.json:`, err)
|
||||
toast.error(err.message || `Error parsing content of keys.json`)
|
||||
return null
|
||||
})
|
||||
}
|
||||
|
||||
const handleDecryptedArrayBuffer = async (arrayBuffer: ArrayBuffer) => {
|
||||
const decryptedZipFile = new File([arrayBuffer], 'decrypted.zip')
|
||||
|
||||
const handleDecryptedArrayBuffer = async (
|
||||
decryptedArrayBuffer: ArrayBuffer
|
||||
) => {
|
||||
setLoadingSpinnerDesc('Parsing zip file')
|
||||
|
||||
const zip = await loadZip(decryptedZipFile)
|
||||
const zip = await loadZip(decryptedArrayBuffer)
|
||||
if (!zip) return
|
||||
|
||||
const files: { [filename: string]: SigitFile } = {}
|
||||
@ -479,16 +375,15 @@ export const SignPage = () => {
|
||||
setMeta(parsedMetaJson)
|
||||
}
|
||||
|
||||
const handleSign = async () => {
|
||||
const initializeSigning = async (type: 'online' | 'offline') => {
|
||||
if (Object.entries(files).length === 0 || !meta) return
|
||||
|
||||
setIsLoading(true)
|
||||
|
||||
setLoadingSpinnerDesc('Signing nostr event')
|
||||
|
||||
const usersNpub = hexToNpub(usersPubkey!)
|
||||
const prevSig = getPrevSignersSig(usersNpub)
|
||||
if (!prevSig) {
|
||||
setIsLoading(false)
|
||||
toast.error('Previous signature is invalid')
|
||||
return
|
||||
}
|
||||
@ -508,7 +403,10 @@ export const SignPage = () => {
|
||||
})
|
||||
}
|
||||
|
||||
const processedMarks = await processMarks(marks, encryptionKey)
|
||||
const processedMarks =
|
||||
type === 'online'
|
||||
? await encryptAndUploadMarks(marks, encryptionKey)
|
||||
: marks
|
||||
|
||||
const signedEvent = await signEventForMeta({
|
||||
prevSig,
|
||||
@ -519,6 +417,22 @@ export const SignPage = () => {
|
||||
|
||||
const updatedMeta = updateMetaSignatures(meta, signedEvent)
|
||||
|
||||
return {
|
||||
encryptionKey,
|
||||
updatedMeta,
|
||||
signedEvent
|
||||
}
|
||||
}
|
||||
|
||||
const handleSign = async () => {
|
||||
const result = await initializeSigning('online')
|
||||
if (!result) {
|
||||
setIsLoading(false)
|
||||
return
|
||||
}
|
||||
|
||||
const { encryptionKey, updatedMeta, signedEvent } = result
|
||||
|
||||
setLoadingSpinnerDesc('Generating an open timestamp.')
|
||||
|
||||
const timestamp = await generateTimestamp(signedEvent.id)
|
||||
@ -527,19 +441,62 @@ export const SignPage = () => {
|
||||
updatedMeta.modifiedAt = unixNow()
|
||||
}
|
||||
|
||||
if (await isOnline()) {
|
||||
await handleOnlineFlow(updatedMeta, encryptionKey)
|
||||
} else {
|
||||
setMeta(updatedMeta)
|
||||
await handleOnlineFlow(updatedMeta, encryptionKey)
|
||||
|
||||
const createSignature = JSON.parse(updatedMeta.createSignature)
|
||||
navigate(`${appPublicRoutes.verify}/${createSignature.id}`)
|
||||
}
|
||||
|
||||
const handleSignOffline = async () => {
|
||||
const result = await initializeSigning('offline')
|
||||
if (!result) {
|
||||
setIsLoading(false)
|
||||
return
|
||||
}
|
||||
|
||||
if (metaInNavState) {
|
||||
const createSignature = JSON.parse(metaInNavState.createSignature)
|
||||
navigate(`${appPublicRoutes.verify}/${createSignature.id}`)
|
||||
} else {
|
||||
navigate(appPrivateRoutes.homePage)
|
||||
const { updatedMeta } = result
|
||||
|
||||
const zip = new JSZip()
|
||||
for (const [filename, value] of Object.entries(files)) {
|
||||
zip.file(`files/${filename}`, await value.arrayBuffer())
|
||||
}
|
||||
const stringifiedMeta = JSON.stringify(updatedMeta, null, 2)
|
||||
zip.file('meta.json', stringifiedMeta)
|
||||
|
||||
// Handle errors during zip file generation
|
||||
const handleZipError = (err: unknown) => {
|
||||
console.log('Error in zip:>> ', err)
|
||||
setIsLoading(false)
|
||||
if (err instanceof Error) {
|
||||
toast.error(err.message || 'Error occurred in generating zip file')
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
setLoadingSpinnerDesc('Generating zip file')
|
||||
|
||||
const arrayBuffer = await zip
|
||||
.generateAsync({
|
||||
type: 'arraybuffer',
|
||||
compression: 'DEFLATE',
|
||||
compressionOptions: { level: 6 }
|
||||
})
|
||||
.catch(handleZipError)
|
||||
|
||||
if (!arrayBuffer) {
|
||||
setIsLoading(false)
|
||||
return
|
||||
}
|
||||
|
||||
// Create a File object with the Blob data
|
||||
const blob = new Blob([arrayBuffer])
|
||||
const file = new File([blob], `request-${unixNow()}.sigit.zip`, {
|
||||
type: 'application/zip'
|
||||
})
|
||||
|
||||
setIsLoading(false)
|
||||
|
||||
navigate(`${appPublicRoutes.verify}`, { state: { uploadedZip: file } })
|
||||
}
|
||||
|
||||
// Sign the event for the meta file
|
||||
@ -570,66 +527,6 @@ export const SignPage = () => {
|
||||
return metaCopy
|
||||
}
|
||||
|
||||
// create final zip file
|
||||
const createFinalZipFile = async (
|
||||
encryptedArrayBuffer: ArrayBuffer,
|
||||
encryptionKey: string
|
||||
): Promise<File | null> => {
|
||||
// Get the current timestamp in seconds
|
||||
const blob = new Blob([encryptedArrayBuffer])
|
||||
// Create a File object with the Blob data
|
||||
const file = new File([blob], `compressed.sigit`, {
|
||||
type: 'application/sigit'
|
||||
})
|
||||
|
||||
const isLastSigner = checkIsLastSigner(signers)
|
||||
|
||||
const userSet = new Set<string>()
|
||||
|
||||
if (isLastSigner) {
|
||||
if (submittedBy) {
|
||||
userSet.add(submittedBy)
|
||||
}
|
||||
|
||||
signers.forEach((signer) => {
|
||||
userSet.add(npubToHex(signer)!)
|
||||
})
|
||||
|
||||
viewers.forEach((viewer) => {
|
||||
userSet.add(npubToHex(viewer)!)
|
||||
})
|
||||
} else {
|
||||
const usersNpub = hexToNpub(usersPubkey!)
|
||||
const signerIndex = signers.indexOf(usersNpub)
|
||||
const nextSigner = signers[signerIndex + 1]
|
||||
userSet.add(npubToHex(nextSigner)!)
|
||||
}
|
||||
|
||||
const keysFileContent = await generateKeysFile(
|
||||
Array.from(userSet),
|
||||
encryptionKey
|
||||
)
|
||||
if (!keysFileContent) return null
|
||||
|
||||
const zip = new JSZip()
|
||||
zip.file(`compressed.sigit`, file)
|
||||
zip.file('keys.json', keysFileContent)
|
||||
|
||||
const arraybuffer = await zip
|
||||
.generateAsync({
|
||||
type: 'arraybuffer',
|
||||
compression: 'DEFLATE',
|
||||
compressionOptions: { level: 6 }
|
||||
})
|
||||
.catch(handleZipError)
|
||||
|
||||
if (!arraybuffer) return null
|
||||
|
||||
return new File([new Blob([arraybuffer])], `${unixNow()}.sigit.zip`, {
|
||||
type: 'application/zip'
|
||||
})
|
||||
}
|
||||
|
||||
// Check if the current user is the last signer
|
||||
const checkIsLastSigner = (signers: string[]): boolean => {
|
||||
const usersNpub = hexToNpub(usersPubkey!)
|
||||
@ -638,16 +535,6 @@ export const SignPage = () => {
|
||||
return signerIndex === lastSignerIndex
|
||||
}
|
||||
|
||||
// Handle errors during zip file generation
|
||||
const handleZipError = (err: unknown) => {
|
||||
console.log('Error in zip:>> ', err)
|
||||
setIsLoading(false)
|
||||
if (err instanceof Error) {
|
||||
toast.error(err.message || 'Error occurred in generating zip file')
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
// Handle the online flow: update users app data and send notifications
|
||||
const handleOnlineFlow = async (
|
||||
meta: Meta,
|
||||
@ -718,99 +605,6 @@ export const SignPage = () => {
|
||||
setIsLoading(false)
|
||||
}
|
||||
|
||||
const handleExport = async () => {
|
||||
const arrayBuffer = await prepareZipExport()
|
||||
if (!arrayBuffer) return
|
||||
|
||||
const blob = new Blob([arrayBuffer])
|
||||
saveAs(blob, `exported-${unixNow()}.sigit.zip`)
|
||||
|
||||
setIsLoading(false)
|
||||
|
||||
navigate(appPublicRoutes.verify)
|
||||
}
|
||||
|
||||
const handleEncryptedExport = async () => {
|
||||
const arrayBuffer = await prepareZipExport()
|
||||
if (!arrayBuffer) return
|
||||
|
||||
const key = await generateEncryptionKey()
|
||||
|
||||
setLoadingSpinnerDesc('Encrypting zip file')
|
||||
const encryptedArrayBuffer = await encryptArrayBuffer(arrayBuffer, key)
|
||||
|
||||
const finalZipFile = await createFinalZipFile(encryptedArrayBuffer, key)
|
||||
|
||||
if (!finalZipFile) return
|
||||
saveAs(finalZipFile, `exported-${unixNow()}.sigit.zip`)
|
||||
|
||||
setIsLoading(false)
|
||||
}
|
||||
|
||||
const prepareZipExport = async (): Promise<ArrayBuffer | null> => {
|
||||
if (Object.entries(files).length === 0 || !meta || !usersPubkey)
|
||||
return Promise.resolve(null)
|
||||
|
||||
const usersNpub = hexToNpub(usersPubkey)
|
||||
if (
|
||||
!signers.includes(usersNpub) &&
|
||||
!viewers.includes(usersNpub) &&
|
||||
submittedBy !== usersNpub
|
||||
)
|
||||
return Promise.resolve(null)
|
||||
|
||||
setIsLoading(true)
|
||||
setLoadingSpinnerDesc('Signing nostr event')
|
||||
|
||||
if (!meta) return Promise.resolve(null)
|
||||
|
||||
const prevSig = getLastSignersSig(meta, signers)
|
||||
if (!prevSig) return Promise.resolve(null)
|
||||
|
||||
const signedEvent = await signEventForMetaFile(
|
||||
JSON.stringify({
|
||||
prevSig
|
||||
}),
|
||||
nostrController,
|
||||
setIsLoading
|
||||
)
|
||||
|
||||
if (!signedEvent) return Promise.resolve(null)
|
||||
|
||||
const exportSignature = JSON.stringify(signedEvent, null, 2)
|
||||
|
||||
const stringifiedMeta = JSON.stringify(
|
||||
{
|
||||
...meta,
|
||||
exportSignature
|
||||
},
|
||||
null,
|
||||
2
|
||||
)
|
||||
|
||||
const zip = await getZipWithFiles(meta, files)
|
||||
zip.file('meta.json', stringifiedMeta)
|
||||
|
||||
const arrayBuffer = await zip
|
||||
.generateAsync({
|
||||
type: ARRAY_BUFFER,
|
||||
compression: DEFLATE,
|
||||
compressionOptions: {
|
||||
level: 6
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log('err in zip:>> ', err)
|
||||
setIsLoading(false)
|
||||
toast.error(err.message || 'Error occurred in generating zip file')
|
||||
return null
|
||||
})
|
||||
|
||||
if (!arrayBuffer) return Promise.resolve(null)
|
||||
|
||||
return Promise.resolve(arrayBuffer)
|
||||
}
|
||||
|
||||
/**
|
||||
* This function accepts an npub of a signer and return the signature of its previous signer.
|
||||
* This prevSig will be used in the content of the provided signer's signedEvent
|
||||
@ -855,8 +649,7 @@ export const SignPage = () => {
|
||||
setCurrentUserMarks={setCurrentUserMarks}
|
||||
setUpdatedMarks={setUpdatedMarks}
|
||||
handleSign={handleSign}
|
||||
handleExport={handleExport}
|
||||
handleEncryptedExport={handleEncryptedExport}
|
||||
handleSignOffline={handleSignOffline}
|
||||
otherUserMarks={otherUserMarks}
|
||||
meta={meta}
|
||||
/>
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { Box, Button, Typography } from '@mui/material'
|
||||
import JSZip from 'jszip'
|
||||
import { MuiFileInput } from 'mui-file-input'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { toast } from 'react-toastify'
|
||||
import { LoadingSpinner } from '../../components/LoadingSpinner'
|
||||
import { NostrController } from '../../controllers'
|
||||
@ -27,14 +27,14 @@ import {
|
||||
generateKeysFile,
|
||||
ARRAY_BUFFER,
|
||||
DEFLATE,
|
||||
uploadMetaToFileStorage
|
||||
uploadMetaToFileStorage,
|
||||
decrypt
|
||||
} from '../../utils'
|
||||
import styles from './style.module.scss'
|
||||
import { useLocation, useParams } from 'react-router-dom'
|
||||
import axios from 'axios'
|
||||
import { FONT_SIZE, FONT_TYPE, inPx } from '../../utils/pdf.ts'
|
||||
import { useAppSelector, useNDK } from '../../hooks'
|
||||
import { getLastSignersSig } from '../../utils/sign.ts'
|
||||
import { saveAs } from 'file-saver'
|
||||
import { Container } from '../../components/Container'
|
||||
import { useSigitMeta } from '../../hooks/useSigitMeta.tsx'
|
||||
@ -60,6 +60,7 @@ import {
|
||||
import { upgradeAndVerifyTimestamp } from '../../utils/opentimestamps.ts'
|
||||
import _ from 'lodash'
|
||||
import { MarkRender } from '../../components/MarkTypeStrategy/MarkRender.tsx'
|
||||
import { SignerService } from '../../services/index.ts'
|
||||
|
||||
interface PdfViewProps {
|
||||
files: CurrentUserFile[]
|
||||
@ -105,7 +106,10 @@ const SlimPdfView = ({
|
||||
const m = parsedSignatureEvents[
|
||||
e as `npub1${string}`
|
||||
].parsedContent?.marks.filter(
|
||||
(m) => m.pdfFileHash == hash && m.location.page == i
|
||||
(m) =>
|
||||
(m.pdfFileHash
|
||||
? m.pdfFileHash == hash
|
||||
: m.fileHash == hash) && m.location.page == i
|
||||
)
|
||||
if (m) {
|
||||
marks.push(...m)
|
||||
@ -185,7 +189,7 @@ export const VerifyPage = () => {
|
||||
* meta will be received in navigation from create & home page in online mode
|
||||
*/
|
||||
let metaInNavState = location?.state?.meta || undefined
|
||||
const { uploadedZip } = location.state || {}
|
||||
const uploadedZip = location?.state?.uploadedZip || undefined
|
||||
const [selectedFile, setSelectedFile] = useState<File | null>(null)
|
||||
|
||||
/**
|
||||
@ -205,12 +209,6 @@ export const VerifyPage = () => {
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (uploadedZip) {
|
||||
setSelectedFile(uploadedZip)
|
||||
}
|
||||
}, [uploadedZip])
|
||||
|
||||
const [meta, setMeta] = useState<Meta>(metaInNavState)
|
||||
|
||||
const {
|
||||
@ -480,17 +478,35 @@ export const VerifyPage = () => {
|
||||
}
|
||||
}, [encryptionKey, metaInNavState, zipUrl])
|
||||
|
||||
const handleVerify = async () => {
|
||||
if (!selectedFile) return
|
||||
const handleVerify = useCallback(async (selectedFile: File) => {
|
||||
setIsLoading(true)
|
||||
setLoadingSpinnerDesc('Loading zip file')
|
||||
|
||||
const zip = await JSZip.loadAsync(selectedFile).catch((err) => {
|
||||
let zip = await JSZip.loadAsync(selectedFile).catch((err) => {
|
||||
console.log('err in loading zip file :>> ', err)
|
||||
toast.error(err.message || 'An error occurred in loading zip file.')
|
||||
return null
|
||||
})
|
||||
|
||||
if (!zip) return
|
||||
if (!zip) {
|
||||
return setIsLoading(false)
|
||||
}
|
||||
|
||||
if ('keys.json' in zip.files) {
|
||||
// Decrypt
|
||||
setLoadingSpinnerDesc('Decrypting zip file content')
|
||||
const arrayBuffer = await decrypt(selectedFile).catch((err) => {
|
||||
console.error(`error occurred in decryption`, err)
|
||||
toast.error(err.message || `error occurred in decryption`)
|
||||
})
|
||||
|
||||
if (arrayBuffer) {
|
||||
// Replace the zip and continue processing
|
||||
zip = await JSZip.loadAsync(arrayBuffer)
|
||||
}
|
||||
}
|
||||
|
||||
setLoadingSpinnerDesc('Opening zip file content')
|
||||
|
||||
const files: { [filename: string]: SigitFile } = {}
|
||||
const fileHashes: { [key: string]: string | null } = {}
|
||||
@ -547,12 +563,21 @@ export const VerifyPage = () => {
|
||||
}
|
||||
)
|
||||
|
||||
if (!parsedMetaJson) return
|
||||
if (!parsedMetaJson) {
|
||||
setIsLoading(false)
|
||||
return
|
||||
}
|
||||
|
||||
setMeta(parsedMetaJson)
|
||||
|
||||
setIsLoading(false)
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (uploadedZip) {
|
||||
handleVerify(uploadedZip)
|
||||
}
|
||||
}, [handleVerify, uploadedZip])
|
||||
|
||||
// Handle errors during zip file generation
|
||||
const handleZipError = (err: unknown) => {
|
||||
@ -564,14 +589,6 @@ export const VerifyPage = () => {
|
||||
return null
|
||||
}
|
||||
|
||||
// Check if the current user is the last signer
|
||||
const checkIsLastSigner = (signers: string[]): boolean => {
|
||||
const usersNpub = hexToNpub(usersPubkey!)
|
||||
const lastSignerIndex = signers.length - 1
|
||||
const signerIndex = signers.indexOf(usersNpub)
|
||||
return signerIndex === lastSignerIndex
|
||||
}
|
||||
|
||||
// create final zip file
|
||||
const createFinalZipFile = async (
|
||||
encryptedArrayBuffer: ArrayBuffer,
|
||||
@ -584,28 +601,16 @@ export const VerifyPage = () => {
|
||||
type: 'application/sigit'
|
||||
})
|
||||
|
||||
const isLastSigner = checkIsLastSigner(signers)
|
||||
|
||||
const userSet = new Set<string>()
|
||||
|
||||
if (isLastSigner) {
|
||||
if (submittedBy) {
|
||||
userSet.add(submittedBy)
|
||||
}
|
||||
|
||||
signers.forEach((signer) => {
|
||||
userSet.add(npubToHex(signer)!)
|
||||
})
|
||||
|
||||
viewers.forEach((viewer) => {
|
||||
userSet.add(npubToHex(viewer)!)
|
||||
})
|
||||
} else {
|
||||
const usersNpub = hexToNpub(usersPubkey!)
|
||||
const signerIndex = signers.indexOf(usersNpub)
|
||||
const nextSigner = signers[signerIndex + 1]
|
||||
userSet.add(npubToHex(nextSigner)!)
|
||||
if (submittedBy) {
|
||||
userSet.add(submittedBy)
|
||||
}
|
||||
signers.forEach((signer) => {
|
||||
userSet.add(npubToHex(signer)!)
|
||||
})
|
||||
viewers.forEach((viewer) => {
|
||||
userSet.add(npubToHex(viewer)!)
|
||||
})
|
||||
|
||||
const keysFileContent = await generateKeysFile(
|
||||
Array.from(userSet),
|
||||
@ -634,7 +639,10 @@ export const VerifyPage = () => {
|
||||
|
||||
const handleExport = async () => {
|
||||
const arrayBuffer = await prepareZipExport()
|
||||
if (!arrayBuffer) return
|
||||
if (!arrayBuffer) {
|
||||
setIsLoading(false)
|
||||
return
|
||||
}
|
||||
|
||||
const blob = new Blob([arrayBuffer])
|
||||
saveAs(blob, `exported-${unixNow()}.sigit.zip`)
|
||||
@ -644,7 +652,10 @@ export const VerifyPage = () => {
|
||||
|
||||
const handleEncryptedExport = async () => {
|
||||
const arrayBuffer = await prepareZipExport()
|
||||
if (!arrayBuffer) return
|
||||
if (!arrayBuffer) {
|
||||
setIsLoading(false)
|
||||
return
|
||||
}
|
||||
|
||||
const key = await generateEncryptionKey()
|
||||
|
||||
@ -653,7 +664,11 @@ export const VerifyPage = () => {
|
||||
|
||||
const finalZipFile = await createFinalZipFile(encryptedArrayBuffer, key)
|
||||
|
||||
if (!finalZipFile) return
|
||||
if (!finalZipFile) {
|
||||
setIsLoading(false)
|
||||
return
|
||||
}
|
||||
|
||||
saveAs(finalZipFile, `exported-${unixNow()}.sigit.zip`)
|
||||
|
||||
setIsLoading(false)
|
||||
@ -675,7 +690,11 @@ export const VerifyPage = () => {
|
||||
setIsLoading(true)
|
||||
setLoadingSpinnerDesc('Signing nostr event')
|
||||
|
||||
const prevSig = getLastSignersSig(meta, signers)
|
||||
if (!meta) return Promise.resolve(null)
|
||||
|
||||
const signerService = new SignerService(meta)
|
||||
const prevSig = signerService.getLastSignerSig()
|
||||
|
||||
if (!prevSig) return Promise.resolve(null)
|
||||
|
||||
const signedEvent = await signEventForMetaFile(
|
||||
@ -736,7 +755,10 @@ export const VerifyPage = () => {
|
||||
|
||||
{selectedFile && (
|
||||
<Box sx={{ mt: 2, display: 'flex', justifyContent: 'center' }}>
|
||||
<Button onClick={handleVerify} variant="contained">
|
||||
<Button
|
||||
onClick={() => handleVerify(selectedFile)}
|
||||
variant="contained"
|
||||
>
|
||||
Verify
|
||||
</Button>
|
||||
</Box>
|
||||
|
@ -1 +1,2 @@
|
||||
export * from './cache'
|
||||
export * from './signer'
|
||||
|
143
src/services/signer/index.ts
Normal file
143
src/services/signer/index.ts
Normal file
@ -0,0 +1,143 @@
|
||||
import { toast } from 'react-toastify'
|
||||
import { Meta, SignedEventContent } from '../../types'
|
||||
import {
|
||||
parseCreateSignatureEventContent,
|
||||
parseNostrEvent,
|
||||
SigitStatus,
|
||||
SignStatus
|
||||
} from '../../utils'
|
||||
import { MetaParseError } from '../../types/errors/MetaParseError'
|
||||
import { verifyEvent } from 'nostr-tools'
|
||||
import { appPrivateRoutes, appPublicRoutes } from '../../routes'
|
||||
|
||||
export class SignerService {
|
||||
#signers: `npub1${string}`[] = []
|
||||
#nextSigner: `npub1${string}` | undefined
|
||||
#signatures = new Map<`npub1${string}`, string>()
|
||||
#signersStatus = new Map<`npub1${string}`, SignStatus>()
|
||||
#lastSignerSig: string | undefined
|
||||
constructor(source: Meta) {
|
||||
this.#process(source.createSignature, source.docSignatures)
|
||||
}
|
||||
|
||||
getNextSigner = () => {
|
||||
return this.#nextSigner
|
||||
}
|
||||
|
||||
isNextSigner = (npub: `npub1${string}`) => {
|
||||
return this.#nextSigner === npub
|
||||
}
|
||||
|
||||
isLastSigner = (npub: `npub1${string}`) => {
|
||||
const lastIndex = this.#signers.length - 1
|
||||
const npubIndex = this.#signers.indexOf(npub)
|
||||
return npubIndex === lastIndex
|
||||
}
|
||||
|
||||
#isFullySigned = () => {
|
||||
const signedBy = Object.keys(this.#signatures) as `npub1${string}`[]
|
||||
const isCompletelySigned = this.#signers.every((signer) =>
|
||||
signedBy.includes(signer)
|
||||
)
|
||||
return isCompletelySigned
|
||||
}
|
||||
|
||||
getSignedStatus = () => {
|
||||
return this.#isFullySigned() ? SigitStatus.Complete : SigitStatus.Partial
|
||||
}
|
||||
|
||||
getSignerStatus = (npub: `npub1${string}`) => {
|
||||
return this.#signersStatus.get(npub)
|
||||
}
|
||||
|
||||
getNavigate = (npub: `npub1${string}`) => {
|
||||
return this.isNextSigner(npub)
|
||||
? appPrivateRoutes.sign
|
||||
: appPublicRoutes.verify
|
||||
}
|
||||
|
||||
getLastSignerSig = () => {
|
||||
return this.#lastSignerSig
|
||||
}
|
||||
|
||||
#process = (
|
||||
createSignature: string,
|
||||
docSignatures: { [key: `npub1${string}`]: string }
|
||||
) => {
|
||||
try {
|
||||
const createSignatureEvent = parseNostrEvent(createSignature)
|
||||
const { signers } = parseCreateSignatureEventContent(
|
||||
createSignatureEvent.content
|
||||
)
|
||||
const getPrevSignerSig = (npub: `npub1${string}`) => {
|
||||
if (signers[0] === npub) {
|
||||
return createSignatureEvent.sig
|
||||
}
|
||||
|
||||
// Find the index of signer
|
||||
const currentSignerIndex = signers.findIndex(
|
||||
(signer) => signer === npub
|
||||
)
|
||||
|
||||
// Return if could not found user in signer's list
|
||||
if (currentSignerIndex === -1) return
|
||||
|
||||
// Find prev signer
|
||||
const prevSigner = signers[currentSignerIndex - 1]
|
||||
|
||||
// Get the signature of prev signer
|
||||
return this.#signatures.get(prevSigner)
|
||||
}
|
||||
|
||||
this.#signers = [...signers]
|
||||
for (const npub in docSignatures) {
|
||||
try {
|
||||
// Parse each signature event
|
||||
const event = parseNostrEvent(docSignatures[npub as `npub1${string}`])
|
||||
this.#signatures.set(npub as `npub1${string}`, event.sig)
|
||||
const isValidSignature = verifyEvent(event)
|
||||
if (isValidSignature) {
|
||||
const prevSignersSig = getPrevSignerSig(npub as `npub1${string}`)
|
||||
const signedEvent: SignedEventContent = JSON.parse(event.content)
|
||||
if (
|
||||
signedEvent.prevSig &&
|
||||
prevSignersSig &&
|
||||
signedEvent.prevSig === prevSignersSig
|
||||
) {
|
||||
this.#signersStatus.set(
|
||||
npub as `npub1${string}`,
|
||||
SignStatus.Signed
|
||||
)
|
||||
this.#lastSignerSig = event.sig
|
||||
}
|
||||
} else {
|
||||
this.#signersStatus.set(
|
||||
npub as `npub1${string}`,
|
||||
SignStatus.Invalid
|
||||
)
|
||||
}
|
||||
} catch (error) {
|
||||
this.#signersStatus.set(npub as `npub1${string}`, SignStatus.Invalid)
|
||||
}
|
||||
}
|
||||
|
||||
this.#signers
|
||||
.filter((s) => !this.#signatures.has(s))
|
||||
.forEach((s) => this.#signersStatus.set(s, SignStatus.Pending))
|
||||
|
||||
// Get the first signer that hasn't signed
|
||||
const nextSigner = this.#signers.find((s) => !this.#signatures.has(s))
|
||||
if (nextSigner) {
|
||||
this.#nextSigner = nextSigner
|
||||
this.#signersStatus.set(nextSigner, SignStatus.Awaiting)
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof MetaParseError) {
|
||||
toast.error(error.message)
|
||||
console.error(error.name, error.message, error.cause, error.context)
|
||||
} else {
|
||||
console.error('Unexpected error', error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,6 +1,7 @@
|
||||
import { Mark } from './mark'
|
||||
import { Keys } from '../store/auth/types'
|
||||
import { Event } from 'nostr-tools'
|
||||
import { SigitStatus, SignStatus } from '../utils'
|
||||
|
||||
export enum UserRole {
|
||||
signer = 'Signer',
|
||||
@ -35,11 +36,6 @@ export interface SignedEventContent {
|
||||
marks: Mark[]
|
||||
}
|
||||
|
||||
export interface Sigit {
|
||||
fileUrl: string
|
||||
meta: Meta
|
||||
}
|
||||
|
||||
export interface OpenTimestamp {
|
||||
nostrId: string
|
||||
value: string
|
||||
@ -92,3 +88,43 @@ export interface SigitNotification {
|
||||
export function isSigitNotification(obj: unknown): obj is SigitNotification {
|
||||
return typeof (obj as SigitNotification).metaUrl === 'string'
|
||||
}
|
||||
|
||||
/**
|
||||
* Flattened interface that combines properties `Meta`, `CreateSignatureEventContent`,
|
||||
* and `Event` (event's fields are made optional and pubkey and created_at replaced with our versions)
|
||||
*/
|
||||
export interface FlatMeta
|
||||
extends Meta,
|
||||
CreateSignatureEventContent,
|
||||
Partial<Omit<Event, 'pubkey' | 'created_at'>> {
|
||||
submittedBy?: string
|
||||
|
||||
// Optional field only present on exported sigits
|
||||
// Exporting adds user's pubkey
|
||||
exportedBy?: string
|
||||
|
||||
// Remove created_at and replace with createdAt
|
||||
createdAt?: number
|
||||
|
||||
// Validated create signature event
|
||||
isValid: boolean
|
||||
|
||||
// Decryption
|
||||
encryptionKey: string | undefined
|
||||
|
||||
// Parsed Document Signatures
|
||||
parsedSignatureEvents: {
|
||||
[signer: `npub1${string}`]: DocSignatureEvent
|
||||
}
|
||||
|
||||
// Calculated completion time
|
||||
completedAt?: number
|
||||
|
||||
// Calculated status fields
|
||||
signedStatus: SigitStatus
|
||||
signersStatus: {
|
||||
[signer: `npub1${string}`]: SignStatus
|
||||
}
|
||||
|
||||
timestamps?: OpenTimestamp[]
|
||||
}
|
||||
|
@ -8,13 +8,16 @@ export interface CurrentUserMark {
|
||||
currentValue?: string
|
||||
}
|
||||
|
||||
// Both PdfFileHash and FileHash currently exist.
|
||||
// It enables backward compatibility for Sigits created before January 2025
|
||||
export interface Mark {
|
||||
id: number
|
||||
npub: string
|
||||
pdfFileHash: string
|
||||
type: MarkType
|
||||
location: MarkLocation
|
||||
fileName: string
|
||||
pdfFileHash?: string
|
||||
fileHash?: string
|
||||
value?: string
|
||||
}
|
||||
|
||||
|
@ -270,21 +270,10 @@ export const getToolboxLabelByMarkType = (markType: MarkType) => {
|
||||
return DEFAULT_TOOLBOX.find((t) => t.identifier === markType)?.label
|
||||
}
|
||||
|
||||
export const getOptimizedPathsWithStrokeWidth = (svgString: string) => {
|
||||
const parser = new DOMParser()
|
||||
const xmlDoc = parser.parseFromString(svgString, 'image/svg+xml')
|
||||
const paths = xmlDoc.querySelectorAll('path')
|
||||
const tuples: string[][] = []
|
||||
paths.forEach((path) => {
|
||||
const d = path.getAttribute('d') ?? ''
|
||||
const strokeWidth = path.getAttribute('stroke-width') ?? ''
|
||||
tuples.push([d, strokeWidth])
|
||||
})
|
||||
|
||||
return tuples
|
||||
}
|
||||
|
||||
export const processMarks = async (marks: Mark[], encryptionKey?: string) => {
|
||||
export const encryptAndUploadMarks = async (
|
||||
marks: Mark[],
|
||||
encryptionKey?: string
|
||||
) => {
|
||||
const _marks = [...marks]
|
||||
for (let i = 0; i < _marks.length; i++) {
|
||||
const mark = _marks[i]
|
||||
|
@ -49,9 +49,9 @@ export interface SigitCardDisplayInfo {
|
||||
* @param raw Raw string for parsing
|
||||
* @returns parsed Event
|
||||
*/
|
||||
export const parseNostrEvent = async (raw: string): Promise<Event> => {
|
||||
export const parseNostrEvent = (raw: string): Event => {
|
||||
try {
|
||||
const event = await parseJson<Event>(raw)
|
||||
const event = JSON.parse(raw) as Event
|
||||
return event
|
||||
} catch (error) {
|
||||
throw new MetaParseError(MetaParseErrorType.PARSE_ERROR_EVENT, {
|
||||
@ -66,12 +66,13 @@ export const parseNostrEvent = async (raw: string): Promise<Event> => {
|
||||
* @param raw Raw string for parsing
|
||||
* @returns parsed CreateSignatureEventContent
|
||||
*/
|
||||
export const parseCreateSignatureEventContent = async (
|
||||
export const parseCreateSignatureEventContent = (
|
||||
raw: string
|
||||
): Promise<CreateSignatureEventContent> => {
|
||||
): CreateSignatureEventContent => {
|
||||
try {
|
||||
const createSignatureEventContent =
|
||||
await parseJson<CreateSignatureEventContent>(raw)
|
||||
const createSignatureEventContent = JSON.parse(
|
||||
raw
|
||||
) as CreateSignatureEventContent
|
||||
return createSignatureEventContent
|
||||
} catch (error) {
|
||||
throw new MetaParseError(
|
||||
@ -89,7 +90,7 @@ export const parseCreateSignatureEventContent = async (
|
||||
* @param meta Sigit metadata
|
||||
* @returns SigitCardDisplayInfo
|
||||
*/
|
||||
export const extractSigitCardDisplayInfo = async (meta: Meta) => {
|
||||
export const extractSigitCardDisplayInfo = (meta: Meta) => {
|
||||
if (!meta?.createSignature) return
|
||||
|
||||
const sigitInfo: SigitCardDisplayInfo = {
|
||||
@ -100,14 +101,14 @@ export const extractSigitCardDisplayInfo = async (meta: Meta) => {
|
||||
}
|
||||
|
||||
try {
|
||||
const createSignatureEvent = await parseNostrEvent(meta.createSignature)
|
||||
const createSignatureEvent = parseNostrEvent(meta.createSignature)
|
||||
|
||||
sigitInfo.isValid = verifyEvent(createSignatureEvent)
|
||||
|
||||
// created_at in nostr events are stored in seconds
|
||||
sigitInfo.createdAt = fromUnixTimestamp(createSignatureEvent.created_at)
|
||||
|
||||
const createSignatureContent = await parseCreateSignatureEventContent(
|
||||
const createSignatureContent = parseCreateSignatureEventContent(
|
||||
createSignatureEvent.content
|
||||
)
|
||||
|
||||
|
@ -1,46 +1,11 @@
|
||||
import { Event } from 'nostr-tools'
|
||||
import { Meta } from '../types'
|
||||
|
||||
/**
|
||||
* This function returns the signature of last signer
|
||||
* It will be used in the content of export signature's signedEvent
|
||||
*/
|
||||
const getLastSignersSig = (
|
||||
meta: Meta,
|
||||
signers: `npub1${string}`[]
|
||||
): string | null => {
|
||||
// if there're no signers then use creator's signature
|
||||
if (signers.length === 0) {
|
||||
try {
|
||||
const createSignatureEvent: Event = JSON.parse(meta.createSignature)
|
||||
return createSignatureEvent.sig
|
||||
} catch (error) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
// get last signer
|
||||
const lastSigner = signers[signers.length - 1]
|
||||
|
||||
// get the signature of last signer
|
||||
try {
|
||||
const lastSignatureEvent: Event = JSON.parse(meta.docSignatures[lastSigner])
|
||||
return lastSignatureEvent.sig
|
||||
} catch (error) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if all signers have signed the sigit
|
||||
* @param signers - an array of npubs of all signers from the Sigit
|
||||
* @param signedBy - an array of npubs that have signed it already
|
||||
*/
|
||||
const isFullySigned = (
|
||||
export const isFullySigned = (
|
||||
signers: `npub1${string}`[],
|
||||
signedBy: `npub1${string}`[]
|
||||
): boolean => {
|
||||
return signers.every((signer) => signedBy.includes(signer))
|
||||
}
|
||||
|
||||
export { getLastSignersSig, isFullySigned }
|
||||
|
140
src/utils/zip.ts
140
src/utils/zip.ts
@ -1,6 +1,12 @@
|
||||
import JSZip from 'jszip'
|
||||
import { toast } from 'react-toastify'
|
||||
import { InputFileFormat, OutputByType, OutputType } from '../types'
|
||||
import { InputFileFormat, Meta, OutputByType, OutputType } from '../types'
|
||||
import { NavigateOptions, To } from 'react-router-dom'
|
||||
import { appPublicRoutes } from '../routes'
|
||||
import { NostrController } from '../controllers'
|
||||
import { decryptArrayBuffer } from './crypto'
|
||||
import { hexToNpub, parseJson, SigitStatus, timeout } from '.'
|
||||
import { SignerService } from '../services'
|
||||
|
||||
/**
|
||||
* Read the content of a file within a zip archive.
|
||||
@ -9,7 +15,7 @@ import { InputFileFormat, OutputByType, OutputType } from '../types'
|
||||
* @param outputType The type of output to return (e.g., 'string', 'arraybuffer', 'uint8array', etc.).
|
||||
* @returns A Promise resolving to the content of the file, or null if an error occurs.
|
||||
*/
|
||||
const readContentOfZipEntry = async <T extends OutputType>(
|
||||
export const readContentOfZipEntry = async <T extends OutputType>(
|
||||
zip: JSZip,
|
||||
filePath: string,
|
||||
outputType: T
|
||||
@ -34,7 +40,7 @@ const readContentOfZipEntry = async <T extends OutputType>(
|
||||
})
|
||||
}
|
||||
|
||||
const loadZip = async (data: InputFileFormat): Promise<JSZip | null> => {
|
||||
export const loadZip = async (data: InputFileFormat): Promise<JSZip | null> => {
|
||||
try {
|
||||
return await JSZip.loadAsync(data)
|
||||
} catch (err) {
|
||||
@ -46,4 +52,130 @@ const loadZip = async (data: InputFileFormat): Promise<JSZip | null> => {
|
||||
}
|
||||
}
|
||||
|
||||
export { readContentOfZipEntry, loadZip }
|
||||
export const decrypt = async (file: File) => {
|
||||
const nostrController = NostrController.getInstance()
|
||||
|
||||
const zip = await loadZip(file)
|
||||
if (!zip) return
|
||||
|
||||
const keysFileContent = await readContentOfZipEntry(
|
||||
zip,
|
||||
'keys.json',
|
||||
'string'
|
||||
)
|
||||
|
||||
if (!keysFileContent) return null
|
||||
|
||||
const parsedKeysJson = await parseJson<{ sender: string; keys: string[] }>(
|
||||
keysFileContent
|
||||
).catch((err) => {
|
||||
console.log(`Error parsing content of keys.json:`, err)
|
||||
toast.error(err.message || `Error parsing content of keys.json`)
|
||||
return null
|
||||
})
|
||||
|
||||
if (!parsedKeysJson) return
|
||||
|
||||
const encryptedArrayBuffer = await readContentOfZipEntry(
|
||||
zip,
|
||||
'compressed.sigit',
|
||||
'arraybuffer'
|
||||
)
|
||||
|
||||
if (!encryptedArrayBuffer) return
|
||||
|
||||
const { keys, sender } = parsedKeysJson
|
||||
|
||||
for (const key of keys) {
|
||||
// decrypt the encryptionKey, with timeout (duration = 60 seconds)
|
||||
const encryptionKey = await Promise.race([
|
||||
nostrController.nip04Decrypt(sender, key),
|
||||
timeout(60000)
|
||||
])
|
||||
.then((res) => {
|
||||
return res
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log('err :>> ', err)
|
||||
return null
|
||||
})
|
||||
|
||||
// Return if encryption failed
|
||||
if (!encryptionKey) continue
|
||||
|
||||
const arrayBuffer = await decryptArrayBuffer(
|
||||
encryptedArrayBuffer,
|
||||
encryptionKey
|
||||
).catch((err) => {
|
||||
console.log('err in decryption:>> ', err)
|
||||
return null
|
||||
})
|
||||
|
||||
if (arrayBuffer) return arrayBuffer
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
type NavigateArgs = { to: To; options?: NavigateOptions }
|
||||
export const navigateFromZip = async (file: File, pubkey: `npub1${string}`) => {
|
||||
if (!file.name.endsWith('.sigit.zip')) {
|
||||
toast.error(`Not a SiGit zip file: ${file.name}`)
|
||||
}
|
||||
|
||||
try {
|
||||
let zip = await JSZip.loadAsync(file)
|
||||
if (!zip) {
|
||||
return null
|
||||
}
|
||||
|
||||
let arrayBuffer: ArrayBuffer | undefined
|
||||
if ('keys.json' in zip.files) {
|
||||
// Decrypt
|
||||
const decryptedArrayBuffer = await decrypt(file).catch((err) => {
|
||||
console.error(`error occurred in decryption`, err)
|
||||
toast.error(err.message || `error occurred in decryption`)
|
||||
})
|
||||
|
||||
if (decryptedArrayBuffer) {
|
||||
// Replace the zip and continue processing
|
||||
zip = await JSZip.loadAsync(decryptedArrayBuffer)
|
||||
arrayBuffer = decryptedArrayBuffer
|
||||
}
|
||||
}
|
||||
|
||||
if ('meta.json' in zip.files) {
|
||||
// Check where we need to navigate
|
||||
// Find Meta and process it for signer state
|
||||
const metaContent = await readContentOfZipEntry(
|
||||
zip,
|
||||
'meta.json',
|
||||
'string'
|
||||
)
|
||||
if (metaContent) {
|
||||
const meta = JSON.parse(metaContent) as Meta
|
||||
const signerService = new SignerService(meta)
|
||||
|
||||
const to =
|
||||
signerService.getSignedStatus() === SigitStatus.Complete
|
||||
? appPublicRoutes.verify
|
||||
: signerService.getNavigate(hexToNpub(pubkey))
|
||||
|
||||
return {
|
||||
to,
|
||||
options: {
|
||||
state: { uploadedZip: arrayBuffer || file }
|
||||
}
|
||||
} as NavigateArgs
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
} catch (err) {
|
||||
console.error('err in processing sigit zip file :>> ', err)
|
||||
if (err instanceof Error) {
|
||||
toast.error(err.message || 'An error occurred in loading zip file.')
|
||||
}
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
@ -2,6 +2,7 @@ import { defineConfig } from 'vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
import tsconfigPaths from 'vite-tsconfig-paths'
|
||||
import { nodePolyfills } from 'vite-plugin-node-polyfills'
|
||||
import zipPack from 'vite-plugin-zip-pack'
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
@ -9,7 +10,8 @@ export default defineConfig({
|
||||
tsconfigPaths(),
|
||||
nodePolyfills({
|
||||
include: ['os']
|
||||
})
|
||||
}),
|
||||
zipPack()
|
||||
],
|
||||
build: {
|
||||
target: 'ES2022'
|
||||
|
Loading…
x
Reference in New Issue
Block a user