diff --git a/src/App.tsx b/src/App.tsx index 47dc74e..907b10e 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,4 +1,4 @@ -import React, { useEffect } from 'react'; +import React, { useState } from 'react'; import './App.css'; import '@rainbow-me/rainbowkit/styles.css'; import { RouterProvider } from 'react-router-dom'; @@ -9,11 +9,16 @@ import { getDefaultConfig, RainbowKitProvider, RainbowKitAuthenticationProvider, + createAuthenticationAdapter, + AuthenticationStatus, } from '@rainbow-me/rainbowkit'; import { WagmiProvider } from 'wagmi'; import { sepolia } from 'viem/chains'; +import { createSiweMessage } from 'viem/siwe'; import theme from './libs/theme'; import { router } from './router'; +import { api } from './api'; +import { AuthProvider, useAuth } from './context/authContext'; const queryClient = new QueryClient({ defaultOptions: { @@ -25,35 +30,86 @@ const queryClient = new QueryClient({ }, }); -const config = getDefaultConfig({ - appName: 'RainbowKit demo', - projectId: '1cf030f3b91e339bc4e6ecf71a694a88', - chains: [sepolia], -}); +const AuthenticationWrapper: React.FC = () => { + const { setAuthInfo, signOut } = useAuth(); + const [authStatus, setAuthStatus] = + useState('unauthenticated'); -const App: React.FC = () => { - const authStatus = useAuthStatus(); + const authenticationAdapter = createAuthenticationAdapter({ + getNonce: async () => { + const { data } = await api.get('auth/siwe/nonce'); + return data.nonce; + }, + createMessage: ({ nonce, address, chainId }) => { + return createSiweMessage({ + address, + chainId, + domain: window.location.host, + nonce, + uri: window.location.origin, + version: '1', + statement: 'Sign in with Ethereum to the app.', + }); + }, + getMessageBody: ({ message }) => message, + verify: async ({ message, signature }) => { + const { data } = await api.post('auth/siwe/verify', { + message, + signature, + chainId: 11155111, + }); + + if (!data) { + throw new Error('Verification response data is empty'); + } + + if (data?.jwt) { + setAuthStatus('authenticated'); + setAuthInfo(data.jwt); + window.location.replace('/'); + } else { + setAuthStatus('unauthenticated'); + } + + return data; + }, + signOut: async () => { + await api.post('auth/logout'); + setAuthStatus('unauthenticated'); + signOut(); + }, + }); - useEffect(() => { - console.log('Auth status changed:', authStatus); - }, [authStatus]); + const config = getDefaultConfig({ + appName: 'RainbowKit demo', + projectId: '1cf030f3b91e339bc4e6ecf71a694a88', + chains: [sepolia], + }); return ( + + + + + + + ); +}; + +const App: React.FC = () => { + return ( + - - - - - + - + ); }; diff --git a/src/ProtectedRoute.tsx b/src/ProtectedRoute.tsx new file mode 100644 index 0000000..69173e4 --- /dev/null +++ b/src/ProtectedRoute.tsx @@ -0,0 +1,37 @@ +import { Navigate } from 'react-router-dom'; +import { useEffect, useState } from 'react'; +import CircularProgress from '@mui/material/CircularProgress'; +import Backdrop from '@mui/material/Backdrop'; +import { useAuth } from './context/authContext'; + +const ProtectedRoute = ({ children }: { children: JSX.Element }) => { + const { isAuthenticated } = useAuth(); + const [loading, setLoading] = useState(true); + + useEffect(() => { + const checkAuthStatus = async () => { + setLoading(true); + setTimeout(() => { + setLoading(false); + }, 1000); + }; + + checkAuthStatus(); + }, [isAuthenticated]); + + if (loading) { + return ( + + + + ); + } + + if (!isAuthenticated) { + return ; + } + + return children; +}; + +export default ProtectedRoute; diff --git a/src/api/index.ts b/src/api/index.ts index 15ec0e3..7cbd484 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -1,4 +1,3 @@ -// src/api/index.ts import axios from 'axios'; const baseURL = import.meta.env.VITE_API_BASE_URL; @@ -15,3 +14,25 @@ export const api = axios.create({ 'Content-Type': 'application/json', }, }); + +api.interceptors.request.use( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (config: any) => { + const token = localStorage.getItem('OCI_TOKEN'); + if (token) { + return { + ...config, + headers: { + ...config.headers, + Authorization: `Bearer ${token}`, + }, + }; + } + return config; + }, + (error) => { + return Promise.reject(error); + } +); + +export default api; diff --git a/src/context/authContext.tsx b/src/context/authContext.tsx new file mode 100644 index 0000000..8328ee3 --- /dev/null +++ b/src/context/authContext.tsx @@ -0,0 +1,62 @@ +import { + createContext, + useState, + useContext, + ReactNode, + useEffect, + useMemo, +} from 'react'; + +interface AuthContextType { + isAuthenticated: boolean; + jwt: string | null; + setAuthInfo: (jwt: string) => void; + signOut: () => void; +} + +const AuthContext = createContext(undefined); + +export const AuthProvider = ({ children }: { children: ReactNode }) => { + const [isAuthenticated, setIsAuthenticated] = useState(false); + const [jwt, setJwt] = useState(null); + + useEffect(() => { + const token = localStorage.getItem('OCI_TOKEN'); + if (token) { + setJwt(token); + setIsAuthenticated(true); + } + }, []); + + const setAuthInfo = (token: string) => { + setJwt(token); + setIsAuthenticated(true); + localStorage.setItem('OCI_TOKEN', token); + }; + + const signOut = () => { + setJwt(null); + setIsAuthenticated(false); + localStorage.removeItem('OCI_TOKEN'); + }; + + const value = useMemo( + () => ({ + isAuthenticated, + jwt, + setAuthInfo, + signOut, + }), + [isAuthenticated, jwt] + ); + + return {children}; +}; + +export const useAuth = (): AuthContextType => { + const context = useContext(AuthContext); + if (context === undefined) { + throw new Error('useAuth must be used within an AuthProvider'); + } + return context; +}; diff --git a/src/main.tsx b/src/main.tsx index 6f00df7..6177530 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -2,12 +2,9 @@ import React from 'react'; import ReactDOM from 'react-dom/client'; import './index.css'; import App from './App'; -import { AuthProvider } from './context/authContext'; ReactDOM.createRoot(document.getElementById('root')!).render( - - - + ); diff --git a/src/router/index.tsx b/src/router/index.tsx index affa680..491d1f6 100644 --- a/src/router/index.tsx +++ b/src/router/index.tsx @@ -7,6 +7,7 @@ import Permissions from '../pages/Permissions'; import Attestation from '../pages/Identifiers/Attestation'; import DefaultLayout from '../layouts/DefaultLayout'; +import ProtectedRoute from '../ProtectedRoute'; export const router = createBrowserRouter([ { @@ -19,20 +20,36 @@ export const router = createBrowserRouter([ children: [ { path: '/', - element: , + element: ( + + + + ), index: true, }, { path: '/identifiers', - element: , + element: ( + + + + ), }, { path: '/attestation', - element: , + element: ( + + + + ), }, { path: '/permissions', - element: , + element: ( + + + + ), }, ], },