fix: when decrypting file, have better error messages #17

Merged
b merged 11 commits from issue-11 into main 2024-05-14 15:09:01 +00:00
8 changed files with 111 additions and 15 deletions

13
package-lock.json generated
View File

@ -17,6 +17,7 @@
"@reduxjs/toolkit": "2.2.1", "@reduxjs/toolkit": "2.2.1",
"axios": "1.6.7", "axios": "1.6.7",
"crypto-hash": "3.0.0", "crypto-hash": "3.0.0",
"crypto-js": "^4.2.0",
"file-saver": "2.0.5", "file-saver": "2.0.5",
"jszip": "3.10.1", "jszip": "3.10.1",
"lodash": "4.17.21", "lodash": "4.17.21",
@ -31,6 +32,7 @@
"tseep": "1.2.1" "tseep": "1.2.1"
}, },
"devDependencies": { "devDependencies": {
"@types/crypto-js": "^4.2.2",
"@types/file-saver": "2.0.7", "@types/file-saver": "2.0.7",
"@types/lodash": "4.14.202", "@types/lodash": "4.14.202",
"@types/react": "^18.2.56", "@types/react": "^18.2.56",
@ -1938,6 +1940,12 @@
"@babel/types": "^7.20.7" "@babel/types": "^7.20.7"
} }
}, },
"node_modules/@types/crypto-js": {
"version": "4.2.2",
"resolved": "https://registry.npmjs.org/@types/crypto-js/-/crypto-js-4.2.2.tgz",
"integrity": "sha512-sDOLlVbHhXpAUAL0YHDUUwDZf3iN4Bwi4W6a0W0b+QcAezUbRtH4FVb+9J4h+XFPW7l/gQ9F8qC7P+Ec4k8QVQ==",
"dev": true
},
"node_modules/@types/estree": { "node_modules/@types/estree": {
"version": "1.0.5", "version": "1.0.5",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz",
@ -2630,6 +2638,11 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/crypto-js": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz",
"integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q=="
},
"node_modules/csstype": { "node_modules/csstype": {
"version": "3.1.3", "version": "3.1.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",

View File

@ -8,6 +8,7 @@
"dev": "vite", "dev": "vite",
"build": "tsc && vite build", "build": "tsc && vite build",
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
"lint:fix": "eslint . --fix --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
"preview": "vite preview" "preview": "vite preview"
}, },
"dependencies": { "dependencies": {
@ -20,6 +21,7 @@
"@reduxjs/toolkit": "2.2.1", "@reduxjs/toolkit": "2.2.1",
"axios": "1.6.7", "axios": "1.6.7",
"crypto-hash": "3.0.0", "crypto-hash": "3.0.0",
"crypto-js": "^4.2.0",
"file-saver": "2.0.5", "file-saver": "2.0.5",
"jszip": "3.10.1", "jszip": "3.10.1",
"lodash": "4.17.21", "lodash": "4.17.21",
@ -34,6 +36,7 @@
"tseep": "1.2.1" "tseep": "1.2.1"
}, },
"devDependencies": { "devDependencies": {
"@types/crypto-js": "^4.2.2",
"@types/file-saver": "2.0.7", "@types/file-saver": "2.0.7",
"@types/lodash": "4.14.202", "@types/lodash": "4.14.202",
"@types/react": "^18.2.56", "@types/react": "^18.2.56",

View File

@ -8,6 +8,7 @@ import styles from './style.module.scss'
import { toast } from 'react-toastify' import { toast } from 'react-toastify'
import { useSearchParams } from 'react-router-dom' import { useSearchParams } from 'react-router-dom'
import axios from 'axios' import axios from 'axios'
import { DecryptionError } from '../../types/errors/DecryptionError'
export const DecryptZip = () => { export const DecryptZip = () => {
const [searchParams] = useSearchParams() const [searchParams] = useSearchParams()
@ -60,9 +61,10 @@ export const DecryptZip = () => {
const arrayBuffer = await decryptArrayBuffer( const arrayBuffer = await decryptArrayBuffer(
encryptedArrayBuffer, encryptedArrayBuffer,
encryptionKey encryptionKey
).catch((err) => { ).catch((err: DecryptionError) => {
console.log('err in decryption:>> ', err) console.log('err in decryption:>> ', err)
toast.error(err.message || 'An error occurred while decrypting file.')
m marked this conversation as resolved
Review

I would say that a more convenient and reliable approach to handle errors is to have dedicated errors of custom type

I would say that a more convenient and reliable approach to handle errors is to have dedicated errors of custom type
toast.error(err.message)
m marked this conversation as resolved Outdated
Outdated
Review

The message should be The decryption key length or format is invalid.

The message should be `The decryption key length or format is invalid.`
setIsLoading(false) setIsLoading(false)
return null return null
m marked this conversation as resolved Outdated
Outdated
Review

The message should be The decryption key is invalid.

The message should be `The decryption key is invalid.`
}) })

View File

@ -1,9 +1,13 @@
import ContentCopyIcon from '@mui/icons-material/ContentCopy' import ContentCopyIcon from '@mui/icons-material/ContentCopy'
import { import {
CircularProgress,
IconButton,
InputProps,
List, List,
ListItem, ListItem,
ListSubheader, ListSubheader,
TextField, TextField,
Tooltip,
Typography, Typography,
useTheme useTheme
} from '@mui/material' } from '@mui/material'
@ -22,6 +26,7 @@ import { Dispatch } from '../../store/store'
import { setMetadataEvent } from '../../store/actions' import { setMetadataEvent } from '../../store/actions'
import { LoadingSpinner } from '../../components/LoadingSpinner' import { LoadingSpinner } from '../../components/LoadingSpinner'
import { LoginMethods } from '../../store/auth/types' import { LoginMethods } from '../../store/auth/types'
import { SmartToy } from '@mui/icons-material'
export const ProfilePage = () => { export const ProfilePage = () => {
const theme = useTheme() const theme = useTheme()
@ -38,6 +43,7 @@ export const ProfilePage = () => {
useState<NostrJoiningBlock | null>(null) useState<NostrJoiningBlock | null>(null)
const [profileMetadata, setProfileMetadata] = useState<ProfileMetadata>() const [profileMetadata, setProfileMetadata] = useState<ProfileMetadata>()
const [savingProfileMetadata, setSavingProfileMetadata] = useState(false) const [savingProfileMetadata, setSavingProfileMetadata] = useState(false)
const [avatarLoading, setAvatarLoading] = useState(false)
const metadataState = useSelector((state: State) => state.metadata) const metadataState = useSelector((state: State) => state.metadata)
const keys = useSelector((state: State) => state.auth?.keyPair) const keys = useSelector((state: State) => state.auth?.keyPair)
const { usersPubkey, loginMethod } = useSelector((state: State) => state.auth) const { usersPubkey, loginMethod } = useSelector((state: State) => state.auth)
@ -97,7 +103,9 @@ export const ProfilePage = () => {
if (metadataEvent) { if (metadataEvent) {
const metadataContent = const metadataContent =
metadataController.extractProfileMetadataContent(metadataEvent) metadataController.extractProfileMetadataContent(metadataEvent)
if (metadataContent) setProfileMetadata(metadataContent) if (metadataContent) {
setProfileMetadata(metadataContent)
}
} }
setIsLoading(false) setIsLoading(false)
@ -111,7 +119,8 @@ export const ProfilePage = () => {
key: keyof ProfileMetadata, key: keyof ProfileMetadata,
label: string, label: string,
multiline = false, multiline = false,
rows = 1 rows = 1,
inputProps?: InputProps
) => ( ) => (
<ListItem sx={{ marginTop: 1 }}> <ListItem sx={{ marginTop: 1 }}>
<TextField <TextField
@ -123,6 +132,7 @@ export const ProfilePage = () => {
rows={rows} rows={rows}
className={styles.textField} className={styles.textField}
disabled={!isUsersOwnProfile} disabled={!isUsersOwnProfile}
InputProps={inputProps}
onChange={(event: React.ChangeEvent<HTMLInputElement>) => { onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
const { value } = event.target const { value } = event.target
@ -201,6 +211,39 @@ export const ProfilePage = () => {
setSavingProfileMetadata(false) setSavingProfileMetadata(false)
} }
const generateRobotAvatar = () => {
setAvatarLoading(true)
const robotAvatarLink = `https://robohash.org/${npub}.png?set=set3`
setProfileMetadata((prev) => ({
...prev,
picture: ''
}))
setTimeout(() => {
setProfileMetadata((prev) => ({
...prev,
picture: robotAvatarLink
}))
})
}
/**
*
* @returns robohash generate button, loading spinner or no button
*/
const robohashButton = () => {
if (profileMetadata?.picture?.includes('robohash')) return null
return <Tooltip title="Generate a robohash avatar">
{avatarLoading ? <CircularProgress style={{padding: 8}} size={22}/>
: <IconButton onClick={generateRobotAvatar}>
<SmartToy/>
</IconButton>}
</Tooltip>
}
return ( return (
<> <>
{isLoading && <LoadingSpinner desc={loadingSpinnerDesc} />} {isLoading && <LoadingSpinner desc={loadingSpinnerDesc} />}
@ -236,10 +279,14 @@ export const ProfilePage = () => {
onError={(event: any) => { onError={(event: any) => {
event.target.src = placeholderAvatar event.target.src = placeholderAvatar
}} }}
onLoad={() => {
setAvatarLoading(false)
}}
className={styles.img} className={styles.img}
src={profileMetadata.picture || placeholderAvatar} src={profileMetadata.picture || placeholderAvatar}
alt='Profile Image' alt='Profile Image'
/> />
{nostrJoiningBlock && ( {nostrJoiningBlock && (
<Typography <Typography
sx={{ sx={{
@ -256,11 +303,16 @@ export const ProfilePage = () => {
)} )}
</ListItem> </ListItem>
{editItem('picture', 'Picture URL', undefined, undefined, {
endAdornment: robohashButton()
})}
{editItem('name', 'Username')} {editItem('name', 'Username')}
{editItem('display_name', 'Display Name')} {editItem('display_name', 'Display Name')}
{editItem('nip05', 'Nostr Address (nip05)')} {editItem('nip05', 'Nostr Address (nip05)')}
{editItem('lud16', 'Lightning Address (lud16)')} {editItem('lud16', 'Lightning Address (lud16)')}
{editItem('about', 'About', true, 4)} {editItem('about', 'About', true, 4)}
{editItem('website', 'Website')}
{isUsersOwnProfile && ( {isUsersOwnProfile && (
<> <>
{usersPubkey && {usersPubkey &&

View File

@ -0,0 +1,20 @@
export class DecryptionError extends Error {
public message: string = ''
constructor(
public inputError: any
) {
super()
if (inputError.message.toLowerCase().includes('expected')) {
this.message = `The decryption key length or format is invalid.`
} else if (inputError.message.includes('The JWK "alg" member was inconsistent')) {
this.message = `The decryption key is invalid.`
} else {
this.message = inputError.message || 'An error occurred while decrypting file.'
}
this.name = 'DecryptionError'
Object.setPrototypeOf(this, DecryptionError.prototype)
}
}

View File

@ -3,6 +3,7 @@ export interface ProfileMetadata {
display_name?: string display_name?: string
picture?: string picture?: string
about?: string about?: string
website?: string
nip05?: string nip05?: string
lud16?: string lud16?: string
} }

View File

@ -4,6 +4,7 @@ import {
stringToHex, stringToHex,
uint8ArrayToHexString uint8ArrayToHexString
} from '.' } from '.'
import { DecryptionError } from '../types/errors/DecryptionError'
const ENCRYPTION_ALGO_NAME = 'AES-GCM' const ENCRYPTION_ALGO_NAME = 'AES-GCM'
@ -63,6 +64,7 @@ export const decryptArrayBuffer = async (
encryptedData: ArrayBuffer, encryptedData: ArrayBuffer,
key: string key: string
) => { ) => {
try {
const { cryptoKey, iv } = await importKey(key) const { cryptoKey, iv } = await importKey(key)
// Decrypt the data // Decrypt the data
@ -73,4 +75,7 @@ export const decryptArrayBuffer = async (
) )
return decryptedData return decryptedData
} catch (err) {
throw new DecryptionError(err)
}
} }

View File

@ -92,7 +92,7 @@ export const sendDM = async (
const timeoutPromise = new Promise<never>((_, reject) => { const timeoutPromise = new Promise<never>((_, reject) => {
setTimeout(() => { setTimeout(() => {
reject(new Error('Timeout occurred')) reject(new Error('Timeout occurred'))
}, 15000) // Timeout duration = 15 seconds }, 60000) // Timeout duration = 60 seconds
}) })
// Encrypt the DM content, with timeout // Encrypt the DM content, with timeout