feat: create signing request and send a DM to first signer with zip file url and encryption key
All checks were successful
Release / build_and_release (push) Successful in 1m2s
All checks were successful
Release / build_and_release (push) Successful in 1m2s
This commit is contained in:
parent
d4b095c5ca
commit
bd1e8417c1
146
package-lock.json
generated
146
package-lock.json
generated
@ -16,16 +16,22 @@
|
||||
"@nostr-dev-kit/ndk": "2.5.0",
|
||||
"@reduxjs/toolkit": "2.2.1",
|
||||
"axios": "1.6.7",
|
||||
"crypto-hash": "3.0.0",
|
||||
"file-saver": "2.0.5",
|
||||
"jszip": "3.10.1",
|
||||
"lodash": "4.17.21",
|
||||
"mui-file-input": "4.0.4",
|
||||
"nostr-tools": "2.3.1",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-redux": "9.1.0",
|
||||
"react-router-dom": "6.22.1",
|
||||
"react-toastify": "10.0.4",
|
||||
"redux": "5.0.1"
|
||||
"redux": "5.0.1",
|
||||
"tseep": "1.2.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/file-saver": "2.0.7",
|
||||
"@types/lodash": "4.14.202",
|
||||
"@types/react": "^18.2.56",
|
||||
"@types/react-dom": "^18.2.19",
|
||||
@ -1938,6 +1944,12 @@
|
||||
"integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/file-saver": {
|
||||
"version": "2.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/file-saver/-/file-saver-2.0.7.tgz",
|
||||
"integrity": "sha512-dNKVfHd/jk0SkR/exKGj2ggkB45MAkzvWCaqLUUgkyjITkGNzH8H+yUwr+BLJUBjZOe9w8X3wgmXhZDRg1ED6A==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/json-schema": {
|
||||
"version": "7.0.15",
|
||||
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
|
||||
@ -2567,6 +2579,11 @@
|
||||
"integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/core-util-is": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
|
||||
"integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ=="
|
||||
},
|
||||
"node_modules/cosmiconfig": {
|
||||
"version": "7.1.0",
|
||||
"resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz",
|
||||
@ -2602,6 +2619,17 @@
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/crypto-hash": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/crypto-hash/-/crypto-hash-3.0.0.tgz",
|
||||
"integrity": "sha512-5l5xGtzuvGTU28GXxGV1JYVFou68buZWpkV1Fx5hIDRPnfbQ8KzabTlNIuDIeSCYGVPFehupzDqlnbXm2IXmdQ==",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/csstype": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
|
||||
@ -3211,6 +3239,11 @@
|
||||
"node": "^10.12.0 || >=12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/file-saver": {
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/file-saver/-/file-saver-2.0.5.tgz",
|
||||
"integrity": "sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA=="
|
||||
},
|
||||
"node_modules/fill-range": {
|
||||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
|
||||
@ -3474,6 +3507,11 @@
|
||||
"node": ">= 4"
|
||||
}
|
||||
},
|
||||
"node_modules/immediate": {
|
||||
"version": "3.0.6",
|
||||
"resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz",
|
||||
"integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ=="
|
||||
},
|
||||
"node_modules/immer": {
|
||||
"version": "10.0.3",
|
||||
"resolved": "https://registry.npmjs.org/immer/-/immer-10.0.3.tgz",
|
||||
@ -3526,8 +3564,7 @@
|
||||
"node_modules/inherits": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
|
||||
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
|
||||
"dev": true
|
||||
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
|
||||
},
|
||||
"node_modules/is-arrayish": {
|
||||
"version": "0.2.1",
|
||||
@ -3601,6 +3638,11 @@
|
||||
"resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz",
|
||||
"integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA=="
|
||||
},
|
||||
"node_modules/isarray": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
|
||||
"integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ=="
|
||||
},
|
||||
"node_modules/isexe": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
|
||||
@ -3671,6 +3713,17 @@
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/jszip": {
|
||||
"version": "3.10.1",
|
||||
"resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz",
|
||||
"integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==",
|
||||
"dependencies": {
|
||||
"lie": "~3.3.0",
|
||||
"pako": "~1.0.2",
|
||||
"readable-stream": "~2.3.6",
|
||||
"setimmediate": "^1.0.5"
|
||||
}
|
||||
},
|
||||
"node_modules/keyv": {
|
||||
"version": "4.5.4",
|
||||
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
|
||||
@ -3693,6 +3746,14 @@
|
||||
"node": ">= 0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/lie": {
|
||||
"version": "3.3.0",
|
||||
"resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz",
|
||||
"integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==",
|
||||
"dependencies": {
|
||||
"immediate": "~3.0.5"
|
||||
}
|
||||
},
|
||||
"node_modules/light-bolt11-decoder": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/light-bolt11-decoder/-/light-bolt11-decoder-3.0.0.tgz",
|
||||
@ -3819,6 +3880,27 @@
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
||||
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
|
||||
},
|
||||
"node_modules/mui-file-input": {
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/mui-file-input/-/mui-file-input-4.0.4.tgz",
|
||||
"integrity": "sha512-WYzPqKg4lahGyuUIt7674vwbgW6WS1CO066ujuvCwcvr7mw7IDPPhCCJ/rD6i36OhVN2f+1hlTMX5dmsCyYxrQ==",
|
||||
"dependencies": {
|
||||
"pretty-bytes": "^6.1.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@emotion/react": "^11.5.0",
|
||||
"@emotion/styled": "^11.3.0",
|
||||
"@mui/material": "^5.0.0",
|
||||
"@types/react": "^18.0.0",
|
||||
"react": "^18.0.0",
|
||||
"react-dom": "^18.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/nanoid": {
|
||||
"version": "3.3.7",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz",
|
||||
@ -4002,6 +4084,11 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/pako": {
|
||||
"version": "1.0.11",
|
||||
"resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz",
|
||||
"integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw=="
|
||||
},
|
||||
"node_modules/parent-module": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
|
||||
@ -4144,6 +4231,22 @@
|
||||
"node": ">= 0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/pretty-bytes": {
|
||||
"version": "6.1.1",
|
||||
"resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-6.1.1.tgz",
|
||||
"integrity": "sha512-mQUvGU6aUFQ+rNvTIAcZuWGRT9a6f6Yrg9bHs4ImKF+HZCEK+plBvnAZYSIQztknZF2qnzNtr6F8s0+IuptdlQ==",
|
||||
"engines": {
|
||||
"node": "^14.13.1 || >=16.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/process-nextick-args": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
|
||||
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag=="
|
||||
},
|
||||
"node_modules/prop-types": {
|
||||
"version": "15.8.1",
|
||||
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
|
||||
@ -4313,6 +4416,20 @@
|
||||
"react-dom": ">=16.6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/readable-stream": {
|
||||
"version": "2.3.8",
|
||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
|
||||
"integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
|
||||
"dependencies": {
|
||||
"core-util-is": "~1.0.0",
|
||||
"inherits": "~2.0.3",
|
||||
"isarray": "~1.0.0",
|
||||
"process-nextick-args": "~2.0.0",
|
||||
"safe-buffer": "~5.1.1",
|
||||
"string_decoder": "~1.1.1",
|
||||
"util-deprecate": "~1.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/readdirp": {
|
||||
"version": "3.6.0",
|
||||
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
|
||||
@ -4452,6 +4569,11 @@
|
||||
"queue-microtask": "^1.2.2"
|
||||
}
|
||||
},
|
||||
"node_modules/safe-buffer": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
|
||||
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
|
||||
},
|
||||
"node_modules/sass": {
|
||||
"version": "1.71.1",
|
||||
"resolved": "https://registry.npmjs.org/sass/-/sass-1.71.1.tgz",
|
||||
@ -4510,6 +4632,11 @@
|
||||
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/setimmediate": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz",
|
||||
"integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA=="
|
||||
},
|
||||
"node_modules/shebang-command": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
|
||||
@ -4557,6 +4684,14 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/string_decoder": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
|
||||
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
|
||||
"dependencies": {
|
||||
"safe-buffer": "~5.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/strip-ansi": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
|
||||
@ -4839,6 +4974,11 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/util-deprecate": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="
|
||||
},
|
||||
"node_modules/v8-compile-cache-lib": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz",
|
||||
|
@ -18,16 +18,22 @@
|
||||
"@nostr-dev-kit/ndk": "2.5.0",
|
||||
"@reduxjs/toolkit": "2.2.1",
|
||||
"axios": "1.6.7",
|
||||
"crypto-hash": "3.0.0",
|
||||
"file-saver": "2.0.5",
|
||||
"jszip": "3.10.1",
|
||||
"lodash": "4.17.21",
|
||||
"mui-file-input": "4.0.4",
|
||||
"nostr-tools": "2.3.1",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-redux": "9.1.0",
|
||||
"react-router-dom": "6.22.1",
|
||||
"react-toastify": "10.0.4",
|
||||
"redux": "5.0.1"
|
||||
"redux": "5.0.1",
|
||||
"tseep": "1.2.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/file-saver": "2.0.7",
|
||||
"@types/lodash": "4.14.202",
|
||||
"@types/react": "^18.2.56",
|
||||
"@types/react-dom": "^18.2.19",
|
||||
|
26
src/App.tsx
26
src/App.tsx
@ -1,10 +1,14 @@
|
||||
import { useEffect } from 'react'
|
||||
import { useSelector } from 'react-redux'
|
||||
import { Route, Routes } from 'react-router-dom'
|
||||
import { Navigate, Route, Routes } from 'react-router-dom'
|
||||
import { AuthController, NostrController } from './controllers'
|
||||
import { MainLayout } from './layouts/Main'
|
||||
import { LandingPage } from './pages/landing/LandingPage'
|
||||
import { privateRoutes, publicRoutes } from './routes'
|
||||
import {
|
||||
appPrivateRoutes,
|
||||
appPublicRoutes,
|
||||
privateRoutes,
|
||||
publicRoutes
|
||||
} from './routes'
|
||||
import { State } from './store/rootReducer'
|
||||
import { getNsecBunkerDelegatedKey, saveNsecBunkerDelegatedKey } from './utils'
|
||||
|
||||
@ -32,7 +36,6 @@ const App = () => {
|
||||
return (
|
||||
<Routes>
|
||||
<Route element={<MainLayout />}>
|
||||
{authState?.loggedIn && <Route path='/' element={<LandingPage />} />}
|
||||
{authState?.loggedIn &&
|
||||
privateRoutes.map((route, index) => (
|
||||
<Route
|
||||
@ -62,8 +65,19 @@ const App = () => {
|
||||
)
|
||||
}
|
||||
})}
|
||||
{!authState ||
|
||||
(!authState.loggedIn && <Route path='*' element={<LandingPage />} />)}
|
||||
|
||||
<Route
|
||||
path='*'
|
||||
element={
|
||||
<Navigate
|
||||
to={
|
||||
authState.loggedIn
|
||||
? appPrivateRoutes.homePage
|
||||
: appPublicRoutes.login
|
||||
}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</Route>
|
||||
</Routes>
|
||||
)
|
||||
|
@ -1,12 +1,17 @@
|
||||
import { Menu as MenuIcon } from '@mui/icons-material'
|
||||
import {
|
||||
AppBar as AppBarMui,
|
||||
Box,
|
||||
Button,
|
||||
IconButton,
|
||||
Menu,
|
||||
MenuItem,
|
||||
Tab,
|
||||
Tabs,
|
||||
Toolbar,
|
||||
Typography
|
||||
} from '@mui/material'
|
||||
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useDispatch, useSelector } from 'react-redux'
|
||||
import { setAuthState } from '../../store/actions'
|
||||
@ -14,11 +19,15 @@ import { State } from '../../store/rootReducer'
|
||||
import { Dispatch } from '../../store/store'
|
||||
import Username from '../username'
|
||||
|
||||
import { Link, useNavigate } from 'react-router-dom'
|
||||
import { Link, useLocation, useNavigate } from 'react-router-dom'
|
||||
import nostrichAvatar from '../../assets/images/avatar.png'
|
||||
import nostrichLogo from '../../assets/images/nostr-logo.jpg'
|
||||
import { NostrController } from '../../controllers'
|
||||
import { appPublicRoutes, getProfileRoute } from '../../routes'
|
||||
import {
|
||||
appPrivateRoutes,
|
||||
appPublicRoutes,
|
||||
getProfileRoute
|
||||
} from '../../routes'
|
||||
import {
|
||||
clearAuthToken,
|
||||
saveNsecBunkerDelegatedKey,
|
||||
@ -26,13 +35,20 @@ import {
|
||||
} from '../../utils'
|
||||
import styles from './style.module.scss'
|
||||
|
||||
const validTabs = [appPrivateRoutes.homePage, appPrivateRoutes.decryptZip]
|
||||
|
||||
export const AppBar = () => {
|
||||
const navigate = useNavigate()
|
||||
const { pathname } = useLocation()
|
||||
const [tabValue, setTabValue] = useState(
|
||||
validTabs.includes(pathname) ? pathname : '/'
|
||||
)
|
||||
const dispatch: Dispatch = useDispatch()
|
||||
|
||||
const [username, setUsername] = useState('')
|
||||
const [userAvatar, setUserAvatar] = useState(nostrichAvatar)
|
||||
const [anchorElUser, setAnchorElUser] = useState<null | HTMLElement>(null)
|
||||
const [anchorElNav, setAnchorElNav] = useState<null | HTMLElement>(null)
|
||||
|
||||
const authState = useSelector((state: State) => state.auth)
|
||||
const metadataState = useSelector((state: State) => state.metadata)
|
||||
@ -51,10 +67,18 @@ export const AppBar = () => {
|
||||
setAnchorElUser(event.currentTarget)
|
||||
}
|
||||
|
||||
const handleOpenNavMenu = (event: React.MouseEvent<HTMLElement>) => {
|
||||
setAnchorElNav(event.currentTarget)
|
||||
}
|
||||
|
||||
const handleCloseUserMenu = () => {
|
||||
setAnchorElUser(null)
|
||||
}
|
||||
|
||||
const handleCloseNavMenu = () => {
|
||||
setAnchorElNav(null)
|
||||
}
|
||||
|
||||
const handleProfile = () => {
|
||||
const hexKey = authState?.usersPubkey
|
||||
if (hexKey) navigate(getProfileRoute(hexKey))
|
||||
@ -63,6 +87,7 @@ export const AppBar = () => {
|
||||
}
|
||||
|
||||
const handleLogout = () => {
|
||||
handleCloseUserMenu()
|
||||
dispatch(
|
||||
setAuthState({
|
||||
loggedIn: false,
|
||||
@ -87,8 +112,98 @@ export const AppBar = () => {
|
||||
return (
|
||||
<AppBarMui position='fixed' className={styles.AppBar}>
|
||||
<Toolbar className={styles.toolbar}>
|
||||
<Box className={styles.logoWrapper}>
|
||||
<img src={nostrichLogo} alt='Logo' onClick={() => navigate('/')} />
|
||||
<Box sx={{ display: { xs: 'none', md: 'flex' } }}>
|
||||
<Box className={styles.logoWrapper}>
|
||||
<img src={nostrichLogo} alt='Logo' onClick={() => navigate('/')} />
|
||||
</Box>
|
||||
|
||||
{isAuthenticated && (
|
||||
<Tabs
|
||||
indicatorColor='secondary'
|
||||
value={tabValue}
|
||||
onChange={(_, value) => setTabValue(value)}
|
||||
>
|
||||
<Tab
|
||||
label='Home'
|
||||
value={appPrivateRoutes.homePage}
|
||||
to={appPrivateRoutes.homePage}
|
||||
component={Link}
|
||||
/>
|
||||
<Tab
|
||||
label='Decrypt Zip'
|
||||
value={appPrivateRoutes.decryptZip}
|
||||
to={appPrivateRoutes.decryptZip}
|
||||
component={Link}
|
||||
/>
|
||||
</Tabs>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
<Box sx={{ flexGrow: 1, display: { xs: 'flex', md: 'none' } }}>
|
||||
{!isAuthenticated && (
|
||||
<Box className={styles.logoWrapper}>
|
||||
<img
|
||||
src={nostrichLogo}
|
||||
alt='Logo'
|
||||
onClick={() => navigate('/')}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{isAuthenticated && (
|
||||
<>
|
||||
<IconButton
|
||||
size='large'
|
||||
onClick={handleOpenNavMenu}
|
||||
color='primary'
|
||||
>
|
||||
<MenuIcon />
|
||||
</IconButton>
|
||||
|
||||
<Menu
|
||||
id='menu-appbar'
|
||||
anchorEl={anchorElNav}
|
||||
anchorOrigin={{
|
||||
vertical: 'bottom',
|
||||
horizontal: 'left'
|
||||
}}
|
||||
keepMounted
|
||||
transformOrigin={{
|
||||
vertical: 'top',
|
||||
horizontal: 'left'
|
||||
}}
|
||||
open={!!anchorElNav}
|
||||
onClose={handleCloseNavMenu}
|
||||
sx={{
|
||||
display: { xs: 'block', md: 'none' }
|
||||
}}
|
||||
>
|
||||
<MenuItem sx={{ justifyContent: 'center' }}>
|
||||
<Button
|
||||
component={Link}
|
||||
to={appPrivateRoutes.homePage}
|
||||
onClick={handleCloseNavMenu}
|
||||
variant='contained'
|
||||
color='primary'
|
||||
>
|
||||
Home
|
||||
</Button>
|
||||
</MenuItem>
|
||||
|
||||
<MenuItem sx={{ justifyContent: 'center' }}>
|
||||
<Button
|
||||
component={Link}
|
||||
to={appPrivateRoutes.decryptZip}
|
||||
onClick={handleCloseNavMenu}
|
||||
variant='contained'
|
||||
color='primary'
|
||||
>
|
||||
Decrypt Zip
|
||||
</Button>
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
<Box className={styles.rightSideBox}>
|
||||
|
@ -1,7 +1,9 @@
|
||||
import { EventEmitter } from 'tseep'
|
||||
import NDK, {
|
||||
NDKEvent,
|
||||
NDKNip46Signer,
|
||||
NDKPrivateKeySigner,
|
||||
NDKUser,
|
||||
NostrEvent
|
||||
} from '@nostr-dev-kit/ndk'
|
||||
import {
|
||||
@ -10,6 +12,7 @@ import {
|
||||
Relay,
|
||||
UnsignedEvent,
|
||||
finalizeEvent,
|
||||
nip04,
|
||||
nip19
|
||||
} from 'nostr-tools'
|
||||
import { updateNsecbunkerPubkey } from '../store/actions'
|
||||
@ -18,13 +21,23 @@ import store from '../store/store'
|
||||
import { SignedEvent } from '../types'
|
||||
import { getNsecBunkerDelegatedKey, verifySignedEvent } from '../utils'
|
||||
|
||||
export class NostrController {
|
||||
export class NostrController extends EventEmitter {
|
||||
private static instance: NostrController
|
||||
|
||||
private bunkerNDK: NDK | undefined
|
||||
private remoteSigner: NDKNip46Signer | undefined
|
||||
|
||||
private constructor() {}
|
||||
private constructor() {
|
||||
super()
|
||||
}
|
||||
|
||||
private getNostrObject = () => {
|
||||
if (window.nostr) return window.nostr
|
||||
|
||||
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
|
||||
@ -248,19 +261,13 @@ export class NostrController {
|
||||
|
||||
return Promise.resolve(signedEvent)
|
||||
} else if (loginMethod === LoginMethods.extension) {
|
||||
if (!window.nostr) {
|
||||
return Promise.reject(
|
||||
`Login method is ${loginMethod} but window.nostr is not present. Make sure extension is working properly`
|
||||
)
|
||||
}
|
||||
const nostr = this.getNostrObject()
|
||||
|
||||
return (await window.nostr
|
||||
.signEvent(event as NostrEvent)
|
||||
.catch((err: any) => {
|
||||
console.log('Error while signing event: ', err)
|
||||
return (await nostr.signEvent(event as NostrEvent).catch((err: any) => {
|
||||
console.log('Error while signing event: ', err)
|
||||
|
||||
throw err
|
||||
})) as Event
|
||||
throw err
|
||||
})) as Event
|
||||
} else {
|
||||
return Promise.reject(
|
||||
`We could not sign the event, none of the signing methods are available`
|
||||
@ -268,26 +275,66 @@ export class NostrController {
|
||||
}
|
||||
}
|
||||
|
||||
nip04Encrypt = async (receiver: string, content: string) => {
|
||||
const loginMethod = (store.getState().auth as AuthState).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 keyPair = (store.getState().auth as AuthState).keyPair
|
||||
|
||||
if (!keyPair) {
|
||||
throw new Error(
|
||||
`Login method is ${LoginMethods.privateKey} but private & public key pair is not found.`
|
||||
)
|
||||
}
|
||||
|
||||
const encrypted = await nip04.encrypt(keyPair.private, 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')
|
||||
}
|
||||
|
||||
/**
|
||||
* Function will capture the public key from the nostr extension or if no extension present
|
||||
* function wil capture the public key from the local storage
|
||||
*/
|
||||
capturePublicKey = async (): Promise<string> => {
|
||||
if (window.nostr) {
|
||||
const pubKey = await window.nostr.getPublicKey().catch((err: any) => {
|
||||
return Promise.reject(err.message)
|
||||
})
|
||||
const nostr = this.getNostrObject()
|
||||
const pubKey = await nostr.getPublicKey().catch((err: any) => {
|
||||
return Promise.reject(err.message)
|
||||
})
|
||||
|
||||
if (!pubKey) {
|
||||
return Promise.reject('Error getting public key, user canceled')
|
||||
}
|
||||
|
||||
return Promise.resolve(pubKey)
|
||||
if (!pubKey) {
|
||||
return Promise.reject('Error getting public key, user canceled')
|
||||
}
|
||||
|
||||
return Promise.reject(
|
||||
'window.nostr object not present. Make sure you have an nostr extension installed.'
|
||||
)
|
||||
return Promise.resolve(pubKey)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1,20 +1,66 @@
|
||||
import { Box } from '@mui/material'
|
||||
import Container from '@mui/material/Container'
|
||||
import { useEffect } from 'react'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useDispatch } from 'react-redux'
|
||||
import { Outlet } from 'react-router-dom'
|
||||
import { AppBar } from '../components/AppBar/AppBar'
|
||||
import { restoreState } from '../store/actions'
|
||||
import { loadState } from '../utils'
|
||||
import { restoreState, setAuthState } from '../store/actions'
|
||||
import { clearAuthToken, loadState, saveNsecBunkerDelegatedKey } from '../utils'
|
||||
import { LoadingSpinner } from '../components/LoadingSpinner'
|
||||
import { Dispatch } from '../store/store'
|
||||
import { NostrController } from '../controllers'
|
||||
import { LoginMethods } from '../store/auth/types'
|
||||
|
||||
export const MainLayout = () => {
|
||||
const dispatch = useDispatch()
|
||||
const dispatch: Dispatch = useDispatch()
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
|
||||
useEffect(() => {
|
||||
const logout = () => {
|
||||
dispatch(
|
||||
setAuthState({
|
||||
loggedIn: false,
|
||||
usersPubkey: undefined,
|
||||
loginMethod: undefined,
|
||||
nsecBunkerPubkey: undefined
|
||||
})
|
||||
)
|
||||
|
||||
// clear authToken saved in local storage
|
||||
clearAuthToken()
|
||||
|
||||
// update nsecBunker delegated key
|
||||
const newDelegatedKey =
|
||||
NostrController.getInstance().generateDelegatedKey()
|
||||
saveNsecBunkerDelegatedKey(newDelegatedKey)
|
||||
}
|
||||
|
||||
const restoredState = loadState()
|
||||
if (restoredState) dispatch(restoreState(restoredState))
|
||||
if (restoredState) {
|
||||
dispatch(restoreState(restoredState))
|
||||
|
||||
const { loggedIn, loginMethod, usersPubkey, nsecBunkerRelays } =
|
||||
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)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setIsLoading(false)
|
||||
}, [dispatch])
|
||||
|
||||
if (isLoading) return <LoadingSpinner desc='Loading App' />
|
||||
|
||||
return (
|
||||
<>
|
||||
<AppBar />
|
||||
@ -22,7 +68,10 @@ export const MainLayout = () => {
|
||||
<Box className='main'>
|
||||
<Container
|
||||
sx={{
|
||||
position: 'relative'
|
||||
position: 'relative',
|
||||
maxWidth: {
|
||||
xs: '550px'
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Outlet />
|
||||
|
93
src/pages/decrypt/index.tsx
Normal file
93
src/pages/decrypt/index.tsx
Normal file
@ -0,0 +1,93 @@
|
||||
import { Box, Button, TextField, Typography } from '@mui/material'
|
||||
import saveAs from 'file-saver'
|
||||
import { MuiFileInput } from 'mui-file-input'
|
||||
import { useState } from 'react'
|
||||
import { LoadingSpinner } from '../../components/LoadingSpinner'
|
||||
import { decryptArrayBuffer } from '../../utils'
|
||||
import styles from './style.module.scss'
|
||||
|
||||
export const DecryptZip = () => {
|
||||
const [selectedFile, setSelectedFile] = useState<File | null>(null)
|
||||
const [encryptionKey, setEncryptionKey] = useState('')
|
||||
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const [loadingSpinnerDesc, setLoadingSpinnerDesc] = useState('')
|
||||
const [isDraggingOver, setIsDraggingOver] = useState(false)
|
||||
|
||||
const handleDecrypt = async () => {
|
||||
if (!selectedFile || !encryptionKey) return
|
||||
|
||||
setIsLoading(true)
|
||||
setLoadingSpinnerDesc('Decrypting zip file')
|
||||
|
||||
const encryptedArrayBuffer = await selectedFile.arrayBuffer()
|
||||
|
||||
const arrayBuffer = await decryptArrayBuffer(
|
||||
encryptedArrayBuffer,
|
||||
encryptionKey
|
||||
)
|
||||
|
||||
const blob = new Blob([arrayBuffer])
|
||||
saveAs(blob, 'decrypted.zip')
|
||||
|
||||
setIsLoading(false)
|
||||
}
|
||||
|
||||
const handleDrop = (event: React.DragEvent<HTMLDivElement>) => {
|
||||
event.preventDefault()
|
||||
setIsDraggingOver(false)
|
||||
const file = event.dataTransfer.files[0]
|
||||
if (file.type === 'application/zip') setSelectedFile(file)
|
||||
}
|
||||
|
||||
const handleDragOver = (event: React.DragEvent<HTMLDivElement>) => {
|
||||
event.preventDefault()
|
||||
setIsDraggingOver(true)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{isLoading && <LoadingSpinner desc={loadingSpinnerDesc} />}
|
||||
<Box className={styles.container}>
|
||||
<Typography component='label' variant='h6'>
|
||||
Select encrypted zip file
|
||||
</Typography>
|
||||
|
||||
<Box
|
||||
className={styles.inputBlock}
|
||||
onDrop={handleDrop}
|
||||
onDragOver={handleDragOver}
|
||||
>
|
||||
{isDraggingOver && (
|
||||
<Box className={styles.fileDragOver}>
|
||||
<Typography variant='body1'>Drop file here</Typography>
|
||||
</Box>
|
||||
)}
|
||||
<MuiFileInput
|
||||
placeholder='Drop file here, or click to select'
|
||||
value={selectedFile}
|
||||
onChange={(value) => setSelectedFile(value)}
|
||||
InputProps={{
|
||||
inputProps: {
|
||||
accept: '.zip'
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
<TextField
|
||||
label='Encryption Key'
|
||||
variant='outlined'
|
||||
value={encryptionKey}
|
||||
onChange={(e) => setEncryptionKey(e.target.value)}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<Box sx={{ mt: 1, display: 'flex', justifyContent: 'center' }}>
|
||||
<Button onClick={handleDecrypt} variant='contained'>
|
||||
Decrypt
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
</>
|
||||
)
|
||||
}
|
27
src/pages/decrypt/style.module.scss
Normal file
27
src/pages/decrypt/style.module.scss
Normal file
@ -0,0 +1,27 @@
|
||||
@import '../../colors.scss';
|
||||
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
color: $text-color;
|
||||
|
||||
.inputBlock {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 25px;
|
||||
}
|
||||
|
||||
.fileDragOver {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgba(255, 255, 255, 0.8);
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
589
src/pages/home/index.tsx
Normal file
589
src/pages/home/index.tsx
Normal file
@ -0,0 +1,589 @@
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
FormControl,
|
||||
IconButton,
|
||||
InputLabel,
|
||||
List,
|
||||
ListItem,
|
||||
ListSubheader,
|
||||
MenuItem,
|
||||
Select,
|
||||
TextField,
|
||||
Typography
|
||||
} from '@mui/material'
|
||||
import { MuiFileInput } from 'mui-file-input'
|
||||
import styles from './style.module.scss'
|
||||
import { Dispatch, SetStateAction, useEffect, useState } from 'react'
|
||||
import placeholderAvatar from '../../assets/images/nostr-logo.jpg'
|
||||
import { ProfileMetadata } from '../../types'
|
||||
import { MetadataController, NostrController } from '../../controllers'
|
||||
import { Link } from 'react-router-dom'
|
||||
import {
|
||||
encryptArrayBuffer,
|
||||
generateEncryptionKey,
|
||||
getFileHash,
|
||||
pubToHex,
|
||||
queryNip05,
|
||||
shorten
|
||||
} from '../../utils'
|
||||
import { LoadingSpinner } from '../../components/LoadingSpinner'
|
||||
import { getProfileRoute } from '../../routes'
|
||||
import { Clear } from '@mui/icons-material'
|
||||
import JSZip from 'jszip'
|
||||
import { toast } from 'react-toastify'
|
||||
import { useSelector } from 'react-redux'
|
||||
import { State } from '../../store/rootReducer'
|
||||
import { EventTemplate } from 'nostr-tools'
|
||||
import axios from 'axios'
|
||||
|
||||
enum SelectionType {
|
||||
signer = 'Signer',
|
||||
viewer = 'Viewer'
|
||||
}
|
||||
|
||||
type MetadataMap = {
|
||||
[key: string]: ProfileMetadata
|
||||
}
|
||||
|
||||
export const HomePage = () => {
|
||||
const [inputValue, setInputValue] = useState('')
|
||||
const [type, setType] = useState<SelectionType>(SelectionType.signer)
|
||||
const [error, setError] = useState<string>()
|
||||
|
||||
const [signers, setSigners] = useState<string[]>([])
|
||||
const [viewers, setViewers] = useState<string[]>([])
|
||||
|
||||
const [metadataMap, setMetadataMap] = useState<MetadataMap>({})
|
||||
|
||||
const [selectedFiles, setSelectedFiles] = useState<File[]>([])
|
||||
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const [loadingSpinnerDesc, setLoadingSpinnerDesc] = useState('')
|
||||
const [authUrl, setAuthUrl] = useState<string>()
|
||||
|
||||
const usersPubkey = useSelector((state: State) => state.auth.usersPubkey)
|
||||
|
||||
const nostrController = NostrController.getInstance()
|
||||
|
||||
const handleAddClick = async () => {
|
||||
setError(undefined)
|
||||
|
||||
const addPubkey = (pubkey: string) => {
|
||||
const addElement = (prev: string[]) => {
|
||||
// if key is already in the list just return that
|
||||
if (prev.includes(pubkey)) return prev
|
||||
|
||||
return [...prev, pubkey]
|
||||
}
|
||||
if (type === SelectionType.signer) {
|
||||
setSigners(addElement)
|
||||
} else {
|
||||
setViewers(addElement)
|
||||
}
|
||||
}
|
||||
|
||||
if (inputValue.startsWith('npub')) {
|
||||
const pubkey = await pubToHex(inputValue)
|
||||
if (pubkey) {
|
||||
addPubkey(pubkey)
|
||||
setInputValue('')
|
||||
} else {
|
||||
setError('Provided npub is not valid. Please enter correct npub.')
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if (inputValue.includes('@')) {
|
||||
setIsLoading(true)
|
||||
setLoadingSpinnerDesc('Querying for nip05')
|
||||
const nip05Profile = await queryNip05(inputValue)
|
||||
.catch((err) => {
|
||||
console.error(`error occurred in querying nip05: ${inputValue}`, err)
|
||||
return null
|
||||
})
|
||||
.finally(() => {
|
||||
setIsLoading(false)
|
||||
setLoadingSpinnerDesc('')
|
||||
})
|
||||
|
||||
if (nip05Profile) {
|
||||
const pubkey = nip05Profile.pubkey
|
||||
addPubkey(pubkey)
|
||||
setInputValue('')
|
||||
} else {
|
||||
setError('Provided nip05 is not valid. Please enter correct nip05.')
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
setError('Invalid input! Make sure to provide correct npub or nip05.')
|
||||
}
|
||||
|
||||
const handleRemove = (pubkey: string, selectionType: SelectionType) => {
|
||||
if (selectionType === SelectionType.signer) {
|
||||
setSigners((prev) => prev.filter((signer) => signer !== pubkey))
|
||||
} else {
|
||||
setViewers((prev) => prev.filter((viewer) => viewer !== pubkey))
|
||||
}
|
||||
}
|
||||
|
||||
const handleSelectFiles = (files: File[]) => {
|
||||
setSelectedFiles((prev) => {
|
||||
const prevFileNames = prev.map((file) => file.name)
|
||||
|
||||
const newFiles = files.filter(
|
||||
(file) => !prevFileNames.includes(file.name)
|
||||
)
|
||||
|
||||
return [...prev, ...newFiles]
|
||||
})
|
||||
}
|
||||
|
||||
const handleRemoveFile = (fileToRemove: File) => {
|
||||
setSelectedFiles((prevFiles) =>
|
||||
prevFiles.filter((file) => file.name !== fileToRemove.name)
|
||||
)
|
||||
}
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (signers.length === 0) {
|
||||
toast.error('No signer is provided. At least provide one signer.')
|
||||
return
|
||||
}
|
||||
|
||||
if (viewers.length === 0) {
|
||||
toast.error('No viewer is provided. At least provide one viewer.')
|
||||
return
|
||||
}
|
||||
|
||||
if (selectedFiles.length === 0) {
|
||||
toast.error('No file is provided. At least provide one file.')
|
||||
return
|
||||
}
|
||||
|
||||
setIsLoading(true)
|
||||
setLoadingSpinnerDesc('Generating hashes for files')
|
||||
|
||||
const fileHashes: { [key: string]: string } = {}
|
||||
|
||||
for (const file of selectedFiles) {
|
||||
const hash = await getFileHash(file)
|
||||
|
||||
fileHashes[file.name] = hash
|
||||
}
|
||||
|
||||
const zip = new JSZip()
|
||||
|
||||
selectedFiles.forEach((file) => {
|
||||
zip.file(`files/${file.name}`, file)
|
||||
})
|
||||
|
||||
const event: EventTemplate = {
|
||||
kind: 1,
|
||||
tags: [['r', signers[0]]],
|
||||
content: JSON.stringify(fileHashes),
|
||||
created_at: Math.floor(Date.now() / 1000)
|
||||
}
|
||||
|
||||
setLoadingSpinnerDesc('Signing nostr event')
|
||||
const signedEvent = await nostrController.signEvent(event).catch((err) => {
|
||||
console.error(err)
|
||||
toast.error(err.message || 'Error occurred in signing nostr event')
|
||||
setIsLoading(false)
|
||||
return null
|
||||
})
|
||||
|
||||
if (!signedEvent) return
|
||||
|
||||
const meta = {
|
||||
signers,
|
||||
viewers,
|
||||
fileHashes,
|
||||
submittedBy: usersPubkey,
|
||||
signedEvents: {
|
||||
[signedEvent.pubkey]: JSON.stringify(signedEvent, null, 2)
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const stringifiedMeta = JSON.stringify(meta, null, 2)
|
||||
|
||||
zip.file('meta.json', stringifiedMeta)
|
||||
} catch (err) {
|
||||
toast.error('An error occurred in converting meta json to string')
|
||||
}
|
||||
|
||||
setLoadingSpinnerDesc('Generating zip file')
|
||||
|
||||
const arraybuffer = await zip
|
||||
.generateAsync({
|
||||
type: 'arraybuffer',
|
||||
compression: 'DEFLATE',
|
||||
compressionOptions: {
|
||||
level: 6
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log('err in zip:>> ', err)
|
||||
setIsLoading(false)
|
||||
toast.error(err.message || 'Error occurred in generating zip file')
|
||||
return null
|
||||
})
|
||||
|
||||
if (!arraybuffer) return
|
||||
|
||||
const encryptionKey = await generateEncryptionKey()
|
||||
|
||||
setLoadingSpinnerDesc('Encrypting zip file')
|
||||
const encryptedArrayBuffer = await encryptArrayBuffer(
|
||||
arraybuffer,
|
||||
encryptionKey
|
||||
)
|
||||
|
||||
const blob = new Blob([encryptedArrayBuffer])
|
||||
|
||||
setLoadingSpinnerDesc('Uploading zip file to file storage.')
|
||||
const fileUrl = await uploadToFileStorage(blob)
|
||||
.then((url) => {
|
||||
toast.success('zip file uploaded to file storage')
|
||||
return url
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log('err in upload:>> ', err)
|
||||
toast.error(err.message || 'Error occurred in uploading zip file')
|
||||
return null
|
||||
})
|
||||
|
||||
if (!fileUrl) return
|
||||
|
||||
await sendDMToFirstSigner(fileUrl, encryptionKey, signers[0])
|
||||
|
||||
setIsLoading(false)
|
||||
}
|
||||
|
||||
const uploadToFileStorage = async (blob: Blob) => {
|
||||
const unixNow = Math.floor(Date.now() / 1000)
|
||||
|
||||
const file = new File([blob], `zipped-${unixNow}.zip`, {
|
||||
type: 'application/zip'
|
||||
})
|
||||
|
||||
const event: EventTemplate = {
|
||||
kind: 24242,
|
||||
content: 'Authorize Upload',
|
||||
created_at: Math.floor(Date.now() / 1000),
|
||||
tags: [
|
||||
['t', 'upload'],
|
||||
['expiration', String(unixNow + 60 * 5)],
|
||||
['name', file.name],
|
||||
['size', String(file.size)]
|
||||
]
|
||||
}
|
||||
|
||||
setLoadingSpinnerDesc('Signing auth event for uploading zip')
|
||||
const authEvent = await nostrController.signEvent(event)
|
||||
|
||||
const FILE_STORAGE_URL = 'https://blossom.sigit.io'
|
||||
|
||||
const response = await axios.put(`${FILE_STORAGE_URL}/upload`, file, {
|
||||
headers: {
|
||||
Authorization: 'Nostr ' + btoa(JSON.stringify(authEvent)),
|
||||
'Content-Type': 'application/zip'
|
||||
}
|
||||
})
|
||||
|
||||
return response.data.url as string
|
||||
}
|
||||
|
||||
const sendDMToFirstSigner = async (
|
||||
fileUrl: string,
|
||||
encryptionKey: string,
|
||||
pubkey: string
|
||||
) => {
|
||||
const content = `You have been requested for a signature.\nHere is the url for zip file that you can download.\n
|
||||
${fileUrl}\nHowever this zip file is encrypted and you need to decrypt it using https://app.sigit.io\n Encryption key: ${encryptionKey}`
|
||||
|
||||
nostrController.on('nsecbunker-auth', (url) => {
|
||||
setAuthUrl(url)
|
||||
})
|
||||
|
||||
setLoadingSpinnerDesc('encrypting content for DM')
|
||||
|
||||
// todo: add timeout
|
||||
const encrypted = await nostrController
|
||||
.nip04Encrypt(pubkey, content)
|
||||
.then((res) => {
|
||||
return res
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log('err :>> ', err)
|
||||
toast.error(
|
||||
err.message || 'An error occurred while encrypting DM content'
|
||||
)
|
||||
return null
|
||||
})
|
||||
.finally(() => {
|
||||
setAuthUrl(undefined)
|
||||
})
|
||||
|
||||
if (!encrypted) return
|
||||
|
||||
const event: EventTemplate = {
|
||||
kind: 4,
|
||||
content: encrypted,
|
||||
created_at: Math.floor(Date.now() / 1000),
|
||||
tags: [['p', signers[0]]]
|
||||
}
|
||||
|
||||
setLoadingSpinnerDesc('signing event for DM')
|
||||
const signedEvent = await nostrController.signEvent(event).catch((err) => {
|
||||
console.log('err :>> ', err)
|
||||
toast.error(err.message || 'An error occurred while signing event for DM')
|
||||
return null
|
||||
})
|
||||
|
||||
if (!signedEvent) return
|
||||
|
||||
// const metadata = metadataMap[pubkey]
|
||||
|
||||
setLoadingSpinnerDesc('Publishing encrypted DM')
|
||||
|
||||
// todo: do not use hardcoded relay
|
||||
await nostrController
|
||||
.publishEvent(signedEvent, 'wss://relayable.org')
|
||||
.then(() => {
|
||||
toast.success('DM sent to first signer')
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log('err :>> ', err)
|
||||
toast.error(err.message || 'An error occurred while publishing DM')
|
||||
return null
|
||||
})
|
||||
}
|
||||
|
||||
if (authUrl) {
|
||||
return (
|
||||
<iframe
|
||||
title='Nsecbunker auth'
|
||||
src={authUrl}
|
||||
width='100%'
|
||||
height='500px'
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{isLoading && <LoadingSpinner desc={loadingSpinnerDesc} />}
|
||||
<Box className={styles.container}>
|
||||
<Typography component='label' variant='h6'>
|
||||
Select signers and viewers
|
||||
</Typography>
|
||||
<Box className={styles.inputBlock}>
|
||||
<TextField
|
||||
label='nip05 / npub'
|
||||
value={inputValue}
|
||||
onChange={(e) => setInputValue(e.target.value)}
|
||||
helperText={error}
|
||||
error={!!error}
|
||||
/>
|
||||
<FormControl fullWidth>
|
||||
<InputLabel id='select-type-label'>Type</InputLabel>
|
||||
<Select
|
||||
labelId='select-type-label'
|
||||
id='demo-simple-select'
|
||||
value={type}
|
||||
label='Type'
|
||||
onChange={(e) => setType(e.target.value as SelectionType)}
|
||||
>
|
||||
<MenuItem value={SelectionType.signer}>
|
||||
{SelectionType.signer}
|
||||
</MenuItem>
|
||||
<MenuItem value={SelectionType.viewer}>
|
||||
{SelectionType.viewer}
|
||||
</MenuItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
|
||||
<Box sx={{ mt: 1, display: 'flex', justifyContent: 'center' }}>
|
||||
<Button
|
||||
disabled={!inputValue}
|
||||
onClick={handleAddClick}
|
||||
variant='contained'
|
||||
>
|
||||
Add
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Typography component='label' variant='h6'>
|
||||
Select files
|
||||
</Typography>
|
||||
|
||||
<MuiFileInput
|
||||
multiple
|
||||
placeholder='Choose Files'
|
||||
value={selectedFiles}
|
||||
onChange={(value) => handleSelectFiles(value)}
|
||||
/>
|
||||
|
||||
<ul>
|
||||
{selectedFiles.map((file, index) => (
|
||||
<li key={index}>
|
||||
<Typography component='label'>{file.name}</Typography>
|
||||
<IconButton onClick={() => handleRemoveFile(file)}>
|
||||
<Clear style={{ color: 'red' }} />{' '}
|
||||
</IconButton>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
{signers.length > 0 && (
|
||||
<List
|
||||
sx={{
|
||||
bgcolor: 'background.paper',
|
||||
marginTop: 2
|
||||
}}
|
||||
subheader={
|
||||
<ListSubheader
|
||||
sx={{
|
||||
paddingBottom: 1,
|
||||
paddingTop: 1,
|
||||
fontSize: '1.5rem'
|
||||
}}
|
||||
className={styles.subHeader}
|
||||
>
|
||||
Signers
|
||||
</ListSubheader>
|
||||
}
|
||||
>
|
||||
{signers.map((signer, index) => (
|
||||
<DisplaySignerOrViewer
|
||||
key={`signer-${index}`}
|
||||
pubkey={signer}
|
||||
metadataMap={metadataMap}
|
||||
setMetadataMap={setMetadataMap}
|
||||
remove={() => handleRemove(signer, SelectionType.signer)}
|
||||
/>
|
||||
))}
|
||||
</List>
|
||||
)}
|
||||
|
||||
{viewers.length > 0 && (
|
||||
<List
|
||||
sx={{
|
||||
bgcolor: 'background.paper',
|
||||
marginTop: 2
|
||||
}}
|
||||
subheader={
|
||||
<ListSubheader
|
||||
sx={{
|
||||
paddingBottom: 1,
|
||||
paddingTop: 1,
|
||||
fontSize: '1.5rem'
|
||||
}}
|
||||
className={styles.subHeader}
|
||||
>
|
||||
Viewers
|
||||
</ListSubheader>
|
||||
}
|
||||
>
|
||||
{viewers.map((viewer, index) => (
|
||||
<DisplaySignerOrViewer
|
||||
key={`viewer-${index}`}
|
||||
pubkey={viewer}
|
||||
metadataMap={metadataMap}
|
||||
setMetadataMap={setMetadataMap}
|
||||
remove={() => handleRemove(viewer, SelectionType.viewer)}
|
||||
/>
|
||||
))}
|
||||
</List>
|
||||
)}
|
||||
|
||||
<Box sx={{ mt: 1, display: 'flex', justifyContent: 'center' }}>
|
||||
<Button onClick={handleSubmit} variant='contained'>
|
||||
Submit
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
type DisplaySignerOrViewerProps = {
|
||||
pubkey: string
|
||||
metadataMap: MetadataMap
|
||||
setMetadataMap: Dispatch<SetStateAction<MetadataMap>>
|
||||
remove: () => void
|
||||
}
|
||||
|
||||
const DisplaySignerOrViewer = ({
|
||||
pubkey,
|
||||
metadataMap,
|
||||
setMetadataMap,
|
||||
remove
|
||||
}: DisplaySignerOrViewerProps) => {
|
||||
const [metadata, setMetadata] = useState<ProfileMetadata>()
|
||||
|
||||
useEffect(() => {
|
||||
const getMetadata = async (pubkey: string) => {
|
||||
console.log('1 :>> ', 1)
|
||||
const metadataController = new MetadataController()
|
||||
const metadataEvent = await metadataController
|
||||
.findMetadata(pubkey)
|
||||
.catch((err) => {
|
||||
console.error(
|
||||
`error occurred in finding metadata for: ${pubkey}`,
|
||||
err
|
||||
)
|
||||
return null
|
||||
})
|
||||
|
||||
if (metadataEvent) {
|
||||
const metadataContent =
|
||||
metadataController.extractProfileMetadataContent(metadataEvent)
|
||||
if (metadataContent) {
|
||||
setMetadata(metadataContent)
|
||||
setMetadataMap((prev) => ({
|
||||
...prev,
|
||||
[pubkey]: metadataContent
|
||||
}))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const existingMetadata = metadataMap[pubkey]
|
||||
|
||||
console.log('metadataMap :>> ', metadataMap)
|
||||
|
||||
if (existingMetadata) {
|
||||
setMetadata(existingMetadata)
|
||||
} else {
|
||||
getMetadata(pubkey)
|
||||
}
|
||||
}, [pubkey, metadataMap])
|
||||
|
||||
const imageLoadError = (event: any) => {
|
||||
event.target.src = placeholderAvatar
|
||||
}
|
||||
|
||||
return (
|
||||
<ListItem sx={{ marginTop: 1 }} className={styles.listItem}>
|
||||
<img
|
||||
onError={imageLoadError}
|
||||
src={metadata?.picture || placeholderAvatar}
|
||||
alt='Profile Image'
|
||||
className={styles.img}
|
||||
/>
|
||||
<Link to={getProfileRoute(pubkey)}>
|
||||
<Typography component='label' className={styles.name}>
|
||||
{metadata?.display_name || metadata?.name || shorten(pubkey)}
|
||||
</Typography>
|
||||
</Link>
|
||||
<IconButton onClick={remove}>
|
||||
<Clear style={{ color: 'red' }} />
|
||||
</IconButton>
|
||||
</ListItem>
|
||||
)
|
||||
}
|
33
src/pages/home/style.module.scss
Normal file
33
src/pages/home/style.module.scss
Normal file
@ -0,0 +1,33 @@
|
||||
@import '../../colors.scss';
|
||||
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
color: $text-color;
|
||||
|
||||
.inputBlock {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 25px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.subHeader {
|
||||
border-bottom: 0.5px solid;
|
||||
}
|
||||
|
||||
.listItem {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between !important;
|
||||
|
||||
.img {
|
||||
max-width: 100px;
|
||||
}
|
||||
|
||||
.name {
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
@ -13,7 +13,8 @@ import {
|
||||
import {
|
||||
updateKeyPair,
|
||||
updateLoginMethod,
|
||||
updateNsecbunkerPubkey
|
||||
updateNsecbunkerPubkey,
|
||||
updateNsecbunkerRelays
|
||||
} from '../../store/actions'
|
||||
import { LoginMethods } from '../../store/auth/types'
|
||||
import { Dispatch } from '../../store/store'
|
||||
@ -183,6 +184,7 @@ export const Login = () => {
|
||||
|
||||
dispatch(updateLoginMethod(LoginMethods.nsecBunker))
|
||||
dispatch(updateNsecbunkerPubkey(pubkey))
|
||||
dispatch(updateNsecbunkerRelays(relays))
|
||||
|
||||
setLoadingSpinnerDesc('Authenticating and finding metadata')
|
||||
|
||||
@ -242,6 +244,7 @@ export const Login = () => {
|
||||
|
||||
dispatch(updateLoginMethod(LoginMethods.nsecBunker))
|
||||
dispatch(updateNsecbunkerPubkey(pubkey))
|
||||
dispatch(updateNsecbunkerRelays([relay]))
|
||||
|
||||
setLoadingSpinnerDesc('Authenticating and finding metadata')
|
||||
|
||||
|
@ -1,14 +1,18 @@
|
||||
import { DecryptZip } from '../pages/decrypt'
|
||||
import { HomePage } from '../pages/home'
|
||||
import { LandingPage } from '../pages/landing/LandingPage'
|
||||
import { Login } from '../pages/login'
|
||||
import { ProfilePage } from '../pages/profile'
|
||||
import { hexToNpub } from '../utils'
|
||||
|
||||
export const appPrivateRoutes = {
|
||||
homePage: '/'
|
||||
homePage: '/',
|
||||
decryptZip: '/decrypt-zip'
|
||||
}
|
||||
|
||||
export const appPublicRoutes = {
|
||||
profile: '/profile/:npub',
|
||||
landingPage: '/',
|
||||
login: '/login',
|
||||
help: 'https://help.sigit.io'
|
||||
}
|
||||
@ -17,6 +21,11 @@ export const getProfileRoute = (hexKey: string) =>
|
||||
appPublicRoutes.profile.replace(':npub', hexToNpub(hexKey))
|
||||
|
||||
export const publicRoutes = [
|
||||
{
|
||||
path: appPublicRoutes.landingPage,
|
||||
hiddenWhenLoggedIn: true,
|
||||
element: <LandingPage />
|
||||
},
|
||||
{
|
||||
path: appPublicRoutes.login,
|
||||
hiddenWhenLoggedIn: true,
|
||||
@ -31,6 +40,10 @@ export const publicRoutes = [
|
||||
export const privateRoutes = [
|
||||
{
|
||||
path: appPrivateRoutes.homePage,
|
||||
element: <LandingPage />
|
||||
element: <HomePage />
|
||||
},
|
||||
{
|
||||
path: appPrivateRoutes.decryptZip,
|
||||
element: <DecryptZip />
|
||||
}
|
||||
]
|
||||
|
@ -4,5 +4,6 @@ export const SET_AUTH_STATE = 'SET_AUTH_STATE'
|
||||
export const UPDATE_LOGIN_METHOD = 'UPDATE_LOGIN_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'
|
||||
|
@ -6,7 +6,8 @@ import {
|
||||
SetAuthState,
|
||||
UpdateKeyPair,
|
||||
UpdateLoginMethod,
|
||||
UpdateNsecBunkerPubkey
|
||||
UpdateNsecBunkerPubkey,
|
||||
UpdateNsecBunkerRelays
|
||||
} from './types'
|
||||
|
||||
export const setAuthState = (payload: AuthState): SetAuthState => ({
|
||||
@ -32,3 +33,10 @@ export const updateNsecbunkerPubkey = (
|
||||
type: ActionTypes.UPDATE_NSECBUNKER_PUBKEY,
|
||||
payload
|
||||
})
|
||||
|
||||
export const updateNsecbunkerRelays = (
|
||||
payload: string[] | undefined
|
||||
): UpdateNsecBunkerRelays => ({
|
||||
type: ActionTypes.UPDATE_NSECBUNKER_RELAYS,
|
||||
payload
|
||||
})
|
||||
|
@ -11,12 +11,13 @@ const reducer = (
|
||||
): AuthState | null => {
|
||||
switch (action.type) {
|
||||
case ActionTypes.SET_AUTH_STATE: {
|
||||
const { loginMethod, keyPair, nsecBunkerPubkey } = state
|
||||
const { loginMethod, keyPair, nsecBunkerPubkey, nsecBunkerRelays } = state
|
||||
|
||||
return {
|
||||
loginMethod,
|
||||
keyPair,
|
||||
nsecBunkerPubkey,
|
||||
nsecBunkerRelays,
|
||||
...action.payload
|
||||
}
|
||||
}
|
||||
@ -47,6 +48,18 @@ const reducer = (
|
||||
}
|
||||
}
|
||||
|
||||
case ActionTypes.UPDATE_NSECBUNKER_RELAYS: {
|
||||
const { payload } = action
|
||||
|
||||
return {
|
||||
...state,
|
||||
nsecBunkerRelays: payload
|
||||
}
|
||||
}
|
||||
|
||||
case ActionTypes.RESTORE_STATE:
|
||||
return action.payload.auth
|
||||
|
||||
default:
|
||||
return state
|
||||
}
|
||||
|
@ -1,4 +1,5 @@
|
||||
import * as ActionTypes from '../actionTypes'
|
||||
import { RestoreState } from '../actions'
|
||||
|
||||
export enum LoginMethods {
|
||||
extension = 'extension',
|
||||
@ -18,6 +19,7 @@ export interface AuthState {
|
||||
loginMethod?: LoginMethods
|
||||
keyPair?: Keys
|
||||
nsecBunkerPubkey?: string
|
||||
nsecBunkerRelays?: string[]
|
||||
}
|
||||
|
||||
export interface SetAuthState {
|
||||
@ -40,8 +42,15 @@ export interface UpdateNsecBunkerPubkey {
|
||||
payload: string | undefined
|
||||
}
|
||||
|
||||
export interface UpdateNsecBunkerRelays {
|
||||
type: typeof ActionTypes.UPDATE_NSECBUNKER_RELAYS
|
||||
payload: string[] | undefined
|
||||
}
|
||||
|
||||
export type AuthDispatchTypes =
|
||||
| RestoreState
|
||||
| SetAuthState
|
||||
| UpdateLoginMethod
|
||||
| UpdateKeyPair
|
||||
| UpdateNsecBunkerPubkey
|
||||
| UpdateNsecBunkerRelays
|
||||
|
71
src/utils/crypto.ts
Normal file
71
src/utils/crypto.ts
Normal file
@ -0,0 +1,71 @@
|
||||
import { hexToString, stringToHex } from '.'
|
||||
|
||||
const ENCRYPTION_ALGO_NAME = 'AES-GCM'
|
||||
|
||||
export const generateEncryptionKey = async () => {
|
||||
const cryptoKey = await window.crypto.subtle.generateKey(
|
||||
{ name: ENCRYPTION_ALGO_NAME, length: 128 },
|
||||
true,
|
||||
['encrypt', 'decrypt']
|
||||
)
|
||||
|
||||
const key = await window.crypto.subtle.exportKey('jwk', cryptoKey)
|
||||
|
||||
const jsonString = JSON.stringify(key)
|
||||
const hexKey = stringToHex(jsonString)
|
||||
|
||||
const iv = new TextDecoder().decode(
|
||||
window.crypto.getRandomValues(new Uint8Array(16))
|
||||
)
|
||||
|
||||
return `${hexKey}?iv=${iv}`
|
||||
}
|
||||
|
||||
export const importKey = async (key: string) => {
|
||||
const splittedKey = key.split('?iv=')
|
||||
const keyString = hexToString(splittedKey[0])
|
||||
const jsonWebKey = JSON.parse(keyString)
|
||||
const iv = new TextEncoder().encode(splittedKey[1])
|
||||
|
||||
const cryptoKey = await window.crypto.subtle.importKey(
|
||||
'jwk',
|
||||
jsonWebKey,
|
||||
{ name: ENCRYPTION_ALGO_NAME },
|
||||
true,
|
||||
['encrypt', 'decrypt']
|
||||
)
|
||||
|
||||
return { cryptoKey, iv }
|
||||
}
|
||||
|
||||
export const encryptArrayBuffer = async (
|
||||
arrayBuffer: ArrayBuffer,
|
||||
key: string
|
||||
) => {
|
||||
const { cryptoKey, iv } = await importKey(key)
|
||||
|
||||
// Encrypt the data
|
||||
const encryptedData = await window.crypto.subtle.encrypt(
|
||||
{ name: ENCRYPTION_ALGO_NAME, iv },
|
||||
cryptoKey,
|
||||
arrayBuffer
|
||||
)
|
||||
|
||||
return encryptedData
|
||||
}
|
||||
|
||||
export const decryptArrayBuffer = async (
|
||||
encryptedData: ArrayBuffer,
|
||||
key: string
|
||||
) => {
|
||||
const { cryptoKey, iv } = await importKey(key)
|
||||
|
||||
// Decrypt the data
|
||||
const decryptedData = await window.crypto.subtle.decrypt(
|
||||
{ name: ENCRYPTION_ALGO_NAME, iv },
|
||||
cryptoKey,
|
||||
encryptedData
|
||||
)
|
||||
|
||||
return decryptedData
|
||||
}
|
16
src/utils/file.ts
Normal file
16
src/utils/file.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import { sha256 } from 'crypto-hash'
|
||||
|
||||
export const getFileHash = (file: File) => {
|
||||
return new Promise<string>((resolve) => {
|
||||
const reader = new FileReader()
|
||||
|
||||
reader.onload = async () => {
|
||||
if (reader.result) {
|
||||
const hash = await sha256(reader.result)
|
||||
resolve(hash)
|
||||
}
|
||||
}
|
||||
|
||||
reader.readAsBinaryString(file)
|
||||
})
|
||||
}
|
@ -1,3 +1,5 @@
|
||||
export * from './crypto'
|
||||
export * from './file'
|
||||
export * from './localStorage'
|
||||
export * from './nostr'
|
||||
export * from './string'
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { nip05, nip19, verifyEvent } from 'nostr-tools'
|
||||
import { nip19, verifyEvent } from 'nostr-tools'
|
||||
import { SignedEvent } from '../types'
|
||||
import axios from 'axios'
|
||||
|
||||
@ -12,19 +12,12 @@ const validateHex = (hexKey: string) => {
|
||||
|
||||
/**
|
||||
* NPUB provided - it will convert NPUB to HEX
|
||||
* NIP-05 provided - it will query NIP-05 profile and return HEX key if found
|
||||
* HEX provided - it will return HEX
|
||||
*
|
||||
* @param pubKey in NPUB, NIP-05 or HEX format
|
||||
* @param pubKey in NPUB, HEX format
|
||||
* @returns HEX format
|
||||
*/
|
||||
export const pubToHex = async (pubKey: string): Promise<string | null> => {
|
||||
// If key is NIP-05
|
||||
if (pubKey.indexOf('@') !== -1)
|
||||
return Promise.resolve(
|
||||
(await nip05.queryProfile(pubKey).then((res) => res?.pubkey)) || null
|
||||
)
|
||||
|
||||
// If key is NPUB
|
||||
if (pubKey.startsWith('npub')) {
|
||||
try {
|
||||
|
@ -7,3 +7,33 @@ export const shorten = (str: string, offset = 9) => {
|
||||
str.length
|
||||
)}`
|
||||
}
|
||||
|
||||
export const stringToHex = (str: string) => {
|
||||
// Convert the string to an array of UTF-16 code units using the spread operator
|
||||
const codeUnits = [...str]
|
||||
|
||||
// Map each code unit to its hexadecimal representation
|
||||
const hexChars = codeUnits.map((codeUnit) => {
|
||||
// Convert the code unit to its hexadecimal representation with leading zeros
|
||||
const hex = codeUnit.charCodeAt(0).toString(16).padStart(2, '0')
|
||||
return hex
|
||||
})
|
||||
|
||||
// Join the hexadecimal characters into a single string
|
||||
const hexString = hexChars.join('')
|
||||
|
||||
// Return the resulting hexadecimal string
|
||||
return hexString
|
||||
}
|
||||
|
||||
export const hexToString = (hex: string) => {
|
||||
// Split the hex string into pairs of two characters
|
||||
const pairs = hex.match(/.{1,2}/g) || []
|
||||
|
||||
// Convert each pair from hexadecimal to its decimal equivalent,
|
||||
// then convert each decimal value to its character representation
|
||||
const chars = pairs.map((pair) => String.fromCharCode(parseInt(pair, 16)))
|
||||
|
||||
// Join the resulting characters into a single string
|
||||
return chars.join('')
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user