├── .env ├── .eslintrc.json ├── .gitignore ├── README.md ├── components.json ├── next.config.mjs ├── package-lock.json ├── package.json ├── postcss.config.mjs ├── prisma ├── migrations │ ├── 20240904103116_delete_users_model │ │ └── migration.sql │ ├── 20240904113418_delete_user_model │ │ └── migration.sql │ ├── 20240904113700_init │ │ └── migration.sql │ └── migration_lock.toml └── schema.prisma ├── public ├── next.svg └── vercel.svg ├── src ├── app │ ├── (auth) │ │ ├── forgot-password │ │ │ └── page.tsx │ │ ├── layout.tsx │ │ ├── otp-verifacation │ │ │ └── page.tsx │ │ ├── set-new-password │ │ │ └── page.tsx │ │ ├── sign-in │ │ │ └── page.tsx │ │ └── sign-up │ │ │ └── page.tsx │ ├── (dashboard) │ │ └── admin │ │ │ └── page.tsx │ ├── api │ │ ├── auth │ │ │ └── [...nextauth] │ │ │ │ └── route.ts │ │ ├── reset-password │ │ │ └── route.ts │ │ ├── update-password │ │ │ └── route.ts │ │ ├── user │ │ │ └── route.ts │ │ └── verify-otp │ │ │ └── route.ts │ ├── favicon.ico │ ├── globals.css │ ├── layout.tsx │ └── page.tsx ├── components │ ├── GoogleSignInButton.tsx │ ├── Navbar.tsx │ ├── Provider.tsx │ ├── User.tsx │ ├── UserAccountnav.tsx │ ├── form │ │ ├── SignInForm.tsx │ │ └── SignUpForm.tsx │ ├── hooks │ │ └── use-toast.ts │ └── ui │ │ ├── button.tsx │ │ ├── form.tsx │ │ ├── input.tsx │ │ ├── label.tsx │ │ ├── toast.tsx │ │ └── toaster.tsx ├── lib │ ├── auth.ts │ ├── prismadb.ts │ └── utils.ts ├── styles │ └── globals.css └── types │ └── next-auth.d.ts ├── tailwind.config.ts └── tsconfig.json /.env: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code-with-zain-hunzai/NextAuth/74fcefd1d38dbd568847d2735bb716b725da1cda/.env -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # NextAuth -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /next.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = {}; 3 | 4 | export default nextConfig; 5 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nextauth", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "@auth/prisma-adapter": "^2.4.2", 13 | "@hookform/resolvers": "^3.9.0", 14 | "@next-auth/prisma-adapter": "^1.0.7", 15 | "@prisma/client": "^5.19.1", 16 | "@radix-ui/react-label": "^2.1.0", 17 | "@radix-ui/react-slot": "^1.1.0", 18 | "@radix-ui/react-toast": "^1.2.1", 19 | "axios": "^1.7.7", 20 | "bcrypt": "^5.1.1", 21 | "bcryptjs": "^2.4.3", 22 | "class-variance-authority": "^0.7.0", 23 | "clsx": "^2.1.1", 24 | "lucide-react": "^0.438.0", 25 | "next": "14.2.7", 26 | "next-auth": "^4.24.7", 27 | "nodemailer": "^6.9.15", 28 | "react": "^18", 29 | "react-dom": "^18", 30 | "react-hook-form": "^7.53.0", 31 | "tailwind-merge": "^2.5.2", 32 | "tailwindcss-animate": "^1.0.7", 33 | "zod": "^3.23.8" 34 | }, 35 | "devDependencies": { 36 | "@types/bcrypt": "^5.0.2", 37 | "@types/bcryptjs": "^2.4.6", 38 | "@types/node": "^20", 39 | "@types/nodemailer": "^6.4.15", 40 | "@types/react": "^18", 41 | "@types/react-dom": "^18", 42 | "eslint": "^8", 43 | "eslint-config-next": "14.2.7", 44 | "postcss": "^8", 45 | "prisma": "^5.19.1", 46 | "tailwindcss": "^3.4.1", 47 | "typescript": "^5" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /prisma/migrations/20240904103116_delete_users_model/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE "User" ( 3 | "id" SERIAL NOT NULL, 4 | "username" TEXT NOT NULL, 5 | "email" TEXT NOT NULL, 6 | "password" TEXT NOT NULL, 7 | 8 | CONSTRAINT "User_pkey" PRIMARY KEY ("id") 9 | ); 10 | 11 | -- CreateIndex 12 | CREATE UNIQUE INDEX "User_username_key" ON "User"("username"); 13 | 14 | -- CreateIndex 15 | CREATE UNIQUE INDEX "User_email_key" ON "User"("email"); 16 | -------------------------------------------------------------------------------- /prisma/migrations/20240904113418_delete_user_model/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - You are about to drop the `User` table. If the table is not empty, all the data it contains will be lost. 5 | 6 | */ 7 | -- DropTable 8 | DROP TABLE "User"; 9 | -------------------------------------------------------------------------------- /prisma/migrations/20240904113700_init/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE "User" ( 3 | "id" SERIAL NOT NULL, 4 | "username" TEXT NOT NULL, 5 | "email" TEXT NOT NULL, 6 | "password" TEXT NOT NULL, 7 | "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 8 | "updateUt" TIMESTAMP(3) NOT NULL, 9 | 10 | CONSTRAINT "User_pkey" PRIMARY KEY ("id") 11 | ); 12 | 13 | -- CreateIndex 14 | CREATE UNIQUE INDEX "User_username_key" ON "User"("username"); 15 | 16 | -- CreateIndex 17 | CREATE UNIQUE INDEX "User_email_key" ON "User"("email"); 18 | -------------------------------------------------------------------------------- /prisma/migrations/migration_lock.toml: -------------------------------------------------------------------------------- 1 | # Please do not edit this file manually 2 | # It should be added in your version-control system (i.e. Git) 3 | provider = "postgresql" -------------------------------------------------------------------------------- /prisma/schema.prisma: -------------------------------------------------------------------------------- 1 | generator client { 2 | provider = "prisma-client-js" 3 | } 4 | 5 | datasource db { 6 | provider = "postgresql" 7 | url = env("DATABASE_URL") 8 | } 9 | 10 | // model User { 11 | // id Int @id @default(autoincrement()) 12 | // username String @unique 13 | // email String @unique 14 | // password String 15 | // createdAt DateTime @default(now()) 16 | // updateUt DateTime @updatedAt 17 | // } 18 | 19 | model Account { 20 | id String @id @default(cuid()) 21 | userId String @map("user_id") 22 | type String 23 | provider String 24 | providerAccountId String @map("provider_account_id") 25 | refresh_token String? @db.Text 26 | access_token String? @db.Text 27 | expires_at Int? 28 | token_type String? 29 | scope String? 30 | id_token String? @db.Text 31 | session_state String? 32 | 33 | user User @relation(fields: [userId], references: [id], onDelete: Cascade) 34 | 35 | @@unique([provider, providerAccountId]) 36 | @@map("accounts") 37 | } 38 | 39 | model Session { 40 | id String @id @default(cuid()) 41 | sessionToken String @unique @map("session_token") 42 | userId String @map("user_id") 43 | expires DateTime 44 | user User @relation(fields: [userId], references: [id], onDelete: Cascade) 45 | 46 | @@map("sessions") 47 | } 48 | 49 | model User { 50 | id String @id @default(cuid()) 51 | username String? @unique 52 | password String? 53 | createdAt DateTime @default(now()) 54 | updatedUt DateTime @updatedAt 55 | name String? 56 | email String? @unique 57 | emailVerified DateTime? @map("email_verified") 58 | image String? 59 | accounts Account[] 60 | sessions Session[] 61 | 62 | @@map("users") 63 | } 64 | 65 | model VerificationToken { 66 | identifier String 67 | token String 68 | expires DateTime 69 | 70 | @@unique([identifier, token]) 71 | @@map("verificationtokens") 72 | } 73 | 74 | model OTP { 75 | id Int @id @default(autoincrement()) 76 | email String @unique 77 | otp String 78 | expiresAt DateTime 79 | } 80 | 81 | 82 | -------------------------------------------------------------------------------- /public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/(auth)/forgot-password/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { useForm } from 'react-hook-form'; 4 | import { Input } from '@/components/ui/input'; 5 | import { Button } from '@/components/ui/button'; 6 | import { useToast } from '@/components/hooks/use-toast'; 7 | import { useRouter } from 'next/navigation'; 8 | import axios from 'axios'; 9 | 10 | const ForgotPassword = () => { 11 | const { toast } = useToast(); 12 | const router = useRouter(); 13 | const form = useForm<{ email: string }>({ 14 | defaultValues: { email: '' }, 15 | }); 16 | 17 | const onSubmit = async (values: { email: string }) => { 18 | try { 19 | const response = await axios.post('/api/auth/reset-password', values); 20 | 21 | if (response.status === 200) { 22 | toast({ 23 | title: 'Success', 24 | description: 'OTP has been sent to your email.', 25 | variant: 'default', 26 | }); 27 | router.push('/otp-verification'); 28 | } 29 | } catch (error) { 30 | toast({ 31 | title: 'Error', 32 | description: 'Failed to send OTP. Please try again.', 33 | variant: 'destructive', 34 | }); 35 | } 36 | }; 37 | 38 | return ( 39 |
40 |

Forgot Password

41 |
42 | 46 | 47 |
48 |
49 | ); 50 | }; 51 | 52 | export default ForgotPassword; 53 | -------------------------------------------------------------------------------- /src/app/(auth)/layout.tsx: -------------------------------------------------------------------------------- 1 | import { FC, ReactNode } from 'react'; 2 | 3 | interface AuthLayoutProps { 4 | children: ReactNode; 5 | } 6 | 7 | const AuthLayout: FC = ({ children }) => { 8 | return
{children}
; 9 | }; 10 | 11 | export default AuthLayout; 12 | -------------------------------------------------------------------------------- /src/app/(auth)/otp-verifacation/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { useState } from 'react'; 4 | import { useForm } from 'react-hook-form'; 5 | import { Input } from '@/components/ui/input'; 6 | import { Button } from '@/components/ui/button'; 7 | import { useToast } from "@/components/hooks/use-toast"; 8 | import { useRouter } from 'next/navigation'; 9 | 10 | const OtpVerification = () => { 11 | const { toast } = useToast(); 12 | const router = useRouter(); 13 | const [otp, setOtp] = useState(new Array(6).fill('')); 14 | 15 | const { handleSubmit } = useForm<{ otp: string }>({ 16 | defaultValues: { otp: otp.join('') }, 17 | }); 18 | 19 | const handleOtpChange = (index: number, value: string) => { 20 | const newOtp = [...otp]; 21 | newOtp[index] = value; 22 | setOtp(newOtp); 23 | 24 | // Handle backspace 25 | if (value === '') { 26 | if (index > 0) { 27 | const prevInput = document.getElementById(`otp-input-${index - 1}`); 28 | prevInput?.focus(); 29 | } 30 | } else if (index < 5 && value.length === 1) { 31 | const nextInput = document.getElementById(`otp-input-${index + 1}`); 32 | nextInput?.focus(); 33 | } 34 | }; 35 | 36 | const onSubmit = async () => { 37 | const otpValue = otp.join(''); 38 | const res = await fetch('/api/auth/verify-otp', { 39 | method: 'POST', 40 | headers: { 'Content-Type': 'application/json' }, 41 | body: JSON.stringify({ otp: otpValue }), 42 | }); 43 | 44 | if (res.ok) { 45 | toast({ 46 | title: 'Success', 47 | description: 'OTP verified. Please set a new password.', 48 | variant: 'default', 49 | }); 50 | router.push('/set-new-password'); 51 | } else { 52 | toast({ 53 | title: 'Error', 54 | description: 'Invalid OTP. Please try again.', 55 | variant: 'destructive', 56 | }); 57 | } 58 | }; 59 | 60 | return ( 61 |
62 |

OTP Verification

63 |
64 |
65 | {otp.map((value, index) => ( 66 | handleOtpChange(index, e.target.value)} 73 | onKeyDown={(e) => { 74 | if (e.key === 'Backspace') { 75 | handleOtpChange(index, ''); 76 | } 77 | }} 78 | className="w-12 text-center" 79 | required 80 | /> 81 | ))} 82 |
83 |
84 | 85 |
86 |
87 |
88 | ); 89 | }; 90 | 91 | export default OtpVerification; 92 | -------------------------------------------------------------------------------- /src/app/(auth)/set-new-password/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { useForm } from 'react-hook-form'; 4 | import { Input } from '@/components/ui/input'; 5 | import { Button } from '@/components/ui/button'; 6 | import { useToast } from "@/components/hooks/use-toast"; 7 | import { useRouter } from 'next/navigation'; 8 | 9 | const ResetPasswordPage = () => { 10 | const { toast } = useToast(); 11 | const router = useRouter(); 12 | 13 | const { register, handleSubmit, watch, formState: { errors } } = useForm<{ password: string; confirmPassword: string }>({ 14 | defaultValues: { password: '', confirmPassword: '' }, 15 | }); 16 | 17 | const onSubmit = async (values: { password: string; confirmPassword: string }) => { 18 | if (values.password !== values.confirmPassword) { 19 | toast({ 20 | title: 'Error', 21 | description: 'Passwords do not match.', 22 | variant: 'destructive', 23 | }); 24 | return; 25 | } 26 | 27 | const response = await fetch('/api/auth/reset-password', { 28 | method: 'POST', 29 | headers: { 'Content-Type': 'application/json' }, 30 | body: JSON.stringify({ password: values.password }), 31 | }); 32 | 33 | const result = await response.json(); 34 | 35 | if (result.success) { 36 | toast({ 37 | title: 'Password Reset', 38 | description: 'Your password has been updated successfully.', 39 | variant: 'default', 40 | }); 41 | router.push('/sign-in'); 42 | } else { 43 | toast({ 44 | title: 'Error', 45 | description: 'Something went wrong!', 46 | variant: 'destructive', 47 | }); 48 | } 49 | }; 50 | 51 | return ( 52 |
53 |

Set New Password

54 |
55 | 60 | 65 | {errors.password &&

Password is required

} 66 | {errors.confirmPassword &&

Confirm password is required

} 67 | 68 |
69 |
70 | ); 71 | }; 72 | 73 | export default ResetPasswordPage; 74 | -------------------------------------------------------------------------------- /src/app/(auth)/sign-in/page.tsx: -------------------------------------------------------------------------------- 1 | import SignInForm from '@/components/form/SignInForm'; 2 | 3 | const page = () => { 4 | return ( 5 |
6 | 7 |
8 | ); 9 | }; 10 | 11 | export default page; 12 | -------------------------------------------------------------------------------- /src/app/(auth)/sign-up/page.tsx: -------------------------------------------------------------------------------- 1 | import SignUpForm from '@/components/form/SignUpForm'; 2 | 3 | const page = () => { 4 | return ( 5 |
6 | 7 |
8 | ); 9 | }; 10 | 11 | export default page; 12 | -------------------------------------------------------------------------------- /src/app/(dashboard)/admin/page.tsx: -------------------------------------------------------------------------------- 1 | import { authOptions } from '@/lib/auth' 2 | import { getServerSession } from 'next-auth' 3 | 4 | const page = async () => { 5 | const session = await getServerSession(authOptions) 6 | 7 | if (session?.user) { 8 | return

Admin page welcome back {session?.user.username || session.user.name}

9 | } 10 | return

Please login to see this admin page

11 | } 12 | 13 | export default page 14 | -------------------------------------------------------------------------------- /src/app/api/auth/[...nextauth]/route.ts: -------------------------------------------------------------------------------- 1 | import NextAuth from "next-auth"; 2 | import { authOptions } from "@/lib/auth"; 3 | const handler = NextAuth(authOptions); 4 | 5 | export { handler as GET, handler as POST }; 6 | -------------------------------------------------------------------------------- /src/app/api/reset-password/route.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from 'next/server'; 2 | import prisma from '@/lib/prismadb'; 3 | import nodemailer from 'nodemailer'; 4 | import crypto from 'crypto'; 5 | 6 | export async function POST(request: Request) { 7 | try { 8 | const { email } = await request.json(); 9 | 10 | console.log("EMAIL_USER:", process.env.EMAIL_USER); 11 | console.log("EMAIL_PASS:", process.env.EMAIL_PASS); 12 | 13 | const user = await prisma.user.findUnique({ where: { email } }); 14 | 15 | if (!user) { 16 | return NextResponse.json({ error: 'User not found' }, { status: 404 }); 17 | } 18 | 19 | const otp = crypto.randomInt(100000, 999999).toString(); 20 | 21 | const transporter = nodemailer.createTransport({ 22 | service: 'gmail', 23 | auth: { 24 | user: process.env.EMAIL_USER, 25 | pass: process.env.EMAIL_PASS, 26 | }, 27 | }); 28 | 29 | const mailOptions = { 30 | from: process.env.EMAIL_USER, 31 | to: email, 32 | subject: 'Password Reset OTP', 33 | text: `Your OTP is ${otp}`, 34 | }; 35 | 36 | await transporter.sendMail(mailOptions); 37 | 38 | await prisma.oTP.create({ 39 | data: { 40 | email, 41 | otp, 42 | expiresAt: new Date(Date.now() + 15 * 60 * 1000), 43 | }, 44 | }); 45 | 46 | return NextResponse.json({ message: 'OTP sent successfully' }, { status: 200 }); 47 | 48 | } catch (error) { 49 | let errorMessage = 'Internal Server Error'; 50 | 51 | 52 | if (error instanceof Error) { 53 | errorMessage = error.message; 54 | } 55 | 56 | console.error('Error:', errorMessage); 57 | return NextResponse.json({ error: errorMessage }, { status: 500 }); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/app/api/update-password/route.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from 'next/server'; 2 | import prisma from '@/lib/prismadb'; 3 | import bcrypt from 'bcryptjs'; 4 | 5 | export async function POST(request: Request) { 6 | const { email, newPassword } = await request.json(); 7 | 8 | const hashedPassword = await bcrypt.hash(newPassword, 10); 9 | 10 | await prisma.user.update({ 11 | where: { email }, 12 | data: { password: hashedPassword }, 13 | }); 14 | 15 | return NextResponse.json({ message: 'Password updated successfully' }); 16 | } 17 | -------------------------------------------------------------------------------- /src/app/api/user/route.ts: -------------------------------------------------------------------------------- 1 | import { prismadb } from "@/lib/prismadb"; 2 | import { hash } from "bcrypt"; 3 | import { NextResponse } from "next/server"; 4 | import * as z from 'zod' 5 | 6 | const userSchema = z 7 | .object({ 8 | username: z.string().min(1, 'Username is required').max(100), 9 | email: z.string().min(1, 'Email is required').email('Invalid email'), 10 | password: z 11 | .string() 12 | .min(1, 'Password is required') 13 | .min(8, 'Password must have more than 8 characters'), 14 | }) 15 | 16 | export async function POST(req: Request) { 17 | try { 18 | const body = await req.json(); 19 | console.log('Request body:', body); 20 | const { email, username, password } = userSchema.parse(body); 21 | 22 | // Check if email already exists 23 | const existingUserByEmail = await prismadb.user.findUnique({ 24 | where: { email }, 25 | }); 26 | if (existingUserByEmail) { 27 | return NextResponse.json({ user: null, message: "User with this email already exists" }, { status: 409 }); 28 | } 29 | 30 | // Check if username already exists 31 | const existingUserByUsername = await prismadb.user.findUnique({ 32 | where: { username }, 33 | }); 34 | if (existingUserByUsername) { 35 | return NextResponse.json({ user: null, message: "User with this username already exists" }, { status: 409 }); 36 | } 37 | 38 | // Hash the password 39 | const hashedPassword = await hash(password, 10); 40 | 41 | // Create the new user 42 | const newUser = await prismadb.user.create({ 43 | data: { 44 | username, 45 | email, 46 | password: hashedPassword, 47 | }, 48 | }); 49 | 50 | const { password: newUserPassword, ...rest } = newUser; 51 | 52 | return NextResponse.json({ user: rest, message: "User created successfully" }, { status: 201 }); 53 | } catch (error) { 54 | console.error("Error creating user:", error); 55 | return NextResponse.json({ message: "Something went wrong!" }, { status: 500 }); 56 | } 57 | } 58 | 59 | -------------------------------------------------------------------------------- /src/app/api/verify-otp/route.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from 'next/server'; 2 | import prisma from '@/lib/prismadb'; 3 | 4 | export async function POST(request: Request) { 5 | const { email, otp } = await request.json(); 6 | 7 | // Query OTP by email 8 | const storedOtp = await prisma.oTP.findUnique({ where: { email } }); 9 | 10 | if (!storedOtp || storedOtp.otp !== otp || new Date() > storedOtp.expiresAt) { 11 | return NextResponse.json({ error: 'Invalid or expired OTP' }, { status: 400 }); 12 | } 13 | 14 | return NextResponse.json({ message: 'OTP verified' }); 15 | } 16 | -------------------------------------------------------------------------------- /src/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code-with-zain-hunzai/NextAuth/74fcefd1d38dbd568847d2735bb716b725da1cda/src/app/favicon.ico -------------------------------------------------------------------------------- /src/app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | :root { 6 | --foreground-rgb: 0, 0, 0; 7 | --background-start-rgb: 214, 219, 220; 8 | --background-end-rgb: 255, 255, 255; 9 | } 10 | 11 | @media (prefers-color-scheme: dark) { 12 | :root { 13 | --foreground-rgb: 255, 255, 255; 14 | --background-start-rgb: 0, 0, 0; 15 | --background-end-rgb: 0, 0, 0; 16 | } 17 | } 18 | 19 | @layer base { 20 | :root { 21 | --background: 0 0% 100%; 22 | --foreground: 0 0% 3.9%; 23 | --card: 0 0% 100%; 24 | --card-foreground: 0 0% 3.9%; 25 | --popover: 0 0% 100%; 26 | --popover-foreground: 0 0% 3.9%; 27 | --primary: 0 0% 9%; 28 | --primary-foreground: 0 0% 98%; 29 | --secondary: 0 0% 96.1%; 30 | --secondary-foreground: 0 0% 9%; 31 | --muted: 0 0% 96.1%; 32 | --muted-foreground: 0 0% 45.1%; 33 | --accent: 0 0% 96.1%; 34 | --accent-foreground: 0 0% 9%; 35 | --destructive: 0 84.2% 60.2%; 36 | --destructive-foreground: 0 0% 98%; 37 | --border: 0 0% 89.8%; 38 | --input: 0 0% 89.8%; 39 | --ring: 0 0% 3.9%; 40 | --chart-1: 12 76% 61%; 41 | --chart-2: 173 58% 39%; 42 | --chart-3: 197 37% 24%; 43 | --chart-4: 43 74% 66%; 44 | --chart-5: 27 87% 67%; 45 | --radius: 0.5rem; 46 | } 47 | .dark { 48 | --background: 0 0% 3.9%; 49 | --foreground: 0 0% 98%; 50 | --card: 0 0% 3.9%; 51 | --card-foreground: 0 0% 98%; 52 | --popover: 0 0% 3.9%; 53 | --popover-foreground: 0 0% 98%; 54 | --primary: 0 0% 98%; 55 | --primary-foreground: 0 0% 9%; 56 | --secondary: 0 0% 14.9%; 57 | --secondary-foreground: 0 0% 98%; 58 | --muted: 0 0% 14.9%; 59 | --muted-foreground: 0 0% 63.9%; 60 | --accent: 0 0% 14.9%; 61 | --accent-foreground: 0 0% 98%; 62 | --destructive: 0 62.8% 30.6%; 63 | --destructive-foreground: 0 0% 98%; 64 | --border: 0 0% 14.9%; 65 | --input: 0 0% 14.9%; 66 | --ring: 0 0% 83.1%; 67 | --chart-1: 220 70% 50%; 68 | --chart-2: 160 60% 45%; 69 | --chart-3: 30 80% 55%; 70 | --chart-4: 280 65% 60%; 71 | --chart-5: 340 75% 55%; 72 | } 73 | } 74 | 75 | @layer base { 76 | * { 77 | @apply border-border; 78 | } 79 | body { 80 | @apply bg-background text-foreground; 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import Navbar from '@/components/Navbar'; 2 | import Provider from '@/components/Provider'; 3 | import { Toaster } from '@/components/ui/toaster'; 4 | import '@/styles/globals.css'; 5 | import type { Metadata } from 'next'; 6 | import { Inter } from 'next/font/google'; 7 | 8 | const inter = Inter({ subsets: ['latin'] }); 9 | 10 | export const metadata: Metadata = { 11 | title: 'NextAuth', 12 | description: 'Generated by create next app', 13 | }; 14 | 15 | export default function RootLayout({ 16 | children, 17 | }: { 18 | children: React.ReactNode; 19 | }) { 20 | return ( 21 | 22 | 23 | 24 |
25 | 26 | {children} 27 |
28 | 29 |
30 | 31 | 32 | ); 33 | } 34 | -------------------------------------------------------------------------------- /src/app/page.tsx: -------------------------------------------------------------------------------- 1 | import User from "@/components/User" 2 | import { buttonVariants } from "@/components/ui/button" 3 | import { authOptions } from "@/lib/auth" 4 | import { getServerSession } from "next-auth" 5 | import Link from "next/link" 6 | export default async function Home() { 7 | const session = await getServerSession(authOptions) 8 | return
9 |

Home

10 | 11 | Open my admin 12 | 13 | 14 |

Client Session

15 | 16 |

Server Session

17 | {JSON.stringify(session)} 18 |
19 | } 20 | -------------------------------------------------------------------------------- /src/components/GoogleSignInButton.tsx: -------------------------------------------------------------------------------- 1 | import { FC, ReactNode } from 'react'; 2 | import { Button } from './ui/button'; 3 | import { signIn } from 'next-auth/react' 4 | 5 | interface GoogleSignInButtonProps { 6 | children: ReactNode; 7 | } 8 | const GoogleSignInButton: FC = ({ children }) => { 9 | const loginWithGoogle = () => signIn('google', { callbackUrl: 'http://localhost:3000/admin' }); 10 | 11 | return ( 12 | 15 | ); 16 | }; 17 | 18 | export default GoogleSignInButton; 19 | -------------------------------------------------------------------------------- /src/components/Navbar.tsx: -------------------------------------------------------------------------------- 1 | import Link from 'next/link'; 2 | import { buttonVariants } from './ui/button'; 3 | import { HandMetal } from 'lucide-react'; 4 | import { getServerSession } from 'next-auth'; 5 | import { authOptions } from '@/lib/auth'; 6 | import UserAccountnav from './UserAccountnav'; 7 | 8 | 9 | const Navbar = async () => { 10 | const session = await getServerSession(authOptions) 11 | return ( 12 |
13 |
14 | 15 | 16 | 17 | {session?.user ? ( 18 | 19 | ) : ( 20 | 21 | Sign in 22 | 23 | )} 24 |
25 |
26 | ); 27 | }; 28 | 29 | export default Navbar; 30 | -------------------------------------------------------------------------------- /src/components/Provider.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | import { SessionProvider } from "next-auth/react" 3 | import { ReactNode, FC } from "react" 4 | 5 | interface ProviderProps { 6 | children: ReactNode 7 | } 8 | 9 | const Provider: FC = ({ children }) => { 10 | return ( 11 | {children} 12 | ) 13 | } 14 | 15 | export default Provider 16 | -------------------------------------------------------------------------------- /src/components/User.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { useSession } from "next-auth/react"; 4 | 5 | const User = () => { 6 | const { data: session } = useSession(); 7 | 8 | return ( 9 |
{JSON.stringify(session, null, 2)}
10 | ); 11 | }; 12 | 13 | export default User; 14 | -------------------------------------------------------------------------------- /src/components/UserAccountnav.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | import { Button } from "./ui/button" 3 | import { signOut } from "next-auth/react" 4 | const UserAccountnav = () => { 5 | return ( 6 |
7 | 12 |
13 | ) 14 | } 15 | 16 | export default UserAccountnav 17 | -------------------------------------------------------------------------------- /src/components/form/SignInForm.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { useForm } from 'react-hook-form'; 4 | import { 5 | Form, 6 | FormControl, 7 | FormField, 8 | FormItem, 9 | FormLabel, 10 | FormMessage, 11 | } from '../ui/form'; 12 | import * as z from 'zod'; 13 | import { zodResolver } from '@hookform/resolvers/zod'; 14 | import { Input } from '../ui/input'; 15 | import { Button } from '../ui/button'; 16 | import Link from 'next/link'; 17 | import GoogleSignInButton from '../GoogleSignInButton'; 18 | import { signIn } from 'next-auth/react'; 19 | import { useRouter } from 'next/navigation'; 20 | import { useToast } from "@/components/hooks/use-toast" 21 | 22 | 23 | 24 | const FormSchema = z.object({ 25 | email: z.string().min(1, 'Email is required').email('Invalid email'), 26 | password: z 27 | .string() 28 | .min(1, 'Password is required') 29 | .min(8, 'Password must have more than 8 characters'), 30 | }); 31 | 32 | const SignInForm = () => { 33 | const router = useRouter() 34 | const { toast } = useToast() 35 | const form = useForm>({ 36 | resolver: zodResolver(FormSchema), 37 | defaultValues: { 38 | email: '', 39 | password: '', 40 | }, 41 | }); 42 | 43 | const onSubmit = async (values: z.infer) => { 44 | try { 45 | const signInData = await signIn('credentials', { 46 | email: values.email, 47 | password: values.password, 48 | redirect: false, 49 | }); 50 | 51 | if (signInData?.error) { 52 | toast({ 53 | title: "Error", 54 | description: "Oops! something wents wrong!", 55 | variant: "destructive" 56 | }) 57 | } else { 58 | router.refresh(); 59 | router.push('./admin'); 60 | } 61 | } catch (error) { 62 | console.error('SignIn error:', error); 63 | } 64 | }; 65 | 66 | return ( 67 |
68 | 69 |
70 | ( 74 | 75 | Email 76 | 77 | 78 | 79 | 80 | 81 | )} 82 | /> 83 | ( 87 | 88 | Password 89 | 90 | 95 | 96 | 97 | 98 | )} 99 | /> 100 |
101 | 104 |
105 |
106 | or 107 |
108 | Sign in with Google 109 |

110 | If you don't have an account, please  111 | 112 | Sign up 113 | 114 |

115 |

116 | 117 | Forgot Password? 118 | 119 |

120 | 121 | ); 122 | }; 123 | 124 | export default SignInForm; 125 | -------------------------------------------------------------------------------- /src/components/form/SignUpForm.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { useForm } from 'react-hook-form'; 4 | import { 5 | Form, 6 | FormControl, 7 | FormField, 8 | FormItem, 9 | FormLabel, 10 | FormMessage, 11 | } from '../ui/form'; 12 | import * as z from 'zod'; 13 | import { zodResolver } from '@hookform/resolvers/zod'; 14 | import { Input } from '../ui/input'; 15 | import { Button } from '../ui/button'; 16 | import Link from 'next/link'; 17 | import GoogleSignInButton from '../GoogleSignInButton'; 18 | import { useRouter } from 'next/navigation'; 19 | import { useToast } from "@/components/hooks/use-toast" 20 | 21 | const FormSchema = z 22 | .object({ 23 | username: z.string().min(1, 'Username is required').max(100), 24 | email: z.string().min(1, 'Email is required').email('Invalid email'), 25 | password: z 26 | .string() 27 | .min(1, 'Password is required') 28 | .min(8, 'Password must have than 8 characters'), 29 | confirmPassword: z.string().min(1, 'Password confirmation is required'), 30 | }) 31 | .refine((data) => data.password === data.confirmPassword, { 32 | path: ['confirmPassword'], 33 | message: 'Password do not match', 34 | }); 35 | 36 | const SignUpForm = () => { 37 | const router = useRouter() 38 | const { toast } = useToast() 39 | const form = useForm>({ 40 | resolver: zodResolver(FormSchema), 41 | defaultValues: { 42 | username: '', 43 | email: '', 44 | password: '', 45 | confirmPassword: '', 46 | }, 47 | }); 48 | 49 | const onSubmit = async (values: z.infer) => { 50 | const response = await fetch('/api/user', { 51 | method: "POST", 52 | headers: { 53 | 'Content-Type': 'application/json' 54 | }, 55 | body: JSON.stringify({ 56 | username: values.username, 57 | email: values.email, 58 | password: values.password 59 | }) 60 | }) 61 | 62 | if (response.ok) { 63 | router.push('/sign-in') 64 | } else { 65 | toast({ 66 | title: "Error", 67 | description: "Oops! something wents wrong!", 68 | variant:"destructive" 69 | }) 70 | } 71 | }; 72 | 73 | return ( 74 |
75 | 76 |
77 | ( 81 | 82 | Username 83 | 84 | 85 | 86 | 87 | 88 | )} 89 | /> 90 | ( 94 | 95 | Email 96 | 97 | 98 | 99 | 100 | 101 | )} 102 | /> 103 | ( 107 | 108 | Password 109 | 110 | 115 | 116 | 117 | 118 | )} 119 | /> 120 | ( 124 | 125 | Re-Enter your password 126 | 127 | 132 | 133 | 134 | 135 | )} 136 | /> 137 |
138 | 141 |
142 |
143 | or 144 |
145 | Sign up with Google 146 |

147 | If you don't have an account, please  148 | 149 | Sign in 150 | 151 |

152 | 153 | ); 154 | }; 155 | 156 | export default SignUpForm; 157 | -------------------------------------------------------------------------------- /src/components/hooks/use-toast.ts: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | // Inspired by react-hot-toast library 4 | import * as React from "react" 5 | 6 | import type { 7 | ToastActionElement, 8 | ToastProps, 9 | } from "@/components/ui/toast" 10 | 11 | const TOAST_LIMIT = 1 12 | const TOAST_REMOVE_DELAY = 1000000 13 | 14 | type ToasterToast = ToastProps & { 15 | id: string 16 | title?: React.ReactNode 17 | description?: React.ReactNode 18 | action?: ToastActionElement 19 | } 20 | 21 | const actionTypes = { 22 | ADD_TOAST: "ADD_TOAST", 23 | UPDATE_TOAST: "UPDATE_TOAST", 24 | DISMISS_TOAST: "DISMISS_TOAST", 25 | REMOVE_TOAST: "REMOVE_TOAST", 26 | } as const 27 | 28 | let count = 0 29 | 30 | function genId() { 31 | count = (count + 1) % Number.MAX_SAFE_INTEGER 32 | return count.toString() 33 | } 34 | 35 | type ActionType = typeof actionTypes 36 | 37 | type Action = 38 | | { 39 | type: ActionType["ADD_TOAST"] 40 | toast: ToasterToast 41 | } 42 | | { 43 | type: ActionType["UPDATE_TOAST"] 44 | toast: Partial 45 | } 46 | | { 47 | type: ActionType["DISMISS_TOAST"] 48 | toastId?: ToasterToast["id"] 49 | } 50 | | { 51 | type: ActionType["REMOVE_TOAST"] 52 | toastId?: ToasterToast["id"] 53 | } 54 | 55 | interface State { 56 | toasts: ToasterToast[] 57 | } 58 | 59 | const toastTimeouts = new Map>() 60 | 61 | const addToRemoveQueue = (toastId: string) => { 62 | if (toastTimeouts.has(toastId)) { 63 | return 64 | } 65 | 66 | const timeout = setTimeout(() => { 67 | toastTimeouts.delete(toastId) 68 | dispatch({ 69 | type: "REMOVE_TOAST", 70 | toastId: toastId, 71 | }) 72 | }, TOAST_REMOVE_DELAY) 73 | 74 | toastTimeouts.set(toastId, timeout) 75 | } 76 | 77 | export const reducer = (state: State, action: Action): State => { 78 | switch (action.type) { 79 | case "ADD_TOAST": 80 | return { 81 | ...state, 82 | toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT), 83 | } 84 | 85 | case "UPDATE_TOAST": 86 | return { 87 | ...state, 88 | toasts: state.toasts.map((t) => 89 | t.id === action.toast.id ? { ...t, ...action.toast } : t 90 | ), 91 | } 92 | 93 | case "DISMISS_TOAST": { 94 | const { toastId } = action 95 | 96 | // ! Side effects ! - This could be extracted into a dismissToast() action, 97 | // but I'll keep it here for simplicity 98 | if (toastId) { 99 | addToRemoveQueue(toastId) 100 | } else { 101 | state.toasts.forEach((toast) => { 102 | addToRemoveQueue(toast.id) 103 | }) 104 | } 105 | 106 | return { 107 | ...state, 108 | toasts: state.toasts.map((t) => 109 | t.id === toastId || toastId === undefined 110 | ? { 111 | ...t, 112 | open: false, 113 | } 114 | : t 115 | ), 116 | } 117 | } 118 | case "REMOVE_TOAST": 119 | if (action.toastId === undefined) { 120 | return { 121 | ...state, 122 | toasts: [], 123 | } 124 | } 125 | return { 126 | ...state, 127 | toasts: state.toasts.filter((t) => t.id !== action.toastId), 128 | } 129 | } 130 | } 131 | 132 | const listeners: Array<(state: State) => void> = [] 133 | 134 | let memoryState: State = { toasts: [] } 135 | 136 | function dispatch(action: Action) { 137 | memoryState = reducer(memoryState, action) 138 | listeners.forEach((listener) => { 139 | listener(memoryState) 140 | }) 141 | } 142 | 143 | type Toast = Omit 144 | 145 | function toast({ ...props }: Toast) { 146 | const id = genId() 147 | 148 | const update = (props: ToasterToast) => 149 | dispatch({ 150 | type: "UPDATE_TOAST", 151 | toast: { ...props, id }, 152 | }) 153 | const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id }) 154 | 155 | dispatch({ 156 | type: "ADD_TOAST", 157 | toast: { 158 | ...props, 159 | id, 160 | open: true, 161 | onOpenChange: (open) => { 162 | if (!open) dismiss() 163 | }, 164 | }, 165 | }) 166 | 167 | return { 168 | id: id, 169 | dismiss, 170 | update, 171 | } 172 | } 173 | 174 | function useToast() { 175 | const [state, setState] = React.useState(memoryState) 176 | 177 | React.useEffect(() => { 178 | listeners.push(setState) 179 | return () => { 180 | const index = listeners.indexOf(setState) 181 | if (index > -1) { 182 | listeners.splice(index, 1) 183 | } 184 | } 185 | }, [state]) 186 | 187 | return { 188 | ...state, 189 | toast, 190 | dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }), 191 | } 192 | } 193 | 194 | export { useToast, toast } 195 | -------------------------------------------------------------------------------- /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 | 5 | import { cn } from "@/lib/utils" 6 | 7 | const buttonVariants = cva( 8 | "inline-flex items-center justify-center 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 | -------------------------------------------------------------------------------- /src/components/ui/form.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import * as LabelPrimitive from '@radix-ui/react-label'; 3 | import { Slot } from '@radix-ui/react-slot'; 4 | import { 5 | Controller, 6 | ControllerProps, 7 | FieldPath, 8 | FieldValues, 9 | FormProvider, 10 | useFormContext, 11 | } from 'react-hook-form'; 12 | 13 | import { cn } from '@/lib/utils'; 14 | import { Label } from '@/components/ui/label'; 15 | 16 | const Form = FormProvider; 17 | 18 | type FormFieldContextValue< 19 | TFieldValues extends FieldValues = FieldValues, 20 | TName extends FieldPath = FieldPath 21 | > = { 22 | name: TName; 23 | }; 24 | 25 | const FormFieldContext = React.createContext( 26 | {} as FormFieldContextValue 27 | ); 28 | 29 | const FormField = < 30 | TFieldValues extends FieldValues = FieldValues, 31 | TName extends FieldPath = FieldPath 32 | >({ 33 | ...props 34 | }: ControllerProps) => { 35 | return ( 36 | 37 | 38 | 39 | ); 40 | }; 41 | 42 | const useFormField = () => { 43 | const fieldContext = React.useContext(FormFieldContext); 44 | const itemContext = React.useContext(FormItemContext); 45 | const { getFieldState, formState } = useFormContext(); 46 | 47 | const fieldState = getFieldState(fieldContext.name, formState); 48 | 49 | if (!fieldContext) { 50 | throw new Error('useFormField should be used within '); 51 | } 52 | 53 | const { id } = itemContext; 54 | 55 | return { 56 | id, 57 | name: fieldContext.name, 58 | formItemId: `${id}-form-item`, 59 | formDescriptionId: `${id}-form-item-description`, 60 | formMessageId: `${id}-form-item-message`, 61 | ...fieldState, 62 | }; 63 | }; 64 | 65 | type FormItemContextValue = { 66 | id: string; 67 | }; 68 | 69 | const FormItemContext = React.createContext( 70 | {} as FormItemContextValue 71 | ); 72 | 73 | const FormItem = React.forwardRef< 74 | HTMLDivElement, 75 | React.HTMLAttributes 76 | >(({ className, ...props }, ref) => { 77 | const id = React.useId(); 78 | 79 | return ( 80 | 81 |
82 | 83 | ); 84 | }); 85 | FormItem.displayName = 'FormItem'; 86 | 87 | const FormLabel = React.forwardRef< 88 | React.ElementRef, 89 | React.ComponentPropsWithoutRef 90 | >(({ className, ...props }, ref) => { 91 | const { error, formItemId } = useFormField(); 92 | 93 | return ( 94 |