├── .env.example ├── .eslintrc.json ├── .gitignore ├── README.md ├── app ├── actions │ └── authActions.ts ├── api │ └── auth │ │ └── [...nextauth] │ │ └── route.ts ├── auth │ └── signin │ │ └── page.tsx ├── favicon.ico ├── globals.css ├── layout.tsx ├── page.tsx ├── page1 │ └── page.tsx └── page2 │ └── page.tsx ├── auth.ts ├── components.json ├── components ├── error-message.tsx ├── loading-button.tsx ├── navbar.tsx └── ui │ ├── button.tsx │ ├── card.tsx │ ├── form.tsx │ ├── input.tsx │ └── label.tsx ├── lib ├── utils.ts └── zod.ts ├── middleware.ts ├── next.config.mjs ├── package.json ├── pnpm-lock.yaml ├── postcss.config.mjs ├── public ├── next.svg └── vercel.svg ├── tailwind.config.ts ├── tsconfig.json └── types └── next-auth.d.ts /.env.example: -------------------------------------------------------------------------------- 1 | AUTH_SECRET= 2 | AUTH_GITHUB_ID= 3 | AUTH_GITHUB_SECRET= 4 | # Replace with your own domain 5 | NEXTAUTH_URL="http://localhost:3000" 6 | AUTH_TRUST_HOST="http://localhost:3000" 7 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | .yarn/install-state.gz 8 | 9 | # testing 10 | /coverage 11 | 12 | # next.js 13 | /.next/ 14 | /out/ 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | .env -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Setup 2 | 3 | ## Install dependencies. 4 | ```bash 5 | npm install 6 | ``` 7 | 8 | ## Generate a secret. 9 | ```bash 10 | npx auth secret 11 | ``` 12 | 13 | ## For Github include your credentials. 14 | 15 | For full setup: Watch the video here: https://www.youtube.com/watch?v=DNtsJlmPda8 16 | -------------------------------------------------------------------------------- /app/actions/authActions.ts: -------------------------------------------------------------------------------- 1 | "use server"; 2 | 3 | import { signIn, signOut } from "@/auth"; 4 | import { AuthError } from "next-auth"; 5 | 6 | export async function handleCredentialsSignin({ email, password }: { 7 | email: string, 8 | password: string 9 | }) { 10 | try { 11 | await signIn("credentials", { email, password, redirectTo: "/" }); 12 | } catch (error) { 13 | if (error instanceof AuthError) { 14 | switch (error.type) { 15 | case 'CredentialsSignin': 16 | return { 17 | message: 'Invalid credentials', 18 | } 19 | default: 20 | return { 21 | message: 'Something went wrong.', 22 | } 23 | } 24 | } 25 | throw error; 26 | } 27 | } 28 | 29 | 30 | export async function handleGithubSignin() { 31 | await signIn("github", { redirectTo: "/" }); 32 | } 33 | 34 | export async function handleSignOut() { 35 | await signOut(); 36 | } -------------------------------------------------------------------------------- /app/api/auth/[...nextauth]/route.ts: -------------------------------------------------------------------------------- 1 | import { handlers } from "@/auth"; 2 | 3 | export const { GET, POST } = handlers; -------------------------------------------------------------------------------- /app/auth/signin/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; 4 | import { 5 | Form, 6 | FormControl, 7 | FormField, 8 | FormItem, 9 | FormLabel, 10 | FormMessage, 11 | } from "@/components/ui/form"; 12 | import { Input } from "@/components/ui/input"; 13 | 14 | import { GitHubLogoIcon } from "@radix-ui/react-icons"; 15 | 16 | import { useForm } from "react-hook-form"; 17 | import { zodResolver } from "@hookform/resolvers/zod"; 18 | import { z } from "zod"; 19 | 20 | import { signInSchema } from "@/lib/zod"; 21 | import LoadingButton from "@/components/loading-button"; 22 | import { 23 | handleCredentialsSignin, 24 | handleGithubSignin, 25 | } from "@/app/actions/authActions"; 26 | import { useState } from "react"; 27 | import ErrorMessage from "@/components/error-message"; 28 | import { Button } from "@/components/ui/button"; 29 | 30 | export default function SignIn() { 31 | const [globalError, setGlobalError] = useState(""); 32 | const form = useForm>({ 33 | resolver: zodResolver(signInSchema), 34 | defaultValues: { 35 | email: "", 36 | password: "", 37 | }, 38 | }); 39 | 40 | const onSubmit = async (values: z.infer) => { 41 | try { 42 | const result = await handleCredentialsSignin(values); 43 | if (result?.message) { 44 | setGlobalError(result.message); 45 | } 46 | } catch (error) { 47 | console.log("An unexpected error occurred. Please try again."); 48 | } 49 | }; 50 | 51 | return ( 52 |
53 | 54 | 55 | 56 | Welcome Back 57 | 58 | 59 | 60 | {globalError && } 61 |
62 | 66 | ( 70 | 71 | Email 72 | 73 | 79 | 80 | 81 | 82 | )} 83 | /> 84 | 85 | ( 89 | 90 | Password 91 | 92 | 97 | 98 | 99 | 100 | )} 101 | /> 102 | 103 | {/* Submit button will go here */} 104 | 107 | 108 | 109 | 110 | 111 | or 112 | 113 |
114 | 122 |
123 |
124 |
125 |
126 | ); 127 | } 128 | -------------------------------------------------------------------------------- /app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adityasinghcodes/nextjs-fullstack-auth/c8198efb394f9f20bc984ea1f10bd9e068b1bea7/app/favicon.ico -------------------------------------------------------------------------------- /app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @layer base { 6 | :root { 7 | --background: 0 0% 100%; 8 | --foreground: 222.2 84% 4.9%; 9 | --card: 0 0% 100%; 10 | --card-foreground: 222.2 84% 4.9%; 11 | --popover: 0 0% 100%; 12 | --popover-foreground: 222.2 84% 4.9%; 13 | --primary: 222.2 47.4% 11.2%; 14 | --primary-foreground: 210 40% 98%; 15 | --secondary: 210 40% 96.1%; 16 | --secondary-foreground: 222.2 47.4% 11.2%; 17 | --muted: 210 40% 96.1%; 18 | --muted-foreground: 215.4 16.3% 46.9%; 19 | --accent: 210 40% 96.1%; 20 | --accent-foreground: 222.2 47.4% 11.2%; 21 | --destructive: 0 84.2% 60.2%; 22 | --destructive-foreground: 210 40% 98%; 23 | --border: 214.3 31.8% 91.4%; 24 | --input: 214.3 31.8% 91.4%; 25 | --ring: 222.2 84% 4.9%; 26 | --radius: 0.5rem; 27 | --chart-1: 12 76% 61%; 28 | --chart-2: 173 58% 39%; 29 | --chart-3: 197 37% 24%; 30 | --chart-4: 43 74% 66%; 31 | --chart-5: 27 87% 67%; 32 | } 33 | 34 | .dark { 35 | --background: 222.2 84% 4.9%; 36 | --foreground: 210 40% 98%; 37 | --card: 222.2 84% 4.9%; 38 | --card-foreground: 210 40% 98%; 39 | --popover: 222.2 84% 4.9%; 40 | --popover-foreground: 210 40% 98%; 41 | --primary: 210 40% 98%; 42 | --primary-foreground: 222.2 47.4% 11.2%; 43 | --secondary: 217.2 32.6% 17.5%; 44 | --secondary-foreground: 210 40% 98%; 45 | --muted: 217.2 32.6% 17.5%; 46 | --muted-foreground: 215 20.2% 65.1%; 47 | --accent: 217.2 32.6% 17.5%; 48 | --accent-foreground: 210 40% 98%; 49 | --destructive: 0 62.8% 30.6%; 50 | --destructive-foreground: 210 40% 98%; 51 | --border: 217.2 32.6% 17.5%; 52 | --input: 217.2 32.6% 17.5%; 53 | --ring: 212.7 26.8% 83.9%; 54 | --chart-1: 220 70% 50%; 55 | --chart-2: 160 60% 45%; 56 | --chart-3: 30 80% 55%; 57 | --chart-4: 280 65% 60%; 58 | --chart-5: 340 75% 55%; 59 | } 60 | } 61 | 62 | @layer base { 63 | * { 64 | @apply border-border; 65 | } 66 | body { 67 | @apply bg-background text-foreground; 68 | } 69 | } -------------------------------------------------------------------------------- /app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from "next"; 2 | import { Inter } from "next/font/google"; 3 | import "./globals.css"; 4 | import Navbar from "@/components/navbar"; 5 | import { SessionProvider } from "next-auth/react"; 6 | 7 | const inter = Inter({ subsets: ["latin"] }); 8 | 9 | export const metadata: Metadata = { 10 | title: "Create Next App", 11 | description: "Generated by create next app", 12 | }; 13 | 14 | export default function RootLayout({ 15 | children, 16 | }: Readonly<{ 17 | children: React.ReactNode; 18 | }>) { 19 | return ( 20 | 21 | 22 | 23 | 24 | {children} 25 | 26 | 27 | 28 | ); 29 | } 30 | -------------------------------------------------------------------------------- /app/page.tsx: -------------------------------------------------------------------------------- 1 | import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; 2 | import Image from "next/image"; 3 | 4 | export default async function Home() { 5 | return ( 6 |
7 | 8 | 9 | img 17 | 18 | 19 | 20 | Welcome, user! 21 | 22 |

23 | If you are learning something valuable from this video, please like 24 | and subscribe to my channel. 25 |

26 |
27 |
28 |
29 | ); 30 | } 31 | -------------------------------------------------------------------------------- /app/page1/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { useSession } from "next-auth/react"; 3 | 4 | export default function Page1() { 5 | const { data: session, update } = useSession(); 6 | return ( 7 | <> 8 | 15 |

Can be accessed by any user.

16 | 17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /app/page2/page.tsx: -------------------------------------------------------------------------------- 1 | export default function Page2() { 2 | return

Can only be accessed by admin user.

; 3 | } 4 | -------------------------------------------------------------------------------- /auth.ts: -------------------------------------------------------------------------------- 1 | import NextAuth from "next-auth"; 2 | import Credentials from "next-auth/providers/credentials"; 3 | import Github from "next-auth/providers/github"; 4 | import { signInSchema } from "./lib/zod"; 5 | 6 | export const { handlers, signIn, signOut, auth } = NextAuth({ 7 | providers: [ 8 | Github, 9 | Credentials({ 10 | credentials: { 11 | email: { label: "Email", type: "email", placeholder: "Email" }, 12 | password: { label: "Password", type: "password", placeholder: "Password" }, 13 | }, 14 | async authorize(credentials) { 15 | let user = null; 16 | 17 | // validate credentials 18 | const parsedCredentials = signInSchema.safeParse(credentials); 19 | if (!parsedCredentials.success) { 20 | console.error("Invalid credentials:", parsedCredentials.error.errors); 21 | return null; 22 | } 23 | // get user 24 | 25 | user = { 26 | id: '1', 27 | name: 'Aditya Singh', 28 | email: 'jojo@jojo.com', 29 | role: "admin" 30 | } 31 | 32 | if (!user) { 33 | console.log("Invalid credentials"); 34 | return null; 35 | } 36 | 37 | return user; 38 | } 39 | }) 40 | ], 41 | callbacks: { 42 | authorized({ request: { nextUrl }, auth }) { 43 | const isLoggedIn = !!auth?.user; 44 | const { pathname } = nextUrl; 45 | const role = auth?.user.role || 'user'; 46 | if (pathname.startsWith('/auth/signin') && isLoggedIn) { 47 | return Response.redirect(new URL('/', nextUrl)); 48 | } 49 | if (pathname.startsWith("/page2") && role !== "admin") { 50 | return Response.redirect(new URL('/', nextUrl)); 51 | } 52 | return !!auth; 53 | }, 54 | jwt({ token, user, trigger, session }) { 55 | if (user) { 56 | token.id = user.id as string; 57 | token.role = user.role as string; 58 | } 59 | if (trigger === "update" && session) { 60 | token = { ...token, ...session }; 61 | } 62 | return token; 63 | }, 64 | session({ session, token }) { 65 | session.user.id = token.id; 66 | session.user.role = token.role; 67 | return session; 68 | } 69 | }, 70 | pages: { 71 | signIn: "/auth/signin" 72 | } 73 | }) -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "default", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.ts", 8 | "css": "app/globals.css", 9 | "baseColor": "slate", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils" 16 | } 17 | } -------------------------------------------------------------------------------- /components/error-message.tsx: -------------------------------------------------------------------------------- 1 | import { TriangleIcon } from "lucide-react"; 2 | 3 | export default function ErrorMessage({ error }: { error: string }) { 4 | return ( 5 |
9 | 10 | Error 11 |
{error}
12 |
13 | ); 14 | } 15 | -------------------------------------------------------------------------------- /components/loading-button.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from "@/components/ui/button"; 2 | 3 | export default function LoadingButton({ pending }: { pending: boolean }) { 4 | return ( 5 | 33 | ); 34 | } 35 | -------------------------------------------------------------------------------- /components/navbar.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | import { Button } from "@/components/ui/button"; 3 | import { auth } from "@/auth"; 4 | import { handleSignOut } from "@/app/actions/authActions"; 5 | 6 | export default async function Navbar() { 7 | const session = await auth(); 8 | console.log({ session }); 9 | return ( 10 | 26 | ); 27 | } 28 | -------------------------------------------------------------------------------- /components/ui/button.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { Slot } from "@radix-ui/react-slot" 3 | import { cva, type VariantProps } from "class-variance-authority" 4 | 5 | import { cn } from "@/lib/utils" 6 | 7 | const buttonVariants = cva( 8 | "inline-flex items-center justify-center 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", 9 | { 10 | variants: { 11 | variant: { 12 | default: "bg-primary text-primary-foreground hover:bg-primary/90", 13 | destructive: 14 | "bg-destructive text-destructive-foreground hover:bg-destructive/90", 15 | outline: 16 | "border border-input bg-background hover:bg-accent hover:text-accent-foreground", 17 | secondary: 18 | "bg-secondary text-secondary-foreground hover:bg-secondary/80", 19 | ghost: "hover:bg-accent hover:text-accent-foreground", 20 | link: "text-primary underline-offset-4 hover:underline", 21 | }, 22 | size: { 23 | default: "h-10 px-4 py-2", 24 | sm: "h-9 rounded-md px-3", 25 | lg: "h-11 rounded-md px-8", 26 | icon: "h-10 w-10", 27 | }, 28 | }, 29 | defaultVariants: { 30 | variant: "default", 31 | size: "default", 32 | }, 33 | } 34 | ) 35 | 36 | export interface ButtonProps 37 | extends React.ButtonHTMLAttributes, 38 | VariantProps { 39 | asChild?: boolean 40 | } 41 | 42 | const Button = React.forwardRef( 43 | ({ className, variant, size, asChild = false, ...props }, ref) => { 44 | const Comp = asChild ? Slot : "button" 45 | return ( 46 | 51 | ) 52 | } 53 | ) 54 | Button.displayName = "Button" 55 | 56 | export { Button, buttonVariants } 57 | -------------------------------------------------------------------------------- /components/ui/card.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@/lib/utils" 4 | 5 | const Card = React.forwardRef< 6 | HTMLDivElement, 7 | React.HTMLAttributes 8 | >(({ className, ...props }, ref) => ( 9 |
17 | )) 18 | Card.displayName = "Card" 19 | 20 | const CardHeader = React.forwardRef< 21 | HTMLDivElement, 22 | React.HTMLAttributes 23 | >(({ className, ...props }, ref) => ( 24 |
29 | )) 30 | CardHeader.displayName = "CardHeader" 31 | 32 | const CardTitle = React.forwardRef< 33 | HTMLParagraphElement, 34 | React.HTMLAttributes 35 | >(({ className, ...props }, ref) => ( 36 |

44 | )) 45 | CardTitle.displayName = "CardTitle" 46 | 47 | const CardDescription = React.forwardRef< 48 | HTMLParagraphElement, 49 | React.HTMLAttributes 50 | >(({ className, ...props }, ref) => ( 51 |

56 | )) 57 | CardDescription.displayName = "CardDescription" 58 | 59 | const CardContent = React.forwardRef< 60 | HTMLDivElement, 61 | React.HTMLAttributes 62 | >(({ className, ...props }, ref) => ( 63 |

64 | )) 65 | CardContent.displayName = "CardContent" 66 | 67 | const CardFooter = React.forwardRef< 68 | HTMLDivElement, 69 | React.HTMLAttributes 70 | >(({ className, ...props }, ref) => ( 71 |
76 | )) 77 | CardFooter.displayName = "CardFooter" 78 | 79 | export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent } 80 | -------------------------------------------------------------------------------- /components/ui/form.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as LabelPrimitive from "@radix-ui/react-label" 5 | import { Slot } from "@radix-ui/react-slot" 6 | import { 7 | Controller, 8 | ControllerProps, 9 | FieldPath, 10 | FieldValues, 11 | FormProvider, 12 | useFormContext, 13 | } from "react-hook-form" 14 | 15 | import { cn } from "@/lib/utils" 16 | import { Label } from "@/components/ui/label" 17 | 18 | const Form = FormProvider 19 | 20 | type FormFieldContextValue< 21 | TFieldValues extends FieldValues = FieldValues, 22 | TName extends FieldPath = FieldPath 23 | > = { 24 | name: TName 25 | } 26 | 27 | const FormFieldContext = React.createContext( 28 | {} as FormFieldContextValue 29 | ) 30 | 31 | const FormField = < 32 | TFieldValues extends FieldValues = FieldValues, 33 | TName extends FieldPath = FieldPath 34 | >({ 35 | ...props 36 | }: ControllerProps) => { 37 | return ( 38 | 39 | 40 | 41 | ) 42 | } 43 | 44 | const useFormField = () => { 45 | const fieldContext = React.useContext(FormFieldContext) 46 | const itemContext = React.useContext(FormItemContext) 47 | const { getFieldState, formState } = useFormContext() 48 | 49 | const fieldState = getFieldState(fieldContext.name, formState) 50 | 51 | if (!fieldContext) { 52 | throw new Error("useFormField should be used within ") 53 | } 54 | 55 | const { id } = itemContext 56 | 57 | return { 58 | id, 59 | name: fieldContext.name, 60 | formItemId: `${id}-form-item`, 61 | formDescriptionId: `${id}-form-item-description`, 62 | formMessageId: `${id}-form-item-message`, 63 | ...fieldState, 64 | } 65 | } 66 | 67 | type FormItemContextValue = { 68 | id: string 69 | } 70 | 71 | const FormItemContext = React.createContext( 72 | {} as FormItemContextValue 73 | ) 74 | 75 | const FormItem = React.forwardRef< 76 | HTMLDivElement, 77 | React.HTMLAttributes 78 | >(({ className, ...props }, ref) => { 79 | const id = React.useId() 80 | 81 | return ( 82 | 83 |
84 | 85 | ) 86 | }) 87 | FormItem.displayName = "FormItem" 88 | 89 | const FormLabel = React.forwardRef< 90 | React.ElementRef, 91 | React.ComponentPropsWithoutRef 92 | >(({ className, ...props }, ref) => { 93 | const { error, formItemId } = useFormField() 94 | 95 | return ( 96 |