Compare commits

...

11 Commits

Author SHA1 Message Date
20d1170f7d fix(sign): allow signing without marks, hide loading and show toast for prevSig error
All checks were successful
Open PR on Staging / audit_and_check (pull_request) Successful in 36s
2024-08-28 13:25:56 +02:00
a8020e6db2 fix(column-layout): wrap content column to prevent expanding 2024-08-28 13:25:56 +02:00
1c998ab99f refactor(error): refactor MetaParseError 2 2024-08-28 13:25:56 +02:00
fa1811a330 refactor(error): refactor SigitMetaParseError 2024-08-28 13:25:56 +02:00
624afae851 fix(create): throw on mark with no counterpart 2024-08-28 13:25:56 +02:00
3a03e3c7e8 chore: comment typo 2024-08-28 13:25:56 +02:00
5570d72f79 chore(git): merge pr #165 from 146-pdf-scaling into staging
All checks were successful
Release to Staging / build_and_release (push) Successful in 1m21s
Reviewed-on: #165
Reviewed-by: eugene <eugene@nostrdev.com>
2024-08-28 11:23:33 +00:00
9dc160e89c Merge pull request 'Signing Order' (#167) from issue-158 into staging
All checks were successful
Release to Staging / build_and_release (push) Successful in 1m13s
Reviewed-on: #167
Reviewed-by: enes <enes@noreply.git.nostrdev.com>
2024-08-28 08:41:33 +00:00
ec305c417b fix: signing order
All checks were successful
Open PR on Staging / audit_and_check (pull_request) Successful in 34s
2024-08-27 16:17:34 +03:00
722aecda39 chore(git): merge pull request #164 from 154-drawfield-escaping into staging
All checks were successful
Release to Staging / build_and_release (push) Successful in 1m17s
Reviewed-on: #164
Reviewed-by: eugene <eugene@nostrdev.com>
2024-08-27 10:29:42 +00:00
f4aefbb200 chore(git): merge pull request #163 from 138-other-file-types into staging
All checks were successful
Release to Staging / build_and_release (push) Successful in 1m15s
Reviewed-on: #163
Reviewed-by: s <sabir@4gl.io>
2024-08-26 12:00:32 +00:00
10 changed files with 322 additions and 276 deletions

View File

@ -23,7 +23,7 @@ interface PdfMarkingProps {
meta: Meta | null
otherUserMarks: Mark[]
setCurrentUserMarks: (currentUserMarks: CurrentUserMark[]) => void
setIsReadyToSign: (isReadyToSign: boolean) => void
setIsMarksCompleted: (isMarksCompleted: boolean) => void
setUpdatedMarks: (markToUpdate: Mark) => void
}
@ -37,7 +37,7 @@ const PdfMarking = (props: PdfMarkingProps) => {
const {
files,
currentUserMarks,
setIsReadyToSign,
setIsMarksCompleted,
setCurrentUserMarks,
setUpdatedMarks,
handleDownload,
@ -101,7 +101,7 @@ const PdfMarking = (props: PdfMarkingProps) => {
)
setCurrentUserMarks(updatedCurrentUserMarks)
setSelectedMark(null)
setIsReadyToSign(true)
setIsMarksCompleted(true)
setUpdatedMarks(updatedMark.mark)
}

View File

@ -11,7 +11,6 @@ import {
hexToNpub,
parseNostrEvent,
parseCreateSignatureEventContent,
SigitMetaParseError,
SigitStatus,
SignStatus
} from '../utils'
@ -21,6 +20,7 @@ import { Event } from 'nostr-tools'
import store from '../store/store'
import { AuthState } from '../store/auth/types'
import { NostrController } from '../controllers'
import { MetaParseError } from '../types/errors/MetaParseError'
/**
* Flattened interface that combines properties `Meta`, `CreateSignatureEventContent`,
@ -247,7 +247,7 @@ export const useSigitMeta = (meta: Meta): FlatMeta => {
)
}
} catch (error) {
if (error instanceof SigitMetaParseError) {
if (error instanceof MetaParseError) {
toast.error(error.message)
}
console.error(error)

View File

@ -17,8 +17,10 @@ export const StickySideColumns = ({
<div className={`${styles.sidesWrap} ${styles.files}`}>
<div className={styles.sides}>{left}</div>
</div>
<div id="content-preview" className={styles.content}>
{children}
<div>
<div id="content-preview" className={styles.content}>
{children}
</div>
</div>
<div className={styles.sidesWrap}>
<div className={styles.sides}>{right}</div>

View File

@ -514,6 +514,9 @@ export const CreatePage = () => {
return (
file.pages?.flatMap((page, index) => {
return page.drawnFields.map((drawnField) => {
if (!drawnField.counterpart) {
throw new Error('Missing counterpart')
}
return {
type: drawnField.type,
location: {
@ -670,6 +673,7 @@ export const CreatePage = () => {
}
const generateCreateSignature = async (
markConfig: Mark[],
fileHashes: {
[key: string]: string
},
@ -677,7 +681,6 @@ export const CreatePage = () => {
) => {
const signers = users.filter((user) => user.role === UserRole.signer)
const viewers = users.filter((user) => user.role === UserRole.viewer)
const markConfig = createMarks(fileHashes)
const content: CreateSignatureEventContent = {
signers: signers.map((signer) => hexToNpub(signer.pubkey)),
@ -721,131 +724,152 @@ export const CreatePage = () => {
}
const handleCreate = async () => {
if (!validateInputs()) return
try {
if (!validateInputs()) return
setIsLoading(true)
setLoadingSpinnerDesc('Generating file hashes')
const fileHashes = await generateFileHashes()
if (!fileHashes) {
setIsLoading(true)
setLoadingSpinnerDesc('Generating file hashes')
const fileHashes = await generateFileHashes()
if (!fileHashes) {
setIsLoading(false)
return
}
setLoadingSpinnerDesc('Generating encryption key')
const encryptionKey = await generateEncryptionKey()
if (await isOnline()) {
setLoadingSpinnerDesc('generating files.zip')
const arrayBuffer = await generateFilesZip()
if (!arrayBuffer) {
setIsLoading(false)
return
}
setLoadingSpinnerDesc('Encrypting files.zip')
const encryptedArrayBuffer = await encryptZipFile(
arrayBuffer,
encryptionKey
)
const markConfig = createMarks(fileHashes)
setLoadingSpinnerDesc('Uploading files.zip to file storage')
const fileUrl = await uploadFile(encryptedArrayBuffer)
if (!fileUrl) {
setIsLoading(false)
return
}
setLoadingSpinnerDesc('Generating create signature')
const createSignature = await generateCreateSignature(
markConfig,
fileHashes,
fileUrl
)
if (!createSignature) {
setIsLoading(false)
return
}
setLoadingSpinnerDesc('Generating keys for decryption')
// generate key pairs for decryption
const pubkeys = users.map((user) => user.pubkey)
// also add creator in the list
if (pubkeys.includes(usersPubkey!)) {
pubkeys.push(usersPubkey!)
}
const keys = await generateKeys(pubkeys, encryptionKey)
if (!keys) {
setIsLoading(false)
return
}
const meta: Meta = {
createSignature,
keys,
modifiedAt: unixNow(),
docSignatures: {}
}
setLoadingSpinnerDesc('Updating user app data')
const event = await updateUsersAppData(meta)
if (!event) {
setIsLoading(false)
return
}
setLoadingSpinnerDesc('Sending notifications to counterparties')
const promises = sendNotifications(meta)
await Promise.all(promises)
.then(() => {
toast.success('Notifications sent successfully')
})
.catch(() => {
toast.error('Failed to publish notifications')
})
navigate(appPrivateRoutes.sign, { state: { meta: meta } })
} else {
const zip = new JSZip()
selectedFiles.forEach((file) => {
zip.file(`files/${file.name}`, file)
})
const markConfig = createMarks(fileHashes)
setLoadingSpinnerDesc('Generating create signature')
const createSignature = await generateCreateSignature(
markConfig,
fileHashes,
''
)
if (!createSignature) {
setIsLoading(false)
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) {
setIsLoading(false)
return
}
setLoadingSpinnerDesc('Encrypting zip file')
const encryptedArrayBuffer = await encryptZipFile(
arrayBuffer,
encryptionKey
)
await handleOfflineFlow(encryptedArrayBuffer, encryptionKey)
}
} catch (error) {
if (error instanceof Error) {
toast.error(error.message)
}
console.error(error)
} finally {
setIsLoading(false)
return
}
setLoadingSpinnerDesc('Generating encryption key')
const encryptionKey = await generateEncryptionKey()
if (await isOnline()) {
setLoadingSpinnerDesc('generating files.zip')
const arrayBuffer = await generateFilesZip()
if (!arrayBuffer) {
setIsLoading(false)
return
}
setLoadingSpinnerDesc('Encrypting files.zip')
const encryptedArrayBuffer = await encryptZipFile(
arrayBuffer,
encryptionKey
)
setLoadingSpinnerDesc('Uploading files.zip to file storage')
const fileUrl = await uploadFile(encryptedArrayBuffer)
if (!fileUrl) {
setIsLoading(false)
return
}
setLoadingSpinnerDesc('Generating create signature')
const createSignature = await generateCreateSignature(fileHashes, fileUrl)
if (!createSignature) {
setIsLoading(false)
return
}
setLoadingSpinnerDesc('Generating keys for decryption')
// generate key pairs for decryption
const pubkeys = users.map((user) => user.pubkey)
// also add creator in the list
if (pubkeys.includes(usersPubkey!)) {
pubkeys.push(usersPubkey!)
}
const keys = await generateKeys(pubkeys, encryptionKey)
if (!keys) {
setIsLoading(false)
return
}
const meta: Meta = {
createSignature,
keys,
modifiedAt: unixNow(),
docSignatures: {}
}
setLoadingSpinnerDesc('Updating user app data')
const event = await updateUsersAppData(meta)
if (!event) {
setIsLoading(false)
return
}
setLoadingSpinnerDesc('Sending notifications to counterparties')
const promises = sendNotifications(meta)
await Promise.all(promises)
.then(() => {
toast.success('Notifications sent successfully')
})
.catch(() => {
toast.error('Failed to publish notifications')
})
navigate(appPrivateRoutes.sign, { state: { meta: meta } })
} else {
const zip = new JSZip()
selectedFiles.forEach((file) => {
zip.file(`files/${file.name}`, file)
})
setLoadingSpinnerDesc('Generating create signature')
const createSignature = await generateCreateSignature(fileHashes, '')
if (!createSignature) {
setIsLoading(false)
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) {
setIsLoading(false)
return
}
setLoadingSpinnerDesc('Encrypting zip file')
const encryptedArrayBuffer = await encryptZipFile(
arrayBuffer,
encryptionKey
)
await handleOfflineFlow(encryptedArrayBuffer, encryptionKey)
}
}

View File

@ -39,7 +39,7 @@ import { Container } from '../../components/Container'
import { DisplayMeta } from './internal/displayMeta'
import styles from './style.module.scss'
import { CurrentUserMark, Mark } from '../../types/mark.ts'
import { getLastSignersSig } from '../../utils/sign.ts'
import { getLastSignersSig, isFullySigned } from '../../utils/sign.ts'
import {
filterMarksByPubkey,
getCurrentUserMarks,
@ -112,13 +112,13 @@ export const SignPage = () => {
const [currentUserMarks, setCurrentUserMarks] = useState<CurrentUserMark[]>(
[]
)
const [isReadyToSign, setIsReadyToSign] = useState(false)
const [isMarksCompleted, setIsMarksCompleted] = useState(false)
const [otherUserMarks, setOtherUserMarks] = useState<Mark[]>([])
useEffect(() => {
if (signers.length > 0) {
// check if all signers have signed then its fully signed
if (signers.every((signer) => signedBy.includes(signer))) {
if (isFullySigned(signers, signedBy)) {
setSignedStatus(SignedStatus.Fully_Signed)
} else {
for (const signer of signers) {
@ -216,7 +216,7 @@ export const SignPage = () => {
const otherUserMarks = findOtherUserMarks(signedMarks, usersPubkey!)
setOtherUserMarks(otherUserMarks)
setCurrentUserMarks(currentUserMarks)
setIsReadyToSign(isCurrentUserMarksComplete(currentUserMarks))
setIsMarksCompleted(isCurrentUserMarksComplete(currentUserMarks))
}
setSignedBy(Object.keys(meta.docSignatures) as `npub1${string}`[])
@ -542,10 +542,13 @@ export const SignPage = () => {
setLoadingSpinnerDesc('Signing nostr event')
const prevSig = getPrevSignersSig(hexToNpub(usersPubkey!))
if (!prevSig) return
if (!prevSig) {
setIsLoading(false)
toast.error('Previous signature is invalid')
return
}
const marks = getSignerMarksForMeta()
if (!marks) return
const marks = getSignerMarksForMeta() || []
const signedEvent = await signEventForMeta({ prevSig, marks })
if (!signedEvent) return
@ -883,90 +886,90 @@ export const SignPage = () => {
return <LoadingSpinner desc={loadingSpinnerDesc} />
}
if (isReadyToSign) {
if (!isMarksCompleted && signedStatus === SignedStatus.User_Is_Next_Signer) {
return (
<>
<Container className={styles.container}>
{displayInput && (
<>
<Typography component="label" variant="h6">
Select sigit file
</Typography>
<Box className={styles.inputBlock}>
<MuiFileInput
placeholder="Select file"
inputProps={{ accept: '.sigit.zip' }}
value={selectedFile}
onChange={(value) => setSelectedFile(value)}
/>
</Box>
{selectedFile && (
<Box sx={{ mt: 2, display: 'flex', justifyContent: 'center' }}>
<Button onClick={handleDecrypt} variant="contained">
Decrypt
</Button>
</Box>
)}
</>
)}
{submittedBy && Object.entries(files).length > 0 && meta && (
<>
<DisplayMeta
meta={meta}
files={files}
submittedBy={submittedBy}
signers={signers}
viewers={viewers}
creatorFileHashes={creatorFileHashes}
currentFileHashes={currentFileHashes}
signedBy={signedBy}
nextSigner={nextSinger}
getPrevSignersSig={getPrevSignersSig}
/>
{signedStatus === SignedStatus.Fully_Signed && (
<Box sx={{ mt: 1, display: 'flex', justifyContent: 'center' }}>
<Button onClick={handleExport} variant="contained">
Export
</Button>
</Box>
)}
{signedStatus === SignedStatus.User_Is_Next_Signer && (
<Box sx={{ mt: 1, display: 'flex', justifyContent: 'center' }}>
<Button onClick={handleSign} variant="contained">
Sign
</Button>
</Box>
)}
{isSignerOrCreator && (
<Box sx={{ mt: 1, display: 'flex', justifyContent: 'center' }}>
<Button onClick={handleExportSigit} variant="contained">
Export Sigit
</Button>
</Box>
)}
</>
)}
</Container>
</>
<PdfMarking
files={getCurrentUserFiles(files, currentFileHashes, creatorFileHashes)}
currentUserMarks={currentUserMarks}
setIsMarksCompleted={setIsMarksCompleted}
setCurrentUserMarks={setCurrentUserMarks}
setUpdatedMarks={setUpdatedMarks}
handleDownload={handleDownload}
otherUserMarks={otherUserMarks}
meta={meta}
/>
)
}
return (
<PdfMarking
files={getCurrentUserFiles(files, currentFileHashes, creatorFileHashes)}
currentUserMarks={currentUserMarks}
setIsReadyToSign={setIsReadyToSign}
setCurrentUserMarks={setCurrentUserMarks}
setUpdatedMarks={setUpdatedMarks}
handleDownload={handleDownload}
otherUserMarks={otherUserMarks}
meta={meta}
/>
<>
<Container className={styles.container}>
{displayInput && (
<>
<Typography component="label" variant="h6">
Select sigit file
</Typography>
<Box className={styles.inputBlock}>
<MuiFileInput
placeholder="Select file"
inputProps={{ accept: '.sigit.zip' }}
value={selectedFile}
onChange={(value) => setSelectedFile(value)}
/>
</Box>
{selectedFile && (
<Box sx={{ mt: 2, display: 'flex', justifyContent: 'center' }}>
<Button onClick={handleDecrypt} variant="contained">
Decrypt
</Button>
</Box>
)}
</>
)}
{submittedBy && Object.entries(files).length > 0 && meta && (
<>
<DisplayMeta
meta={meta}
files={files}
submittedBy={submittedBy}
signers={signers}
viewers={viewers}
creatorFileHashes={creatorFileHashes}
currentFileHashes={currentFileHashes}
signedBy={signedBy}
nextSigner={nextSinger}
getPrevSignersSig={getPrevSignersSig}
/>
{signedStatus === SignedStatus.Fully_Signed && (
<Box sx={{ mt: 1, display: 'flex', justifyContent: 'center' }}>
<Button onClick={handleExport} variant="contained">
Export
</Button>
</Box>
)}
{signedStatus === SignedStatus.User_Is_Next_Signer && (
<Box sx={{ mt: 1, display: 'flex', justifyContent: 'center' }}>
<Button onClick={handleSign} variant="contained">
Sign
</Button>
</Box>
)}
{isSignerOrCreator && (
<Box sx={{ mt: 1, display: 'flex', justifyContent: 'center' }}>
<Button onClick={handleExportSigit} variant="contained">
Export Sigit
</Button>
</Box>
)}
</>
)}
</Container>
</>
)
}

View File

@ -0,0 +1,23 @@
import { Jsonable } from '.'
// Reuse common error messages for meta parsing
export enum MetaParseErrorType {
'PARSE_ERROR_EVENT' = 'error occurred in parsing the create signature event',
'PARSE_ERROR_SIGNATURE_EVENT_CONTENT' = "err in parsing the createSignature event's content"
}
export class MetaParseError extends Error {
public readonly context?: Jsonable
constructor(
message: string,
options: { cause?: Error; context?: Jsonable } = {}
) {
const { cause, context } = options
super(message, { cause })
this.name = this.constructor.name
this.context = context
}
}

29
src/types/errors/index.ts Normal file
View File

@ -0,0 +1,29 @@
export type Jsonable =
| string
| number
| boolean
| null
| undefined
| readonly Jsonable[]
| { readonly [key: string]: Jsonable }
| { toJSON(): Jsonable }
/**
* Handle errors
* Wraps the errors without message property and stringify to a message so we can use it later
* @param error
* @returns
*/
export function handleError(error: unknown): Error {
if (error instanceof Error) return error
// No message error, wrap it and stringify
let stringified = 'Unable to stringify the thrown value'
try {
stringified = JSON.stringify(error)
} catch (error) {
console.error(stringified, error)
}
return new Error(`Wrapped Error: ${stringified}`)
}

View File

@ -47,7 +47,7 @@ const filterMarksByPubkey = (marks: Mark[], pubkey: string): Mark[] => {
/**
* Takes Signed Doc Signatures part of Meta and extracts
* all Marks into one flar array, regardless of the user.
* all Marks into one flat array, regardless of the user.
* @param meta
*/
const extractMarksFromSignedMeta = (meta: Meta): Mark[] => {

View File

@ -3,6 +3,11 @@ import { fromUnixTimestamp, parseJson } from '.'
import { Event, verifyEvent } from 'nostr-tools'
import { toast } from 'react-toastify'
import { extractFileExtensions } from './file'
import { handleError } from '../types/errors'
import {
MetaParseError,
MetaParseErrorType
} from '../types/errors/MetaParseError'
export enum SignStatus {
Signed = 'Signed',
@ -17,58 +22,6 @@ export enum SigitStatus {
Complete = 'Completed'
}
type Jsonable =
| string
| number
| boolean
| null
| undefined
| readonly Jsonable[]
| { readonly [key: string]: Jsonable }
| { toJSON(): Jsonable }
export class SigitMetaParseError extends Error {
public readonly context?: Jsonable
constructor(
message: string,
options: { cause?: Error; context?: Jsonable } = {}
) {
const { cause, context } = options
super(message, { cause })
this.name = this.constructor.name
this.context = context
}
}
/**
* Handle meta errors
* Wraps the errors without message property and stringify to a message so we can use it later
* @param error
* @returns
*/
function handleError(error: unknown): Error {
if (error instanceof Error) return error
// No message error, wrap it and stringify
let stringified = 'Unable to stringify the thrown value'
try {
stringified = JSON.stringify(error)
} catch (error) {
console.error(stringified, error)
}
return new Error(`[SiGit Error]: ${stringified}`)
}
// Reuse common error messages for meta parsing
export enum SigitMetaParseErrorType {
'PARSE_ERROR_EVENT' = 'error occurred in parsing the create signature event',
'PARSE_ERROR_SIGNATURE_EVENT_CONTENT' = "err in parsing the createSignature event's content"
}
export interface SigitCardDisplayInfo {
createdAt?: number
title?: string
@ -89,7 +42,7 @@ export const parseNostrEvent = async (raw: string): Promise<Event> => {
const event = await parseJson<Event>(raw)
return event
} catch (error) {
throw new SigitMetaParseError(SigitMetaParseErrorType.PARSE_ERROR_EVENT, {
throw new MetaParseError(MetaParseErrorType.PARSE_ERROR_EVENT, {
cause: handleError(error),
context: raw
})
@ -109,8 +62,8 @@ export const parseCreateSignatureEventContent = async (
await parseJson<CreateSignatureEventContent>(raw)
return createSignatureEventContent
} catch (error) {
throw new SigitMetaParseError(
SigitMetaParseErrorType.PARSE_ERROR_SIGNATURE_EVENT_CONTENT,
throw new MetaParseError(
MetaParseErrorType.PARSE_ERROR_SIGNATURE_EVENT_CONTENT,
{
cause: handleError(error),
context: raw
@ -165,7 +118,7 @@ export const extractSigitCardDisplayInfo = async (meta: Meta) => {
return sigitInfo
} catch (error) {
if (error instanceof SigitMetaParseError) {
if (error instanceof MetaParseError) {
toast.error(error.message)
console.error(error.name, error.message, error.cause, error.context)
} else {

View File

@ -31,4 +31,16 @@ const getLastSignersSig = (
}
}
export { getLastSignersSig }
/**
* Checks if all signers have signed the sigit
* @param signers - an array of npubs of all signers from the Sigit
* @param signedBy - an array of npubs that have signed it already
*/
const isFullySigned = (
signers: `npub1${string}`[],
signedBy: `npub1${string}`[]
): boolean => {
return signers.every((signer) => signedBy.includes(signer))
}
export { getLastSignersSig, isFullySigned }