Skip to content

Commit 01673c3

Browse files
authored
Merge pull request #1727 from kleros/refactor/optimize-session-invalidation
refactor(web): optimize-session-invalidation
2 parents d1a6020 + a859958 commit 01673c3

File tree

4 files changed

+68
-20
lines changed

4 files changed

+68
-20
lines changed

web/src/context/AtlasProvider.tsx

+41-12
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,11 @@ import {
2222
type ConfirmEmailResponse,
2323
Roles,
2424
Products,
25+
AuthorizationError,
2526
} from "utils/atlas";
2627

2728
import { isUndefined } from "src/utils";
29+
import { GraphQLError } from "graphql";
2830

2931
interface IAtlasProvider {
3032
isVerified: boolean;
@@ -94,16 +96,30 @@ const AtlasProvider: React.FC<{ children?: React.ReactNode }> = ({ children }) =
9496
}, [authToken, address]);
9597

9698
useEffect(() => {
97-
// initial verfiy check
98-
setIsVerified(verifySession());
99+
let timeoutId: ReturnType<typeof setTimeout>;
100+
101+
const verifyAndSchedule = () => {
102+
// initial verify check
103+
const isValid = verifySession();
104+
setIsVerified(isValid);
105+
106+
if (isValid && authToken) {
107+
try {
108+
const payload = decodeJwt(authToken);
109+
const expiresIn = (payload.exp as number) * 1000 - Date.now();
110+
111+
timeoutId = setTimeout(verifyAndSchedule, Math.max(0, expiresIn));
112+
} catch (err) {
113+
console.error("Error decoding JWT:", err);
114+
setIsVerified(false);
115+
}
116+
}
117+
};
99118

100-
// verify session every 5 sec
101-
const intervalId = setInterval(() => {
102-
setIsVerified(verifySession());
103-
}, 5000);
119+
verifyAndSchedule();
104120

105121
return () => {
106-
clearInterval(intervalId);
122+
clearTimeout(timeoutId);
107123
};
108124
}, [authToken, verifySession, address]);
109125

@@ -140,6 +156,20 @@ const AtlasProvider: React.FC<{ children?: React.ReactNode }> = ({ children }) =
140156
return !isUndefined(user.email);
141157
}, [user]);
142158

159+
async function fetchWithAuthErrorHandling<T>(request: () => Promise<T>): Promise<T> {
160+
try {
161+
return await request();
162+
} catch (error) {
163+
if (
164+
error instanceof AuthorizationError ||
165+
(error instanceof GraphQLError && error?.extensions?.["code"] === "UNAUTHENTICATED")
166+
) {
167+
setIsVerified(false);
168+
}
169+
throw error;
170+
}
171+
}
172+
143173
/**
144174
* @description authorise user and enable authorised calls
145175
*/
@@ -173,7 +203,7 @@ const AtlasProvider: React.FC<{ children?: React.ReactNode }> = ({ children }) =
173203
if (!address || !isVerified) return false;
174204
setIsAddingUser(true);
175205

176-
const userAdded = await addUserToAtlas(atlasGqlClient, userSettings);
206+
const userAdded = await fetchWithAuthErrorHandling(() => addUserToAtlas(atlasGqlClient, userSettings));
177207
refetchUser();
178208

179209
return userAdded;
@@ -199,7 +229,7 @@ const AtlasProvider: React.FC<{ children?: React.ReactNode }> = ({ children }) =
199229
if (!address || !isVerified) return false;
200230
setIsUpdatingUser(true);
201231

202-
const emailUpdated = await updateEmailInAtlas(atlasGqlClient, userSettings);
232+
const emailUpdated = await fetchWithAuthErrorHandling(() => updateEmailInAtlas(atlasGqlClient, userSettings));
203233
refetchUser();
204234

205235
return emailUpdated;
@@ -227,9 +257,8 @@ const AtlasProvider: React.FC<{ children?: React.ReactNode }> = ({ children }) =
227257
if (!address || !isVerified || !atlasUri || !authToken) return null;
228258
setIsUploadingFile(true);
229259

230-
const hash = await uploadToIpfs(
231-
{ baseUrl: atlasUri, authToken },
232-
{ file, name: file.name, role, product: Products.CourtV2 }
260+
const hash = await fetchWithAuthErrorHandling(() =>
261+
uploadToIpfs({ baseUrl: atlasUri, authToken }, { file, name: file.name, role, product: Products.CourtV2 })
233262
);
234263
return hash ? `/ipfs/${hash}` : null;
235264
} catch (err: any) {

web/src/utils/atlas/addUser.ts

+7-4
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { GraphQLError } from "graphql";
12
import { gql, type GraphQLClient } from "graphql-request";
23
import { toast } from "react-toastify";
34

@@ -30,10 +31,12 @@ export function addUser(client: GraphQLClient, userData: AddUserData): Promise<b
3031
// eslint-disable-next-line no-console
3132
console.log("Add User error:", { errors });
3233

33-
const errorMessage = Array.isArray(errors?.response?.errors)
34-
? errors.response.errors[0]?.message
35-
: "Unknown error";
36-
throw new Error(errorMessage);
34+
const error = errors?.response?.errors?.[0];
35+
36+
if (error) {
37+
throw new GraphQLError(error?.message, { ...error });
38+
}
39+
throw new Error("Unknown Error");
3740
}),
3841
{
3942
pending: `Adding User ...`,

web/src/utils/atlas/updateEmail.ts

+7-4
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { GraphQLError } from "graphql";
12
import { gql, type GraphQLClient } from "graphql-request";
23
import { toast } from "react-toastify";
34

@@ -28,10 +29,12 @@ export function updateEmail(client: GraphQLClient, userData: UpdateEmailData): P
2829
// eslint-disable-next-line no-console
2930
console.log("Update Email error:", { errors });
3031

31-
const errorMessage = Array.isArray(errors?.response?.errors)
32-
? errors.response.errors[0]?.message
33-
: "Unknown error";
34-
throw new Error(errorMessage);
32+
const error = errors?.response?.errors?.[0];
33+
34+
if (error) {
35+
throw new GraphQLError(error?.message, { ...error });
36+
}
37+
throw new Error("Unknown Error");
3538
}),
3639
{
3740
pending: `Updating Email ...`,

web/src/utils/atlas/uploadToIpfs.ts

+13
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@ export async function uploadToIpfs(config: Config, payload: IpfsUploadPayload):
4242
}).then(async (response) => {
4343
if (!response.ok) {
4444
const error = await response.json().catch(() => ({ message: "Error uploading to IPFS" }));
45+
46+
if (response.status === 401) throw new AuthorizationError(error.message);
4547
throw new Error(error.message);
4648
}
4749

@@ -59,3 +61,14 @@ export async function uploadToIpfs(config: Config, payload: IpfsUploadPayload):
5961
OPTIONS
6062
);
6163
}
64+
65+
export class AuthorizationError extends Error {
66+
readonly name = "AuthorizationError" as const;
67+
constructor(message: string) {
68+
super(message);
69+
70+
if (Error.captureStackTrace) {
71+
Error.captureStackTrace(this, this.constructor);
72+
}
73+
}
74+
}

0 commit comments

Comments
 (0)