Skip to content

Commit

Permalink
Admin and siwe auth (#14)
Browse files Browse the repository at this point in the history
  • Loading branch information
damianmarti authored Aug 5, 2024
1 parent 454f131 commit 3380069
Show file tree
Hide file tree
Showing 16 changed files with 514 additions and 16 deletions.
3 changes: 2 additions & 1 deletion packages/nextjs/.env.development
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@

POSTGRES_URL="postgresql://postgres:mysecretpassword@localhost:5432/postgres"
NEXTAUTH_URL=http://localhost:3000
NEXTAUTH_SECRET=somereallysecretsecret
39 changes: 39 additions & 0 deletions packages/nextjs/app/admin/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import type { NextPage } from "next";
import { getServerSession } from "next-auth";
import { Address } from "~~/components/scaffold-eth";
import { getAllSubmissions } from "~~/services/database/repositories/submissions";
import { authOptions } from "~~/utils/auth";

const Admin: NextPage = async () => {
const session = await getServerSession(authOptions);

if (session?.user?.role !== "admin") {
return <div className="flex items-center text-xl flex-col flex-grow pt-10 space-y-4">Access denied</div>;
}

const submissions = await getAllSubmissions();
return (
<>
<div className="flex items-center flex-col flex-grow pt-10 space-y-4">
{submissions.map(submission => {
return (
<div key={submission.id} className="card bg-primary text-primary-content">
<div className="card-body">
<h2 className="card-title">{submission.title}</h2>
{submission.linkToRepository && (
<a href={submission.linkToRepository} className="link" target="_blank">
{submission.linkToRepository}
</a>
)}
<p>{submission.description}</p>
{submission.builder && <Address address={submission.builder} />}
</div>
</div>
);
})}
</div>
</>
);
};

export default Admin;
18 changes: 18 additions & 0 deletions packages/nextjs/app/api/auth/[...nextauth]/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { NextRequest, NextResponse } from "next/server";
import NextAuth from "next-auth";
import { authOptions, providers } from "~~/utils/auth";

// For more information on each option (and a full list of options) go to
// https://next-auth.js.org/configuration/options
async function auth(req: NextRequest, res: NextResponse) {
const { searchParams } = new URL(req.url as string);
const isDefaultSigninPage = searchParams.get("nextauth")?.includes("signin");

if (isDefaultSigninPage) {
providers.pop();
}

return await NextAuth(req, res as unknown as { params: { nextauth: string[] } }, authOptions);
}

export { auth as GET, auth as POST };
38 changes: 38 additions & 0 deletions packages/nextjs/app/siwe/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
"use client";

import { useEffect } from "react";
import { useRouter } from "next/navigation";
import { useConnectModal } from "@rainbow-me/rainbowkit";
import { useAccount } from "wagmi";
import { useAuthSession } from "~~/hooks/useAuthSession";
import { useHandleLogin } from "~~/hooks/useHandleLogin";

export default function Siwe() {
const { isConnected } = useAccount();
const { openConnectModal } = useConnectModal();
const { handleLogin } = useHandleLogin();
const { isAuthenticated } = useAuthSession();
const router = useRouter();

useEffect(() => {
if (isAuthenticated) {
router.push("/");
}
}, [router, isAuthenticated]);

return (
<button
className="btn btn-primary my-10 self-center"
onClick={e => {
e.preventDefault();
if (!isConnected) {
openConnectModal?.();
} else {
handleLogin();
}
}}
>
Sign in with Ethereum
</button>
);
}
25 changes: 24 additions & 1 deletion packages/nextjs/components/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@ import React, { useCallback, useRef, useState } from "react";
import Image from "next/image";
import Link from "next/link";
import { usePathname } from "next/navigation";
import { Bars3Icon, BugAntIcon } from "@heroicons/react/24/outline";
import { Bars3Icon, BugAntIcon, LockClosedIcon } from "@heroicons/react/24/outline";
import { FaucetButton, RainbowKitCustomConnectButton } from "~~/components/scaffold-eth";
import { useOutsideClick } from "~~/hooks/scaffold-eth";
import { useAuthSession } from "~~/hooks/useAuthSession";

type HeaderMenuLink = {
label: string;
Expand All @@ -23,6 +24,15 @@ export const menuLinks: HeaderMenuLink[] = [
label: "Submit",
href: "/submit",
},
{
label: "Admin",
href: "/admin",
icon: <LockClosedIcon className="h-4 w-4" />,
},
{
label: "Siwe",
href: "/siwe",
},
{
label: "Debug Contracts",
href: "/debug",
Expand All @@ -32,10 +42,23 @@ export const menuLinks: HeaderMenuLink[] = [

export const HeaderMenuLinks = () => {
const pathname = usePathname();
const { isAdmin, data: session, isAuthenticated } = useAuthSession();

return (
<>
{menuLinks.map(({ label, href, icon }) => {
if (!session && label !== "Siwe") {
return null;
}

if (isAuthenticated && label === "Siwe") {
return null;
}

if (label === "Admin" && !isAdmin) {
return null;
}

const isActive = pathname === href;
return (
<li key={href}>
Expand Down
24 changes: 15 additions & 9 deletions packages/nextjs/components/ScaffoldEthAppWithProviders.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@

import { useEffect, useState } from "react";
import { RainbowKitProvider, darkTheme, lightTheme } from "@rainbow-me/rainbowkit";
import { RainbowKitSiweNextAuthProvider } from "@rainbow-me/rainbowkit-siwe-next-auth";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { SessionProvider } from "next-auth/react";
import { useTheme } from "next-themes";
import { Toaster } from "react-hot-toast";
import { WagmiProvider } from "wagmi";
Expand Down Expand Up @@ -47,15 +49,19 @@ export const ScaffoldEthAppWithProviders = ({ children }: { children: React.Reac

return (
<WagmiProvider config={wagmiConfig}>
<QueryClientProvider client={queryClient}>
<ProgressBar />
<RainbowKitProvider
avatar={BlockieAvatar}
theme={mounted ? (isDarkMode ? darkTheme() : lightTheme()) : lightTheme()}
>
<ScaffoldEthApp>{children}</ScaffoldEthApp>
</RainbowKitProvider>
</QueryClientProvider>
<SessionProvider>
<QueryClientProvider client={queryClient}>
<ProgressBar />
<RainbowKitSiweNextAuthProvider>
<RainbowKitProvider
avatar={BlockieAvatar}
theme={mounted ? (isDarkMode ? darkTheme() : lightTheme()) : lightTheme()}
>
<ScaffoldEthApp>{children}</ScaffoldEthApp>
</RainbowKitProvider>
</RainbowKitSiweNextAuthProvider>
</QueryClientProvider>
</SessionProvider>
</WagmiProvider>
);
};
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { useRef, useState } from "react";
import { NetworkOptions } from "./NetworkOptions";
import { signOut } from "next-auth/react";
import CopyToClipboard from "react-copy-to-clipboard";
import { getAddress } from "viem";
import { Address } from "viem";
Expand Down Expand Up @@ -125,7 +126,10 @@ export const AddressInfoDropdown = ({
<button
className="menu-item text-error btn-sm !rounded-xl flex gap-3 py-3"
type="button"
onClick={() => disconnect()}
onClick={() => {
disconnect();
signOut();
}}
>
<ArrowLeftOnRectangleIcon className="h-6 w-4 ml-2 sm:ml-0" /> <span>Disconnect</span>
</button>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
"use client";

// @refresh reset
import { useEffect } from "react";
import { Balance } from "../Balance";
import { AddressInfoDropdown } from "./AddressInfoDropdown";
import { AddressQRCodeModal } from "./AddressQRCodeModal";
import { WrongNetworkDropdown } from "./WrongNetworkDropdown";
import { ConnectButton } from "@rainbow-me/rainbowkit";
import { signOut } from "next-auth/react";
import { Address } from "viem";
import { useAccount } from "wagmi";
import { useNetworkColor } from "~~/hooks/scaffold-eth";
import { useTargetNetwork } from "~~/hooks/scaffold-eth/useTargetNetwork";
import { useAuthSession } from "~~/hooks/useAuthSession";
import { getBlockExplorerAddressLink } from "~~/utils/scaffold-eth";

/**
Expand All @@ -17,6 +21,14 @@ import { getBlockExplorerAddressLink } from "~~/utils/scaffold-eth";
export const RainbowKitCustomConnectButton = ({ fullWidth }: { fullWidth?: boolean }) => {
const networkColor = useNetworkColor();
const { targetNetwork } = useTargetNetwork();
const { address, isConnected } = useAccount();
const { address: sessionAddress } = useAuthSession();

useEffect(() => {
if (isConnected && sessionAddress && sessionAddress !== address) {
signOut();
}
}, [address, isConnected, sessionAddress]);

return (
<ConnectButton.Custom>
Expand Down
11 changes: 11 additions & 0 deletions packages/nextjs/hooks/useAuthSession.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { UseSessionOptions, useSession } from "next-auth/react";

export const useAuthSession = <R extends boolean>(options?: UseSessionOptions<R>) => {
const sessionData = useSession(options);

const isAdmin = sessionData?.data?.user?.role === "admin";
const address = sessionData?.data?.user?.address;
const isAuthenticated = sessionData.status === "authenticated";

return { ...sessionData, isAdmin, address, isAuthenticated };
};
34 changes: 34 additions & 0 deletions packages/nextjs/hooks/useHandleLogin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { useCallback } from "react";
import { getCsrfToken, signIn } from "next-auth/react";
import { SiweMessage } from "siwe";
import { useAccount, useSignMessage } from "wagmi";

export const useHandleLogin = () => {
const { signMessageAsync } = useSignMessage();
const { address, chain } = useAccount();

const handleLogin = useCallback(async () => {
try {
const message = new SiweMessage({
domain: window.location.host,
address: address,
statement: "Sign in with Ethereum to the app.",
uri: window.location.origin,
version: "1",
chainId: chain?.id,
nonce: await getCsrfToken(),
});
const signature = await signMessageAsync({
message: message.prepareMessage(),
});
signIn("credentials", {
message: JSON.stringify(message),
signature,
});
} catch (error) {
console.log(error);
}
}, [address, chain?.id, signMessageAsync]);

return { handleLogin };
};
23 changes: 23 additions & 0 deletions packages/nextjs/middleware.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
import { getToken } from "next-auth/jwt";

const SIGN_IN_PAGE = "/siwe";

export async function middleware(request: NextRequest) {
const token = await getToken({ req: request, secret: process.env.NEXTAUTH_SECRET });

// If no token and the request is not for the sign-in page, redirect to the sign-in page
if (!token && request.nextUrl.pathname !== SIGN_IN_PAGE) {
const url = request.nextUrl.clone();
url.pathname = SIGN_IN_PAGE;
return NextResponse.redirect(url);
}

// If a token is found or the request is for the sign-in page, proceed as normal
return NextResponse.next();
}

export const config = {
matcher: "/((?!api|static|.*\\..*|_next).*)",
};
4 changes: 4 additions & 0 deletions packages/nextjs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
"dependencies": {
"@heroicons/react": "~2.0.11",
"@rainbow-me/rainbowkit": "2.1.2",
"@rainbow-me/rainbowkit-siwe-next-auth": "^0.4.1",
"@tanstack/react-query": "~5.28.6",
"@uniswap/sdk-core": "~4.0.1",
"@uniswap/v2-sdk": "~3.0.1",
Expand All @@ -26,7 +27,9 @@
"daisyui": "4.5.0",
"dotenv": "^16.4.5",
"drizzle-orm": "^0.32.1",
"ethers": "^6.13.1",
"next": "~14.0.4",
"next-auth": "^4.24.7",
"next-themes": "~0.2.1",
"nprogress": "~0.2.0",
"pg": "^8.12.0",
Expand All @@ -35,6 +38,7 @@
"react-copy-to-clipboard": "~5.1.0",
"react-dom": "~18.2.0",
"react-hot-toast": "~2.4.0",
"siwe": "^2.3.2",
"use-debounce": "~8.0.4",
"usehooks-ts": "2.13.0",
"viem": "2.17.4",
Expand Down
2 changes: 1 addition & 1 deletion packages/nextjs/services/database/repositories/builders.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,5 @@ export async function getBuilderById(id: string) {
}

export async function createBuilder(builder: BuilderInsert) {
return await db.insert(builders).values(builder);
return await db.insert(builders).values(builder).returning({ id: builders.id, role: builders.role });
}
23 changes: 23 additions & 0 deletions packages/nextjs/types/next-auth/next-auth.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import "next-auth";
import { DefaultUser } from "next-auth";
import { DefaultJWT } from "next-auth/jwt";

declare module "next-auth" {
export interface Session {
user: {
address?: string | null;
role?: string | null;
};
expires: ISODateString;
}

export interface User extends DefaultUser {
role?: string | null;
address?: string | null;
}

export interface JWT extends DefaultJWT {
role?: string | null;
address?: string | null;
}
}
Loading

0 comments on commit 3380069

Please sign in to comment.