fix(callback): login and private route redirect

Fix #229
This commit is contained in:
en 2025-01-31 19:32:28 +01:00
parent efe3c2c9c7
commit 37baf57093
6 changed files with 97 additions and 84 deletions

View File

@ -4,8 +4,6 @@ import { Navigate, Route, Routes } from 'react-router-dom'
import { useAppSelector, useAuth } from './hooks' import { useAppSelector, useAuth } from './hooks'
import { MainLayout } from './layouts/Main' import { MainLayout } from './layouts/Main'
import { appPrivateRoutes, appPublicRoutes } from './routes'
import { import {
privateRoutes, privateRoutes,
publicRoutes, publicRoutes,
@ -16,7 +14,7 @@ import './App.scss'
const App = () => { const App = () => {
const { checkSession } = useAuth() const { checkSession } = useAuth()
const authState = useAppSelector((state) => state.auth) const isLoggedIn = useAppSelector((state) => state.auth?.loggedIn)
useEffect(() => { useEffect(() => {
if (window.location.hostname === '0.0.0.0') { if (window.location.hostname === '0.0.0.0') {
@ -29,19 +27,9 @@ const App = () => {
checkSession() checkSession()
}, [checkSession]) }, [checkSession])
const handleRootRedirect = () => {
if (authState.loggedIn) return appPrivateRoutes.homePage
const callbackPathEncoded = btoa(
window.location.href.split(`${window.location.origin}/#`)[1]
)
return `${appPublicRoutes.landingPage}?callbackPath=${callbackPathEncoded}`
}
// Hide route only if loggedIn and r.hiddenWhenLoggedIn are both true // Hide route only if loggedIn and r.hiddenWhenLoggedIn are both true
const publicRoutesList = recursiveRouteRenderer(publicRoutes, (r) => { const publicRoutesList = recursiveRouteRenderer(publicRoutes, (r) => {
return !authState.loggedIn || !r.hiddenWhenLoggedIn return !isLoggedIn || !r.hiddenWhenLoggedIn
}) })
const privateRouteList = recursiveRouteRenderer(privateRoutes) const privateRouteList = recursiveRouteRenderer(privateRoutes)
@ -49,9 +37,9 @@ const App = () => {
return ( return (
<Routes> <Routes>
<Route element={<MainLayout />}> <Route element={<MainLayout />}>
{authState?.loggedIn && privateRouteList}
{publicRoutesList} {publicRoutesList}
<Route path="*" element={<Navigate to={handleRootRedirect()} />} /> {privateRouteList}
<Route path="*" element={<Navigate to={'/'} />} />
</Route> </Route>
</Routes> </Routes>
) )

View File

@ -1,16 +1,11 @@
import { useCallback, useEffect, useRef, useState } from 'react' import { useCallback, useEffect, useRef, useState } from 'react'
import { Outlet, useNavigate, useSearchParams } from 'react-router-dom' import { Outlet, useNavigate, useSearchParams } from 'react-router-dom'
import { getPublicKey, nip19 } from 'nostr-tools' import { getPublicKey, nip19 } from 'nostr-tools'
import { init as initNostrLogin } from 'nostr-login' import { init as initNostrLogin } from 'nostr-login'
import { NostrLoginAuthOptions } from 'nostr-login/dist/types' import { NostrLoginAuthOptions } from 'nostr-login/dist/types'
import { AppBar } from '../components/AppBar/AppBar' import { AppBar } from '../components/AppBar/AppBar'
import { LoadingSpinner } from '../components/LoadingSpinner' import { LoadingSpinner } from '../components/LoadingSpinner'
import { NostrController } from '../controllers' import { NostrController } from '../controllers'
import { import {
useAppDispatch, useAppDispatch,
useAppSelector, useAppSelector,
@ -19,7 +14,6 @@ import {
useNDK, useNDK,
useNDKContext useNDKContext
} from '../hooks' } from '../hooks'
import { import {
restoreState, restoreState,
setUserProfile, setUserProfile,
@ -30,9 +24,7 @@ import {
setUserRobotImage setUserRobotImage
} from '../store/actions' } from '../store/actions'
import { LoginMethod } from '../store/auth/types' import { LoginMethod } from '../store/auth/types'
import { getRoboHashPicture, loadState } from '../utils' import { getRoboHashPicture, loadState } from '../utils'
import styles from './style.module.scss' import styles from './style.module.scss'
export const MainLayout = () => { export const MainLayout = () => {
@ -53,29 +45,32 @@ export const MainLayout = () => {
// Ref to track if `subscribeForSigits` has been called // Ref to track if `subscribeForSigits` has been called
const hasSubscribed = useRef(false) const hasSubscribed = useRef(false)
const navigateAfterLogin = (path: string) => { const navigateAfterLogin = useCallback(
const callbackPath = searchParams.get('callbackPath') (path: string) => {
const isCallback = window.location.hash.startsWith('#/?callbackPath=')
if (callbackPath) { if (isCallback) {
// base64 decoded path const path = atob(window.location.hash.replace('#/?callbackPath=', ''))
const path = atob(callbackPath) setSearchParams((prev) => {
prev.delete('callbackPath')
return prev
})
navigate(path)
return
}
navigate(path) navigate(path)
return },
} [navigate, setSearchParams]
)
navigate(path)
}
const login = useCallback(async () => { const login = useCallback(async () => {
dispatch(updateLoginMethod(LoginMethod.nostrLogin)) try {
dispatch(updateLoginMethod(LoginMethod.nostrLogin))
const nostrController = NostrController.getInstance() const nostrController = NostrController.getInstance()
const pubkey = await nostrController.capturePublicKey() const pubkey = await nostrController.capturePublicKey()
const redirectPath = await authAndGetMetadataAndRelaysMap(pubkey)
const redirectPath = await authAndGetMetadataAndRelaysMap(pubkey)
if (redirectPath) {
navigateAfterLogin(redirectPath) navigateAfterLogin(redirectPath)
} catch (error) {
console.error(`Error occured during login`, error)
} }
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [dispatch]) }, [dispatch])

View File

@ -1,7 +1,5 @@
import { Box, Button } from '@mui/material' import { Box, Button } from '@mui/material'
import { useEffect } from 'react' import { Outlet } from 'react-router-dom'
import { Outlet, useLocation } from 'react-router-dom'
import { saveVisitedLink } from '../../utils'
import { CardComponent } from '../../components/Landing/CardComponent/CardComponent' import { CardComponent } from '../../components/Landing/CardComponent/CardComponent'
import { Container } from '../../components/Container' import { Container } from '../../components/Container'
import styles from './style.module.scss' import styles from './style.module.scss'
@ -20,13 +18,19 @@ import {
import { FontAwesomeIconStack } from '../../components/FontAwesomeIconStack' import { FontAwesomeIconStack } from '../../components/FontAwesomeIconStack'
import { Footer } from '../../components/Footer/Footer' import { Footer } from '../../components/Footer/Footer'
import { launch as launchNostrLoginDialog } from 'nostr-login' import { launch as launchNostrLoginDialog } from 'nostr-login'
import { useDidMount } from '../../hooks'
export const LandingPage = () => { export const LandingPage = () => {
const location = useLocation()
const onSignInClick = async () => { const onSignInClick = async () => {
launchNostrLoginDialog() launchNostrLoginDialog()
} }
useDidMount(() => {
const isCallback = window.location.hash.startsWith('#/?callbackPath=')
// Open nostr login if detect callback
if (isCallback) {
onSignInClick()
}
})
const cards = [ const cards = [
{ {
@ -101,10 +105,6 @@ export const LandingPage = () => {
} }
] ]
useEffect(() => {
saveVisitedLink(location.pathname, location.search)
}, [location])
return ( return (
<div className={styles.background}> <div className={styles.background}>
<div <div

View File

@ -0,0 +1,21 @@
import { Navigate, useLocation } from 'react-router-dom'
import { useAppSelector } from '../hooks'
import { appPublicRoutes } from '.'
export function PrivateRoute({ children }: { children: JSX.Element }) {
const location = useLocation()
const isLoggedIn = useAppSelector((state) => state.auth?.loggedIn)
if (!isLoggedIn) {
return (
<Navigate
to={{
pathname: appPublicRoutes.landingPage,
search: `?callbackPath=${btoa(location.pathname)}`
}}
replace
/>
)
}
return children
}

View File

@ -11,6 +11,7 @@ import { RelaysPage } from '../pages/settings/relays'
import { SettingsPage } from '../pages/settings/Settings' import { SettingsPage } from '../pages/settings/Settings'
import { SignPage } from '../pages/sign' import { SignPage } from '../pages/sign'
import { VerifyPage } from '../pages/verify' import { VerifyPage } from '../pages/verify'
import { PrivateRoute } from './PrivateRoute'
/** /**
* Helper type allows for extending react-router-dom's **RouteProps** with generic type * Helper type allows for extending react-router-dom's **RouteProps** with generic type
@ -70,34 +71,66 @@ export const publicRoutes: PublicRouteProps[] = [
export const privateRoutes = [ export const privateRoutes = [
{ {
path: appPrivateRoutes.homePage, path: appPrivateRoutes.homePage,
element: <HomePage /> element: (
<PrivateRoute>
<HomePage />
</PrivateRoute>
)
}, },
{ {
path: appPrivateRoutes.create, path: appPrivateRoutes.create,
element: <CreatePage /> element: (
<PrivateRoute>
<CreatePage />
</PrivateRoute>
)
}, },
{ {
path: `${appPrivateRoutes.sign}/:id?`, path: `${appPrivateRoutes.sign}/:id?`,
element: <SignPage /> element: (
<PrivateRoute>
<SignPage />
</PrivateRoute>
)
}, },
{ {
path: appPrivateRoutes.settings, path: appPrivateRoutes.settings,
element: <SettingsPage /> element: (
<PrivateRoute>
<SettingsPage />
</PrivateRoute>
)
}, },
{ {
path: appPrivateRoutes.profileSettings, path: appPrivateRoutes.profileSettings,
element: <ProfileSettingsPage /> element: (
<PrivateRoute>
<ProfileSettingsPage />
</PrivateRoute>
)
}, },
{ {
path: appPrivateRoutes.cacheSettings, path: appPrivateRoutes.cacheSettings,
element: <CacheSettingsPage /> element: (
<PrivateRoute>
<CacheSettingsPage />
</PrivateRoute>
)
}, },
{ {
path: appPrivateRoutes.relays, path: appPrivateRoutes.relays,
element: <RelaysPage /> element: (
<PrivateRoute>
<RelaysPage />
</PrivateRoute>
)
}, },
{ {
path: appPrivateRoutes.nostrLogin, path: appPrivateRoutes.nostrLogin,
element: <NostrLoginPage /> element: (
<PrivateRoute>
<NostrLoginPage />
</PrivateRoute>
)
} }
] ]

View File

@ -26,30 +26,6 @@ export const clearState = () => {
localStorage.removeItem('state') localStorage.removeItem('state')
} }
export const saveVisitedLink = (pathname: string, search: string) => {
localStorage.setItem(
'visitedLink',
JSON.stringify({
pathname,
search
})
)
}
export const getVisitedLink = () => {
const visitedLink = localStorage.getItem('visitedLink')
if (!visitedLink) return null
try {
return JSON.parse(visitedLink) as {
pathname: string
search: string
}
} catch {
return null
}
}
export const saveAuthToken = (token: string) => { export const saveAuthToken = (token: string) => {
localStorage.setItem('authToken', token) localStorage.setItem('authToken', token)
} }