Skip to content

Commit ba2442a

Browse files
refactor(web): structured-atlas-interaction
1 parent 33b818e commit ba2442a

File tree

8 files changed

+307
-238
lines changed

8 files changed

+307
-238
lines changed

web/src/app.tsx

+68-65
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { Route } from "react-router-dom";
44

55
import "react-loading-skeleton/dist/skeleton.css";
66
import "react-toastify/dist/ReactToastify.css";
7+
import AtlasProvider from "context/AtlasProvider";
78
import GraphqlBatcherProvider from "context/GraphqlBatcher";
89
import IsListProvider from "context/IsListProvider";
910
import { NewDisputeProvider } from "context/NewDisputeContext";
@@ -29,71 +30,73 @@ const App: React.FC = () => {
2930
<Web3Provider>
3031
<QueryClientProvider>
3132
<GraphqlBatcherProvider>
32-
<IsListProvider>
33-
<NewDisputeProvider>
34-
<SentryRoutes>
35-
<Route path="/" element={<Layout />}>
36-
<Route
37-
index
38-
element={
39-
<Suspense fallback={<Loader width={"48px"} height={"48px"} />}>
40-
<Home />
41-
</Suspense>
42-
}
43-
/>
44-
<Route
45-
path="cases/*"
46-
element={
47-
<Suspense fallback={<Loader width={"48px"} height={"48px"} />}>
48-
<Cases />
49-
</Suspense>
50-
}
51-
/>
52-
<Route
53-
path="courts/*"
54-
element={
55-
<Suspense fallback={<Loader width={"48px"} height={"48px"} />}>
56-
<Courts />
57-
</Suspense>
58-
}
59-
/>
60-
<Route
61-
path="dashboard/:page/:order/:filter"
62-
element={
63-
<Suspense fallback={<Loader width={"48px"} height={"48px"} />}>
64-
<Dashboard />
65-
</Suspense>
66-
}
67-
/>
68-
<Route
69-
path="dispute-template"
70-
element={
71-
<Suspense fallback={<Loader width={"48px"} height={"48px"} />}>
72-
<DisputeTemplateView />
73-
</Suspense>
74-
}
75-
/>
76-
<Route
77-
path="resolver/*"
78-
element={
79-
<Suspense fallback={<Loader width={"48px"} height={"48px"} />}>
80-
<DisputeResolver />
81-
</Suspense>
82-
}
83-
/>
84-
<Route
85-
path="get-pnk/*"
86-
element={
87-
<Suspense fallback={<Loader width={"48px"} height={"48px"} />}>
88-
<GetPnk />
89-
</Suspense>
90-
}
91-
/>
92-
<Route path="*" element={<h1>Justice not found here ¯\_( ͡° ͜ʖ ͡°)_/¯</h1>} />
93-
</Route>
94-
</SentryRoutes>
95-
</NewDisputeProvider>
96-
</IsListProvider>
33+
<AtlasProvider>
34+
<IsListProvider>
35+
<NewDisputeProvider>
36+
<SentryRoutes>
37+
<Route path="/" element={<Layout />}>
38+
<Route
39+
index
40+
element={
41+
<Suspense fallback={<Loader width={"48px"} height={"48px"} />}>
42+
<Home />
43+
</Suspense>
44+
}
45+
/>
46+
<Route
47+
path="cases/*"
48+
element={
49+
<Suspense fallback={<Loader width={"48px"} height={"48px"} />}>
50+
<Cases />
51+
</Suspense>
52+
}
53+
/>
54+
<Route
55+
path="courts/*"
56+
element={
57+
<Suspense fallback={<Loader width={"48px"} height={"48px"} />}>
58+
<Courts />
59+
</Suspense>
60+
}
61+
/>
62+
<Route
63+
path="dashboard/:page/:order/:filter"
64+
element={
65+
<Suspense fallback={<Loader width={"48px"} height={"48px"} />}>
66+
<Dashboard />
67+
</Suspense>
68+
}
69+
/>
70+
<Route
71+
path="dispute-template"
72+
element={
73+
<Suspense fallback={<Loader width={"48px"} height={"48px"} />}>
74+
<DisputeTemplateView />
75+
</Suspense>
76+
}
77+
/>
78+
<Route
79+
path="resolver/*"
80+
element={
81+
<Suspense fallback={<Loader width={"48px"} height={"48px"} />}>
82+
<DisputeResolver />
83+
</Suspense>
84+
}
85+
/>
86+
<Route
87+
path="get-pnk/*"
88+
element={
89+
<Suspense fallback={<Loader width={"48px"} height={"48px"} />}>
90+
<GetPnk />
91+
</Suspense>
92+
}
93+
/>
94+
<Route path="*" element={<h1>Justice not found here ¯\_( ͡° ͜ʖ ͡°)_/¯</h1>} />
95+
</Route>
96+
</SentryRoutes>
97+
</NewDisputeProvider>
98+
</IsListProvider>
99+
</AtlasProvider>
97100
</GraphqlBatcherProvider>
98101
</QueryClientProvider>
99102
</Web3Provider>

web/src/components/EnsureAuth.tsx

+7-77
Original file line numberDiff line numberDiff line change
@@ -1,100 +1,30 @@
1-
import React, { useMemo, useState } from "react";
1+
import React from "react";
22

3-
import * as jwt from "jose";
4-
import { createSiweMessage } from "viem/siwe";
5-
import { useAccount, useChainId, useSignMessage } from "wagmi";
3+
import { useAccount } from "wagmi";
64

75
import { Button } from "@kleros/ui-components-library";
86

9-
import { DEFAULT_CHAIN } from "consts/chains";
10-
import { useSessionStorage } from "hooks/useSessionStorage";
11-
import { authoriseUser, getNonce } from "utils/authoriseUser";
7+
import { useAtlasProvider } from "context/AtlasProvider";
128

139
interface IEnsureAuth {
1410
children: React.ReactElement;
1511
className?: string;
1612
}
1713

1814
const EnsureAuth: React.FC<IEnsureAuth> = ({ children, className }) => {
19-
const localToken = window.sessionStorage.getItem("auth-token");
20-
const [isLoading, setIsLoading] = useState(false);
21-
22-
const [authToken, setAuthToken] = useSessionStorage<string | null>("auth-token", localToken);
2315
const { address } = useAccount();
24-
const chainId = useChainId();
25-
26-
const { signMessageAsync } = useSignMessage();
27-
28-
const isVerified = useMemo(() => {
29-
if (!authToken || !address) return false;
30-
31-
const payload = jwt.decodeJwt(authToken);
32-
33-
if ((payload?.sub as string)?.toLowerCase() !== address.toLowerCase()) return false;
34-
if (payload.exp && payload.exp < Date.now() / 1000) return false;
35-
36-
return true;
37-
}, [authToken, address]);
38-
39-
const handleSignIn = async () => {
40-
try {
41-
setIsLoading(true);
42-
if (!address) return;
43-
44-
const message = await createMessage(address, "Sign In to Kleros with Ethereum.", chainId);
45-
46-
const signature = await signMessageAsync({ message });
47-
48-
if (!signature) return;
49-
50-
authoriseUser({
51-
address,
52-
signature,
53-
message,
54-
})
55-
.then(async (token) => {
56-
setAuthToken(token);
57-
})
58-
.catch((err) => console.log({ err }))
59-
.finally(() => setIsLoading(false));
60-
} catch (err) {
61-
setIsLoading(false);
62-
console.log({ err });
63-
}
64-
};
65-
16+
const { isVerified, isSigningIn, authoriseUser } = useAtlasProvider();
6617
return isVerified ? (
6718
children
6819
) : (
6920
<Button
7021
text="Sign In"
71-
onClick={handleSignIn}
72-
disabled={isLoading || !address}
73-
isLoading={isLoading}
22+
onClick={authoriseUser}
23+
disabled={isSigningIn || !address}
24+
isLoading={isSigningIn}
7425
{...{ className }}
7526
/>
7627
);
7728
};
7829

79-
async function createMessage(address: `0x${string}`, statement: string, chainId: number = DEFAULT_CHAIN) {
80-
const domain = window.location.host;
81-
const origin = window.location.origin;
82-
const nonce = await getNonce(address);
83-
84-
// signature is valid only for 10 mins
85-
const expirationTime = new Date(Date.now() + 10 * 60 * 1000);
86-
87-
const message = createSiweMessage({
88-
domain,
89-
address,
90-
statement,
91-
uri: origin,
92-
version: "1",
93-
chainId,
94-
nonce,
95-
expirationTime,
96-
});
97-
return message;
98-
}
99-
10030
export default EnsureAuth;

web/src/context/AtlasProvider.tsx

+118
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
import React, { useMemo, createContext, useContext, useState, useCallback, useEffect } from "react";
2+
3+
import { GraphQLClient } from "graphql-request";
4+
import { decodeJwt } from "jose";
5+
import { useAccount, useChainId, useSignMessage } from "wagmi";
6+
7+
import { useSessionStorage } from "hooks/useSessionStorage";
8+
import { createMessage, getNonce, loginUser } from "utils/atlas";
9+
10+
import { isUndefined } from "src/utils";
11+
12+
interface IAtlasProvider {
13+
isVerified: boolean;
14+
isSigningIn: boolean;
15+
16+
authoriseUser: () => void;
17+
}
18+
19+
const Context = createContext<IAtlasProvider | undefined>(undefined);
20+
21+
// eslint-disable-next-line
22+
// @ts-ignore
23+
const atlasUri = import.meta.env.REACT_APP_ATLAS_URI ?? "";
24+
25+
const AtlasProvider: React.FC<{ children?: React.ReactNode }> = ({ children }) => {
26+
const { address } = useAccount();
27+
const chainId = useChainId();
28+
const [authToken, setAuthToken] = useSessionStorage<string | undefined>("authToken", undefined);
29+
const [isSigningIn, setIsSigningIn] = useState(false);
30+
const [isVerified, setIsVerified] = useState(false);
31+
const { signMessageAsync } = useSignMessage();
32+
33+
const atlasGqlClient = useMemo(() => {
34+
const headers = authToken
35+
? {
36+
authorization: authToken,
37+
}
38+
: undefined;
39+
return new GraphQLClient(atlasUri, { headers });
40+
}, [authToken]);
41+
42+
/**
43+
* @description verifies user authorisation
44+
* @returns boolean - true if user is authorized
45+
*/
46+
const verifySession = useCallback(() => {
47+
try {
48+
if (!authToken || !address) return false;
49+
50+
const payload = decodeJwt(authToken);
51+
52+
if ((payload?.sub as string)?.toLowerCase() !== address.toLowerCase()) return false;
53+
if (payload.exp && payload.exp < Date.now() / 1000) return false;
54+
55+
return true;
56+
} catch {
57+
return false;
58+
}
59+
}, [authToken, address]);
60+
61+
useEffect(() => {
62+
// initial verfiy check
63+
setIsVerified(verifySession());
64+
65+
// verify session every 5 sec
66+
const intervalId = setInterval(() => {
67+
setIsVerified(verifySession());
68+
}, 5000);
69+
70+
return () => {
71+
clearInterval(intervalId);
72+
};
73+
}, [authToken, verifySession]);
74+
75+
/**
76+
* @description authorise user and enable authorised calls
77+
*/
78+
const authoriseUser = useCallback(() => {
79+
if (!address) return;
80+
setIsSigningIn(true);
81+
getNonce(atlasGqlClient, address)
82+
.then((nonce) => {
83+
const message = createMessage(address, chainId, nonce);
84+
signMessageAsync({ message }).then((signature) => {
85+
if (!isUndefined(signature)) {
86+
loginUser(atlasGqlClient, { signature, message })
87+
.then((token) => {
88+
setAuthToken(token);
89+
})
90+
.finally(() => setIsSigningIn(false));
91+
}
92+
});
93+
})
94+
.catch((err) => {
95+
// eslint-disable-next-line no-console
96+
console.log(`authorise user error : ${err?.message}`);
97+
setIsSigningIn(false);
98+
});
99+
}, [address, chainId, setAuthToken, signMessageAsync, atlasGqlClient]);
100+
101+
return (
102+
<Context.Provider
103+
value={useMemo(() => ({ isVerified, isSigningIn, authoriseUser }), [isVerified, isSigningIn, authoriseUser])}
104+
>
105+
{children}
106+
</Context.Provider>
107+
);
108+
};
109+
110+
export const useAtlasProvider = () => {
111+
const context = useContext(Context);
112+
if (!context) {
113+
throw new Error("Context Provider not found.");
114+
}
115+
return context;
116+
};
117+
118+
export default AtlasProvider;

web/src/utils/atlas/createMessage.ts

+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { createSiweMessage } from "viem/siwe";
2+
3+
import { DEFAULT_CHAIN } from "consts/chains";
4+
5+
export const createMessage = (address: `0x${string}`, chainId: number = DEFAULT_CHAIN, nonce: string) => {
6+
const domain = window.location.host;
7+
const origin = window.location.origin;
8+
9+
// signature is valid only for 10 mins
10+
const expirationTime = new Date(Date.now() + 10 * 60 * 1000);
11+
12+
const message = createSiweMessage({
13+
domain,
14+
address,
15+
statement: "Sign In to Kleros with Ethereum.",
16+
uri: origin,
17+
version: "1",
18+
chainId,
19+
nonce,
20+
expirationTime,
21+
});
22+
return message;
23+
};

0 commit comments

Comments
 (0)