😍 Auth with clerk and NextAuth.js (ok)
Last updated
Was this helpful?
Last updated
Was this helpful?
https://viblo.asia/p/cung-minh-tao-boilerplate-cho-du-an-nextjs-v12-phan-2-i18n-va-nextauthjs-zOQJwQYkVMP
https://viblo.asia/p/authentication-google-one-tap-nextjs-14-va-nestjs-aNj4vvAx46r
https://www.youtube.com/watch?v=n-fVrzaikBQ
.env.local
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_ZWxlY3RyaWMtZXdlLTEyLmNsZXJrLmFjY291bnRzLmRldiQ
CLERK_SECRET_KEY=sk_test_LuBQm3aL6TPFMa3NjvmXAdy9ZNE9rJRRDsp8zBSXv6
NEXT_PUBLIC_CLERK_SIGN_IN_URL=/sign-in
NEXT_PUBLIC_CLERK_SIGN_UP_URL=/sign-up
NEXT_PUBLIC_CLERK_AFTER_SIGN_IN_URL=/dashboard
NEXT_PUBLIC_CLERK_AFTER_SIGN_UP_URL=/dashboard
src\middleware.ts
import { authMiddleware } from "@clerk/nextjs";
// This example protects all routes including api/trpc routes
// Please edit this to allow other routes to be public as needed.
// See https://clerk.com/docs/references/nextjs/auth-middleware for more information about configuring your Middleware
export default authMiddleware({
publicRoutes: ['/', '/about']
});
export const config = {
matcher: ["/((?!.+\\.[\\w]+$|_next).*)", "/", "/(api|trpc)(.*)"],
};
src\components\header.tsx
import { UserButton, auth } from '@clerk/nextjs';
import Link from 'next/link';
export default async function Header() {
const { userId } = auth();
return (
<div className='bg-gray-600 text-neutral-100'>
<div className='container mx-auto flex items-center justify-between py-4'>
<Link href='/'>Home</Link>
<div>
{userId ? (
<div className='flex gap-4 items-center'>
<Link href='/dashboard'>Dashboard</Link>
<UserButton afterSignOutUrl='/' />
</div>
) : (
<div className='flex gap-4 items-center'>
<Link href='/sign-up'>Sign up</Link>
<Link href='/sign-in'>Sign In</Link>
</div>
)}
</div>
</div>
</div>
);
}
src\app\page.tsx
export default function Home() {
return (
<div>
<h1 className='font-bold text-center mt-10'>Home page</h1>
</div>
);
}
src\app\layout.tsx
import type { Metadata } from 'next';
import { Inter } from 'next/font/google';
import './globals.css';
import { ClerkProvider } from '@clerk/nextjs';
import Header from '@/components/header';
const inter = Inter({ subsets: ['latin'] });
export const metadata: Metadata = {
title: 'Create Next App',
description: 'Generated by create next app',
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang='en'>
<body className={inter.className}>
<ClerkProvider>
<Header />
{children}
</ClerkProvider>
</body>
</html>
);
}
src\app\sign-up\[[...sign-up]]\page.tsx
import { SignUp } from '@clerk/nextjs';
export default function Page() {
return (
<div className='flex items-center justify-center flex-col gap-10'>
<h1 className='text-4xl font-bold mt-20'>This is signup page</h1>
<SignUp />
</div>
);
}
src\app\sign-in[[...sign-in]]\page.tsx
import { SignIn } from '@clerk/nextjs';
export default function Page() {
return (
<div className='flex items-center justify-center flex-col gap-10'>
<h1 className='text-4xl font-bold mt-20'>This is signin page</h1>
<SignIn />
</div>
);
}
src\app\dashboard\page.tsx
import { auth, currentUser } from '@clerk/nextjs';
export default async function DashboardPage() {
const { userId } = auth();
const user = await currentUser();
if (!userId || !user) {
return <div>You are not logged in</div>;
}
return (
<div className='mt-10 text-start max-w-xl mx-auto bg-neutral-200 p-5 rounded'>
<h1 className='text-4xl font-bold'>Welcome</h1>
<ul className='list-none mt-10'>
<li className='mb-2'>
<span className='font-semibold'>First Name:</span> {user.firstName}
</li>
<li className='mb-2'>
<span className='font-semibold'>Last Name:</span> {user.lastName}
</li>
<li className='mb-2'>
<span className='font-semibold'>Email:</span>{' '}
{user.emailAddresses[0].emailAddress}
</li>
</ul>
</div>
);
}
src\app\about\page.tsx
export default function AboutPage() {
return <div>AboutPage</div>;
}
https://github.com/codegenixdev/auth-nextjs-tutorial/tree/starter
package.json
{
"name": "auth-nextjs",
"version": "0.1.0",
"private": true,
"prisma": {
"schema": "src/lib/db/schema.prisma"
},
"scripts": {
"dev": "next dev --turbopack",
"build": "next build",
"start": "next start",
"lint": "next lint",
"db:migrate": "npx prisma migrate dev",
"db:reset": "prisma migrate reset && prisma migrate dev",
"db:studio": "prisma studio"
},
"dependencies": {
"@auth/prisma-adapter": "^2.7.4",
"@prisma/client": "^6.1.0",
"@radix-ui/react-slot": "^1.1.1",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"lucide-react": "^0.469.0",
"next": "15.1.3",
"next-auth": "^5.0.0-beta.25",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"tailwind-merge": "^2.6.0",
"tailwindcss-animate": "^1.0.7",
"uuid": "^11.0.3",
"zod": "^3.24.1"
},
"devDependencies": {
"@eslint/eslintrc": "^3",
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
"eslint": "^9",
"eslint-config-next": "15.1.3",
"postcss": "^8",
"prisma": "^6.1.0",
"tailwindcss": "^3.4.1",
"typescript": "^5"
}
}
.env
AUTH_SECRET="Dz5oI7sPE0Os/Xn7sxhU9tQGlSeG9J9f52l56Yk97Ro=" # Added by `npx auth`. Read more: https://cli.authjs.dev
AUTH_GITHUB_ID=Ov23lir84nNhYsRt3vvZ
AUTH_GITHUB_SECRET=41355bd062ff5433da463defcf069ece92567995
DATABASE_URL="file:./dev.db"
AUTH_TRUST_HOST=true
src\components\sign-out.tsx
"use client";
import { Button } from "@/components/ui/button";
import { signOut } from "next-auth/react";
const SignOut = () => {
const handleSignOut = async () => {
await signOut();
};
return (
<div className="flex justify-center">
<Button variant="destructive" onClick={handleSignOut}>
Sign Out
</Button>
</div>
);
};
export { SignOut };
src\components\github-sign-in.tsx
import { signIn } from "@/lib/auth";
import { Button } from "@/components/ui/button";
import { Github } from "@/components/ui/github";
const GithubSignIn = () => {
return (
<form
action={async () => {
"use server";
await signIn("github");
}}
>
<Button className="w-full" variant="outline">
<Github />
Continue with GitHub
</Button>
</form>
);
};
export { GithubSignIn };
src\components\ui\button.tsx
import * as React from "react";
import { Slot } from "@radix-ui/react-slot";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";
const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
destructive:
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
outline:
"border border-input bg-background hover:bg-accent hover:text-accent-foreground",
secondary:
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-10 px-4 py-2",
sm: "h-9 rounded-md px-3",
lg: "h-11 rounded-md px-8",
icon: "h-10 w-10",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
);
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean;
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button";
return (
<Comp
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props}
/>
);
}
);
Button.displayName = "Button";
export { Button, buttonVariants };
src\components\ui\github.tsx
const Github = () => {
return (
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<title>GitHub</title>
<path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12" />
</svg>
);
};
export { Github };
src\app\page.tsx
import { SignOut } from "@/components/sign-out";
import { auth } from "@/lib/auth";
import { redirect } from "next/navigation";
const Page = async () => {
const session = await auth();
if (!session) redirect("/sign-in");
return (
<>
<div className="bg-gray-100 rounded-lg p-4 text-center mb-6">
<p className="text-gray-600">Signed in as:</p>
<p className="font-medium">{session.user?.email}</p>
</div>
<SignOut />
</>
);
};
export default Page;
src\app\layout.tsx
import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import "@/app/globals.css";
import { ReactNode } from "react";
const geistSans = Geist({
variable: "--font-geist-sans",
subsets: ["latin"],
});
const geistMono = Geist_Mono({
variable: "--font-geist-mono",
subsets: ["latin"],
});
const metadata: Metadata = {
title: "Create Next App",
description: "Generated by create next app",
};
type LayoutProps = {
children: ReactNode;
};
const Layout = ({ children }: LayoutProps) => {
return (
<html lang="en">
<body
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
>
<main className="flex items-center justify-center min-h-screen bg-gray-100">
<div className="bg-white p-8 rounded-lg shadow-md w-full max-w-md">
{children}
</div>
</main>
</body>
</html>
);
};
export { metadata };
export default Layout;
src\app\api\auth\[...nextauth]\route.ts
import { handlers } from "@/lib/auth";
export const { GET, POST } = handlers;
src\app\(auth)\sign-in\page.tsx
import { auth } from "@/lib/auth";
import { signIn } from "@/lib/auth";
import { GithubSignIn } from "@/components/github-sign-in";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { executeAction } from "@/lib/executeAction";
import Link from "next/link";
import { redirect } from "next/navigation";
const Page = async () => {
const session = await auth();
if (session) redirect("/");
return (
<div className="w-full max-w-sm mx-auto space-y-6">
<h1 className="text-2xl font-bold text-center mb-6">Sign In</h1>
<GithubSignIn />
<div className="relative">
<div className="absolute inset-0 flex items-center">
<span className="w-full border-t" />
</div>
<div className="relative flex justify-center text-sm">
<span className="bg-background px-2 text-muted-foreground">
Or continue with email
</span>
</div>
</div>
{/* Email/Password Sign In */}
<form
className="space-y-4"
action={async (formData) => {
"use server";
await executeAction({
actionFn: async () => {
await signIn("credentials", formData);
},
});
}}
>
<Input
name="email"
placeholder="Email"
type="email"
required
autoComplete="email"
/>
<Input
name="password"
placeholder="Password"
type="password"
required
autoComplete="current-password"
/>
<Button className="w-full" type="submit">
Sign In
</Button>
</form>
<div className="text-center">
<Button asChild variant="link">
<Link href="/sign-up">Don't have an account? Sign up</Link>
</Button>
</div>
</div>
);
};
export default Page;
src\app\(auth)\sign-up\page.tsx
import { signUp } from "@/lib/actions";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { redirect } from "next/navigation";
import Link from "next/link";
import { GithubSignIn } from "@/components/github-sign-in";
import { auth } from "@/lib/auth";
const Page = async () => {
const session = await auth();
if (session) redirect("/");
return (
<div className="w-full max-w-sm mx-auto space-y-6">
<h1 className="text-2xl font-bold text-center mb-6">Create Account</h1>
<GithubSignIn />
<div className="relative">
<div className="absolute inset-0 flex items-center">
<span className="w-full border-t" />
</div>
<div className="relative flex justify-center text-sm">
<span className="bg-background px-2 text-muted-foreground">
Or continue with email
</span>
</div>
</div>
{/* Email/Password Sign Up */}
<form
className="space-y-4"
action={async (formData) => {
"use server";
const res = await signUp(formData);
if (res.success) {
redirect("/sign-in");
}
}}
>
<Input
name="email"
placeholder="Email"
type="email"
required
autoComplete="email"
/>
<Input
name="password"
placeholder="Password"
type="password"
required
autoComplete="new-password"
/>
<Button className="w-full" type="submit">
Sign Up
</Button>
</form>
<div className="text-center">
<Button asChild variant="link">
<Link href="/sign-in">Already have an account? Sign in</Link>
</Button>
</div>
</div>
);
};
export default Page;