Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor(web): optimize-session-invalidation #1727

Merged
merged 7 commits into from
Oct 29, 2024
Merged
53 changes: 41 additions & 12 deletions web/src/context/AtlasProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,11 @@ import {
type ConfirmEmailResponse,
Roles,
Products,
AuthorizationError,
} from "utils/atlas";

import { isUndefined } from "src/utils";
import { GraphQLError } from "graphql";

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

useEffect(() => {
// initial verfiy check
setIsVerified(verifySession());
let timeoutId: ReturnType<typeof setTimeout>;

const verifyAndSchedule = () => {
// initial verify check
const isValid = verifySession();
setIsVerified(isValid);

if (isValid && authToken) {
try {
const payload = decodeJwt(authToken);
const expiresIn = (payload.exp as number) * 1000 - Date.now();

timeoutId = setTimeout(verifyAndSchedule, Math.max(0, expiresIn));
} catch (err) {
console.error("Error decoding JWT:", err);
setIsVerified(false);
}
}
};

// verify session every 5 sec
const intervalId = setInterval(() => {
setIsVerified(verifySession());
}, 5000);
verifyAndSchedule();

return () => {
clearInterval(intervalId);
clearTimeout(timeoutId);
};
}, [authToken, verifySession, address]);

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

async function fetchWithAuthErrorHandling<T>(request: () => Promise<T>): Promise<T> {
try {
return await request();
} catch (error) {
if (
error instanceof AuthorizationError ||
(error instanceof GraphQLError && error?.extensions?.["code"] === "UNAUTHENTICATED")
) {
setIsVerified(false);
}
throw error;
}
}

/**
* @description authorise user and enable authorised calls
*/
Expand Down Expand Up @@ -173,7 +203,7 @@ const AtlasProvider: React.FC<{ children?: React.ReactNode }> = ({ children }) =
if (!address || !isVerified) return false;
setIsAddingUser(true);

const userAdded = await addUserToAtlas(atlasGqlClient, userSettings);
const userAdded = await fetchWithAuthErrorHandling(() => addUserToAtlas(atlasGqlClient, userSettings));
refetchUser();

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

const emailUpdated = await updateEmailInAtlas(atlasGqlClient, userSettings);
const emailUpdated = await fetchWithAuthErrorHandling(() => updateEmailInAtlas(atlasGqlClient, userSettings));
refetchUser();

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

const hash = await uploadToIpfs(
{ baseUrl: atlasUri, authToken },
{ file, name: file.name, role, product: Products.CourtV2 }
const hash = await fetchWithAuthErrorHandling(() =>
uploadToIpfs({ baseUrl: atlasUri, authToken }, { file, name: file.name, role, product: Products.CourtV2 })
);
return hash ? `/ipfs/${hash}` : null;
} catch (err: any) {
Expand Down
11 changes: 7 additions & 4 deletions web/src/utils/atlas/addUser.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { GraphQLError } from "graphql";
import { gql, type GraphQLClient } from "graphql-request";
import { toast } from "react-toastify";

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

const errorMessage = Array.isArray(errors?.response?.errors)
? errors.response.errors[0]?.message
: "Unknown error";
throw new Error(errorMessage);
const error = errors?.response?.errors?.[0];

if (error) {
throw new GraphQLError(error?.message, { ...error });
}
throw new Error("Unknown Error");
}),
{
pending: `Adding User ...`,
Expand Down
11 changes: 7 additions & 4 deletions web/src/utils/atlas/updateEmail.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { GraphQLError } from "graphql";
import { gql, type GraphQLClient } from "graphql-request";
import { toast } from "react-toastify";

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

const errorMessage = Array.isArray(errors?.response?.errors)
? errors.response.errors[0]?.message
: "Unknown error";
throw new Error(errorMessage);
const error = errors?.response?.errors?.[0];

if (error) {
throw new GraphQLError(error?.message, { ...error });
}
throw new Error("Unknown Error");
}),
{
pending: `Updating Email ...`,
Expand Down
13 changes: 13 additions & 0 deletions web/src/utils/atlas/uploadToIpfs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ export async function uploadToIpfs(config: Config, payload: IpfsUploadPayload):
}).then(async (response) => {
if (!response.ok) {
const error = await response.json().catch(() => ({ message: "Error uploading to IPFS" }));

if (response.status === 401) throw new AuthorizationError(error.message);
throw new Error(error.message);
}

Expand All @@ -59,3 +61,14 @@ export async function uploadToIpfs(config: Config, payload: IpfsUploadPayload):
OPTIONS
);
}

export class AuthorizationError extends Error {
readonly name = "AuthorizationError" as const;
constructor(message: string) {
super(message);

if (Error.captureStackTrace) {
Error.captureStackTrace(this, this.constructor);
}
}
}
Loading