Skip to content

Commit

Permalink
verify signer on settings save
Browse files Browse the repository at this point in the history
  • Loading branch information
riccardobl committed Dec 23, 2024
1 parent 2063894 commit 462af88
Show file tree
Hide file tree
Showing 3 changed files with 62 additions and 18 deletions.
34 changes: 23 additions & 11 deletions components/nostr-auth.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,12 +35,10 @@ function NostrError ({ message }) {
}

export function useNostrAuthState ({
showModalStatus = false,
challengeTitle = 'Waiting for confirmation',
challengeMessage = 'Please confirm this action on your remote signer',
challengeButtonLabel = 'open signer'
} = {}) {
const showModal = useShowModal()
const toaster = useToast()

const [status, setStatus] = useState({
Expand Down Expand Up @@ -87,16 +85,30 @@ export function useNostrAuthState ({
}
}, [])

return { status, setStatus, setError, challengeResolver }
}

export function useNostrAuthStateModal ({
...args
}) {
const showModal = useShowModal()

const { status, setStatus, setError, challengeResolver } = useNostrAuthState(args)
const closeModalRef = useRef(null)

useEffect(() => {
if (!showModalStatus || !status?.loading) return
showModal(onClose => {
return (
<>
<h3 className='w-100 pb-2'>{status.title}</h3>
<NostrAuthStatus status={status} />
</>
)
})
closeModalRef?.current?.()
if (status.loading) {
showModal(onClose => {
closeModalRef.current = onClose
return (
<>
<h3 className='w-100 pb-2'>{status.title}</h3>
<NostrAuthStatus status={status} />
</>
)
})
}
}, [status])

return { status, setStatus, setError, challengeResolver }
Expand Down
8 changes: 4 additions & 4 deletions components/use-crossposter.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { ITEM_FULL_FIELDS, POLL_FIELDS } from '@/fragments/items'
import { useMe } from '@/components/me'
import { useEncryptedPrivates } from '@/components/use-encrypted-privates'
import { NDKNip46Signer } from '@nostr-dev-kit/ndk'
import { useNostrAuthState } from '@/components/nostr-auth'
import { useNostrAuthStateModal } from '@/components/nostr-auth'

function itemToContent (item, { includeTitle = true } = {}) {
let content = includeTitle ? item.title : ''
Expand Down Expand Up @@ -93,8 +93,7 @@ export default function useCrossposter () {
const { me } = useMe()
const { encryptedPrivates } = useEncryptedPrivates({ me })

const { challengeResolver } = useNostrAuthState({
showModalStatus: true,
const { challengeResolver: nostrAuthChallengeResolver, setStatus: nostrAuthSetStatus } = useNostrAuthStateModal({
challengeTitle: 'Crossposting to Nostr'
})

Expand Down Expand Up @@ -219,9 +218,10 @@ export default function useCrossposter () {
nip07: true
})
if (signer instanceof NDKNip46Signer) {
signer.once('authUrl', challengeResolver)
signer.once('authUrl', nostrAuthChallengeResolver)
}
await signer.blockUntilReady()
nostrAuthSetStatus({ success: true })

do {
try {
Expand Down
38 changes: 35 additions & 3 deletions pages/settings/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,13 @@ import Info from '@/components/info'
import Link from 'next/link'
import AccordianItem from '@/components/accordian-item'
import { bech32 } from 'bech32'
import { NOSTR_MAX_RELAY_NUM, NOSTR_PUBKEY_BECH32, DEFAULT_CROSSPOSTING_RELAYS } from '@/lib/nostr'
import Nostr, { NOSTR_MAX_RELAY_NUM, NOSTR_PUBKEY_BECH32, DEFAULT_CROSSPOSTING_RELAYS } from '@/lib/nostr'
import { emailSchema, lastAuthRemovalSchema, settingsSchema } from '@/lib/validate'
import { SUPPORTED_CURRENCIES } from '@/lib/currency'
import PageLoading from '@/components/page-loading'
import { useShowModal } from '@/components/modal'
import { authErrorMessage } from '@/components/login'
import { NostrAuth } from '@/components/nostr-auth'
import { NostrAuth, useNostrAuthStateModal } from '@/components/nostr-auth'
import { useToast } from '@/components/toast'
import { useServiceWorkerLogger } from '@/components/logger'
import { useMe } from '@/components/me'
Expand All @@ -34,6 +34,8 @@ import { AuthBanner } from '@/components/banners'
import { useEncryptedPrivates } from '@/components/use-encrypted-privates'
import { generateSecretKey } from 'nostr-tools'
import { bytesToHex } from '@noble/hashes/utils'
import { NDKNip46Signer } from '@nostr-dev-kit/ndk'
import { callWithTimeout } from '@/lib/time'

export const getServerSideProps = getGetServerSideProps({ query: SETTINGS, authRequired: true })

Expand Down Expand Up @@ -93,7 +95,7 @@ export function SettingsHeader () {
export default function Settings ({ ssrData }) {
const toaster = useToast()
const { me } = useMe()
const { encryptedPrivates, setEncryptedSettings } = useEncryptedPrivates({ me })
const { encryptedPrivates, setEncryptedSettings, refreshEncryptedPrivates } = useEncryptedPrivates({ me })

const [setSettings] = useMutation(SET_SETTINGS, {
update (cache, { data: { setSettings } }) {
Expand All @@ -112,6 +114,10 @@ export default function Settings ({ ssrData }) {
const { data } = useQuery(SETTINGS)
const [settings, setSettingsState] = useState(() => (data ?? ssrData)?.settings?.privates)

const { challengeResolver: nostrAuthChallengeResolver, setStatus: nostrAuthSetStatus } = useNostrAuthStateModal({
challengeTitle: 'Configuring crosspost to Nostr'
})

useEffect(() => {
let settings = (data ?? ssrData)?.settings?.privates
if (!settings) return
Expand Down Expand Up @@ -221,8 +227,34 @@ export default function Settings ({ ssrData }) {
// this is used to identify the app instance by nip46 and permit
// token reuse
signerInstanceKey = bytesToHex(generateSecretKey())

// we create the initial connection for the nip46 signer here
// because it makes for better ux (no confirmation delay on crosspost)
// and because we can test immediately if the connection works
if (signerType === 'nip46' && signer) {
const nostr = new Nostr()
try {
await callWithTimeout(async () => {
const testSignerInstance = nostr.getSigner({
userPreferences: { signer, signerType, signerInstanceKey }
})
if (testSignerInstance instanceof NDKNip46Signer) {
testSignerInstance.once('authUrl', nostrAuthChallengeResolver)
}
await testSignerInstance.blockUntilReady()
nostrAuthSetStatus({ success: true })
}, 60_000 * 5) // after some time we give up, likely the signer is not responding at this point
} catch (err) {
toaster.danger('invalid nostr signer: ' + err.message)
throw err
} finally {
nostr.close()
}
}
}

await setEncryptedSettings({ signer, signerType, signerInstanceKey })
await refreshEncryptedPrivates()

toaster.success('saved settings')
} catch (err) {
Expand Down

0 comments on commit 462af88

Please sign in to comment.