{"id":15374417,"url":"https://github.com/warengonzaga/tw-gated-site","last_synced_at":"2026-03-18T01:30:19.484Z","repository":{"id":92764211,"uuid":"524161565","full_name":"warengonzaga/tw-gated-site","owner":"warengonzaga","description":null,"archived":false,"fork":false,"pushed_at":"2022-08-12T16:55:19.000Z","size":363,"stargazers_count":1,"open_issues_count":0,"forks_count":0,"subscribers_count":3,"default_branch":"main","last_synced_at":"2024-04-14T00:25:39.414Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"tw-gated-site.vercel.app","language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/warengonzaga.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE.md","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null}},"created_at":"2022-08-12T16:50:36.000Z","updated_at":"2024-04-06T20:53:41.000Z","dependencies_parsed_at":"2023-03-14T23:30:55.106Z","dependency_job_id":null,"html_url":"https://github.com/warengonzaga/tw-gated-site","commit_stats":{"total_commits":2,"total_committers":1,"mean_commits":2.0,"dds":0.0,"last_synced_commit":"470ed69fc0833c2b5ab5b9dca669fb2514be83c6"},"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/warengonzaga%2Ftw-gated-site","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/warengonzaga%2Ftw-gated-site/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/warengonzaga%2Ftw-gated-site/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/warengonzaga%2Ftw-gated-site/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/warengonzaga","download_url":"https://codeload.github.com/warengonzaga/tw-gated-site/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":239817823,"owners_count":19702021,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-10-01T13:58:44.882Z","updated_at":"2026-03-18T01:30:19.419Z","avatar_url":"https://github.com/warengonzaga.png","language":"JavaScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# NFT Gated Website\n\nThis project demonstrates how you can restrict content on your website to only those users who own an NFT from your collection.\n\nWe use an [Edition Drop](https://portal.thirdweb.com/pre-built-contracts/edition-drop) contract to enable users to claim one of the NFTs, and serve users\nthe restricted content if they have at least one of the NFTs claimed.\n\n## Tools:\n\n- [React SDK](https://docs.thirdweb.com/react): To access the connected wallet, switch the user's network, and claim an NFT from our Edition Drop collection.\n- [Auth](https://portal.thirdweb.com/building-web3-apps/authenticating-users): To ask users to sign a message and verify they own the wallet they claim to be, while on the server-side.\n\n## Using This Template\n\nCreate a project using this example:\n\n```bash\nnpx thirdweb create --nft-gated-website\n```\n\n- Create an [Edition Drop](https://thirdweb.com/contracts/new/pre-built/drop/edition-drop) contract using the dashboard.\n- Update the information in the [yourDetails.js](./const/yourDetails.js) file to use your contract address and auth domain name.\n- Add your wallet's private key as an environment variable in a `.env.local` file called `PRIVATE_KEY`:\n\n```text title=\".env.local\"\nPRIVATE_KEY=your-wallet-private-key\n```\n\n## How It Works\n\nUsing [Auth](https://portal.thirdweb.com/building-web3-apps/authenticating-users), we can verify a user's identity on the server-side, by asking them to sign a message and verify they own the wallet they claim to be, and validating the signature.\n\nWhen we verified the user's identity on the server-side, we check their wallet to see if they have an NFT from our collection. We can then serve different content and restrict what pages they can access based on their balance.\n\n## Restricting Access\n\nTo begin with, the user will reach the website with no authentication.\n\nWhen they try to access the restricted page (the `/` route), we use [getServerSideProps](https://nextjs.org/docs/basic-features/data-fetching/get-server-side-props) to check two things:\n\n1. If the user is currently authenticated (if they have a **valid** `access_token` cookie).\n2. If the user's wallet balance is greater than 0 of the NFTs in our NFT collection.\n\nIf either of these checks is `false`, we redirect the user to the `/login` page before they are allowed to access the restricted page.\n\nLet's break that down into steps:\n\n### Checking For Authentication Token\n\nFirst, we check if this user has already been authenticated.\n\nIf this is the first time the user has visited the website, they will not have an `access_token` cookie.\n\n```js\n// This gets called on every request\nexport async function getServerSideProps(context) {\n  // Check to see if they have an authentication cookie\n  const parsedCookies = cookie?.parse(context?.req?.headers?.cookie || \"\");\n  const authToken = parsedCookies?.[\"access_token\"];\n\n  // if there is no auth token, redirect them to the login page\n  if (!authToken) {\n    return {\n      redirect: {\n        destination: \"/login\",\n        permanent: false,\n      },\n    };\n  }\n\n  // ...\n}\n```\n\nIf the user is not authenticated, then we don't check the user's wallet balance; we just immediately redirect them to the `/login` page.\n\nIf there _is_ an authentication token in this user's cookies, we need to **validate** that token is legitimate:\n\n```js\n// Instantiate our SDK\nconst PRIVATE_KEY = process.env.PRIVATE_KEY;\nconst sdk = ThirdwebSDK.fromPrivateKey(PRIVATE_KEY, \"mumbai\");\n\n// Authenticate token with the SDK\nconst domain = domainName;\nconst address = await sdk.auth.authenticate(domain, authToken);\n```\n\nOnce again, if the token is not valid, then we redirect the user to the `/login` page.\n\n```js\n// If the auth token is invalid, redirect them to the login page\nif (!address) {\n  return {\n    redirect: {\n      destination: \"/login\",\n      permanent: false,\n    },\n  };\n}\n```\n\n### Checking Wallet Balance\n\nNow we're ready to check the user's wallet balance.\n\nTo do this, we have created a utility function called [checkBalance](./util/checkBalance.js) that we can use to check the user's balance for a given NFT.\n\n```js\nimport { contractAddress } from \"../const/yourDetails\";\n\nexport default async function checkBalance(sdk, address) {\n  const editionDrop = sdk.getEditionDrop(\n    contractAddress // replace this with your contract address\n  );\n\n  const balance = await editionDrop.balanceOf(address, 0);\n\n  // gt = greater than\n  return balance.gt(0);\n}\n```\n\nThis function returns true or false that we can store in a variable:\n\n```js\nconst hasNft = await checkBalance(sdk, address);\n```\n\nHere's our final check, if the user has a `balance` of `0`, then we redirect them to the `/login` page.\n\n```js\n// If they don't have an NFT, redirect them to the login page\nif (!hasNft) {\n  return {\n    redirect: {\n      destination: \"/login\",\n      permanent: false,\n    },\n  };\n}\n```\n\nIf the user gets past these checks, then we allow them to view the restricted page.\n\n```js\n// Finally, return the props\nreturn {\n  props: {},\n};\n```\n\n## Signing In\n\nWe've now successfully restricted access to our home page, now let's explore the `/login` page.\n\nFirst, we ask the user to connect their wallet with our `useMetaMask` hook:\n\n```js\nconst connectWithMetamask = useMetamask();\n\n// ...\n\n\u003cbutton onClick={() =\u003e connectWithMetamask()}\u003eConnect Wallet\u003c/button\u003e;\n```\n\nOnce an `address` is detected from the `useAddress` hook, we show them the `Sign In` button:\n\n```js\n{\n  address ? (\n    \u003c\u003e\n      \u003cbutton onClick={signIn}\u003eSign In\u003c/button\u003e\n    \u003c/\u003e\n  ) : (\n    \u003c\u003e\n      \u003cbutton onClick={() =\u003e connectWithMetamask()}\u003eConnect Wallet\u003c/button\u003e\n    \u003c/\u003e\n  );\n}\n```\n\nThe `Sign In` button calls the `signIn` function, which:\n\n1.  Asks the user to sign a message, and creates a **login payload** with that signature.\n2.  Redirects the user to our API route [api/login](./pages/api/login.js), and sends the login payload as a query parameter.\n\n```js\n// Function to make a request to our /api/get-restricted-content route to check if we own an NFT.\nasync function signIn() {\n  // Add the domain of the application users will login to, this will be used throughout the login process\n  const domain = domainName;\n  // Generate a signed login payload for the connected wallet to authenticate with\n  const payload = await sdk.auth.login(domain);\n\n  // Make api request to server\n  window.location = `/api/login?payload=${JSON.stringify(payload)}`;\n}\n```\n\n### Generating Auth Tokens\n\nOn the [api/login](./pages/api/login.js) route, we:\n\n1. Read in the login payload sent as a query parameter.\n\n```js\n// Get signed login payload from the frontend\nconst payload = JSON.parse(req.query.payload);\n```\n\n2. Verify the login payload\n\n```js\n// Generate an access token with the SDK using the signed payload\nconst domain = domainName;\n// Verify the token and get the address, so we can check their NFT balance\nconst address = sdk.auth.verify(domain, payload);\n```\n\n3. If the login payload is valid, check the user's wallet balance.\n\n```js\nconst hasNft = await checkBalance(sdk, address);\n```\n\n4. If the user has an NFT, create an authentication token for them.\n\n```js\n// At this point, the user has authenticated and owns at least 1 NFT.\n// Generate an auth token for them\nconst token = await sdk.auth.generateAuthToken(domain, payload);\n```\n\n5. Set the authentication token as a cookie\n\n```js\n// Securely set httpOnly cookie on request to prevent XSS on frontend\n// And set path to / to enable access_token usage on all endpoints\nres.setHeader(\n  \"Set-Cookie\",\n  serialize(\"access_token\", token, {\n    path: \"/\",\n    httpOnly: true,\n    secure: true,\n    sameSite: \"strict\",\n  })\n);\n```\n\n6. Redirect the user to the homepage\n\n```js\nres.redirect(\"/\", 302);\n```\n\nIf you recall, in the `getServerSideProps` of the home page, we check for this `access_token` cookie. This means if the user refreshes the page while this cookie is still valid, they will be able to view the restricted page _without_ signing in again; assuming they haven't transferred their NFT from this wallet.\n\n### Sign Out\n\nFinally, on the home page, we have a `Sign Out` button for the user, which clears their cookie by sending the user to our [/api/logout](./pages/api/logout.js) route, then sending them back to the login page.\n\n```js\n// Set the access token to 'none' and expire in 5 seconds\nres.setHeader(\n  \"Set-Cookie\",\n  serialize(\"access_token\", \"none\", {\n    path: \"/\",\n    expires: new Date(Date.now() + 5 * 1000),\n  })\n);\n\nres.redirect(\"/login\", 302);\n```\n\n## Join our Discord!\n\nFor any questions, suggestions, join our discord at [https://discord.gg/thirdweb](https://discord.gg/thirdweb).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fwarengonzaga%2Ftw-gated-site","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fwarengonzaga%2Ftw-gated-site","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fwarengonzaga%2Ftw-gated-site/lists"}