Compare commits

...

7 Commits

26 changed files with 630 additions and 497 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 80 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 72 KiB

34
public/logo.svg Normal file
View 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

View File

@ -7,7 +7,8 @@ import {
appPrivateRoutes,
appPublicRoutes,
privateRoutes,
publicRoutes
publicRoutes,
recursiveRouteRenderer
} from './routes'
import { State } from './store/rootReducer'
import { getNsecBunkerDelegatedKey, saveNsecBunkerDelegatedKey } from './utils'
@ -48,39 +49,28 @@ const App = () => {
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 (
<Routes>
<Route element={<MainLayout />}>
{authState?.loggedIn &&
privateRoutes.map((route, index) => (
<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}
/>
)
}
})}
{authState?.loggedIn && privateRouteList}
{publicRoutesList}
<Route path="*" element={<Navigate to={handleRootRedirect()} />} />
</Route>
</Routes>

View File

@ -122,13 +122,14 @@ export const AppBar = () => {
position="fixed"
className={styles.AppBar}
sx={{
boxShadow: 'none'
boxShadow: 'none',
'--AppBar-height': isAuthenticated ? '60px' : ''
}}
>
<Container maxWidth="xl" disableGutters={true}>
<Toolbar className={styles.toolbar}>
<Box className={styles.logoWrapper}>
<img src="/logo.png" alt="Logo" onClick={() => navigate('/')} />
<img src="/logo.svg" alt="Logo" onClick={() => navigate('/')} />
</Box>
<Box className={styles.rightSideBox}>

View File

@ -1,9 +1,8 @@
@import '../../colors.scss';
@import '../../sizes.scss';
@import '../../styles/colors.scss';
@import '../../styles/sizes.scss';
.AppBar {
background-color: $background-color !important;
z-index: 1400 !important;
height: $header-height;
flex-direction: row !important;
align-items: center;

View File

@ -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 styles from './style.module.scss'
@ -35,7 +35,7 @@ export const Footer = () => (
to={'/'}
className={styles.logo}
>
<img src="/logo.png" alt="Logo" />
<img src="/logo.svg" alt="Logo" />
</Link>
<Box
display={'grid'}

View File

@ -1,4 +1,4 @@
@import '../../../colors.scss';
@import '../../../styles/colors.scss';
.card {
border-radius: 32px;

View 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>
)
}

View 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;
}

View File

@ -1,5 +1,5 @@
@import '../colors.scss';
@import '../sizes.scss';
@import '../styles/colors.scss';
@import '../styles/sizes.scss';
.main {
padding: $header-height 0 0 0;

View File

@ -1,4 +1,4 @@
@import '../../colors.scss';
@import '../../styles/colors.scss';
.container {
display: flex;

View File

@ -1,29 +1,15 @@
import {
Box,
Button,
Container,
Link,
Typography,
useTheme
} from '@mui/material'
import { Box, Button, Container, Link, Typography } from '@mui/material'
import { useEffect } from 'react'
import { useSelector } from 'react-redux'
import { useLocation, useNavigate } from 'react-router-dom'
import { Outlet, useLocation, useNavigate } from 'react-router-dom'
import { appPublicRoutes } from '../../routes'
import { State } from '../../store/rootReducer'
import { saveVisitedLink } from '../../utils'
import styles from './style.module.scss'
import { CardComponent } from '../../components/Landing/CardComponent/CardComponent'
const bodyBackgroundColor = document.body.style.backgroundColor
export const LandingPage = () => {
const authState = useSelector((state: State) => state.auth)
const navigate = useNavigate()
const location = useLocation()
const theme = useTheme()
const onSignInClick = async () => {
navigate(appPublicRoutes.login)
}
@ -173,7 +159,7 @@ export const LandingPage = () => {
gap: '50px'
}}
>
<img src="/logo.png" alt="Logo" width={300} />
<img src="/logo.svg" alt="Logo" width={300} />
<Typography
variant="h1"
sx={{
@ -183,7 +169,7 @@ export const LandingPage = () => {
textAlign: 'center'
}}
>
SIGit - Secure &amp; Private Document Signing
Secure &amp; Private Document Signing
<Typography
mt={'15px'}
sx={{
@ -234,51 +220,7 @@ export const LandingPage = () => {
GET STARTED
</Button>
{/* <Box>
<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>
)} */}
<Outlet />
</Container>
)
}

View File

@ -1,6 +1,7 @@
@import '../../colors.scss';
@import '../../styles/colors.scss';
.container {
position: relative;
padding: 5vh 0%;
flex-direction: column;
align-items: center;

View File

@ -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 = () => {
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 (
<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>
)
return <>Login</>
}

382
src/pages/nostr/index.tsx Normal file
View 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>
</>
)
}

View File

@ -1,4 +1,4 @@
@import '../../colors.scss';
@import '../../styles/colors.scss';
.loginPage {
color: $text-color;

View File

@ -0,0 +1,3 @@
export const Register = () => {
return <>Register</>
}

View File

@ -1,4 +1,4 @@
@import '../../../colors.scss';
@import '../../../styles/colors.scss';
.container {
margin-top: 25px;

View File

@ -1,4 +1,4 @@
@import '../../colors.scss';
@import '../../styles/colors.scss';
.container {
color: $text-color;

View File

@ -1,4 +1,4 @@
@import '../../colors.scss';
@import '../../styles/colors.scss';
.container {
color: $text-color;

View File

@ -1,8 +1,11 @@
import { Modal } from '../layouts/modal'
import { CreatePage } from '../pages/create'
import { HomePage } from '../pages/home'
import { LandingPage } from '../pages/landing/LandingPage'
import { LandingPage } from '../pages/landing'
import { Login } from '../pages/login'
import { Nostr } from '../pages/nostr'
import { ProfilePage } from '../pages/profile'
import { Register } from '../pages/register'
import { SettingsPage } from '../pages/settings/Settings'
import { CacheSettingsPage } from '../pages/settings/cache'
import { ProfileSettingsPage } from '../pages/settings/profile'
@ -10,6 +13,7 @@ import { RelaysPage } from '../pages/settings/relays'
import { SignPage } from '../pages/sign'
import { VerifyPage } from '../pages/verify'
import { hexToNpub } from '../utils'
import { Route, RouteProps } from 'react-router-dom'
export const appPrivateRoutes = {
homePage: '/',
@ -25,6 +29,8 @@ export const appPublicRoutes = {
profile: '/profile/:npub',
landingPage: '/',
login: '/login',
register: '/login/register',
nostr: '/login/nostr',
verify: '/verify',
source: 'https://git.sigit.io/sig/it'
}
@ -35,17 +41,70 @@ export const getProfileRoute = (hexKey: string) =>
export const getProfileSettingsRoute = (hexKey: string) =>
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,
hiddenWhenLoggedIn: true,
element: <LandingPage />
},
element: <LandingPage />,
children: [
{
element: <Modal />,
children: [
{
path: appPublicRoutes.login,
hiddenWhenLoggedIn: true,
element: <Login />
},
{
path: appPublicRoutes.register,
hiddenWhenLoggedIn: true,
element: <Register />
},
{
path: appPublicRoutes.nostr,
hiddenWhenLoggedIn: true,
element: <Nostr />
}
]
}
]
},
{
path: appPublicRoutes.profile,
element: <ProfilePage />

View File

@ -1 +0,0 @@
$header-height: 78.5px;

1
src/styles/sizes.scss Normal file
View File

@ -0,0 +1 @@
$header-height: var(--AppBar-height, 78.5px);

View File

@ -21,6 +21,14 @@ export const theme = extendTheme({
}
},
components: {
MuiModal: {
styleOverrides: {
root: {
insetBlock: '25px',
insetInline: '20px'
}
}
},
MuiButton: {
styleOverrides: {
root: {