├── README.md
├── src
├── app
│ ├── api
│ │ ├── auth
│ │ │ └── [...nextauth]
│ │ │ │ └── route.ts
│ │ └── admin
│ │ │ └── route.ts
│ ├── favicon.ico
│ ├── fonts
│ │ ├── GeistVF.woff
│ │ └── GeistMonoVF.woff
│ ├── auth
│ │ ├── error
│ │ │ └── page.tsx
│ │ ├── login
│ │ │ └── page.tsx
│ │ ├── reset
│ │ │ └── page.tsx
│ │ ├── register
│ │ │ └── page.tsx
│ │ ├── new-password
│ │ │ └── page.tsx
│ │ ├── new-verification
│ │ │ └── page.tsx
│ │ └── layout.tsx
│ ├── (protected)
│ │ ├── server
│ │ │ └── page.tsx
│ │ ├── client
│ │ │ └── page.tsx
│ │ ├── layout.tsx
│ │ ├── _components
│ │ │ └── navbar.tsx
│ │ ├── admin
│ │ │ └── page.tsx
│ │ └── settings
│ │ │ └── page.tsx
│ ├── page.tsx
│ ├── layout.tsx
│ └── globals.css
├── lib
│ ├── utils.ts
│ ├── db.ts
│ ├── auth.ts
│ ├── mail.ts
│ └── tokens.ts
├── components
│ ├── auth
│ │ ├── logout-button.tsx
│ │ ├── error-card.tsx
│ │ ├── back-button.tsx
│ │ ├── header.tsx
│ │ ├── role-gate.tsx
│ │ ├── social.tsx
│ │ ├── login-button.tsx
│ │ ├── card-wrapper.tsx
│ │ ├── user-button.tsx
│ │ ├── new-verification-form.tsx
│ │ ├── reset-form.tsx
│ │ ├── new-password-form.tsx
│ │ ├── register-form.tsx
│ │ └── login-form.tsx
│ ├── form-success.tsx
│ ├── form-error.tsx
│ ├── ui
│ │ ├── label.tsx
│ │ ├── input.tsx
│ │ ├── sonner.tsx
│ │ ├── switch.tsx
│ │ ├── badge.tsx
│ │ ├── avatar.tsx
│ │ ├── button.tsx
│ │ ├── card.tsx
│ │ ├── dialog.tsx
│ │ ├── form.tsx
│ │ ├── select.tsx
│ │ └── dropdown-menu.tsx
│ └── user-info.tsx
└── middleware.ts
├── next.config.mjs
├── actions
├── logout.ts
├── admin.ts
├── reset.ts
├── new-verification.ts
├── register.ts
├── new-password.ts
├── setting.ts
└── login.ts
├── postcss.config.mjs
├── hooks
├── use-current-role.ts
└── use-current-user.ts
├── .eslintrc.json
├── data
├── account.ts
├── two-factor-confirmation.ts
├── user.ts
├── two-fator-token.ts
├── password-reset-token.ts
└── verficiation-token.ts
├── next-auth.d.ts
├── components.json
├── .gitignore
├── tsconfig.json
├── routes.ts
├── auth.config.ts
├── package.json
├── tailwind.config.ts
├── schemas
└── index.ts
├── prisma
└── schema.prisma
└── auth.ts
/README.md:
--------------------------------------------------------------------------------
1 | # NEXT_AUTH
--------------------------------------------------------------------------------
/src/app/api/auth/[...nextauth]/route.ts:
--------------------------------------------------------------------------------
1 | export { GET, POST } from '../../../../../auth'
--------------------------------------------------------------------------------
/src/app/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/code-with-zain-hunzai/auth-2Fa/HEAD/src/app/favicon.ico
--------------------------------------------------------------------------------
/src/app/fonts/GeistVF.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/code-with-zain-hunzai/auth-2Fa/HEAD/src/app/fonts/GeistVF.woff
--------------------------------------------------------------------------------
/next.config.mjs:
--------------------------------------------------------------------------------
1 | /** @type {import('next').NextConfig} */
2 | const nextConfig = {};
3 |
4 | export default nextConfig;
5 |
--------------------------------------------------------------------------------
/src/app/fonts/GeistMonoVF.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/code-with-zain-hunzai/auth-2Fa/HEAD/src/app/fonts/GeistMonoVF.woff
--------------------------------------------------------------------------------
/actions/logout.ts:
--------------------------------------------------------------------------------
1 | "use server"
2 |
3 | import { signOut } from "../auth"
4 |
5 | export const logout = async () => {
6 | //some server stuff
7 | await signOut();
8 | }
--------------------------------------------------------------------------------
/postcss.config.mjs:
--------------------------------------------------------------------------------
1 | /** @type {import('postcss-load-config').Config} */
2 | const config = {
3 | plugins: {
4 | tailwindcss: {},
5 | },
6 | };
7 |
8 | export default config;
9 |
--------------------------------------------------------------------------------
/src/app/auth/error/page.tsx:
--------------------------------------------------------------------------------
1 | import { ErrorCard } from "@/components/auth/error-card"
2 |
3 | const AuthErrorPage = () => {
4 | return (
5 |
6 | )
7 | }
8 | export default AuthErrorPage
--------------------------------------------------------------------------------
/src/lib/utils.ts:
--------------------------------------------------------------------------------
1 | import { clsx, type ClassValue } from "clsx"
2 | import { twMerge } from "tailwind-merge"
3 |
4 | export function cn(...inputs: ClassValue[]) {
5 | return twMerge(clsx(inputs))
6 | }
7 |
--------------------------------------------------------------------------------
/hooks/use-current-role.ts:
--------------------------------------------------------------------------------
1 | import { useSession } from "next-auth/react";
2 |
3 | export const useCurrentRole = () => {
4 |
5 | const session = useSession();
6 |
7 | return session.data?.user.role
8 | }
--------------------------------------------------------------------------------
/hooks/use-current-user.ts:
--------------------------------------------------------------------------------
1 |
2 | import { useSession } from "next-auth/react";
3 |
4 | export const useCurrentUser = () => {
5 | const session = useSession();
6 |
7 | return session.data?.user
8 | }
9 |
--------------------------------------------------------------------------------
/src/app/auth/login/page.tsx:
--------------------------------------------------------------------------------
1 | import LoginForm from "@/components/auth/login-form"
2 |
3 | const LoginPage = () => {
4 | return (
5 |
6 |
7 |
8 | )
9 | }
10 | export default LoginPage
--------------------------------------------------------------------------------
/src/app/auth/reset/page.tsx:
--------------------------------------------------------------------------------
1 | import ResetForm from "@/components/auth/reset-form"
2 |
3 | const ResetPage = () => {
4 | return (
5 |
6 |
7 |
8 | )
9 | }
10 |
11 | export default ResetPage
12 |
--------------------------------------------------------------------------------
/src/app/auth/register/page.tsx:
--------------------------------------------------------------------------------
1 | import RegisterForm from "@/components/auth/register-form"
2 |
3 | const RegisterPage = () => {
4 | return (
5 |
6 |
7 |
8 | )
9 | }
10 | export default RegisterPage
--------------------------------------------------------------------------------
/src/app/auth/new-password/page.tsx:
--------------------------------------------------------------------------------
1 | import ForgetForm from "@/components/auth/new-password-form"
2 |
3 |
4 | const NewPasswordPage = () => {
5 | return (
6 |
7 |
8 |
9 | )
10 | }
11 |
12 | export default NewPasswordPage
13 |
--------------------------------------------------------------------------------
/src/lib/db.ts:
--------------------------------------------------------------------------------
1 | import { PrismaClient } from "@prisma/client";
2 |
3 | declare global {
4 | var prisma: PrismaClient | undefined
5 | }
6 |
7 | export const db = globalThis.prisma || new PrismaClient();
8 |
9 | if (process.env.NODE_ENV !== "production") globalThis.prisma = db
--------------------------------------------------------------------------------
/src/app/auth/new-verification/page.tsx:
--------------------------------------------------------------------------------
1 | import NewVerificationForm from "@/components/auth/new-verification-form";
2 |
3 | const NewVerificationPage = () => {
4 | return (
5 |
6 |
7 |
8 | )
9 | }
10 |
11 | export default NewVerificationPage;
--------------------------------------------------------------------------------
/src/lib/auth.ts:
--------------------------------------------------------------------------------
1 | import { auth } from "../../auth";
2 |
3 | export const currentUser = async () => {
4 | const session = await auth()
5 |
6 | return session?.user
7 | }
8 |
9 | export const currentRole = async () => {
10 | const session = await auth()
11 |
12 | return session?.user?.role;
13 | }
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "next/core-web-vitals",
3 | "rules": {
4 | "react/no-unescaped-entities": "off",
5 | "@typescript-eslint/quotes": "off",
6 | "quotes": [
7 | 0
8 | ],
9 | "avoidEscape": 0,
10 | "allowTemplateLiterals": 0,
11 | "no-useless-escape": 0
12 | }
13 | }
--------------------------------------------------------------------------------
/data/account.ts:
--------------------------------------------------------------------------------
1 | import { db } from "@/lib/db";
2 |
3 | export const getAccountByUserId = async (userId: string) => {
4 | try {
5 | const account = await db.account.findFirst({
6 | where: { userId }
7 | });
8 | return account
9 | } catch {
10 | return null
11 | }
12 | }
--------------------------------------------------------------------------------
/actions/admin.ts:
--------------------------------------------------------------------------------
1 | "use server"
2 |
3 | import { currentRole } from "@/lib/auth"
4 | import { UserRole } from "@prisma/client";
5 |
6 | export const admin = async () => {
7 | const role = await currentRole();
8 |
9 | if (role === UserRole.ADMIN) {
10 | return { success: "Allowed Server Action" }
11 | }
12 | return { error: "Forbidden Server Action" }
13 | }
--------------------------------------------------------------------------------
/data/two-factor-confirmation.ts:
--------------------------------------------------------------------------------
1 | import { db } from "@/lib/db";
2 |
3 | export const getTwoFactorConfirmationByUserId = async (userId: string) => {
4 | try {
5 | const twoFactorConfirmation = await db.twoFactorConfirmation.findUnique({
6 | where: { userId }
7 | });
8 | return twoFactorConfirmation;
9 | } catch {
10 | return null;
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/src/app/api/admin/route.ts:
--------------------------------------------------------------------------------
1 | import { currentRole } from "@/lib/auth";
2 | import { UserRole } from "@prisma/client";
3 | import { NextResponse } from "next/server";
4 |
5 | export async function GET() {
6 | const role = await currentRole();
7 |
8 | if (role === UserRole.ADMIN) {
9 | return new NextResponse(null, { status: 200 });
10 | }
11 |
12 | return new NextResponse(null, { status: 403 })
13 | }
--------------------------------------------------------------------------------
/src/app/auth/layout.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | const AuthLayout = (
4 | { children }: { children: React.ReactNode }
5 | ) => {
6 | return (
7 |
8 | {children}
9 |
10 | );
11 | }
12 |
13 | export default AuthLayout;
14 |
--------------------------------------------------------------------------------
/src/app/(protected)/server/page.tsx:
--------------------------------------------------------------------------------
1 | import { UserInfo } from "@/components/user-info";
2 | import { currentUser } from "@/lib/auth";
3 |
4 |
5 | const ServerPage = async () => {
6 | const user = await currentUser();
7 | return (
8 |
9 |
12 |
13 | );
14 | }
15 |
16 | export default ServerPage
17 |
--------------------------------------------------------------------------------
/next-auth.d.ts:
--------------------------------------------------------------------------------
1 | import NextAuth, { type DefaultSession } from "next-auth"
2 |
3 | export type ExtendedUser = DefaultSession["user"] & {
4 | role: "ADMIN" | "USER"
5 | isTwoFactorEnabled: boolean;
6 | isOAuth: boolean;
7 | };
8 |
9 | declare module "next-auth" {
10 | interface Session {
11 | user: ExtendedUser;
12 | }
13 | }
14 |
15 |
16 | import { JWT } from "next-auth/jwt"
17 |
18 | declare module "next-auth/jwt" {
19 | interface JWT {
20 | role?: "ADMIN" | "USER"
21 | }
22 | }
--------------------------------------------------------------------------------
/src/app/(protected)/client/page.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import { UserInfo } from "@/components/user-info";
4 | import { useCurrentUser } from "../../../../hooks/use-current-user";
5 |
6 | const ClientPage = () => {
7 | const user = useCurrentUser();
8 |
9 | return (
10 |
11 |
15 |
16 | );
17 | }
18 |
19 | export default ClientPage;
20 |
--------------------------------------------------------------------------------
/src/components/auth/logout-button.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { logout } from "../../../actions/logout";
3 |
4 | interface LogoutButtonProps {
5 | children?: React.ReactNode;
6 | }
7 |
8 | export const LogoutButton = ({ children }:
9 | LogoutButtonProps) => {
10 | const onClick = () => {
11 | logout();
12 | };
13 |
14 | return (
15 |
16 | {children}
17 |
18 | );
19 | };
20 |
--------------------------------------------------------------------------------
/components.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://ui.shadcn.com/schema.json",
3 | "style": "new-york",
4 | "rsc": true,
5 | "tsx": true,
6 | "tailwind": {
7 | "config": "tailwind.config.ts",
8 | "css": "src/app/globals.css",
9 | "baseColor": "zinc",
10 | "cssVariables": true,
11 | "prefix": ""
12 | },
13 | "aliases": {
14 | "components": "@/components",
15 | "utils": "@/lib/utils",
16 | "ui": "@/components/ui",
17 | "lib": "@/lib",
18 | "hooks": "@/hooks"
19 | }
20 | }
--------------------------------------------------------------------------------
/data/user.ts:
--------------------------------------------------------------------------------
1 | import { db } from "@/lib/db";
2 |
3 | export const getUserByEmail = async (email: string) => {
4 | try {
5 | const user = await db.user.findUnique({ where: { email } })
6 |
7 | return user
8 | } catch {
9 | return null
10 | }
11 | };
12 |
13 | export const getUserById = async (id: string) => {
14 | try {
15 | const user = await db.user.findUnique({ where: { id } })
16 |
17 | return user
18 | } catch {
19 | return null
20 | }
21 | };
--------------------------------------------------------------------------------
/src/components/form-success.tsx:
--------------------------------------------------------------------------------
1 | import { CheckCircledIcon } from "@radix-ui/react-icons";
2 |
3 | interface FormSuccessProps {
4 | message?: string
5 | };
6 |
7 | export const FormSuccess = ({
8 | message,
9 | }: FormSuccessProps) => {
10 | if (!message) return null
11 |
12 | return (
13 |
17 | )
18 | }
--------------------------------------------------------------------------------
/src/components/form-error.tsx:
--------------------------------------------------------------------------------
1 | import { ExclamationTriangleIcon } from "@radix-ui/react-icons";
2 |
3 | interface FormErrorProps {
4 | message?: string
5 | };
6 |
7 | export const FormError = ({
8 | message,
9 | }: FormErrorProps) => {
10 | if (!message) return null
11 |
12 | return (
13 |
17 | )
18 | }
--------------------------------------------------------------------------------
/src/app/(protected)/layout.tsx:
--------------------------------------------------------------------------------
1 | import { Navbar } from "./_components/navbar"
2 |
3 | interface ProtectedLayoutProps{
4 | children: React.ReactNode
5 | }
6 |
7 | const ProtectedLayout=({children}:ProtectedLayoutProps) => {
8 | return(
9 |
10 |
11 | {children}
12 |
13 | )
14 | }
15 |
16 | export default ProtectedLayout
--------------------------------------------------------------------------------
/.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 | .env
31 |
32 | # vercel
33 | .vercel
34 |
35 | # typescript
36 | *.tsbuildinfo
37 | next-env.d.ts
38 |
--------------------------------------------------------------------------------
/src/components/auth/error-card.tsx:
--------------------------------------------------------------------------------
1 | import { CardWrapper } from "@/components/auth/card-wrapper";
2 | import { ExclamationTriangleIcon } from "@radix-ui/react-icons";
3 |
4 | export const ErrorCard = () => {
5 | return (
6 |
11 |
12 |
13 |
14 |
15 | );
16 | };
--------------------------------------------------------------------------------
/src/components/auth/back-button.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import { Button } from "@/components/ui/button"
4 | import Link from "next/link";
5 |
6 | interface BackButtonProps {
7 | href: string;
8 | label: string;
9 | }
10 | export const BackButton = ({
11 | href,
12 | label,
13 | }: BackButtonProps) => {
14 | return (
15 |
24 | )
25 | }
--------------------------------------------------------------------------------
/data/two-fator-token.ts:
--------------------------------------------------------------------------------
1 | import { db } from "@/lib/db";
2 |
3 | export const getTwoFactorTokenByToken = async (token: string) => {
4 | try {
5 | const twoFactorToken = await db.twoFactorToken.findUnique({
6 | where: { token }
7 | });
8 | return twoFactorToken
9 | } catch {
10 | return null
11 | }
12 | }
13 |
14 |
15 | export const getTwoFactorTokenByEmail = async (email: string) => {
16 | try {
17 | const twoFactorToken = await db.twoFactorToken.findFirst({
18 | where: { email }
19 | });
20 | return twoFactorToken
21 | } catch {
22 | return null
23 | }
24 | }
--------------------------------------------------------------------------------
/src/components/auth/header.tsx:
--------------------------------------------------------------------------------
1 | import { Poppins } from "next/font/google"
2 |
3 | import { cn } from "@/lib/utils"
4 |
5 | const font = Poppins({
6 | subsets: ["latin"],
7 | weight: ["600"]
8 | });
9 |
10 | interface HeaderProps {
11 | label: string
12 | };
13 |
14 | export const Header = ({ label, }: HeaderProps) => {
15 | return (
16 |
17 |
🔐 Auth
19 |
20 | {label}
21 |
22 |
23 | )
24 | }
--------------------------------------------------------------------------------
/data/password-reset-token.ts:
--------------------------------------------------------------------------------
1 | import { db } from "@/lib/db"
2 |
3 | export const getPasswordResetTokenByToken = async (token: string) => {
4 |
5 | try {
6 | const passwordResetToken = await db.passwordResetToken.findUnique({
7 | where: { token }
8 | });
9 | return passwordResetToken;
10 |
11 | } catch {
12 | return null;
13 | }
14 | };
15 |
16 | export const getPasswordResetTokenByEmail = async (email: string) => {
17 |
18 | try {
19 | const passwordResetToken = await db.passwordResetToken.findFirst({
20 | where: { email }
21 | });
22 | return passwordResetToken;
23 |
24 | } catch {
25 | return null;
26 | }
27 | };
--------------------------------------------------------------------------------
/data/verficiation-token.ts:
--------------------------------------------------------------------------------
1 | import { db } from "@/lib/db";
2 |
3 | export const getVerificationTokenByToken = async (
4 | token: string
5 | ) => {
6 | try {
7 | const verificationToken = await db.verificationToken.findFirst({
8 | where: { token }
9 | });
10 | return verificationToken;
11 | } catch {
12 | return null
13 | }
14 | }
15 |
16 |
17 | export const getVerificationTokenByEmail = async (
18 | email: string
19 | ) => {
20 | try {
21 | const verificationToken = await db.verificationToken.findFirst({
22 | where: { email }
23 | });
24 | return verificationToken;
25 | } catch {
26 | return null
27 | }
28 | }
--------------------------------------------------------------------------------
/src/components/auth/role-gate.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 | import { UserRole } from "@prisma/client";
3 | import { useCurrentRole } from "../../../hooks/use-current-role";
4 | import { FormError } from "../form-error";
5 |
6 | interface RoleGateProps {
7 | children: React.ReactNode;
8 | allowedRole: UserRole;
9 | };
10 |
11 | export const RoleGate = ({
12 | children,
13 | allowedRole
14 | }: RoleGateProps) => {
15 | const role = useCurrentRole()
16 |
17 | if (role !== allowedRole) {
18 | return (
19 |
20 | )
21 | }
22 |
23 | return (
24 | <>
25 | {children}
26 | >
27 | );
28 | };
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "lib": [
4 | "dom",
5 | "dom.iterable",
6 | "esnext"
7 | ],
8 | "allowJs": true,
9 | "skipLibCheck": true,
10 | "strict": true,
11 | "noEmit": true,
12 | "esModuleInterop": true,
13 | "module": "esnext",
14 | "moduleResolution": "bundler",
15 | "resolveJsonModule": true,
16 | "isolatedModules": true,
17 | "jsx": "preserve",
18 | "incremental": true,
19 | "plugins": [
20 | {
21 | "name": "next"
22 | }
23 | ],
24 | "paths": {
25 | "@/*": [
26 | "./src/*"
27 | ]
28 | },
29 | "target": "ES2017"
30 | },
31 | "include": [
32 | "next-env.d.ts",
33 | "**/*.ts",
34 | "**/*.tsx",
35 | ".next/types/**/*.ts"
36 | ],
37 | "exclude": [
38 | "node_modules"
39 | ]
40 | }
41 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/routes.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * An array of routes that are accessible to the public
3 | * These routes do not require authentication
4 | * @type {string[]}
5 | */
6 | export const publicRoutes = [
7 | '/',
8 | '/auth/new-verification'
9 | ];
10 |
11 | /**
12 | * An array of routes that are used for authentication
13 | * These routes will redirect logged-in users to /settings
14 | * @type {string[]}
15 | */
16 | export const authRoutes = [
17 | "/auth/login",
18 | "/auth/register",
19 | "/auth/error",
20 | "/auth/reset",
21 | "/auth/new-password"
22 | ];
23 |
24 | /**
25 | * The prefix for API authentication routes
26 | * Routes that start with this prefix are used for API authentication purposes
27 | * @type {string}
28 | */
29 | export const apiAuthPrefix = "/api/auth";
30 |
31 | /**
32 | * The default redirect path after logging in
33 | * @type {string}
34 | */
35 | export const DEFAULT_LOGIN_REDIRECT = '/settings';
36 |
--------------------------------------------------------------------------------
/actions/reset.ts:
--------------------------------------------------------------------------------
1 | "use server"
2 |
3 | import * as z from "zod"
4 |
5 | import { ResetSchema } from "../schemas";
6 | import { getUserByEmail } from "../data/user";
7 | import { sendPasswordResetEmail } from "@/lib/mail";
8 | import { generatePasswordResetToken } from "@/lib/tokens";
9 |
10 | export const reset = async (values: z.infer) => {
11 | const validatedFields = ResetSchema.safeParse(values);
12 |
13 | if (!validatedFields.success) {
14 | return { error: "Invalid email!" };
15 | }
16 |
17 | const { email } = validatedFields.data;
18 |
19 | const existingUser = await getUserByEmail(email);
20 |
21 | if (!existingUser) {
22 | return { error: "Email not found!" }
23 | }
24 |
25 | const passwordResetToken = await generatePasswordResetToken(email);
26 | await sendPasswordResetEmail(
27 | passwordResetToken.email,
28 | passwordResetToken.token
29 | )
30 |
31 | return { success: "Reset email sent!" };
32 | }
33 |
--------------------------------------------------------------------------------
/src/components/ui/sonner.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import { useTheme } from "next-themes"
4 | import { Toaster as Sonner } from "sonner"
5 |
6 | type ToasterProps = React.ComponentProps
7 |
8 | const Toaster = ({ ...props }: ToasterProps) => {
9 | const { theme = "system" } = useTheme()
10 |
11 | return (
12 |
28 | )
29 | }
30 |
31 | export { Toaster }
32 |
--------------------------------------------------------------------------------
/src/app/page.tsx:
--------------------------------------------------------------------------------
1 | import { Poppins } from "next/font/google"
2 | import { cn } from "../lib/utils"
3 | import { Button } from "../components/ui/button";
4 | import { LoginButton } from "../components/auth/login-button";
5 | const font = Poppins({
6 | subsets: ["latin"],
7 | weight: ["600"]
8 | })
9 | export default function Home() {
10 | return (
11 |
12 |
13 |
15 | 🔐 Auth
16 |
17 |
18 | A simple authentication service
19 |
20 |
21 |
22 |
25 |
26 |
27 |
28 |
29 | );
30 | }
31 |
--------------------------------------------------------------------------------
/actions/new-verification.ts:
--------------------------------------------------------------------------------
1 | "use server"
2 |
3 | import { db } from "@/lib/db"
4 | import { getUserByEmail } from "../data/user"
5 | import { getVerificationTokenByToken } from "../data/verficiation-token"
6 |
7 | export const newVerification = async (token: string) => {
8 |
9 | const existingToken = await getVerificationTokenByToken(token);
10 |
11 | if (!existingToken) {
12 | return { error: "Token does not exist" }
13 | }
14 |
15 | const hasExpired = new Date(existingToken.expires) < new Date();
16 |
17 | if (hasExpired) {
18 | return { error: "Token has expired!" }
19 | }
20 |
21 | const existingUser = await getUserByEmail(existingToken.email);
22 |
23 | if (!existingUser) {
24 | return { error: "Email does not exist!" }
25 | }
26 |
27 | await db.user.update({
28 | where: { id: existingUser.id },
29 | data: {
30 | emailVerified: new Date(),
31 | email: existingUser.email
32 | }
33 | });
34 |
35 | await db.verificationToken.delete({
36 | where: { id: existingToken.id }
37 | });
38 |
39 | return { success: "Email verified!" }
40 | }
--------------------------------------------------------------------------------
/src/app/layout.tsx:
--------------------------------------------------------------------------------
1 | import type { Metadata } from "next";
2 | import localFont from "next/font/local";
3 | import { SessionProvider } from "next-auth/react";
4 | import { auth } from "../../auth";
5 | import "./globals.css";
6 | import { Session } from "inspector/promises";
7 | import { Toaster } from "@/components/ui/sonner"
8 |
9 | const geistSans = localFont({
10 | src: "./fonts/GeistVF.woff",
11 | variable: "--font-geist-sans",
12 | weight: "100 900",
13 | });
14 | const geistMono = localFont({
15 | src: "./fonts/GeistMonoVF.woff",
16 | variable: "--font-geist-mono",
17 | weight: "100 900",
18 | });
19 |
20 | export const metadata: Metadata = {
21 | title: "Auth 2FA",
22 | description: "Generated by create next app",
23 | };
24 |
25 | export default async function RootLayout({
26 | children,
27 | }: Readonly<{
28 | children: React.ReactNode;
29 | }>) {
30 | const session = await auth()
31 | return (
32 |
33 |
34 |
37 |
38 | {children}
39 |
40 |
41 |
42 | );
43 | }
44 |
--------------------------------------------------------------------------------
/src/components/auth/social.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import { signIn } from "next-auth/react"
4 | import { FcGoogle } from "react-icons/fc"
5 | import { FaGithub } from "react-icons/fa"
6 | import { Button } from "../ui/button"
7 | import { useSearchParams } from "next/navigation"
8 |
9 | const DEFAULT_LOGIN_REDIRECT = "/settings"
10 |
11 | export const Social = () => {
12 | const searchParams = useSearchParams();
13 | const callbackUrl = searchParams.get("callbackUrl");
14 | const onClick = (provider: "google" | "github") => {
15 | signIn(provider, {
16 | callbackUrl: callbackUrl || DEFAULT_LOGIN_REDIRECT
17 | }).catch(err => console.error(err)); // Error handling
18 | }
19 |
20 | return (
21 |
22 |
30 |
38 |
39 | );
40 | };
41 |
--------------------------------------------------------------------------------
/src/components/ui/switch.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as SwitchPrimitives from "@radix-ui/react-switch"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const Switch = React.forwardRef<
9 | React.ElementRef,
10 | React.ComponentPropsWithoutRef
11 | >(({ className, ...props }, ref) => (
12 |
20 |
25 |
26 | ))
27 | Switch.displayName = SwitchPrimitives.Root.displayName
28 |
29 | export { Switch }
30 |
--------------------------------------------------------------------------------
/src/lib/mail.ts:
--------------------------------------------------------------------------------
1 | import { Resend } from "resend";
2 |
3 | const resend = new Resend(process.env.RESEND_API_KEY);
4 |
5 | const domain=process.env.NEXT_PUBLICE_APP_URL;
6 |
7 | export const sendTwoFactorTokenEmail = async (
8 | email: string,
9 | token: string
10 | ) => {
11 | await resend.emails.send({
12 | from: "onboarding@resend.dev",
13 | to: email,
14 | subject: "2Fa Code",
15 | html: `Your 2Fa code ${token}
`
16 | })
17 | }
18 | export const sendPasswordResetEmail = async (
19 | email: string,
20 | token: string,
21 | ) => {
22 | const resetLink = `${domain}/auth/new-password?token=${token}`
23 |
24 | await resend.emails.send({
25 | from: "onboarding@resend.dev",
26 | to: email,
27 | subject: "Reset your password",
28 | html: `here to confrim email
`
29 | })
30 | }
31 |
32 | export const sendVerificationEmail = async (
33 | email: string,
34 | token: string) => {
35 | const confirmLink = `${domain}/auth/new-verification?token=${token}`;
36 |
37 |
38 | await resend.emails.send({
39 | from: "onboarding@resend.dev",
40 | to: email,
41 | subject: "Confrim your email",
42 | html: `here to Reset password
`
43 | })
44 | }
--------------------------------------------------------------------------------
/actions/register.ts:
--------------------------------------------------------------------------------
1 | "use server"
2 | import * as z from "zod"
3 | import bcrypt from "bcrypt"
4 | import { db } from "@/lib/db";
5 | import { RegisterSchema } from "../schemas";
6 | import { getUserByEmail } from "../data/user";
7 | import { generateVerificationToken } from "@/lib/tokens";
8 | import { sendVerificationEmail } from "@/lib/mail";
9 |
10 | export const register = async (values: z.infer) => {
11 | const validatedFields = RegisterSchema.safeParse(values);
12 |
13 | if (!validatedFields.success) {
14 | return { error: "Invalid fields" };
15 | }
16 |
17 | const { email, password, name } = validatedFields.data;
18 | const hashedPassword = await bcrypt.hash(password, 10)
19 |
20 | const existingUser = await getUserByEmail(email);
21 |
22 | if (existingUser) {
23 | return { error: "Email already in use!" }
24 | }
25 |
26 | await db.user.create({
27 | data: {
28 | name,
29 | email,
30 | password: hashedPassword
31 | },
32 | });
33 |
34 |
35 | const verficationToken = await generateVerificationToken(email)
36 | await sendVerificationEmail(
37 | verficationToken.email,
38 | verficationToken.token,
39 | )
40 |
41 | return { success: "Confirmation email sent!" };
42 | }
--------------------------------------------------------------------------------
/src/components/auth/login-button.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 | import { useRouter } from "next/navigation";
3 |
4 | import {
5 | Dialog,
6 | DialogContent,
7 | DialogTrigger,
8 | DialogTitle,
9 | } from "@/components/ui/dialog";
10 | import LoginForm from "./login-form";
11 |
12 | interface LoginButtonProps {
13 | children: React.ReactNode;
14 | mode?: "modal" | "redirect";
15 | asChild?: boolean;
16 | }
17 |
18 | export const LoginButton = ({
19 | children,
20 | mode = "redirect",
21 | asChild,
22 | }: LoginButtonProps) => {
23 | const router = useRouter();
24 |
25 | const onclick = () => {
26 | router.push("/auth/login");
27 | };
28 |
29 | if (mode === "modal") {
30 | return (
31 |
41 | );
42 | }
43 | return (
44 |
45 | {children}
46 |
47 | );
48 | };
49 |
--------------------------------------------------------------------------------
/src/components/ui/badge.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import { cva, type VariantProps } from "class-variance-authority"
3 |
4 | import { cn } from "@/lib/utils"
5 |
6 | const badgeVariants = cva(
7 | "inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
8 | {
9 | variants: {
10 | variant: {
11 | default:
12 | "border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/80",
13 | secondary:
14 | "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
15 | destructive:
16 | "border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80",
17 | outline: "text-foreground",
18 | success:"border-transparent bg-emerald-500 text-primary-foreground"
19 | },
20 | },
21 | defaultVariants: {
22 | variant: "default",
23 | },
24 | }
25 | )
26 |
27 | export interface BadgeProps
28 | extends React.HTMLAttributes,
29 | VariantProps {}
30 |
31 | function Badge({ className, variant, ...props }: BadgeProps) {
32 | return (
33 |
34 | )
35 | }
36 |
37 | export { Badge, badgeVariants }
38 |
--------------------------------------------------------------------------------
/src/components/auth/card-wrapper.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import {
4 | Card,
5 | CardContent,
6 | CardFooter,
7 | CardHeader
8 | } from "@/components/ui/card";
9 | import { Header } from "@/components/auth/header";
10 | import { Social } from "@/components/auth/social";
11 | import { BackButton } from "@/components/auth/back-button";
12 |
13 | interface CardWrapperProps {
14 | children: React.ReactNode;
15 | headerLabel: string;
16 | backButtonLabel: string;
17 | backButtonHref: string;
18 | showSocial?: boolean;
19 | };
20 |
21 | export const CardWrapper = ({
22 | children,
23 | headerLabel,
24 | backButtonLabel,
25 | backButtonHref,
26 | showSocial
27 | }: CardWrapperProps) => {
28 | return (
29 |
30 |
31 |
32 |
33 |
34 | {children}
35 |
36 | {showSocial && (
37 |
38 |
39 |
40 | )}
41 |
42 |
46 |
47 |
48 | )
49 | }
--------------------------------------------------------------------------------
/src/components/auth/user-button.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import {
4 | DropdownMenu,
5 | DropdownMenuContent,
6 | DropdownMenuItem,
7 | DropdownMenuTrigger,
8 | } from '@/components/ui/dropdown-menu';
9 |
10 | import {
11 | Avatar,
12 | AvatarImage,
13 | AvatarFallback
14 | } from "@/components/ui/avatar";
15 | import { FaUser } from 'react-icons/fa';
16 | import { useCurrentUser } from '../../../hooks/use-current-user';
17 | import { LogoutButton } from './logout-button';
18 | import { ExitIcon } from '@radix-ui/react-icons';
19 |
20 | export const UserButton = () => {
21 | const user = useCurrentUser();
22 | return (
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 | Logout
37 |
38 |
39 |
40 |
41 | );
42 | }
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/actions/new-password.ts:
--------------------------------------------------------------------------------
1 | "use server"
2 |
3 | import * as z from "zod";
4 | import bcrypt from "bcryptjs"
5 | import { NewPasswordSchema } from "../schemas";
6 | import { getPasswordResetTokenByToken } from "../data/password-reset-token";
7 | import { getUserByEmail } from "../data/user";
8 | import { db } from "@/lib/db";
9 |
10 | export const newPassword = async (
11 | values: z.infer,
12 | token?: string | null,
13 | ) => {
14 | if (!token) {
15 | return { error: "Missing token!" }
16 | }
17 |
18 | const validatedFields = NewPasswordSchema.safeParse(values)
19 |
20 | if (!validatedFields.success) {
21 | return { error: "Invalid fields!" };
22 | }
23 |
24 | const { password } = validatedFields.data;
25 |
26 | const existingToken = await getPasswordResetTokenByToken(token)
27 |
28 | if (!existingToken) {
29 | return { error: "Invalid token!" }
30 | }
31 |
32 | const hasExpired = new Date(existingToken.expires) < new Date();
33 |
34 | if (hasExpired) {
35 | return { error: "Token has expired!" };
36 | }
37 |
38 | const existingUser = await getUserByEmail(existingToken.email);
39 |
40 | if (!existingUser) {
41 | return { error: "Email does not exist!" }
42 | }
43 |
44 | const hashedPassword = await bcrypt.hash(password, 10);
45 |
46 | await db.user.update({
47 | where: { id: existingUser.id },
48 | data: { password: hashedPassword }
49 | })
50 |
51 | await db.passwordResetToken.delete({
52 | where: { id: existingToken.id }
53 | });
54 |
55 | return { success: "password updated!" }
56 | }
--------------------------------------------------------------------------------
/auth.config.ts:
--------------------------------------------------------------------------------
1 | import bcrypt from "bcryptjs";
2 | import type { NextAuthConfig } from "next-auth";
3 | import Credentials from "next-auth/providers/credentials";
4 | import { LoginSchema } from "./schemas";
5 | import { getUserByEmail } from "./data/user";
6 | import Github from "next-auth/providers/github"
7 | import Google from "next-auth/providers/google"
8 | export default {
9 | providers: [
10 | Github({
11 | clientId: process.env.GITHUB_CLIENT_ID,
12 | clientSecret: process.env.GITHUB_CLIENT_SECRET
13 | }),
14 | Google
15 | ({
16 | clientId: process.env.GOOGLE_CLIENT_ID,
17 | clientSecret: process.env.GOOGLE_CLIENT_SECRET
18 | }),
19 | Credentials({
20 | async authorize(credentials) {
21 | const validatedFields = LoginSchema.safeParse(credentials);
22 |
23 | if (validatedFields.success) {
24 | const { email, password } = validatedFields.data;
25 |
26 | const user = await getUserByEmail(email);
27 |
28 | // Check if user and password exist, and if the password is defined
29 | if (!user || typeof user.password !== 'string') {
30 | return null;
31 | }
32 |
33 | // Compare the provided password with the stored hash
34 | const passwordsMatch = await bcrypt.compare(
35 | password,
36 | user.password
37 | );
38 |
39 | if (passwordsMatch) return user;
40 | }
41 | return null;
42 | }
43 | })
44 | ]
45 | } satisfies NextAuthConfig;
46 |
--------------------------------------------------------------------------------
/src/middleware.ts:
--------------------------------------------------------------------------------
1 | import authConfig from "../auth.config";
2 | import NextAuth from "next-auth";
3 | import {
4 | DEFAULT_LOGIN_REDIRECT,
5 | apiAuthPrefix,
6 | publicRoutes,
7 | authRoutes,
8 | } from "../routes";
9 | import { NextResponse } from "next/server";
10 |
11 | const { auth } = NextAuth(authConfig);
12 |
13 | export default auth((req) => {
14 | const { nextUrl } = req;
15 | const isLoggedIn = !!req.auth;
16 |
17 | const isApiAuthRoute = nextUrl.pathname.startsWith(apiAuthPrefix);
18 | const isPublicRoute = publicRoutes.includes(nextUrl.pathname);
19 | const isAuthRoute = authRoutes.includes(nextUrl.pathname);
20 |
21 | // Allow API auth routes
22 | if (isApiAuthRoute) {
23 | return NextResponse.next();
24 | }
25 |
26 | // Redirect logged-in users away from auth routes
27 | if (isAuthRoute) {
28 | if (isLoggedIn) {
29 | return NextResponse.redirect(new URL(DEFAULT_LOGIN_REDIRECT, nextUrl));
30 | }
31 | return NextResponse.next();
32 | }
33 |
34 | // Handle unauthenticated access to protected routes
35 | if (!isLoggedIn && !isPublicRoute) {
36 | let callbackUrl = nextUrl.pathname;
37 | if (nextUrl.search) {
38 | callbackUrl += nextUrl.search;
39 | }
40 |
41 | const encodedCallbackUrl = encodeURIComponent(callbackUrl);
42 | return NextResponse.redirect(
43 | new URL(`/auth/login?callbackUrl=${encodedCallbackUrl}`, nextUrl)
44 | );
45 | }
46 |
47 | return NextResponse.next();
48 | });
49 |
50 | export const config = {
51 | matcher: [
52 | '/((?!_next|[^?]*\\.(?:html?|css|js(?!on)|jpe?g|webp|png|gif|svg|ttf|woff2?|ico|csv|docx?|xlsx?|zip|webmanifest)).*)',
53 | '/(api|trpc)(.*)',
54 | ],
55 | };
56 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "next-auth-2fa",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "dev": "next dev",
7 | "build": "next build",
8 | "start": "next start",
9 | "lint": "next lint",
10 | "postinstall": "prisma generate"
11 | },
12 | "dependencies": {
13 | "@auth/prisma-adapter": "^2.5.0",
14 | "@hookform/resolvers": "^3.9.0",
15 | "@prisma/client": "^5.21.1",
16 | "@radix-ui/react-avatar": "^1.1.1",
17 | "@radix-ui/react-dialog": "^1.1.2",
18 | "@radix-ui/react-dropdown-menu": "^2.1.2",
19 | "@radix-ui/react-icons": "^1.3.0",
20 | "@radix-ui/react-label": "^2.1.0",
21 | "@radix-ui/react-select": "^2.1.2",
22 | "@radix-ui/react-slot": "^1.1.0",
23 | "@radix-ui/react-switch": "^1.1.1",
24 | "bcrypt": "^5.1.1",
25 | "bcryptjs": "^2.4.3",
26 | "class-variance-authority": "^0.7.0",
27 | "clsx": "^2.1.1",
28 | "lucide-react": "^0.441.0",
29 | "next": "^15.0.3",
30 | "next-auth": "^5.0.0-beta.25",
31 | "next-themes": "^0.3.0",
32 | "react": "^18",
33 | "react-dom": "^18",
34 | "react-hook-form": "^7.53.0",
35 | "react-icons": "^5.3.0",
36 | "react-spinners": "^0.14.1",
37 | "resend": "^4.0.0",
38 | "shadcn-ui": "^0.2.3",
39 | "sonner": "^1.5.0",
40 | "tailwind-merge": "^2.5.2",
41 | "tailwindcss-animate": "^1.0.7",
42 | "three": "^0.169.0",
43 | "uuid": "^10.0.0",
44 | "zod": "^3.23.8"
45 | },
46 | "devDependencies": {
47 | "@types/bcrypt": "^5.0.2",
48 | "@types/bcryptjs": "^2.4.6",
49 | "@types/node": "^20",
50 | "@types/react": "^18",
51 | "@types/react-dom": "^18",
52 | "@types/uuid": "^10.0.0",
53 | "eslint": "^8",
54 | "eslint-config-next": "14.2.11",
55 | "postcss": "^8",
56 | "prisma": "^5.21.1",
57 | "tailwindcss": "^3.4.1",
58 | "typescript": "^5.6.3"
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/src/app/(protected)/_components/navbar.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import Link from "next/link";
4 | import { usePathname } from "next/navigation";
5 | import { Button } from "@/components/ui/button";
6 | import { UserButton } from "@/components/auth/user-button";
7 |
8 | export const Navbar = () => {
9 | const pathname = usePathname();
10 |
11 | return (
12 |
49 | );
50 | };
51 |
--------------------------------------------------------------------------------
/tailwind.config.ts:
--------------------------------------------------------------------------------
1 | import type { Config } from "tailwindcss";
2 |
3 | const config: Config = {
4 | darkMode: ["class"],
5 | content: [
6 | "./src/pages/**/*.{js,ts,jsx,tsx,mdx}",
7 | "./src/components/**/*.{js,ts,jsx,tsx,mdx}",
8 | "./src/app/**/*.{js,ts,jsx,tsx,mdx}",
9 | ],
10 | theme: {
11 | extend: {
12 | colors: {
13 | background: 'hsl(var(--background))',
14 | foreground: 'hsl(var(--foreground))',
15 | card: {
16 | DEFAULT: 'hsl(var(--card))',
17 | foreground: 'hsl(var(--card-foreground))'
18 | },
19 | popover: {
20 | DEFAULT: 'hsl(var(--popover))',
21 | foreground: 'hsl(var(--popover-foreground))'
22 | },
23 | primary: {
24 | DEFAULT: 'hsl(var(--primary))',
25 | foreground: 'hsl(var(--primary-foreground))'
26 | },
27 | secondary: {
28 | DEFAULT: 'hsl(var(--secondary))',
29 | foreground: 'hsl(var(--secondary-foreground))'
30 | },
31 | muted: {
32 | DEFAULT: 'hsl(var(--muted))',
33 | foreground: 'hsl(var(--muted-foreground))'
34 | },
35 | accent: {
36 | DEFAULT: 'hsl(var(--accent))',
37 | foreground: 'hsl(var(--accent-foreground))'
38 | },
39 | destructive: {
40 | DEFAULT: 'hsl(var(--destructive))',
41 | foreground: 'hsl(var(--destructive-foreground))'
42 | },
43 | border: 'hsl(var(--border))',
44 | input: 'hsl(var(--input))',
45 | ring: 'hsl(var(--ring))',
46 | chart: {
47 | '1': 'hsl(var(--chart-1))',
48 | '2': 'hsl(var(--chart-2))',
49 | '3': 'hsl(var(--chart-3))',
50 | '4': 'hsl(var(--chart-4))',
51 | '5': 'hsl(var(--chart-5))'
52 | }
53 | },
54 | borderRadius: {
55 | lg: 'var(--radius)',
56 | md: 'calc(var(--radius) - 2px)',
57 | sm: 'calc(var(--radius) - 4px)'
58 | }
59 | }
60 | },
61 | plugins: [require("tailwindcss-animate")],
62 | };
63 | export default config;
64 |
--------------------------------------------------------------------------------
/schemas/index.ts:
--------------------------------------------------------------------------------
1 | import { UserRole } from "@prisma/client";
2 | import * as z from "zod";
3 | import { newPassword } from "../actions/new-password";
4 |
5 | export const SettingsSchema = z.object({
6 | name: z.optional(z.string()),
7 | isTwoFactorEnabled: z.optional(z.boolean()),
8 | role: z.enum([UserRole.ADMIN, UserRole.USER]),
9 | email: z.optional(z.string().email()),
10 | password: z.optional(z.string().min(6)),
11 | newPassword: z.optional(z.string().min(6))
12 | })
13 | .refine((data) => {
14 | if (data.password && !data.newPassword) {
15 | return false
16 | }
17 | return true
18 | },{
19 | message:"New password is required?",
20 | path:["newPassword"]
21 | })
22 | .refine((data) => {
23 | if (data.newPassword && !data.password) {
24 | return false
25 | }
26 | return true
27 | },{
28 | message:"password is required?",
29 | path:["password"]
30 | })
31 |
32 | export const NewPasswordSchema = z.object({
33 | password: z.string().min(6, {
34 | message: "Password must be at least 6 characters",
35 | }),
36 | });
37 |
38 |
39 | export const ResetSchema = z.object({
40 | email: z.string().email({
41 | message: "Email is required"
42 | }),
43 | })
44 |
45 |
46 | export const LoginSchema = z.object({
47 | email: z.string().email({
48 | message: "Email is required"
49 | }),
50 | password: z.string().min(6, {
51 | message: "Password must be at least 6 characters"
52 | }),
53 |
54 | code: z.optional(z.string())
55 | })
56 |
57 |
58 |
59 | export const RegisterSchema = z.object({
60 | email: z.string().email({
61 | message: "Email is required"
62 | }),
63 | password: z.string().min(6,
64 | {
65 | message: "Password must be at least 6 characters"
66 | }),
67 | name: z.string().min(3, {
68 | message: "Name is required"
69 | }),
70 | })
--------------------------------------------------------------------------------
/src/components/auth/new-verification-form.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import { CardWrapper } from "./card-wrapper";
4 | import { useCallback, useEffect, useState } from "react";
5 | import { BeatLoader } from "react-spinners";
6 | import { useSearchParams } from "next/navigation";
7 | import { newVerification } from "../../../actions/new-verification";
8 | import { FormError } from "@/components/form-error";
9 | import { FormSuccess } from "@/components/form-success";
10 |
11 | const NewVerificationForm = () => {
12 | const [error, setError] = useState()
13 | const [success, setSuccess] = useState()
14 |
15 | const searchParams = useSearchParams();
16 |
17 | const token = searchParams.get("token")
18 |
19 | const onSubmit = useCallback(() => {
20 |
21 | if (success || error) return;
22 |
23 | if (!token) {
24 | setError("Missing Token!");
25 | return;
26 | }
27 |
28 | newVerification(token)
29 | .then((data) => {
30 | setSuccess(data.success)
31 | setError(data.error)
32 | })
33 | .catch(() => {
34 | setError("Somethings went wrong!")
35 | })
36 | }, [token, success, error])
37 |
38 | useEffect(() => {
39 | onSubmit();
40 | }, [onSubmit])
41 | return (
42 |
46 |
47 |
48 | {!success && !error &&
49 | }
50 |
51 |
52 | {!success && (
53 |
54 | )}
55 |
56 |
57 |
58 | )
59 | }
60 |
61 | export default NewVerificationForm
62 |
--------------------------------------------------------------------------------
/src/components/ui/button.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import { Slot } from "@radix-ui/react-slot"
3 | import { cva, type VariantProps } from "class-variance-authority"
4 |
5 | import { cn } from "@/lib/utils"
6 |
7 | const buttonVariants = cva(
8 | "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50",
9 | {
10 | variants: {
11 | variant: {
12 | default:
13 | "bg-primary text-primary-foreground shadow hover:bg-primary/90",
14 | destructive:
15 | "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
16 | outline:
17 | "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground",
18 | secondary:
19 | "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
20 | ghost: "hover:bg-accent hover:text-accent-foreground",
21 | link: "text-primary underline-offset-4 hover:underline",
22 | },
23 | size: {
24 | default: "h-9 px-4 py-2",
25 | sm: "h-8 rounded-md px-3 text-xs",
26 | lg: "h-10 rounded-md px-8",
27 | icon: "h-9 w-9",
28 | },
29 | },
30 | defaultVariants: {
31 | variant: "default",
32 | size: "default",
33 | },
34 | }
35 | )
36 |
37 | export interface ButtonProps
38 | extends React.ButtonHTMLAttributes,
39 | VariantProps {
40 | asChild?: boolean
41 | }
42 |
43 | const Button = React.forwardRef(
44 | ({ className, variant, size, asChild = false, ...props }, ref) => {
45 | const Comp = asChild ? Slot : "button"
46 | return (
47 |
52 | )
53 | }
54 | )
55 | Button.displayName = "Button"
56 |
57 | export { Button, buttonVariants }
58 |
--------------------------------------------------------------------------------
/src/components/ui/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 |
41 | ))
42 | CardTitle.displayName = "CardTitle"
43 |
44 | const CardDescription = React.forwardRef<
45 | HTMLParagraphElement,
46 | React.HTMLAttributes
47 | >(({ className, ...props }, ref) => (
48 |
53 | ))
54 | CardDescription.displayName = "CardDescription"
55 |
56 | const CardContent = React.forwardRef<
57 | HTMLDivElement,
58 | React.HTMLAttributes
59 | >(({ className, ...props }, ref) => (
60 |
61 | ))
62 | CardContent.displayName = "CardContent"
63 |
64 | const CardFooter = React.forwardRef<
65 | HTMLDivElement,
66 | React.HTMLAttributes
67 | >(({ className, ...props }, ref) => (
68 |
73 | ))
74 | CardFooter.displayName = "CardFooter"
75 |
76 | export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
77 |
--------------------------------------------------------------------------------
/src/lib/tokens.ts:
--------------------------------------------------------------------------------
1 | import crypto from "crypto";
2 | import { v4 as uuidv4 } from "uuid";
3 | import { db } from "@/lib/db";
4 | import { getVerificationTokenByEmail } from "../../data/verficiation-token";
5 | import { getPasswordResetTokenByEmail } from "../../data/password-reset-token";
6 | import { getTwoFactorTokenByEmail } from "../../data/two-fator-token";
7 |
8 | export const generateTwoFactorToken = async (email: string) => {
9 | const token = crypto.randomInt(100_000, 1_000_000).toString();
10 | const expires = new Date(new Date().getTime() + 5 * 60 * 1000);
11 |
12 | const existingToken = await getTwoFactorTokenByEmail(email);
13 |
14 | if (existingToken) {
15 | await db.twoFactorToken.delete({
16 | where: {
17 | id: existingToken.id,
18 | },
19 | });
20 | }
21 |
22 | const twoFactorToken = await db.twoFactorToken.create({
23 | data: {
24 | email,
25 | token,
26 | expires,
27 | },
28 | });
29 |
30 | return twoFactorToken;
31 | };
32 |
33 | export const generatePasswordResetToken = async (email: string) => {
34 | const token = uuidv4();
35 | const expires = new Date(new Date().getTime() + 3600 * 1000);
36 |
37 | const existingToken = await getPasswordResetTokenByEmail(email);
38 |
39 | if (existingToken) {
40 | await db.passwordResetToken.delete({
41 | where: { id: existingToken.id },
42 | });
43 | }
44 |
45 | const passwordResetToken = await db.passwordResetToken.create({
46 | data: {
47 | email,
48 | token,
49 | expires,
50 | },
51 | });
52 |
53 | return passwordResetToken;
54 | };
55 |
56 | export const generateVerificationToken = async (email: string) => {
57 | const token = uuidv4();
58 | const expires = new Date(new Date().getTime() + 3600 * 1000);
59 |
60 | const existingToken = await getVerificationTokenByEmail(email);
61 |
62 | if (existingToken) {
63 | await db.verificationToken.delete({
64 | where: {
65 | id: existingToken.id,
66 | },
67 | });
68 | }
69 |
70 | const verificationToken = await db.verificationToken.create({
71 | data: {
72 | email,
73 | token,
74 | expires,
75 | },
76 | });
77 |
78 | return verificationToken;
79 | };
80 |
--------------------------------------------------------------------------------
/src/app/globals.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | html,
6 | body,
7 | :root{
8 | height: 100%;
9 | }
10 | :root {
11 | --background: #ffffff;
12 | --foreground: #171717;
13 | }
14 |
15 | @media (prefers-color-scheme: dark) {
16 | :root {
17 | --background: #0a0a0a;
18 | --foreground: #ededed;
19 | }
20 | }
21 |
22 | body {
23 | color: var(--foreground);
24 | background: var(--background);
25 | font-family: Arial, Helvetica, sans-serif;
26 | }
27 |
28 | @layer utilities {
29 | .text-balance {
30 | text-wrap: balance;
31 | }
32 | }
33 |
34 | @layer base {
35 | :root {
36 | --background: 0 0% 100%;
37 | --foreground: 240 10% 3.9%;
38 | --card: 0 0% 100%;
39 | --card-foreground: 240 10% 3.9%;
40 | --popover: 0 0% 100%;
41 | --popover-foreground: 240 10% 3.9%;
42 | --primary: 240 5.9% 10%;
43 | --primary-foreground: 0 0% 98%;
44 | --secondary: 240 4.8% 95.9%;
45 | --secondary-foreground: 240 5.9% 10%;
46 | --muted: 240 4.8% 95.9%;
47 | --muted-foreground: 240 3.8% 46.1%;
48 | --accent: 240 4.8% 95.9%;
49 | --accent-foreground: 240 5.9% 10%;
50 | --destructive: 0 84.2% 60.2%;
51 | --destructive-foreground: 0 0% 98%;
52 | --border: 240 5.9% 90%;
53 | --input: 240 5.9% 90%;
54 | --ring: 240 10% 3.9%;
55 | --chart-1: 12 76% 61%;
56 | --chart-2: 173 58% 39%;
57 | --chart-3: 197 37% 24%;
58 | --chart-4: 43 74% 66%;
59 | --chart-5: 27 87% 67%;
60 | --radius: 0.5rem;
61 | }
62 | .dark {
63 | --background: 240 10% 3.9%;
64 | --foreground: 0 0% 98%;
65 | --card: 240 10% 3.9%;
66 | --card-foreground: 0 0% 98%;
67 | --popover: 240 10% 3.9%;
68 | --popover-foreground: 0 0% 98%;
69 | --primary: 0 0% 98%;
70 | --primary-foreground: 240 5.9% 10%;
71 | --secondary: 240 3.7% 15.9%;
72 | --secondary-foreground: 0 0% 98%;
73 | --muted: 240 3.7% 15.9%;
74 | --muted-foreground: 240 5% 64.9%;
75 | --accent: 240 3.7% 15.9%;
76 | --accent-foreground: 0 0% 98%;
77 | --destructive: 0 62.8% 30.6%;
78 | --destructive-foreground: 0 0% 98%;
79 | --border: 240 3.7% 15.9%;
80 | --input: 240 3.7% 15.9%;
81 | --ring: 240 4.9% 83.9%;
82 | --chart-1: 220 70% 50%;
83 | --chart-2: 160 60% 45%;
84 | --chart-3: 30 80% 55%;
85 | --chart-4: 280 65% 60%;
86 | --chart-5: 340 75% 55%;
87 | }
88 | }
89 |
90 | @layer base {
91 | * {
92 | @apply border-border;
93 | }
94 | body {
95 | @apply bg-background text-foreground;
96 | }
97 | }
98 |
--------------------------------------------------------------------------------
/prisma/schema.prisma:
--------------------------------------------------------------------------------
1 | // This is your Prisma schema file,
2 | // learn more about it in the docs: https://pris.ly/d/prisma-schema
3 |
4 | // Looking for ways to speed up your queries, or scale easily with your serverless or edge functions?
5 | // Try Prisma Accelerate: https://pris.ly/cli/accelerate-init
6 |
7 | generator client {
8 | provider = "prisma-client-js"
9 | }
10 |
11 | datasource db {
12 | provider = "postgresql"
13 | url = env("DATABASE_URL")
14 | }
15 |
16 | enum UserRole {
17 | ADMIN
18 | USER
19 | }
20 |
21 | model User {
22 | id String @id @default(cuid())
23 | name String?
24 | email String? @unique
25 | emailVerified DateTime? @map("email_verified")
26 | image String?
27 | password String?
28 | role UserRole @default(USER)
29 | accounts Account[]
30 | isTwoFactorEnabled Boolean @default(false)
31 | twoFactorConfirmation TwoFactorConfirmation?
32 |
33 | @@map("users")
34 | }
35 |
36 | model Account {
37 | id String @id @default(cuid())
38 | userId String @map("user_id")
39 | type String
40 | provider String
41 | providerAccountId String @map("provider_account_id")
42 | refresh_token String? @db.Text
43 | access_token String? @db.Text
44 | expires_at Int?
45 | token_type String?
46 | scope String?
47 | id_token String? @db.Text
48 | session_state String?
49 |
50 | user User @relation(fields: [userId], references: [id], onDelete: Cascade)
51 |
52 | @@unique([provider, providerAccountId])
53 | @@map("accounts")
54 | }
55 |
56 | model VerificationToken {
57 | id String @id @default(cuid())
58 | email String
59 | token String @unique
60 | expires DateTime
61 |
62 | @@unique([email, token])
63 | }
64 |
65 | model PasswordResetToken {
66 | id String @id @default(cuid())
67 | email String
68 | token String @unique
69 | expires DateTime
70 |
71 | @@unique([email, token])
72 | }
73 |
74 | model TwoFactorToken {
75 | id String @id @default(cuid())
76 | email String
77 | token String @unique
78 | expires DateTime
79 |
80 | @@unique([email, token])
81 | }
82 |
83 | model TwoFactorConfirmation {
84 | id String @id @default(cuid())
85 | userId String
86 | user User @relation(fields: [userId], references: [id], onDelete: Cascade)
87 |
88 | @@unique([userId])
89 | }
90 |
--------------------------------------------------------------------------------
/src/app/(protected)/admin/page.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import { RoleGate } from "@/components/auth/role-gate";
4 | import { FormSuccess } from "@/components/form-success";
5 | import { Button } from "@/components/ui/button";
6 | import { Card, CardContent, CardHeader } from "@/components/ui/card";
7 | import { UserRole } from "@prisma/client";
8 | import { toast } from "sonner";
9 | import { admin } from "../../../../actions/admin";
10 |
11 | const AdminPage = () => {
12 | const onServerActionClick = () => {
13 | admin()
14 | .then((data) => {
15 | if (data.error) {
16 | toast.error(data.error);
17 | }
18 |
19 | if (data.success) {
20 | toast.success(data.success)
21 | }
22 | })
23 | }
24 |
25 | const onApiRouteClick = () => {
26 | fetch("/api/admin")
27 | .then((response) => {
28 | if (response.ok) {
29 | toast.success("Allowed API Route!")
30 | } else {
31 | toast.error("Forbidden API Route!")
32 | }
33 | })
34 | }
35 | return (
36 |
37 |
38 |
39 | 🔑 Admin
40 |
41 |
42 |
43 |
44 |
46 |
47 |
48 |
49 | Admin Only Api Route
50 |
51 |
54 |
55 |
56 |
57 | Admin Only Server Action
58 |
59 |
62 |
63 |
64 |
65 | )
66 | }
67 | export default AdminPage
--------------------------------------------------------------------------------
/actions/setting.ts:
--------------------------------------------------------------------------------
1 | "use server"
2 |
3 | import * as z from "zod";
4 | import bcrypt from "bcryptjs"
5 | import { db } from "@/lib/db"
6 | import { SettingsSchema } from "../schemas";
7 | import { getUserByEmail, getUserById } from "../data/user";
8 | import { currentUser } from "@/lib/auth";
9 | import { generateVerificationToken } from "@/lib/tokens";
10 | import { sendVerificationEmail } from "@/lib/mail";
11 | import { update } from "../auth";
12 |
13 | export const settings = async (
14 | values: z.infer
15 | ) => {
16 | const user = await currentUser();
17 |
18 | if (!user || !user.id) {
19 | return { error: "Unauthorized" };
20 | }
21 |
22 | const dbUser = await getUserById(user.id);
23 | if (!dbUser) {
24 | return { error: "Unauthorized" };
25 | }
26 |
27 | if (user.isOAuth) {
28 | values.email = undefined,
29 | values.password = undefined,
30 | values.newPassword = undefined,
31 | values.isTwoFactorEnabled = undefined
32 | }
33 |
34 | if (values.email && values.email !== user.email) {
35 | const existingUser = await getUserByEmail(values.email);
36 |
37 | if (existingUser && existingUser.id !== user.id) {
38 | return { error: "Email already in use!" }
39 | }
40 | const verficationToken = await generateVerificationToken(
41 | values.email
42 | );
43 | await sendVerificationEmail(
44 | verficationToken.email,
45 | verficationToken.token,
46 | );
47 | return { success: "Verfication email sent!" };
48 | }
49 |
50 | if (values.password && values.newPassword && dbUser.password) {
51 | const passwordsMatch = await bcrypt.compare(
52 | values.password,
53 | dbUser.password,
54 | );
55 |
56 | if (!passwordsMatch) {
57 | return { error: "Incorrect password!" };
58 | }
59 |
60 | const hashedPassword = await bcrypt.hash(
61 | values.newPassword,
62 | 10
63 | );
64 | values.password = hashedPassword;
65 | values.newPassword = undefined;
66 | }
67 |
68 |
69 | const updatedUser = await db.user.update({
70 | where: { id: dbUser.id },
71 | data: {
72 | ...values,
73 | },
74 | });
75 | update({
76 | user: {
77 | name: updatedUser.name,
78 | email: updatedUser.email,
79 | isTwoFactorEnabled: updatedUser.isTwoFactorEnabled,
80 | role: updatedUser.role,
81 | }
82 | })
83 |
84 | return { success: "Settings Updated!" }
85 | }
--------------------------------------------------------------------------------
/src/components/auth/reset-form.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 | import * as z from "zod";
3 | import { useForm } from "react-hook-form";
4 | import { zodResolver } from "@hookform/resolvers/zod";
5 | import { CardWrapper } from "./card-wrapper";
6 | import { ResetSchema } from "../../../schemas";
7 | import {
8 | Form,
9 | FormControl,
10 | FormField,
11 | FormItem,
12 | FormLabel,
13 | FormMessage,
14 | } from "@/components/ui/form";
15 | import { Input } from "@/components/ui/input";
16 | import { Button } from "@/components/ui/button";
17 | import { FormError } from "@/components/form-error";
18 | import { FormSuccess } from "@/components/form-success";
19 | import { reset } from "../../../actions/reset";
20 | import { useState, useTransition } from "react";
21 |
22 | const ResetForm = () => {
23 | const [error, setError] = useState("");
24 | const [success, setSuccess] = useState("");
25 | const [isPending, startTransition] = useTransition();
26 |
27 | const form = useForm>({
28 | resolver: zodResolver(ResetSchema),
29 | defaultValues: {
30 | email: "",
31 | },
32 | });
33 |
34 | const onSubmit = (values: z.infer) => {
35 | setError("");
36 | setSuccess("");
37 |
38 | startTransition(() => {
39 | reset(values)
40 | .then((data) => {
41 | if (data?.error) {
42 | setError(data.error);
43 | } else {
44 | setSuccess(data.success);
45 | }
46 | })
47 | })
48 | }
49 |
50 | return (
51 |
56 |
79 |
80 |
81 | );
82 | };
83 |
84 | export default ResetForm;
85 |
--------------------------------------------------------------------------------
/src/components/user-info.tsx:
--------------------------------------------------------------------------------
1 | import { ExtendedUser } from "../../next-auth";
2 | import { Badge } from "./ui/badge";
3 | import { Card, CardHeader, CardContent } from "./ui/card";
4 |
5 | interface UserInfoProps {
6 | user?: ExtendedUser;
7 | label: string;
8 | }
9 |
10 | export const UserInfo = ({
11 | user,
12 | label,
13 | }: UserInfoProps) => {
14 | return (
15 |
16 |
17 |
18 | {label}
19 |
20 |
21 |
22 |
23 |
24 | Name
25 |
26 |
27 | {user?.name}
28 |
29 |
30 |
31 |
32 | ID
33 |
34 |
35 | {user?.id}
36 |
37 |
38 |
39 |
40 | Email
41 |
42 |
43 | {user?.email}
44 |
45 |
46 |
47 |
48 | Role
49 |
50 |
51 | {user?.role}
52 |
53 |
54 |
55 |
56 | Two Factor Authentication
57 |
58 |
59 | {user?.isTwoFactorEnabled ? "ON" : "OFF"}
60 |
61 |
62 |
63 |
64 | );
65 | };
66 |
--------------------------------------------------------------------------------
/auth.ts:
--------------------------------------------------------------------------------
1 | import NextAuth from "next-auth"
2 | import authConfig from "./auth.config"
3 | import { getUserById } from "./data/user"
4 | import { PrismaAdapter } from "@auth/prisma-adapter"
5 | import { db } from "@/lib/db"
6 | import { getTwoFactorConfirmationByUserId } from "./data/two-factor-confirmation"
7 | import { getAccountByUserId } from "./data/account"
8 |
9 |
10 |
11 |
12 | export const {
13 | handlers: { GET, POST },
14 | auth,
15 | signIn,
16 | signOut,
17 | update
18 | } = NextAuth({
19 | pages: {
20 | signIn: "/auth/login",
21 | error: "/auth/error"
22 | },
23 | events: {
24 | async linkAccount({ user }) {
25 | await db.user.update({
26 | where: { id: user.id },
27 | data: { emailVerified: new Date() }
28 | })
29 | }
30 | },
31 | callbacks: {
32 | async signIn({ user, account }) {
33 | if (account?.provider !== "credentials") return true;
34 |
35 |
36 | if (!user?.id) {
37 | console.log("User ID not found.");
38 | return false;
39 | }
40 |
41 |
42 | const existingUser = await getUserById(user.id);
43 |
44 |
45 | // Prevent sign in without email verification
46 | if (!existingUser?.emailVerified) {
47 | console.log("Email not verified.");
48 | return false;
49 | }
50 |
51 |
52 | // Check if 2FA is enabled for the user
53 | if (existingUser.isTwoFactorEnabled) {
54 | const twoFactorConfirmation = await getTwoFactorConfirmationByUserId(existingUser.id);
55 |
56 |
57 | console.log({ twoFactorConfirmation })
58 |
59 |
60 | if (!twoFactorConfirmation) return false;
61 |
62 |
63 | await db.twoFactorConfirmation.delete({
64 | where: { id: twoFactorConfirmation.id }
65 | })
66 | }
67 |
68 |
69 | return true; // Allow login if all checks pass
70 | },
71 | async session({ token, session }) {
72 | if (token.sub && session.user) {
73 | session.user.id = token.sub;
74 | }
75 |
76 |
77 | if (token.role && session.user) {
78 | session.user.role = token.role;
79 | }
80 |
81 | if (session.user) {
82 | session.user.isTwoFactorEnabled = token.isTwoFactorEnabled as boolean;
83 | }
84 |
85 | if (session.user) {
86 | session.user.name = token.name;
87 | session.user.email = token.email as string;
88 | session.user.isOAuth = token.isOAuth as boolean;
89 | }
90 |
91 | return session;
92 | },
93 | async jwt({ token }) {
94 | if (!token.sub) return token;
95 | const existingUser = await getUserById(token.sub);
96 |
97 |
98 | if (!existingUser) return token;
99 |
100 | const existingAccount = await getAccountByUserId(
101 | existingUser.id
102 | )
103 | token.isOAuth = !!existingAccount
104 | token.name = existingUser.name;
105 | token.email = existingUser.email
106 | token.role = existingUser.role;
107 |
108 | token.isTwoFactorEnabled = existingUser.isTwoFactorEnabled;
109 |
110 |
111 | return token;
112 | }
113 | },
114 | adapter: PrismaAdapter(db),
115 | session: { strategy: "jwt" },
116 | ...authConfig
117 | })
118 |
--------------------------------------------------------------------------------
/src/components/auth/new-password-form.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 | import * as z from "zod";
3 | import { useForm } from "react-hook-form";
4 | import { zodResolver } from "@hookform/resolvers/zod";
5 | import { CardWrapper } from "./card-wrapper";
6 | import { NewPasswordSchema } from "../../../schemas"; // Ensure the schema is correct
7 | import { useSearchParams } from "next/navigation";
8 | import {
9 | Form,
10 | FormControl,
11 | FormField,
12 | FormItem,
13 | FormLabel,
14 | FormMessage,
15 | } from "@/components/ui/form";
16 | import { Input } from "@/components/ui/input";
17 | import { Button } from "@/components/ui/button";
18 | import { FormError } from "@/components/form-error";
19 | import { FormSuccess } from "@/components/form-success";
20 | import { newPassword } from "../../../actions/new-password";
21 | import { useState, useTransition } from "react";
22 |
23 | const NewPasswordForm = () => {
24 | const searchParams = useSearchParams();
25 | const token = searchParams.get('token');
26 |
27 | const [error, setError] = useState("");
28 | const [success, setSuccess] = useState("");
29 | const [isPending, startTransition] = useTransition();
30 |
31 | const form = useForm>({
32 | resolver: zodResolver(NewPasswordSchema),
33 | defaultValues: {
34 | password: "",
35 | },
36 | });
37 |
38 | const onSubmit = (values: z.infer) => {
39 | if (!token) {
40 | setError("Invalid or missing token.");
41 | return;
42 | }
43 |
44 | setError("");
45 | setSuccess("");
46 |
47 | startTransition(() => {
48 | newPassword(values, token)
49 | .then((data) => {
50 | if (data?.error) {
51 | setError(data?.error);
52 | } else {
53 | setSuccess(data?.success);
54 | }
55 | });
56 | });
57 | };
58 |
59 | return (
60 |
65 |
92 |
93 |
94 | );
95 | };
96 |
97 | export default NewPasswordForm;
98 |
--------------------------------------------------------------------------------
/actions/login.ts:
--------------------------------------------------------------------------------
1 | "use server"
2 | import * as z from "zod"
3 |
4 |
5 | import { signIn } from '../auth'
6 | import { LoginSchema } from "../schemas";
7 | import { db } from "@/lib/db";
8 | import { DEFAULT_LOGIN_REDIRECT } from "../routes";
9 | import { getTwoFactorTokenByEmail } from "../data/two-fator-token";
10 | import { AuthError } from "next-auth";
11 | import { generateVerificationToken, generateTwoFactorToken } from "@/lib/tokens";
12 | import { getUserByEmail } from "../data/user";
13 | import { sendVerificationEmail, sendTwoFactorTokenEmail } from "@/lib/mail";
14 | import { getTwoFactorConfirmationByUserId } from "../data/two-factor-confirmation";
15 |
16 |
17 | export const Login = async (values: z.infer,
18 | callbackUrl: string | null
19 | ) => {
20 | const validatedFields = LoginSchema.safeParse(values);
21 |
22 |
23 | if (!validatedFields.success) {
24 | return { error: "Invalid fields" };
25 | }
26 | const { email, password, code } = validatedFields.data;
27 |
28 |
29 | const existingUser = await getUserByEmail(email);
30 |
31 |
32 | if (!existingUser || !existingUser.email || !existingUser.password) {
33 | return { error: "Email does not exist!" };
34 | }
35 |
36 |
37 | if (!existingUser.emailVerified) {
38 | const verificationToken = await generateVerificationToken(existingUser.email)
39 |
40 |
41 | await sendVerificationEmail(
42 | verificationToken.email,
43 | verificationToken.token
44 | );
45 |
46 |
47 | return { success: "Confirmation email sent!" }
48 | }
49 |
50 |
51 | if (existingUser.isTwoFactorEnabled && existingUser.email) {
52 | if (code) {
53 | // Ensure async retrieval of the token
54 | const twoFactorToken = await getTwoFactorTokenByEmail(existingUser.email);
55 |
56 |
57 | if (!twoFactorToken) {
58 | return { error: "Invalid code!" }
59 | }
60 |
61 |
62 | // Make sure the comparison is correct and case-sensitive
63 | if (twoFactorToken.token !== code.trim()) {
64 | return { error: "Invalid code!" }
65 | }
66 |
67 |
68 | const hasExpired = new Date(twoFactorToken.expires) < new Date();
69 |
70 |
71 | if (hasExpired) {
72 | return { error: "Code expired!" }
73 | }
74 |
75 |
76 | // Delete the token after use
77 | await db.twoFactorToken.delete({
78 | where: { id: twoFactorToken.id }
79 | });
80 |
81 |
82 | // Handle existing 2FA confirmation
83 | const existingConfirmation = await getTwoFactorConfirmationByUserId(
84 | existingUser.id
85 | );
86 |
87 |
88 | if (existingConfirmation) {
89 | await db.twoFactorConfirmation.delete({
90 | where: { id: existingConfirmation.id }
91 | })
92 | }
93 |
94 |
95 | // Create new confirmation entry
96 | await db.twoFactorConfirmation.create({
97 | data: {
98 | userId: existingUser.id
99 | }
100 | })
101 | } else {
102 | // If no code was provided, send a new token
103 | const twoFactorToken = await generateTwoFactorToken(existingUser.email)
104 | await sendTwoFactorTokenEmail(
105 | twoFactorToken.email,
106 | twoFactorToken.token
107 | );
108 | return { twoFactor: true }
109 | }
110 | }
111 |
112 |
113 | try {
114 | await signIn("credentials", {
115 | email,
116 | password,
117 | redirectTo: callbackUrl || DEFAULT_LOGIN_REDIRECT
118 | })
119 | } catch (error) {
120 | if (error instanceof AuthError) {
121 | switch (error.type) {
122 | case "CredentialsSignin":
123 | return { error: "Invalid credentials" }
124 | default:
125 | return { error: "Something went wrong!" }
126 | }
127 | }
128 | throw error;
129 | }
130 | }
131 |
--------------------------------------------------------------------------------
/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 { cn } from "@/lib/utils"
6 | import { Cross2Icon } from "@radix-ui/react-icons"
7 |
8 | const Dialog = DialogPrimitive.Root
9 |
10 | const DialogTrigger = DialogPrimitive.Trigger
11 |
12 | const DialogPortal = DialogPrimitive.Portal
13 |
14 | const DialogClose = DialogPrimitive.Close
15 |
16 | const DialogOverlay = React.forwardRef<
17 | React.ElementRef,
18 | React.ComponentPropsWithoutRef
19 | >(({ className, ...props }, ref) => (
20 |
28 | ))
29 | DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
30 |
31 | const DialogContent = React.forwardRef<
32 | React.ElementRef,
33 | React.ComponentPropsWithoutRef
34 | >(({ className, children, ...props }, ref) => (
35 |
36 |
37 |
45 | {children}
46 |
47 |
48 | Close
49 |
50 |
51 |
52 | ))
53 | DialogContent.displayName = DialogPrimitive.Content.displayName
54 |
55 | const DialogHeader = ({
56 | className,
57 | ...props
58 | }: React.HTMLAttributes) => (
59 |
66 | )
67 | DialogHeader.displayName = "DialogHeader"
68 |
69 | const DialogFooter = ({
70 | className,
71 | ...props
72 | }: React.HTMLAttributes) => (
73 |
80 | )
81 | DialogFooter.displayName = "DialogFooter"
82 |
83 | const DialogTitle = React.forwardRef<
84 | React.ElementRef,
85 | React.ComponentPropsWithoutRef
86 | >(({ className, ...props }, ref) => (
87 |
95 | ))
96 | DialogTitle.displayName = DialogPrimitive.Title.displayName
97 |
98 | const DialogDescription = React.forwardRef<
99 | React.ElementRef,
100 | React.ComponentPropsWithoutRef
101 | >(({ className, ...props }, ref) => (
102 |
107 | ))
108 | DialogDescription.displayName = DialogPrimitive.Description.displayName
109 |
110 | export {
111 | Dialog,
112 | DialogPortal,
113 | DialogOverlay,
114 | DialogTrigger,
115 | DialogClose,
116 | DialogContent,
117 | DialogHeader,
118 | DialogFooter,
119 | DialogTitle,
120 | DialogDescription,
121 | }
122 |
--------------------------------------------------------------------------------
/src/components/auth/register-form.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 | import * as z from "zod";
3 | import { useForm } from "react-hook-form";
4 | import { zodResolver } from "@hookform/resolvers/zod";
5 | import { CardWrapper } from './card-wrapper';
6 | import { RegisterSchema } from "../../../schemas";
7 | import {
8 | Form,
9 | FormControl,
10 | FormField,
11 | FormItem,
12 | FormLabel,
13 | FormMessage,
14 | } from "@/components/ui/form";
15 | import { Input } from "@/components/ui/input";
16 | import { Button } from "@/components//ui/button";
17 | import { FormError } from "@/components/form-error";
18 | import { FormSuccess } from "@/components/form-success";
19 | import { register } from "../../../actions/register";
20 | import { useState, useTransition } from "react";
21 |
22 | const RegisterForm = () => {
23 | const [error, setError] = useState("")
24 | const [success, setSuccess] = useState("")
25 | const [isPending, startTransition] = useTransition()
26 |
27 | const form = useForm>({
28 | resolver: zodResolver(RegisterSchema),
29 | defaultValues: {
30 | email: "",
31 | password: "",
32 | name: "",
33 | },
34 | });
35 |
36 | const onSubmit = (values: z.infer) => {
37 | setError("");
38 | setSuccess("");
39 |
40 | startTransition(() => {
41 | register(values)
42 | .then((data) => {
43 | setError(data.error)
44 | setSuccess(data.success)
45 | })
46 | })
47 | }
48 |
49 | return (
50 |
56 |
109 |
110 |
111 | );
112 | };
113 |
114 | export default RegisterForm;
115 |
--------------------------------------------------------------------------------
/src/components/ui/form.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as LabelPrimitive from "@radix-ui/react-label"
5 | import { Slot } from "@radix-ui/react-slot"
6 | import {
7 | Controller,
8 | ControllerProps,
9 | FieldPath,
10 | FieldValues,
11 | FormProvider,
12 | useFormContext,
13 | } from "react-hook-form"
14 |
15 | import { cn } from "@/lib/utils"
16 | import { Label } from "@/components/ui/label"
17 |
18 | const Form = FormProvider
19 |
20 | type FormFieldContextValue<
21 | TFieldValues extends FieldValues = FieldValues,
22 | TName extends FieldPath = FieldPath
23 | > = {
24 | name: TName
25 | }
26 |
27 | const FormFieldContext = React.createContext(
28 | {} as FormFieldContextValue
29 | )
30 |
31 | const FormField = <
32 | TFieldValues extends FieldValues = FieldValues,
33 | TName extends FieldPath = FieldPath
34 | >({
35 | ...props
36 | }: ControllerProps) => {
37 | return (
38 |
39 |
40 |
41 | )
42 | }
43 |
44 | const useFormField = () => {
45 | const fieldContext = React.useContext(FormFieldContext)
46 | const itemContext = React.useContext(FormItemContext)
47 | const { getFieldState, formState } = useFormContext()
48 |
49 | const fieldState = getFieldState(fieldContext.name, formState)
50 |
51 | if (!fieldContext) {
52 | throw new Error("useFormField should be used within ")
53 | }
54 |
55 | const { id } = itemContext
56 |
57 | return {
58 | id,
59 | name: fieldContext.name,
60 | formItemId: `${id}-form-item`,
61 | formDescriptionId: `${id}-form-item-description`,
62 | formMessageId: `${id}-form-item-message`,
63 | ...fieldState,
64 | }
65 | }
66 |
67 | type FormItemContextValue = {
68 | id: string
69 | }
70 |
71 | const FormItemContext = React.createContext(
72 | {} as FormItemContextValue
73 | )
74 |
75 | const FormItem = React.forwardRef<
76 | HTMLDivElement,
77 | React.HTMLAttributes
78 | >(({ className, ...props }, ref) => {
79 | const id = React.useId()
80 |
81 | return (
82 |
83 |
84 |
85 | )
86 | })
87 | FormItem.displayName = "FormItem"
88 |
89 | const FormLabel = React.forwardRef<
90 | React.ElementRef,
91 | React.ComponentPropsWithoutRef
92 | >(({ className, ...props }, ref) => {
93 | const { error, formItemId } = useFormField()
94 |
95 | return (
96 |
102 | )
103 | })
104 | FormLabel.displayName = "FormLabel"
105 |
106 | const FormControl = React.forwardRef<
107 | React.ElementRef,
108 | React.ComponentPropsWithoutRef
109 | >(({ ...props }, ref) => {
110 | const { error, formItemId, formDescriptionId, formMessageId } = useFormField()
111 |
112 | return (
113 |
124 | )
125 | })
126 | FormControl.displayName = "FormControl"
127 |
128 | const FormDescription = React.forwardRef<
129 | HTMLParagraphElement,
130 | React.HTMLAttributes
131 | >(({ className, ...props }, ref) => {
132 | const { formDescriptionId } = useFormField()
133 |
134 | return (
135 |
141 | )
142 | })
143 | FormDescription.displayName = "FormDescription"
144 |
145 | const FormMessage = React.forwardRef<
146 | HTMLParagraphElement,
147 | React.HTMLAttributes
148 | >(({ className, children, ...props }, ref) => {
149 | const { error, formMessageId } = useFormField()
150 | const body = error ? String(error?.message) : children
151 |
152 | if (!body) {
153 | return null
154 | }
155 |
156 | return (
157 |
163 | {body}
164 |
165 | )
166 | })
167 | FormMessage.displayName = "FormMessage"
168 |
169 | export {
170 | useFormField,
171 | Form,
172 | FormItem,
173 | FormLabel,
174 | FormControl,
175 | FormDescription,
176 | FormMessage,
177 | FormField,
178 | }
179 |
--------------------------------------------------------------------------------
/src/components/ui/select.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as SelectPrimitive from "@radix-ui/react-select"
5 | import { cn } from "@/lib/utils"
6 | import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "@radix-ui/react-icons"
7 |
8 | const Select = SelectPrimitive.Root
9 |
10 | const SelectGroup = SelectPrimitive.Group
11 |
12 | const SelectValue = SelectPrimitive.Value
13 |
14 | const SelectTrigger = React.forwardRef<
15 | React.ElementRef,
16 | React.ComponentPropsWithoutRef
17 | >(({ className, children, ...props }, ref) => (
18 | span]:line-clamp-1",
22 | className
23 | )}
24 | {...props}
25 | >
26 | {children}
27 |
28 |
29 |
30 |
31 | ))
32 | SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
33 |
34 | const SelectScrollUpButton = React.forwardRef<
35 | React.ElementRef,
36 | React.ComponentPropsWithoutRef
37 | >(({ className, ...props }, ref) => (
38 |
46 |
47 |
48 | ))
49 | SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName
50 |
51 | const SelectScrollDownButton = React.forwardRef<
52 | React.ElementRef,
53 | React.ComponentPropsWithoutRef
54 | >(({ className, ...props }, ref) => (
55 |
63 |
64 |
65 | ))
66 | SelectScrollDownButton.displayName =
67 | SelectPrimitive.ScrollDownButton.displayName
68 |
69 | const SelectContent = React.forwardRef<
70 | React.ElementRef,
71 | React.ComponentPropsWithoutRef
72 | >(({ className, children, position = "popper", ...props }, ref) => (
73 |
74 |
85 |
86 |
93 | {children}
94 |
95 |
96 |
97 |
98 | ))
99 | SelectContent.displayName = SelectPrimitive.Content.displayName
100 |
101 | const SelectLabel = React.forwardRef<
102 | React.ElementRef,
103 | React.ComponentPropsWithoutRef
104 | >(({ className, ...props }, ref) => (
105 |
110 | ))
111 | SelectLabel.displayName = SelectPrimitive.Label.displayName
112 |
113 | const SelectItem = React.forwardRef<
114 | React.ElementRef,
115 | React.ComponentPropsWithoutRef
116 | >(({ className, children, ...props }, ref) => (
117 |
125 |
126 |
127 |
128 |
129 |
130 | {children}
131 |
132 | ))
133 | SelectItem.displayName = SelectPrimitive.Item.displayName
134 |
135 | const SelectSeparator = React.forwardRef<
136 | React.ElementRef,
137 | React.ComponentPropsWithoutRef
138 | >(({ className, ...props }, ref) => (
139 |
144 | ))
145 | SelectSeparator.displayName = SelectPrimitive.Separator.displayName
146 |
147 | export {
148 | Select,
149 | SelectGroup,
150 | SelectValue,
151 | SelectTrigger,
152 | SelectContent,
153 | SelectLabel,
154 | SelectItem,
155 | SelectSeparator,
156 | SelectScrollUpButton,
157 | SelectScrollDownButton,
158 | }
159 |
--------------------------------------------------------------------------------
/src/components/auth/login-form.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 | import * as z from "zod";
3 | import { useForm } from "react-hook-form";
4 | import { zodResolver } from "@hookform/resolvers/zod"
5 | import Link from "next/link";;
6 | import { useSearchParams } from "next/navigation";
7 | import { CardWrapper } from './card-wrapper';
8 | import { LoginSchema } from "../../../schemas";
9 | import {
10 | Form,
11 | FormControl,
12 | FormField,
13 | FormItem,
14 | FormLabel,
15 | FormMessage,
16 | } from "@/components/ui/form";
17 | import { Input } from "@/components/ui/input";
18 | import { Button } from "@/components/ui/button";
19 | import { FormError } from "@/components/form-error";
20 | import { FormSuccess } from "@/components/form-success";
21 | import { Login } from "../../../actions/login";
22 | import { useState, useTransition } from "react";
23 |
24 |
25 |
26 |
27 | const LoginForm = () => {
28 | const searchParams = useSearchParams();
29 | const callbackUrl = searchParams.get("callbackUrl")
30 | const urlError = searchParams.get("error") === "OAuthAccountNotLinked" ? "Email already in use with different provider!" : "";
31 |
32 |
33 | const [showTwoFactor, setShowTwoFactor] = useState(false)
34 | const [error, setError] = useState("")
35 | const [success, setSuccess] = useState("")
36 | const [isPending, startTransition] = useTransition()
37 |
38 |
39 | const form = useForm>({
40 | resolver: zodResolver(LoginSchema),
41 | defaultValues: {
42 | email: "",
43 | password: "",
44 | },
45 | });
46 |
47 |
48 | const onSubmit = (values: z.infer) => {
49 | setError("");
50 | setSuccess("");
51 |
52 |
53 | startTransition(() => {
54 | Login(values, callbackUrl)
55 | .then((data) => {
56 | if (data?.error) {
57 | form.reset()
58 | setError(data.error)
59 | }
60 |
61 |
62 | if (data?.success) {
63 | form.reset()
64 | setSuccess(data.success)
65 | }
66 |
67 |
68 | if (data?.twoFactor) {
69 | setShowTwoFactor(true)
70 | }
71 | })
72 | .catch(() => setError("Something went wrong"))
73 | })
74 | }
75 |
76 |
77 | return (
78 |
84 |
154 |
155 |
156 | );
157 | };
158 |
159 |
160 | export default LoginForm;
--------------------------------------------------------------------------------
/src/components/ui/dropdown-menu.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
5 | import {
6 | CheckIcon,
7 | ChevronRightIcon,
8 | DotFilledIcon,
9 | } from "@radix-ui/react-icons"
10 |
11 | import { cn } from "@/lib/utils"
12 |
13 | const DropdownMenu = DropdownMenuPrimitive.Root
14 |
15 | const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
16 |
17 | const DropdownMenuGroup = DropdownMenuPrimitive.Group
18 |
19 | const DropdownMenuPortal = DropdownMenuPrimitive.Portal
20 |
21 | const DropdownMenuSub = DropdownMenuPrimitive.Sub
22 |
23 | const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup
24 |
25 | const DropdownMenuSubTrigger = React.forwardRef<
26 | React.ElementRef,
27 | React.ComponentPropsWithoutRef & {
28 | inset?: boolean
29 | }
30 | >(({ className, inset, children, ...props }, ref) => (
31 |
40 | {children}
41 |
42 |
43 | ))
44 | DropdownMenuSubTrigger.displayName =
45 | DropdownMenuPrimitive.SubTrigger.displayName
46 |
47 | const DropdownMenuSubContent = React.forwardRef<
48 | React.ElementRef,
49 | React.ComponentPropsWithoutRef
50 | >(({ className, ...props }, ref) => (
51 |
59 | ))
60 | DropdownMenuSubContent.displayName =
61 | DropdownMenuPrimitive.SubContent.displayName
62 |
63 | const DropdownMenuContent = React.forwardRef<
64 | React.ElementRef,
65 | React.ComponentPropsWithoutRef
66 | >(({ className, sideOffset = 4, ...props }, ref) => (
67 |
68 |
78 |
79 | ))
80 | DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
81 |
82 | const DropdownMenuItem = React.forwardRef<
83 | React.ElementRef,
84 | React.ComponentPropsWithoutRef & {
85 | inset?: boolean
86 | }
87 | >(({ className, inset, ...props }, ref) => (
88 | svg]:size-4 [&>svg]:shrink-0",
92 | inset && "pl-8",
93 | className
94 | )}
95 | {...props}
96 | />
97 | ))
98 | DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
99 |
100 | const DropdownMenuCheckboxItem = React.forwardRef<
101 | React.ElementRef,
102 | React.ComponentPropsWithoutRef
103 | >(({ className, children, checked, ...props }, ref) => (
104 |
113 |
114 |
115 |
116 |
117 |
118 | {children}
119 |
120 | ))
121 | DropdownMenuCheckboxItem.displayName =
122 | DropdownMenuPrimitive.CheckboxItem.displayName
123 |
124 | const DropdownMenuRadioItem = React.forwardRef<
125 | React.ElementRef,
126 | React.ComponentPropsWithoutRef
127 | >(({ className, children, ...props }, ref) => (
128 |
136 |
137 |
138 |
139 |
140 |
141 | {children}
142 |
143 | ))
144 | DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName
145 |
146 | const DropdownMenuLabel = React.forwardRef<
147 | React.ElementRef,
148 | React.ComponentPropsWithoutRef & {
149 | inset?: boolean
150 | }
151 | >(({ className, inset, ...props }, ref) => (
152 |
161 | ))
162 | DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
163 |
164 | const DropdownMenuSeparator = React.forwardRef<
165 | React.ElementRef,
166 | React.ComponentPropsWithoutRef
167 | >(({ className, ...props }, ref) => (
168 |
173 | ))
174 | DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
175 |
176 | const DropdownMenuShortcut = ({
177 | className,
178 | ...props
179 | }: React.HTMLAttributes) => {
180 | return (
181 |
185 | )
186 | }
187 | DropdownMenuShortcut.displayName = "DropdownMenuShortcut"
188 |
189 | export {
190 | DropdownMenu,
191 | DropdownMenuTrigger,
192 | DropdownMenuContent,
193 | DropdownMenuItem,
194 | DropdownMenuCheckboxItem,
195 | DropdownMenuRadioItem,
196 | DropdownMenuLabel,
197 | DropdownMenuSeparator,
198 | DropdownMenuShortcut,
199 | DropdownMenuGroup,
200 | DropdownMenuPortal,
201 | DropdownMenuSub,
202 | DropdownMenuSubContent,
203 | DropdownMenuSubTrigger,
204 | DropdownMenuRadioGroup,
205 | }
206 |
--------------------------------------------------------------------------------
/src/app/(protected)/settings/page.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as z from "zod"
4 | import { useForm } from "react-hook-form"
5 | import { zodResolver } from "@hookform/resolvers/zod"
6 | import { useState, useTransition } from "react"
7 | import { useSession } from "next-auth/react"
8 |
9 | import { SettingsSchema } from "../../../../schemas"
10 | import {
11 | Card,
12 | CardHeader,
13 | CardContent
14 | } from "@/components/ui/card"
15 | import { Button } from "@/components/ui/button"
16 | import { settings } from "../../../../actions/setting"
17 | import {
18 | Form,
19 | FormField,
20 | FormControl,
21 | FormItem,
22 | FormLabel,
23 | FormDescription,
24 | FormMessage
25 | } from "@/components/ui/form"
26 | import { Input } from "@/components/ui/input"
27 | import { useCurrentUser } from "../../../../hooks/use-current-user"
28 | import { FormSuccess } from "@/components/form-success"
29 | import { FormError } from "@/components/form-error"
30 | import {
31 | Select,
32 | SelectContent,
33 | SelectItem,
34 | SelectTrigger,
35 | SelectValue
36 | } from "@/components/ui/select"
37 | import { UserRole } from "@prisma/client"
38 | import { Switch } from "@/components/ui/switch"
39 |
40 | const SettingsPage = () => {
41 | const user = useCurrentUser()
42 | const [error, setError] = useState();
43 | const [success, setSuccess] = useState();
44 |
45 | const { update } = useSession();
46 | const [isPending, startTransition] = useTransition();
47 |
48 | const form = useForm>({
49 | resolver: zodResolver(SettingsSchema),
50 | defaultValues: {
51 | password: undefined,
52 | name: user?.name || undefined,
53 | email: user?.email || undefined,
54 | role: user?.role || undefined,
55 | isTwoFactorEnabled: user?.isTwoFactorEnabled || undefined
56 | }
57 | });
58 |
59 | const onSubmit = (values: z.infer) => {
60 | startTransition(() => {
61 | settings(values)
62 | .then((data) => {
63 | if (data.error) {
64 | setError(data.error)
65 | }
66 | if (data.success) {
67 | update()
68 | setSuccess(data.success)
69 | }
70 | })
71 | .catch(() => setError("Somethings went wrong!"))
72 | });
73 |
74 | }
75 |
76 | return (
77 |
78 |
79 |
80 | ⚙ Settings
81 |
82 |
83 |
84 |
208 |
209 |
210 |
211 | )
212 | }
213 | export default SettingsPage
214 |
--------------------------------------------------------------------------------