├── .eslintrc.json ├── .gitignore ├── README.md ├── components.json ├── next.config.js ├── package-lock.json ├── package.json ├── postcss.config.js ├── public ├── next.svg └── vercel.svg ├── sample-env ├── src ├── app │ ├── api │ │ └── users │ │ │ ├── forgotpassword │ │ │ └── route.ts │ │ │ ├── login │ │ │ └── route.ts │ │ │ ├── logout │ │ │ └── route.js │ │ │ ├── me │ │ │ └── route.ts │ │ │ ├── passwordreset │ │ │ └── route.ts │ │ │ ├── signup │ │ │ └── route.ts │ │ │ └── verifyemail │ │ │ └── route.ts │ ├── favicon.ico │ ├── forgotpassword │ │ └── page.tsx │ ├── globals.css │ ├── layout.tsx │ ├── login │ │ └── page.tsx │ ├── page.tsx │ ├── passwordreset │ │ └── page.tsx │ ├── profile │ │ ├── [id] │ │ │ └── page.tsx │ │ └── page.tsx │ ├── signup │ │ └── page.tsx │ └── verifyemail │ │ └── page.tsx ├── components │ ├── theme-provider.tsx │ └── ui │ │ ├── button.tsx │ │ ├── card.tsx │ │ ├── hover-card.tsx │ │ ├── input.tsx │ │ └── label.tsx ├── dbConfig │ └── dbConfig.ts ├── helpers │ ├── getDataFromToken.ts │ └── mailer.ts ├── lib │ └── utils.ts ├── middleware.ts └── models │ └── userModel.js ├── tailwind.config.js └── tsconfig.json /.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 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # Secret file 28 | .env 29 | 30 | # vercel 31 | .vercel 32 | 33 | # typescript 34 | *.tsbuildinfo 35 | next-env.d.ts 36 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). 2 | 3 | ## Getting Started 4 | First, Replace the values from .env-example with your actual values 5 | 6 | Install the required packages 7 | 8 | run the development server: 9 | 10 | ```bash 11 | npm run dev 12 | # or 13 | yarn dev 14 | # or 15 | pnpm dev 16 | ``` 17 | 18 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 19 | 20 | You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. 21 | 22 | This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font. 23 | 24 | ## Learn More 25 | 26 | To learn more about Next.js, take a look at the following resources: 27 | 28 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 29 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 30 | 31 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! 32 | 33 | ## Deploy on Vercel 34 | 35 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. 36 | 37 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. 38 | -------------------------------------------------------------------------------- /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.js", 8 | "css": "src/app/globals.css", 9 | "baseColor": "slate", 10 | "cssVariables": true 11 | }, 12 | "aliases": { 13 | "components": "@/components", 14 | "utils": "@/lib/utils" 15 | } 16 | } -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = {} 3 | 4 | module.exports = nextConfig 5 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "authnextjs", 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 | "@radix-ui/react-hover-card": "^1.0.6", 13 | "@radix-ui/react-label": "^2.0.2", 14 | "@radix-ui/react-slot": "^1.0.2", 15 | "@types/node": "20.4.5", 16 | "@types/react": "18.2.16", 17 | "@types/react-dom": "18.2.7", 18 | "autoprefixer": "10.4.14", 19 | "axios": "^1.4.0", 20 | "bcryptjs": "^2.4.3", 21 | "class-variance-authority": "^0.7.0", 22 | "clsx": "^2.0.0", 23 | "eslint": "8.45.0", 24 | "eslint-config-next": "13.4.12", 25 | "jsonwebtoken": "^9.0.1", 26 | "lucide-react": "^0.265.0", 27 | "mongoose": "^7.4.1", 28 | "next": "13.4.12", 29 | "next-themes": "^0.2.1", 30 | "nodemailer": "^6.9.4", 31 | "postcss": "8.4.27", 32 | "react": "18.2.0", 33 | "react-dom": "18.2.0", 34 | "react-hot-toast": "^2.4.1", 35 | "tailwind-merge": "^1.14.0", 36 | "tailwindcss": "3.3.3", 37 | "tailwindcss-animate": "^1.0.6", 38 | "typescript": "5.1.6" 39 | }, 40 | "devDependencies": { 41 | "@types/bcryptjs": "^2.4.2", 42 | "@types/jsonwebtoken": "^9.0.2", 43 | "@types/nodemailer": "^6.4.9" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /sample-env: -------------------------------------------------------------------------------- 1 | MONGO_URI= "Your Mongo DB Connection String" 2 | TOKEN_SECRET= "Your Token Secret" 3 | DOMAIN= "Your Domain" 4 | MAILTRAP_USER=" Mailtrap user id " 5 | MAILTRAP_PASS=" Mailtrap password " -------------------------------------------------------------------------------- /src/app/api/users/forgotpassword/route.ts: -------------------------------------------------------------------------------- 1 | import {connect} from '@/dbConfig/dbConfig' 2 | import User from '@/models/userModel' 3 | import { NextRequest, NextResponse } from 'next/server' 4 | import { sendEmail } from '@/helpers/mailer' 5 | 6 | connect() 7 | 8 | export async function POST(request:NextRequest){ 9 | 10 | try { 11 | const reqBody= await request.json() 12 | const {email} = reqBody 13 | console.log(reqBody) 14 | 15 | const user = await User.findOne({email}) 16 | if(!user){ 17 | return NextResponse.json({error: "User not found"}, {status: 400}) 18 | } 19 | 20 | //send reset password email 21 | await sendEmail({email, emailType: "RESET", userId: user._id}) 22 | 23 | user.forgotPasswordToken = undefined; 24 | user.forgotPasswordTokenExpiry = undefined; 25 | await user.save() 26 | 27 | console.log(user) 28 | return NextResponse.json({ 29 | message: "User Found", 30 | success: true 31 | }) 32 | 33 | 34 | 35 | } catch (error: any) { 36 | return NextResponse.json({error: error.message}, {status: 500}) 37 | } 38 | } -------------------------------------------------------------------------------- /src/app/api/users/login/route.ts: -------------------------------------------------------------------------------- 1 | import { connect } from '@/dbConfig/dbConfig' 2 | import User from '@/models/userModel' 3 | import { NextRequest, NextResponse } from 'next/server' 4 | import bcryptjs from "bcryptjs" 5 | import jwt from "jsonwebtoken" 6 | 7 | connect() 8 | 9 | export async function POST(request:NextRequest){ 10 | try { 11 | const reqBody = await request.json() 12 | const {email, password} = reqBody 13 | console.log(reqBody) 14 | 15 | //Check if user is exists 16 | const user = await User.findOne({email}) 17 | if(!user){ 18 | return NextResponse.json({error: "User not found"}, {status: 400}) 19 | } 20 | 21 | //Check if password is correct 22 | const validPassword = await bcryptjs.compare(password, user.password) 23 | if(!validPassword){ 24 | return NextResponse.json({error: "Invalid Password"}, {status: 400}) 25 | } 26 | 27 | //Create token data 28 | const tokenData = { 29 | id: user._id, 30 | username: user.username, 31 | email: user.email, 32 | } 33 | 34 | //Create token 35 | const token = await jwt.sign(tokenData, process.env.TOKEN_SECRET!, {expiresIn: "1d"}) 36 | 37 | //Sent token to user cookies 38 | const response = NextResponse.json({ 39 | message: "Login successful", 40 | success: true, 41 | }) 42 | response.cookies.set("token", token, { 43 | httpOnly: true, 44 | }) 45 | return response 46 | 47 | } catch (error: any) { 48 | return NextResponse.json({error: error.message}, {status: 500}) 49 | } 50 | } -------------------------------------------------------------------------------- /src/app/api/users/logout/route.js: -------------------------------------------------------------------------------- 1 | import { NextResponse } from "next/server"; 2 | 3 | export async function GET() { 4 | try { 5 | const response = NextResponse.json({ 6 | message: "Logout Successfull", 7 | success: true, 8 | }) 9 | response.cookies.set("token", "", { httpOnly: true, expires: new Date(0)}) 10 | return response 11 | } catch (error) { 12 | return NextResponse.json({ error: error.message}, {status: 500}) 13 | } 14 | } -------------------------------------------------------------------------------- /src/app/api/users/me/route.ts: -------------------------------------------------------------------------------- 1 | import { getDataFromToken } from '@/helpers/getDataFromToken' 2 | import { NextRequest, NextResponse } from 'next/server' 3 | import User from "@/models/userModel" 4 | import {connect} from "@/dbConfig/dbConfig" 5 | 6 | connect() 7 | 8 | export async function GET(request:NextRequest) { 9 | try { 10 | const userId = await getDataFromToken(request) 11 | const user = await User.findOne({_id: userId}).select("-password") 12 | return NextResponse.json({ 13 | message: "User found", 14 | data: user, 15 | }) 16 | } catch (error:any) { 17 | return NextResponse.json({ error: error.message}, {status: 400}) 18 | 19 | } 20 | } -------------------------------------------------------------------------------- /src/app/api/users/passwordreset/route.ts: -------------------------------------------------------------------------------- 1 | import {connect} from '@/dbConfig/dbConfig' 2 | import { NextRequest, NextResponse } from 'next/server' 3 | import User from '@/models/userModel' 4 | import bcryptjs from 'bcryptjs' 5 | 6 | connect() 7 | 8 | export async function POST(request: NextRequest){ 9 | try { 10 | 11 | const reqBody = await request.json() 12 | const {token, password} = reqBody 13 | console.log(token, password) 14 | 15 | const user = await User.findOne({ forgotPasswordToken: token, forgotPasswordTokenExpiry: { $gt: Date.now() }}) 16 | 17 | if(!user){ 18 | return NextResponse.json({error: "Invalid Token"}, {status: 400}) 19 | } 20 | 21 | //Hash the password 22 | const salt = await bcryptjs.genSalt(10) 23 | const hashedPassword = await bcryptjs.hash(password, salt) 24 | 25 | user.password = hashedPassword; 26 | user.forgorPasswordToken = undefined 27 | user.forgotPasswordTokenExpiry = undefined 28 | await user.save() 29 | 30 | return NextResponse.json({ 31 | message: "Password changed successfully", 32 | success: true 33 | }) 34 | 35 | } catch (error:any) { 36 | return NextResponse.json({error: error.message} , {status: 500}) 37 | } 38 | } -------------------------------------------------------------------------------- /src/app/api/users/signup/route.ts: -------------------------------------------------------------------------------- 1 | import { connect } from '@/dbConfig/dbConfig' 2 | import User from '@/models/userModel' 3 | import { NextRequest, NextResponse } from 'next/server' 4 | import bcryptjs from "bcryptjs" 5 | import { sendEmail } from '@/helpers/mailer' 6 | 7 | connect() 8 | 9 | 10 | export async function POST(request: NextRequest){ 11 | try { 12 | const reqBody = await request.json() 13 | const {username, email, password} = reqBody 14 | console.log(reqBody) 15 | 16 | //check if user already exists 17 | const user = await User.findOne({email}) 18 | if(user){ 19 | return NextResponse.json({error :'User Already Exists'}, {status: 400}) 20 | } 21 | //Hash the password 22 | const salt = await bcryptjs.genSalt(10) 23 | const hashedPassword = await bcryptjs.hash(password, salt) 24 | //Create a new user 25 | const newUser = new User({ 26 | username, 27 | email, 28 | password: hashedPassword 29 | }) 30 | //Save the new user to the database 31 | const savedUser = await newUser.save() 32 | console.log(savedUser) 33 | 34 | //send verification email 35 | 36 | await sendEmail({email, emailType: "VERIFY", userId: savedUser._id}) 37 | 38 | //Returning the response 39 | return NextResponse.json({ 40 | message: "User created successfully", 41 | success: true, 42 | savedUser 43 | }) 44 | 45 | } catch(error: any) { 46 | return NextResponse.json({ error: error.message }, 47 | {status: 500}) 48 | } 49 | } -------------------------------------------------------------------------------- /src/app/api/users/verifyemail/route.ts: -------------------------------------------------------------------------------- 1 | import {connect} from '@/dbConfig/dbConfig' 2 | import { NextRequest, NextResponse } from 'next/server' 3 | import User from '@/models/userModel' 4 | 5 | 6 | 7 | connect() 8 | 9 | export async function POST(request: NextRequest){ 10 | try { 11 | const reqBody = await request.json() 12 | const {token} = reqBody 13 | 14 | const user = await User.findOne({verifyToken: token, verifyTokenExpiry: {$gt: Date.now()}}) 15 | 16 | if(!user){ 17 | return NextResponse.json({error: "Invalid Token"}, {status: 400}) 18 | } 19 | 20 | user.isVerified = true 21 | user.verifyToken = undefined 22 | user.verifyTokenExpiry = undefined 23 | await user.save() 24 | 25 | return NextResponse.json({ 26 | message: "Email Verified Successfully", 27 | success: true 28 | }) 29 | 30 | } catch (error:any) { 31 | return NextResponse.json({error: error.message} , {status: 500}) 32 | } 33 | } -------------------------------------------------------------------------------- /src/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samabid/authnextjs/d640987a912c818be103e4652823f1b6a8370857/src/app/favicon.ico -------------------------------------------------------------------------------- /src/app/forgotpassword/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { useState } from "react" 4 | import axios from "axios" 5 | import { toast } from "react-hot-toast" 6 | import { 7 | Card, 8 | CardContent, 9 | CardDescription, 10 | CardFooter, 11 | CardHeader, 12 | CardTitle, 13 | } from "@/components/ui/card" 14 | import { Button } from "@/components/ui/button" 15 | import { Input } from "@/components/ui/input" 16 | import { Label } from "@/components/ui/label" 17 | import { Loader2 } from "lucide-react" 18 | import { set } from "mongoose" 19 | 20 | export default function ForgotPasswordPage() { 21 | const [email, setEmail] = useState("") 22 | const [loading, setLoading] = useState(false) 23 | 24 | const verifyEmail = async () => { 25 | try { 26 | setLoading(true) 27 | await axios.post('/api/users/forgotpassword', {email}) 28 | toast.success("User Found!") 29 | toast("Please check your inbox and click on verification link.", {duration: 10000}) 30 | } catch (error: any) { 31 | toast.error("Something went wrong try again", error.message) 32 | } finally { 33 | setLoading(false) 34 | setEmail("") 35 | } 36 | } 37 | 38 | return ( 39 |
40 | 41 | 42 | Passwrod Reset 43 | 44 | Enter your email to reset the password 45 | 46 | 47 | 48 |
49 | 50 | setEmail(e.target.value)} 56 | /> 57 |
58 |
59 | 60 | 63 | 64 |
65 |
66 | ) 67 | } -------------------------------------------------------------------------------- /src/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 | 10 | --card: 0 0% 100%; 11 | --card-foreground: 222.2 84% 4.9%; 12 | 13 | --popover: 0 0% 100%; 14 | --popover-foreground: 222.2 84% 4.9%; 15 | 16 | --primary: 222.2 47.4% 11.2%; 17 | --primary-foreground: 210 40% 98%; 18 | 19 | --secondary: 210 40% 96.1%; 20 | --secondary-foreground: 222.2 47.4% 11.2%; 21 | 22 | --muted: 210 40% 96.1%; 23 | --muted-foreground: 215.4 16.3% 46.9%; 24 | 25 | --accent: 210 40% 96.1%; 26 | --accent-foreground: 222.2 47.4% 11.2%; 27 | 28 | --destructive: 0 84.2% 60.2%; 29 | --destructive-foreground: 210 40% 98%; 30 | 31 | --border: 214.3 31.8% 91.4%; 32 | --input: 214.3 31.8% 91.4%; 33 | --ring: 222.2 84% 4.9%; 34 | 35 | --radius: 0.5rem; 36 | } 37 | 38 | .dark { 39 | --background: 222.2 84% 4.9%; 40 | --foreground: 210 40% 98%; 41 | 42 | --card: 222.2 84% 4.9%; 43 | --card-foreground: 210 40% 98%; 44 | 45 | --popover: 222.2 84% 4.9%; 46 | --popover-foreground: 210 40% 98%; 47 | 48 | --primary: 210 40% 98%; 49 | --primary-foreground: 222.2 47.4% 11.2%; 50 | 51 | --secondary: 217.2 32.6% 17.5%; 52 | --secondary-foreground: 210 40% 98%; 53 | 54 | --muted: 217.2 32.6% 17.5%; 55 | --muted-foreground: 215 20.2% 65.1%; 56 | 57 | --accent: 217.2 32.6% 17.5%; 58 | --accent-foreground: 210 40% 98%; 59 | 60 | --destructive: 0 62.8% 30.6%; 61 | --destructive-foreground: 210 40% 98%; 62 | 63 | --border: 217.2 32.6% 17.5%; 64 | --input: 217.2 32.6% 17.5%; 65 | --ring: hsl(212.7,26.8%,83.9); 66 | } 67 | } 68 | 69 | @layer base { 70 | * { 71 | @apply border-border; 72 | } 73 | body { 74 | @apply bg-background text-foreground; 75 | } 76 | } -------------------------------------------------------------------------------- /src/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import './globals.css' 2 | import type { Metadata } from 'next' 3 | import { Inter } from 'next/font/google' 4 | import { Toaster } from 'react-hot-toast'; 5 | import { ThemeProvider } from '@/components/theme-provider' 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 | }: { 17 | children: React.ReactNode 18 | }) { 19 | return ( 20 | 21 | 22 | 23 | {children} 24 | 25 | 26 | 27 | 28 | ) 29 | } 30 | -------------------------------------------------------------------------------- /src/app/login/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | import Link from "next/link" 3 | import React, {useEffect, useState} from "react" 4 | import { useRouter } from "next/navigation" 5 | import axios from "axios" 6 | import toast from "react-hot-toast" 7 | import { 8 | Card, 9 | CardContent, 10 | CardDescription, 11 | CardFooter, 12 | CardHeader, 13 | CardTitle, 14 | } from "@/components/ui/card" 15 | import { Button } from "@/components/ui/button" 16 | import { Input } from "@/components/ui/input" 17 | import { Label } from "@/components/ui/label" 18 | import { Loader2 } from "lucide-react" 19 | 20 | 21 | export default function LoginPage() { 22 | const router = useRouter() 23 | const [user, setUser] = useState({ 24 | email: "", 25 | password: "", 26 | }) 27 | const [loading, setLoading] = useState(false) 28 | 29 | const onLogin = async () => { 30 | try { 31 | setLoading(true) 32 | await axios.post("/api/users/login", user) 33 | toast.success("Login Successfull") 34 | router.push("/profile") 35 | 36 | } catch (error: any) { 37 | toast.error("Email or Password is incorrect", error.message) 38 | }finally{ 39 | setLoading(false) 40 | } 41 | } 42 | 43 | 44 | return ( 45 |
46 | 47 | 48 | Login 49 | 50 | Enter your email and password to login 51 | 52 | 53 | 54 |
55 | 56 | setUser({...user, email:e.target.value})} 62 | /> 63 | Forgot Password 64 |
65 |
66 | 67 | setUser({...user, password:e.target.value})} 72 | /> 73 |
74 |
75 | 76 | 79 | 80 | 81 |
82 |
83 | 84 | ) 85 | } -------------------------------------------------------------------------------- /src/app/page.tsx: -------------------------------------------------------------------------------- 1 | import Image from 'next/image' 2 | 3 | export default function Home() { 4 | return ( 5 |
6 |
7 |

8 | Get started by editing  9 | src/app/page.tsx 10 |

11 |
12 | 18 | By{' '} 19 | Vercel Logo 27 | 28 |
29 |
30 | 31 |
32 | Next.js Logo 40 |
41 | 42 |
43 | 49 |

50 | Docs{' '} 51 | 52 | -> 53 | 54 |

55 |

56 | Find in-depth information about Next.js features and API. 57 |

58 |
59 | 60 | 66 |

67 | Learn{' '} 68 | 69 | -> 70 | 71 |

72 |

73 | Learn about Next.js in an interactive course with quizzes! 74 |

75 |
76 | 77 | 83 |

84 | Templates{' '} 85 | 86 | -> 87 | 88 |

89 |

90 | Explore the Next.js 13 playground. 91 |

92 |
93 | 94 | 100 |

101 | Deploy{' '} 102 | 103 | -> 104 | 105 |

106 |

107 | Instantly deploy your Next.js site to a shareable URL with Vercel. 108 |

109 |
110 |
111 |
112 | ) 113 | } 114 | -------------------------------------------------------------------------------- /src/app/passwordreset/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | import { useState, useEffect } from "react" 3 | import axios from "axios" 4 | import { toast } from "react-hot-toast" 5 | import { useRouter } from "next/navigation" 6 | import { Button } from "@/components/ui/button" 7 | import { Input } from "@/components/ui/input" 8 | import { 9 | Card, 10 | CardContent, 11 | CardDescription, 12 | CardHeader, 13 | CardTitle, 14 | } from "@/components/ui/card" 15 | import { 16 | HoverCard, 17 | HoverCardContent, 18 | HoverCardTrigger, 19 | } from "@/components/ui/hover-card" 20 | import { Loader2 } from "lucide-react" 21 | 22 | export default function PasswordReset() { 23 | const router = useRouter() 24 | const [token, setToken] = useState("") 25 | const [password, setPassword] = useState("") 26 | const [loading, setLoading] = useState(false) 27 | 28 | const changePassword = async () => { 29 | try { 30 | setLoading(true) 31 | const response = await axios.post('/api/users/passwordreset/', {token, password}) 32 | toast.success("Password Changed Successfully", response.data.message) 33 | setPassword("") 34 | router.push("/login") 35 | } catch (error:any) { 36 | toast.error(error.message) 37 | }finally{ 38 | setLoading(false) 39 | } 40 | } 41 | 42 | useEffect(() => { 43 | const urlToken = window.location.search.split("=")[1] 44 | setToken(urlToken || "") 45 | }, []) 46 | 47 | return ( 48 |
49 | 50 | 51 | 52 | 53 | 54 | {token ? `${token}` : "no token"} 55 | 56 | 57 | 58 | 59 | Password Reset 60 | 61 | Please Enter Your New Password 62 | 63 | 64 | 65 |
66 | setPassword(e.target.value)} /> 67 | 70 |
71 |
72 |
73 |
74 | ) 75 | } -------------------------------------------------------------------------------- /src/app/profile/[id]/page.tsx: -------------------------------------------------------------------------------- 1 | export default function UserProfile({params}:any) { 2 | return ( 3 |
4 |

Profile

5 |

{params.id}

6 |
7 | ) 8 | } -------------------------------------------------------------------------------- /src/app/profile/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | import { useState } from "react" 3 | import axios from "axios" 4 | import Link from "next/link" 5 | import { useRouter } from "next/navigation" 6 | import toast from "react-hot-toast" 7 | import { 8 | Card, 9 | CardContent, 10 | CardDescription, 11 | CardFooter, 12 | CardHeader, 13 | CardTitle, 14 | } from "@/components/ui/card" 15 | import { Button } from "@/components/ui/button" 16 | 17 | 18 | export default function ProfilePage() { 19 | 20 | const router = useRouter() 21 | const [data, setData] = useState("nothing") 22 | 23 | const logout = async () => { 24 | try { 25 | await axios.get('/api/users/logout') 26 | toast.success('Logout Successfully') 27 | router.push('/login') 28 | } catch (error: any) { 29 | toast.error(error.message) 30 | } 31 | } 32 | 33 | const getUserDetails = async () => { 34 | const res = await axios.get('/api/users/me') 35 | setData(res.data.data._id) 36 | } 37 | 38 | return ( 39 |
40 | 41 | 42 | User Details 43 | 44 | {data === "nothing" ? "Nothing" : {data}} 45 | 46 | 47 | 48 |
49 | 50 | 51 |
52 |
53 |
54 |
55 | 56 | ) 57 | } -------------------------------------------------------------------------------- /src/app/signup/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | import Link from "next/link" 3 | import React, {useEffect, useState} from "react" 4 | import { useRouter } from "next/navigation" 5 | import axios from "axios" 6 | import toast from "react-hot-toast" 7 | import { 8 | Card, 9 | CardContent, 10 | CardDescription, 11 | CardFooter, 12 | CardHeader, 13 | CardTitle, 14 | } from "@/components/ui/card" 15 | import { Button } from "@/components/ui/button" 16 | import { Input } from "@/components/ui/input" 17 | import { Label } from "@/components/ui/label" 18 | import { Loader2 } from "lucide-react" 19 | 20 | 21 | export default function SignupPage() { 22 | const router = useRouter() 23 | const [user, setUser] = useState({ 24 | email: "", 25 | password: "", 26 | username:"", 27 | }) 28 | 29 | const [loading, setLoading] = useState(false) 30 | 31 | const onSignup = async () => { 32 | try { 33 | setLoading(true) 34 | await axios.post("/api/users/signup", user) 35 | toast.success("Signup successfull") 36 | toast("Please check your inbox and click on verification link.", {duration: 10000}) 37 | router.push("/login") 38 | } catch (error: any) { 39 | toast.error(error.message) 40 | }finally{ 41 | setLoading(false) 42 | } 43 | } 44 | 45 | 46 | return ( 47 |
48 | 49 | 50 | Signup 51 | 52 | Enter your username, email and password to Signup 53 | 54 | 55 | 56 |
57 | 58 | setUser({...user, username:e.target.value})} 64 | /> 65 |
66 |
67 | 68 | setUser({...user, email:e.target.value})} 74 | /> 75 | 76 |
77 |
78 | 79 | setUser({...user, password:e.target.value})} 84 | /> 85 |
86 |
87 | 88 | 91 | 92 | 93 |
94 |
95 | ) 96 | } -------------------------------------------------------------------------------- /src/app/verifyemail/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | import axios from "axios" 3 | import Link from "next/link" 4 | import { useState, useEffect } from "react" 5 | import { toast } from "react-hot-toast" 6 | import {Button} from '@/components/ui/button' 7 | import { useRouter } from "next/navigation" 8 | 9 | 10 | export default function VerifyEmailPage() { 11 | 12 | const router = useRouter() 13 | 14 | const [token, setToken] = useState("") 15 | const [verified, setVerified] = useState(false) 16 | const [error, setError] = useState(false) 17 | 18 | const verifyUserEmail = async () => { 19 | try { 20 | await axios.post('/api/users/verifyemail', {token}) 21 | setVerified(true) 22 | toast.success("Your account has been verified") 23 | } catch (error: any) { 24 | setError(true) 25 | toast.error(error.message) 26 | } 27 | } 28 | 29 | useEffect(() => { 30 | const urlToken = window.location.search.split("=")[1] 31 | setToken(urlToken || "") 32 | }, []) 33 | 34 | useEffect(() => { 35 | if(token.length > 0){ 36 | verifyUserEmail() 37 | } 38 | }, [token]) 39 | 40 | useEffect(() => { 41 | const timer = setTimeout(() => { 42 | router.push("/login") 43 | }, 5000); 44 | return () => clearTimeout(timer); 45 | }, []); 46 | 47 | return ( 48 |
49 |

Email Verification

50 |

{token ? `${token}` : "no token"}

51 | {verified && ( 52 |
53 | 54 |
55 | )} 56 | {error && ( 57 |
58 |

Error

59 |
60 | )} 61 |
62 | ) 63 | } -------------------------------------------------------------------------------- /src/components/theme-provider.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import { ThemeProvider as NextThemesProvider } from "next-themes" 5 | import { type ThemeProviderProps } from "next-themes/dist/types" 6 | 7 | export function ThemeProvider({ children, ...props }: ThemeProviderProps) { 8 | return {children} 9 | } 10 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /src/components/ui/hover-card.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as HoverCardPrimitive from "@radix-ui/react-hover-card" 5 | 6 | import { cn } from "@/lib/utils" 7 | 8 | const HoverCard = HoverCardPrimitive.Root 9 | 10 | const HoverCardTrigger = HoverCardPrimitive.Trigger 11 | 12 | const HoverCardContent = React.forwardRef< 13 | React.ElementRef, 14 | React.ComponentPropsWithoutRef 15 | >(({ className, align = "center", sideOffset = 4, ...props }, ref) => ( 16 | 26 | )) 27 | HoverCardContent.displayName = HoverCardPrimitive.Content.displayName 28 | 29 | export { HoverCard, HoverCardTrigger, HoverCardContent } 30 | -------------------------------------------------------------------------------- /src/components/ui/input.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@/lib/utils" 4 | 5 | export interface InputProps 6 | extends React.InputHTMLAttributes {} 7 | 8 | const Input = React.forwardRef( 9 | ({ className, type, ...props }, ref) => { 10 | return ( 11 | 20 | ) 21 | } 22 | ) 23 | Input.displayName = "Input" 24 | 25 | export { Input } 26 | -------------------------------------------------------------------------------- /src/components/ui/label.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as LabelPrimitive from "@radix-ui/react-label" 5 | import { cva, type VariantProps } from "class-variance-authority" 6 | 7 | import { cn } from "@/lib/utils" 8 | 9 | const labelVariants = cva( 10 | "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70" 11 | ) 12 | 13 | const Label = React.forwardRef< 14 | React.ElementRef, 15 | React.ComponentPropsWithoutRef & 16 | VariantProps 17 | >(({ className, ...props }, ref) => ( 18 | 23 | )) 24 | Label.displayName = LabelPrimitive.Root.displayName 25 | 26 | export { Label } 27 | -------------------------------------------------------------------------------- /src/dbConfig/dbConfig.ts: -------------------------------------------------------------------------------- 1 | import mongoose from "mongoose"; 2 | 3 | export async function connect() { 4 | try{ 5 | mongoose.connect(process.env.MONGO_URI!) 6 | const connection = mongoose.connection 7 | 8 | connection.on('connected', () =>{ 9 | console.log('Connected Successfully') 10 | }) 11 | 12 | connection.on('error', (err) =>{ 13 | console.log('MongoDB Connection Error, Please try again. ' + err) 14 | process.exit() 15 | }) 16 | } catch(err){ 17 | console.log("Something went wrong") 18 | console.log(err) 19 | } 20 | } -------------------------------------------------------------------------------- /src/helpers/getDataFromToken.ts: -------------------------------------------------------------------------------- 1 | import { NextRequest } from "next/server"; 2 | import jwt from "jsonwebtoken" 3 | 4 | export const getDataFromToken = (request: NextRequest) => { 5 | try { 6 | const token = request.cookies.get("token")?.value || '' 7 | const decodedToken: any = jwt.verify(token, process.env.TOKEN_SECRET!) 8 | return decodedToken.id 9 | } catch (error: any) { 10 | throw new Error(error.message) 11 | 12 | } 13 | } -------------------------------------------------------------------------------- /src/helpers/mailer.ts: -------------------------------------------------------------------------------- 1 | import nodemailer from 'nodemailer' 2 | import User from '@/models/userModel' 3 | import bcryptjs from 'bcryptjs' 4 | 5 | 6 | export const sendEmail = async ({email, emailType, userId} :any) => { 7 | try { 8 | //Create Hashed Token 9 | const hashedToken = await bcryptjs.hash(userId.toString(), 10) 10 | 11 | if(emailType === "VERIFY"){ 12 | await User.findByIdAndUpdate(userId, { verifyToken: hashedToken, verifyTokenExpiry: Date.now() + 3600000}) 13 | }else if(emailType === "RESET") { 14 | await User.findByIdAndUpdate(userId, { forgotPasswordToken: hashedToken, forgotPasswordTokenExpiry: Date.now() + 3600000}) 15 | } 16 | 17 | //Create Transporter 18 | var transport = nodemailer.createTransport({ 19 | host: "sandbox.smtp.mailtrap.io", 20 | port: 2525, 21 | auth: { 22 | user: process.env.MAILTRAP_USER, 23 | pass: process.env.MAILTRAP_PASS 24 | } 25 | }) 26 | 27 | //Create Mail Options 28 | const mailOptions = { 29 | from: 'usamaabid.wp@gmail.com', 30 | to: email, 31 | subject: emailType === "VERIFY" ? "Verify Your Email" : "Reset Your Password", 32 | html: `

Click here to ${emailType === "VERIFY" ? "verify your email" : "reset your password"} 33 | or copy and paste the link below in your browser 34 |
35 | ${emailType === "VERIFY" ? `${process.env.DOMAIN}/verifyemail?token=${hashedToken}` : `${process.env.DOMAIN}/passwordreset?token=${hashedToken}`} 36 |

` 37 | } 38 | 39 | const mailResponse = await transport.sendMail(mailOptions) 40 | return mailResponse 41 | 42 | } catch (error: any) { 43 | throw new Error(error.message) 44 | } 45 | } -------------------------------------------------------------------------------- /src/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { type ClassValue, clsx } from "clsx" 2 | import { twMerge } from "tailwind-merge" 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)) 6 | } 7 | -------------------------------------------------------------------------------- /src/middleware.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from 'next/server' 2 | import { NextRequest } from 'next/server' 3 | 4 | // This function can be marked `async` if using `await` inside 5 | export function middleware(request: NextRequest) { 6 | const path = request.nextUrl.pathname 7 | const isPublicPath = path === '/login' || path === '/signup' || path === '/verifyemail' || path === '/forgotpassword' 8 | const token = request.cookies.get('token')?.value || '' 9 | 10 | if(isPublicPath && token) { 11 | return NextResponse.redirect( new URL('/', request.nextUrl)) 12 | } 13 | if(!isPublicPath && !token) { 14 | return NextResponse.redirect( new URL('/login', request.nextUrl)) 15 | } 16 | } 17 | 18 | // See "Matching Paths" below to learn more 19 | export const config = { 20 | matcher: [ 21 | '/', 22 | '/profile/:path*', 23 | '/login', 24 | '/signup', 25 | '/verifyemail', 26 | '/forgotpassword', 27 | '/resetpassword' 28 | ] 29 | } -------------------------------------------------------------------------------- /src/models/userModel.js: -------------------------------------------------------------------------------- 1 | import mongoose from 'mongoose' 2 | 3 | const userSchema = new mongoose.Schema({ 4 | username: { 5 | type: String, 6 | required: [true, "Please Provide a username"], 7 | unique: true, 8 | }, 9 | email: { 10 | type: String, 11 | required: [true, "Please Provide a email"], 12 | unique: true, 13 | }, 14 | password: { 15 | type: String, 16 | required: [true, "Please Provide a password"], 17 | }, 18 | isVerified: { 19 | type: Boolean, 20 | default: false, 21 | }, 22 | isAdmin: { 23 | type: Boolean, 24 | default: false, 25 | }, 26 | forgotPasswordToken: String, 27 | forgotPasswordTokenExpiry: Date, 28 | verifyToken: String, 29 | verifyTokenExpiry: Date, 30 | }) 31 | 32 | const User = mongoose.models.users || mongoose.model("users", userSchema) 33 | 34 | export default User -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | darkMode: ["class"], 4 | content: [ 5 | './pages/**/*.{ts,tsx}', 6 | './components/**/*.{ts,tsx}', 7 | './app/**/*.{ts,tsx}', 8 | './src/**/*.{ts,tsx}', 9 | ], 10 | theme: { 11 | container: { 12 | center: true, 13 | padding: "2rem", 14 | screens: { 15 | "2xl": "1400px", 16 | }, 17 | }, 18 | extend: { 19 | colors: { 20 | border: "hsl(var(--border))", 21 | input: "hsl(var(--input))", 22 | ring: "hsl(var(--ring))", 23 | background: "hsl(var(--background))", 24 | foreground: "hsl(var(--foreground))", 25 | primary: { 26 | DEFAULT: "hsl(var(--primary))", 27 | foreground: "hsl(var(--primary-foreground))", 28 | }, 29 | secondary: { 30 | DEFAULT: "hsl(var(--secondary))", 31 | foreground: "hsl(var(--secondary-foreground))", 32 | }, 33 | destructive: { 34 | DEFAULT: "hsl(var(--destructive))", 35 | foreground: "hsl(var(--destructive-foreground))", 36 | }, 37 | muted: { 38 | DEFAULT: "hsl(var(--muted))", 39 | foreground: "hsl(var(--muted-foreground))", 40 | }, 41 | accent: { 42 | DEFAULT: "hsl(var(--accent))", 43 | foreground: "hsl(var(--accent-foreground))", 44 | }, 45 | popover: { 46 | DEFAULT: "hsl(var(--popover))", 47 | foreground: "hsl(var(--popover-foreground))", 48 | }, 49 | card: { 50 | DEFAULT: "hsl(var(--card))", 51 | foreground: "hsl(var(--card-foreground))", 52 | }, 53 | }, 54 | borderRadius: { 55 | lg: "var(--radius)", 56 | md: "calc(var(--radius) - 2px)", 57 | sm: "calc(var(--radius) - 4px)", 58 | }, 59 | keyframes: { 60 | "accordion-down": { 61 | from: { height: 0 }, 62 | to: { height: "var(--radix-accordion-content-height)" }, 63 | }, 64 | "accordion-up": { 65 | from: { height: "var(--radix-accordion-content-height)" }, 66 | to: { height: 0 }, 67 | }, 68 | }, 69 | animation: { 70 | "accordion-down": "accordion-down 0.2s ease-out", 71 | "accordion-up": "accordion-up 0.2s ease-out", 72 | }, 73 | }, 74 | }, 75 | plugins: [require("tailwindcss-animate")], 76 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "noEmit": true, 10 | "esModuleInterop": true, 11 | "module": "esnext", 12 | "moduleResolution": "bundler", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "jsx": "preserve", 16 | "incremental": true, 17 | "plugins": [ 18 | { 19 | "name": "next" 20 | } 21 | ], 22 | "paths": { 23 | "@/*": ["./src/*"] 24 | } 25 | }, 26 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 27 | "exclude": ["node_modules"] 28 | } 29 | --------------------------------------------------------------------------------