Compare commits
7 Commits
45f0764fa8
...
3c22429941
Author | SHA1 | Date | |
---|---|---|---|
3c22429941 | |||
868ae6f23e | |||
87c6807ba0 | |||
06deecba76 | |||
0b35f11abf | |||
017d1ab88b | |||
8fa074899c |
Binary file not shown.
Before Width: | Height: | Size: 80 KiB After Width: | Height: | Size: 15 KiB |
BIN
public/logo.png
BIN
public/logo.png
Binary file not shown.
Before Width: | Height: | Size: 72 KiB |
34
public/logo.svg
Normal file
34
public/logo.svg
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<svg id="Layer_2" data-name="Layer 2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 734.95 255.06">
|
||||||
|
<defs>
|
||||||
|
<style>
|
||||||
|
.cls-1 {
|
||||||
|
fill: #47b17d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cls-2 {
|
||||||
|
fill: #4c82a3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cls-3 {
|
||||||
|
fill: #7d54a3;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</defs>
|
||||||
|
<g id="Layer_1-2" data-name="Layer 1">
|
||||||
|
<g>
|
||||||
|
<g>
|
||||||
|
<path class="cls-3" d="M335.64,100.98c0-4.9,.94-9.5,2.81-13.79,1.87-4.29,4.42-8.05,7.64-11.27,3.22-3.22,6.98-5.77,11.27-7.64,4.29-1.87,8.89-2.81,13.79-2.81h54.34v23.7h-54.34c-1.65,0-3.19,.3-4.62,.91-1.43,.61-2.68,1.45-3.76,2.52s-1.91,2.33-2.52,3.76c-.61,1.43-.91,2.97-.91,4.62s.3,3.21,.91,4.67c.61,1.46,1.45,2.73,2.52,3.8,1.07,1.07,2.33,1.91,3.76,2.52,1.43,.61,2.97,.91,4.62,.91h23.7c4.9,0,9.51,.92,13.83,2.77,4.32,1.85,8.09,4.38,11.31,7.6s5.75,6.99,7.6,11.31c1.84,4.32,2.77,8.93,2.77,13.83s-.92,9.5-2.77,13.79c-1.85,4.29-4.38,8.05-7.6,11.27s-6.99,5.77-11.31,7.64c-4.32,1.87-8.93,2.81-13.83,2.81h-52.61v-23.7h52.61c1.65,0,3.19-.3,4.62-.91,1.43-.61,2.68-1.45,3.76-2.52s1.91-2.33,2.52-3.76c.61-1.43,.91-2.97,.91-4.62s-.3-3.19-.91-4.62c-.61-1.43-1.45-2.68-2.52-3.76-1.07-1.07-2.33-1.91-3.76-2.52-1.43-.6-2.97-.91-4.62-.91h-23.7c-4.9,0-9.5-.94-13.79-2.81-4.29-1.87-8.05-4.42-11.27-7.64-3.22-3.22-5.77-6.99-7.64-11.31-1.87-4.32-2.81-8.93-2.81-13.83Z"/>
|
||||||
|
<path class="cls-3" d="M469.59,183.9h-23.7V65.47h23.7v118.43Z"/>
|
||||||
|
<path class="cls-3" d="M585.8,171.92c-5.51,4.68-11.64,8.27-18.42,10.78-6.77,2.5-13.82,3.76-21.14,3.76-5.62,0-11.03-.73-16.23-2.19-5.2-1.46-10.06-3.52-14.58-6.19-4.52-2.67-8.65-5.86-12.39-9.58-3.75-3.72-6.94-7.85-9.58-12.39s-4.7-9.43-6.15-14.66c-1.46-5.23-2.19-10.65-2.19-16.27s.73-11.01,2.19-16.19c1.46-5.17,3.51-10.03,6.15-14.58,2.64-4.54,5.83-8.67,9.58-12.39,3.74-3.72,7.87-6.9,12.39-9.54,4.51-2.64,9.37-4.69,14.58-6.15s10.61-2.19,16.23-2.19c7.32,0,14.37,1.25,21.14,3.76,6.77,2.51,12.91,6.1,18.42,10.78l-12.39,20.65c-3.58-3.63-7.71-6.48-12.39-8.55-4.68-2.06-9.61-3.1-14.78-3.1s-10.03,.99-14.58,2.97c-4.54,1.98-8.52,4.67-11.93,8.05-3.41,3.39-6.11,7.35-8.09,11.89-1.98,4.54-2.97,9.4-2.97,14.58s.99,10.13,2.97,14.7c1.98,4.57,4.68,8.56,8.09,11.98,3.41,3.41,7.39,6.11,11.93,8.09,4.54,1.98,9.4,2.97,14.58,2.97,2.97,0,5.86-.36,8.67-1.07,2.81-.71,5.48-1.71,8.01-2.97v-33.7h22.88v46.75Z"/>
|
||||||
|
<path class="cls-3" d="M627.75,183.9h-23.7V65.47h23.7v118.43Z"/>
|
||||||
|
<path class="cls-3" d="M699.44,183.9h-23.62V89.17h-35.6v-23.7h94.73v23.7h-35.51v94.73Z"/>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
<path class="cls-2" d="M181.53,115.06h0c-9.4-36.67-56.77-24.79-121.09-12.57C-3.54,114.64-25.35,19.85,37.72,3.62,46.91,1.26,56.55,0,66.47,0c63.55,0,115.06,51.51,115.06,115.06Z"/>
|
||||||
|
<path class="cls-1" d="M100,140h0c9.4,36.67,56.77,24.79,121.09,12.57,63.98-12.16,85.79,82.64,22.72,98.86-9.19,2.36-18.83,3.62-28.76,3.62-63.55,0-115.06-51.51-115.06-115.06Z"/>
|
||||||
|
<circle class="cls-3" cx="140.77" cy="127.53" r="24.88"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 2.9 KiB |
52
src/App.tsx
52
src/App.tsx
@ -7,7 +7,8 @@ import {
|
|||||||
appPrivateRoutes,
|
appPrivateRoutes,
|
||||||
appPublicRoutes,
|
appPublicRoutes,
|
||||||
privateRoutes,
|
privateRoutes,
|
||||||
publicRoutes
|
publicRoutes,
|
||||||
|
recursiveRouteRenderer
|
||||||
} from './routes'
|
} from './routes'
|
||||||
import { State } from './store/rootReducer'
|
import { State } from './store/rootReducer'
|
||||||
import { getNsecBunkerDelegatedKey, saveNsecBunkerDelegatedKey } from './utils'
|
import { getNsecBunkerDelegatedKey, saveNsecBunkerDelegatedKey } from './utils'
|
||||||
@ -48,39 +49,28 @@ const App = () => {
|
|||||||
return `${appPublicRoutes.login}?callbackPath=${callbackPathEncoded}`
|
return `${appPublicRoutes.login}?callbackPath=${callbackPathEncoded}`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const publicRoutesList = recursiveRouteRenderer({
|
||||||
|
routes: publicRoutes,
|
||||||
|
renderConditionCallback: (r) => {
|
||||||
|
if (authState?.loggedIn) {
|
||||||
|
if (!r.hiddenWhenLoggedIn) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const privateRouteList = recursiveRouteRenderer({ routes: privateRoutes })
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route element={<MainLayout />}>
|
<Route element={<MainLayout />}>
|
||||||
{authState?.loggedIn &&
|
{authState?.loggedIn && privateRouteList}
|
||||||
privateRoutes.map((route, index) => (
|
{publicRoutesList}
|
||||||
<Route
|
|
||||||
key={route.path + index}
|
|
||||||
path={route.path}
|
|
||||||
element={route.element}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
{publicRoutes.map((route, index) => {
|
|
||||||
if (authState?.loggedIn) {
|
|
||||||
if (!route.hiddenWhenLoggedIn) {
|
|
||||||
return (
|
|
||||||
<Route
|
|
||||||
key={route.path + index}
|
|
||||||
path={route.path}
|
|
||||||
element={route.element}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return (
|
|
||||||
<Route
|
|
||||||
key={route.path + index}
|
|
||||||
path={route.path}
|
|
||||||
element={route.element}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
})}
|
|
||||||
|
|
||||||
<Route path="*" element={<Navigate to={handleRootRedirect()} />} />
|
<Route path="*" element={<Navigate to={handleRootRedirect()} />} />
|
||||||
</Route>
|
</Route>
|
||||||
</Routes>
|
</Routes>
|
||||||
|
@ -122,13 +122,14 @@ export const AppBar = () => {
|
|||||||
position="fixed"
|
position="fixed"
|
||||||
className={styles.AppBar}
|
className={styles.AppBar}
|
||||||
sx={{
|
sx={{
|
||||||
boxShadow: 'none'
|
boxShadow: 'none',
|
||||||
|
'--AppBar-height': isAuthenticated ? '60px' : ''
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Container maxWidth="xl" disableGutters={true}>
|
<Container maxWidth="xl" disableGutters={true}>
|
||||||
<Toolbar className={styles.toolbar}>
|
<Toolbar className={styles.toolbar}>
|
||||||
<Box className={styles.logoWrapper}>
|
<Box className={styles.logoWrapper}>
|
||||||
<img src="/logo.png" alt="Logo" onClick={() => navigate('/')} />
|
<img src="/logo.svg" alt="Logo" onClick={() => navigate('/')} />
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Box className={styles.rightSideBox}>
|
<Box className={styles.rightSideBox}>
|
||||||
|
@ -1,9 +1,8 @@
|
|||||||
@import '../../colors.scss';
|
@import '../../styles/colors.scss';
|
||||||
@import '../../sizes.scss';
|
@import '../../styles/sizes.scss';
|
||||||
|
|
||||||
.AppBar {
|
.AppBar {
|
||||||
background-color: $background-color !important;
|
background-color: $background-color !important;
|
||||||
z-index: 1400 !important;
|
|
||||||
height: $header-height;
|
height: $header-height;
|
||||||
flex-direction: row !important;
|
flex-direction: row !important;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { Box, Button, Container, Grid, Link } from '@mui/material'
|
import { Box, Button, Container, Link } from '@mui/material'
|
||||||
import { Link as RouterLink } from 'react-router-dom'
|
import { Link as RouterLink } from 'react-router-dom'
|
||||||
import styles from './style.module.scss'
|
import styles from './style.module.scss'
|
||||||
|
|
||||||
@ -35,7 +35,7 @@ export const Footer = () => (
|
|||||||
to={'/'}
|
to={'/'}
|
||||||
className={styles.logo}
|
className={styles.logo}
|
||||||
>
|
>
|
||||||
<img src="/logo.png" alt="Logo" />
|
<img src="/logo.svg" alt="Logo" />
|
||||||
</Link>
|
</Link>
|
||||||
<Box
|
<Box
|
||||||
display={'grid'}
|
display={'grid'}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
@import '../../../colors.scss';
|
@import '../../../styles/colors.scss';
|
||||||
|
|
||||||
.card {
|
.card {
|
||||||
border-radius: 32px;
|
border-radius: 32px;
|
||||||
|
77
src/layouts/modal/index.tsx
Normal file
77
src/layouts/modal/index.tsx
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
import { Box, Button, Modal as ModalMui, Typography } from '@mui/material'
|
||||||
|
import {
|
||||||
|
Link,
|
||||||
|
matchPath,
|
||||||
|
Outlet,
|
||||||
|
useLocation,
|
||||||
|
useNavigate
|
||||||
|
} from 'react-router-dom'
|
||||||
|
import styles from './style.module.scss'
|
||||||
|
import { appPublicRoutes } from '../../routes'
|
||||||
|
|
||||||
|
function useRouteMatch(patterns: readonly string[]) {
|
||||||
|
const { pathname } = useLocation()
|
||||||
|
|
||||||
|
for (let i = 0; i < patterns.length; i += 1) {
|
||||||
|
const pattern = patterns[i]
|
||||||
|
const possibleMatch = matchPath(pattern, pathname)
|
||||||
|
if (possibleMatch !== null) {
|
||||||
|
return possibleMatch
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Modal = () => {
|
||||||
|
const navigate = useNavigate()
|
||||||
|
const tabs = [
|
||||||
|
{ to: appPublicRoutes.login, title: 'Login', label: 'Login' },
|
||||||
|
{ to: appPublicRoutes.register, title: 'Register', label: 'Register' },
|
||||||
|
{ to: appPublicRoutes.nostr, title: 'Login', label: <>Ostrich</> }
|
||||||
|
]
|
||||||
|
const routeMatch = useRouteMatch(tabs.map((t) => t.to))
|
||||||
|
const activeTab = routeMatch?.pattern?.path
|
||||||
|
const handleClose = () => {
|
||||||
|
navigate(appPublicRoutes.landingPage)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ModalMui
|
||||||
|
open={true}
|
||||||
|
onClose={handleClose}
|
||||||
|
aria-labelledby="modal-title"
|
||||||
|
aria-describedby="modal-description"
|
||||||
|
>
|
||||||
|
<Box className={styles.modal}>
|
||||||
|
<Typography id="modal-title" variant="h2">
|
||||||
|
Login
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<Box component={'ul'}>
|
||||||
|
{tabs.map((t) => {
|
||||||
|
return (
|
||||||
|
<Link to={t.to} key={t.to}>
|
||||||
|
<Button
|
||||||
|
variant={
|
||||||
|
activeTab === t.to || t.to === appPublicRoutes.nostr
|
||||||
|
? 'contained'
|
||||||
|
: 'text'
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{t.label}
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Outlet />
|
||||||
|
|
||||||
|
<Typography id="modal-description" variant="h4">
|
||||||
|
Welcome to Sigit
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
</ModalMui>
|
||||||
|
)
|
||||||
|
}
|
14
src/layouts/modal/style.module.scss
Normal file
14
src/layouts/modal/style.module.scss
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
.modal {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
|
||||||
|
background-color: white;
|
||||||
|
padding: 4px;
|
||||||
|
|
||||||
|
width: 100%;
|
||||||
|
max-width: 500px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
@ -1,5 +1,5 @@
|
|||||||
@import '../colors.scss';
|
@import '../styles/colors.scss';
|
||||||
@import '../sizes.scss';
|
@import '../styles/sizes.scss';
|
||||||
|
|
||||||
.main {
|
.main {
|
||||||
padding: $header-height 0 0 0;
|
padding: $header-height 0 0 0;
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
@import '../../colors.scss';
|
@import '../../styles/colors.scss';
|
||||||
|
|
||||||
.container {
|
.container {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
@ -1,29 +1,15 @@
|
|||||||
import {
|
import { Box, Button, Container, Link, Typography } from '@mui/material'
|
||||||
Box,
|
|
||||||
Button,
|
|
||||||
Container,
|
|
||||||
Link,
|
|
||||||
Typography,
|
|
||||||
useTheme
|
|
||||||
} from '@mui/material'
|
|
||||||
import { useEffect } from 'react'
|
import { useEffect } from 'react'
|
||||||
import { useSelector } from 'react-redux'
|
import { Outlet, useLocation, useNavigate } from 'react-router-dom'
|
||||||
import { useLocation, useNavigate } from 'react-router-dom'
|
|
||||||
import { appPublicRoutes } from '../../routes'
|
import { appPublicRoutes } from '../../routes'
|
||||||
import { State } from '../../store/rootReducer'
|
|
||||||
import { saveVisitedLink } from '../../utils'
|
import { saveVisitedLink } from '../../utils'
|
||||||
import styles from './style.module.scss'
|
import styles from './style.module.scss'
|
||||||
import { CardComponent } from '../../components/Landing/CardComponent/CardComponent'
|
import { CardComponent } from '../../components/Landing/CardComponent/CardComponent'
|
||||||
|
|
||||||
const bodyBackgroundColor = document.body.style.backgroundColor
|
|
||||||
|
|
||||||
export const LandingPage = () => {
|
export const LandingPage = () => {
|
||||||
const authState = useSelector((state: State) => state.auth)
|
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const location = useLocation()
|
const location = useLocation()
|
||||||
|
|
||||||
const theme = useTheme()
|
|
||||||
|
|
||||||
const onSignInClick = async () => {
|
const onSignInClick = async () => {
|
||||||
navigate(appPublicRoutes.login)
|
navigate(appPublicRoutes.login)
|
||||||
}
|
}
|
||||||
@ -173,7 +159,7 @@ export const LandingPage = () => {
|
|||||||
gap: '50px'
|
gap: '50px'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<img src="/logo.png" alt="Logo" width={300} />
|
<img src="/logo.svg" alt="Logo" width={300} />
|
||||||
<Typography
|
<Typography
|
||||||
variant="h1"
|
variant="h1"
|
||||||
sx={{
|
sx={{
|
||||||
@ -183,7 +169,7 @@ export const LandingPage = () => {
|
|||||||
textAlign: 'center'
|
textAlign: 'center'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
SIGit - Secure & Private Document Signing
|
Secure & Private Document Signing
|
||||||
<Typography
|
<Typography
|
||||||
mt={'15px'}
|
mt={'15px'}
|
||||||
sx={{
|
sx={{
|
||||||
@ -234,51 +220,7 @@ export const LandingPage = () => {
|
|||||||
GET STARTED
|
GET STARTED
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
{/* <Box>
|
<Outlet />
|
||||||
<Typography
|
|
||||||
sx={{
|
|
||||||
fontWeight: 'bold',
|
|
||||||
marginBottom: 5,
|
|
||||||
color: bodyBackgroundColor
|
|
||||||
? theme.palette.getContrastText(bodyBackgroundColor)
|
|
||||||
: ''
|
|
||||||
}}
|
|
||||||
variant="h4"
|
|
||||||
>
|
|
||||||
Secure Document Signing
|
|
||||||
</Typography>
|
|
||||||
<Typography
|
|
||||||
sx={{
|
|
||||||
color: bodyBackgroundColor
|
|
||||||
? theme.palette.getContrastText(bodyBackgroundColor)
|
|
||||||
: ''
|
|
||||||
}}
|
|
||||||
variant="body1"
|
|
||||||
>
|
|
||||||
SIGit is an open-source and self-hostable solution for secure document
|
|
||||||
signing and verification. Code is MIT licenced and available at{' '}
|
|
||||||
<a
|
|
||||||
className="bold-link"
|
|
||||||
target="_blank"
|
|
||||||
href="https://git.sigit.io/sig/it"
|
|
||||||
>
|
|
||||||
https://git.sigit.io/sig/it
|
|
||||||
</a>
|
|
||||||
.
|
|
||||||
<br />
|
|
||||||
<br />
|
|
||||||
SIGit lets you Create, Sign and Verify from any device with a browser.
|
|
||||||
<br />
|
|
||||||
<br />
|
|
||||||
Unlike other solutions, SIGit is totally private - files are encrypted
|
|
||||||
locally, and can only be exported by named recipients.
|
|
||||||
<br />
|
|
||||||
<br />
|
|
||||||
Anyone can <Link to={appPublicRoutes.verify}>VERIFY</Link> the
|
|
||||||
exported document.
|
|
||||||
</Typography>
|
|
||||||
</Box>
|
|
||||||
)} */}
|
|
||||||
</Container>
|
</Container>
|
||||||
)
|
)
|
||||||
}
|
}
|
@ -1,6 +1,7 @@
|
|||||||
@import '../../colors.scss';
|
@import '../../styles/colors.scss';
|
||||||
|
|
||||||
.container {
|
.container {
|
||||||
|
position: relative;
|
||||||
padding: 5vh 0%;
|
padding: 5vh 0%;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
@ -1,380 +1,3 @@
|
|||||||
import { Box, Button, Container, TextField, Typography } from '@mui/material'
|
|
||||||
import { getPublicKey, nip19 } from 'nostr-tools'
|
|
||||||
import { useEffect, useState } from 'react'
|
|
||||||
import { useDispatch } from 'react-redux'
|
|
||||||
import { useNavigate, useSearchParams } from 'react-router-dom'
|
|
||||||
import { toast } from 'react-toastify'
|
|
||||||
import { LoadingSpinner } from '../../components/LoadingSpinner'
|
|
||||||
import {
|
|
||||||
AuthController,
|
|
||||||
MetadataController,
|
|
||||||
NostrController
|
|
||||||
} from '../../controllers'
|
|
||||||
import {
|
|
||||||
updateKeyPair,
|
|
||||||
updateLoginMethod,
|
|
||||||
updateNsecbunkerPubkey,
|
|
||||||
updateNsecbunkerRelays
|
|
||||||
} from '../../store/actions'
|
|
||||||
import { LoginMethods } from '../../store/auth/types'
|
|
||||||
import { Dispatch } from '../../store/store'
|
|
||||||
import { npubToHex, queryNip05 } from '../../utils'
|
|
||||||
import styles from './style.module.scss'
|
|
||||||
import { hexToBytes } from '@noble/hashes/utils'
|
|
||||||
import { NIP05_REGEX } from '../../constants'
|
|
||||||
|
|
||||||
export const Login = () => {
|
export const Login = () => {
|
||||||
const [searchParams] = useSearchParams()
|
return <>Login</>
|
||||||
|
|
||||||
const dispatch: Dispatch = useDispatch()
|
|
||||||
const navigate = useNavigate()
|
|
||||||
|
|
||||||
const authController = new AuthController()
|
|
||||||
const metadataController = new MetadataController()
|
|
||||||
const nostrController = NostrController.getInstance()
|
|
||||||
|
|
||||||
const [isLoading, setIsLoading] = useState(false)
|
|
||||||
const [loadingSpinnerDesc, setLoadingSpinnerDesc] = useState('')
|
|
||||||
const [inputValue, setInputValue] = useState('')
|
|
||||||
const [authUrl, setAuthUrl] = useState<string>()
|
|
||||||
|
|
||||||
const [isNostrExtensionAvailable, setIsNostrExtensionAvailable] =
|
|
||||||
useState(false)
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setTimeout(() => {
|
|
||||||
setIsNostrExtensionAvailable(!!window.nostr)
|
|
||||||
}, 500)
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Call login function when enter is pressed
|
|
||||||
*/
|
|
||||||
const handleInputKeyDown = (event: any) => {
|
|
||||||
if (event.code === 'Enter' || event.code === 'NumpadEnter') {
|
|
||||||
event.preventDefault()
|
|
||||||
login()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const navigateAfterLogin = (path: string) => {
|
|
||||||
const callbackPath = searchParams.get('callbackPath')
|
|
||||||
|
|
||||||
if (callbackPath) {
|
|
||||||
// base64 decoded path
|
|
||||||
const path = atob(callbackPath)
|
|
||||||
navigate(path)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
navigate(path)
|
|
||||||
}
|
|
||||||
|
|
||||||
const loginWithExtension = async () => {
|
|
||||||
setIsLoading(true)
|
|
||||||
setLoadingSpinnerDesc('Capturing pubkey from nostr extension')
|
|
||||||
|
|
||||||
nostrController
|
|
||||||
.capturePublicKey()
|
|
||||||
.then(async (pubkey) => {
|
|
||||||
dispatch(updateLoginMethod(LoginMethods.extension))
|
|
||||||
|
|
||||||
setLoadingSpinnerDesc('Authenticating and finding metadata')
|
|
||||||
const redirectPath =
|
|
||||||
await authController.authAndGetMetadataAndRelaysMap(pubkey)
|
|
||||||
|
|
||||||
if (redirectPath) navigateAfterLogin(redirectPath)
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
toast.error('Error capturing public key from nostr extension: ' + err)
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
setIsLoading(false)
|
|
||||||
setLoadingSpinnerDesc('')
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Login with NSEC or HEX private key
|
|
||||||
* @param privateKey in HEX format
|
|
||||||
*/
|
|
||||||
const loginWithNsec = async (privateKey?: Uint8Array) => {
|
|
||||||
let nsec = ''
|
|
||||||
|
|
||||||
if (privateKey) {
|
|
||||||
nsec = nip19.nsecEncode(privateKey)
|
|
||||||
} else {
|
|
||||||
nsec = inputValue
|
|
||||||
|
|
||||||
try {
|
|
||||||
privateKey = nip19.decode(nsec).data as Uint8Array
|
|
||||||
} catch (err) {
|
|
||||||
toast.error(`Error decoding the nsec. ${err}`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!privateKey) {
|
|
||||||
toast.error(
|
|
||||||
'Snap, we failed to convert the private key you provided. Please make sure key is valid.'
|
|
||||||
)
|
|
||||||
setIsLoading(false)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const publickey = getPublicKey(privateKey)
|
|
||||||
|
|
||||||
dispatch(
|
|
||||||
updateKeyPair({
|
|
||||||
private: nsec,
|
|
||||||
public: publickey
|
|
||||||
})
|
|
||||||
)
|
|
||||||
dispatch(updateLoginMethod(LoginMethods.privateKey))
|
|
||||||
|
|
||||||
setIsLoading(true)
|
|
||||||
setLoadingSpinnerDesc('Authenticating and finding metadata')
|
|
||||||
|
|
||||||
const redirectPath = await authController
|
|
||||||
.authAndGetMetadataAndRelaysMap(publickey)
|
|
||||||
.catch((err) => {
|
|
||||||
toast.error('Error occurred in authentication: ' + err)
|
|
||||||
return null
|
|
||||||
})
|
|
||||||
|
|
||||||
if (redirectPath) navigateAfterLogin(redirectPath)
|
|
||||||
|
|
||||||
setIsLoading(false)
|
|
||||||
setLoadingSpinnerDesc('')
|
|
||||||
}
|
|
||||||
|
|
||||||
const loginWithNsecBunker = async () => {
|
|
||||||
let relays: string[] | undefined
|
|
||||||
let pubkey: string | undefined
|
|
||||||
|
|
||||||
setIsLoading(true)
|
|
||||||
|
|
||||||
const displayError = (message: string) => {
|
|
||||||
toast.error(message)
|
|
||||||
setIsLoading(false)
|
|
||||||
setLoadingSpinnerDesc('')
|
|
||||||
}
|
|
||||||
|
|
||||||
if (inputValue.match(NIP05_REGEX)) {
|
|
||||||
const nip05Profile = await queryNip05(inputValue).catch((err) => {
|
|
||||||
toast.error('An error occurred while querying nip05 profile: ' + err)
|
|
||||||
return null
|
|
||||||
})
|
|
||||||
|
|
||||||
if (nip05Profile) {
|
|
||||||
pubkey = nip05Profile.pubkey
|
|
||||||
relays = nip05Profile.relays
|
|
||||||
}
|
|
||||||
} else if (inputValue.startsWith('npub')) {
|
|
||||||
pubkey = nip19.decode(inputValue).data as string
|
|
||||||
const metadataEvent = await metadataController
|
|
||||||
.findMetadata(pubkey)
|
|
||||||
.catch(() => {
|
|
||||||
return null
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!metadataEvent) {
|
|
||||||
return displayError('metadata not found!')
|
|
||||||
}
|
|
||||||
|
|
||||||
const metadataContent =
|
|
||||||
metadataController.extractProfileMetadataContent(metadataEvent)
|
|
||||||
|
|
||||||
if (!metadataContent?.nip05) {
|
|
||||||
return displayError('nip05 not present in metadata')
|
|
||||||
}
|
|
||||||
|
|
||||||
const nip05Profile = await queryNip05(inputValue).catch((err) => {
|
|
||||||
toast.error('An error occurred while querying nip05 profile: ' + err)
|
|
||||||
return null
|
|
||||||
})
|
|
||||||
|
|
||||||
if (nip05Profile) {
|
|
||||||
if (nip05Profile.pubkey !== pubkey) {
|
|
||||||
return displayError(
|
|
||||||
'pubkey in nip05 does not match with provided npub'
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
relays = nip05Profile.relays
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!relays || relays.length === 0) {
|
|
||||||
return displayError('No relay found for nsecbunker')
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!pubkey) {
|
|
||||||
return displayError('pubkey not found')
|
|
||||||
}
|
|
||||||
|
|
||||||
setLoadingSpinnerDesc('Initializing nsecBunker')
|
|
||||||
await nostrController.nsecBunkerInit(relays)
|
|
||||||
|
|
||||||
setLoadingSpinnerDesc('Creating nsecbunker singer')
|
|
||||||
await nostrController
|
|
||||||
.createNsecBunkerSigner(pubkey)
|
|
||||||
.then(async (signer) => {
|
|
||||||
signer.on('authUrl', (url: string) => {
|
|
||||||
setAuthUrl(url)
|
|
||||||
})
|
|
||||||
|
|
||||||
dispatch(updateLoginMethod(LoginMethods.nsecBunker))
|
|
||||||
dispatch(updateNsecbunkerPubkey(pubkey))
|
|
||||||
dispatch(updateNsecbunkerRelays(relays))
|
|
||||||
|
|
||||||
setLoadingSpinnerDesc('Authenticating and finding metadata')
|
|
||||||
|
|
||||||
const redirectPath = await authController
|
|
||||||
.authAndGetMetadataAndRelaysMap(pubkey!)
|
|
||||||
.catch((err) => {
|
|
||||||
toast.error('Error occurred in authentication: ' + err)
|
|
||||||
return null
|
|
||||||
})
|
|
||||||
|
|
||||||
if (redirectPath) navigateAfterLogin(redirectPath)
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
toast.error(
|
|
||||||
'An error occurred while creating nsecbunker signer: ' + err
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
setIsLoading(false)
|
|
||||||
setLoadingSpinnerDesc('')
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const loginWithBunkerConnectionString = async () => {
|
|
||||||
// Extract the key
|
|
||||||
const keyStartIndex = inputValue.indexOf('bunker://') + 'bunker://'.length
|
|
||||||
const keyEndIndex = inputValue.indexOf('?relay=')
|
|
||||||
const key = inputValue.substring(keyStartIndex, keyEndIndex)
|
|
||||||
|
|
||||||
const pubkey = npubToHex(key)
|
|
||||||
|
|
||||||
if (!pubkey) {
|
|
||||||
toast.error('Invalid pubkey in bunker connection string.')
|
|
||||||
setIsLoading(false)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Extract the relay value
|
|
||||||
const relayIndex = inputValue.indexOf('relay=')
|
|
||||||
const relay = inputValue.substring(
|
|
||||||
relayIndex + 'relay='.length,
|
|
||||||
inputValue.length
|
|
||||||
)
|
|
||||||
|
|
||||||
setIsLoading(true)
|
|
||||||
setLoadingSpinnerDesc('Initializing bunker NDK')
|
|
||||||
|
|
||||||
await nostrController.nsecBunkerInit([relay])
|
|
||||||
|
|
||||||
setLoadingSpinnerDesc('Creating remote signer')
|
|
||||||
await nostrController
|
|
||||||
.createNsecBunkerSigner(pubkey)
|
|
||||||
.then(async (signer) => {
|
|
||||||
signer.on('authUrl', (url: string) => {
|
|
||||||
setAuthUrl(url)
|
|
||||||
})
|
|
||||||
|
|
||||||
dispatch(updateLoginMethod(LoginMethods.nsecBunker))
|
|
||||||
dispatch(updateNsecbunkerPubkey(pubkey))
|
|
||||||
dispatch(updateNsecbunkerRelays([relay]))
|
|
||||||
|
|
||||||
setLoadingSpinnerDesc('Authenticating and finding metadata')
|
|
||||||
|
|
||||||
const redirectPath = await authController
|
|
||||||
.authAndGetMetadataAndRelaysMap(pubkey!)
|
|
||||||
.catch((err) => {
|
|
||||||
toast.error('Error occurred in authentication: ' + err)
|
|
||||||
return null
|
|
||||||
})
|
|
||||||
|
|
||||||
if (redirectPath) navigateAfterLogin(redirectPath)
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
toast.error(
|
|
||||||
'An error occurred while creating nsecbunker signer: ' + err
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
setIsLoading(false)
|
|
||||||
setLoadingSpinnerDesc('')
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const login = () => {
|
|
||||||
if (inputValue.startsWith('bunker://')) {
|
|
||||||
return loginWithBunkerConnectionString()
|
|
||||||
}
|
|
||||||
|
|
||||||
if (inputValue.startsWith('nsec')) {
|
|
||||||
return loginWithNsec()
|
|
||||||
}
|
|
||||||
if (inputValue.startsWith('npub')) {
|
|
||||||
return loginWithNsecBunker()
|
|
||||||
}
|
|
||||||
if (inputValue.match(NIP05_REGEX)) {
|
|
||||||
return loginWithNsecBunker()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if maybe hex nsec
|
|
||||||
try {
|
|
||||||
const privateKey = hexToBytes(inputValue)
|
|
||||||
const publickey = getPublicKey(privateKey)
|
|
||||||
|
|
||||||
if (publickey) return loginWithNsec(privateKey)
|
|
||||||
} catch (err) {
|
|
||||||
console.warn('err', err)
|
|
||||||
}
|
|
||||||
|
|
||||||
toast.error(
|
|
||||||
'Invalid format, please use: private key (hex), nsec..., bunker:// or nip05 format.'
|
|
||||||
)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (authUrl) {
|
|
||||||
return (
|
|
||||||
<iframe
|
|
||||||
title="Nsecbunker auth"
|
|
||||||
src={authUrl}
|
|
||||||
width="100%"
|
|
||||||
height="500px"
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Container maxWidth={'sm'}>
|
|
||||||
{isLoading && <LoadingSpinner desc={loadingSpinnerDesc} />}
|
|
||||||
<div className={styles.loginPage}>
|
|
||||||
<Typography variant="h4">Welcome to Sigit</Typography>
|
|
||||||
<TextField
|
|
||||||
onKeyDown={handleInputKeyDown}
|
|
||||||
label="nip05 login / nip46 bunker string"
|
|
||||||
value={inputValue}
|
|
||||||
onChange={(e) => setInputValue(e.target.value)}
|
|
||||||
sx={{ width: '100%', mt: 2 }}
|
|
||||||
/>
|
|
||||||
{isNostrExtensionAvailable && (
|
|
||||||
<Button onClick={loginWithExtension} variant="text">
|
|
||||||
Login with extension
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Box sx={{ mt: 1, display: 'flex', justifyContent: 'center' }}>
|
|
||||||
<Button disabled={!inputValue} onClick={login} variant="contained">
|
|
||||||
Login
|
|
||||||
</Button>
|
|
||||||
</Box>
|
|
||||||
</div>
|
|
||||||
</Container>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
382
src/pages/nostr/index.tsx
Normal file
382
src/pages/nostr/index.tsx
Normal file
@ -0,0 +1,382 @@
|
|||||||
|
import { Box, Button, TextField } from '@mui/material'
|
||||||
|
import { getPublicKey, nip19 } from 'nostr-tools'
|
||||||
|
import { useEffect, useState } from 'react'
|
||||||
|
import { useDispatch } from 'react-redux'
|
||||||
|
import { useNavigate, useSearchParams } from 'react-router-dom'
|
||||||
|
import { toast } from 'react-toastify'
|
||||||
|
import { LoadingSpinner } from '../../components/LoadingSpinner'
|
||||||
|
import {
|
||||||
|
AuthController,
|
||||||
|
MetadataController,
|
||||||
|
NostrController
|
||||||
|
} from '../../controllers'
|
||||||
|
import {
|
||||||
|
updateKeyPair,
|
||||||
|
updateLoginMethod,
|
||||||
|
updateNsecbunkerPubkey,
|
||||||
|
updateNsecbunkerRelays
|
||||||
|
} from '../../store/actions'
|
||||||
|
import { LoginMethods } from '../../store/auth/types'
|
||||||
|
import { Dispatch } from '../../store/store'
|
||||||
|
import { npubToHex, queryNip05 } from '../../utils'
|
||||||
|
import styles from './style.module.scss'
|
||||||
|
import { hexToBytes } from '@noble/hashes/utils'
|
||||||
|
import { NIP05_REGEX } from '../../constants'
|
||||||
|
|
||||||
|
export const Nostr = () => {
|
||||||
|
const [searchParams] = useSearchParams()
|
||||||
|
|
||||||
|
const dispatch: Dispatch = useDispatch()
|
||||||
|
const navigate = useNavigate()
|
||||||
|
|
||||||
|
const authController = new AuthController()
|
||||||
|
const metadataController = new MetadataController()
|
||||||
|
const nostrController = NostrController.getInstance()
|
||||||
|
|
||||||
|
const [isLoading, setIsLoading] = useState(false)
|
||||||
|
const [loadingSpinnerDesc, setLoadingSpinnerDesc] = useState('')
|
||||||
|
const [inputValue, setInputValue] = useState('')
|
||||||
|
const [authUrl, setAuthUrl] = useState<string>()
|
||||||
|
|
||||||
|
const [isNostrExtensionAvailable, setIsNostrExtensionAvailable] =
|
||||||
|
useState(false)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setTimeout(() => {
|
||||||
|
setIsNostrExtensionAvailable(!!window.nostr)
|
||||||
|
}, 500)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Call login function when enter is pressed
|
||||||
|
*/
|
||||||
|
const handleInputKeyDown = (event: any) => {
|
||||||
|
if (event.code === 'Enter' || event.code === 'NumpadEnter') {
|
||||||
|
event.preventDefault()
|
||||||
|
login()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const navigateAfterLogin = (path: string) => {
|
||||||
|
const callbackPath = searchParams.get('callbackPath')
|
||||||
|
|
||||||
|
if (callbackPath) {
|
||||||
|
// base64 decoded path
|
||||||
|
const path = atob(callbackPath)
|
||||||
|
navigate(path)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
navigate(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
const loginWithExtension = async () => {
|
||||||
|
setIsLoading(true)
|
||||||
|
setLoadingSpinnerDesc('Capturing pubkey from nostr extension')
|
||||||
|
|
||||||
|
nostrController
|
||||||
|
.capturePublicKey()
|
||||||
|
.then(async (pubkey) => {
|
||||||
|
dispatch(updateLoginMethod(LoginMethods.extension))
|
||||||
|
|
||||||
|
setLoadingSpinnerDesc('Authenticating and finding metadata')
|
||||||
|
const redirectPath =
|
||||||
|
await authController.authAndGetMetadataAndRelaysMap(pubkey)
|
||||||
|
|
||||||
|
if (redirectPath) navigateAfterLogin(redirectPath)
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
toast.error('Error capturing public key from nostr extension: ' + err)
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
setIsLoading(false)
|
||||||
|
setLoadingSpinnerDesc('')
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Login with NSEC or HEX private key
|
||||||
|
* @param privateKey in HEX format
|
||||||
|
*/
|
||||||
|
const loginWithNsec = async (privateKey?: Uint8Array) => {
|
||||||
|
let nsec = ''
|
||||||
|
|
||||||
|
if (privateKey) {
|
||||||
|
nsec = nip19.nsecEncode(privateKey)
|
||||||
|
} else {
|
||||||
|
nsec = inputValue
|
||||||
|
|
||||||
|
try {
|
||||||
|
privateKey = nip19.decode(nsec).data as Uint8Array
|
||||||
|
} catch (err) {
|
||||||
|
toast.error(`Error decoding the nsec. ${err}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!privateKey) {
|
||||||
|
toast.error(
|
||||||
|
'Snap, we failed to convert the private key you provided. Please make sure key is valid.'
|
||||||
|
)
|
||||||
|
setIsLoading(false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const publickey = getPublicKey(privateKey)
|
||||||
|
|
||||||
|
dispatch(
|
||||||
|
updateKeyPair({
|
||||||
|
private: nsec,
|
||||||
|
public: publickey
|
||||||
|
})
|
||||||
|
)
|
||||||
|
dispatch(updateLoginMethod(LoginMethods.privateKey))
|
||||||
|
|
||||||
|
setIsLoading(true)
|
||||||
|
setLoadingSpinnerDesc('Authenticating and finding metadata')
|
||||||
|
|
||||||
|
const redirectPath = await authController
|
||||||
|
.authAndGetMetadataAndRelaysMap(publickey)
|
||||||
|
.catch((err) => {
|
||||||
|
toast.error('Error occurred in authentication: ' + err)
|
||||||
|
return null
|
||||||
|
})
|
||||||
|
|
||||||
|
if (redirectPath) navigateAfterLogin(redirectPath)
|
||||||
|
|
||||||
|
setIsLoading(false)
|
||||||
|
setLoadingSpinnerDesc('')
|
||||||
|
}
|
||||||
|
|
||||||
|
const loginWithNsecBunker = async () => {
|
||||||
|
let relays: string[] | undefined
|
||||||
|
let pubkey: string | undefined
|
||||||
|
|
||||||
|
setIsLoading(true)
|
||||||
|
|
||||||
|
const displayError = (message: string) => {
|
||||||
|
toast.error(message)
|
||||||
|
setIsLoading(false)
|
||||||
|
setLoadingSpinnerDesc('')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (inputValue.match(NIP05_REGEX)) {
|
||||||
|
const nip05Profile = await queryNip05(inputValue).catch((err) => {
|
||||||
|
toast.error('An error occurred while querying nip05 profile: ' + err)
|
||||||
|
return null
|
||||||
|
})
|
||||||
|
|
||||||
|
if (nip05Profile) {
|
||||||
|
pubkey = nip05Profile.pubkey
|
||||||
|
relays = nip05Profile.relays
|
||||||
|
}
|
||||||
|
} else if (inputValue.startsWith('npub')) {
|
||||||
|
pubkey = nip19.decode(inputValue).data as string
|
||||||
|
const metadataEvent = await metadataController
|
||||||
|
.findMetadata(pubkey)
|
||||||
|
.catch(() => {
|
||||||
|
return null
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!metadataEvent) {
|
||||||
|
return displayError('metadata not found!')
|
||||||
|
}
|
||||||
|
|
||||||
|
const metadataContent =
|
||||||
|
metadataController.extractProfileMetadataContent(metadataEvent)
|
||||||
|
|
||||||
|
if (!metadataContent?.nip05) {
|
||||||
|
return displayError('nip05 not present in metadata')
|
||||||
|
}
|
||||||
|
|
||||||
|
const nip05Profile = await queryNip05(inputValue).catch((err) => {
|
||||||
|
toast.error('An error occurred while querying nip05 profile: ' + err)
|
||||||
|
return null
|
||||||
|
})
|
||||||
|
|
||||||
|
if (nip05Profile) {
|
||||||
|
if (nip05Profile.pubkey !== pubkey) {
|
||||||
|
return displayError(
|
||||||
|
'pubkey in nip05 does not match with provided npub'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
relays = nip05Profile.relays
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!relays || relays.length === 0) {
|
||||||
|
return displayError('No relay found for nsecbunker')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!pubkey) {
|
||||||
|
return displayError('pubkey not found')
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoadingSpinnerDesc('Initializing nsecBunker')
|
||||||
|
await nostrController.nsecBunkerInit(relays)
|
||||||
|
|
||||||
|
setLoadingSpinnerDesc('Creating nsecbunker singer')
|
||||||
|
await nostrController
|
||||||
|
.createNsecBunkerSigner(pubkey)
|
||||||
|
.then(async (signer) => {
|
||||||
|
signer.on('authUrl', (url: string) => {
|
||||||
|
setAuthUrl(url)
|
||||||
|
})
|
||||||
|
|
||||||
|
dispatch(updateLoginMethod(LoginMethods.nsecBunker))
|
||||||
|
dispatch(updateNsecbunkerPubkey(pubkey))
|
||||||
|
dispatch(updateNsecbunkerRelays(relays))
|
||||||
|
|
||||||
|
setLoadingSpinnerDesc('Authenticating and finding metadata')
|
||||||
|
|
||||||
|
const redirectPath = await authController
|
||||||
|
.authAndGetMetadataAndRelaysMap(pubkey!)
|
||||||
|
.catch((err) => {
|
||||||
|
toast.error('Error occurred in authentication: ' + err)
|
||||||
|
return null
|
||||||
|
})
|
||||||
|
|
||||||
|
if (redirectPath) navigateAfterLogin(redirectPath)
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
toast.error(
|
||||||
|
'An error occurred while creating nsecbunker signer: ' + err
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
setIsLoading(false)
|
||||||
|
setLoadingSpinnerDesc('')
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const loginWithBunkerConnectionString = async () => {
|
||||||
|
// Extract the key
|
||||||
|
const keyStartIndex = inputValue.indexOf('bunker://') + 'bunker://'.length
|
||||||
|
const keyEndIndex = inputValue.indexOf('?relay=')
|
||||||
|
const key = inputValue.substring(keyStartIndex, keyEndIndex)
|
||||||
|
|
||||||
|
const pubkey = npubToHex(key)
|
||||||
|
|
||||||
|
if (!pubkey) {
|
||||||
|
toast.error('Invalid pubkey in bunker connection string.')
|
||||||
|
setIsLoading(false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract the relay value
|
||||||
|
const relayIndex = inputValue.indexOf('relay=')
|
||||||
|
const relay = inputValue.substring(
|
||||||
|
relayIndex + 'relay='.length,
|
||||||
|
inputValue.length
|
||||||
|
)
|
||||||
|
|
||||||
|
setIsLoading(true)
|
||||||
|
setLoadingSpinnerDesc('Initializing bunker NDK')
|
||||||
|
|
||||||
|
await nostrController.nsecBunkerInit([relay])
|
||||||
|
|
||||||
|
setLoadingSpinnerDesc('Creating remote signer')
|
||||||
|
await nostrController
|
||||||
|
.createNsecBunkerSigner(pubkey)
|
||||||
|
.then(async (signer) => {
|
||||||
|
signer.on('authUrl', (url: string) => {
|
||||||
|
setAuthUrl(url)
|
||||||
|
})
|
||||||
|
|
||||||
|
dispatch(updateLoginMethod(LoginMethods.nsecBunker))
|
||||||
|
dispatch(updateNsecbunkerPubkey(pubkey))
|
||||||
|
dispatch(updateNsecbunkerRelays([relay]))
|
||||||
|
|
||||||
|
setLoadingSpinnerDesc('Authenticating and finding metadata')
|
||||||
|
|
||||||
|
const redirectPath = await authController
|
||||||
|
.authAndGetMetadataAndRelaysMap(pubkey!)
|
||||||
|
.catch((err) => {
|
||||||
|
toast.error('Error occurred in authentication: ' + err)
|
||||||
|
return null
|
||||||
|
})
|
||||||
|
|
||||||
|
if (redirectPath) navigateAfterLogin(redirectPath)
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
toast.error(
|
||||||
|
'An error occurred while creating nsecbunker signer: ' + err
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
setIsLoading(false)
|
||||||
|
setLoadingSpinnerDesc('')
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const login = () => {
|
||||||
|
if (inputValue.startsWith('bunker://')) {
|
||||||
|
return loginWithBunkerConnectionString()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (inputValue.startsWith('nsec')) {
|
||||||
|
return loginWithNsec()
|
||||||
|
}
|
||||||
|
if (inputValue.startsWith('npub')) {
|
||||||
|
return loginWithNsecBunker()
|
||||||
|
}
|
||||||
|
if (inputValue.match(NIP05_REGEX)) {
|
||||||
|
return loginWithNsecBunker()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if maybe hex nsec
|
||||||
|
try {
|
||||||
|
const privateKey = hexToBytes(inputValue)
|
||||||
|
const publickey = getPublicKey(privateKey)
|
||||||
|
|
||||||
|
if (publickey) return loginWithNsec(privateKey)
|
||||||
|
} catch (err) {
|
||||||
|
console.warn('err', err)
|
||||||
|
}
|
||||||
|
|
||||||
|
toast.error(
|
||||||
|
'Invalid format, please use: private key (hex), nsec..., bunker:// or nip05 format.'
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (authUrl) {
|
||||||
|
return (
|
||||||
|
<iframe
|
||||||
|
title="Nsecbunker auth"
|
||||||
|
src={authUrl}
|
||||||
|
width="100%"
|
||||||
|
height="500px"
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{isLoading && <LoadingSpinner desc={loadingSpinnerDesc} />}
|
||||||
|
|
||||||
|
<Box>
|
||||||
|
<div className={styles.loginPage}>
|
||||||
|
<TextField
|
||||||
|
onKeyDown={handleInputKeyDown}
|
||||||
|
label="nip05 login / nip46 bunker string"
|
||||||
|
value={inputValue}
|
||||||
|
onChange={(e) => setInputValue(e.target.value)}
|
||||||
|
sx={{ width: '100%', mt: 2 }}
|
||||||
|
/>
|
||||||
|
{isNostrExtensionAvailable && (
|
||||||
|
<Button onClick={loginWithExtension} variant="text">
|
||||||
|
Login with extension
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Box sx={{ mt: 1, display: 'flex', justifyContent: 'center' }}>
|
||||||
|
<Button disabled={!inputValue} onClick={login} variant="contained">
|
||||||
|
Login
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
</div>
|
||||||
|
</Box>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
@ -1,4 +1,4 @@
|
|||||||
@import '../../colors.scss';
|
@import '../../styles/colors.scss';
|
||||||
|
|
||||||
.loginPage {
|
.loginPage {
|
||||||
color: $text-color;
|
color: $text-color;
|
3
src/pages/register/index.tsx
Normal file
3
src/pages/register/index.tsx
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export const Register = () => {
|
||||||
|
return <>Register</>
|
||||||
|
}
|
@ -1,4 +1,4 @@
|
|||||||
@import '../../../colors.scss';
|
@import '../../../styles/colors.scss';
|
||||||
|
|
||||||
.container {
|
.container {
|
||||||
margin-top: 25px;
|
margin-top: 25px;
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
@import '../../colors.scss';
|
@import '../../styles/colors.scss';
|
||||||
|
|
||||||
.container {
|
.container {
|
||||||
color: $text-color;
|
color: $text-color;
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
@import '../../colors.scss';
|
@import '../../styles/colors.scss';
|
||||||
|
|
||||||
.container {
|
.container {
|
||||||
color: $text-color;
|
color: $text-color;
|
||||||
|
@ -1,8 +1,11 @@
|
|||||||
|
import { Modal } from '../layouts/modal'
|
||||||
import { CreatePage } from '../pages/create'
|
import { CreatePage } from '../pages/create'
|
||||||
import { HomePage } from '../pages/home'
|
import { HomePage } from '../pages/home'
|
||||||
import { LandingPage } from '../pages/landing/LandingPage'
|
import { LandingPage } from '../pages/landing'
|
||||||
import { Login } from '../pages/login'
|
import { Login } from '../pages/login'
|
||||||
|
import { Nostr } from '../pages/nostr'
|
||||||
import { ProfilePage } from '../pages/profile'
|
import { ProfilePage } from '../pages/profile'
|
||||||
|
import { Register } from '../pages/register'
|
||||||
import { SettingsPage } from '../pages/settings/Settings'
|
import { SettingsPage } from '../pages/settings/Settings'
|
||||||
import { CacheSettingsPage } from '../pages/settings/cache'
|
import { CacheSettingsPage } from '../pages/settings/cache'
|
||||||
import { ProfileSettingsPage } from '../pages/settings/profile'
|
import { ProfileSettingsPage } from '../pages/settings/profile'
|
||||||
@ -10,6 +13,7 @@ import { RelaysPage } from '../pages/settings/relays'
|
|||||||
import { SignPage } from '../pages/sign'
|
import { SignPage } from '../pages/sign'
|
||||||
import { VerifyPage } from '../pages/verify'
|
import { VerifyPage } from '../pages/verify'
|
||||||
import { hexToNpub } from '../utils'
|
import { hexToNpub } from '../utils'
|
||||||
|
import { Route, RouteProps } from 'react-router-dom'
|
||||||
|
|
||||||
export const appPrivateRoutes = {
|
export const appPrivateRoutes = {
|
||||||
homePage: '/',
|
homePage: '/',
|
||||||
@ -25,6 +29,8 @@ export const appPublicRoutes = {
|
|||||||
profile: '/profile/:npub',
|
profile: '/profile/:npub',
|
||||||
landingPage: '/',
|
landingPage: '/',
|
||||||
login: '/login',
|
login: '/login',
|
||||||
|
register: '/login/register',
|
||||||
|
nostr: '/login/nostr',
|
||||||
verify: '/verify',
|
verify: '/verify',
|
||||||
source: 'https://git.sigit.io/sig/it'
|
source: 'https://git.sigit.io/sig/it'
|
||||||
}
|
}
|
||||||
@ -35,17 +41,70 @@ export const getProfileRoute = (hexKey: string) =>
|
|||||||
export const getProfileSettingsRoute = (hexKey: string) =>
|
export const getProfileSettingsRoute = (hexKey: string) =>
|
||||||
appPrivateRoutes.profileSettings.replace(':npub', hexToNpub(hexKey))
|
appPrivateRoutes.profileSettings.replace(':npub', hexToNpub(hexKey))
|
||||||
|
|
||||||
export const publicRoutes = [
|
type CustomRouteProps<T> = T &
|
||||||
|
Omit<RouteProps, 'children'> & {
|
||||||
|
children?: Array<CustomRouteProps<T>>
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CustomRoutes<T> {
|
||||||
|
routes?: CustomRouteProps<T>[]
|
||||||
|
renderConditionCallback?: (route: CustomRouteProps<T>) => boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
type PublicRouteProps = CustomRouteProps<{
|
||||||
|
hiddenWhenLoggedIn?: boolean
|
||||||
|
}>
|
||||||
|
|
||||||
|
export function recursiveRouteRenderer<T>({
|
||||||
|
routes,
|
||||||
|
renderConditionCallback = () => true
|
||||||
|
}: CustomRoutes<T>) {
|
||||||
|
if (!routes) return null
|
||||||
|
|
||||||
|
return routes.map((route, index) =>
|
||||||
|
renderConditionCallback(route) ? (
|
||||||
|
<Route
|
||||||
|
key={`${route.path}${index}`}
|
||||||
|
path={route.path}
|
||||||
|
element={route.element}
|
||||||
|
>
|
||||||
|
{recursiveRouteRenderer({
|
||||||
|
routes: route.children,
|
||||||
|
renderConditionCallback
|
||||||
|
})}
|
||||||
|
</Route>
|
||||||
|
) : null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const publicRoutes: PublicRouteProps[] = [
|
||||||
{
|
{
|
||||||
path: appPublicRoutes.landingPage,
|
path: appPublicRoutes.landingPage,
|
||||||
hiddenWhenLoggedIn: true,
|
hiddenWhenLoggedIn: true,
|
||||||
element: <LandingPage />
|
element: <LandingPage />,
|
||||||
},
|
children: [
|
||||||
|
{
|
||||||
|
element: <Modal />,
|
||||||
|
children: [
|
||||||
{
|
{
|
||||||
path: appPublicRoutes.login,
|
path: appPublicRoutes.login,
|
||||||
hiddenWhenLoggedIn: true,
|
hiddenWhenLoggedIn: true,
|
||||||
element: <Login />
|
element: <Login />
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: appPublicRoutes.register,
|
||||||
|
hiddenWhenLoggedIn: true,
|
||||||
|
element: <Register />
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: appPublicRoutes.nostr,
|
||||||
|
hiddenWhenLoggedIn: true,
|
||||||
|
element: <Nostr />
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: appPublicRoutes.profile,
|
path: appPublicRoutes.profile,
|
||||||
element: <ProfilePage />
|
element: <ProfilePage />
|
||||||
|
@ -1 +0,0 @@
|
|||||||
$header-height: 78.5px;
|
|
1
src/styles/sizes.scss
Normal file
1
src/styles/sizes.scss
Normal file
@ -0,0 +1 @@
|
|||||||
|
$header-height: var(--AppBar-height, 78.5px);
|
@ -21,6 +21,14 @@ export const theme = extendTheme({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
|
MuiModal: {
|
||||||
|
styleOverrides: {
|
||||||
|
root: {
|
||||||
|
insetBlock: '25px',
|
||||||
|
insetInline: '20px'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
MuiButton: {
|
MuiButton: {
|
||||||
styleOverrides: {
|
styleOverrides: {
|
||||||
root: {
|
root: {
|
||||||
|
Loading…
Reference in New Issue
Block a user