Add name processing for signup, add pow to nip98 and to sendName, minor UI changes

This commit is contained in:
artur 2024-02-05 14:29:25 +03:00
parent 9c18310fd9
commit 5b57b42111
11 changed files with 294 additions and 112 deletions

2
.env
View File

@ -2,5 +2,5 @@
# change if you're using a different noauthd server
REACT_APP_WEB_PUSH_PUBKEY=BNW_39YcKbV4KunFxFhvMW5JUs8AljfFnGUeZpaerO-gwCoWyQat5ol0xOGB8MLaqqCbz0iptd2Qv3SToSGynMk
#REACT_APP_NOAUTHD_URL=http://localhost:8000
REACT_APP_NOAUTHD_URL=https://noauthd.login.nostrapps.org
REACT_APP_NOAUTHD_URL=https://noauthd.nsec.app
REACT_APP_DOMAIN=nsec.app

View File

@ -28,7 +28,8 @@ export const ModalImportKeys = () => {
e.preventDefault()
try {
if (!enteredNsec.trim().length) return
const k: any = await swicCall('importKey', enteredNsec)
const enteredName = '' // FIXME get from input
const k: any = await swicCall('importKey', enteredName, enteredNsec)
notify('Key imported!', 'success')
navigate(`/key/${k.npub}`)
} catch (error: any) {

View File

@ -1,4 +1,4 @@
import React, { useCallback, useEffect, useState } from 'react'
import { useCallback, useEffect, useState } from 'react'
import { useEnqueueSnackbar } from '@/hooks/useEnqueueSnackbar'
import { useModalSearchParams } from '@/hooks/useModalSearchParams'
import { swicCall } from '@/modules/swic'
@ -6,7 +6,6 @@ import { Modal } from '@/shared/Modal/Modal'
import { MODAL_PARAMS_KEYS } from '@/types/modal'
import { IconButton, Stack, Typography } from '@mui/material'
import { StyledAppLogo } from './styled'
import { nip19 } from 'nostr-tools'
import { Input } from '@/shared/Input/Input'
import { Button } from '@/shared/Button/Button'
import VisibilityOffOutlinedIcon from '@mui/icons-material/VisibilityOffOutlined'
@ -15,6 +14,8 @@ import { useNavigate } from 'react-router-dom'
import { useForm } from 'react-hook-form'
import { FormInputType, schema } from './const'
import { yupResolver } from '@hookform/resolvers/yup'
import { DOMAIN } from '@/utils/consts'
import { fetchNip05 } from '@/utils/helpers/helpers'
export const ModalLogin = () => {
const { getModalOpened, createHandleCloseReplace } = useModalSearchParams()
@ -51,18 +52,15 @@ export const ModalLogin = () => {
const submitHandler = async (values: FormInputType) => {
try {
const [username, domain] = values.username.split('@')
const response = await fetch(
`https://${domain}/.well-known/nostr.json?name=${username}`,
)
const getNpub: {
names: {
[name: string]: string
}
} = await response.json()
const pubkey = getNpub.names[username]
const npub = nip19.npubEncode(pubkey)
let npub = values.username
if (!npub.startsWith('npub1') && !npub.includes('@')) {
npub += '@' + DOMAIN
}
if (npub.includes('@')) {
const npubNip05 = await fetchNip05(npub)
if (!npubNip05) throw new Error(`Username ${npub} not found`)
npub = npubNip05
}
const passphrase = values.password
console.log('fetch', npub, passphrase)
@ -103,9 +101,9 @@ export const ModalLogin = () => {
</Typography>
</Stack>
<Input
label='Enter a Username'
label='Username or nip05 or npub'
fullWidth
placeholder='user@nsec.app'
placeholder='name or name@domain.com or npub1...'
{...register('username')}
error={!!errors.username}
/>
@ -130,7 +128,7 @@ export const ModalLogin = () => {
error={!!errors.password}
/>
<Button type='submit' fullWidth>
Login
Add account
</Button>
</Stack>
</Modal>

View File

@ -10,6 +10,8 @@ import { Button } from '@/shared/Button/Button'
import { CheckmarkIcon } from '@/assets'
import { swicCall } from '@/modules/swic'
import { useNavigate } from 'react-router-dom'
import { DOMAIN, NOAUTHD_URL } from '@/utils/consts'
import { fetchNip05 } from '@/utils/helpers/helpers'
export const ModalSignUp = () => {
const { getModalOpened, createHandleCloseReplace } = useModalSearchParams()
@ -21,27 +23,41 @@ export const ModalSignUp = () => {
const navigate = useNavigate()
const [enteredValue, setEnteredValue] = useState('')
const [isAvailable, setIsAvailable] = useState(false)
const handleInputChange = (e: ChangeEvent<HTMLInputElement>) => {
const handleInputChange = async (e: ChangeEvent<HTMLInputElement>) => {
setEnteredValue(e.target.value)
const name = e.target.value.trim()
if (name) {
const npubNip05 = await fetchNip05(`${name}@${DOMAIN}`)
setIsAvailable(!npubNip05)
} else {
setIsAvailable(false)
}
}
const isAvailable = enteredValue.trim().length > 2
const inputHelperText = isAvailable ? (
<>
<CheckmarkIcon /> Available
</>
const inputHelperText = enteredValue
? (
isAvailable ? (
<>
<CheckmarkIcon /> Available
</>
) : (
<>
Already taken
</>
)
) : (
"Don't worry, username can be changed later."
)
);
const handleSubmit = async (e: React.FormEvent) => {
if (!enteredValue.trim().length) return
const name = enteredValue.trim()
if (!name.length) return
e.preventDefault()
try {
const k: any = await swicCall('generateKey')
notify(`New key ${k.npub}`, 'success')
const k: any = await swicCall('generateKey', name)
notify(`Account created for "${name}"`, 'success')
navigate(`/key/${k.npub}`)
} catch (error: any) {
notify(error.message, 'error')
@ -73,22 +89,26 @@ export const ModalSignUp = () => {
placeholder='Username'
helperText={inputHelperText}
endAdornment={
<Typography color={'#FFFFFFA8'}>@nsec.app</Typography>
<Typography color={'#FFFFFFA8'}>@{DOMAIN}</Typography>
}
onChange={handleInputChange}
value={enteredValue}
helperTextProps={{
sx: {
'&.helper_text': {
color: isAvailable
color: enteredValue && isAvailable
? theme.palette.success.main
: theme.palette.textSecondaryDecorate.main,
: (enteredValue && !isAvailable
? theme.palette.error.main
: theme.palette.textSecondaryDecorate.main
)
,
},
},
}}
/>
<Button fullWidth type='submit'>
Sign up
Create account
</Button>
</Stack>
</Modal>

View File

@ -8,9 +8,10 @@ import NDK, {
NDKPrivateKeySigner,
NDKSigner,
} from '@nostr-dev-kit/ndk'
import { NOAUTHD_URL, WEB_PUSH_PUBKEY, NIP46_RELAYS } from '../utils/consts'
import { NOAUTHD_URL, WEB_PUSH_PUBKEY, NIP46_RELAYS, MIN_POW, MAX_POW } from '../utils/consts'
import { Nip04 } from './nip04'
import { getReqPerm, getShortenNpub, isPackagePerm } from '@/utils/helpers/helpers'
import { NostrPowEvent, minePow } from './pow'
//import { PrivateKeySigner } from './signer'
//const PERF_TEST = false
@ -286,7 +287,8 @@ export class NoauthBackend {
})
if (r.status !== 200 && r.status !== 201) {
console.log('Fetch error', url, method, r.status)
throw new Error('Failed to fetch ' + url)
const body = await r.json()
throw new Error('Failed to fetch ' + url, { cause: body })
}
return await r.json()
@ -297,11 +299,13 @@ export class NoauthBackend {
url,
method = 'GET',
body = '',
pow = 0
}: {
npub: string
url: string
method: string
body: string
pow?: number
}) {
const { data: pubkey } = nip19.decode(npub)
@ -320,6 +324,15 @@ export class NoauthBackend {
})
if (body) authEvent.tags.push(['payload', await this.sha256(body)])
// generate pow on auth evevnt
if (pow) {
const start = Date.now()
const powEvent: NostrPowEvent = authEvent.rawEvent()
const minedEvent = minePow(powEvent, pow)
console.log("mined pow of", pow, "in", Date.now() - start, "ms", minedEvent)
authEvent.tags = minedEvent.tags
}
authEvent.sig = await authEvent.sign(key.signer)
const auth = this.swg.btoa(JSON.stringify(authEvent.rawEvent()))
@ -354,6 +367,7 @@ export class NoauthBackend {
body,
})
}
private async sendKeyToServer(npub: string, enckey: string, pwh: string) {
const body = JSON.stringify({
npub,
@ -389,6 +403,38 @@ export class NoauthBackend {
})
}
private async sendNameToServer(npub: string, name: string) {
const body = JSON.stringify({
npub,
name
})
const method = 'POST'
const url = `${NOAUTHD_URL}/name`
// mas pow should be 21 or something like that
let pow = MIN_POW;
while(pow <= MAX_POW) {
console.log("Try name", name, "pow", pow);
try {
return await this.sendPostAuthd({
npub,
url,
method,
body,
pow
})
} catch (e: any) {
console.log("error", e.cause);
if (e.cause && e.cause.minPow > pow)
pow = e.cause.minPow
else
throw e;
}
}
throw new Error("Too many requests, retry later")
}
private notify() {
// FIXME collect info from accessBuffer and confirmBuffer
// and update the notifications
@ -475,7 +521,11 @@ export class NoauthBackend {
return generatePrivateKey()
}
public async addKey(nsec?: string): Promise<KeyInfo> {
public async addKey(name: string, nsec?: string): Promise<KeyInfo> {
// lowercase
name = name.trim().toLocaleLowerCase()
let sk = ''
if (nsec) {
const { type, data } = nip19.decode(nsec)
@ -486,14 +536,22 @@ export class NoauthBackend {
}
const pubkey = getPublicKey(sk)
const npub = nip19.npubEncode(pubkey)
const localKey = await this.keysModule.generateLocalKey()
const enckey = await this.keysModule.encryptKeyLocal(sk, localKey)
// @ts-ignore
const dbKey: DbKey = { npub, enckey, localKey }
const dbKey: DbKey = { npub, name, enckey, localKey }
await dbi.addKey(dbKey)
this.enckeys.push(dbKey)
await this.startKey({ npub, sk })
// assign nip05 before adding the key
// FIXME set name to db and if this call to 'send' fails
// then retry later
console.log("adding key", npub, name)
if (name)
await this.sendNameToServer(npub, name)
const sub = await this.swg.registration.pushManager.getSubscription()
if (sub) await this.sendSubscriptionToServer(npub, sub)
@ -766,14 +824,14 @@ export class NoauthBackend {
await this.startKey({ npub, sk })
}
private async generateKey() {
const k = await this.addKey()
private async generateKey(name: string) {
const k = await this.addKey(name)
this.updateUI()
return k
}
private async importKey(nsec: string) {
const k = await this.addKey(nsec)
private async importKey(name: string, nsec: string) {
const k = await this.addKey(name, nsec)
this.updateUI()
return k
}
@ -879,9 +937,9 @@ export class NoauthBackend {
//console.log("UI message", id, method, args)
let result = undefined
if (method === 'generateKey') {
result = await this.generateKey()
result = await this.generateKey(args[0])
} else if (method === 'importKey') {
result = await this.importKey(args[0])
result = await this.importKey(args[0], args[1])
} else if (method === 'saveKey') {
result = await this.saveKey(args[0], args[1])
} else if (method === 'fetchKey') {
@ -902,6 +960,7 @@ export class NoauthBackend {
result,
})
} catch (e: any) {
console.log("backend error", e)
event.source.postMessage({
id,
error: e.toString(),

51
src/modules/pow.ts Normal file
View File

@ -0,0 +1,51 @@
// based on https://git.v0l.io/Kieran/snort/src/branch/main/packages/system/src/pow-util.ts
import { sha256 } from "@noble/hashes/sha256";
import { bytesToHex } from "@noble/hashes/utils";
export interface NostrPowEvent {
id?: string;
pubkey: string;
created_at: number;
kind?: number;
tags: Array<Array<string>>;
content: string;
sig?: string;
}
export function minePow(e: NostrPowEvent, target: number) {
let ctr = 0;
let nonceTagIdx = e.tags.findIndex(a => a[0] === "nonce");
if (nonceTagIdx === -1) {
nonceTagIdx = e.tags.length;
e.tags.push(["nonce", ctr.toString(), target.toString()]);
}
do {
e.tags[nonceTagIdx][1] = (++ctr).toString();
e.id = createId(e);
} while (countLeadingZeros(e.id) < target);
return e;
}
function createId(e: NostrPowEvent) {
const payload = [0, e.pubkey, e.created_at, e.kind, e.tags, e.content];
return bytesToHex(sha256(JSON.stringify(payload)));
}
export function countLeadingZeros(hex: string) {
let count = 0;
for (let i = 0; i < hex.length; i++) {
const nibble = parseInt(hex[i], 16);
if (nibble === 0) {
count += 4;
} else {
count += Math.clz32(nibble) - 28;
break;
}
}
return count;
}

View File

@ -24,7 +24,7 @@ const HomePage = () => {
return (
<Stack maxHeight={'100%'} overflow={'auto'}>
<SectionTitle marginBottom={'0.5rem'}>
{isNoKeys ? 'Welcome' : 'Keys:'}
{isNoKeys ? 'Welcome' : 'Accounts:'}
</SectionTitle>
<Stack gap={'0.5rem'} overflow={'auto'}>
{isNoKeys && (

View File

@ -18,18 +18,22 @@ import UserValueSection from './components/UserValueSection'
import { useTriggerConfirmModal } from './hooks/useTriggerConfirmModal'
import { useLiveQuery } from 'dexie-react-hooks'
import { checkNpubSyncQuerier } from './utils'
import { DOMAIN } from '@/utils/consts'
const KeyPage = () => {
const { npub = '' } = useParams<{ npub: string }>()
const { apps, pending, perms } = useAppSelector((state) => state.content)
const { keys, apps, pending, perms } = useAppSelector((state) => state.content)
const isSynced = useLiveQuery(checkNpubSyncQuerier(npub), [npub], false)
const { handleOpen } = useModalSearchParams()
const { userNameWithPrefix } = useProfile(npub)
// const { userNameWithPrefix } = useProfile(npub)
const { handleEnableBackground, showWarning, isEnabling } =
useBackgroundSigning()
const key = keys.find(k => k.npub === npub)
const username = key?.name ? `${key?.name}@${DOMAIN}` : ''
const filteredApps = apps.filter((a) => a.npub === npub)
const { prepareEventPendings } = useTriggerConfirmModal(
npub,
@ -53,8 +57,8 @@ const KeyPage = () => {
)}
<UserValueSection
title='Your login'
value={userNameWithPrefix}
copyValue={npub + '@nsec.app'}
value={username}
copyValue={username}
explanationType={EXPLANATION_MODAL_KEYS.NPUB}
/>
<UserValueSection

View File

@ -0,0 +1,27 @@
import { forwardRef, useRef } from "react";
import { Input, InputProps } from "../Input/Input";
export type DebounceProps = {
handleDebounce: (value: string) => void;
debounceTimeout: number;
};
export const DebounceInput = (props: InputProps & DebounceProps) => {
const { handleDebounce, debounceTimeout, ...rest } = props;
const timerRef = useRef<number>();
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
if (timerRef.current) {
clearTimeout(timerRef.current);
}
timerRef.current = window.setTimeout(() => {
handleDebounce(event.target.value);
}, debounceTimeout);
};
// @ts-ignore
return <Input {...rest} onChange={handleChange} />;
}

View File

@ -3,6 +3,9 @@ export const NOAUTHD_URL = process.env.REACT_APP_NOAUTHD_URL
export const WEB_PUSH_PUBKEY = process.env.REACT_APP_WEB_PUSH_PUBKEY
export const DOMAIN = process.env.REACT_APP_DOMAIN
export const MIN_POW = 14
export const MAX_POW = 19
export enum ACTION_TYPE {
BASIC = 'basic',
ADVANCED = 'advanced',

View File

@ -1,82 +1,101 @@
import { nip19 } from 'nostr-tools'
import { ACTION_TYPE, NIP46_RELAYS } from '../consts'
import { DbPending } from '@/modules/db'
import { MetaEvent } from '@/types/meta-event'
import { nip19 } from "nostr-tools";
import { ACTION_TYPE, NIP46_RELAYS } from "../consts";
import { DbPending } from "@/modules/db";
import { MetaEvent } from "@/types/meta-event";
export async function call(cb: () => any) {
try {
return await cb()
} catch (e) {
console.log(`Error: ${e}`)
}
try {
return await cb();
} catch (e) {
console.log(`Error: ${e}`);
}
}
export const getShortenNpub = (npub = '') => {
return npub.substring(0, 10) + '...' + npub.slice(-4)
}
export const getShortenNpub = (npub = "") => {
return npub.substring(0, 10) + "..." + npub.slice(-4);
};
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)
);
};
export const getBunkerLink = (npub = '') => {
if (!npub) return ''
const { data: pubkey } = nip19.decode(npub)
return `bunker://${pubkey}?relay=${NIP46_RELAYS[0]}`
}
export const getBunkerLink = (npub = "") => {
if (!npub) return "";
const { data: pubkey } = nip19.decode(npub);
return `bunker://${pubkey}?relay=${NIP46_RELAYS[0]}`;
};
export async function askNotificationPermission() {
return new Promise<void>((ok, rej) => {
// Let's check if the browser supports notifications
if (!('Notification' in window)) {
rej('This browser does not support notifications.')
} else {
Notification.requestPermission().then(() => {
if (Notification.permission === 'granted') ok()
else rej()
})
}
})
return new Promise<void>((ok, rej) => {
// Let's check if the browser supports notifications
if (!("Notification" in window)) {
rej("This browser does not support notifications.");
} else {
Notification.requestPermission().then(() => {
if (Notification.permission === "granted") ok();
else rej();
});
}
});
}
export function getSignReqKind(req: DbPending): number | undefined {
try {
const data = JSON.parse(JSON.parse(req.params)[0])
return data.kind
} catch {}
return undefined
try {
const data = JSON.parse(JSON.parse(req.params)[0]);
return data.kind;
} catch {}
return undefined;
}
export function getReqPerm(req: DbPending): string {
if (req.method === 'sign_event') {
const kind = getSignReqKind(req)
if (kind !== undefined) return `${req.method}:${kind}`
}
return req.method
if (req.method === "sign_event") {
const kind = getSignReqKind(req);
if (kind !== undefined) return `${req.method}:${kind}`;
}
return req.method;
}
export function isPackagePerm(perm: string, reqPerm: string) {
if (perm === ACTION_TYPE.BASIC) {
switch (reqPerm) {
case 'connect':
case 'get_public_key':
case 'nip04_decrypt':
case 'nip04_encrypt':
case 'sign_event:0':
case 'sign_event:1':
case 'sign_event:3':
case 'sign_event:6':
case 'sign_event:7':
case 'sign_event:9734':
case 'sign_event:10002':
case 'sign_event:30023':
case 'sign_event:10000':
return true
}
}
return false
if (perm === ACTION_TYPE.BASIC) {
switch (reqPerm) {
case "connect":
case "get_public_key":
case "nip04_decrypt":
case "nip04_encrypt":
case "sign_event:0":
case "sign_event:1":
case "sign_event:3":
case "sign_event:6":
case "sign_event:7":
case "sign_event:9734":
case "sign_event:10002":
case "sign_event:30023":
case "sign_event:10000":
return true;
}
}
return false;
}
export async function fetchNip05(value: string, origin?: string) {
try {
const [username, domain] = value.split("@");
if (!origin) origin = `https://${domain}`
const response = await fetch(
`${origin}/.well-known/nostr.json?name=${username}`
);
const getNpub: {
names: {
[name: string]: string;
};
} = await response.json();
const pubkey = getNpub.names[username];
return nip19.npubEncode(pubkey);
} catch (e) {
console.log("Failed to fetch nip05", value, "error: " + e);
return ''
}
}