Browse Source

feat: private key login

imwald
codytseng 1 year ago
parent
commit
3d3f603596
  1. 3
      src/common/constants.ts
  2. 17
      src/common/types.ts
  3. 9
      src/main/index.ts
  4. 10
      src/main/services/storage.service.ts
  5. 6
      src/preload/index.ts
  6. 66
      src/renderer/src/components/LoginDialog/NsecLogin.tsx
  7. 68
      src/renderer/src/components/LoginDialog/index.tsx
  8. 2
      src/renderer/src/hooks/useSearchProfiles.tsx
  9. 8
      src/renderer/src/i18n/en.ts
  10. 7
      src/renderer/src/i18n/zh.ts
  11. 184
      src/renderer/src/providers/NostrProvider.tsx
  12. 41
      src/renderer/src/providers/NostrProvider/browser-nsec.signer.ts
  13. 222
      src/renderer/src/providers/NostrProvider/index.tsx
  14. 26
      src/renderer/src/providers/NostrProvider/nip-07.signer.ts
  15. 31
      src/renderer/src/providers/NostrProvider/nsec.signer.ts
  16. 28
      src/renderer/src/services/storage.service.ts

3
src/common/constants.ts

@ -1,4 +1,5 @@ @@ -1,4 +1,5 @@
export const StorageKey = {
THEME_SETTING: 'themeSetting',
RELAY_GROUPS: 'relayGroups'
RELAY_GROUPS: 'relayGroups',
ACCOUNT: 'account'
}

17
src/common/types.ts

@ -17,11 +17,17 @@ export type TTheme = 'light' | 'dark' @@ -17,11 +17,17 @@ export type TTheme = 'light' | 'dark'
export type TDraftEvent = Pick<Event, 'content' | 'created_at' | 'kind' | 'tags'>
export interface ISigner {
getPublicKey: () => Promise<string | null>
signEvent: (draftEvent: TDraftEvent) => Promise<Event | null>
}
export type TElectronWindow = {
electron: ElectronAPI
api: {
system: {
isEncryptionAvailable: () => Promise<boolean>
getSelectedStorageBackend: () => Promise<string>
}
theme: {
addChangeListener: (listener: (theme: TTheme) => void) => void
@ -31,6 +37,7 @@ export type TElectronWindow = { @@ -31,6 +37,7 @@ export type TElectronWindow = {
storage: {
getItem: (key: string) => Promise<string>
setItem: (key: string, value: string) => Promise<void>
removeItem: (key: string) => Promise<void>
}
nostr: {
login: (nsec: string) => Promise<{
@ -40,8 +47,10 @@ export type TElectronWindow = { @@ -40,8 +47,10 @@ export type TElectronWindow = {
logout: () => Promise<void>
}
}
nostr: {
getPublicKey: () => Promise<string | null>
signEvent: (draftEvent: TDraftEvent) => Promise<Event | null>
}
nostr: ISigner
}
export type TAccount = {
signerType: 'nsec' | 'browser-nsec' | 'nip-07'
nsec?: string
}

9
src/main/index.ts

@ -78,6 +78,15 @@ app.whenReady().then(async () => { @@ -78,6 +78,15 @@ app.whenReady().then(async () => {
nostrService.init()
ipcMain.handle('system:isEncryptionAvailable', () => safeStorage.isEncryptionAvailable())
ipcMain.handle('system:getSelectedStorageBackend', () => {
if (process.platform === 'darwin') {
return 'keychain'
}
if (process.platform === 'win32') {
return 'dpapi'
}
return safeStorage.getSelectedStorageBackend()
})
createWindow()

10
src/main/services/storage.service.ts

@ -17,6 +17,7 @@ export class StorageService { @@ -17,6 +17,7 @@ export class StorageService {
init() {
ipcMain.handle('storage:getItem', (_, key: string) => this.getItem(key))
ipcMain.handle('storage:setItem', (_, key: string, value: string) => this.setItem(key, value))
ipcMain.handle('storage:removeItem', (_, key: string) => this.removeItem(key))
}
getItem(key: string): string | undefined {
@ -29,6 +30,15 @@ export class StorageService { @@ -29,6 +30,15 @@ export class StorageService {
setItem(key: string, value: string) {
this.config[key] = value
this.setWriteTimeout()
}
removeItem(key: string) {
delete this.config[key]
this.setWriteTimeout()
}
private setWriteTimeout() {
if (this.writeTimer) return
this.writeTimer = setTimeout(() => {

6
src/preload/index.ts

@ -5,7 +5,8 @@ import { contextBridge, ipcRenderer } from 'electron' @@ -5,7 +5,8 @@ import { contextBridge, ipcRenderer } from 'electron'
// Custom APIs for renderer
const api = {
system: {
isEncryptionAvailable: () => ipcRenderer.invoke('system:isEncryptionAvailable')
isEncryptionAvailable: () => ipcRenderer.invoke('system:isEncryptionAvailable'),
getSelectedStorageBackend: () => ipcRenderer.invoke('system:getSelectedStorageBackend')
},
theme: {
addChangeListener: (listener: (theme: TTheme) => void) => {
@ -20,7 +21,8 @@ const api = { @@ -20,7 +21,8 @@ const api = {
},
storage: {
getItem: (key: string) => ipcRenderer.invoke('storage:getItem', key),
setItem: (key: string, value: string) => ipcRenderer.invoke('storage:setItem', key, value)
setItem: (key: string, value: string) => ipcRenderer.invoke('storage:setItem', key, value),
removeItem: (key: string) => ipcRenderer.invoke('storage:removeItem', key)
},
nostr: {
login: (nsec: string) => ipcRenderer.invoke('nostr:login', nsec),

66
src/renderer/src/components/LoginDialog/NsecLogin.tsx

@ -0,0 +1,66 @@ @@ -0,0 +1,66 @@
import { Button } from '@renderer/components/ui/button'
import { Input } from '@renderer/components/ui/input'
import { IS_ELECTRON, isElectron } from '@renderer/lib/env'
import { useNostr } from '@renderer/providers/NostrProvider'
import { useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
export default function PrivateKeyLogin({ onLoginSuccess }: { onLoginSuccess: () => void }) {
const { t } = useTranslation()
const { nsecLogin } = useNostr()
const [nsec, setNsec] = useState('')
const [errMsg, setErrMsg] = useState<string | null>(null)
const [storageBackend, setStorageBackend] = useState('unknown')
useEffect(() => {
const init = async () => {
if (!isElectron(window)) return
const backend = await window.api.system.getSelectedStorageBackend()
setStorageBackend(backend)
}
init()
}, [])
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setNsec(e.target.value)
setErrMsg(null)
}
const handleLogin = () => {
if (nsec === '') return
nsecLogin(nsec)
.then(() => onLoginSuccess())
.catch((err) => {
setErrMsg(err.message)
})
}
return (
<>
<div className="text-orange-400">
{!IS_ELECTRON
? t(
'Using private key login is insecure. It is recommended to use a browser extension for login, such as alby, nostr-keyx or nos2x.'
)
: ['unknown', 'basic_text'].includes(storageBackend)
? t('There are no secret keys stored on this device. Your nsec will be unprotected.')
: t('Your nsec will be encrypted using the {{backend}}.', {
backend: storageBackend
})}
</div>
<div className="space-y-1">
<Input
type="password"
placeholder="nsec1.."
value={nsec}
onChange={handleInputChange}
className={errMsg ? 'border-destructive' : ''}
/>
{errMsg && <div className="text-xs text-destructive pl-3">{errMsg}</div>}
</div>
<Button onClick={handleLogin}>{t('Login')}</Button>
</>
)
}

68
src/renderer/src/components/LoginDialog/index.tsx

@ -6,10 +6,11 @@ import { @@ -6,10 +6,11 @@ import {
DialogHeader,
DialogTitle
} from '@renderer/components/ui/dialog'
import { Input } from '@renderer/components/ui/input'
import { IS_ELECTRON } from '@renderer/lib/env'
import { useNostr } from '@renderer/providers/NostrProvider'
import { ArrowLeft } from 'lucide-react'
import { Dispatch, useState } from 'react'
import { useTranslation } from 'react-i18next'
import PrivateKeyLogin from './NsecLogin'
export default function LoginDialog({
open,
@ -18,49 +19,38 @@ export default function LoginDialog({ @@ -18,49 +19,38 @@ export default function LoginDialog({
open: boolean
setOpen: Dispatch<boolean>
}) {
const { t } = useTranslation()
const { login, canLogin } = useNostr()
const [nsec, setNsec] = useState('')
const [errMsg, setErrMsg] = useState<string | null>(null)
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setNsec(e.target.value)
setErrMsg(null)
}
const handleLogin = () => {
if (nsec === '') return
login(nsec)
.then(() => setOpen(false))
.catch((err) => {
setErrMsg(err.message)
})
}
const [loginMethod, setLoginMethod] = useState<'nsec' | 'nip07' | null>(null)
const { nip07Login } = useNostr()
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogContent className="w-80">
<DialogContent className="w-96">
<DialogHeader>
<DialogTitle className="hidden" />
<DialogDescription className="text-destructive">
{!canLogin && 'Encryption is not available in your device.'}
</DialogDescription>
<DialogDescription className="hidden" />
</DialogHeader>
<div className="space-y-1">
<Input
type="password"
placeholder="nsec1.."
value={nsec}
onChange={handleInputChange}
className={errMsg ? 'border-destructive' : ''}
disabled={!canLogin}
/>
{errMsg && <div className="text-xs text-destructive pl-3">{errMsg}</div>}
</div>
<Button onClick={handleLogin} disabled={!canLogin}>
{t('Login')}
</Button>
{loginMethod === 'nsec' ? (
<>
<div
className="absolute left-4 top-4 opacity-70 hover:opacity-100 cursor-pointer"
onClick={() => setLoginMethod(null)}
>
<ArrowLeft className="h-4 w-4" />
</div>
<PrivateKeyLogin onLoginSuccess={() => setOpen(false)} />
</>
) : (
<>
{!IS_ELECTRON && !!window.nostr && (
<Button onClick={() => nip07Login().then(() => setOpen(false))} className="w-full">
Login with NIP-07
</Button>
)}
<Button variant="secondary" onClick={() => setLoginMethod('nsec')} className="w-full">
Login with Private Key
</Button>
</>
)}
</DialogContent>
</Dialog>
)

2
src/renderer/src/hooks/useSearchProfiles.tsx

@ -11,6 +11,8 @@ export function useSearchProfiles(search: string, limit: number) { @@ -11,6 +11,8 @@ export function useSearchProfiles(search: string, limit: number) {
useEffect(() => {
const fetchProfiles = async () => {
if (!search) return
setIsFetching(true)
setProfiles([])
if (searchableRelayUrls.length === 0) {

8
src/renderer/src/i18n/en.ts

@ -78,6 +78,12 @@ export default { @@ -78,6 +78,12 @@ export default {
'Notes & Replies': 'Notes & Replies',
notifications: 'notifications',
Notifications: 'Notifications',
'no more notifications': 'no more notifications'
'no more notifications': 'no more notifications',
'There are no secret keys stored on this device. Your nsec will be unprotected.':
'There are no secret keys stored on this device. Your nsec will be unprotected.',
'Your nsec will be encrypted using the {{backend}}.':
'Your nsec will be encrypted using the {{backend}}.',
'Using private key login is insecure. It is recommended to use a browser extension for login, such as alby, nostr-keyx or nos2x.':
'Using private key login is insecure. It is recommended to use a browser extension for login, such as alby, nostr-keyx or nos2x.'
}
}

7
src/renderer/src/i18n/zh.ts

@ -77,6 +77,11 @@ export default { @@ -77,6 +77,11 @@ export default {
'Notes & Replies': '笔记 & 回复',
notifications: '通知',
Notifications: '通知',
'no more notifications': '到底了'
'no more notifications': '到底了',
'There are no secret keys stored on this device. Your nsec will be unprotected.':
'此设备上没有可用的密码管理工具。您的密钥将不受保护',
'Your nsec will be encrypted using the {{backend}}.': '您的密钥将使用 {{backend}} 加密',
'Using private key login is insecure. It is recommended to use a browser extension for login, such as alby, nostr-keyx or nos2x.':
'使用私钥登录是不安全的。建议使用浏览器插件进行登录,例如 alby、nostr-keyx 或 nos2x'
}
}

184
src/renderer/src/providers/NostrProvider.tsx

@ -1,184 +0,0 @@ @@ -1,184 +0,0 @@
import { TDraftEvent } from '@common/types'
import LoginDialog from '@renderer/components/LoginDialog'
import { useToast } from '@renderer/hooks'
import { useFetchRelayList } from '@renderer/hooks/useFetchRelayList'
import { IS_ELECTRON, isElectron } from '@renderer/lib/env'
import client from '@renderer/services/client.service'
import dayjs from 'dayjs'
import { Event, kinds } from 'nostr-tools'
import { createContext, useContext, useEffect, useState } from 'react'
import { useRelaySettings } from './RelaySettingsProvider'
type TNostrContext = {
isReady: boolean
pubkey: string | null
canLogin: boolean
login: (nsec: string) => Promise<string>
logout: () => Promise<void>
nip07Login: () => Promise<string>
/**
* Default publish the event to current relays, user's write relays and additional relays
*/
publish: (draftEvent: TDraftEvent, additionalRelayUrls?: string[]) => Promise<Event>
signHttpAuth: (url: string, method: string) => Promise<string>
signEvent: (draftEvent: TDraftEvent) => Promise<Event>
checkLogin: (cb?: () => void | Promise<void>) => void
}
const NostrContext = createContext<TNostrContext | undefined>(undefined)
export const useNostr = () => {
const context = useContext(NostrContext)
if (!context) {
throw new Error('useNostr must be used within a NostrProvider')
}
return context
}
export function NostrProvider({ children }: { children: React.ReactNode }) {
const { toast } = useToast()
const [isReady, setIsReady] = useState(false)
const [pubkey, setPubkey] = useState<string | null>(null)
const [canLogin, setCanLogin] = useState(false)
const [openLoginDialog, setOpenLoginDialog] = useState(false)
const { relayUrls: currentRelayUrls } = useRelaySettings()
const relayList = useFetchRelayList(pubkey)
useEffect(() => {
if (window.nostr) {
window.nostr.getPublicKey().then((pubkey) => {
if (pubkey) {
setPubkey(pubkey)
}
setIsReady(true)
})
} else {
setIsReady(true)
}
if (isElectron(window)) {
window.api?.system.isEncryptionAvailable().then((isEncryptionAvailable) => {
setCanLogin(isEncryptionAvailable)
})
} else {
setCanLogin(!!window.nostr)
}
}, [])
const login = async (nsec: string) => {
if (!canLogin) {
throw new Error('encryption is not available')
}
if (!isElectron(window)) {
throw new Error('login is not available')
}
const { pubkey, reason } = await window.api.nostr.login(nsec)
if (!pubkey) {
throw new Error(reason ?? 'invalid nsec')
}
setPubkey(pubkey)
return pubkey
}
const nip07Login = async () => {
if (IS_ELECTRON) {
throw new Error('electron app should not use nip07 login')
}
if (!window.nostr) {
throw new Error(
'You need to install a nostr signer extension to login. Such as Alby or nos2x'
)
}
const pubkey = await window.nostr.getPublicKey()
if (!pubkey) {
throw new Error('You did not allow to access your pubkey')
}
setPubkey(pubkey)
return pubkey
}
const logout = async () => {
if (isElectron(window)) {
await window.api.nostr.logout()
}
setPubkey(null)
client.clearNotificationsCache()
}
const publish = async (draftEvent: TDraftEvent, additionalRelayUrls: string[] = []) => {
const event = await window.nostr?.signEvent(draftEvent)
if (!event) {
throw new Error('sign event failed')
}
await client.publishEvent(
relayList.write.concat(additionalRelayUrls).concat(currentRelayUrls),
event
)
return event
}
const signEvent = async (draftEvent: TDraftEvent) => {
const event = await window.nostr?.signEvent(draftEvent)
if (!event) {
throw new Error('sign event failed')
}
return event
}
const signHttpAuth = async (url: string, method: string) => {
const event = await window.nostr?.signEvent({
content: '',
kind: kinds.HTTPAuth,
created_at: dayjs().unix(),
tags: [
['u', url],
['method', method]
]
})
if (!event) {
throw new Error('sign event failed')
}
return 'Nostr ' + btoa(JSON.stringify(event))
}
const checkLogin = async (cb?: () => void) => {
if (pubkey) {
return cb && cb()
}
if (IS_ELECTRON) {
return setOpenLoginDialog(true)
}
try {
await nip07Login()
} catch (err) {
toast({
title: 'Login failed',
description: (err as Error).message,
variant: 'destructive'
})
return
}
return cb && cb()
}
return (
<NostrContext.Provider
value={{
isReady,
pubkey,
canLogin,
login,
nip07Login,
logout,
publish,
signHttpAuth,
checkLogin,
signEvent
}}
>
{children}
<LoginDialog open={openLoginDialog} setOpen={setOpenLoginDialog} />
</NostrContext.Provider>
)
}

41
src/renderer/src/providers/NostrProvider/browser-nsec.signer.ts

@ -0,0 +1,41 @@ @@ -0,0 +1,41 @@
import { ISigner, TDraftEvent } from '@common/types'
import { bytesToHex } from '@noble/hashes/utils'
import { finalizeEvent, getPublicKey, nip19 } from 'nostr-tools'
export class BrowserNsecSigner implements ISigner {
private privkey: Uint8Array | null = null
private pubkey: string | null = null
login(nsec: string) {
const { type, data } = nip19.decode(nsec)
if (type !== 'nsec') {
throw new Error('invalid nsec')
}
this.privkey = data
this.pubkey = getPublicKey(data)
window.localStorage.setItem('private_key', bytesToHex(data))
return this.pubkey
}
logout() {
window.localStorage.removeItem('private_key')
}
async getPublicKey() {
return this.pubkey
}
async signEvent(draftEvent: TDraftEvent) {
if (!this.privkey) {
return null
}
try {
return finalizeEvent(draftEvent, this.privkey)
} catch (error) {
console.error(error)
return null
}
}
}

222
src/renderer/src/providers/NostrProvider/index.tsx

@ -0,0 +1,222 @@ @@ -0,0 +1,222 @@
import { ISigner, TDraftEvent } from '@common/types'
import LoginDialog from '@renderer/components/LoginDialog'
import { useToast } from '@renderer/hooks'
import { useFetchRelayList } from '@renderer/hooks/useFetchRelayList'
import { isElectron } from '@renderer/lib/env'
import client from '@renderer/services/client.service'
import storage from '@renderer/services/storage.service'
import dayjs from 'dayjs'
import { Event, kinds } from 'nostr-tools'
import { createContext, useContext, useEffect, useState } from 'react'
import { useRelaySettings } from '../RelaySettingsProvider'
import { BrowserNsecSigner } from './browser-nsec.signer'
import { Nip07Signer } from './nip-07.signer'
import { NsecSigner } from './nsec.signer'
type TNostrContext = {
isReady: boolean
pubkey: string | null
setPubkey: (pubkey: string) => void
nsecLogin: (nsec: string) => Promise<string>
logout: () => Promise<void>
nip07Login: () => Promise<void>
/**
* Default publish the event to current relays, user's write relays and additional relays
*/
publish: (draftEvent: TDraftEvent, additionalRelayUrls?: string[]) => Promise<Event>
signHttpAuth: (url: string, method: string) => Promise<string>
signEvent: (draftEvent: TDraftEvent) => Promise<Event>
checkLogin: (cb?: () => void | Promise<void>) => void
}
const NostrContext = createContext<TNostrContext | undefined>(undefined)
export const useNostr = () => {
const context = useContext(NostrContext)
if (!context) {
throw new Error('useNostr must be used within a NostrProvider')
}
return context
}
export function NostrProvider({ children }: { children: React.ReactNode }) {
const { toast } = useToast()
const [isReady, setIsReady] = useState(false)
const [pubkey, setPubkey] = useState<string | null>(null)
const [signer, setSigner] = useState<ISigner | null>(null)
const [openLoginDialog, setOpenLoginDialog] = useState(false)
const { relayUrls: currentRelayUrls } = useRelaySettings()
const relayList = useFetchRelayList(pubkey)
useEffect(() => {
const init = async () => {
const account = await storage.getAccountInfo()
if (!account) {
if (isElectron(window) || !window.nostr) {
return setIsReady(true)
}
// For browser env, attempt to login with nip-07
const nip07Signer = new Nip07Signer()
const pubkey = await nip07Signer.getPublicKey()
if (!pubkey) {
return setIsReady(true)
}
setPubkey(pubkey)
setSigner(nip07Signer)
return setIsReady(true)
}
if (account.signerType === 'nsec') {
const nsecSigner = new NsecSigner()
const pubkey = await nsecSigner.getPublicKey()
if (!pubkey) {
await storage.setAccountInfo(null)
return setIsReady(true)
}
setPubkey(pubkey)
setSigner(nsecSigner)
return setIsReady(true)
}
if (account.signerType === 'browser-nsec') {
if (!account.nsec) {
await storage.setAccountInfo(null)
return setIsReady(true)
}
const browserNsecSigner = new BrowserNsecSigner()
const pubkey = browserNsecSigner.login(account.nsec)
setPubkey(pubkey)
setSigner(browserNsecSigner)
return setIsReady(true)
}
if (account.signerType === 'nip-07') {
const nip07Signer = new Nip07Signer()
const pubkey = await nip07Signer.getPublicKey()
if (!pubkey) {
await storage.setAccountInfo(null)
return setIsReady(true)
}
setPubkey(pubkey)
setSigner(nip07Signer)
return setIsReady(true)
}
await storage.setAccountInfo(null)
return setIsReady(true)
}
init().catch(() => {
storage.setAccountInfo(null)
setIsReady(true)
})
}, [])
const nsecLogin = async (nsec: string) => {
if (isElectron(window)) {
const nsecSigner = new NsecSigner()
const { pubkey, reason } = await nsecSigner.login(nsec)
if (!pubkey) {
throw new Error(reason ?? 'invalid nsec')
}
await storage.setAccountInfo({ signerType: 'nsec' })
setPubkey(pubkey)
setSigner(nsecSigner)
return pubkey
}
const browserNsecSigner = new BrowserNsecSigner()
const pubkey = browserNsecSigner.login(nsec)
await storage.setAccountInfo({ signerType: 'browser-nsec', nsec })
setPubkey(pubkey)
setSigner(browserNsecSigner)
return pubkey
}
const nip07Login = async () => {
try {
const nip07Signer = new Nip07Signer()
const pubkey = await nip07Signer.getPublicKey()
if (!pubkey) {
throw new Error('You did not allow to access your pubkey')
}
await storage.setAccountInfo({ signerType: 'nip-07' })
setPubkey(pubkey)
setSigner(nip07Signer)
} catch (err) {
toast({
title: 'Login failed',
description: (err as Error).message,
variant: 'destructive'
})
throw err
}
}
const logout = async () => {
if (signer instanceof NsecSigner) {
await signer.logout()
} else if (signer instanceof BrowserNsecSigner) {
signer.logout()
}
setPubkey(null)
await storage.setAccountInfo(null)
client.clearNotificationsCache()
}
const signEvent = async (draftEvent: TDraftEvent) => {
const event = await signer?.signEvent(draftEvent)
if (!event) {
throw new Error('sign event failed')
}
return event
}
const publish = async (draftEvent: TDraftEvent, additionalRelayUrls: string[] = []) => {
const event = await signEvent(draftEvent)
await client.publishEvent(
relayList.write.concat(additionalRelayUrls).concat(currentRelayUrls),
event
)
return event
}
const signHttpAuth = async (url: string, method: string) => {
const event = await signEvent({
content: '',
kind: kinds.HTTPAuth,
created_at: dayjs().unix(),
tags: [
['u', url],
['method', method]
]
})
return 'Nostr ' + btoa(JSON.stringify(event))
}
const checkLogin = async (cb?: () => void) => {
if (pubkey) {
return cb && cb()
}
return setOpenLoginDialog(true)
}
return (
<NostrContext.Provider
value={{
isReady,
pubkey,
setPubkey,
nsecLogin,
nip07Login,
logout,
publish,
signHttpAuth,
checkLogin,
signEvent
}}
>
{children}
<LoginDialog open={openLoginDialog} setOpen={setOpenLoginDialog} />
</NostrContext.Provider>
)
}

26
src/renderer/src/providers/NostrProvider/nip-07.signer.ts

@ -0,0 +1,26 @@ @@ -0,0 +1,26 @@
import { ISigner, TDraftEvent } from '@common/types'
import { isElectron } from '@renderer/lib/env'
export class Nip07Signer implements ISigner {
private signer: ISigner
constructor() {
if (isElectron(window) || !window.nostr) {
throw new Error('nip-07 is not available')
}
if (!window.nostr) {
throw new Error(
'You need to install a nostr signer extension to login. Such as alby, nostr-keyx or nos2x.'
)
}
this.signer = window.nostr
}
async getPublicKey() {
return await this.signer.getPublicKey()
}
async signEvent(draftEvent: TDraftEvent) {
return await this.signer.signEvent(draftEvent)
}
}

31
src/renderer/src/providers/NostrProvider/nsec.signer.ts

@ -0,0 +1,31 @@ @@ -0,0 +1,31 @@
import { ISigner, TDraftEvent, TElectronWindow } from '@common/types'
import { isElectron } from '@renderer/lib/env'
export class NsecSigner implements ISigner {
private electronNostrApi: TElectronWindow['api']['nostr']
private signer: ISigner
constructor() {
if (!isElectron(window)) {
throw new Error('nsec login is not available')
}
this.electronNostrApi = window.api.nostr
this.signer = window.nostr
}
async login(nsec: string) {
return await this.electronNostrApi.login(nsec)
}
async logout() {
return await this.electronNostrApi.logout()
}
async getPublicKey() {
return await this.signer.getPublicKey()
}
async signEvent(draftEvent: TDraftEvent) {
return await this.signer.signEvent(draftEvent)
}
}

28
src/renderer/src/services/storage.service.ts

@ -1,5 +1,5 @@ @@ -1,5 +1,5 @@
import { StorageKey } from '@common/constants'
import { TRelayGroup, TThemeSetting } from '@common/types'
import { TAccount, TRelayGroup, TThemeSetting } from '@common/types'
import { isElectron } from '@renderer/lib/env'
const DEFAULT_RELAY_GROUPS: TRelayGroup[] = [
@ -26,6 +26,14 @@ class Storage { @@ -26,6 +26,14 @@ class Storage {
return localStorage.setItem(key, value)
}
}
async removeItem(key: string) {
if (isElectron(window)) {
return window.api.storage.removeItem(key)
} else {
return localStorage.removeItem(key)
}
}
}
class StorageService {
@ -34,6 +42,7 @@ class StorageService { @@ -34,6 +42,7 @@ class StorageService {
private initPromise!: Promise<void>
private relayGroups: TRelayGroup[] = []
private themeSetting: TThemeSetting = 'system'
private account: TAccount | null = null
private storage: Storage = new Storage()
constructor() {
@ -49,6 +58,8 @@ class StorageService { @@ -49,6 +58,8 @@ class StorageService {
this.relayGroups = relayGroupsStr ? JSON.parse(relayGroupsStr) : DEFAULT_RELAY_GROUPS
this.themeSetting =
((await this.storage.getItem(StorageKey.THEME_SETTING)) as TThemeSetting) ?? 'system'
const accountStr = await this.storage.getItem(StorageKey.ACCOUNT)
this.account = accountStr ? JSON.parse(accountStr) : null
}
async getRelayGroups() {
@ -72,6 +83,21 @@ class StorageService { @@ -72,6 +83,21 @@ class StorageService {
await this.storage.setItem(StorageKey.THEME_SETTING, themeSetting)
this.themeSetting = themeSetting
}
async getAccountInfo() {
await this.initPromise
return this.account
}
async setAccountInfo(account: TAccount | null) {
await this.initPromise
if (account === null) {
await this.storage.removeItem(StorageKey.ACCOUNT)
} else {
await this.storage.setItem(StorageKey.ACCOUNT, JSON.stringify(account))
}
this.account = account
}
}
const instance = new StorageService()

Loading…
Cancel
Save