diff --git a/package-lock.json b/package-lock.json index f1556fa..f5270b3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,6 +13,7 @@ "@mui/icons-material": "5.15.11", "@mui/lab": "5.0.0-alpha.166", "@mui/material": "5.15.11", + "@noble/hashes": "^1.4.0", "@nostr-dev-kit/ndk": "2.5.0", "@reduxjs/toolkit": "2.2.1", "axios": "1.6.7", @@ -1505,9 +1506,9 @@ } }, "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==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.4.0.tgz", + "integrity": "sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==", "engines": { "node": ">= 16" }, @@ -1598,6 +1599,17 @@ "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": { "version": "1.17.0", "resolved": "https://registry.npmjs.org/nostr-tools/-/nostr-tools-1.17.0.tgz", @@ -1863,6 +1875,28 @@ "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": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.2.1.tgz", @@ -1875,6 +1909,17 @@ "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": { "version": "1.0.9", "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": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/nostr-wasm/-/nostr-wasm-0.1.0.tgz", diff --git a/package.json b/package.json index 75a6c85..0eca093 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "@mui/icons-material": "5.15.11", "@mui/lab": "5.0.0-alpha.166", "@mui/material": "5.15.11", + "@noble/hashes": "^1.4.0", "@nostr-dev-kit/ndk": "2.5.0", "@reduxjs/toolkit": "2.2.1", "axios": "1.6.7", diff --git a/src/components/AppBar/AppBar.tsx b/src/components/AppBar/AppBar.tsx index c7326b4..af03c5c 100644 --- a/src/components/AppBar/AppBar.tsx +++ b/src/components/AppBar/AppBar.tsx @@ -10,22 +10,24 @@ import { import { useEffect, useState } from 'react' import { useDispatch, useSelector } from 'react-redux' -import { setAuthState } from '../../store/actions' +import { setAuthState, setMetadataEvent } from '../../store/actions' import { State } from '../../store/rootReducer' import { Dispatch } from '../../store/store' import Username from '../username' import { Link, useNavigate } from 'react-router-dom' -import { NostrController } from '../../controllers' +import { MetadataController, NostrController } from '../../controllers' import { appPublicRoutes, getProfileRoute } from '../../routes' import { clearAuthToken, - getRoboHashPicture, + clearState, saveNsecBunkerDelegatedKey, shorten } from '../../utils' import styles from './style.module.scss' +const metadataController = new MetadataController() + export const AppBar = () => { const navigate = useNavigate() @@ -39,17 +41,19 @@ export const AppBar = () => { const metadataState = useSelector((state: State) => state.metadata) useEffect(() => { - if (metadataState && metadataState.content) { - const { picture, display_name, name } = JSON.parse(metadataState.content) - const pubkey = authState?.usersPubkey || '' - - if (picture) { - setUserAvatar(picture) + if (metadataState) { + if (metadataState.content) { + const { picture, display_name, name } = JSON.parse(metadataState.content) + + if (picture) { + setUserAvatar(picture) + } + + setUsername(shorten(display_name || name || '', 7)) } else { - setUserAvatar(getRoboHashPicture(pubkey)) + setUserAvatar('') + setUsername('') } - - setUsername(shorten(display_name || name || '', 7)) } }, [metadataState]) @@ -72,6 +76,7 @@ export const AppBar = () => { handleCloseUserMenu() dispatch( setAuthState({ + keyPair: undefined, loggedIn: false, usersPubkey: undefined, loginMethod: undefined, @@ -79,8 +84,13 @@ export const AppBar = () => { }) ) + dispatch( + setMetadataEvent(metadataController.getEmptyMetadataEvent()) + ) + // clear authToken saved in local storage clearAuthToken() + clearState() // update nsecBunker delegated key after logout const nostrController = NostrController.getInstance() diff --git a/src/controllers/AuthController.ts b/src/controllers/AuthController.ts index 8e488c5..7e1c00d 100644 --- a/src/controllers/AuthController.ts +++ b/src/controllers/AuthController.ts @@ -6,6 +6,7 @@ import { base64DecodeAuthToken, base64EncodeSignedEvent, getAuthToken, + getRoboHashPicture, getVisitedLink, saveAuthToken } from '../utils' @@ -31,13 +32,45 @@ export class AuthController { * or error if otherwise */ async authenticateAndFindMetadata(pubkey: string) { + const emptyMetadata = this.metadataController.getEmptyMetadataEvent() + + emptyMetadata.content = JSON.stringify({ + picture: getRoboHashPicture(pubkey) + }) + this.metadataController .findMetadata(pubkey) .then((event) => { - store.dispatch(setMetadataEvent(event)) + if (event) { + // In case of NIP05 there is scenario where login content will be populated but without an image + // In such case we will add robohash image + if (event.content) { + const content = JSON.parse(event.content) + + if (!content) { + event.content = '' + } + + if (!content.picture) { + content.picture = getRoboHashPicture(pubkey) + } + + event.content = JSON.stringify(content) + } else { + event.content = JSON.stringify({ + picture: getRoboHashPicture(pubkey) + }) + } + + store.dispatch(setMetadataEvent(event)) + } else { + store.dispatch(setMetadataEvent(emptyMetadata)) + } }) .catch((err) => { - console.error('Error occurred while finding metadata', err) + console.warn('Error occurred while finding metadata', err) + + store.dispatch(setMetadataEvent(emptyMetadata)) }) // Nostr uses unix timestamps diff --git a/src/controllers/MetadataController.ts b/src/controllers/MetadataController.ts index a2a2e5d..f13440d 100644 --- a/src/controllers/MetadataController.ts +++ b/src/controllers/MetadataController.ts @@ -23,6 +23,18 @@ export class MetadataController { 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) => { const eventFilter: Filter = { kinds: [kinds.Metadata], diff --git a/src/controllers/NostrController.ts b/src/controllers/NostrController.ts index 05dd429..7980f67 100644 --- a/src/controllers/NostrController.ts +++ b/src/controllers/NostrController.ts @@ -319,15 +319,18 @@ export class NostrController extends EventEmitter { } 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( `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 } diff --git a/src/layouts/Main.tsx b/src/layouts/Main.tsx index b452354..e258e36 100644 --- a/src/layouts/Main.tsx +++ b/src/layouts/Main.tsx @@ -4,13 +4,15 @@ import { useEffect, useState } from 'react' import { useDispatch } from 'react-redux' import { Outlet } from 'react-router-dom' import { AppBar } from '../components/AppBar/AppBar' -import { restoreState, setAuthState } from '../store/actions' -import { clearAuthToken, loadState, saveNsecBunkerDelegatedKey } from '../utils' +import { restoreState, setAuthState, setMetadataEvent } from '../store/actions' +import { clearAuthToken, clearState, loadState, saveNsecBunkerDelegatedKey } from '../utils' import { LoadingSpinner } from '../components/LoadingSpinner' import { Dispatch } from '../store/store' -import { NostrController } from '../controllers' +import { MetadataController, NostrController } from '../controllers' import { LoginMethods } from '../store/auth/types' +const metadataController = new MetadataController() + export const MainLayout = () => { const dispatch: Dispatch = useDispatch() const [isLoading, setIsLoading] = useState(true) @@ -19,6 +21,7 @@ export const MainLayout = () => { const logout = () => { dispatch( setAuthState({ + keyPair: undefined, loggedIn: false, usersPubkey: undefined, loginMethod: undefined, @@ -26,8 +29,13 @@ export const MainLayout = () => { }) ) + dispatch( + setMetadataEvent(metadataController.getEmptyMetadataEvent()) + ) + // clear authToken saved in local storage clearAuthToken() + clearState() // update nsecBunker delegated key const newDelegatedKey = diff --git a/src/pages/profile/index.tsx b/src/pages/profile/index.tsx index e57e9f7..ca6fbd5 100644 --- a/src/pages/profile/index.tsx +++ b/src/pages/profile/index.tsx @@ -26,7 +26,6 @@ import { setMetadataEvent } from '../../store/actions' import { LoadingSpinner } from '../../components/LoadingSpinner' import { LoginMethods } from '../../store/auth/types' import { SmartToy } from '@mui/icons-material' -import { getRoboHashPicture } from '../../utils' export const ProfilePage = () => { const theme = useTheme() @@ -281,14 +280,11 @@ export const ProfilePage = () => { }} > { - event.target.src = npub ? getRoboHashPicture(npub) : '' - }} onLoad={() => { setAvatarLoading(false) }} className={styles.img} - src={profileMetadata.picture || npub ? getRoboHashPicture(npub!) : ''} + src={profileMetadata.picture} alt="Profile Image" /> diff --git a/src/utils/localStorage.ts b/src/utils/localStorage.ts index 15c17db..92b2db2 100644 --- a/src/utils/localStorage.ts +++ b/src/utils/localStorage.ts @@ -22,6 +22,10 @@ export const loadState = (): State | undefined => { } } +export const clearState = () => { + localStorage.removeItem('state') +} + export const saveNsecBunkerDelegatedKey = (privateKey: string) => { localStorage.setItem('nsecbunker-delegated-key', privateKey) } diff --git a/src/utils/misc.ts b/src/utils/misc.ts index 0b9f994..521b6b5 100644 --- a/src/utils/misc.ts +++ b/src/utils/misc.ts @@ -81,7 +81,7 @@ export const sendDM = async ( encryptionKey )}` - const content = `${initialLine}\n\n${decryptionUrl}\n\nDirect download${fileUrl}` + const content = `${initialLine}\n\n${decryptionUrl}` // Set up event listener for authentication event nostrController.on('nsecbunker-auth', (url) => {