Assign name on login, change confirm modals, change push warning, reject reqs before connect

This commit is contained in:
artur
2024-02-07 10:41:00 +03:00
parent 326d824451
commit d00e16139e
8 changed files with 146 additions and 61 deletions

View File

@@ -1,7 +1,7 @@
import { useModalSearchParams } from '@/hooks/useModalSearchParams' import { useModalSearchParams } from '@/hooks/useModalSearchParams'
import { Modal } from '@/shared/Modal/Modal' import { Modal } from '@/shared/Modal/Modal'
import { MODAL_PARAMS_KEYS } from '@/types/modal' import { MODAL_PARAMS_KEYS } from '@/types/modal'
import { call, getShortenNpub } from '@/utils/helpers/helpers' import { call, getAppIconTitle, getShortenNpub } from '@/utils/helpers/helpers'
import { Avatar, Box, Stack, Typography } from '@mui/material' import { Avatar, Box, Stack, Typography } from '@mui/material'
import { useParams, useSearchParams } from 'react-router-dom' import { useParams, useSearchParams } from 'react-router-dom'
import { useAppSelector } from '@/store/hooks/redux' import { useAppSelector } from '@/store/hooks/redux'
@@ -29,19 +29,20 @@ export const ModalConfirmConnect = () => {
const triggerApp = apps.find((app) => app.appNpub === appNpub) const triggerApp = apps.find((app) => app.appNpub === appNpub)
const { name, icon = '' } = triggerApp || {} const { name, icon = '' } = triggerApp || {}
const appName = name || getShortenNpub(appNpub) const appName = name || getShortenNpub(appNpub)
const appAvatarTitle = getAppIconTitle(name, appNpub)
const handleActionTypeChange = (_: any, value: ACTION_TYPE | null) => { const handleActionTypeChange = (_: any, value: ACTION_TYPE | null) => {
if (!value) return undefined if (!value) return undefined
return setSelectedActionType(value) return setSelectedActionType(value)
} }
const handleCloseModal = createHandleCloseReplace(MODAL_PARAMS_KEYS.CONFIRM_CONNECT, { // const handleCloseModal = createHandleCloseReplace(MODAL_PARAMS_KEYS.CONFIRM_CONNECT, {
onClose: async (sp) => { // onClose: async (sp) => {
sp.delete('appNpub') // sp.delete('appNpub')
sp.delete('reqId') // sp.delete('reqId')
await swicCall('confirm', pendingReqId, false, false) // await swicCall('confirm', pendingReqId, false, false)
}, // },
}) // })
const closeModalAfterRequest = createHandleCloseReplace(MODAL_PARAMS_KEYS.CONFIRM_CONNECT, { const closeModalAfterRequest = createHandleCloseReplace(MODAL_PARAMS_KEYS.CONFIRM_CONNECT, {
onClose: (sp) => { onClose: (sp) => {
sp.delete('appNpub') sp.delete('appNpub')
@@ -79,23 +80,27 @@ export const ModalConfirmConnect = () => {
} }
return ( return (
<Modal open={isModalOpened} withCloseButton={!isPopup} onClose={!isPopup ? handleCloseModal : undefined}> <Modal title='Connection request' open={isModalOpened} withCloseButton={false}
// withCloseButton={!isPopup} onClose={!isPopup ? handleCloseModal : undefined}
>
<Stack gap={'1rem'} paddingTop={'1rem'}> <Stack gap={'1rem'} paddingTop={'1rem'}>
<Stack direction={'row'} gap={'1rem'} alignItems={'center'} marginBottom={'1rem'}> <Stack direction={'row'} gap={'1rem'} alignItems={'center'} marginBottom={'1rem'}>
<Avatar <Avatar
variant="square" variant="rounded"
sx={{ sx={{
width: 56, width: 56,
height: 56, height: 56,
}} }}
src={icon} src={icon}
/> >
{appAvatarTitle}
</Avatar>
<Box> <Box>
<Typography variant="h5" fontWeight={600}> <Typography variant="h5" fontWeight={600}>
{appName} {appName}
</Typography> </Typography>
<Typography variant="body2" color={'GrayText'}> <Typography variant="body2" color={'GrayText'}>
Would like to connect to your account New app would like to connect
</Typography> </Typography>
</Box> </Box>
</Stack> </Stack>
@@ -103,7 +108,7 @@ export const ModalConfirmConnect = () => {
<ActionToggleButton <ActionToggleButton
value={ACTION_TYPE.BASIC} value={ACTION_TYPE.BASIC}
title="Basic permissions" title="Basic permissions"
description="Read your public key, sign notes and reactions" description="Read your public key, sign notes, reactions, zaps, etc"
// hasinfo // hasinfo
/> />
{/* <ActionToggleButton {/* <ActionToggleButton
@@ -115,7 +120,7 @@ export const ModalConfirmConnect = () => {
<ActionToggleButton <ActionToggleButton
value={ACTION_TYPE.CUSTOM} value={ACTION_TYPE.CUSTOM}
title="On demand" title="On demand"
description="Assign permissions when the app asks for them" description="Confirm permissions when the app asks for them"
/> />
</StyledToggleButtonsGroup> </StyledToggleButtonsGroup>
<Stack direction={'row'} gap={'1rem'}> <Stack direction={'row'} gap={'1rem'}>

View File

@@ -1,7 +1,7 @@
import { useModalSearchParams } from '@/hooks/useModalSearchParams' import { useModalSearchParams } from '@/hooks/useModalSearchParams'
import { Modal } from '@/shared/Modal/Modal' import { Modal } from '@/shared/Modal/Modal'
import { MODAL_PARAMS_KEYS } from '@/types/modal' import { MODAL_PARAMS_KEYS } from '@/types/modal'
import { call, getShortenNpub, getSignReqKind } from '@/utils/helpers/helpers' import { call, getAppIconTitle, getShortenNpub, getSignReqKind } from '@/utils/helpers/helpers'
import { Avatar, Box, List, ListItem, ListItemIcon, ListItemText, Stack, Typography } from '@mui/material' import { Avatar, Box, List, ListItem, ListItemIcon, ListItemText, Stack, Typography } from '@mui/material'
import { useParams, useSearchParams } from 'react-router-dom' import { useParams, useSearchParams } from 'react-router-dom'
import { useAppSelector } from '@/store/hooks/redux' import { useAppSelector } from '@/store/hooks/redux'
@@ -57,6 +57,7 @@ export const ModalConfirmEvent: FC<ModalConfirmEventProps> = ({ confirmEventReqs
const triggerApp = apps.find((app) => app.appNpub === appNpub) const triggerApp = apps.find((app) => app.appNpub === appNpub)
const { name, icon = '' } = triggerApp || {} const { name, icon = '' } = triggerApp || {}
const appName = name || getShortenNpub(appNpub) const appName = name || getShortenNpub(appNpub)
const appAvatarTitle = getAppIconTitle(name, appNpub)
const handleActionTypeChange = (_: any, value: ACTION_TYPE | null) => { const handleActionTypeChange = (_: any, value: ACTION_TYPE | null) => {
if (!value) return undefined if (!value) return undefined
@@ -118,7 +119,9 @@ export const ModalConfirmEvent: FC<ModalConfirmEventProps> = ({ confirmEventReqs
} }
return ( return (
<Modal open={isModalOpened} withCloseButton={!isPopup} onClose={!isPopup ? handleCloseModal : undefined}> <Modal title='Permission request' open={isModalOpened} withCloseButton={false}
// withCloseButton={!isPopup} onClose={!isPopup ? handleCloseModal : undefined}
>
<Stack gap={'1rem'} paddingTop={'1rem'}> <Stack gap={'1rem'} paddingTop={'1rem'}>
<Stack direction={'row'} gap={'1rem'} alignItems={'center'} marginBottom={'1rem'}> <Stack direction={'row'} gap={'1rem'} alignItems={'center'} marginBottom={'1rem'}>
<Avatar <Avatar
@@ -129,13 +132,15 @@ export const ModalConfirmEvent: FC<ModalConfirmEventProps> = ({ confirmEventReqs
borderRadius: '12px', borderRadius: '12px',
}} }}
src={icon} src={icon}
/> >
{appAvatarTitle}
</Avatar>
<Box> <Box>
<Typography variant="h5" fontWeight={600}> <Typography variant="h5" fontWeight={600}>
{appName} {appName}
</Typography> </Typography>
<Typography variant="body2" color={'GrayText'}> <Typography variant="body2" color={'GrayText'}>
Would like your permission to App wants to perform these actions
</Typography> </Typography>
</Box> </Box>
</Stack> </Stack>

View File

@@ -1,19 +1,27 @@
import React, { FC, ReactNode } from 'react' import { FC, ReactNode } from 'react'
import { IconContainer, StyledContainer } from './styled' import { IconContainer, StyledContainer } from './styled'
import { BoxProps, Typography } from '@mui/material' import { BoxProps, Stack, Typography } from '@mui/material'
type WarningProps = { type WarningProps = {
message: string | ReactNode message?: string | ReactNode
Icon?: ReactNode hint?: string | ReactNode
icon?: ReactNode
} & BoxProps } & BoxProps
export const Warning: FC<WarningProps> = ({ message, Icon, ...restProps }) => { export const Warning: FC<WarningProps> = ({ hint, message, icon, ...restProps }) => {
return ( return (
<StyledContainer {...restProps}> <StyledContainer {...restProps}>
{Icon && <IconContainer>{Icon}</IconContainer>} {icon && <IconContainer>{icon}</IconContainer>}
<Typography flex={1} noWrap> <Stack flex={1} direction={'column'} gap={'0.2rem'}>
<Typography noWrap>
{message} {message}
</Typography> </Typography>
{hint && (
<Typography>
{hint}
</Typography>
)}
</Stack>
</StyledContainer> </StyledContainer>
) )
} }

View File

@@ -16,7 +16,7 @@ export const IconContainer = styled((props: BoxProps) => <Box {...props} />)(()
width: '40px', width: '40px',
height: '40px', height: '40px',
borderRadius: '50%', borderRadius: '50%',
background: 'blue', background: 'grey',
display: 'grid', display: 'grid',
placeItems: 'center', placeItems: 'center',
})) }))

View File

@@ -8,9 +8,9 @@ import NDK, {
NDKPrivateKeySigner, NDKPrivateKeySigner,
NDKSigner, NDKSigner,
} from '@nostr-dev-kit/ndk' } from '@nostr-dev-kit/ndk'
import { NOAUTHD_URL, WEB_PUSH_PUBKEY, NIP46_RELAYS, MIN_POW, MAX_POW, KIND_RPC } from '../utils/consts' import { NOAUTHD_URL, WEB_PUSH_PUBKEY, NIP46_RELAYS, MIN_POW, MAX_POW, KIND_RPC, DOMAIN } from '../utils/consts'
import { Nip04 } from './nip04' import { Nip04 } from './nip04'
import { getReqPerm, getShortenNpub, isPackagePerm } from '@/utils/helpers/helpers' import { fetchNip05, getReqPerm, getShortenNpub, isPackagePerm } from '@/utils/helpers/helpers'
import { NostrPowEvent, minePow } from './pow' import { NostrPowEvent, minePow } from './pow'
//import { PrivateKeySigner } from './signer' //import { PrivateKeySigner } from './signer'
@@ -225,7 +225,7 @@ export class NoauthBackend {
public setNotifCallback(cb: () => void) { public setNotifCallback(cb: () => void) {
if (this.notifCallback) { if (this.notifCallback) {
this.notify() // this.notify()
} }
this.notifCallback = cb this.notifCallback = cb
} }
@@ -246,6 +246,13 @@ export class NoauthBackend {
return Buffer.from(await this.swg.crypto.subtle.digest('SHA-256', Buffer.from(s))).toString('hex') return Buffer.from(await this.swg.crypto.subtle.digest('SHA-256', Buffer.from(s))).toString('hex')
} }
private async fetchNpubName(npub: string) {
const url = `${NOAUTHD_URL}/name?npub=${npub}`
const r = await fetch(url)
const d = await r.json()
return d?.names?.length ? d.names[0] as string : ''
}
private async sendPost({ url, method, headers, body }: { url: string; method: string; headers: any; body: string }) { private async sendPost({ url, method, headers, body }: { url: string; method: string; headers: any; body: string }) {
const r = await fetch(url, { const r = await fetch(url, {
method, method,
@@ -559,6 +566,12 @@ export class NoauthBackend {
} }
const appNpub = nip19.npubEncode(remotePubkey) const appNpub = nip19.npubEncode(remotePubkey)
const connected = !!this.apps.find(a => a.appNpub === appNpub)
if (!connected && method !== 'connect') {
console.log('ignoring request before connect', method, id, appNpub, npub)
return false
}
const req: DbPending = { const req: DbPending = {
id, id,
npub, npub,
@@ -574,12 +587,13 @@ export class NoauthBackend {
const onAllow = async (manual: boolean, allow: boolean, remember: boolean, options?: any) => { const onAllow = async (manual: boolean, allow: boolean, remember: boolean, options?: any) => {
// confirm // confirm
console.log(Date.now(), allow ? 'allowed' : 'disallowed', npub, method, options, params) console.log(Date.now(), allow ? 'allowed' : 'disallowed', npub, method, options, params)
if (manual) { if (manual) {
await dbi.confirmPending(id, allow) await dbi.confirmPending(id, allow)
if (!(method === 'connect' && !allow)) { // add app on 'allow connect'
// only add app if it's not 'disallow connect' if (method === 'connect' && allow) {
if (!(await dbi.getApp(req.appNpub))) { // if (!(await dbi.getApp(req.appNpub))) {
await dbi.addApp({ await dbi.addApp({
appNpub: req.appNpub, appNpub: req.appNpub,
npub: req.npub, npub: req.npub,
@@ -592,7 +606,6 @@ export class NoauthBackend {
// reload // reload
self.apps = await dbi.listApps() self.apps = await dbi.listApps()
} }
}
} else { } else {
// just send to db w/o waiting for it // just send to db w/o waiting for it
dbi.addConfirmed({ dbi.addConfirmed({
@@ -612,7 +625,8 @@ export class NoauthBackend {
let newPerms = [getReqPerm(req)] let newPerms = [getReqPerm(req)]
if (allow && options && options.perms) newPerms = options.perms if (allow && options && options.perms) newPerms = options.perms
for (const p of newPerms) // write new perms confirmed by user
for (const p of newPerms) {
await dbi.addPerm({ await dbi.addPerm({
id: req.id, id: req.id,
npub: req.npub, npub: req.npub,
@@ -621,13 +635,17 @@ export class NoauthBackend {
value: allow ? '1' : '0', value: allow ? '1' : '0',
timestamp: Date.now(), timestamp: Date.now(),
}) })
}
// reload
this.perms = await dbi.listPerms() this.perms = await dbi.listPerms()
// confirm pending requests that might now have
// the proper perms
const otherReqs = self.confirmBuffer.filter((r) => r.req.appNpub === req.appNpub) const otherReqs = self.confirmBuffer.filter((r) => r.req.appNpub === req.appNpub)
console.log('updated perms', this.perms, 'otherReqs', otherReqs) console.log('updated perms', this.perms, 'otherReqs', otherReqs, 'connected', connected)
for (const r of otherReqs) { for (const r of otherReqs) {
const perm = this.getPerm(r.req) let perm = this.getPerm(r.req)
if (perm) { if (perm) {
r.cb(perm === '1', false) r.cb(perm === '1', false)
} }
@@ -675,7 +693,7 @@ export class NoauthBackend {
backend.rpc.sendResponse(id, remotePubkey, 'auth_url', KIND_RPC, authUrl) backend.rpc.sendResponse(id, remotePubkey, 'auth_url', KIND_RPC, authUrl)
// show notifs // show notifs
this.notify() // this.notify()
// notify main thread to ask for user concent // notify main thread to ask for user concent
this.updateUI() this.updateUI()
@@ -793,7 +811,7 @@ export class NoauthBackend {
await this.sendKeyToServer(npub, enckey, pwh) await this.sendKeyToServer(npub, enckey, pwh)
} }
private async fetchKey(npub: string, passphrase: string, name: string) { private async fetchKey(npub: string, passphrase: string, nip05: string) {
const { type, data: pubkey } = nip19.decode(npub) const { type, data: pubkey } = nip19.decode(npub)
if (type !== 'npub') throw new Error(`Invalid npub ${npub}`) if (type !== 'npub') throw new Error(`Invalid npub ${npub}`)
const { pwh } = await this.keysModule.generatePassKey(pubkey, passphrase) const { pwh } = await this.keysModule.generatePassKey(pubkey, passphrase)
@@ -803,13 +821,50 @@ export class NoauthBackend {
const key = this.enckeys.find((k) => k.npub === npub) const key = this.enckeys.find((k) => k.npub === npub)
if (key) return this.keyInfo(key) if (key) return this.keyInfo(key)
let name = ''
let existingName = true
// check name - user might have provided external nip05,
// or just his npub - we must fetch their name from our
// server, and if not exists - try to assign one
const npubName = await this.fetchNpubName(npub)
if (npubName) {
// already have name for this npub
console.log("existing npub name", npub, npubName)
name = npubName
} else if (nip05.includes('@')) {
// no name for them?
const [nip05name, domain] = nip05.split('@')
if (domain === DOMAIN) {
// wtf? how did we learn their npub if
// it's the name on our server but we can't fetch it?
console.log("existing name", nip05name)
name = nip05name
} else {
// try to take same name on our domain
existingName = false
name = nip05name
let takenName = await fetchNip05(`${name}@${DOMAIN}`)
if (takenName) {
// already taken? try name_domain as name
name = `${nip05name}_${domain}`
takenName = await fetchNip05(`${name}@${DOMAIN}`)
}
if (takenName) {
console.log("All names taken, leave without a name?")
name = ''
}
}
}
console.log("fetch", { name, existingName })
// add new key // add new key
const nsec = await this.keysModule.decryptKeyPass({ const nsec = await this.keysModule.decryptKeyPass({
pubkey, pubkey,
enckey, enckey,
passphrase, passphrase,
}) })
const k = await this.addKey({ name, nsec, existingName: true }) const k = await this.addKey({ name, nsec, existingName })
this.updateUI() this.updateUI()
return k return k
} }

View File

@@ -1,7 +1,7 @@
import React, { FC } from 'react' import { FC } from 'react'
import { Warning } from '@/components/Warning/Warning' import { Warning } from '@/components/Warning/Warning'
import { CircularProgress, Stack } from '@mui/material' import { CircularProgress, Stack, Typography } from '@mui/material'
import GppMaybeIcon from '@mui/icons-material/GppMaybe' import AutoModeOutlinedIcon from '@mui/icons-material/AutoModeOutlined'
type BackgroundSigningWarningProps = { type BackgroundSigningWarningProps = {
isEnabling: boolean isEnabling: boolean
@@ -13,10 +13,16 @@ export const BackgroundSigningWarning: FC<BackgroundSigningWarningProps> = ({ is
<Warning <Warning
message={ message={
<Stack direction={'row'} alignItems={'center'} gap={'1rem'}> <Stack direction={'row'} alignItems={'center'} gap={'1rem'}>
Please enable push notifications {isEnabling ? <CircularProgress size={'1.5rem'} /> : null} Enable background service {isEnabling ? <CircularProgress size={'1.5rem'} /> : null}
</Stack> </Stack>
} }
Icon={<GppMaybeIcon htmlColor="white" />} hint={
<Typography variant='body2'>
Please allow notifications
for background operation.
</Typography>
}
icon={<AutoModeOutlinedIcon htmlColor="white" />}
onClick={isEnabling ? undefined : onEnableBackSigning} onClick={isEnabling ? undefined : onEnableBackSigning}
/> />
) )

View File

@@ -20,7 +20,7 @@ export const useBackgroundSigning = () => {
await askNotificationPermission() await askNotificationPermission()
const result = await swicCall('enablePush') const result = await swicCall('enablePush')
if (!result) throw new Error('Failed to activate the push subscription') if (!result) throw new Error('Failed to activate the push subscription')
notify('Push notifications enabled!', 'success') notify('Background service enabled!', 'success')
setShowWarning(false) setShowWarning(false)
} catch (error: any) { } catch (error: any) {
notify(`Failed to enable push subscription: ${error}`, 'error') notify(`Failed to enable push subscription: ${error}`, 'error')

View File

@@ -15,6 +15,12 @@ export const getShortenNpub = (npub = '') => {
return npub.substring(0, 10) + '...' + npub.slice(-4) return npub.substring(0, 10) + '...' + npub.slice(-4)
} }
export const getAppIconTitle = (name: string | undefined, appNpub: string) => {
return name
? name[0].toLocaleUpperCase()
: appNpub.substring(4, 7);
}
export const getProfileUsername = (profile: MetaEvent | null, npub: string) => { export const getProfileUsername = (profile: MetaEvent | null, npub: string) => {
return profile?.info?.name || profile?.info?.display_name || getShortenNpub(npub) return profile?.info?.name || profile?.info?.display_name || getShortenNpub(npub)
} }