├── .env.example
├── .eslintignore
├── .eslintrc.json
├── .gitignore
├── .husky
└── pre-commit
├── .huskyrc
├── .idx
└── dev.nix
├── .lintstagedrc
├── .prettierignore
├── .prettierrc
├── .vscode
└── settings.json
├── LICENSE
├── README.md
├── bun.lockb
├── components.json
├── drizzle.config.ts
├── drizzle
├── 0000_closed_lady_vermin.sql
├── 0001_brainy_domino.sql
├── 0002_naive_sunspot.sql
├── 0003_amazing_stranger.sql
├── 0004_passkey.sql
├── 0005_equal_havok.sql
└── meta
│ ├── 0000_snapshot.json
│ ├── 0001_snapshot.json
│ ├── 0002_snapshot.json
│ ├── 0003_snapshot.json
│ ├── 0004_snapshot.json
│ ├── 0005_snapshot.json
│ └── _journal.json
├── emails
├── InviteAdminEmail.tsx
├── addPasswordEmail.tsx
├── forgotPasswordEmail.tsx
├── twoFactorVerificationEmail.tsx
└── verificationEmail.tsx
├── next.config.ts
├── package.json
├── postcss.config.cjs
├── public
├── authjs-template.png
├── authjs.webp
├── drizzle.jpg
├── magicui.png
├── nextjs.svg
├── placeholder-user.jpg
├── react_dark.svg
├── resend.jpg
├── turso.jpg
├── turso.svg
└── vercel.svg
├── src
├── actions
│ ├── admin
│ │ ├── addAdmin.ts
│ │ └── index.ts
│ └── auth
│ │ ├── delete.ts
│ │ ├── index.ts
│ │ ├── mfa.ts
│ │ ├── oauth.ts
│ │ ├── password.ts
│ │ ├── signin.ts
│ │ └── signup.ts
├── app
│ ├── (auth)
│ │ ├── add-admin
│ │ │ ├── AddAdminForm.tsx
│ │ │ ├── page.tsx
│ │ │ └── verify
│ │ │ │ └── page.tsx
│ │ ├── add-password
│ │ │ ├── addPasswordForm.tsx
│ │ │ └── page.tsx
│ │ ├── error
│ │ │ └── page.tsx
│ │ ├── forgot-password
│ │ │ └── page.tsx
│ │ ├── onboarding
│ │ │ ├── OnBoardingForm.tsx
│ │ │ └── page.tsx
│ │ ├── reset-password
│ │ │ ├── page.tsx
│ │ │ └── resetPasswordForm.tsx
│ │ ├── sign-in
│ │ │ ├── SignInForm.tsx
│ │ │ ├── page.tsx
│ │ │ └── two-factor
│ │ │ │ ├── email
│ │ │ │ ├── emailVerifyForm.tsx
│ │ │ │ └── page.tsx
│ │ │ │ ├── otp.tsx
│ │ │ │ └── page.tsx
│ │ └── sign-up
│ │ │ ├── SignUpForm.tsx
│ │ │ └── page.tsx
│ ├── (dashboard)
│ │ ├── dashboard
│ │ │ └── page.tsx
│ │ └── profile
│ │ │ ├── _Components
│ │ │ ├── AddPasswordButton.tsx
│ │ │ ├── DeleteAccountButton.tsx
│ │ │ ├── DisableTwoFactorButton.tsx
│ │ │ ├── EditProfileForm.tsx
│ │ │ ├── LinkAccountButton.tsx
│ │ │ └── UnlinkAccountButton.tsx
│ │ │ ├── change-password
│ │ │ ├── changePasswordForm.tsx
│ │ │ └── page.tsx
│ │ │ ├── page.tsx
│ │ │ └── two-factor
│ │ │ ├── TwoFactorForm.tsx
│ │ │ └── page.tsx
│ ├── admin
│ │ ├── columns.tsx
│ │ ├── data-table-pagination.tsx
│ │ ├── data-table.tsx
│ │ └── page.tsx
│ ├── api
│ │ └── auth
│ │ │ └── [...nextauth]
│ │ │ └── route.ts
│ ├── favicon.ico
│ ├── globals.css
│ ├── layout.tsx
│ └── page.tsx
├── auth.ts
├── components
│ ├── AuthButton.tsx
│ ├── Hero.tsx
│ ├── Navbar.tsx
│ ├── SubmitButton.tsx
│ ├── ThemeToggle.tsx
│ ├── TokenNotFound.tsx
│ ├── WebAuthnButton.tsx
│ ├── icons.tsx
│ └── ui
│ │ ├── avatar.tsx
│ │ ├── button.tsx
│ │ ├── card.tsx
│ │ ├── checkbox.tsx
│ │ ├── dialog.tsx
│ │ ├── dot-pattern.tsx
│ │ ├── dropdown-menu.tsx
│ │ ├── input-otp.tsx
│ │ ├── input.tsx
│ │ ├── label.tsx
│ │ ├── orbiting-circles.tsx
│ │ ├── radial-gradient.tsx
│ │ ├── select.tsx
│ │ ├── sonner.tsx
│ │ ├── sparkles-text.tsx
│ │ └── table.tsx
├── db
│ ├── index.ts
│ ├── query
│ │ ├── Token.ts
│ │ └── User.ts
│ └── schema.ts
├── lib
│ ├── Email.ts
│ ├── resend.ts
│ └── utils.ts
└── middleware.ts
├── tailwind.config.ts
└── tsconfig.json
/.env.example:
--------------------------------------------------------------------------------
1 | # Turso configuration
2 | TURSO_DATABASE_URL=YOUR_DATABASE_URL
3 | TURSO_AUTH_TOKEN=YOUR_DATABASE_AUTH_TOKEN
4 |
5 | # Authjs configuration
6 | AUTH_SECRET=
7 |
8 | AUTH_GOOGLE_ID=
9 | AUTH_GOOGLE_SECRET=
10 |
11 | AUTH_GITHUB_ID=
12 | AUTH_GITHUB_SECRET=
13 |
14 | # Resend
15 | RESEND_API_KEY=
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | .vscode
3 | public
4 | .next
5 | drizzle
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": ["next/core-web-vitals", "eslint:recommended", "next", "prettier"],
3 | "plugins": ["prettier"],
4 | "rules": {
5 | "prettier/prettier": ["error", { "endOfLine": "auto" }]
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 | .yarn/install-state.gz
8 |
9 | # testing
10 | /coverage
11 |
12 | # next.js
13 | /.next/
14 | /out/
15 |
16 | # production
17 | /build
18 |
19 | # misc
20 | .DS_Store
21 | *.pem
22 |
23 | # debug
24 | npm-debug.log*
25 | yarn-debug.log*
26 | yarn-error.log*
27 |
28 | # local env files
29 | .env*.local
30 |
31 | # vercel
32 | .vercel
33 |
34 | # typescript
35 | *.tsbuildinfo
36 | next-env.d.ts
37 |
--------------------------------------------------------------------------------
/.husky/pre-commit:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 | . "$(dirname -- "$0")/_/husky.sh"
3 |
4 | npx lint-staged
--------------------------------------------------------------------------------
/.huskyrc:
--------------------------------------------------------------------------------
1 | {
2 | "hooks": {
3 | "pre-commit": "lint-staged"
4 | }
5 | }
--------------------------------------------------------------------------------
/.idx/dev.nix:
--------------------------------------------------------------------------------
1 | { pkgs }: {
2 | channel = "stable-23.11";
3 | packages = [
4 | pkgs.bun
5 | pkgs.nodejs_20
6 | ];
7 | idx.extensions = [
8 | "bradlc.vscode-tailwindcss"
9 | "esbenp.prettier-vscode"
10 | "streetsidesoftware.code-spell-checker"
11 | "WakaTime.vscode-wakatime"
12 | ];
13 | }
14 |
--------------------------------------------------------------------------------
/.lintstagedrc:
--------------------------------------------------------------------------------
1 | {
2 | "*/**/*.{js,jsx,ts,tsx}": ["prettier --write", "eslint --fix", "eslint"],
3 | "*/**/*.{json,css,md,mdx}": ["prettier --write"]
4 | }
5 |
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | bun.lock
2 | package-lock.json
3 | node_modules
4 | .next
5 | .cache
6 | .vscode
7 | .husky
8 | public
9 | next-env.d.ts
10 | next.config.ts
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "bracketSpacing": true,
3 | "trailingComma": "all",
4 | "arrowParens": "always",
5 | "tabWidth": 2,
6 | "printWidth": 80,
7 | "semi": true,
8 | "singleQuote": true,
9 | "jsxSingleQuote": true,
10 | "plugins": ["prettier-plugin-tailwindcss"]
11 | }
12 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "IDX.aI.enableCodebaseIndexing": false,
3 | "IDX.aI.enableInlineCompletion": false
4 | }
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024 VIVEK PATEL
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # AuthJs Template
2 |
3 |

4 |
5 | > Updated to nextjs15 and react19.
6 |
7 | A template repo for starting authentication in your upcoming Next.js application. Used Drizzle, Auth.js(Next-Auth), Turso.
8 |
9 | 
10 |
11 | ## What's inside?
12 |
13 | - How to setup Credentials login with username/password in NextJs.
14 | - How to setup Two factor in NextJs.
15 | - How to setup Passkey in NextJs.
16 | - How to setup Oauth Login in NextJs.
17 | - How to Link accounts in NextJs.
18 |
19 | All of these with Next-auth(AuthJs v5) and Nextjs 15 App router
20 |
21 | ---
22 |
23 | ## Basic
24 |
25 | - [x] Register | Public
26 | - [x] Login | Public
27 | - [x] Credentials Login | Public
28 | - [x] Social Login(Google, Github) | Public
29 | - [x] Email Verification | Public
30 |
31 | ## Password Reset
32 |
33 | - [x] Password Reset (email link) | Public
34 | - [x] change password(Email user can change password) | Protected
35 | - [x] Add Password(Oauth user can set Password.) | Protected
36 |
37 | ## Profile Update
38 |
39 | - [ ] Profile Update | Protected
40 | - [ ] Profile Picture Update | Protected
41 | - [ ] Email Update | Protected
42 | - [ ] Username Update | Protected
43 | - [x] Delete Account | Protected
44 |
45 | ## Link Accounts
46 |
47 | - [x] Account linking | Protected
48 | - [x] Account Unlinking | Protected
49 |
50 | ## Two Factor Authentication
51 |
52 | - [x] Two Factor - Register with QRCode | Protected
53 | - [x] Two Factor - Verify after register | Protected
54 | - [x] Two Factor - Used After login(Oauth as well as Credentials) | Public
55 | - [x] Add Backup options(Verify using email)
56 | - [x] Disable Two Factor.
57 |
58 | ## Passkey/ Passwordless Login
59 |
60 | - [x] Passkey/ Passwordless Login
61 |
62 | ## Role Based Access
63 |
64 | - [x] Role Based Access | Protected
65 |
66 | > Here, I used my domain to select the admin user. After that, the admin can add another admin
67 |
68 | ## User Management (Admin)
69 |
70 | - [x] Admin Dashboard
71 | - [ ] Permission
72 |
73 | ## Database sessions
74 |
75 | - [x] Database sessions checkout [db-session](https://github.com/patelvivekdev/drizzle-next-auth-turso/tree/db-session)
76 |
77 | ## Emails
78 |
79 | - [ ] Send an email for every account change event and store the history.
80 |
81 | ## Links
82 |
83 | - [Live Link JWT Sessions](https://authjs-template.patelvivek.dev)
84 | - [Live Link DB Sessions](https://authjs-template-db-sessions.patelvivek.dev)
85 |
86 | - [AuthJS](https://authjs.dev)
87 | - [Drizzle](https://drizzle.team)
88 | - [Turso](https://turso.dev)
89 | - [NextJs](https://nextjs.org)
90 | - [Magic Ui](https://magicui.design)
91 | - [patelvivek.dev](https://patelvivek.dev/projects/authjs-template-for-nextjs-developers)
92 |
93 | ## Open In IDX
94 |
95 |
96 |
97 |
98 |
99 |
100 |
105 | Made with ❤️ by Vivek Patel
106 |
107 |
--------------------------------------------------------------------------------
/bun.lockb:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/patelvivekdev/AuthJs-Template/3899f6c476dbd6616bc0757d4e8ee1fd0db3217e/bun.lockb
--------------------------------------------------------------------------------
/components.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://ui.shadcn.com/schema.json",
3 | "style": "default",
4 | "rsc": true,
5 | "tsx": true,
6 | "tailwind": {
7 | "config": "tailwind.config.ts",
8 | "css": "src/app/globals.css",
9 | "baseColor": "gray",
10 | "cssVariables": false
11 | },
12 | "aliases": {
13 | "utils": "@/lib/utils",
14 | "components": "@/components"
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/drizzle.config.ts:
--------------------------------------------------------------------------------
1 | import type { Config } from 'drizzle-kit';
2 | import dotenv from 'dotenv';
3 |
4 | dotenv.config({
5 | path: '.env.local',
6 | });
7 |
8 | export default {
9 | schema: './src/db/schema.ts',
10 | dialect: 'turso',
11 | dbCredentials: {
12 | url: process.env.TURSO_DATABASE_URL! as string,
13 | authToken: process.env.TURSO_AUTH_TOKEN! as string,
14 | },
15 | out: './drizzle',
16 | verbose: true,
17 | strict: true,
18 | } satisfies Config;
19 |
--------------------------------------------------------------------------------
/drizzle/0000_closed_lady_vermin.sql:
--------------------------------------------------------------------------------
1 | CREATE TABLE `account` (
2 | `userId` text NOT NULL,
3 | `type` text NOT NULL,
4 | `provider` text NOT NULL,
5 | `providerAccountId` text NOT NULL,
6 | `refresh_token` text,
7 | `access_token` text,
8 | `expires_at` integer,
9 | `token_type` text,
10 | `scope` text,
11 | `id_token` text,
12 | `session_state` text,
13 | PRIMARY KEY(`provider`, `providerAccountId`),
14 | FOREIGN KEY (`userId`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE cascade
15 | );
16 | --> statement-breakpoint
17 | CREATE TABLE `session` (
18 | `sessionToken` text PRIMARY KEY NOT NULL,
19 | `userId` text NOT NULL,
20 | `expires` integer NOT NULL,
21 | FOREIGN KEY (`userId`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE cascade
22 | );
23 | --> statement-breakpoint
24 | CREATE TABLE `user` (
25 | `id` text PRIMARY KEY NOT NULL,
26 | `name` text,
27 | `email` text NOT NULL,
28 | `emailVerified` integer,
29 | `image` text
30 | );
31 | --> statement-breakpoint
32 | CREATE TABLE `verificationToken` (
33 | `identifier` text NOT NULL,
34 | `token` text NOT NULL,
35 | `expires` integer NOT NULL,
36 | PRIMARY KEY(`identifier`, `token`)
37 | );
38 |
--------------------------------------------------------------------------------
/drizzle/0001_brainy_domino.sql:
--------------------------------------------------------------------------------
1 | ALTER TABLE `user` ADD `username` text;--> statement-breakpoint
2 | ALTER TABLE `user` ADD `password` text;
--------------------------------------------------------------------------------
/drizzle/0002_naive_sunspot.sql:
--------------------------------------------------------------------------------
1 | ALTER TABLE `user` ADD `role` text DEFAULT 'USER';
--------------------------------------------------------------------------------
/drizzle/0003_amazing_stranger.sql:
--------------------------------------------------------------------------------
1 | ALTER TABLE `user` ADD `totpSecret` text;--> statement-breakpoint
2 | ALTER TABLE `user` ADD `isTotpEnabled` integer DEFAULT false NOT NULL;
--------------------------------------------------------------------------------
/drizzle/0004_passkey.sql:
--------------------------------------------------------------------------------
1 | CREATE TABLE `authenticator` (
2 | `credentialID` text NOT NULL,
3 | `userId` text NOT NULL,
4 | `providerAccountId` text NOT NULL,
5 | `credentialPublicKey` text NOT NULL,
6 | `counter` integer NOT NULL,
7 | `credentialDeviceType` text NOT NULL,
8 | `credentialBackedUp` integer NOT NULL,
9 | `transports` text,
10 | PRIMARY KEY(`credentialID`, `userId`),
11 | FOREIGN KEY (`userId`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE cascade
12 | );
13 | --> statement-breakpoint
14 | CREATE UNIQUE INDEX `authenticator_credentialID_unique` ON `authenticator` (`credentialID`);
--------------------------------------------------------------------------------
/drizzle/0005_equal_havok.sql:
--------------------------------------------------------------------------------
1 | ALTER TABLE `user` ADD `created_at` text DEFAULT (CURRENT_TIMESTAMP) NOT NULL;--> statement-breakpoint
2 | ALTER TABLE `verificationToken` ADD `created_at` text DEFAULT (CURRENT_TIMESTAMP) NOT NULL;
--------------------------------------------------------------------------------
/drizzle/meta/_journal.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "6",
3 | "dialect": "sqlite",
4 | "entries": [
5 | {
6 | "idx": 0,
7 | "version": "6",
8 | "when": 1716520079876,
9 | "tag": "0000_closed_lady_vermin",
10 | "breakpoints": true
11 | },
12 | {
13 | "idx": 1,
14 | "version": "6",
15 | "when": 1716522844951,
16 | "tag": "0001_brainy_domino",
17 | "breakpoints": true
18 | },
19 | {
20 | "idx": 2,
21 | "version": "6",
22 | "when": 1718512560721,
23 | "tag": "0002_naive_sunspot",
24 | "breakpoints": true
25 | },
26 | {
27 | "idx": 3,
28 | "version": "6",
29 | "when": 1718681907655,
30 | "tag": "0003_amazing_stranger",
31 | "breakpoints": true
32 | },
33 | {
34 | "idx": 4,
35 | "version": "6",
36 | "when": 1718860420199,
37 | "tag": "0004_passkey",
38 | "breakpoints": true
39 | },
40 | {
41 | "idx": 5,
42 | "version": "6",
43 | "when": 1718952389590,
44 | "tag": "0005_equal_havok",
45 | "breakpoints": true
46 | }
47 | ]
48 | }
49 |
--------------------------------------------------------------------------------
/emails/InviteAdminEmail.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | Body,
3 | Button,
4 | Container,
5 | Head,
6 | Heading,
7 | Hr,
8 | Html,
9 | Link,
10 | Preview,
11 | Row,
12 | Section,
13 | Text,
14 | } from '@react-email/components';
15 | import * as React from 'react';
16 |
17 | interface InviteAdminProps {
18 | adminName?: string;
19 | invitedUserEmail?: string;
20 | inviteLink?: string;
21 | time?: number;
22 | }
23 |
24 | const baseUrl = process.env.BASE_URL
25 | ? `${process.env.BASE_URL}`
26 | : 'http://localhost:3000';
27 |
28 | export const InviteAdmin = ({
29 | adminName,
30 | invitedUserEmail,
31 | inviteLink,
32 | time,
33 | }: InviteAdminProps) => {
34 | return (
35 |
36 |
39 |
40 |
41 | Hello {invitedUserEmail},
42 |
43 |
44 |
45 | You have been invited by {adminName} to join our platform. Click
46 | the button below to accept the invitation and set up your account:
47 |
48 |
51 |
52 | This invitation will expire in {time} minutes. If the button
53 | doesn't work, you can copy and paste the following URL into
54 | your browser:
55 |
56 | {inviteLink}
57 |
58 |
59 | AuthJs Template
60 |
61 |
62 |
63 |
64 |
65 | );
66 | };
67 |
68 | export default InviteAdmin;
69 |
70 | const main = {
71 | backgroundColor: '#f6f9fc',
72 | padding: '10px 0',
73 | };
74 |
75 | const container = {
76 | backgroundColor: '#ffffff',
77 | border: '1px solid #f0f0f0',
78 | padding: '45px',
79 | };
80 |
81 | const text = {
82 | fontSize: '16px',
83 | fontFamily:
84 | "'Open Sans', 'HelveticaNeue-Light', 'Helvetica Neue Light', 'Helvetica Neue', Helvetica, Arial, 'Lucida Grande', sans-serif",
85 | fontWeight: '300',
86 | color: '#404040',
87 | lineHeight: '26px',
88 | };
89 |
90 | const button = {
91 | backgroundColor: '#2e026d',
92 | borderRadius: '4px',
93 | color: '#fff',
94 | fontFamily: "'Open Sans', 'Helvetica Neue', Arial",
95 | fontSize: '15px',
96 | textDecoration: 'none',
97 | textAlign: 'center' as const,
98 | display: 'block',
99 | width: '210px',
100 | padding: '14px 7px',
101 | };
102 |
103 | const reportLink = {
104 | fontSize: '14px',
105 | color: '#b4becc',
106 | };
107 |
108 | const hr = {
109 | borderColor: '#dfe1e4',
110 | margin: '42px 0 26px',
111 | };
112 |
113 | const code = {
114 | fontFamily: 'monospace',
115 | fontWeight: '500',
116 | padding: '1px 4px',
117 | backgroundColor: '#dfe1e4',
118 | letterSpacing: '-0.3px',
119 | fontSize: '10px',
120 | borderRadius: '4px',
121 | color: '#3c4149',
122 | };
123 |
--------------------------------------------------------------------------------
/emails/addPasswordEmail.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | Body,
3 | Button,
4 | Container,
5 | Head,
6 | Heading,
7 | Hr,
8 | Html,
9 | Link,
10 | Preview,
11 | Row,
12 | Section,
13 | Text,
14 | } from '@react-email/components';
15 | import * as React from 'react';
16 |
17 | interface AddPasswordEmailProps {
18 | userFirstName?: string;
19 | addPasswordLink?: string;
20 | time?: number;
21 | }
22 |
23 | const baseUrl = process.env.BASE_URL
24 | ? `${process.env.BASE_URL}`
25 | : 'http://localhost:3000';
26 |
27 | export const AddPasswordEmail = ({
28 | userFirstName,
29 | addPasswordLink,
30 | time,
31 | }: AddPasswordEmailProps) => {
32 | return (
33 |
34 |
37 |
38 |
39 | Hello {userFirstName},
40 |
41 |
42 |
43 | Someone recently requested to add password to your account. If
44 | this was you, you can set a new password here:
45 |
46 |
49 |
50 | If you don't want to add a password, just ignore and delete
51 | this message.
52 |
53 |
54 | If you didn't request this, and have account on with us, you
55 | can contact us at admin@patelvivek.dev.
56 |
57 |
58 | To keep your account secure, please don't forward this email
59 | to anyone.
60 |
61 |
62 | This link and code will only be valid for the next {time} minutes.
63 | If the link does not work, you can copy and paste the following
64 | URL into your browser:
65 |
66 | {addPasswordLink}
67 |
68 |
69 | AuthJs Template
70 |
71 |
72 |
73 |
74 |
75 | );
76 | };
77 |
78 | export default AddPasswordEmail;
79 |
80 | const main = {
81 | backgroundColor: '#f6f9fc',
82 | padding: '10px 0',
83 | };
84 |
85 | const container = {
86 | backgroundColor: '#ffffff',
87 | border: '1px solid #f0f0f0',
88 | padding: '45px',
89 | };
90 |
91 | const text = {
92 | fontSize: '16px',
93 | fontFamily:
94 | "'Open Sans', 'HelveticaNeue-Light', 'Helvetica Neue Light', 'Helvetica Neue', Helvetica, Arial, 'Lucida Grande', sans-serif",
95 | fontWeight: '300',
96 | color: '#404040',
97 | lineHeight: '26px',
98 | };
99 |
100 | const button = {
101 | backgroundColor: '#2e026d',
102 | borderRadius: '4px',
103 | color: '#fff',
104 | fontFamily: "'Open Sans', 'Helvetica Neue', Arial",
105 | fontSize: '15px',
106 | textDecoration: 'none',
107 | textAlign: 'center' as const,
108 | display: 'block',
109 | width: '210px',
110 | padding: '14px 7px',
111 | };
112 |
113 | const reportLink = {
114 | fontSize: '14px',
115 | color: '#b4becc',
116 | };
117 |
118 | const hr = {
119 | borderColor: '#dfe1e4',
120 | margin: '42px 0 26px',
121 | };
122 |
123 | const code = {
124 | fontFamily: 'monospace',
125 | fontWeight: '500',
126 | padding: '1px 4px',
127 | backgroundColor: '#dfe1e4',
128 | letterSpacing: '-0.3px',
129 | fontSize: '10px',
130 | borderRadius: '4px',
131 | color: '#3c4149',
132 | };
133 |
--------------------------------------------------------------------------------
/emails/forgotPasswordEmail.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | Body,
3 | Button,
4 | Container,
5 | Head,
6 | Heading,
7 | Hr,
8 | Html,
9 | Link,
10 | Preview,
11 | Row,
12 | Section,
13 | Text,
14 | } from '@react-email/components';
15 | import * as React from 'react';
16 |
17 | interface ForgotPasswordEmailProps {
18 | userFirstName?: string;
19 | resetPasswordLink?: string;
20 | time?: number;
21 | }
22 |
23 | const baseUrl = process.env.BASE_URL
24 | ? `${process.env.BASE_URL}`
25 | : 'http://localhost:3000';
26 |
27 | export const ForgotPasswordEmail = ({
28 | userFirstName,
29 | resetPasswordLink,
30 | time,
31 | }: ForgotPasswordEmailProps) => {
32 | return (
33 |
34 |
37 |
38 |
39 | Hello {userFirstName},
40 |
41 |
42 |
43 | Someone recently requested a password change for your AuthJs
44 | Template account. If this was you, you can set a new password
45 | here:
46 |
47 |
50 |
51 | If you don't want to change your password or didn't
52 | request this, just ignore and delete this message.
53 |
54 |
55 | To keep your account secure, please don't forward this email
56 | to anyone.
57 |
58 |
59 | This link and code will only be valid for the next {time} minutes.
60 | If the link does not work, you can copy and paste the following
61 | URL into your browser:
62 |
63 | {resetPasswordLink}
64 |
65 |
66 | AuthJs Template
67 |
68 |
69 |
70 |
71 |
72 | );
73 | };
74 |
75 | export default ForgotPasswordEmail;
76 |
77 | const main = {
78 | backgroundColor: '#f6f9fc',
79 | padding: '10px 0',
80 | };
81 |
82 | const container = {
83 | backgroundColor: '#ffffff',
84 | border: '1px solid #f0f0f0',
85 | padding: '45px',
86 | };
87 |
88 | const text = {
89 | fontSize: '16px',
90 | fontFamily:
91 | "'Open Sans', 'HelveticaNeue-Light', 'Helvetica Neue Light', 'Helvetica Neue', Helvetica, Arial, 'Lucida Grande', sans-serif",
92 | fontWeight: '300',
93 | color: '#404040',
94 | lineHeight: '26px',
95 | };
96 |
97 | const button = {
98 | backgroundColor: '#2e026d',
99 | borderRadius: '4px',
100 | color: '#fff',
101 | fontFamily: "'Open Sans', 'Helvetica Neue', Arial",
102 | fontSize: '15px',
103 | textDecoration: 'none',
104 | textAlign: 'center' as const,
105 | display: 'block',
106 | width: '210px',
107 | padding: '14px 7px',
108 | };
109 |
110 | const reportLink = {
111 | fontSize: '14px',
112 | color: '#b4becc',
113 | };
114 |
115 | const hr = {
116 | borderColor: '#dfe1e4',
117 | margin: '42px 0 26px',
118 | };
119 |
120 | const code = {
121 | fontFamily: 'monospace',
122 | fontWeight: '500',
123 | padding: '1px 4px',
124 | backgroundColor: '#dfe1e4',
125 | letterSpacing: '-0.3px',
126 | fontSize: '10px',
127 | borderRadius: '4px',
128 | color: '#3c4149',
129 | };
130 |
--------------------------------------------------------------------------------
/emails/twoFactorVerificationEmail.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | Body,
3 | Container,
4 | Head,
5 | Heading,
6 | Hr,
7 | Html,
8 | Link,
9 | Preview,
10 | Row,
11 | Section,
12 | Text,
13 | } from '@react-email/components';
14 | import * as React from 'react';
15 |
16 | interface TwoFactorEmailProps {
17 | userFirstName?: string;
18 | OTP?: string;
19 | time?: number;
20 | }
21 |
22 | const baseUrl = process.env.BASE_URL
23 | ? `${process.env.BASE_URL}`
24 | : 'http://localhost:3000';
25 |
26 | export const TwoFactorEmail = ({
27 | userFirstName,
28 | OTP,
29 | time,
30 | }: TwoFactorEmailProps) => {
31 | return (
32 |
33 |
36 |
37 |
38 | Hello {userFirstName},
39 |
40 |
41 |
42 | You have requested OTP for verify your account.
43 |
44 |
45 | Verification code
46 |
47 | {OTP}
48 |
49 | (This code is valid for {time} minutes)
50 |
51 |
52 |
53 |
54 | If you don't want to change your password or didn't
55 | request this, just ignore and delete this message.
56 |
57 |
58 |
59 | AuthJs Template
60 |
61 |
62 |
63 |
64 |
65 | );
66 | };
67 |
68 | export default TwoFactorEmail;
69 |
70 | const main = {
71 | backgroundColor: '#f6f9fc',
72 | padding: '10px 0',
73 | };
74 |
75 | const container = {
76 | backgroundColor: '#ffffff',
77 | border: '1px solid #f0f0f0',
78 | padding: '45px',
79 | };
80 |
81 | const text = {
82 | fontSize: '16px',
83 | fontFamily:
84 | "'Open Sans', 'HelveticaNeue-Light', 'Helvetica Neue Light', 'Helvetica Neue', Helvetica, Arial, 'Lucida Grande', sans-serif",
85 | fontWeight: '300',
86 | color: '#404040',
87 | lineHeight: '26px',
88 | };
89 |
90 | const reportLink = {
91 | fontSize: '14px',
92 | color: '#b4becc',
93 | };
94 |
95 | const hr = {
96 | borderColor: '#dfe1e4',
97 | margin: '42px 0 26px',
98 | };
99 |
100 | const verifyText = {
101 | ...text,
102 | margin: 0,
103 | fontWeight: 'bold',
104 | textAlign: 'center' as const,
105 | };
106 |
107 | const codeText = {
108 | ...text,
109 | fontWeight: 'bold',
110 | fontSize: '36px',
111 | margin: '10px 0',
112 | textAlign: 'center' as const,
113 | };
114 |
115 | const validityText = {
116 | ...text,
117 | margin: '0px',
118 | textAlign: 'center' as const,
119 | };
120 |
121 | const verificationSection = {
122 | display: 'flex',
123 | alignItems: 'center',
124 | justifyContent: 'center',
125 | };
126 |
--------------------------------------------------------------------------------
/emails/verificationEmail.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | Body,
3 | Button,
4 | Container,
5 | Head,
6 | Heading,
7 | Hr,
8 | Html,
9 | Link,
10 | Preview,
11 | Row,
12 | Section,
13 | Text,
14 | } from '@react-email/components';
15 | import * as React from 'react';
16 |
17 | interface VerificationEmailProps {
18 | email?: string;
19 | validationCode?: string;
20 | time: number;
21 | }
22 |
23 | const baseUrl = process.env.BASE_URL
24 | ? `${process.env.BASE_URL}`
25 | : 'http://localhost:3000';
26 |
27 | export const VerificationEmail = ({
28 | email,
29 | validationCode,
30 | time,
31 | }: VerificationEmailProps) => (
32 |
33 |
36 |
37 |
38 | Hello {email},
39 |
40 |
41 |
47 |
48 | This link and code will only be valid for the next {time} minutes.
49 | If the link does not work, you can copy and paste the following URL
50 | into your browser:
51 |
52 |
53 | {`${baseUrl}/onboarding?code=${validationCode}`}
54 |
55 |
56 |
57 | AuthJs Template
58 |
59 |
60 |
61 |
62 |
63 | );
64 |
65 | export default VerificationEmail;
66 |
67 | const main = {
68 | backgroundColor: '#f6f9fc',
69 | padding: '10px 0',
70 | };
71 |
72 | const container = {
73 | backgroundColor: '#ffffff',
74 | border: '1px solid #f0f0f0',
75 | padding: '45px',
76 | };
77 |
78 | const text = {
79 | fontSize: '16px',
80 | fontFamily:
81 | "'Open Sans', 'HelveticaNeue-Light', 'Helvetica Neue Light', 'Helvetica Neue', Helvetica, Arial, 'Lucida Grande', sans-serif",
82 | fontWeight: '300',
83 | color: '#404040',
84 | lineHeight: '26px',
85 | };
86 |
87 | const button = {
88 | backgroundColor: '#2e026d',
89 | borderRadius: '4px',
90 | fontWeight: '600',
91 | color: '#fff',
92 | fontFamily: "'Open Sans', 'Helvetica Neue', Arial",
93 | fontSize: '15px',
94 | textDecoration: 'none',
95 | textAlign: 'center' as const,
96 | display: 'block',
97 | padding: '11px 23px',
98 | };
99 |
100 | const reportLink = {
101 | fontSize: '14px',
102 | color: '#b4becc',
103 | };
104 |
105 | const hr = {
106 | borderColor: '#dfe1e4',
107 | margin: '42px 0 26px',
108 | };
109 |
110 | const code = {
111 | fontFamily: 'monospace',
112 | fontWeight: '700',
113 | padding: '1px 4px',
114 | backgroundColor: '#dfe1e4',
115 | letterSpacing: '-0.3px',
116 | fontSize: '21px',
117 | borderRadius: '4px',
118 | color: '#3c4149',
119 | };
120 |
--------------------------------------------------------------------------------
/next.config.ts:
--------------------------------------------------------------------------------
1 |
2 | const nextConfig: import('next').NextConfig = {
3 | images: {
4 | remotePatterns: [
5 | {
6 | protocol: 'https',
7 | hostname: 'avatars.githubusercontent.com',
8 | },
9 | {
10 | protocol: 'https',
11 | hostname: 'lh3.googleusercontent.com',
12 | },
13 | ],
14 | },
15 | };
16 |
17 | export default nextConfig;
18 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "authjs-template",
3 | "description": "A starter authentication template for Next.js",
4 | "repository": "https://github.com/patelvivekdev/drizzle-next-auth-turso.git",
5 | "version": "0.3.2",
6 | "author": "Vivek Patel ",
7 | "private": true,
8 | "scripts": {
9 | "dev": "next dev --turbo",
10 | "build": "next build",
11 | "start": "next start",
12 | "format": "prettier --write .",
13 | "lint": "next lint",
14 | "lint:fix": "next lint --fix",
15 | "prettier": "prettier --write .",
16 | "analyze": "ANALYZE=true npm run build",
17 | "prepare": "husky",
18 | "db:push": "drizzle-kit push",
19 | "db:generate": "drizzle-kit generate",
20 | "db:studio": "drizzle-kit studio",
21 | "db:migrate": "drizzle-kit migrate"
22 | },
23 | "dependencies": {
24 | "@auth/drizzle-adapter": "^1.7.4",
25 | "@epic-web/totp": "^2.0.0",
26 | "@libsql/client": "^0.14.0",
27 | "@radix-ui/react-avatar": "^1.1.1",
28 | "@radix-ui/react-checkbox": "^1.1.2",
29 | "@radix-ui/react-dialog": "^1.1.2",
30 | "@radix-ui/react-dropdown-menu": "^2.1.2",
31 | "@radix-ui/react-icons": "^1.3.2",
32 | "@radix-ui/react-label": "^2.1.0",
33 | "@radix-ui/react-select": "^2.1.2",
34 | "@radix-ui/react-slot": "^1.1.0",
35 | "@react-email/components": "^0.0.19",
36 | "@simplewebauthn/browser": "9.0.1",
37 | "@simplewebauthn/server": "9.0.3",
38 | "@tanstack/react-table": "^8.20.5",
39 | "bcryptjs": "^2.4.3",
40 | "class-variance-authority": "^0.7.1",
41 | "clsx": "^2.1.1",
42 | "dotenv": "^16.4.7",
43 | "drizzle-orm": "^0.38.0",
44 | "framer-motion": "^11.13.3",
45 | "input-otp": "^1.4.1",
46 | "lucide-react": "^0.468.0",
47 | "next": "^15.0.4",
48 | "next-auth": "^5.0.0-beta.25",
49 | "next-themes": "^0.4.4",
50 | "qrcode": "^1.5.4",
51 | "react": "^19",
52 | "react-dom": "^19",
53 | "resend": "^3.5.0",
54 | "sharp": "^0.33.5",
55 | "sonner": "^1.7.1",
56 | "tailwind-merge": "^2.5.5",
57 | "tailwindcss-animate": "^1.0.7",
58 | "zod": "^3.23.8"
59 | },
60 | "devDependencies": {
61 | "@types/bcryptjs": "^2.4.6",
62 | "@types/node": "^20.17.9",
63 | "@types/qrcode": "^1.5.5",
64 | "@types/react": "^19",
65 | "@types/react-dom": "^19",
66 | "autoprefixer": "^10.4.20",
67 | "drizzle-kit": "^0.30.0",
68 | "eslint": "^8.57.1",
69 | "eslint-config-next": "^15.0.4",
70 | "eslint-config-prettier": "^9.1.0",
71 | "eslint-plugin-prettier": "^5.2.1",
72 | "husky": "^9.1.7",
73 | "lint-staged": "^15.2.10",
74 | "postcss": "^8.4.49",
75 | "prettier": "^3.4.2",
76 | "prettier-plugin-tailwindcss": "^0.6.9",
77 | "tailwindcss": "^3.4.16",
78 | "typescript": "^5.7.2"
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/postcss.config.cjs:
--------------------------------------------------------------------------------
1 | /** @type {import('postcss-load-config').Config} */
2 | const config = {
3 | plugins: {
4 | tailwindcss: {},
5 | autoprefixer: {},
6 | },
7 | };
8 |
9 | module.exports = config;
10 |
--------------------------------------------------------------------------------
/public/authjs-template.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/patelvivekdev/AuthJs-Template/3899f6c476dbd6616bc0757d4e8ee1fd0db3217e/public/authjs-template.png
--------------------------------------------------------------------------------
/public/authjs.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/patelvivekdev/AuthJs-Template/3899f6c476dbd6616bc0757d4e8ee1fd0db3217e/public/authjs.webp
--------------------------------------------------------------------------------
/public/drizzle.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/patelvivekdev/AuthJs-Template/3899f6c476dbd6616bc0757d4e8ee1fd0db3217e/public/drizzle.jpg
--------------------------------------------------------------------------------
/public/magicui.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/patelvivekdev/AuthJs-Template/3899f6c476dbd6616bc0757d4e8ee1fd0db3217e/public/magicui.png
--------------------------------------------------------------------------------
/public/nextjs.svg:
--------------------------------------------------------------------------------
1 |
21 |
--------------------------------------------------------------------------------
/public/placeholder-user.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/patelvivekdev/AuthJs-Template/3899f6c476dbd6616bc0757d4e8ee1fd0db3217e/public/placeholder-user.jpg
--------------------------------------------------------------------------------
/public/react_dark.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/public/resend.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/patelvivekdev/AuthJs-Template/3899f6c476dbd6616bc0757d4e8ee1fd0db3217e/public/resend.jpg
--------------------------------------------------------------------------------
/public/turso.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/patelvivekdev/AuthJs-Template/3899f6c476dbd6616bc0757d4e8ee1fd0db3217e/public/turso.jpg
--------------------------------------------------------------------------------
/public/turso.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/vercel.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/actions/admin/addAdmin.ts:
--------------------------------------------------------------------------------
1 | 'use server';
2 |
3 | import { z } from 'zod';
4 | import { createTokenForAddAdmin } from '@/db/query/Token';
5 |
6 | // =============================== addAdmin ===============================
7 | const addAdminSchema = z.object({
8 | email: z.string().email('Please enter valid email address.').min(5),
9 | });
10 | export async function addAdmin(
11 | adminName: string,
12 | prevState: any,
13 | formData: FormData,
14 | ) {
15 | const validatedFields = addAdminSchema.safeParse({
16 | email: formData.get('email'),
17 | });
18 |
19 | if (!validatedFields.success) {
20 | return {
21 | type: 'error',
22 | errors: validatedFields.error.flatten().fieldErrors,
23 | message: 'Missing Fields!!',
24 | resetKey: '',
25 | };
26 | }
27 |
28 | try {
29 | let emailData = await createTokenForAddAdmin(
30 | validatedFields.data.email,
31 | adminName,
32 | );
33 |
34 | if (!emailData.success) {
35 | return {
36 | type: 'error',
37 | errors: {
38 | email: undefined,
39 | },
40 | message: emailData.message,
41 | resetKey: '',
42 | };
43 | }
44 |
45 | return {
46 | type: 'success',
47 | errors: null,
48 | message: emailData.message,
49 | resetKey: Date.now().toString(),
50 | };
51 | } catch (error: any) {
52 | console.error('Failed to signUp', error);
53 | return {
54 | type: 'error',
55 | errors: {
56 | email: undefined,
57 | },
58 | message: error.message || 'Failed to signUp.',
59 | resetKey: '',
60 | };
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/src/actions/admin/index.ts:
--------------------------------------------------------------------------------
1 | export * from './addAdmin';
2 |
--------------------------------------------------------------------------------
/src/actions/auth/delete.ts:
--------------------------------------------------------------------------------
1 | 'use server';
2 |
3 | import { deleteUser } from '@/db/query/User';
4 | import { signOut } from '@/auth';
5 | import { redirect } from 'next/navigation';
6 |
7 | // =============================== deleteAccount ===============================
8 | export async function deleteAccount(userId: string) {
9 | await deleteUser(userId);
10 | await signOut();
11 | redirect('/');
12 | }
13 |
--------------------------------------------------------------------------------
/src/actions/auth/index.ts:
--------------------------------------------------------------------------------
1 | export * from './oauth';
2 | export * from './signin';
3 | export * from './signup';
4 | export * from './password';
5 | export * from './delete';
6 | export * from './mfa';
7 |
--------------------------------------------------------------------------------
/src/actions/auth/oauth.ts:
--------------------------------------------------------------------------------
1 | 'use server';
2 |
3 | import { signIn } from '@/auth';
4 | import { redirect } from 'next/navigation';
5 | import { OAuthAccountNotLinked } from '@auth/core/errors';
6 | import { deleteUserAccount } from '@/db/query/User';
7 | import { revalidatePath } from 'next/cache';
8 |
9 | // =============================== Oauth Login ===============================
10 | export async function oAuthLogin(provider: string) {
11 | try {
12 | await signIn(provider);
13 | } catch (error) {
14 | console.log('Error------', error);
15 | if (error instanceof OAuthAccountNotLinked) {
16 | redirect('/error?error=OAuthAccountNotLinked');
17 | } else {
18 | throw error;
19 | }
20 | }
21 | }
22 |
23 | // =============================== Oauth Remove ===============================
24 | export async function oAuthRemove(userId: string, provider: string) {
25 | await deleteUserAccount(userId, provider);
26 | revalidatePath('/', 'layout');
27 | }
28 |
--------------------------------------------------------------------------------
/src/actions/auth/signin.ts:
--------------------------------------------------------------------------------
1 | 'use server';
2 | import { z } from 'zod';
3 | import { signIn as signInUser } from '@/auth';
4 | import { redirect } from 'next/navigation';
5 |
6 | // =============================== signIn ===============================
7 | const signInSchema = z.object({
8 | username: z.string(),
9 | password: z.string(),
10 | });
11 | export async function signIn(prevState: any, formData: FormData) {
12 | const validatedFields = signInSchema.safeParse({
13 | username: formData.get('username'),
14 | password: formData.get('password'),
15 | });
16 |
17 | // Return early if the form data is invalid
18 | if (!validatedFields.success) {
19 | return {
20 | type: 'error',
21 | data: {
22 | username: formData.get('username') as string,
23 | password: formData.get('password') as string,
24 | },
25 | errors: validatedFields.error.flatten().fieldErrors,
26 | message: 'Missing Fields!!',
27 | };
28 | }
29 |
30 | try {
31 | await signInUser('credentials', {
32 | username: validatedFields.data.username,
33 | password: validatedFields.data.password,
34 | redirect: true,
35 | });
36 | } catch (error: any) {
37 | if (error.code === 'invalid-credentials') {
38 | return {
39 | type: 'error',
40 | errors: {
41 | username: undefined,
42 | password: undefined,
43 | },
44 | data: {
45 | username: validatedFields.data.username,
46 | password: validatedFields.data.password,
47 | },
48 | message: error.message,
49 | };
50 | } else if (error.code === 'OauthError') {
51 | return {
52 | type: 'error',
53 | errors: {
54 | username: undefined,
55 | password: undefined,
56 | },
57 | data: {
58 | username: validatedFields.data.username,
59 | password: validatedFields.data.password,
60 | },
61 | message: error.message,
62 | };
63 | } else {
64 | throw error;
65 | }
66 | }
67 | redirect('/profile');
68 | }
69 |
--------------------------------------------------------------------------------
/src/actions/auth/signup.ts:
--------------------------------------------------------------------------------
1 | 'use server';
2 |
3 | import { createTokenForCreateUser, deleteToken } from '@/db/query/Token';
4 | import { createUser } from '@/db/query/User';
5 | import { z } from 'zod';
6 |
7 | // =============================== signUp ===============================
8 | const signUpSchema = z.object({
9 | email: z.string().email('Please enter valid email address.').min(5),
10 | });
11 |
12 | export async function signUp(prevState: any, formData: FormData) {
13 | const validatedFields = signUpSchema.safeParse({
14 | email: formData.get('email'),
15 | });
16 |
17 | // Return early if the form data is invalid
18 | if (!validatedFields.success) {
19 | return {
20 | type: 'error',
21 | errors: validatedFields.error.flatten().fieldErrors,
22 | message: 'Missing Fields!!',
23 | resetKey: '',
24 | };
25 | }
26 |
27 | try {
28 | let emailData = await createTokenForCreateUser(validatedFields.data.email);
29 |
30 | if (!emailData.success) {
31 | return {
32 | type: 'error',
33 | errors: {
34 | email: undefined,
35 | },
36 | message: 'Failed to signUp.',
37 | resetKey: '',
38 | };
39 | }
40 |
41 | return {
42 | type: 'success',
43 | errors: null,
44 | message: 'Please check your email for next step',
45 | resetKey: Date.now().toString(),
46 | };
47 | } catch (error: any) {
48 | console.error('Failed to signUp', error);
49 | return {
50 | type: 'error',
51 | errors: {
52 | email: undefined,
53 | },
54 | message: error.message || 'Failed to signUp.',
55 | resetKey: '',
56 | };
57 | }
58 | }
59 |
60 | // =============================== signUp > onBoarding ===============================
61 | const onBoardingSchema = z.object({
62 | name: z.string().min(2, { message: 'Must be 2 or more characters long' }),
63 | username: z.string().min(3, { message: 'Must be 3 or more characters long' }),
64 | email: z.string().email('Please enter valid email address.').min(5),
65 | password: z.string().min(8, { message: 'Must be 8 or more characters long' }),
66 | password2: z.string(),
67 | });
68 |
69 | export async function onBoarding(
70 | email: string,
71 | isAdmin: boolean,
72 | prevState: any,
73 | formData: FormData,
74 | ) {
75 | const validatedFields = onBoardingSchema.safeParse({
76 | name: formData.get('name'),
77 | email: email,
78 | username: formData.get('username'),
79 | password: formData.get('password'),
80 | password2: formData.get('password2'),
81 | });
82 |
83 | // Return early if the form data is invalid
84 | if (!validatedFields.success) {
85 | return {
86 | type: 'error',
87 | errors: validatedFields.error.flatten().fieldErrors,
88 | message: 'Missing Fields!!',
89 | };
90 | }
91 |
92 | // check for password match
93 | if (validatedFields.data.password !== validatedFields.data.password2) {
94 | return {
95 | type: 'error',
96 | errors: {
97 | name: undefined,
98 | username: undefined,
99 | email: undefined,
100 | password: undefined,
101 | password2: undefined,
102 | },
103 | message: 'Passwords do not match.',
104 | };
105 | }
106 |
107 | try {
108 | let user = await createUser(
109 | validatedFields.data.name,
110 | email,
111 | validatedFields.data.username,
112 | validatedFields.data.password,
113 | isAdmin,
114 | );
115 | if (user.length === 0) {
116 | return {
117 | type: 'error',
118 | errors: {
119 | name: undefined,
120 | username: undefined,
121 | email: undefined,
122 | password: undefined,
123 | password2: undefined,
124 | },
125 | message: 'Failed to signUp. Please try again.',
126 | };
127 | }
128 |
129 | // delete the token
130 | await deleteToken(email);
131 |
132 | return {
133 | type: 'success',
134 | errors: null,
135 | message: 'Successfully signed up.',
136 | };
137 | } catch (error: any) {
138 | console.error('Failed to signUp', error);
139 | return {
140 | type: 'error',
141 | errors: {
142 | name: undefined,
143 | username: undefined,
144 | email: undefined,
145 | password: undefined,
146 | password2: undefined,
147 | },
148 | message: error.message || 'Failed to signUp.',
149 | };
150 | }
151 | }
152 |
--------------------------------------------------------------------------------
/src/app/(auth)/add-admin/AddAdminForm.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { addAdmin } from '@/actions/admin';
4 | import { Label } from '@/components/ui/label';
5 | import { Input } from '@/components/ui/input';
6 | import { useActionState } from 'react';
7 |
8 | const initialState = {
9 | type: '',
10 | message: '',
11 | errors: null,
12 | resetKey: '',
13 | };
14 |
15 | export default function AddAdminForm({ adminName }: { adminName: string }) {
16 | const action = addAdmin.bind(null, adminName as string);
17 |
18 | const [state, submitAction, isPending] = useActionState(action, initialState);
19 |
20 | return (
21 |
50 | );
51 | }
52 |
--------------------------------------------------------------------------------
/src/app/(auth)/add-admin/page.tsx:
--------------------------------------------------------------------------------
1 | import AddAdminForm from './AddAdminForm';
2 | import { auth } from '@/auth';
3 | import { User as DefaultUser } from 'next-auth';
4 | import { redirect } from 'next/navigation';
5 |
6 | import type { Metadata } from 'next';
7 |
8 | export const metadata: Metadata = {
9 | title: 'Add Admin',
10 | description: 'Send invitation for user to register as Admin',
11 | };
12 |
13 | // Extend User interface
14 | interface User extends DefaultUser {
15 | role: string;
16 | username: string;
17 | }
18 |
19 | export default async function AddAdmin() {
20 | const session = await auth();
21 | const user = session?.user as User;
22 | if (!user) {
23 | redirect('/sign-in');
24 | }
25 |
26 | if (user.role !== 'ADMIN') {
27 | redirect('/');
28 | }
29 | return (
30 |
31 |
32 |
33 |
34 | Invite user for Admin
35 |
36 |
37 | Enter email below to create send invitation.
38 |
39 |
40 |
41 |
42 |
43 | );
44 | }
45 |
--------------------------------------------------------------------------------
/src/app/(auth)/add-admin/verify/page.tsx:
--------------------------------------------------------------------------------
1 | import { getVerificationToken } from '@/db/query/Token';
2 | import { TokenNotFound } from '@/components/TokenNotFound';
3 | import { changeUserToAdmin, getUserByUsername } from '@/db/query/User';
4 | import OnBoardingForm from '../../onboarding/OnBoardingForm';
5 | import RadialGradient from '@/components/ui/radial-gradient';
6 |
7 | export default async function ResetPasswordPage(props: {
8 | searchParams?: Promise<{
9 | token?: string;
10 | }>;
11 | }) {
12 | const searchParams = await props.searchParams;
13 | const token = searchParams?.token || '';
14 | if (token === '') {
15 | return (
16 |
22 | );
23 | }
24 |
25 | const data = await getVerificationToken(token);
26 |
27 | // check expires of token
28 | if (data.data?.expires! < new Date()) {
29 | return (
30 |
36 | );
37 | }
38 |
39 | if (!data.success) {
40 | return (
41 |
47 | );
48 | }
49 |
50 | // check if user already have account as USER
51 | let user = await getUserByUsername(data.data?.identifier!);
52 |
53 | if (user.length === 0) {
54 | return ;
55 | } else {
56 | let response = await changeUserToAdmin(data.data?.identifier!);
57 | return (
58 |
59 |
60 |
61 | {!response.success && (
62 |
63 |
{response.message}
64 |
65 | )}
66 | {response.success && (
67 |
68 |
{response.message}
69 |
70 | )}
71 |
72 |
73 |
74 |
75 | );
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/src/app/(auth)/add-password/addPasswordForm.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { useActionState, useState } from 'react';
4 | import { useRouter } from 'next/navigation';
5 | import { addPassword } from '@/actions/auth';
6 | import { useEffect } from 'react';
7 | import { toast } from 'sonner';
8 | import { Label } from '@/components/ui/label';
9 | import { Input } from '@/components/ui/input';
10 | import { Eye, EyeOff } from 'lucide-react';
11 | import { Button } from '@/components/ui/button';
12 |
13 | const initialState = {
14 | type: '',
15 | message: '',
16 | data: {
17 | password: '',
18 | password2: '',
19 | },
20 | errors: null,
21 | };
22 |
23 | export default function AddPasswordForm({ email }: { email: string }) {
24 | const [showPassword, setShowPassword] = useState(false);
25 | const [showConfirmPassword, setShowConfirmPassword] = useState(false);
26 |
27 | const action = addPassword.bind(null, email as string);
28 |
29 | const [state, submitAction, isPending] = useActionState(action, initialState);
30 |
31 | const router = useRouter();
32 |
33 | useEffect(() => {
34 | if (state.type === 'success') {
35 | toast.success(state.message);
36 | router.push('/profile');
37 | }
38 | }, [router, state]);
39 |
40 | return (
41 |
105 | );
106 | }
107 |
--------------------------------------------------------------------------------
/src/app/(auth)/add-password/page.tsx:
--------------------------------------------------------------------------------
1 | import { getVerificationToken } from '@/db/query/Token';
2 | import AddPasswordForm from './addPasswordForm';
3 | import { TokenNotFound } from '@/components/TokenNotFound';
4 |
5 | import type { Metadata } from 'next';
6 | import RadialGradient from '@/components/ui/radial-gradient';
7 |
8 | export const metadata: Metadata = {
9 | title: 'Add password',
10 | description: 'Add password to your account',
11 | };
12 |
13 | export default async function AddPasswordPage(props: {
14 | searchParams?: Promise<{
15 | token?: string;
16 | }>;
17 | }) {
18 | const searchParams = await props.searchParams;
19 | const token = searchParams?.token || '';
20 | if (token === '') {
21 | return (
22 |
28 | );
29 | }
30 |
31 | const data = await getVerificationToken(token);
32 |
33 | // check expires of token
34 | if (data.data?.expires! < new Date()) {
35 | return (
36 |
42 | );
43 | }
44 |
45 | if (!data.success) {
46 | return (
47 |
53 | );
54 | }
55 | return (
56 |
57 |
58 |
59 |
60 | Add Password
61 |
62 |
63 | Enter your new strong and unique password.
64 |
65 |
66 |
67 |
68 |
69 |
70 | );
71 | }
72 |
--------------------------------------------------------------------------------
/src/app/(auth)/error/page.tsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-unused-vars */
2 | 'use client';
3 |
4 | import RadialGradient from '@/components/ui/radial-gradient';
5 | import Link from 'next/link';
6 | import { useSearchParams } from 'next/navigation';
7 | import { Suspense } from 'react';
8 |
9 | enum Error {
10 | Configuration = 'Configuration',
11 | AccessDenied = 'AccessDenied',
12 | Verification = 'Verification',
13 | OAuthAccountNotLinked = 'OAuthAccountNotLinked',
14 | Default = 'Default',
15 | }
16 |
17 | const errorMap = {
18 | [Error.Configuration]: (
19 |
20 | There was a problem when trying to authenticate. Please contact us if this
21 | error persists. Unique error code:{' '}
22 | Configuration
23 |
24 | ),
25 | [Error.AccessDenied]: (
26 |
27 |
28 | You are not authorized to access this page.
29 |
30 |
31 | Unique error code:{' '}
32 |
33 | AccessDenied
34 |
35 |
36 |
37 | Go to Home
38 |
39 |
40 | ),
41 | [Error.OAuthAccountNotLinked]: (
42 |
43 |
44 | Oauth Account is already linked to another account.
45 |
46 |
47 | Unique error code:{' '}
48 |
49 | OAuthAccountNotLinked
50 |
51 |
52 |
53 | Go to Profile
54 |
55 |
56 | ),
57 | [Error.Verification]: (
58 |
59 | Verification error. Please contact us if this error persists. Unique error
60 | code:{' '}
61 | Verification
62 |
63 | ),
64 | [Error.Default]: (
65 |
66 | An unexpected error occurred. Please contact us if this error persists.
67 | Unique error code:{' '}
68 | Default
69 |
70 | ),
71 | };
72 |
73 | export default function AuthErrorPage() {
74 | return (
75 |
76 |
77 |
78 | );
79 | }
80 |
81 | function Search() {
82 | const search = useSearchParams();
83 | const error = search.get('error') as Error;
84 |
85 | return (
86 |
87 |
88 | {/*
89 | Something went wrong
90 |
*/}
91 |
92 | {errorMap[error] || 'Please contact us if this error persists.'}
93 |
94 |
95 |
96 |
97 | );
98 | }
99 |
--------------------------------------------------------------------------------
/src/app/(auth)/forgot-password/page.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 | import { useRouter } from 'next/navigation';
3 | import { useActionState, useEffect } from 'react';
4 | import { Input } from '@/components/ui/input';
5 | import { Label } from '@/components/ui/label';
6 | import { forgotPassword } from '@/actions/auth';
7 | import { toast } from 'sonner';
8 | import RadialGradient from '@/components/ui/radial-gradient';
9 | import { Button } from '@/components/ui/button';
10 |
11 | const initialState = {
12 | type: '',
13 | message: '',
14 | errors: null,
15 | };
16 |
17 | export default function ForgotPasswordPage() {
18 | const [state, submitAction, isPending] = useActionState(
19 | forgotPassword,
20 | initialState,
21 | );
22 | const router = useRouter();
23 | useEffect(() => {
24 | if (state.type === 'success') {
25 | toast.success(state.message);
26 | router.push('/');
27 | }
28 | }, [router, state]);
29 | return (
30 |
31 |
32 |
33 |
34 | Forgot password
35 |
36 |
37 | Enter your email below to reset your password
38 |
39 |
40 | {state.errors && (
41 |
42 |
{state.message}
43 |
44 | )}
45 |
65 |
66 |
67 |
68 | );
69 | }
70 |
--------------------------------------------------------------------------------
/src/app/(auth)/onboarding/OnBoardingForm.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 | import { useActionState, useState } from 'react';
3 | import { useRouter } from 'next/navigation';
4 | import { onBoarding } from '@/actions/auth';
5 | import { useEffect } from 'react';
6 | import { toast } from 'sonner';
7 | import { Label } from '@/components/ui/label';
8 | import { Input } from '@/components/ui/input';
9 | import { Eye, EyeOff } from 'lucide-react';
10 | import { Button } from '@/components/ui/button';
11 |
12 | const initialState = {
13 | type: '',
14 | message: '',
15 | errors: null,
16 | };
17 |
18 | export default function OnBoardingForm({
19 | email,
20 | isAdmin,
21 | }: {
22 | email: string;
23 | isAdmin: boolean;
24 | }) {
25 | const [showPassword, setShowPassword] = useState(false);
26 | const [showConfirmPassword, setShowConfirmPassword] = useState(false);
27 |
28 | let onBoardingWithEmail = onBoarding.bind(null, email as string);
29 | let onBoardingWithIsAdmin = onBoardingWithEmail.bind(
30 | null,
31 | isAdmin as boolean,
32 | );
33 |
34 | const [state, submitAction, isPending] = useActionState(
35 | onBoardingWithIsAdmin,
36 | initialState,
37 | );
38 |
39 | const router = useRouter();
40 | useEffect(() => {
41 | if (state.type === 'success') {
42 | toast.success(state.message);
43 | router.push('/sign-in');
44 | }
45 | }, [router, state]);
46 |
47 | return (
48 |
150 | );
151 | }
152 |
--------------------------------------------------------------------------------
/src/app/(auth)/onboarding/page.tsx:
--------------------------------------------------------------------------------
1 | import { getVerificationToken } from '@/db/query/Token';
2 | import OnBoardingForm from './OnBoardingForm';
3 | import { TokenNotFound } from '@/components/TokenNotFound';
4 |
5 | import type { Metadata } from 'next';
6 | import RadialGradient from '@/components/ui/radial-gradient';
7 |
8 | export const metadata: Metadata = {
9 | title: 'Register',
10 | description: 'A simple Register page',
11 | };
12 |
13 | export default async function onBoarding(props: {
14 | searchParams?: Promise<{
15 | code?: string;
16 | }>;
17 | }) {
18 | const searchParams = await props.searchParams;
19 | const token = searchParams?.code || '';
20 | if (token === '') {
21 | return (
22 |
28 | );
29 | }
30 |
31 | const data = await getVerificationToken(token);
32 |
33 | // check expires of token
34 | if (data.data?.expires! < new Date()) {
35 | return (
36 |
42 | );
43 | }
44 |
45 | if (!data.success) {
46 | return (
47 |
53 | );
54 | }
55 | return (
56 |
57 |
58 |
59 |
60 | Create new account
61 |
62 |
63 | Fill these details to get started.
64 |
65 |
66 |
67 |
68 |
69 |
70 | );
71 | }
72 |
--------------------------------------------------------------------------------
/src/app/(auth)/reset-password/page.tsx:
--------------------------------------------------------------------------------
1 | import { getVerificationToken } from '@/db/query/Token';
2 | import ResetPasswordForm from './resetPasswordForm';
3 | import { TokenNotFound } from '@/components/TokenNotFound';
4 |
5 | import type { Metadata } from 'next';
6 | import RadialGradient from '@/components/ui/radial-gradient';
7 |
8 | export const metadata: Metadata = {
9 | title: 'Reset Password',
10 | description: 'Reset your password',
11 | };
12 |
13 | export default async function ResetPasswordPage(props: {
14 | searchParams?: Promise<{
15 | token?: string;
16 | }>;
17 | }) {
18 | const searchParams = await props.searchParams;
19 | const token = searchParams?.token || '';
20 | if (token === '') {
21 | return (
22 |
28 | );
29 | }
30 |
31 | const data = await getVerificationToken(token);
32 |
33 | // check expires of token
34 | if (data.data?.expires! < new Date()) {
35 | return (
36 |
42 | );
43 | }
44 |
45 | if (!data.success) {
46 | return (
47 |
53 | );
54 | }
55 | return (
56 |
57 |
58 |
59 |
60 | Reset Password
61 |
62 |
63 | Enter your new strong and unique password.
64 |
65 |
66 |
67 |
68 |
69 |
70 | );
71 | }
72 |
--------------------------------------------------------------------------------
/src/app/(auth)/reset-password/resetPasswordForm.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 | // import { useActionState } from 'react';
3 | import { useActionState, useState } from 'react';
4 | import { useRouter } from 'next/navigation';
5 | import { resetPassword } from '@/actions/auth';
6 | import { useEffect } from 'react';
7 | import { toast } from 'sonner';
8 | import { Label } from '@/components/ui/label';
9 | import { Input } from '@/components/ui/input';
10 | import { Eye, EyeOff } from 'lucide-react';
11 | import { Button } from '@/components/ui/button';
12 |
13 | const initialState = {
14 | type: '',
15 | message: '',
16 | errors: null,
17 | };
18 |
19 | export default function ResetPasswordForm({ email }: { email: string }) {
20 | const [showPassword, setShowPassword] = useState(false);
21 | const [showConfirmPassword, setShowConfirmPassword] = useState(false);
22 |
23 | const resetPasswordWithEmail = resetPassword.bind(null, email as string);
24 |
25 | const [state, submitAction, isPending] = useActionState(
26 | resetPasswordWithEmail,
27 | initialState,
28 | );
29 |
30 | const router = useRouter();
31 | useEffect(() => {
32 | if (state.type === 'success') {
33 | toast.success(state.message);
34 | router.push('/sign-in');
35 | }
36 | }, [router, state]);
37 |
38 | return (
39 |
101 | );
102 | }
103 |
--------------------------------------------------------------------------------
/src/app/(auth)/sign-in/SignInForm.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { useActionState, useState } from 'react';
4 | import { signIn } from '@/actions/auth';
5 | import Link from 'next/link';
6 | import { Label } from '@/components/ui/label';
7 | import { Input } from '@/components/ui/input';
8 | import { Eye, EyeOff } from 'lucide-react';
9 | import { Button } from '@/components/ui/button';
10 |
11 | const initialState = {
12 | type: '',
13 | message: '',
14 | data: {
15 | username: '',
16 | password: '',
17 | },
18 | errors: {
19 | username: undefined,
20 | password: undefined,
21 | },
22 | };
23 |
24 | export default function SignInForm() {
25 | const [showPassword, setShowPassword] = useState(false);
26 | const [state, submitAction, isPending] = useActionState(signIn, initialState);
27 |
28 | return (
29 |
88 | );
89 | }
90 |
--------------------------------------------------------------------------------
/src/app/(auth)/sign-in/page.tsx:
--------------------------------------------------------------------------------
1 | import Link from 'next/link';
2 | import SignInForm from './SignInForm';
3 | import { GithubSignIn, GoogleSignIn } from '@/components/AuthButton';
4 | import { redirect } from 'next/navigation';
5 | import { auth } from '@/auth';
6 | import type { Metadata } from 'next';
7 | import { WebAuthnLogin } from '@/components/WebAuthnButton';
8 | import RadialGradient from '@/components/ui/radial-gradient';
9 |
10 | export const metadata: Metadata = {
11 | title: 'Login',
12 | description: 'A simple login page',
13 | };
14 |
15 | export default async function SignIn(props: {
16 | searchParams?: Promise<{
17 | error?: string;
18 | }>;
19 | }) {
20 | const searchParams = await props.searchParams;
21 | const error = searchParams?.error || '';
22 | if (error) {
23 | redirect(`/error?error=${error}`);
24 | }
25 | const session = await auth();
26 | const user = session?.user;
27 | if (user) {
28 | redirect('/profile');
29 | }
30 | return (
31 |
32 |
33 |
34 |
35 | Welcome back
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 | Or
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 | Don’t have an account ?{' '}
54 |
58 | Sign up
59 |
60 |
61 |
62 |
63 |
64 |
65 | );
66 | }
67 |
--------------------------------------------------------------------------------
/src/app/(auth)/sign-in/two-factor/email/emailVerifyForm.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 | import {
3 | Card,
4 | CardHeader,
5 | CardTitle,
6 | CardDescription,
7 | CardContent,
8 | } from '@/components/ui/card';
9 | import { Label } from '@/components/ui/label';
10 | import {
11 | InputOTP,
12 | InputOTPGroup,
13 | InputOTPSlot,
14 | } from '@/components/ui/input-otp';
15 | import { verifyEmailTwoFactor } from '@/actions/auth';
16 | import Link from 'next/link';
17 | import { useActionState } from 'react';
18 | import { Button } from '@/components/ui/button';
19 |
20 | const initialState = {
21 | type: '',
22 | message: '',
23 | errors: { otp: undefined },
24 | };
25 |
26 | export default function TwoFactorEmailForm({ userId }: { userId: string }) {
27 | const actionWithUserId = verifyEmailTwoFactor.bind(null, userId as string);
28 | const [state, submitAction, isPending] = useActionState(
29 | actionWithUserId,
30 | initialState,
31 | );
32 | return (
33 |
34 |
35 | Verify your identity
36 |
37 | Enter the 6-digit code from your email.
38 |
39 |
40 |
41 |
72 |
73 | Go back
74 |
75 |
76 |
77 | );
78 | }
79 |
--------------------------------------------------------------------------------
/src/app/(auth)/sign-in/two-factor/email/page.tsx:
--------------------------------------------------------------------------------
1 | import { redirect } from 'next/navigation';
2 | import { cookies } from 'next/headers';
3 | import type { Metadata } from 'next';
4 | import TwoFactorEmailForm from './emailVerifyForm';
5 | import RadialGradient from '@/components/ui/radial-gradient';
6 |
7 | export const metadata: Metadata = {
8 | title: '2FA Email Verify',
9 | description: 'Enter code from your authenticator',
10 | };
11 |
12 | export default async function emailTwoFactorVerify() {
13 | const cookieStore = await cookies();
14 | const userId = cookieStore.get('authjs.two-factor');
15 |
16 | if (!userId) {
17 | redirect('/sign-in');
18 | }
19 |
20 | return (
21 |
22 |
23 |
24 |
25 | );
26 | }
27 |
--------------------------------------------------------------------------------
/src/app/(auth)/sign-in/two-factor/otp.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 | import {
3 | Card,
4 | CardHeader,
5 | CardTitle,
6 | CardDescription,
7 | CardContent,
8 | CardFooter,
9 | } from '@/components/ui/card';
10 | import { Label } from '@/components/ui/label';
11 | import {
12 | InputOTP,
13 | InputOTPGroup,
14 | InputOTPSlot,
15 | } from '@/components/ui/input-otp';
16 | import { verifyTwoFactor, twoFactorEmail } from '@/actions/auth';
17 | import Link from 'next/link';
18 | import { useActionState, useEffect } from 'react';
19 | import { useRouter } from 'next/navigation';
20 | import { Button } from '@/components/ui/button';
21 |
22 | const initialState = {
23 | type: '',
24 | message: '',
25 | errors: { otp: undefined },
26 | };
27 |
28 | export default function OtpForm({ userId }: { userId: string }) {
29 | const actionWithUserId = verifyTwoFactor.bind(null, userId as string);
30 | const [state, submitAction, isPending] = useActionState(
31 | actionWithUserId,
32 | initialState,
33 | );
34 |
35 | const twoFactorEmailWithUserID = twoFactorEmail.bind(null, userId as string);
36 |
37 | const [emailState, emailAction, isEmailPending] = useActionState(
38 | twoFactorEmailWithUserID,
39 | initialState,
40 | );
41 |
42 | const router = useRouter();
43 | useEffect(() => {
44 | if (emailState.type === 'success') {
45 | router.push('/sign-in/two-factor/email');
46 | }
47 | }, [emailState, router]);
48 |
49 | return (
50 |
51 |
52 | Verify your identity
53 |
54 | Enter the 6-digit code from your registered authenticator.
55 |
56 |
57 |
58 |
89 |
90 |
91 |
92 |
97 |
98 | Having problem accessing your account?{' '}
99 |
104 | Contact Admin
105 |
106 | .
107 |
108 |
109 |
110 |
111 | );
112 | }
113 |
--------------------------------------------------------------------------------
/src/app/(auth)/sign-in/two-factor/page.tsx:
--------------------------------------------------------------------------------
1 | import { redirect } from 'next/navigation';
2 | import { cookies } from 'next/headers';
3 | import OtpForm from './otp';
4 |
5 | import type { Metadata } from 'next';
6 | import RadialGradient from '@/components/ui/radial-gradient';
7 |
8 | export const metadata: Metadata = {
9 | title: '2FA Verify',
10 | description: 'Enter code from your authenticator',
11 | };
12 |
13 | export default async function TwoFactorLogin() {
14 | const cookieStore = await cookies();
15 | const userId = cookieStore.get('authjs.two-factor');
16 |
17 | if (!userId) {
18 | redirect('/sign-in');
19 | }
20 |
21 | return (
22 |
23 |
24 |
25 |
26 | );
27 | }
28 |
--------------------------------------------------------------------------------
/src/app/(auth)/sign-up/SignUpForm.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 | import { useActionState } from 'react';
3 | import { signUp } from '@/actions/auth';
4 | import { Label } from '@/components/ui/label';
5 | import { Input } from '@/components/ui/input';
6 | import { Button } from '@/components/ui/button';
7 |
8 | const initialState = {
9 | type: '',
10 | message: '',
11 | errors: null,
12 | resetKey: '',
13 | };
14 |
15 | export default function SignUpForm() {
16 | const [state, submitAction, isPending] = useActionState(signUp, initialState);
17 |
18 | return (
19 |
47 | );
48 | }
49 |
--------------------------------------------------------------------------------
/src/app/(auth)/sign-up/page.tsx:
--------------------------------------------------------------------------------
1 | import Link from 'next/link';
2 | import { GithubSignIn, GoogleSignIn } from '@/components/AuthButton';
3 | import SignUpForm from './SignUpForm';
4 | import type { Metadata } from 'next';
5 | import RadialGradient from '@/components/ui/radial-gradient';
6 |
7 | export const metadata: Metadata = {
8 | title: 'Register',
9 | description: 'A simple Register page',
10 | };
11 |
12 | export default function SignUp() {
13 | return (
14 |
15 |
16 |
17 |
18 | Create an account
19 |
20 |
21 | Enter your email below to create your account
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 | Or
31 |
32 |
33 |
34 |
35 |
36 |
37 | Already have an account ?{' '}
38 |
42 | Sign in
43 |
44 |
45 |
46 |
47 |
48 |
49 | );
50 | }
51 |
--------------------------------------------------------------------------------
/src/app/(dashboard)/dashboard/page.tsx:
--------------------------------------------------------------------------------
1 | import Link from 'next/link';
2 | import {
3 | CardTitle,
4 | CardDescription,
5 | CardHeader,
6 | CardContent,
7 | Card,
8 | } from '@/components/ui/card';
9 |
10 | import { Metadata } from 'next';
11 |
12 | export const metadata: Metadata = {
13 | title: 'Dashboard',
14 | description: 'Manage your tasks',
15 | };
16 |
17 | export default function Dashboard() {
18 | return (
19 |
20 |
21 |
22 | Welcome to your dashboard
23 |
24 | This is where you can manage your account and access your data.
25 |
26 |
27 |
28 |
29 |
30 |
Your Profile
31 |
32 | View and update your profile information.
33 |
34 |
35 | Go to profile
36 |
37 |
38 |
39 |
Settings
40 |
41 | Customize your app settings.
42 |
43 |
44 | Go to settings
45 |
46 |
47 |
48 |
49 |
50 |
51 | );
52 | }
53 |
--------------------------------------------------------------------------------
/src/app/(dashboard)/profile/_Components/AddPasswordButton.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 | import { useActionState, useEffect } from 'react';
3 | import { toast } from 'sonner';
4 | import { sendAddPasswordEmail } from '@/actions/auth';
5 | import { Button } from '@/components/ui/button';
6 |
7 | const initialState = {
8 | type: '',
9 | message: '',
10 | };
11 |
12 | export default function AddPasswordButton({ email }: { email: string }) {
13 | const action = sendAddPasswordEmail.bind(null, email as string);
14 | const [state, submitAction, isPending] = useActionState(action, initialState);
15 |
16 | useEffect(() => {
17 | if (state.type === 'success') {
18 | toast.success(state.message);
19 | } else if (state.type === 'error') {
20 | toast.error(state.message);
21 | }
22 | }, [state]);
23 |
24 | return (
25 | <>
26 |
36 | >
37 | );
38 | }
39 |
--------------------------------------------------------------------------------
/src/app/(dashboard)/profile/_Components/DeleteAccountButton.tsx:
--------------------------------------------------------------------------------
1 | import { deleteAccount } from '@/actions/auth';
2 | import { Button } from '@/components/ui/button';
3 | import {
4 | Dialog,
5 | DialogClose,
6 | DialogContent,
7 | DialogDescription,
8 | DialogFooter,
9 | DialogHeader,
10 | DialogTitle,
11 | DialogTrigger,
12 | } from '@/components/ui/dialog';
13 | import { SubmitButton } from '@/components/SubmitButton';
14 |
15 | export default function DeleteAccount({ userId }: { userId: string }) {
16 | const deleteAccountWithEmail = deleteAccount.bind(null, userId as string);
17 |
18 | return (
19 |
53 | );
54 | }
55 |
--------------------------------------------------------------------------------
/src/app/(dashboard)/profile/_Components/DisableTwoFactorButton.tsx:
--------------------------------------------------------------------------------
1 | import { disableTwoFactor } from '@/actions/auth';
2 | import { SubmitButton } from '@/components/SubmitButton';
3 |
4 | export default function DisableTwoFactorButton({ userId }: { userId: string }) {
5 | const bindUserId = disableTwoFactor.bind(null, userId as string);
6 | return (
7 |
12 | );
13 | }
14 |
--------------------------------------------------------------------------------
/src/app/(dashboard)/profile/_Components/EditProfileForm.tsx:
--------------------------------------------------------------------------------
1 | import { Label } from '@/components/ui/label';
2 | import { Input } from '@/components/ui/input';
3 | import { Button } from '@/components/ui/button';
4 |
5 | export default function EditProfileForm({ userData }: { userData: any }) {
6 | return (
7 | <>
8 |
42 | >
43 | );
44 | }
45 |
--------------------------------------------------------------------------------
/src/app/(dashboard)/profile/_Components/LinkAccountButton.tsx:
--------------------------------------------------------------------------------
1 | import { oAuthLogin } from '@/actions/auth';
2 | import { SubmitButton } from '@/components/SubmitButton';
3 |
4 | export default function LinkAccountButton({ provider }: { provider: string }) {
5 | const action = oAuthLogin.bind(null, provider as string);
6 |
7 | return (
8 |
13 | );
14 | }
15 |
--------------------------------------------------------------------------------
/src/app/(dashboard)/profile/_Components/UnlinkAccountButton.tsx:
--------------------------------------------------------------------------------
1 | import { oAuthRemove } from '@/actions/auth';
2 | import { SubmitButton } from '@/components/SubmitButton';
3 |
4 | export default function UnlinkAccountButton({
5 | userId,
6 | provider,
7 | }: {
8 | userId: string;
9 | provider: string;
10 | }) {
11 | const bindUserId = oAuthRemove.bind(null, userId as string);
12 | const bindProvider = bindUserId.bind(null, provider as string);
13 | return (
14 |
23 | );
24 | }
25 |
--------------------------------------------------------------------------------
/src/app/(dashboard)/profile/change-password/changePasswordForm.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { useActionState, useState } from 'react';
4 | import { useRouter } from 'next/navigation';
5 | import { changePassword } from '@/actions/auth';
6 | import { useEffect } from 'react';
7 | import { toast } from 'sonner';
8 | import { Label } from '@/components/ui/label';
9 | import { Input } from '@/components/ui/input';
10 | import { Eye, EyeOff } from 'lucide-react';
11 | import { Button } from '@/components/ui/button';
12 |
13 | const initialState = {
14 | type: '',
15 | message: '',
16 | data: {
17 | oldPassword: '',
18 | newPassword: '',
19 | password2: '',
20 | },
21 | errors: null,
22 | };
23 |
24 | export default function ChangePasswordForm({ email }: { email: string }) {
25 | const [showCurrentPassword, setShowCurrentPassword] = useState(false);
26 | const [showPassword, setShowPassword] = useState(false);
27 | const [showConfirmPassword, setShowConfirmPassword] = useState(false);
28 |
29 | const changePasswordWithEmail = changePassword.bind(null, email as string);
30 |
31 | const [state, submitAction, isPending] = useActionState(
32 | changePasswordWithEmail,
33 | initialState,
34 | );
35 |
36 | const router = useRouter();
37 | useEffect(() => {
38 | if (state.type === 'success') {
39 | toast.success(state.message);
40 | router.push('/profile');
41 | }
42 | }, [state, router]);
43 |
44 | return (
45 |
136 | );
137 | }
138 |
--------------------------------------------------------------------------------
/src/app/(dashboard)/profile/change-password/page.tsx:
--------------------------------------------------------------------------------
1 | import { redirect } from 'next/navigation';
2 | import ChangePasswordForm from './changePasswordForm';
3 | import { auth } from '@/auth';
4 | import Link from 'next/link';
5 | import { Metadata } from 'next';
6 | import RadialGradient from '@/components/ui/radial-gradient';
7 |
8 | export const metadata: Metadata = {
9 | title: 'Change Password',
10 | description: 'Change your password',
11 | };
12 |
13 | export default async function ChangePasswordPage() {
14 | const session = await auth();
15 | const user = session?.user;
16 | if (!user) {
17 | redirect('/sign-in');
18 | }
19 | return (
20 |
21 |
22 |
23 |
24 | Change Password
25 |
26 |
27 |
28 |
29 |
30 | Don’t remember current password ?{' '}
31 |
35 | Forgot password
36 |
37 |
38 |
39 |
40 |
41 |
42 | );
43 | }
44 |
--------------------------------------------------------------------------------
/src/app/(dashboard)/profile/two-factor/TwoFactorForm.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { useActionState, useEffect } from 'react';
4 | import { toast } from 'sonner';
5 | import { useRouter } from 'next/navigation';
6 |
7 | import { enableMfa } from '@/actions/auth';
8 | import { Label } from '@/components/ui/label';
9 | import { Input } from '@/components/ui/input';
10 | import { Button } from '@/components/ui/button';
11 |
12 | const initialState = {
13 | type: '',
14 | message: '',
15 | errors: null,
16 | };
17 |
18 | export default function TwoFactorForm({
19 | secret,
20 | email,
21 | }: {
22 | secret: string;
23 | email: string;
24 | }) {
25 | const actionWithSecret = enableMfa.bind(null, secret as string);
26 | const actionWithEmail = actionWithSecret.bind(null, email as string);
27 | const [state, submitAction, isPending] = useActionState(
28 | actionWithEmail,
29 | initialState,
30 | );
31 |
32 | const router = useRouter();
33 | useEffect(() => {
34 | if (state.type === 'success') {
35 | toast.success(state.message);
36 | router.push('/profile');
37 | }
38 | }, [router, state]);
39 |
40 | return (
41 |
42 | {state.errors && (
43 |
44 |
{state.message}
45 |
46 | )}
47 |
59 |
60 | );
61 | }
62 |
--------------------------------------------------------------------------------
/src/app/(dashboard)/profile/two-factor/page.tsx:
--------------------------------------------------------------------------------
1 | import { generateTOTP, getTOTPAuthUri } from '@epic-web/totp';
2 | import * as QRCode from 'qrcode';
3 | import { auth } from '@/auth';
4 | import { redirect } from 'next/navigation';
5 | import Image from 'next/image';
6 | import TwoFactorForm from './TwoFactorForm';
7 |
8 | import type { Metadata } from 'next';
9 |
10 | export const metadata: Metadata = {
11 | title: 'Register 2FA',
12 | description: 'Register two factor authentication.',
13 | };
14 |
15 | export default async function Component() {
16 | const session = await auth();
17 | const user = session?.user;
18 | if (!user) {
19 | redirect('/sign-in');
20 | }
21 | const { secret, period, digits, algorithm } = await generateTOTP();
22 | const otpUri = getTOTPAuthUri({
23 | period,
24 | digits,
25 | algorithm,
26 | secret,
27 | accountName: user.email!,
28 | issuer: 'AuthJs Template',
29 | });
30 |
31 | const qrCode = await QRCode.toDataURL(otpUri);
32 | return (
33 |
34 |
35 |
36 |
37 | Enable Two-Factor Authentication
38 |
39 |
40 | Protect your account with an extra layer of security by setting up
41 | two-factor authentication (2FA).
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
Scan the QR code with your authenticator app to set up 2FA.
51 |
52 |
53 | {secret}
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 | );
63 | }
64 |
--------------------------------------------------------------------------------
/src/app/admin/columns.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { ColumnDef } from '@tanstack/react-table';
4 | import { ArrowUpDown, MoreHorizontal } from 'lucide-react';
5 | import { Button } from '@/components/ui/button';
6 | import { Checkbox } from '@/components/ui/checkbox';
7 | import {
8 | DropdownMenu,
9 | DropdownMenuContent,
10 | DropdownMenuItem,
11 | DropdownMenuLabel,
12 | DropdownMenuSeparator,
13 | DropdownMenuTrigger,
14 | } from '@/components/ui/dropdown-menu';
15 |
16 | export type TUser = {
17 | id: string;
18 | name: string;
19 | email: string;
20 | username: string;
21 | role: string;
22 | };
23 |
24 | export const columns: ColumnDef[] = [
25 | {
26 | id: 'select',
27 | header: ({ table }) => (
28 | table.toggleAllPageRowsSelected(!!value)}
34 | aria-label='Select all'
35 | />
36 | ),
37 | cell: ({ row }) => (
38 | row.toggleSelected(!!value)}
41 | aria-label='Select row'
42 | />
43 | ),
44 | enableSorting: false,
45 | enableHiding: false,
46 | },
47 | {
48 | accessorKey: 'name',
49 | header: ({ column }) => {
50 | return (
51 |
58 | );
59 | },
60 | enableHiding: false,
61 | },
62 | {
63 | accessorKey: 'email',
64 | header: ({ column }) => {
65 | return (
66 |
73 | );
74 | },
75 | },
76 | {
77 | accessorKey: 'username',
78 | header: ({ column }) => {
79 | return (
80 |
87 | );
88 | },
89 | },
90 | {
91 | accessorKey: 'role',
92 | header: ({ column }) => {
93 | return (
94 |
101 | );
102 | },
103 | },
104 | {
105 | id: 'actions',
106 | cell: ({ row }) => {
107 | const user = row.original;
108 |
109 | return (
110 |
111 |
112 |
116 |
117 |
118 | Actions
119 | navigator.clipboard.writeText(user.email)}
121 | >
122 | Copy user email
123 |
124 |
125 | View User
126 | Edit User
127 |
128 |
131 |
132 |
133 |
134 | );
135 | },
136 | },
137 | ];
138 |
--------------------------------------------------------------------------------
/src/app/admin/data-table-pagination.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | ChevronLeftIcon,
3 | ChevronRightIcon,
4 | DoubleArrowLeftIcon,
5 | DoubleArrowRightIcon,
6 | } from '@radix-ui/react-icons';
7 | import { Table } from '@tanstack/react-table';
8 |
9 | import { Button } from '@/components/ui/button';
10 | import {
11 | Select,
12 | SelectContent,
13 | SelectItem,
14 | SelectTrigger,
15 | SelectValue,
16 | } from '@/components/ui/select';
17 |
18 | interface DataTablePaginationProps {
19 | table: Table;
20 | }
21 |
22 | export function DataTablePagination({
23 | table,
24 | }: DataTablePaginationProps) {
25 | return (
26 |
27 |
28 | {table.getFilteredSelectedRowModel().rows.length} of{' '}
29 | {table.getFilteredRowModel().rows.length} row(s) selected.
30 |
31 |
32 |
33 |
Rows per page
34 |
51 |
52 |
53 | Page {table.getState().pagination.pageIndex + 1} of{' '}
54 | {table.getPageCount()}
55 |
56 |
57 |
66 |
75 |
84 |
93 |
94 |
95 |
96 | );
97 | }
98 |
--------------------------------------------------------------------------------
/src/app/admin/data-table.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import React from 'react';
4 |
5 | import {
6 | ColumnDef,
7 | ColumnFiltersState,
8 | SortingState,
9 | flexRender,
10 | getCoreRowModel,
11 | getFilteredRowModel,
12 | getPaginationRowModel,
13 | getSortedRowModel,
14 | useReactTable,
15 | VisibilityState,
16 | } from '@tanstack/react-table';
17 |
18 | import {
19 | Table,
20 | TableBody,
21 | TableCell,
22 | TableHead,
23 | TableHeader,
24 | TableRow,
25 | } from '@/components/ui/table';
26 |
27 | import { MixerHorizontalIcon } from '@radix-ui/react-icons';
28 |
29 | import { Button } from '@/components/ui/button';
30 | import { Input } from '@/components/ui/input';
31 | import {
32 | DropdownMenu,
33 | DropdownMenuCheckboxItem,
34 | DropdownMenuContent,
35 | DropdownMenuLabel,
36 | DropdownMenuTrigger,
37 | DropdownMenuSeparator,
38 | } from '@/components/ui/dropdown-menu';
39 |
40 | import { DataTablePagination } from './data-table-pagination';
41 |
42 | interface DataTableProps {
43 | columns: ColumnDef[];
44 | data: TData[];
45 | }
46 |
47 | export function DataTable({
48 | columns,
49 | data,
50 | }: DataTableProps) {
51 | const [sorting, setSorting] = React.useState([]);
52 | const [columnFilters, setColumnFilters] = React.useState(
53 | [],
54 | );
55 | const [columnVisibility, setColumnVisibility] =
56 | React.useState({});
57 |
58 | const [rowSelection, setRowSelection] = React.useState({});
59 | const table = useReactTable({
60 | data,
61 | columns,
62 | getCoreRowModel: getCoreRowModel(),
63 | getPaginationRowModel: getPaginationRowModel(),
64 | onSortingChange: setSorting,
65 | getSortedRowModel: getSortedRowModel(),
66 | onColumnFiltersChange: setColumnFilters,
67 | getFilteredRowModel: getFilteredRowModel(),
68 | onColumnVisibilityChange: setColumnVisibility,
69 | onRowSelectionChange: setRowSelection,
70 | state: {
71 | sorting,
72 | columnFilters,
73 | columnVisibility,
74 | rowSelection,
75 | },
76 | });
77 |
78 | return (
79 |
80 |
81 |
85 | table.getColumn('role')?.setFilterValue(event.target.value)
86 | }
87 | className='max-w-sm'
88 | />
89 |
90 |
91 |
92 |
100 |
101 |
102 | Toggle columns
103 |
104 | {table
105 | .getAllColumns()
106 | .filter(
107 | (column) =>
108 | typeof column.accessorFn !== 'undefined' &&
109 | column.getCanHide(),
110 | )
111 | .map((column) => {
112 | return (
113 |
118 | column.toggleVisibility(!!value)
119 | }
120 | >
121 | {column.id}
122 |
123 | );
124 | })}
125 |
126 |
127 |
128 |
129 |
130 |
131 | {table.getHeaderGroups().map((headerGroup) => (
132 |
133 | {headerGroup.headers.map((header) => {
134 | return (
135 |
136 | {header.isPlaceholder
137 | ? null
138 | : flexRender(
139 | header.column.columnDef.header,
140 | header.getContext(),
141 | )}
142 |
143 | );
144 | })}
145 |
146 | ))}
147 |
148 |
149 | {table.getRowModel().rows?.length ? (
150 | table.getRowModel().rows.map((row) => (
151 |
155 | {row.getVisibleCells().map((cell) => (
156 |
157 | {flexRender(
158 | cell.column.columnDef.cell,
159 | cell.getContext(),
160 | )}
161 |
162 | ))}
163 |
164 | ))
165 | ) : (
166 |
167 |
171 | No results.
172 |
173 |
174 | )}
175 |
176 |
177 |
178 |
179 |
180 | );
181 | }
182 |
--------------------------------------------------------------------------------
/src/app/admin/page.tsx:
--------------------------------------------------------------------------------
1 | import { auth } from '@/auth';
2 | import { redirect } from 'next/navigation';
3 | import { columns } from './columns';
4 | import { DataTable } from './data-table';
5 | import { getUsers } from '@/db/query/User';
6 | import { User as DefaultUser } from 'next-auth';
7 | import { Button } from '@/components/ui/button';
8 | import Link from 'next/link';
9 |
10 | import type { Metadata } from 'next';
11 |
12 | export const metadata: Metadata = {
13 | title: 'Admin page',
14 | description: 'Manage users',
15 | };
16 |
17 | // Extend User interface
18 | interface User extends DefaultUser {
19 | role: string;
20 | username: string;
21 | }
22 | export default async function AdminPage() {
23 | const session = await auth();
24 | const user = session?.user as User;
25 | if (!user) {
26 | redirect('/sign-in');
27 | }
28 |
29 | if (user.role !== 'ADMIN') {
30 | redirect('/');
31 | }
32 |
33 | const users = await getUsers();
34 |
35 | return (
36 |
37 |
38 |
39 |
40 | Welcome back {user.name} !
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 | );
52 | }
53 |
--------------------------------------------------------------------------------
/src/app/api/auth/[...nextauth]/route.ts:
--------------------------------------------------------------------------------
1 | import { handlers } from '@/auth';
2 | export const { GET, POST } = handlers;
3 |
--------------------------------------------------------------------------------
/src/app/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/patelvivekdev/AuthJs-Template/3899f6c476dbd6616bc0757d4e8ee1fd0db3217e/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 | }
28 |
29 | .dark {
30 | --background: 0 0% 3.9%;
31 | --foreground: 0 0% 98%;
32 | --card: 0 0% 3.9%;
33 | --card-foreground: 0 0% 98%;
34 | --popover: 0 0% 3.9%;
35 | --popover-foreground: 0 0% 98%;
36 | --primary: 0 0% 98%;
37 | --primary-foreground: 0 0% 9%;
38 | --secondary: 0 0% 14.9%;
39 | --secondary-foreground: 0 0% 98%;
40 | --muted: 0 0% 14.9%;
41 | --muted-foreground: 0 0% 63.9%;
42 | --accent: 0 0% 14.9%;
43 | --accent-foreground: 0 0% 98%;
44 | --destructive: 0 62.8% 30.6%;
45 | --destructive-foreground: 0 0% 98%;
46 | --border: 0 0% 14.9%;
47 | --input: 0 0% 14.9%;
48 | --ring: 0 0% 83.1%;
49 | }
50 | }
51 |
52 | body {
53 | font-family: var(--font-rubik), sans-serif;
54 | }
55 |
56 | h1,
57 | h2,
58 | h3,
59 | h4,
60 | h5,
61 | h6 {
62 | font-family: var(--font-inter), serif;
63 | }
64 |
--------------------------------------------------------------------------------
/src/app/layout.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import type { Metadata } from 'next';
3 | import './globals.css';
4 | import { ThemeProvider } from 'next-themes';
5 | import { ThemeToggle } from '@/components/ThemeToggle';
6 | import { cn } from '@/lib/utils';
7 | import Navbar from '@/components/Navbar';
8 | import { Inter } from 'next/font/google';
9 | import { Rubik } from 'next/font/google';
10 | import { Toaster } from '@/components/ui/sonner';
11 |
12 | const inter = Inter({
13 | subsets: ['latin'],
14 | display: 'swap',
15 | variable: '--font-inter',
16 | });
17 | const rubik = Rubik({
18 | subsets: ['latin'],
19 | display: 'swap',
20 | variable: '--font-rubik',
21 | });
22 |
23 | export const metadata: Metadata = {
24 | title: 'AuthJs Template',
25 | description: 'A starter authentication template for Next.js',
26 | };
27 |
28 | export default async function RootLayout({
29 | children,
30 | }: Readonly<{
31 | children: React.ReactNode;
32 | }>) {
33 | return (
34 |
35 |
42 |
48 |
49 | {children}
50 |
51 |
52 |
53 |
54 |
55 | );
56 | }
57 |
--------------------------------------------------------------------------------
/src/app/page.tsx:
--------------------------------------------------------------------------------
1 | import OrbitingCirclesDemo from '@/components/Hero';
2 | import { cn } from '@/lib/utils';
3 | import DotPattern from '@/components/ui/dot-pattern';
4 |
5 | export default function Home() {
6 | return (
7 |
8 |
9 |
19 |
20 | );
21 | }
22 |
--------------------------------------------------------------------------------
/src/auth.ts:
--------------------------------------------------------------------------------
1 | import NextAuth, { AuthError } from 'next-auth';
2 | import Google from 'next-auth/providers/google';
3 | import Github from 'next-auth/providers/github';
4 | import Credentials from 'next-auth/providers/credentials';
5 | import { DrizzleAdapter } from '@auth/drizzle-adapter';
6 | import { db } from '@/db';
7 | import {
8 | getUserById,
9 | getUserByProviderAccountId,
10 | getUserByUsername,
11 | getUserForTotp,
12 | } from './db/query/User';
13 | import bcrypt from 'bcryptjs';
14 | import { encode, decode } from 'next-auth/jwt';
15 | import {
16 | users,
17 | accounts,
18 | sessions,
19 | verificationTokens,
20 | authenticators,
21 | } from '@/db/schema';
22 | import { cookies } from 'next/headers';
23 | import Passkey from 'next-auth/providers/passkey';
24 |
25 | class InvalidCredentialsError extends AuthError {
26 | code = 'invalid-credentials';
27 | message = 'Invalid credentials';
28 | }
29 |
30 | class OauthError extends AuthError {
31 | code = 'OauthError';
32 | message = 'Please use Social Login to continue';
33 | }
34 |
35 | export const { handlers, signIn, signOut, auth } = NextAuth({
36 | experimental: { enableWebAuthn: true },
37 | adapter: DrizzleAdapter(db, {
38 | usersTable: users,
39 | accountsTable: accounts,
40 | sessionsTable: sessions,
41 | verificationTokensTable: verificationTokens,
42 | authenticatorsTable: authenticators,
43 | }),
44 | providers: [
45 | Passkey({
46 | enableConditionalUI: true,
47 | getRelayingParty: () => ({
48 | id: process.env.BASE_ID ? process.env.BASE_ID : '',
49 | name: 'AuthJs Template',
50 | origin: process.env.BASE_URL ? process.env.BASE_URL : '',
51 | }),
52 | }),
53 | Google({
54 | async profile(profile) {
55 | return {
56 | id: profile.sub,
57 | name: profile.name,
58 | email: profile.email,
59 | image: profile.picture,
60 | username: profile.email,
61 | role: profile.email.endsWith('@patelvivek.dev') ? 'ADMIN' : 'USER',
62 | };
63 | },
64 | allowDangerousEmailAccountLinking: true,
65 | }),
66 | Github({
67 | async profile(profile) {
68 | return {
69 | id: profile.id.toString(),
70 | name: profile.name,
71 | email: profile.email,
72 | image: profile.avatar_url,
73 | username: profile.login,
74 | role: profile.email!.endsWith('@patelvivek.dev') ? 'ADMIN' : 'USER',
75 | };
76 | },
77 | allowDangerousEmailAccountLinking: true,
78 | }),
79 | Credentials({
80 | id: 'credentials',
81 | name: 'credentials',
82 | credentials: {
83 | username: { label: 'Username', type: 'text' },
84 | password: { label: 'Password', type: 'password' },
85 | },
86 | async authorize(credentials) {
87 | const user = await getUserByUsername(credentials.username as string);
88 |
89 | if (user.length === 0) {
90 | throw new InvalidCredentialsError();
91 | }
92 |
93 | if (user[0].password! === null) {
94 | throw new OauthError();
95 | }
96 |
97 | const isValid = await bcrypt.compare(
98 | credentials.password as string,
99 | user[0].password!,
100 | );
101 |
102 | if (!isValid) {
103 | throw new InvalidCredentialsError();
104 | }
105 | return user[0];
106 | },
107 | }),
108 | Credentials({
109 | id: 'TOTP',
110 | name: 'TOTP',
111 | credentials: {
112 | userId: { label: 'userId', type: 'text' },
113 | TOTP: { label: 'TOTP', type: 'text' },
114 | },
115 | async authorize(credentials) {
116 | const user = await getUserForTotp(credentials.userId as string);
117 |
118 | if (user.length === 0) {
119 | throw new InvalidCredentialsError();
120 | }
121 | return user[0];
122 | },
123 | }),
124 | ],
125 | callbacks: {
126 | async signIn({ user, credentials, account }) {
127 | const cookieStore = await cookies();
128 | const sessionToken = cookieStore.get('authjs.session-token');
129 | if (credentials) {
130 | // @ts-ignore
131 | if (credentials.TOTP === 'TOTP') {
132 | return true;
133 | }
134 | }
135 |
136 | // check if account is already linked or not after user is authenticated
137 | if (sessionToken?.value) {
138 | if (account) {
139 | const user = await getUserByProviderAccountId(
140 | account.providerAccountId,
141 | );
142 | if (!user) {
143 | return true;
144 | }
145 | return '/sign-in/?error=OAuthAccountNotLinked';
146 | }
147 | }
148 |
149 | // @ts-ignore
150 | if (user.isTotpEnabled) {
151 | const cookieStore = await cookies();
152 | cookieStore.set({
153 | name: 'authjs.two-factor',
154 | // @ts-ignore
155 | value: user.id!,
156 | httpOnly: true,
157 | path: '/',
158 | });
159 | return '/sign-in/two-factor';
160 | }
161 | return true;
162 | },
163 | authorized({ auth, request: { nextUrl } }) {
164 | const isLoggedIn = !!auth?.user;
165 | const paths = ['/profile', '/dashboard'];
166 | const isProtected = paths.some((path) =>
167 | nextUrl.pathname.startsWith(path),
168 | );
169 |
170 | const publicPath = ['/sign-up'];
171 | const isPublic = publicPath.some((path) =>
172 | nextUrl.pathname.startsWith(path),
173 | );
174 | if (isPublic && isLoggedIn) {
175 | return Response.redirect(new URL('/profile', nextUrl.origin));
176 | }
177 |
178 | if (isProtected && !isLoggedIn) {
179 | const redirectUrl = new URL('/sign-in', nextUrl.origin);
180 | redirectUrl.searchParams.append('callbackUrl', nextUrl.href);
181 | return Response.redirect(redirectUrl);
182 | }
183 | return true;
184 | },
185 | jwt: async ({ token }) => {
186 | const user = await getUserById(token.sub!);
187 | if (user) {
188 | token.user = user;
189 | token.role = user.role;
190 | return token;
191 | } else {
192 | return null;
193 | }
194 | },
195 | session: async ({ session, token }) => {
196 | if (token) {
197 | // @ts-ignore
198 | session.role = token.role;
199 | // @ts-ignore
200 | session.user = token.user;
201 | session.user.id = token.sub!;
202 | return session;
203 | }
204 | return session;
205 | },
206 | },
207 | session: { strategy: 'jwt' },
208 | jwt: { encode, decode },
209 | secret: process.env.AUTH_SECRET,
210 | pages: {
211 | signIn: '/sign-in',
212 | error: '/error',
213 | },
214 | });
215 |
--------------------------------------------------------------------------------
/src/components/AuthButton.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { signIn, signOut } from '@/auth';
3 | import { Button } from './ui/button';
4 | import { Icons } from './icons';
5 |
6 | export function SignIn({ provider }: { provider?: string }) {
7 | return (
8 |
22 | );
23 | }
24 |
25 | export function GithubSignIn() {
26 | return (
27 |
43 | );
44 | }
45 |
46 | export function GoogleSignIn() {
47 | return (
48 |
64 | );
65 | }
66 |
67 | export function WebAuthIn() {
68 | return (
69 |
80 | );
81 | }
82 |
83 | export function SignOut() {
84 | return (
85 |
99 | );
100 | }
101 |
--------------------------------------------------------------------------------
/src/components/Hero.tsx:
--------------------------------------------------------------------------------
1 | import Image from 'next/image';
2 | import SparklesText from '@/components/ui/sparkles-text';
3 | import OrbitingCircles from '@/components/ui/orbiting-circles';
4 | import magicUiIcon from '@/../public/magicui.png';
5 | import reactDarkIcon from '@/../public/react_dark.svg';
6 | import vercelIcon from '@/../public/vercel.svg';
7 | import nextIcon from '@/../public/nextjs.svg';
8 | import authJsIcon from '@/../public/authjs.webp';
9 | import drizzleIcon from '@/../public/drizzle.jpg';
10 | import tursoIcon from '@/../public/turso.jpg';
11 | import resendIcon from '@/../public/resend.jpg';
12 |
13 | export default function OrbitingCirclesDemo() {
14 | return (
15 |
16 |
20 |
21 |
22 | A starter authentication template for Next.js
23 |
24 |
25 |
31 |
32 |
33 |
39 |
40 |
41 |
47 |
48 |
49 |
50 |
56 |
57 |
58 |
64 |
65 |
66 |
72 |
73 |
74 |
80 |
81 |
82 |
88 |
89 |
90 |
91 | {/* 100 Circles */}
92 |
98 |
99 |
100 |
106 |
107 |
108 |
114 |
115 |
116 |
117 |
123 |
124 |
125 |
131 |
132 |
133 |
139 |
140 |
141 |
147 |
148 |
149 |
155 |
156 |
157 |
158 | );
159 | }
160 |
--------------------------------------------------------------------------------
/src/components/Navbar.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { auth } from '@/auth';
3 | import Link from 'next/link';
4 | import { Button } from './ui/button';
5 | import { SignOut } from './AuthButton';
6 | import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
7 |
8 | export default async function Navbar() {
9 | const session = await auth();
10 | const user = session?.user;
11 |
12 | return (
13 |
54 | );
55 | }
56 |
--------------------------------------------------------------------------------
/src/components/SubmitButton.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import React from 'react';
4 | import { useFormStatus } from 'react-dom';
5 | import { Button } from './ui/button';
6 |
7 | export function SubmitButton({
8 | variant,
9 | size,
10 | children,
11 | pendingText,
12 | }: {
13 | variant?:
14 | | 'default'
15 | | 'destructive'
16 | | 'outline'
17 | | 'secondary'
18 | | 'ghost'
19 | | 'link'
20 | | null;
21 | size?: 'sm' | 'default' | 'lg' | 'icon' | null;
22 | children: React.ReactNode;
23 | pendingText?: string;
24 | }) {
25 | const { pending } = useFormStatus();
26 |
27 | pendingText = pendingText || 'Submitting...';
28 |
29 | return (
30 |
39 | );
40 | }
41 |
--------------------------------------------------------------------------------
/src/components/ThemeToggle.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 | import React, { useEffect, useState } from 'react';
3 | import { useTheme } from 'next-themes';
4 |
5 | export function ThemeToggle() {
6 | const { resolvedTheme, setTheme } = useTheme();
7 | const [loaded, setLoaded] = useState(false);
8 | useEffect(() => {
9 | setLoaded(true);
10 | }, [setLoaded]);
11 |
12 | return (
13 |
47 | );
48 | }
49 |
--------------------------------------------------------------------------------
/src/components/TokenNotFound.tsx:
--------------------------------------------------------------------------------
1 | import Link from 'next/link';
2 | import { Button } from './ui/button';
3 |
4 | export function TokenNotFound({
5 | header,
6 | description,
7 | url,
8 | buttonText,
9 | }: {
10 | header: string;
11 | description: string;
12 | url: string;
13 | buttonText: string;
14 | }) {
15 | return (
16 |
17 |
18 |
19 | {header}
20 |
21 |
22 | {description}
23 |
24 |
25 |
26 |
27 |
28 |
29 | );
30 | }
31 |
--------------------------------------------------------------------------------
/src/components/WebAuthnButton.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { signIn } from 'next-auth/webauthn';
4 | import { Button } from './ui/button';
5 | import { Icons } from './icons';
6 |
7 | export function WebAuthnRegister() {
8 | return (
9 |
17 | );
18 | }
19 |
20 | export function WebAuthnLogin() {
21 | return (
22 |
30 | );
31 | }
32 |
--------------------------------------------------------------------------------
/src/components/ui/avatar.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import * as React from 'react';
4 | import * as AvatarPrimitive from '@radix-ui/react-avatar';
5 |
6 | import { cn } from '@/lib/utils';
7 |
8 | const Avatar = React.forwardRef<
9 | React.ElementRef,
10 | React.ComponentPropsWithoutRef
11 | >(({ className, ...props }, ref) => (
12 |
20 | ));
21 | Avatar.displayName = AvatarPrimitive.Root.displayName;
22 |
23 | const AvatarImage = React.forwardRef<
24 | React.ElementRef,
25 | React.ComponentPropsWithoutRef
26 | >(({ className, ...props }, ref) => (
27 |
32 | ));
33 | AvatarImage.displayName = AvatarPrimitive.Image.displayName;
34 |
35 | const AvatarFallback = React.forwardRef<
36 | React.ElementRef,
37 | React.ComponentPropsWithoutRef
38 | >(({ className, ...props }, ref) => (
39 |
47 | ));
48 | AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName;
49 |
50 | export { Avatar, AvatarImage, AvatarFallback };
51 |
--------------------------------------------------------------------------------
/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 ring-offset-white transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-gray-950 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 dark:ring-offset-gray-950 dark:focus-visible:ring-gray-300',
9 | {
10 | variants: {
11 | variant: {
12 | default:
13 | 'bg-gray-900 text-gray-50 hover:bg-gray-900/90 dark:bg-gray-50 dark:text-gray-900 dark:hover:bg-gray-50/90',
14 | destructive:
15 | 'bg-red-500 text-gray-50 hover:bg-red-500/90 dark:bg-red-900 dark:text-gray-50 dark:hover:bg-red-900/90',
16 | outline:
17 | 'border border-gray-200 bg-white hover:bg-gray-100 hover:text-gray-900 dark:border-gray-800 dark:bg-gray-950 dark:hover:bg-gray-800 dark:hover:text-gray-50',
18 | secondary:
19 | 'bg-gray-100 text-gray-900 hover:bg-gray-100/80 dark:bg-gray-800 dark:text-gray-50 dark:hover:bg-gray-800/80',
20 | ghost:
21 | 'hover:bg-gray-100 hover:text-gray-900 dark:hover:bg-gray-800 dark:hover:text-gray-50',
22 | link: 'text-gray-900 underline-offset-4 hover:underline dark:text-gray-50',
23 | },
24 | size: {
25 | default: 'h-10 px-4 py-2',
26 | sm: 'h-9 rounded-md px-3',
27 | lg: 'h-11 rounded-md px-8',
28 | icon: 'h-10 w-10',
29 | },
30 | },
31 | defaultVariants: {
32 | variant: 'default',
33 | size: 'default',
34 | },
35 | },
36 | );
37 |
38 | export interface ButtonProps
39 | extends React.ButtonHTMLAttributes,
40 | VariantProps {
41 | asChild?: boolean;
42 | }
43 |
44 | const Button = React.forwardRef(
45 | ({ className, variant, size, asChild = false, ...props }, ref) => {
46 | const Comp = asChild ? Slot : 'button';
47 | return (
48 |
53 | );
54 | },
55 | );
56 | Button.displayName = 'Button';
57 |
58 | export { Button, buttonVariants };
59 |
--------------------------------------------------------------------------------
/src/components/ui/card.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 |
3 | import { cn } from '@/lib/utils';
4 |
5 | const Card = React.forwardRef<
6 | HTMLDivElement,
7 | React.HTMLAttributes
8 | >(({ className, ...props }, ref) => (
9 |
17 | ));
18 | Card.displayName = 'Card';
19 |
20 | const CardHeader = React.forwardRef<
21 | HTMLDivElement,
22 | React.HTMLAttributes
23 | >(({ className, ...props }, ref) => (
24 |
29 | ));
30 | CardHeader.displayName = 'CardHeader';
31 |
32 | const CardTitle = React.forwardRef<
33 | HTMLParagraphElement,
34 | React.HTMLAttributes
35 | >(({ className, ...props }, ref) => (
36 |
44 | ));
45 | CardTitle.displayName = 'CardTitle';
46 |
47 | const CardDescription = React.forwardRef<
48 | HTMLParagraphElement,
49 | React.HTMLAttributes
50 | >(({ className, ...props }, ref) => (
51 |
56 | ));
57 | CardDescription.displayName = 'CardDescription';
58 |
59 | const CardContent = React.forwardRef<
60 | HTMLDivElement,
61 | React.HTMLAttributes
62 | >(({ className, ...props }, ref) => (
63 |
64 | ));
65 | CardContent.displayName = 'CardContent';
66 |
67 | const CardFooter = React.forwardRef<
68 | HTMLDivElement,
69 | React.HTMLAttributes
70 | >(({ className, ...props }, ref) => (
71 |
76 | ));
77 | CardFooter.displayName = 'CardFooter';
78 |
79 | export {
80 | Card,
81 | CardHeader,
82 | CardFooter,
83 | CardTitle,
84 | CardDescription,
85 | CardContent,
86 | };
87 |
--------------------------------------------------------------------------------
/src/components/ui/checkbox.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import * as React from 'react';
4 | import * as CheckboxPrimitive from '@radix-ui/react-checkbox';
5 | import { Check } from 'lucide-react';
6 |
7 | import { cn } from '@/lib/utils';
8 |
9 | const Checkbox = React.forwardRef<
10 | React.ElementRef,
11 | React.ComponentPropsWithoutRef
12 | >(({ className, ...props }, ref) => (
13 |
21 |
24 |
25 |
26 |
27 | ));
28 | Checkbox.displayName = CheckboxPrimitive.Root.displayName;
29 |
30 | export { Checkbox };
31 |
--------------------------------------------------------------------------------
/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 { X } from 'lucide-react';
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 | DialogClose,
116 | DialogTrigger,
117 | DialogContent,
118 | DialogHeader,
119 | DialogFooter,
120 | DialogTitle,
121 | DialogDescription,
122 | };
123 |
--------------------------------------------------------------------------------
/src/components/ui/dot-pattern.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from '@/lib/utils';
2 | import { useId } from 'react';
3 |
4 | interface DotPatternProps {
5 | width?: any;
6 | height?: any;
7 | x?: any;
8 | y?: any;
9 | cx?: any;
10 | cy?: any;
11 | cr?: any;
12 | className?: string;
13 | [key: string]: any;
14 | }
15 | export function DotPattern({
16 | width = 16,
17 | height = 16,
18 | x = 0,
19 | y = 0,
20 | cx = 1,
21 | cy = 1,
22 | cr = 1,
23 | className,
24 | ...props
25 | }: DotPatternProps) {
26 | const id = useId();
27 |
28 | return (
29 |
52 | );
53 | }
54 |
55 | export default DotPattern;
56 |
--------------------------------------------------------------------------------
/src/components/ui/input-otp.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import * as React from 'react';
4 | import { OTPInput, OTPInputContext } from 'input-otp';
5 | import { Dot } from 'lucide-react';
6 |
7 | import { cn } from '@/lib/utils';
8 |
9 | const InputOTP = React.forwardRef<
10 | React.ElementRef,
11 | React.ComponentPropsWithoutRef
12 | >(({ className, containerClassName, ...props }, ref) => (
13 |
22 | ));
23 | InputOTP.displayName = 'InputOTP';
24 |
25 | const InputOTPGroup = React.forwardRef<
26 | React.ElementRef<'div'>,
27 | React.ComponentPropsWithoutRef<'div'>
28 | >(({ className, ...props }, ref) => (
29 |
30 | ));
31 | InputOTPGroup.displayName = 'InputOTPGroup';
32 |
33 | const InputOTPSlot = React.forwardRef<
34 | React.ElementRef<'div'>,
35 | React.ComponentPropsWithoutRef<'div'> & { index: number }
36 | >(({ index, className, ...props }, ref) => {
37 | const inputOTPContext = React.useContext(OTPInputContext);
38 | const { char, hasFakeCaret, isActive } = inputOTPContext.slots[index];
39 |
40 | return (
41 |
51 | {char}
52 | {hasFakeCaret && (
53 |
56 | )}
57 |
58 | );
59 | });
60 | InputOTPSlot.displayName = 'InputOTPSlot';
61 |
62 | const InputOTPSeparator = React.forwardRef<
63 | React.ElementRef<'div'>,
64 | React.ComponentPropsWithoutRef<'div'>
65 | >(({ ...props }, ref) => (
66 |
67 |
68 |
69 | ));
70 | InputOTPSeparator.displayName = 'InputOTPSeparator';
71 |
72 | export { InputOTP, InputOTPGroup, InputOTPSlot, InputOTPSeparator };
73 |
--------------------------------------------------------------------------------
/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/orbiting-circles.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 | import React from 'react';
3 | import { cn } from '@/lib/utils';
4 |
5 | export default function OrbitingCircles({
6 | className,
7 | children,
8 | reverse,
9 | duration = 20,
10 | delay = 10,
11 | radius = 50,
12 | path = false,
13 | }: {
14 | className?: string;
15 | children?: React.ReactNode;
16 | reverse?: boolean;
17 | duration?: number;
18 | delay?: number;
19 | radius?: number;
20 | path?: boolean;
21 | }) {
22 | return (
23 | <>
24 | {path && (
25 |
39 | )}
40 |
41 |
55 | {children}
56 |
57 | >
58 | );
59 | }
60 |
--------------------------------------------------------------------------------
/src/components/ui/radial-gradient.tsx:
--------------------------------------------------------------------------------
1 | import { CSSProperties } from 'react';
2 |
3 | type Type = 'circle' | 'ellipse';
4 |
5 | type Origin =
6 | | 'center'
7 | | 'top'
8 | | 'bottom'
9 | | 'left'
10 | | 'right'
11 | | 'top left'
12 | | 'top right'
13 | | 'bottom left'
14 | | 'bottom right';
15 |
16 | interface RadialProps {
17 | /**
18 | * The type of radial gradient
19 | * @default circle
20 | * @type string
21 | */
22 | type?: Type;
23 | /**
24 | * The color to transition from
25 | * @default #00000000
26 | * @type string
27 | * */
28 | from?: string;
29 |
30 | /**
31 | * The color to transition to
32 | * @default #290A5C
33 | * @type string
34 | * */
35 | to?: string;
36 |
37 | /**
38 | * The width of the ellipse gradient in pixels
39 | * @default 300
40 | * @type number
41 | * */
42 | width?: number;
43 |
44 | /**
45 | * The height of the ellipse gradient in pixels
46 | * @default 300
47 | * @type number
48 | * */
49 | height?: number;
50 |
51 | /**
52 | * The size of the gradient in pixels
53 | * @default 300
54 | * @type number
55 | * */
56 | size?: number;
57 |
58 | /**
59 | * The origin of the gradient
60 | * @default center
61 | * @type string
62 | * */
63 | origin?: Origin;
64 |
65 | /**
66 | * The class name to apply to the gradient
67 | * @default ""
68 | * @type string
69 | * */
70 | className?: string;
71 | }
72 |
73 | const RadialGradient = ({
74 | type = 'circle',
75 | from = 'rgba(255, 99, 71, 0.8)',
76 | to = 'hsla(0, 0%, 0%, 0)',
77 | size = 300,
78 | width = 500,
79 | height = 200,
80 | origin = 'center',
81 | className,
82 | }: RadialProps) => {
83 | const styles: CSSProperties =
84 | type === 'ellipse'
85 | ? {
86 | position: 'absolute',
87 | pointerEvents: 'none',
88 | inset: 0,
89 | backgroundImage: `radial-gradient(${width}px ${height}px at ${origin}, ${from}, ${to})`,
90 | }
91 | : {
92 | position: 'absolute',
93 | pointerEvents: 'none',
94 | inset: 0,
95 | backgroundImage: `radial-gradient(${type} ${size}px at ${origin}, ${from}, ${to})`,
96 | };
97 |
98 | return ;
99 | };
100 |
101 | export default RadialGradient;
102 |
--------------------------------------------------------------------------------
/src/components/ui/select.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import * as React from 'react';
4 | import * as SelectPrimitive from '@radix-ui/react-select';
5 | import { Check, ChevronDown, ChevronUp } from 'lucide-react';
6 |
7 | import { cn } from '@/lib/utils';
8 |
9 | const Select = SelectPrimitive.Root;
10 |
11 | const SelectGroup = SelectPrimitive.Group;
12 |
13 | const SelectValue = SelectPrimitive.Value;
14 |
15 | const SelectTrigger = React.forwardRef<
16 | React.ElementRef,
17 | React.ComponentPropsWithoutRef
18 | >(({ className, children, ...props }, ref) => (
19 | span]:line-clamp-1',
23 | className,
24 | )}
25 | {...props}
26 | >
27 | {children}
28 |
29 |
30 |
31 |
32 | ));
33 | SelectTrigger.displayName = SelectPrimitive.Trigger.displayName;
34 |
35 | const SelectScrollUpButton = React.forwardRef<
36 | React.ElementRef,
37 | React.ComponentPropsWithoutRef
38 | >(({ className, ...props }, ref) => (
39 |
47 |
48 |
49 | ));
50 | SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName;
51 |
52 | const SelectScrollDownButton = React.forwardRef<
53 | React.ElementRef,
54 | React.ComponentPropsWithoutRef
55 | >(({ className, ...props }, ref) => (
56 |
64 |
65 |
66 | ));
67 | SelectScrollDownButton.displayName =
68 | SelectPrimitive.ScrollDownButton.displayName;
69 |
70 | const SelectContent = React.forwardRef<
71 | React.ElementRef,
72 | React.ComponentPropsWithoutRef
73 | >(({ className, children, position = 'popper', ...props }, ref) => (
74 |
75 |
86 |
87 |
94 | {children}
95 |
96 |
97 |
98 |
99 | ));
100 | SelectContent.displayName = SelectPrimitive.Content.displayName;
101 |
102 | const SelectLabel = React.forwardRef<
103 | React.ElementRef,
104 | React.ComponentPropsWithoutRef
105 | >(({ className, ...props }, ref) => (
106 |
111 | ));
112 | SelectLabel.displayName = SelectPrimitive.Label.displayName;
113 |
114 | const SelectItem = React.forwardRef<
115 | React.ElementRef,
116 | React.ComponentPropsWithoutRef
117 | >(({ className, children, ...props }, ref) => (
118 |
126 |
127 |
128 |
129 |
130 |
131 |
132 | {children}
133 |
134 | ));
135 | SelectItem.displayName = SelectPrimitive.Item.displayName;
136 |
137 | const SelectSeparator = React.forwardRef<
138 | React.ElementRef,
139 | React.ComponentPropsWithoutRef
140 | >(({ className, ...props }, ref) => (
141 |
146 | ));
147 | SelectSeparator.displayName = SelectPrimitive.Separator.displayName;
148 |
149 | export {
150 | Select,
151 | SelectGroup,
152 | SelectValue,
153 | SelectTrigger,
154 | SelectContent,
155 | SelectLabel,
156 | SelectItem,
157 | SelectSeparator,
158 | SelectScrollUpButton,
159 | SelectScrollDownButton,
160 | };
161 |
--------------------------------------------------------------------------------
/src/components/ui/sonner.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { useTheme } from 'next-themes';
4 | import React from 'react';
5 | import { Toaster as Sonner } from 'sonner';
6 |
7 | type ToasterProps = React.ComponentProps;
8 |
9 | const Toaster = ({ ...props }: ToasterProps) => {
10 | const { theme = 'system' } = useTheme();
11 |
12 | return (
13 |
30 | );
31 | };
32 |
33 | export { Toaster };
34 |
--------------------------------------------------------------------------------
/src/components/ui/sparkles-text.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import React from 'react';
4 | import { cn } from '@/lib/utils';
5 | import { motion } from 'framer-motion';
6 | import { CSSProperties, ReactElement, useEffect, useState } from 'react';
7 |
8 | interface ISparkle {
9 | id: string;
10 | x: string;
11 | y: string;
12 | color: string;
13 | delay: number;
14 | scale: number;
15 | lifespan: number;
16 | }
17 |
18 | interface SparklesTextProps {
19 | /**
20 | * @default
21 | * @type ReactElement
22 | * @description
23 | * The component to be rendered as the text
24 | * */
25 | as?: ReactElement;
26 |
27 | /**
28 | * @default ""
29 | * @type string
30 | * @description
31 | * The className of the text
32 | */
33 | className?: string;
34 |
35 | /**
36 | * @required
37 | * @type string
38 | * @description
39 | * The text to be displayed
40 | * */
41 | text: string;
42 |
43 | /**
44 | * @default 10
45 | * @type number
46 | * @description
47 | * The count of sparkles
48 | * */
49 | sparklesCount?: number;
50 |
51 | /**
52 | * @default "{first: '#A07CFE', second: '#FE8FB5'}"
53 | * @type string
54 | * @description
55 | * The colors of the sparkles
56 | * */
57 | colors?: {
58 | first: string;
59 | second: string;
60 | };
61 | }
62 |
63 | const SparklesText: React.FC = ({
64 | text,
65 | colors = { first: '#A07CFE', second: '#FE8FB5' },
66 | className,
67 | sparklesCount = 10,
68 | ...props
69 | }) => {
70 | const [sparkles, setSparkles] = useState([]);
71 |
72 | useEffect(() => {
73 | const generateStar = (): ISparkle => {
74 | const starX = `${Math.random() * 100}%`;
75 | const starY = `${Math.random() * 100}%`;
76 | const color = Math.random() > 0.5 ? colors.first : colors.second;
77 | const delay = Math.random() * 2;
78 | const scale = Math.random() * 1 + 0.3;
79 | const lifespan = Math.random() * 10 + 5;
80 | const id = `${starX}-${starY}-${Date.now()}`;
81 | return { id, x: starX, y: starY, color, delay, scale, lifespan };
82 | };
83 |
84 | const initializeStars = () => {
85 | const newSparkles = Array.from({ length: sparklesCount }, generateStar);
86 | setSparkles(newSparkles);
87 | };
88 |
89 | const updateStars = () => {
90 | setSparkles((currentSparkles) =>
91 | currentSparkles.map((star) => {
92 | if (star.lifespan <= 0) {
93 | return generateStar();
94 | } else {
95 | return { ...star, lifespan: star.lifespan - 0.1 };
96 | }
97 | }),
98 | );
99 | };
100 |
101 | initializeStars();
102 | const interval = setInterval(updateStars, 100);
103 |
104 | return () => clearInterval(interval);
105 | }, [colors.first, colors.second]);
106 |
107 | return (
108 |
118 |
119 | {sparkles.map((sparkle) => (
120 |
121 | ))}
122 |
123 | {text}
124 |
125 |
126 |
127 | );
128 | };
129 |
130 | const Sparkle: React.FC = ({ id, x, y, color, delay, scale }) => {
131 | return (
132 |
146 |
150 |
151 | );
152 | };
153 |
154 | export default SparklesText;
155 |
--------------------------------------------------------------------------------
/src/components/ui/table.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 |
3 | import { cn } from '@/lib/utils';
4 |
5 | const Table = React.forwardRef<
6 | HTMLTableElement,
7 | React.HTMLAttributes
8 | >(({ className, ...props }, ref) => (
9 |
16 | ));
17 | Table.displayName = 'Table';
18 |
19 | const TableHeader = React.forwardRef<
20 | HTMLTableSectionElement,
21 | React.HTMLAttributes
22 | >(({ className, ...props }, ref) => (
23 |
24 | ));
25 | TableHeader.displayName = 'TableHeader';
26 |
27 | const TableBody = React.forwardRef<
28 | HTMLTableSectionElement,
29 | React.HTMLAttributes
30 | >(({ className, ...props }, ref) => (
31 |
36 | ));
37 | TableBody.displayName = 'TableBody';
38 |
39 | const TableFooter = React.forwardRef<
40 | HTMLTableSectionElement,
41 | React.HTMLAttributes
42 | >(({ className, ...props }, ref) => (
43 | tr]:last:border-b-0',
47 | className,
48 | )}
49 | {...props}
50 | />
51 | ));
52 | TableFooter.displayName = 'TableFooter';
53 |
54 | const TableRow = React.forwardRef<
55 | HTMLTableRowElement,
56 | React.HTMLAttributes
57 | >(({ className, ...props }, ref) => (
58 |
66 | ));
67 | TableRow.displayName = 'TableRow';
68 |
69 | const TableHead = React.forwardRef<
70 | HTMLTableCellElement,
71 | React.ThHTMLAttributes
72 | >(({ className, ...props }, ref) => (
73 | |
81 | ));
82 | TableHead.displayName = 'TableHead';
83 |
84 | const TableCell = React.forwardRef<
85 | HTMLTableCellElement,
86 | React.TdHTMLAttributes
87 | >(({ className, ...props }, ref) => (
88 | |
93 | ));
94 | TableCell.displayName = 'TableCell';
95 |
96 | const TableCaption = React.forwardRef<
97 | HTMLTableCaptionElement,
98 | React.HTMLAttributes
99 | >(({ className, ...props }, ref) => (
100 |
105 | ));
106 | TableCaption.displayName = 'TableCaption';
107 |
108 | export {
109 | Table,
110 | TableHeader,
111 | TableBody,
112 | TableFooter,
113 | TableHead,
114 | TableRow,
115 | TableCell,
116 | TableCaption,
117 | };
118 |
--------------------------------------------------------------------------------
/src/db/index.ts:
--------------------------------------------------------------------------------
1 | import { drizzle } from 'drizzle-orm/libsql';
2 | import { createClient } from '@libsql/client';
3 | import * as schema from './schema';
4 |
5 | if (!process.env.TURSO_DATABASE_URL) {
6 | throw new Error('TURSO_DATABASE_URL is not defined');
7 | }
8 |
9 | if (!process.env.TURSO_AUTH_TOKEN) {
10 | throw new Error('TURSO_AUTH_TOKEN is not defined');
11 | }
12 |
13 | const client = createClient({
14 | url: process.env.TURSO_DATABASE_URL!,
15 | authToken: process.env.TURSO_AUTH_TOKEN,
16 | });
17 |
18 | export const db = drizzle(client, { schema });
19 |
--------------------------------------------------------------------------------
/src/db/schema.ts:
--------------------------------------------------------------------------------
1 | import {
2 | integer,
3 | sqliteTable,
4 | text,
5 | primaryKey,
6 | } from 'drizzle-orm/sqlite-core';
7 | import { relations, sql } from 'drizzle-orm';
8 | import type { AdapterAccountType } from 'next-auth/adapters';
9 |
10 | export const users = sqliteTable('user', {
11 | id: text('id')
12 | .primaryKey()
13 | .$defaultFn(() => crypto.randomUUID()),
14 | name: text('name'),
15 | email: text('email').unique().notNull(),
16 | username: text('username'),
17 | password: text('password'),
18 | emailVerified: integer('emailVerified', { mode: 'timestamp_ms' }),
19 | role: text('role').default('USER'),
20 | image: text('image'),
21 | totpSecret: text('totpSecret'),
22 | isTotpEnabled: integer('isTotpEnabled', { mode: 'boolean' })
23 | .notNull()
24 | .default(false),
25 | createdAt: text('created_at')
26 | .default(sql`(CURRENT_TIMESTAMP)`)
27 | .notNull(),
28 | });
29 |
30 | export const accounts = sqliteTable(
31 | 'account',
32 | {
33 | userId: text('userId')
34 | .notNull()
35 | .references(() => users.id, { onDelete: 'cascade' }),
36 | type: text('type').$type().notNull(),
37 | provider: text('provider').notNull(),
38 | providerAccountId: text('providerAccountId').notNull(),
39 | refresh_token: text('refresh_token'),
40 | access_token: text('access_token'),
41 | expires_at: integer('expires_at'),
42 | token_type: text('token_type'),
43 | scope: text('scope'),
44 | id_token: text('id_token'),
45 | session_state: text('session_state'),
46 | },
47 | (account) => ({
48 | compoundKey: primaryKey({
49 | columns: [account.provider, account.providerAccountId],
50 | }),
51 | }),
52 | );
53 |
54 | export const accountsRelations = relations(accounts, ({ one }) => ({
55 | user: one(users, { fields: [accounts.userId], references: [users.id] }),
56 | }));
57 |
58 | export const usersRelations = relations(users, ({ many }) => ({
59 | accounts: many(accounts),
60 | }));
61 |
62 | export const sessions = sqliteTable('session', {
63 | sessionToken: text('sessionToken').primaryKey(),
64 | userId: text('userId')
65 | .notNull()
66 | .references(() => users.id, { onDelete: 'cascade' }),
67 | expires: integer('expires', { mode: 'timestamp_ms' }).notNull(),
68 | });
69 |
70 | export const verificationTokens = sqliteTable(
71 | 'verificationToken',
72 | {
73 | identifier: text('identifier').notNull(),
74 | token: text('token').notNull(),
75 | expires: integer('expires', { mode: 'timestamp_ms' }).notNull(),
76 | createdAt: text('created_at')
77 | .default(sql`(CURRENT_TIMESTAMP)`)
78 | .notNull(),
79 | },
80 | (vt) => ({
81 | compoundKey: primaryKey({ columns: [vt.identifier, vt.token] }),
82 | }),
83 | );
84 |
85 | export const authenticators = sqliteTable(
86 | 'authenticator',
87 | {
88 | credentialID: text('credentialID').notNull().unique(),
89 | userId: text('userId')
90 | .notNull()
91 | .references(() => users.id, { onDelete: 'cascade' }),
92 | providerAccountId: text('providerAccountId').notNull(),
93 | credentialPublicKey: text('credentialPublicKey').notNull(),
94 | counter: integer('counter').notNull(),
95 | credentialDeviceType: text('credentialDeviceType').notNull(),
96 | credentialBackedUp: integer('credentialBackedUp', {
97 | mode: 'boolean',
98 | }).notNull(),
99 | transports: text('transports'),
100 | },
101 | (authenticator) => ({
102 | compositePK: primaryKey({
103 | columns: [authenticator.userId, authenticator.credentialID],
104 | }),
105 | }),
106 | );
107 |
108 | export type InsertAccounts = typeof accounts.$inferInsert;
109 | export type SelectAccounts = typeof accounts.$inferSelect;
110 |
--------------------------------------------------------------------------------
/src/lib/Email.ts:
--------------------------------------------------------------------------------
1 | import { resend } from './resend';
2 | import VerificationEmail from '../../emails/verificationEmail';
3 | import ForgotPasswordEmail from '../../emails/forgotPasswordEmail';
4 | import AddPasswordEmail from '../../emails/addPasswordEmail';
5 | import InviteAdmin from '../../emails/InviteAdminEmail';
6 | import TwoFactorEmail from '../../emails/twoFactorVerificationEmail';
7 |
8 | export async function sendVerificationEmail(
9 | email: string,
10 | validationCode: string,
11 | time: number,
12 | ) {
13 | try {
14 | await resend.emails.send({
15 | from: 'no-reply@patelvivek.dev',
16 | to: email,
17 | subject: 'Verification Code for creating a new Account',
18 | react: VerificationEmail({ email, validationCode: validationCode, time }),
19 | });
20 | return { success: true, message: 'Verification email sent successfully.' };
21 | } catch (emailError) {
22 | console.error('Error sending verification email:', emailError);
23 | return { success: false, message: 'Failed to send verification email.' };
24 | }
25 | }
26 |
27 | export async function sendForgotPasswordEmail(
28 | email: string,
29 | userFirstName: string,
30 | resetPasswordLink: string,
31 | time: number,
32 | ) {
33 | try {
34 | await resend.emails.send({
35 | from: 'no-reply@patelvivek.dev',
36 | to: email,
37 | subject: 'Password Reset Request',
38 | react: ForgotPasswordEmail({
39 | userFirstName,
40 | resetPasswordLink,
41 | time,
42 | }),
43 | });
44 | return {
45 | success: true,
46 | message: 'Password reset email sent successfully.',
47 | };
48 | } catch (emailError) {
49 | console.error('Error sending verification email:', emailError);
50 | return { success: false, message: 'Failed to send password reset email.' };
51 | }
52 | }
53 |
54 | export async function sendAddPasswordEmail(
55 | email: string,
56 | userFirstName: string,
57 | addPasswordLink: string,
58 | time: number,
59 | ) {
60 | try {
61 | await resend.emails.send({
62 | from: 'no-reply@patelvivek.dev',
63 | to: email,
64 | subject: 'Request for adding password',
65 | react: AddPasswordEmail({
66 | userFirstName,
67 | addPasswordLink,
68 | time,
69 | }),
70 | });
71 | return {
72 | success: true,
73 | message: 'Add password email sent successfully.',
74 | };
75 | } catch (emailError) {
76 | console.error('Error sending add password email:', emailError);
77 | return {
78 | success: false,
79 | message: 'Failed to send password add password email.',
80 | };
81 | }
82 | }
83 |
84 | export async function sendAdminInviteEmail(
85 | adminName: string,
86 | invitedUserEmail: string,
87 | inviteLink: string,
88 | time: number,
89 | ) {
90 | try {
91 | await resend.emails.send({
92 | from: 'no-reply@patelvivek.dev',
93 | to: invitedUserEmail,
94 | subject: 'Invite Link for admin',
95 | react: InviteAdmin({
96 | adminName,
97 | inviteLink,
98 | time,
99 | }),
100 | });
101 | return {
102 | success: true,
103 | message: 'Invite link sent successfully.',
104 | };
105 | } catch (emailError) {
106 | console.error('Error sending Invite Link:', emailError);
107 | return {
108 | success: false,
109 | message: 'Failed to send invite link.',
110 | };
111 | }
112 | }
113 |
114 | export async function sendTwoFactorVerificationEmail(
115 | email: string,
116 | userFirstName: string,
117 | OTP: string,
118 | time: number,
119 | ) {
120 | try {
121 | await resend.emails.send({
122 | from: 'no-reply@patelvivek.dev',
123 | to: email,
124 | subject: '2FA OTP',
125 | react: TwoFactorEmail({
126 | userFirstName,
127 | OTP,
128 | time,
129 | }),
130 | });
131 | return {
132 | success: true,
133 | message: 'Email sent successfully.',
134 | };
135 | } catch (emailError) {
136 | console.error('Error sending two factor verification email:', emailError);
137 | return { success: false, message: 'Failed to send email.' };
138 | }
139 | }
140 |
--------------------------------------------------------------------------------
/src/lib/resend.ts:
--------------------------------------------------------------------------------
1 | import { Resend } from 'resend';
2 |
3 | export const resend = new Resend(process.env.RESEND_API_KEY);
4 |
--------------------------------------------------------------------------------
/src/lib/utils.ts:
--------------------------------------------------------------------------------
1 | import clsx, { ClassValue } from 'clsx';
2 | import { twMerge } from 'tailwind-merge';
3 |
4 | export function cn(...inputs: ClassValue[]) {
5 | return twMerge(clsx(inputs));
6 | }
7 |
--------------------------------------------------------------------------------
/src/middleware.ts:
--------------------------------------------------------------------------------
1 | export { auth as middleware } from '@/auth';
2 |
--------------------------------------------------------------------------------
/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 | keyframes: {
57 | 'caret-blink': {
58 | '0%,70%,100%': { opacity: '1' },
59 | '20%,50%': { opacity: '0' },
60 | },
61 | orbit: {
62 | '0%': {
63 | transform:
64 | 'rotate(0deg) translateY(calc(var(--radius) * 1px)) rotate(0deg)',
65 | },
66 | '100%': {
67 | transform:
68 | 'rotate(360deg) translateY(calc(var(--radius) * 1px)) rotate(-360deg)',
69 | },
70 | },
71 | 'border-beam': {
72 | '100%': {
73 | 'offset-distance': '100%',
74 | },
75 | },
76 | },
77 | animation: {
78 | 'caret-blink': 'caret-blink 1.25s ease-out infinite',
79 | orbit: 'orbit calc(var(--duration)*1s) linear infinite',
80 | 'border-beam': 'border-beam calc(var(--duration)*1s) infinite linear',
81 | },
82 | },
83 | },
84 | plugins: [require('tailwindcss-animate')],
85 | } satisfies Config;
86 |
87 | export default config;
88 |
--------------------------------------------------------------------------------
/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 | "target": "ES2017"
24 | },
25 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
26 | "exclude": ["node_modules"]
27 | }
28 |
--------------------------------------------------------------------------------