fix: nsec login, metadata overlapping, robohash image in metadata state #49

Merged
b merged 1 commits from issue-23 into main 2024-05-16 14:54:37 +00:00
11 changed files with 132 additions and 28 deletions

62
package-lock.json generated
View File

@ -13,6 +13,7 @@
"@mui/icons-material": "5.15.11", "@mui/icons-material": "5.15.11",
"@mui/lab": "5.0.0-alpha.166", "@mui/lab": "5.0.0-alpha.166",
"@mui/material": "5.15.11", "@mui/material": "5.15.11",
"@noble/hashes": "^1.4.0",
"@nostr-dev-kit/ndk": "2.5.0", "@nostr-dev-kit/ndk": "2.5.0",
"@reduxjs/toolkit": "2.2.1", "@reduxjs/toolkit": "2.2.1",
"axios": "1.6.7", "axios": "1.6.7",
@ -1505,9 +1506,9 @@
} }
}, },
"node_modules/@noble/hashes": { "node_modules/@noble/hashes": {
"version": "1.3.1", "version": "1.4.0",
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.1.tgz", "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.4.0.tgz",
"integrity": "sha512-EbqwksQwz9xDRGfDST86whPBgM65E0OH/pCgqW0GBVzO22bNE+NuIbeTb714+IfSjU3aRk47EUvXIb5bTsenKA==", "integrity": "sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==",
"engines": { "engines": {
"node": ">= 16" "node": ">= 16"
}, },
@ -1598,6 +1599,17 @@
"url": "https://paulmillr.com/funding/" "url": "https://paulmillr.com/funding/"
} }
}, },
"node_modules/@nostr-dev-kit/ndk/node_modules/@noble/hashes": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.1.tgz",
"integrity": "sha512-EbqwksQwz9xDRGfDST86whPBgM65E0OH/pCgqW0GBVzO22bNE+NuIbeTb714+IfSjU3aRk47EUvXIb5bTsenKA==",
"engines": {
"node": ">= 16"
},
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/@nostr-dev-kit/ndk/node_modules/nostr-tools": { "node_modules/@nostr-dev-kit/ndk/node_modules/nostr-tools": {
"version": "1.17.0", "version": "1.17.0",
"resolved": "https://registry.npmjs.org/nostr-tools/-/nostr-tools-1.17.0.tgz", "resolved": "https://registry.npmjs.org/nostr-tools/-/nostr-tools-1.17.0.tgz",
@ -1863,6 +1875,28 @@
"url": "https://paulmillr.com/funding/" "url": "https://paulmillr.com/funding/"
} }
}, },
"node_modules/@scure/bip32/node_modules/@noble/curves/node_modules/@noble/hashes": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.1.tgz",
"integrity": "sha512-EbqwksQwz9xDRGfDST86whPBgM65E0OH/pCgqW0GBVzO22bNE+NuIbeTb714+IfSjU3aRk47EUvXIb5bTsenKA==",
"engines": {
"node": ">= 16"
},
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/@scure/bip32/node_modules/@noble/hashes": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.3.tgz",
"integrity": "sha512-V7/fPHgl+jsVPXqqeOzT8egNj2iBIVt+ECeMMG8TdcnTikP3oaBtUVqpT/gYCR68aEBJSF+XbYUxStjbFMqIIA==",
"engines": {
"node": ">= 16"
},
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/@scure/bip39": { "node_modules/@scure/bip39": {
"version": "1.2.1", "version": "1.2.1",
"resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.2.1.tgz", "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.2.1.tgz",
@ -1875,6 +1909,17 @@
"url": "https://paulmillr.com/funding/" "url": "https://paulmillr.com/funding/"
} }
}, },
"node_modules/@scure/bip39/node_modules/@noble/hashes": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.3.tgz",
"integrity": "sha512-V7/fPHgl+jsVPXqqeOzT8egNj2iBIVt+ECeMMG8TdcnTikP3oaBtUVqpT/gYCR68aEBJSF+XbYUxStjbFMqIIA==",
"engines": {
"node": ">= 16"
},
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/@tsconfig/node10": { "node_modules/@tsconfig/node10": {
"version": "1.0.9", "version": "1.0.9",
"resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz", "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz",
@ -4027,6 +4072,17 @@
} }
} }
}, },
"node_modules/nostr-tools/node_modules/@noble/hashes": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.1.tgz",
"integrity": "sha512-EbqwksQwz9xDRGfDST86whPBgM65E0OH/pCgqW0GBVzO22bNE+NuIbeTb714+IfSjU3aRk47EUvXIb5bTsenKA==",
"engines": {
"node": ">= 16"
},
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/nostr-wasm": { "node_modules/nostr-wasm": {
"version": "0.1.0", "version": "0.1.0",
"resolved": "https://registry.npmjs.org/nostr-wasm/-/nostr-wasm-0.1.0.tgz", "resolved": "https://registry.npmjs.org/nostr-wasm/-/nostr-wasm-0.1.0.tgz",

View File

@ -19,6 +19,7 @@
"@mui/icons-material": "5.15.11", "@mui/icons-material": "5.15.11",
"@mui/lab": "5.0.0-alpha.166", "@mui/lab": "5.0.0-alpha.166",
"@mui/material": "5.15.11", "@mui/material": "5.15.11",
"@noble/hashes": "^1.4.0",
"@nostr-dev-kit/ndk": "2.5.0", "@nostr-dev-kit/ndk": "2.5.0",
"@reduxjs/toolkit": "2.2.1", "@reduxjs/toolkit": "2.2.1",
"axios": "1.6.7", "axios": "1.6.7",

View File

@ -10,22 +10,24 @@ import {
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import { useDispatch, useSelector } from 'react-redux' import { useDispatch, useSelector } from 'react-redux'
import { setAuthState } from '../../store/actions' import { setAuthState, setMetadataEvent } from '../../store/actions'
import { State } from '../../store/rootReducer' import { State } from '../../store/rootReducer'
import { Dispatch } from '../../store/store' import { Dispatch } from '../../store/store'
import Username from '../username' import Username from '../username'
import { Link, useNavigate } from 'react-router-dom' import { Link, useNavigate } from 'react-router-dom'
import { NostrController } from '../../controllers' import { MetadataController, NostrController } from '../../controllers'
import { appPublicRoutes, getProfileRoute } from '../../routes' import { appPublicRoutes, getProfileRoute } from '../../routes'
import { import {
clearAuthToken, clearAuthToken,
getRoboHashPicture, clearState,
saveNsecBunkerDelegatedKey, saveNsecBunkerDelegatedKey,
shorten shorten
} from '../../utils' } from '../../utils'
import styles from './style.module.scss' import styles from './style.module.scss'
const metadataController = new MetadataController()
export const AppBar = () => { export const AppBar = () => {
const navigate = useNavigate() const navigate = useNavigate()
@ -39,17 +41,19 @@ export const AppBar = () => {
const metadataState = useSelector((state: State) => state.metadata) const metadataState = useSelector((state: State) => state.metadata)
useEffect(() => { useEffect(() => {
if (metadataState && metadataState.content) { if (metadataState) {
if (metadataState.content) {
const { picture, display_name, name } = JSON.parse(metadataState.content) const { picture, display_name, name } = JSON.parse(metadataState.content)
const pubkey = authState?.usersPubkey || ''
if (picture) { if (picture) {
setUserAvatar(picture) setUserAvatar(picture)
} else {
setUserAvatar(getRoboHashPicture(pubkey))
} }
setUsername(shorten(display_name || name || '', 7)) setUsername(shorten(display_name || name || '', 7))
} else {
setUserAvatar('')
setUsername('')
}
} }
}, [metadataState]) }, [metadataState])
@ -72,6 +76,7 @@ export const AppBar = () => {
handleCloseUserMenu() handleCloseUserMenu()
dispatch( dispatch(
setAuthState({ setAuthState({
keyPair: undefined,
loggedIn: false, loggedIn: false,
usersPubkey: undefined, usersPubkey: undefined,
loginMethod: undefined, loginMethod: undefined,
@ -79,8 +84,13 @@ export const AppBar = () => {
}) })
) )
dispatch(
setMetadataEvent(metadataController.getEmptyMetadataEvent())
)
// clear authToken saved in local storage // clear authToken saved in local storage
clearAuthToken() clearAuthToken()
clearState()
// update nsecBunker delegated key after logout // update nsecBunker delegated key after logout
const nostrController = NostrController.getInstance() const nostrController = NostrController.getInstance()

View File

@ -6,6 +6,7 @@ import {
base64DecodeAuthToken, base64DecodeAuthToken,
base64EncodeSignedEvent, base64EncodeSignedEvent,
getAuthToken, getAuthToken,
getRoboHashPicture,
getVisitedLink, getVisitedLink,
saveAuthToken saveAuthToken
} from '../utils' } from '../utils'
@ -37,7 +38,15 @@ export class AuthController {
store.dispatch(setMetadataEvent(event)) store.dispatch(setMetadataEvent(event))
}) })
.catch((err) => { .catch((err) => {
console.error('Error occurred while finding metadata', err) console.warn('Error occurred while finding metadata', err)
const emptyMetadata = this.metadataController.getEmptyMetadataEvent()
emptyMetadata.content = JSON.stringify({
picture: getRoboHashPicture(pubkey)
})
store.dispatch(setMetadataEvent(emptyMetadata))
}) })
// Nostr uses unix timestamps // Nostr uses unix timestamps

View File

@ -23,6 +23,18 @@ export class MetadataController {
this.nostrController = NostrController.getInstance() this.nostrController = NostrController.getInstance()
} }
public getEmptyMetadataEvent = (): Event => {
return {
content: '',
created_at: new Date().valueOf(),
id: '',
kind: 0,
pubkey: '',
sig: '',
tags: []
}
}
public findMetadata = async (hexKey: string) => { public findMetadata = async (hexKey: string) => {
const eventFilter: Filter = { const eventFilter: Filter = {
kinds: [kinds.Metadata], kinds: [kinds.Metadata],

View File

@ -319,15 +319,18 @@ export class NostrController extends EventEmitter {
} }
if (loginMethod === LoginMethods.privateKey) { if (loginMethod === LoginMethods.privateKey) {
const keyPair = (store.getState().auth as AuthState).keyPair const keys = (store.getState().auth as AuthState).keyPair
if (!keyPair) { if (!keys) {
throw new Error( throw new Error(
`Login method is ${LoginMethods.privateKey} but private & public key pair is not found.` `Login method is ${LoginMethods.privateKey} but private & public key pair is not found.`
) )
} }
const encrypted = await nip04.encrypt(keyPair.private, receiver, content) const { private: nsec } = keys
const privateKey = nip19.decode(nsec).data as Uint8Array
const encrypted = await nip04.encrypt(privateKey, receiver, content)
return encrypted return encrypted
} }

View File

@ -4,13 +4,15 @@ import { useEffect, useState } from 'react'
import { useDispatch } from 'react-redux' import { useDispatch } from 'react-redux'
import { Outlet } from 'react-router-dom' import { Outlet } from 'react-router-dom'
import { AppBar } from '../components/AppBar/AppBar' import { AppBar } from '../components/AppBar/AppBar'
import { restoreState, setAuthState } from '../store/actions' import { restoreState, setAuthState, setMetadataEvent } from '../store/actions'
import { clearAuthToken, loadState, saveNsecBunkerDelegatedKey } from '../utils' import { clearAuthToken, clearState, loadState, saveNsecBunkerDelegatedKey } from '../utils'
import { LoadingSpinner } from '../components/LoadingSpinner' import { LoadingSpinner } from '../components/LoadingSpinner'
import { Dispatch } from '../store/store' import { Dispatch } from '../store/store'
import { NostrController } from '../controllers' import { MetadataController, NostrController } from '../controllers'
import { LoginMethods } from '../store/auth/types' import { LoginMethods } from '../store/auth/types'
const metadataController = new MetadataController()
export const MainLayout = () => { export const MainLayout = () => {
const dispatch: Dispatch = useDispatch() const dispatch: Dispatch = useDispatch()
const [isLoading, setIsLoading] = useState(true) const [isLoading, setIsLoading] = useState(true)
@ -19,6 +21,7 @@ export const MainLayout = () => {
const logout = () => { const logout = () => {
dispatch( dispatch(
setAuthState({ setAuthState({
keyPair: undefined,
loggedIn: false, loggedIn: false,
usersPubkey: undefined, usersPubkey: undefined,
loginMethod: undefined, loginMethod: undefined,
@ -26,8 +29,13 @@ export const MainLayout = () => {
}) })
) )
dispatch(
setMetadataEvent(metadataController.getEmptyMetadataEvent())
)
// clear authToken saved in local storage // clear authToken saved in local storage
clearAuthToken() clearAuthToken()
clearState()
// update nsecBunker delegated key // update nsecBunker delegated key
const newDelegatedKey = const newDelegatedKey =

View File

@ -489,6 +489,11 @@ const DisplayUser = ({
}) })
}, [users]) }, [users])
/**
* Use robohash if any of the users images fail to load
* @param event img tag onError event
* @param pubkey of the user
*/
const imageLoadError = (event: any, pubkey: string) => { const imageLoadError = (event: any, pubkey: string) => {
event.target.src = getRoboHashPicture(pubkey) event.target.src = getRoboHashPicture(pubkey)
} }

View File

@ -26,7 +26,6 @@ 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' import { SmartToy } from '@mui/icons-material'
import { getRoboHashPicture } from '../../utils'
export const ProfilePage = () => { export const ProfilePage = () => {
const theme = useTheme() const theme = useTheme()
@ -281,14 +280,11 @@ export const ProfilePage = () => {
}} }}
> >
<img <img
onError={(event: any) => {
event.target.src = npub ? getRoboHashPicture(npub) : ''
}}
onLoad={() => { onLoad={() => {
setAvatarLoading(false) setAvatarLoading(false)
}} }}
className={styles.img} className={styles.img}
src={profileMetadata.picture || npub ? getRoboHashPicture(npub!) : ''} src={profileMetadata.picture}
alt="Profile Image" alt="Profile Image"
/> />

View File

@ -115,7 +115,7 @@ export const VerifyPage = () => {
const imageLoadError = (event: any, pubkey: string) => { const imageLoadError = (event: any, pubkey: string) => {
const npub = hexToNpub(pubkey) const npub = hexToNpub(pubkey)
event.target.src = npub event.target.src = getRoboImageUrl(npub)
} }
const getRoboImageUrl = (pubkey: string) => { const getRoboImageUrl = (pubkey: string) => {

View File

@ -22,6 +22,10 @@ export const loadState = (): State | undefined => {
} }
} }
export const clearState = () => {
localStorage.removeItem('state')
}
export const saveNsecBunkerDelegatedKey = (privateKey: string) => { export const saveNsecBunkerDelegatedKey = (privateKey: string) => {
localStorage.setItem('nsecbunker-delegated-key', privateKey) localStorage.setItem('nsecbunker-delegated-key', privateKey)
} }