refactor: init nostr-login, login method strategies, remove bunker
This commit is contained in:
parent
110621a125
commit
637e26bf35
16
src/App.tsx
16
src/App.tsx
@ -1,7 +1,7 @@
|
||||
import { useEffect } from 'react'
|
||||
import { useSelector } from 'react-redux'
|
||||
import { Navigate, Route, Routes } from 'react-router-dom'
|
||||
import { AuthController, NostrController } from './controllers'
|
||||
import { AuthController } from './controllers'
|
||||
import { MainLayout } from './layouts/Main'
|
||||
import {
|
||||
appPrivateRoutes,
|
||||
@ -11,7 +11,6 @@ import {
|
||||
recursiveRouteRenderer
|
||||
} from './routes'
|
||||
import { State } from './store/rootReducer'
|
||||
import { getNsecBunkerDelegatedKey, saveNsecBunkerDelegatedKey } from './utils'
|
||||
import './App.scss'
|
||||
|
||||
const App = () => {
|
||||
@ -25,23 +24,10 @@ const App = () => {
|
||||
window.location.hostname = 'localhost'
|
||||
}
|
||||
|
||||
generateBunkerDelegatedKey()
|
||||
|
||||
const authController = new AuthController()
|
||||
authController.checkSession()
|
||||
}, [])
|
||||
|
||||
const generateBunkerDelegatedKey = () => {
|
||||
const existingKey = getNsecBunkerDelegatedKey()
|
||||
|
||||
if (!existingKey) {
|
||||
const nostrController = NostrController.getInstance()
|
||||
const newDelegatedKey = nostrController.generateDelegatedKey()
|
||||
|
||||
saveNsecBunkerDelegatedKey(newDelegatedKey)
|
||||
}
|
||||
}
|
||||
|
||||
const handleRootRedirect = () => {
|
||||
if (authState.loggedIn) return appPrivateRoutes.homePage
|
||||
const callbackPathEncoded = btoa(
|
||||
|
@ -9,44 +9,28 @@ import {
|
||||
} from '@mui/material'
|
||||
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useDispatch, useSelector } from 'react-redux'
|
||||
import {
|
||||
setAuthState,
|
||||
setMetadataEvent,
|
||||
userLogOutAction
|
||||
} from '../../store/actions'
|
||||
import { useSelector } from 'react-redux'
|
||||
import { State } from '../../store/rootReducer'
|
||||
import { Dispatch } from '../../store/store'
|
||||
import Username from '../username'
|
||||
|
||||
import { Link, useNavigate } from 'react-router-dom'
|
||||
import { MetadataController, NostrController } from '../../controllers'
|
||||
import {
|
||||
appPublicRoutes,
|
||||
appPrivateRoutes,
|
||||
getProfileRoute
|
||||
} from '../../routes'
|
||||
import {
|
||||
clearAuthToken,
|
||||
getProfileUsername,
|
||||
hexToNpub,
|
||||
saveNsecBunkerDelegatedKey
|
||||
} from '../../utils'
|
||||
import { getProfileUsername, hexToNpub } from '../../utils'
|
||||
import styles from './style.module.scss'
|
||||
import { setUserRobotImage } from '../../store/userRobotImage/action'
|
||||
import { Container } from '../Container'
|
||||
import { ButtonIcon } from '../ButtonIcon'
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||
import { faClose } from '@fortawesome/free-solid-svg-icons'
|
||||
import useMediaQuery from '@mui/material/useMediaQuery'
|
||||
|
||||
const metadataController = MetadataController.getInstance()
|
||||
import { useLogout } from '../../hooks/useLogout'
|
||||
|
||||
export const AppBar = () => {
|
||||
const navigate = useNavigate()
|
||||
|
||||
const dispatch: Dispatch = useDispatch()
|
||||
|
||||
const logout = useLogout()
|
||||
const [username, setUsername] = useState('')
|
||||
const [userAvatar, setUserAvatar] = useState('')
|
||||
const [anchorElUser, setAnchorElUser] = useState<null | HTMLElement>(null)
|
||||
@ -94,28 +78,7 @@ export const AppBar = () => {
|
||||
|
||||
const handleLogout = () => {
|
||||
handleCloseUserMenu()
|
||||
dispatch(
|
||||
setAuthState({
|
||||
keyPair: undefined,
|
||||
loggedIn: false,
|
||||
usersPubkey: undefined,
|
||||
loginMethod: undefined,
|
||||
nsecBunkerPubkey: undefined
|
||||
})
|
||||
)
|
||||
dispatch(setMetadataEvent(metadataController.getEmptyMetadataEvent()))
|
||||
dispatch(setUserRobotImage(null))
|
||||
|
||||
// clear authToken saved in local storage
|
||||
clearAuthToken()
|
||||
|
||||
dispatch(userLogOutAction())
|
||||
|
||||
// update nsecBunker delegated key after logout
|
||||
const nostrController = NostrController.getInstance()
|
||||
const newDelegatedKey = nostrController.generateDelegatedKey()
|
||||
saveNsecBunkerDelegatedKey(newDelegatedKey)
|
||||
|
||||
logout()
|
||||
navigate('/')
|
||||
}
|
||||
const isAuthenticated = authState?.loggedIn === true
|
||||
|
@ -31,7 +31,7 @@ export class AuthController {
|
||||
/**
|
||||
* Function will authenticate user by signing an auth event
|
||||
* which is done by calling the sign() function, where appropriate
|
||||
* method will be chosen (extension, nsecbunker or keys)
|
||||
* method will be chosen (extension or keys)
|
||||
*
|
||||
* @param pubkey of the user trying to login
|
||||
* @returns url to redirect if authentication successfull
|
||||
|
@ -1,194 +1,25 @@
|
||||
import NDK, {
|
||||
NDKEvent,
|
||||
NDKNip46Signer,
|
||||
NDKPrivateKeySigner,
|
||||
NDKUser,
|
||||
NostrEvent
|
||||
} from '@nostr-dev-kit/ndk'
|
||||
import {
|
||||
Event,
|
||||
EventTemplate,
|
||||
UnsignedEvent,
|
||||
finalizeEvent,
|
||||
nip04,
|
||||
nip19,
|
||||
nip44
|
||||
} from 'nostr-tools'
|
||||
import { EventTemplate, UnsignedEvent } from 'nostr-tools'
|
||||
import { WindowNostr } from 'nostr-tools/nip07'
|
||||
import { EventEmitter } from 'tseep'
|
||||
import { updateNsecbunkerPubkey } from '../store/actions'
|
||||
import { AuthState, LoginMethods } from '../store/auth/types'
|
||||
import { AuthState } from '../store/auth/types'
|
||||
import store from '../store/store'
|
||||
import { SignedEvent } from '../types'
|
||||
import { getNsecBunkerDelegatedKey, verifySignedEvent } from '../utils'
|
||||
import { LoginMethodContext } from '../services/LoginMethodStrategy/loginMethodContext'
|
||||
|
||||
export class NostrController extends EventEmitter {
|
||||
private static instance: NostrController
|
||||
|
||||
private bunkerNDK: NDK | undefined
|
||||
private remoteSigner: NDKNip46Signer | undefined
|
||||
|
||||
private constructor() {
|
||||
super()
|
||||
}
|
||||
|
||||
private getNostrObject = () => {
|
||||
// fix: this is not picking up type declaration from src/system/index.d.ts
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
if (window.nostr) return window.nostr as any
|
||||
if (window.nostr) return window.nostr as WindowNostr
|
||||
|
||||
throw new Error(
|
||||
`window.nostr object not present. Make sure you have an nostr extension installed/working properly.`
|
||||
)
|
||||
}
|
||||
|
||||
public nsecBunkerInit = async (relays: string[]) => {
|
||||
// Don't reinstantiate bunker NDK if exists with same relays
|
||||
if (
|
||||
this.bunkerNDK &&
|
||||
this.bunkerNDK.explicitRelayUrls?.length === relays.length &&
|
||||
this.bunkerNDK.explicitRelayUrls?.every((relay) => relays.includes(relay))
|
||||
)
|
||||
return
|
||||
|
||||
this.bunkerNDK = new NDK({
|
||||
explicitRelayUrls: relays
|
||||
})
|
||||
|
||||
try {
|
||||
await this.bunkerNDK
|
||||
.connect(2000)
|
||||
.then(() => {
|
||||
console.log(
|
||||
`Successfully connected to the nsecBunker relays: ${relays.join(
|
||||
','
|
||||
)}`
|
||||
)
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(
|
||||
`Error connecting to the nsecBunker relays: ${relays.join(
|
||||
','
|
||||
)} ${err}`
|
||||
)
|
||||
})
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates nSecBunker signer instance for the given npub
|
||||
* Or if npub omitted it will return existing signer
|
||||
* If neither, error will be thrown
|
||||
* @param npub nPub / public key in hex format
|
||||
* @returns nsecBunker Signer instance
|
||||
*/
|
||||
public createNsecBunkerSigner = async (
|
||||
npub: string | undefined
|
||||
): Promise<NDKNip46Signer> => {
|
||||
const nsecBunkerDelegatedKey = getNsecBunkerDelegatedKey()
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!nsecBunkerDelegatedKey) {
|
||||
reject('nsecBunker delegated key is not found in the browser.')
|
||||
return
|
||||
}
|
||||
const localSigner = new NDKPrivateKeySigner(nsecBunkerDelegatedKey)
|
||||
|
||||
if (!npub) {
|
||||
if (this.remoteSigner) resolve(this.remoteSigner)
|
||||
|
||||
const npubFromStorage = (store.getState().auth as AuthState)
|
||||
.nsecBunkerPubkey
|
||||
|
||||
if (npubFromStorage) {
|
||||
npub = npubFromStorage
|
||||
} else {
|
||||
reject(
|
||||
'No signer instance present, no npub provided by user or found in the browser.'
|
||||
)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
store.dispatch(updateNsecbunkerPubkey(npub))
|
||||
}
|
||||
|
||||
// Pubkey of a key pair stored in nsecbunker that will be used to sign event with
|
||||
const appPubkeyOrToken = npub.includes('npub')
|
||||
? npub
|
||||
: nip19.npubEncode(npub)
|
||||
|
||||
/**
|
||||
* When creating and NDK instance we create new connection to the relay
|
||||
* To prevent too much connections and hitting rate limits, if npub against which we sign
|
||||
* we will reuse existing instance. Otherwise we will create new NDK and signer instance.
|
||||
*/
|
||||
if (!this.remoteSigner || this.remoteSigner?.remotePubkey !== npub) {
|
||||
this.remoteSigner = new NDKNip46Signer(
|
||||
this.bunkerNDK!,
|
||||
appPubkeyOrToken,
|
||||
localSigner
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* when nsecbunker-delegated-key is regenerated we have to reinitialize the remote signer
|
||||
*/
|
||||
if (this.remoteSigner.localSigner !== localSigner) {
|
||||
this.remoteSigner = new NDKNip46Signer(
|
||||
this.bunkerNDK!,
|
||||
appPubkeyOrToken,
|
||||
localSigner
|
||||
)
|
||||
}
|
||||
|
||||
resolve(this.remoteSigner)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Signs the nostr event and returns the sig and id or full raw nostr event
|
||||
* @param npub stored in nsecBunker to sign with
|
||||
* @param event to be signed
|
||||
* @param returnFullEvent whether to return full raw nostr event or just SIG and ID values
|
||||
*/
|
||||
public signWithNsecBunker = async (
|
||||
npub: string | undefined,
|
||||
event: NostrEvent,
|
||||
returnFullEvent = true
|
||||
): Promise<{ id: string; sig: string } | NostrEvent> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.createNsecBunkerSigner(npub)
|
||||
.then(async (signer) => {
|
||||
const ndkEvent = new NDKEvent(undefined, event)
|
||||
|
||||
const timeout = setTimeout(() => {
|
||||
reject('Timeout occurred while waiting for event signing')
|
||||
}, 60000) // 60000 ms (1 min) = 1000 * 60
|
||||
|
||||
await ndkEvent.sign(signer).catch((err) => {
|
||||
clearTimeout(timeout)
|
||||
reject(err)
|
||||
return
|
||||
})
|
||||
|
||||
clearTimeout(timeout)
|
||||
|
||||
if (returnFullEvent) {
|
||||
resolve(ndkEvent.rawEvent())
|
||||
} else {
|
||||
resolve({
|
||||
id: ndkEvent.id,
|
||||
sig: ndkEvent.sig!
|
||||
})
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
reject(err)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
public static getInstance(): NostrController {
|
||||
if (!NostrController.instance) {
|
||||
NostrController.instance = new NostrController()
|
||||
@ -207,59 +38,10 @@ export class NostrController extends EventEmitter {
|
||||
nip44Encrypt = async (receiver: string, content: string) => {
|
||||
// Retrieve the current login method from the application's redux state.
|
||||
const loginMethod = (store.getState().auth as AuthState).loginMethod
|
||||
const context = new LoginMethodContext(loginMethod)
|
||||
|
||||
// Handle encryption when the login method is via an extension.
|
||||
if (loginMethod === LoginMethods.extension) {
|
||||
const nostr = this.getNostrObject()
|
||||
|
||||
// Check if the nostr object supports NIP-44 encryption.
|
||||
if (!nostr.nip44) {
|
||||
throw new Error(
|
||||
`Your nostr extension does not support nip44 encryption & decryption`
|
||||
)
|
||||
}
|
||||
|
||||
// Encrypt the content using NIP-44 provided by the nostr extension.
|
||||
const encrypted = await nostr.nip44.encrypt(receiver, content)
|
||||
return encrypted as string
|
||||
}
|
||||
|
||||
// Handle encryption when the login method is via a private key.
|
||||
if (loginMethod === LoginMethods.privateKey) {
|
||||
const keys = (store.getState().auth as AuthState).keyPair
|
||||
|
||||
// Check if the private and public key pair is available.
|
||||
if (!keys) {
|
||||
throw new Error(
|
||||
`Login method is ${LoginMethods.privateKey} but private & public key pair is not found.`
|
||||
)
|
||||
}
|
||||
|
||||
// Decode the private key.
|
||||
const { private: nsec } = keys
|
||||
const privateKey = nip19.decode(nsec).data as Uint8Array
|
||||
|
||||
// Generate the conversation key using NIP-44 utilities.
|
||||
const nip44ConversationKey = nip44.v2.utils.getConversationKey(
|
||||
privateKey,
|
||||
receiver
|
||||
)
|
||||
|
||||
// Encrypt the content using the generated conversation key.
|
||||
const encrypted = nip44.v2.encrypt(content, nip44ConversationKey)
|
||||
|
||||
return encrypted
|
||||
}
|
||||
|
||||
// Throw an error if the login method is nsecBunker (not supported).
|
||||
if (loginMethod === LoginMethods.nsecBunker) {
|
||||
throw new Error(
|
||||
`nip44 encryption is not yet supported for login method '${LoginMethods.nsecBunker}'`
|
||||
)
|
||||
}
|
||||
|
||||
// Throw an error if the login method is undefined or unsupported.
|
||||
throw new Error('Login method is undefined')
|
||||
return await context.nip44Encrypt(receiver, content)
|
||||
}
|
||||
|
||||
/**
|
||||
@ -273,65 +55,15 @@ export class NostrController extends EventEmitter {
|
||||
nip44Decrypt = async (sender: string, content: string) => {
|
||||
// Retrieve the current login method from the application's redux state.
|
||||
const loginMethod = (store.getState().auth as AuthState).loginMethod
|
||||
const context = new LoginMethodContext(loginMethod)
|
||||
|
||||
// Handle decryption when the login method is via an extension.
|
||||
if (loginMethod === LoginMethods.extension) {
|
||||
const nostr = this.getNostrObject()
|
||||
|
||||
// Check if the nostr object supports NIP-44 decryption.
|
||||
if (!nostr.nip44) {
|
||||
throw new Error(
|
||||
`Your nostr extension does not support nip44 encryption & decryption`
|
||||
)
|
||||
}
|
||||
|
||||
// Decrypt the content using NIP-44 provided by the nostr extension.
|
||||
const decrypted = await nostr.nip44.decrypt(sender, content)
|
||||
return decrypted as string
|
||||
}
|
||||
|
||||
// Handle decryption when the login method is via a private key.
|
||||
if (loginMethod === LoginMethods.privateKey) {
|
||||
const keys = (store.getState().auth as AuthState).keyPair
|
||||
|
||||
// Check if the private and public key pair is available.
|
||||
if (!keys) {
|
||||
throw new Error(
|
||||
`Login method is ${LoginMethods.privateKey} but private & public key pair is not found.`
|
||||
)
|
||||
}
|
||||
|
||||
// Decode the private key.
|
||||
const { private: nsec } = keys
|
||||
const privateKey = nip19.decode(nsec).data as Uint8Array
|
||||
|
||||
// Generate the conversation key using NIP-44 utilities.
|
||||
const nip44ConversationKey = nip44.v2.utils.getConversationKey(
|
||||
privateKey,
|
||||
sender
|
||||
)
|
||||
|
||||
// Decrypt the content using the generated conversation key.
|
||||
const decrypted = nip44.v2.decrypt(content, nip44ConversationKey)
|
||||
|
||||
return decrypted
|
||||
}
|
||||
|
||||
// Throw an error if the login method is nsecBunker (not supported).
|
||||
if (loginMethod === LoginMethods.nsecBunker) {
|
||||
throw new Error(
|
||||
`nip44 decryption is not yet supported for login method '${LoginMethods.nsecBunker}'`
|
||||
)
|
||||
}
|
||||
|
||||
// Throw an error if the login method is undefined or unsupported.
|
||||
throw new Error('Login method is undefined')
|
||||
// Handle decryption
|
||||
return await context.nip44EDecrypt(sender, content)
|
||||
}
|
||||
|
||||
/**
|
||||
* Signs an event with private key (if it is present in local storage) or
|
||||
* with browser extension (if it is present) or
|
||||
* with nSecBunker instance.
|
||||
* with browser extension (if it is present)
|
||||
* @param event - unsigned nostr event.
|
||||
* @returns - a promised that is resolved with signed nostr event.
|
||||
*/
|
||||
@ -339,113 +71,16 @@ export class NostrController extends EventEmitter {
|
||||
event: UnsignedEvent | EventTemplate
|
||||
): Promise<SignedEvent> => {
|
||||
const loginMethod = (store.getState().auth as AuthState).loginMethod
|
||||
const context = new LoginMethodContext(loginMethod)
|
||||
|
||||
if (!loginMethod) {
|
||||
return Promise.reject('No login method found in the browser storage')
|
||||
}
|
||||
|
||||
if (loginMethod === LoginMethods.nsecBunker) {
|
||||
// Check if nsecBunker is available
|
||||
if (!this.bunkerNDK) {
|
||||
return Promise.reject(
|
||||
`Login method is ${loginMethod} but bunkerNDK is not created`
|
||||
)
|
||||
}
|
||||
|
||||
if (!this.remoteSigner) {
|
||||
return Promise.reject(
|
||||
`Login method is ${loginMethod} but bunkerNDK is not created`
|
||||
)
|
||||
}
|
||||
|
||||
const signedEvent = await this.signWithNsecBunker(
|
||||
'',
|
||||
event as NostrEvent
|
||||
).catch((err) => {
|
||||
throw err
|
||||
})
|
||||
|
||||
return Promise.resolve(signedEvent as SignedEvent)
|
||||
} else if (loginMethod === LoginMethods.privateKey) {
|
||||
const keys = (store.getState().auth as AuthState).keyPair
|
||||
|
||||
if (!keys) {
|
||||
return Promise.reject(
|
||||
`Login method is ${loginMethod}, but keys are not found`
|
||||
)
|
||||
}
|
||||
|
||||
const { private: nsec } = keys
|
||||
const privateKey = nip19.decode(nsec).data as Uint8Array
|
||||
|
||||
const signedEvent = finalizeEvent(event, privateKey)
|
||||
|
||||
verifySignedEvent(signedEvent)
|
||||
|
||||
return Promise.resolve(signedEvent)
|
||||
} else if (loginMethod === LoginMethods.extension) {
|
||||
const nostr = this.getNostrObject()
|
||||
|
||||
return (await nostr
|
||||
.signEvent(event as NostrEvent)
|
||||
.catch((err: unknown) => {
|
||||
console.log('Error while signing event: ', err)
|
||||
|
||||
throw err
|
||||
})) as Event
|
||||
} else {
|
||||
return Promise.reject(
|
||||
`We could not sign the event, none of the signing methods are available`
|
||||
)
|
||||
}
|
||||
return await context.signEvent(event)
|
||||
}
|
||||
|
||||
nip04Encrypt = async (receiver: string, content: string): Promise<string> => {
|
||||
const loginMethod = (store.getState().auth as AuthState).loginMethod
|
||||
const context = new LoginMethodContext(loginMethod)
|
||||
|
||||
if (loginMethod === LoginMethods.extension) {
|
||||
const nostr = this.getNostrObject()
|
||||
|
||||
if (!nostr.nip04) {
|
||||
throw new Error(
|
||||
`Your nostr extension does not support nip04 encryption & decryption`
|
||||
)
|
||||
}
|
||||
|
||||
const encrypted = await nostr.nip04.encrypt(receiver, content)
|
||||
return encrypted
|
||||
}
|
||||
|
||||
if (loginMethod === LoginMethods.privateKey) {
|
||||
const keys = (store.getState().auth as AuthState).keyPair
|
||||
|
||||
if (!keys) {
|
||||
throw new Error(
|
||||
`Login method is ${LoginMethods.privateKey} but private & public key pair is not found.`
|
||||
)
|
||||
}
|
||||
|
||||
const { private: nsec } = keys
|
||||
const privateKey = nip19.decode(nsec).data as Uint8Array
|
||||
|
||||
const encrypted = await nip04.encrypt(privateKey, receiver, content)
|
||||
return encrypted
|
||||
}
|
||||
|
||||
if (loginMethod === LoginMethods.nsecBunker) {
|
||||
const user = new NDKUser({ pubkey: receiver })
|
||||
|
||||
this.remoteSigner?.on('authUrl', (authUrl) => {
|
||||
this.emit('nsecbunker-auth', authUrl)
|
||||
})
|
||||
|
||||
if (!this.remoteSigner) throw new Error('Remote signer is undefined.')
|
||||
const encrypted = await this.remoteSigner.encrypt(user, content)
|
||||
|
||||
return encrypted
|
||||
}
|
||||
|
||||
throw new Error('Login method is undefined')
|
||||
return await context.nip04Encrypt(receiver, content)
|
||||
}
|
||||
|
||||
/**
|
||||
@ -457,50 +92,9 @@ export class NostrController extends EventEmitter {
|
||||
*/
|
||||
nip04Decrypt = async (sender: string, content: string): Promise<string> => {
|
||||
const loginMethod = (store.getState().auth as AuthState).loginMethod
|
||||
const context = new LoginMethodContext(loginMethod)
|
||||
|
||||
if (loginMethod === LoginMethods.extension) {
|
||||
const nostr = this.getNostrObject()
|
||||
|
||||
if (!nostr.nip04) {
|
||||
throw new Error(
|
||||
`Your nostr extension does not support nip04 encryption & decryption`
|
||||
)
|
||||
}
|
||||
|
||||
const decrypted = await nostr.nip04.decrypt(sender, content)
|
||||
return decrypted
|
||||
}
|
||||
|
||||
if (loginMethod === LoginMethods.privateKey) {
|
||||
const keys = (store.getState().auth as AuthState).keyPair
|
||||
|
||||
if (!keys) {
|
||||
throw new Error(
|
||||
`Login method is ${LoginMethods.privateKey} but private & public key pair is not found.`
|
||||
)
|
||||
}
|
||||
|
||||
const { private: nsec } = keys
|
||||
const privateKey = nip19.decode(nsec).data as Uint8Array
|
||||
|
||||
const decrypted = await nip04.decrypt(privateKey, sender, content)
|
||||
return decrypted
|
||||
}
|
||||
|
||||
if (loginMethod === LoginMethods.nsecBunker) {
|
||||
const user = new NDKUser({ pubkey: sender })
|
||||
|
||||
this.remoteSigner?.on('authUrl', (authUrl) => {
|
||||
this.emit('nsecbunker-auth', authUrl)
|
||||
})
|
||||
|
||||
if (!this.remoteSigner) throw new Error('Remote signer is undefined.')
|
||||
const decrypted = await this.remoteSigner.decrypt(user, content)
|
||||
|
||||
return decrypted
|
||||
}
|
||||
|
||||
throw new Error('Login method is undefined')
|
||||
return await context.nip04EDecrypt(sender, content)
|
||||
}
|
||||
|
||||
/**
|
||||
@ -523,12 +117,4 @@ export class NostrController extends EventEmitter {
|
||||
|
||||
return Promise.resolve(pubKey)
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates NDK Private Signer
|
||||
* @returns nSecBunker delegated key
|
||||
*/
|
||||
generateDelegatedKey = (): string => {
|
||||
return NDKPrivateKeySigner.generate().privateKey!
|
||||
}
|
||||
}
|
||||
|
27
src/hooks/useLogout.tsx
Normal file
27
src/hooks/useLogout.tsx
Normal file
@ -0,0 +1,27 @@
|
||||
import { logout as nostrLogout } from 'nostr-login'
|
||||
import { clear } from '../utils/localStorage'
|
||||
import { useDispatch } from 'react-redux'
|
||||
import { Dispatch } from '../store/store'
|
||||
import { userLogOutAction } from '../store/actions'
|
||||
import { LoginMethod } from '../store/auth/types'
|
||||
import { useAppSelector } from './store'
|
||||
|
||||
export const useLogout = () => {
|
||||
const loginMethod = useAppSelector((state) => state.auth?.loginMethod)
|
||||
const dispatch: Dispatch = useDispatch()
|
||||
|
||||
const logout = () => {
|
||||
// Log out of the nostr-login
|
||||
if (loginMethod === LoginMethod.nostrLogin) {
|
||||
nostrLogout()
|
||||
}
|
||||
|
||||
// Reset redux state with the logout
|
||||
dispatch(userLogOutAction())
|
||||
|
||||
// Clear the local storage states
|
||||
clear()
|
||||
}
|
||||
|
||||
return logout
|
||||
}
|
@ -1,34 +1,42 @@
|
||||
import { Event, kinds } from 'nostr-tools'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { useDispatch, useSelector } from 'react-redux'
|
||||
import { Outlet } from 'react-router-dom'
|
||||
import { Outlet, useNavigate, useSearchParams } from 'react-router-dom'
|
||||
import { AppBar } from '../components/AppBar/AppBar'
|
||||
import { LoadingSpinner } from '../components/LoadingSpinner'
|
||||
import { MetadataController, NostrController } from '../controllers'
|
||||
import {
|
||||
AuthController,
|
||||
MetadataController,
|
||||
NostrController
|
||||
} from '../controllers'
|
||||
import {
|
||||
restoreState,
|
||||
setAuthState,
|
||||
setMetadataEvent,
|
||||
updateLoginMethod,
|
||||
updateNostrLoginAuthMethod,
|
||||
updateUserAppData
|
||||
} from '../store/actions'
|
||||
import { LoginMethods } from '../store/auth/types'
|
||||
import { State } from '../store/rootReducer'
|
||||
import { Dispatch } from '../store/store'
|
||||
import { setUserRobotImage } from '../store/userRobotImage/action'
|
||||
import {
|
||||
clearAuthToken,
|
||||
clearState,
|
||||
getRoboHashPicture,
|
||||
getUsersAppData,
|
||||
loadState,
|
||||
saveNsecBunkerDelegatedKey,
|
||||
subscribeForSigits
|
||||
} from '../utils'
|
||||
import { 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] = useSearchParams()
|
||||
const navigate = useNavigate()
|
||||
const dispatch: Dispatch = useDispatch()
|
||||
const logout = useLogout()
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
const [loadingSpinnerDesc, setLoadingSpinnerDesc] = useState(`Loading App`)
|
||||
const authState = useSelector((state: State) => state.auth)
|
||||
@ -37,51 +45,63 @@ export const MainLayout = () => {
|
||||
// Ref to track if `subscribeForSigits` has been called
|
||||
const hasSubscribed = useRef(false)
|
||||
|
||||
useEffect(() => {
|
||||
const metadataController = MetadataController.getInstance()
|
||||
const navigateAfterLogin = (path: string) => {
|
||||
const callbackPath = searchParams.get('callbackPath')
|
||||
|
||||
const logout = () => {
|
||||
dispatch(
|
||||
setAuthState({
|
||||
keyPair: undefined,
|
||||
loggedIn: false,
|
||||
usersPubkey: undefined,
|
||||
loginMethod: undefined,
|
||||
nsecBunkerPubkey: undefined
|
||||
})
|
||||
)
|
||||
|
||||
dispatch(setMetadataEvent(metadataController.getEmptyMetadataEvent()))
|
||||
|
||||
// clear authToken saved in local storage
|
||||
clearAuthToken()
|
||||
clearState()
|
||||
|
||||
// update nsecBunker delegated key
|
||||
const newDelegatedKey =
|
||||
NostrController.getInstance().generateDelegatedKey()
|
||||
saveNsecBunkerDelegatedKey(newDelegatedKey)
|
||||
if (callbackPath) {
|
||||
// base64 decoded path
|
||||
const path = atob(callbackPath)
|
||||
navigate(path)
|
||||
return
|
||||
}
|
||||
|
||||
navigate(path)
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
if (redirectPath) {
|
||||
navigateAfterLogin(redirectPath)
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [dispatch])
|
||||
|
||||
useEffect(() => {
|
||||
const handleNostrAuth = (_: string, opts: NostrLoginAuthOptions) => {
|
||||
console.log(opts.method)
|
||||
if (opts.type === 'logout') {
|
||||
logout()
|
||||
} else {
|
||||
dispatch(updateNostrLoginAuthMethod(opts.method))
|
||||
login()
|
||||
}
|
||||
}
|
||||
|
||||
initNostrLogin({
|
||||
darkMode: false,
|
||||
noBanner: true,
|
||||
onAuth: handleNostrAuth
|
||||
})
|
||||
|
||||
const metadataController = MetadataController.getInstance()
|
||||
|
||||
const restoredState = loadState()
|
||||
if (restoredState) {
|
||||
dispatch(restoreState(restoredState))
|
||||
|
||||
const { loggedIn, loginMethod, usersPubkey, nsecBunkerRelays } =
|
||||
restoredState.auth
|
||||
const { loggedIn, loginMethod, usersPubkey } = restoredState.auth
|
||||
|
||||
if (loggedIn) {
|
||||
if (!loginMethod || !usersPubkey) return logout()
|
||||
|
||||
if (loginMethod === LoginMethods.nsecBunker) {
|
||||
if (!nsecBunkerRelays) return logout()
|
||||
|
||||
const nostrController = NostrController.getInstance()
|
||||
nostrController.nsecBunkerInit(nsecBunkerRelays).then(() => {
|
||||
nostrController.createNsecBunkerSigner(usersPubkey)
|
||||
})
|
||||
}
|
||||
|
||||
const handleMetadataEvent = (event: Event) => {
|
||||
dispatch(setMetadataEvent(event))
|
||||
}
|
||||
@ -101,7 +121,8 @@ export const MainLayout = () => {
|
||||
} else {
|
||||
setIsLoading(false)
|
||||
}
|
||||
}, [dispatch])
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (authState.loggedIn && usersAppData) {
|
||||
|
@ -74,8 +74,6 @@ export const CreatePage = () => {
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const [loadingSpinnerDesc, setLoadingSpinnerDesc] = useState('')
|
||||
|
||||
const [authUrl, setAuthUrl] = useState<string>()
|
||||
|
||||
const [title, setTitle] = useState(`sigit_${formatTimestamp(Date.now())}`)
|
||||
|
||||
const [selectedFiles, setSelectedFiles] = useState<File[]>([])
|
||||
@ -183,10 +181,6 @@ export const CreatePage = () => {
|
||||
}
|
||||
})
|
||||
}, [metadata, users])
|
||||
// Set up event listener for authentication event
|
||||
nostrController.on('nsecbunker-auth', (url) => {
|
||||
setAuthUrl(url)
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
if (uploadedFiles) {
|
||||
@ -761,17 +755,6 @@ export const CreatePage = () => {
|
||||
}
|
||||
}
|
||||
|
||||
if (authUrl) {
|
||||
return (
|
||||
<iframe
|
||||
title="Nsecbunker auth"
|
||||
src={authUrl}
|
||||
width="100%"
|
||||
height="500px"
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{isLoading && <LoadingSpinner desc={loadingSpinnerDesc} />}
|
||||
|
@ -7,29 +7,14 @@ import { useDispatch } from 'react-redux'
|
||||
import { useNavigate, useSearchParams } from 'react-router-dom'
|
||||
import { toast } from 'react-toastify'
|
||||
import { LoadingSpinner } from '../../components/LoadingSpinner'
|
||||
import {
|
||||
AuthController,
|
||||
MetadataController,
|
||||
NostrController
|
||||
} from '../../controllers'
|
||||
import {
|
||||
updateKeyPair,
|
||||
updateLoginMethod,
|
||||
updateNsecbunkerPubkey,
|
||||
updateNsecbunkerRelays
|
||||
} from '../../store/actions'
|
||||
import { LoginMethods } from '../../store/auth/types'
|
||||
import { AuthController } from '../../controllers'
|
||||
import { updateKeyPair, updateLoginMethod } from '../../store/actions'
|
||||
import { LoginMethod } from '../../store/auth/types'
|
||||
import { Dispatch } from '../../store/store'
|
||||
import { npubToHex, queryNip05, timeout } from '../../utils'
|
||||
import { hexToBytes } from '@noble/hashes/utils'
|
||||
import { NIP05_REGEX } from '../../constants'
|
||||
|
||||
import styles from './styles.module.scss'
|
||||
|
||||
import { TimeoutError } from '../../types/errors/TimeoutError'
|
||||
const EXTENSION_LOGIN_DELAY_SECONDS = 5
|
||||
const EXTENSION_LOGIN_TIMEOUT_SECONDS = EXTENSION_LOGIN_DELAY_SECONDS + 55
|
||||
|
||||
export const Nostr = () => {
|
||||
const [searchParams] = useSearchParams()
|
||||
|
||||
@ -37,14 +22,23 @@ export const Nostr = () => {
|
||||
const navigate = useNavigate()
|
||||
|
||||
const authController = new AuthController()
|
||||
const metadataController = MetadataController.getInstance()
|
||||
const nostrController = NostrController.getInstance()
|
||||
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const [loadingSpinnerDesc, setLoadingSpinnerDesc] = useState('')
|
||||
const [isExtensionSlow, setIsExtensionSlow] = useState(false)
|
||||
const [inputValue, setInputValue] = useState('')
|
||||
const [authUrl, setAuthUrl] = useState<string>()
|
||||
|
||||
const navigateAfterLogin = (path: string) => {
|
||||
const callbackPath = searchParams.get('callbackPath')
|
||||
|
||||
if (callbackPath) {
|
||||
// base64 decoded path
|
||||
const path = atob(callbackPath)
|
||||
navigate(path)
|
||||
return
|
||||
}
|
||||
|
||||
navigate(path)
|
||||
}
|
||||
|
||||
const [isNostrExtensionAvailable, setIsNostrExtensionAvailable] =
|
||||
useState(false)
|
||||
@ -65,59 +59,6 @@ export const Nostr = () => {
|
||||
}
|
||||
}
|
||||
|
||||
const navigateAfterLogin = (path: string) => {
|
||||
const callbackPath = searchParams.get('callbackPath')
|
||||
|
||||
if (callbackPath) {
|
||||
// base64 decoded path
|
||||
const path = atob(callbackPath)
|
||||
navigate(path)
|
||||
return
|
||||
}
|
||||
|
||||
navigate(path)
|
||||
}
|
||||
|
||||
const loginWithExtension = async () => {
|
||||
let waitTimeout: number | undefined
|
||||
try {
|
||||
// Wait EXTENSION_LOGIN_DELAY_SECONDS before showing extension delay message
|
||||
waitTimeout = window.setTimeout(() => {
|
||||
setIsExtensionSlow(true)
|
||||
}, EXTENSION_LOGIN_DELAY_SECONDS * 1000)
|
||||
|
||||
setIsLoading(true)
|
||||
setLoadingSpinnerDesc('Capturing pubkey from nostr extension')
|
||||
|
||||
const pubkey = await nostrController.capturePublicKey()
|
||||
dispatch(updateLoginMethod(LoginMethods.extension))
|
||||
|
||||
setLoadingSpinnerDesc('Authenticating and finding metadata')
|
||||
const redirectPath = await Promise.race([
|
||||
authController.authAndGetMetadataAndRelaysMap(pubkey),
|
||||
timeout(EXTENSION_LOGIN_TIMEOUT_SECONDS * 1000)
|
||||
])
|
||||
|
||||
if (redirectPath) {
|
||||
navigateAfterLogin(redirectPath)
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof TimeoutError) {
|
||||
// Just log the error, no toast, user has already been notified with the loading screen
|
||||
console.error("Extension didn't respond in time")
|
||||
} else {
|
||||
toast.error('Error capturing public key from nostr extension: ' + error)
|
||||
}
|
||||
} finally {
|
||||
// Clear the wait timeout so we don't change the state unnecessarily
|
||||
window.clearTimeout(waitTimeout)
|
||||
|
||||
setIsLoading(false)
|
||||
setLoadingSpinnerDesc('')
|
||||
setIsExtensionSlow(false)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Login with NSEC or HEX private key
|
||||
* @param privateKey in HEX format
|
||||
@ -153,7 +94,7 @@ export const Nostr = () => {
|
||||
public: publickey
|
||||
})
|
||||
)
|
||||
dispatch(updateLoginMethod(LoginMethods.privateKey))
|
||||
dispatch(updateLoginMethod(LoginMethod.privateKey))
|
||||
|
||||
setIsLoading(true)
|
||||
setLoadingSpinnerDesc('Authenticating and finding metadata')
|
||||
@ -171,182 +112,10 @@ export const Nostr = () => {
|
||||
setLoadingSpinnerDesc('')
|
||||
}
|
||||
|
||||
const loginWithNsecBunker = async () => {
|
||||
let relays: string[] | undefined
|
||||
let pubkey: string | undefined
|
||||
|
||||
setIsLoading(true)
|
||||
|
||||
const displayError = (message: string) => {
|
||||
toast.error(message)
|
||||
setIsLoading(false)
|
||||
setLoadingSpinnerDesc('')
|
||||
}
|
||||
|
||||
if (inputValue.match(NIP05_REGEX)) {
|
||||
const nip05Profile = await queryNip05(inputValue).catch((err) => {
|
||||
toast.error('An error occurred while querying nip05 profile: ' + err)
|
||||
return null
|
||||
})
|
||||
|
||||
if (nip05Profile) {
|
||||
pubkey = nip05Profile.pubkey
|
||||
relays = nip05Profile.relays
|
||||
}
|
||||
} else if (inputValue.startsWith('npub')) {
|
||||
pubkey = nip19.decode(inputValue).data as string
|
||||
const metadataEvent = await metadataController
|
||||
.findMetadata(pubkey)
|
||||
.catch(() => {
|
||||
return null
|
||||
})
|
||||
|
||||
if (!metadataEvent) {
|
||||
return displayError('metadata not found!')
|
||||
}
|
||||
|
||||
const metadataContent =
|
||||
metadataController.extractProfileMetadataContent(metadataEvent)
|
||||
|
||||
if (!metadataContent?.nip05) {
|
||||
return displayError('nip05 not present in metadata')
|
||||
}
|
||||
|
||||
const nip05Profile = await queryNip05(inputValue).catch((err) => {
|
||||
toast.error('An error occurred while querying nip05 profile: ' + err)
|
||||
return null
|
||||
})
|
||||
|
||||
if (nip05Profile) {
|
||||
if (nip05Profile.pubkey !== pubkey) {
|
||||
return displayError(
|
||||
'pubkey in nip05 does not match with provided npub'
|
||||
)
|
||||
}
|
||||
|
||||
relays = nip05Profile.relays
|
||||
}
|
||||
}
|
||||
|
||||
if (!relays || relays.length === 0) {
|
||||
return displayError('No relay found for nsecbunker')
|
||||
}
|
||||
|
||||
if (!pubkey) {
|
||||
return displayError('pubkey not found')
|
||||
}
|
||||
|
||||
setLoadingSpinnerDesc('Initializing nsecBunker')
|
||||
await nostrController.nsecBunkerInit(relays)
|
||||
|
||||
setLoadingSpinnerDesc('Creating nsecbunker singer')
|
||||
await nostrController
|
||||
.createNsecBunkerSigner(pubkey)
|
||||
.then(async (signer) => {
|
||||
signer.on('authUrl', (url: string) => {
|
||||
setAuthUrl(url)
|
||||
})
|
||||
|
||||
dispatch(updateLoginMethod(LoginMethods.nsecBunker))
|
||||
dispatch(updateNsecbunkerPubkey(pubkey))
|
||||
dispatch(updateNsecbunkerRelays(relays))
|
||||
|
||||
setLoadingSpinnerDesc('Authenticating and finding metadata')
|
||||
|
||||
const redirectPath = await authController
|
||||
.authAndGetMetadataAndRelaysMap(pubkey!)
|
||||
.catch((err) => {
|
||||
toast.error('Error occurred in authentication: ' + err)
|
||||
return null
|
||||
})
|
||||
|
||||
if (redirectPath) navigateAfterLogin(redirectPath)
|
||||
})
|
||||
.catch((err) => {
|
||||
toast.error(
|
||||
'An error occurred while creating nsecbunker signer: ' + err
|
||||
)
|
||||
})
|
||||
.finally(() => {
|
||||
setIsLoading(false)
|
||||
setLoadingSpinnerDesc('')
|
||||
})
|
||||
}
|
||||
|
||||
const loginWithBunkerConnectionString = async () => {
|
||||
// Extract the key
|
||||
const keyStartIndex = inputValue.indexOf('bunker://') + 'bunker://'.length
|
||||
const keyEndIndex = inputValue.indexOf('?relay=')
|
||||
const key = inputValue.substring(keyStartIndex, keyEndIndex)
|
||||
|
||||
const pubkey = npubToHex(key)
|
||||
|
||||
if (!pubkey) {
|
||||
toast.error('Invalid pubkey in bunker connection string.')
|
||||
setIsLoading(false)
|
||||
return
|
||||
}
|
||||
|
||||
// Extract the relay value
|
||||
const relayIndex = inputValue.indexOf('relay=')
|
||||
const relay = inputValue.substring(
|
||||
relayIndex + 'relay='.length,
|
||||
inputValue.length
|
||||
)
|
||||
|
||||
setIsLoading(true)
|
||||
setLoadingSpinnerDesc('Initializing bunker NDK')
|
||||
|
||||
await nostrController.nsecBunkerInit([relay])
|
||||
|
||||
setLoadingSpinnerDesc('Creating remote signer')
|
||||
await nostrController
|
||||
.createNsecBunkerSigner(pubkey)
|
||||
.then(async (signer) => {
|
||||
signer.on('authUrl', (url: string) => {
|
||||
setAuthUrl(url)
|
||||
})
|
||||
|
||||
dispatch(updateLoginMethod(LoginMethods.nsecBunker))
|
||||
dispatch(updateNsecbunkerPubkey(pubkey))
|
||||
dispatch(updateNsecbunkerRelays([relay]))
|
||||
|
||||
setLoadingSpinnerDesc('Authenticating and finding metadata')
|
||||
|
||||
const redirectPath = await authController
|
||||
.authAndGetMetadataAndRelaysMap(pubkey!)
|
||||
.catch((err) => {
|
||||
toast.error('Error occurred in authentication: ' + err)
|
||||
return null
|
||||
})
|
||||
|
||||
if (redirectPath) navigateAfterLogin(redirectPath)
|
||||
})
|
||||
.catch((err) => {
|
||||
toast.error(
|
||||
'An error occurred while creating nsecbunker signer: ' + err
|
||||
)
|
||||
})
|
||||
.finally(() => {
|
||||
setIsLoading(false)
|
||||
setLoadingSpinnerDesc('')
|
||||
})
|
||||
}
|
||||
|
||||
const login = () => {
|
||||
if (inputValue.startsWith('bunker://')) {
|
||||
return loginWithBunkerConnectionString()
|
||||
}
|
||||
|
||||
if (inputValue.startsWith('nsec')) {
|
||||
return loginWithNsec()
|
||||
}
|
||||
if (inputValue.startsWith('npub')) {
|
||||
return loginWithNsecBunker()
|
||||
}
|
||||
if (inputValue.match(NIP05_REGEX)) {
|
||||
return loginWithNsecBunker()
|
||||
}
|
||||
|
||||
// Check if maybe hex nsec
|
||||
try {
|
||||
@ -358,64 +127,33 @@ export const Nostr = () => {
|
||||
console.warn('err', err)
|
||||
}
|
||||
|
||||
toast.error(
|
||||
'Invalid format, please use: private key (hex), nsec..., bunker:// or nip05 format.'
|
||||
)
|
||||
toast.error('Invalid format, please use: private key (hex or nsec)')
|
||||
return
|
||||
}
|
||||
|
||||
if (authUrl) {
|
||||
return (
|
||||
<iframe
|
||||
title="Nsecbunker auth"
|
||||
src={authUrl}
|
||||
width="100%"
|
||||
height="500px"
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{isLoading && (
|
||||
<LoadingSpinner desc={loadingSpinnerDesc}>
|
||||
{isExtensionSlow && (
|
||||
<>
|
||||
<p>
|
||||
Your nostr extension is not responding. Check these
|
||||
alternatives:{' '}
|
||||
<a href="https://github.com/aljazceru/awesome-nostr?tab=readme-ov-file#nip-07-browser-extensions">
|
||||
https://github.com/aljazceru/awesome-nostr
|
||||
</a>
|
||||
</p>
|
||||
<br />
|
||||
<Button
|
||||
fullWidth
|
||||
variant="contained"
|
||||
onClick={() => {
|
||||
setLoadingSpinnerDesc('')
|
||||
setIsLoading(false)
|
||||
setIsExtensionSlow(false)
|
||||
}}
|
||||
>
|
||||
Close
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</LoadingSpinner>
|
||||
)}
|
||||
|
||||
{isLoading && <LoadingSpinner desc={loadingSpinnerDesc} />}
|
||||
{isNostrExtensionAvailable && (
|
||||
<>
|
||||
<label className={styles.label} htmlFor="extension-login">
|
||||
Login by using a browser extension
|
||||
Login by using a{' '}
|
||||
<a
|
||||
rel="noopener"
|
||||
href="https://github.com/nostrband/nostr-login"
|
||||
target="_blank"
|
||||
>
|
||||
nostr-login
|
||||
</a>
|
||||
</label>
|
||||
<Button
|
||||
id="extension-login"
|
||||
onClick={loginWithExtension}
|
||||
id="nostr-login"
|
||||
variant="contained"
|
||||
onClick={() => {
|
||||
launchNostrLoginDialog('welcome')
|
||||
}}
|
||||
>
|
||||
Extension Login
|
||||
Nostr Login
|
||||
</Button>
|
||||
<Divider
|
||||
sx={{
|
||||
@ -426,42 +164,18 @@ export const Nostr = () => {
|
||||
</Divider>
|
||||
</>
|
||||
)}
|
||||
<label className={styles.label} htmlFor="extension-login">
|
||||
Login by using a{' '}
|
||||
<a
|
||||
rel="noopener"
|
||||
href="https://github.com/nostrband/nostr-login"
|
||||
target="_blank"
|
||||
>
|
||||
nostr-login
|
||||
</a>
|
||||
</label>
|
||||
<Button
|
||||
id="nostr-login"
|
||||
variant="contained"
|
||||
onClick={() => {
|
||||
launchNostrLoginDialog('welcome')
|
||||
}}
|
||||
>
|
||||
Nostr Login
|
||||
</Button>
|
||||
<Divider
|
||||
sx={{
|
||||
fontSize: '16px'
|
||||
}}
|
||||
>
|
||||
or
|
||||
</Divider>
|
||||
<TextField
|
||||
onKeyDown={handleInputKeyDown}
|
||||
label="nip05 login / nip46 bunker string"
|
||||
helperText="Private key (Not recommended)"
|
||||
value={inputValue}
|
||||
onChange={(e) => setInputValue(e.target.value)}
|
||||
fullWidth
|
||||
margin="dense"
|
||||
/>
|
||||
|
||||
<form autoComplete="off">
|
||||
<TextField
|
||||
onKeyDown={handleInputKeyDown}
|
||||
label="Private key (Not recommended)"
|
||||
type="password"
|
||||
autoComplete="off"
|
||||
value={inputValue}
|
||||
onChange={(e) => setInputValue(e.target.value)}
|
||||
fullWidth
|
||||
margin="dense"
|
||||
/>
|
||||
</form>
|
||||
<Button
|
||||
disabled={!inputValue}
|
||||
onClick={login}
|
||||
|
@ -2,25 +2,23 @@ import AccountCircleIcon from '@mui/icons-material/AccountCircle'
|
||||
import ArrowForwardIosIcon from '@mui/icons-material/ArrowForwardIos'
|
||||
import CachedIcon from '@mui/icons-material/Cached'
|
||||
import RouterIcon from '@mui/icons-material/Router'
|
||||
import { useTheme } from '@mui/material'
|
||||
import { ListItem, useTheme } from '@mui/material'
|
||||
import List from '@mui/material/List'
|
||||
import ListItemButton from '@mui/material/ListItemButton'
|
||||
import ListItemIcon from '@mui/material/ListItemIcon'
|
||||
import ListItemText from '@mui/material/ListItemText'
|
||||
import ListSubheader from '@mui/material/ListSubheader'
|
||||
import { useSelector } from 'react-redux'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { appPrivateRoutes, getProfileSettingsRoute } from '../../routes'
|
||||
import { State } from '../../store/rootReducer'
|
||||
import { Container } from '../../components/Container'
|
||||
import { Footer } from '../../components/Footer/Footer'
|
||||
import ExtensionIcon from '@mui/icons-material/Extension'
|
||||
import { LoginMethod } from '../../store/auth/types'
|
||||
|
||||
export const SettingsPage = () => {
|
||||
const theme = useTheme()
|
||||
|
||||
const navigate = useNavigate()
|
||||
const usersPubkey = useSelector((state: State) => state.auth.usersPubkey)
|
||||
|
||||
const { usersPubkey, loginMethod } = useSelector((state: State) => state.auth)
|
||||
const listItem = (label: string, disabled = false) => {
|
||||
return (
|
||||
<>
|
||||
@ -57,43 +55,40 @@ export const SettingsPage = () => {
|
||||
fontSize: '1.5rem',
|
||||
borderBottom: '0.5px solid',
|
||||
paddingBottom: 2,
|
||||
paddingTop: 2
|
||||
paddingTop: 2,
|
||||
zIndex: 2
|
||||
}}
|
||||
>
|
||||
Settings
|
||||
</ListSubheader>
|
||||
}
|
||||
>
|
||||
<ListItemButton
|
||||
onClick={() => {
|
||||
navigate(getProfileSettingsRoute(usersPubkey!))
|
||||
}}
|
||||
>
|
||||
<ListItem component={Link} to={getProfileSettingsRoute(usersPubkey!)}>
|
||||
<ListItemIcon>
|
||||
<AccountCircleIcon />
|
||||
</ListItemIcon>
|
||||
{listItem('Profile')}
|
||||
</ListItemButton>
|
||||
<ListItemButton
|
||||
onClick={() => {
|
||||
navigate(appPrivateRoutes.relays)
|
||||
}}
|
||||
>
|
||||
</ListItem>
|
||||
<ListItem component={Link} to={appPrivateRoutes.relays}>
|
||||
<ListItemIcon>
|
||||
<RouterIcon />
|
||||
</ListItemIcon>
|
||||
{listItem('Relays')}
|
||||
</ListItemButton>
|
||||
<ListItemButton
|
||||
onClick={() => {
|
||||
navigate(appPrivateRoutes.cacheSettings)
|
||||
}}
|
||||
>
|
||||
</ListItem>
|
||||
<ListItem component={Link} to={appPrivateRoutes.cacheSettings}>
|
||||
<ListItemIcon>
|
||||
<CachedIcon />
|
||||
</ListItemIcon>
|
||||
{listItem('Local Cache')}
|
||||
</ListItemButton>
|
||||
</ListItem>
|
||||
{loginMethod === LoginMethod.nostrLogin && (
|
||||
<ListItem component={Link} to={appPrivateRoutes.nostrLogin}>
|
||||
<ListItemIcon>
|
||||
<ExtensionIcon />
|
||||
</ListItemIcon>
|
||||
{listItem('Nostr Login')}
|
||||
</ListItem>
|
||||
)}
|
||||
</List>
|
||||
</Container>
|
||||
<Footer />
|
||||
|
3
src/pages/settings/cache/index.tsx
vendored
3
src/pages/settings/cache/index.tsx
vendored
@ -66,7 +66,8 @@ export const CacheSettingsPage = () => {
|
||||
fontSize: '1.5rem',
|
||||
borderBottom: '0.5px solid',
|
||||
paddingBottom: 2,
|
||||
paddingTop: 2
|
||||
paddingTop: 2,
|
||||
zIndex: 2
|
||||
}}
|
||||
>
|
||||
Cache Setting
|
||||
|
71
src/pages/settings/nostrLogin/index.tsx
Normal file
71
src/pages/settings/nostrLogin/index.tsx
Normal file
@ -0,0 +1,71 @@
|
||||
import {
|
||||
List,
|
||||
ListItemButton,
|
||||
ListItemIcon,
|
||||
ListItemText,
|
||||
ListSubheader,
|
||||
useTheme
|
||||
} from '@mui/material'
|
||||
import { launch as launchNostrLoginDialog } from 'nostr-login'
|
||||
import { Container } from '../../../components/Container'
|
||||
import PeopleIcon from '@mui/icons-material/People'
|
||||
import ImportExportIcon from '@mui/icons-material/ImportExport'
|
||||
|
||||
export const NostrLoginPage = () => {
|
||||
const theme = useTheme()
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<List
|
||||
sx={{
|
||||
width: '100%',
|
||||
bgcolor: 'background.paper'
|
||||
}}
|
||||
subheader={
|
||||
<ListSubheader
|
||||
sx={{
|
||||
fontSize: '1.5rem',
|
||||
borderBottom: '0.5px solid',
|
||||
paddingBottom: 2,
|
||||
paddingTop: 2,
|
||||
zIndex: 2
|
||||
}}
|
||||
>
|
||||
Nostr Settings
|
||||
</ListSubheader>
|
||||
}
|
||||
>
|
||||
<ListItemButton
|
||||
onClick={() => {
|
||||
launchNostrLoginDialog('switch-account')
|
||||
}}
|
||||
>
|
||||
<ListItemIcon>
|
||||
<PeopleIcon />
|
||||
</ListItemIcon>
|
||||
<ListItemText
|
||||
primary={'Nostr Login Accounts'}
|
||||
sx={{
|
||||
color: theme.palette.text.primary
|
||||
}}
|
||||
/>
|
||||
</ListItemButton>
|
||||
<ListItemButton
|
||||
onClick={() => {
|
||||
launchNostrLoginDialog('import')
|
||||
}}
|
||||
>
|
||||
<ListItemIcon>
|
||||
<ImportExportIcon />
|
||||
</ListItemIcon>
|
||||
<ListItemText
|
||||
primary={'Import / Export Keys'}
|
||||
sx={{
|
||||
color: theme.palette.text.primary
|
||||
}}
|
||||
/>
|
||||
</ListItemButton>
|
||||
</List>
|
||||
</Container>
|
||||
)
|
||||
}
|
@ -24,7 +24,7 @@ import { LoadingButton } from '@mui/lab'
|
||||
import { Dispatch } from '../../../store/store'
|
||||
import { setMetadataEvent } from '../../../store/actions'
|
||||
import { LoadingSpinner } from '../../../components/LoadingSpinner'
|
||||
import { LoginMethods } from '../../../store/auth/types'
|
||||
import { LoginMethod, NostrLoginAuthMethod } from '../../../store/auth/types'
|
||||
import { SmartToy } from '@mui/icons-material'
|
||||
import {
|
||||
getNostrJoiningBlockNumber,
|
||||
@ -33,6 +33,8 @@ import {
|
||||
} from '../../../utils'
|
||||
import { Container } from '../../../components/Container'
|
||||
import { Footer } from '../../../components/Footer/Footer'
|
||||
import LaunchIcon from '@mui/icons-material/Launch'
|
||||
import { launch as launchNostrLoginDialog } from 'nostr-login'
|
||||
|
||||
export const ProfileSettingsPage = () => {
|
||||
const theme = useTheme()
|
||||
@ -51,7 +53,9 @@ export const ProfileSettingsPage = () => {
|
||||
const [savingProfileMetadata, setSavingProfileMetadata] = useState(false)
|
||||
const metadataState = useSelector((state: State) => state.metadata)
|
||||
const keys = useSelector((state: State) => state.auth?.keyPair)
|
||||
const { usersPubkey, loginMethod } = useSelector((state: State) => state.auth)
|
||||
const { usersPubkey, loginMethod, nostrLoginAuthMethod } = useSelector(
|
||||
(state: State) => state.auth
|
||||
)
|
||||
const userRobotImage = useSelector((state: State) => state.userRobotImage)
|
||||
|
||||
const [isUsersOwnProfile, setIsUsersOwnProfile] = useState(false)
|
||||
@ -287,7 +291,8 @@ export const ProfileSettingsPage = () => {
|
||||
sx={{
|
||||
paddingBottom: 1,
|
||||
paddingTop: 1,
|
||||
fontSize: '1.5rem'
|
||||
fontSize: '1.5rem',
|
||||
zIndex: 2
|
||||
}}
|
||||
className={styles.subHeader}
|
||||
>
|
||||
@ -363,7 +368,7 @@ export const ProfileSettingsPage = () => {
|
||||
<>
|
||||
{usersPubkey &&
|
||||
copyItem(nip19.npubEncode(usersPubkey), 'Public Key')}
|
||||
{loginMethod === LoginMethods.privateKey &&
|
||||
{loginMethod === LoginMethod.privateKey &&
|
||||
keys &&
|
||||
keys.private &&
|
||||
copyItem(
|
||||
@ -373,6 +378,33 @@ export const ProfileSettingsPage = () => {
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{isUsersOwnProfile && (
|
||||
<>
|
||||
{loginMethod === LoginMethod.nostrLogin &&
|
||||
nostrLoginAuthMethod === NostrLoginAuthMethod.Local && (
|
||||
<ListItem
|
||||
sx={{ marginTop: 1 }}
|
||||
onClick={() => {
|
||||
launchNostrLoginDialog('import')
|
||||
}}
|
||||
>
|
||||
<TextField
|
||||
label="Private Key (nostr-login)"
|
||||
defaultValue="••••••••••••••••••••••••••••••••••••••••••••••••••"
|
||||
size="small"
|
||||
className={styles.textField}
|
||||
disabled
|
||||
type={'password'}
|
||||
InputProps={{
|
||||
endAdornment: (
|
||||
<LaunchIcon className={styles.copyItem} />
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</ListItem>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</List>
|
||||
|
@ -131,7 +131,6 @@ export const SignPage = () => {
|
||||
|
||||
const usersPubkey = useSelector((state: State) => state.auth.usersPubkey)
|
||||
|
||||
const [authUrl, setAuthUrl] = useState<string>()
|
||||
const nostrController = NostrController.getInstance()
|
||||
const [currentUserMarks, setCurrentUserMarks] = useState<CurrentUserMark[]>(
|
||||
[]
|
||||
@ -295,11 +294,6 @@ export const SignPage = () => {
|
||||
const { keys, sender } = parsedKeysJson
|
||||
|
||||
for (const key of keys) {
|
||||
// Set up event listener for authentication event
|
||||
nostrController.on('nsecbunker-auth', (url) => {
|
||||
setAuthUrl(url)
|
||||
})
|
||||
|
||||
// decrypt the encryptionKey, with timeout (duration = 60 seconds)
|
||||
const encryptionKey = await Promise.race([
|
||||
nostrController.nip04Decrypt(sender, key),
|
||||
@ -312,9 +306,6 @@ export const SignPage = () => {
|
||||
console.log('err :>> ', err)
|
||||
return null
|
||||
})
|
||||
.finally(() => {
|
||||
setAuthUrl(undefined) // Clear authentication URL
|
||||
})
|
||||
|
||||
// Return if encryption failed
|
||||
if (!encryptionKey) continue
|
||||
@ -888,17 +879,6 @@ export const SignPage = () => {
|
||||
}
|
||||
}
|
||||
|
||||
if (authUrl) {
|
||||
return (
|
||||
<iframe
|
||||
title="Nsecbunker auth"
|
||||
src={authUrl}
|
||||
width="100%"
|
||||
height="500px"
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
if (isLoading) {
|
||||
return <LoadingSpinner desc={loadingSpinnerDesc} />
|
||||
}
|
||||
|
@ -14,6 +14,7 @@
|
||||
border-bottom: 0.5px solid;
|
||||
padding: 8px 16px;
|
||||
font-size: 1.5rem;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.filesWrapper {
|
||||
|
@ -8,6 +8,7 @@ import { ProfilePage } from '../pages/profile'
|
||||
import { Register } from '../pages/register'
|
||||
import { SettingsPage } from '../pages/settings/Settings'
|
||||
import { CacheSettingsPage } from '../pages/settings/cache'
|
||||
import { NostrLoginPage } from '../pages/settings/nostrLogin'
|
||||
import { ProfileSettingsPage } from '../pages/settings/profile'
|
||||
import { RelaysPage } from '../pages/settings/relays'
|
||||
import { SignPage } from '../pages/sign'
|
||||
@ -22,7 +23,8 @@ export const appPrivateRoutes = {
|
||||
settings: '/settings',
|
||||
profileSettings: '/settings/profile/:npub',
|
||||
cacheSettings: '/settings/cache',
|
||||
relays: '/settings/relays'
|
||||
relays: '/settings/relays',
|
||||
nostrLogin: '/settings/nostrLogin'
|
||||
}
|
||||
|
||||
export const appPublicRoutes = {
|
||||
@ -147,5 +149,9 @@ export const privateRoutes = [
|
||||
{
|
||||
path: appPrivateRoutes.relays,
|
||||
element: <RelaysPage />
|
||||
},
|
||||
{
|
||||
path: appPrivateRoutes.nostrLogin,
|
||||
element: <NostrLoginPage />
|
||||
}
|
||||
]
|
||||
|
75
src/services/LoginMethodStrategy/NostrLoginStrategy.ts
Normal file
75
src/services/LoginMethodStrategy/NostrLoginStrategy.ts
Normal file
@ -0,0 +1,75 @@
|
||||
import { Event, UnsignedEvent, EventTemplate, NostrEvent } from 'nostr-tools'
|
||||
import { SignedEvent } from '../../types'
|
||||
import { LoginMethodStrategy } from './loginMethodStrategy'
|
||||
import { WindowNostr } from 'nostr-tools/nip07'
|
||||
|
||||
export class NostrLoginStrategy extends LoginMethodStrategy {
|
||||
private nostr: WindowNostr
|
||||
|
||||
constructor() {
|
||||
super()
|
||||
if (!window.nostr) {
|
||||
throw new Error(
|
||||
`window.nostr object not present. Make sure you have an nostr extension installed/working properly.`
|
||||
)
|
||||
}
|
||||
|
||||
this.nostr = window.nostr as WindowNostr
|
||||
}
|
||||
|
||||
async nip04Encrypt(receiver: string, content: string): Promise<string> {
|
||||
if (!this.nostr.nip04) {
|
||||
throw new Error(
|
||||
`Your nostr extension does not support nip04 encryption & decryption`
|
||||
)
|
||||
}
|
||||
|
||||
const encrypted = await this.nostr.nip04.encrypt(receiver, content)
|
||||
return encrypted
|
||||
}
|
||||
|
||||
async nip04EDecrypt(sender: string, content: string): Promise<string> {
|
||||
if (!this.nostr.nip04) {
|
||||
throw new Error(
|
||||
`Your nostr extension does not support nip04 encryption & decryption`
|
||||
)
|
||||
}
|
||||
|
||||
const decrypted = await this.nostr.nip04.decrypt(sender, content)
|
||||
return decrypted
|
||||
}
|
||||
|
||||
async nip44Encrypt(receiver: string, content: string): Promise<string> {
|
||||
if (!this.nostr.nip44) {
|
||||
throw new Error(
|
||||
`Your nostr extension does not support nip44 encryption & decryption`
|
||||
)
|
||||
}
|
||||
|
||||
// Encrypt the content using NIP-44 provided by the nostr extension.
|
||||
const encrypted = await this.nostr.nip44.encrypt(receiver, content)
|
||||
return encrypted as string
|
||||
}
|
||||
|
||||
async nip44EDecrypt(sender: string, content: string): Promise<string> {
|
||||
if (!this.nostr.nip44) {
|
||||
throw new Error(
|
||||
`Your nostr extension does not support nip44 encryption & decryption`
|
||||
)
|
||||
}
|
||||
|
||||
// Decrypt the content using NIP-44 provided by the nostr extension.
|
||||
const decrypted = await this.nostr.nip44.decrypt(sender, content)
|
||||
return decrypted as string
|
||||
}
|
||||
|
||||
async signEvent(event: UnsignedEvent | EventTemplate): Promise<SignedEvent> {
|
||||
return (await this.nostr
|
||||
.signEvent(event as NostrEvent)
|
||||
.catch((err: unknown) => {
|
||||
console.log('Error while signing event: ', err)
|
||||
|
||||
throw err
|
||||
})) as Event
|
||||
}
|
||||
}
|
118
src/services/LoginMethodStrategy/PrivateKeyStrategy.ts
Normal file
118
src/services/LoginMethodStrategy/PrivateKeyStrategy.ts
Normal file
@ -0,0 +1,118 @@
|
||||
import {
|
||||
UnsignedEvent,
|
||||
EventTemplate,
|
||||
nip19,
|
||||
nip44,
|
||||
finalizeEvent,
|
||||
nip04
|
||||
} from 'nostr-tools'
|
||||
import { SignedEvent } from '../../types'
|
||||
import store from '../../store/store'
|
||||
import { AuthState, LoginMethod } from '../../store/auth/types'
|
||||
import { LoginMethodStrategy } from './loginMethodStrategy'
|
||||
import { verifySignedEvent } from '../../utils/nostr'
|
||||
|
||||
export class PrivateKeyStrategy extends LoginMethodStrategy {
|
||||
async nip04Encrypt(receiver: string, content: string): Promise<string> {
|
||||
const keys = (store.getState().auth as AuthState).keyPair
|
||||
|
||||
if (!keys) {
|
||||
throw new Error(
|
||||
`Login method is ${LoginMethod.privateKey} but private & public key pair is not found.`
|
||||
)
|
||||
}
|
||||
|
||||
const { private: nsec } = keys
|
||||
const privateKey = nip19.decode(nsec).data as Uint8Array
|
||||
|
||||
const encrypted = await nip04.encrypt(privateKey, receiver, content)
|
||||
return encrypted
|
||||
}
|
||||
|
||||
async nip04EDecrypt(sender: string, content: string): Promise<string> {
|
||||
const keys = (store.getState().auth as AuthState).keyPair
|
||||
|
||||
if (!keys) {
|
||||
throw new Error(
|
||||
`Login method is ${LoginMethod.privateKey} but private & public key pair is not found.`
|
||||
)
|
||||
}
|
||||
|
||||
const { private: nsec } = keys
|
||||
const privateKey = nip19.decode(nsec).data as Uint8Array
|
||||
|
||||
const decrypted = await nip04.decrypt(privateKey, sender, content)
|
||||
return decrypted
|
||||
}
|
||||
|
||||
async nip44Encrypt(receiver: string, content: string): Promise<string> {
|
||||
const keys = (store.getState().auth as AuthState).keyPair
|
||||
|
||||
// Check if the private and public key pair is available.
|
||||
if (!keys) {
|
||||
throw new Error(
|
||||
`Login method is ${LoginMethod.privateKey} but private & public key pair is not found.`
|
||||
)
|
||||
}
|
||||
|
||||
// Decode the private key.
|
||||
const { private: nsec } = keys
|
||||
const privateKey = nip19.decode(nsec).data as Uint8Array
|
||||
|
||||
// Generate the conversation key using NIP-44 utilities.
|
||||
const nip44ConversationKey = nip44.v2.utils.getConversationKey(
|
||||
privateKey,
|
||||
receiver
|
||||
)
|
||||
|
||||
// Encrypt the content using the generated conversation key.
|
||||
const encrypted = nip44.v2.encrypt(content, nip44ConversationKey)
|
||||
|
||||
return encrypted
|
||||
}
|
||||
|
||||
async nip44EDecrypt(sender: string, content: string): Promise<string> {
|
||||
const keys = (store.getState().auth as AuthState).keyPair
|
||||
|
||||
// Check if the private and public key pair is available.
|
||||
if (!keys) {
|
||||
throw new Error(
|
||||
`Login method is ${LoginMethod.privateKey} but private & public key pair is not found.`
|
||||
)
|
||||
}
|
||||
|
||||
// Decode the private key.
|
||||
const { private: nsec } = keys
|
||||
const privateKey = nip19.decode(nsec).data as Uint8Array
|
||||
|
||||
// Generate the conversation key using NIP-44 utilities.
|
||||
const nip44ConversationKey = nip44.v2.utils.getConversationKey(
|
||||
privateKey,
|
||||
sender
|
||||
)
|
||||
|
||||
// Decrypt the content using the generated conversation key.
|
||||
const decrypted = nip44.v2.decrypt(content, nip44ConversationKey)
|
||||
|
||||
return decrypted
|
||||
}
|
||||
|
||||
async signEvent(event: UnsignedEvent | EventTemplate): Promise<SignedEvent> {
|
||||
const keys = (store.getState().auth as AuthState).keyPair
|
||||
|
||||
if (!keys) {
|
||||
return Promise.reject(
|
||||
`Login method is ${LoginMethod.privateKey}, but keys are not found`
|
||||
)
|
||||
}
|
||||
|
||||
const { private: nsec } = keys
|
||||
const privateKey = nip19.decode(nsec).data as Uint8Array
|
||||
|
||||
const signedEvent = finalizeEvent(event, privateKey)
|
||||
|
||||
verifySignedEvent(signedEvent)
|
||||
|
||||
return Promise.resolve(signedEvent)
|
||||
}
|
||||
}
|
43
src/services/LoginMethodStrategy/loginMethodContext.ts
Normal file
43
src/services/LoginMethodStrategy/loginMethodContext.ts
Normal file
@ -0,0 +1,43 @@
|
||||
import { UnsignedEvent, EventTemplate } from 'nostr-tools'
|
||||
import { SignedEvent } from '../../types'
|
||||
import {
|
||||
LoginMethodStrategy,
|
||||
LoginMethodOperations
|
||||
} from './loginMethodStrategy'
|
||||
import { LoginMethod } from '../../store/auth/types'
|
||||
import { NostrLoginStrategy } from './NostrLoginStrategy'
|
||||
import { PrivateKeyStrategy } from './PrivateKeyStrategy'
|
||||
|
||||
export class LoginMethodContext implements LoginMethodOperations {
|
||||
private strategy: LoginMethodStrategy
|
||||
|
||||
constructor(loginMethod?: LoginMethod) {
|
||||
switch (loginMethod) {
|
||||
case LoginMethod.nostrLogin:
|
||||
this.strategy = new NostrLoginStrategy()
|
||||
break
|
||||
case LoginMethod.privateKey:
|
||||
this.strategy = new PrivateKeyStrategy()
|
||||
break
|
||||
default:
|
||||
this.strategy = new LoginMethodStrategy()
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
nip04Encrypt(receiver: string, content: string): Promise<string> {
|
||||
return this.strategy.nip04Encrypt(receiver, content)
|
||||
}
|
||||
nip04EDecrypt(sender: string, content: string): Promise<string> {
|
||||
return this.strategy.nip04EDecrypt(sender, content)
|
||||
}
|
||||
nip44Encrypt(receiver: string, content: string): Promise<string> {
|
||||
return this.strategy.nip44Encrypt(receiver, content)
|
||||
}
|
||||
nip44EDecrypt(sender: string, content: string): Promise<string> {
|
||||
return this.strategy.nip44EDecrypt(sender, content)
|
||||
}
|
||||
signEvent(event: UnsignedEvent | EventTemplate): Promise<SignedEvent> {
|
||||
return this.strategy.signEvent(event)
|
||||
}
|
||||
}
|
31
src/services/LoginMethodStrategy/loginMethodStrategy.ts
Normal file
31
src/services/LoginMethodStrategy/loginMethodStrategy.ts
Normal file
@ -0,0 +1,31 @@
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
import { EventTemplate, UnsignedEvent } from 'nostr-tools'
|
||||
import { SignedEvent } from '../../types/nostr'
|
||||
|
||||
export interface LoginMethodOperations {
|
||||
nip04Encrypt(receiver: string, content: string): Promise<string>
|
||||
nip04EDecrypt(sender: string, content: string): Promise<string>
|
||||
nip44Encrypt(receiver: string, content: string): Promise<string>
|
||||
nip44EDecrypt(sender: string, content: string): Promise<string>
|
||||
signEvent(event: UnsignedEvent | EventTemplate): Promise<SignedEvent>
|
||||
}
|
||||
|
||||
export class LoginMethodStrategy implements LoginMethodOperations {
|
||||
async nip04Encrypt(_receiver: string, _content: string): Promise<string> {
|
||||
throw new Error('Login method strategy is undefined')
|
||||
}
|
||||
async nip04EDecrypt(_sender: string, _content: string): Promise<string> {
|
||||
throw new Error('Login method strategy is undefined')
|
||||
}
|
||||
async nip44Encrypt(_receiver: string, _content: string): Promise<string> {
|
||||
throw new Error('Login method strategy is undefined')
|
||||
}
|
||||
async nip44EDecrypt(_sender: string, _content: string): Promise<string> {
|
||||
throw new Error('Login method strategy is undefined')
|
||||
}
|
||||
async signEvent(_event: UnsignedEvent | EventTemplate): Promise<SignedEvent> {
|
||||
return Promise.reject(
|
||||
`We could not sign the event, none of the signing methods are available`
|
||||
)
|
||||
}
|
||||
}
|
@ -6,8 +6,6 @@ export const SET_AUTH_STATE = 'SET_AUTH_STATE'
|
||||
export const UPDATE_LOGIN_METHOD = 'UPDATE_LOGIN_METHOD'
|
||||
export const UPDATE_NOSTR_LOGIN_AUTH_METHOD = 'UPDATE_NOSTR_LOGIN_AUTH_METHOD'
|
||||
export const UPDATE_KEYPAIR = 'UPDATE_KEYPAIR'
|
||||
export const UPDATE_NSECBUNKER_PUBKEY = 'UPDATE_NSECBUNKER_PUBKEY'
|
||||
export const UPDATE_NSECBUNKER_RELAYS = 'UPDATE_NSECBUNKER_RELAYS'
|
||||
|
||||
export const SET_METADATA_EVENT = 'SET_METADATA_EVENT'
|
||||
|
||||
|
@ -2,12 +2,10 @@ import * as ActionTypes from '../actionTypes'
|
||||
import {
|
||||
AuthState,
|
||||
Keys,
|
||||
LoginMethods,
|
||||
LoginMethod,
|
||||
SetAuthState,
|
||||
UpdateKeyPair,
|
||||
UpdateLoginMethod,
|
||||
UpdateNsecBunkerPubkey,
|
||||
UpdateNsecBunkerRelays,
|
||||
NostrLoginAuthMethod,
|
||||
UpdateNostrLoginAuthMethod
|
||||
} from './types'
|
||||
@ -18,7 +16,7 @@ export const setAuthState = (payload: AuthState): SetAuthState => ({
|
||||
})
|
||||
|
||||
export const updateLoginMethod = (
|
||||
payload: LoginMethods | undefined
|
||||
payload: LoginMethod | undefined
|
||||
): UpdateLoginMethod => ({
|
||||
type: ActionTypes.UPDATE_LOGIN_METHOD,
|
||||
payload
|
||||
@ -35,17 +33,3 @@ export const updateKeyPair = (payload: Keys | undefined): UpdateKeyPair => ({
|
||||
type: ActionTypes.UPDATE_KEYPAIR,
|
||||
payload
|
||||
})
|
||||
|
||||
export const updateNsecbunkerPubkey = (
|
||||
payload: string | undefined
|
||||
): UpdateNsecBunkerPubkey => ({
|
||||
type: ActionTypes.UPDATE_NSECBUNKER_PUBKEY,
|
||||
payload
|
||||
})
|
||||
|
||||
export const updateNsecbunkerRelays = (
|
||||
payload: string[] | undefined
|
||||
): UpdateNsecBunkerRelays => ({
|
||||
type: ActionTypes.UPDATE_NSECBUNKER_RELAYS,
|
||||
payload
|
||||
})
|
||||
|
@ -11,13 +11,12 @@ const reducer = (
|
||||
): AuthState | null => {
|
||||
switch (action.type) {
|
||||
case ActionTypes.SET_AUTH_STATE: {
|
||||
const { loginMethod, keyPair, nsecBunkerPubkey, nsecBunkerRelays } = state
|
||||
const { loginMethod, nostrLoginAuthMethod, keyPair } = state
|
||||
|
||||
return {
|
||||
loginMethod,
|
||||
nostrLoginAuthMethod,
|
||||
keyPair,
|
||||
nsecBunkerPubkey,
|
||||
nsecBunkerRelays,
|
||||
...action.payload
|
||||
}
|
||||
}
|
||||
@ -48,24 +47,6 @@ const reducer = (
|
||||
}
|
||||
}
|
||||
|
||||
case ActionTypes.UPDATE_NSECBUNKER_PUBKEY: {
|
||||
const { payload } = action
|
||||
|
||||
return {
|
||||
...state,
|
||||
nsecBunkerPubkey: payload
|
||||
}
|
||||
}
|
||||
|
||||
case ActionTypes.UPDATE_NSECBUNKER_RELAYS: {
|
||||
const { payload } = action
|
||||
|
||||
return {
|
||||
...state,
|
||||
nsecBunkerRelays: payload
|
||||
}
|
||||
}
|
||||
|
||||
case ActionTypes.RESTORE_STATE:
|
||||
return action.payload.auth
|
||||
|
||||
|
@ -9,10 +9,9 @@ export enum NostrLoginAuthMethod {
|
||||
OTP = 'otp'
|
||||
}
|
||||
|
||||
export enum LoginMethods {
|
||||
extension = 'extension',
|
||||
export enum LoginMethod {
|
||||
nostrLogin = 'nostrLogin',
|
||||
privateKey = 'privateKey',
|
||||
nsecBunker = 'nsecBunker',
|
||||
register = 'register'
|
||||
}
|
||||
|
||||
@ -24,11 +23,9 @@ export interface Keys {
|
||||
export interface AuthState {
|
||||
loggedIn: boolean
|
||||
usersPubkey?: string
|
||||
loginMethod?: LoginMethods
|
||||
loginMethod?: LoginMethod
|
||||
nostrLoginAuthMethod?: NostrLoginAuthMethod
|
||||
keyPair?: Keys
|
||||
nsecBunkerPubkey?: string
|
||||
nsecBunkerRelays?: string[]
|
||||
}
|
||||
|
||||
export interface SetAuthState {
|
||||
@ -38,7 +35,7 @@ export interface SetAuthState {
|
||||
|
||||
export interface UpdateLoginMethod {
|
||||
type: typeof ActionTypes.UPDATE_LOGIN_METHOD
|
||||
payload: LoginMethods | undefined
|
||||
payload: LoginMethod | undefined
|
||||
}
|
||||
|
||||
export interface UpdateNostrLoginAuthMethod {
|
||||
@ -51,22 +48,10 @@ export interface UpdateKeyPair {
|
||||
payload: Keys | undefined
|
||||
}
|
||||
|
||||
export interface UpdateNsecBunkerPubkey {
|
||||
type: typeof ActionTypes.UPDATE_NSECBUNKER_PUBKEY
|
||||
payload: string | undefined
|
||||
}
|
||||
|
||||
export interface UpdateNsecBunkerRelays {
|
||||
type: typeof ActionTypes.UPDATE_NSECBUNKER_RELAYS
|
||||
payload: string[] | undefined
|
||||
}
|
||||
|
||||
export type AuthDispatchTypes =
|
||||
| RestoreState
|
||||
| SetAuthState
|
||||
| UpdateLoginMethod
|
||||
| UpdateNostrLoginAuthMethod
|
||||
| UpdateKeyPair
|
||||
| UpdateNsecBunkerPubkey
|
||||
| UpdateNsecBunkerRelays
|
||||
| UserLogout
|
||||
|
@ -26,14 +26,6 @@ export const clearState = () => {
|
||||
localStorage.removeItem('state')
|
||||
}
|
||||
|
||||
export const saveNsecBunkerDelegatedKey = (privateKey: string) => {
|
||||
localStorage.setItem('nsecbunker-delegated-key', privateKey)
|
||||
}
|
||||
|
||||
export const getNsecBunkerDelegatedKey = () => {
|
||||
return localStorage.getItem('nsecbunker-delegated-key')
|
||||
}
|
||||
|
||||
export const saveVisitedLink = (pathname: string, search: string) => {
|
||||
localStorage.setItem(
|
||||
'visitedLink',
|
||||
@ -69,3 +61,8 @@ export const getAuthToken = () => {
|
||||
export const clearAuthToken = () => {
|
||||
localStorage.removeItem('authToken')
|
||||
}
|
||||
|
||||
export const clear = () => {
|
||||
clearAuthToken()
|
||||
clearState()
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user