├── .env
├── .eslintrc.json
├── .gitignore
├── README.md
├── bun.lockb
├── next.config.js
├── nextjs14-full-authentication.code-workspace
├── package-lock.json
├── package.json
├── postcss.config.js
├── prisma
├── dev.db
├── migrations
│ ├── 20231116113603_first
│ │ └── migration.sql
│ └── migration_lock.toml
└── schema.prisma
├── public
├── forgotPass.png
├── login.png
├── next.svg
└── vercel.svg
├── src
├── app
│ ├── api
│ │ └── auth
│ │ │ └── [...nextauth]
│ │ │ └── route.ts
│ ├── auth
│ │ ├── activation
│ │ │ └── [jwt]
│ │ │ │ └── page.tsx
│ │ ├── forgotPassword
│ │ │ └── page.tsx
│ │ ├── resetPass
│ │ │ └── [jwt]
│ │ │ │ └── page.tsx
│ │ ├── signin
│ │ │ └── page.tsx
│ │ └── signup
│ │ │ └── page.tsx
│ ├── components
│ │ ├── Appbar.tsx
│ │ ├── NextAuthProviders.tsx
│ │ ├── PasswordStrength.tsx
│ │ ├── ResetPasswordForm.tsx
│ │ ├── SignInForm.tsx
│ │ ├── SignUpForm.tsx
│ │ └── SigninButton.tsx
│ ├── favicon.ico
│ ├── globals.css
│ ├── layout.tsx
│ ├── page.tsx
│ ├── profile
│ │ └── page.tsx
│ └── providers.tsx
├── lib
│ ├── actions
│ │ └── authActions.ts
│ ├── emailTemplates
│ │ ├── activation.ts
│ │ └── resetPass.ts
│ ├── jwt.ts
│ ├── mail.ts
│ ├── prisma.ts
│ └── types.d.ts
└── middleware.ts
├── tailwind.config.ts
└── tsconfig.json
/.env:
--------------------------------------------------------------------------------
1 | # Environment variables declared in this file are automatically made available to Prisma.
2 | # See the documentation for more detail: https://pris.ly/d/prisma-schema#accessing-environment-variables-from-the-schema
3 |
4 | # Prisma supports the native connection string format for PostgreSQL, MySQL, SQLite, SQL Server, MongoDB and CockroachDB.
5 | # See the documentation for all the connection string options: https://pris.ly/d/connection-strings
6 |
7 | DATABASE_URL="file:./dev.db"
8 |
9 | NEXTAUTH_URL=http://localhost:3000
10 | NEXTAUTH_SECRET="dssdfbefabnjdvjslvnb"
11 | # ======================Email Setting==================
12 | SMPT_EMAIL = sakuradev23@gmail.com
13 | SMTP_GMAIL_PASS ="lfqt tusu pseu phmd"
14 | SMTP_USER = "20ba5cc654d264"
15 | SMTP_PASS ="cd969bfb0d238a"
16 |
17 |
18 | JWT_USER_ID_SECRET="jkOLuHyY9DpVs8Alxn41PuiDm3LAfxDIu3DvgrMIpBE="
19 |
20 |
21 | # Google Provider=====================
22 |
23 | GOOGLE_CLIENT_ID=761310940373-bpmcurctp0hejupb1v44gkqr8t84agbd.apps.googleusercontent.com
24 | GOOGLE_CLIENT_SECRET=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
--------------------------------------------------------------------------------
/.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 | 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 |
5 | First, run the development server:
6 |
7 | ```bash
8 | npm run dev
9 | # or
10 | yarn dev
11 | # or
12 | pnpm dev
13 | # or
14 | bun dev
15 | ```
16 |
17 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
18 |
19 | You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
20 |
21 | This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font.
22 |
23 | ## Learn More
24 |
25 | To learn more about Next.js, take a look at the following resources:
26 |
27 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
28 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
29 |
30 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
31 |
32 | ## Deploy on Vercel
33 |
34 | 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.
35 |
36 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.
37 |
--------------------------------------------------------------------------------
/bun.lockb:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vahid-nejad/Nextjs14-Comprehensive-authentication-Course/07f056c61a50299e85b5bf914b91df2c31eba6cd/bun.lockb
--------------------------------------------------------------------------------
/next.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('next').NextConfig} */
2 | const nextConfig = {}
3 |
4 | module.exports = nextConfig
5 |
--------------------------------------------------------------------------------
/nextjs14-full-authentication.code-workspace:
--------------------------------------------------------------------------------
1 | {
2 | "folders": [
3 | {
4 | "path": "."
5 | }
6 | ],
7 | "settings": {
8 | "window.zoomLevel": 3,
9 | "editor.fontSize": 18,
10 | "prettier.printWidth": 80,
11 | "editor.wordWrap": "on",
12 | "editor.hover.enabled": false,
13 | "terminal.integrated.fontSize": 18
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "nextjs14-full-authentication",
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 | "@heroicons/react": "^2.0.18",
13 | "@hookform/resolvers": "^3.3.2",
14 | "@nextui-org/react": "^2.2.9",
15 | "@prisma/client": "^5.6.0",
16 | "bcrypt": "^5.1.1",
17 | "check-password-strength": "^2.0.7",
18 | "clsx-tailwind-merge": "^1.0.4",
19 | "cn": "^0.1.1",
20 | "framer-motion": "^10.16.5",
21 | "handlebars": "^4.7.8",
22 | "jsonwebtoken": "^9.0.2",
23 | "next": "latest",
24 | "next-auth": "^4.24.5",
25 | "nodemailer": "^6.9.7",
26 | "prisma": "^5.6.0",
27 | "react": "^18",
28 | "react-dom": "^18",
29 | "react-hook-form": "^7.48.2",
30 | "react-toastify": "^9.1.3",
31 | "validator": "^13.11.0",
32 | "z": "^1.0.9",
33 | "zod": "^3.22.4"
34 | },
35 | "devDependencies": {
36 | "@types/bcrypt": "^5.0.2",
37 | "@types/jsonwebtoken": "^9.0.5",
38 | "@types/node": "^20",
39 | "@types/nodemailer": "^6.4.14",
40 | "@types/react": "^18",
41 | "@types/react-dom": "^18",
42 | "@types/validator": "^13.11.6",
43 | "autoprefixer": "^10.0.1",
44 | "eslint": "^8",
45 | "eslint-config-next": "14.0.2",
46 | "postcss": "^8",
47 | "tailwindcss": "^3.3.0",
48 | "typescript": "^5"
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | }
7 |
--------------------------------------------------------------------------------
/prisma/dev.db:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vahid-nejad/Nextjs14-Comprehensive-authentication-Course/07f056c61a50299e85b5bf914b91df2c31eba6cd/prisma/dev.db
--------------------------------------------------------------------------------
/prisma/migrations/20231116113603_first/migration.sql:
--------------------------------------------------------------------------------
1 | -- CreateTable
2 | CREATE TABLE "Account" (
3 | "id" TEXT NOT NULL PRIMARY KEY,
4 | "userId" TEXT NOT NULL,
5 | "type" TEXT NOT NULL,
6 | "provider" TEXT NOT NULL,
7 | "providerAccountId" TEXT NOT NULL,
8 | "refresh_token" TEXT,
9 | "access_token" TEXT,
10 | "expires_at" INTEGER,
11 | "token_type" TEXT,
12 | "scope" TEXT,
13 | "id_token" TEXT,
14 | "session_state" TEXT,
15 | CONSTRAINT "Account_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User" ("id") ON DELETE CASCADE ON UPDATE CASCADE
16 | );
17 |
18 | -- CreateTable
19 | CREATE TABLE "Session" (
20 | "id" TEXT NOT NULL PRIMARY KEY,
21 | "sessionToken" TEXT NOT NULL,
22 | "userId" TEXT NOT NULL,
23 | "expires" DATETIME NOT NULL,
24 | CONSTRAINT "Session_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User" ("id") ON DELETE CASCADE ON UPDATE CASCADE
25 | );
26 |
27 | -- CreateTable
28 | CREATE TABLE "User" (
29 | "id" TEXT NOT NULL PRIMARY KEY,
30 | "firstName" TEXT NOT NULL,
31 | "lastName" TEXT NOT NULL,
32 | "password" TEXT NOT NULL,
33 | "email" TEXT NOT NULL,
34 | "emailVerified" DATETIME,
35 | "phone" TEXT NOT NULL,
36 | "image" TEXT
37 | );
38 |
39 | -- CreateTable
40 | CREATE TABLE "VerificationToken" (
41 | "identifier" TEXT NOT NULL,
42 | "token" TEXT NOT NULL,
43 | "expires" DATETIME NOT NULL
44 | );
45 |
46 | -- CreateIndex
47 | CREATE UNIQUE INDEX "Account_provider_providerAccountId_key" ON "Account"("provider", "providerAccountId");
48 |
49 | -- CreateIndex
50 | CREATE UNIQUE INDEX "Session_sessionToken_key" ON "Session"("sessionToken");
51 |
52 | -- CreateIndex
53 | CREATE UNIQUE INDEX "User_email_key" ON "User"("email");
54 |
55 | -- CreateIndex
56 | CREATE UNIQUE INDEX "VerificationToken_token_key" ON "VerificationToken"("token");
57 |
58 | -- CreateIndex
59 | CREATE UNIQUE INDEX "VerificationToken_identifier_token_key" ON "VerificationToken"("identifier", "token");
60 |
--------------------------------------------------------------------------------
/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 = "sqlite"
--------------------------------------------------------------------------------
/prisma/schema.prisma:
--------------------------------------------------------------------------------
1 | // This is your Prisma schema file,
2 | // learn more about it in the docs: https://pris.ly/d/prisma-schema
3 |
4 | generator client {
5 | provider = "prisma-client-js"
6 | }
7 |
8 | datasource db {
9 | provider = "sqlite"
10 | url = env("DATABASE_URL")
11 | }
12 |
13 | model Account {
14 | id String @id @default(cuid())
15 | userId String
16 | type String
17 | provider String
18 | providerAccountId String
19 | refresh_token String?
20 | access_token String?
21 | expires_at Int?
22 | token_type String?
23 | scope String?
24 | id_token String?
25 | session_state String?
26 |
27 | user User @relation(fields: [userId], references: [id], onDelete: Cascade)
28 |
29 | @@unique([provider, providerAccountId])
30 | }
31 |
32 | model Session {
33 | id String @id @default(cuid())
34 | sessionToken String @unique
35 | userId String
36 | expires DateTime
37 | user User @relation(fields: [userId], references: [id], onDelete: Cascade)
38 | }
39 |
40 | model User {
41 | id String @id @default(cuid())
42 | firstName String
43 | lastName String
44 | password String
45 | email String @unique
46 | emailVerified DateTime?
47 | phone String
48 | image String?
49 | accounts Account[]
50 | sessions Session[]
51 | }
52 |
53 | model VerificationToken {
54 | identifier String
55 | token String @unique
56 | expires DateTime
57 |
58 | @@unique([identifier, token])
59 | }
60 |
--------------------------------------------------------------------------------
/public/forgotPass.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vahid-nejad/Nextjs14-Comprehensive-authentication-Course/07f056c61a50299e85b5bf914b91df2c31eba6cd/public/forgotPass.png
--------------------------------------------------------------------------------
/public/login.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vahid-nejad/Nextjs14-Comprehensive-authentication-Course/07f056c61a50299e85b5bf914b91df2c31eba6cd/public/login.png
--------------------------------------------------------------------------------
/public/next.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/vercel.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/app/api/auth/[...nextauth]/route.ts:
--------------------------------------------------------------------------------
1 | import prisma from "@/lib/prisma";
2 | import { AuthOptions } from "next-auth";
3 | import CredentialsProvider from "next-auth/providers/credentials";
4 | import GoogleProvider from "next-auth/providers/google";
5 | import * as bcrypt from "bcrypt";
6 | import NextAuth from "next-auth/next";
7 |
8 | import { use } from "react";
9 | import { User } from "@prisma/client";
10 |
11 | export const authOptions: AuthOptions = {
12 | pages: {
13 | signIn: "/auth/signin",
14 | },
15 | session: {
16 | strategy: "jwt",
17 | },
18 | jwt: {
19 | secret: process.env.NEXTAUTH_SECRET,
20 | },
21 | providers: [
22 | GoogleProvider({
23 | clientId: process.env.GOOGLE_CLIENT_ID ?? "",
24 | clientSecret: process.env.GOOGLE_CLIENT_SECRET ?? "",
25 | idToken: true,
26 |
27 | authorization: {
28 | params: {
29 | scope: "openid profile email",
30 | },
31 | },
32 | }),
33 | CredentialsProvider({
34 | name: "Credentials",
35 |
36 | credentials: {
37 | username: {
38 | label: "User Name",
39 | type: "text",
40 | placeholder: "Your User Name",
41 | },
42 | password: {
43 | label: "Password",
44 | type: "password",
45 | },
46 | },
47 | async authorize(credentials) {
48 | const user = await prisma.user.findUnique({
49 | where: {
50 | email: credentials?.username,
51 | },
52 | });
53 |
54 | if (!user) throw new Error("User name or password is not correct");
55 |
56 | // This is Naive Way of Comparing The Passwords
57 | // const isPassowrdCorrect = credentials?.password === user.password;
58 | if (!credentials?.password) throw new Error("Please Provide Your Password");
59 | const isPassowrdCorrect = await bcrypt.compare(credentials.password, user.password);
60 |
61 | if (!isPassowrdCorrect) throw new Error("User name or password is not correct");
62 |
63 | if (!user.emailVerified) throw new Error("Please verify your email first!");
64 |
65 | const { password, ...userWithoutPass } = user;
66 | return userWithoutPass;
67 | },
68 | }),
69 | ],
70 |
71 | callbacks: {
72 | async jwt({ token, user }) {
73 | if (user) token.user = user as User;
74 | return token;
75 | },
76 |
77 | async session({ token, session }) {
78 | session.user = token.user;
79 | return session;
80 | },
81 | },
82 | };
83 |
84 | const handler = NextAuth(authOptions);
85 |
86 | export { handler as GET, handler as POST };
87 |
--------------------------------------------------------------------------------
/src/app/auth/activation/[jwt]/page.tsx:
--------------------------------------------------------------------------------
1 | import { activateUser } from "@/lib/actions/authActions";
2 | import { verifyJwt } from "@/lib/jwt";
3 |
4 | interface Props {
5 | params: {
6 | jwt: string;
7 | };
8 | }
9 |
10 | const ActivationPage = async ({ params }: Props) => {
11 | const result = await activateUser(params.jwt);
12 | return (
13 |
14 | {result === "userNotExist" ? (
15 |
The user does not exist
16 | ) : result === "alreadyActivated" ? (
17 |
The user is already activated
18 | ) : result === "success" ? (
19 |
20 | Success! The user is now activated
21 |
22 | ) : (
23 |
Oops! Something went wrong!
24 | )}
25 |
26 | );
27 | };
28 |
29 | export default ActivationPage;
30 |
--------------------------------------------------------------------------------
/src/app/auth/forgotPassword/page.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 | import { forgotPassword } from "@/lib/actions/authActions";
3 | import { EnvelopeIcon } from "@heroicons/react/20/solid";
4 | import { zodResolver } from "@hookform/resolvers/zod";
5 | import { Button, Input } from "@nextui-org/react";
6 | import Image from "next/image";
7 | import { SubmitHandler, useForm } from "react-hook-form";
8 | import { toast } from "react-toastify";
9 | import { z } from "zod";
10 |
11 | const FormSchema = z.object({
12 | email: z.string().email("Please enter a valid email!"),
13 | });
14 |
15 | type InputType = z.infer;
16 |
17 | const ForgotPasswordPage = () => {
18 | const {
19 | register,
20 | handleSubmit,
21 | reset,
22 | formState: { errors, isSubmitting },
23 | } = useForm({
24 | resolver: zodResolver(FormSchema),
25 | });
26 |
27 | const submitRequest: SubmitHandler = async (data) => {
28 | try {
29 | const result = await forgotPassword(data.email);
30 | if (result) toast.success("Reset password link was sent to your email.");
31 | reset();
32 | } catch (e) {
33 | console.log(e);
34 | toast.error("Something went wrong!");
35 | }
36 | };
37 | return (
38 |
67 | );
68 | };
69 |
70 | export default ForgotPasswordPage;
71 |
--------------------------------------------------------------------------------
/src/app/auth/resetPass/[jwt]/page.tsx:
--------------------------------------------------------------------------------
1 | import ResetPasswordForm from "@/app/components/ResetPasswordForm";
2 | import { verifyJwt } from "@/lib/jwt";
3 |
4 | interface Props {
5 | params: {
6 | jwt: string;
7 | };
8 | }
9 |
10 | const ResetPasswordPage = ({ params }: Props) => {
11 | const payload = verifyJwt(params.jwt);
12 | if (!payload)
13 | return (
14 |
15 | The URL is not valid!
16 |
17 | );
18 | return (
19 |
20 |
21 |
22 | );
23 | };
24 |
25 | export default ResetPasswordPage;
26 |
--------------------------------------------------------------------------------
/src/app/auth/signin/page.tsx:
--------------------------------------------------------------------------------
1 | import SignInForm from "@/app/components/SignInForm";
2 | import Link from "next/link";
3 |
4 | interface Props {
5 | searchParams: {
6 | callbackUrl?: string;
7 | };
8 | }
9 |
10 | const SigninPage = ({ searchParams }: Props) => {
11 | console.log({ searchParams });
12 |
13 | return (
14 |
15 |
16 | Forgot Your Password?
17 |
18 | );
19 | };
20 |
21 | export default SigninPage;
22 |
--------------------------------------------------------------------------------
/src/app/auth/signup/page.tsx:
--------------------------------------------------------------------------------
1 | import SignUpForm from "@/app/components/SignUpForm";
2 | import { Image, Link } from "@nextui-org/react";
3 |
4 | const SignupPage = () => {
5 | return (
6 |
7 |
8 |
Already Signed up?
9 |
Sign In
10 |
11 |
12 |
13 |
14 | );
15 | };
16 |
17 | export default SignupPage;
18 |
--------------------------------------------------------------------------------
/src/app/components/Appbar.tsx:
--------------------------------------------------------------------------------
1 | import { Button, Navbar, NavbarContent, NavbarItem } from "@nextui-org/react";
2 | import SigninButton from "./SigninButton";
3 | import Link from "next/link";
4 |
5 | const Appbar = () => {
6 | return (
7 |
8 |
9 |
10 |
15 | Home
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 | );
26 | };
27 |
28 | export default Appbar;
29 |
--------------------------------------------------------------------------------
/src/app/components/NextAuthProviders.tsx:
--------------------------------------------------------------------------------
1 | import { Button } from "@nextui-org/react";
2 |
3 | import { signIn } from "next-auth/react";
4 |
5 | const NextAuthProviders = () => {
6 | const googleSignIn = async () => {
7 | const result = await signIn("google", {
8 | callbackUrl: "/",
9 | });
10 | console.log({ result });
11 | };
12 | return (
13 |
14 |
15 |
16 | );
17 | };
18 |
19 | export default NextAuthProviders;
20 |
--------------------------------------------------------------------------------
/src/app/components/PasswordStrength.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from "clsx-tailwind-merge";
2 |
3 | interface Props {
4 | passStrength: number;
5 | }
6 |
7 | const PasswordStrength = ({ passStrength }: Props) => {
8 | return (
9 |
15 | {Array.from({ length: passStrength + 1 }).map((i, index) => (
16 |
25 | ))}
26 |
27 | );
28 | };
29 |
30 | export default PasswordStrength;
31 |
--------------------------------------------------------------------------------
/src/app/components/ResetPasswordForm.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 | import { EyeIcon, EyeSlashIcon } from "@heroicons/react/20/solid";
3 | import { zodResolver } from "@hookform/resolvers/zod";
4 | import { Button, Input } from "@nextui-org/react";
5 | import { passwordStrength } from "check-password-strength";
6 | import { useEffect, useState } from "react";
7 | import { SubmitHandler, useForm } from "react-hook-form";
8 | import { z } from "zod";
9 | import PasswordStrength from "./PasswordStrength";
10 | import { resetPassword } from "@/lib/actions/authActions";
11 | import { toast } from "react-toastify";
12 |
13 | interface Props {
14 | jwtUserId: string;
15 | }
16 |
17 | const FormSchema = z
18 | .object({
19 | password: z
20 | .string()
21 | .min(6, "Password must be at least 6 characters!")
22 | .max(52, "Password must be less than 52 characters"),
23 | confirmPassword: z.string(),
24 | })
25 | .refine((data) => data.password === data.confirmPassword, {
26 | message: "Password does not match!",
27 | path: ["confirmPassword"],
28 | });
29 |
30 | type InputType = z.infer;
31 |
32 | const ResetPasswordForm = ({ jwtUserId }: Props) => {
33 | const [visiblePass, setVisiblePass] = useState(false);
34 | const [passStrength, setPassStrength] = useState(0);
35 |
36 | const {
37 | register,
38 | handleSubmit,
39 | reset,
40 | watch,
41 | formState: { errors, isSubmitting },
42 | } = useForm({
43 | resolver: zodResolver(FormSchema),
44 | });
45 | useEffect(() => {
46 | setPassStrength(passwordStrength(watch().password).id);
47 | }, [watch().password]);
48 |
49 | const resetPass: SubmitHandler = async (data) => {
50 | try {
51 | const result = await resetPassword(jwtUserId, data.password);
52 | if (result === "success")
53 | toast.success("Your password has been reset successfully!");
54 | } catch (err) {
55 | toast.error("Something went wrong!");
56 | console.error(err);
57 | }
58 | };
59 | return (
60 |
98 | );
99 | };
100 |
101 | export default ResetPasswordForm;
102 |
--------------------------------------------------------------------------------
/src/app/components/SignInForm.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 | import { EyeIcon, EyeSlashIcon } from "@heroicons/react/20/solid";
3 | import { zodResolver } from "@hookform/resolvers/zod";
4 | import { Button, Input } from "@nextui-org/react";
5 | import { signIn } from "next-auth/react";
6 |
7 | import Link from "next/link";
8 | import { useRouter } from "next/navigation";
9 | import { useState } from "react";
10 | import { SubmitHandler, useForm } from "react-hook-form";
11 | import { toast } from "react-toastify";
12 | import { z } from "zod";
13 | import NextAuthProviders from "./NextAuthProviders";
14 |
15 | interface Props {
16 | callbackUrl?: string;
17 | }
18 |
19 | const FormSchema = z.object({
20 | email: z.string().email("Please enter a valid email address"),
21 | password: z.string({
22 | required_error: "Please enter your password",
23 | }),
24 | });
25 |
26 | type InputType = z.infer;
27 |
28 | const SignInForm = (props: Props) => {
29 | const router = useRouter();
30 | const [visiblePass, setVisiblePass] = useState(false);
31 | const {
32 | register,
33 | handleSubmit,
34 | formState: { errors, isSubmitting },
35 | } = useForm({
36 | resolver: zodResolver(FormSchema),
37 | });
38 |
39 | const onSubmit: SubmitHandler = async (data) => {
40 | const result = await signIn("credentials", {
41 | redirect: false,
42 | username: data.email,
43 | password: data.password,
44 | });
45 | if (!result?.ok) {
46 | toast.error(result?.error);
47 | return;
48 | }
49 | toast.success("Welcome To Sakura Dev Channel");
50 | router.push(props.callbackUrl ? props.callbackUrl : "/");
51 | };
52 |
53 | return (
54 |
85 | );
86 | };
87 |
88 | export default SignInForm;
89 |
--------------------------------------------------------------------------------
/src/app/components/SignUpForm.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 | import {
3 | EnvelopeIcon,
4 | EyeIcon,
5 | EyeSlashIcon,
6 | KeyIcon,
7 | PhoneIcon,
8 | UserIcon,
9 | } from "@heroicons/react/20/solid";
10 | import { Button, Checkbox, Input, Link } from "@nextui-org/react";
11 | import { useEffect, useState } from "react";
12 | import { z } from "zod";
13 | import validator from "validator";
14 | import { Controller, SubmitHandler, useForm } from "react-hook-form";
15 | import { zodResolver } from "@hookform/resolvers/zod";
16 | import { passwordStrength } from "check-password-strength";
17 | import PasswordStrength from "./PasswordStrength";
18 | import { registerUser } from "@/lib/actions/authActions";
19 | import { toast } from "react-toastify";
20 |
21 | const FormSchema = z
22 | .object({
23 | firstName: z
24 | .string()
25 | .min(2, "First name must be atleast 2 characters")
26 | .max(45, "First name must be less than 45 characters")
27 | .regex(new RegExp("^[a-zA-Z]+$"), "No special character allowed!"),
28 | lastName: z
29 | .string()
30 | .min(2, "Last name must be atleast 2 characters")
31 | .max(45, "Last name must be less than 45 characters")
32 | .regex(new RegExp("^[a-zA-Z]+$"), "No special character allowed!"),
33 | email: z.string().email("Please enter a valid email address"),
34 | phone: z
35 | .string()
36 | .refine(validator.isMobilePhone, "Please enter a valid phone number!"),
37 | password: z
38 | .string()
39 | .min(6, "Password must be at least 6 characters ")
40 | .max(50, "Password must be less than 50 characters"),
41 | confirmPassword: z
42 | .string()
43 | .min(6, "Password must be at least 6 characters ")
44 | .max(50, "Password must be less than 50 characters"),
45 | accepted: z.literal(true, {
46 | errorMap: () => ({
47 | message: "Please accept all terms",
48 | }),
49 | }),
50 | })
51 | .refine((data) => data.password === data.confirmPassword, {
52 | message: "Password and confirm password doesn't match!",
53 | path: ["confirmPassword"],
54 | });
55 |
56 | type InputType = z.infer;
57 |
58 | const SignUpForm = () => {
59 | const {
60 | register,
61 | handleSubmit,
62 | reset,
63 | control,
64 | watch,
65 | formState: { errors },
66 | } = useForm({
67 | resolver: zodResolver(FormSchema),
68 | });
69 | const [passStrength, setPassStrength] = useState(0);
70 | const [isVisiblePass, setIsVisiblePass] = useState(false);
71 |
72 | useEffect(() => {
73 | setPassStrength(passwordStrength(watch().password).id);
74 | }, [watch().password]);
75 | const toggleVisblePass = () => setIsVisiblePass((prev) => !prev);
76 |
77 | const saveUser: SubmitHandler = async (data) => {
78 | const { accepted, confirmPassword, ...user } = data;
79 | try {
80 | const result = await registerUser(user);
81 | toast.success("The User Registered Successfully.");
82 | } catch (error) {
83 | toast.error("Something Went Wrong!");
84 | console.error(error);
85 | }
86 | };
87 | return (
88 |
176 | );
177 | };
178 |
179 | export default SignUpForm;
180 |
--------------------------------------------------------------------------------
/src/app/components/SigninButton.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { Button } from "@nextui-org/react";
4 | import { signIn, useSession } from "next-auth/react";
5 | import Link from "next/link";
6 |
7 | const SigninButton = () => {
8 | const { data: session } = useSession();
9 |
10 | return (
11 |
12 | {session && session.user ? (
13 | <>
14 | {`${session.user.firstName} ${session.user.lastName}`}
15 |
19 | Sign Out
20 |
21 | >
22 | ) : (
23 | <>
24 |
25 |
28 | >
29 | )}
30 |
31 | );
32 | };
33 |
34 | export default SigninButton;
35 |
--------------------------------------------------------------------------------
/src/app/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vahid-nejad/Nextjs14-Comprehensive-authentication-Course/07f056c61a50299e85b5bf914b91df2c31eba6cd/src/app/favicon.ico
--------------------------------------------------------------------------------
/src/app/globals.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
--------------------------------------------------------------------------------
/src/app/layout.tsx:
--------------------------------------------------------------------------------
1 | import type { Metadata } from "next";
2 | import { Inter } from "next/font/google";
3 | import "./globals.css";
4 | import { Providers } from "./providers";
5 | import Appbar from "./components/Appbar";
6 | import { ToastContainer } from "react-toastify";
7 | import "react-toastify/dist/ReactToastify.css";
8 |
9 | const inter = Inter({ subsets: ["latin"] });
10 |
11 | export const metadata: Metadata = {
12 | title: "Create Next App",
13 | description: "Generated by create next app",
14 | };
15 |
16 | export default function RootLayout({
17 | children,
18 | }: {
19 | children: React.ReactNode;
20 | }) {
21 | return (
22 |
23 |
24 |
25 |
26 | {children}
27 |
28 |
29 |
30 |
31 | );
32 | }
33 |
--------------------------------------------------------------------------------
/src/app/page.tsx:
--------------------------------------------------------------------------------
1 | import { sendMail } from "@/lib/mail";
2 | import Image from "next/image";
3 |
4 | export default async function Home() {
5 | return (
6 |
7 |
8 |
9 | Get started by editing
10 | src/app/page.tsx
11 |
12 |
30 |
31 |
32 |
33 |
41 |
42 |
43 |
112 |
113 | );
114 | }
115 |
--------------------------------------------------------------------------------
/src/app/profile/page.tsx:
--------------------------------------------------------------------------------
1 | import { getServerSession } from "next-auth";
2 | import { authOptions } from "../api/auth/[...nextauth]/route";
3 | import Image from "next/image";
4 | import { redirect } from "next/navigation";
5 |
6 | const ProfilePage = async () => {
7 | const session = await getServerSession(authOptions);
8 | const user = session?.user;
9 | // if (!session || !session.user) redirect("/auth/signin");
10 | return (
11 |
12 |
19 |
20 |
First Name:
{user?.firstName}
21 |
Last Name:
{user?.lastName}
22 |
Phone:
{user?.phone}
23 |
Email:
{user?.email}
24 |
25 |
26 | );
27 | };
28 |
29 | export default ProfilePage;
30 |
--------------------------------------------------------------------------------
/src/app/providers.tsx:
--------------------------------------------------------------------------------
1 | // app/providers.tsx
2 | "use client";
3 |
4 | import { NextUIProvider } from "@nextui-org/react";
5 | import { SessionProvider } from "next-auth/react";
6 |
7 | export function Providers({ children }: { children: React.ReactNode }) {
8 | return (
9 |
10 | {children}
11 |
12 | );
13 | }
14 |
--------------------------------------------------------------------------------
/src/lib/actions/authActions.ts:
--------------------------------------------------------------------------------
1 | "use server";
2 |
3 | import { User } from "@prisma/client";
4 |
5 | import * as bcrypt from "bcrypt";
6 | import {
7 | compileActivationTemplate,
8 | compileResetPassTemplate,
9 | sendMail,
10 | } from "../mail";
11 | import { signJwt, verifyJwt } from "../jwt";
12 | import { prisma } from "@/lib/prisma";
13 |
14 | export async function registerUser(
15 | user: Omit
16 | ) {
17 | const result = await prisma.user.create({
18 | data: {
19 | ...user,
20 | password: await bcrypt.hash(user.password, 10),
21 | },
22 | });
23 |
24 | const jwtUserId = signJwt({
25 | id: result.id,
26 | });
27 | const activationUrl = `${process.env.NEXTAUTH_URL}/auth/activation/${jwtUserId}`;
28 | const body = compileActivationTemplate(user.firstName, activationUrl);
29 | await sendMail({ to: user.email, subject: "Activate Your Account", body });
30 | return result;
31 | }
32 |
33 | type ActivateUserFunc = (
34 | jwtUserId: string
35 | ) => Promise<"userNotExist" | "alreadyActivated" | "success">;
36 |
37 | export const activateUser: ActivateUserFunc = async (jwtUserID) => {
38 | const payload = verifyJwt(jwtUserID);
39 | const userId = payload?.id;
40 | const user = await prisma.user.findUnique({
41 | where: {
42 | id: userId,
43 | },
44 | });
45 | if (!user) return "userNotExist";
46 | if (user.emailVerified) return "alreadyActivated";
47 | const result = await prisma.user.update({
48 | where: {
49 | id: userId,
50 | },
51 | data: {
52 | emailVerified: new Date(),
53 | },
54 | });
55 | return "success";
56 | };
57 |
58 | export async function forgotPassword(email: string) {
59 | const user = await prisma.user.findUnique({
60 | where: {
61 | email: email,
62 | },
63 | });
64 |
65 | if (!user) throw new Error("The User Does Not Exist!");
66 |
67 | // Send Email with Password Reset Link
68 | const jwtUserId = signJwt({
69 | id: user.id,
70 | });
71 | const resetPassUrl = `${process.env.NEXTAUTH_URL}/auth/resetPass/${jwtUserId}`;
72 | const body = compileResetPassTemplate(user.firstName, resetPassUrl);
73 | const sendResult = await sendMail({
74 | to: user.email,
75 | subject: "Reset Password",
76 | body: body,
77 | });
78 | return sendResult;
79 | }
80 |
81 | type ResetPasswordFucn = (
82 | jwtUserId: string,
83 | password: string
84 | ) => Promise<"userNotExist" | "success">;
85 |
86 | export const resetPassword: ResetPasswordFucn = async (jwtUserId, password) => {
87 | const payload = verifyJwt(jwtUserId);
88 | if (!payload) return "userNotExist";
89 | const userId = payload.id;
90 | const user = await prisma.user.findUnique({
91 | where: {
92 | id: userId,
93 | },
94 | });
95 | if (!user) return "userNotExist";
96 |
97 | const result = await prisma.user.update({
98 | where: {
99 | id: userId,
100 | },
101 | data: {
102 | password: await bcrypt.hash(password, 10),
103 | },
104 | });
105 | if (result) return "success";
106 | else throw new Error("Something went wrong!");
107 | };
108 |
--------------------------------------------------------------------------------
/src/lib/emailTemplates/activation.ts:
--------------------------------------------------------------------------------
1 | export const activationTemplate = `
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 | |
112 |
113 |
114 |
115 |
116 |
117 |
118 | |
119 |
120 |
121 | |
122 |
123 |
124 |
125 | |
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 |
134 |
135 |
136 |
137 |
138 |
139 |
140 |
141 | |
142 |
143 |
144 | |
145 |
146 |
147 |
148 | |
149 |
150 |
151 |
152 |
153 |
154 |
155 |
156 |
157 |
158 |
159 |
160 |
161 |
162 |
163 | Hi {{name}}
164 | |
165 |
166 |
167 |
168 |
169 |
170 | Welcome to my authentication course
171 | |
172 |
173 |
174 |
175 |
176 |
177 |
178 | Please activate your account with link below!
179 |
180 | |
181 |
182 |
183 |
190 |
191 |
192 |
193 |
194 | |
195 |
196 |
197 | |
198 |
199 |
200 |
201 | |
202 |
203 |
204 |
205 |
206 |
207 |
208 |
209 |
210 |
211 |
212 |
213 |
214 |
215 |
216 |
217 | |
218 |
219 |
220 | |
221 |
222 |
223 |
224 |
225 |
226 | |
227 |
228 |
229 | |
230 |
231 |
232 |
233 | |
234 |
235 |
236 |
237 |
238 |
239 |
240 |
241 |
242 |
243 |
244 |
245 |
246 |
247 |
248 | Your books, online and off
249 | |
250 |
251 |
252 |
253 |
254 |
255 |
256 | You now get unlimited downloads to take all your books with you on up to 5 devices.
257 |
258 | |
259 |
260 |
261 |
262 |
263 |
264 |
265 | |
266 |
267 |
268 |
269 |
270 |
271 |
274 | |
275 |
276 |
277 |
278 |
279 |
280 |
281 |
282 |
283 |  |
284 |  |
285 |  |
286 |
287 |
288 |
289 | |
290 |
291 |
292 | |
293 |
294 |
295 |
296 | |
297 |
298 |
299 |
300 |
301 |
302 |
303 |
304 |
305 |
306 |
307 |
308 |
309 |
310 |
311 |
312 | |
313 |
314 |
315 |
316 |
317 |
318 |
319 |
320 | © 2020 Company Name | 123 Main St. City, State, Country 12345
321 |
322 |
323 | |
324 |
325 |
326 |
327 |
328 |
329 |
334 | |
335 |
336 |
337 | |
338 |
339 |
340 |
341 | |
342 |
343 |
344 |
345 |
346 |
347 |
348 |
349 |
350 |
351 |
352 |
353 |
354 |
355 |
356 |
357 |
358 |
359 |
360 |
366 | |
367 |
368 |
369 | |
370 |
371 |
372 | |
373 |
374 |
375 |
376 | |
377 |
378 |
379 |
380 | |
381 |
382 |
383 |
384 |
385 |
386 |
387 | `;
388 |
--------------------------------------------------------------------------------
/src/lib/emailTemplates/resetPass.ts:
--------------------------------------------------------------------------------
1 | export const resetPasswordTemplate = `
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 | |
112 |
113 |
114 |
115 |
116 |
117 |
118 | |
119 |
120 |
121 | |
122 |
123 |
124 |
125 | |
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 |
134 |
135 |
136 |
137 |
138 |
139 |
140 |
141 | |
142 |
143 |
144 | |
145 |
146 |
147 |
148 | |
149 |
150 |
151 |
152 |
153 |
154 |
155 |
156 |
157 |
158 |
159 |
160 |
161 |
162 |
163 | Hi {{name}}
164 | |
165 |
166 |
167 |
168 |
169 |
170 | We've sent this email because you have forgotten your password!
171 | |
172 |
173 |
174 |
175 |
176 |
177 |
178 | You can reset your password through the link below.
179 |
180 | |
181 |
182 |
183 |
190 |
191 |
192 |
193 |
194 | |
195 |
196 |
197 | |
198 |
199 |
200 |
201 | |
202 |
203 |
204 |
205 |
206 |
207 |
208 |
209 |
210 |
211 |
212 |
213 |
214 |
215 |
216 |
217 | |
218 |
219 |
220 | |
221 |
222 |
223 |
224 |
225 |
226 | |
227 |
228 |
229 | |
230 |
231 |
232 |
233 | |
234 |
235 |
236 |
237 |
238 |
239 |
240 |
241 |
242 |
243 |
244 |
245 |
246 |
247 |
248 | Your books, online and off
249 | |
250 |
251 |
252 |
253 |
254 |
255 |
256 | You now get unlimited downloads to take all your books with you on up to 5 devices.
257 |
258 | |
259 |
260 |
261 |
262 |
263 |
264 |
265 | |
266 |
267 |
268 |
269 |
270 |
271 |
274 | |
275 |
276 |
277 |
278 |
279 |
280 |
281 |
282 |
283 |  |
284 |  |
285 |  |
286 |
287 |
288 |
289 | |
290 |
291 |
292 | |
293 |
294 |
295 |
296 | |
297 |
298 |
299 |
300 |
301 |
302 |
303 |
304 |
305 |
306 |
307 |
308 |
309 |
310 |
311 |
312 | |
313 |
314 |
315 |
316 |
317 |
318 |
319 |
320 | © 2020 Company Name | 123 Main St. City, State, Country 12345
321 |
322 |
323 | |
324 |
325 |
326 |
327 |
328 |
329 |
334 | |
335 |
336 |
337 | |
338 |
339 |
340 |
341 | |
342 |
343 |
344 |
345 |
346 |
347 |
348 |
349 |
350 |
351 |
352 |
353 |
354 |
355 |
356 |
357 |
358 |
359 |
360 |
366 | |
367 |
368 |
369 | |
370 |
371 |
372 | |
373 |
374 |
375 |
376 | |
377 |
378 |
379 |
380 | |
381 |
382 |
383 |
384 |
385 |
386 |
387 | `;
388 |
--------------------------------------------------------------------------------
/src/lib/jwt.ts:
--------------------------------------------------------------------------------
1 | import jwt, { JwtPayload } from "jsonwebtoken";
2 |
3 | interface SignOption {
4 | expiresIn: string | number;
5 | }
6 |
7 | const DEFAULT_SIGN_OPTION: SignOption = {
8 | expiresIn: "1d",
9 | };
10 |
11 | export function signJwt(
12 | payload: JwtPayload,
13 | option: SignOption = DEFAULT_SIGN_OPTION
14 | ) {
15 | const secretKey = process.env.JWT_USER_ID_SECRET!;
16 | const token = jwt.sign(payload, secretKey);
17 | return token;
18 | }
19 |
20 | export function verifyJwt(token: string) {
21 | try {
22 | const secretKey = process.env.JWT_USER_ID_SECRET!;
23 | const decoded = jwt.verify(token, secretKey);
24 | return decoded as JwtPayload;
25 | } catch (e) {
26 | console.log(e);
27 | return null;
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/src/lib/mail.ts:
--------------------------------------------------------------------------------
1 | import Handlebars from "handlebars";
2 | import nodemailer from "nodemailer";
3 | import { activationTemplate } from "./emailTemplates/activation";
4 | import { resetPasswordTemplate } from "./emailTemplates/resetPass";
5 |
6 | export async function sendMail({
7 | to,
8 | subject,
9 | body,
10 | }: {
11 | to: string;
12 | subject: string;
13 | body: string;
14 | }) {
15 | const { SMPT_EMAIL, SMTP_GMAIL_PASS, SMTP_USER, SMTP_PASS } = process.env;
16 | //
17 | var transport = nodemailer.createTransport({
18 | host: "sandbox.smtp.mailtrap.io",
19 | port: 2525,
20 | auth: {
21 | user: SMTP_USER,
22 | pass: SMTP_PASS,
23 | },
24 | });
25 |
26 | try {
27 | const testResult = await transport.verify();
28 | console.log("Test Result Of Transport", testResult);
29 | } catch (e) {
30 | console.log(e);
31 | }
32 | try {
33 | const sendResult = await transport.sendMail({
34 | from: SMPT_EMAIL,
35 | to,
36 | subject,
37 | html: body,
38 | });
39 | console.log({ sendResult });
40 | return sendResult;
41 | } catch (e) {
42 | console.log(e);
43 | }
44 | }
45 |
46 | export function compileActivationTemplate(name: string, url: string) {
47 | const template = Handlebars.compile(activationTemplate);
48 | const htmlBody = template({
49 | name,
50 | url,
51 | });
52 | return htmlBody;
53 | }
54 | export function compileResetPassTemplate(name: string, url: string) {
55 | const template = Handlebars.compile(resetPasswordTemplate);
56 | const htmlBody = template({
57 | name,
58 | url,
59 | });
60 | return htmlBody;
61 | }
62 |
--------------------------------------------------------------------------------
/src/lib/prisma.ts:
--------------------------------------------------------------------------------
1 | import { PrismaClient } from "@prisma/client";
2 |
3 | const globalForPrisma = global as unknown as {
4 | prisma: PrismaClient;
5 | };
6 |
7 | export const prisma = globalForPrisma.prisma || new PrismaClient();
8 |
9 | if (process.env.NODE_ENV !== "production") globalForPrisma.prisma = prisma;
10 |
11 | export default prisma;
12 |
--------------------------------------------------------------------------------
/src/lib/types.d.ts:
--------------------------------------------------------------------------------
1 | import { User } from "@prisma/client";
2 |
3 | declare module "next-auth" {
4 | interface Session {
5 | user: User;
6 | }
7 | }
8 |
9 | declare module "next-auth/jwt" {
10 | interface JWT {
11 | user: User;
12 | }
13 | }
14 |
15 | declare module NodeJS {
16 | interface ProcessEnv {
17 | SMPT_EMAIL: string;
18 | SMTP_GMAIL_PASS: string;
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/src/middleware.ts:
--------------------------------------------------------------------------------
1 | export { default } from "next-auth/middleware";
2 |
3 | export const config = {
4 | matcher: ["/profile", "/admin/:path*"],
5 | };
6 |
--------------------------------------------------------------------------------
/tailwind.config.ts:
--------------------------------------------------------------------------------
1 | import type { Config } from "tailwindcss";
2 | import { nextui } from "@nextui-org/react";
3 | const config: Config = {
4 | content: [
5 | "./src/pages/**/*.{js,ts,jsx,tsx,mdx}",
6 | "./src/components/**/*.{js,ts,jsx,tsx,mdx}",
7 | "./src/app/**/*.{js,ts,jsx,tsx,mdx}",
8 | "./node_modules/@nextui-org/theme/dist/**/*.{js,ts,jsx,tsx}",
9 | ],
10 | theme: {
11 | extend: {
12 | backgroundImage: {
13 | "gradient-radial": "radial-gradient(var(--tw-gradient-stops))",
14 | "gradient-conic":
15 | "conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))",
16 | },
17 | },
18 | },
19 | darkMode: "class",
20 | plugins: [nextui()],
21 | };
22 | export default config;
23 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
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 |
--------------------------------------------------------------------------------