├── .eslintrc.json
├── app
├── api
│ ├── auth
│ │ └── [...nextauth]
│ │ │ └── route.ts
│ └── admin
│ │ └── route.ts
├── favicon.ico
├── auth
│ ├── login
│ │ └── page.tsx
│ ├── reset
│ │ └── page.tsx
│ ├── error
│ │ └── 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
├── layout.tsx
├── page.tsx
└── globals.css
├── next.config.js
├── postcss.config.js
├── actions
├── logout.ts
├── admin.ts
├── reset.ts
├── new-verification.ts
├── register.ts
├── new-password.ts
├── settings.ts
└── login.ts
├── hooks
├── use-current-user.ts
└── use-current-role.ts
├── lib
├── utils.ts
├── db.ts
├── auth.ts
├── mail.ts
└── token.ts
├── data
├── account.ts
├── two-factor-confirmation.ts
├── user.ts
├── two-factor-token.ts
├── verification-token.ts
└── password-reset-token.ts
├── next-auth.d.ts
├── components.json
├── components
├── auth
│ ├── logout-button.tsx
│ ├── back-button.tsx
│ ├── error-card.tsx
│ ├── header.tsx
│ ├── role-gate.tsx
│ ├── login-button.tsx
│ ├── card-wrapper.tsx
│ ├── social.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
├── .gitignore
├── public
├── vercel.svg
└── next.svg
├── tsconfig.json
├── routes.ts
├── auth.config.ts
├── middleware.ts
├── README.md
├── package.json
├── schemas
└── index.ts
├── prisma
└── schema.prisma
├── tailwind.config.ts
└── auth.ts
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "next/core-web-vitals"
3 | }
4 |
--------------------------------------------------------------------------------
/app/api/auth/[...nextauth]/route.ts:
--------------------------------------------------------------------------------
1 | export { GET, POST } from "@/auth";
2 |
3 |
--------------------------------------------------------------------------------
/app/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hafisn07/next-auth-v5-advanced-guide-2024/HEAD/app/favicon.ico
--------------------------------------------------------------------------------
/next.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('next').NextConfig} */
2 | const nextConfig = {}
3 |
4 | module.exports = nextConfig
5 |
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | }
7 |
--------------------------------------------------------------------------------
/actions/logout.ts:
--------------------------------------------------------------------------------
1 | "use server";
2 |
3 | import { signOut } from "@/auth";
4 |
5 | export const logout = async () => {
6 | await signOut();
7 | };
8 |
--------------------------------------------------------------------------------
/hooks/use-current-user.ts:
--------------------------------------------------------------------------------
1 | import { useSession } from "next-auth/react";
2 |
3 | export const useCurrentUser = () => {
4 | const session = useSession();
5 |
6 | return session.data?.user;
7 | };
8 |
--------------------------------------------------------------------------------
/lib/utils.ts:
--------------------------------------------------------------------------------
1 | import { type ClassValue, clsx } from "clsx"
2 | import { twMerge } from "tailwind-merge"
3 |
4 | export function cn(...inputs: ClassValue[]) {
5 | return twMerge(clsx(inputs))
6 | }
7 |
--------------------------------------------------------------------------------
/hooks/use-current-role.ts:
--------------------------------------------------------------------------------
1 | import { useSession } from "next-auth/react";
2 |
3 | export const useCurrentRole = () => {
4 | const session = useSession();
5 |
6 | return session.data?.user?.role;
7 | };
8 |
--------------------------------------------------------------------------------
/app/auth/login/page.tsx:
--------------------------------------------------------------------------------
1 | import { LoginForm } from "@/components/auth/login-form";
2 |
3 | const LoginPage = () => {
4 | return (
5 |
6 | );
7 | };
8 |
9 | export default LoginPage;
10 |
--------------------------------------------------------------------------------
/app/auth/reset/page.tsx:
--------------------------------------------------------------------------------
1 | import { ResetForm } from "@/components/auth/reset-form";
2 |
3 | const ResetPage = () => {
4 | return (
5 |
6 | );
7 | };
8 |
9 | export default ResetPage;
10 |
--------------------------------------------------------------------------------
/app/auth/error/page.tsx:
--------------------------------------------------------------------------------
1 | import { ErrorCard } from "@/components/auth/error-card";
2 |
3 | const AuthErrorPage = () => {
4 | return (
5 |
6 | );
7 | };
8 |
9 | export default AuthErrorPage;
10 |
--------------------------------------------------------------------------------
/app/auth/register/page.tsx:
--------------------------------------------------------------------------------
1 | import { RegisterForm } from "@/components/auth/register-form";
2 |
3 | const RegisterPage = () => {
4 | return (
5 |
6 | );
7 | };
8 |
9 | export default RegisterPage;
10 |
--------------------------------------------------------------------------------
/app/auth/new-password/page.tsx:
--------------------------------------------------------------------------------
1 | import { NewPasswordForm } from "@/components/auth/new-password-form";
2 |
3 | const NewPasswordPage = () => {
4 | return (
5 |
6 | );
7 | }
8 |
9 | export default NewPasswordPage;
--------------------------------------------------------------------------------
/app/auth/new-verification/page.tsx:
--------------------------------------------------------------------------------
1 | import { NewVerificationForm } from "@/components/auth/new-verification-form";
2 |
3 | const NewVerificationPage = () => {
4 | return ;
5 | };
6 |
7 | export default NewVerificationPage;
8 |
--------------------------------------------------------------------------------
/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;
10 |
--------------------------------------------------------------------------------
/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 | };
14 |
--------------------------------------------------------------------------------
/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 |
9 | return account;
10 | } catch {
11 | return null;
12 | }
13 | };
14 |
--------------------------------------------------------------------------------
/app/(protected)/server/page.tsx:
--------------------------------------------------------------------------------
1 | import { currentUser } from "@/lib/auth";
2 | import { UserInfo } from "@/components/user-info";
3 |
4 | const ServerPage = async () => {
5 | const user = await currentUser();
6 |
7 | return ;
8 | };
9 |
10 | export default ServerPage;
11 |
--------------------------------------------------------------------------------
/app/auth/layout.tsx:
--------------------------------------------------------------------------------
1 | const AuthLayout = ({ children }: { children: React.ReactNode }) => {
2 | return (
3 |
4 | {children}
5 |
6 | );
7 | };
8 |
9 | export default AuthLayout;
10 |
--------------------------------------------------------------------------------
/app/(protected)/client/page.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useCurrentUser } from "@/hooks/use-current-user";
4 | import { UserInfo } from "@/components/user-info";
5 |
6 | const ClientPage = () => {
7 | const user = useCurrentUser();
8 |
9 | return ;
10 | };
11 |
12 | export default ClientPage;
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 |
9 | return twoFactorConfirmation;
10 | } catch {
11 | return null;
12 | }
13 | };
14 |
--------------------------------------------------------------------------------
/next-auth.d.ts:
--------------------------------------------------------------------------------
1 | import { UserRole } from "@prisma/client";
2 | import NextAuth, { type DefaultSession } from "next-auth";
3 |
4 | export type ExtendedUser = DefaultSession["user"] & {
5 | role: UserRole;
6 | isTwoFactorEnabled: boolean;
7 | isOAuth: boolean;
8 | };
9 |
10 | declare module "next-auth" {
11 | interface Session {
12 | user: ExtendedUser;
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/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 |
13 | return { error: "Forbidden Server Action!" };
14 | };
15 |
--------------------------------------------------------------------------------
/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 | }
14 |
--------------------------------------------------------------------------------
/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": "app/globals.css",
9 | "baseColor": "slate",
10 | "cssVariables": true,
11 | "prefix": ""
12 | },
13 | "aliases": {
14 | "components": "@/components",
15 | "utils": "@/lib/utils"
16 | }
17 | }
--------------------------------------------------------------------------------
/components/auth/logout-button.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { logout } from "@/actions/logout";
4 |
5 | interface LogoutButtonProps {
6 | children?: React.ReactNode;
7 | }
8 |
9 | export const LogoutButton = ({ children }: LogoutButtonProps) => {
10 | const onClick = () => {
11 | logout();
12 | };
13 |
14 | return (
15 |
16 | {children}
17 |
18 | );
19 | };
20 |
--------------------------------------------------------------------------------
/components/auth/back-button.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import Link from "next/link";
4 |
5 | import { Button } from "@/components/ui/button";
6 |
7 | interface BackButtonProps {
8 | href: string;
9 | label: string;
10 | }
11 |
12 | export const BackButton = ({ href, label }: BackButtonProps) => {
13 | return (
14 |
17 | );
18 | };
19 |
--------------------------------------------------------------------------------
/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 | };
22 |
--------------------------------------------------------------------------------
/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 = ({ message }: FormSuccessProps) => {
8 | if (!message) return null;
9 |
10 | return (
11 |
15 | );
16 | };
17 |
--------------------------------------------------------------------------------
/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 = ({ message }: FormErrorProps) => {
8 | if (!message) return null;
9 |
10 | return (
11 |
15 | );
16 | };
17 |
--------------------------------------------------------------------------------
/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;
17 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 | .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 |
--------------------------------------------------------------------------------
/components/auth/error-card.tsx:
--------------------------------------------------------------------------------
1 | import { ExclamationTriangleIcon } from "@radix-ui/react-icons";
2 | import { CardWrapper } from "@/components/auth/card-wrapper";
3 |
4 | export const ErrorCard = () => {
5 | return (
6 |
11 |
12 |
13 |
14 |
15 | );
16 | };
17 |
--------------------------------------------------------------------------------
/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
18 |
{label}
19 |
20 | );
21 | };
22 |
--------------------------------------------------------------------------------
/public/vercel.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/data/two-factor-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 |
9 | return twoFactorToken;
10 | } catch {
11 | return null;
12 | }
13 | };
14 |
15 | export const getTwoFactorTokenByEmail = async (email: string) => {
16 | try {
17 | const twoFactorToken = await db.twoFactorToken.findFirst({
18 | where: { email },
19 | });
20 |
21 | return twoFactorToken;
22 | } catch {
23 | return null;
24 | }
25 | };
26 |
--------------------------------------------------------------------------------
/components/auth/role-gate.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { UserRole } from "@prisma/client";
4 |
5 | import { useCurrentRole } from "@/hooks/use-current-role";
6 | import { FormError } from "@/components/form-error";
7 |
8 | interface RoleGateProps {
9 | children: React.ReactNode;
10 | allowedRole: UserRole;
11 | }
12 |
13 | export const RoleGate = ({ children, allowedRole }: RoleGateProps) => {
14 | const role = useCurrentRole();
15 |
16 | if (role !== allowedRole) {
17 | return (
18 |
19 | );
20 | }
21 |
22 | return <>{children}>;
23 | };
24 |
--------------------------------------------------------------------------------
/data/verification-token.ts:
--------------------------------------------------------------------------------
1 | import { db } from "@/lib/db";
2 |
3 | export const getVerificationTokenByToken = async (token: string) => {
4 | try {
5 | const verificationToken = await db.verificationToken.findUnique({
6 | where: { token },
7 | });
8 |
9 | return verificationToken;
10 | } catch {
11 | return null;
12 | }
13 | };
14 |
15 | export const getVerificationTokenByEmail = async (email: string) => {
16 | try {
17 | const verificationToken = await db.verificationToken.findFirst({
18 | where: { email },
19 | });
20 |
21 | return verificationToken;
22 | } catch {
23 | return null;
24 | }
25 | };
26 |
--------------------------------------------------------------------------------
/data/password-reset-token.ts:
--------------------------------------------------------------------------------
1 | import { db } from "@/lib/db";
2 |
3 | export const getPasswordResetTokenByToken = async (token: string) => {
4 | try {
5 | const passwordResetToken = await db.passwordResetToken.findUnique({
6 | where: { token },
7 | });
8 |
9 | return passwordResetToken;
10 | } catch {
11 | return null;
12 | }
13 | };
14 |
15 | export const getPasswordResetokenByEmail = async (email: string) => {
16 | try {
17 | const passwordResetToken = await db.passwordResetToken.findFirst({
18 | where: { email },
19 | });
20 |
21 | return passwordResetToken;
22 | } catch {
23 | return null;
24 | }
25 | };
26 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": ["dom", "dom.iterable", "esnext"],
5 | "allowJs": true,
6 | "skipLibCheck": true,
7 | "strict": true,
8 | "noEmit": true,
9 | "esModuleInterop": true,
10 | "module": "esnext",
11 | "moduleResolution": "bundler",
12 | "resolveJsonModule": true,
13 | "isolatedModules": true,
14 | "jsx": "preserve",
15 | "incremental": true,
16 | "plugins": [
17 | {
18 | "name": "next"
19 | }
20 | ],
21 | "paths": {
22 | "@/*": ["./*"]
23 | }
24 | },
25 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
26 | "exclude": ["node_modules"]
27 | }
28 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/app/layout.tsx:
--------------------------------------------------------------------------------
1 | import type { Metadata } from "next";
2 | import { Inter } from "next/font/google";
3 | import "./globals.css";
4 | import { auth } from "@/auth";
5 | import { SessionProvider } from "next-auth/react";
6 | import { Toaster } from "@/components/ui/sonner";
7 |
8 | const inter = Inter({ subsets: ["latin"] });
9 |
10 | export const metadata: Metadata = {
11 | title: "Create Next App",
12 | description: "Generated by create next app",
13 | };
14 |
15 | export default async function RootLayout({
16 | children,
17 | }: {
18 | children: React.ReactNode;
19 | }) {
20 | const session = await auth();
21 |
22 | return (
23 |
24 |
25 |
26 |
27 | {children}
28 |
29 |
30 |
31 | );
32 | }
33 |
--------------------------------------------------------------------------------
/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 = ["/", "/auth/new-verification"];
7 |
8 | /**
9 | * An array of routes that are used for authentication.
10 | * These routes will redirect logged in users to /settings.
11 | * @type {string[]}
12 | */
13 | export const authRoutes = [
14 | "/auth/login",
15 | "/auth/register",
16 | "/auth/error",
17 | "/auth/reset",
18 | "/auth/new-password",
19 | ];
20 |
21 | /**
22 | * The prefix for api authentication routes.
23 | * Routes that starts with this prefix are used for API authentication purposes.
24 | * @type {string}
25 | */
26 | export const apiAuthPrefix = "/api/auth";
27 |
28 | /**
29 | * The default redirect path after a successful login.
30 | * @type {string}
31 | */
32 | export const DEFAULT_LOGIN_REDIRECT = "/settings";
33 |
--------------------------------------------------------------------------------
/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/token";
9 |
10 | export const reset = async (values: z.infer) => {
11 | const validateFields = ResetSchema.safeParse(values);
12 |
13 | if (!validateFields.success) {
14 | return { error: "Invalid email!" };
15 | }
16 |
17 | const { email } = validateFields.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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/components/auth/login-button.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useRouter } from "next/navigation";
4 |
5 | import { Dialog, DialogContent, DialogTrigger } from "@/components/ui/dialog";
6 | import { LoginForm } from "@/components/auth/login-form";
7 |
8 | interface LoginButtonProps {
9 | children: React.ReactNode;
10 | mode?: "modal" | "redirect";
11 | asChild?: boolean;
12 | }
13 |
14 | export const LoginButton = ({
15 | children,
16 | mode = "redirect",
17 | asChild,
18 | }: LoginButtonProps) => {
19 | const router = useRouter();
20 |
21 | const onClick = () => {
22 | router.push("/auth/login");
23 | };
24 |
25 | if (mode === "modal") {
26 | return (
27 |
33 | );
34 | }
35 |
36 | return (
37 |
38 | {children}
39 |
40 | );
41 | };
42 |
--------------------------------------------------------------------------------
/actions/new-verification.ts:
--------------------------------------------------------------------------------
1 | "use server";
2 |
3 | import { getUserByEmail } from "@/data/user";
4 | import { getVerificationTokenByToken } from "@/data/verification-token";
5 | import { db } from "@/lib/db";
6 |
7 | export const newVerification = async (token: string) => {
8 | const existingToken = await getVerificationTokenByToken(token);
9 |
10 | if (!existingToken) {
11 | return { error: "Token does not exists!" };
12 | }
13 |
14 | const hasExpired = new Date(existingToken.expires) < new Date();
15 |
16 | if (hasExpired) {
17 | return { error: "Token expired!" };
18 | }
19 |
20 | const existingUser = await getUserByEmail(existingToken.email);
21 |
22 | if (!existingUser) {
23 | return { error: "Email does not exists!" };
24 | }
25 |
26 | await db.user.update({
27 | where: { id: existingUser.id },
28 | data: {
29 | emailVerified: new Date(),
30 | email: existingToken.email,
31 | },
32 | });
33 |
34 | await db.verificationToken.delete({
35 | where: { id: existingToken.id },
36 | });
37 |
38 | return { success: "Email verified!" };
39 | };
40 |
--------------------------------------------------------------------------------
/app/page.tsx:
--------------------------------------------------------------------------------
1 | import { Poppins } from "next/font/google";
2 | import { cn } from "@/lib/utils";
3 |
4 | import { Button } from "@/components/ui/button";
5 | import { LoginButton } from "@/components/auth/login-button";
6 |
7 | const font = Poppins({
8 | subsets: ["latin"],
9 | weight: ["600"],
10 | });
11 |
12 | export default function Home() {
13 | return (
14 |
15 |
16 |
22 | 🔐 Auth
23 |
24 |
A simple authentication service!
25 |
26 |
27 |
30 |
31 |
32 |
33 |
34 | );
35 | }
36 |
--------------------------------------------------------------------------------
/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 | {children}
34 | {showSocial && (
35 |
36 |
37 |
38 | )}
39 |
40 |
41 |
42 |
43 | );
44 | };
45 |
--------------------------------------------------------------------------------
/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 { useSearchParams } from "next/navigation";
7 |
8 | import { Button } from "@/components/ui/button";
9 | import { DEFAULT_LOGIN_REDIRECT } from "@/routes";
10 |
11 | export const Social = () => {
12 | const searchParams = useSearchParams();
13 | const callbackUrl = searchParams.get("callbackUrl");
14 |
15 | const onClick = (provider: "google" | "github") => {
16 | signIn(provider, {
17 | callbackUrl: callbackUrl || DEFAULT_LOGIN_REDIRECT,
18 | });
19 | };
20 |
21 | return (
22 |
23 |
31 |
39 |
40 | );
41 | };
42 |
--------------------------------------------------------------------------------
/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_PUBLIC_APP_URL;
6 |
7 | export const sendTwoFactorTokenEmail = async (email: string, token: string) => {
8 | await resend.emails.send({
9 | from: "onboarding@resend.dev",
10 | to: email,
11 | subject: "2FA code",
12 | html: `Your 2FA code is ${token}
`,
13 | });
14 | };
15 |
16 | export const sendPasswordResetEmail = async (email: string, token: string) => {
17 | const resetLink = `${domain}/auth/new-password?token=${token}`;
18 |
19 | await resend.emails.send({
20 | from: "onboarding@resend.dev",
21 | to: email,
22 | subject: "Reset your password",
23 | html: `Click here to reset your password.
`,
24 | });
25 | };
26 |
27 | export const sendVerificationEmail = async (email: string, token: string) => {
28 | const confirmLink = `${domain}/auth/new-verification?token=${token}`;
29 |
30 | await resend.emails.send({
31 | from: "onboarding@resend.dev",
32 | to: email,
33 | subject: "Confirm your email",
34 | html: `Click here to confirm email.
`,
35 | });
36 | };
37 |
--------------------------------------------------------------------------------
/actions/register.ts:
--------------------------------------------------------------------------------
1 | "use server";
2 |
3 | import * as z from "zod";
4 | import bcrypt from "bcryptjs";
5 | import { db } from "@/lib/db";
6 | import { RegisterSchema } from "@/schemas";
7 | import { getUserByEmail } from "@/data/user";
8 | import { generateVerificationToken } from "@/lib/token";
9 | import { sendVerificationEmail } from "@/lib/mail";
10 |
11 | export const register = async (values: z.infer) => {
12 | const validatedFields = RegisterSchema.safeParse(values);
13 |
14 | if (!validatedFields.success) {
15 | return { error: "Invalid fields" };
16 | }
17 |
18 | const { email, password, name } = validatedFields.data;
19 | const hashedPassword = await bcrypt.hash(password, 10);
20 |
21 | const existingUser = await getUserByEmail(email);
22 |
23 | if (existingUser) {
24 | return { error: "Email already in use!" };
25 | }
26 |
27 | await db.user.create({
28 | data: {
29 | name,
30 | email,
31 | password: hashedPassword,
32 | },
33 | });
34 |
35 | const verificationToken = await generateVerificationToken(email);
36 | await sendVerificationEmail(verificationToken.email, verificationToken.token);
37 |
38 | return { success: "Confirmation email sent!" };
39 | };
40 |
--------------------------------------------------------------------------------
/components/auth/user-button.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { FaUser } from "react-icons/fa";
4 | import { ExitIcon } from "@radix-ui/react-icons";
5 |
6 | import {
7 | DropdownMenu,
8 | DropdownMenuContent,
9 | DropdownMenuItem,
10 | DropdownMenuTrigger,
11 | } from "@/components/ui/dropdown-menu";
12 | import { Avatar, AvatarImage, AvatarFallback } from "@/components/ui/avatar";
13 | import { useCurrentUser } from "@/hooks/use-current-user";
14 | import { LogoutButton } from "@/components/auth/logout-button";
15 |
16 | export const UserButton = () => {
17 | const user = useCurrentUser();
18 |
19 | return (
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 | Logout
34 |
35 |
36 |
37 |
38 | );
39 | };
40 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 Github from "next-auth/providers/github";
5 | import Google from "next-auth/providers/google";
6 |
7 | import { LoginSchema } from "@/schemas";
8 | import { getUserByEmail } from "@/data/user";
9 |
10 | export default {
11 | providers: [
12 | Google({
13 | clientId: process.env.GOOGLE_CLIENT_ID,
14 | clientSecret: process.env.GOOGLE_CLIENT_SECRET,
15 | }),
16 | Github({
17 | clientId: process.env.GITHUB_CLIENT_ID,
18 | clientSecret: process.env.GITHUB_CLIENT_SECRET,
19 | }),
20 | Credentials({
21 | async authorize(credentials) {
22 | const validatedFields = LoginSchema.safeParse(credentials);
23 |
24 | if (validatedFields.success) {
25 | const { email, password } = validatedFields.data;
26 |
27 | const user = await getUserByEmail(email);
28 | if (!user || !user.password) return null;
29 |
30 | const passwordsMatch = await bcrypt.compare(password, user.password);
31 |
32 | if (passwordsMatch) return user;
33 | }
34 |
35 | return null;
36 | },
37 | }),
38 | ],
39 | } satisfies NextAuthConfig;
40 |
--------------------------------------------------------------------------------
/app/(protected)/_components/navbar.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import Link from "next/link";
4 | import { usePathname } from "next/navigation";
5 |
6 | import { Button } from "@/components/ui/button";
7 | import { UserButton } from "@/components/auth/user-button";
8 |
9 | export const Navbar = () => {
10 | const pathname = usePathname();
11 |
12 | return (
13 |
39 | );
40 | };
41 |
--------------------------------------------------------------------------------
/public/next.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/middleware.ts:
--------------------------------------------------------------------------------
1 | import NextAuth from "next-auth";
2 |
3 | import authConfig from "@/auth.config";
4 | import {
5 | DEFAULT_LOGIN_REDIRECT,
6 | apiAuthPrefix,
7 | authRoutes,
8 | publicRoutes,
9 | } from "@/routes";
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 | if (isApiAuthRoute) {
22 | return null;
23 | }
24 |
25 | if (isAuthRoute) {
26 | if (isLoggedIn) {
27 | return Response.redirect(new URL(DEFAULT_LOGIN_REDIRECT, nextUrl));
28 | }
29 |
30 | return null;
31 | }
32 |
33 | if (!isLoggedIn && !isPublicRoute) {
34 | let callbackUrl = nextUrl.pathname;
35 | if (nextUrl.search) {
36 | callbackUrl += nextUrl.search;
37 | }
38 |
39 | const encodedCallbackUrl = encodeURIComponent(callbackUrl);
40 |
41 | return Response.redirect(
42 | new URL(`/auth/login?callbackUrl=${encodedCallbackUrl}`, nextUrl)
43 | );
44 | }
45 |
46 | return null;
47 | });
48 |
49 | // Optionally, don't invoke Middleware on some paths
50 | export const config = {
51 | matcher: ["/((?!.+\\.[\\w]+$|_next).*)", "/", "/(api|trpc)(.*)"],
52 | };
53 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).
2 |
3 | ## Getting Started
4 |
5 | First, run the development server:
6 |
7 | ```bash
8 | npm run dev
9 | # or
10 | yarn dev
11 | # or
12 | pnpm dev
13 | # or
14 | bun dev
15 | ```
16 |
17 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
18 |
19 | You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
20 |
21 | This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font.
22 |
23 | ## Learn More
24 |
25 | To learn more about Next.js, take a look at the following resources:
26 |
27 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
28 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
29 |
30 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
31 |
32 | ## Deploy on Vercel
33 |
34 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
35 |
36 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.
37 |
--------------------------------------------------------------------------------
/actions/new-password.ts:
--------------------------------------------------------------------------------
1 | "use server";
2 |
3 | import * as z from "zod";
4 | import bcrypt from "bcryptjs";
5 |
6 | import { NewPasswordSchema } from "@/schemas";
7 | import { getPasswordResetTokenByToken } from "@/data/password-reset-token";
8 | import { getUserByEmail } from "@/data/user";
9 | import { db } from "@/lib/db";
10 |
11 | export const newPassword = async (
12 | values: z.infer,
13 | token: string | null
14 | ) => {
15 | if (!token) {
16 | return { error: "Missing token!" };
17 | }
18 |
19 | const validateFields = NewPasswordSchema.safeParse(values);
20 |
21 | if (!validateFields.success) {
22 | return { error: "Invalid fields!" };
23 | }
24 |
25 | const { password } = validateFields.data;
26 |
27 | const existingToken = await getPasswordResetTokenByToken(token);
28 |
29 | if (!existingToken) {
30 | return { error: "Invalid token!" };
31 | }
32 |
33 | const hasExpired = new Date(existingToken.expires) < new Date();
34 |
35 | if (hasExpired) {
36 | return { error: "Token expired!" };
37 | }
38 |
39 | const existingUser = await getUserByEmail(existingToken.email);
40 |
41 | if (!existingUser) {
42 | return { error: "Email not found!" };
43 | }
44 |
45 | const hashedPassword = await bcrypt.hash(password, 10);
46 |
47 | await db.user.update({
48 | where: { id: existingUser.id },
49 | data: { password: hashedPassword },
50 | });
51 |
52 | await db.passwordResetToken.delete({
53 | where: { id: existingToken.id },
54 | });
55 |
56 | return { success: "Password updated!" };
57 | };
58 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/components/auth/new-verification-form.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useCallback, useEffect, useState } from "react";
4 | import { BeatLoader } from "react-spinners";
5 | import { useSearchParams } from "next/navigation";
6 |
7 | import { newVerification } from "@/actions/new-verification";
8 | import { CardWrapper } from "@/components/auth/card-wrapper";
9 | import { FormError } from "@/components/form-error";
10 | import { FormSuccess } from "@/components/form-success";
11 |
12 | export const NewVerificationForm = () => {
13 | const [error, setError] = useState("");
14 | const [success, setSuccess] = useState("");
15 |
16 | const searchParams = useSearchParams();
17 |
18 | const token = searchParams.get("token");
19 |
20 | const onSubmit = useCallback(() => {
21 | if (success || error) return;
22 |
23 | if (!token) {
24 | setError("Missing token!");
25 |
26 | return;
27 | }
28 |
29 | newVerification(token)
30 | .then((data) => {
31 | setSuccess(data.success);
32 | setError(data.error);
33 | })
34 | .catch(() => {
35 | setError("Something went wrong!");
36 | });
37 | }, [token, success, error]);
38 |
39 | useEffect(() => {
40 | onSubmit();
41 | }, [onSubmit]);
42 |
43 | return (
44 |
49 |
50 | {!success && !error && }
51 |
52 | {!success && }
53 |
54 |
55 | );
56 | };
57 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "next-auth-v5-advanced-guide-2024",
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": "^1.0.14",
14 | "@hookform/resolvers": "^3.3.4",
15 | "@prisma/client": "^5.8.0",
16 | "@radix-ui/react-avatar": "^1.0.4",
17 | "@radix-ui/react-dialog": "^1.0.5",
18 | "@radix-ui/react-dropdown-menu": "^2.0.6",
19 | "@radix-ui/react-icons": "^1.3.0",
20 | "@radix-ui/react-label": "^2.0.2",
21 | "@radix-ui/react-select": "^2.0.0",
22 | "@radix-ui/react-slot": "^1.0.2",
23 | "@radix-ui/react-switch": "^1.0.3",
24 | "bcrypt": "^5.1.1",
25 | "bcryptjs": "^2.4.3",
26 | "class-variance-authority": "^0.7.0",
27 | "clsx": "^2.1.0",
28 | "next": "14.0.4",
29 | "next-auth": "^5.0.0-beta.4",
30 | "next-themes": "^0.2.1",
31 | "react": "^18",
32 | "react-dom": "^18",
33 | "react-hook-form": "^7.49.2",
34 | "react-icons": "^4.12.0",
35 | "react-spinners": "^0.13.8",
36 | "resend": "^2.1.0",
37 | "sonner": "^1.3.1",
38 | "tailwind-merge": "^2.2.0",
39 | "tailwindcss-animate": "^1.0.7",
40 | "uuid": "^9.0.1",
41 | "zod": "^3.22.4"
42 | },
43 | "devDependencies": {
44 | "@types/bcrypt": "^5.0.2",
45 | "@types/bcryptjs": "^2.4.6",
46 | "@types/node": "^20",
47 | "@types/react": "^18",
48 | "@types/react-dom": "^18",
49 | "@types/uuid": "^9.0.7",
50 | "autoprefixer": "^10.0.1",
51 | "eslint": "^8",
52 | "eslint-config-next": "14.0.4",
53 | "postcss": "^8",
54 | "prisma": "^5.8.0",
55 | "tailwindcss": "^3.3.0",
56 | "typescript": "^5"
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/schemas/index.ts:
--------------------------------------------------------------------------------
1 | import * as z from "zod";
2 | import { UserRole } from "@prisma/client";
3 |
4 | export const SettingsSchema = z
5 | .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(
14 | (data) => {
15 | if (data.password && !data.newPassword) {
16 | return false;
17 | }
18 |
19 | return true;
20 | },
21 | {
22 | message: "New password is required!",
23 | path: ["newPassword"],
24 | }
25 | )
26 | .refine(
27 | (data) => {
28 | if (data.newPassword && !data.password) {
29 | return false;
30 | }
31 |
32 | return true;
33 | },
34 | {
35 | message: "Password is required!",
36 | path: ["password"],
37 | }
38 | );
39 |
40 | export const NewPasswordSchema = z.object({
41 | password: z.string().min(6, {
42 | message: "Minimum 6 characters required",
43 | }),
44 | });
45 |
46 | export const ResetSchema = z.object({
47 | email: z.string().email({
48 | message: "Email is required",
49 | }),
50 | });
51 |
52 | export const LoginSchema = z.object({
53 | email: z.string().email({
54 | message: "Email is required",
55 | }),
56 | password: z.string().min(1, {
57 | message: "Password is required",
58 | }),
59 | code: z.optional(z.string()),
60 | });
61 |
62 | export const RegisterSchema = z.object({
63 | email: z.string().email({
64 | message: "Email is required",
65 | }),
66 | password: z.string().min(6, {
67 | message: "Minimum 6 characters required",
68 | }),
69 | name: z.string().min(1, {
70 | message: "Name is required",
71 | }),
72 | });
73 |
--------------------------------------------------------------------------------
/app/globals.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | html,
6 | body,
7 | :root {
8 | height: 100%;
9 | }
10 |
11 | @layer base {
12 | :root {
13 | --background: 0 0% 100%;
14 | --foreground: 222.2 84% 4.9%;
15 |
16 | --card: 0 0% 100%;
17 | --card-foreground: 222.2 84% 4.9%;
18 |
19 | --popover: 0 0% 100%;
20 | --popover-foreground: 222.2 84% 4.9%;
21 |
22 | --primary: 222.2 47.4% 11.2%;
23 | --primary-foreground: 210 40% 98%;
24 |
25 | --secondary: 210 40% 96.1%;
26 | --secondary-foreground: 222.2 47.4% 11.2%;
27 |
28 | --muted: 210 40% 96.1%;
29 | --muted-foreground: 215.4 16.3% 46.9%;
30 |
31 | --accent: 210 40% 96.1%;
32 | --accent-foreground: 222.2 47.4% 11.2%;
33 |
34 | --destructive: 0 84.2% 60.2%;
35 | --destructive-foreground: 210 40% 98%;
36 |
37 | --border: 214.3 31.8% 91.4%;
38 | --input: 214.3 31.8% 91.4%;
39 | --ring: 222.2 84% 4.9%;
40 |
41 | --radius: 0.5rem;
42 | }
43 |
44 | .dark {
45 | --background: 222.2 84% 4.9%;
46 | --foreground: 210 40% 98%;
47 |
48 | --card: 222.2 84% 4.9%;
49 | --card-foreground: 210 40% 98%;
50 |
51 | --popover: 222.2 84% 4.9%;
52 | --popover-foreground: 210 40% 98%;
53 |
54 | --primary: 210 40% 98%;
55 | --primary-foreground: 222.2 47.4% 11.2%;
56 |
57 | --secondary: 217.2 32.6% 17.5%;
58 | --secondary-foreground: 210 40% 98%;
59 |
60 | --muted: 217.2 32.6% 17.5%;
61 | --muted-foreground: 215 20.2% 65.1%;
62 |
63 | --accent: 217.2 32.6% 17.5%;
64 | --accent-foreground: 210 40% 98%;
65 |
66 | --destructive: 0 62.8% 30.6%;
67 | --destructive-foreground: 210 40% 98%;
68 |
69 | --border: 217.2 32.6% 17.5%;
70 | --input: 217.2 32.6% 17.5%;
71 | --ring: 212.7 26.8% 83.9%;
72 | }
73 | }
74 |
75 | @layer base {
76 | * {
77 | @apply border-border;
78 | }
79 | body {
80 | @apply bg-background text-foreground;
81 | }
82 | }
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/app/(protected)/admin/page.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { admin } from "@/actions/admin";
4 | import { RoleGate } from "@/components/auth/role-gate";
5 | import { FormSuccess } from "@/components/form-success";
6 | import { Button } from "@/components/ui/button";
7 | import { Card, CardContent, CardHeader } from "@/components/ui/card";
8 | import { UserRole } from "@prisma/client";
9 | import { toast } from "sonner";
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 |
36 | return (
37 |
38 |
39 |
40 | 🔑 Admin
41 |
42 |
43 |
44 |
45 |
48 |
49 |
50 |
51 | Admin-only API Route
52 |
53 |
56 |
57 |
58 |
59 |
60 | Admin-only Server Action
61 |
62 |
65 |
66 |
67 |
68 | );
69 | };
70 |
71 | export default AdminPage;
--------------------------------------------------------------------------------
/prisma/schema.prisma:
--------------------------------------------------------------------------------
1 | // prisma/schema.prisma
2 | datasource db {
3 | provider = "postgresql"
4 | url = env("DATABASE_URL")
5 | directUrl = env("DIRECT_URL")
6 | }
7 |
8 | generator client {
9 | provider = "prisma-client-js"
10 | }
11 |
12 | enum UserRole {
13 | ADMIN
14 | USER
15 | }
16 |
17 | model User {
18 | id String @id @default(cuid())
19 | name String?
20 | email String? @unique
21 | emailVerified DateTime?
22 | image String?
23 | password String?
24 | role UserRole @default(USER)
25 | accounts Account[]
26 | isTwoFactorEnabled Boolean @default(false)
27 | twoFactorConfirmation TwoFactorConfirmation?
28 | }
29 |
30 | model Account {
31 | id String @id @default(cuid())
32 | userId String
33 | type String
34 | provider String
35 | providerAccountId String
36 | refresh_token String? @db.Text
37 | access_token String? @db.Text
38 | expires_at Int?
39 | token_type String?
40 | scope String?
41 | id_token String? @db.Text
42 | session_state String?
43 |
44 | user User @relation(fields: [userId], references: [id], onDelete: Cascade)
45 |
46 | @@unique([provider, providerAccountId])
47 | }
48 |
49 | model VerificationToken {
50 | id String @id @default(cuid())
51 | email String
52 | token String @unique
53 | expires DateTime
54 |
55 | @@unique([email, token])
56 | }
57 |
58 | model PasswordResetToken {
59 | id String @id @default(cuid())
60 | email String
61 | token String @unique
62 | expires DateTime
63 |
64 | @@unique([email, token])
65 | }
66 |
67 | model TwoFactorToken {
68 | id String @id @default(cuid())
69 | email String
70 | token String @unique
71 | expires DateTime
72 |
73 | @@unique([email, token])
74 | }
75 |
76 | model TwoFactorConfirmation {
77 | id String @id @default(cuid())
78 |
79 | userId String
80 | user User @relation(fields: [userId], references: [id], onDelete: Cascade)
81 |
82 | @@unique([userId])
83 | }
84 |
--------------------------------------------------------------------------------
/lib/token.ts:
--------------------------------------------------------------------------------
1 | import crypto from "crypto";
2 | import { v4 as uuidv4 } from "uuid";
3 |
4 | import { db } from "@/lib/db";
5 | import { getVerificationTokenByEmail } from "@/data/verification-token";
6 | import { getPasswordResetokenByEmail } from "@/data/password-reset-token";
7 | import { getTwoFactorTokenByEmail } from "@/data/two-factor-token";
8 |
9 | export const generateTwoFactorToken = async (email: string) => {
10 | const token = crypto.randomInt(100_000, 1000_000).toString();
11 | const expires = new Date(new Date().getTime() + 3600 * 1000);
12 |
13 | const existingToken = await getTwoFactorTokenByEmail(email);
14 |
15 | if (existingToken) {
16 | await db.twoFactorToken.delete({
17 | where: {
18 | id: existingToken.id,
19 | },
20 | });
21 | }
22 |
23 | const twoFactorToken = await db.twoFactorToken.create({
24 | data: {
25 | email,
26 | token,
27 | expires,
28 | },
29 | });
30 |
31 | return twoFactorToken;
32 | };
33 |
34 | export const generatePasswordResetToken = async (email: string) => {
35 | const token = uuidv4();
36 | const expires = new Date(new Date().getTime() + 5 * 60 * 1000);
37 |
38 | const existingToken = await getPasswordResetokenByEmail(email);
39 |
40 | if (existingToken) {
41 | await db.passwordResetToken.delete({
42 | where: {
43 | id: existingToken.id,
44 | },
45 | });
46 | }
47 |
48 | const passwordResetToken = await db.passwordResetToken.create({
49 | data: {
50 | email,
51 | token,
52 | expires,
53 | },
54 | });
55 |
56 | return passwordResetToken;
57 | };
58 |
59 | export const generateVerificationToken = async (email: string) => {
60 | const token = uuidv4();
61 | const expires = new Date(new Date().getTime() + 3600 * 1000);
62 |
63 | const existingToken = await getVerificationTokenByEmail(email);
64 |
65 | if (existingToken) {
66 | await db.verificationToken.delete({
67 | where: {
68 | id: existingToken.id,
69 | },
70 | });
71 | }
72 |
73 | const verificationToken = await db.verificationToken.create({
74 | data: {
75 | email,
76 | token,
77 | expires,
78 | },
79 | });
80 |
81 | return verificationToken;
82 | };
83 |
--------------------------------------------------------------------------------
/components/user-info.tsx:
--------------------------------------------------------------------------------
1 | import { ExtendedUser } from "@/next-auth";
2 | import { Card, CardContent, CardHeader } from "@/components/ui/card";
3 | import { Badge } from "@/components/ui/badge";
4 |
5 | interface UserInfoProps {
6 | user?: ExtendedUser;
7 | label: string;
8 | }
9 |
10 | export const UserInfo = ({ user, label }: UserInfoProps) => {
11 | return (
12 |
13 |
14 | {label}
15 |
16 |
17 |
18 |
ID
19 |
20 | {user?.id}
21 |
22 |
23 |
24 |
Name
25 |
26 | {user?.name}
27 |
28 |
29 |
30 |
Email
31 |
32 | {user?.email}
33 |
34 |
35 |
36 |
Role
37 |
38 | {user?.role}
39 |
40 |
41 |
42 |
43 |
Two Factor Authentication
44 |
45 | {user?.isTwoFactorEnabled ? "ON" : "OFF"}
46 |
47 |
48 |
49 |
50 | );
51 | };
52 |
--------------------------------------------------------------------------------
/actions/settings.ts:
--------------------------------------------------------------------------------
1 | "use server";
2 |
3 | import * as z from "zod";
4 | import bcrypt from "bcryptjs";
5 |
6 | import { update } from "@/auth";
7 | import { db } from "@/lib/db";
8 | import { SettingsSchema } from "@/schemas";
9 | import { getUserByEmail, getUserById } from "@/data/user";
10 | import { currentUser } from "@/lib/auth";
11 | import { generateVerificationToken } from "@/lib/token";
12 | import { sendVerificationEmail } from "@/lib/mail";
13 |
14 | export const settings = async (values: z.infer) => {
15 | const user = await currentUser();
16 |
17 | if (!user) {
18 | return { error: "Unauthorized" };
19 | }
20 |
21 | const dbUser = await getUserById(user.id);
22 |
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 |
41 | const verificationToken = await generateVerificationToken(values.email);
42 | await sendVerificationEmail(
43 | verificationToken.email,
44 | verificationToken.token
45 | );
46 |
47 | return { success: "Verification 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(values.newPassword, 10);
61 | values.password = hashedPassword;
62 | values.newPassword = undefined;
63 | }
64 |
65 | const updatedUser = await db.user.update({
66 | where: { id: dbUser.id },
67 | data: {
68 | ...values,
69 | },
70 | });
71 |
72 | update({
73 | user: {
74 | name: updatedUser.name,
75 | email: updatedUser.email,
76 | isTwoFactorEnabled: updatedUser.isTwoFactorEnabled,
77 | role: updatedUser.role,
78 | },
79 | });
80 |
81 | return { success: "Settings Updated!" };
82 | };
83 |
--------------------------------------------------------------------------------
/tailwind.config.ts:
--------------------------------------------------------------------------------
1 | import type { Config } from "tailwindcss"
2 |
3 | const config = {
4 | darkMode: ["class"],
5 | content: [
6 | './pages/**/*.{ts,tsx}',
7 | './components/**/*.{ts,tsx}',
8 | './app/**/*.{ts,tsx}',
9 | './src/**/*.{ts,tsx}',
10 | ],
11 | prefix: "",
12 | theme: {
13 | container: {
14 | center: true,
15 | padding: "2rem",
16 | screens: {
17 | "2xl": "1400px",
18 | },
19 | },
20 | extend: {
21 | colors: {
22 | border: "hsl(var(--border))",
23 | input: "hsl(var(--input))",
24 | ring: "hsl(var(--ring))",
25 | background: "hsl(var(--background))",
26 | foreground: "hsl(var(--foreground))",
27 | primary: {
28 | DEFAULT: "hsl(var(--primary))",
29 | foreground: "hsl(var(--primary-foreground))",
30 | },
31 | secondary: {
32 | DEFAULT: "hsl(var(--secondary))",
33 | foreground: "hsl(var(--secondary-foreground))",
34 | },
35 | destructive: {
36 | DEFAULT: "hsl(var(--destructive))",
37 | foreground: "hsl(var(--destructive-foreground))",
38 | },
39 | muted: {
40 | DEFAULT: "hsl(var(--muted))",
41 | foreground: "hsl(var(--muted-foreground))",
42 | },
43 | accent: {
44 | DEFAULT: "hsl(var(--accent))",
45 | foreground: "hsl(var(--accent-foreground))",
46 | },
47 | popover: {
48 | DEFAULT: "hsl(var(--popover))",
49 | foreground: "hsl(var(--popover-foreground))",
50 | },
51 | card: {
52 | DEFAULT: "hsl(var(--card))",
53 | foreground: "hsl(var(--card-foreground))",
54 | },
55 | },
56 | borderRadius: {
57 | lg: "var(--radius)",
58 | md: "calc(var(--radius) - 2px)",
59 | sm: "calc(var(--radius) - 4px)",
60 | },
61 | keyframes: {
62 | "accordion-down": {
63 | from: { height: "0" },
64 | to: { height: "var(--radix-accordion-content-height)" },
65 | },
66 | "accordion-up": {
67 | from: { height: "var(--radix-accordion-content-height)" },
68 | to: { height: "0" },
69 | },
70 | },
71 | animation: {
72 | "accordion-down": "accordion-down 0.2s ease-out",
73 | "accordion-up": "accordion-up 0.2s ease-out",
74 | },
75 | },
76 | },
77 | plugins: [require("tailwindcss-animate")],
78 | } satisfies Config
79 |
80 | export default config
--------------------------------------------------------------------------------
/components/auth/reset-form.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as z from "zod";
4 | import { useState, useTransition } from "react";
5 | import { useForm } from "react-hook-form";
6 | import { zodResolver } from "@hookform/resolvers/zod";
7 |
8 | import { ResetSchema } from "@/schemas";
9 | import {
10 | Form,
11 | FormControl,
12 | FormField,
13 | FormItem,
14 | FormLabel,
15 | FormMessage,
16 | } from "@/components/ui/form";
17 | import { CardWrapper } from "@/components/auth/card-wrapper";
18 | import { Input } from "@/components/ui/input";
19 | import { Button } from "@/components/ui/button";
20 | import { FormError } from "@/components/form-error";
21 | import { FormSuccess } from "@/components/form-success";
22 | import { reset } from "@/actions/reset";
23 |
24 | export const ResetForm = () => {
25 | const [error, setError] = useState("");
26 | const [success, setSuccess] = useState("");
27 | const [isPending, startTransition] = useTransition();
28 |
29 | const form = useForm>({
30 | resolver: zodResolver(ResetSchema),
31 | defaultValues: {
32 | email: "",
33 | },
34 | });
35 |
36 | const onSubmit = (values: z.infer) => {
37 | setError("");
38 | setSuccess("");
39 |
40 | startTransition(() => {
41 | reset(values).then((data) => {
42 | setError(data?.error);
43 | setSuccess(data?.success);
44 | });
45 | });
46 | };
47 |
48 | return (
49 |
54 |
82 |
83 |
84 | );
85 | };
86 |
--------------------------------------------------------------------------------
/auth.ts:
--------------------------------------------------------------------------------
1 | import NextAuth from "next-auth";
2 | import { UserRole } from "@prisma/client";
3 | import { PrismaAdapter } from "@auth/prisma-adapter";
4 |
5 | import { getUserById } from "@/data/user";
6 | import { db } from "@/lib/db";
7 | import authConfig from "@/auth.config";
8 | import { getTwoFactorConfirmationByUserId } from "./data/two-factor-confirmation";
9 | import { getAccountByUserId } from "@/data/account";
10 |
11 | export const {
12 | handlers: { GET, POST },
13 | auth,
14 | signIn,
15 | signOut,
16 | update,
17 | } = NextAuth({
18 | pages: {
19 | signIn: "/auth/login",
20 | error: "/auth/error",
21 | },
22 | events: {
23 | async linkAccount({ user }) {
24 | await db.user.update({
25 | where: { id: user.id },
26 | data: { emailVerified: new Date() },
27 | });
28 | },
29 | },
30 | callbacks: {
31 | async signIn({ user, account }) {
32 | //Allow OAuth without email verification
33 | if (account?.provider !== "credentials") return true;
34 |
35 | const existingUser = await getUserById(user.id);
36 |
37 | //Prevent sign in without email verification
38 | if (!existingUser?.emailVerified) return false;
39 |
40 | //2FA check
41 | if (existingUser.isTwoFactorEnabled) {
42 | const twoFactorConfirmation = await getTwoFactorConfirmationByUserId(
43 | existingUser.id
44 | );
45 |
46 | if (!twoFactorConfirmation) return false;
47 |
48 | //Delete the two factor confirmation for next sign in
49 | await db.twoFactorConfirmation.delete({
50 | where: { id: twoFactorConfirmation.id },
51 | });
52 | }
53 |
54 | return true;
55 | },
56 |
57 | async session({ token, session }) {
58 | if (token.sub && session.user) {
59 | session.user.id = token.sub;
60 | }
61 |
62 | if (token.role && session.user) {
63 | session.user.role = token.role as UserRole;
64 | }
65 |
66 | if (session.user) {
67 | session.user.name = token.name;
68 | session.user.email = token.email;
69 | session.user.isOAuth = token.isOAuth as boolean;
70 | session.user.isTwoFactorEnabled = token.isTwoFactorEnabled as boolean;
71 | }
72 |
73 | return session;
74 | },
75 | async jwt({ token }) {
76 | if (!token.sub) return token;
77 |
78 | const existingUser = await getUserById(token.sub);
79 |
80 | if (!existingUser) return token;
81 |
82 | const existingAccount = await getAccountByUserId(existingUser.id);
83 |
84 | token.isOAuth = !!existingAccount;
85 | token.name = existingUser.name;
86 | token.email = existingUser.email;
87 | token.role = existingUser.role;
88 | token.isTwoFactorEnabled = existingUser.isTwoFactorEnabled;
89 |
90 | return token;
91 | },
92 | },
93 | adapter: PrismaAdapter(db),
94 | session: { strategy: "jwt" },
95 | ...authConfig,
96 | });
97 |
--------------------------------------------------------------------------------
/components/auth/new-password-form.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as z from "zod";
4 | import { useSearchParams } from "next/navigation";
5 | import { useState, useTransition } from "react";
6 | import { useForm } from "react-hook-form";
7 | import { zodResolver } from "@hookform/resolvers/zod";
8 |
9 | import { NewPasswordSchema } from "@/schemas";
10 | import {
11 | Form,
12 | FormControl,
13 | FormField,
14 | FormItem,
15 | FormLabel,
16 | FormMessage,
17 | } from "@/components/ui/form";
18 | import { CardWrapper } from "@/components/auth/card-wrapper";
19 | import { Input } from "@/components/ui/input";
20 | import { Button } from "@/components/ui/button";
21 | import { FormError } from "@/components/form-error";
22 | import { FormSuccess } from "@/components/form-success";
23 | import { newPassword } from "@/actions/new-password";
24 |
25 | export const NewPasswordForm = () => {
26 | const searchParams = useSearchParams();
27 | const token = searchParams.get("token");
28 |
29 | const [error, setError] = useState("");
30 | const [success, setSuccess] = useState("");
31 | const [isPending, startTransition] = useTransition();
32 |
33 | const form = useForm>({
34 | resolver: zodResolver(NewPasswordSchema),
35 | defaultValues: {
36 | password: "",
37 | },
38 | });
39 |
40 | const onSubmit = (values: z.infer) => {
41 | setError("");
42 | setSuccess("");
43 |
44 | startTransition(() => {
45 | newPassword(values, token)
46 | .then((data) => {
47 | setError(data?.error);
48 | setSuccess(data?.success);
49 | });
50 | });
51 | };
52 |
53 | return (
54 |
59 |
87 |
88 |
89 | );
90 | };
91 |
--------------------------------------------------------------------------------
/actions/login.ts:
--------------------------------------------------------------------------------
1 | "use server";
2 |
3 | import * as z from "zod";
4 | import bcrypt from "bcryptjs";
5 | import { AuthError } from "next-auth";
6 |
7 | import { db } from "@/lib/db";
8 | import { signIn } from "@/auth";
9 | import { LoginSchema } from "@/schemas";
10 | import { getUserByEmail } from "@/data/user";
11 | import { getTwoFactorTokenByEmail } from "@/data/two-factor-token";
12 | import { sendVerificationEmail, sendTwoFactorTokenEmail } from "@/lib/mail";
13 | import { DEFAULT_LOGIN_REDIRECT } from "@/routes";
14 | import { generateVerificationToken, generateTwoFactorToken } from "@/lib/token";
15 | import { getTwoFactorConfirmationByUserId } from "@/data/two-factor-confirmation";
16 |
17 | export const login = async (
18 | values: z.infer,
19 | callbackUrl?: string | null
20 | ) => {
21 | const validatedFields = LoginSchema.safeParse(values);
22 |
23 | if (!validatedFields.success) {
24 | return { error: "Invalid fields!" };
25 | }
26 |
27 | const { email, password, code } = validatedFields.data;
28 |
29 | const existingUser = await getUserByEmail(email);
30 |
31 | if (!existingUser || !existingUser.email || !existingUser.password) {
32 | return { error: "Email does not exist!" };
33 | }
34 |
35 | if (!existingUser.emailVerified) {
36 | const verificationToken = await generateVerificationToken(
37 | existingUser.email
38 | );
39 |
40 | await sendVerificationEmail(
41 | verificationToken.email,
42 | verificationToken.token
43 | );
44 |
45 | return { success: "Confirmation email sent!" };
46 | }
47 |
48 | if (existingUser.isTwoFactorEnabled && existingUser.email) {
49 | // Check password before proceeding with 2FA
50 | const isPasswordValid = await bcrypt.compare(
51 | password,
52 | existingUser.password
53 | );
54 |
55 | if (!isPasswordValid) {
56 | return { error: "Invalid credentials!" };
57 | }
58 |
59 | if (code) {
60 | const twoFactorToken = await getTwoFactorTokenByEmail(existingUser.email);
61 |
62 | if (!twoFactorToken) {
63 | return { error: "Invalid code!" };
64 | }
65 |
66 | if (twoFactorToken.token !== code) {
67 | return { error: "Invalid code!" };
68 | }
69 |
70 | const hasExpired = new Date(twoFactorToken.expires) < new Date();
71 |
72 | if (hasExpired) {
73 | return { error: "Code expired!" };
74 | }
75 |
76 | await db.twoFactorToken.delete({
77 | where: { id: twoFactorToken.id },
78 | });
79 |
80 | const existingConfirmation = await getTwoFactorConfirmationByUserId(
81 | existingUser.id
82 | );
83 |
84 | if (existingConfirmation) {
85 | await db.twoFactorConfirmation.delete({
86 | where: { id: existingConfirmation.id },
87 | });
88 | }
89 |
90 | await db.twoFactorConfirmation.create({
91 | data: { userId: existingUser.id },
92 | });
93 | } else {
94 | const twoFactorToken = await generateTwoFactorToken(existingUser.email);
95 | await sendTwoFactorTokenEmail(twoFactorToken.email, twoFactorToken.token);
96 |
97 | return { twoFactor: true };
98 | }
99 | }
100 |
101 | try {
102 | await signIn("credentials", {
103 | email,
104 | password,
105 | redirectTo: callbackUrl || DEFAULT_LOGIN_REDIRECT,
106 | });
107 | } catch (error) {
108 | if (error instanceof AuthError) {
109 | switch (error.type) {
110 | case "CredentialsSignin":
111 | return { error: "Invalid credentials!" };
112 | default:
113 | return { error: "Something went wrong!" };
114 | }
115 | }
116 |
117 | throw error;
118 | }
119 | };
120 |
--------------------------------------------------------------------------------
/components/auth/register-form.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as z from "zod";
4 | import { useState, useTransition } from "react";
5 | import { useForm } from "react-hook-form";
6 | import { zodResolver } from "@hookform/resolvers/zod";
7 | import { RegisterSchema } from "@/schemas";
8 | import {
9 | Form,
10 | FormControl,
11 | FormField,
12 | FormItem,
13 | FormLabel,
14 | FormMessage,
15 | } from "@/components/ui/form";
16 | import { CardWrapper } from "@/components/auth/card-wrapper";
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 { register } from "@/actions/register";
22 |
23 | export const RegisterForm = () => {
24 | const [error, setError] = useState("");
25 | const [success, setSuccess] = useState("");
26 | const [isPending, startTransition] = useTransition();
27 |
28 | const form = useForm>({
29 | resolver: zodResolver(RegisterSchema),
30 | defaultValues: {
31 | email: "",
32 | password: "",
33 | name: "",
34 | },
35 | });
36 |
37 | const onSubmit = (values: z.infer) => {
38 | setError("");
39 | setSuccess("");
40 |
41 | startTransition(() => {
42 | register(values).then((data) => {
43 | setError(data.error);
44 | setSuccess(data.success);
45 | });
46 | });
47 | };
48 |
49 | return (
50 |
56 |
119 |
120 |
121 | );
122 | };
123 |
--------------------------------------------------------------------------------
/components/ui/dialog.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as DialogPrimitive from "@radix-ui/react-dialog"
5 | import { Cross2Icon } from "@radix-ui/react-icons"
6 |
7 | import { cn } from "@/lib/utils"
8 |
9 | const Dialog = DialogPrimitive.Root
10 |
11 | const DialogTrigger = DialogPrimitive.Trigger
12 |
13 | const DialogPortal = DialogPrimitive.Portal
14 |
15 | const DialogClose = DialogPrimitive.Close
16 |
17 | const DialogOverlay = React.forwardRef<
18 | React.ElementRef,
19 | React.ComponentPropsWithoutRef
20 | >(({ className, ...props }, ref) => (
21 |
29 | ))
30 | DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
31 |
32 | const DialogContent = React.forwardRef<
33 | React.ElementRef,
34 | React.ComponentPropsWithoutRef
35 | >(({ className, children, ...props }, ref) => (
36 |
37 |
38 |
46 | {children}
47 |
48 |
49 | Close
50 |
51 |
52 |
53 | ))
54 | DialogContent.displayName = DialogPrimitive.Content.displayName
55 |
56 | const DialogHeader = ({
57 | className,
58 | ...props
59 | }: React.HTMLAttributes) => (
60 |
67 | )
68 | DialogHeader.displayName = "DialogHeader"
69 |
70 | const DialogFooter = ({
71 | className,
72 | ...props
73 | }: React.HTMLAttributes) => (
74 |
81 | )
82 | DialogFooter.displayName = "DialogFooter"
83 |
84 | const DialogTitle = React.forwardRef<
85 | React.ElementRef,
86 | React.ComponentPropsWithoutRef
87 | >(({ className, ...props }, ref) => (
88 |
96 | ))
97 | DialogTitle.displayName = DialogPrimitive.Title.displayName
98 |
99 | const DialogDescription = React.forwardRef<
100 | React.ElementRef,
101 | React.ComponentPropsWithoutRef
102 | >(({ className, ...props }, ref) => (
103 |
108 | ))
109 | DialogDescription.displayName = DialogPrimitive.Description.displayName
110 |
111 | export {
112 | Dialog,
113 | DialogPortal,
114 | DialogOverlay,
115 | DialogTrigger,
116 | DialogClose,
117 | DialogContent,
118 | DialogHeader,
119 | DialogFooter,
120 | DialogTitle,
121 | DialogDescription,
122 | }
123 |
--------------------------------------------------------------------------------
/components/ui/form.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import * as LabelPrimitive from "@radix-ui/react-label"
3 | import { Slot } from "@radix-ui/react-slot"
4 | import {
5 | Controller,
6 | ControllerProps,
7 | FieldPath,
8 | FieldValues,
9 | FormProvider,
10 | useFormContext,
11 | } from "react-hook-form"
12 |
13 | import { cn } from "@/lib/utils"
14 | import { Label } from "@/components/ui/label"
15 |
16 | const Form = FormProvider
17 |
18 | type FormFieldContextValue<
19 | TFieldValues extends FieldValues = FieldValues,
20 | TName extends FieldPath = FieldPath
21 | > = {
22 | name: TName
23 | }
24 |
25 | const FormFieldContext = React.createContext(
26 | {} as FormFieldContextValue
27 | )
28 |
29 | const FormField = <
30 | TFieldValues extends FieldValues = FieldValues,
31 | TName extends FieldPath = FieldPath
32 | >({
33 | ...props
34 | }: ControllerProps) => {
35 | return (
36 |
37 |
38 |
39 | )
40 | }
41 |
42 | const useFormField = () => {
43 | const fieldContext = React.useContext(FormFieldContext)
44 | const itemContext = React.useContext(FormItemContext)
45 | const { getFieldState, formState } = useFormContext()
46 |
47 | const fieldState = getFieldState(fieldContext.name, formState)
48 |
49 | if (!fieldContext) {
50 | throw new Error("useFormField should be used within ")
51 | }
52 |
53 | const { id } = itemContext
54 |
55 | return {
56 | id,
57 | name: fieldContext.name,
58 | formItemId: `${id}-form-item`,
59 | formDescriptionId: `${id}-form-item-description`,
60 | formMessageId: `${id}-form-item-message`,
61 | ...fieldState,
62 | }
63 | }
64 |
65 | type FormItemContextValue = {
66 | id: string
67 | }
68 |
69 | const FormItemContext = React.createContext(
70 | {} as FormItemContextValue
71 | )
72 |
73 | const FormItem = React.forwardRef<
74 | HTMLDivElement,
75 | React.HTMLAttributes
76 | >(({ className, ...props }, ref) => {
77 | const id = React.useId()
78 |
79 | return (
80 |
81 |
82 |
83 | )
84 | })
85 | FormItem.displayName = "FormItem"
86 |
87 | const FormLabel = React.forwardRef<
88 | React.ElementRef,
89 | React.ComponentPropsWithoutRef
90 | >(({ className, ...props }, ref) => {
91 | const { error, formItemId } = useFormField()
92 |
93 | return (
94 |
100 | )
101 | })
102 | FormLabel.displayName = "FormLabel"
103 |
104 | const FormControl = React.forwardRef<
105 | React.ElementRef,
106 | React.ComponentPropsWithoutRef
107 | >(({ ...props }, ref) => {
108 | const { error, formItemId, formDescriptionId, formMessageId } = useFormField()
109 |
110 | return (
111 |
122 | )
123 | })
124 | FormControl.displayName = "FormControl"
125 |
126 | const FormDescription = React.forwardRef<
127 | HTMLParagraphElement,
128 | React.HTMLAttributes
129 | >(({ className, ...props }, ref) => {
130 | const { formDescriptionId } = useFormField()
131 |
132 | return (
133 |
139 | )
140 | })
141 | FormDescription.displayName = "FormDescription"
142 |
143 | const FormMessage = React.forwardRef<
144 | HTMLParagraphElement,
145 | React.HTMLAttributes
146 | >(({ className, children, ...props }, ref) => {
147 | const { error, formMessageId } = useFormField()
148 | const body = error ? String(error?.message) : children
149 |
150 | if (!body) {
151 | return null
152 | }
153 |
154 | return (
155 |
161 | {body}
162 |
163 | )
164 | })
165 | FormMessage.displayName = "FormMessage"
166 |
167 | export {
168 | useFormField,
169 | Form,
170 | FormItem,
171 | FormLabel,
172 | FormControl,
173 | FormDescription,
174 | FormMessage,
175 | FormField,
176 | }
177 |
--------------------------------------------------------------------------------
/components/auth/login-form.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as z from "zod";
4 | import { useSearchParams } from "next/navigation";
5 | import { useState, useTransition } from "react";
6 | import { useForm } from "react-hook-form";
7 | import { zodResolver } from "@hookform/resolvers/zod";
8 | import Link from "next/link";
9 |
10 | import { LoginSchema } from "@/schemas";
11 | import {
12 | Form,
13 | FormControl,
14 | FormField,
15 | FormItem,
16 | FormLabel,
17 | FormMessage,
18 | } from "@/components/ui/form";
19 | import { CardWrapper } from "@/components/auth/card-wrapper";
20 | import { Input } from "@/components/ui/input";
21 | import { Button } from "@/components/ui/button";
22 | import { FormError } from "@/components/form-error";
23 | import { FormSuccess } from "@/components/form-success";
24 | import { login } from "@/actions/login";
25 |
26 | export const LoginForm = () => {
27 | const searchParams = useSearchParams();
28 | const callbackUrl = searchParams.get("callbackUrl");
29 |
30 | const errorUrl =
31 | searchParams.get("error") === "OAuthAccountNotLinked"
32 | ? "Email already in use with a different provider"
33 | : "";
34 |
35 | const [showTwoFactor, setShowTwoFactor] = useState(false);
36 | const [error, setError] = useState("");
37 | const [success, setSuccess] = useState("");
38 | const [isPending, startTransition] = useTransition();
39 |
40 | const form = useForm>({
41 | resolver: zodResolver(LoginSchema),
42 | defaultValues: {
43 | email: "",
44 | password: "",
45 | },
46 | });
47 |
48 | const onSubmit = (values: z.infer) => {
49 | setError("");
50 | setSuccess("");
51 |
52 | startTransition(() => {
53 | login(values, callbackUrl)
54 | .then((data) => {
55 | if (data?.error) {
56 | form.reset();
57 | setError(data.error);
58 | }
59 |
60 | if (data?.success) {
61 | form.reset();
62 | setSuccess(data.success);
63 | }
64 |
65 | if (data?.twoFactor) {
66 | setShowTwoFactor(true);
67 | }
68 | })
69 | .catch(() => setError("Something went wrong!"));
70 | });
71 | };
72 |
73 | return (
74 |
80 |
157 |
158 |
159 | );
160 | };
161 |
--------------------------------------------------------------------------------
/components/ui/select.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import {
5 | CaretSortIcon,
6 | CheckIcon,
7 | ChevronDownIcon,
8 | ChevronUpIcon,
9 | } from "@radix-ui/react-icons"
10 | import * as SelectPrimitive from "@radix-ui/react-select"
11 |
12 | import { cn } from "@/lib/utils"
13 |
14 | const Select = SelectPrimitive.Root
15 |
16 | const SelectGroup = SelectPrimitive.Group
17 |
18 | const SelectValue = SelectPrimitive.Value
19 |
20 | const SelectTrigger = React.forwardRef<
21 | React.ElementRef,
22 | React.ComponentPropsWithoutRef
23 | >(({ className, children, ...props }, ref) => (
24 | span]:line-clamp-1",
28 | className
29 | )}
30 | {...props}
31 | >
32 | {children}
33 |
34 |
35 |
36 |
37 | ))
38 | SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
39 |
40 | const SelectScrollUpButton = React.forwardRef<
41 | React.ElementRef,
42 | React.ComponentPropsWithoutRef
43 | >(({ className, ...props }, ref) => (
44 |
52 |
53 |
54 | ))
55 | SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName
56 |
57 | const SelectScrollDownButton = React.forwardRef<
58 | React.ElementRef,
59 | React.ComponentPropsWithoutRef
60 | >(({ className, ...props }, ref) => (
61 |
69 |
70 |
71 | ))
72 | SelectScrollDownButton.displayName =
73 | SelectPrimitive.ScrollDownButton.displayName
74 |
75 | const SelectContent = React.forwardRef<
76 | React.ElementRef,
77 | React.ComponentPropsWithoutRef
78 | >(({ className, children, position = "popper", ...props }, ref) => (
79 |
80 |
91 |
92 |
99 | {children}
100 |
101 |
102 |
103 |
104 | ))
105 | SelectContent.displayName = SelectPrimitive.Content.displayName
106 |
107 | const SelectLabel = React.forwardRef<
108 | React.ElementRef,
109 | React.ComponentPropsWithoutRef
110 | >(({ className, ...props }, ref) => (
111 |
116 | ))
117 | SelectLabel.displayName = SelectPrimitive.Label.displayName
118 |
119 | const SelectItem = React.forwardRef<
120 | React.ElementRef,
121 | React.ComponentPropsWithoutRef
122 | >(({ className, children, ...props }, ref) => (
123 |
131 |
132 |
133 |
134 |
135 |
136 | {children}
137 |
138 | ))
139 | SelectItem.displayName = SelectPrimitive.Item.displayName
140 |
141 | const SelectSeparator = React.forwardRef<
142 | React.ElementRef,
143 | React.ComponentPropsWithoutRef
144 | >(({ className, ...props }, ref) => (
145 |
150 | ))
151 | SelectSeparator.displayName = SelectPrimitive.Separator.displayName
152 |
153 | export {
154 | Select,
155 | SelectGroup,
156 | SelectValue,
157 | SelectTrigger,
158 | SelectContent,
159 | SelectLabel,
160 | SelectItem,
161 | SelectSeparator,
162 | SelectScrollUpButton,
163 | SelectScrollDownButton,
164 | }
165 |
--------------------------------------------------------------------------------
/components/ui/dropdown-menu.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
5 | import {
6 | CheckIcon,
7 | ChevronRightIcon,
8 | DotFilledIcon,
9 | } from "@radix-ui/react-icons"
10 |
11 | import { cn } from "@/lib/utils"
12 |
13 | const DropdownMenu = DropdownMenuPrimitive.Root
14 |
15 | const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
16 |
17 | const DropdownMenuGroup = DropdownMenuPrimitive.Group
18 |
19 | const DropdownMenuPortal = DropdownMenuPrimitive.Portal
20 |
21 | const DropdownMenuSub = DropdownMenuPrimitive.Sub
22 |
23 | const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup
24 |
25 | const DropdownMenuSubTrigger = React.forwardRef<
26 | React.ElementRef,
27 | React.ComponentPropsWithoutRef & {
28 | inset?: boolean
29 | }
30 | >(({ className, inset, children, ...props }, ref) => (
31 |
40 | {children}
41 |
42 |
43 | ))
44 | DropdownMenuSubTrigger.displayName =
45 | DropdownMenuPrimitive.SubTrigger.displayName
46 |
47 | const DropdownMenuSubContent = React.forwardRef<
48 | React.ElementRef,
49 | React.ComponentPropsWithoutRef
50 | >(({ className, ...props }, ref) => (
51 |
59 | ))
60 | DropdownMenuSubContent.displayName =
61 | DropdownMenuPrimitive.SubContent.displayName
62 |
63 | const DropdownMenuContent = React.forwardRef<
64 | React.ElementRef,
65 | React.ComponentPropsWithoutRef
66 | >(({ className, sideOffset = 4, ...props }, ref) => (
67 |
68 |
78 |
79 | ))
80 | DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
81 |
82 | const DropdownMenuItem = React.forwardRef<
83 | React.ElementRef,
84 | React.ComponentPropsWithoutRef & {
85 | inset?: boolean
86 | }
87 | >(({ className, inset, ...props }, ref) => (
88 |
97 | ))
98 | DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
99 |
100 | const DropdownMenuCheckboxItem = React.forwardRef<
101 | React.ElementRef,
102 | React.ComponentPropsWithoutRef
103 | >(({ className, children, checked, ...props }, ref) => (
104 |
113 |
114 |
115 |
116 |
117 |
118 | {children}
119 |
120 | ))
121 | DropdownMenuCheckboxItem.displayName =
122 | DropdownMenuPrimitive.CheckboxItem.displayName
123 |
124 | const DropdownMenuRadioItem = React.forwardRef<
125 | React.ElementRef,
126 | React.ComponentPropsWithoutRef
127 | >(({ className, children, ...props }, ref) => (
128 |
136 |
137 |
138 |
139 |
140 |
141 | {children}
142 |
143 | ))
144 | DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName
145 |
146 | const DropdownMenuLabel = React.forwardRef<
147 | React.ElementRef,
148 | React.ComponentPropsWithoutRef & {
149 | inset?: boolean
150 | }
151 | >(({ className, inset, ...props }, ref) => (
152 |
161 | ))
162 | DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
163 |
164 | const DropdownMenuSeparator = React.forwardRef<
165 | React.ElementRef,
166 | React.ComponentPropsWithoutRef
167 | >(({ className, ...props }, ref) => (
168 |
173 | ))
174 | DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
175 |
176 | const DropdownMenuShortcut = ({
177 | className,
178 | ...props
179 | }: React.HTMLAttributes) => {
180 | return (
181 |
185 | )
186 | }
187 | DropdownMenuShortcut.displayName = "DropdownMenuShortcut"
188 |
189 | export {
190 | DropdownMenu,
191 | DropdownMenuTrigger,
192 | DropdownMenuContent,
193 | DropdownMenuItem,
194 | DropdownMenuCheckboxItem,
195 | DropdownMenuRadioItem,
196 | DropdownMenuLabel,
197 | DropdownMenuSeparator,
198 | DropdownMenuShortcut,
199 | DropdownMenuGroup,
200 | DropdownMenuPortal,
201 | DropdownMenuSub,
202 | DropdownMenuSubContent,
203 | DropdownMenuSubTrigger,
204 | DropdownMenuRadioGroup,
205 | }
206 |
--------------------------------------------------------------------------------
/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 { useTransition, useState } from "react";
7 | import { useSession } from "next-auth/react";
8 |
9 | import { Switch } from "@/components/ui/switch";
10 | import {
11 | Select,
12 | SelectContent,
13 | SelectItem,
14 | SelectTrigger,
15 | SelectValue,
16 | } from "@/components/ui/select";
17 | import { SettingsSchema } from "@/schemas";
18 | import { Card, CardHeader, CardContent } from "@/components/ui/card";
19 | import { Button } from "@/components/ui/button";
20 | import { settings } from "@/actions/settings";
21 | import {
22 | Form,
23 | FormField,
24 | FormControl,
25 | FormItem,
26 | FormLabel,
27 | FormDescription,
28 | FormMessage,
29 | } from "@/components/ui/form";
30 | import { Input } from "@/components/ui/input";
31 | import { useCurrentUser } from "@/hooks/use-current-user";
32 | import { FormError } from "@/components/form-error";
33 | import { FormSuccess } from "@/components/form-success";
34 | import { UserRole } from "@prisma/client";
35 |
36 | const SettingsPage = () => {
37 | const user = useCurrentUser();
38 |
39 | const [error, setError] = useState();
40 | const [success, setSuccess] = useState();
41 | const { update } = useSession();
42 | const [isPending, startTransition] = useTransition();
43 |
44 | const form = useForm>({
45 | resolver: zodResolver(SettingsSchema),
46 | defaultValues: {
47 | password: undefined,
48 | newPassword: undefined,
49 | name: user?.name || undefined,
50 | email: user?.email || undefined,
51 | role: user?.role || undefined,
52 | isTwoFactorEnabled: user?.isTwoFactorEnabled || undefined,
53 | },
54 | });
55 |
56 | const onSubmit = (values: z.infer) => {
57 | startTransition(() => {
58 | settings(values)
59 | .then((data) => {
60 | if (data.error) {
61 | setError(data.error);
62 | }
63 |
64 | if (data.success) {
65 | update();
66 | setSuccess(data.success);
67 | }
68 | })
69 | .catch(() => setError("Something went wrong!"));
70 | });
71 | };
72 |
73 | return (
74 |
75 |
76 | ⚙️ Settings
77 |
78 |
79 |
212 |
213 |
214 |
215 | );
216 | };
217 |
218 | export default SettingsPage;
219 |
--------------------------------------------------------------------------------