├── .eslintrc.json
├── public
├── auth.png
├── home.png
├── favicon.ico
├── vercel.svg
└── next.svg
├── postcss.config.js
├── next.config.js
├── tailwind.config.js
├── utils
├── tokens.ts
├── connectDb.ts
└── sendMail.ts
├── src
├── types
│ └── next-auth.d.ts
├── pages
│ ├── forgot.tsx
│ ├── reset
│ │ └── [token].tsx
│ ├── _app.tsx
│ ├── api
│ │ └── auth
│ │ │ ├── reset.ts
│ │ │ ├── activate.ts
│ │ │ ├── forgot.ts
│ │ │ ├── signup.ts
│ │ │ └── [...nextauth].ts
│ ├── activate
│ │ └── [token].tsx
│ ├── auth.tsx
│ └── index.tsx
├── middleware.ts
├── lib
│ └── mongodb.ts
├── components
│ ├── buttons
│ │ ├── SocialButton.tsx
│ │ └── SlideButton.tsx
│ ├── inputs
│ │ └── input.tsx
│ └── forms
│ │ ├── Forgot.tsx
│ │ ├── Login.tsx
│ │ ├── Reset.tsx
│ │ └── Register.tsx
├── styles
│ └── globals.css
└── emailTemplates
│ ├── reset.ts
│ └── activate.ts
├── .gitignore
├── tsconfig.json
├── models
└── User.ts
├── package.json
└── README.md
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "next/core-web-vitals"
3 | }
4 |
--------------------------------------------------------------------------------
/public/auth.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Mehranlip/Mojave-Next-Auth/HEAD/public/auth.png
--------------------------------------------------------------------------------
/public/home.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Mehranlip/Mojave-Next-Auth/HEAD/public/home.png
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Mehranlip/Mojave-Next-Auth/HEAD/public/favicon.ico
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | }
7 |
--------------------------------------------------------------------------------
/next.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('next').NextConfig} */
2 | const nextConfig = {
3 | reactStrictMode: true,
4 | }
5 |
6 | module.exports = nextConfig
7 |
--------------------------------------------------------------------------------
/tailwind.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('tailwindcss').Config} */
2 | module.exports = {
3 | content: [
4 | "./src/app/**/*.{js,ts,jsx,tsx,mdx}",
5 | "./src/pages/**/*.{js,ts,jsx,tsx,mdx}",
6 | "./src/components/**/*.{js,ts,jsx,tsx,mdx}",
7 |
8 | // Or if using `src` directory:
9 | "./src/**/*.{js,ts,jsx,tsx,mdx}",
10 | ],
11 | theme: {
12 | extend: {},
13 | },
14 | plugins: [],
15 | }
--------------------------------------------------------------------------------
/utils/tokens.ts:
--------------------------------------------------------------------------------
1 | import jwt from "jsonwebtoken";
2 |
3 | const { ACTIVATION_TOKEN_SECRET, RESET_TOKEN_SECRET } = process.env;
4 | export const createActivationToken = (payload: any) => {
5 | return jwt.sign(payload, ACTIVATION_TOKEN_SECRET!, {
6 | expiresIn: "2d",
7 | });
8 | };
9 | export const createResetToken = (payload: any) => {
10 | return jwt.sign(payload, RESET_TOKEN_SECRET!, {
11 | expiresIn: "6h",
12 | });
13 | };
14 |
--------------------------------------------------------------------------------
/src/types/next-auth.d.ts:
--------------------------------------------------------------------------------
1 | import NextAuth from "next-auth";
2 |
3 | declare module "next-auth" {
4 | /**
5 | * Returned by `useSession`, `getSession` and received as a prop on the `SessionProvider` React Context
6 | */
7 | interface Session {
8 | user: {
9 | /** The user's postal address. */
10 | name: string;
11 | email: string;
12 | image: string;
13 | provider: string;
14 | };
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/src/pages/forgot.tsx:
--------------------------------------------------------------------------------
1 | import ForgotForm from "@/components/forms/Forgot";
2 |
3 | export default function auth() {
4 | return (
5 |
6 |
7 |
8 |
9 | {/* forgot FORM */}
10 |
11 |
12 |
13 |
14 |
15 | );
16 | }
17 |
--------------------------------------------------------------------------------
/.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 | .env
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 | .pnpm-debug.log*
28 |
29 | # local env files
30 | .env*.local
31 |
32 | # vercel
33 | .vercel
34 |
35 | # typescript
36 | *.tsbuildinfo
37 | next-env.d.ts
--------------------------------------------------------------------------------
/public/vercel.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/middleware.ts:
--------------------------------------------------------------------------------
1 | import { getToken } from "next-auth/jwt";
2 | import { NextRequest, NextResponse } from "next/server";
3 |
4 | export async function middleware(req: NextRequest) {
5 | const { pathname, origin } = req.nextUrl;
6 | const session = await getToken({
7 | req,
8 | secret: process.env.NEXTAUTH_SECRET,
9 | secureCookie: process.env.NODE_ENV === "production",
10 | });
11 | if (pathname == "/") {
12 | if (!session)
13 | return NextResponse.redirect(`${process.env.NEXTAUTH_URL}/auth`);
14 | }
15 | if (pathname == "/auth") {
16 | if (session) return NextResponse.redirect(`${origin}`);
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/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": "node",
13 | "resolveJsonModule": true,
14 | "isolatedModules": true,
15 | "jsx": "preserve",
16 | "incremental": true,
17 | "paths": {
18 | "@/*": ["./src/*"]
19 | }
20 | },
21 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
22 | "exclude": ["node_modules"]
23 | }
24 |
--------------------------------------------------------------------------------
/src/pages/reset/[token].tsx:
--------------------------------------------------------------------------------
1 | import ForgotForm from "@/components/forms/Forgot";
2 | import ResetForm from "@/components/forms/Reset";
3 | import { NextPageContext } from "next";
4 |
5 | export default function Reset({ token }: { token: string }) {
6 | return (
7 |
8 |
9 |
10 |
11 | {/* forgot FORM */}
12 |
13 |
14 |
15 |
16 |
17 | );
18 | }
19 |
20 | export async function getServerSideProps(ctx: NextPageContext) {
21 | const { query } = ctx;
22 | const token = query.token;
23 |
24 | return {
25 | props: { token },
26 | };
27 | }
28 |
--------------------------------------------------------------------------------
/src/pages/_app.tsx:
--------------------------------------------------------------------------------
1 | import "@/styles/globals.css";
2 | import type { AppProps } from "next/app";
3 | import { SessionProvider } from "next-auth/react";
4 |
5 | import { ToastContainer } from "react-toastify";
6 | import "react-toastify/dist/ReactToastify.css";
7 |
8 | // test
9 |
10 | export default function App({
11 | Component,
12 | pageProps: { session, ...pageProps },
13 | }: AppProps) {
14 | return (
15 |
16 |
28 |
29 |
30 | );
31 | }
32 |
--------------------------------------------------------------------------------
/models/User.ts:
--------------------------------------------------------------------------------
1 | import mongoose from "mongoose";
2 |
3 | const userSchema = new mongoose.Schema({
4 | name: {
5 | type: String,
6 | required: true,
7 | },
8 | email: {
9 | type: String,
10 | required: true,
11 | unique: true,
12 | },
13 | image: {
14 | type: String,
15 | default:
16 | "https://res.cloudinary.com/dmhcnhtng/image/upload/v1664642479/992490_sskqn3.png",
17 | },
18 | password: {
19 | type: String,
20 | required: true,
21 | minlength: 6,
22 | },
23 | emailVerified: {
24 | type: Boolean,
25 | default: false,
26 | },
27 | phone: {
28 | type: String,
29 | required: true,
30 | },
31 | role: {
32 | type: String,
33 | default: "user",
34 | },
35 | });
36 |
37 | const UserModal =
38 | mongoose.models.UserModal || mongoose.model("UserModal", userSchema);
39 |
40 | export default UserModal;
41 |
--------------------------------------------------------------------------------
/src/pages/api/auth/reset.ts:
--------------------------------------------------------------------------------
1 | import UserModal from "../../../../models/User";
2 | import connectDb from "../../../../utils/connectDb";
3 | import jwt, { JwtPayload } from "jsonwebtoken";
4 | import type { NextApiRequest, NextApiResponse } from "next";
5 | const { RESET_TOKEN_SECRET } = process.env;
6 | import bcrypt from "bcryptjs";
7 | interface UserToken {
8 | id: string;
9 | }
10 | export default async function handler(
11 | req: NextApiRequest,
12 | res: NextApiResponse
13 | ) {
14 | try {
15 | await connectDb();
16 | const { token, password } = req.body;
17 | const userToken = jwt.verify(token, RESET_TOKEN_SECRET!) as UserToken;
18 | const userDb = await UserModal.findById(userToken.id);
19 | if (!userDb) {
20 | return res.status(400).json({ message: "This account no longer exist." });
21 | }
22 | const cryptedPassword = await bcrypt.hash(password, 12);
23 | await UserModal.findByIdAndUpdate(userDb.id, { password: cryptedPassword });
24 | res.json({
25 | message: "Your account password has beeen successfully updated.",
26 | });
27 | } catch (error) {
28 | res.status(500).json({ message: (error as Error).message });
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/utils/connectDb.ts:
--------------------------------------------------------------------------------
1 | import mongoose from "mongoose";
2 |
3 |
4 |
5 | if (!process.env.DATABASE_URL) {
6 | throw new Error("please add the database url in .env file");
7 | }
8 |
9 | const DATABASE_URL: string = process.env.DATABASE_URL;
10 |
11 | let globalWithMongoose = global as typeof globalThis & {
12 | mongoose: any;
13 | };
14 |
15 | let cached = globalWithMongoose.mongoose;
16 |
17 | if (!cached) {
18 | cached = globalWithMongoose.mongoose = { conn: null, promise: null };
19 | }
20 |
21 | async function connectDb() {
22 | if (cached.conn) {
23 | return cached.conn;
24 | }
25 | if (!cached.promise) {
26 | const options = {
27 | bufferCommands: false,
28 | useNewUrlParser: true,
29 | useUnifiedTopology: true,
30 | };
31 | cached.promise = mongoose
32 | .connect(DATABASE_URL, options)
33 | .then((mongoose) => {
34 | console.log("Connection has been established");
35 | return mongoose;
36 | })
37 | .catch((error) => {
38 | console.log(error as Error);
39 | });
40 | }
41 | cached.conn = await cached.promise;
42 | console.log(cached.conn);
43 | return cached.conn;
44 | }
45 |
46 |
47 |
48 | export default connectDb;
49 |
--------------------------------------------------------------------------------
/src/pages/api/auth/activate.ts:
--------------------------------------------------------------------------------
1 | import connectDb from "../../../../utils/connectDb";
2 | import User from "../../../../models/User";
3 | import jwt from "jsonwebtoken";
4 | import type { NextApiRequest, NextApiResponse } from "next";
5 | const { ACTIVATION_TOKEN_SECRET } = process.env;
6 | interface UserToken {
7 | id: string;
8 | }
9 | export default async function handler(
10 | req: NextApiRequest,
11 | res: NextApiResponse
12 | ) {
13 | try {
14 | await connectDb();
15 | const { token } = req.body;
16 | const userToken = jwt.verify(token, ACTIVATION_TOKEN_SECRET!) as UserToken;
17 | const userDb = await User.findById(userToken.id);
18 | if (userDb) {
19 | return res
20 | .status(400)
21 | .json({ message: "This account no longer exists." });
22 | }
23 | if (userDb.emailVerified == true) {
24 | return res
25 | .status(400)
26 | .json({ message: "Email address alredy verified." });
27 | }
28 | await User.findByIdAndUpdate(userDb.id, { emailVerified: true });
29 |
30 | res.json({
31 | message: "Your account has been successfully verified.",
32 | });
33 | } catch (error) {
34 | res.status(500).json({ message: (error as Error).message });
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/src/pages/api/auth/forgot.ts:
--------------------------------------------------------------------------------
1 | import { resetPasswordEmail } from "@/emailTemplates/reset";
2 | import UserModal from "../../../../models/User";
3 | import connectDb from "../../../../utils/connectDb";
4 | import sendMail from "../../../../utils/sendMail";
5 | import { createResetToken } from "../../../../utils/tokens";
6 | import type { NextApiRequest, NextApiResponse } from "next";
7 |
8 | export default async function handler(
9 | req: NextApiRequest,
10 | res: NextApiResponse
11 | ) {
12 | try {
13 | await connectDb();
14 | const { email } = req.body;
15 | const user = await UserModal.findOne({ email });
16 | if (!user) {
17 | return res.status(400).json({ message: "This email does not exist." });
18 | }
19 | const user_id = createResetToken({
20 | id: user._id.toString(),
21 | });
22 | const url = `${process.env.NEXTAUTH_URL}/reset/${user_id}`;
23 | await sendMail(
24 | email,
25 | user.name,
26 | user.image,
27 | url,
28 | "Reset your password - Mehranlip",
29 | resetPasswordEmail
30 | );
31 | res.json({
32 | message: "An email has been sent to you, use it to reset your password.",
33 | });
34 | } catch (error) {
35 | res.status(500).json({ message: (error as Error).message });
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/public/next.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/lib/mongodb.ts:
--------------------------------------------------------------------------------
1 | // This approach is taken from https://github.com/vercel/next.js/tree/canary/examples/with-mongodb
2 | import { MongoClient } from "mongodb";
3 |
4 | if (!process.env.DATABASE_URL) {
5 | throw new Error("Please add your Mongo URI to .env.local");
6 | }
7 |
8 | const uri: string = process.env.DATABASE_URL;
9 | let client: MongoClient;
10 | let clientPromise: Promise;
11 |
12 | if (process.env.NODE_ENV === "development") {
13 | // In development mode, use a global variable so that the value
14 | // is preserved across module reloads caused by HMR (Hot Module Replacement).
15 |
16 | let globalWithMongoClientPromise = global as typeof globalThis & {
17 | _mongoClientPromise: Promise;
18 | };
19 |
20 | if (!globalWithMongoClientPromise._mongoClientPromise) {
21 | client = new MongoClient(uri);
22 | globalWithMongoClientPromise._mongoClientPromise = client.connect();
23 | }
24 | clientPromise = globalWithMongoClientPromise._mongoClientPromise;
25 | } else {
26 | // In production mode, it's best to not use a global variable.
27 | client = new MongoClient(uri);
28 | clientPromise = client.connect();
29 | }
30 |
31 | // Export a module-scoped MongoClient promise. By doing this in a
32 | // separate module, the client can be shared across functions.
33 | export default clientPromise;
34 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "epicurus-next-auth",
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/mongodb-adapter": "^1.0.0",
13 | "@hookform/resolvers": "^2.9.11",
14 | "@next-auth/mongodb-adapter": "^1.1.3",
15 | "@next/font": "13.1.5",
16 | "@types/bcryptjs": "^2.4.2",
17 | "@types/jsonwebtoken": "^9.0.1",
18 | "@types/node": "18.11.18",
19 | "@types/nodemailer": "^6.4.7",
20 | "@types/react": "18.0.27",
21 | "@types/react-dom": "18.0.10",
22 | "@types/validator": "^13.7.17",
23 | "axios": "^1.3.0",
24 | "bcryptjs": "^2.4.3",
25 | "eslint": "8.32.0",
26 | "eslint-config-next": "13.1.5",
27 | "handlebars": "^4.7.7",
28 | "jsonwebtoken": "^9.0.0",
29 | "mongodb": "^4.16.0",
30 | "mongoose": "^7.3.1",
31 | "next": "13.1.5",
32 | "next-auth": "^4.18.10",
33 | "nodemailer": "^6.9.3",
34 | "react": "18.2.0",
35 | "react-dom": "18.2.0",
36 | "react-hook-form": "^7.45.1",
37 | "react-icons": "^4.10.1",
38 | "react-spinners": "^0.13.8",
39 | "react-toastify": "^9.1.1",
40 | "typescript": "4.9.4",
41 | "validator": "^13.9.0",
42 | "zod": "^3.21.4",
43 | "zxcvbn": "^4.4.2"
44 | },
45 | "devDependencies": {
46 | "@types/zxcvbn": "^4.4.1",
47 | "autoprefixer": "^10.4.13",
48 | "postcss": "^8.4.21",
49 | "tailwindcss": "^3.2.4"
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/src/components/buttons/SocialButton.tsx:
--------------------------------------------------------------------------------
1 | import { signIn } from "next-auth/react";
2 | import * as React from "react";
3 | import { FaDiscord, FaGithub } from "react-icons/fa";
4 | import { SiAuth0 } from "react-icons/si";
5 |
6 | interface ISocialButtonProps {
7 | id: string;
8 | text: string;
9 | csrfToken: string;
10 | }
11 |
12 | const colors: any = {
13 | auth0: "#eb5424",
14 | github: "#333",
15 | discord: "#7289da",
16 | };
17 | const SocialButton: React.FunctionComponent = (props) => {
18 | const { id, text, csrfToken } = props;
19 | const createIconJsx = () => {
20 | switch (id) {
21 | case "auth0":
22 | return ;
23 | case "github":
24 | return ;
25 | case "discord":
26 | return ;
27 | default:
28 | return;
29 | }
30 | };
31 |
32 | return (
33 |
45 | );
46 | };
47 |
48 | export default SocialButton;
49 |
--------------------------------------------------------------------------------
/src/components/buttons/SlideButton.tsx:
--------------------------------------------------------------------------------
1 | import { BeatLoader } from "react-spinners";
2 |
3 | interface ISlideButtonProps {
4 | type?: "submit" | "reset" | "button";
5 | text: string;
6 | slide_text: string;
7 | icon: JSX.Element;
8 | disabled: boolean;
9 | }
10 |
11 | const SlideButton: React.FunctionComponent = (props) => {
12 | const { type, text, slide_text, icon, disabled } = props;
13 | return (
14 |
33 | );
34 | };
35 |
36 | export default SlideButton;
37 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | ## Getting Started
5 |
6 | First, run the development server:
7 |
8 | ```bash
9 | npm run dev
10 | # or
11 | yarn dev
12 | ```
13 |
14 |
15 |
16 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
17 |
18 | You can start editing the page by modifying `pages/index.js`. The page auto-updates as you edit the file.
19 |
20 | [API routes](https://nextjs.org/docs/api-routes/introduction) can be accessed on [http://localhost:3000/api/hello](http://localhost:3000/api/hello). This endpoint can be edited in `pages/api/hello.js`.
21 |
22 | The `pages/api` directory is mapped to `/api/*`. Files in this directory are treated as [API routes](https://nextjs.org/docs/api-routes/introduction) instead of React pages.
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 |
--------------------------------------------------------------------------------
/src/styles/globals.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | /* home page style */
6 | .home {
7 | background-image: url("../../public/home.png");
8 | width: 100%;
9 | height: 100%;
10 | background-repeat: no-repeat;
11 | background-size: cover;
12 | }
13 |
14 | .box-home {
15 | border-radius: 40px;
16 | background: rgba(196, 196, 196, 0.068);
17 | backdrop-filter: blur(12.5px);
18 | border: solid 1px rgb(116, 114, 114);
19 | }
20 |
21 | .name-user {
22 | color: #fff;
23 | font-size: 25px;
24 | font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
25 | font-style: normal;
26 | font-weight: 700;
27 | line-height: normal;
28 | }
29 |
30 | .email-user {
31 | font-size: 15px;
32 | font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
33 | font-style: normal;
34 | line-height: normal;
35 | }
36 |
37 | .signout {
38 | font-size: 18px;
39 | font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
40 | font-style: normal;
41 | line-height: normal;
42 | }
43 |
44 | /* Auth page style */
45 |
46 | .auth {
47 | background-image: url("../../public/auth.png");
48 | background-repeat: no-repeat;
49 | background-size: cover;
50 | }
51 |
52 | .login-text {
53 | color: #fff;
54 | font-size: 20px;
55 | font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
56 | font-style: normal;
57 | font-weight: 700;
58 | line-height: normal;
59 | }
60 |
61 | .input-com {
62 | border-radius: 10px;
63 | border: 1px solid #bcbec0;
64 | background: #fff;
65 | }
66 |
67 | .input-com::placeholder {
68 | color: #bcbec0;
69 | font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
70 | font-size: 14px;
71 | font-style: normal;
72 | font-weight: 400;
73 | line-height: normal;
74 | }
--------------------------------------------------------------------------------
/utils/sendMail.ts:
--------------------------------------------------------------------------------
1 | import nodemailer from "nodemailer";
2 | import * as handlebars from "handlebars";
3 |
4 | export default async function sendMail(
5 | to: string,
6 | name: string,
7 | image: string,
8 | url: string,
9 | subject: string,
10 | template: string
11 | ) {
12 | const {
13 | MAILING_EMAIL,
14 | MAILING_PASSWORD,
15 | SMTP_HOST,
16 | SMTP_EMAIL,
17 | SMTP_PASSWORD,
18 | SMTP_PORT,
19 | } = process.env;
20 |
21 | let transporter = await nodemailer.createTransport({
22 | /*
23 | port: Number(SMTP_PORT),
24 | host: SMTP_HOST,
25 | auth: {
26 | user: SMTP_EMAIL,
27 | pass: SMTP_PASSWORD,
28 | },
29 | */
30 | service: "gmail",
31 | auth: {
32 | user: MAILING_EMAIL,
33 | pass: MAILING_PASSWORD,
34 | },
35 | });
36 | //-----Html replacment
37 | const data = handlebars.compile(template);
38 | const replacments = {
39 | name: name,
40 | email_link: url,
41 | image: image,
42 | };
43 | const html = data(replacments);
44 | //------------verify connection config
45 | await new Promise((resolve, reject) => {
46 | transporter.verify((error, success) => {
47 | if (error) {
48 | console.log(error);
49 | reject(error);
50 | } else {
51 | console.log("server is listening...");
52 | resolve(success);
53 | }
54 | });
55 | });
56 | //---------send email
57 | const options = {
58 | from: MAILING_EMAIL,
59 | to,
60 | subject,
61 | html,
62 | };
63 | await new Promise((resolve, reject) => {
64 | transporter.sendMail(options, (err, info) => {
65 | if (err) {
66 | console.error(err);
67 | reject(err);
68 | } else {
69 | console.log(info);
70 | resolve(info);
71 | }
72 | });
73 | });
74 | }
75 |
--------------------------------------------------------------------------------
/src/components/inputs/input.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import { useState } from "react";
3 |
4 | import { IoAlertCircle } from "react-icons/io5";
5 | import { ImEyeBlocked, ImEye } from "react-icons/im";
6 |
7 | interface InputProps {
8 | name: string;
9 | label: string;
10 | type: string;
11 | icon: JSX.Element;
12 | placeholder: string;
13 | register: any;
14 | error: any;
15 | disabled: boolean;
16 | }
17 |
18 | const Input: React.FunctionComponent = (props) => {
19 | const { name, label, type, icon, placeholder, register, error, disabled } =
20 | props;
21 | const [showPassword, setShowPassword] = useState(false);
22 | return (
23 |
24 |
27 |
28 |
32 | {icon}
33 |
34 |
43 | {/* {show and hide password} */}
44 | {(name == "password" || name == "confirmPassword") && (
45 |
setShowPassword((prev) => !prev)}
49 | >
50 | {showPassword ? : }
51 |
52 | )}
53 | {error && (
54 |
55 |
56 |
57 | )}
58 | {error && (
59 |
60 | {error}
61 |
62 | )}
63 |
64 |
65 | );
66 | };
67 |
68 | export default Input;
69 |
--------------------------------------------------------------------------------
/src/pages/activate/[token].tsx:
--------------------------------------------------------------------------------
1 | import axios from "axios";
2 | import { NextPageContext } from "next";
3 | import { signIn } from "next-auth/react";
4 | import { useEffect, useState } from "react";
5 |
6 | export default function Activate({ token }: { token: string }) {
7 | const [error, setError] = useState("");
8 | const [success, setSuccess] = useState("");
9 | useEffect(() => {
10 | activateAccount();
11 | }, [token]);
12 | const activateAccount = async () => {
13 | try {
14 | const { data } = await axios.put("/api/auth/activate", { token });
15 | setSuccess(data.message);
16 | } catch (error: any) {
17 | setError((error?.response?.data as Error).message);
18 | }
19 | };
20 | return (
21 |
22 | {error && (
23 |
24 |
{error}
25 |
31 |
32 | )}
33 | {success && (
34 |
35 |
{success}
36 |
42 |
43 | )}
44 |
45 | );
46 | }
47 |
48 | export async function getServerSideProps(ctx: NextPageContext) {
49 | const { query } = ctx;
50 | const token = query.token;
51 | return {
52 | props: { token },
53 | };
54 | }
55 |
--------------------------------------------------------------------------------
/src/pages/api/auth/signup.ts:
--------------------------------------------------------------------------------
1 | import type { NextApiRequest, NextApiResponse } from "next";
2 | import connectDb from "../../../../utils/connectDb";
3 | import validator from "validator";
4 | import User from "../../../../models/User";
5 | import bcrypt from "bcryptjs";
6 | import { createActivationToken } from "../../../../utils/tokens";
7 | import sendMail from "../../../../utils/sendMail";
8 | import { activateTemplateEmail } from "@/emailTemplates/activate";
9 |
10 | export default async function handler(
11 | req: NextApiRequest,
12 | res: NextApiResponse
13 | ) {
14 | try {
15 | await connectDb();
16 | const { first_name, last_name, email, phone, password } = req.body;
17 | if (!first_name || !last_name || !email || !phone || !password) {
18 | return res.status(400).json({ message: "Please fill in all fields." });
19 | }
20 | if (!validator.isEmail(email)) {
21 | return res
22 | .status(400)
23 | .json({ message: "Please Add a valid email address." });
24 | }
25 | if (!validator.isMobilePhone(phone)) {
26 | return res
27 | .status(400)
28 | .json({ message: "Please Add a valid phone number." });
29 | }
30 | const user = await User.findOne({
31 | email: email,
32 | });
33 | if (user) {
34 | return res
35 | .status(400)
36 | .json({ message: "This email address already exists." });
37 | }
38 | if (password.length < 6) {
39 | return res
40 | .status(400)
41 | .json({ message: "Password must be atleast 6 characters." });
42 | }
43 | const cryptedPassword = await bcrypt.hash(password, 12);
44 | const newuser = new User({
45 | name: `${first_name + " " + last_name}`,
46 | email,
47 | phone,
48 | password: cryptedPassword,
49 | });
50 | await newuser.save();
51 | const activation_token = createActivationToken({
52 | id: newuser._id.toString(),
53 | });
54 | const url = `${process.env.NEXTAUTH_URL}/activate/${activation_token}`;
55 | await sendMail(
56 | newuser.email,
57 | newuser.name,
58 | "",
59 | url,
60 | "Activate your account - Mojave Next Auth - Mehranlip",
61 | activateTemplateEmail
62 | );
63 | res.json({
64 | message: "Register success! Please activate your account to start.",
65 | });
66 | } catch (error) {
67 | res.status(500).json({ message: (error as Error).message });
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/src/components/forms/Forgot.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import Input from "../inputs/input";
3 | import { FiLock, FiMail } from "react-icons/fi";
4 | import { SubmitHandler, useForm } from "react-hook-form";
5 |
6 | import { z } from "zod";
7 | import { zodResolver } from "@hookform/resolvers/zod";
8 | import SlideButton from "../buttons/SlideButton";
9 | import { useRouter } from "next/router";
10 | import { toast } from "react-toastify";
11 | import { signIn } from "next-auth/react";
12 | import Link from "next/link";
13 | import axios from "axios";
14 |
15 | interface IForgotFormProps {}
16 | const FormSchema = z.object({
17 | email: z.string().email("Please enter a valid email adress."),
18 | });
19 |
20 | type FormSchemaType = z.infer;
21 |
22 | const ForgotForm: React.FunctionComponent = (props) => {
23 | const {
24 | register,
25 | handleSubmit,
26 | formState: { errors, isSubmitting },
27 | } = useForm({
28 | resolver: zodResolver(FormSchema),
29 | });
30 |
31 | const onSubmit: SubmitHandler = async (values) => {
32 | try {
33 | const { data } = await axios.post("/api/auth/forgot", {
34 | email: values.email,
35 | });
36 | toast.success(data.message);
37 | } catch (error: any) {
38 | toast.error(error.response.data.message);
39 | }
40 | };
41 |
42 | return (
43 |
44 |
Forgot password
45 |
46 | Sign in instead
47 |
51 | Sing in
52 |
53 |
54 |
77 |
78 | );
79 | };
80 |
81 | export default ForgotForm;
82 |
--------------------------------------------------------------------------------
/src/pages/auth.tsx:
--------------------------------------------------------------------------------
1 | import SocialButton from "@/components/buttons/SocialButton";
2 | import Loginform from "@/components/forms/Login";
3 | import RegisterForm from "@/components/forms/Register";
4 | import { NextPageContext } from "next";
5 | import { getCsrfToken, getProviders } from "next-auth/react";
6 |
7 | export default function auth({
8 | tab,
9 | callbackUrl,
10 | csrfToken,
11 | providers,
12 | }: {
13 | tab: string;
14 | callbackUrl: string;
15 | csrfToken: string;
16 | providers: any;
17 | }) {
18 | return (
19 |
20 |
21 |
22 |
23 | {/* SIGN UP FORM */}
24 | {tab == "signin" ? (
25 |
26 | ) : (
27 |
28 | )}
29 |
34 | {/* coustom providers */}
35 |
36 | {providers.map((provider: any) => {
37 | if (provider.name == "Credentials") return;
38 | return (
39 |
47 | );
48 | })}
49 |
50 |
51 |
52 |
53 |
54 | );
55 | }
56 |
57 | export async function getServerSideProps(ctx: NextPageContext) {
58 | const { req, query } = ctx;
59 | const tab = query.tab ? query.tab : "signin";
60 | const callbackUrl = query.callbackUrl
61 | ? query.callbackUrl
62 | : process.env.NEXTAUTH_URL;
63 |
64 | const csrfToken = await getCsrfToken(ctx);
65 | const providers = await getProviders();
66 | return {
67 | props: {
68 | providers: Object.values(providers!),
69 | tab: JSON.parse(JSON.stringify(tab)),
70 | callbackUrl,
71 | csrfToken,
72 | },
73 | };
74 | }
75 |
--------------------------------------------------------------------------------
/src/pages/api/auth/[...nextauth].ts:
--------------------------------------------------------------------------------
1 | import NextAuth, { Account, Profile, User } from "next-auth";
2 | import GitHubProvider from "next-auth/providers/github";
3 | import DiscordProvider from "next-auth/providers/discord";
4 | import CredentialsProvider from "next-auth/providers/credentials";
5 |
6 | import { MongoDBAdapter } from "@next-auth/mongodb-adapter";
7 | import clientPromise from "@/lib/mongodb";
8 | import { JWT } from "next-auth/jwt";
9 | import { Adapter, AdapterUser } from "next-auth/adapters";
10 | import connectDb from "../../../../utils/connectDb";
11 | import UserModal from "../../../../models/User";
12 | import bcrypt from "bcryptjs";
13 | export default NextAuth({
14 | adapter: MongoDBAdapter(clientPromise),
15 | providers: [
16 | CredentialsProvider({
17 | name: "Credentials",
18 | credentials: {
19 | email: {
20 | label: "Name",
21 | type: "text",
22 | },
23 | password: {
24 | label: "Password",
25 | type: "password",
26 | },
27 | },
28 | async authorize(credentials) {
29 | await connectDb();
30 | const user = await UserModal.findOne({ email: credentials!.email });
31 | if (!user) {
32 | throw new Error("Email is not registered.");
33 | }
34 | const isPasswordCorrect = await bcrypt.compare(
35 | credentials!.password,
36 | user.password
37 | );
38 | if (!isPasswordCorrect) {
39 | throw new Error("Password is incorrect.");
40 | }
41 | return user;
42 | },
43 | }),
44 | GitHubProvider({
45 | clientId: process.env.GITHUB_ID as string,
46 | clientSecret: process.env.GITHUB_SECRET as string,
47 | }),
48 | DiscordProvider({
49 | clientId: process.env.DISCORD_CLIENT_ID as string,
50 | clientSecret: process.env.DISCORD_CLIENT_SECRET as string,
51 | }),
52 | ],
53 | secret: process.env.NEXTAUTH_SECRET,
54 | session: {
55 | strategy: "jwt",
56 | },
57 | pages: {
58 | signIn: "/auth",
59 | },
60 | callbacks: {
61 | async jwt({
62 | token,
63 | user,
64 | account,
65 | profile,
66 | isNewUser,
67 | }: {
68 | token: JWT;
69 | user?: User | Adapter | undefined;
70 | account?: Account | null | undefined;
71 | profile?: Profile | undefined;
72 | isNewUser?: boolean | undefined;
73 | }) {
74 | if (user) {
75 | token.provider = account?.provider;
76 | }
77 | return token;
78 | },
79 | async session({ session, token }: { session: any; token: JWT }) {
80 | if (session.user) {
81 | session.user.provider = token.provider;
82 | }
83 | return session;
84 | },
85 | },
86 | });
87 |
--------------------------------------------------------------------------------
/src/pages/index.tsx:
--------------------------------------------------------------------------------
1 | import { NextPageContext } from "next";
2 | import { useSession, signIn, signOut, getSession } from "next-auth/react";
3 |
4 | export default function Home() {
5 | const { data: session } = useSession();
6 | console.log(session);
7 |
8 | return (
9 |
10 |
11 |
12 | {session ? (
13 |
14 |

18 |
{session?.user?.name}
19 |
20 | {session?.user?.email!}
21 |
22 |
23 | Provider :{" "}
24 |
25 | {session?.user?.provider}
26 |
27 |
28 |
34 |
35 | ) : (
36 |
37 |
43 |
44 | )}
45 |
46 |
47 |
48 | );
49 | }
50 |
51 | export async function getServerSideProps(ctx: NextPageContext) {
52 | const session = await getSession(ctx);
53 | console.log(session);
54 |
55 | return {
56 | props: {
57 | session,
58 | },
59 | };
60 | }
61 |
--------------------------------------------------------------------------------
/src/components/forms/Login.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import Input from "../inputs/input";
3 | import { FiLock, FiMail } from "react-icons/fi";
4 | import { SubmitHandler, useForm } from "react-hook-form";
5 |
6 | import { z } from "zod";
7 | import { zodResolver } from "@hookform/resolvers/zod";
8 | import SlideButton from "../buttons/SlideButton";
9 | import { useRouter } from "next/router";
10 | import { toast } from "react-toastify";
11 | import { signIn } from "next-auth/react";
12 | import Link from "next/link";
13 |
14 | interface ILoginformProps {
15 | callbackUrl: string;
16 | csrfToken: string;
17 | }
18 | const FormSchema = z.object({
19 | email: z.string().email("Please enter a valid email adress."),
20 | password: z
21 | .string()
22 | .min(6, "Password must be atleast 6 characters.")
23 | .max(52, "Password must be less than 52 characters."),
24 | });
25 |
26 | type FormSchemaType = z.infer;
27 |
28 | const Loginform: React.FunctionComponent = (props) => {
29 | const { callbackUrl, csrfToken } = props;
30 | const router = useRouter();
31 | const path = router.pathname;
32 | const {
33 | register,
34 | handleSubmit,
35 | formState: { errors, isSubmitting },
36 | } = useForm({
37 | resolver: zodResolver(FormSchema),
38 | });
39 |
40 | const onSubmit: SubmitHandler = async (values) => {
41 | const res: any = await signIn("credentials", {
42 | redirect: false,
43 | email: values.email,
44 | password: values.password,
45 | callbackUrl,
46 | });
47 | if (res.error) {
48 | return toast.error(res.error);
49 | } else {
50 | return router.push("/");
51 | }
52 | };
53 |
54 | return (
55 |
116 | );
117 | };
118 |
119 | export default Loginform;
120 |
--------------------------------------------------------------------------------
/src/components/forms/Reset.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import Input from "../inputs/input";
3 | import { FiLock, FiMail } from "react-icons/fi";
4 | import { SubmitHandler, useForm } from "react-hook-form";
5 |
6 | import { useState, useEffect } from "react";
7 |
8 | import { z } from "zod";
9 | import { zodResolver } from "@hookform/resolvers/zod";
10 | import zxcvbn from "zxcvbn";
11 | import SlideButton from "../buttons/SlideButton";
12 | import { toast } from "react-toastify";
13 | import axios from "axios";
14 | import Link from "next/link";
15 |
16 | interface IResetFormProps {
17 | token: string;
18 | }
19 | const FormSchema = z
20 | .object({
21 | password: z
22 | .string()
23 | .min(6, "Password must be atleast 6 characters.")
24 | .max(52, "Password must be less than 52 characters."),
25 | confirmPassword: z.string(),
26 | })
27 | .refine((data) => data.password === data.confirmPassword, {
28 | message: "Password doesn't match",
29 | path: ["confirmPassword"],
30 | });
31 |
32 | type FormSchemaType = z.infer;
33 |
34 | const ResetForm: React.FunctionComponent = (props) => {
35 | const { token } = props;
36 | const [passwordScore, setPasswordScore] = useState(0);
37 | const {
38 | register,
39 | handleSubmit,
40 | reset,
41 | watch,
42 | formState: { errors, isSubmitting },
43 | } = useForm({
44 | resolver: zodResolver(FormSchema),
45 | });
46 |
47 | const onSubmit: SubmitHandler = async (values) => {
48 | try {
49 | const { data } = await axios.post("/api/auth/reset", {
50 | password: values.password,
51 | token,
52 | });
53 | reset();
54 | toast.success(data.message);
55 | } catch (error: any) {
56 | toast.error(error.response.data.message);
57 | }
58 | };
59 |
60 | const validatePasswordStrength = () => {
61 | let password = watch().password;
62 | return zxcvbn(password ? password : "").score;
63 | };
64 |
65 | useEffect(() => {
66 | setPasswordScore(validatePasswordStrength());
67 | }, [watch().password]);
68 |
69 | return (
70 |
71 |
Reset password
72 |
73 | Sign in instead ?
74 |
78 | Sing in
79 |
80 |
81 |
131 |
132 | );
133 | };
134 |
135 | export default ResetForm;
136 |
--------------------------------------------------------------------------------
/src/components/forms/Register.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import Input from "../inputs/input";
3 | import { CiUser } from "react-icons/ci";
4 | import { FiLock, FiMail } from "react-icons/fi";
5 | import { SubmitHandler, useForm } from "react-hook-form";
6 | import { BsTelephone } from "react-icons/bs";
7 |
8 | import { useState, useEffect } from "react";
9 |
10 | import { z } from "zod";
11 | import { zodResolver } from "@hookform/resolvers/zod";
12 | import validator from "validator";
13 | import zxcvbn from "zxcvbn";
14 | import SlideButton from "../buttons/SlideButton";
15 | import { toast } from "react-toastify";
16 | import axios from "axios";
17 | import Link from "next/link";
18 |
19 | interface IRegisterFromProps {}
20 | const FormSchema = z
21 | .object({
22 | first_name: z
23 | .string()
24 | .min(2, "First name atleast 2 characters")
25 | .max(32, "First name less than 32 characters")
26 | .regex(new RegExp("^[a-zA-z]+$"), "No special characters allowed."),
27 | last_name: z
28 | .string()
29 | .min(2, "Last name atleast 2 characters")
30 | .max(32, "Last name less than 32 characters")
31 | .regex(new RegExp("^[a-zA-z]+$"), "No special characters allowed."),
32 | email: z.string().email("Please enter a valid email adress."),
33 | phone: z.string().refine(validator.isMobilePhone, {
34 | message: "Please enter a valid phone number",
35 | }),
36 | password: z
37 | .string()
38 | .min(6, "Password must be atleast 6 characters.")
39 | .max(52, "Password must be less than 52 characters."),
40 | confirmPassword: z.string(),
41 | accept: z.literal(true, {
42 | errorMap: () => ({
43 | message:
44 | "Please agree to all the terms and conditions before continuing.",
45 | }),
46 | }),
47 | })
48 | .refine((data) => data.password === data.confirmPassword, {
49 | message: "Password doesn't match",
50 | path: ["confirmPassword"],
51 | });
52 |
53 | type FormSchemaType = z.infer;
54 | const RegisterForm: React.FunctionComponent = (props) => {
55 | const [passwordScore, setPasswordScore] = useState(0);
56 | const {
57 | register,
58 | handleSubmit,
59 | reset,
60 | watch,
61 | formState: { errors, isSubmitting },
62 | } = useForm({
63 | resolver: zodResolver(FormSchema),
64 | });
65 |
66 | const onSubmit: SubmitHandler = async (values) => {
67 | try {
68 | const { data } = await axios.post("/api/auth/signup", {
69 | ...values,
70 | });
71 | reset();
72 | toast.success(data.message);
73 | } catch (error: any) {
74 | toast.error(error.response.data.message);
75 | }
76 | };
77 |
78 | const validatePasswordStrength = () => {
79 | let password = watch().password;
80 | return zxcvbn(password ? password : "").score;
81 | };
82 |
83 | useEffect(() => {
84 | setPasswordScore(validatePasswordStrength());
85 | }, [watch().password]);
86 |
87 | return (
88 |
89 |
Sign up
90 |
91 | You already have an account ?
92 |
96 | Sing in
97 |
98 |
99 |
223 |
224 | );
225 | };
226 |
227 | export default RegisterForm;
228 |
--------------------------------------------------------------------------------
/src/emailTemplates/reset.ts:
--------------------------------------------------------------------------------
1 | export const resetPasswordEmail = `New message
2 |  |
3 |
|
| Welcome, {{name}}
4 |
You're receiving this message because you recently signed up for a account.
Confirm your email address by clicking the button below. This step adds extra security to your business by verifying you own this email. |
|
|
|
5 |
 |
9 |
|
| |
10 | |
|
|
|
12 | `;
13 |
--------------------------------------------------------------------------------
/src/emailTemplates/activate.ts:
--------------------------------------------------------------------------------
1 | export const activateTemplateEmail = `New message
2 |  |
3 |
|
| Welcome, {{name}}
4 |
You're receiving this message because you recently signed up for a account.
Confirm your email address by clicking the button below. This step adds extra security to your business by verifying you own this email. |
|
|
|
5 |
 |
9 |
|
| |
10 | |
|
|
|
12 | `;
13 |
--------------------------------------------------------------------------------