diff --git a/components/account-select.tsx b/components/account-select.tsx index 3d595432ff47041bc7fa1df4dc43686e57671886..e1eb159b85f16a2c4d9af346c02c21d9877f336a 100644 --- a/components/account-select.tsx +++ b/components/account-select.tsx @@ -4,7 +4,6 @@ import 'primereact/resources/themes/md-dark-indigo/theme.css' import 'primereact/resources/primereact.min.css' import { Dropdown } from 'primereact/dropdown' import styles from '@/styles/Home.module.css' -import { usePolkadotExtension } from '@/hooks/use-polkadot-extension'; import { usePolkadotExtensionWithContext } from '@/context/polkadotExtensionContext'; export const accountValueTemplate = (option: any, props: any) => { diff --git a/components/login.tsx b/components/login.tsx index e1217355852c763790a94a7a57eeceaa7f8cecae..695ed5e72a31ec0bd0bfa251cee6673fce0eda60 100644 --- a/components/login.tsx +++ b/components/login.tsx @@ -49,7 +49,7 @@ export default function LoginButton() { // will return a promise https://next-auth.js.org/getting-started/client#using-the-redirect-false-option const result = await signIn('credentials', { redirect: false, - callbackUrl: '/protected', + callbackUrl: '/protected-api', message: JSON.stringify(message), name: actingAccount?.meta?.name, signature, @@ -58,7 +58,7 @@ export default function LoginButton() { // take the user to the protected page if they are allowed if(result?.url) { - router.push("/protected"); + router.push("/protected-api"); } setError( result?.error ) @@ -85,7 +85,7 @@ export default function LoginButton() { { session ? <> <Link - href="/protected" + href="/protected-api" className={styles.card} > <h2 className={inter.className}> diff --git a/context/polkadotExtensionContext.tsx b/context/polkadotExtensionContext.tsx index 232b4d624083633c0b9af0df3cc62ca42bef3748..1e3e36b4e61adb5c373d85e878964e781b9fb349 100644 --- a/context/polkadotExtensionContext.tsx +++ b/context/polkadotExtensionContext.tsx @@ -6,10 +6,8 @@ import { createContext, ReactNode, useContext, - useEffect, - useState, + } from "react"; -import { InjectedAccountWithMeta } from "@polkadot/extension-inject/types"; import { usePolkadotExtension, UsePolkadotExtensionReturnType, diff --git a/hooks/use-is-mounted.ts b/hooks/use-is-mounted.ts deleted file mode 100644 index e14bbfc83472b2ec496c51bdf44c39842eec5f1c..0000000000000000000000000000000000000000 --- a/hooks/use-is-mounted.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { useEffect, useRef } from 'react'; - -export const useIsMounted = (): { readonly current: boolean } => { - const isMountedRef = useRef<boolean>(false); - - useEffect(() => { - isMountedRef.current = true; - return () => { - isMountedRef.current = false; - }; - }, []); - - return isMountedRef; -}; \ No newline at end of file diff --git a/hooks/use-polkadot-extension.ts b/hooks/use-polkadot-extension.ts index bbbf0ba568491c06a94ad9856ad0ff3f1366ec92..036053204b0cebb623a72b6f86c725e649f52a48 100644 --- a/hooks/use-polkadot-extension.ts +++ b/hooks/use-polkadot-extension.ts @@ -3,55 +3,8 @@ import { InjectedExtension, } from "@polkadot/extension-inject/types"; import { useEffect, useState } from "react"; -import { useIsMounted } from "./use-is-mounted"; import { documentReadyPromise } from "./utils"; -interface checkEnabledReturnType { - accounts: InjectedAccountWithMeta[] | null; - error: Error | null; -} - -export const extensionSetup = async () => { - const extensionDapp = await import("@polkadot/extension-dapp"); - const { web3Accounts, web3Enable, web3AccountsSubscribe } = extensionDapp; - const enabledApps = await web3Enable("polkadot-extension"); - console.log("enabled Apps", enabledApps); - if (enabledApps.length === 0) { - console.log("no extension"); - return; - } -}; - -export const checkEnabled: ( - extensionName: string -) => Promise<checkEnabledReturnType> = async ( - extensionName: string = "polkadot-extension" -) => { - const extensionDapp = await import("@polkadot/extension-dapp"); - const { web3Accounts, web3Enable } = extensionDapp; - try { - const enabledApps = await web3Enable(extensionName); - console.log("enabled Apps", enabledApps); - const w3Enabled = enabledApps.length > 0; - let accounts = null; - - if (w3Enabled) { - accounts = await web3Accounts(); - console.log("accounts", accounts); - return { accounts, error: null }; - } - - return { - accounts: null, - error: new Error( - "please allow your extension to access this dApp and refresh the page or install a substrate wallet" - ), - }; - } catch (error: any) { - return { accounts: null, error }; - } -}; - export interface UsePolkadotExtensionReturnType { isReady: boolean; accounts: InjectedAccountWithMeta[] | null; @@ -62,7 +15,6 @@ export interface UsePolkadotExtensionReturnType { } export const usePolkadotExtension = (): UsePolkadotExtensionReturnType => { - const isMounted = useIsMounted(); const [isReady, setIsReady] = useState(false); const [accounts, setAccounts] = useState<InjectedAccountWithMeta[] | null>( null @@ -77,15 +29,14 @@ export const usePolkadotExtension = (): UsePolkadotExtensionReturnType => { const actingAccount = accounts && accounts[actingAccountIdx]; useEffect(() => { - const setup = async () => { + // This effect is used to setup the browser extension + const extensionSetup = async () => { const extensionDapp = await import("@polkadot/extension-dapp"); - const { web3AccountsSubscribe, web3Enable, web3Accounts } = extensionDapp; - // const enabledApps = await web3Enable("polkadot-extension"); + const { web3AccountsSubscribe, web3Enable } = extensionDapp; const injectedPromise = documentReadyPromise(() => - web3Enable("polkadot-extension") + web3Enable("Polkadot Tokengated Website Demo") ); - // const injectedPromise = await web3Enable("polkadot-extension"); const extensions = await injectedPromise; setExtensions(extensions); @@ -95,8 +46,6 @@ export const usePolkadotExtension = (): UsePolkadotExtensionReturnType => { return; } - // const accounts = await web3Accounts(); - if (accounts) { setIsReady(true); } else { @@ -113,11 +62,13 @@ export const usePolkadotExtension = (): UsePolkadotExtensionReturnType => { }; if (!isReady) { - setup(); + extensionSetup(); } - }, [extensions, isReady]); + }, [extensions]); useEffect(() => { + // This effect is used to get the injector from the selected account + // and is triggered when the accounts or the actingAccountIdx change const getInjector = async () => { const { web3FromSource } = await import("@polkadot/extension-dapp"); const actingAccount = diff --git a/hooks/utils.ts b/hooks/utils.ts index 3ba6685f3cb2d87ee582a540b2feb1550756f201..d769be7705335d094bf95cb41d850a23d91ea884 100644 --- a/hooks/utils.ts +++ b/hooks/utils.ts @@ -1,3 +1,8 @@ +/** + * Returns a promise that resolves when the document is ready. + * @param creator + * @returns + */ export function documentReadyPromise<T>(creator: () => Promise<T>): Promise<T> { return new Promise((resolve): void => { if (document.readyState === "complete") { diff --git a/pages/api/auth.ts b/pages/api/auth.ts new file mode 100644 index 0000000000000000000000000000000000000000..4f0f98077dc5505698403b5cf56ee8ef39c7e5ee --- /dev/null +++ b/pages/api/auth.ts @@ -0,0 +1,22 @@ +import { getToken } from "next-auth/jwt" + +import type { NextApiRequest, NextApiResponse } from "next" + +export default async function handler( + req: NextApiRequest, + res: NextApiResponse +) { + // If you don't have the NEXTAUTH_SECRET environment variable set, + // you will have to pass your secret as `secret` to `getToken` + const token = await getToken({ req, secret: process.env.NEXTAUTH_SECRET }) + console.log( 'token', token) + if (token) { + // Signed in + console.log("JSON Web Token", JSON.stringify(token, null, 2)) + res.send(JSON.stringify(token, null, 2)) + } else { + // Not Signed in + res.status(401) + } + res.end() +} \ No newline at end of file diff --git a/pages/api/auth/[...nextauth].ts b/pages/api/auth/[...nextauth].ts index 954efff19dbd0118e6af8db2b51f258856a822c9..664395fb2276018d06f03516d5da5242c1d1c43c 100644 --- a/pages/api/auth/[...nextauth].ts +++ b/pages/api/auth/[...nextauth].ts @@ -1,4 +1,4 @@ -import NextAuth, { NextAuthOptions } from 'next-auth'; +import NextAuth, { NextAuthOptions, User } from 'next-auth'; import CredentialsProvider from 'next-auth/providers/credentials'; import { signatureVerify } from '@polkadot/util-crypto'; import { encodeAddress } from '@polkadot/keyring'; @@ -57,7 +57,7 @@ export const authOptions: NextAuthOptions = { placeholder: 'name', }, }, - async authorize(credentials): Promise<any | null> { + async authorize(credentials): Promise<User | null> { if (credentials === undefined) { return null; } @@ -65,25 +65,23 @@ export const authOptions: NextAuthOptions = { const message = JSON.parse(credentials.message); //verify the message is from the same domain - console.log(message.uri, message.uri); - console.log('process.env.NEXTAUTH_URL', process.env.NEXTAUTH_URL); if (message.uri !== process.env.NEXTAUTH_URL) { return Promise.reject(new Error('🚫 You shall not pass!')); } // verify the message was not compromised - console.log(message.nonce, message.nonce); - console.log('credentials.csrfToken', credentials.csrfToken); if (message.nonce !== credentials.csrfToken) { return Promise.reject(new Error('🚫 You shall not pass!')); } // verify signature of the message + // highlight-start const { isValid } = signatureVerify( credentials.message, credentials.signature, credentials.address, ); + // highlight-end if (!isValid) { return Promise.reject(new Error('🚫 Invalid Signature')); @@ -98,6 +96,7 @@ export const authOptions: NextAuthOptions = { if (credentials?.address) { const ksmAddress = encodeAddress(credentials.address, 2); + // highlight-start const accountInfo = await api.query.system.account(ksmAddress); if (accountInfo.data.free.gt(new BN(1_000_000_000_000))) { @@ -111,6 +110,7 @@ export const authOptions: NextAuthOptions = { } else { return Promise.reject(new Error('🚫 The gate is closed for you')); } + // highlight-end } return Promise.reject(new Error('🚫 API Error')); diff --git a/pages/index.tsx b/pages/index.tsx index cf0456ac19d85baf1e17785ef7bd130056c092f5..bb426fef06253cb001f665e7927ad13dbf12a40b 100644 --- a/pages/index.tsx +++ b/pages/index.tsx @@ -55,7 +55,13 @@ export default function Home() { href="/protected" rel="noopener noreferrer" > - 🔠Go to /protected + 🔠Go to /protected (SSR) + </Link> + <Link + href="/protected-api" + rel="noopener noreferrer" + > + 🔠Go to /protected-api (Static) </Link> </div> </main> diff --git a/pages/protected-api.tsx b/pages/protected-api.tsx new file mode 100644 index 0000000000000000000000000000000000000000..a6087a900be585b3b2aadb234faf83e4a5ac1deb --- /dev/null +++ b/pages/protected-api.tsx @@ -0,0 +1,57 @@ +import { useState, useEffect } from "react" +import { useSession } from "next-auth/react" +import styles from '@/styles/Home.module.css' +import Link from "next/link" +import { formatBalance } from '@polkadot/util'; + +import PolkadotParticles from "@/components/polkadot-particles" + +/** + * This is a protected page, it can only be accessed by authenticated users. Instead of using Server Side + * Rendering (SSR) to fetch the content, we use Static Generation and a protected API Route `/pages/api/auth.ts` + * to fetch the content client side after the user has logged in. + */ +export default function ProtectedPage() { + const { data: session } = useSession() + const [content, setContent] = useState() + + // Fetch content from protected route + useEffect(() => { + const fetchData = async () => { + const res = await fetch("/api/auth") + + if (res.ok) { + const json = await res.json() + if (json?.content) { + setContent(json.content) + } + } + } + fetchData() + }, [session]) + + + // If no session exists, display access denied message + if (!session) { + return ( + <main className={ styles.protected }> + <h1>You did not pass the 🚪</h1> + <p><Link href="/" className={ styles.colorA }>< go back</Link></p> + </main> + ) + } + + // format the big number to a human readable format + const ksmBalance = formatBalance( session.freeBalance, { decimals: 12, withSi: true, withUnit: 'KSM' } ) + + // If session exists, display content + return ( + <main className={ styles.protected }> + <h1>🎉 Welcome { session.user?.name }, you passed the 🚪</h1> + <p>with { ksmBalance }</p> + <p>You are seeing a protected route that uses a static page and a protected api route. See the code at <code>/pages/protected.tsx</code></p> + <p> <Link href="/" className={ styles.colorA }>< go back</Link></p> + <PolkadotParticles /> + </main> + ) +} \ No newline at end of file diff --git a/pages/protected.tsx b/pages/protected.tsx index cd34d9cbce5c91bbaced5460ae1476673bc0ec01..2e49e64af640134e8255acd015a53594ae5fa18c 100644 --- a/pages/protected.tsx +++ b/pages/protected.tsx @@ -5,9 +5,7 @@ import { authOptions } from "./api/auth/[...nextauth]" import { BN, formatBalance, BN_ZERO } from '@polkadot/util'; import { GetServerSideProps } from 'next' - import styles from '@/styles/Home.module.css' - import PolkadotParticles from "@/components/polkadot-particles" export default function Admin( { freeBalance } : { freeBalance : BN } ) : JSX.Element { @@ -34,16 +32,14 @@ export default function Admin( { freeBalance } : { freeBalance : BN } ) : JSX.El <main className={ styles.protected }> <h1>🎉 Welcome { session.user?.name }, you passed the 🚪</h1> <p>with { ksmBalance }</p> - <p>You are seeing a /protected route. <Link href="/" className={ styles.colorA }>< go back</Link></p> + <p>You are seeing a /protected route that uses Server-Side Generation at <code>/pages/protected.tsx</code> </p> + <p><Link href="/" className={ styles.colorA }>< go back</Link></p> <PolkadotParticles /> </main> ) } -type Data = { - freeBalance: BN, -} - +// this tells next to render the page on the server side export const getServerSideProps: GetServerSideProps = async (context) => { // Get the user's session based on the request diff --git a/styles/Home.module.css b/styles/Home.module.css index 12795aaf54b0b3aafb1edab755ef5be8bb7972a7..72ce5b5ad31e8a9a46203195d7f8323212f2a227 100644 --- a/styles/Home.module.css +++ b/styles/Home.module.css @@ -17,6 +17,7 @@ width: 100%; z-index: 2; font-family: var(--font-mono); + flex-wrap: wrap; } .description a { @@ -24,6 +25,7 @@ justify-content: center; align-items: center; gap: 0.5rem; + padding: 0.5rem; } .description p {