├── .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 | Ask DeepWiki 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 | ![Image](./public/authjs-template.png) 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 | Open in IDX 97 | 98 | 99 |
100 |
101 | Stars 102 | Forks 103 | Issues 104 |
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 | 37 | You're invited to join as Admin! 38 | 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 | 35 | Add password to your account 36 | 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 | 35 | Reset your password 36 | 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 | 34 | Verify 2FA 35 | 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 | 34 | Your login code for AuthJs Template 35 | 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 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /public/placeholder-user.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/patelvivekdev/AuthJs-Template/3899f6c476dbd6616bc0757d4e8ee1fd0db3217e/public/placeholder-user.jpg -------------------------------------------------------------------------------- /public/react_dark.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | React-Logo-Filled (1) 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /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 |
22 | {state.errors && ( 23 |
24 |

{state.message}

25 |
26 | )} 27 | {state.type === 'success' && ( 28 |
29 |

{state.message}

30 |
31 | )} 32 |
33 | 34 | 41 |
42 | {state.errors?.email && ( 43 |

{state.errors.email}

44 | )} 45 | 46 | 49 |
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 |
42 | {state.errors && ( 43 |
44 |

{state.message}

45 |
46 | )} 47 |
48 | 49 |
50 | 59 |
setShowPassword(!showPassword)} 62 | > 63 | {showPassword ? ( 64 | 65 | ) : ( 66 | 67 | )} 68 |
69 |
70 | {state.errors?.password && ( 71 |

{state.errors.password}

72 | )} 73 |
74 |
75 | 76 |
77 | 86 |
setShowConfirmPassword(!showConfirmPassword)} 89 | > 90 | {showConfirmPassword ? ( 91 | 92 | ) : ( 93 | 94 | )} 95 |
96 |
97 | {state.errors?.password2 && ( 98 |

{state.errors.password2}

99 | )} 100 |
101 | 104 |
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 |
46 |
47 |
48 | 49 | 56 | {state.errors?.email && ( 57 |

{state.errors.email}

58 | )} 59 |
60 | 63 |
64 |
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 |
49 | {state.errors && ( 50 |
51 |

{state.message}

52 |
53 | )} 54 |
55 | 56 | 63 | {state.errors?.name && ( 64 |

{state.errors.name}

65 | )} 66 |
67 |
68 | 69 | 76 | {state.errors?.username && ( 77 |

{state.errors.username}

78 | )} 79 |
80 |
81 | 82 | 90 |
91 | {state.errors?.email && ( 92 |

{state.errors.email}

93 | )} 94 |
95 | 96 |
97 | 105 |
setShowPassword(!showPassword)} 108 | > 109 | {showPassword ? ( 110 | 111 | ) : ( 112 | 113 | )} 114 |
115 |
116 | {state.errors?.password && ( 117 |

{state.errors.password}

118 | )} 119 |
120 |
121 | 122 |
123 | 131 |
setShowConfirmPassword(!showConfirmPassword)} 134 | > 135 | {showConfirmPassword ? ( 136 | 137 | ) : ( 138 | 139 | )} 140 |
141 |
142 | {state.errors?.password2 && ( 143 |

{state.errors.password2}

144 | )} 145 |
146 | 149 |
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 |
40 | {state.errors && ( 41 |
42 |

{state.message}

43 |
44 | )} 45 |
46 | 47 |
48 | 56 |
setShowPassword(!showPassword)} 59 | > 60 | {showPassword ? ( 61 | 62 | ) : ( 63 | 64 | )} 65 |
66 |
67 | {state.errors?.password && ( 68 |

{state.errors.password}

69 | )} 70 |
71 |
72 | 73 |
74 | 82 |
setShowConfirmPassword(!showConfirmPassword)} 85 | > 86 | {showConfirmPassword ? ( 87 | 88 | ) : ( 89 | 90 | )} 91 |
92 |
93 | {state.errors?.password2 && ( 94 |

{state.errors.password2}

95 | )} 96 |
97 | 100 |
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 |
30 | {state.type === 'error' && ( 31 |
32 |

{state.message}

33 |
34 | )} 35 |
36 | 37 | 45 | {state.errors?.username && ( 46 |

{state.errors.username}

47 | )} 48 |
49 |
50 |
51 | 52 | 56 | Forgot your password? 57 | 58 |
59 |
60 | 69 |
setShowPassword(!showPassword)} 72 | > 73 | {showPassword ? ( 74 | 75 | ) : ( 76 | 77 | )} 78 |
79 |
80 | {state.errors?.password && ( 81 |

{state.errors.password}

82 | )} 83 |
84 | 87 |
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 |
42 |
43 | {state.type === 'error' && ( 44 |
45 |

{state.message}

46 |
47 | )} 48 | 49 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | {state.errors?.otp && ( 65 |

{state.errors.otp}

66 | )} 67 |
68 | 71 |
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 |
59 |
60 | {state.type === 'error' && ( 61 |
62 |

{state.message}

63 |
64 | )} 65 | 66 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | {state.errors?.otp && ( 82 |

{state.errors.otp}

83 | )} 84 |
85 | 88 |
89 |
90 | 91 |
92 |
93 | 96 |
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 |
20 | {state.errors && ( 21 |
22 |

{state.message}

23 |
24 | )} 25 | {state.type === 'success' && ( 26 |
27 |

{state.message}

28 |
29 | )} 30 |
31 | 32 | 39 |
40 | {state.errors?.email && ( 41 |

{state.errors.email}

42 | )} 43 | 46 |
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 |
27 | 35 |
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 | 20 | 21 | 24 | 25 | 26 | 27 | Account Delete 28 | 29 | Are you sure you want to delete your account? 30 | 31 | 32 |
33 |

34 | Once you delete your account, there is no going back. Please be 35 | certain. Deleting account will also delete Oauth accounts if you 36 | have it. 37 |

38 |
39 | 40 | 41 | 44 | 45 |
46 | 47 | Delete Account 48 | 49 |
50 |
51 |
52 |
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 |
8 | 9 | Disable Two Factor 10 | 11 |
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 |
9 |
10 | 11 | 17 |
18 |
19 | 20 | 26 |
27 |
28 | 29 | 35 |
36 |
37 | 40 |
41 |
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 |
9 | 10 | Connect 11 | 12 |
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 |
15 | 20 | Disconnect 21 | 22 |
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 |
46 | {state.errors && ( 47 |
48 |

{state.message}

49 |
50 | )} 51 |
52 | 53 |
54 | 63 |
setShowCurrentPassword(!showCurrentPassword)} 66 | > 67 | {showCurrentPassword ? ( 68 | 69 | ) : ( 70 | 71 | )} 72 |
73 |
74 | {state.errors?.oldPassword && ( 75 |

{state.errors.oldPassword}

76 | )} 77 |
78 |
79 | 80 |
81 | 90 |
setShowPassword(!showPassword)} 93 | > 94 | {showPassword ? ( 95 | 96 | ) : ( 97 | 98 | )} 99 |
100 |
101 | {state.errors?.newPassword && ( 102 |

{state.errors.newPassword}

103 | )} 104 |
105 |
106 | 107 |
108 | 117 |
setShowConfirmPassword(!showConfirmPassword)} 120 | > 121 | {showConfirmPassword ? ( 122 | 123 | ) : ( 124 | 125 | )} 126 |
127 |
128 | {state.errors?.password2 && ( 129 |

{state.errors.password2}

130 | )} 131 |
132 | 135 |
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 |
48 |
49 | 50 | 51 | {state.errors?.otp && ( 52 |

{state.errors.otp}

53 | )} 54 |
55 | 58 |
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 | 2FA QR Code 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 |
{ 10 | 'use server'; 11 | await signIn(provider || 'google', { 12 | callbackUrl: '/', 13 | }); 14 | }} 15 | > 16 | {/* Create button with tailwind css */} 17 | 21 |
22 | ); 23 | } 24 | 25 | export function GithubSignIn() { 26 | return ( 27 |
{ 30 | 'use server'; 31 | await signIn('github', { 32 | redirectTo: '/profile', 33 | redirect: true, 34 | callbackUrl: '/', 35 | }); 36 | }} 37 | > 38 | 42 |
43 | ); 44 | } 45 | 46 | export function GoogleSignIn() { 47 | return ( 48 |
{ 51 | 'use server'; 52 | await signIn('google', { 53 | redirectTo: '/profile', 54 | redirect: true, 55 | callbackUrl: '/', 56 | }); 57 | }} 58 | > 59 | 63 |
64 | ); 65 | } 66 | 67 | export function WebAuthIn() { 68 | return ( 69 |
{ 72 | 'use server'; 73 | await signIn('passkey', { action: 'register' }); 74 | }} 75 | > 76 | 79 |
80 | ); 81 | } 82 | 83 | export function SignOut() { 84 | return ( 85 |
{ 87 | 'use server'; 88 | await signOut({ 89 | redirectTo: '/', 90 | redirect: true, 91 | }); 92 | }} 93 | className='w-full' 94 | > 95 | 98 |
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 | Next Js 32 | 33 | 39 | Drizzle ORM 40 | 41 | 47 | Resend 48 | 49 | 50 | 56 | Auth Js 57 | 58 | 64 | Turso 65 | 66 | 72 | Vercel 73 | 74 | 80 | React 81 | 82 | 88 | Magic UI 89 | 90 | 91 | {/* 100 Circles */} 92 | 98 | Next Js 99 | 100 | 106 | Drizzle ORM 107 | 108 | 114 | Resend 115 | 116 | 117 | 123 | Auth Js 124 | 125 | 131 | Turso 132 | 133 | 139 | Vercel 140 | 141 | 147 | React 148 | 149 | 155 | Magic UI 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 |
14 |
15 | 16 | AuthJs Template 17 | 18 | {session ? ( 19 |
20 | 24 | Dashboard 25 | 26 | 30 | 31 | 35 | 36 | {user?.name?.charAt(0)} 37 | 38 | 39 | 40 | 41 |
42 | ) : ( 43 |
44 | 45 | 46 | 47 | 48 | 49 | 50 |
51 | )} 52 |
53 |
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 |
54 |
55 |
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 | 30 | 38 | 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 |
10 | 15 | 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 | --------------------------------------------------------------------------------