Merge pull request 'release' (#306) from staging into main
All checks were successful
Release to Production / build_and_release (push) Successful in 1m30s
All checks were successful
Release to Production / build_and_release (push) Successful in 1m30s
Reviewed-on: #306
This commit is contained in:
commit
15000a2d14
@ -6,19 +6,19 @@ Welcome to Sigit! We are thrilled that you are interested in contributing to thi
|
|||||||
|
|
||||||
### Reporting Bugs
|
### Reporting Bugs
|
||||||
|
|
||||||
If you encounter a bug while using Sigit, please [open an issue](https://git.sigit.io/g/web/issues/new) on this repository. Provide as much detail as possible, including steps to reproduce the bug.
|
If you encounter a bug while using Sigit, please [open an issue](https://git.nostrdev.com/sigit/sigit.io/issues/new) on this repository. Provide as much detail as possible, including steps to reproduce the bug.
|
||||||
|
|
||||||
### Suggesting Enhancements
|
### Suggesting Enhancements
|
||||||
|
|
||||||
If you have an idea for how to improve Sigit, we would love to hear from you! [Open an issue](https://git.sigit.io/g/web/issues/new) to suggest an enhancement.
|
If you have an idea for how to improve Sigit, we would love to hear from you! [Open an issue](https://git.nostrdev.com/sigit/sigit.io/issues/new) to suggest an enhancement.
|
||||||
|
|
||||||
### Pull Requests
|
### Pull Requests
|
||||||
|
|
||||||
We welcome pull requests from contributors! To contribute code changes:
|
We welcome pull requests from contributors! To contribute code changes:
|
||||||
|
|
||||||
1. Fork the repository and create your branch from `main`.
|
1. Fork the repository and create your branch from `staging`.
|
||||||
2. Make your changes and ensure they pass any existing tests.
|
2. Make your changes and ensure they pass any existing tests.
|
||||||
3. Write meaningful commit messages.
|
3. Write meaningful commit messages (conventional commit standard)
|
||||||
4. Submit a pull request, describing your changes in detail and referencing any related issues.
|
4. Submit a pull request, describing your changes in detail and referencing any related issues.
|
||||||
|
|
||||||
## Development Setup
|
## Development Setup
|
||||||
@ -35,4 +35,14 @@ All contributions, including pull requests, undergo code review. Code review ens
|
|||||||
|
|
||||||
## Contact
|
## Contact
|
||||||
|
|
||||||
If you have questions or need further assistance, you can reach out to [maintainer's email].
|
If you have questions or need further assistance, you can reach out to `npub1d0csynrrxcynkcedktdzrdj6gnras2psg48mf46kxjazs8skrjgq9uzhlq`
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
The following items should be tested with each release:
|
||||||
|
|
||||||
|
- Create a SIGit with at least 3 signers
|
||||||
|
- Create a SIGit where the creator is not the first signer
|
||||||
|
- Create a SIGit where one co-signer has no marks
|
||||||
|
- Create a SIGit using a file other than a PDF
|
||||||
|
- Use several login mechanisms, browsers, operating systems whilst testing
|
||||||
|
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 { CurrentUserFile } from '../../types/file.ts'
|
||||||
import styles from './style.module.scss'
|
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 {
|
interface FileListProps {
|
||||||
files: CurrentUserFile[]
|
files: CurrentUserFile[]
|
||||||
currentFile: CurrentUserFile
|
currentFile: CurrentUserFile
|
||||||
setCurrentFile: (file: CurrentUserFile) => void
|
setCurrentFile: (file: CurrentUserFile) => void
|
||||||
handleExport: () => void
|
handleExport?: () => void
|
||||||
handleEncryptedExport?: () => void
|
handleEncryptedExport?: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -45,34 +49,48 @@ const FileList = ({
|
|||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<PopupState variant="popover" popupId="download-popup-menu">
|
{(typeof handleExport === 'function' ||
|
||||||
{(popupState) => (
|
typeof handleEncryptedExport === 'function') && (
|
||||||
<React.Fragment>
|
<PopupState variant="popover" popupId="download-popup-menu">
|
||||||
<Button variant="contained" {...bindTrigger(popupState)}>
|
{(popupState) => (
|
||||||
Export files
|
<React.Fragment>
|
||||||
</Button>
|
<Button variant="contained" {...bindTrigger(popupState)}>
|
||||||
<Menu {...bindMenu(popupState)}>
|
Export files
|
||||||
<MenuItem
|
</Button>
|
||||||
onClick={() => {
|
<Menu {...bindMenu(popupState)}>
|
||||||
popupState.close
|
{typeof handleEncryptedExport === 'function' && (
|
||||||
handleExport()
|
<MenuItem
|
||||||
}}
|
onClick={() => {
|
||||||
>
|
popupState.close
|
||||||
Export Files
|
handleEncryptedExport()
|
||||||
</MenuItem>
|
}}
|
||||||
<MenuItem
|
>
|
||||||
onClick={() => {
|
<FontAwesomeIcon
|
||||||
popupState.close
|
color={'var(--mui-palette-primary-main)'}
|
||||||
typeof handleEncryptedExport === 'function' &&
|
icon={faLock}
|
||||||
handleEncryptedExport()
|
/>
|
||||||
}}
|
ENCRYPTED
|
||||||
>
|
</MenuItem>
|
||||||
Export Encrypted Files
|
)}
|
||||||
</MenuItem>
|
{typeof handleExport === 'function' && (
|
||||||
</Menu>
|
<MenuItem
|
||||||
</React.Fragment>
|
onClick={() => {
|
||||||
)}
|
popupState.close
|
||||||
</PopupState>
|
handleExport()
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon
|
||||||
|
color={'var(--mui-palette-primary-main)'}
|
||||||
|
icon={faTriangleExclamation}
|
||||||
|
/>
|
||||||
|
UNENCRYPTED
|
||||||
|
</MenuItem>
|
||||||
|
)}
|
||||||
|
</Menu>
|
||||||
|
</React.Fragment>
|
||||||
|
)}
|
||||||
|
</PopupState>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -8,15 +8,19 @@ import {
|
|||||||
import React, { useState } from 'react'
|
import React, { useState } from 'react'
|
||||||
import { MarkInput } from '../MarkTypeStrategy/MarkInput.tsx'
|
import { MarkInput } from '../MarkTypeStrategy/MarkInput.tsx'
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
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 { Button } from '@mui/material'
|
||||||
import styles from './style.module.scss'
|
import styles from './style.module.scss'
|
||||||
|
import { ButtonUnderline } from '../ButtonUnderline/index.tsx'
|
||||||
|
|
||||||
interface MarkFormFieldProps {
|
interface MarkFormFieldProps {
|
||||||
currentUserMarks: CurrentUserMark[]
|
currentUserMarks: CurrentUserMark[]
|
||||||
handleCurrentUserMarkChange: (mark: CurrentUserMark) => void
|
handleCurrentUserMarkChange: (mark: CurrentUserMark) => void
|
||||||
handleSelectedMarkValueChange: (value: string) => void
|
handleSelectedMarkValueChange: (value: string) => void
|
||||||
handleSubmit: (event: React.MouseEvent<HTMLButtonElement>) => void
|
handleSubmit: (
|
||||||
|
event: React.MouseEvent<HTMLButtonElement>,
|
||||||
|
type: 'online' | 'offline'
|
||||||
|
) => void
|
||||||
selectedMark: CurrentUserMark | null
|
selectedMark: CurrentUserMark | null
|
||||||
selectedMarkValue: string
|
selectedMarkValue: string
|
||||||
}
|
}
|
||||||
@ -73,11 +77,11 @@ const MarkFormField = ({
|
|||||||
setComplete(true)
|
setComplete(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleSignAndComplete = (
|
const handleSignAndComplete =
|
||||||
event: React.MouseEvent<HTMLButtonElement>
|
(type: 'online' | 'offline') =>
|
||||||
) => {
|
(event: React.MouseEvent<HTMLButtonElement>) => {
|
||||||
handleSubmit(event)
|
handleSubmit(event, type)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.container}>
|
<div className={styles.container}>
|
||||||
@ -129,18 +133,28 @@ const MarkFormField = ({
|
|||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
) : (
|
) : (
|
||||||
<div className={styles.actionsBottom}>
|
<>
|
||||||
<Button
|
<div className={styles.actionsBottom}>
|
||||||
onClick={handleSignAndComplete}
|
<Button
|
||||||
className={[styles.submitButton, styles.completeButton].join(
|
onClick={handleSignAndComplete('online')}
|
||||||
' '
|
className={[
|
||||||
)}
|
styles.submitButton,
|
||||||
|
styles.completeButton
|
||||||
|
].join(' ')}
|
||||||
|
disabled={!isReadyToSign()}
|
||||||
|
autoFocus
|
||||||
|
>
|
||||||
|
SIGN AND BROADCAST
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<ButtonUnderline
|
||||||
|
onClick={handleSignAndComplete('offline')}
|
||||||
disabled={!isReadyToSign()}
|
disabled={!isReadyToSign()}
|
||||||
autoFocus
|
|
||||||
>
|
>
|
||||||
SIGN AND COMPLETE
|
<FontAwesomeIcon icon={faDownload} />
|
||||||
</Button>
|
Sign and export locally instead
|
||||||
</div>
|
</ButtonUnderline>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className={styles.footerContainer}>
|
<div className={styles.footerContainer}>
|
||||||
|
@ -3,7 +3,6 @@ import {
|
|||||||
decryptArrayBuffer,
|
decryptArrayBuffer,
|
||||||
encryptArrayBuffer,
|
encryptArrayBuffer,
|
||||||
getHash,
|
getHash,
|
||||||
isOnline,
|
|
||||||
uploadToFileStorage
|
uploadToFileStorage
|
||||||
} from '../../../utils'
|
} from '../../../utils'
|
||||||
import { MarkStrategy } from '../MarkStrategy'
|
import { MarkStrategy } from '../MarkStrategy'
|
||||||
@ -37,21 +36,17 @@ export const SignatureStrategy: MarkStrategy = {
|
|||||||
// Create the encrypted json file from array buffer and hash
|
// Create the encrypted json file from array buffer and hash
|
||||||
const file = new File([encryptedArrayBuffer], `${hash}.json`)
|
const file = new File([encryptedArrayBuffer], `${hash}.json`)
|
||||||
|
|
||||||
if (await isOnline()) {
|
try {
|
||||||
try {
|
const url = await uploadToFileStorage(file)
|
||||||
const url = await uploadToFileStorage(file)
|
console.info(`${file.name} uploaded to file storage`)
|
||||||
console.info(`${file.name} uploaded to file storage`)
|
return url
|
||||||
return url
|
} catch (error) {
|
||||||
} catch (error) {
|
if (error instanceof Error) {
|
||||||
if (error instanceof Error) {
|
console.error(
|
||||||
console.error(
|
`Error occurred in uploading file ${file.name}`,
|
||||||
`Error occurred in uploading file ${file.name}`,
|
error.message
|
||||||
error.message
|
)
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
// TOOD: offline
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return value
|
return value
|
||||||
@ -89,7 +84,6 @@ export const SignatureStrategy: MarkStrategy = {
|
|||||||
return json
|
return json
|
||||||
}
|
}
|
||||||
|
|
||||||
// TOOD: offline
|
|
||||||
return value
|
return value
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -24,9 +24,8 @@ import {
|
|||||||
interface PdfMarkingProps {
|
interface PdfMarkingProps {
|
||||||
currentUserMarks: CurrentUserMark[]
|
currentUserMarks: CurrentUserMark[]
|
||||||
files: CurrentUserFile[]
|
files: CurrentUserFile[]
|
||||||
handleExport: () => void
|
|
||||||
handleEncryptedExport: () => void
|
|
||||||
handleSign: () => void
|
handleSign: () => void
|
||||||
|
handleSignOffline: () => void
|
||||||
meta: Meta | null
|
meta: Meta | null
|
||||||
otherUserMarks: Mark[]
|
otherUserMarks: Mark[]
|
||||||
setCurrentUserMarks: (currentUserMarks: CurrentUserMark[]) => void
|
setCurrentUserMarks: (currentUserMarks: CurrentUserMark[]) => void
|
||||||
@ -39,18 +38,16 @@ interface PdfMarkingProps {
|
|||||||
* @param props
|
* @param props
|
||||||
* @constructor
|
* @constructor
|
||||||
*/
|
*/
|
||||||
const PdfMarking = (props: PdfMarkingProps) => {
|
const PdfMarking = ({
|
||||||
const {
|
files,
|
||||||
files,
|
currentUserMarks,
|
||||||
currentUserMarks,
|
setCurrentUserMarks,
|
||||||
setCurrentUserMarks,
|
setUpdatedMarks,
|
||||||
setUpdatedMarks,
|
handleSign,
|
||||||
handleExport,
|
handleSignOffline,
|
||||||
handleEncryptedExport,
|
meta,
|
||||||
handleSign,
|
otherUserMarks
|
||||||
meta,
|
}: PdfMarkingProps) => {
|
||||||
otherUserMarks
|
|
||||||
} = props
|
|
||||||
const [selectedMark, setSelectedMark] = useState<CurrentUserMark | null>(null)
|
const [selectedMark, setSelectedMark] = useState<CurrentUserMark | null>(null)
|
||||||
const [selectedMarkValue, setSelectedMarkValue] = useState<string>('')
|
const [selectedMarkValue, setSelectedMarkValue] = useState<string>('')
|
||||||
const [currentFile, setCurrentFile] = useState<CurrentUserFile | null>(null)
|
const [currentFile, setCurrentFile] = useState<CurrentUserFile | null>(null)
|
||||||
@ -99,7 +96,10 @@ const PdfMarking = (props: PdfMarkingProps) => {
|
|||||||
/**
|
/**
|
||||||
* Sign and Complete
|
* Sign and Complete
|
||||||
*/
|
*/
|
||||||
const handleSubmit = (event: React.MouseEvent<HTMLButtonElement>) => {
|
const handleSubmit = (
|
||||||
|
event: React.MouseEvent<HTMLButtonElement>,
|
||||||
|
type: 'online' | 'offline'
|
||||||
|
) => {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
if (selectedMarkValue && selectedMark) {
|
if (selectedMarkValue && selectedMark) {
|
||||||
const updatedMark: CurrentUserMark = getUpdatedMark(
|
const updatedMark: CurrentUserMark = getUpdatedMark(
|
||||||
@ -117,16 +117,10 @@ const PdfMarking = (props: PdfMarkingProps) => {
|
|||||||
setUpdatedMarks(updatedMark.mark)
|
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) => {
|
const handleChange = (value: string) => {
|
||||||
setSelectedMarkValue(value)
|
setSelectedMarkValue(value)
|
||||||
}
|
}
|
||||||
@ -142,8 +136,6 @@ const PdfMarking = (props: PdfMarkingProps) => {
|
|||||||
files={files}
|
files={files}
|
||||||
currentFile={currentFile}
|
currentFile={currentFile}
|
||||||
setCurrentFile={setCurrentFile}
|
setCurrentFile={setCurrentFile}
|
||||||
handleExport={handleExport}
|
|
||||||
handleEncryptedExport={handleEncryptedExport}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
@ -38,12 +38,16 @@ const PdfView = ({
|
|||||||
currentUserMarks: CurrentUserMark[],
|
currentUserMarks: CurrentUserMark[],
|
||||||
hash: string
|
hash: string
|
||||||
): CurrentUserMark[] => {
|
): CurrentUserMark[] => {
|
||||||
return currentUserMarks.filter(
|
return currentUserMarks.filter((currentUserMark) =>
|
||||||
(currentUserMark) => currentUserMark.mark.pdfFileHash === hash
|
currentUserMark.mark.pdfFileHash
|
||||||
|
? currentUserMark.mark.pdfFileHash === hash
|
||||||
|
: currentUserMark.mark.fileHash === hash
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
const filterMarksByFile = (marks: Mark[], hash: string): Mark[] => {
|
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 (
|
return (
|
||||||
<div className="files-wrapper">
|
<div className="files-wrapper">
|
||||||
|
@ -19,7 +19,7 @@
|
|||||||
"page": 1
|
"page": 1
|
||||||
},
|
},
|
||||||
"npub": "npub1x77qywdllzetv9ncnhlfpv62kshlgtt0uqlsq3v22uzzkk2xvvrsn6uyfy",
|
"npub": "npub1x77qywdllzetv9ncnhlfpv62kshlgtt0uqlsq3v22uzzkk2xvvrsn6uyfy",
|
||||||
"pdfFileHash": "da5f857e77d3aa59c461efad804116931c059b36e6b4da0b5d9452753ec70c05"
|
"fileHash": "da5f857e77d3aa59c461efad804116931c059b36e6b4da0b5d9452753ec70c05"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"da5f857e77d3aa59c461efad804116931c059b36e6b4da0b5d9452753ec70c05/2.png": [
|
"da5f857e77d3aa59c461efad804116931c059b36e6b4da0b5d9452753ec70c05/2.png": [
|
||||||
@ -34,7 +34,7 @@
|
|||||||
"page": 2
|
"page": 2
|
||||||
},
|
},
|
||||||
"npub": "npub1x77qywdllzetv9ncnhlfpv62kshlgtt0uqlsq3v22uzzkk2xvvrsn6uyfy",
|
"npub": "npub1x77qywdllzetv9ncnhlfpv62kshlgtt0uqlsq3v22uzzkk2xvvrsn6uyfy",
|
||||||
"pdfFileHash": "da5f857e77d3aa59c461efad804116931c059b36e6b4da0b5d9452753ec70c05"
|
"fileHash": "da5f857e77d3aa59c461efad804116931c059b36e6b4da0b5d9452753ec70c05"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@ -54,7 +54,7 @@
|
|||||||
"page": 1
|
"page": 1
|
||||||
},
|
},
|
||||||
"npub": "npub1x77qywdllzetv9ncnhlfpv62kshlgtt0uqlsq3v22uzzkk2xvvrsn6uyfy",
|
"npub": "npub1x77qywdllzetv9ncnhlfpv62kshlgtt0uqlsq3v22uzzkk2xvvrsn6uyfy",
|
||||||
"pdfFileHash": "da5f857e77d3aa59c461efad804116931c059b36e6b4da0b5d9452753ec70c05",
|
"fileHash": "da5f857e77d3aa59c461efad804116931c059b36e6b4da0b5d9452753ec70c05",
|
||||||
"value": "Pera Peric"
|
"value": "Pera Peric"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -68,7 +68,7 @@
|
|||||||
"page": 2
|
"page": 2
|
||||||
},
|
},
|
||||||
"npub": "npub1x77qywdllzetv9ncnhlfpv62kshlgtt0uqlsq3v22uzzkk2xvvrsn6uyfy",
|
"npub": "npub1x77qywdllzetv9ncnhlfpv62kshlgtt0uqlsq3v22uzzkk2xvvrsn6uyfy",
|
||||||
"pdfFileHash": "da5f857e77d3aa59c461efad804116931c059b36e6b4da0b5d9452753ec70c05",
|
"fileHash": "da5f857e77d3aa59c461efad804116931c059b36e6b4da0b5d9452753ec70c05",
|
||||||
"value": "Pera Peric"
|
"value": "Pera Peric"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
@ -1,11 +1,5 @@
|
|||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import {
|
import { DocSignatureEvent, Meta, SignedEventContent, FlatMeta } from '../types'
|
||||||
CreateSignatureEventContent,
|
|
||||||
DocSignatureEvent,
|
|
||||||
Meta,
|
|
||||||
SignedEventContent,
|
|
||||||
OpenTimestamp
|
|
||||||
} from '../types'
|
|
||||||
import { Mark } from '../types/mark'
|
import { Mark } from '../types/mark'
|
||||||
import {
|
import {
|
||||||
fromUnixTimestamp,
|
fromUnixTimestamp,
|
||||||
@ -17,53 +11,11 @@ import {
|
|||||||
} from '../utils'
|
} from '../utils'
|
||||||
import { toast } from 'react-toastify'
|
import { toast } from 'react-toastify'
|
||||||
import { verifyEvent } from 'nostr-tools'
|
import { verifyEvent } from 'nostr-tools'
|
||||||
import { Event } from 'nostr-tools'
|
|
||||||
import store from '../store/store'
|
import store from '../store/store'
|
||||||
import { NostrController } from '../controllers'
|
import { NostrController } from '../controllers'
|
||||||
import { MetaParseError } from '../types/errors/MetaParseError'
|
import { MetaParseError } from '../types/errors/MetaParseError'
|
||||||
import { MARK_TYPE_CONFIG } from '../components/MarkTypeStrategy/MarkStrategy'
|
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
|
* Custom use hook for parsing the Sigit Meta
|
||||||
* @param meta Sigit Meta
|
* @param meta Sigit Meta
|
||||||
@ -74,8 +26,8 @@ export const useSigitMeta = (meta: Meta): FlatMeta => {
|
|||||||
const [kind, setKind] = useState<number>()
|
const [kind, setKind] = useState<number>()
|
||||||
const [tags, setTags] = useState<string[][]>()
|
const [tags, setTags] = useState<string[][]>()
|
||||||
const [createdAt, setCreatedAt] = useState<number>()
|
const [createdAt, setCreatedAt] = useState<number>()
|
||||||
const [submittedBy, setSubmittedBy] = useState<`npub1${string}`>() // submittedBy, pubkey from nostr event
|
const [submittedBy, setSubmittedBy] = useState<string>() // submittedBy, pubkey from nostr event (hex)
|
||||||
const [exportedBy, setExportedBy] = useState<`npub1${string}`>() // pubkey from export signature nostr event
|
const [exportedBy, setExportedBy] = useState<string>() // pubkey from export signature nostr event (hex)
|
||||||
const [id, setId] = useState<string>()
|
const [id, setId] = useState<string>()
|
||||||
const [sig, setSig] = useState<string>()
|
const [sig, setSig] = useState<string>()
|
||||||
|
|
||||||
@ -108,18 +60,16 @@ export const useSigitMeta = (meta: Meta): FlatMeta => {
|
|||||||
;(async function () {
|
;(async function () {
|
||||||
try {
|
try {
|
||||||
if (meta.exportSignature) {
|
if (meta.exportSignature) {
|
||||||
const exportSignatureEvent = await parseNostrEvent(
|
const exportSignatureEvent = parseNostrEvent(meta.exportSignature)
|
||||||
meta.exportSignature
|
|
||||||
)
|
|
||||||
if (
|
if (
|
||||||
verifyEvent(exportSignatureEvent) &&
|
verifyEvent(exportSignatureEvent) &&
|
||||||
exportSignatureEvent.pubkey
|
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 } =
|
const { kind, tags, created_at, pubkey, id, sig, content } =
|
||||||
createSignatureEvent
|
createSignatureEvent
|
||||||
@ -129,12 +79,12 @@ export const useSigitMeta = (meta: Meta): FlatMeta => {
|
|||||||
setTags(tags)
|
setTags(tags)
|
||||||
// created_at in nostr events are stored in seconds
|
// created_at in nostr events are stored in seconds
|
||||||
setCreatedAt(fromUnixTimestamp(created_at))
|
setCreatedAt(fromUnixTimestamp(created_at))
|
||||||
setSubmittedBy(pubkey as `npub1${string}`)
|
setSubmittedBy(pubkey)
|
||||||
setId(id)
|
setId(id)
|
||||||
setSig(sig)
|
setSig(sig)
|
||||||
|
|
||||||
const { title, signers, viewers, fileHashes, markConfig, zipUrl } =
|
const { title, signers, viewers, fileHashes, markConfig, zipUrl } =
|
||||||
await parseCreateSignatureEventContent(content)
|
parseCreateSignatureEventContent(content)
|
||||||
|
|
||||||
setTitle(title)
|
setTitle(title)
|
||||||
setSigners(signers)
|
setSigners(signers)
|
||||||
|
@ -8,7 +8,6 @@ import {
|
|||||||
Tooltip
|
Tooltip
|
||||||
} from '@mui/material'
|
} from '@mui/material'
|
||||||
import type { Identifier, XYCoord } from 'dnd-core'
|
import type { Identifier, XYCoord } from 'dnd-core'
|
||||||
import saveAs from 'file-saver'
|
|
||||||
import JSZip from 'jszip'
|
import JSZip from 'jszip'
|
||||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||||
import { DndProvider, useDrag, useDrop } from 'react-dnd'
|
import { DndProvider, useDrag, useDrop } from 'react-dnd'
|
||||||
@ -39,7 +38,6 @@ import {
|
|||||||
generateKeysFile,
|
generateKeysFile,
|
||||||
getHash,
|
getHash,
|
||||||
hexToNpub,
|
hexToNpub,
|
||||||
isOnline,
|
|
||||||
unixNow,
|
unixNow,
|
||||||
npubToHex,
|
npubToHex,
|
||||||
queryNip05,
|
queryNip05,
|
||||||
@ -57,6 +55,7 @@ import { Mark } from '../../types/mark.ts'
|
|||||||
import { StickySideColumns } from '../../layouts/StickySideColumns.tsx'
|
import { StickySideColumns } from '../../layouts/StickySideColumns.tsx'
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||||
import {
|
import {
|
||||||
|
faDownload,
|
||||||
faEllipsis,
|
faEllipsis,
|
||||||
faEye,
|
faEye,
|
||||||
faFile,
|
faFile,
|
||||||
@ -79,6 +78,7 @@ import { NDKUserProfile, NostrEvent } from '@nostr-dev-kit/ndk'
|
|||||||
import { useNDKContext } from '../../hooks/useNDKContext.ts'
|
import { useNDKContext } from '../../hooks/useNDKContext.ts'
|
||||||
import { useNDK } from '../../hooks/useNDK.ts'
|
import { useNDK } from '../../hooks/useNDK.ts'
|
||||||
import { useImmer } from 'use-immer'
|
import { useImmer } from 'use-immer'
|
||||||
|
import { ButtonUnderline } from '../../components/ButtonUnderline/index.tsx'
|
||||||
|
|
||||||
type FoundUser = NostrEvent & { npub: string }
|
type FoundUser = NostrEvent & { npub: string }
|
||||||
|
|
||||||
@ -636,8 +636,8 @@ export const CreatePage = () => {
|
|||||||
width: drawnField.width
|
width: drawnField.width
|
||||||
},
|
},
|
||||||
npub: drawnField.counterpart,
|
npub: drawnField.counterpart,
|
||||||
pdfFileHash: fileHash,
|
fileName: file.name,
|
||||||
fileName: file.name
|
fileHash
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}) || []
|
}) || []
|
||||||
@ -692,10 +692,18 @@ export const CreatePage = () => {
|
|||||||
type: 'application/sigit'
|
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(
|
const keysFileContent = await generateKeysFile(
|
||||||
[firstSigner.pubkey],
|
Array.from(userSet),
|
||||||
encryptionKey
|
encryptionKey
|
||||||
)
|
)
|
||||||
if (!keysFileContent) return null
|
if (!keysFileContent) return null
|
||||||
@ -747,30 +755,6 @@ export const CreatePage = () => {
|
|||||||
.catch(handleUploadError)
|
.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 generateFilesZip = async (): Promise<ArrayBuffer | null> => {
|
||||||
const zip = new JSZip()
|
const zip = new JSZip()
|
||||||
selectedFiles.forEach((file) => {
|
selectedFiles.forEach((file) => {
|
||||||
@ -836,7 +820,7 @@ export const CreatePage = () => {
|
|||||||
return e.id
|
return e.id
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleCreate = async () => {
|
const initCreation = async () => {
|
||||||
try {
|
try {
|
||||||
if (!validateInputs()) return
|
if (!validateInputs()) return
|
||||||
|
|
||||||
@ -848,132 +832,183 @@ export const CreatePage = () => {
|
|||||||
setLoadingSpinnerDesc('Generating encryption key')
|
setLoadingSpinnerDesc('Generating encryption key')
|
||||||
const encryptionKey = await generateEncryptionKey()
|
const encryptionKey = await generateEncryptionKey()
|
||||||
|
|
||||||
if (await isOnline()) {
|
setLoadingSpinnerDesc('Creating marks')
|
||||||
setLoadingSpinnerDesc('generating files.zip')
|
const markConfig = createMarks(fileHashes)
|
||||||
const arrayBuffer = await generateFilesZip()
|
|
||||||
if (!arrayBuffer) return
|
|
||||||
|
|
||||||
setLoadingSpinnerDesc('Encrypting files.zip')
|
return {
|
||||||
const encryptedArrayBuffer = await encryptZipFile(
|
encryptionKey,
|
||||||
arrayBuffer,
|
markConfig,
|
||||||
encryptionKey
|
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 { encryptionKey, markConfig, fileHashes } = result
|
||||||
const fileUrl = await uploadFile(encryptedArrayBuffer)
|
|
||||||
if (!fileUrl) return
|
|
||||||
|
|
||||||
setLoadingSpinnerDesc('Generating create signature')
|
setLoadingSpinnerDesc('generating files.zip')
|
||||||
const createSignature = await generateCreateSignature(
|
const arrayBuffer = await generateFilesZip()
|
||||||
markConfig,
|
if (!arrayBuffer) return
|
||||||
fileHashes,
|
|
||||||
fileUrl
|
|
||||||
)
|
|
||||||
if (!createSignature) return
|
|
||||||
|
|
||||||
setLoadingSpinnerDesc('Generating keys for decryption')
|
setLoadingSpinnerDesc('Encrypting files.zip')
|
||||||
|
const encryptedArrayBuffer = await encryptZipFile(
|
||||||
|
arrayBuffer,
|
||||||
|
encryptionKey
|
||||||
|
)
|
||||||
|
|
||||||
// generate key pairs for decryption
|
setLoadingSpinnerDesc('Uploading files.zip to file storage')
|
||||||
const pubkeys = users.map((user) => user.pubkey)
|
const fileUrl = await uploadFile(encryptedArrayBuffer)
|
||||||
// also add creator in the list
|
if (!fileUrl) return
|
||||||
if (pubkeys.includes(usersPubkey!)) {
|
|
||||||
pubkeys.push(usersPubkey!)
|
|
||||||
}
|
|
||||||
|
|
||||||
const keys = await generateKeys(pubkeys, encryptionKey)
|
setLoadingSpinnerDesc('Generating create signature')
|
||||||
if (!keys) return
|
const createSignature = await generateCreateSignature(
|
||||||
|
markConfig,
|
||||||
|
fileHashes,
|
||||||
|
fileUrl
|
||||||
|
)
|
||||||
|
if (!createSignature) return
|
||||||
|
|
||||||
setLoadingSpinnerDesc('Generating an open timestamp.')
|
setLoadingSpinnerDesc('Generating keys for decryption')
|
||||||
|
|
||||||
const timestamp = await generateTimestamp(
|
// generate key pairs for decryption
|
||||||
extractNostrId(createSignature)
|
const pubkeys = users.map((user) => user.pubkey)
|
||||||
)
|
// also add creator in the list
|
||||||
|
if (pubkeys.includes(usersPubkey!)) {
|
||||||
|
pubkeys.push(usersPubkey!)
|
||||||
|
}
|
||||||
|
|
||||||
const meta: Meta = {
|
const keys = await generateKeys(pubkeys, encryptionKey)
|
||||||
createSignature,
|
if (!keys) return
|
||||||
keys,
|
|
||||||
modifiedAt: unixNow(),
|
|
||||||
docSignatures: {}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (timestamp) {
|
setLoadingSpinnerDesc('Generating an open timestamp.')
|
||||||
meta.timestamps = [timestamp]
|
|
||||||
}
|
|
||||||
|
|
||||||
setLoadingSpinnerDesc('Updating user app data')
|
const timestamp = await generateTimestamp(extractNostrId(createSignature))
|
||||||
|
|
||||||
const event = await updateUsersAppData([meta])
|
const meta: Meta = {
|
||||||
if (!event) return
|
createSignature,
|
||||||
|
keys,
|
||||||
|
modifiedAt: unixNow(),
|
||||||
|
docSignatures: {}
|
||||||
|
}
|
||||||
|
|
||||||
const metaUrl = await uploadMetaToFileStorage(meta, encryptionKey)
|
if (timestamp) {
|
||||||
|
meta.timestamps = [timestamp]
|
||||||
|
}
|
||||||
|
|
||||||
setLoadingSpinnerDesc('Sending notifications to counterparties')
|
setLoadingSpinnerDesc('Updating user app data')
|
||||||
const promises = sendNotifications({
|
|
||||||
metaUrl,
|
const event = await updateUsersAppData([meta])
|
||||||
keys: meta.keys
|
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)
|
const isFirstSigner = signers[0].pubkey === usersPubkey
|
||||||
.then(() => {
|
if (isFirstSigner) {
|
||||||
toast.success('Notifications sent successfully')
|
navigate(appPrivateRoutes.sign, { state: { meta } })
|
||||||
})
|
|
||||||
.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}`)
|
|
||||||
}
|
|
||||||
} else {
|
} 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) => {
|
const handleCreateOffline = async () => {
|
||||||
zip.file(`files/${file.name}`, file)
|
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) {
|
} catch (error) {
|
||||||
if (error instanceof Error) {
|
if (error instanceof Error) {
|
||||||
@ -1231,6 +1266,11 @@ export const CreatePage = () => {
|
|||||||
Publish
|
Publish
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
|
<ButtonUnderline onClick={handleCreateOffline}>
|
||||||
|
<FontAwesomeIcon icon={faDownload} />
|
||||||
|
Create and export locally
|
||||||
|
</ButtonUnderline>
|
||||||
|
|
||||||
{!!error && (
|
{!!error && (
|
||||||
<FormHelperText error={!!error}>{error}</FormHelperText>
|
<FormHelperText error={!!error}>{error}</FormHelperText>
|
||||||
)}
|
)}
|
||||||
|
@ -1,10 +1,9 @@
|
|||||||
import { Button, TextField } from '@mui/material'
|
import { Button, TextField } from '@mui/material'
|
||||||
import JSZip from 'jszip'
|
|
||||||
import { useCallback, useEffect, useState } from 'react'
|
import { useCallback, useEffect, useState } from 'react'
|
||||||
import { useNavigate, useSearchParams } from 'react-router-dom'
|
import { useNavigate, useSearchParams } from 'react-router-dom'
|
||||||
import { toast } from 'react-toastify'
|
import { toast } from 'react-toastify'
|
||||||
import { useAppSelector } from '../../hooks'
|
import { useAppSelector } from '../../hooks'
|
||||||
import { appPrivateRoutes, appPublicRoutes } from '../../routes'
|
import { appPrivateRoutes } from '../../routes'
|
||||||
import { Meta } from '../../types'
|
import { Meta } from '../../types'
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||||
import { faSearch } from '@fortawesome/free-solid-svg-icons'
|
import { faSearch } from '@fortawesome/free-solid-svg-icons'
|
||||||
@ -15,6 +14,7 @@ import { Container } from '../../components/Container'
|
|||||||
import styles from './style.module.scss'
|
import styles from './style.module.scss'
|
||||||
import {
|
import {
|
||||||
extractSigitCardDisplayInfo,
|
extractSigitCardDisplayInfo,
|
||||||
|
navigateFromZip,
|
||||||
SigitCardDisplayInfo,
|
SigitCardDisplayInfo,
|
||||||
SigitStatus
|
SigitStatus
|
||||||
} from '../../utils'
|
} from '../../utils'
|
||||||
@ -56,6 +56,7 @@ export const HomePage = () => {
|
|||||||
[key: string]: SigitCardDisplayInfo
|
[key: string]: SigitCardDisplayInfo
|
||||||
}>({})
|
}>({})
|
||||||
const usersAppData = useAppSelector((state) => state.userAppData)
|
const usersAppData = useAppSelector((state) => state.userAppData)
|
||||||
|
const usersPubkey = useAppSelector((state) => state.auth.usersPubkey)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (usersAppData?.sigits) {
|
if (usersAppData?.sigits) {
|
||||||
@ -63,7 +64,7 @@ export const HomePage = () => {
|
|||||||
const parsedSigits: { [key: string]: SigitCardDisplayInfo } = {}
|
const parsedSigits: { [key: string]: SigitCardDisplayInfo } = {}
|
||||||
for (const key in usersAppData.sigits) {
|
for (const key in usersAppData.sigits) {
|
||||||
if (Object.prototype.hasOwnProperty.call(usersAppData.sigits, key)) {
|
if (Object.prototype.hasOwnProperty.call(usersAppData.sigits, key)) {
|
||||||
const sigitInfo = await extractSigitCardDisplayInfo(
|
const sigitInfo = extractSigitCardDisplayInfo(
|
||||||
usersAppData.sigits[key]
|
usersAppData.sigits[key]
|
||||||
)
|
)
|
||||||
if (sigitInfo) {
|
if (sigitInfo) {
|
||||||
@ -92,27 +93,12 @@ export const HomePage = () => {
|
|||||||
const fileName = file.name
|
const fileName = file.name
|
||||||
const fileExtension = fileName.slice(-10) // ".sigit.zip" has 10 characters
|
const fileExtension = fileName.slice(-10) // ".sigit.zip" has 10 characters
|
||||||
if (fileExtension === '.sigit.zip') {
|
if (fileExtension === '.sigit.zip') {
|
||||||
const zip = await JSZip.loadAsync(file).catch((err) => {
|
const nav = await navigateFromZip(
|
||||||
console.log('err in loading zip file :>> ', err)
|
file,
|
||||||
toast.error(err.message || 'An error occurred in loading zip file.')
|
usersPubkey as `npub1${string}`
|
||||||
return null
|
)
|
||||||
})
|
|
||||||
|
|
||||||
if (!zip) return
|
if (nav) return navigate(nav.to, nav.options)
|
||||||
|
|
||||||
// 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 }
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
toast.error('Invalid SiGit zip file')
|
toast.error('Invalid SiGit zip file')
|
||||||
return
|
return
|
||||||
@ -124,7 +110,7 @@ export const HomePage = () => {
|
|||||||
state: { uploadedFiles: acceptedFiles }
|
state: { uploadedFiles: acceptedFiles }
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
[navigate]
|
[navigate, usersPubkey]
|
||||||
)
|
)
|
||||||
|
|
||||||
const { getRootProps, getInputProps, isDragActive, open } = useDropzone({
|
const { getRootProps, getInputProps, isDragActive, open } = useDropzone({
|
||||||
|
@ -54,7 +54,10 @@ export const RelaysPage = () => {
|
|||||||
const relayMap = useAppSelector((state) => state.relays?.map)
|
const relayMap = useAppSelector((state) => state.relays?.map)
|
||||||
const relaysInfo = useAppSelector((state) => state.relays?.info)
|
const relaysInfo = useAppSelector((state) => state.relays?.info)
|
||||||
|
|
||||||
const webSocketPrefix = 'wss://'
|
const webSocketPrefix =
|
||||||
|
newRelayURI?.startsWith('wss://') || newRelayURI?.startsWith('ws://')
|
||||||
|
? ''
|
||||||
|
: 'wss://'
|
||||||
|
|
||||||
// fetch relay list from relays
|
// fetch relay list from relays
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -197,7 +200,7 @@ export const RelaysPage = () => {
|
|||||||
// Check if new relay URI is a valid string
|
// Check if new relay URI is a valid string
|
||||||
if (
|
if (
|
||||||
relayURI &&
|
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
|
relayURI
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
@ -260,7 +263,13 @@ export const RelaysPage = () => {
|
|||||||
}}
|
}}
|
||||||
className={styles.relayURItextfield}
|
className={styles.relayURItextfield}
|
||||||
/>
|
/>
|
||||||
<Button variant="contained" onClick={() => handleAddNewRelay()}>
|
<Button
|
||||||
|
sx={{
|
||||||
|
height: '56px'
|
||||||
|
}}
|
||||||
|
variant="contained"
|
||||||
|
onClick={() => handleAddNewRelay()}
|
||||||
|
>
|
||||||
Add
|
Add
|
||||||
</Button>
|
</Button>
|
||||||
</Box>
|
</Box>
|
||||||
|
@ -12,6 +12,7 @@
|
|||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
align-items: start;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sectionIcon {
|
.sectionIcon {
|
||||||
|
@ -1,54 +1,41 @@
|
|||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import saveAs from 'file-saver'
|
|
||||||
import JSZip from 'jszip'
|
import JSZip from 'jszip'
|
||||||
import _ from 'lodash'
|
import _ from 'lodash'
|
||||||
import { Event, verifyEvent } from 'nostr-tools'
|
import { Event, verifyEvent } from 'nostr-tools'
|
||||||
import { useCallback, useEffect, useState } from 'react'
|
import { useEffect, useMemo, useState } from 'react'
|
||||||
import { useAppSelector } from '../../hooks'
|
import { useAppSelector } from '../../hooks'
|
||||||
import { useLocation, useNavigate, useParams } from 'react-router-dom'
|
import { useLocation, useNavigate, useParams } from 'react-router-dom'
|
||||||
import { toast } from 'react-toastify'
|
import { toast } from 'react-toastify'
|
||||||
import { LoadingSpinner } from '../../components/LoadingSpinner'
|
import { LoadingSpinner } from '../../components/LoadingSpinner'
|
||||||
import { NostrController } from '../../controllers'
|
import { NostrController } from '../../controllers'
|
||||||
import { appPrivateRoutes, appPublicRoutes } from '../../routes'
|
import { appPublicRoutes } from '../../routes'
|
||||||
import { CreateSignatureEventContent, Meta, SignedEvent } from '../../types'
|
import { CreateSignatureEventContent, Meta, SignedEvent } from '../../types'
|
||||||
import {
|
import {
|
||||||
ARRAY_BUFFER,
|
|
||||||
decryptArrayBuffer,
|
decryptArrayBuffer,
|
||||||
DEFLATE,
|
|
||||||
encryptArrayBuffer,
|
|
||||||
extractMarksFromSignedMeta,
|
extractMarksFromSignedMeta,
|
||||||
extractZipUrlAndEncryptionKey,
|
extractZipUrlAndEncryptionKey,
|
||||||
filterMarksByPubkey,
|
filterMarksByPubkey,
|
||||||
findOtherUserMarks,
|
findOtherUserMarks,
|
||||||
generateEncryptionKey,
|
|
||||||
generateKeysFile,
|
|
||||||
getCurrentUserFiles,
|
getCurrentUserFiles,
|
||||||
getCurrentUserMarks,
|
getCurrentUserMarks,
|
||||||
getHash,
|
getHash,
|
||||||
hexToNpub,
|
hexToNpub,
|
||||||
isOnline,
|
|
||||||
loadZip,
|
loadZip,
|
||||||
npubToHex,
|
npubToHex,
|
||||||
parseJson,
|
parseJson,
|
||||||
processMarks,
|
encryptAndUploadMarks,
|
||||||
readContentOfZipEntry,
|
readContentOfZipEntry,
|
||||||
signEventForMetaFile,
|
signEventForMetaFile,
|
||||||
timeout,
|
|
||||||
unixNow,
|
unixNow,
|
||||||
updateMarks,
|
updateMarks,
|
||||||
uploadMetaToFileStorage
|
uploadMetaToFileStorage
|
||||||
} from '../../utils'
|
} from '../../utils'
|
||||||
import { CurrentUserMark, Mark } from '../../types/mark.ts'
|
import { CurrentUserMark, Mark } from '../../types/mark.ts'
|
||||||
import PdfMarking from '../../components/PDFView/PdfMarking.tsx'
|
import PdfMarking from '../../components/PDFView/PdfMarking.tsx'
|
||||||
import {
|
import { convertToSigitFile, SigitFile } from '../../utils/file.ts'
|
||||||
convertToSigitFile,
|
|
||||||
getZipWithFiles,
|
|
||||||
SigitFile
|
|
||||||
} from '../../utils/file.ts'
|
|
||||||
import { generateTimestamp } from '../../utils/opentimestamps.ts'
|
import { generateTimestamp } from '../../utils/opentimestamps.ts'
|
||||||
import { MARK_TYPE_CONFIG } from '../../components/MarkTypeStrategy/MarkStrategy.tsx'
|
import { MARK_TYPE_CONFIG } from '../../components/MarkTypeStrategy/MarkStrategy.tsx'
|
||||||
import { useNDK } from '../../hooks/useNDK.ts'
|
import { useNDK } from '../../hooks/useNDK.ts'
|
||||||
import { getLastSignersSig } from '../../utils/sign.ts'
|
|
||||||
|
|
||||||
export const SignPage = () => {
|
export const SignPage = () => {
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
@ -59,33 +46,32 @@ export const SignPage = () => {
|
|||||||
const usersAppData = useAppSelector((state) => state.userAppData)
|
const usersAppData = useAppSelector((state) => state.userAppData)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Received from `location.state`
|
* In the online mode, Sigit ID can be obtained either from the router state
|
||||||
*
|
* using location or from UsersAppData
|
||||||
* 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
|
|
||||||
* meta (metaInNavState) will be received in navigation from create & home page in online mode
|
|
||||||
*/
|
*/
|
||||||
let metaInNavState = location?.state?.meta || undefined
|
const metaInNavState = useMemo(() => {
|
||||||
const { arrayBuffer: decryptedArrayBuffer, uploadedZip } = location.state || {
|
if (usersAppData) {
|
||||||
decryptedArrayBuffer: undefined,
|
const sigitCreateId = params.id
|
||||||
uploadedZip: undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
if (sigitCreateId) {
|
||||||
* If userAppData (redux) is available, and we have the route param (sigit id)
|
const sigit = usersAppData.sigits[sigitCreateId]
|
||||||
* which is actually a `createEventId`, we will fetch a `sigit`
|
|
||||||
* based on the provided route ID and set fetched `sigit` to the `metaInNavState`
|
|
||||||
*/
|
|
||||||
if (usersAppData) {
|
|
||||||
const sigitCreateId = params.id
|
|
||||||
|
|
||||||
if (sigitCreateId) {
|
if (sigit) {
|
||||||
const sigit = usersAppData.sigits[sigitCreateId]
|
return sigit
|
||||||
|
}
|
||||||
if (sigit) {
|
|
||||||
metaInNavState = sigit
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return location?.state?.meta || undefined
|
||||||
|
}, [location, usersAppData, params.id])
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Received from `location.state`
|
||||||
|
*
|
||||||
|
* arrayBuffer (decryptedArrayBuffer) will be received in navigation from create page in offline mode
|
||||||
|
*/
|
||||||
|
const { arrayBuffer: decryptedArrayBuffer, uploadedZip } = location.state || {
|
||||||
|
decryptedArrayBuffer: undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
const [files, setFiles] = useState<{ [filename: string]: SigitFile }>({})
|
const [files, setFiles] = useState<{ [filename: string]: SigitFile }>({})
|
||||||
@ -217,65 +203,7 @@ export const SignPage = () => {
|
|||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [meta, usersPubkey])
|
}, [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(() => {
|
useEffect(() => {
|
||||||
// online mode - from create and home page views
|
|
||||||
if (metaInNavState) {
|
if (metaInNavState) {
|
||||||
const processSigit = async () => {
|
const processSigit = async () => {
|
||||||
setIsLoading(true)
|
setIsLoading(true)
|
||||||
@ -310,26 +238,20 @@ export const SignPage = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
processSigit()
|
processSigit()
|
||||||
} else if (decryptedArrayBuffer) {
|
}
|
||||||
handleDecryptedArrayBuffer(decryptedArrayBuffer).finally(() =>
|
|
||||||
setIsLoading(false)
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
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 {
|
} else {
|
||||||
setIsLoading(false)
|
setIsLoading(false)
|
||||||
}
|
}
|
||||||
}, [decryptedArrayBuffer, uploadedZip, metaInNavState, decrypt])
|
}, [decryptedArrayBuffer, uploadedZip])
|
||||||
|
|
||||||
const handleArrayBufferFromBlossom = async (
|
const handleArrayBufferFromBlossom = async (
|
||||||
arrayBuffer: ArrayBuffer,
|
arrayBuffer: ArrayBuffer,
|
||||||
@ -388,30 +310,12 @@ export const SignPage = () => {
|
|||||||
setMarks(updatedMarks)
|
setMarks(updatedMarks)
|
||||||
}
|
}
|
||||||
|
|
||||||
const parseKeysJson = async (zip: JSZip) => {
|
const handleDecryptedArrayBuffer = async (
|
||||||
const keysFileContent = await readContentOfZipEntry(
|
decryptedArrayBuffer: ArrayBuffer
|
||||||
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')
|
|
||||||
|
|
||||||
setLoadingSpinnerDesc('Parsing zip file')
|
setLoadingSpinnerDesc('Parsing zip file')
|
||||||
|
|
||||||
const zip = await loadZip(decryptedZipFile)
|
const zip = await loadZip(decryptedArrayBuffer)
|
||||||
if (!zip) return
|
if (!zip) return
|
||||||
|
|
||||||
const files: { [filename: string]: SigitFile } = {}
|
const files: { [filename: string]: SigitFile } = {}
|
||||||
@ -471,16 +375,15 @@ export const SignPage = () => {
|
|||||||
setMeta(parsedMetaJson)
|
setMeta(parsedMetaJson)
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleSign = async () => {
|
const initializeSigning = async (type: 'online' | 'offline') => {
|
||||||
if (Object.entries(files).length === 0 || !meta) return
|
if (Object.entries(files).length === 0 || !meta) return
|
||||||
|
|
||||||
setIsLoading(true)
|
setIsLoading(true)
|
||||||
|
|
||||||
setLoadingSpinnerDesc('Signing nostr event')
|
setLoadingSpinnerDesc('Signing nostr event')
|
||||||
|
|
||||||
const usersNpub = hexToNpub(usersPubkey!)
|
const usersNpub = hexToNpub(usersPubkey!)
|
||||||
const prevSig = getPrevSignersSig(usersNpub)
|
const prevSig = getPrevSignersSig(usersNpub)
|
||||||
if (!prevSig) {
|
if (!prevSig) {
|
||||||
setIsLoading(false)
|
|
||||||
toast.error('Previous signature is invalid')
|
toast.error('Previous signature is invalid')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -500,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({
|
const signedEvent = await signEventForMeta({
|
||||||
prevSig,
|
prevSig,
|
||||||
@ -511,6 +417,22 @@ export const SignPage = () => {
|
|||||||
|
|
||||||
const updatedMeta = updateMetaSignatures(meta, signedEvent)
|
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.')
|
setLoadingSpinnerDesc('Generating an open timestamp.')
|
||||||
|
|
||||||
const timestamp = await generateTimestamp(signedEvent.id)
|
const timestamp = await generateTimestamp(signedEvent.id)
|
||||||
@ -519,19 +441,62 @@ export const SignPage = () => {
|
|||||||
updatedMeta.modifiedAt = unixNow()
|
updatedMeta.modifiedAt = unixNow()
|
||||||
}
|
}
|
||||||
|
|
||||||
if (await isOnline()) {
|
await handleOnlineFlow(updatedMeta, encryptionKey)
|
||||||
await handleOnlineFlow(updatedMeta, encryptionKey)
|
|
||||||
} else {
|
const createSignature = JSON.parse(updatedMeta.createSignature)
|
||||||
setMeta(updatedMeta)
|
navigate(`${appPublicRoutes.verify}/${createSignature.id}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSignOffline = async () => {
|
||||||
|
const result = await initializeSigning('offline')
|
||||||
|
if (!result) {
|
||||||
setIsLoading(false)
|
setIsLoading(false)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (metaInNavState) {
|
const { updatedMeta } = result
|
||||||
const createSignature = JSON.parse(metaInNavState.createSignature)
|
|
||||||
navigate(`${appPublicRoutes.verify}/${createSignature.id}`)
|
const zip = new JSZip()
|
||||||
} else {
|
for (const [filename, value] of Object.entries(files)) {
|
||||||
navigate(appPrivateRoutes.homePage)
|
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
|
// Sign the event for the meta file
|
||||||
@ -562,66 +527,6 @@ export const SignPage = () => {
|
|||||||
return metaCopy
|
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
|
// Check if the current user is the last signer
|
||||||
const checkIsLastSigner = (signers: string[]): boolean => {
|
const checkIsLastSigner = (signers: string[]): boolean => {
|
||||||
const usersNpub = hexToNpub(usersPubkey!)
|
const usersNpub = hexToNpub(usersPubkey!)
|
||||||
@ -630,16 +535,6 @@ export const SignPage = () => {
|
|||||||
return signerIndex === lastSignerIndex
|
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
|
// Handle the online flow: update users app data and send notifications
|
||||||
const handleOnlineFlow = async (
|
const handleOnlineFlow = async (
|
||||||
meta: Meta,
|
meta: Meta,
|
||||||
@ -710,99 +605,6 @@ export const SignPage = () => {
|
|||||||
setIsLoading(false)
|
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 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
|
* This prevSig will be used in the content of the provided signer's signedEvent
|
||||||
@ -847,8 +649,7 @@ export const SignPage = () => {
|
|||||||
setCurrentUserMarks={setCurrentUserMarks}
|
setCurrentUserMarks={setCurrentUserMarks}
|
||||||
setUpdatedMarks={setUpdatedMarks}
|
setUpdatedMarks={setUpdatedMarks}
|
||||||
handleSign={handleSign}
|
handleSign={handleSign}
|
||||||
handleExport={handleExport}
|
handleSignOffline={handleSignOffline}
|
||||||
handleEncryptedExport={handleEncryptedExport}
|
|
||||||
otherUserMarks={otherUserMarks}
|
otherUserMarks={otherUserMarks}
|
||||||
meta={meta}
|
meta={meta}
|
||||||
/>
|
/>
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { Box, Button, Typography } from '@mui/material'
|
import { Box, Button, Typography } from '@mui/material'
|
||||||
import JSZip from 'jszip'
|
import JSZip from 'jszip'
|
||||||
import { MuiFileInput } from 'mui-file-input'
|
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 { toast } from 'react-toastify'
|
||||||
import { LoadingSpinner } from '../../components/LoadingSpinner'
|
import { LoadingSpinner } from '../../components/LoadingSpinner'
|
||||||
import { NostrController } from '../../controllers'
|
import { NostrController } from '../../controllers'
|
||||||
@ -27,14 +27,14 @@ import {
|
|||||||
generateKeysFile,
|
generateKeysFile,
|
||||||
ARRAY_BUFFER,
|
ARRAY_BUFFER,
|
||||||
DEFLATE,
|
DEFLATE,
|
||||||
uploadMetaToFileStorage
|
uploadMetaToFileStorage,
|
||||||
|
decrypt
|
||||||
} from '../../utils'
|
} from '../../utils'
|
||||||
import styles from './style.module.scss'
|
import styles from './style.module.scss'
|
||||||
import { useLocation, useParams } from 'react-router-dom'
|
import { useLocation, useParams } from 'react-router-dom'
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import { FONT_SIZE, FONT_TYPE, inPx } from '../../utils/pdf.ts'
|
import { FONT_SIZE, FONT_TYPE, inPx } from '../../utils/pdf.ts'
|
||||||
import { useAppSelector, useNDK } from '../../hooks'
|
import { useAppSelector, useNDK } from '../../hooks'
|
||||||
import { getLastSignersSig } from '../../utils/sign.ts'
|
|
||||||
import { saveAs } from 'file-saver'
|
import { saveAs } from 'file-saver'
|
||||||
import { Container } from '../../components/Container'
|
import { Container } from '../../components/Container'
|
||||||
import { useSigitMeta } from '../../hooks/useSigitMeta.tsx'
|
import { useSigitMeta } from '../../hooks/useSigitMeta.tsx'
|
||||||
@ -60,6 +60,7 @@ import {
|
|||||||
import { upgradeAndVerifyTimestamp } from '../../utils/opentimestamps.ts'
|
import { upgradeAndVerifyTimestamp } from '../../utils/opentimestamps.ts'
|
||||||
import _ from 'lodash'
|
import _ from 'lodash'
|
||||||
import { MarkRender } from '../../components/MarkTypeStrategy/MarkRender.tsx'
|
import { MarkRender } from '../../components/MarkTypeStrategy/MarkRender.tsx'
|
||||||
|
import { SignerService } from '../../services/index.ts'
|
||||||
|
|
||||||
interface PdfViewProps {
|
interface PdfViewProps {
|
||||||
files: CurrentUserFile[]
|
files: CurrentUserFile[]
|
||||||
@ -105,7 +106,10 @@ const SlimPdfView = ({
|
|||||||
const m = parsedSignatureEvents[
|
const m = parsedSignatureEvents[
|
||||||
e as `npub1${string}`
|
e as `npub1${string}`
|
||||||
].parsedContent?.marks.filter(
|
].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) {
|
if (m) {
|
||||||
marks.push(...m)
|
marks.push(...m)
|
||||||
@ -185,7 +189,7 @@ export const VerifyPage = () => {
|
|||||||
* meta will be received in navigation from create & home page in online mode
|
* meta will be received in navigation from create & home page in online mode
|
||||||
*/
|
*/
|
||||||
let metaInNavState = location?.state?.meta || undefined
|
let metaInNavState = location?.state?.meta || undefined
|
||||||
const { uploadedZip } = location.state || {}
|
const uploadedZip = location?.state?.uploadedZip || undefined
|
||||||
const [selectedFile, setSelectedFile] = useState<File | null>(null)
|
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 [meta, setMeta] = useState<Meta>(metaInNavState)
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@ -480,17 +478,35 @@ export const VerifyPage = () => {
|
|||||||
}
|
}
|
||||||
}, [encryptionKey, metaInNavState, zipUrl])
|
}, [encryptionKey, metaInNavState, zipUrl])
|
||||||
|
|
||||||
const handleVerify = async () => {
|
const handleVerify = useCallback(async (selectedFile: File) => {
|
||||||
if (!selectedFile) return
|
|
||||||
setIsLoading(true)
|
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)
|
console.log('err in loading zip file :>> ', err)
|
||||||
toast.error(err.message || 'An error occurred in loading zip file.')
|
toast.error(err.message || 'An error occurred in loading zip file.')
|
||||||
return null
|
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 files: { [filename: string]: SigitFile } = {}
|
||||||
const fileHashes: { [key: string]: string | null } = {}
|
const fileHashes: { [key: string]: string | null } = {}
|
||||||
@ -547,12 +563,21 @@ export const VerifyPage = () => {
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
if (!parsedMetaJson) return
|
if (!parsedMetaJson) {
|
||||||
|
setIsLoading(false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
setMeta(parsedMetaJson)
|
setMeta(parsedMetaJson)
|
||||||
|
|
||||||
setIsLoading(false)
|
setIsLoading(false)
|
||||||
}
|
}, [])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (uploadedZip) {
|
||||||
|
handleVerify(uploadedZip)
|
||||||
|
}
|
||||||
|
}, [handleVerify, uploadedZip])
|
||||||
|
|
||||||
// Handle errors during zip file generation
|
// Handle errors during zip file generation
|
||||||
const handleZipError = (err: unknown) => {
|
const handleZipError = (err: unknown) => {
|
||||||
@ -564,14 +589,6 @@ export const VerifyPage = () => {
|
|||||||
return null
|
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
|
// create final zip file
|
||||||
const createFinalZipFile = async (
|
const createFinalZipFile = async (
|
||||||
encryptedArrayBuffer: ArrayBuffer,
|
encryptedArrayBuffer: ArrayBuffer,
|
||||||
@ -584,28 +601,16 @@ export const VerifyPage = () => {
|
|||||||
type: 'application/sigit'
|
type: 'application/sigit'
|
||||||
})
|
})
|
||||||
|
|
||||||
const isLastSigner = checkIsLastSigner(signers)
|
|
||||||
|
|
||||||
const userSet = new Set<string>()
|
const userSet = new Set<string>()
|
||||||
|
if (submittedBy) {
|
||||||
if (isLastSigner) {
|
userSet.add(submittedBy)
|
||||||
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)!)
|
|
||||||
}
|
}
|
||||||
|
signers.forEach((signer) => {
|
||||||
|
userSet.add(npubToHex(signer)!)
|
||||||
|
})
|
||||||
|
viewers.forEach((viewer) => {
|
||||||
|
userSet.add(npubToHex(viewer)!)
|
||||||
|
})
|
||||||
|
|
||||||
const keysFileContent = await generateKeysFile(
|
const keysFileContent = await generateKeysFile(
|
||||||
Array.from(userSet),
|
Array.from(userSet),
|
||||||
@ -634,7 +639,10 @@ export const VerifyPage = () => {
|
|||||||
|
|
||||||
const handleExport = async () => {
|
const handleExport = async () => {
|
||||||
const arrayBuffer = await prepareZipExport()
|
const arrayBuffer = await prepareZipExport()
|
||||||
if (!arrayBuffer) return
|
if (!arrayBuffer) {
|
||||||
|
setIsLoading(false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const blob = new Blob([arrayBuffer])
|
const blob = new Blob([arrayBuffer])
|
||||||
saveAs(blob, `exported-${unixNow()}.sigit.zip`)
|
saveAs(blob, `exported-${unixNow()}.sigit.zip`)
|
||||||
@ -644,7 +652,10 @@ export const VerifyPage = () => {
|
|||||||
|
|
||||||
const handleEncryptedExport = async () => {
|
const handleEncryptedExport = async () => {
|
||||||
const arrayBuffer = await prepareZipExport()
|
const arrayBuffer = await prepareZipExport()
|
||||||
if (!arrayBuffer) return
|
if (!arrayBuffer) {
|
||||||
|
setIsLoading(false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const key = await generateEncryptionKey()
|
const key = await generateEncryptionKey()
|
||||||
|
|
||||||
@ -653,7 +664,11 @@ export const VerifyPage = () => {
|
|||||||
|
|
||||||
const finalZipFile = await createFinalZipFile(encryptedArrayBuffer, key)
|
const finalZipFile = await createFinalZipFile(encryptedArrayBuffer, key)
|
||||||
|
|
||||||
if (!finalZipFile) return
|
if (!finalZipFile) {
|
||||||
|
setIsLoading(false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
saveAs(finalZipFile, `exported-${unixNow()}.sigit.zip`)
|
saveAs(finalZipFile, `exported-${unixNow()}.sigit.zip`)
|
||||||
|
|
||||||
setIsLoading(false)
|
setIsLoading(false)
|
||||||
@ -675,7 +690,11 @@ export const VerifyPage = () => {
|
|||||||
setIsLoading(true)
|
setIsLoading(true)
|
||||||
setLoadingSpinnerDesc('Signing nostr event')
|
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)
|
if (!prevSig) return Promise.resolve(null)
|
||||||
|
|
||||||
const signedEvent = await signEventForMetaFile(
|
const signedEvent = await signEventForMetaFile(
|
||||||
@ -736,7 +755,10 @@ export const VerifyPage = () => {
|
|||||||
|
|
||||||
{selectedFile && (
|
{selectedFile && (
|
||||||
<Box sx={{ mt: 2, display: 'flex', justifyContent: 'center' }}>
|
<Box sx={{ mt: 2, display: 'flex', justifyContent: 'center' }}>
|
||||||
<Button onClick={handleVerify} variant="contained">
|
<Button
|
||||||
|
onClick={() => handleVerify(selectedFile)}
|
||||||
|
variant="contained"
|
||||||
|
>
|
||||||
Verify
|
Verify
|
||||||
</Button>
|
</Button>
|
||||||
</Box>
|
</Box>
|
||||||
|
@ -1 +1,2 @@
|
|||||||
export * from './cache'
|
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 { Mark } from './mark'
|
||||||
import { Keys } from '../store/auth/types'
|
import { Keys } from '../store/auth/types'
|
||||||
import { Event } from 'nostr-tools'
|
import { Event } from 'nostr-tools'
|
||||||
|
import { SigitStatus, SignStatus } from '../utils'
|
||||||
|
|
||||||
export enum UserRole {
|
export enum UserRole {
|
||||||
signer = 'Signer',
|
signer = 'Signer',
|
||||||
@ -35,11 +36,6 @@ export interface SignedEventContent {
|
|||||||
marks: Mark[]
|
marks: Mark[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Sigit {
|
|
||||||
fileUrl: string
|
|
||||||
meta: Meta
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface OpenTimestamp {
|
export interface OpenTimestamp {
|
||||||
nostrId: string
|
nostrId: string
|
||||||
value: string
|
value: string
|
||||||
@ -92,3 +88,43 @@ export interface SigitNotification {
|
|||||||
export function isSigitNotification(obj: unknown): obj is SigitNotification {
|
export function isSigitNotification(obj: unknown): obj is SigitNotification {
|
||||||
return typeof (obj as SigitNotification).metaUrl === 'string'
|
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
|
currentValue?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Both PdfFileHash and FileHash currently exist.
|
||||||
|
// It enables backward compatibility for Sigits created before January 2025
|
||||||
export interface Mark {
|
export interface Mark {
|
||||||
id: number
|
id: number
|
||||||
npub: string
|
npub: string
|
||||||
pdfFileHash: string
|
|
||||||
type: MarkType
|
type: MarkType
|
||||||
location: MarkLocation
|
location: MarkLocation
|
||||||
fileName: string
|
fileName: string
|
||||||
|
pdfFileHash?: string
|
||||||
|
fileHash?: string
|
||||||
value?: string
|
value?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -270,21 +270,10 @@ export const getToolboxLabelByMarkType = (markType: MarkType) => {
|
|||||||
return DEFAULT_TOOLBOX.find((t) => t.identifier === markType)?.label
|
return DEFAULT_TOOLBOX.find((t) => t.identifier === markType)?.label
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getOptimizedPathsWithStrokeWidth = (svgString: string) => {
|
export const encryptAndUploadMarks = async (
|
||||||
const parser = new DOMParser()
|
marks: Mark[],
|
||||||
const xmlDoc = parser.parseFromString(svgString, 'image/svg+xml')
|
encryptionKey?: string
|
||||||
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) => {
|
|
||||||
const _marks = [...marks]
|
const _marks = [...marks]
|
||||||
for (let i = 0; i < _marks.length; i++) {
|
for (let i = 0; i < _marks.length; i++) {
|
||||||
const mark = _marks[i]
|
const mark = _marks[i]
|
||||||
|
@ -49,9 +49,9 @@ export interface SigitCardDisplayInfo {
|
|||||||
* @param raw Raw string for parsing
|
* @param raw Raw string for parsing
|
||||||
* @returns parsed Event
|
* @returns parsed Event
|
||||||
*/
|
*/
|
||||||
export const parseNostrEvent = async (raw: string): Promise<Event> => {
|
export const parseNostrEvent = (raw: string): Event => {
|
||||||
try {
|
try {
|
||||||
const event = await parseJson<Event>(raw)
|
const event = JSON.parse(raw) as Event
|
||||||
return event
|
return event
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new MetaParseError(MetaParseErrorType.PARSE_ERROR_EVENT, {
|
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
|
* @param raw Raw string for parsing
|
||||||
* @returns parsed CreateSignatureEventContent
|
* @returns parsed CreateSignatureEventContent
|
||||||
*/
|
*/
|
||||||
export const parseCreateSignatureEventContent = async (
|
export const parseCreateSignatureEventContent = (
|
||||||
raw: string
|
raw: string
|
||||||
): Promise<CreateSignatureEventContent> => {
|
): CreateSignatureEventContent => {
|
||||||
try {
|
try {
|
||||||
const createSignatureEventContent =
|
const createSignatureEventContent = JSON.parse(
|
||||||
await parseJson<CreateSignatureEventContent>(raw)
|
raw
|
||||||
|
) as CreateSignatureEventContent
|
||||||
return createSignatureEventContent
|
return createSignatureEventContent
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new MetaParseError(
|
throw new MetaParseError(
|
||||||
@ -89,7 +90,7 @@ export const parseCreateSignatureEventContent = async (
|
|||||||
* @param meta Sigit metadata
|
* @param meta Sigit metadata
|
||||||
* @returns SigitCardDisplayInfo
|
* @returns SigitCardDisplayInfo
|
||||||
*/
|
*/
|
||||||
export const extractSigitCardDisplayInfo = async (meta: Meta) => {
|
export const extractSigitCardDisplayInfo = (meta: Meta) => {
|
||||||
if (!meta?.createSignature) return
|
if (!meta?.createSignature) return
|
||||||
|
|
||||||
const sigitInfo: SigitCardDisplayInfo = {
|
const sigitInfo: SigitCardDisplayInfo = {
|
||||||
@ -100,14 +101,14 @@ export const extractSigitCardDisplayInfo = async (meta: Meta) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const createSignatureEvent = await parseNostrEvent(meta.createSignature)
|
const createSignatureEvent = parseNostrEvent(meta.createSignature)
|
||||||
|
|
||||||
sigitInfo.isValid = verifyEvent(createSignatureEvent)
|
sigitInfo.isValid = verifyEvent(createSignatureEvent)
|
||||||
|
|
||||||
// created_at in nostr events are stored in seconds
|
// created_at in nostr events are stored in seconds
|
||||||
sigitInfo.createdAt = fromUnixTimestamp(createSignatureEvent.created_at)
|
sigitInfo.createdAt = fromUnixTimestamp(createSignatureEvent.created_at)
|
||||||
|
|
||||||
const createSignatureContent = await parseCreateSignatureEventContent(
|
const createSignatureContent = parseCreateSignatureEventContent(
|
||||||
createSignatureEvent.content
|
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
|
* Checks if all signers have signed the sigit
|
||||||
* @param signers - an array of npubs of all signers from 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
|
* @param signedBy - an array of npubs that have signed it already
|
||||||
*/
|
*/
|
||||||
const isFullySigned = (
|
export const isFullySigned = (
|
||||||
signers: `npub1${string}`[],
|
signers: `npub1${string}`[],
|
||||||
signedBy: `npub1${string}`[]
|
signedBy: `npub1${string}`[]
|
||||||
): boolean => {
|
): boolean => {
|
||||||
return signers.every((signer) => signedBy.includes(signer))
|
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 JSZip from 'jszip'
|
||||||
import { toast } from 'react-toastify'
|
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.
|
* 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.).
|
* @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.
|
* @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,
|
zip: JSZip,
|
||||||
filePath: string,
|
filePath: string,
|
||||||
outputType: T
|
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 {
|
try {
|
||||||
return await JSZip.loadAsync(data)
|
return await JSZip.loadAsync(data)
|
||||||
} catch (err) {
|
} 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user