chore(refactor): replace authContoller with useAuth hook

This commit is contained in:
daniyal 2024-12-20 15:25:02 +05:00
parent 2248128001
commit 0cc1a32059
10 changed files with 227 additions and 354 deletions

View File

@ -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

View File

@ -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
}
}

View File

@ -1,4 +1,6 @@
export * from './store'
export * from './useAuth'
export * from './useDidMount'
export * from './useDvm'
export * from './useLogout'
export * from './useNDKContext'

127
src/hooks/useAuth.ts Normal file
View File

@ -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
}
}

View File

@ -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}`)
}

View File

@ -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)

44
src/utils/auth.ts Normal file
View File

@ -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: []
}
}

View File

@ -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<NostrJoiningBlock | null> => {
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<string> => {
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.

View File

@ -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'

View File

@ -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