diff --git a/src/App.tsx b/src/App.tsx index 64fb4e9..b0bf01c 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,7 +1,7 @@ import { useEffect } from 'react' import { useSelector } from 'react-redux' import { Route, Routes } from 'react-router-dom' -import { NostrController } from './controllers' +import { AuthController, NostrController } from './controllers' import { MainLayout } from './layouts/Main' import { LandingPage } from './pages/landing/LandingPage' import { privateRoutes, publicRoutes } from './routes' @@ -13,6 +13,9 @@ const App = () => { useEffect(() => { generateBunkerDelegatedKey() + + const authController = new AuthController() + authController.checkSession() }, []) const generateBunkerDelegatedKey = () => { diff --git a/src/components/AppBar/AppBar.tsx b/src/components/AppBar/AppBar.tsx index 04aa4ba..7beae68 100644 --- a/src/components/AppBar/AppBar.tsx +++ b/src/components/AppBar/AppBar.tsx @@ -18,7 +18,11 @@ import { Link, useNavigate } from 'react-router-dom' import nostrichAvatar from '../../assets/images/avatar.png' import nostrichLogo from '../../assets/images/nostr-logo.jpg' import { appPublicRoutes, getProfileRoute } from '../../routes' -import { saveNsecBunkerDelegatedKey, shorten } from '../../utils' +import { + clearAuthToken, + saveNsecBunkerDelegatedKey, + shorten +} from '../../utils' import styles from './style.module.scss' import { NostrController } from '../../controllers' @@ -67,6 +71,9 @@ export const AppBar = () => { }) ) + // clear authToken saved in local storage + clearAuthToken() + // update nsecBunker delegated key after logout const nostrController = NostrController.getInstance() const newDelegatedKey = nostrController.generateDelegatedKey() diff --git a/src/controllers/AuthController.ts b/src/controllers/AuthController.ts index 7b6a77d..4992274 100644 --- a/src/controllers/AuthController.ts +++ b/src/controllers/AuthController.ts @@ -2,8 +2,15 @@ import { EventTemplate } from 'nostr-tools' import { MetadataController, NostrController } from '.' import { setAuthState, setMetadataEvent } from '../store/actions' import store from '../store/store' -import { getVisitedLink } from '../utils' +import { + base64DecodeAuthToken, + base64EncodeSignedEvent, + getAuthToken, + getVisitedLink, + saveAuthToken +} from '../utils' import { appPrivateRoutes } from '../routes' +import { AuthToken, SignedEvent } from '../types' export class AuthController { private nostrController: NostrController @@ -44,7 +51,8 @@ export class AuthController { created_at: timestamp } - await this.nostrController.signEvent(authEvent) + const signedAuthEvent = await this.nostrController.signEvent(authEvent) + this.createAndSaveAuthToken(signedAuthEvent) store.dispatch( setAuthState({ @@ -64,4 +72,50 @@ export class AuthController { 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 + } } diff --git a/src/main.tsx b/src/main.tsx index 967317b..30a290a 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -13,7 +13,8 @@ import { saveState } from './utils' store.subscribe( _.throttle(() => { saveState({ - auth: store.getState().auth + auth: store.getState().auth, + metadata: store.getState().metadata }) }, 1000) ) diff --git a/src/types/auth.ts b/src/types/auth.ts new file mode 100644 index 0000000..1bffb49 --- /dev/null +++ b/src/types/auth.ts @@ -0,0 +1,4 @@ +export interface AuthToken { + token: string + expiresAt: number +} diff --git a/src/types/index.ts b/src/types/index.ts index 3002541..0f820f1 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -1,2 +1,3 @@ +export * from './auth' export * from './nostr' export * from './profile' diff --git a/src/utils/localStorage.ts b/src/utils/localStorage.ts index 5e0c285..a20fffb 100644 --- a/src/utils/localStorage.ts +++ b/src/utils/localStorage.ts @@ -1,4 +1,5 @@ import { State } from '../store/rootReducer' +import { AuthToken } from '../types' export const saveState = (state: object) => { try { @@ -53,3 +54,29 @@ export const getVisitedLink = () => { 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') +} diff --git a/src/utils/nostr.ts b/src/utils/nostr.ts index 10e2b88..1424b88 100644 --- a/src/utils/nostr.ts +++ b/src/utils/nostr.ts @@ -124,3 +124,24 @@ export const queryNip05 = async ( 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') + } +}