├── .eslintrc.json
├── .gitignore
├── README.md
├── components.json
├── global.d.ts
├── next.config.mjs
├── package-lock.json
├── package.json
├── postcss.config.mjs
├── prisma
└── schema.prisma
├── public
├── next.svg
├── screenshots
│ ├── email_signin.png
│ ├── home.png
│ ├── intercepting_modal.png
│ ├── login.png
│ ├── signin.png
│ └── signup_modal.png
└── vercel.svg
├── src
├── app
│ ├── (protected-routes)
│ │ ├── dashboard
│ │ │ └── page.tsx
│ │ └── signout
│ │ │ └── page.tsx
│ ├── _fonts.ts
│ ├── _fonts
│ │ └── fonts.ts
│ ├── api
│ │ └── auth
│ │ │ └── [...nextAuth]
│ │ │ └── route.ts
│ ├── auth-error
│ │ └── page.tsx
│ ├── auth
│ │ ├── email_verify
│ │ │ └── page.tsx
│ │ ├── forgot-password
│ │ │ └── page.tsx
│ │ ├── layout.tsx
│ │ ├── new-password
│ │ │ └── page.tsx
│ │ ├── signin
│ │ │ └── page.tsx
│ │ ├── signup
│ │ │ └── page.tsx
│ │ └── verify-request
│ │ │ └── page.tsx
│ ├── favicon.ico
│ ├── globals.css
│ ├── home
│ │ ├── @auth
│ │ │ ├── (..)auth
│ │ │ │ ├── forgot-password
│ │ │ │ │ └── page.tsx
│ │ │ │ ├── layout.tsx
│ │ │ │ ├── signin
│ │ │ │ │ └── page.tsx
│ │ │ │ └── signup
│ │ │ │ │ └── page.tsx
│ │ │ └── default.tsx
│ │ ├── layout.tsx
│ │ └── page.tsx
│ └── layout.tsx
├── assets
│ ├── Helvetica-Bold.woff
│ ├── Helvetica-Oblique.woff
│ ├── Helvetica.woff
│ ├── PlaywriteCU-VariableFont_wght.ttf
│ └── auth-image-3d-cartoon.jpg
├── auth.ts
├── components
│ ├── Modal.tsx
│ ├── Spinner.tsx
│ ├── ThemeSwitch.tsx
│ └── ui
│ │ ├── button.tsx
│ │ ├── dialog.tsx
│ │ ├── dropdown-menu.tsx
│ │ ├── form.tsx
│ │ ├── input.tsx
│ │ ├── label.tsx
│ │ ├── sonner.tsx
│ │ ├── toast.tsx
│ │ ├── toaster.tsx
│ │ └── tooltip.tsx
├── constants.ts
├── hooks
│ ├── use-toast.ts
│ ├── useDebounce.tsx
│ ├── useFormSubmit.tsx
│ └── useUpdateQueryParams.tsx
├── lib
│ ├── db.ts
│ ├── fetch.ts
│ ├── utils.ts
│ └── validate-utils.ts
├── middleware.ts
├── modules
│ └── auth
│ │ ├── auth.config.ts
│ │ ├── auth.schema.ts
│ │ ├── auth.ts
│ │ ├── components
│ │ ├── AuthProvidersCTA.tsx
│ │ ├── CaptchaProvider.tsx
│ │ ├── EmailVerifyForm.tsx
│ │ ├── ForgotPassword.tsx
│ │ ├── FormFeedback.tsx
│ │ ├── MagicLinkSignin.tsx
│ │ ├── NewPasswordForm.tsx
│ │ ├── SignInForm.tsx
│ │ ├── SignOutButton.tsx
│ │ └── SignupForm.tsx
│ │ ├── data
│ │ ├── index.ts
│ │ ├── resetpassword-token.ts
│ │ ├── user.ts
│ │ └── verification-token.ts
│ │ ├── icons.tsx
│ │ ├── lib
│ │ ├── common.ts
│ │ ├── emailVerifyAction.ts
│ │ ├── forgot-password.ts
│ │ ├── index.ts
│ │ ├── recaptcha.ts
│ │ ├── signin-action.ts
│ │ ├── signin_magic-action.ts
│ │ └── signup-action.ts
│ │ ├── sendRequest.ts
│ │ ├── services
│ │ ├── mail.sender.ts
│ │ ├── template-service.ts
│ │ └── templates
│ │ │ ├── reset-password.html
│ │ │ └── verification.html
│ │ ├── types
│ │ ├── auth.d.ts
│ │ └── captcha.ts
│ │ └── useAuthProviders.tsx
├── providers
│ ├── ThemeProvider.tsx
│ └── index.tsx
├── routes.ts
└── types
│ └── types.ts
├── tailwind.config.ts
└── tsconfig.json
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "next/core-web-vitals"
3 | }
4 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 | .yarn/install-state.gz
8 |
9 | # testing
10 | /coverage
11 |
12 | # next.js
13 | /.next/
14 | /out/
15 |
16 | # production
17 | /build
18 |
19 | # misc
20 | .DS_Store
21 | *.pem
22 |
23 | # debug
24 | npm-debug.log*
25 | yarn-debug.log*
26 | yarn-error.log*
27 |
28 | # local env files
29 | .env*.local
30 | <<<<<<< HEAD
31 | .env*
32 | =======
33 | .env
34 | >>>>>>> 35a2b12 (updated readme)
35 | # vercel
36 | .vercel
37 |
38 | # typescript
39 | *.tsbuildinfo
40 | next-env.d.ts
41 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # NextAuth Starter
2 |
3 | A robust authentication solution for Next.js applications, leveraging NextAuth with custom enhancements like RBAC, multi-provider support, and email handling.
4 |
5 | ## Tools and Adapters Used
6 |
7 | - **Next.js**
8 | - **TypeScript**
9 | - **Auth.js (v5)**
10 | - **PostgreSQL**
11 | - **Prisma**
12 |
13 | ## Getting Started
14 |
15 | ### Installation
16 |
17 | ```bash
18 | git clone https://github.com/codersaadi/next-auth5-shadcn.git
19 | cd next-auth5-shadcn
20 | pnpm install
21 | ```
22 |
23 | ## Setup & Configuration
24 |
25 | Create a .env file in the root directory and add the following configuration:
26 |
27 | ```
28 | DB_URL="postgresql://dbuser:password@localhost:5432/dbname"
29 |
30 | AUTH_SECRET="your-secret"
31 |
32 | GITHUB_CLIENT_ID="your-client-id"
33 | GITHUB_CLIENT_SECRET="your-client-secret"
34 |
35 | GOOGLE_CLIENT_ID="your-client-id"
36 | GOOGLE_CLIENT_SECRET="your-client-secret"
37 |
38 | FACEBOOK_CLIENT_ID="your-client-id"
39 | FACEBOOK_CLIENT_SECRET="your-client-secret"
40 |
41 | GMAIL_SENDER_EMAIL="your-app-gmail"
42 | GMAIL_SENDER_PASSWORD="gmail-app-password"
43 |
44 | HOST="http://localhost:3000"
45 |
46 | NEXT_PUBLIC_RECAPTCHA_SITE_KEY : ""
47 | RECAPTCHA_SECRET : ""
48 | ```
49 |
50 | ## Features
51 |
52 | Credential-Based Authentication
53 | -Sign-In, Sign-Up, and Forgot Password functionality.
54 | Custom email templates for password recovery and account verification using Nodemailer.
55 | OAuth Providers
56 |
57 | - Google and Facebook authentication for seamless social logins.
58 | Role-Based Access Control (RBAC)
59 |
60 | - Define user roles and permissions with Prisma for secure access management.
61 | Google Captcha V3
62 | - useFormSubmit Hooks supports the google captcha v3 just pass captcha options , and use reCaptchaSiteVerify in your action.
63 | Database Integration
64 | - Built with Prisma and PostgreSQL for powerful and scalable database interactions.
65 | Schema Validation
66 | - Validate user inputs and responses using Zod.
67 | TypeScript Integration
68 | - Type-safe development with TypeScript, ensuring robust and maintainable code.
69 | Customizable UI
70 | - Tailor the UI components with Shadcn UI, allowing for easy styling adjustments.
71 | Contributions
72 | - Feel free to contribute—contributions are always appreciated!
73 |
74 | # ScreenShots
75 |
76 | ### Home Page
77 |
78 | 
79 |
80 | ### Login Page
81 |
82 | 
83 |
84 | ### Sign In Page
85 |
86 | 
87 |
88 | ### Email Sign In
89 |
90 | 
91 |
92 | ### Signup Modal
93 |
94 | 
95 |
96 | ### Intercepting Modal
97 |
98 | 
99 |
100 | ### Enhancements:
101 |
102 | - **Clearer Section Headers:** Sections are clearly separated for easy navigation.
103 | - **Enhanced Setup Instructions:** The environment setup is clearly outlined.
104 | - **Organized Screenshots:** The screenshots are presented in a clean and structured manner.
105 | - **Features Detailed:** Each feature is highlighted with bold titles for quick reference.
106 | - **Encouragement to Contribute:** The contributions section is friendly and welcoming.
107 |
--------------------------------------------------------------------------------
/components.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://ui.shadcn.com/schema.json",
3 | "style": "new-york",
4 | "rsc": true,
5 | "tsx": true,
6 | "tailwind": {
7 | "config": "tailwind.config.ts",
8 | "css": "src/app/globals.css",
9 | "baseColor": "neutral",
10 | "cssVariables": true,
11 | "prefix": ""
12 | },
13 | "aliases": {
14 | "components": "@/components",
15 | "utils": "@/lib/utils"
16 | }
17 | }
--------------------------------------------------------------------------------
/global.d.ts:
--------------------------------------------------------------------------------
1 | namespace NodeJS {
2 | interface ProcessEnv {
3 | GOOGLE_API_AI_KEY: string;
4 | GITHUB_CLIENT_ID : string;
5 | GITHUB_CLIENT_SECRET : string;
6 | GOOGLE_CLIENT_ID : string;
7 | GOOGLE_CLIENT_SECRET : string;
8 | DB_URL : string;
9 | AUTH_SECRET : string;
10 | GMAIL_SENDER_EMAIL : string;
11 | GMAIL_SENDER_PASSWORD : string;
12 | HOST : string;
13 | FACEBOOK_CLIENT_ID : string
14 | FACEBOOK_CLIENT_SECRET : string
15 | NEXT_PUBLIC_RECAPTCHA_SITE_KEY : string
16 | RECAPTCHA_SECRET : string
17 | }
18 | }
--------------------------------------------------------------------------------
/next.config.mjs:
--------------------------------------------------------------------------------
1 | /** @type {import('next').NextConfig} */
2 | const nextConfig = {
3 | images :{
4 | // Should be used according to your needs
5 | remotePatterns : [{"hostname" :"**", pathname:"**"}]
6 | }
7 | };
8 |
9 | export default nextConfig;
10 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "my-app",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "dev": "next dev",
7 | "build": "next build",
8 | "start": "next start",
9 | "lint": "next lint"
10 | },
11 | "dependencies": {
12 | "@auth/prisma-adapter": "^2.4.2",
13 | "@hookform/resolvers": "^3.9.0",
14 | "@prisma/client": "^5.19.0",
15 | "@radix-ui/react-dialog": "^1.1.1",
16 | "@radix-ui/react-dropdown-menu": "^2.1.1",
17 | "@radix-ui/react-icons": "^1.3.0",
18 | "@radix-ui/react-label": "^2.1.0",
19 | "@radix-ui/react-slot": "^1.1.0",
20 | "@radix-ui/react-toast": "^1.2.1",
21 | "@radix-ui/react-tooltip": "^1.1.2",
22 | "class-variance-authority": "^0.7.0",
23 | "clsx": "^2.1.1",
24 | "next": "14.2.7",
25 | "next-auth": "^5.0.0-beta.20",
26 | "next-themes": "^0.3.0",
27 | "nodemailer": "^6.9.14",
28 | "react": "^18",
29 | "react-dom": "^18",
30 | "react-google-recaptcha-v3": "^1.10.1",
31 | "react-hook-form": "^7.53.0",
32 | "shadcn": "^1.0.0",
33 | "sonner": "^1.5.0",
34 | "tailwind-merge": "^2.5.2",
35 | "tailwindcss-animate": "^1.0.7",
36 | "zod": "^3.23.8"
37 | },
38 | "devDependencies": {
39 | "@types/bcryptjs": "^2.4.6",
40 | "@types/node": "^20",
41 | "@types/nodemailer": "^6.4.15",
42 | "@types/react": "^18",
43 | "@types/react-dom": "^18",
44 | "@types/uuid": "^10.0.0",
45 | "bcryptjs": "^2.4.3",
46 | "eslint": "^8",
47 | "eslint-config-next": "14.2.7",
48 | "postcss": "^8",
49 | "prisma": "^5.19.0",
50 | "tailwindcss": "^3.4.1",
51 | "typescript": "^5",
52 | "uuid": "^10.0.0"
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/postcss.config.mjs:
--------------------------------------------------------------------------------
1 | /** @type {import('postcss-load-config').Config} */
2 | const config = {
3 | plugins: {
4 | tailwindcss: {},
5 | },
6 | };
7 |
8 | export default config;
9 |
--------------------------------------------------------------------------------
/prisma/schema.prisma:
--------------------------------------------------------------------------------
1 | // Prisma schema file
2 |
3 | datasource db {
4 | provider = "postgresql"
5 | url = env("DB_URL")
6 | }
7 |
8 | generator client {
9 | provider = "prisma-client-js"
10 | // previewFeatures = ["driverAdapters"]
11 | }
12 |
13 | enum UserRole {
14 | ADMIN
15 | USER
16 | SUPPORT
17 | }
18 |
19 | model User {
20 | id String @id @default(cuid())
21 | name String?
22 | email String @unique
23 | emailVerified DateTime?
24 | password String?
25 | image String?
26 | role UserRole @default(USER)
27 | accounts Account[]
28 | sessions Session[]
29 | createdAt DateTime @default(now())
30 | updatedAt DateTime @updatedAt
31 | }
32 |
33 |
34 | model Account {
35 | userId String
36 | type String
37 | provider String
38 | providerAccountId String
39 | refresh_token String?
40 | access_token String?
41 | expires_at Int?
42 | token_type String?
43 | scope String?
44 | id_token String?
45 | session_state String?
46 | createdAt DateTime @default(now())
47 | updatedAt DateTime @updatedAt
48 | user User @relation(fields: [userId], references: [id], onDelete: Cascade)
49 | @@id([provider, providerAccountId])
50 | }
51 |
52 | model Session {
53 | sessionToken String @unique
54 | userId String
55 | expires DateTime
56 | user User @relation(fields: [userId], references: [id], onDelete: Cascade)
57 | createdAt DateTime @default(now())
58 | updatedAt DateTime @updatedAt
59 | }
60 |
61 | model VerificationToken {
62 | identifier String
63 | token String
64 | expires DateTime
65 | @@id([identifier, token])
66 | }
67 |
68 | model ResetPasswordToken {
69 | id String @id @default(cuid())
70 | email String
71 | token String @unique
72 | expires DateTime
73 |
74 | @@unique([email, token])
75 | }
76 |
--------------------------------------------------------------------------------
/public/next.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/screenshots/email_signin.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codersaadi/next-auth5-shadcn/bec70b4a330b4212f774b5f7c7ef68f02f43b10e/public/screenshots/email_signin.png
--------------------------------------------------------------------------------
/public/screenshots/home.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codersaadi/next-auth5-shadcn/bec70b4a330b4212f774b5f7c7ef68f02f43b10e/public/screenshots/home.png
--------------------------------------------------------------------------------
/public/screenshots/intercepting_modal.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codersaadi/next-auth5-shadcn/bec70b4a330b4212f774b5f7c7ef68f02f43b10e/public/screenshots/intercepting_modal.png
--------------------------------------------------------------------------------
/public/screenshots/login.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codersaadi/next-auth5-shadcn/bec70b4a330b4212f774b5f7c7ef68f02f43b10e/public/screenshots/login.png
--------------------------------------------------------------------------------
/public/screenshots/signin.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codersaadi/next-auth5-shadcn/bec70b4a330b4212f774b5f7c7ef68f02f43b10e/public/screenshots/signin.png
--------------------------------------------------------------------------------
/public/screenshots/signup_modal.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codersaadi/next-auth5-shadcn/bec70b4a330b4212f774b5f7c7ef68f02f43b10e/public/screenshots/signup_modal.png
--------------------------------------------------------------------------------
/public/vercel.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/app/(protected-routes)/dashboard/page.tsx:
--------------------------------------------------------------------------------
1 | import { auth } from '@/auth'
2 | import SignOutButton from '@/modules/auth/components/SignOutButton'
3 | import { AvatarIcon } from '@radix-ui/react-icons'
4 | import React from 'react'
5 |
6 | export default async function page() {
7 | const session = await auth()
8 |
9 | return (
10 | <>
11 |
12 | {
13 | JSON.stringify(session, null, 2)
14 | }
15 |
16 |
17 | >
18 | )
19 | }
20 |
21 |
--------------------------------------------------------------------------------
/src/app/(protected-routes)/signout/page.tsx:
--------------------------------------------------------------------------------
1 | import { signOut } from "@/auth";
2 |
3 | export default function SignOutPage() {
4 | return (
5 |
6 |
7 |
8 | Are you sure you want to sign out?
9 |
10 |
30 |
31 |
32 | );
33 | }
34 |
--------------------------------------------------------------------------------
/src/app/_fonts.ts:
--------------------------------------------------------------------------------
1 | import { Roboto_Serif } from "next/font/google";
2 | const inter = Roboto_Serif({ subsets: ["latin"] , variable : "--font-robo-serif"});
3 |
4 | const fonts = {
5 | inter,
6 | };
7 | export default fonts;
8 |
--------------------------------------------------------------------------------
/src/app/_fonts/fonts.ts:
--------------------------------------------------------------------------------
1 | import { Open_Sans, Roboto_Flex, } from "next/font/google";
2 | const inter = Open_Sans({ subsets: ["latin"] , variable : "--font-sans"});
3 | import localFont from 'next/font/local';
4 | const hvFont = localFont({
5 | src : [
6 | {
7 | path : "../../assets/Helvetica.woff",
8 | weight: '400',
9 | style: 'normal',
10 | },
11 | {
12 | path : "../../assets/Helvetica-Bold.woff",
13 | weight: '600',
14 | style: 'normal',
15 | },
16 |
17 | ]
18 | })
19 | const fonts = {
20 | inter,hvFont
21 | };
22 | export default fonts;
23 |
--------------------------------------------------------------------------------
/src/app/api/auth/[...nextAuth]/route.ts:
--------------------------------------------------------------------------------
1 | import { handlers } from "@/auth"
2 | export const { GET, POST } = handlers
--------------------------------------------------------------------------------
/src/app/auth-error/page.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 |
4 | enum Error {
5 | Configuration = "Configuration",
6 | AccessDenied = "AccessDenied",
7 | Verification = "Verification",
8 | Default = "Default",
9 | }
10 |
11 | const errorMap = {
12 | [Error.Configuration]: (
13 |
14 | There was a problem when trying to authenticate. Please contact us if this
15 | error persists. Unique error code:{" "}
16 |
17 | Configuration
18 |
19 |
20 | ),
21 | [Error.AccessDenied]: (
22 |
23 | Access was denied. If you believe this is an error, please contact support.
24 | Unique error code:{" "}
25 |
26 | AccessDenied
27 |
28 |
29 | ),
30 | [Error.Verification]: (
31 |
32 | The verification link has expired or was already used. Please request a new one.
33 | Unique error code:{" "}
34 |
35 | Verification
36 |
37 |
38 | ),
39 | [Error.Default]: (
40 |
41 | An unexpected error occurred. Please try again later or contact support.
42 | Unique error code:{" "}
43 |
44 | Default
45 |
46 |
47 | ),
48 | };
49 |
50 | export default function AuthErrorPage({searchParams} :{
51 | searchParams : any
52 | }) {
53 | const error = searchParams["error"] as Error;
54 |
55 | return (
56 |
57 |
58 |
59 | Oops! Something went wrong
60 |
61 |
62 | {errorMap[error] || errorMap[Error.Default]}
63 |
64 |
68 | Go to Homepage
69 |
70 |
71 |
72 | );
73 | }
74 |
--------------------------------------------------------------------------------
/src/app/auth/email_verify/page.tsx:
--------------------------------------------------------------------------------
1 | import EmailVerifyForm from '@/modules/auth/components/EmailVerifyForm'
2 | import React from 'react'
3 |
4 | interface EmailVerifyProps {
5 | searchParams : {
6 | token? : string
7 | }
8 | }
9 |
10 | export default function page({searchParams} : EmailVerifyProps) {
11 | const {token } = searchParams
12 |
13 | return (
14 |
15 | )
16 | }
17 |
--------------------------------------------------------------------------------
/src/app/auth/forgot-password/page.tsx:
--------------------------------------------------------------------------------
1 | import ForgotPasswordForm from "@/modules/auth/components/ForgotPassword";
2 | export default ForgotPasswordForm
--------------------------------------------------------------------------------
/src/app/auth/layout.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 | import CaptchaClientProvider from '@/modules/auth/components/CaptchaProvider';
3 | import React, { useState } from 'react';
4 | import authImage from '@/assets/auth-image-3d-cartoon.jpg'
5 | import Image from 'next/image';
6 | const AuthLayout = ({ children }: { children: React.ReactNode }) => {
7 | const [loading , setLoading] = useState(true)
8 | const layoutComponent = (
9 |
10 |
11 |
12 |
13 | Welcome Back to X-UI
14 |
15 |
Everything should have a good start.
16 |
17 |
18 | {children}
19 |
20 |
21 |
22 | setLoading(false)} quality={75} className={`w-full h-full object-cover ${loading && "blur"}`}/>
23 |
24 |
25 | )
26 | return (
27 |
28 | {layoutComponent}
29 |
30 | );
31 | };
32 |
33 | export default AuthLayout;
34 |
--------------------------------------------------------------------------------
/src/app/auth/new-password/page.tsx:
--------------------------------------------------------------------------------
1 | import NewPasswordForm from '@/modules/auth/components/NewPasswordForm'
2 | import React from 'react'
3 | interface ResetPasswordProps {
4 | searchParams : {
5 | token? : string
6 | }
7 | }
8 |
9 | export default function page({searchParams} : ResetPasswordProps) {
10 | const {token } = searchParams
11 |
12 | return (
13 |
14 | )
15 | }
16 |
--------------------------------------------------------------------------------
/src/app/auth/signin/page.tsx:
--------------------------------------------------------------------------------
1 | import { Metadata } from "next";
2 | import SignInForm from "@/modules/auth/components/SignInForm";
3 | export default SignInForm
4 |
5 | /**
6 | * Meta data for the signin form page
7 | */
8 | export const metadata: Metadata = {
9 | title: 'AppName - Signin to Continue ',
10 | description: '...',
11 | }
--------------------------------------------------------------------------------
/src/app/auth/signup/page.tsx:
--------------------------------------------------------------------------------
1 | import SignUpForm from "@/modules/auth/components/SignupForm";
2 | export default SignUpForm
3 |
4 | import { Metadata } from "next";
5 |
6 | /**
7 | * Meta data for the signin form page
8 | */
9 | export const metadata: Metadata= {
10 | title: 'AppName -Create an Account for free ',
11 | description: '...',
12 | }
--------------------------------------------------------------------------------
/src/app/auth/verify-request/page.tsx:
--------------------------------------------------------------------------------
1 | import { logoUrl } from "@/constants";
2 | import Image from "next/image";
3 | import Link from "next/link";
4 |
5 | const VerifyRequest = ({searchParams} :{
6 | searchParams : Record
7 | }) => {
8 | const email = searchParams?.email
9 |
10 | return (
11 |
12 |
13 |
14 |
19 |
20 | Check your email
21 |
22 |
23 | We have sent a sign-in link to your email address. {email && (
24 | {email}
25 | )}
26 |
27 |
28 |
29 |
30 |
31 |
34 | Back to Sign In
35 |
36 |
37 |
38 |
39 |
40 |
41 | );
42 | };
43 |
44 | export default VerifyRequest;
45 |
--------------------------------------------------------------------------------
/src/app/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codersaadi/next-auth5-shadcn/bec70b4a330b4212f774b5f7c7ef68f02f43b10e/src/app/favicon.ico
--------------------------------------------------------------------------------
/src/app/globals.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | @layer base {
6 | :root {
7 | --background: 0 0% 100%;
8 | --foreground: 0 0% 3.9%;
9 | --card: 0 0% 100%;
10 | --card-foreground: 0 0% 3.9%;
11 | --popover: 0 0% 100%;
12 | --popover-foreground: 0 0% 3.9%;
13 | --primary: 0 0% 9%;
14 | --primary-foreground: 0 0% 98%;
15 | --secondary: 0 0% 96.1%;
16 | --secondary-foreground: 0 0% 9%;
17 | --muted: 0 0% 96.1%;
18 | --muted-foreground: 0 0% 45.1%;
19 | --accent: 0 0% 96.1%;
20 | --accent-foreground: 0 0% 9%;
21 | --destructive: 0 84.2% 60.2%;
22 | --destructive-foreground: 0 0% 98%;
23 | --border: 0 0% 89.8%;
24 | --input: 0 0% 89.8%;
25 | --ring: 0 0% 3.9%;
26 | --radius: 0.5rem;
27 | --chart-1: 12 76% 61%;
28 | --chart-2: 173 58% 39%;
29 | --chart-3: 197 37% 24%;
30 | --chart-4: 43 74% 66%;
31 | --chart-5: 27 87% 67%;
32 | }
33 |
34 | .dark {
35 | --background: 0 0% 3.9%;
36 | --foreground: 0 0% 98%;
37 | --card: 0 0% 3.9%;
38 | --card-foreground: 0 0% 98%;
39 | --popover: 0 0% 3.9%;
40 | --popover-foreground: 0 0% 98%;
41 | --primary: 0 0% 98%;
42 | --primary-foreground: 0 0% 9%;
43 | --secondary: 0 0% 14.9%;
44 | --secondary-foreground: 0 0% 98%;
45 | --muted: 0 0% 14.9%;
46 | --muted-foreground: 0 0% 63.9%;
47 | --accent: 0 0% 14.9%;
48 | --accent-foreground: 0 0% 98%;
49 | --destructive: 0 62.8% 30.6%;
50 | --destructive-foreground: 0 0% 98%;
51 | --border: 0 0% 14.9%;
52 | --input: 0 0% 14.9%;
53 | --ring: 0 0% 83.1%;
54 | --chart-1: 220 70% 50%;
55 | --chart-2: 160 60% 45%;
56 | --chart-3: 30 80% 55%;
57 | --chart-4: 280 65% 60%;
58 | --chart-5: 340 75% 55%;
59 | }
60 | }
61 |
62 | @layer base {
63 | * {
64 | @apply border-border;
65 | }
66 | body {
67 | @apply bg-background text-foreground;
68 | }
69 | }
--------------------------------------------------------------------------------
/src/app/home/@auth/(..)auth/forgot-password/page.tsx:
--------------------------------------------------------------------------------
1 | import ForgotPasswordForm from "@/modules/auth/components/ForgotPassword";
2 |
3 | export default ForgotPasswordForm;
--------------------------------------------------------------------------------
/src/app/home/@auth/(..)auth/layout.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 | import Modal from '@/components/Modal'
3 | import CaptchaClientProvider from '@/modules/auth/components/CaptchaProvider'
4 | import React from 'react'
5 |
6 | export default function AuthModalLayout({children} :{
7 | children: React.ReactNode,
8 | }) {
9 |
10 | return (
11 |
12 |
13 | {children}
14 |
15 |
16 | )
17 | }
18 |
--------------------------------------------------------------------------------
/src/app/home/@auth/(..)auth/signin/page.tsx:
--------------------------------------------------------------------------------
1 | import SignInForm from "@/modules/auth/components/SignInForm";
2 |
3 | export default SignInForm
4 |
--------------------------------------------------------------------------------
/src/app/home/@auth/(..)auth/signup/page.tsx:
--------------------------------------------------------------------------------
1 | import SignUpForm from "@/modules/auth/components/SignupForm";
2 |
3 | export default SignUpForm
--------------------------------------------------------------------------------
/src/app/home/@auth/default.tsx:
--------------------------------------------------------------------------------
1 |
2 | export default function Default() {
3 | return null
4 | }
5 |
--------------------------------------------------------------------------------
/src/app/home/layout.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | export default function AppLayout({
4 | children,
5 | auth
6 | }: {
7 | children: React.ReactNode;
8 | auth: React.ReactNode;
9 | }) {
10 | return (
11 | <>
12 | {auth}
13 | {children}
14 | >
15 | )
16 | }
17 |
--------------------------------------------------------------------------------
/src/app/home/page.tsx:
--------------------------------------------------------------------------------
1 | import { Button } from "@/components/ui/button";
2 | import Image from "next/image";
3 | import Link from "next/link";
4 |
5 | export default function Home() {
6 | return (
7 |
8 |
9 |
10 | Get started by editing
11 | src/app/page.tsx
12 |
13 |
31 |
32 |
33 |
34 |
35 | Sign In
36 |
37 |
38 |
39 |
40 | Sign Up
41 |
42 |
43 |
44 |
45 |
53 |
54 |
55 | With
56 | Auth.js
57 |
58 |
59 |
60 |
129 |
130 | );
131 | }
132 |
--------------------------------------------------------------------------------
/src/app/layout.tsx:
--------------------------------------------------------------------------------
1 | import type { Metadata } from "next";
2 | import "./globals.css"
3 | import fonts from './_fonts/fonts'
4 | import Provider from "@/providers";
5 | import { ThemeSwitch } from "@/components/ThemeSwitch";
6 | import React from 'react'
7 | import { Toaster } from "@/components/ui/toaster";
8 | export const metadata: Metadata = {
9 | title: "Create Next App",
10 | description: "Generated by create next app",
11 | };
12 |
13 | export default function RootLayout({
14 | children,
15 | }: {
16 | children: React.ReactNode;
17 | }) {
18 | return (
19 |
20 |
23 |
24 |
25 |
26 |
27 |
28 | {children}
29 |
30 |
31 |
32 | );
33 | }
34 |
--------------------------------------------------------------------------------
/src/assets/Helvetica-Bold.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codersaadi/next-auth5-shadcn/bec70b4a330b4212f774b5f7c7ef68f02f43b10e/src/assets/Helvetica-Bold.woff
--------------------------------------------------------------------------------
/src/assets/Helvetica-Oblique.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codersaadi/next-auth5-shadcn/bec70b4a330b4212f774b5f7c7ef68f02f43b10e/src/assets/Helvetica-Oblique.woff
--------------------------------------------------------------------------------
/src/assets/Helvetica.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codersaadi/next-auth5-shadcn/bec70b4a330b4212f774b5f7c7ef68f02f43b10e/src/assets/Helvetica.woff
--------------------------------------------------------------------------------
/src/assets/PlaywriteCU-VariableFont_wght.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codersaadi/next-auth5-shadcn/bec70b4a330b4212f774b5f7c7ef68f02f43b10e/src/assets/PlaywriteCU-VariableFont_wght.ttf
--------------------------------------------------------------------------------
/src/assets/auth-image-3d-cartoon.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codersaadi/next-auth5-shadcn/bec70b4a330b4212f774b5f7c7ef68f02f43b10e/src/assets/auth-image-3d-cartoon.jpg
--------------------------------------------------------------------------------
/src/auth.ts:
--------------------------------------------------------------------------------
1 | import { nextAuth } from "./modules/auth/auth";
2 | export const { handlers, signIn, signOut, auth } = nextAuth
3 |
--------------------------------------------------------------------------------
/src/components/Modal.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 | import React from 'react'
3 | import { Dialog, DialogOverlay, DialogContent } from '@/components/ui/dialog'
4 | import { useRouter } from 'next/navigation'
5 | export default function Modal({
6 | children
7 | } :{
8 | children: React.ReactNode
9 | }) {
10 | const router = useRouter()
11 | function handleOpenChange() {
12 | router.back()
13 | }
14 | return (
15 |
16 |
17 |
18 | {children}
19 |
20 |
21 |
22 | )
23 | }
24 |
--------------------------------------------------------------------------------
/src/components/Spinner.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from '@/lib/utils'
2 | import React from 'react'
3 |
4 | export const LoadingSpinner = ({className} :{
5 | className?: string
6 | }) => {
7 | return
19 |
20 |
21 |
22 | }
--------------------------------------------------------------------------------
/src/components/ThemeSwitch.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 | import { MoonIcon, SunIcon } from '@radix-ui/react-icons'
3 | import { useTheme } from 'next-themes'
4 | import Image from 'next/image'
5 | import React, { useEffect } from 'react'
6 | export function ThemeSwitch() {
7 | const DARK_THEME = 'dark'
8 | const LIGHT_THEME = 'light'
9 | const [mounted, setMounted] = React.useState(false)
10 | const {setTheme, resolvedTheme} = useTheme()
11 | useEffect(()=> setMounted(true), [])
12 | if (!mounted) {
13 | return
22 | }
23 | if (resolvedTheme === DARK_THEME) {
24 | return setTheme(LIGHT_THEME)}/>
25 | }
26 | if (resolvedTheme === LIGHT_THEME) {
27 | return setTheme(DARK_THEME)}/>
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/src/components/ui/button.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import { Slot } from "@radix-ui/react-slot"
3 | import { cva, type VariantProps } from "class-variance-authority"
4 |
5 | import { cn } from "@/lib/utils"
6 |
7 | const buttonVariants = cva(
8 | "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50",
9 | {
10 | variants: {
11 | variant: {
12 | default:
13 | "bg-primary text-primary-foreground shadow hover:bg-primary/90",
14 | destructive:
15 | "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
16 | outline:
17 | "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground",
18 | secondary:
19 | "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
20 | ghost: "hover:bg-accent hover:text-accent-foreground",
21 | link: "text-primary underline-offset-4 hover:underline",
22 | },
23 | size: {
24 | default: "h-9 px-4 py-2",
25 | sm: "h-8 rounded-md px-3 text-xs",
26 | lg: "h-10 rounded-md px-8",
27 | icon: "h-9 w-9",
28 | },
29 | },
30 | defaultVariants: {
31 | variant: "default",
32 | size: "default",
33 | },
34 | }
35 | )
36 |
37 | export interface ButtonProps
38 | extends React.ButtonHTMLAttributes,
39 | VariantProps {
40 | asChild?: boolean
41 | }
42 |
43 | const Button = React.forwardRef(
44 | ({ className, variant, size, asChild = false, ...props }, ref) => {
45 | const Comp = asChild ? Slot : "button"
46 | return (
47 |
52 | )
53 | }
54 | )
55 | Button.displayName = "Button"
56 |
57 | export { Button, buttonVariants }
58 |
--------------------------------------------------------------------------------
/src/components/ui/dialog.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as DialogPrimitive from "@radix-ui/react-dialog"
5 | import { Cross2Icon } from "@radix-ui/react-icons"
6 |
7 | import { cn } from "@/lib/utils"
8 |
9 | const Dialog = DialogPrimitive.Root
10 |
11 | const DialogTrigger = DialogPrimitive.Trigger
12 |
13 | const DialogPortal = DialogPrimitive.Portal
14 |
15 | const DialogClose = DialogPrimitive.Close
16 |
17 | const DialogOverlay = React.forwardRef<
18 | React.ElementRef,
19 | React.ComponentPropsWithoutRef
20 | >(({ className, ...props }, ref) => (
21 |
29 | ))
30 | DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
31 |
32 | const DialogContent = React.forwardRef<
33 | React.ElementRef,
34 | React.ComponentPropsWithoutRef
35 | >(({ className, children, ...props }, ref) => (
36 |
37 |
38 |
46 | {children}
47 |
48 |
49 | Close
50 |
51 |
52 |
53 | ))
54 | DialogContent.displayName = DialogPrimitive.Content.displayName
55 |
56 | const DialogHeader = ({
57 | className,
58 | ...props
59 | }: React.HTMLAttributes) => (
60 |
67 | )
68 | DialogHeader.displayName = "DialogHeader"
69 |
70 | const DialogFooter = ({
71 | className,
72 | ...props
73 | }: React.HTMLAttributes) => (
74 |
81 | )
82 | DialogFooter.displayName = "DialogFooter"
83 |
84 | const DialogTitle = React.forwardRef<
85 | React.ElementRef,
86 | React.ComponentPropsWithoutRef
87 | >(({ className, ...props }, ref) => (
88 |
96 | ))
97 | DialogTitle.displayName = DialogPrimitive.Title.displayName
98 |
99 | const DialogDescription = React.forwardRef<
100 | React.ElementRef,
101 | React.ComponentPropsWithoutRef
102 | >(({ className, ...props }, ref) => (
103 |
108 | ))
109 | DialogDescription.displayName = DialogPrimitive.Description.displayName
110 |
111 | export {
112 | Dialog,
113 | DialogPortal,
114 | DialogOverlay,
115 | DialogTrigger,
116 | DialogClose,
117 | DialogContent,
118 | DialogHeader,
119 | DialogFooter,
120 | DialogTitle,
121 | DialogDescription,
122 | }
123 |
--------------------------------------------------------------------------------
/src/components/ui/dropdown-menu.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
5 | import {
6 | CheckIcon,
7 | ChevronRightIcon,
8 | DotFilledIcon,
9 | } from "@radix-ui/react-icons"
10 |
11 | import { cn } from "@/lib/utils"
12 |
13 | const DropdownMenu = DropdownMenuPrimitive.Root
14 |
15 | const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
16 |
17 | const DropdownMenuGroup = DropdownMenuPrimitive.Group
18 |
19 | const DropdownMenuPortal = DropdownMenuPrimitive.Portal
20 |
21 | const DropdownMenuSub = DropdownMenuPrimitive.Sub
22 |
23 | const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup
24 |
25 | const DropdownMenuSubTrigger = React.forwardRef<
26 | React.ElementRef,
27 | React.ComponentPropsWithoutRef & {
28 | inset?: boolean
29 | }
30 | >(({ className, inset, children, ...props }, ref) => (
31 |
40 | {children}
41 |
42 |
43 | ))
44 | DropdownMenuSubTrigger.displayName =
45 | DropdownMenuPrimitive.SubTrigger.displayName
46 |
47 | const DropdownMenuSubContent = React.forwardRef<
48 | React.ElementRef,
49 | React.ComponentPropsWithoutRef
50 | >(({ className, ...props }, ref) => (
51 |
59 | ))
60 | DropdownMenuSubContent.displayName =
61 | DropdownMenuPrimitive.SubContent.displayName
62 |
63 | const DropdownMenuContent = React.forwardRef<
64 | React.ElementRef,
65 | React.ComponentPropsWithoutRef
66 | >(({ className, sideOffset = 4, ...props }, ref) => (
67 |
68 |
78 |
79 | ))
80 | DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
81 |
82 | const DropdownMenuItem = React.forwardRef<
83 | React.ElementRef,
84 | React.ComponentPropsWithoutRef & {
85 | inset?: boolean
86 | }
87 | >(({ className, inset, ...props }, ref) => (
88 |
97 | ))
98 | DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
99 |
100 | const DropdownMenuCheckboxItem = React.forwardRef<
101 | React.ElementRef,
102 | React.ComponentPropsWithoutRef
103 | >(({ className, children, checked, ...props }, ref) => (
104 |
113 |
114 |
115 |
116 |
117 |
118 | {children}
119 |
120 | ))
121 | DropdownMenuCheckboxItem.displayName =
122 | DropdownMenuPrimitive.CheckboxItem.displayName
123 |
124 | const DropdownMenuRadioItem = React.forwardRef<
125 | React.ElementRef,
126 | React.ComponentPropsWithoutRef
127 | >(({ className, children, ...props }, ref) => (
128 |
136 |
137 |
138 |
139 |
140 |
141 | {children}
142 |
143 | ))
144 | DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName
145 |
146 | const DropdownMenuLabel = React.forwardRef<
147 | React.ElementRef,
148 | React.ComponentPropsWithoutRef & {
149 | inset?: boolean
150 | }
151 | >(({ className, inset, ...props }, ref) => (
152 |
161 | ))
162 | DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
163 |
164 | const DropdownMenuSeparator = React.forwardRef<
165 | React.ElementRef,
166 | React.ComponentPropsWithoutRef
167 | >(({ className, ...props }, ref) => (
168 |
173 | ))
174 | DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
175 |
176 | const DropdownMenuShortcut = ({
177 | className,
178 | ...props
179 | }: React.HTMLAttributes) => {
180 | return (
181 |
185 | )
186 | }
187 | DropdownMenuShortcut.displayName = "DropdownMenuShortcut"
188 |
189 | export {
190 | DropdownMenu,
191 | DropdownMenuTrigger,
192 | DropdownMenuContent,
193 | DropdownMenuItem,
194 | DropdownMenuCheckboxItem,
195 | DropdownMenuRadioItem,
196 | DropdownMenuLabel,
197 | DropdownMenuSeparator,
198 | DropdownMenuShortcut,
199 | DropdownMenuGroup,
200 | DropdownMenuPortal,
201 | DropdownMenuSub,
202 | DropdownMenuSubContent,
203 | DropdownMenuSubTrigger,
204 | DropdownMenuRadioGroup,
205 | }
206 |
--------------------------------------------------------------------------------
/src/components/ui/form.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as LabelPrimitive from "@radix-ui/react-label"
5 | import { Slot } from "@radix-ui/react-slot"
6 | import {
7 | Controller,
8 | ControllerProps,
9 | FieldPath,
10 | FieldValues,
11 | FormProvider,
12 | useFormContext,
13 | } from "react-hook-form"
14 |
15 | import { cn } from "@/lib/utils"
16 | import { Label } from "@/components/ui/label"
17 |
18 | const Form = FormProvider
19 |
20 | type FormFieldContextValue<
21 | TFieldValues extends FieldValues = FieldValues,
22 | TName extends FieldPath = FieldPath
23 | > = {
24 | name: TName
25 | }
26 |
27 | const FormFieldContext = React.createContext(
28 | {} as FormFieldContextValue
29 | )
30 |
31 | const FormField = <
32 | TFieldValues extends FieldValues = FieldValues,
33 | TName extends FieldPath = FieldPath
34 | >({
35 | ...props
36 | }: ControllerProps) => {
37 | return (
38 |
39 |
40 |
41 | )
42 | }
43 |
44 | const useFormField = () => {
45 | const fieldContext = React.useContext(FormFieldContext)
46 | const itemContext = React.useContext(FormItemContext)
47 | const { getFieldState, formState } = useFormContext()
48 |
49 | const fieldState = getFieldState(fieldContext.name, formState)
50 |
51 | if (!fieldContext) {
52 | throw new Error("useFormField should be used within ")
53 | }
54 |
55 | const { id } = itemContext
56 |
57 | return {
58 | id,
59 | name: fieldContext.name,
60 | formItemId: `${id}-form-item`,
61 | formDescriptionId: `${id}-form-item-description`,
62 | formMessageId: `${id}-form-item-message`,
63 | ...fieldState,
64 | }
65 | }
66 |
67 | type FormItemContextValue = {
68 | id: string
69 | }
70 |
71 | const FormItemContext = React.createContext(
72 | {} as FormItemContextValue
73 | )
74 |
75 | const FormItem = React.forwardRef<
76 | HTMLDivElement,
77 | React.HTMLAttributes
78 | >(({ className, ...props }, ref) => {
79 | const id = React.useId()
80 |
81 | return (
82 |
83 |
84 |
85 | )
86 | })
87 | FormItem.displayName = "FormItem"
88 |
89 | const FormLabel = React.forwardRef<
90 | React.ElementRef,
91 | React.ComponentPropsWithoutRef
92 | >(({ className, ...props }, ref) => {
93 | const { error, formItemId } = useFormField()
94 |
95 | return (
96 |
102 | )
103 | })
104 | FormLabel.displayName = "FormLabel"
105 |
106 | const FormControl = React.forwardRef<
107 | React.ElementRef,
108 | React.ComponentPropsWithoutRef
109 | >(({ ...props }, ref) => {
110 | const { error, formItemId, formDescriptionId, formMessageId } = useFormField()
111 |
112 | return (
113 |
124 | )
125 | })
126 | FormControl.displayName = "FormControl"
127 |
128 | const FormDescription = React.forwardRef<
129 | HTMLParagraphElement,
130 | React.HTMLAttributes
131 | >(({ className, ...props }, ref) => {
132 | const { formDescriptionId } = useFormField()
133 |
134 | return (
135 |
141 | )
142 | })
143 | FormDescription.displayName = "FormDescription"
144 |
145 | const FormMessage = React.forwardRef<
146 | HTMLParagraphElement,
147 | React.HTMLAttributes
148 | >(({ className, children, ...props }, ref) => {
149 | const { error, formMessageId } = useFormField()
150 | const body = error ? String(error?.message) : children
151 |
152 | if (!body) {
153 | return null
154 | }
155 |
156 | return (
157 |
163 | {body}
164 |
165 | )
166 | })
167 | FormMessage.displayName = "FormMessage"
168 |
169 | export {
170 | useFormField,
171 | Form,
172 | FormItem,
173 | FormLabel,
174 | FormControl,
175 | FormDescription,
176 | FormMessage,
177 | FormField,
178 | }
179 |
--------------------------------------------------------------------------------
/src/components/ui/input.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | import { cn } from "@/lib/utils"
4 |
5 | export interface InputProps
6 | extends React.InputHTMLAttributes {}
7 |
8 | const Input = React.forwardRef(
9 | ({ className, type, ...props }, ref) => {
10 | return (
11 |
20 | )
21 | }
22 | )
23 | Input.displayName = "Input"
24 |
25 | export { Input }
26 |
--------------------------------------------------------------------------------
/src/components/ui/label.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as LabelPrimitive from "@radix-ui/react-label"
5 | import { cva, type VariantProps } from "class-variance-authority"
6 |
7 | import { cn } from "@/lib/utils"
8 |
9 | const labelVariants = cva(
10 | "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
11 | )
12 |
13 | const Label = React.forwardRef<
14 | React.ElementRef,
15 | React.ComponentPropsWithoutRef &
16 | VariantProps
17 | >(({ className, ...props }, ref) => (
18 |
23 | ))
24 | Label.displayName = LabelPrimitive.Root.displayName
25 |
26 | export { Label }
27 |
--------------------------------------------------------------------------------
/src/components/ui/sonner.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import { useTheme } from "next-themes"
4 | import { Toaster as Sonner } from "sonner"
5 |
6 | type ToasterProps = React.ComponentProps
7 |
8 | const Toaster = ({ ...props }: ToasterProps) => {
9 | const { theme = "system" } = useTheme()
10 |
11 | return (
12 |
28 | )
29 | }
30 |
31 | export { Toaster }
32 |
--------------------------------------------------------------------------------
/src/components/ui/toast.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import { Cross2Icon } from "@radix-ui/react-icons"
5 | import * as ToastPrimitives from "@radix-ui/react-toast"
6 | import { cva, type VariantProps } from "class-variance-authority"
7 |
8 | import { cn } from "@/lib/utils"
9 |
10 | const ToastProvider = ToastPrimitives.Provider
11 |
12 | const ToastViewport = React.forwardRef<
13 | React.ElementRef,
14 | React.ComponentPropsWithoutRef
15 | >(({ className, ...props }, ref) => (
16 |
24 | ))
25 | ToastViewport.displayName = ToastPrimitives.Viewport.displayName
26 |
27 | const toastVariants = cva(
28 | "group pointer-events-auto relative flex w-full items-center justify-between space-x-2 overflow-hidden rounded-md border p-4 pr-6 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full",
29 | {
30 | variants: {
31 | variant: {
32 | default: "border bg-background text-foreground",
33 | destructive:
34 | "destructive group border-destructive bg-destructive text-destructive-foreground",
35 | },
36 | },
37 | defaultVariants: {
38 | variant: "default",
39 | },
40 | }
41 | )
42 |
43 | const Toast = React.forwardRef<
44 | React.ElementRef,
45 | React.ComponentPropsWithoutRef &
46 | VariantProps
47 | >(({ className, variant, ...props }, ref) => {
48 | return (
49 |
54 | )
55 | })
56 | Toast.displayName = ToastPrimitives.Root.displayName
57 |
58 | const ToastAction = React.forwardRef<
59 | React.ElementRef,
60 | React.ComponentPropsWithoutRef
61 | >(({ className, ...props }, ref) => (
62 |
70 | ))
71 | ToastAction.displayName = ToastPrimitives.Action.displayName
72 |
73 | const ToastClose = React.forwardRef<
74 | React.ElementRef,
75 | React.ComponentPropsWithoutRef
76 | >(({ className, ...props }, ref) => (
77 |
86 |
87 |
88 | ))
89 | ToastClose.displayName = ToastPrimitives.Close.displayName
90 |
91 | const ToastTitle = React.forwardRef<
92 | React.ElementRef,
93 | React.ComponentPropsWithoutRef
94 | >(({ className, ...props }, ref) => (
95 |
100 | ))
101 | ToastTitle.displayName = ToastPrimitives.Title.displayName
102 |
103 | const ToastDescription = React.forwardRef<
104 | React.ElementRef,
105 | React.ComponentPropsWithoutRef
106 | >(({ className, ...props }, ref) => (
107 |
112 | ))
113 | ToastDescription.displayName = ToastPrimitives.Description.displayName
114 |
115 | type ToastProps = React.ComponentPropsWithoutRef
116 |
117 | type ToastActionElement = React.ReactElement
118 |
119 | export {
120 | type ToastProps,
121 | type ToastActionElement,
122 | ToastProvider,
123 | ToastViewport,
124 | Toast,
125 | ToastTitle,
126 | ToastDescription,
127 | ToastClose,
128 | ToastAction,
129 | }
130 |
--------------------------------------------------------------------------------
/src/components/ui/toaster.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import { useToast } from "@/hooks/use-toast"
4 | import {
5 | Toast,
6 | ToastClose,
7 | ToastDescription,
8 | ToastProvider,
9 | ToastTitle,
10 | ToastViewport,
11 | } from "@/components/ui/toast"
12 |
13 | export function Toaster() {
14 | const { toasts } = useToast()
15 |
16 | return (
17 |
18 | {toasts.map(function ({ id, title, description, action, ...props }) {
19 | return (
20 |
21 |
22 | {title && {title} }
23 | {description && (
24 | {description}
25 | )}
26 |
27 | {action}
28 |
29 |
30 | )
31 | })}
32 |
33 |
34 | )
35 | }
36 |
--------------------------------------------------------------------------------
/src/components/ui/tooltip.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as TooltipPrimitive from "@radix-ui/react-tooltip"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const TooltipProvider = TooltipPrimitive.Provider
9 |
10 | const Tooltip = TooltipPrimitive.Root
11 |
12 | const TooltipTrigger = TooltipPrimitive.Trigger
13 |
14 | const TooltipContent = React.forwardRef<
15 | React.ElementRef,
16 | React.ComponentPropsWithoutRef
17 | >(({ className, sideOffset = 4, ...props }, ref) => (
18 |
27 | ))
28 | TooltipContent.displayName = TooltipPrimitive.Content.displayName
29 |
30 | export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
31 |
--------------------------------------------------------------------------------
/src/constants.ts:
--------------------------------------------------------------------------------
1 | export const logoUrl = "https://img.freepik.com/free-psd/engraved-black-logo-mockup_125540-223.jpg?size=626&ext=jpg&ga=GA1.1.358797363.1725023893&semt=ais_hybrid"
2 |
3 | export const HOST = process.env.HOST || "http://localhost:3000"
4 |
5 | export const RECAPTCHA_SECRET = process.env.RECAPTCHA_SECRET
6 | export const recaptcha_config = {
7 | sitekey : process.env.NEXT_PUBLIC_RECAPTCHA_SITE_KEY || "",
8 | secret : process.env.RECAPTCHA_SECRET,
9 | project_id : "",
10 |
11 | }
12 |
13 |
--------------------------------------------------------------------------------
/src/hooks/use-toast.ts:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | // Inspired by react-hot-toast library
4 | import * as React from "react"
5 |
6 | import type {
7 | ToastActionElement,
8 | ToastProps,
9 | } from "@/components/ui/toast"
10 |
11 | const TOAST_LIMIT = 1
12 | const TOAST_REMOVE_DELAY = 1000000
13 |
14 | export type ToasterToast = ToastProps & {
15 | id: string
16 | title?: React.ReactNode
17 | description?: React.ReactNode
18 | action?: ToastActionElement
19 | }
20 |
21 | const actionTypes = {
22 | ADD_TOAST: "ADD_TOAST",
23 | UPDATE_TOAST: "UPDATE_TOAST",
24 | DISMISS_TOAST: "DISMISS_TOAST",
25 | REMOVE_TOAST: "REMOVE_TOAST",
26 | } as const
27 |
28 | let count = 0
29 |
30 | function genId() {
31 | count = (count + 1) % Number.MAX_SAFE_INTEGER
32 | return count.toString()
33 | }
34 |
35 | type ActionType = typeof actionTypes
36 |
37 | type Action =
38 | | {
39 | type: ActionType["ADD_TOAST"]
40 | toast: ToasterToast
41 | }
42 | | {
43 | type: ActionType["UPDATE_TOAST"]
44 | toast: Partial
45 | }
46 | | {
47 | type: ActionType["DISMISS_TOAST"]
48 | toastId?: ToasterToast["id"]
49 | }
50 | | {
51 | type: ActionType["REMOVE_TOAST"]
52 | toastId?: ToasterToast["id"]
53 | }
54 |
55 | interface State {
56 | toasts: ToasterToast[]
57 | }
58 |
59 | const toastTimeouts = new Map>()
60 |
61 | const addToRemoveQueue = (toastId: string) => {
62 | if (toastTimeouts.has(toastId)) {
63 | return
64 | }
65 |
66 | const timeout = setTimeout(() => {
67 | toastTimeouts.delete(toastId)
68 | dispatch({
69 | type: "REMOVE_TOAST",
70 | toastId: toastId,
71 | })
72 | }, TOAST_REMOVE_DELAY)
73 |
74 | toastTimeouts.set(toastId, timeout)
75 | }
76 |
77 | export const reducer = (state: State, action: Action): State => {
78 | switch (action.type) {
79 | case "ADD_TOAST":
80 | return {
81 | ...state,
82 | toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
83 | }
84 |
85 | case "UPDATE_TOAST":
86 | return {
87 | ...state,
88 | toasts: state.toasts.map((t) =>
89 | t.id === action.toast.id ? { ...t, ...action.toast } : t
90 | ),
91 | }
92 |
93 | case "DISMISS_TOAST": {
94 | const { toastId } = action
95 |
96 | // ! Side effects ! - This could be extracted into a dismissToast() action,
97 | // but I'll keep it here for simplicity
98 | if (toastId) {
99 | addToRemoveQueue(toastId)
100 | } else {
101 | state.toasts.forEach((toast) => {
102 | addToRemoveQueue(toast.id)
103 | })
104 | }
105 |
106 | return {
107 | ...state,
108 | toasts: state.toasts.map((t) =>
109 | t.id === toastId || toastId === undefined
110 | ? {
111 | ...t,
112 | open: false,
113 | }
114 | : t
115 | ),
116 | }
117 | }
118 | case "REMOVE_TOAST":
119 | if (action.toastId === undefined) {
120 | return {
121 | ...state,
122 | toasts: [],
123 | }
124 | }
125 | return {
126 | ...state,
127 | toasts: state.toasts.filter((t) => t.id !== action.toastId),
128 | }
129 | }
130 | }
131 |
132 | const listeners: Array<(state: State) => void> = []
133 |
134 | let memoryState: State = { toasts: [] }
135 |
136 | function dispatch(action: Action) {
137 | memoryState = reducer(memoryState, action)
138 | listeners.forEach((listener) => {
139 | listener(memoryState)
140 | })
141 | }
142 |
143 | type Toast = Omit
144 |
145 | function toast({ ...props }: Toast) {
146 | const id = genId()
147 |
148 | const update = (props: ToasterToast) =>
149 | dispatch({
150 | type: "UPDATE_TOAST",
151 | toast: { ...props, id },
152 | })
153 | const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id })
154 |
155 | dispatch({
156 | type: "ADD_TOAST",
157 | toast: {
158 | ...props,
159 | id,
160 | open: true,
161 | onOpenChange: (open) => {
162 | if (!open) dismiss()
163 | },
164 | },
165 | })
166 |
167 | return {
168 | id: id,
169 | dismiss,
170 | update,
171 | }
172 | }
173 |
174 | function useToast() {
175 | const [state, setState] = React.useState(memoryState)
176 |
177 | React.useEffect(() => {
178 | listeners.push(setState)
179 | return () => {
180 | const index = listeners.indexOf(setState)
181 | if (index > -1) {
182 | listeners.splice(index, 1)
183 | }
184 | }
185 | }, [state])
186 |
187 | return {
188 | ...state,
189 | toast,
190 | dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }),
191 | }
192 | }
193 |
194 | export { useToast, toast }
195 |
--------------------------------------------------------------------------------
/src/hooks/useDebounce.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 | import * as React from "react"
3 |
4 | export function useDebounce(value: T, delay?: number): T {
5 | const [debouncedValue, setDebouncedValue] = React.useState(value)
6 |
7 | React.useEffect(() => {
8 | const timer = setTimeout(() => setDebouncedValue(value), delay ?? 500)
9 |
10 | return () => {
11 | clearTimeout(timer)
12 | }
13 | }, [value, delay])
14 |
15 | return debouncedValue
16 | }
17 |
--------------------------------------------------------------------------------
/src/hooks/useFormSubmit.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 | import React, { useState, useEffect } from 'react';
3 | import { useForm, UseFormReturn } from 'react-hook-form';
4 | import { zodResolver } from '@hookform/resolvers/zod';
5 | import { useTransition } from 'react';
6 | import * as z from 'zod';
7 | import { MessageResponse } from '@/modules/auth/types/auth';
8 | import { CaptchaActionOptions } from '@/modules/auth/types/captcha';
9 | import { isRedirectError } from 'next/dist/client/components/redirect';
10 |
11 | type FormMessage = {
12 | type: 'error' | 'success';
13 | message: string;
14 | };
15 |
16 | interface CaptchaOptions {
17 | enableCaptcha: boolean;
18 | executeRecaptcha?: (action?: string) => Promise;
19 | action?: string;
20 | tokenExpiryMs?: number;
21 | }
22 |
23 | interface FormSubmitProps> {
24 | schema : T,
25 | defaultValues: z.infer,
26 | onSubmitAction : (data: z.infer, captchaOptions: CaptchaActionOptions) => Promise,
27 | captcha?: CaptchaOptions,
28 | }
29 |
30 |
31 | export const useFormSubmit = >(
32 | props : FormSubmitProps
33 | ) => {
34 | const {schema, defaultValues, captcha, onSubmitAction} = props
35 | const initialCaptchaState = {
36 | validating: false,
37 | token: '',
38 | tokenTimestamp: Date.now(),
39 | };
40 |
41 | const form: UseFormReturn> = useForm>({
42 | resolver: zodResolver(schema),
43 | defaultValues,
44 | });
45 |
46 | const [captchaState, setCaptchaState] = useState(initialCaptchaState);
47 | const [message, setMessage] = useState(null); // Use null to signify no message
48 | const [isPending, startTransition] = useTransition();
49 |
50 | const refreshCaptcha = async (): Promise => {
51 | if (captcha?.enableCaptcha && captcha.executeRecaptcha) {
52 | setCaptchaState(prev => ({ ...prev, validating: true }));
53 | try {
54 | const token = await captcha.executeRecaptcha(captcha.action || 'default_action');
55 | if (!token) {
56 | setMessage({ message: "Error Getting Captcha", type: "error" });
57 | return null; // Early return to prevent submission
58 | }
59 | setCaptchaState({
60 | token,
61 | tokenTimestamp: Date.now(),
62 | validating: false,
63 | });
64 | return token;
65 | } catch (error) {
66 | console.error("Error refreshing CAPTCHA:", error);
67 | setMessage({ message: 'Captcha verification failed', type: 'error' });
68 | setCaptchaState(prev => ({ ...prev, validating: false }));
69 | return null;
70 | }
71 | }
72 | return null;
73 | };
74 |
75 | const isCaptchaInvalid = (): boolean => {
76 | const now = Date.now();
77 | const invalid = !captchaState.token || (now - captchaState.tokenTimestamp > (captcha?.tokenExpiryMs || 120000));
78 | if (invalid) {
79 | setCaptchaState(initialCaptchaState);
80 | }
81 | return invalid;
82 | };
83 |
84 | useEffect(() => {
85 | if (captcha?.enableCaptcha && isCaptchaInvalid()) {
86 | refreshCaptcha();
87 | }
88 | }, [captchaState.token, captcha?.tokenExpiryMs, captcha?.enableCaptcha]);
89 |
90 | const onSubmit = (
91 | action: (data: z.infer, captchaOptions: CaptchaActionOptions) => Promise
92 | ) => {
93 | const setResponseMessage = (res: MessageResponse) => {
94 | if (res && "success" in res) {
95 | setMessage({
96 | type: res.success ? 'success' : 'error',
97 | message: res?.message || "",
98 | });
99 | if (captcha?.enableCaptcha) {
100 | setCaptchaState(initialCaptchaState);
101 | }
102 | }
103 | };
104 |
105 | return async (data: z.infer) => {
106 | setMessage(null); // Clear previous messages
107 |
108 | let captchaToken: string | undefined | null = captchaState.token;
109 |
110 | if (captcha?.enableCaptcha) {
111 | if (!captcha.executeRecaptcha) {
112 | setMessage({ message: 'Captcha not available', type: 'error' });
113 | return;
114 | }
115 |
116 | if (isCaptchaInvalid()) {
117 | console.log('Token expired, refreshing CAPTCHA');
118 | captchaToken = await refreshCaptcha();
119 | if (!captchaToken) return; // Return if CAPTCHA refresh failed
120 | }
121 | }
122 |
123 | startTransition(() => {
124 | action(data, {
125 | token: captchaToken || '',
126 | tokenExpiryMs: captcha?.tokenExpiryMs || 120000,
127 | action: captcha?.action,
128 | }).then(setResponseMessage)
129 | .catch((error) => {
130 | if (isRedirectError(error)) return
131 | console.error('Submission Error:', error);
132 | setMessage({ type: 'error', message: 'Submission failed. Please try again.' });
133 | });
134 | });
135 | };
136 | };
137 |
138 | return { form, message, isPending, onSubmit : form.handleSubmit(onSubmit(onSubmitAction)), captchaState };
139 | };
140 |
--------------------------------------------------------------------------------
/src/hooks/useUpdateQueryParams.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 | import * as React from "react"
3 |
4 | type QueryParams = Record
5 |
6 | export function useQueryString(searchParams: URLSearchParams) {
7 | const createQueryString = React.useCallback(
8 | (params: QueryParams) => {
9 | const newSearchParams = new URLSearchParams(searchParams?.toString())
10 |
11 | for (const [key, value] of Object.entries(params)) {
12 | if (value === null) {
13 | newSearchParams.delete(key)
14 | } else {
15 | newSearchParams.set(key, String(value))
16 | }
17 | }
18 |
19 | return newSearchParams.toString()
20 | },
21 | [searchParams]
22 | )
23 |
24 | return { createQueryString }
25 | }
26 |
--------------------------------------------------------------------------------
/src/lib/db.ts:
--------------------------------------------------------------------------------
1 | import { PrismaClient } from "@prisma/client";
2 |
3 | declare global {
4 | var prisma: PrismaClient | undefined;
5 | }
6 | export const db = globalThis.prisma || new PrismaClient();
7 |
8 | if (process.env.NODE_ENV !== "production") {
9 | globalThis.prisma = db;
10 | }
--------------------------------------------------------------------------------
/src/lib/fetch.ts:
--------------------------------------------------------------------------------
1 | type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH';
2 |
3 | interface FetchOptions extends RequestInit {
4 | method?: HttpMethod;
5 | body?: TBody;
6 | params?: Record;
7 | }
8 |
9 | interface FetchResponse {
10 | data: T | null;
11 | error: string | null;
12 | }
13 |
14 | async function baseFetch(
15 | url: string,
16 | options?: FetchOptions
17 | ): Promise> {
18 | const {
19 | method = 'GET',
20 | headers = {},
21 | body,
22 | params,
23 | ...restOptions // Destructure and collect remaining options
24 | } = options || {};
25 |
26 | // Build the query string if params are provided
27 | const queryString = params
28 | ? '?' + new URLSearchParams(params as Record).toString()
29 | : '';
30 |
31 | // Construct the full URL with query string
32 | const fullUrl = `${url}${queryString}`;
33 |
34 | try {
35 | const response = await fetch(fullUrl, {
36 | method,
37 | headers: {
38 | 'Content-Type': 'application/json',
39 | ...headers,
40 | },
41 | body, // Use body directly since it's already type-safe
42 | ...restOptions, // Spread other RequestInit options like mode, credentials, etc.
43 | });
44 |
45 | if (!response.ok) {
46 | throw new Error(`HTTP error! status: ${response.status}`);
47 | }
48 |
49 | const data: TResponse = await response.json();
50 | return { data, error: null };
51 | } catch (error: unknown) {
52 | return {
53 | data: null,
54 | error: (error as Error).message,
55 | };
56 | }
57 | }
58 |
59 | export default baseFetch;
60 |
--------------------------------------------------------------------------------
/src/lib/utils.ts:
--------------------------------------------------------------------------------
1 | import { type ClassValue, clsx } from "clsx";
2 | import { twMerge } from "tailwind-merge";
3 |
4 | export function cn(...inputs: ClassValue[]) {
5 | return twMerge(clsx(inputs));
6 | }
7 |
--------------------------------------------------------------------------------
/src/lib/validate-utils.ts:
--------------------------------------------------------------------------------
1 | import * as z from "zod";
2 | type createFormErrorType = (
3 | validate: z.SafeParseReturnType,
4 | failMessage?: string,
5 | ) => { message: string; success: boolean } | void;
6 | export const createFormError: createFormErrorType = (validate, failMessage) => {
7 | if (!validate.success) {
8 | return {
9 | message: validate.error.errors[0].message || failMessage || "",
10 | success: false,
11 | };
12 | }
13 | };
14 |
--------------------------------------------------------------------------------
/src/middleware.ts:
--------------------------------------------------------------------------------
1 | import NextAuth from "next-auth";
2 | import {
3 | apiAuthPrefix,
4 | authRoutes,
5 | DEFAULT_LOGIN_REDIRECT,
6 | publicRoutes,
7 | } from "./routes";
8 | import authConfig from "./modules/auth/auth.config";
9 | const { auth } = NextAuth(authConfig);
10 | export default auth((req) => {
11 | const isLoggedIn = !!req.auth;
12 | const { nextUrl } = req;
13 | const isApiAuth = nextUrl.pathname.startsWith(apiAuthPrefix);
14 | const isPublicRoute = publicRoutes.includes(nextUrl.pathname);
15 | const isAuthRoute = authRoutes.includes(nextUrl.pathname);
16 | if (isApiAuth) {
17 | return;
18 | }
19 | if (isAuthRoute) {
20 | if (isLoggedIn) {
21 | return Response.redirect(new URL(DEFAULT_LOGIN_REDIRECT, nextUrl));
22 | }
23 | return
24 | }
25 | if (!isLoggedIn && !isPublicRoute) {
26 | return Response.redirect(new URL("/auth/signin", nextUrl));
27 | }
28 | return;
29 | });
30 | export const config = {
31 | matcher: ["/((?!.*\\..*|_next).*)", "/", "/(api|trpc)(.*)"],
32 | };
33 |
--------------------------------------------------------------------------------
/src/modules/auth/auth.config.ts:
--------------------------------------------------------------------------------
1 | import type { NextAuthConfig } from "next-auth";
2 | import Credentials from "next-auth/providers/credentials";
3 | import Github from "next-auth/providers/github";
4 | import bcrypt from "bcryptjs";
5 | import Google from "next-auth/providers/google";
6 | import FacebookProvider from "next-auth/providers/facebook";
7 | import { LoginSchema } from "./auth.schema";
8 | import userRepository from "./data/user";
9 | export default {
10 | trustHost : true,
11 | providers: [
12 | Credentials({
13 | credentials: {
14 | email: { label: "Email", type: "email" },
15 | password: { label: "Password", type: "password" },
16 | },
17 | async authorize(credentials) {
18 | const validate = await LoginSchema.parseAsync(credentials);
19 | if (!validate) return null;
20 | const { email, password } = validate;
21 | const user = await userRepository.getUserByEmail(email);
22 | if (!user || !user.password) return null;
23 | const matched = await bcrypt.compare(password, user.password);
24 | if (matched) return user;
25 | return user;
26 | },
27 | }),
28 | Github({
29 | clientId: process.env.GITHUB_CLIENT_ID,
30 | clientSecret: process.env.GITHUB_CLIENT_SECRET,
31 | }),
32 | Google({
33 | clientSecret: process.env.GOOGLE_CLIENT_SECRET,
34 | clientId: process.env.GOOGLE_CLIENT_ID,
35 | }),
36 | FacebookProvider({
37 | clientId: process.env.FACEBOOK_CLIENT_ID,
38 | clientSecret: process.env.FACEBOOK_CLIENT_SECRET,
39 | }),
40 |
41 |
42 |
43 | ],
44 | } satisfies NextAuthConfig;
45 |
46 | const providersUsed = ["credentials", "github", "google", "facebook"] as const;
47 | export type availableProviders = (typeof providersUsed)[number];
48 |
--------------------------------------------------------------------------------
/src/modules/auth/auth.schema.ts:
--------------------------------------------------------------------------------
1 | import * as z from 'zod';
2 | export const ForgotPasswordSchema = z.object({
3 | email: z.string().email({
4 | message: 'Please enter a valid email address'
5 | })
6 | });
7 |
8 | export const ResetPasswordSchema = z.object({
9 | password: z.string().min(7, {
10 | message: 'Password is required'
11 | }),
12 | confirmPassword: z.string().min(7, {
13 | message: 'Password is required'
14 | })
15 | });
16 |
17 | export const LoginSchema = z.object({
18 | email: z.string().email({
19 | message: 'Please enter a valid email address'
20 | }),
21 | password: z.string().min(1, {
22 | message: 'Password is required'
23 | })
24 | });
25 |
26 | export const SignupSchema = z.object({
27 | name: z.string(),
28 | email: z.string().email({
29 | message: 'Please enter a valid email address'
30 | }),
31 | password: z.string().min(7, {message: 'Password is required'}),
32 | });
33 | export const MagicSignInSchema = z.object({ email: z.string().email({
34 | message: 'Please enter a valid email address'
35 | })})
36 | export type MagicSignInType = z.infer
37 | export type ForgotPasswordSchemaType = z.infer
38 | export type LoginSchemaType = z.infer
39 | export type SignupSchemaType = z.infer
40 | export type ResetPasswordSchemaType = z.infer
41 |
--------------------------------------------------------------------------------
/src/modules/auth/auth.ts:
--------------------------------------------------------------------------------
1 | import NextAuth from "next-auth";
2 | import authConfig from "./auth.config";
3 | import { PrismaAdapter } from "@auth/prisma-adapter";
4 | import { UserRole } from "@prisma/client";
5 | import { db } from "@/lib/db";
6 | import userRepository from "./data/user";
7 | import { sendVerificationRequest } from "./sendRequest";
8 |
9 | export const nextAuth = NextAuth({
10 | pages: {
11 | signIn: "/auth/signin",
12 | verifyRequest : "/auth/verify-request",
13 | "error" : "/auth-error",
14 | signOut : "/signout"
15 | },
16 | events: {
17 | linkAccount: async ({ user }) => {
18 | if (!user.id) return
19 | await userRepository.verifyUserEmail(user.id)
20 | },
21 | },
22 | callbacks: {
23 | async signIn({ user, account }) {
24 | const provider = account?.provider;
25 | if (provider !== "credentials" && provider !== "http-email") return true;
26 |
27 | if (!user || !user.id) return false;
28 | const existingUser = await userRepository.getUserById(user.id);
29 |
30 | if (
31 | !existingUser ||
32 | (provider === "credentials" && !existingUser.emailVerified)
33 | ) {
34 | if (provider === "http-email") return "/auth/signup";
35 | return false;
36 | }
37 | return true;
38 | },
39 |
40 | // jwt is called when the JWT is created
41 |
42 | async jwt(jwt) {
43 | const { token, user } = jwt;
44 |
45 | if (!token.sub) return token;
46 | const existingUser = await userRepository.getUserById(token.sub);
47 | if (!existingUser) return token;
48 | token.email = existingUser.email;
49 | token.name = existingUser.name;
50 | token.picture = existingUser.image;
51 |
52 | token.role = existingUser.role;
53 | return token;
54 | },
55 | // session uses the JWT token to create and generate the session object
56 | async session({ session, user, token }) {
57 | if (session.user) {
58 | if (token.role) session.user.role = token.role as UserRole;
59 | if (token.sub) session.user.id = token.sub;
60 | if (token.email) session.user.email = token.email;
61 | if (token.name) session.user.name = token.name;
62 | if (token.picture) session.user.image = token.picture;
63 | }
64 |
65 | return session;
66 | },
67 | },
68 | adapter: PrismaAdapter(db),
69 |
70 | session: { strategy: "jwt" },
71 | trustHost: authConfig.trustHost,
72 |
73 | providers: [
74 |
75 | ...authConfig.providers,
76 | {
77 | id: "http-email",
78 | name: "Email",
79 | sendVerificationRequest: sendVerificationRequest,
80 | options: {
81 | },
82 | maxAge: 60 * 60,
83 | from: "onboarding@codestacklab.com",
84 | type: "email",
85 |
86 | },
87 | ],
88 | });
89 |
--------------------------------------------------------------------------------
/src/modules/auth/components/AuthProvidersCTA.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import React from 'react';
4 | import { signIn } from 'next-auth/react';
5 | import { DEFAULT_LOGIN_REDIRECT } from '@/routes';
6 | import { cn } from '@/lib/utils';
7 | import { Button } from '@/components/ui/button';
8 | import useAuthProviders from '../useAuthProviders';
9 | import { availableProviders } from '../auth.config';
10 |
11 | export default function AuthProvidersCTA({
12 | withDescription = false,
13 | }: {
14 | withDescription?: boolean;
15 | }) {
16 | const providers = useAuthProviders();
17 |
18 | const handleSignIn = async (provider: availableProviders) => {
19 | try {
20 | await signIn(provider, { callbackUrl: DEFAULT_LOGIN_REDIRECT });
21 | } catch (error) {
22 | console.error(`Error signing in with ${provider}:`, error);
23 | }
24 | };
25 |
26 | return (
27 |
28 |
33 |
34 |
35 | {providers.map((provider) => (
36 |
handleSignIn(provider.id)}
39 | className={cn(
40 | 'flex items-center mx-auto w-full',
41 | withDescription
42 | ? 'w-full gap-3 py-2 px-4 rounded-full '
43 | : 'aspect-square p-1'
44 | )}
45 | >
46 |
47 | {withDescription && (
48 |
49 | Continue with {provider.name}
50 |
51 | )}
52 |
53 | ))}
54 |
55 |
56 | );
57 | }
58 |
--------------------------------------------------------------------------------
/src/modules/auth/components/CaptchaProvider.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 | import { recaptcha_config } from "@/constants"
3 | import { GoogleReCaptchaProvider } from "react-google-recaptcha-v3"
4 |
5 |
6 | export default function CaptchaClientProvider({ children }: {
7 | children: React.ReactNode
8 | }) {
9 | return (
10 | {children}
11 | )
12 | }
13 |
--------------------------------------------------------------------------------
/src/modules/auth/components/EmailVerifyForm.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 | import { Button } from '@/components/ui/button'
3 | import Link from 'next/link'
4 | import React, { useCallback, useEffect, useState } from 'react'
5 | import { emailVerifyAction } from '../lib/emailVerifyAction'
6 | import FormFeedback from './FormFeedback'
7 |
8 | export default function EmailVerifyForm({ token }: {
9 | token?: string
10 | }) {
11 | const [message, setMessage] = useState<{
12 | type: "success" | "error",
13 | message: string
14 | }>()
15 | const onSubmit = useCallback(async () => {
16 | const res = await emailVerifyAction(token)
17 | setMessage({
18 | type: res.success ? "success" : "error",
19 | message: res.message
20 | })
21 | }, [token])
22 | useEffect(() => {
23 | onSubmit()
24 | }, [token])
25 |
26 | return (
27 |
28 | {!message &&
29 | <>
30 |
31 |
32 | Processing your email verification request
33 |
34 | >
35 | }
36 |
37 | {message && message.type === "success" && (
38 |
39 | you may close this page and signin to continue
40 |
41 | )}
42 |
43 |
44 | Go Back to Signin
45 |
46 |
47 |
48 | )
49 | }
50 |
--------------------------------------------------------------------------------
/src/modules/auth/components/ForgotPassword.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 | import { Form, FormField, FormItem, FormLabel, FormControl } from '@/components/ui/form'
3 | import React from 'react'
4 | import { Input } from '@/components/ui/input';
5 | import { cn } from '@/lib/utils';
6 | import { Button } from '@/components/ui/button';
7 | import { forgotPasswordAction } from '../lib/forgot-password';
8 | import { ForgotPasswordSchema } from '../auth.schema';
9 | import FormFeedback from './FormFeedback';
10 | import { useFormSubmit } from '@/hooks/useFormSubmit';
11 | import { LoadingSpinner } from '@/components/Spinner';
12 | import { useGoogleReCaptcha } from 'react-google-recaptcha-v3';
13 | export default function ForgotPasswordForm() {
14 | const {executeRecaptcha} = useGoogleReCaptcha()
15 | const {form , isPending ,onSubmit, message} = useFormSubmit({
16 | schema : ForgotPasswordSchema,
17 | onSubmitAction : forgotPasswordAction,
18 | defaultValues :{
19 | "email" : ""
20 | },
21 | captcha :{
22 | enableCaptcha: true,
23 | executeRecaptcha,
24 | action: "forgotpassword_form",
25 | tokenExpiryMs: 120000,
26 | }
27 |
28 | })
29 | return (
30 | <>
31 | Reset Your Password
32 |
60 |
61 | >
62 | )
63 | }
64 |
--------------------------------------------------------------------------------
/src/modules/auth/components/FormFeedback.tsx:
--------------------------------------------------------------------------------
1 | import {CheckCircledIcon, ExclamationTriangleIcon} from '@radix-ui/react-icons'
2 | interface FormFeedbackProps {
3 | message?: string;
4 | type ?: 'error' | 'success';
5 | }
6 | function FormFeedback({
7 | message, type
8 | }: FormFeedbackProps) {
9 | if (!message) return null;
10 | const errorClass = `
11 | bg-destructive/15 text-destructive
12 | `
13 | const successClass = `
14 | bg-emerald-600/15 text-emerald-600
15 | `
16 |
17 | return (
18 |
21 | {
22 | type === "success" ?
23 | : }
24 | {message}
25 |
26 | )
27 | }
28 |
29 | export default FormFeedback
--------------------------------------------------------------------------------
/src/modules/auth/components/MagicLinkSignin.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 | import { useFormSubmit } from "@/hooks/useFormSubmit";
3 | import React from "react";
4 | import { MagicSignInSchema } from "../auth.schema";
5 | import { Form, FormControl, FormField, FormItem, FormLabel } from "@/components/ui/form";
6 | import FormFeedback from "./FormFeedback";
7 | import { AvatarIcon } from "@radix-ui/react-icons";
8 | import { cn } from "@/lib/utils";
9 | import { Input } from "@/components/ui/input";
10 | import { Button } from "@/components/ui/button";
11 | import { CheckCircledIcon } from "@radix-ui/react-icons"
12 | import { LoadingSpinner } from "@/components/Spinner";
13 | import { useGoogleReCaptcha } from "react-google-recaptcha-v3";
14 | import { signinMagic } from "../lib/signin_magic-action";
15 |
16 | export default function MagicLinkSigninForm() {
17 | const { executeRecaptcha } = useGoogleReCaptcha();
18 | const { isPending, form, message, onSubmit, captchaState } = useFormSubmit({
19 | schema: MagicSignInSchema,
20 | captcha: {
21 | enableCaptcha: true,
22 | executeRecaptcha,
23 | action: "email_signin",
24 | tokenExpiryMs: 120000,
25 | },
26 | onSubmitAction: signinMagic,
27 | defaultValues: {
28 | "email": ""
29 | }}
30 | );
31 |
32 | return (
33 | <>
34 | Sign In with Email
35 |
36 |
85 |
86 | {/* Show a subtle progress bar during CAPTCHA validation */}
87 | {captchaState.validating && (
88 |
91 | )}
92 |
93 | {/* Feedback message */}
94 | {message && (
95 |
96 |
97 |
98 | )}
99 |
100 |
101 |
115 | >
116 | );
117 | }
118 |
--------------------------------------------------------------------------------
/src/modules/auth/components/NewPasswordForm.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 | import { Form, FormField, FormItem, FormLabel, FormControl } from '@/components/ui/form'
3 | import React, { useState } from 'react'
4 | import { Input } from '@/components/ui/input';
5 | import { cn } from '@/lib/utils';
6 | import { EyeOpenIcon as EyeIcon, EyeClosedIcon as EyeOff } from '@radix-ui/react-icons'
7 | import { Button } from '@/components/ui/button';
8 | import { ResetPasswordSchema } from '../auth.schema';
9 | import FormFeedback from './FormFeedback';
10 | import { useFormSubmit } from '@/hooks/useFormSubmit';
11 | import { resetPasswordAction } from '../lib/forgot-password';
12 | import { LoadingSpinner } from '@/components/Spinner';
13 | export default function NewPasswordForm({ token }: {
14 | token?: string
15 | }) {
16 |
17 | const [showPassword, setShowPassword] = useState(false)
18 | const { form, isPending, message, onSubmit } = useFormSubmit({
19 | schema: ResetPasswordSchema,
20 | onSubmitAction: async (data) => {
21 | if (!token) return { message: "No Token Found", "success": false }
22 | return await resetPasswordAction(token, data)
23 | },
24 | defaultValues: {
25 | confirmPassword: "",
26 | password: ""
27 | }
28 | })
29 | return (
30 | <>
31 | Reset Your Password
32 |
84 |
85 | >
86 | )
87 | }
88 |
--------------------------------------------------------------------------------
/src/modules/auth/components/SignInForm.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 | import React, { Suspense, useState } from 'react';
3 | import Link from 'next/link';
4 | import { EyeClosedIcon, EyeOpenIcon, AvatarIcon, CheckCircledIcon } from '@radix-ui/react-icons'
5 | import { Form, FormControl, FormField, FormItem, FormLabel } from '@/components/ui/form';
6 | import { Input } from '@/components/ui/input';
7 | import { Button } from '@/components/ui/button';
8 | import { cn } from '@/lib/utils';
9 | import { LoginSchema } from '../auth.schema';
10 | import { signInAction } from '../lib/signin-action';
11 | import AuthProvidersCTA from './AuthProvidersCTA';
12 | import FormFeedback from './FormFeedback';
13 | import { useFormSubmit } from '@/hooks/useFormSubmit';
14 | import { LoadingSpinner } from '@/components/Spinner';
15 | import { useGoogleReCaptcha } from 'react-google-recaptcha-v3';
16 | const MagicLinkSigninForm = React.lazy(() => import("@/modules/auth/components/MagicLinkSignin"))
17 |
18 | const SignInForm: React.FC = () => {
19 | const { executeRecaptcha } = useGoogleReCaptcha()
20 | const [showPassword, setShowPassword] = useState(false)
21 | const { form, onSubmit, isPending: submitting, message, captchaState } = useFormSubmit({
22 | schema: LoginSchema,
23 | defaultValues: {
24 | email: "",
25 | password: ""
26 | },
27 | captcha: {
28 | enableCaptcha: true,
29 | executeRecaptcha,
30 | action: "credentials_signin",
31 | tokenExpiryMs: 120000,
32 | },
33 | onSubmitAction: signInAction
34 | })
35 |
36 | const isPending = captchaState.validating || submitting
37 | return (
38 | <>
39 | Sign In to Continue
40 |
114 |
115 |
116 |
117 | >
118 | );
119 | };
120 |
121 |
122 | const SignInComponent = ({ searchParams }: {
123 | searchParams: Record
124 | }) => {
125 | const [withCredentials, setWithCredentials] = useState(searchParams["signin-with-link"] ? false : true);
126 |
127 | const onSignInTypeChange = () => setWithCredentials(!withCredentials);
128 |
129 | const changeButtonTitle = withCredentials
130 | ? "Sign In With Magic Link"
131 | : "Sign In With Credentials";
132 |
133 | return (
134 |
135 |
140 |
141 |
142 |
143 |
144 |
}>
145 |
146 |
147 |
148 |
149 |
150 |
151 | Create an Account!
152 |
153 |
154 | if you don't have one.
155 |
156 |
157 |
162 | {changeButtonTitle}
163 |
164 |
165 |
166 | );
167 | };
168 |
169 | export default SignInComponent;
170 |
--------------------------------------------------------------------------------
/src/modules/auth/components/SignOutButton.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 | import { Button } from '@/components/ui/button'
3 | import { cn } from '@/lib/utils'
4 | import { signOut } from 'next-auth/react'
5 | import React from 'react'
6 |
7 | export default function SignOutButton() {
8 | return (
9 | signOut()}>
10 | Sign Out
11 |
12 | )
13 | }
14 |
--------------------------------------------------------------------------------
/src/modules/auth/components/SignupForm.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 | import React, { useEffect, useState } from 'react';
3 | import Link from 'next/link';
4 | import { EyeClosedIcon, EyeOpenIcon, AvatarIcon, EnvelopeOpenIcon } from '@radix-ui/react-icons'
5 | import { Form, FormControl, FormField, FormItem, FormLabel } from '@/components/ui/form';
6 | import { Input } from '@/components/ui/input';
7 | import { Button } from '@/components/ui/button';
8 | import { cn } from '@/lib/utils';
9 | import { signUpAction } from '../lib/signup-action';
10 | import AuthProvidersCTA from './AuthProvidersCTA';
11 | import FormFeedback from './FormFeedback';
12 | import { useFormSubmit } from '@/hooks/useFormSubmit';
13 | import { LoadingSpinner } from '@/components/Spinner';
14 | import { toast } from '@/hooks/use-toast';
15 | import { SignupSchema } from '../auth.schema';
16 | import { useGoogleReCaptcha } from 'react-google-recaptcha-v3';
17 | const SignUpForm: React.FC<{
18 | searchParams?: Record
19 | }> = ({ searchParams }) => {
20 | const callBackError = searchParams?.["callbackError"]
21 | const {executeRecaptcha} = useGoogleReCaptcha()
22 | const [showPassword, setShowPassword] = useState(false)
23 | const { form, message, isPending, onSubmit } = useFormSubmit({
24 | schema: SignupSchema,
25 | defaultValues: {
26 | email: '',
27 | password: '',
28 | name: '',
29 | }, captcha: {
30 | enableCaptcha: true,
31 | executeRecaptcha,
32 | action: "credentials_signup",
33 | tokenExpiryMs: 120000,
34 | },
35 | onSubmitAction: signUpAction
36 | })
37 | useEffect(() => {
38 | if (!callBackError) return
39 | toast({
40 | title: `User with this email ${searchParams["email"]} does not exists or verified `,
41 | variant: "destructive",
42 | description:
43 | Create a New Account or Verify it exists
44 |
45 | Occurred at {searchParams["at"]}
46 |
47 |
48 |
49 | })
50 |
51 | }, [callBackError])
52 | return (
53 | <>
54 | Create a New Account
55 |
126 |
127 |
128 | Aleady Have an Account?
129 |
130 |
131 |
132 |
133 |
134 | >
135 | );
136 | };
137 |
138 | export default SignUpForm;
139 |
140 |
141 |
--------------------------------------------------------------------------------
/src/modules/auth/data/index.ts:
--------------------------------------------------------------------------------
1 |
2 | export * from './resetpassword-token'
3 | export * from './user'
4 | export * from './verification-token'
--------------------------------------------------------------------------------
/src/modules/auth/data/resetpassword-token.ts:
--------------------------------------------------------------------------------
1 | import { db } from "@/lib/db";
2 | import * as v4 from "uuid";
3 |
4 | class ResetPasswordTokenRepository {
5 | private async getResetPasswordToken(email: string) {
6 | try {
7 | const t = await db.resetPasswordToken.findFirst({
8 | where: {
9 | email,
10 | },
11 | });
12 | return t;
13 | } catch (error) {
14 | console.log("Error in getResetPasswordToken", error);
15 | return null;
16 | }
17 | }
18 | async getResetPasswordTokenByToken(token: string) {
19 | try {
20 | const t = await db.resetPasswordToken.findUnique({
21 | where: {
22 | token,
23 | },
24 | });
25 | return t;
26 | } catch (error) {
27 | console.log("Error in getResetPasswordToken", error);
28 | return null;
29 | }
30 | }
31 | async createResetPasswordToken(email: string) {
32 | try {
33 | const exists = await this.getResetPasswordToken(email);
34 | if (exists) {
35 | await db.resetPasswordToken.delete({
36 | where: {
37 | id: exists.id,
38 | },
39 | });
40 | }
41 | // creating a new token
42 | const token = await db.resetPasswordToken.create({
43 | data: {
44 | email,
45 | token: v4.v4(),
46 | expires: new Date(Date.now() + 1000 * 60 * 60),
47 | },
48 | });
49 | return token;
50 | } catch (error) {
51 | console.log("Error in getResetPasswordToken", error);
52 | return null;
53 | }
54 | }
55 |
56 | }
57 | const ResetTokenRepo = new ResetPasswordTokenRepository()
58 | export default ResetTokenRepo
--------------------------------------------------------------------------------
/src/modules/auth/data/user.ts:
--------------------------------------------------------------------------------
1 | import { db } from '@/lib/db';
2 |
3 | class UserRepository {
4 | async getUserByEmail(email: string) {
5 | try {
6 | return await db.user.findUnique({
7 | where: { email },
8 | });
9 | } catch (error) {
10 | console.error('Error fetching user by email:', error);
11 | throw new Error('Could not fetch user by email');
12 | }
13 | }
14 |
15 | async getUserById(id: string) {
16 | try {
17 | return await db.user.findUnique({
18 | where: { id },
19 | });
20 | } catch (error) {
21 | console.error('Error fetching user by ID:', error);
22 | throw new Error('Could not fetch user by ID');
23 | }
24 | }
25 | async verifyUserEmail(id : string){
26 | await db.user.update({
27 | where: { id },
28 | data: {
29 | emailVerified: new Date(),
30 | },
31 | });
32 | }
33 |
34 | }
35 |
36 | const userRepository = new UserRepository();
37 | export default userRepository;
38 |
--------------------------------------------------------------------------------
/src/modules/auth/data/verification-token.ts:
--------------------------------------------------------------------------------
1 | import { db } from "@/lib/db";
2 | import * as v4 from "uuid";
3 |
4 |
5 | export async function getVerificationToken(email: string) {
6 | try {
7 | const t = await db.verificationToken.findFirst({
8 | where: {
9 | identifier : email,
10 |
11 | },
12 | });
13 | return t;
14 | } catch (error) {
15 | console.log("Error in getVerificationToken", error);
16 | return null;
17 | }
18 | }
19 | export async function getVerificationTokenByToken(token: string) {
20 | try {
21 | const t = await db.verificationToken.findFirst({
22 | where: {
23 | token
24 | },
25 | });
26 | return t;
27 | } catch (error) {
28 | console.log("Error in getVerificationToken", error);
29 | return null;
30 | }
31 | }
32 |
33 | export async function createVerificationToken(email: string) {
34 | try {
35 | const exists = await getVerificationToken(email);
36 | if (exists) {
37 | await db.verificationToken.delete({
38 | where: {
39 | identifier_token : {
40 | identifier : exists.identifier,
41 | token : exists.token
42 | }
43 | },
44 | });
45 | }
46 | // creating a new token
47 | const token = await db.verificationToken.create({
48 | data: {
49 | identifier : email,
50 | token: v4.v4(),
51 | expires: new Date(Date.now() + 1000 * 60 * 60),
52 | },
53 | });
54 | return token;
55 | } catch (error) {
56 | console.log("Error in getVerificationToken", error);
57 | return null;
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/src/modules/auth/icons.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | function getIconProps(size? : number, className? : string) {
3 | const defaultSize = 18
4 | return {
5 | width : size || defaultSize,
6 | height : size || defaultSize,
7 | className : className || ''
8 | }
9 | }
10 | export default function GoogleIcon({
11 | size
12 | } : {
13 | size? : number
14 | }) {
15 | const props = getIconProps(size)
16 | return (
17 |
18 | )
19 | }
20 |
21 | export function GithubIcon({
22 | size
23 | } :{
24 | size? : number
25 | }) {
26 | const props = getIconProps(size)
27 |
28 | return (
29 |
30 | )
31 | }
32 |
33 | export function TwitterIcon({
34 | size
35 | } :{
36 | size? : number
37 | }) {
38 | const props = getIconProps(size)
39 |
40 | return (
41 |
42 |
43 |
44 | )
45 | }
46 |
47 |
48 | export function FBIcon({size} : {size : number}) {
49 | const props = getIconProps(size)
50 | return (
51 |
52 |
53 |
54 | )
55 | }
56 |
--------------------------------------------------------------------------------
/src/modules/auth/lib/common.ts:
--------------------------------------------------------------------------------
1 | import { EmailService } from "@/modules/auth/services/mail.sender";
2 | import bcrypt from "bcryptjs";
3 | import { auth } from "@/auth";
4 | export async function sendEmailVerification(email: string, token: string) {
5 | const verificationLink = `http://localhost:3000/auth/email_verify?token=${token}`;
6 | const sender = new EmailService();
7 | await sender.sendVerificationEmail(email, verificationLink);
8 | }
9 |
10 | export const hashMyPassword = async (password: string) => {
11 | return await bcrypt.hash(password, 10);
12 | }
13 |
14 |
15 | export async function sendResetPasswordEmail(email: string, token : string) {
16 | const sender = new EmailService()
17 | const link = `http://localhost:3000/auth/new-password?token=${token}`
18 | await sender.sendResetPasswordEmail(email, link)
19 | }
20 |
21 | export async function currentUser(){
22 | const session = await auth()
23 | return session?.user
24 | }
25 | export async function currentRole(){
26 | const session = await auth()
27 | return session?.user?.role
28 | }
29 |
30 |
31 |
32 |
--------------------------------------------------------------------------------
/src/modules/auth/lib/emailVerifyAction.ts:
--------------------------------------------------------------------------------
1 | "use server";
2 | import { db } from "@/lib/db";
3 | import { getVerificationTokenByToken } from "../data";
4 | import { MessageResponse } from "../types/auth";
5 | import userRepository from "../data/user";
6 |
7 | export async function emailVerifyAction(
8 | token?: string,
9 | ): Promise {
10 | if (!token) {
11 | return {
12 | message: "invaild token",
13 | success: false,
14 | };
15 | }
16 | try {
17 | const dbToken = await getVerificationTokenByToken(token);
18 | if (!dbToken)
19 | return {
20 | message: "invalid request or token may have been expired",
21 | success: false,
22 | };
23 | // check if token is valid
24 | const expired = new Date(dbToken.expires) < new Date();
25 | if (expired)
26 | return {
27 | message: "token has been expired",
28 | success: false,
29 | };
30 | // check if user exists
31 | const userExists = await userRepository.getUserByEmail(dbToken.identifier);
32 |
33 | if (!userExists)
34 | return {
35 | message: "user not found, try to signup first",
36 | success: false,
37 | };
38 |
39 | await db.user.update({
40 | where: {
41 | id: userExists.id,
42 | },
43 | data: {
44 | emailVerified: new Date(),
45 | email: dbToken.identifier,
46 | },
47 | });
48 |
49 |
50 | return {
51 | message: "Email Verified Sucessfully",
52 | success: true,
53 | };
54 | } catch (error) {
55 | console.log(error);
56 |
57 | return {
58 | message: "something went wrong",
59 | success: false,
60 | };
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/src/modules/auth/lib/forgot-password.ts:
--------------------------------------------------------------------------------
1 | "use server"
2 |
3 | import { ForgotPasswordSchema, ForgotPasswordSchemaType, ResetPasswordSchema, ResetPasswordSchemaType } from "../auth.schema";
4 | import { MessageResponse } from "../types/auth";
5 | import { db } from "@/lib/db";
6 | import { hashMyPassword, sendResetPasswordEmail } from "./common";
7 | import ResetTokenRepo from "../data/resetpassword-token";
8 | import userRepository from "../data/user";
9 | import { reCaptchaSiteVerify } from "./recaptcha";
10 | import { CaptchaActionOptions } from "../types/captcha";
11 |
12 |
13 | export async function forgotPasswordAction( data: ForgotPasswordSchemaType, captchaOptions : CaptchaActionOptions) : Promise {
14 | try {
15 | const validate = ForgotPasswordSchema.safeParse(data);
16 | if (!validate.success) {
17 | return {
18 | message: validate.error.errors[0].message || "Invalid email",
19 | success: false,
20 | };
21 | }
22 | const googleResponse = await reCaptchaSiteVerify(captchaOptions);
23 |
24 | if (!googleResponse.success) {
25 | return {
26 | message: googleResponse.message,
27 | success: false,
28 | };
29 | }
30 | const { email } = validate.data;
31 | // send email with reset password link
32 | const existingUser = await userRepository.getUserByEmail(email);
33 | if (!existingUser) {
34 | return {
35 | message: "user may not exist or verified",
36 | success: false,
37 | };
38 | }
39 | if (!existingUser?.password) {
40 | // means the user signed up with social media
41 | return {
42 | message : "this account requires social media login or it does not exist",
43 | success: false
44 | }
45 | }
46 | const token = await ResetTokenRepo.createResetPasswordToken(email);
47 | if (!token) {
48 | return {
49 | message: "Something went wrong",
50 | success: false,
51 | };
52 | }
53 | await sendResetPasswordEmail(email, token?.token);
54 |
55 | return {
56 | message: "Email Sent Successfully",
57 | success: true,
58 | };
59 | } catch (error) {
60 | return {
61 | message: "Something went wrong",
62 | success: false,
63 | }
64 | }
65 | }
66 |
67 |
68 |
69 |
70 |
71 | export async function resetPasswordAction(token: string, data : ResetPasswordSchemaType) : Promise {
72 | try {
73 |
74 |
75 | const validate = ResetPasswordSchema.safeParse(data);
76 | if (!validate.success) {
77 | return {
78 | message: validate.error.errors[0].message || "Invalid password",
79 | success: false,
80 | };
81 | }
82 | const {
83 | password,
84 | confirmPassword
85 | } = validate.data;
86 |
87 | if (password !== confirmPassword) {
88 | return {
89 | message: "Passwords do not match",
90 | success: false,
91 | };
92 | }
93 | const tokenExists = await ResetTokenRepo.getResetPasswordTokenByToken(token);
94 |
95 | if (!tokenExists) {
96 | return {
97 | message: "Invalid token",
98 | success: false,
99 | };
100 | }
101 |
102 | // update password
103 | const user = await userRepository.getUserByEmail(tokenExists.email);
104 | if (!user) {
105 | return {
106 | message: "user may not exist or verified",
107 | success: false,
108 | };
109 | }
110 | const hashedPassword = await hashMyPassword(password);
111 | await db.user.update({
112 | where: {
113 | email: tokenExists.email,
114 | },
115 | data: {
116 | password: hashedPassword,
117 | },
118 | })
119 |
120 |
121 |
122 | return {
123 | message: "Password Updated Successfully",
124 | success: true,
125 | }
126 | } catch (error) {
127 | return {
128 | message: "Something went wrong",
129 | success: false
130 | }
131 | }
132 | }
--------------------------------------------------------------------------------
/src/modules/auth/lib/index.ts:
--------------------------------------------------------------------------------
1 | export * from './common'
--------------------------------------------------------------------------------
/src/modules/auth/lib/recaptcha.ts:
--------------------------------------------------------------------------------
1 | "use server";
2 | import { recaptcha_config } from "@/constants";
3 | import { CaptchaActionOptions } from "../types/captcha";
4 |
5 | interface GoogleCaptchaResponse {
6 | success: boolean;
7 | challenge_ts: string; // '2024-09-05T18:44:32Z'
8 | hostname: string;
9 | action: string;
10 | score: number;
11 | }
12 |
13 | export async function reCaptchaSiteVerify({
14 | token,
15 | action,
16 | tokenExpiryMs = 120000, //ms
17 | }: CaptchaActionOptions) {
18 | try {
19 |
20 | if (!token) {
21 | console.error("Captcha token missing");
22 | return {
23 | message: "Captcha verification failed. Please try again.",
24 | success: false,
25 | };
26 | }
27 | const url = `https://www.google.com/recaptcha/api/siteverify?secret=${recaptcha_config.secret}&response=${token}`;
28 | const res = await fetch(url, { method: "POST" });
29 | const googleResponse: GoogleCaptchaResponse = await res.json();
30 |
31 | if (
32 | !googleResponse ||
33 | googleResponse.score < 0.5 ||
34 | googleResponse.action !== action
35 | ) {
36 | console.error("Captcha validation failed", googleResponse);
37 | return {
38 | message: "Captcha validation failed. Please try again.",
39 | success: false,
40 | };
41 | }
42 |
43 | // const tokenTimestamp = new Date(googleResponse.challenge_ts).getTime();
44 | // const currentTime = Date.now();
45 | // const tokenAge = currentTime - tokenTimestamp;
46 | // if (tokenAge > tokenExpiryMs) {
47 | // return { success: false, message: "Captcha token expired." };
48 | // }
49 |
50 | return { success: true, message: "Captcha verified successfully." };
51 | } catch (error) {
52 | console.error("Error in CAPTCHA verification", error);
53 | return {
54 | message: "CAPTCHA verification encountered an error.",
55 | success: false,
56 | };
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/src/modules/auth/lib/signin-action.ts:
--------------------------------------------------------------------------------
1 | "use server";
2 | import { db } from "@/lib/db";
3 | import { signIn } from "@/auth";
4 | import { AuthError } from "next-auth";
5 | import { MessageResponse } from "../types/auth";
6 | import { sendEmailVerification } from "./common";
7 |
8 | export async function signInAction(
9 | data: LoginSchemaType, captchaOptions : CaptchaActionOptions
10 | ): Promise {
11 |
12 | const validate = await LoginSchema.safeParseAsync(data);
13 | if (!validate.success) {
14 | return {
15 | message: validate.error.errors[0].message,
16 | success: false,
17 | };
18 | }
19 | const googleResponse = await reCaptchaSiteVerify(captchaOptions);
20 |
21 | if (!googleResponse.success) {
22 | return {
23 | message: googleResponse.message,
24 | success: false,
25 | };
26 | }
27 | const { email, password } = validate.data;
28 | const user = await db.user.findUnique({
29 | where: { email },
30 | });
31 | const ERROR_INVALID_CREDENTIALS = "Invalid credentials";
32 | // if user is not found or email or password is not provided
33 | if (!user || !user.email || !user.password) {
34 | return {
35 | message: ERROR_INVALID_CREDENTIALS,
36 | success: false,
37 | };
38 | }
39 | // if user is not verified
40 | if (!user.emailVerified) {
41 | const token = await createVerificationToken(email);
42 | if (!token) return { message: "Something went wrong!", success: false };
43 | await sendEmailVerification(email, token?.token);
44 | return {
45 | message: "Confirmation Email Sent",
46 | success: true,
47 | };
48 | }
49 | try {
50 | await signIn("credentials", {
51 | email,
52 | password,
53 | });
54 |
55 | return {
56 | message: "Sign In Sucessfully",
57 | success: true,
58 | };
59 | } catch (error) {
60 | if (error instanceof AuthError) {
61 | switch (error.type) {
62 | case "CredentialsSignin":
63 | return { message: ERROR_INVALID_CREDENTIALS, success: false };
64 | default:
65 | return { message: "Something went wrong!", success: false };
66 | }
67 | }
68 | throw error;
69 | }
70 | }
71 |
72 | import { signOut } from "@/auth";
73 | import { LoginSchema, LoginSchemaType } from "../auth.schema";
74 | import { createVerificationToken } from "../data";
75 | import { CaptchaActionOptions } from "../types/captcha";
76 | import { reCaptchaSiteVerify } from "./recaptcha";
77 |
78 | export { signOut };
79 |
80 |
--------------------------------------------------------------------------------
/src/modules/auth/lib/signin_magic-action.ts:
--------------------------------------------------------------------------------
1 | "use server";
2 | import {
3 | isRedirectError,
4 | RedirectError,
5 | } from "next/dist/client/components/redirect";
6 | import { HOST } from "@/constants";
7 | import { reCaptchaSiteVerify } from "./recaptcha";
8 | import { MagicSignInType } from "../auth.schema";
9 | import { signIn } from "@/auth";
10 | import { CaptchaActionOptions } from "../types/captcha";
11 |
12 | export async function signinMagic(
13 | data: MagicSignInType,
14 | captchaOptions : CaptchaActionOptions
15 | ) {
16 | try {
17 |
18 | const googleResponse = await reCaptchaSiteVerify(captchaOptions);
19 |
20 | if (!googleResponse.success) {
21 | return {
22 | message: googleResponse.message,
23 | success: false,
24 | };
25 | }
26 | await signIn("http-email", { email: data.email });
27 |
28 |
29 | return {
30 | message: `An email link has been sent to ${data.email}. Please check your inbox.`,
31 | success: true,
32 | };
33 | } catch (error) {
34 | if (isRedirectError(error)) handleSignInRedirectError(error, data);
35 | return {
36 | message: error instanceof Error ? error.message : "Failed to Send Email",
37 | success: false,
38 | };
39 | }
40 | }
41 |
42 | const handleSignInRedirectError = (
43 | error: RedirectError,
44 | data: MagicSignInType
45 | ) => {
46 | let digest = error.digest;
47 | const replace = "NEXT_REDIRECT;replace;";
48 | const isNotFound = replace + HOST + "/auth/signup;303;" === error.digest;
49 | const successDigest =
50 | replace +
51 | HOST +
52 | "/api/auth/verify-request?provider=http-email&type=email;303;";
53 | const isSuccess = error.digest === successDigest;
54 | if (isNotFound)
55 | digest = replace + `${HOST}/auth/signup?callbackError=badSignInEmail&email=${data.email}&at=${(new Date).toLocaleTimeString()};303`;
56 | else if (isSuccess) {
57 | digest =
58 | replace +
59 | `${HOST}/auth/verify-request?email=${data.email}&provider=http-email&type=email;303`;
60 | }
61 |
62 | let err = { ...error, digest };
63 | throw err;
64 | };
65 |
--------------------------------------------------------------------------------
/src/modules/auth/lib/signup-action.ts:
--------------------------------------------------------------------------------
1 | "use server";
2 | import { db } from "@/lib/db";
3 | import { MessageResponse } from "../types/auth";
4 | import { hashMyPassword, sendEmailVerification } from "./common";
5 | import { SignupSchema, SignupSchemaType } from "../auth.schema";
6 | import { createVerificationToken } from "../data";
7 | import { reCaptchaSiteVerify } from "./recaptcha";
8 | import { CaptchaActionOptions } from "../types/captcha";
9 |
10 | export async function signUpAction(
11 | data: SignupSchemaType,captchaOptions : CaptchaActionOptions
12 | ): Promise {
13 | const validate = SignupSchema.safeParse(data);
14 |
15 | if (!validate.success) {
16 | return {
17 | message: validate.error.errors[0].message || "Invalid credentials",
18 | success: false,
19 | };
20 | }
21 | const googleResponse = await reCaptchaSiteVerify(captchaOptions);
22 |
23 | if (!googleResponse.success) {
24 | return {
25 | message: googleResponse.message,
26 | success: false,
27 | };
28 | }
29 |
30 | const { email, password, name } = validate.data;
31 | try {
32 | const userExists = await db.user.findUnique({
33 | where: { email },
34 | });
35 | if (userExists)
36 | return {
37 | message: "User already exists",
38 | success: false,
39 | };
40 |
41 | // Hash the password
42 | const hashedPassword = await hashMyPassword(password);
43 | await db.user.create({
44 | data: {
45 | email,
46 | password: hashedPassword,
47 | name,
48 | },
49 | });
50 | const token = await createVerificationToken(email);
51 | if (!token) return { message: "Something went wrong!", success: false };
52 | await sendEmailVerification(email, token?.token);
53 | return { message: "Confirmation Email Sent", success: true };
54 | } catch (error) {
55 | console.error(error);
56 | return {
57 | message: "An error occurred during sign up",
58 | success: false,
59 | };
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/src/modules/auth/sendRequest.ts:
--------------------------------------------------------------------------------
1 | import { NodemailerConfig } from "next-auth/providers/nodemailer";
2 | import { EmailService } from "./services/mail.sender";
3 | import { logoUrl } from "@/constants";
4 | import { db } from "@/lib/db";
5 |
6 | export interface EmailTheme {
7 | colorScheme?: "auto" | "dark" | "light";
8 | logo?: string;
9 | brandColor?: string;
10 | buttonText?: string;
11 | }
12 |
13 | export async function sendVerificationRequest(params: {
14 | identifier: string;
15 | url: string;
16 | expires: Date;
17 | provider: NodemailerConfig;
18 | token: string;
19 | theme: EmailTheme;
20 | request: Request;
21 | }) {
22 |
23 | const { identifier, url, provider, theme } = params;
24 | const { host } = new URL(url);
25 | const service = new EmailService();
26 | const transport = service.getTransport();
27 | await db.verificationToken.deleteMany({
28 | where: {
29 | identifier,
30 | token: { not: params.token },
31 | },
32 | });
33 | try {
34 | const result = await transport.sendMail({
35 | to: identifier,
36 | from: provider.from,
37 | subject: `Sign in to ${host}`,
38 | text: generateText({ url, host }),
39 | html: generateHTML({ url, host, theme }),
40 | });
41 |
42 | const failed = result.rejected.concat(result.pending).filter(Boolean);
43 | if (failed.length) {
44 | throw new Error(`Email(s) (${failed.join(", ")}) could not be sent`);
45 | }
46 | } catch (error) {
47 | console.error("Error sending verification email:", error);
48 | // throw error;
49 | }
50 | }
51 | function generateHTML(params: { url: string; host: string; theme: EmailTheme }) {
52 | const { url, host, theme } = params;
53 | const escapedHost = host.replace(/\./g, ".");
54 |
55 | const brandColor = theme.brandColor || "#4F46E5"; // A modern shade of blue (indigo-600 from Tailwind)
56 | const color = {
57 | background: "#f3f4f6", // Slightly darker gray for better contrast
58 | text: "#333333", // Darker text color for improved readability
59 | mainBackground: "#ffffff",
60 | buttonBackground: brandColor,
61 | buttonBorder: brandColor,
62 | buttonText: theme.buttonText || "#ffffff",
63 | footerText: "#9CA3AF", // Neutral gray for footer text
64 | };
65 |
66 | return `
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 | ${` `}
75 | Sign in to ${escapedHost}
76 |
77 |
78 |
79 |
80 |
81 | Click the button below to sign in. This link is only valid for the next 24 hours.
82 |
83 |
85 | Sign in
86 |
87 |
88 |
89 |
90 |
91 | If you did not request this email, you can safely ignore it.
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 | `;
100 | }
101 |
102 | function generateText({ url, host }: { url: string; host: string }) {
103 | return `Sign in to ${host}\n${url}\n\nIf you did not request this email, you can safely ignore it.\nThis link is only valid for the next 24 hours.`;
104 | }
105 |
--------------------------------------------------------------------------------
/src/modules/auth/services/mail.sender.ts:
--------------------------------------------------------------------------------
1 | import * as nodemailer from "nodemailer";
2 | import { TemplateService } from "@/modules/auth/services/template-service";
3 |
4 | export class EmailService {
5 | private transporter: nodemailer.Transporter;
6 |
7 | constructor() {
8 | this.transporter = nodemailer.createTransport({
9 | service: "gmail",
10 | auth: {
11 | user: process.env.GMAIL_SENDER_EMAIL,
12 | pass: process.env.GMAIL_SENDER_PASSWORD,
13 | },
14 | });
15 | }
16 |
17 | getTransport(){
18 | return this.transporter
19 | }
20 |
21 | async sendResetPasswordEmail(email: string, verificationLink: string): Promise {
22 | const year = "2024"
23 | const template = await TemplateService.getTemplate("reset-password.html", { verificationLink, year });
24 |
25 | const mailOptions = {
26 | from: process.env.GMAIL_SENDER_EMAIL,
27 | to: email,
28 | subject: "Resetting Password",
29 | html: template, // Use HTML template instead of plain text
30 | };
31 |
32 | const res = await this.transporter.sendMail(mailOptions);
33 |
34 | return res.accepted.length > 0;
35 | }
36 | async sendVerificationEmail(email: string, verificationLink: string): Promise {
37 | const year = "2024"
38 | const template = await TemplateService.getTemplate("verification.html", { verificationLink, year });
39 |
40 | const mailOptions = {
41 | from: "emailverification@yourapp.com",
42 | to: email,
43 | subject: "Email Verification",
44 | html: template, // Use HTML template instead of plain text
45 | };
46 |
47 | const res = await this.transporter.sendMail(mailOptions);
48 |
49 | return res.accepted.length > 0;
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/src/modules/auth/services/template-service.ts:
--------------------------------------------------------------------------------
1 | import * as fs from "fs";
2 | import * as path from "path";
3 | import { promisify } from "util";
4 |
5 | type TemplateFileName = "verification" | "reset-password";
6 |
7 |
8 | const readFileAsync = promisify(fs.readFile);
9 | export class TemplateService {
10 | static async getTemplate(fileName: `${TemplateFileName}.html`, replacements: { [key: string]: string }): Promise {
11 | const manualResolve = "src/modules/auth/services/templates"
12 | const filePath = path.join(process.cwd(), manualResolve, fileName);
13 | let template = await readFileAsync(filePath, "utf-8");
14 |
15 | for (const [key, value] of Object.entries(replacements)) {
16 | template = template.replace(new RegExp(`{{${key}}}`, "g"), value);
17 | }
18 |
19 | return template;
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/src/modules/auth/services/templates/reset-password.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Alert! Reset Your Password
7 |
8 |
9 |
10 |
11 |
12 | Resetting Your Password
13 | You have sent a request to reset your password.
14 | Reset Password
15 | Do not share this with anyone, if you have not requested this , ignore this mail fully and report us
16 | © {{year}} App Inc. All rights reserved.
17 |
18 |
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/src/modules/auth/services/templates/verification.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Email Verification
7 |
8 |
9 |
10 |
11 |
12 | Email Verification Required
13 | Thank you for registering with us! To complete your registration, please verify your email address by clicking the button below.
14 | Verify Email Address
15 | If you did not create an account with us, please disregard this email.
16 | © {{year}} Ecommerce Inc. All rights reserved.
17 |
18 |
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/src/modules/auth/types/auth.d.ts:
--------------------------------------------------------------------------------
1 | export interface MessageResponse {
2 | message: string;
3 | success: boolean;
4 | }
5 | import { DefaultSession} from "next-auth"
6 | import { UserRole } from "@prisma/client"
7 |
8 | /**
9 | * Here you can extend your session and auth types
10 | */
11 |
12 | type SessionUser = DefaultSession["user"] & {
13 | role : UserRole
14 | }
15 | declare module "next-auth" {
16 | interface Session extends DefaultSession {
17 | user : SessionUser
18 | }
19 | }
20 |
21 |
22 |
--------------------------------------------------------------------------------
/src/modules/auth/types/captcha.ts:
--------------------------------------------------------------------------------
1 | export interface CaptchaActionOptions {
2 | action?: string;
3 | tokenExpiryMs?: number;
4 | token? : string
5 | }
--------------------------------------------------------------------------------
/src/modules/auth/useAuthProviders.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 | import { useMemo } from "react"
3 | import GoogleIcon, { FBIcon, GithubIcon } from './icons';
4 | import { availableProviders } from "./auth.config";
5 | export default function useAuthProviders() {
6 | const providers: {
7 | name: string,
8 | id: availableProviders,
9 | Icon: any
10 | }[] = useMemo(() => [
11 | {
12 | name: 'Google',
13 | id: 'google',
14 | Icon: GoogleIcon,
15 | },
16 | {
17 | name: 'Github',
18 | id: 'github',
19 | Icon: GithubIcon
20 | }
21 | , {
22 | name: 'facebook',
23 | id: 'facebook',
24 | Icon: FBIcon
25 | }
26 |
27 | ], [])
28 | return providers
29 | }
30 |
--------------------------------------------------------------------------------
/src/providers/ThemeProvider.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { ThemeProvider } from 'next-themes'
3 |
4 | export default function NextThemeProvider({ children }: { children: React.ReactNode }) {
5 | return (
6 |
12 |
13 | {children}
14 |
15 |
16 | )
17 | }
18 |
19 |
20 |
--------------------------------------------------------------------------------
/src/providers/index.tsx:
--------------------------------------------------------------------------------
1 | import { SessionProvider } from 'next-auth/react'
2 | import React from 'react'
3 | import NextThemeProvider from './ThemeProvider'
4 | import { TooltipProvider } from '@/components/ui/tooltip'
5 |
6 | export default function Provider({
7 | children
8 | }: {
9 | children: React.ReactNode
10 | }) {
11 | return (
12 |
13 |
14 |
15 | {children}
16 |
17 |
18 |
19 |
20 | )
21 | }
22 |
--------------------------------------------------------------------------------
/src/routes.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Routes accessible to the public are listed here in an array
3 | * @type {string[]}
4 | */
5 | export const publicRoutes: string[] = [
6 | '/home',
7 | ]
8 |
9 | /**
10 | * Routes for authentication are listed here in an array
11 | * @type {string[]}
12 | */
13 | export const authRoutes: string[] = [
14 | '/auth/signin', '/auth/signup', '/auth/forgot-password', '/auth/new-password', "/auth/email_verify", "/auth/verify-request", "/auth-error",
15 | ]
16 | /**
17 | * Routes start with the api are used for api auth purpose
18 | * @type {string}
19 | */
20 | export const apiAuthPrefix: string = '/api/auth';
21 |
22 | /**
23 | * The default route to redirect to after login
24 | */
25 | export const DEFAULT_LOGIN_REDIRECT = '/dashboard';
26 |
27 |
28 |
29 |
--------------------------------------------------------------------------------
/src/types/types.ts:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codersaadi/next-auth5-shadcn/bec70b4a330b4212f774b5f7c7ef68f02f43b10e/src/types/types.ts
--------------------------------------------------------------------------------
/tailwind.config.ts:
--------------------------------------------------------------------------------
1 | import type { Config } from "tailwindcss"
2 |
3 | const config = {
4 | darkMode: ["class"],
5 | content: [
6 | './pages/**/*.{ts,tsx}',
7 | './components/**/*.{ts,tsx}',
8 | './app/**/*.{ts,tsx}',
9 | './src/**/*.{ts,tsx}',
10 | ],
11 | prefix: "",
12 | theme: {
13 | container: {
14 | center: true,
15 | padding: "2rem",
16 | screens: {
17 | "2xl": "1400px",
18 | },
19 | },
20 | extend: {
21 | colors: {
22 | border: "hsl(var(--border))",
23 | input: "hsl(var(--input))",
24 | ring: "hsl(var(--ring))",
25 | background: "hsl(var(--background))",
26 | foreground: "hsl(var(--foreground))",
27 | primary: {
28 | DEFAULT: "hsl(var(--primary))",
29 | foreground: "hsl(var(--primary-foreground))",
30 | },
31 | secondary: {
32 | DEFAULT: "hsl(var(--secondary))",
33 | foreground: "hsl(var(--secondary-foreground))",
34 | },
35 | destructive: {
36 | DEFAULT: "hsl(var(--destructive))",
37 | foreground: "hsl(var(--destructive-foreground))",
38 | },
39 | muted: {
40 | DEFAULT: "hsl(var(--muted))",
41 | foreground: "hsl(var(--muted-foreground))",
42 | },
43 | accent: {
44 | DEFAULT: "hsl(var(--accent))",
45 | foreground: "hsl(var(--accent-foreground))",
46 | },
47 | popover: {
48 | DEFAULT: "hsl(var(--popover))",
49 | foreground: "hsl(var(--popover-foreground))",
50 | },
51 | card: {
52 | DEFAULT: "hsl(var(--card))",
53 | foreground: "hsl(var(--card-foreground))",
54 | },
55 | },
56 | borderRadius: {
57 | lg: "var(--radius)",
58 | md: "calc(var(--radius) - 2px)",
59 | sm: "calc(var(--radius) - 4px)",
60 | },
61 | keyframes: {
62 | "accordion-down": {
63 | from: { height: "0" },
64 | to: { height: "var(--radix-accordion-content-height)" },
65 | },
66 | "accordion-up": {
67 | from: { height: "var(--radix-accordion-content-height)" },
68 | to: { height: "0" },
69 | },
70 | },
71 | animation: {
72 | "accordion-down": "accordion-down 0.2s ease-out",
73 | "accordion-up": "accordion-up 0.2s ease-out",
74 | },
75 | },
76 | },
77 | plugins: [require("tailwindcss-animate")],
78 | } satisfies Config
79 |
80 | export default config
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "lib": ["dom", "dom.iterable", "esnext"],
4 | "allowJs": true,
5 | "skipLibCheck": true,
6 | "strict": true,
7 | "noEmit": true,
8 | "esModuleInterop": true,
9 | "module": "esnext",
10 | "moduleResolution": "bundler",
11 | "resolveJsonModule": true,
12 | "isolatedModules": true,
13 | "jsx": "preserve",
14 | "incremental": true,
15 | "plugins": [
16 | {
17 | "name": "next"
18 | }
19 | ],
20 | "paths": {
21 | "@/*": ["./src/*"]
22 | }
23 | },
24 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
25 | "exclude": ["node_modules"]
26 | }
27 |
--------------------------------------------------------------------------------