From 7830c54c27e155a4ee40f3f0694a2f97573d67fe Mon Sep 17 00:00:00 2001 From: jipstavenuiter Date: Wed, 18 Oct 2023 12:05:56 +0200 Subject: [PATCH 01/39] update usage of hypercertclient to use a single client in the entire app --- components/providers.tsx | 94 ++++++++++++++++++-- hooks/registry.ts | 98 ++++++++++++--------- hooks/store.ts | 131 ++++++++++++++------------- lib/hypercert-client.ts | 6 -- package.json | 2 +- pages/api/nonce.ts | 0 yarn.lock | 185 +++++++++++++++++++++++++++++++++++---- 7 files changed, 380 insertions(+), 136 deletions(-) delete mode 100644 lib/hypercert-client.ts create mode 100644 pages/api/nonce.ts diff --git a/components/providers.tsx b/components/providers.tsx index a251495..a5ff55f 100644 --- a/components/providers.tsx +++ b/components/providers.tsx @@ -1,15 +1,30 @@ -import { PropsWithChildren } from "react"; -import { configureChains, createConfig, WagmiConfig } from "wagmi"; +import React, { PropsWithChildren, useEffect, useState } from "react"; +import { + configureChains, + createConfig, + useChainId, + useWalletClient, + WagmiConfig, + WalletClient, +} from "wagmi"; import { goerli } from "viem/chains"; +import { alchemyProvider } from "wagmi/providers/alchemy"; import { publicProvider } from "wagmi/providers/public"; import { getDefaultWallets, RainbowKitProvider } from "@rainbow-me/rainbowkit"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { ChakraProvider } from "@chakra-ui/react"; import { ReactQueryDevtools } from "@tanstack/react-query-devtools"; +import { HypercertClient } from "@hypercerts-org/sdk"; +import { providers } from "ethers"; const { chains, publicClient, webSocketPublicClient } = configureChains( [goerli], - [publicProvider()], + [ + alchemyProvider({ + apiKey: process.env.NEXT_PUBLIC_ALCHEMY_KEY_GOERLI!, + }), + publicProvider(), + ], ); const { connectors } = getDefaultWallets({ @@ -30,16 +45,77 @@ export const Providers = ({ showReactQueryDevtools = true, children, }: PropsWithChildren<{ showReactQueryDevtools?: boolean }>) => { + const [client] = useState(() => { + return new HypercertClient({ + chainId: 5, + nftStorageToken: process.env.NEXT_PUBLIC_NFT_STORAGE_TOKEN!, + }); + }); + return ( - - {children} - {showReactQueryDevtools && ( - - )} - + + + {children} + {showReactQueryDevtools && ( + + )} + + ); }; + +const HypercertClientContext = React.createContext( + undefined, +); + +export const HypercertClientProvider = ({ children }: PropsWithChildren) => { + const chainId = useChainId(); + const { data: walletClient } = useWalletClient(); + + const [client, setClient] = useState(); + + useEffect(() => { + console.log("creating hypercert client", chainId, walletClient); + + const walletClientToSigner = (walletClient: WalletClient) => { + const { account, chain, transport } = walletClient; + const network = { + chainId: chain.id, + name: chain.name, + ensAddress: chain.contracts?.ensRegistry?.address, + }; + const provider = new providers.Web3Provider(transport, network); + const signer = provider.getSigner(account.address); + console.log("signer", signer); + return signer; + }; + + const operator = walletClient + ? walletClientToSigner(walletClient) + : undefined; + + const hypercertClient = new HypercertClient({ + chainId, + nftStorageToken: process.env.NEXT_PUBLIC_NFT_STORAGE_TOKEN!, + operator, + }); + + setClient(hypercertClient); + }, [chainId, walletClient]); + + return ( + + {children} + + ); +}; + +export const useHypercertClient = () => { + const client = React.useContext(HypercertClientContext); + + return client; +}; diff --git a/hooks/registry.ts b/hooks/registry.ts index 8885f93..dfeca13 100644 --- a/hooks/registry.ts +++ b/hooks/registry.ts @@ -4,7 +4,7 @@ import { supabase } from "@/lib/supabase"; import { ClaimToken } from "@hypercerts-org/sdk"; import _ from "lodash"; import { HyperboardEntry } from "@/types/Hyperboard"; -import { client } from "@/lib/hypercert-client"; +import { useHypercertClient } from "@/components/providers"; interface RegistryWithClaims { id: string; @@ -41,51 +41,65 @@ export const useListRegistries = () => { }; export const useRegistryContents = (registryId: string) => { - return useQuery(["registry", registryId], async () => { - return getRegistryWithClaims(registryId).then(async (registry) => { - if (!registry?.data) { - return null; - } + const client = useHypercertClient(); - // Create one big list of all fractions, for all hypercerts in registry - const allFractions = await Promise.all( - registry.data["hyperboard-claims"].map((claim) => { - return client.indexer.fractionsByClaim(claim.hypercert_id); - }), - ); - const fractions = _.chain(allFractions) - .flatMap((res) => res.claimTokens) - .value(); + return useQuery( + ["registry", registryId], + async () => { + return getRegistryWithClaims(registryId).then(async (registry) => { + if (!registry?.data) { + return null; + } - // Get display data for all owners and convert to dictionary - const ownerAddresses = _.uniq(fractions.map((x) => x.owner)) as string[]; - const claimDisplayDataResponse = - await getEntryDisplayData(ownerAddresses); - const claimDisplayData = _.keyBy( - claimDisplayDataResponse?.data || [], - (x) => x.address.toLowerCase(), - ); + if (!client) { + return null; + } - // Group by owner, merge with display data and calculate total value of all fractions per owner - const content = _.chain(fractions) - .groupBy((fraction) => fraction.owner) - .mapValues((fractionsPerOwner, owner) => { - return { - fractions: fractionsPerOwner, - displayData: claimDisplayData[owner], - totalValue: _.sum( - fractionsPerOwner.map((x) => parseInt(x.units, 10)), - ), - }; - }) - .value(); + // Create one big list of all fractions, for all hypercerts in registry + const allFractions = await Promise.all( + registry.data["hyperboard-claims"].map((claim) => { + return client.indexer.fractionsByClaim(claim.hypercert_id); + }), + ); + const fractions = _.chain(allFractions) + .flatMap((res) => res.claimTokens) + .value(); - return { - registry: registry.data, - content, - }; - }); - }); + // Get display data for all owners and convert to dictionary + const ownerAddresses = _.uniq( + fractions.map((x) => x.owner), + ) as string[]; + const claimDisplayDataResponse = + await getEntryDisplayData(ownerAddresses); + const claimDisplayData = _.keyBy( + claimDisplayDataResponse?.data || [], + (x) => x.address.toLowerCase(), + ); + + // Group by owner, merge with display data and calculate total value of all fractions per owner + const content = _.chain(fractions) + .groupBy((fraction) => fraction.owner) + .mapValues((fractionsPerOwner, owner) => { + return { + fractions: fractionsPerOwner, + displayData: claimDisplayData[owner], + totalValue: _.sum( + fractionsPerOwner.map((x) => parseInt(x.units, 10)), + ), + }; + }) + .value(); + + return { + registry: registry.data, + content, + }; + }); + }, + { + enabled: !!registryId && !!client, + }, + ); }; const getRegistryWithClaims = async (registryId: string) => diff --git a/hooks/store.ts b/hooks/store.ts index 70b3c25..7e87607 100644 --- a/hooks/store.ts +++ b/hooks/store.ts @@ -1,11 +1,11 @@ import { supabase } from "@/lib/supabase"; import { useQuery } from "@tanstack/react-query"; -import { client } from "@/lib/hypercert-client"; import { Claim } from "@hypercerts-org/sdk"; import { createPublicClient, getContract, http } from "viem"; import { goerli } from "viem/chains"; import { TRADER_CONTRACT } from "@/config"; import IHypercertTrader from "@/abi/IHypercertTrader.json"; +import { useHypercertClient } from "@/components/providers"; export interface OfferFromContract { id: string; @@ -74,69 +74,80 @@ const offersQuery = ` `; export const useStoreHypercerts = () => { - return useQuery(["store-hypercerts"], async () => { - const offers = await fetch( - "https://api.thegraph.com/subgraphs/name/hypercerts-admin/hypercerts-testnet", - { - method: "POST", - body: JSON.stringify({ - query: offersQuery, - }), - }, - ) - .then((res) => res.json()) - .then((res) => res.data.offers as Offer[]); + const client = useHypercertClient(); - const offersFromContract = await getOfferPrices( - offers.map((offer) => parseInt(offer.id.split("-")[1], 10)), - ); + return useQuery( + ["store-hypercerts"], + async () => { + if (!client) { + return null; + } + const offers = await fetch( + "https://api.thegraph.com/subgraphs/name/hypercerts-admin/hypercerts-testnet", + { + method: "POST", + body: JSON.stringify({ + query: offersQuery, + }), + }, + ) + .then((res) => res.json()) + .then((res) => res.data.offers as Offer[]); - return supabase - .from("hypercerts-store") - .select("*") - .neq("hidden", true) - .then(async (res) => { - if (!res.data) { - return; - } + const offersFromContract = await getOfferPrices( + offers.map((offer) => parseInt(offer.id.split("-")[1], 10)), + ); - return await Promise.all( - res.data - .filter((x) => x.claimId !== null) - .map(async ({ claimId }) => { - const [claim, fractions] = await Promise.all([ - client.indexer.claimById(claimId!).then(async (res) => { - const metadata = await client.storage.getMetadata( - res.claim?.uri || "", - ); - return { - claim: res.claim, - metadata, - }; - }), - client.indexer - .fractionsByClaim(claimId!) - .then((res) => res.claimTokens), - ]); + return supabase + .from("hypercerts-store") + .select("*") + .neq("hidden", true) + .then(async (res) => { + if (!res.data) { + return; + } - return { - ...claim, - fractions, - offer: offers.find( - (offer) => offer.fractionID.claim.id === claimId, - ), - offerFromContract: offersFromContract.find((offer) => - fractions - .map((fraction) => - BigInt((fraction.id as string).split("-")[1]), - ) - .includes(offer.fractionID), - ), - }; - }), - ); - }); - }); + return await Promise.all( + res.data + .filter((x) => x.claimId !== null) + .map(async ({ claimId }) => { + const [claim, fractions] = await Promise.all([ + client.indexer.claimById(claimId!).then(async (res) => { + const metadata = await client.storage.getMetadata( + res.claim?.uri || "", + ); + return { + claim: res.claim, + metadata, + }; + }), + client.indexer + .fractionsByClaim(claimId!) + .then((res) => res.claimTokens), + ]); + + return { + ...claim, + fractions, + offer: offers.find( + (offer) => offer.fractionID.claim.id === claimId, + ), + offerFromContract: offersFromContract.find((offer) => + fractions + .map((fraction) => + BigInt((fraction.id as string).split("-")[1]), + ) + .includes(offer.fractionID), + ), + }; + }), + ); + }); + }, + { + enabled: !!client, + }, + ); }; const getOfferPrices = async (offerIds: number[]) => { diff --git a/lib/hypercert-client.ts b/lib/hypercert-client.ts deleted file mode 100644 index eb08275..0000000 --- a/lib/hypercert-client.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { HypercertClient } from "@hypercerts-org/sdk"; - -export const client = new HypercertClient({ - chainId: 5, - nftStorageToken: process.env.NEXT_PUBLIC_NFT_STORAGE_TOKEN!, -}); diff --git a/package.json b/package.json index dc8b0f3..2b740bc 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,7 @@ "@chakra-ui/react": "^2.8.0", "@emotion/react": "^11.11.1", "@emotion/styled": "^11.11.0", - "@hypercerts-org/sdk": "0.8.8", + "@hypercerts-org/sdk": "^0.5.0", "@plasmicapp/cli": "^0.1.307", "@plasmicapp/loader-nextjs": "^1.0.309", "@plasmicapp/react-web": "^0.2.248", diff --git a/pages/api/nonce.ts b/pages/api/nonce.ts new file mode 100644 index 0000000..e69de29 diff --git a/yarn.lock b/yarn.lock index aa2ae88..8f6dcf2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2903,25 +2903,24 @@ resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz#b520529ec21d8e5945a1851dfd1c32e94e39ff45" integrity sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA== -"@hypercerts-org/contracts@0.8.7": - version "0.8.7" - resolved "https://registry.yarnpkg.com/@hypercerts-org/contracts/-/contracts-0.8.7.tgz#208e19021a9b2b9977689e9fd6c97a2abc268714" - integrity sha512-vs4sC51cZT2t5/TOQh57Wqx9oofRFX3tLkAQbQ6737uqxFIKKZWAb3vgbSEHrch7gSxJMPIht4AxKqI/nbjzUg== +"@hypercerts-org/contracts@0.2.0": + version "0.2.0" + resolved "https://registry.yarnpkg.com/@hypercerts-org/contracts/-/contracts-0.2.0.tgz#9a1eac5083e9c76680aae3a7eb4c82b344776155" + integrity sha512-6pjqfLNy1lH91EFF1y/oUTwI2JrJBTAIz1Bkd9raAu781J8rFaIHpx7p8DIuKgGp3JGeRb8mbbe5YKFsl2dt0A== -"@hypercerts-org/sdk@0.8.8": - version "0.8.8" - resolved "https://registry.yarnpkg.com/@hypercerts-org/sdk/-/sdk-0.8.8.tgz#88d67ae00287723b4c4079e51ac50cffc923491b" - integrity sha512-IvezK/sywmTkG/Ped7UP5Kc+c2dR8GjF6r9OjQkasfFstjgvdJRReAcbCsiQul1V/Yc2SvE4uZfNilURoOn+Fw== +"@hypercerts-org/sdk@^0.5.0": + version "0.5.0" + resolved "https://registry.yarnpkg.com/@hypercerts-org/sdk/-/sdk-0.5.0.tgz#b60a31082d7c33eb288438b876f4c65b4c7e8935" + integrity sha512-Mnn/eUjVl/cDlk8rwNEHTHiaPZnA4J6vD9ClEHumchlgQ/6MR8TJ+5hxCdtjbWn1sudOUHxqLSbSt2oDoyDeLg== dependencies: "@ethereum-attestation-service/eas-sdk" "^0.28.3" - "@ethersproject/abstract-signer" "^5.7.0" "@graphprotocol/client-add-source-name" "^1.0.16" "@graphprotocol/client-cli" "^2.2.15" - "@hypercerts-org/contracts" "0.8.7" + "@hypercerts-org/contracts" "0.2.0" "@openzeppelin/merkle-tree" "^1.0.4" + "@types/jest" "^29.2.5" ajv "^8.11.2" axios "^1.2.2" - dotenv "^16.0.3" ethers "^5.7.2" graphql "^16.6.0" ipfs-core "^0.17.0" @@ -2929,6 +2928,8 @@ loglevel "^1.8.1" mime "^3.0.0" nft.storage "^7.1.1" + ts-jest "^29.0.3" + ts-mocha "^10.0.0" web3.storage "^4.5.5" "@internationalized/date@^3.4.0": @@ -3124,6 +3125,13 @@ dependencies: jest-get-type "^29.6.3" +"@jest/expect-utils@^29.7.0": + version "29.7.0" + resolved "https://registry.yarnpkg.com/@jest/expect-utils/-/expect-utils-29.7.0.tgz#023efe5d26a8a70f21677d0a1afc0f0a44e3a1c6" + integrity sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA== + dependencies: + jest-get-type "^29.6.3" + "@jest/expect@^29.6.4": version "29.6.4" resolved "https://registry.yarnpkg.com/@jest/expect/-/expect-29.6.4.tgz#1d6ae17dc68d906776198389427ab7ce6179dba6" @@ -6120,6 +6128,14 @@ dependencies: "@types/istanbul-lib-report" "*" +"@types/jest@^29.2.5": + version "29.5.6" + resolved "https://registry.yarnpkg.com/@types/jest/-/jest-29.5.6.tgz#f4cf7ef1b5b0bfc1aa744e41b24d9cc52533130b" + integrity sha512-/t9NnzkOpXb4Nfvg17ieHE6EeSjDS2SGSpNYfoLbUAeL/EOueU/RSdOWFpfQTXBEM7BguYW1XQ0EbM+6RlIh6w== + dependencies: + expect "^29.0.0" + pretty-format "^29.0.0" + "@types/json-schema@^7.0.9": version "7.0.12" resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.12.tgz#d70faba7039d5fca54c83c7dbab41051d2b6f6cb" @@ -7317,7 +7333,7 @@ arraybuffer.prototype.slice@^1.0.1: is-array-buffer "^3.0.2" is-shared-array-buffer "^1.0.2" -arrify@^1.0.1: +arrify@^1.0.0, arrify@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/arrify/-/arrify-1.0.1.tgz#898508da2226f380df904728456849c1501a4b0d" integrity sha512-3CYzex9M9FGQjCGMGyi6/31c8GJbgb0qGyrx5HWxPd0aCwh4cB2YjMb2Xf9UuoogrMrlO9cTqnB5rI5GHZTcUA== @@ -7749,6 +7765,13 @@ browserslist@^4.21.9: node-releases "^2.0.13" update-browserslist-db "^1.0.11" +bs-logger@0.x: + version "0.2.6" + resolved "https://registry.yarnpkg.com/bs-logger/-/bs-logger-0.2.6.tgz#eb7d365307a72cf974cc6cda76b68354ad336bd8" + integrity sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog== + dependencies: + fast-json-stable-stringify "2.x" + bs58@^4.0.0, bs58@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/bs58/-/bs58-4.0.1.tgz#be161e76c354f6f788ae4071f63f34e8c4f0a42a" @@ -7772,7 +7795,7 @@ bser@2.1.1: dependencies: node-int64 "^0.4.0" -buffer-from@^1.0.0: +buffer-from@^1.0.0, buffer-from@^1.1.0: version "1.1.2" resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5" integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ== @@ -8835,6 +8858,11 @@ diff@5.0.0: resolved "https://registry.yarnpkg.com/diff/-/diff-5.0.0.tgz#7ed6ad76d859d030787ec35855f5b1daf31d852b" integrity sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w== +diff@^3.1.0: + version "3.5.0" + resolved "https://registry.yarnpkg.com/diff/-/diff-3.5.0.tgz#800c0dd1e0a8bfbc95835c202ad220fe317e5a12" + integrity sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA== + diff@^4.0.1: version "4.0.2" resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d" @@ -9621,6 +9649,17 @@ exit@^0.1.2: resolved "https://registry.yarnpkg.com/exit/-/exit-0.1.2.tgz#0632638f8d877cc82107d30a0fff1a17cba1cd0c" integrity sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ== +expect@^29.0.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/expect/-/expect-29.7.0.tgz#578874590dcb3214514084c08115d8aee61e11bc" + integrity sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw== + dependencies: + "@jest/expect-utils" "^29.7.0" + jest-get-type "^29.6.3" + jest-matcher-utils "^29.7.0" + jest-message-util "^29.7.0" + jest-util "^29.7.0" + expect@^29.6.4: version "29.6.4" resolved "https://registry.yarnpkg.com/expect/-/expect-29.6.4.tgz#a6e6f66d4613717859b2fe3da98a739437b6f4b8" @@ -9675,7 +9714,7 @@ fast-glob@^3.2.9, fast-glob@^3.3.1: merge2 "^1.3.0" micromatch "^4.0.4" -fast-json-stable-stringify@^2.0.0, fast-json-stable-stringify@^2.1.0: +fast-json-stable-stringify@2.x, fast-json-stable-stringify@^2.0.0, fast-json-stable-stringify@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== @@ -12053,6 +12092,16 @@ jest-diff@^29.6.4: jest-get-type "^29.6.3" pretty-format "^29.6.3" +jest-diff@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-29.7.0.tgz#017934a66ebb7ecf6f205e84699be10afd70458a" + integrity sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw== + dependencies: + chalk "^4.0.0" + diff-sequences "^29.6.3" + jest-get-type "^29.6.3" + pretty-format "^29.7.0" + jest-docblock@^29.6.3: version "29.6.3" resolved "https://registry.yarnpkg.com/jest-docblock/-/jest-docblock-29.6.3.tgz#293dca5188846c9f7c0c2b1bb33e5b11f21645f2" @@ -12125,6 +12174,16 @@ jest-matcher-utils@^29.6.4: jest-get-type "^29.6.3" pretty-format "^29.6.3" +jest-matcher-utils@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz#ae8fec79ff249fd592ce80e3ee474e83a6c44f12" + integrity sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g== + dependencies: + chalk "^4.0.0" + jest-diff "^29.7.0" + jest-get-type "^29.6.3" + pretty-format "^29.7.0" + jest-message-util@^29.6.3: version "29.6.3" resolved "https://registry.yarnpkg.com/jest-message-util/-/jest-message-util-29.6.3.tgz#bce16050d86801b165f20cfde34dc01d3cf85fbf" @@ -12140,6 +12199,21 @@ jest-message-util@^29.6.3: slash "^3.0.0" stack-utils "^2.0.3" +jest-message-util@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-message-util/-/jest-message-util-29.7.0.tgz#8bc392e204e95dfe7564abbe72a404e28e51f7f3" + integrity sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w== + dependencies: + "@babel/code-frame" "^7.12.13" + "@jest/types" "^29.6.3" + "@types/stack-utils" "^2.0.0" + chalk "^4.0.0" + graceful-fs "^4.2.9" + micromatch "^4.0.4" + pretty-format "^29.7.0" + slash "^3.0.0" + stack-utils "^2.0.3" + jest-mock@^29.6.3: version "29.6.3" resolved "https://registry.yarnpkg.com/jest-mock/-/jest-mock-29.6.3.tgz#433f3fd528c8ec5a76860177484940628bdf5e0a" @@ -12263,6 +12337,18 @@ jest-snapshot@^29.6.4: pretty-format "^29.6.3" semver "^7.5.3" +jest-util@^29.0.0, jest-util@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-29.7.0.tgz#23c2b62bfb22be82b44de98055802ff3710fc0bc" + integrity sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA== + dependencies: + "@jest/types" "^29.6.3" + "@types/node" "*" + chalk "^4.0.0" + ci-info "^3.2.0" + graceful-fs "^4.2.9" + picomatch "^2.2.3" + jest-util@^29.6.3: version "29.6.3" resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-29.6.3.tgz#e15c3eac8716440d1ed076f09bc63ace1aebca63" @@ -12778,6 +12864,11 @@ lodash.isequal@4.5.0: resolved "https://registry.yarnpkg.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz#415c4478f2bcc30120c22ce10ed3226f7d3e18e0" integrity sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ== +lodash.memoize@4.x: + version "4.1.2" + resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe" + integrity sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag== + lodash.merge@^4.6.2: version "4.6.2" resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a" @@ -12886,7 +12977,7 @@ make-dir@^4.0.0: dependencies: semver "^7.5.3" -make-error@^1.1.1: +make-error@1.x, make-error@^1.1.1: version "1.3.6" resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2" integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw== @@ -13098,6 +13189,13 @@ minizlib@^2.1.1: minipass "^3.0.0" yallist "^4.0.0" +mkdirp@^0.5.1: + version "0.5.6" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.6.tgz#7def03d2432dcae4ba1d611445c48396062255f6" + integrity sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw== + dependencies: + minimist "^1.2.6" + mkdirp@^1.0.3, mkdirp@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e" @@ -13993,6 +14091,15 @@ prettier@^3.0.3: resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.0.3.tgz#432a51f7ba422d1469096c0fdc28e235db8f9643" integrity sha512-L/4pUDMxcNa8R/EthV08Zt42WBO4h1rarVtK0K+QJG0X187OLo7l699jWw0GKuwzkPQ//jMFA/8Xm6Fh3J/DAg== +pretty-format@^29.0.0, pretty-format@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-29.7.0.tgz#ca42c758310f365bfa71a0bda0a807160b776812" + integrity sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ== + dependencies: + "@jest/schemas" "^29.6.3" + ansi-styles "^5.0.0" + react-is "^18.0.0" + pretty-format@^29.6.3: version "29.6.3" resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-29.6.3.tgz#d432bb4f1ca6f9463410c3fb25a0ba88e594ace7" @@ -15323,7 +15430,7 @@ source-map-support@0.5.13: buffer-from "^1.0.0" source-map "^0.6.0" -source-map-support@^0.5.13: +source-map-support@^0.5.13, source-map-support@^0.5.6: version "0.5.21" resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.21.tgz#04fe7c7f9e1ed2d662233c28cb2b35b9f63f6e4f" integrity sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w== @@ -15858,6 +15965,43 @@ ts-essentials@^7.0.1: resolved "https://registry.yarnpkg.com/ts-essentials/-/ts-essentials-7.0.3.tgz#686fd155a02133eedcc5362dc8b5056cde3e5a38" integrity sha512-8+gr5+lqO3G84KdiTSMRLtuyJ+nTBVRKuCrK4lidMPdVeEp0uqC875uE5NMcaA7YYMN7XsNiFQuMvasF8HT/xQ== +ts-jest@^29.0.3: + version "29.1.1" + resolved "https://registry.yarnpkg.com/ts-jest/-/ts-jest-29.1.1.tgz#f58fe62c63caf7bfcc5cc6472082f79180f0815b" + integrity sha512-D6xjnnbP17cC85nliwGiL+tpoKN0StpgE0TeOjXQTU6MVCfsB4v7aW05CgQ/1OywGb0x/oy9hHFnN+sczTiRaA== + dependencies: + bs-logger "0.x" + fast-json-stable-stringify "2.x" + jest-util "^29.0.0" + json5 "^2.2.3" + lodash.memoize "4.x" + make-error "1.x" + semver "^7.5.3" + yargs-parser "^21.0.1" + +ts-mocha@^10.0.0: + version "10.0.0" + resolved "https://registry.yarnpkg.com/ts-mocha/-/ts-mocha-10.0.0.tgz#41a8d099ac90dbbc64b06976c5025ffaebc53cb9" + integrity sha512-VRfgDO+iiuJFlNB18tzOfypJ21xn2xbuZyDvJvqpTbWgkAgD17ONGr8t+Tl8rcBtOBdjXp5e/Rk+d39f7XBHRw== + dependencies: + ts-node "7.0.1" + optionalDependencies: + tsconfig-paths "^3.5.0" + +ts-node@7.0.1: + version "7.0.1" + resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-7.0.1.tgz#9562dc2d1e6d248d24bc55f773e3f614337d9baf" + integrity sha512-BVwVbPJRspzNh2yfslyT1PSbl5uIk03EZlb493RKHN4qej/D06n1cEhjlOJG69oFsE7OT8XjpTUcYf6pKTLMhw== + dependencies: + arrify "^1.0.0" + buffer-from "^1.1.0" + diff "^3.1.0" + make-error "^1.1.1" + minimist "^1.2.0" + mkdirp "^0.5.1" + source-map-support "^0.5.6" + yn "^2.0.0" + ts-node@^10.9.1: version "10.9.1" resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-10.9.1.tgz#e73de9102958af9e1f0b168a6ff320e25adcff4b" @@ -15877,7 +16021,7 @@ ts-node@^10.9.1: v8-compile-cache-lib "^3.0.1" yn "3.1.1" -tsconfig-paths@^3.14.2: +tsconfig-paths@^3.14.2, tsconfig-paths@^3.5.0: version "3.14.2" resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-3.14.2.tgz#6e32f1f79412decd261f92d633a9dc1cfa99f088" integrity sha512-o/9iXgCYc5L/JxCHPe3Hvh8Q/2xm5Z+p18PESBU6Ff33695QnCHBEjcytY2q19ua7Mbl/DavtBOLq+oG0RCL+g== @@ -16675,7 +16819,7 @@ yargs-parser@^20.2.2, yargs-parser@^20.2.3: resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.9.tgz#2eb7dc3b0289718fc295f362753845c41a0c94ee" integrity sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w== -yargs-parser@^21.1.1: +yargs-parser@^21.0.1, yargs-parser@^21.1.1: version "21.1.1" resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-21.1.1.tgz#9096bceebf990d21bb31fa9516e0ede294a77d35" integrity sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw== @@ -16738,6 +16882,11 @@ yn@3.1.1: resolved "https://registry.yarnpkg.com/yn/-/yn-3.1.1.tgz#1e87401a09d767c1d5eab26a6e4c185182d2eb50" integrity sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q== +yn@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/yn/-/yn-2.0.0.tgz#e5adabc8acf408f6385fc76495684c88e6af689a" + integrity sha512-uTv8J/wiWTgUTg+9vLTi//leUl5vDQS6uii/emeTb2ssY7vl6QWf2fFbIIGjnhjvbdKlU0ed7QPgY1htTC86jQ== + yocto-queue@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" From 0057fdb2dea2077b17b9006f85d0eaf297bcce88 Mon Sep 17 00:00:00 2001 From: jipstavenuiter Date: Wed, 18 Oct 2023 15:31:37 +0200 Subject: [PATCH 02/39] add basic authentication --- components/AdminSignInButton.tsx | 7 + hooks/useLogin.ts | 98 +++++++++++++ lib/supabase.ts | 13 +- package.json | 8 +- pages/{api/nonce.ts => admin.tsx} | 0 pages/api/auth/login.ts | 133 ++++++++++++++++++ pages/api/auth/nonce.ts | 37 +++++ .../20231018133039_add_authentication.sql | 38 +++++ yarn.lock | 87 ++++++++++++ 9 files changed, 417 insertions(+), 4 deletions(-) create mode 100644 components/AdminSignInButton.tsx create mode 100644 hooks/useLogin.ts rename pages/{api/nonce.ts => admin.tsx} (100%) create mode 100644 pages/api/auth/login.ts create mode 100644 pages/api/auth/nonce.ts create mode 100644 supabase/migrations/20231018133039_add_authentication.sql diff --git a/components/AdminSignInButton.tsx b/components/AdminSignInButton.tsx new file mode 100644 index 0000000..d0340d8 --- /dev/null +++ b/components/AdminSignInButton.tsx @@ -0,0 +1,7 @@ +import { Button } from "@chakra-ui/react"; +import { useLogin } from "@/hooks/useLogin"; + +export const AdminSignInButton = () => { + const login = useLogin(); + return ; +}; diff --git a/hooks/useLogin.ts b/hooks/useLogin.ts new file mode 100644 index 0000000..cbab19a --- /dev/null +++ b/hooks/useLogin.ts @@ -0,0 +1,98 @@ +import { useAccount, useSignMessage } from "wagmi"; + +const fetchNonce = async (address: string) => { + const res = await fetch("/api/auth/nonce", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ address }), + }); + const { nonce } = await res.json(); + return nonce; +}; + +export const fetchLogin = async ( + address: string, + signed: string, + nonce: string, +) => { + const res = await fetch("/api/auth/login", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ address, signed, nonce }), + }); + const result = await res.json(); + return result; +}; + +export const readableMessageToSign = "Sign in to Hypercerts"; + +const tokenName = "hyperboards-token"; +const storeToken = (token: string) => { + if (typeof window === "undefined") { + return; + } + + window.localStorage.setItem(tokenName, token); +}; + +export const getToken = () => { + if (typeof window === "undefined") { + return; + } + + return window.localStorage.getItem(tokenName); +}; + +export const useLogin = () => { + const { address } = useAccount(); + const { signMessageAsync } = useSignMessage({ + onSuccess: (signature) => { + console.log("Signature: ", signature); + }, + }); + + return async () => { + if (!address) { + throw new Error("No address found"); + } + + let nonce: string | undefined; + + try { + nonce = await fetchNonce(address); + } catch (e) { + console.error("Error requesting nonce", e); + } + + if (!nonce) { + throw new Error("Nonce not found"); + } + + let signed: string | undefined; + + try { + signed = await signMessageAsync({ + message: readableMessageToSign, + }); + } catch (e) { + console.error("Error signing message", e); + } + + if (!signed) { + throw new Error("Signed message not found"); + } + + try { + const result = await fetchLogin(address, signed, nonce); + console.log("Result", result); + const token = result.token; + storeToken(token); + } catch (e) { + console.error("Error logging in", e); + } + }; +}; diff --git a/lib/supabase.ts b/lib/supabase.ts index de537db..59b39e3 100644 --- a/lib/supabase.ts +++ b/lib/supabase.ts @@ -1,7 +1,18 @@ import { createClient } from "@supabase/supabase-js"; import { Database } from "@/types/database"; +import { getToken } from "@/hooks/useLogin"; export const supabase = createClient( - "https://clagjjfinooizoqdkvqc.supabase.co", + process.env.NEXT_PUBLIC_SUPABASE_URL!, process.env.NEXT_PUBLIC_SUPABASE_PRIVATE_KEY!, ); + +export const getSupabaseAuthenticated = () => { + const token = getToken(); + + if (!token) { + throw new Error("No token found"); + } + + return createClient(process.env.NEXT_PUBLIC_SUPABASE_URL!, token); +}; diff --git a/package.json b/package.json index 2b740bc..34ebfc0 100644 --- a/package.json +++ b/package.json @@ -34,13 +34,11 @@ "@supabase/supabase-js": "^2.33.1", "@tanstack/react-query": "^4.33.0", "@tanstack/react-query-devtools": "^4.33.0", - "@types/node": "20.5.6", - "@types/react": "18.2.21", - "@types/react-dom": "18.2.7", "d3": "^7.8.5", "eslint": "8.48.0", "eslint-config-next": "13.4.19", "framer-motion": "^10.16.2", + "jsonwebtoken": "^9.0.2", "lodash": "^4.17.21", "next": "13.4.19", "react": "18.2.0", @@ -54,6 +52,10 @@ }, "devDependencies": { "@types/d3": "^7.4.0", + "@types/jsonwebtoken": "^9.0.4", + "@types/node": "20.5.6", + "@types/react": "18.2.21", + "@types/react-dom": "18.2.7", "prettier": "^3.0.3" } } diff --git a/pages/api/nonce.ts b/pages/admin.tsx similarity index 100% rename from pages/api/nonce.ts rename to pages/admin.tsx diff --git a/pages/api/auth/login.ts b/pages/api/auth/login.ts new file mode 100644 index 0000000..f6bfe16 --- /dev/null +++ b/pages/api/auth/login.ts @@ -0,0 +1,133 @@ +// Next.js API route support: https://nextjs.org/docs/api-routes/introduction +import type { NextApiRequest, NextApiResponse } from "next"; +import { readableMessageToSign } from "@/hooks/useLogin"; +import { ethers } from "ethers"; +import { createClient } from "@supabase/supabase-js"; +import { Database } from "@/types/database"; +import jwt from "jsonwebtoken"; + +type Data = { + token: string; +}; + +export default async function handler( + req: NextApiRequest, + res: NextApiResponse, +) { + /* + 1. verify the signed message matches the requested address + 2. select * from public.user table where address matches + 3. verify the nonce included in the request matches what's + already in public.users table for that address + 4. if there's no public.users.id for that address, then you + need to create a user in the auth.users table + */ + + const { address, nonce, signed } = req.body; + + const lowerCaseAddress = address.toLowerCase(); + + const supabase = createClient( + process.env.NEXT_PUBLIC_SUPABASE_URL!, + process.env.NEXT_PUBLIC_SUPABASE_SERVICE_ROLE_KEY!, + ); + + // 1. verify the signed message matches the requested address + const recoveredAddress = ethers.utils.verifyMessage( + readableMessageToSign, + signed, + ); + + if ( + !recoveredAddress || + recoveredAddress.toLowerCase() !== lowerCaseAddress + ) { + return res.status(401).json({ error: "Invalid signature" }); + } + + // 2. select * from public.user table where address matches + const { data: user, error } = await supabase + .from("users") + .select("*") + .eq("address", lowerCaseAddress) + .single(); + + if (error) { + console.log("Error selecting user", error); + return res.status(500).json({ error: error.message }); + } + + if (!user) { + return res.status(500).json({ error: "User not found" }); + } + + // 3. verify the nonce included in the request matches what's + // already in public.users table for that address + const userNonce = user.auth.genNonce; + if (userNonce !== nonce) { + return res.status(401).json({ error: "Invalid nonce" }); + } + + let userId = user.id; + + // 4. if there's no public.users.id for that address, then you + // need to create a user in the auth.users table + const newNonce = Math.floor(Math.random() * 1000000); + if (!user.id) { + const { data: authUser, error } = await supabase.auth.admin.createUser({ + email: "info@jips.dev", + user_metadata: { address: lowerCaseAddress }, + email_confirm: true, + }); + + if (error) { + return res.status(500).json({ error: error.message }); + } + + // 5. insert response into public.users table with id + await supabase + .from("users") + .update({ + auth: { + genNonce: newNonce, // update the nonce, so it can't be reused + lastAuth: new Date().toISOString(), + lastAuthStatus: "success", + }, + id: authUser.user.id, + }) + .eq("address", lowerCaseAddress); // primary key + userId = authUser.user.id; + } else { + // Otherwise just update the nonce + await supabase + .from("users") + .update({ + auth: { + genNonce: newNonce, // update the nonce, so it can't be reused + lastAuth: new Date().toISOString(), + lastAuthStatus: "success", + }, + }) + .eq("address", lowerCaseAddress); // primary key + } + // 6. lastly, we sign the token, then return it to client + const JWT = process.env.NEXT_PUBLIC_JWT_SECRET; + + if (!JWT) { + console.error("JWT not set"); + return res.status(500).json({ error: "JWT not set" }); + } + + const token = jwt.sign( + { + address: lowerCaseAddress, // this will be read by RLS policy + sub: userId, + aud: "authenticated", + role: "authenticated", + }, + JWT, + { expiresIn: 60 * 2 }, + ); + + return res.status(200).send({ token }); +} diff --git a/pages/api/auth/nonce.ts b/pages/api/auth/nonce.ts new file mode 100644 index 0000000..38da2fc --- /dev/null +++ b/pages/api/auth/nonce.ts @@ -0,0 +1,37 @@ +// Next.js API route support: https://nextjs.org/docs/api-routes/introduction +import type { NextApiRequest, NextApiResponse } from "next"; +import { createClient } from "@supabase/supabase-js"; +import { Database } from "@/types/database"; + +type Data = { + nonce: number; +}; + +export default async function handler( + req: NextApiRequest, + res: NextApiResponse, +) { + const { address } = req.body; + const nonce = Math.floor(Math.random() * 1000000); + + const supabase = createClient( + process.env.NEXT_PUBLIC_SUPABASE_URL!, + process.env.NEXT_PUBLIC_SUPABASE_SERVICE_ROLE_KEY!, + ); + + const { error } = await supabase.from("users").upsert({ + address: address.toLowerCase(), + auth: { + genNonce: nonce, + lastAuth: new Date().toISOString(), + lastAuthStatus: "pending", + }, + }); + + if (error) { + console.log("Error updating nonce", error); + return res.status(500).json({ error: error.message }); + } + + return res.status(200).json({ nonce }); +} diff --git a/supabase/migrations/20231018133039_add_authentication.sql b/supabase/migrations/20231018133039_add_authentication.sql new file mode 100644 index 0000000..644f5bf --- /dev/null +++ b/supabase/migrations/20231018133039_add_authentication.sql @@ -0,0 +1,38 @@ +create table "public"."users" ( + "id" uuid, + "created_at" timestamp with time zone not null default now(), + "auth" json not null, + "email" text, + "address" text not null +); + + +alter table "public"."users" enable row level security; + +CREATE UNIQUE INDEX users_pkey ON public.users USING btree (address); + +alter table "public"."users" add constraint "users_pkey" PRIMARY KEY using index "users_pkey"; + +create policy "Enable insert for authenticated users only" +on "public"."hyperboards" +as permissive +for insert +to authenticated +with check (true); + + +create policy "Enable read access for all users" +on "public"."hyperboards" +as permissive +for select +to public +using (true); + + +create policy "Enable update for users based on address" +on "public"."hyperboards" +as permissive +for update +to public +using (((auth.jwt() ->> 'address'::text) = admin_id)) +with check (((auth.jwt() ->> 'address'::text) = admin_id)); diff --git a/yarn.lock b/yarn.lock index 8f6dcf2..d5deae3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6146,6 +6146,13 @@ resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee" integrity sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ== +"@types/jsonwebtoken@^9.0.4": + version "9.0.4" + resolved "https://registry.yarnpkg.com/@types/jsonwebtoken/-/jsonwebtoken-9.0.4.tgz#8b74bbe87bde81a3469d4b32a80609bec62c23ec" + integrity sha512-8UYapdmR0QlxgvJmyE8lP7guxD0UGVMfknsdtCFZh4ovShdBl3iOI4zdvqBHrB/IS+xUj3PSx73Qkey1fhWz+g== + dependencies: + "@types/node" "*" + "@types/lodash.mergewith@4.6.7": version "4.6.7" resolved "https://registry.yarnpkg.com/@types/lodash.mergewith/-/lodash.mergewith-4.6.7.tgz#eaa65aa5872abdd282f271eae447b115b2757212" @@ -7795,6 +7802,11 @@ bser@2.1.1: dependencies: node-int64 "^0.4.0" +buffer-equal-constant-time@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz#f8e71132f7ffe6e01a5c9697a4c6f3e48d5cc819" + integrity sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA== + buffer-from@^1.0.0, buffer-from@^1.1.0: version "1.1.2" resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5" @@ -8971,6 +8983,13 @@ eastasianwidth@^0.2.0: resolved "https://registry.yarnpkg.com/eastasianwidth/-/eastasianwidth-0.2.0.tgz#696ce2ec0aa0e6ea93a397ffcf24aa7840c827cb" integrity sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA== +ecdsa-sig-formatter@1.0.11: + version "1.0.11" + resolved "https://registry.yarnpkg.com/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz#ae0f0fa2d85045ef14a817daa3ce9acd0489e5bf" + integrity sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ== + dependencies: + safe-buffer "^5.0.1" + electron-fetch@^1.7.2: version "1.9.1" resolved "https://registry.yarnpkg.com/electron-fetch/-/electron-fetch-1.9.1.tgz#e28bfe78d467de3f2dec884b1d72b8b05322f30f" @@ -12563,6 +12582,22 @@ jsonparse@^1.2.0: resolved "https://registry.yarnpkg.com/jsonparse/-/jsonparse-1.3.1.tgz#3f4dae4a91fac315f71062f8521cc239f1366280" integrity sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg== +jsonwebtoken@^9.0.2: + version "9.0.2" + resolved "https://registry.yarnpkg.com/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz#65ff91f4abef1784697d40952bb1998c504caaf3" + integrity sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ== + dependencies: + jws "^3.2.2" + lodash.includes "^4.3.0" + lodash.isboolean "^3.0.3" + lodash.isinteger "^4.0.4" + lodash.isnumber "^3.0.3" + lodash.isplainobject "^4.0.6" + lodash.isstring "^4.0.1" + lodash.once "^4.0.0" + ms "^2.1.1" + semver "^7.5.4" + "jsx-ast-utils@^2.4.1 || ^3.0.0", jsx-ast-utils@^3.3.3: version "3.3.5" resolved "https://registry.yarnpkg.com/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz#4766bd05a8e2a11af222becd19e15575e52a853a" @@ -12588,6 +12623,23 @@ just-safe-set@^4.0.2, just-safe-set@^4.1.1: resolved "https://registry.yarnpkg.com/just-safe-set/-/just-safe-set-4.2.1.tgz#aa2d26abedc670ef247c1eaabeba73dc07a05cea" integrity sha512-La5CP41Ycv52+E4g7w1sRV8XXk7Sp8a/TwWQAYQKn6RsQz1FD4Z/rDRRmqV3wJznS1MDF3YxK7BCudX1J8FxLg== +jwa@^1.4.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/jwa/-/jwa-1.4.1.tgz#743c32985cb9e98655530d53641b66c8645b039a" + integrity sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA== + dependencies: + buffer-equal-constant-time "1.0.1" + ecdsa-sig-formatter "1.0.11" + safe-buffer "^5.0.1" + +jws@^3.2.2: + version "3.2.2" + resolved "https://registry.yarnpkg.com/jws/-/jws-3.2.2.tgz#001099f3639468c9414000e99995fa52fb478304" + integrity sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA== + dependencies: + jwa "^1.4.1" + safe-buffer "^5.0.1" + k-bucket@^5.1.0: version "5.1.0" resolved "https://registry.yarnpkg.com/k-bucket/-/k-bucket-5.1.0.tgz#db2c9e72bd168b432e3f3e8fc092e2ccb61bff89" @@ -12859,11 +12911,41 @@ lodash.get@4.4.2, lodash.get@^4.4.2: resolved "https://registry.yarnpkg.com/lodash.get/-/lodash.get-4.4.2.tgz#2d177f652fa31e939b4438d5341499dfa3825e99" integrity sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ== +lodash.includes@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/lodash.includes/-/lodash.includes-4.3.0.tgz#60bb98a87cb923c68ca1e51325483314849f553f" + integrity sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w== + +lodash.isboolean@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz#6c2e171db2a257cd96802fd43b01b20d5f5870f6" + integrity sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg== + lodash.isequal@4.5.0: version "4.5.0" resolved "https://registry.yarnpkg.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz#415c4478f2bcc30120c22ce10ed3226f7d3e18e0" integrity sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ== +lodash.isinteger@^4.0.4: + version "4.0.4" + resolved "https://registry.yarnpkg.com/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz#619c0af3d03f8b04c31f5882840b77b11cd68343" + integrity sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA== + +lodash.isnumber@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz#3ce76810c5928d03352301ac287317f11c0b1ffc" + integrity sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw== + +lodash.isplainobject@^4.0.6: + version "4.0.6" + resolved "https://registry.yarnpkg.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz#7c526a52d89b45c45cc690b88163be0497f550cb" + integrity sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA== + +lodash.isstring@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/lodash.isstring/-/lodash.isstring-4.0.1.tgz#d527dfb5456eca7cc9bb95d5daeaf88ba54a5451" + integrity sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw== + lodash.memoize@4.x: version "4.1.2" resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe" @@ -12879,6 +12961,11 @@ lodash.mergewith@4.6.2: resolved "https://registry.yarnpkg.com/lodash.mergewith/-/lodash.mergewith-4.6.2.tgz#617121f89ac55f59047c7aec1ccd6654c6590f55" integrity sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ== +lodash.once@^4.0.0: + version "4.1.1" + resolved "https://registry.yarnpkg.com/lodash.once/-/lodash.once-4.1.1.tgz#0dd3971213c7c56df880977d504c88fb471a97ac" + integrity sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg== + lodash.topath@^4.5.2: version "4.5.2" resolved "https://registry.yarnpkg.com/lodash.topath/-/lodash.topath-4.5.2.tgz#3616351f3bba61994a0931989660bd03254fd009" From 75435307659f1148bf86664c5976ffc04eddbbe2 Mon Sep 17 00:00:00 2001 From: jipstavenuiter Date: Wed, 18 Oct 2023 15:57:24 +0200 Subject: [PATCH 03/39] improve ux and dx --- components/AdminSignInButton.tsx | 7 --- ...eLogin.ts => useGetAuthenticatedClient.ts} | 60 ++++++++++--------- lib/supabase.ts | 9 +-- pages/api/auth/login.ts | 2 +- 4 files changed, 34 insertions(+), 44 deletions(-) delete mode 100644 components/AdminSignInButton.tsx rename hooks/{useLogin.ts => useGetAuthenticatedClient.ts} (62%) diff --git a/components/AdminSignInButton.tsx b/components/AdminSignInButton.tsx deleted file mode 100644 index d0340d8..0000000 --- a/components/AdminSignInButton.tsx +++ /dev/null @@ -1,7 +0,0 @@ -import { Button } from "@chakra-ui/react"; -import { useLogin } from "@/hooks/useLogin"; - -export const AdminSignInButton = () => { - const login = useLogin(); - return ; -}; diff --git a/hooks/useLogin.ts b/hooks/useGetAuthenticatedClient.ts similarity index 62% rename from hooks/useLogin.ts rename to hooks/useGetAuthenticatedClient.ts index cbab19a..178170b 100644 --- a/hooks/useLogin.ts +++ b/hooks/useGetAuthenticatedClient.ts @@ -1,4 +1,6 @@ import { useAccount, useSignMessage } from "wagmi"; +import { getSupabaseAuthenticatedClient } from "@/lib/supabase"; +import { useToast } from "@chakra-ui/react"; const fetchNonce = async (address: string) => { const res = await fetch("/api/auth/nonce", { @@ -12,11 +14,7 @@ const fetchNonce = async (address: string) => { return nonce; }; -export const fetchLogin = async ( - address: string, - signed: string, - nonce: string, -) => { +const fetchLogin = async (address: string, signed: string, nonce: string) => { const res = await fetch("/api/auth/login", { method: "POST", headers: { @@ -24,31 +22,15 @@ export const fetchLogin = async ( }, body: JSON.stringify({ address, signed, nonce }), }); - const result = await res.json(); - return result; + return await res.json(); }; export const readableMessageToSign = "Sign in to Hypercerts"; -const tokenName = "hyperboards-token"; -const storeToken = (token: string) => { - if (typeof window === "undefined") { - return; - } - - window.localStorage.setItem(tokenName, token); -}; - -export const getToken = () => { - if (typeof window === "undefined") { - return; - } - - return window.localStorage.getItem(tokenName); -}; - -export const useLogin = () => { +export const useGetAuthenticatedClient = () => { const { address } = useAccount(); + const toast = useToast(); + const { signMessageAsync } = useSignMessage({ onSuccess: (signature) => { console.log("Signature: ", signature); @@ -66,6 +48,11 @@ export const useLogin = () => { nonce = await fetchNonce(address); } catch (e) { console.error("Error requesting nonce", e); + toast({ + title: "Authentication failed", + status: "error", + }); + return; } if (!nonce) { @@ -80,19 +67,36 @@ export const useLogin = () => { }); } catch (e) { console.error("Error signing message", e); + toast({ + title: "Authentication failed", + description: "Please sign message", + status: "error", + }); + return; } if (!signed) { throw new Error("Signed message not found"); } + let token: string | undefined; try { - const result = await fetchLogin(address, signed, nonce); + const result = await fetchLogin(address, signed, nonce!); console.log("Result", result); - const token = result.token; - storeToken(token); + token = result.token; } catch (e) { console.error("Error logging in", e); + toast({ + title: "Authentication failed", + status: "error", + }); + return; } + + if (!token) { + throw new Error("Token not found"); + } + + return getSupabaseAuthenticatedClient(token); }; }; diff --git a/lib/supabase.ts b/lib/supabase.ts index 59b39e3..1c64067 100644 --- a/lib/supabase.ts +++ b/lib/supabase.ts @@ -1,18 +1,11 @@ import { createClient } from "@supabase/supabase-js"; import { Database } from "@/types/database"; -import { getToken } from "@/hooks/useLogin"; export const supabase = createClient( process.env.NEXT_PUBLIC_SUPABASE_URL!, process.env.NEXT_PUBLIC_SUPABASE_PRIVATE_KEY!, ); -export const getSupabaseAuthenticated = () => { - const token = getToken(); - - if (!token) { - throw new Error("No token found"); - } - +export const getSupabaseAuthenticatedClient = (token: string) => { return createClient(process.env.NEXT_PUBLIC_SUPABASE_URL!, token); }; diff --git a/pages/api/auth/login.ts b/pages/api/auth/login.ts index f6bfe16..8857537 100644 --- a/pages/api/auth/login.ts +++ b/pages/api/auth/login.ts @@ -1,6 +1,6 @@ // Next.js API route support: https://nextjs.org/docs/api-routes/introduction import type { NextApiRequest, NextApiResponse } from "next"; -import { readableMessageToSign } from "@/hooks/useLogin"; +import { readableMessageToSign } from "@/hooks/useGetAuthenticatedClient"; import { ethers } from "ethers"; import { createClient } from "@supabase/supabase-js"; import { Database } from "@/types/database"; From f2591acab64e84bdd0a83d9add39f1e7a3124541 Mon Sep 17 00:00:00 2001 From: jipstavenuiter Date: Thu, 19 Oct 2023 09:22:59 +0200 Subject: [PATCH 04/39] add initial version of hyperboards admin --- .../admin/create-hyperboard-modal.ts | 0 components/admin/create-hyperboard-modal.tsx | 79 +++ components/admin/hyperboards-admin.tsx | 28 + components/admin/sidebar.tsx | 152 ++++ hooks/useAddress.ts | 6 + hooks/useMyHyperboards.ts | 20 + package.json | 3 +- pages/admin/[page].tsx | 33 + types/database.ts | 657 +++++++----------- yarn.lock | 5 + 10 files changed, 564 insertions(+), 419 deletions(-) rename pages/admin.tsx => components/admin/create-hyperboard-modal.ts (100%) create mode 100644 components/admin/create-hyperboard-modal.tsx create mode 100644 components/admin/hyperboards-admin.tsx create mode 100644 components/admin/sidebar.tsx create mode 100644 hooks/useAddress.ts create mode 100644 hooks/useMyHyperboards.ts create mode 100644 pages/admin/[page].tsx diff --git a/pages/admin.tsx b/components/admin/create-hyperboard-modal.ts similarity index 100% rename from pages/admin.tsx rename to components/admin/create-hyperboard-modal.ts diff --git a/components/admin/create-hyperboard-modal.tsx b/components/admin/create-hyperboard-modal.tsx new file mode 100644 index 0000000..fa08311 --- /dev/null +++ b/components/admin/create-hyperboard-modal.tsx @@ -0,0 +1,79 @@ +import { + ModalBody, + ModalCloseButton, + ModalContent, + ModalHeader, + ModalOverlay, + ModalProps, +} from "@chakra-ui/modal"; +import { Button, Flex, Modal, useToast } from "@chakra-ui/react"; +import { useGetAuthenticatedClient } from "@/hooks/useGetAuthenticatedClient"; +import { useAddress } from "@/hooks/useAddress"; + +export const CreateHyperboardModal = ({ + ...modalProps +}: Omit) => { + const getClient = useGetAuthenticatedClient(); + const address = useAddress(); + const toast = useToast(); + + const onConfirm = async () => { + if (!address) { + toast({ + title: "Error", + description: "You must be connected to create a hyperboard", + status: "error", + duration: 9000, + isClosable: true, + }); + return; + } + const supabase = await getClient(); + + if (!supabase) { + return; + } + + const { error } = await supabase + .from("hyperboards") + .insert({ + name: "test", + admin_id: address, + }) + .select(); + + if (error) { + toast({ + title: "Error", + description: error.message, + status: "error", + duration: 9000, + isClosable: true, + }); + return; + } + + toast({ + title: "Success", + description: "Hyperboard created", + status: "success", + }); + + modalProps.onClose(); + }; + + return ( + + + + Create Hyperboard + + + + + + + + + ); +}; diff --git a/components/admin/hyperboards-admin.tsx b/components/admin/hyperboards-admin.tsx new file mode 100644 index 0000000..526c0ac --- /dev/null +++ b/components/admin/hyperboards-admin.tsx @@ -0,0 +1,28 @@ +import { Button, Flex, useDisclosure } from "@chakra-ui/react"; +import { CreateHyperboardModal } from "@/components/admin/create-hyperboard-modal"; +import { useMyHyperboards } from "@/hooks/useMyHyperboards"; + +export const HyperboardsAdmin = () => { + const { + isOpen: createIsOpen, + onClose: createOnClose, + onOpen: createOnOpen, + } = useDisclosure(); + + const { data } = useMyHyperboards(); + + return ( + <> + + + + {data?.data?.map((hyperboard) => ( +
{hyperboard.name}
+ ))} +
+ + + ); +}; diff --git a/components/admin/sidebar.tsx b/components/admin/sidebar.tsx new file mode 100644 index 0000000..f43e899 --- /dev/null +++ b/components/admin/sidebar.tsx @@ -0,0 +1,152 @@ +import React from "react"; +import { + IconButton, + Box, + CloseButton, + Flex, + Icon, + useColorModeValue, + Text, + Drawer, + DrawerContent, + useDisclosure, + BoxProps, + FlexProps, +} from "@chakra-ui/react"; +import { FiMenu } from "react-icons/fi"; +import { IconType } from "react-icons"; +import Link from "next/link"; + +interface LinkItemProps { + name: string; + icon: IconType; + href: string; +} + +export default function SimpleSidebar({ + linkItems, +}: { + linkItems: LinkItemProps[]; +}) { + const { isOpen, onOpen, onClose } = useDisclosure(); + return ( + + onClose} + display={{ base: "none", md: "block" }} + linkItems={linkItems} + /> + + + + + + {/* mobilenav */} + + + ); +} + +interface SidebarProps extends BoxProps { + onClose: () => void; + linkItems: LinkItemProps[]; +} + +const SidebarContent = ({ onClose, linkItems, ...rest }: SidebarProps) => { + return ( + + {linkItems.map((link) => ( + + {link.name} + + ))} + + + + + ); +}; + +interface NavItemProps extends FlexProps { + icon: IconType; + children: string | number; + href: string; +} +const NavItem = ({ icon, children, href, ...rest }: NavItemProps) => { + return ( + + + + {icon && ( + + )} + {children} + + + + ); +}; + +interface MobileProps extends FlexProps { + onOpen: () => void; +} +const MobileNav = ({ onOpen, ...rest }: MobileProps) => { + return ( + + } + /> + + + Logo + + + ); +}; diff --git a/hooks/useAddress.ts b/hooks/useAddress.ts new file mode 100644 index 0000000..7cd0c2e --- /dev/null +++ b/hooks/useAddress.ts @@ -0,0 +1,6 @@ +import { useAccount } from "wagmi"; + +export const useAddress = () => { + const { address } = useAccount(); + return address?.toLowerCase(); +}; diff --git a/hooks/useMyHyperboards.ts b/hooks/useMyHyperboards.ts new file mode 100644 index 0000000..f2af3cf --- /dev/null +++ b/hooks/useMyHyperboards.ts @@ -0,0 +1,20 @@ +import { useAddress } from "@/hooks/useAddress"; +import { useQuery } from "@tanstack/react-query"; +import { supabase } from "@/lib/supabase"; + +export const useMyHyperboards = () => { + const address = useAddress(); + + return useQuery( + ["myHyperboards", address], + async () => { + if (!address) { + throw new Error("No address found"); + } + return supabase.from("hyperboards").select("*").eq("admin_id", address); + }, + { + enabled: !!address, + }, + ); +}; diff --git a/package.json b/package.json index 34ebfc0..16bc6f3 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ "lint": "next lint", "supabase:init": "supabase init", "supabase:start": "supabase start", - "supabase:types": "supabase gen types typescript --project-id clagjjfinooizoqdkvqc > ./types/database.ts", + "supabase:types": "supabase gen types typescript --local > ./types/database.ts", "supabase:migration:new": "supabase migration new", "supabase:migration:push": "supabase migration up", "supabase:migration:diff": "supabase db diff --schema public", @@ -43,6 +43,7 @@ "next": "13.4.19", "react": "18.2.0", "react-dom": "18.2.0", + "react-icons": "^4.11.0", "react-slick": "^0.29.0", "slick-carousel": "^1.8.1", "supabase": "^1.106.1", diff --git a/pages/admin/[page].tsx b/pages/admin/[page].tsx new file mode 100644 index 0000000..a873047 --- /dev/null +++ b/pages/admin/[page].tsx @@ -0,0 +1,33 @@ +import { Flex, useColorModeValue } from "@chakra-ui/react"; +import SimpleSidebar from "@/components/admin/sidebar"; +import { FiCompass, FiHome, FiTrendingUp } from "react-icons/fi"; +import { useRouter } from "next/router"; +import { HyperboardsAdmin } from "@/components/admin/hyperboards-admin"; + +const SIDEBAR_ITEMS = [ + { name: "Hyperboards", icon: FiHome, href: "/admin/hyperboards" }, + { name: "Registries", icon: FiTrendingUp, href: "/admin/registries" }, + { name: "Blueprints", icon: FiCompass, href: "/admin/blueprints" }, +]; + +const Admin = () => { + const router = useRouter(); + const page = router.query["page"]; + + return ( + + + + {page === "hyperboards" && } + + + ); +}; + +export default Admin; diff --git a/types/database.ts b/types/database.ts index 1b3dd44..54bc692 100644 --- a/types/database.ts +++ b/types/database.ts @@ -7,302 +7,96 @@ export type Json = | Json[] export interface Database { - public: { + graphql_public: { Tables: { - allowlistCache: { - Row: { - address: string | null - claimId: string | null - created_at: string | null - fractionCounter: number | null - hidden: boolean - id: number - } - Insert: { - address?: string | null - claimId?: string | null - created_at?: string | null - fractionCounter?: number | null - hidden?: boolean - id?: number - } - Update: { - address?: string | null - claimId?: string | null - created_at?: string | null - fractionCounter?: number | null - hidden?: boolean - id?: number - } - Relationships: [] - } - "allowlistCache-goerli": { - Row: { - address: string | null - claimId: string | null - created_at: string | null - fractionCounter: number | null - hidden: boolean - id: number - } - Insert: { - address?: string | null - claimId?: string | null - created_at?: string | null - fractionCounter?: number | null - hidden?: boolean - id?: number - } - Update: { - address?: string | null - claimId?: string | null - created_at?: string | null - fractionCounter?: number | null - hidden?: boolean - id?: number - } - Relationships: [] - } - "allowlistCache-optimism": { - Row: { - address: string | null - claimId: string | null - created_at: string | null - fractionCounter: number | null - hidden: boolean - id: number - } - Insert: { - address?: string | null - claimId?: string | null - created_at?: string | null - fractionCounter?: number | null - hidden?: boolean - id?: number - } - Update: { - address?: string | null - claimId?: string | null - created_at?: string | null - fractionCounter?: number | null - hidden?: boolean - id?: number + [_ in never]: never + } + Views: { + [_ in never]: never + } + Functions: { + graphql: { + Args: { + operationName?: string + query?: string + variables?: Json + extensions?: Json } - Relationships: [] + Returns: Json } - "claim-blueprints-optimism": { + } + Enums: { + [_ in never]: never + } + CompositeTypes: { + [_ in never]: never + } + } + public: { + Tables: { + hyperboard_claims: { Row: { + chain_id: number created_at: string - form_values: string + hypercert_id: string id: string - minter_address: string registry_id: string } Insert: { + chain_id: number created_at?: string - form_values: string + hypercert_id: string id?: string - minter_address: string registry_id: string } Update: { + chain_id?: number created_at?: string - form_values?: string + hypercert_id?: string id?: string - minter_address?: string registry_id?: string } Relationships: [ { - foreignKeyName: "claim-blueprints-optimism_registry_id_fkey" + foreignKeyName: "hyperboard_claims_registry_id_fkey" columns: ["registry_id"] - referencedRelation: "registries-optimism" + referencedRelation: "registries" referencedColumns: ["id"] } ] } - "claims-metadata-mapping": { + hyperboard_registries: { Row: { - chainId: number | null - claimId: string - collectionName: string | null - collision: string | null - createdAt: number | null - creatorAddress: string | null - date: string | null - featured: boolean | null - hidden: boolean - hypercert: Json | null - id: number - properties: Json | null - title: string | null - totalPrice: number | null - totalUnits: number | null - } - Insert: { - chainId?: number | null - claimId: string - collectionName?: string | null - collision?: string | null - createdAt?: number | null - creatorAddress?: string | null - date?: string | null - featured?: boolean | null - hidden?: boolean - hypercert?: Json | null - id?: number - properties?: Json | null - title?: string | null - totalPrice?: number | null - totalUnits?: number | null - } - Update: { - chainId?: number | null - claimId?: string - collectionName?: string | null - collision?: string | null - createdAt?: number | null - creatorAddress?: string | null - date?: string | null - featured?: boolean | null - hidden?: boolean - hypercert?: Json | null - id?: number - properties?: Json | null - title?: string | null - totalPrice?: number | null - totalUnits?: number | null - } - Relationships: [] - } - collections: { - Row: { - chainId: number | null - claimId: string | null - collectionName: string | null created_at: string | null - featured: boolean | null - id: number + hyperboard_id: string + registries_id: string } Insert: { - chainId?: number | null - claimId?: string | null - collectionName?: string | null created_at?: string | null - featured?: boolean | null - id?: number + hyperboard_id: string + registries_id: string } Update: { - chainId?: number | null - claimId?: string | null - collectionName?: string | null created_at?: string | null - featured?: boolean | null - id?: number - } - Relationships: [] - } - "ftc-purchase": { - Row: { - address: string - ethValue: number - id: number - textForSponsor: string | null - timestamp: string - values: Json - } - Insert: { - address: string - ethValue: number - id?: number - textForSponsor?: string | null - timestamp?: string - values: Json - } - Update: { - address?: string - ethValue?: number - id?: number - textForSponsor?: string | null - timestamp?: string - values?: Json - } - Relationships: [] - } - "gtc-alpha-allowlist": { - Row: { - address: string | null - id: number - project: string | null - units: number | null - } - Insert: { - address?: string | null - id: number - project?: string | null - units?: number | null - } - Update: { - address?: string | null - id?: number - project?: string | null - units?: number | null - } - Relationships: [] - } - "hidden-hypercerts": { - Row: { - chainId: number | null - claimId: string | null - entry_created_at: string - hidden: boolean - id: number - } - Insert: { - chainId?: number | null - claimId?: string | null - entry_created_at?: string - hidden?: boolean - id?: number - } - Update: { - chainId?: number | null - claimId?: string | null - entry_created_at?: string - hidden?: boolean - id?: number - } - Relationships: [] - } - "hyperboard-claims": { - Row: { - created_at: string - hypercert_id: string - id: string - registry_id: string - } - Insert: { - created_at?: string - hypercert_id: string - id?: string - registry_id: string - } - Update: { - created_at?: string - hypercert_id?: string - id?: string - registry_id?: string + hyperboard_id?: string + registries_id?: string } Relationships: [ { - foreignKeyName: "hyperboard-claims_registry_id_fkey" - columns: ["registry_id"] - referencedRelation: "registries-optimism" + foreignKeyName: "hyperboard_registries_hyperboard_id_fkey" + columns: ["hyperboard_id"] + referencedRelation: "hyperboards" + referencedColumns: ["id"] + }, + { + foreignKeyName: "hyperboard_registries_registries_id_fkey" + columns: ["registries_id"] + referencedRelation: "registries" referencedColumns: ["id"] } ] } - "hyperboard-sponsor-metadata": { + hyperboard_sponsor_metadata: { Row: { address: string companyName: string | null @@ -335,84 +129,31 @@ export interface Database { } Relationships: [] } - "hypercert-projects": { - Row: { - date_to_order: string | null - description: string | null - hidden: boolean - id: number - link: string | null - link_display_text: string | null - name: string | null - organization: string | null - stage: string | null - time_created: string | null - type: string | null - visible_date: string | null - } - Insert: { - date_to_order?: string | null - description?: string | null - hidden?: boolean - id?: number - link?: string | null - link_display_text?: string | null - name?: string | null - organization?: string | null - stage?: string | null - time_created?: string | null - type?: string | null - visible_date?: string | null - } - Update: { - date_to_order?: string | null - description?: string | null - hidden?: boolean - id?: number - link?: string | null - link_display_text?: string | null - name?: string | null - organization?: string | null - stage?: string | null - time_created?: string | null - type?: string | null - visible_date?: string | null - } - Relationships: [] - } - "hypercerts-store": { + hyperboards: { Row: { - chainId: number | null - claimId: string | null - collectionName: string | null - created_at: string - hidden: boolean - id: number - maxPurchase: number + admin_id: string + created_at: string | null + id: string + name: string } Insert: { - chainId?: number | null - claimId?: string | null - collectionName?: string | null - created_at?: string - hidden?: boolean - id?: number - maxPurchase?: number + admin_id: string + created_at?: string | null + id?: string + name: string } Update: { - chainId?: number | null - claimId?: string | null - collectionName?: string | null - created_at?: string - hidden?: boolean - id?: number - maxPurchase?: number + admin_id?: string + created_at?: string | null + id?: string + name?: string } Relationships: [] } - "registries-optimism": { + registries: { Row: { admin_id: string + chain_id: number created_at: string description: string hidden: boolean @@ -421,6 +162,7 @@ export interface Database { } Insert: { admin_id: string + chain_id: number created_at?: string description: string hidden?: boolean @@ -429,6 +171,7 @@ export interface Database { } Update: { admin_id?: string + chain_id?: number created_at?: string description?: string hidden?: boolean @@ -437,137 +180,214 @@ export interface Database { } Relationships: [] } - "zuzalu-community-hypercerts": { + users: { Row: { - chainId: number | null - claimId: string - collectionName: string | null - collision: string | null - createdAt: number | null - creatorAddress: string | null - date: string | null - featured: boolean | null - hidden: boolean - hypercert: Json | null - id: number - properties: Json | null - title: string | null - totalUnits: number | null + address: string + auth: Json + created_at: string + email: string | null + id: string | null } Insert: { - chainId?: number | null - claimId: string - collectionName?: string | null - collision?: string | null - createdAt?: number | null - creatorAddress?: string | null - date?: string | null - featured?: boolean | null - hidden?: boolean - hypercert?: Json | null - id?: number - properties?: Json | null - title?: string | null - totalUnits?: number | null + address: string + auth: Json + created_at?: string + email?: string | null + id?: string | null } Update: { - chainId?: number | null - claimId?: string - collectionName?: string | null - collision?: string | null - createdAt?: number | null - creatorAddress?: string | null - date?: string | null - featured?: boolean | null - hidden?: boolean - hypercert?: Json | null - id?: number - properties?: Json | null - title?: string | null - totalUnits?: number | null + address?: string + auth?: Json + created_at?: string + email?: string | null + id?: string | null } Relationships: [] } - "zuzalu-purchase": { + } + Views: { + [_ in never]: never + } + Functions: { + [_ in never]: never + } + Enums: { + [_ in never]: never + } + CompositeTypes: { + [_ in never]: never + } + } + storage: { + Tables: { + buckets: { Row: { - address: string - ethValue: number + allowed_mime_types: string[] | null + avif_autodetection: boolean | null + created_at: string | null + file_size_limit: number | null + id: string + name: string + owner: string | null + public: boolean | null + updated_at: string | null + } + Insert: { + allowed_mime_types?: string[] | null + avif_autodetection?: boolean | null + created_at?: string | null + file_size_limit?: number | null + id: string + name: string + owner?: string | null + public?: boolean | null + updated_at?: string | null + } + Update: { + allowed_mime_types?: string[] | null + avif_autodetection?: boolean | null + created_at?: string | null + file_size_limit?: number | null + id?: string + name?: string + owner?: string | null + public?: boolean | null + updated_at?: string | null + } + Relationships: [ + { + foreignKeyName: "buckets_owner_fkey" + columns: ["owner"] + referencedRelation: "users" + referencedColumns: ["id"] + } + ] + } + migrations: { + Row: { + executed_at: string | null + hash: string id: number - textForSponsor: string | null - timestamp: string - values: Json + name: string } Insert: { - address: string - ethValue: number - id?: number - textForSponsor?: string | null - timestamp?: string - values: Json + executed_at?: string | null + hash: string + id: number + name: string } Update: { - address?: string - ethValue?: number + executed_at?: string | null + hash?: string id?: number - textForSponsor?: string | null - timestamp?: string - values?: Json + name?: string } Relationships: [] } + objects: { + Row: { + bucket_id: string | null + created_at: string | null + id: string + last_accessed_at: string | null + metadata: Json | null + name: string | null + owner: string | null + path_tokens: string[] | null + updated_at: string | null + version: string | null + } + Insert: { + bucket_id?: string | null + created_at?: string | null + id?: string + last_accessed_at?: string | null + metadata?: Json | null + name?: string | null + owner?: string | null + path_tokens?: string[] | null + updated_at?: string | null + version?: string | null + } + Update: { + bucket_id?: string | null + created_at?: string | null + id?: string + last_accessed_at?: string | null + metadata?: Json | null + name?: string | null + owner?: string | null + path_tokens?: string[] | null + updated_at?: string | null + version?: string | null + } + Relationships: [ + { + foreignKeyName: "objects_bucketId_fkey" + columns: ["bucket_id"] + referencedRelation: "buckets" + referencedColumns: ["id"] + } + ] + } } Views: { [_ in never]: never } Functions: { - citext: - | { - Args: { - "": string - } - Returns: string - } - | { - Args: { - "": boolean - } - Returns: string - } - | { - Args: { - "": unknown - } - Returns: string - } - citext_hash: { + can_insert_object: { Args: { - "": string + bucketid: string + name: string + owner: string + metadata: Json } - Returns: number + Returns: undefined } - citextin: { + extension: { Args: { - "": unknown + name: string } Returns: string } - citextout: { + filename: { Args: { - "": string + name: string } - Returns: unknown + Returns: string } - citextrecv: { + foldername: { Args: { - "": unknown + name: string } - Returns: string + Returns: unknown + } + get_size_by_bucket: { + Args: Record + Returns: { + size: number + bucket_id: string + }[] } - citextsend: { + search: { Args: { - "": string - } - Returns: string + prefix: string + bucketname: string + limits?: number + levels?: number + offsets?: number + search?: string + sortcolumn?: string + sortorder?: string + } + Returns: { + name: string + id: string + updated_at: string + created_at: string + last_accessed_at: string + metadata: Json + }[] } } Enums: { @@ -578,3 +398,4 @@ export interface Database { } } } + diff --git a/yarn.lock b/yarn.lock index d5deae3..6f95729 100644 --- a/yarn.lock +++ b/yarn.lock @@ -14846,6 +14846,11 @@ react-focus-lock@^2.9.4: use-callback-ref "^1.3.0" use-sidecar "^1.1.2" +react-icons@^4.11.0: + version "4.11.0" + resolved "https://registry.yarnpkg.com/react-icons/-/react-icons-4.11.0.tgz#4b0e31c9bfc919608095cc429c4f1846f4d66c65" + integrity sha512-V+4khzYcE5EBk/BvcuYRq6V/osf11ODUM2J8hg2FDSswRrGvqiYUYPRy4OdrWaQOBj4NcpJfmHZLNaD+VH0TyA== + react-is@^16.12.0, react-is@^16.13.1, react-is@^16.7.0: version "16.13.1" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" From 7e485e9cbfd6a4f693d0c881b9bbbe6ea334412e Mon Sep 17 00:00:00 2001 From: jipstavenuiter Date: Thu, 19 Oct 2023 10:36:44 +0200 Subject: [PATCH 05/39] enable rls for all tables --- components/providers.tsx | 12 ++----- hooks/registry.ts | 6 ++-- lib/supabase.ts | 2 +- .../migrations/20231019083311_enable_rls.sql | 36 +++++++++++++++++++ 4 files changed, 42 insertions(+), 14 deletions(-) create mode 100644 supabase/migrations/20231019083311_enable_rls.sql diff --git a/components/providers.tsx b/components/providers.tsx index a5ff55f..3d8bd87 100644 --- a/components/providers.tsx +++ b/components/providers.tsx @@ -39,18 +39,12 @@ const config = createConfig({ webSocketPublicClient, connectors, }); -const queryClient = new QueryClient(); export const Providers = ({ showReactQueryDevtools = true, children, }: PropsWithChildren<{ showReactQueryDevtools?: boolean }>) => { - const [client] = useState(() => { - return new HypercertClient({ - chainId: 5, - nftStorageToken: process.env.NEXT_PUBLIC_NFT_STORAGE_TOKEN!, - }); - }); + const [queryClient] = React.useState(() => new QueryClient()); return ( @@ -115,7 +109,5 @@ export const HypercertClientProvider = ({ children }: PropsWithChildren) => { }; export const useHypercertClient = () => { - const client = React.useContext(HypercertClientContext); - - return client; + return React.useContext(HypercertClientContext); }; diff --git a/hooks/registry.ts b/hooks/registry.ts index dfeca13..d099e2d 100644 --- a/hooks/registry.ts +++ b/hooks/registry.ts @@ -36,7 +36,7 @@ interface RegistryContentItem { export const useListRegistries = () => { return useQuery(["list-registries"], async () => - supabase.from("registries-optimism").select("*").neq("hidden", true), + supabase.from("registries").select("*").neq("hidden", true), ); }; @@ -104,8 +104,8 @@ export const useRegistryContents = (registryId: string) => { const getRegistryWithClaims = async (registryId: string) => supabase - .from("registries-optimism") - .select("*, hyperboard-claims ( * )") + .from("registries") + .select("*, hyperboard_claims ( * )") .eq("id", registryId) .single(); diff --git a/lib/supabase.ts b/lib/supabase.ts index 1c64067..7cf0f1c 100644 --- a/lib/supabase.ts +++ b/lib/supabase.ts @@ -3,7 +3,7 @@ import { Database } from "@/types/database"; export const supabase = createClient( process.env.NEXT_PUBLIC_SUPABASE_URL!, - process.env.NEXT_PUBLIC_SUPABASE_PRIVATE_KEY!, + process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!, ); export const getSupabaseAuthenticatedClient = (token: string) => { diff --git a/supabase/migrations/20231019083311_enable_rls.sql b/supabase/migrations/20231019083311_enable_rls.sql new file mode 100644 index 0000000..de7f556 --- /dev/null +++ b/supabase/migrations/20231019083311_enable_rls.sql @@ -0,0 +1,36 @@ +alter table "public"."hyperboard_claims" enable row level security; + +alter table "public"."hyperboard_sponsor_metadata" enable row level security; + +alter table "public"."registries" enable row level security; + +create policy "Enable read access for all users" +on "public"."hyperboard_claims" +as permissive +for select +to public +using (true); + + +create policy "Enable read access for all users" +on "public"."hyperboard_registries" +as permissive +for select +to public +using (true); + + +create policy "Enable read access for all users" +on "public"."hyperboard_sponsor_metadata" +as permissive +for select +to public +using (true); + + +create policy "Enable read access for all users" +on "public"."registries" +as permissive +for select +to public +using (true); From 7b218458ccf31d06c768e20232b50eb1179ade81 Mon Sep 17 00:00:00 2001 From: jipstavenuiter Date: Thu, 19 Oct 2023 10:46:06 +0200 Subject: [PATCH 06/39] fix naming of tables and queries --- hooks/registry.ts | 17 ++--- .../20231019083656_rename_tables.sql | 3 + types/database.ts | 68 +++++++++---------- 3 files changed, 41 insertions(+), 47 deletions(-) create mode 100644 supabase/migrations/20231019083656_rename_tables.sql diff --git a/hooks/registry.ts b/hooks/registry.ts index d099e2d..bbaeb70 100644 --- a/hooks/registry.ts +++ b/hooks/registry.ts @@ -6,15 +6,6 @@ import _ from "lodash"; import { HyperboardEntry } from "@/types/Hyperboard"; import { useHypercertClient } from "@/components/providers"; -interface RegistryWithClaims { - id: string; - name: string; - "hyperboard-claims": { - id: string; - hypercert_id: string; - }[]; -} - interface EntryDisplayData { image: string; address: string; @@ -57,7 +48,7 @@ export const useRegistryContents = (registryId: string) => { // Create one big list of all fractions, for all hypercerts in registry const allFractions = await Promise.all( - registry.data["hyperboard-claims"].map((claim) => { + registry.data["claims"].map((claim) => { return client.indexer.fractionsByClaim(claim.hypercert_id); }), ); @@ -105,13 +96,13 @@ export const useRegistryContents = (registryId: string) => { const getRegistryWithClaims = async (registryId: string) => supabase .from("registries") - .select("*, hyperboard_claims ( * )") + .select("*, claims ( * )") .eq("id", registryId) - .single(); + .single(); const getEntryDisplayData = async (addresses: string[]) => { return supabase - .from("hyperboard-sponsor-metadata") + .from("sponsor_metadata") .select<"*", EntryDisplayData>("*") .in("address", addresses); }; diff --git a/supabase/migrations/20231019083656_rename_tables.sql b/supabase/migrations/20231019083656_rename_tables.sql new file mode 100644 index 0000000..65e9d36 --- /dev/null +++ b/supabase/migrations/20231019083656_rename_tables.sql @@ -0,0 +1,3 @@ +ALTER TABLE hyperboard_claims RENAME TO claims; + +ALTER TABLE hyperboard_sponsor_metadata RENAME TO sponsor_metadata; diff --git a/types/database.ts b/types/database.ts index 54bc692..118d860 100644 --- a/types/database.ts +++ b/types/database.ts @@ -34,7 +34,7 @@ export interface Database { } public: { Tables: { - hyperboard_claims: { + claims: { Row: { chain_id: number created_at: string @@ -96,39 +96,6 @@ export interface Database { } ] } - hyperboard_sponsor_metadata: { - Row: { - address: string - companyName: string | null - created_at: string - firstName: string | null - id: string - image: string - lastName: string | null - type: string - } - Insert: { - address: string - companyName?: string | null - created_at?: string - firstName?: string | null - id?: string - image: string - lastName?: string | null - type: string - } - Update: { - address?: string - companyName?: string | null - created_at?: string - firstName?: string | null - id?: string - image?: string - lastName?: string | null - type?: string - } - Relationships: [] - } hyperboards: { Row: { admin_id: string @@ -180,6 +147,39 @@ export interface Database { } Relationships: [] } + sponsor_metadata: { + Row: { + address: string + companyName: string | null + created_at: string + firstName: string | null + id: string + image: string + lastName: string | null + type: string + } + Insert: { + address: string + companyName?: string | null + created_at?: string + firstName?: string | null + id?: string + image: string + lastName?: string | null + type: string + } + Update: { + address?: string + companyName?: string | null + created_at?: string + firstName?: string | null + id?: string + image?: string + lastName?: string | null + type?: string + } + Relationships: [] + } users: { Row: { address: string From 826a2cdae32f02d25c756931843fd746aa7dd880 Mon Sep 17 00:00:00 2001 From: jipstavenuiter Date: Thu, 19 Oct 2023 20:55:53 +0200 Subject: [PATCH 07/39] add basic admin panel --- components/GenericModal.tsx | 28 ++++ components/admin/create-hyperboard-modal.ts | 0 components/admin/create-hyperboard-modal.tsx | 32 ++--- components/admin/create-registry-modal.tsx | 122 ++++++++++++++++++ components/admin/delete-registry-button.tsx | 57 ++++++++ components/admin/hyperboards-admin.tsx | 44 +++++-- components/admin/registries-admin.tsx | 84 ++++++++++++ components/admin/sidebar.tsx | 3 + components/alerts/connect-wallet-alert.tsx | 20 +++ .../dialogs/AlertConfirmationDialog.tsx | 48 +++++++ .../forms/CreateOrUpdateHyperboardForm.tsx | 55 ++++++++ .../forms/CreateOrUpdateRegistryForm.tsx | 122 ++++++++++++++++++ hooks/useDeleteRegistry.ts | 11 ++ hooks/useMyRegistries.ts | 23 ++++ package.json | 1 + pages/admin/[page].tsx | 8 +- types/database-entities.ts | 13 ++ yarn.lock | 5 + 18 files changed, 642 insertions(+), 34 deletions(-) create mode 100644 components/GenericModal.tsx delete mode 100644 components/admin/create-hyperboard-modal.ts create mode 100644 components/admin/create-registry-modal.tsx create mode 100644 components/admin/delete-registry-button.tsx create mode 100644 components/admin/registries-admin.tsx create mode 100644 components/alerts/connect-wallet-alert.tsx create mode 100644 components/dialogs/AlertConfirmationDialog.tsx create mode 100644 components/forms/CreateOrUpdateHyperboardForm.tsx create mode 100644 components/forms/CreateOrUpdateRegistryForm.tsx create mode 100644 hooks/useDeleteRegistry.ts create mode 100644 hooks/useMyRegistries.ts create mode 100644 types/database-entities.ts diff --git a/components/GenericModal.tsx b/components/GenericModal.tsx new file mode 100644 index 0000000..3d4d249 --- /dev/null +++ b/components/GenericModal.tsx @@ -0,0 +1,28 @@ +import { + ModalBody, + ModalCloseButton, + ModalContent, + ModalHeader, + ModalOverlay, + ModalProps, +} from "@chakra-ui/modal"; +import { Flex, Modal } from "@chakra-ui/react"; + +export const GenericModal = ({ + title, + children, + ...modalProps +}: ModalProps & { title: string }) => { + return ( + + + + {title} + + + {children} + + + + ); +}; diff --git a/components/admin/create-hyperboard-modal.ts b/components/admin/create-hyperboard-modal.ts deleted file mode 100644 index e69de29..0000000 diff --git a/components/admin/create-hyperboard-modal.tsx b/components/admin/create-hyperboard-modal.tsx index fa08311..d9e5cbb 100644 --- a/components/admin/create-hyperboard-modal.tsx +++ b/components/admin/create-hyperboard-modal.tsx @@ -1,14 +1,10 @@ -import { - ModalBody, - ModalCloseButton, - ModalContent, - ModalHeader, - ModalOverlay, - ModalProps, -} from "@chakra-ui/modal"; -import { Button, Flex, Modal, useToast } from "@chakra-ui/react"; +import { ModalProps } from "@chakra-ui/modal"; +import { useToast } from "@chakra-ui/react"; import { useGetAuthenticatedClient } from "@/hooks/useGetAuthenticatedClient"; import { useAddress } from "@/hooks/useAddress"; +import { CreateOrUpdateHyperboardForm } from "@/components/forms/CreateOrUpdateHyperboardForm"; +import { GenericModal } from "@/components/GenericModal"; +import { useMyHyperboards } from "@/hooks/useMyHyperboards"; export const CreateHyperboardModal = ({ ...modalProps @@ -17,6 +13,8 @@ export const CreateHyperboardModal = ({ const address = useAddress(); const toast = useToast(); + const { refetch } = useMyHyperboards(); + const onConfirm = async () => { if (!address) { toast({ @@ -59,21 +57,13 @@ export const CreateHyperboardModal = ({ status: "success", }); + await refetch(); modalProps.onClose(); }; return ( - - - - Create Hyperboard - - - - - - - - + + + ); }; diff --git a/components/admin/create-registry-modal.tsx b/components/admin/create-registry-modal.tsx new file mode 100644 index 0000000..01a48e0 --- /dev/null +++ b/components/admin/create-registry-modal.tsx @@ -0,0 +1,122 @@ +import { ModalProps } from "@chakra-ui/modal"; +import { useToast } from "@chakra-ui/react"; +import { useGetAuthenticatedClient } from "@/hooks/useGetAuthenticatedClient"; +import { useAddress } from "@/hooks/useAddress"; +import { CreateOrUpdateHyperboardForm } from "@/components/forms/CreateOrUpdateHyperboardForm"; +import { GenericModal } from "@/components/GenericModal"; +import { useMyHyperboards } from "@/hooks/useMyHyperboards"; +import { useMyRegistries } from "@/hooks/useMyRegistries"; +import { ClaimInsert, RegistryInsert } from "@/types/database-entities"; +import { + CreateOrUpdateRegistryForm, + CreateUpdateRegistryFormValues, +} from "@/components/forms/CreateOrUpdateRegistryForm"; +import { useChainId } from "wagmi"; + +export const CreateRegistryModal = ({ + initialValues, + ...modalProps +}: Omit & { + initialValues: CreateUpdateRegistryFormValues | undefined; +}) => { + const getClient = useGetAuthenticatedClient(); + const address = useAddress(); + const toast = useToast(); + const chainId = useChainId(); + + const { refetch } = useMyRegistries(); + + const onConfirm = async ({ + claims, + ...registry + }: CreateUpdateRegistryFormValues) => { + if (!address) { + toast({ + title: "Error", + description: "You must be connected to create a registry", + status: "error", + duration: 9000, + isClosable: true, + }); + return; + } + + const supabase = await getClient(); + + if (!supabase) { + return; + } + + const admin_id = registry.admin_id || address; + const chain_id = registry.chain_id || chainId; + + const { error, data } = await supabase + .from("registries") + .upsert([{ ...registry, admin_id, chain_id }]) + .select(); + + if (error) { + toast({ + title: "Error", + description: error.message, + status: "error", + duration: 9000, + isClosable: true, + }); + return; + } + + const insertedRegistry = data[0]; + + if (!insertedRegistry) { + toast({ + title: "Error", + description: "Something went wrong with inserting a registry", + status: "error", + duration: 9000, + isClosable: true, + }); + return; + } + + toast({ + title: "Success", + description: "Registry created", + status: "success", + }); + + const claimInserts: ClaimInsert[] = claims.map(({ hypercert_id }) => ({ + registry_id: insertedRegistry.id, + hypercert_id, + chain_id: chainId, + })); + + const { error: insertClaimsError } = await supabase + .from("claims") + .insert(claimInserts) + .select(); + + if (insertClaimsError) { + toast({ + title: "Error", + description: insertClaimsError.message, + status: "error", + duration: 9000, + isClosable: true, + }); + return; + } + + await refetch(); + modalProps.onClose(); + }; + + return ( + + + + ); +}; diff --git a/components/admin/delete-registry-button.tsx b/components/admin/delete-registry-button.tsx new file mode 100644 index 0000000..23752b7 --- /dev/null +++ b/components/admin/delete-registry-button.tsx @@ -0,0 +1,57 @@ +import { IconButton, useDisclosure, useToast } from "@chakra-ui/react"; +import { useDeleteRegistry } from "@/hooks/useDeleteRegistry"; +import { RegistryEntity } from "@/types/database-entities"; +import { useMyRegistries } from "@/hooks/useMyRegistries"; +import { AiFillDelete } from "react-icons/ai"; +import { AlertDialog } from "@/components/dialogs/AlertConfirmationDialog"; + +export const DeleteRegistryButton = ({ + registryId, +}: { + registryId: string; +}) => { + const { refetch } = useMyRegistries(); + const { onClose, onOpen, isOpen } = useDisclosure(); + + const toast = useToast(); + const { mutateAsync: deleteRegistryAsync } = useDeleteRegistry(); + + const onDeleteRegistry = async (registryId: string) => { + try { + await deleteRegistryAsync(registryId); + } catch (e) { + console.error(e); + toast({ + title: "Error", + description: "Could not delete registry", + status: "error", + duration: 9000, + isClosable: true, + }); + } + + await refetch(); + toast({ + title: "Success", + description: "Registry deleted", + status: "success", + }); + }; + + return ( + <> + } + colorScheme="red" + onClick={onOpen} + /> + onDeleteRegistry(registryId)} + onClose={onClose} + isOpen={isOpen} + /> + + ); +}; diff --git a/components/admin/hyperboards-admin.tsx b/components/admin/hyperboards-admin.tsx index 526c0ac..6abfe08 100644 --- a/components/admin/hyperboards-admin.tsx +++ b/components/admin/hyperboards-admin.tsx @@ -1,6 +1,14 @@ -import { Button, Flex, useDisclosure } from "@chakra-ui/react"; +import { + Button, + Card, + Flex, + Heading, + useDisclosure, + VStack, +} from "@chakra-ui/react"; import { CreateHyperboardModal } from "@/components/admin/create-hyperboard-modal"; import { useMyHyperboards } from "@/hooks/useMyHyperboards"; +import { HyperboardEntity } from "@/types/database-entities"; export const HyperboardsAdmin = () => { const { @@ -12,17 +20,33 @@ export const HyperboardsAdmin = () => { const { data } = useMyHyperboards(); return ( - <> - - - + + + {data?.data?.map((hyperboard) => ( -
{hyperboard.name}
+ ))} -
+ - +
+ ); +}; + +const HyperboardAdminRow = ({ + hyperboard, +}: { + hyperboard: HyperboardEntity; +}) => { + return ( + + {hyperboard.name} + ); }; diff --git a/components/admin/registries-admin.tsx b/components/admin/registries-admin.tsx new file mode 100644 index 0000000..0edb451 --- /dev/null +++ b/components/admin/registries-admin.tsx @@ -0,0 +1,84 @@ +import { + Button, + Card, + Flex, + Heading, + HStack, + IconButton, + useDisclosure, + VStack, +} from "@chakra-ui/react"; +import { useMyRegistries } from "@/hooks/useMyRegistries"; +import { CreateRegistryModal } from "@/components/admin/create-registry-modal"; +import { useState } from "react"; +import { AiFillEdit } from "react-icons/ai"; +import { CreateUpdateRegistryFormValues } from "@/components/forms/CreateOrUpdateRegistryForm"; +import { DeleteRegistryButton } from "@/components/admin/delete-registry-button"; + +export const RegistriesAdmin = () => { + const { + isOpen: createIsOpen, + onClose: createOnClose, + onOpen: createOnOpen, + } = useDisclosure(); + + const { data, refetch } = useMyRegistries(); + + const [selectedRegistry, setSelectedRegistry] = + useState(); + + const onModalClose = () => { + createOnClose(); + setSelectedRegistry(undefined); + }; + + return ( + + + + {data?.data?.map((registry) => ( + + + + {registry.name} + {registry.claims.map((claim) => ( + + {claim.hypercert_id} + + ))} + + + { + setSelectedRegistry({ + ...registry, + claims: registry.claims.map((claim) => ({ + hypercert_id: claim.hypercert_id, + })), + }); + createOnOpen(); + }} + aria-label="Edit registry" + icon={} + /> + + + + + ))} + + + + ); +}; diff --git a/components/admin/sidebar.tsx b/components/admin/sidebar.tsx index f43e899..8612ecc 100644 --- a/components/admin/sidebar.tsx +++ b/components/admin/sidebar.tsx @@ -16,6 +16,7 @@ import { import { FiMenu } from "react-icons/fi"; import { IconType } from "react-icons"; import Link from "next/link"; +import { useRouter } from "next/router"; interface LinkItemProps { name: string; @@ -88,6 +89,7 @@ interface NavItemProps extends FlexProps { href: string; } const NavItem = ({ icon, children, href, ...rest }: NavItemProps) => { + const { asPath } = useRouter(); return ( @@ -98,6 +100,7 @@ const NavItem = ({ icon, children, href, ...rest }: NavItemProps) => { borderRadius="lg" role="group" cursor="pointer" + fontWeight={asPath === href ? "bold" : "normal"} _hover={{ bg: "cyan.400", color: "white", diff --git a/components/alerts/connect-wallet-alert.tsx b/components/alerts/connect-wallet-alert.tsx new file mode 100644 index 0000000..9299a92 --- /dev/null +++ b/components/alerts/connect-wallet-alert.tsx @@ -0,0 +1,20 @@ +import { + Alert, + AlertDescription, + AlertIcon, + AlertTitle, +} from "@chakra-ui/alert"; + +export const ConnectWalletAlert = ({ + description, +}: { + description?: string; +}) => { + return ( + + + Connect your wallet + {description && {description}} + + ); +}; diff --git a/components/dialogs/AlertConfirmationDialog.tsx b/components/dialogs/AlertConfirmationDialog.tsx new file mode 100644 index 0000000..df316da --- /dev/null +++ b/components/dialogs/AlertConfirmationDialog.tsx @@ -0,0 +1,48 @@ +import { + AlertDialog as ChakraAlertDialog, + AlertDialogBody, + AlertDialogContent, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogOverlay, + Button, +} from "@chakra-ui/react"; +import { useRef } from "react"; +import { FocusableElement } from "@chakra-ui/utils"; +import { ModalProps } from "@chakra-ui/modal"; + +export function AlertDialog({ + onConfirm, + isOpen, + onClose, + title, +}: { onConfirm: () => void; title: string } & Omit) { + const cancelRef = useRef(null); + + return ( + + + + + {title} + + + + Are you sure? You can{"'"}t undo this action afterwards. + + + + + + + + + + ); +} diff --git a/components/forms/CreateOrUpdateHyperboardForm.tsx b/components/forms/CreateOrUpdateHyperboardForm.tsx new file mode 100644 index 0000000..0b83988 --- /dev/null +++ b/components/forms/CreateOrUpdateHyperboardForm.tsx @@ -0,0 +1,55 @@ +import { + Button, + FormControl, + FormErrorMessage, + FormLabel, + Input, +} from "@chakra-ui/react"; +import { useForm } from "react-hook-form"; +import { HyperboardInsert } from "@/types/database-entities"; + +export interface CreateOrUpdateHyperboardFormValues { + name: string; +} + +export interface CreateOrUpdateHyperboardFormProps { + onSubmitted: (values: CreateOrUpdateHyperboardFormValues) => void; + initialValues?: Partial; +} + +export const CreateOrUpdateHyperboardForm = ({ + onSubmitted, + initialValues = {}, +}: CreateOrUpdateHyperboardFormProps) => { + const { + handleSubmit, + register, + formState: { errors, isSubmitting }, + } = useForm({ + defaultValues: initialValues, + }); + return ( +
+ + Name + + + {errors.name && errors.name.message} + + + +
+ ); +}; diff --git a/components/forms/CreateOrUpdateRegistryForm.tsx b/components/forms/CreateOrUpdateRegistryForm.tsx new file mode 100644 index 0000000..c53170d --- /dev/null +++ b/components/forms/CreateOrUpdateRegistryForm.tsx @@ -0,0 +1,122 @@ +import { + Button, + FormControl, + FormErrorMessage, + FormLabel, + HStack, + Input, + Textarea, + VStack, +} from "@chakra-ui/react"; +import { + Control, + useFieldArray, + useForm, + UseFormRegister, +} from "react-hook-form"; +import { RegistryInsert } from "@/types/database-entities"; + +export interface CreateOrUpdateHyperboardFormProps { + onSubmitted: (values: CreateUpdateRegistryFormValues) => void; + initialValues?: Partial; +} + +const minimumCharacters = 40; + +export type CreateUpdateRegistryFormValues = RegistryInsert & { + claims: { hypercert_id: string }[]; +}; + +export const CreateOrUpdateRegistryForm = ({ + onSubmitted, + initialValues = {}, +}: CreateOrUpdateHyperboardFormProps) => { + const { + handleSubmit, + register, + formState: { errors, isSubmitting }, + control, + } = useForm({ + defaultValues: { claims: [], ...initialValues }, + }); + + return ( +
+ + + Name + + + {errors.name && errors.name.message} + + + + Description +