├── .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 |
34 | 35 | 44 |
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 |
58 | } 63 | placeholder="contact@email.ir" 64 | register={register} 65 | error={errors?.email?.message} 66 | disabled={isSubmitting} 67 | /> 68 | 69 | } 74 | disabled={isSubmitting} 75 | /> 76 | 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 |
30 |
31 | Or 32 |
33 |
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 |
56 |
Sign in
57 |

58 | You do not have an account ?   59 | { 62 | router.push({ 63 | pathname: path, 64 | query: { 65 | tab: "signup", 66 | }, 67 | }); 68 | }} 69 | > 70 | Sing up 71 | 72 |

73 |
79 | 80 | } 85 | placeholder="contact@email.ir" 86 | register={register} 87 | error={errors?.email?.message} 88 | disabled={isSubmitting} 89 | /> 90 | 91 | } 96 | placeholder="***********" 97 | register={register} 98 | error={errors?.password?.message} 99 | disabled={isSubmitting} 100 | /> 101 |
102 | 103 | Forgot password ? 104 | 105 |
106 | 107 | } 112 | disabled={isSubmitting} 113 | /> 114 | 115 |
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 |
85 | } 90 | placeholder="***********" 91 | register={register} 92 | error={errors?.password?.message} 93 | disabled={isSubmitting} 94 | /> 95 | {watch().password?.length > 0 && ( 96 |
97 | {Array.from(Array(5).keys()).map((span, i) => ( 98 | 99 |
108 |
109 | ))} 110 |
111 | )} 112 | } 117 | placeholder="***********" 118 | register={register} 119 | error={errors?.confirmPassword?.message} 120 | disabled={isSubmitting} 121 | /> 122 | 123 | } 128 | disabled={isSubmitting} 129 | /> 130 | 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 |
103 |
104 | } 109 | placeholder="Mehran" 110 | register={register} 111 | error={errors?.first_name?.message} 112 | disabled={isSubmitting} 113 | /> 114 | } 119 | placeholder="Asadi" 120 | register={register} 121 | error={errors?.last_name?.message} 122 | disabled={isSubmitting} 123 | /> 124 |
125 | } 130 | placeholder="contact@email.ir" 131 | register={register} 132 | error={errors?.email?.message} 133 | disabled={isSubmitting} 134 | /> 135 | } 140 | placeholder="+(xxx) xxx-xx-xx" 141 | register={register} 142 | error={errors?.phone?.message} 143 | disabled={isSubmitting} 144 | /> 145 | } 150 | placeholder="***********" 151 | register={register} 152 | error={errors?.password?.message} 153 | disabled={isSubmitting} 154 | /> 155 | {watch().password?.length > 0 && ( 156 |
157 | {Array.from(Array(5).keys()).map((span, i) => ( 158 | 159 |
168 |
169 | ))} 170 |
171 | )} 172 | } 177 | placeholder="***********" 178 | register={register} 179 | error={errors?.confirmPassword?.message} 180 | disabled={isSubmitting} 181 | /> 182 |
183 | 189 | 207 |
208 |
209 | {errors.accept && ( 210 |

211 | {errors?.accept?.message} 212 |

213 | )} 214 |
215 | } 220 | disabled={isSubmitting} 221 | /> 222 | 223 |
224 | ); 225 | }; 226 | 227 | export default RegisterForm; 228 | -------------------------------------------------------------------------------- /src/emailTemplates/reset.ts: -------------------------------------------------------------------------------- 1 | export const resetPasswordEmail = `New message 2 |
5 |
3 |
Confirm email

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.

8 |
7 |
6 |
Reset Your Password
10 |
9 |
Demo

This link expire in 24 hours. If you have questions, we're here to help

11 |
12 | `; 13 | -------------------------------------------------------------------------------- /src/emailTemplates/activate.ts: -------------------------------------------------------------------------------- 1 | export const activateTemplateEmail = `New message 2 |
5 |
3 |
Confirm email

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.

8 |
6 |
Confirm email
7 |
10 |
9 |
Demo

This link expire in 24 hours. If you have questions, we're here to help

11 |
12 | `; 13 | --------------------------------------------------------------------------------