feat: maintain logged in sesssion
All checks were successful
Release / build_and_release (push) Successful in 43s

This commit is contained in:
Sabir Hassan 2024-03-19 15:27:18 +05:00
parent a9bdd1f95e
commit 2ed092bcbd
8 changed files with 123 additions and 5 deletions

View File

@ -1,7 +1,7 @@
import { useEffect } from 'react' import { useEffect } from 'react'
import { useSelector } from 'react-redux' import { useSelector } from 'react-redux'
import { Route, Routes } from 'react-router-dom' import { Route, Routes } from 'react-router-dom'
import { NostrController } from './controllers' import { AuthController, NostrController } from './controllers'
import { MainLayout } from './layouts/Main' import { MainLayout } from './layouts/Main'
import { LandingPage } from './pages/landing/LandingPage' import { LandingPage } from './pages/landing/LandingPage'
import { privateRoutes, publicRoutes } from './routes' import { privateRoutes, publicRoutes } from './routes'
@ -13,6 +13,9 @@ const App = () => {
useEffect(() => { useEffect(() => {
generateBunkerDelegatedKey() generateBunkerDelegatedKey()
const authController = new AuthController()
authController.checkSession()
}, []) }, [])
const generateBunkerDelegatedKey = () => { const generateBunkerDelegatedKey = () => {

View File

@ -18,7 +18,11 @@ import { Link, useNavigate } from 'react-router-dom'
import nostrichAvatar from '../../assets/images/avatar.png' import nostrichAvatar from '../../assets/images/avatar.png'
import nostrichLogo from '../../assets/images/nostr-logo.jpg' import nostrichLogo from '../../assets/images/nostr-logo.jpg'
import { appPublicRoutes, getProfileRoute } from '../../routes' import { appPublicRoutes, getProfileRoute } from '../../routes'
import { saveNsecBunkerDelegatedKey, shorten } from '../../utils' import {
clearAuthToken,
saveNsecBunkerDelegatedKey,
shorten
} from '../../utils'
import styles from './style.module.scss' import styles from './style.module.scss'
import { NostrController } from '../../controllers' import { NostrController } from '../../controllers'
@ -67,6 +71,9 @@ export const AppBar = () => {
}) })
) )
// clear authToken saved in local storage
clearAuthToken()
// update nsecBunker delegated key after logout // update nsecBunker delegated key after logout
const nostrController = NostrController.getInstance() const nostrController = NostrController.getInstance()
const newDelegatedKey = nostrController.generateDelegatedKey() const newDelegatedKey = nostrController.generateDelegatedKey()

View File

@ -2,8 +2,15 @@ import { EventTemplate } from 'nostr-tools'
import { MetadataController, NostrController } from '.' import { MetadataController, NostrController } from '.'
import { setAuthState, setMetadataEvent } from '../store/actions' import { setAuthState, setMetadataEvent } from '../store/actions'
import store from '../store/store' import store from '../store/store'
import { getVisitedLink } from '../utils' import {
base64DecodeAuthToken,
base64EncodeSignedEvent,
getAuthToken,
getVisitedLink,
saveAuthToken
} from '../utils'
import { appPrivateRoutes } from '../routes' import { appPrivateRoutes } from '../routes'
import { AuthToken, SignedEvent } from '../types'
export class AuthController { export class AuthController {
private nostrController: NostrController private nostrController: NostrController
@ -44,7 +51,8 @@ export class AuthController {
created_at: timestamp created_at: timestamp
} }
await this.nostrController.signEvent(authEvent) const signedAuthEvent = await this.nostrController.signEvent(authEvent)
this.createAndSaveAuthToken(signedAuthEvent)
store.dispatch( store.dispatch(
setAuthState({ setAuthState({
@ -64,4 +72,50 @@ export class AuthController {
return Promise.resolve(appPrivateRoutes.homePage) return Promise.resolve(appPrivateRoutes.homePage)
} }
} }
checkSession() {
const savedAuthToken = getAuthToken()
if (savedAuthToken && this.isTokenValid(savedAuthToken)) {
const signedEvent = base64DecodeAuthToken(savedAuthToken.token)
store.dispatch(
setAuthState({
loggedIn: true,
usersPubkey: signedEvent.pubkey
})
)
return
}
store.dispatch(
setAuthState({
loggedIn: false,
usersPubkey: undefined
})
)
}
private isTokenValid(authToken: AuthToken): boolean {
const timeNow = Math.round(Date.now() / 1000)
const eventExpiresAt = authToken.expiresAt
const timeDifference = eventExpiresAt - timeNow
// check if previous authToken has expired or not
if (timeDifference > 0) {
return true
}
return false
}
private createAndSaveAuthToken(signedAuthEvent: SignedEvent) {
const base64Encoded = base64EncodeSignedEvent(signedAuthEvent)
// save newly created auth token (base64 nostr singed event) in local storage along with expiry time
const createdAt = Math.round(Date.now() / 1000)
saveAuthToken(base64Encoded, createdAt + 3600) // 3600 secs = 1 hour
return base64Encoded
}
} }

View File

@ -13,7 +13,8 @@ import { saveState } from './utils'
store.subscribe( store.subscribe(
_.throttle(() => { _.throttle(() => {
saveState({ saveState({
auth: store.getState().auth auth: store.getState().auth,
metadata: store.getState().metadata
}) })
}, 1000) }, 1000)
) )

4
src/types/auth.ts Normal file
View File

@ -0,0 +1,4 @@
export interface AuthToken {
token: string
expiresAt: number
}

View File

@ -1,2 +1,3 @@
export * from './auth'
export * from './nostr' export * from './nostr'
export * from './profile' export * from './profile'

View File

@ -1,4 +1,5 @@
import { State } from '../store/rootReducer' import { State } from '../store/rootReducer'
import { AuthToken } from '../types'
export const saveState = (state: object) => { export const saveState = (state: object) => {
try { try {
@ -53,3 +54,29 @@ export const getVisitedLink = () => {
return null return null
} }
} }
export const saveAuthToken = (token: string, expiresAt: number) => {
localStorage.setItem(
'authToken',
JSON.stringify({
token,
expiresAt
})
)
}
export const getAuthToken = () => {
const serializedAuthDetail = localStorage.getItem('authToken')
if (!serializedAuthDetail) return null
try {
return JSON.parse(serializedAuthDetail) as AuthToken
} catch {
return null
}
}
export const clearAuthToken = () => {
localStorage.removeItem('authToken')
}

View File

@ -124,3 +124,24 @@ export const queryNip05 = async (
relays relays
} }
} }
export const base64EncodeSignedEvent = (event: SignedEvent) => {
try {
const authEventSerialized = JSON.stringify(event)
const token = btoa(authEventSerialized)
return token
} catch (error) {
throw new Error('An error occurred in JSON.stringify of signedAuthEvent')
}
}
export const base64DecodeAuthToken = (authToken: string): SignedEvent => {
const decodedToken = atob(authToken)
try {
const signedEvent = JSON.parse(decodedToken)
return signedEvent
} catch (error) {
throw new Error('An error occurred in JSON.parse of the auth token')
}
}