From 0cc1a320599b0153b20ea6fe23ba0e9ea8ea12a6 Mon Sep 17 00:00:00 2001 From: daniyal Date: Fri, 20 Dec 2024 15:25:02 +0500 Subject: [PATCH] chore(refactor): replace authContoller with useAuth hook --- src/App.tsx | 15 +-- src/controllers/AuthController.ts | 153 ------------------------------ src/hooks/index.ts | 2 + src/hooks/useAuth.ts | 127 +++++++++++++++++++++++++ src/layouts/Main.tsx | 42 ++++---- src/pages/nostr/index.tsx | 32 +++---- src/utils/auth.ts | 44 +++++++++ src/utils/dvm.ts | 140 +-------------------------- src/utils/index.ts | 3 +- src/utils/nostr.ts | 23 +---- 10 files changed, 227 insertions(+), 354 deletions(-) delete mode 100644 src/controllers/AuthController.ts create mode 100644 src/hooks/useAuth.ts create mode 100644 src/utils/auth.ts diff --git a/src/App.tsx b/src/App.tsx index 9f58f21..d1f9cc0 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,17 +1,21 @@ import { useEffect } from 'react' -import { useAppSelector } from './hooks' import { Navigate, Route, Routes } from 'react-router-dom' -import { AuthController } from './controllers' + +import { useAppSelector, useAuth } from './hooks' + import { MainLayout } from './layouts/Main' + import { appPrivateRoutes, appPublicRoutes } from './routes' -import './App.scss' import { privateRoutes, publicRoutes, recursiveRouteRenderer } from './routes/util' +import './App.scss' + const App = () => { + const { checkSession } = useAuth() const authState = useAppSelector((state) => state.auth) useEffect(() => { @@ -22,9 +26,8 @@ const App = () => { window.location.hostname = 'localhost' } - const authController = new AuthController() - authController.checkSession() - }, []) + checkSession() + }, [checkSession]) const handleRootRedirect = () => { if (authState.loggedIn) return appPrivateRoutes.homePage diff --git a/src/controllers/AuthController.ts b/src/controllers/AuthController.ts deleted file mode 100644 index 9cdf85a..0000000 --- a/src/controllers/AuthController.ts +++ /dev/null @@ -1,153 +0,0 @@ -import { EventTemplate } from 'nostr-tools' -import { MetadataController, NostrController } from '.' -import { appPrivateRoutes } from '../routes' -import { - setAuthState, - setMetadataEvent, - setRelayMapAction -} from '../store/actions' -import store from '../store/store' -import { SignedEvent } from '../types' -import { - base64DecodeAuthToken, - base64EncodeSignedEvent, - compareObjects, - getAuthToken, - getRelayMap, - saveAuthToken, - unixNow -} from '../utils' - -export class AuthController { - private nostrController: NostrController - private metadataController: MetadataController - - constructor() { - this.nostrController = NostrController.getInstance() - this.metadataController = MetadataController.getInstance() - } - - /** - * Function will authenticate user by signing an auth event - * which is done by calling the sign() function, where appropriate - * method will be chosen (extension or keys) - * - * @param pubkey of the user trying to login - * @returns url to redirect if authentication successfull - * or error if otherwise - */ - async authAndGetMetadataAndRelaysMap(pubkey: string) { - const emptyMetadata = this.metadataController.getEmptyMetadataEvent() - - this.metadataController - .findMetadata(pubkey) - .then((event) => { - if (event) { - store.dispatch(setMetadataEvent(event)) - } else { - store.dispatch(setMetadataEvent(emptyMetadata)) - } - }) - .catch((err) => { - console.warn('Error occurred while finding metadata', err) - - store.dispatch(setMetadataEvent(emptyMetadata)) - }) - - // Nostr uses unix timestamps - const timestamp = unixNow() - const { href } = window.location - - const authEvent: EventTemplate = { - kind: 27235, - tags: [ - ['u', href], - ['method', 'GET'] - ], - content: '', - created_at: timestamp - } - - const signedAuthEvent = await this.nostrController.signEvent(authEvent) - this.createAndSaveAuthToken(signedAuthEvent) - - store.dispatch( - setAuthState({ - loggedIn: true, - usersPubkey: pubkey - }) - ) - - const relayMap = await getRelayMap(pubkey) - - if (Object.keys(relayMap).length < 1) { - // Navigate user to relays page if relay map is empty - return Promise.resolve(appPrivateRoutes.relays) - } - - if (store.getState().auth.loggedIn) { - if (!compareObjects(store.getState().relays?.map, relayMap.map)) - store.dispatch(setRelayMapAction(relayMap.map)) - } - - /** - * This block was added before we started using the `nostr-login` package - * At this point it seems it's not needed anymore and it's even blocking the flow (reloading on /verify) - * TODO to remove this if app works fine - */ - // const currentLocation = window.location.hash.replace('#', '') - - // if (!Object.values(appPrivateRoutes).includes(currentLocation)) { - // // Since verify is both public and private route, we don't use the `visitedLink` - // // value for it. Otherwise, when linking to /verify/:id we get redirected - // // to the root `/` - // if (currentLocation.includes(appPublicRoutes.verify)) { - // return Promise.resolve(currentLocation) - // } - // - // // User did change the location to one of the private routes - // const visitedLink = getVisitedLink() - // - // if (visitedLink) { - // const { pathname, search } = visitedLink - // - // return Promise.resolve(`${pathname}${search}`) - // } else { - // // Navigate user in - // return Promise.resolve(appPrivateRoutes.homePage) - // } - // } - } - - checkSession() { - const savedAuthToken = getAuthToken() - - if (savedAuthToken) { - const signedEvent = base64DecodeAuthToken(savedAuthToken) - - store.dispatch( - setAuthState({ - loggedIn: true, - usersPubkey: signedEvent.pubkey - }) - ) - - return - } - - store.dispatch( - setAuthState({ - loggedIn: false, - usersPubkey: undefined - }) - ) - } - - private createAndSaveAuthToken(signedAuthEvent: SignedEvent) { - const base64Encoded = base64EncodeSignedEvent(signedAuthEvent) - - // save newly created auth token (base64 nostr singed event) in local storage along with expiry time - saveAuthToken(base64Encoded) - return base64Encoded - } -} diff --git a/src/hooks/index.ts b/src/hooks/index.ts index 91dc278..e0c77b0 100644 --- a/src/hooks/index.ts +++ b/src/hooks/index.ts @@ -1,4 +1,6 @@ export * from './store' +export * from './useAuth' export * from './useDidMount' export * from './useDvm' +export * from './useLogout' export * from './useNDKContext' diff --git a/src/hooks/useAuth.ts b/src/hooks/useAuth.ts new file mode 100644 index 0000000..4ad0cd0 --- /dev/null +++ b/src/hooks/useAuth.ts @@ -0,0 +1,127 @@ +import { Event, EventTemplate } from 'nostr-tools' +import { useCallback } from 'react' +import { NostrController } from '../controllers' +import { appPrivateRoutes } from '../routes' +import { + setAuthState, + setMetadataEvent, + setRelayMapAction +} from '../store/actions' +import { + base64DecodeAuthToken, + compareObjects, + createAndSaveAuthToken, + getAuthToken, + getEmptyMetadataEvent, + getRelayMap, + unixNow +} from '../utils' +import { useAppDispatch, useAppSelector } from './store' +import { useNDKContext } from './useNDKContext' + +export const useAuth = () => { + const dispatch = useAppDispatch() + const { findMetadata } = useNDKContext() + + const { auth: authState, relays: relaysState } = useAppSelector( + (state) => state + ) + + const checkSession = useCallback(() => { + const savedAuthToken = getAuthToken() + + if (savedAuthToken) { + const signedEvent = base64DecodeAuthToken(savedAuthToken) + + dispatch( + setAuthState({ + loggedIn: true, + usersPubkey: signedEvent.pubkey + }) + ) + return + } + + dispatch( + setAuthState({ + loggedIn: false, + usersPubkey: undefined + }) + ) + }, [dispatch]) + + /** + * Function will authenticate user by signing an auth event + * which is done by calling the sign() function, where appropriate + * method will be chosen (extension or keys) + * + * @param pubkey of the user trying to login + * @returns url to redirect if authentication successfull + * or error if otherwise + */ + const authAndGetMetadataAndRelaysMap = useCallback( + async (pubkey: string) => { + const emptyMetadata = getEmptyMetadataEvent() + + try { + const profile = await findMetadata(pubkey, {}, true) + + if (profile && profile.profileEvent) { + const event: Event = JSON.parse(profile.profileEvent) + dispatch(setMetadataEvent(event)) + } else { + dispatch(setMetadataEvent(emptyMetadata)) + } + } catch (err) { + console.warn('Error occurred while finding metadata', err) + dispatch(setMetadataEvent(emptyMetadata)) + } + + const timestamp = unixNow() + const { href } = window.location + + const authEvent: EventTemplate = { + kind: 27235, + tags: [ + ['u', href], + ['method', 'GET'] + ], + content: '', + created_at: timestamp + } + + const nostrController = NostrController.getInstance() + const signedAuthEvent = await nostrController.signEvent(authEvent) + createAndSaveAuthToken(signedAuthEvent) + + dispatch( + setAuthState({ + loggedIn: true, + usersPubkey: pubkey + }) + ) + + const relayMap = await getRelayMap(pubkey) + + if (Object.keys(relayMap).length < 1) { + // Navigate user to relays page if relay map is empty + return appPrivateRoutes.relays + } + + if ( + authState.loggedIn && + !compareObjects(relaysState?.map, relayMap.map) + ) { + dispatch(setRelayMapAction(relayMap.map)) + } + + return appPrivateRoutes.homePage + }, + [dispatch, findMetadata, authState, relaysState] + ) + + return { + authAndGetMetadataAndRelaysMap, + checkSession + } +} diff --git a/src/layouts/Main.tsx b/src/layouts/Main.tsx index 19ac4d9..2738bcb 100644 --- a/src/layouts/Main.tsx +++ b/src/layouts/Main.tsx @@ -1,13 +1,18 @@ -import { Event, getPublicKey, kinds, nip19 } from 'nostr-tools' import { useCallback, useEffect, useRef, useState } from 'react' import { Outlet, useNavigate, useSearchParams } from 'react-router-dom' + +import { Event, getPublicKey, kinds, nip19 } from 'nostr-tools' + +import { init as initNostrLogin } from 'nostr-login' +import { NostrLoginAuthOptions } from 'nostr-login/dist/types' + import { AppBar } from '../components/AppBar/AppBar' import { LoadingSpinner } from '../components/LoadingSpinner' -import { - AuthController, - MetadataController, - NostrController -} from '../controllers' + +import { MetadataController, NostrController } from '../controllers' + +import { useAppDispatch, useAppSelector, useAuth, useLogout } from '../hooks' + import { restoreState, setMetadataEvent, @@ -16,25 +21,25 @@ import { updateNostrLoginAuthMethod, updateUserAppData } from '../store/actions' +import { LoginMethod } from '../store/auth/types' import { setUserRobotImage } from '../store/userRobotImage/action' + import { getRoboHashPicture, getUsersAppData, loadState, subscribeForSigits } from '../utils' -import { useAppDispatch, useAppSelector } from '../hooks' + import styles from './style.module.scss' -import { useLogout } from '../hooks/useLogout' -import { LoginMethod } from '../store/auth/types' -import { NostrLoginAuthOptions } from 'nostr-login/dist/types' -import { init as initNostrLogin } from 'nostr-login' export const MainLayout = () => { const [searchParams, setSearchParams] = useSearchParams() const navigate = useNavigate() const dispatch = useAppDispatch() const logout = useLogout() + const { authAndGetMetadataAndRelaysMap } = useAuth() + const [isLoading, setIsLoading] = useState(true) const [loadingSpinnerDesc, setLoadingSpinnerDesc] = useState(`Loading App`) const isLoggedIn = useAppSelector((state) => state.auth?.loggedIn) @@ -59,13 +64,11 @@ export const MainLayout = () => { const login = useCallback(async () => { const nostrController = NostrController.getInstance() - const authController = new AuthController() const pubkey = await nostrController.capturePublicKey() dispatch(updateLoginMethod(LoginMethod.nostrLogin)) - const redirectPath = - await authController.authAndGetMetadataAndRelaysMap(pubkey) + const redirectPath = await authAndGetMetadataAndRelaysMap(pubkey) if (redirectPath) { navigateAfterLogin(redirectPath) @@ -105,13 +108,10 @@ export const MainLayout = () => { ) dispatch(updateLoginMethod(LoginMethod.privateKey)) - const authController = new AuthController() - authController - .authAndGetMetadataAndRelaysMap(publickey) - .catch((err) => { - console.error('Error occurred in authentication: ' + err) - return null - }) + authAndGetMetadataAndRelaysMap(publickey).catch((err) => { + console.error('Error occurred in authentication: ' + err) + return null + }) } catch (err) { console.error(`Error decoding the nsec. ${err}`) } diff --git a/src/pages/nostr/index.tsx b/src/pages/nostr/index.tsx index 5f2dc2f..738223a 100644 --- a/src/pages/nostr/index.tsx +++ b/src/pages/nostr/index.tsx @@ -1,28 +1,28 @@ -import { launch as launchNostrLoginDialog } from 'nostr-login' +import { useEffect, useState } from 'react' +import { useNavigate, useSearchParams } from 'react-router-dom' import { Button, Divider, TextField } from '@mui/material' -import { getPublicKey, nip19 } from 'nostr-tools' -import { useEffect, useState } from 'react' -import { useAppDispatch } from '../../hooks/store' -import { useNavigate, useSearchParams } from 'react-router-dom' import { toast } from 'react-toastify' -import { LoadingSpinner } from '../../components/LoadingSpinner' -import { AuthController } from '../../controllers' -import { updateKeyPair, updateLoginMethod } from '../../store/actions' -import { KeyboardCode } from '../../types' -import { LoginMethod } from '../../store/auth/types' + import { hexToBytes } from '@noble/hashes/utils' +import { launch as launchNostrLoginDialog } from 'nostr-login' +import { getPublicKey, nip19 } from 'nostr-tools' + +import { LoadingSpinner } from '../../components/LoadingSpinner' +import { useAppDispatch, useAuth } from '../../hooks' +import { updateKeyPair, updateLoginMethod } from '../../store/actions' +import { LoginMethod } from '../../store/auth/types' +import { KeyboardCode } from '../../types' import styles from './styles.module.scss' export const Nostr = () => { const [searchParams] = useSearchParams() + const { authAndGetMetadataAndRelaysMap } = useAuth() const dispatch = useAppDispatch() const navigate = useNavigate() - const authController = new AuthController() - const [isLoading, setIsLoading] = useState(false) const [loadingSpinnerDesc, setLoadingSpinnerDesc] = useState('') const [inputValue, setInputValue] = useState('') @@ -102,12 +102,12 @@ export const Nostr = () => { setIsLoading(true) setLoadingSpinnerDesc('Authenticating and finding metadata') - const redirectPath = await authController - .authAndGetMetadataAndRelaysMap(publickey) - .catch((err) => { + const redirectPath = await authAndGetMetadataAndRelaysMap(publickey).catch( + (err) => { toast.error('Error occurred in authentication: ' + err) return null - }) + } + ) if (redirectPath) navigateAfterLogin(redirectPath) diff --git a/src/utils/auth.ts b/src/utils/auth.ts new file mode 100644 index 0000000..afd91f9 --- /dev/null +++ b/src/utils/auth.ts @@ -0,0 +1,44 @@ +import { Event } from 'nostr-tools' +import { SignedEvent } from '../types' +import { saveAuthToken } from './localStorage' + +export const base64EncodeSignedEvent = (event: SignedEvent) => { + try { + const authEventSerialized = JSON.stringify(event) + const token = btoa(authEventSerialized) + return token + } catch (error) { + throw new Error('An error occurred in JSON.stringify of signedAuthEvent') + } +} + +export const base64DecodeAuthToken = (authToken: string): SignedEvent => { + const decodedToken = atob(authToken) + + try { + const signedEvent = JSON.parse(decodedToken) + return signedEvent + } catch (error) { + throw new Error('An error occurred in JSON.parse of the auth token') + } +} + +export const createAndSaveAuthToken = (signedAuthEvent: SignedEvent) => { + const base64Encoded = base64EncodeSignedEvent(signedAuthEvent) + + // save newly created auth token (base64 nostr signed event) in local storage along with expiry time + saveAuthToken(base64Encoded) + return base64Encoded +} + +export const getEmptyMetadataEvent = (pubkey?: string): Event => { + return { + content: '', + created_at: new Date().valueOf(), + id: '', + kind: 0, + pubkey: pubkey || '', + sig: '', + tags: [] + } +} diff --git a/src/utils/dvm.ts b/src/utils/dvm.ts index c2f33e8..24d637e 100644 --- a/src/utils/dvm.ts +++ b/src/utils/dvm.ts @@ -1,140 +1,10 @@ -import { EventTemplate, Filter, kinds, nip19 } from 'nostr-tools' -import { compareObjects, queryNip05, unixNow } from '.' -import { - MetadataController, - NostrController, - relayController -} from '../controllers' -import { NostrJoiningBlock, RelayInfoObject } from '../types' import NDK, { NDKEvent, NDKSubscription } from '@nostr-dev-kit/ndk' -import store from '../store/store' +import { EventTemplate } from 'nostr-tools' +import { compareObjects, unixNow } from '.' +import { NostrController, relayController } from '../controllers' import { setRelayInfoAction } from '../store/actions' - -export const getNostrJoiningBlockNumber = async ( - hexKey: string -): Promise => { - const metadataController = MetadataController.getInstance() - - const relaySet = await metadataController.findRelayListMetadata(hexKey) - - const userRelays: string[] = [] - - // find user's relays - if (relaySet.write.length > 0) { - userRelays.push(...relaySet.write) - } else { - const metadata = await metadataController.findMetadata(hexKey) - if (!metadata) return null - - const metadataContent = - metadataController.extractProfileMetadataContent(metadata) - - if (metadataContent?.nip05) { - const nip05Profile = await queryNip05(metadataContent.nip05) - - if (nip05Profile && nip05Profile.pubkey === hexKey) { - userRelays.push(...nip05Profile.relays) - } - } - } - - if (userRelays.length === 0) return null - - // filter for finding user's first kind 0 event - const eventFilter: Filter = { - kinds: [kinds.Metadata], - authors: [hexKey] - } - - // find user's kind 0 event published on user's relays - const event = await relayController.fetchEvent(eventFilter, userRelays) - - if (event) { - const { created_at } = event - - // initialize job request - const jobEventTemplate: EventTemplate = { - content: '', - created_at: unixNow(), - kind: 68001, - tags: [ - ['i', `${created_at * 1000}`], - ['j', 'blockChain-block-number'] - ] - } - - const nostrController = NostrController.getInstance() - - // sign job request event - const jobSignedEvent = await nostrController.signEvent(jobEventTemplate) - - const relays = [ - 'wss://relay.damus.io', - 'wss://relay.primal.net', - 'wss://relayable.org' - ] - - await relayController.publish(jobSignedEvent, relays).catch((err) => { - console.error( - 'Error occurred in publish blockChain-block-number DVM job', - err - ) - }) - - const subscribeWithTimeout = ( - subscription: NDKSubscription, - timeoutMs: number - ): Promise => { - return new Promise((resolve, reject) => { - const eventHandler = (event: NDKEvent) => { - subscription.stop() - resolve(event.content) - } - - subscription.on('event', eventHandler) - - // Set up a timeout to stop the subscription after a specified time - const timeout = setTimeout(() => { - subscription.stop() // Stop the subscription - reject(new Error('Subscription timed out')) // Reject the promise with a timeout error - }, timeoutMs) - - // Handle subscription close event - subscription.on('close', () => clearTimeout(timeout)) - }) - } - - const dvmNDK = new NDK({ - explicitRelayUrls: relays - }) - - await dvmNDK.connect(2000) - - // filter for getting DVM job's result - const sub = dvmNDK.subscribe({ - kinds: [68002 as number], - '#e': [jobSignedEvent.id], - '#p': [jobSignedEvent.pubkey] - }) - - // asynchronously get block number from dvm job with 20 seconds timeout - const dvmJobResult = await subscribeWithTimeout(sub, 20000) - - const encodedEventPointer = nip19.neventEncode({ - id: event.id, - relays: userRelays, - author: event.pubkey, - kind: event.kind - }) - - return { - block: parseInt(dvmJobResult), - encodedEventPointer - } - } - - return null -} +import store from '../store/store' +import { RelayInfoObject } from '../types' /** * Sets information about relays into relays.info app state. diff --git a/src/utils/index.ts b/src/utils/index.ts index 791c39b..1b00e7f 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -1,3 +1,5 @@ +export * from './auth' +export * from './const' export * from './crypto' export * from './dvm' export * from './hash' @@ -11,4 +13,3 @@ export * from './string' export * from './url' export * from './utils' export * from './zip' -export * from './const' diff --git a/src/utils/nostr.ts b/src/utils/nostr.ts index e7dbcbe..3775837 100644 --- a/src/utils/nostr.ts +++ b/src/utils/nostr.ts @@ -199,27 +199,6 @@ export const queryNip05 = async ( } } -export const base64EncodeSignedEvent = (event: SignedEvent) => { - try { - const authEventSerialized = JSON.stringify(event) - const token = btoa(authEventSerialized) - return token - } catch (error) { - throw new Error('An error occurred in JSON.stringify of signedAuthEvent') - } -} - -export const base64DecodeAuthToken = (authToken: string): SignedEvent => { - const decodedToken = atob(authToken) - - try { - const signedEvent = JSON.parse(decodedToken) - return signedEvent - } catch (error) { - throw new Error('An error occurred in JSON.parse of the auth token') - } -} - /** * @param pubkey in hex or npub format * @returns robohash.org url for the avatar @@ -985,7 +964,7 @@ export const sendNotification = async (receiver: string, meta: Meta) => { */ export const getProfileUsername = ( npub: `npub1${string}` | string, - profile?: ProfileMetadata + profile?: ProfileMetadata // todo: use NDKUserProfile ) => truncate(profile?.display_name || profile?.name || hexToNpub(npub), { length: 16