diff --git a/web/netlify/functions/authUser.ts b/web/netlify/functions/authUser.ts deleted file mode 100644 index 2ab50d7..0000000 --- a/web/netlify/functions/authUser.ts +++ /dev/null @@ -1,116 +0,0 @@ -import middy from "@middy/core"; -import jsonBodyParser from "@middy/http-json-body-parser"; -import { createClient } from "@supabase/supabase-js"; -import * as jwt from "jose"; -import { SiweMessage } from "siwe"; - -import { netlifyUri, netlifyDeployUri, netlifyDeployPrimeUri } from "src/generatedNetlifyInfo.json"; -import { Database } from "src/types/supabase-notification"; -import { ethers } from "ethers"; -import { ETH_SIGNATURE_REGEX, isProductionDeployment, DEFAULT_CHAIN } from "consts/processEnvConst"; - -const authUser = async (event) => { - try { - if (!event.body) { - throw new Error("No body provided"); - } - - const signature = event?.body?.signature; - if (!signature) { - throw new Error("Missing key : signature"); - } - - if (!ETH_SIGNATURE_REGEX.test(signature)) { - throw new Error("Invalid signature"); - } - - const message = event?.body?.message; - if (!message) { - throw new Error("Missing key : message"); - } - - const address = event?.body?.address; - if (!address) { - throw new Error("Missing key : address"); - } - - const siweMessage = new SiweMessage(message); - - if ( - !( - (netlifyUri && netlifyUri === siweMessage.uri) || - (netlifyDeployUri && netlifyDeployUri === siweMessage.uri) || - (netlifyDeployPrimeUri && netlifyDeployPrimeUri === siweMessage.uri) - ) - ) { - console.debug( - `Invalid URI: expected one of [${netlifyUri} ${netlifyDeployUri} ${netlifyDeployPrimeUri}] but got ${siweMessage.uri}` - ); - throw new Error(`Invalid URI`); - } - - if (siweMessage.chainId !== DEFAULT_CHAIN) { - console.debug(`Invalid chain ID: expected ${DEFAULT_CHAIN} but got ${siweMessage.chainId}`); - throw new Error(`Invalid chain ID`); - } - - const lowerCaseAddress = siweMessage.address.toLowerCase(); - if (lowerCaseAddress !== address.toLowerCase()) { - throw new Error("Address mismatch in provided address and message"); - } - - const supabase = createClient(process.env.SUPABASE_URL!, process.env.SUPABASE_CLIENT_API_KEY!); - - // get nonce from db, if its null that means it was already used - const { error: nonceError, data: nonceData } = await supabase - .from("user-nonce") - .select("nonce") - .eq("address", lowerCaseAddress) - .single(); - - if (nonceError || !nonceData?.nonce) { - throw new Error("Unable to fetch nonce from DB"); - } - - try { - // If the main Alchemy API key is permissioned, it won't work in a Netlify Function so we use a dedicated API key - const alchemyApiKey = process.env.ALCHEMY_FUNCTIONS_API_KEY ?? process.env.ALCHEMY_API_KEY; - const alchemyChain = isProductionDeployment() ? "arb-mainnet" : "arb-sepolia"; - const alchemyRpcURL = `https://${alchemyChain}.g.alchemy.com/v2/${alchemyApiKey}`; - const provider = new ethers.providers.JsonRpcProvider(alchemyRpcURL); - await siweMessage.verify({ signature, nonce: nonceData.nonce, time: new Date().toISOString() }, { provider }); - } catch (err) { - throw new Error("Invalid signer: " + JSON.stringify(err)); - } - - const { error } = await supabase.from("user-nonce").delete().match({ address: lowerCaseAddress }); - - if (error) { - throw new Error("Error updating nonce in DB"); - } - - const issuer = process.env.JWT_ISSUER ?? "Kleros"; // ex :- Kleros - const audience = process.env.JWT_AUDIENCE ?? "Curate"; // ex :- Court, Curate, Escrow - const authExp = process.env.JWT_EXP_TIME ?? "2h"; - const secret = process.env.JWT_SECRET; - - if (!secret) { - throw new Error("Secret not set in environment"); - } - // user verified, generate auth token - const encodedSecret = new TextEncoder().encode(secret); - - const token = await new jwt.SignJWT({ id: address.toLowerCase() }) - .setProtectedHeader({ alg: "HS256" }) - .setIssuer(issuer) - .setAudience(audience) - .setExpirationTime(authExp) - .sign(encodedSecret); - - return { statusCode: 200, body: JSON.stringify({ message: "User authorised", token }) }; - } catch (err) { - return { statusCode: 500, body: JSON.stringify({ message: `${err}` }) }; - } -}; - -export const handler = middy(authUser).use(jsonBodyParser()); diff --git a/web/netlify/functions/fetch-settings.ts b/web/netlify/functions/fetch-settings.ts deleted file mode 100644 index 2932399..0000000 --- a/web/netlify/functions/fetch-settings.ts +++ /dev/null @@ -1,39 +0,0 @@ -import middy from "@middy/core"; -import { createClient } from "@supabase/supabase-js"; - -import { Database } from "../../src/types/supabase-notification"; -import { authMiddleware } from "../middleware/authMiddleware"; - -const fetchSettings = async (event) => { - try { - const address = event.auth.id; - const lowerCaseAddress = address.toLowerCase() as `0x${string}`; - - const supabaseUrl = process.env.SUPABASE_URL; - const supabaseApiKey = process.env.SUPABASE_CLIENT_API_KEY; - - if (!supabaseUrl || !supabaseApiKey) { - throw new Error("Supabase URL or API key is undefined"); - } - const supabase = createClient(supabaseUrl, supabaseApiKey); - - const { error, data } = await supabase - .from("user-settings") - .select("address, email, telegram") - .eq("address", lowerCaseAddress) - .single(); - - if (!data) { - return { statusCode: 404, message: "Error : User not found" }; - } - - if (error) { - throw error; - } - return { statusCode: 200, body: JSON.stringify({ data }) }; - } catch (err) { - return { statusCode: 500, message: `Error ${err?.message ?? err}` }; - } -}; - -export const handler = middy(fetchSettings).use(authMiddleware()); diff --git a/web/netlify/functions/getNonce.ts b/web/netlify/functions/getNonce.ts deleted file mode 100644 index 3635ca9..0000000 --- a/web/netlify/functions/getNonce.ts +++ /dev/null @@ -1,56 +0,0 @@ -import middy from "@middy/core"; -import { createClient } from "@supabase/supabase-js"; -import { generateNonce } from "siwe"; - -import { ETH_ADDRESS_REGEX } from "consts/processEnvConst"; - -import { Database } from "../../src/types/supabase-notification"; - -const getNonce = async (event) => { - try { - const { queryStringParameters } = event; - - if (!queryStringParameters?.address) { - return { - statusCode: 400, - body: JSON.stringify({ message: "Invalid query parameters" }), - }; - } - - const { address } = queryStringParameters; - - if (!ETH_ADDRESS_REGEX.test(address)) { - throw new Error("Invalid Ethereum address format"); - } - - const lowerCaseAddress = address.toLowerCase() as `0x${string}`; - - const supabaseUrl = process.env.SUPABASE_URL; - const supabaseApiKey = process.env.SUPABASE_CLIENT_API_KEY; - - if (!supabaseUrl || !supabaseApiKey) { - throw new Error("Supabase URL or API key is undefined"); - } - const supabase = createClient(supabaseUrl, supabaseApiKey); - - // generate nonce and save in db - const nonce = generateNonce(); - - const { error } = await supabase - .from("user-nonce") - .upsert({ address: lowerCaseAddress, nonce: nonce }) - .eq("address", lowerCaseAddress); - - if (error) { - throw error; - } - - return { statusCode: 200, body: JSON.stringify({ nonce }) }; - } catch (err) { - console.log(err); - - return { statusCode: 500, message: `Error ${err?.message ?? err}` }; - } -}; - -export const handler = middy(getNonce); diff --git a/web/netlify/functions/update-settings.ts b/web/netlify/functions/update-settings.ts deleted file mode 100644 index fc75ff9..0000000 --- a/web/netlify/functions/update-settings.ts +++ /dev/null @@ -1,97 +0,0 @@ -import middy from "@middy/core"; -import jsonBodyParser from "@middy/http-json-body-parser"; -import { createClient } from "@supabase/supabase-js"; - -import { EMAIL_REGEX, TELEGRAM_REGEX, ETH_ADDRESS_REGEX } from "consts/processEnvConst"; -import { Database } from "../../src/types/supabase-notification"; -import { authMiddleware } from "../middleware/authMiddleware"; - -type NotificationSettings = { - email?: string; - telegram?: string; - address: `0x${string}`; -}; - -const validate = (input: any): NotificationSettings => { - const requiredKeys: (keyof NotificationSettings)[] = ["address"]; - const optionalKeys: (keyof NotificationSettings)[] = ["email", "telegram"]; - const receivedKeys = Object.keys(input); - - for (const key of requiredKeys) { - if (!receivedKeys.includes(key)) { - throw new Error(`Missing key: ${key}`); - } - } - - const allExpectedKeys = [...requiredKeys, ...optionalKeys]; - for (const key of receivedKeys) { - if (!allExpectedKeys.includes(key as keyof NotificationSettings)) { - throw new Error(`Unexpected key: ${key}`); - } - } - - const email = input.email ? input.email.trim() : ""; - if (email && !EMAIL_REGEX.test(email)) { - throw new Error("Invalid email format"); - } - - const telegram = input.telegram ? input.telegram.trim() : ""; - if (telegram && !TELEGRAM_REGEX.test(telegram)) { - throw new Error("Invalid Telegram username format"); - } - - if (!ETH_ADDRESS_REGEX.test(input.address)) { - throw new Error("Invalid Ethereum address format"); - } - - return { - email: input.email?.trim(), - telegram: input.telegram?.trim(), - address: input.address.trim().toLowerCase(), - }; -}; - -const updateSettings = async (event) => { - try { - if (!event.body) { - throw new Error("No body provided"); - } - - const { email, telegram, address } = validate(event.body); - const lowerCaseAddress = address.toLowerCase() as `0x${string}`; - - // Prevent using someone else's token - if (event?.auth?.id.toLowerCase() !== lowerCaseAddress) { - throw new Error("Unauthorised user"); - } - - const supabaseUrl = process.env.SUPABASE_URL; - const supabaseApiKey = process.env.SUPABASE_CLIENT_API_KEY; - - if (!supabaseUrl || !supabaseApiKey) { - throw new Error("Supabase URL or API key is undefined"); - } - const supabase = createClient(supabaseUrl, supabaseApiKey); - - // If the message is empty, delete the user record - if (email === "" && telegram === "") { - const { error } = await supabase.from("user-settings").delete().match({ address: lowerCaseAddress }); - if (error) throw error; - return { statusCode: 200, body: JSON.stringify({ message: "Record deleted successfully." }) }; - } - - // For a user matching this address, upsert the user record - const { error } = await supabase - .from("user-settings") - .upsert({ address: lowerCaseAddress, email: email, telegram: telegram }) - .match({ address: lowerCaseAddress }); - if (error) { - throw error; - } - return { statusCode: 200, body: JSON.stringify({ message: "Record updated successfully." }) }; - } catch (err) { - return { statusCode: 500, body: JSON.stringify({ message: `${err}` }) }; - } -}; - -export const handler = middy(updateSettings).use(jsonBodyParser()).use(authMiddleware()); diff --git a/web/netlify/functions/uploadToIPFS.ts b/web/netlify/functions/uploadToIPFS.ts deleted file mode 100644 index a5f2630..0000000 --- a/web/netlify/functions/uploadToIPFS.ts +++ /dev/null @@ -1,100 +0,0 @@ -import { File, FilebaseClient } from "@filebase/client"; -import amqp, { Connection } from "amqplib"; -import busboy from "busboy"; -import middy from "@middy/core"; -import { authMiddleware } from "../middleware/authMiddleware"; - -const { FILEBASE_TOKEN, RABBITMQ_URL, FILEBASE_API_WRAPPER } = process.env; -const filebase = new FilebaseClient({ token: FILEBASE_TOKEN ?? "" }); - -type FormElement = - | { isFile: true; filename: string; mimeType: string; content: Buffer } - | { isFile: false; content: string }; -type FormData = { [key: string]: FormElement }; - -const emitRabbitMQLog = async (cid: string, operation: string) => { - let connection: Connection | undefined; - try { - connection = await amqp.connect(RABBITMQ_URL ?? ""); - const channel = await connection.createChannel(); - - await channel.assertExchange("ipfs", "topic"); - channel.publish("ipfs", operation, Buffer.from(cid)); - - //eslint-disable-next-line no-console - console.log(`Sent IPFS CID '${cid}' to exchange 'ipfs'`); - } catch (err) { - console.warn(err); - } finally { - if (typeof connection !== "undefined") await connection.close(); - } -}; - -const parseMultipart = ({ headers, body, isBase64Encoded }) => - new Promise((resolve, reject) => { - const fields: FormData = {}; - - const bb = busboy({ headers }); - - bb.on("file", (name, file, { filename, mimeType }) => - file.on("data", (content) => { - fields[name] = { isFile: true, filename, mimeType, content }; - }) - ) - .on("field", (name, value) => { - if (value) fields[name] = { isFile: false, content: value }; - }) - .on("close", () => resolve(fields)) - .on("error", (err) => reject(err)); - - bb.write(body, isBase64Encoded ? "base64" : "binary"); - bb.end(); - }); - -const pinToFilebase = async (data: FormData, operation: string): Promise> => { - const cids = new Array(); - for (const [_, dataElement] of Object.entries(data)) { - if (dataElement.isFile) { - const { filename, mimeType, content } = dataElement; - const path = `${filename}`; - const cid = await filebase.storeDirectory([new File([content], path, { type: mimeType })]); - await emitRabbitMQLog(cid, operation); - cids.push(`/ipfs/${cid}/${path}`); - } - } - - return cids; -}; - -export const uploadToIpfs = async (event) => { - const { queryStringParameters } = event; - - if (!queryStringParameters?.operation) { - return { - statusCode: 400, - body: JSON.stringify({ message: "Invalid query parameters, missing query : operation " }), - }; - } - - const { operation } = queryStringParameters; - - try { - const parsed = await parseMultipart(event); - const cids = await pinToFilebase(parsed, operation); - - return { - statusCode: 200, - body: JSON.stringify({ - message: "File has been stored successfully", - cids, - }), - }; - } catch (err: any) { - return { - statusCode: 500, - body: JSON.stringify({ message: err.message }), - }; - } -}; - -export const handler = middy(uploadToIpfs).use(authMiddleware()); diff --git a/web/netlify/middleware/authMiddleware.ts b/web/netlify/middleware/authMiddleware.ts deleted file mode 100644 index 31c3c56..0000000 --- a/web/netlify/middleware/authMiddleware.ts +++ /dev/null @@ -1,39 +0,0 @@ -import * as jwt from "jose"; - -export const authMiddleware = () => { - return { - before: async (request) => { - const { event } = request; - - const authToken = event?.headers?.["x-auth-token"]; - if (!authToken) { - return { - statusCode: 400, - body: JSON.stringify({ message: "Error : Missing x-auth-token in Header" }), - }; - } - - try { - const issuer = process.env.JWT_ISSUER ?? "Kleros"; // ex :- Kleros - const audience = process.env.JWT_AUDIENCE ?? "Curate"; // ex :- Court, Curate, Escrow - const secret = process.env.JWT_SECRET; - - if (!secret) { - throw new Error("Secret not set in environment"); - } - - const encodedSecret = new TextEncoder().encode(secret); - - const { payload } = await jwt.jwtVerify(authToken, encodedSecret, { issuer, audience }); - - // add auth details to event - request.event.auth = payload; - } catch (err) { - return { - statusCode: 401, - body: JSON.stringify({ message: `Error : ${err?.message ?? "Not Authorised"}` }), - }; - } - }, - }; -}; diff --git a/web/package.json b/web/package.json index 847ca1d..bce5183 100644 --- a/web/package.json +++ b/web/package.json @@ -40,17 +40,13 @@ "check-types": "tsc --noEmit", "generate": "yarn generate:gql && yarn generate:hooks", "generate:gql": "graphql-codegen --require tsconfig-paths/register", - "generate:hooks": "NODE_NO_WARNINGS=1 wagmi generate", - "generate:supabase": "scripts/generateSupabaseTypes.sh" + "generate:hooks": "NODE_NO_WARNINGS=1 wagmi generate" }, "prettier": "@kleros/curate-v2-prettier-config", "devDependencies": { "@graphql-codegen/cli": "^4.0.1", "@graphql-codegen/client-preset": "^4.2.0", "@kleros/kleros-v2-contracts": "^0.3.2", - "@netlify/functions": "^1.6.0", - "@types/amqplib": "^0.10.4", - "@types/busboy": "^1.5.3", "@types/react": "^18.2.59", "@types/react-dom": "^18.2.19", "@types/react-modal": "^3.16.3", @@ -64,7 +60,6 @@ "eslint-plugin-react": "^7.33.2", "eslint-plugin-react-hooks": "^4.6.0", "lru-cache": "^7.18.3", - "supabase": "^1.133.3", "typescript": "^5.3.3", "vite": "^5.4.2", "vite-plugin-node-polyfills": "^0.22.0", @@ -73,20 +68,15 @@ }, "dependencies": { "@cyntler/react-doc-viewer": "^1.17.0", - "@filebase/client": "^0.0.5", "@kleros/curate-v2-templates": "workspace:^", "@kleros/ui-components-library": "^2.15.0", - "@middy/core": "^5.3.5", - "@middy/http-json-body-parser": "^5.3.5", "@sentry/react": "^7.93.0", "@sentry/tracing": "^7.93.0", - "@supabase/supabase-js": "^2.39.3", "@tanstack/react-query": "^5.40.1", "@web3modal/ethereum": "^2.7.1", "@web3modal/react": "^2.2.2", "@web3modal/wagmi": "^5.1.6", "@yornaath/batshit": "^0.9.0", - "amqplib": "^0.10.3", "chart.js": "^3.9.1", "chartjs-adapter-moment": "^1.0.1", "core-js": "^3.35.0", @@ -111,7 +101,6 @@ "react-scripts": "^5.0.1", "react-toastify": "^9.1.3", "react-use": "^17.4.3", - "siwe": "^2.3.2", "styled-components": "^5.3.11", "subgraph-status": "^1.2.3", "viem": "^2.21.2", diff --git a/web/scripts/generateSupabaseTypes.sh b/web/scripts/generateSupabaseTypes.sh deleted file mode 100755 index 1b63c8d..0000000 --- a/web/scripts/generateSupabaseTypes.sh +++ /dev/null @@ -1,10 +0,0 @@ -#!/usr/bin/env bash - -SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" - -mkdir -p $SCRIPT_DIR/../src/types - -# Docs: https://supabase.com/docs/guides/api/rest/generating-types#generating-types-using-supabase-cli -supabase=$SCRIPT_DIR/../../node_modules/supabase/bin/supabase -$supabase gen types typescript --project-id elokscrfhzodpgvaixfd --schema public > $SCRIPT_DIR/../src/types/supabase-notification.ts -$supabase gen types typescript --project-id pyyfdntuqbsfquzzatkz --schema public > $SCRIPT_DIR/../src/types/supabase-datalake.ts diff --git a/web/src/app.tsx b/web/src/app.tsx index a261b22..082eb22 100644 --- a/web/src/app.tsx +++ b/web/src/app.tsx @@ -17,6 +17,8 @@ import SubmitList from "./pages/SubmitList"; import { RegistryDetailsProvider } from "context/RegistryDetailsContext"; import { SubmitListProvider } from "./context/SubmitListContext"; import AttachmentDisplay from "./pages/AttachmentDisplay"; +import AtlasProvider from "./context/AtlasProvider"; +import Settings from "./pages/Settings"; const App: React.FC = () => { return ( @@ -24,29 +26,32 @@ const App: React.FC = () => { - - - - - }> - } /> - } /> - - - - } - /> - } /> - } /> - 404 not found} /> - - - - - + + + + + + }> + } /> + } /> + + + + } + /> + } /> + } /> + } /> + 404 not found} /> + + + + + + diff --git a/web/src/assets/svgs/icons/minus-circle.svg b/web/src/assets/svgs/icons/minus-circle.svg new file mode 100644 index 0000000..2d03196 --- /dev/null +++ b/web/src/assets/svgs/icons/minus-circle.svg @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/web/src/assets/svgs/icons/warning-outline.svg b/web/src/assets/svgs/icons/warning-outline.svg new file mode 100644 index 0000000..fc09d8c --- /dev/null +++ b/web/src/assets/svgs/icons/warning-outline.svg @@ -0,0 +1,15 @@ + + + + + + + + + + diff --git a/web/src/components/ActionButton/Modal/Buttons/index.tsx b/web/src/components/ActionButton/Modal/Buttons/index.tsx index 782c1bf..5cdd1d5 100644 --- a/web/src/components/ActionButton/Modal/Buttons/index.tsx +++ b/web/src/components/ActionButton/Modal/Buttons/index.tsx @@ -2,7 +2,7 @@ import React from "react"; import styled from "styled-components"; import { Button } from "@kleros/ui-components-library"; import { EnsureChain } from "components/EnsureChain"; -import { EnsureAuth } from "components/EnsureAuth"; +import EnsureAuth from "components/EnsureAuth"; import ClosedCircleIcon from "components/StyledIcons/ClosedCircleIcon"; import { ErrorButtonMessage } from "./ErrorButtonMessage"; diff --git a/web/src/components/ActionButton/Modal/EvidenceUpload.tsx b/web/src/components/ActionButton/Modal/EvidenceUpload.tsx index bd7bf5c..7e6c502 100644 --- a/web/src/components/ActionButton/Modal/EvidenceUpload.tsx +++ b/web/src/components/ActionButton/Modal/EvidenceUpload.tsx @@ -5,8 +5,8 @@ import { FileUploader, Textarea } from "@kleros/ui-components-library"; import LabeledInput from "components/LabeledInput"; import { responsiveSize } from "styles/responsiveSize"; import { OPTIONS as toastOptions } from "utils/wrapWithToast"; -import { uploadFileToIPFS } from "utils/uploadFileToIPFS"; -import { SUPPORTED_FILE_TYPES } from "src/consts"; +import { useAtlasProvider } from "context/AtlasProvider"; +import { Roles } from "utils/atlas"; const Container = styled.div` width: 100%; @@ -59,7 +59,7 @@ const EvidenceUpload: React.FC = ({ setEvidence, setIsEvidenceU const [title, setTitle] = useState(""); const [description, setDescription] = useState(""); const [fileURI, setFileURI] = useState(""); - + const { uploadFile } = useAtlasProvider(); useEffect(() => { setEvidence({ name: title, @@ -70,18 +70,16 @@ const EvidenceUpload: React.FC = ({ setEvidence, setIsEvidenceU }, [title, description, fileURI]); const handleFileUpload = (file: File) => { - if (!SUPPORTED_FILE_TYPES.includes(file?.type)) { - toast.error("File type not supported", toastOptions); - return; - } setIsEvidenceUploading(true); - uploadFileToIPFS(file) - .then(async (res) => { - const response = await res.json(); - const fileURI = response["cids"][0]; + uploadFile(file, Roles.Evidence) + .then(async (fileURI) => { + if (!fileURI) throw new Error("Error uploading file to IPFS"); setFileURI(fileURI); }) - .catch((err) => console.log(err)) + .catch((err) => { + console.log(err); + toast.error(err?.message, toastOptions); + }) .finally(() => setIsEvidenceUploading(false)); }; diff --git a/web/src/components/EnsureAuth.tsx b/web/src/components/EnsureAuth.tsx index bf45d4e..3414bdf 100644 --- a/web/src/components/EnsureAuth.tsx +++ b/web/src/components/EnsureAuth.tsx @@ -1,93 +1,30 @@ -import React, { useMemo, useState } from "react"; +import React from "react"; -import * as jwt from "jose"; -import { SiweMessage } from "siwe"; -import { useAccount, useSignMessage } from "wagmi"; +import { useAccount } from "wagmi"; import { Button } from "@kleros/ui-components-library"; -import { DEFAULT_CHAIN } from "consts/chains"; -import { useSessionStorage } from "hooks/useSessionStorage"; -import { authoriseUser, getNonce } from "utils/authoriseUser"; +import { useAtlasProvider } from "context/AtlasProvider"; interface IEnsureAuth { children: React.ReactElement; className?: string; } -export const EnsureAuth: React.FC = ({ children, className }) => { - const localToken = window.sessionStorage.getItem("auth-token"); - const [isLoading, setIsLoading] = useState(false); - - const [authToken, setAuthToken] = useSessionStorage("auth-token", localToken); - const { address, chain } = useAccount(); - - const { signMessageAsync } = useSignMessage(); - - const isVerified = useMemo(() => { - if (!authToken || !address) return false; - - const payload = jwt.decodeJwt(authToken); - - if ((payload?.id as string).toLowerCase() !== address.toLowerCase()) return false; - if (payload.exp && payload.exp < Date.now() / 1000) return false; - - return true; - }, [authToken, address]); - - const handleSignIn = async () => { - try { - setIsLoading(true); - if (!address) return; - - const message = await createSiweMessage(address, "Sign In to Kleros with Ethereum.", chain?.id); - - const signature = await signMessageAsync({ message }); - - if (!signature) return; - - authoriseUser({ - address, - signature, - message, - }) - .then(async (res) => { - const response = await res.json(); - setAuthToken(response["token"]); - }) - .catch((err) => console.log({ err })) - .finally(() => setIsLoading(false)); - } catch (err) { - setIsLoading(false); - console.log({ err }); - } - }; - +const EnsureAuth: React.FC = ({ children, className }) => { + const { address } = useAccount(); + const { isVerified, isSigningIn, authoriseUser } = useAtlasProvider(); return isVerified ? ( children ) : ( -