├── .nvmrc ├── src ├── app │ ├── api │ │ └── auth │ │ │ └── [...nextauth] │ │ │ └── route.ts │ ├── page.tsx │ ├── layout.tsx │ ├── globals.css │ └── (auth) │ │ ├── sign-up │ │ └── page.tsx │ │ └── sign-in │ │ └── page.tsx ├── lib │ ├── utils.ts │ ├── schema.ts │ ├── db │ │ ├── db.ts │ │ └── schema.prisma │ ├── actions.ts │ ├── executeAction.ts │ └── auth.ts └── components │ ├── sign-out.tsx │ ├── github-sign-in.tsx │ └── ui │ ├── input.tsx │ ├── github.tsx │ └── button.tsx ├── next.config.ts ├── .env.sample ├── postcss.config.mjs ├── eslint.config.mjs ├── components.json ├── .gitignore ├── tsconfig.json ├── README.md ├── package.json ├── tailwind.config.ts └── script.md /.nvmrc: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/api/auth/[...nextauth]/route.ts: -------------------------------------------------------------------------------- 1 | import { handlers } from "@/lib/auth"; 2 | 3 | export const { GET, POST } = handlers; 4 | -------------------------------------------------------------------------------- /next.config.ts: -------------------------------------------------------------------------------- 1 | import type { NextConfig } from "next"; 2 | 3 | const nextConfig: NextConfig = {}; 4 | 5 | export default nextConfig; 6 | -------------------------------------------------------------------------------- /.env.sample: -------------------------------------------------------------------------------- 1 | 2 | AUTH_SECRET= 3 | 4 | AUTH_GITHUB_ID= 5 | AUTH_GITHUB_SECRET= 6 | 7 | DATABASE_URL="file:./data/dev.db" 8 | AUTH_TRUST_HOST=true -------------------------------------------------------------------------------- /postcss.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('postcss-load-config').Config} */ 2 | const config = { 3 | plugins: { 4 | tailwindcss: {}, 5 | }, 6 | }; 7 | 8 | export default config; 9 | -------------------------------------------------------------------------------- /src/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { clsx, type ClassValue } from "clsx"; 2 | import { twMerge } from "tailwind-merge"; 3 | 4 | export const cn = (...inputs: ClassValue[]) => { 5 | return twMerge(clsx(inputs)); 6 | }; 7 | -------------------------------------------------------------------------------- /src/lib/schema.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | 3 | const schema = z.object({ 4 | email: z.string().email(), 5 | password: z.string().min(1), 6 | }); 7 | 8 | type Schema = z.infer; 9 | 10 | export { schema, type Schema }; 11 | -------------------------------------------------------------------------------- /src/lib/db/db.ts: -------------------------------------------------------------------------------- 1 | import { PrismaClient } from "@prisma/client"; 2 | 3 | const prismaClientSingleton = () => { 4 | return new PrismaClient(); 5 | }; 6 | 7 | declare const globalThis: { 8 | prismaGlobal: ReturnType; 9 | } & typeof global; 10 | 11 | const db = globalThis.prismaGlobal ?? prismaClientSingleton(); 12 | 13 | export default db; 14 | 15 | if (process.env.NODE_ENV !== "production") globalThis.prismaGlobal = db; 16 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import { dirname } from "path"; 2 | import { fileURLToPath } from "url"; 3 | import { FlatCompat } from "@eslint/eslintrc"; 4 | 5 | const __filename = fileURLToPath(import.meta.url); 6 | const __dirname = dirname(__filename); 7 | 8 | const compat = new FlatCompat({ 9 | baseDirectory: __dirname, 10 | }); 11 | 12 | const eslintConfig = [ 13 | ...compat.extends("next/core-web-vitals", "next/typescript"), 14 | ]; 15 | 16 | export default eslintConfig; 17 | -------------------------------------------------------------------------------- /src/components/sign-out.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { Button } from "@/components/ui/button"; 3 | import { signOut } from "next-auth/react"; 4 | 5 | const SignOut = () => { 6 | const handleSignOut = async () => { 7 | await signOut(); 8 | }; 9 | 10 | return ( 11 |
12 | 15 |
16 | ); 17 | }; 18 | 19 | export { SignOut }; 20 | -------------------------------------------------------------------------------- /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": "src/app/globals.css", 9 | "baseColor": "neutral", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils", 16 | "ui": "@/components/ui", 17 | "lib": "@/lib", 18 | "hooks": "@/hooks" 19 | }, 20 | "iconLibrary": "lucide" 21 | } 22 | -------------------------------------------------------------------------------- /src/components/github-sign-in.tsx: -------------------------------------------------------------------------------- 1 | import { signIn } from "@/lib/auth"; 2 | import { Button } from "@/components/ui/button"; 3 | import { Github } from "@/components/ui/github"; 4 | 5 | const GithubSignIn = () => { 6 | return ( 7 |
{ 9 | "use server"; 10 | await signIn("github"); 11 | }} 12 | > 13 | 17 |
18 | ); 19 | }; 20 | 21 | export { GithubSignIn }; 22 | -------------------------------------------------------------------------------- /src/app/page.tsx: -------------------------------------------------------------------------------- 1 | import { SignOut } from "@/components/sign-out"; 2 | import { auth } from "@/lib/auth"; 3 | import { redirect } from "next/navigation"; 4 | 5 | const Page = async () => { 6 | const session = await auth(); 7 | if (!session) redirect("/sign-in"); 8 | 9 | return ( 10 | <> 11 |
12 |

Signed in as:

13 |

{session.user?.email}

14 |
15 | 16 | 17 | 18 | ); 19 | }; 20 | 21 | export default Page; 22 | -------------------------------------------------------------------------------- /.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.* 7 | .yarn/* 8 | !.yarn/patches 9 | !.yarn/plugins 10 | !.yarn/releases 11 | !.yarn/versions 12 | 13 | # testing 14 | /coverage 15 | 16 | # next.js 17 | /.next/ 18 | /out/ 19 | 20 | # production 21 | /build 22 | 23 | # misc 24 | .DS_Store 25 | *.pem 26 | 27 | # debug 28 | npm-debug.log* 29 | yarn-debug.log* 30 | yarn-error.log* 31 | .pnpm-debug.log* 32 | 33 | 34 | # env files (can opt-in for committing if needed) 35 | .env 36 | 37 | # vercel 38 | .vercel 39 | 40 | # typescript 41 | *.tsbuildinfo 42 | next-env.d.ts 43 | 44 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2017", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "noEmit": true, 9 | "esModuleInterop": true, 10 | "module": "esnext", 11 | "moduleResolution": "bundler", 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "jsx": "preserve", 15 | "incremental": true, 16 | "plugins": [ 17 | { 18 | "name": "next" 19 | } 20 | ], 21 | "paths": { 22 | "@/*": ["./src/*"] 23 | } 24 | }, 25 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 26 | "exclude": ["node_modules"] 27 | } 28 | -------------------------------------------------------------------------------- /src/lib/actions.ts: -------------------------------------------------------------------------------- 1 | import { schema } from "@/lib/schema"; 2 | import db from "@/lib/db/db"; 3 | import { executeAction } from "@/lib/executeAction"; 4 | 5 | const signUp = async (formData: FormData) => { 6 | return executeAction({ 7 | actionFn: async () => { 8 | const email = formData.get("email"); 9 | const password = formData.get("password"); 10 | const validatedData = schema.parse({ email, password }); 11 | await db.user.create({ 12 | data: { 13 | email: validatedData.email.toLocaleLowerCase(), 14 | password: validatedData.password, 15 | }, 16 | }); 17 | }, 18 | successMessage: "Signed up successfully", 19 | }); 20 | }; 21 | 22 | export { signUp }; 23 | -------------------------------------------------------------------------------- /src/lib/executeAction.ts: -------------------------------------------------------------------------------- 1 | import { isRedirectError } from "next/dist/client/components/redirect-error"; 2 | 3 | type Options = { 4 | actionFn: () => Promise; 5 | successMessage?: string; 6 | }; 7 | 8 | const executeAction = async ({ 9 | actionFn, 10 | successMessage = "The actions was successful", 11 | }: Options): Promise<{ success: boolean; message: string }> => { 12 | try { 13 | await actionFn(); 14 | 15 | return { 16 | success: true, 17 | message: successMessage, 18 | }; 19 | } catch (error) { 20 | if (isRedirectError(error)) { 21 | throw error; 22 | } 23 | 24 | return { 25 | success: false, 26 | message: "An error has occurred during executing the action", 27 | }; 28 | } 29 | }; 30 | 31 | export { executeAction }; 32 | -------------------------------------------------------------------------------- /src/components/ui/input.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@/lib/utils" 4 | 5 | const Input = React.forwardRef>( 6 | ({ className, type, ...props }, ref) => { 7 | return ( 8 | 17 | ) 18 | } 19 | ) 20 | Input.displayName = "Input" 21 | 22 | export { Input } 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # NextJS 15 Authentication Tutorial 2 | Simple implementation of Auth.js with Prisma in Next.js 15 3 | 4 | ## Requirements 5 | - Node.js 22+ 6 | 7 | ## Quick Start 8 | 9 | 1. **Clone the repository** 10 | ```bash 11 | # For tutorial 12 | git clone -b starter https://github.com/codegenixdev/auth-nextjs-tutorial.git 13 | 14 | # For complete code 15 | git clone https://github.com/codegenixdev/auth-nextjs-tutorial.git 16 | ``` 17 | 18 | 2. **Setup environment for complete code** 19 | ```bash 20 | cp .env.sample .env 21 | # Update .env with your values 22 | ``` 23 | 24 | 3. **Install & Run** 25 | ```bash 26 | npm install 27 | npm run db:migrate 28 | npm run dev 29 | ``` 30 | 31 | ## Branches 32 | - `starter`: Initial setup 33 | - `master`: Complete implementation 34 | 35 | ## Tech Stack 36 | - Next.js 15 37 | - Node.js 22 38 | - TypeScript 39 | - Prisma 40 | - Auth.js 41 | 42 | --- 43 | Happy coding! 🚀 44 | -------------------------------------------------------------------------------- /src/components/ui/github.tsx: -------------------------------------------------------------------------------- 1 | const Github = () => { 2 | return ( 3 | 4 | GitHub 5 | 6 | 7 | ); 8 | }; 9 | 10 | export { Github }; 11 | -------------------------------------------------------------------------------- /src/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from "next"; 2 | import { Geist, Geist_Mono } from "next/font/google"; 3 | import "@/app/globals.css"; 4 | import { ReactNode } from "react"; 5 | 6 | const geistSans = Geist({ 7 | variable: "--font-geist-sans", 8 | subsets: ["latin"], 9 | }); 10 | 11 | const geistMono = Geist_Mono({ 12 | variable: "--font-geist-mono", 13 | subsets: ["latin"], 14 | }); 15 | 16 | const metadata: Metadata = { 17 | title: "Create Next App", 18 | description: "Generated by create next app", 19 | }; 20 | 21 | type LayoutProps = { 22 | children: ReactNode; 23 | }; 24 | const Layout = ({ children }: LayoutProps) => { 25 | return ( 26 | 27 | 30 |
31 |
32 | {children} 33 |
34 |
35 | 36 | 37 | ); 38 | }; 39 | 40 | export { metadata }; 41 | export default Layout; 42 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "auth-nextjs", 3 | "version": "0.1.0", 4 | "private": true, 5 | "prisma": { 6 | "schema": "src/lib/db/schema.prisma" 7 | }, 8 | "scripts": { 9 | "dev": "next dev --turbopack", 10 | "build": "next build", 11 | "start": "next start", 12 | "lint": "next lint", 13 | "db:migrate": "npx prisma migrate dev", 14 | "db:reset": "prisma migrate reset && prisma migrate dev", 15 | "db:studio": "prisma studio" 16 | }, 17 | "dependencies": { 18 | "@auth/prisma-adapter": "^2.7.4", 19 | "@prisma/client": "^6.1.0", 20 | "@radix-ui/react-slot": "^1.1.1", 21 | "class-variance-authority": "^0.7.1", 22 | "clsx": "^2.1.1", 23 | "lucide-react": "^0.469.0", 24 | "next": "15.1.3", 25 | "next-auth": "^5.0.0-beta.25", 26 | "react": "^19.0.0", 27 | "react-dom": "^19.0.0", 28 | "tailwind-merge": "^2.6.0", 29 | "tailwindcss-animate": "^1.0.7", 30 | "uuid": "^11.0.3", 31 | "zod": "^3.24.1" 32 | }, 33 | "devDependencies": { 34 | "@eslint/eslintrc": "^3", 35 | "@types/node": "^20", 36 | "@types/react": "^19", 37 | "@types/react-dom": "^19", 38 | "eslint": "^9", 39 | "eslint-config-next": "15.1.3", 40 | "postcss": "^8", 41 | "prisma": "^6.1.0", 42 | "tailwindcss": "^3.4.1", 43 | "typescript": "^5" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /tailwind.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from "tailwindcss"; 2 | 3 | export default { 4 | darkMode: ["class"], 5 | content: [ 6 | "./src/pages/**/*.{js,ts,jsx,tsx,mdx}", 7 | "./src/components/**/*.{js,ts,jsx,tsx,mdx}", 8 | "./src/app/**/*.{js,ts,jsx,tsx,mdx}", 9 | ], 10 | theme: { 11 | extend: { 12 | colors: { 13 | background: 'hsl(var(--background))', 14 | foreground: 'hsl(var(--foreground))', 15 | card: { 16 | DEFAULT: 'hsl(var(--card))', 17 | foreground: 'hsl(var(--card-foreground))' 18 | }, 19 | popover: { 20 | DEFAULT: 'hsl(var(--popover))', 21 | foreground: 'hsl(var(--popover-foreground))' 22 | }, 23 | primary: { 24 | DEFAULT: 'hsl(var(--primary))', 25 | foreground: 'hsl(var(--primary-foreground))' 26 | }, 27 | secondary: { 28 | DEFAULT: 'hsl(var(--secondary))', 29 | foreground: 'hsl(var(--secondary-foreground))' 30 | }, 31 | muted: { 32 | DEFAULT: 'hsl(var(--muted))', 33 | foreground: 'hsl(var(--muted-foreground))' 34 | }, 35 | accent: { 36 | DEFAULT: 'hsl(var(--accent))', 37 | foreground: 'hsl(var(--accent-foreground))' 38 | }, 39 | destructive: { 40 | DEFAULT: 'hsl(var(--destructive))', 41 | foreground: 'hsl(var(--destructive-foreground))' 42 | }, 43 | border: 'hsl(var(--border))', 44 | input: 'hsl(var(--input))', 45 | ring: 'hsl(var(--ring))', 46 | chart: { 47 | '1': 'hsl(var(--chart-1))', 48 | '2': 'hsl(var(--chart-2))', 49 | '3': 'hsl(var(--chart-3))', 50 | '4': 'hsl(var(--chart-4))', 51 | '5': 'hsl(var(--chart-5))' 52 | } 53 | }, 54 | borderRadius: { 55 | lg: 'var(--radius)', 56 | md: 'calc(var(--radius) - 2px)', 57 | sm: 'calc(var(--radius) - 4px)' 58 | } 59 | } 60 | }, 61 | plugins: [require("tailwindcss-animate")], 62 | } satisfies Config; 63 | -------------------------------------------------------------------------------- /src/app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | body { 6 | font-family: Arial, Helvetica, sans-serif; 7 | } 8 | 9 | @layer base { 10 | :root { 11 | --background: 0 0% 100%; 12 | --foreground: 0 0% 3.9%; 13 | --card: 0 0% 100%; 14 | --card-foreground: 0 0% 3.9%; 15 | --popover: 0 0% 100%; 16 | --popover-foreground: 0 0% 3.9%; 17 | --primary: 0 0% 9%; 18 | --primary-foreground: 0 0% 98%; 19 | --secondary: 0 0% 96.1%; 20 | --secondary-foreground: 0 0% 9%; 21 | --muted: 0 0% 96.1%; 22 | --muted-foreground: 0 0% 45.1%; 23 | --accent: 0 0% 96.1%; 24 | --accent-foreground: 0 0% 9%; 25 | --destructive: 0 84.2% 60.2%; 26 | --destructive-foreground: 0 0% 98%; 27 | --border: 0 0% 89.8%; 28 | --input: 0 0% 89.8%; 29 | --ring: 0 0% 3.9%; 30 | --chart-1: 12 76% 61%; 31 | --chart-2: 173 58% 39%; 32 | --chart-3: 197 37% 24%; 33 | --chart-4: 43 74% 66%; 34 | --chart-5: 27 87% 67%; 35 | --radius: 0.5rem; 36 | } 37 | .dark { 38 | --background: 0 0% 3.9%; 39 | --foreground: 0 0% 98%; 40 | --card: 0 0% 3.9%; 41 | --card-foreground: 0 0% 98%; 42 | --popover: 0 0% 3.9%; 43 | --popover-foreground: 0 0% 98%; 44 | --primary: 0 0% 98%; 45 | --primary-foreground: 0 0% 9%; 46 | --secondary: 0 0% 14.9%; 47 | --secondary-foreground: 0 0% 98%; 48 | --muted: 0 0% 14.9%; 49 | --muted-foreground: 0 0% 63.9%; 50 | --accent: 0 0% 14.9%; 51 | --accent-foreground: 0 0% 98%; 52 | --destructive: 0 62.8% 30.6%; 53 | --destructive-foreground: 0 0% 98%; 54 | --border: 0 0% 14.9%; 55 | --input: 0 0% 14.9%; 56 | --ring: 0 0% 83.1%; 57 | --chart-1: 220 70% 50%; 58 | --chart-2: 160 60% 45%; 59 | --chart-3: 30 80% 55%; 60 | --chart-4: 280 65% 60%; 61 | --chart-5: 340 75% 55%; 62 | } 63 | } 64 | 65 | @layer base { 66 | * { 67 | @apply border-border; 68 | } 69 | body { 70 | @apply bg-background text-foreground; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/lib/auth.ts: -------------------------------------------------------------------------------- 1 | import { v4 as uuid } from "uuid"; 2 | import { encode as defaultEncode } from "next-auth/jwt"; 3 | 4 | import db from "@/lib/db/db"; 5 | import { PrismaAdapter } from "@auth/prisma-adapter"; 6 | import NextAuth from "next-auth"; 7 | import Credentials from "next-auth/providers/credentials"; 8 | import GitHub from "next-auth/providers/github"; 9 | import { schema } from "@/lib/schema"; 10 | 11 | const adapter = PrismaAdapter(db); 12 | 13 | export const { handlers, signIn, signOut, auth } = NextAuth({ 14 | adapter, 15 | providers: [ 16 | GitHub, 17 | Credentials({ 18 | credentials: { 19 | email: {}, 20 | password: {}, 21 | }, 22 | authorize: async (credentials) => { 23 | const validatedCredentials = schema.parse(credentials); 24 | 25 | const user = await db.user.findFirst({ 26 | where: { 27 | email: validatedCredentials.email, 28 | password: validatedCredentials.password, 29 | }, 30 | }); 31 | 32 | if (!user) { 33 | throw new Error("Invalid credentials."); 34 | } 35 | 36 | return user; 37 | }, 38 | }), 39 | ], 40 | callbacks: { 41 | async jwt({ token, account }) { 42 | if (account?.provider === "credentials") { 43 | token.credentials = true; 44 | } 45 | return token; 46 | }, 47 | }, 48 | jwt: { 49 | encode: async function (params) { 50 | if (params.token?.credentials) { 51 | const sessionToken = uuid(); 52 | 53 | if (!params.token.sub) { 54 | throw new Error("No user ID found in token"); 55 | } 56 | 57 | const createdSession = await adapter?.createSession?.({ 58 | sessionToken: sessionToken, 59 | userId: params.token.sub, 60 | expires: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000), 61 | }); 62 | 63 | if (!createdSession) { 64 | throw new Error("Failed to create session"); 65 | } 66 | 67 | return sessionToken; 68 | } 69 | return defaultEncode(params); 70 | }, 71 | }, 72 | }); 73 | -------------------------------------------------------------------------------- /src/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 | import { cn } from "@/lib/utils"; 5 | 6 | const buttonVariants = cva( 7 | "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", 8 | { 9 | variants: { 10 | variant: { 11 | default: "bg-primary text-primary-foreground hover:bg-primary/90", 12 | destructive: 13 | "bg-destructive text-destructive-foreground hover:bg-destructive/90", 14 | outline: 15 | "border border-input bg-background hover:bg-accent hover:text-accent-foreground", 16 | secondary: 17 | "bg-secondary text-secondary-foreground hover:bg-secondary/80", 18 | ghost: "hover:bg-accent hover:text-accent-foreground", 19 | link: "text-primary underline-offset-4 hover:underline", 20 | }, 21 | size: { 22 | default: "h-10 px-4 py-2", 23 | sm: "h-9 rounded-md px-3", 24 | lg: "h-11 rounded-md px-8", 25 | icon: "h-10 w-10", 26 | }, 27 | }, 28 | defaultVariants: { 29 | variant: "default", 30 | size: "default", 31 | }, 32 | } 33 | ); 34 | 35 | export interface ButtonProps 36 | extends React.ButtonHTMLAttributes, 37 | VariantProps { 38 | asChild?: boolean; 39 | } 40 | 41 | const Button = React.forwardRef( 42 | ({ className, variant, size, asChild = false, ...props }, ref) => { 43 | const Comp = asChild ? Slot : "button"; 44 | return ( 45 | 50 | ); 51 | } 52 | ); 53 | Button.displayName = "Button"; 54 | 55 | export { Button, buttonVariants }; 56 | -------------------------------------------------------------------------------- /src/app/(auth)/sign-up/page.tsx: -------------------------------------------------------------------------------- 1 | import { signUp } from "@/lib/actions"; 2 | import { Button } from "@/components/ui/button"; 3 | import { Input } from "@/components/ui/input"; 4 | import { redirect } from "next/navigation"; 5 | import Link from "next/link"; 6 | import { GithubSignIn } from "@/components/github-sign-in"; 7 | import { auth } from "@/lib/auth"; 8 | 9 | const Page = async () => { 10 | const session = await auth(); 11 | if (session) redirect("/"); 12 | 13 | return ( 14 |
15 |

Create Account

16 | 17 | 18 | 19 |
20 |
21 | 22 |
23 |
24 | 25 | Or continue with email 26 | 27 |
28 |
29 | 30 | {/* Email/Password Sign Up */} 31 |
{ 34 | "use server"; 35 | const res = await signUp(formData); 36 | if (res.success) { 37 | redirect("/sign-in"); 38 | } 39 | }} 40 | > 41 | 48 | 55 | 58 |
59 | 60 |
61 | 64 |
65 |
66 | ); 67 | }; 68 | 69 | export default Page; 70 | -------------------------------------------------------------------------------- /src/app/(auth)/sign-in/page.tsx: -------------------------------------------------------------------------------- 1 | import { auth } from "@/lib/auth"; 2 | 3 | import { signIn } from "@/lib/auth"; 4 | import { GithubSignIn } from "@/components/github-sign-in"; 5 | import { Button } from "@/components/ui/button"; 6 | import { Input } from "@/components/ui/input"; 7 | import { executeAction } from "@/lib/executeAction"; 8 | import Link from "next/link"; 9 | import { redirect } from "next/navigation"; 10 | 11 | const Page = async () => { 12 | const session = await auth(); 13 | if (session) redirect("/"); 14 | 15 | return ( 16 |
17 |

Sign In

18 | 19 | 20 |
21 |
22 | 23 |
24 |
25 | 26 | Or continue with email 27 | 28 |
29 |
30 | 31 | {/* Email/Password Sign In */} 32 |
{ 35 | "use server"; 36 | await executeAction({ 37 | actionFn: async () => { 38 | await signIn("credentials", formData); 39 | }, 40 | }); 41 | }} 42 | > 43 | 50 | 57 | 60 |
61 | 62 |
63 | 66 |
67 |
68 | ); 69 | }; 70 | 71 | export default Page; 72 | -------------------------------------------------------------------------------- /src/lib/db/schema.prisma: -------------------------------------------------------------------------------- 1 | generator client { 2 | provider = "prisma-client-js" 3 | } 4 | 5 | datasource db { 6 | provider = "sqlite" 7 | url = env("DATABASE_URL") 8 | } 9 | 10 | model User { 11 | id String @id @default(cuid()) 12 | name String? 13 | email String? @unique 14 | password String? 15 | emailVerified DateTime? 16 | image String? 17 | accounts Account[] 18 | sessions Session[] 19 | // Optional for WebAuthn support 20 | Authenticator Authenticator[] 21 | 22 | createdAt DateTime @default(now()) 23 | updatedAt DateTime @updatedAt 24 | } 25 | 26 | model Account { 27 | id String @id @default(cuid()) 28 | userId String 29 | type String 30 | provider String 31 | providerAccountId String 32 | refresh_token String? 33 | access_token String? 34 | expires_at Int? 35 | token_type String? 36 | scope String? 37 | id_token String? 38 | session_state String? 39 | 40 | createdAt DateTime @default(now()) 41 | updatedAt DateTime @updatedAt 42 | 43 | user User @relation(fields: [userId], references: [id], onDelete: Cascade) 44 | 45 | @@unique([provider, providerAccountId]) 46 | } 47 | 48 | model Session { 49 | id String @id @default(cuid()) 50 | sessionToken String @unique 51 | userId String 52 | expires DateTime 53 | user User @relation(fields: [userId], references: [id], onDelete: Cascade) 54 | 55 | createdAt DateTime @default(now()) 56 | updatedAt DateTime @updatedAt 57 | } 58 | 59 | model VerificationToken { 60 | identifier String 61 | token String 62 | expires DateTime 63 | 64 | @@unique([identifier, token]) 65 | } 66 | 67 | // Optional for WebAuthn support 68 | model Authenticator { 69 | credentialID String @unique 70 | userId String 71 | providerAccountId String 72 | credentialPublicKey String 73 | counter Int 74 | credentialDeviceType String 75 | credentialBackedUp Boolean 76 | transports String? 77 | 78 | user User @relation(fields: [userId], references: [id], onDelete: Cascade) 79 | 80 | @@id([userId, credentialID]) 81 | } 82 | -------------------------------------------------------------------------------- /script.md: -------------------------------------------------------------------------------- 1 | todo: 2 | 3 | # starting template 4 | 5 | npx shadcn@latest init (default, neutral) 6 | .nvmrc 7 | npx create-next-app@latest . 8 | 9 | npx shadcn@latest add button 10 | npx shadcn@latest add input 11 | 12 | ```ts src/components/sign-out.tsx 13 | "use client"; 14 | const SignOut = () => { 15 | const handleSignOut = async () => {}; 16 | 17 | return ( 18 | 21 | ); 22 | }; 23 | ``` 24 | 25 | ```ts ui/github.tsx 26 | const Github = () => { 27 | return ( 28 | 29 | GitHub 30 | 31 | 32 | ); 33 | }; 34 | ``` 35 | 36 | ```ts src/lib/executeAction.ts 37 | type Options = { 38 | actionFn: () => Promise; 39 | successMessage?: string; 40 | }; 41 | 42 | const executeAction = async ({ 43 | actionFn, 44 | successMessage = "The actions was successful", 45 | }: Options): Promise<{ success: boolean; message: string }> => { 46 | try { 47 | await actionFn(); 48 | 49 | return { 50 | success: true, 51 | message: successMessage, 52 | }; 53 | } catch (error) { 54 | if (isRedirectError(error)) { 55 | throw error; 56 | } 57 | 58 | return { 59 | success: false, 60 | message: "An error has occurred during executing the action", 61 | }; 62 | } 63 | }; 64 | ``` 65 | 66 | ```ts sign-in page.tsx 67 | const Page = async () => { 68 | return ( 69 |
70 |

Sign In

71 | 72 | 73 |
74 |
75 | 76 |
77 |
78 | 79 | Or continue with email 80 | 81 |
82 |
83 | 84 | {/* Email/Password Sign In */} 85 |
{}}> 86 | 93 | 100 | 103 |
104 | 105 |
106 | 109 |
110 |
111 | ); 112 | }; 113 | 114 | export default Page; 115 | ``` 116 | 117 | ```ts src/components/github-sign-in.tsx 118 | const GithubSignIn = () => { 119 | return ( 120 |
{}}> 121 | 125 |
126 | ); 127 | }; 128 | 129 | export { GithubSignIn }; 130 | ``` 131 | 132 | ```ts sign-up page.tsx 133 | const Page = () => { 134 | return ( 135 |
136 |

Create Account

137 | 138 | 139 | 140 |
141 |
142 | 143 |
144 |
145 | 146 | Or continue with email 147 | 148 |
149 |
150 | 151 | {/* Email/Password Sign Up */} 152 |
{}}> 153 | 160 | 167 | 170 |
171 | 172 |
173 | 176 |
177 |
178 | ); 179 | }; 180 | 181 | export default Page; 182 | ``` 183 | 184 | ```ts layout 185 | import type { Metadata } from "next"; 186 | import { Geist, Geist_Mono } from "next/font/google"; 187 | import "./globals.css"; 188 | 189 | const geistSans = Geist({ 190 | variable: "--font-geist-sans", 191 | subsets: ["latin"], 192 | }); 193 | 194 | const geistMono = Geist_Mono({ 195 | variable: "--font-geist-mono", 196 | subsets: ["latin"], 197 | }); 198 | 199 | export const metadata: Metadata = { 200 | title: "Create Next App", 201 | description: "Generated by create next app", 202 | }; 203 | 204 | export default function RootLayout({ 205 | children, 206 | }: Readonly<{ 207 | children: React.ReactNode; 208 | }>) { 209 | return ( 210 | 211 | 214 |
215 |
216 | {children} 217 |
218 |
219 | 220 | 221 | ); 222 | } 223 | ``` 224 | 225 | ```ts / 226 | import { SignOut } from "@/components/sign-out"; 227 | 228 | const Page = async () => { 229 | return ( 230 | <> 231 |
232 |

Signed in as:

233 |

TODO

234 |
235 | 236 | 237 | 238 | ); 239 | }; 240 | 241 | export default Page; 242 | ``` 243 | 244 | ```ts components/ui/github 245 | const Github = () => { 246 | return ( 247 | 248 | GitHub 249 | 250 | 251 | ); 252 | }; 253 | 254 | export { Github }; 255 | ``` 256 | 257 | --- 258 | 259 | # tutorial starts 260 | 261 | npm install next-auth@beta 262 | npx auth secret 263 | 264 | ```ts (auth)/page.tsx 265 | const session = await auth(); 266 | if (!session) redirect("/sign-in"); 267 | 268 |

{session.user?.email}

; 269 | ``` 270 | 271 | npm run dev to see everything works 272 | 273 | ```ts src/lib/auth.ts 274 | import NextAuth from "next-auth"; 275 | 276 | export const { handlers, signIn, signOut, auth } = NextAuth({ 277 | providers: [], 278 | }); 279 | ``` 280 | 281 | app/api/auth/[...nextauth]/route.ts 282 | 283 | ```ts 284 | import { handlers } from "@/app/(auth)/_utils/auth"; 285 | export const { GET, POST } = handlers; 286 | ``` 287 | 288 | oauth 289 | https://github.com/settings/developers 290 | new oauth app 291 | app name: auth-tutorial 292 | homepage: http://localhost:3000 293 | callback url: http://localhost:3000/api/auth/callback/github 294 | 295 | copy client id and client secrets 296 | 297 | and paste in env 298 | AUTH_GITHUB_ID= 299 | AUTH_GITHUB_SECRET= 300 | 301 | AUTH_TRUST_HOST =true 302 | 303 | ```ts 304 | providers: [GitHub], 305 | ``` 306 | 307 | ```ts src/components/github-sign-in.tsx 308 |
{ 310 | "use server"; 311 | await signIn("github"); 312 | }} 313 | > 314 | ``` 315 | 316 | ```ts sign-in page.tsx 317 | const session = await auth(); 318 | if (session) redirect("/"); 319 | 320 | action={async (formData) => { 321 | "use server"; 322 | await executeAction({ 323 | actionFn: async () => { 324 | await signIn("credentials", formData); 325 | }, 326 | }); 327 | }} 328 | 329 | ``` 330 | 331 | do sign up we github 332 | 333 | go to app on github and see user has added 334 | see user's session 335 | 336 | ```ts src/components/sign-out.tsx 337 | const handleSignOut = async () => { 338 | await signOut(); 339 | }; 340 | ``` 341 | 342 | you can add other providers like this according to docs if you want but they are mostly similar and very easy to do with authjs 343 | 344 | put it in page.tsx and show user logs out and again log in 345 | 346 | now for credentials 347 | 348 | ```ts 349 | Credentials({ 350 | credentials: { 351 | email: {}, 352 | password: {}, 353 | }, 354 | authorize: async (credentials) => { 355 | const email = "admin@admin.com"; 356 | const password = "1234"; 357 | 358 | if (credentials.email === email && credentials.password === password) { 359 | return { email, password }; 360 | } else { 361 | throw new Error("Invalid credentials."); 362 | 363 | } 364 | }, 365 | }), 366 | ``` 367 | 368 | ```ts 369 | {/* Email/Password Sign In */} 370 | action={async (formData) => { 371 | "use server"; 372 | await executeAction({ 373 | actionFn: async () => { 374 | await signIn("credentials", formData); 375 | }, 376 | }); 377 | }} 378 | ``` 379 | 380 | try to sign in 381 | so now we have oauth and credentials base 382 | but we want a real data base to save our users data 383 | 384 | --- 385 | 386 | database 387 | 388 | prisma extension vscode 389 | 390 | npm install prisma --save-dev 391 | 392 | src/db/schema.prisma 393 | 394 | ```prisma 395 | 396 | generator client { 397 | provider = "prisma-client-js" 398 | } 399 | 400 | datasource db { 401 | provider = "sqlite" 402 | url = env("DATABASE_URL") 403 | } 404 | 405 | model User { 406 | id String @id @default(uuid()) 407 | email String @unique 408 | password String 409 | } 410 | ``` 411 | 412 | src/lib/db/data empty folder 413 | 414 | DATABASE_URL="file:./data/dev.db" 415 | 416 | ```json package.json 417 | "prisma": { 418 | "schema": "src/lib/db/schema.prisma" 419 | }, 420 | 421 | script 422 | "db:migrate": "npx prisma migrate dev" 423 | ``` 424 | 425 | npm run db:migrate, this will generate ts types, migrations and dev.db and @prisma/client package 426 | show the dev.db created 427 | add db/data to git ignore 428 | 429 | npm run db:studio 430 | 431 | add a admin@admin.com, 1234 432 | emphasize that never directly save password and always hash of it 433 | 434 | now we want to implement it in credentials 435 | 436 | ```ts src/lib/db/db.ts 437 | import { PrismaClient } from "@prisma/client"; 438 | 439 | const prismaClientSingleton = () => { 440 | return new PrismaClient(); 441 | }; 442 | 443 | declare const globalThis: { 444 | prismaGlobal: ReturnType; 445 | } & typeof global; 446 | 447 | const db = globalThis.prismaGlobal ?? prismaClientSingleton(); 448 | 449 | export default db; 450 | 451 | if (process.env.NODE_ENV !== "production") globalThis.prismaGlobal = db; 452 | ``` 453 | 454 | ```ts 455 | 456 | Credentials({ 457 | credentials: { 458 | email: {}, 459 | password: {}, 460 | }, 461 | authorize: async (credentials) => { 462 | const user = await db.user.findFirst({ 463 | where: { email: credentials.email, password: credentials.password }, 464 | }); 465 | 466 | if (!user) { 467 | throw new Error("Invalid credentials."); 468 | } 469 | 470 | return user; 471 | }, 472 | }), 473 | ``` 474 | 475 | ```ts 476 | { 478 | "use server"; 479 | await executeAction({ 480 | actionFn: async () => { 481 | await signIn("credentials", formData); 482 | }, 483 | }); 484 | }} 485 | > 486 | 487 | 488 | 491 | 492 | ``` 493 | 494 | --- 495 | 496 | npm i @auth/prisma-adapter 497 | 498 | ```ts 499 | adapter: PrismaAdapter(db), 500 | ``` 501 | 502 | ```prisma 503 | model User { 504 | id String @id @default(cuid()) 505 | name String? 506 | email String? @unique 507 | emailVerified DateTime? 508 | image String? 509 | accounts Account[] 510 | sessions Session[] 511 | // Optional for WebAuthn support 512 | Authenticator Authenticator[] 513 | 514 | createdAt DateTime @default(now()) 515 | updatedAt DateTime @updatedAt 516 | } 517 | 518 | model Account { 519 | id String @id @default(cuid()) 520 | userId String 521 | type String 522 | provider String 523 | providerAccountId String 524 | refresh_token String? 525 | access_token String? 526 | expires_at Int? 527 | token_type String? 528 | scope String? 529 | id_token String? 530 | session_state String? 531 | 532 | createdAt DateTime @default(now()) 533 | updatedAt DateTime @updatedAt 534 | 535 | user User @relation(fields: [userId], references: [id], onDelete: Cascade) 536 | 537 | @@unique([provider, providerAccountId]) 538 | } 539 | 540 | model Session { 541 | id String @id @default(cuid()) 542 | sessionToken String @unique 543 | userId String 544 | expires DateTime 545 | user User @relation(fields: [userId], references: [id], onDelete: Cascade) 546 | 547 | createdAt DateTime @default(now()) 548 | updatedAt DateTime @updatedAt 549 | } 550 | 551 | model VerificationToken { 552 | identifier String 553 | token String 554 | expires DateTime 555 | 556 | @@unique([identifier, token]) 557 | } 558 | 559 | // Optional for WebAuthn support 560 | model Authenticator { 561 | credentialID String @unique 562 | userId String 563 | providerAccountId String 564 | credentialPublicKey String 565 | counter Int 566 | credentialDeviceType String 567 | credentialBackedUp Boolean 568 | transports String? 569 | 570 | user User @relation(fields: [userId], references: [id], onDelete: Cascade) 571 | 572 | @@id([userId, credentialID]) 573 | } 574 | 575 | ``` 576 | 577 | "db:reset": "prisma migrate reset && prisma migrate dev", 578 | 579 | npm run db:reset 580 | 581 | go to github and revoke all users and again sign in with github 582 | show full session 583 | 584 | npm run db:studio 585 | see new things added 586 | 587 | lets enhance things 588 | 589 | npm i zod 590 | 591 | ```ts src/lib/schema.ts 592 | const schema = z.object({ 593 | email: z.string().email(), 594 | password: z.string().min(1), 595 | }); 596 | ``` 597 | 598 | ```ts auth.ts 599 | const validatedCredentials = await schema.parseAsync(credentials); 600 | 601 | const user = await db.user.findFirst({ 602 | where: { 603 | email: validatedCredentials.email, 604 | password: validatedCredentials.password, 605 | }, 606 | }); 607 | ``` 608 | 609 | add password to user prisma 610 | npm run db:reset 611 | errors are gone 612 | we want register user by credentials 613 | 614 | ```ts src/lib/actions.ts 615 | const signUp = async (formData: FormData) => { 616 | return executeAction({ 617 | actionFn: async () => { 618 | const email = formData.get("email"); 619 | const password = formData.get("password"); 620 | const validatedData = schema.parse({ email, password }); 621 | await db.user.create({ 622 | data: { 623 | email: validatedData.email.toLocaleLowerCase(), 624 | password: validatedData.password, 625 | }, 626 | }); 627 | }, 628 | }); 629 | }; 630 | ``` 631 | 632 | ```ts sign-up/page.tsx 633 | 634 | const session = await auth(); 635 | if (session) redirect("/"); 636 | 637 |
{ 640 | "use server"; 641 | const res = await signUp(formData); 642 | if (res.success) { 643 | redirect("/sign-in"); 644 | } 645 | }} 646 | > 647 | ``` 648 | 649 | when it comes to sign in with credentials with auth js with database session things become a little complete because we need to expand the auth config 650 | 651 | ```ts 652 | callbacks: { 653 | async jwt({ token, account }) { 654 | if (account?.provider === "credentials") { 655 | token.credentials = true; 656 | } 657 | return token; 658 | }, 659 | }, 660 | ``` 661 | 662 | show what callbacks returns 663 | 664 | ```ts 665 | const adapter = PrismaAdapter(db); 666 | 667 | export const { handlers, signIn, signOut, auth } = NextAuth({ 668 | adapter, 669 | ``` 670 | 671 | ```ts 672 | jwt: { 673 | encode: async function (params) { 674 | if (params.token?.credentials) { 675 | const sessionToken = uuid(); 676 | 677 | if (!params.token.sub) { 678 | throw new Error("No user ID found in token"); 679 | } 680 | 681 | const createdSession = await adapter?.createSession?.({ 682 | sessionToken: sessionToken, 683 | userId: params.token.sub, 684 | expires: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000), 685 | }); 686 | 687 | if (!createdSession) { 688 | throw new Error("Failed to create session"); 689 | } 690 | 691 | return sessionToken; 692 | } 693 | return defaultEncode(params); 694 | }, 695 | }, 696 | ``` 697 | --------------------------------------------------------------------------------