Browse Source

Merge branch 'main' of https://github.com/fosrl/pangolin

Owen Schwartz 7 months ago
parent
commit
29bd88ebdf

+ 83 - 0
server/emails/templates/TwoFactorAuthNotification.tsx

@@ -0,0 +1,83 @@
+import {
+    Body,
+    Container,
+    Head,
+    Heading,
+    Html,
+    Preview,
+    Section,
+    Text,
+    Tailwind
+} from "@react-email/components";
+import * as React from "react";
+
+interface Props {
+    email: string;
+    enabled: boolean;
+}
+
+export const TwoFactorAuthNotification = ({ email, enabled }: Props) => {
+    const previewText = `Two-Factor Authentication has been ${enabled ? "enabled" : "disabled"}`;
+
+    return (
+        <Html>
+            <Head />
+            <Preview>{previewText}</Preview>
+            <Tailwind
+                config={{
+                    theme: {
+                        extend: {
+                            colors: {
+                                primary: "#16A34A"
+                            }
+                        }
+                    }
+                }}
+            >
+                <Body className="font-sans">
+                    <Container className="bg-white border border-solid border-gray-200 p-6 max-w-lg mx-auto my-8 rounded-lg">
+                        <Heading className="text-2xl font-semibold text-gray-800 text-center">
+                            Two-Factor Authentication{" "}
+                            {enabled ? "Enabled" : "Disabled"}
+                        </Heading>
+                        <Text className="text-base text-gray-700 mt-4">
+                            Hi {email || "there"},
+                        </Text>
+                        <Text className="text-base text-gray-700 mt-2">
+                            This email confirms that Two-Factor Authentication
+                            has been successfully{" "}
+                            {enabled ? "enabled" : "disabled"} on your account.
+                        </Text>
+                        <Section className="text-center my-6">
+                            {enabled ? (
+                                <Text className="text-base text-gray-700">
+                                    With Two-Factor Authentication enabled, your
+                                    account is now more secure. Please ensure
+                                    you keep your authentication method safe.
+                                </Text>
+                            ) : (
+                                <Text className="text-base text-gray-700">
+                                    With Two-Factor Authentication disabled,
+                                    your account may be less secure. We
+                                    recommend enabling it to protect your
+                                    account.
+                                </Text>
+                            )}
+                        </Section>
+                        <Text className="text-base text-gray-700 mt-2">
+                            If you did not make this change, please contact our
+                            support team immediately.
+                        </Text>
+                        <Text className="text-sm text-gray-500 mt-6">
+                            Best regards,
+                            <br />
+                            Fossorial
+                        </Text>
+                    </Container>
+                </Body>
+            </Tailwind>
+        </Html>
+    );
+};
+
+export default TwoFactorAuthNotification;

+ 18 - 10
server/routers/auth/disable2fa.ts

@@ -11,6 +11,9 @@ import { response } from "@server/utils";
 import { verifyPassword } from "@server/auth/password";
 import { verifyPassword } from "@server/auth/password";
 import { verifyTotpCode } from "@server/auth/2fa";
 import { verifyTotpCode } from "@server/auth/2fa";
 import logger from "@server/logger";
 import logger from "@server/logger";
+import { sendEmail } from "@server/emails";
+import TwoFactorAuthNotification from "@server/emails/templates/TwoFactorAuthNotification";
+import config from "@server/config";
 
 
 export const disable2faBody = z
 export const disable2faBody = z
     .object({
     .object({
@@ -84,17 +87,22 @@ export async function disable2fa(
             );
             );
         }
         }
 
 
-        await db.transaction(async (trx) => {
-            await trx
-                .update(users)
-                .set({ twoFactorEnabled: false })
-                .where(eq(users.userId, user.userId));
+        await db
+            .update(users)
+            .set({ twoFactorEnabled: false })
+            .where(eq(users.userId, user.userId));
 
 
-            await trx
-                .delete(twoFactorBackupCodes)
-                .where(eq(twoFactorBackupCodes.userId, user.userId));
-        });
-        // TODO: send email to user confirming two-factor authentication is disabled
+        sendEmail(
+            TwoFactorAuthNotification({
+                email: user.email,
+                enabled: false
+            }),
+            {
+                to: user.email,
+                from: config.email?.no_reply,
+                subject: "Two-factor authentication disabled"
+            }
+        );
 
 
         return response<null>(res, {
         return response<null>(res, {
             data: null,
             data: null,

+ 3 - 1
server/routers/auth/requestTotpSecret.ts

@@ -25,6 +25,7 @@ export type RequestTotpSecretBody = z.infer<typeof requestTotpSecretBody>;
 
 
 export type RequestTotpSecretResponse = {
 export type RequestTotpSecretResponse = {
     secret: string;
     secret: string;
+    uri: string;
 };
 };
 
 
 export async function requestTotpSecret(
 export async function requestTotpSecret(
@@ -75,7 +76,8 @@ export async function requestTotpSecret(
 
 
         return response<RequestTotpSecretResponse>(res, {
         return response<RequestTotpSecretResponse>(res, {
             data: {
             data: {
-                secret: uri
+                secret,
+                uri
             },
             },
             success: true,
             success: true,
             error: false,
             error: false,

+ 15 - 2
server/routers/auth/verifyTotp.ts

@@ -11,6 +11,9 @@ import { alphabet, generateRandomString } from "oslo/crypto";
 import { hashPassword } from "@server/auth/password";
 import { hashPassword } from "@server/auth/password";
 import { verifyTotpCode } from "@server/auth/2fa";
 import { verifyTotpCode } from "@server/auth/2fa";
 import logger from "@server/logger";
 import logger from "@server/logger";
+import { sendEmail } from "@server/emails";
+import TwoFactorAuthNotification from "@server/emails/templates/TwoFactorAuthNotification";
+import config from "@server/config";
 
 
 export const verifyTotpBody = z
 export const verifyTotpBody = z
     .object({
     .object({
@@ -92,8 +95,6 @@ export async function verifyTotp(
             });
             });
         }
         }
 
 
-        // TODO: send email to user confirming two-factor authentication is enabled
-
         if (!valid) {
         if (!valid) {
             return next(
             return next(
                 createHttpError(
                 createHttpError(
@@ -103,6 +104,18 @@ export async function verifyTotp(
             );
             );
         }
         }
 
 
+        sendEmail(
+            TwoFactorAuthNotification({
+                email: user.email,
+                enabled: true
+            }),
+            {
+                to: user.email,
+                from: config.email?.no_reply,
+                subject: "Two-factor authentication enabled"
+            }
+        );
+
         return response<VerifyTotpResponse>(res, {
         return response<VerifyTotpResponse>(res, {
             data: {
             data: {
                 valid,
                 valid,

+ 2 - 2
src/app/[orgId]/settings/access/users/components/UsersTable.tsx

@@ -13,7 +13,6 @@ import { UsersDataTable } from "./UsersDataTable";
 import { useState } from "react";
 import { useState } from "react";
 import InviteUserForm from "./InviteUserForm";
 import InviteUserForm from "./InviteUserForm";
 import ConfirmDeleteDialog from "@app/components/ConfirmDeleteDialog";
 import ConfirmDeleteDialog from "@app/components/ConfirmDeleteDialog";
-import { useUserContext } from "@app/hooks/useUserContext";
 import { useOrgContext } from "@app/hooks/useOrgContext";
 import { useOrgContext } from "@app/hooks/useOrgContext";
 import { useToast } from "@app/hooks/useToast";
 import { useToast } from "@app/hooks/useToast";
 import Link from "next/link";
 import Link from "next/link";
@@ -21,6 +20,7 @@ import { useRouter } from "next/navigation";
 import { formatAxiosError } from "@app/lib/utils";
 import { formatAxiosError } from "@app/lib/utils";
 import { createApiClient } from "@app/api";
 import { createApiClient } from "@app/api";
 import { useEnvContext } from "@app/hooks/useEnvContext";
 import { useEnvContext } from "@app/hooks/useEnvContext";
+import { useUserContext } from "@app/hooks/useUserContext";
 
 
 export type UserRow = {
 export type UserRow = {
     id: string;
     id: string;
@@ -45,7 +45,7 @@ export default function UsersTable({ users: u }: UsersTableProps) {
 
 
     const api = createApiClient(useEnvContext());
     const api = createApiClient(useEnvContext());
 
 
-    const user = useUserContext();
+    const { user, updateUser } = useUserContext();
     const { org } = useOrgContext();
     const { org } = useOrgContext();
     const { toast } = useToast();
     const { toast } = useToast();
 
 

+ 0 - 1
src/app/auth/reset-password/ResetPasswordForm.tsx

@@ -368,7 +368,6 @@ export default function ResetPasswordForm({
                                                                     index={2}
                                                                     index={2}
                                                                 />
                                                                 />
                                                             </InputOTPGroup>
                                                             </InputOTPGroup>
-                                                            <InputOTPSeparator />
                                                             <InputOTPGroup>
                                                             <InputOTPGroup>
                                                                 <InputOTPSlot
                                                                 <InputOTPSlot
                                                                     index={3}
                                                                     index={3}

+ 15 - 0
src/app/not-found.tsx

@@ -0,0 +1,15 @@
+import Link from "next/link";
+
+export default async function NotFound() {
+    return (
+        <div className="w-full max-w-md mx-auto p-3 md:mt-32 text-center">
+            <h1 className="text-6xl font-bold text-gray-800 mb-4">404</h1>
+            <h2 className="text-2xl font-semibold text-gray-600 mb-4">
+                Page Not Found
+            </h2>
+            <p className="text-gray-500 mb-8">
+                Oops! The page you're looking for doesn't exist.
+            </p>
+        </div>
+    );
+}

+ 14 - 0
src/app/profile/general/page.tsx

@@ -0,0 +1,14 @@
+"use client";
+
+import { useState } from "react";
+import Enable2FaForm from "../../../components/Enable2FaForm";
+
+export default function ProfileGeneralPage() {
+    const [open, setOpen] = useState(true);
+
+    return (
+        <>
+            {/* <Enable2FaForm open={open} setOpen={setOpen} /> */}
+        </>
+    );
+}

+ 27 - 31
src/components/DataTablePagination.tsx

@@ -2,7 +2,7 @@ import {
     ChevronLeftIcon,
     ChevronLeftIcon,
     ChevronRightIcon,
     ChevronRightIcon,
     DoubleArrowLeftIcon,
     DoubleArrowLeftIcon,
-    DoubleArrowRightIcon,
+    DoubleArrowRightIcon
 } from "@radix-ui/react-icons";
 } from "@radix-ui/react-icons";
 import { Table } from "@tanstack/react-table";
 import { Table } from "@tanstack/react-table";
 
 
@@ -12,7 +12,7 @@ import {
     SelectContent,
     SelectContent,
     SelectItem,
     SelectItem,
     SelectTrigger,
     SelectTrigger,
-    SelectValue,
+    SelectValue
 } from "@app/components/ui/select";
 } from "@app/components/ui/select";
 
 
 interface DataTablePaginationProps<TData> {
 interface DataTablePaginationProps<TData> {
@@ -20,38 +20,34 @@ interface DataTablePaginationProps<TData> {
 }
 }
 
 
 export function DataTablePagination<TData>({
 export function DataTablePagination<TData>({
-    table,
+    table
 }: DataTablePaginationProps<TData>) {
 }: DataTablePaginationProps<TData>) {
     return (
     return (
-        <div className="flex items-center justify-end px-2">
+        <div className="flex items-center justify-between text-muted-foreground">
+            <div className="flex items-center space-x-2">
+                <p className="text-sm font-medium">Rows per page</p>
+                <Select
+                    value={`${table.getState().pagination.pageSize}`}
+                    onValueChange={(value) => {
+                        table.setPageSize(Number(value));
+                    }}
+                >
+                    <SelectTrigger className="h-8 w-[70px]">
+                        <SelectValue
+                            placeholder={table.getState().pagination.pageSize}
+                        />
+                    </SelectTrigger>
+                    <SelectContent side="top">
+                        {[10, 20, 30, 40, 50, 100, 200].map((pageSize) => (
+                            <SelectItem key={pageSize} value={`${pageSize}`}>
+                                {pageSize}
+                            </SelectItem>
+                        ))}
+                    </SelectContent>
+                </Select>
+            </div>
+
             <div className="flex items-center space-x-6 lg:space-x-8">
             <div className="flex items-center space-x-6 lg:space-x-8">
-                <div className="flex items-center space-x-2">
-                    <p className="text-sm font-medium">Rows per page</p>
-                    <Select
-                        value={`${table.getState().pagination.pageSize}`}
-                        onValueChange={(value) => {
-                            table.setPageSize(Number(value));
-                        }}
-                    >
-                        <SelectTrigger className="h-8 w-[70px]">
-                            <SelectValue
-                                placeholder={
-                                    table.getState().pagination.pageSize
-                                }
-                            />
-                        </SelectTrigger>
-                        <SelectContent side="top">
-                            {[10, 20, 30, 40, 50, 100, 200].map((pageSize) => (
-                                <SelectItem
-                                    key={pageSize}
-                                    value={`${pageSize}`}
-                                >
-                                    {pageSize}
-                                </SelectItem>
-                            ))}
-                        </SelectContent>
-                    </Select>
-                </div>
                 <div className="flex w-[100px] items-center justify-center text-sm font-medium">
                 <div className="flex w-[100px] items-center justify-center text-sm font-medium">
                     Page {table.getState().pagination.pageIndex + 1} of{" "}
                     Page {table.getState().pagination.pageIndex + 1} of{" "}
                     {table.getPageCount()}
                     {table.getPageCount()}

+ 227 - 0
src/components/Disable2FaForm.tsx

@@ -0,0 +1,227 @@
+"use client";
+
+import { useState } from "react";
+import { Button } from "@/components/ui/button";
+import { Input } from "@/components/ui/input";
+import { createApiClient } from "@app/api";
+import { useEnvContext } from "@app/hooks/useEnvContext";
+import { AxiosResponse } from "axios";
+import { Disable2faBody, Disable2faResponse } from "@server/routers/auth";
+import { z } from "zod";
+import { useForm } from "react-hook-form";
+import { zodResolver } from "@hookform/resolvers/zod";
+import {
+    Form,
+    FormControl,
+    FormField,
+    FormItem,
+    FormLabel,
+    FormMessage
+} from "@app/components/ui/form";
+import {
+    Credenza,
+    CredenzaBody,
+    CredenzaClose,
+    CredenzaContent,
+    CredenzaDescription,
+    CredenzaFooter,
+    CredenzaHeader,
+    CredenzaTitle
+} from "@app/components/Credenza";
+import { useToast } from "@app/hooks/useToast";
+import { formatAxiosError } from "@app/lib/utils";
+import { useUserContext } from "@app/hooks/useUserContext";
+import { InputOTP, InputOTPGroup, InputOTPSlot } from "./ui/input-otp";
+import { REGEXP_ONLY_DIGITS_AND_CHARS } from "input-otp";
+import { CheckCircle2 } from "lucide-react";
+
+const disableSchema = z.object({
+    password: z.string().min(1, { message: "Password is required" }),
+    code: z.string().min(1, { message: "Code is required" })
+});
+
+type Disable2FaProps = {
+    open: boolean;
+    setOpen: (val: boolean) => void;
+};
+
+export default function Disable2FaForm({ open, setOpen }: Disable2FaProps) {
+    const [loading, setLoading] = useState(false);
+
+    const [step, setStep] = useState<"password" | "success">("password");
+
+    const { toast } = useToast();
+
+    const { user, updateUser } = useUserContext();
+
+    const api = createApiClient(useEnvContext());
+
+    const disableForm = useForm<z.infer<typeof disableSchema>>({
+        resolver: zodResolver(disableSchema),
+        defaultValues: {
+            password: "",
+            code: ""
+        }
+    });
+
+    const request2fa = async (values: z.infer<typeof disableSchema>) => {
+        setLoading(true);
+
+        const res = await api
+            .post<AxiosResponse<Disable2faResponse>>(`/auth/2fa/disable`, {
+                password: values.password,
+                code: values.code
+            } as Disable2faBody)
+            .catch((e) => {
+                toast({
+                    title: "Unable to disable 2FA",
+                    description: formatAxiosError(
+                        e,
+                        "An error occurred while disabling 2FA"
+                    ),
+                    variant: "destructive"
+                });
+            });
+
+        if (res) {
+            // toast({
+            //     title: "Two-factor disabled",
+            //     description:
+            //         "Two-factor authentication has been disabled for your account"
+            // });
+            updateUser({ twoFactorEnabled: false });
+            setStep("success");
+        }
+
+        setLoading(false);
+    };
+
+    return (
+        <Credenza
+            open={open}
+            onOpenChange={(val) => {
+                setOpen(val);
+                setLoading(false);
+            }}
+        >
+            <CredenzaContent>
+                <CredenzaHeader>
+                    <CredenzaTitle>
+                        Disable Two-factor Authentication
+                    </CredenzaTitle>
+                    <CredenzaDescription>
+                        Disable two-factor authentication for your account
+                    </CredenzaDescription>
+                </CredenzaHeader>
+                <CredenzaBody>
+                    {step === "password" && (
+                        <Form {...disableForm}>
+                            <form
+                                onSubmit={disableForm.handleSubmit(request2fa)}
+                                className="space-y-4"
+                                id="form"
+                            >
+                                <div className="space-y-4">
+                                    <FormField
+                                        control={disableForm.control}
+                                        name="password"
+                                        render={({ field }) => (
+                                            <FormItem>
+                                                <FormLabel>Password</FormLabel>
+                                                <FormControl>
+                                                    <Input
+                                                        type="password"
+                                                        placeholder="Enter your password"
+                                                        {...field}
+                                                    />
+                                                </FormControl>
+                                                <FormMessage />
+                                            </FormItem>
+                                        )}
+                                    />
+
+                                    <FormField
+                                        control={disableForm.control}
+                                        name="code"
+                                        render={({ field }) => (
+                                            <FormItem>
+                                                <FormLabel>
+                                                    Authenticator Code
+                                                </FormLabel>
+                                                <FormControl>
+                                                    <InputOTP
+                                                        maxLength={6}
+                                                        {...field}
+                                                        pattern={
+                                                            REGEXP_ONLY_DIGITS_AND_CHARS
+                                                        }
+                                                    >
+                                                        <InputOTPGroup>
+                                                            <InputOTPSlot
+                                                                index={0}
+                                                            />
+                                                            <InputOTPSlot
+                                                                index={1}
+                                                            />
+                                                            <InputOTPSlot
+                                                                index={2}
+                                                            />
+                                                        </InputOTPGroup>
+                                                        <InputOTPGroup>
+                                                            <InputOTPSlot
+                                                                index={3}
+                                                            />
+                                                            <InputOTPSlot
+                                                                index={4}
+                                                            />
+                                                            <InputOTPSlot
+                                                                index={5}
+                                                            />
+                                                        </InputOTPGroup>
+                                                    </InputOTP>
+                                                </FormControl>
+                                                <FormMessage />
+                                            </FormItem>
+                                        )}
+                                    />
+                                </div>
+                            </form>
+                        </Form>
+                    )}
+
+                    {step === "success" && (
+                        <div className="space-y-4 text-center">
+                            <CheckCircle2
+                                className="mx-auto text-green-500"
+                                size={48}
+                            />
+                            <p className="font-semibold text-lg">
+                                Two-Factor Authentication Disabled
+                            </p>
+                            <p>
+                                Two-factor authentication has been disabled for
+                                your account. You can enable it again at any
+                                time.
+                            </p>
+                        </div>
+                    )}
+                </CredenzaBody>
+                <CredenzaFooter>
+                    {step === "password" && (
+                        <Button
+                            type="submit"
+                            form="form"
+                            loading={loading}
+                            disabled={loading}
+                        >
+                            Disable 2FA
+                        </Button>
+                    )}
+                    <CredenzaClose asChild>
+                        <Button variant="outline">Close</Button>
+                    </CredenzaClose>
+                </CredenzaFooter>
+            </CredenzaContent>
+        </Credenza>
+    );
+}

+ 9 - 7
src/components/Enable2FaForm.tsx

@@ -39,7 +39,7 @@ import { useToast } from "@app/hooks/useToast";
 import { formatAxiosError } from "@app/lib/utils";
 import { formatAxiosError } from "@app/lib/utils";
 import CopyTextBox from "@app/components/CopyTextBox";
 import CopyTextBox from "@app/components/CopyTextBox";
 import { QRCodeSVG } from "qrcode.react";
 import { QRCodeSVG } from "qrcode.react";
-import { userUserContext } from "@app/hooks/useUserContext";
+import { useUserContext } from "@app/hooks/useUserContext";
 
 
 const enableSchema = z.object({
 const enableSchema = z.object({
     password: z.string().min(1, { message: "Password is required" })
     password: z.string().min(1, { message: "Password is required" })
@@ -57,6 +57,7 @@ type Enable2FaProps = {
 export default function Enable2FaForm({ open, setOpen }: Enable2FaProps) {
 export default function Enable2FaForm({ open, setOpen }: Enable2FaProps) {
     const [step, setStep] = useState(1);
     const [step, setStep] = useState(1);
     const [secretKey, setSecretKey] = useState("");
     const [secretKey, setSecretKey] = useState("");
+    const [secretUri, setSecretUri] = useState("");
     const [verificationCode, setVerificationCode] = useState("");
     const [verificationCode, setVerificationCode] = useState("");
     const [error, setError] = useState("");
     const [error, setError] = useState("");
     const [success, setSuccess] = useState(false);
     const [success, setSuccess] = useState(false);
@@ -65,7 +66,7 @@ export default function Enable2FaForm({ open, setOpen }: Enable2FaProps) {
 
 
     const { toast } = useToast();
     const { toast } = useToast();
 
 
-    const { user, updateUser } = userUserContext();
+    const { user, updateUser } = useUserContext();
 
 
     const api = createApiClient(useEnvContext());
     const api = createApiClient(useEnvContext());
 
 
@@ -106,6 +107,7 @@ export default function Enable2FaForm({ open, setOpen }: Enable2FaProps) {
 
 
         if (res && res.data.data.secret) {
         if (res && res.data.data.secret) {
             setSecretKey(res.data.data.secret);
             setSecretKey(res.data.data.secret);
+            setSecretUri(res.data.data.uri);
             setStep(2);
             setStep(2);
         }
         }
 
 
@@ -132,7 +134,7 @@ export default function Enable2FaForm({ open, setOpen }: Enable2FaProps) {
 
 
         if (res && res.data.data.valid) {
         if (res && res.data.data.valid) {
             setBackupCodes(res.data.data.backupCodes || []);
             setBackupCodes(res.data.data.backupCodes || []);
-            updateUser({ twoFactorEnabled: true })
+            updateUser({ twoFactorEnabled: true });
             setStep(3);
             setStep(3);
         }
         }
 
 
@@ -203,11 +205,11 @@ export default function Enable2FaForm({ open, setOpen }: Enable2FaProps) {
                     {step === 2 && (
                     {step === 2 && (
                         <div className="space-y-4">
                         <div className="space-y-4">
                             <p>
                             <p>
-                                scan this qr code with your authenticator app or
+                                Scan this QR code with your authenticator app or
                                 enter the secret key manually:
                                 enter the secret key manually:
                             </p>
                             </p>
-                            <div classname="w-64 h-64 mx-auto flex items-center justify-center">
-                                <qrcodesvg value={secretkey} size={256} />
+                            <div className="w-64 h-64 mx-auto flex items-center justify-center">
+                                <QRCodeSVG value={secretUri} size={256} />
                             </div>
                             </div>
                             <div className="max-w-md mx-auto">
                             <div className="max-w-md mx-auto">
                                 <CopyTextBox
                                 <CopyTextBox
@@ -231,7 +233,7 @@ export default function Enable2FaForm({ open, setOpen }: Enable2FaProps) {
                                             render={({ field }) => (
                                             render={({ field }) => (
                                                 <FormItem>
                                                 <FormItem>
                                                     <FormLabel>
                                                     <FormLabel>
-                                                        Verification Code
+                                                        Authenticator Code
                                                     </FormLabel>
                                                     </FormLabel>
                                                     <FormControl>
                                                     <FormControl>
                                                         <Input
                                                         <Input

+ 8 - 3
src/components/Header.tsx

@@ -43,7 +43,8 @@ import Link from "next/link";
 import { useRouter } from "next/navigation";
 import { useRouter } from "next/navigation";
 import { useState } from "react";
 import { useState } from "react";
 import Enable2FaForm from "./Enable2FaForm";
 import Enable2FaForm from "./Enable2FaForm";
-import { userUserContext } from "@app/hooks/useUserContext";
+import { useUserContext } from "@app/hooks/useUserContext";
+import Disable2FaForm from "./Disable2FaForm";
 
 
 type HeaderProps = {
 type HeaderProps = {
     orgId?: string;
     orgId?: string;
@@ -54,7 +55,7 @@ export function Header({ orgId, orgs }: HeaderProps) {
     const { toast } = useToast();
     const { toast } = useToast();
     const { setTheme, theme } = useTheme();
     const { setTheme, theme } = useTheme();
 
 
-    const { user, updateUser } = userUserContext();
+    const { user, updateUser } = useUserContext();
 
 
     const [open, setOpen] = useState(false);
     const [open, setOpen] = useState(false);
     const [userTheme, setUserTheme] = useState<"light" | "dark" | "system">(
     const [userTheme, setUserTheme] = useState<"light" | "dark" | "system">(
@@ -62,6 +63,7 @@ export function Header({ orgId, orgs }: HeaderProps) {
     );
     );
 
 
     const [openEnable2fa, setOpenEnable2fa] = useState(false);
     const [openEnable2fa, setOpenEnable2fa] = useState(false);
+    const [openDisable2fa, setOpenDisable2fa] = useState(false);
 
 
     const router = useRouter();
     const router = useRouter();
 
 
@@ -93,6 +95,7 @@ export function Header({ orgId, orgs }: HeaderProps) {
     return (
     return (
         <>
         <>
             <Enable2FaForm open={openEnable2fa} setOpen={setOpenEnable2fa} />
             <Enable2FaForm open={openEnable2fa} setOpen={setOpenEnable2fa} />
+            <Disable2FaForm open={openDisable2fa} setOpen={setOpenDisable2fa} />
 
 
             <div className="flex items-center justify-between">
             <div className="flex items-center justify-between">
                 <div className="flex items-center gap-4">
                 <div className="flex items-center gap-4">
@@ -133,7 +136,9 @@ export function Header({ orgId, orgs }: HeaderProps) {
                                 </DropdownMenuItem>
                                 </DropdownMenuItem>
                             )}
                             )}
                             {user.twoFactorEnabled && (
                             {user.twoFactorEnabled && (
-                                <DropdownMenuItem>
+                                <DropdownMenuItem
+                                    onClick={() => setOpenDisable2fa(true)}
+                                >
                                     <span>Disable Two-factor</span>
                                     <span>Disable Two-factor</span>
                                 </DropdownMenuItem>
                                 </DropdownMenuItem>
                             )}
                             )}

+ 108 - 82
src/components/LoginForm.tsx

@@ -103,8 +103,6 @@ export default function LoginForm({ redirect, onLogin }: LoginFormProps) {
 
 
             const data = res.data.data;
             const data = res.data.data;
 
 
-            console.log(data);
-
             if (data?.codeRequested) {
             if (data?.codeRequested) {
                 setMfaRequested(true);
                 setMfaRequested(true);
                 setLoading(false);
                 setLoading(false);
@@ -136,6 +134,7 @@ export default function LoginForm({ redirect, onLogin }: LoginFormProps) {
                     <form
                     <form
                         onSubmit={form.handleSubmit(onSubmit)}
                         onSubmit={form.handleSubmit(onSubmit)}
                         className="space-y-4"
                         className="space-y-4"
+                        id="form"
                     >
                     >
                         <FormField
                         <FormField
                             control={form.control}
                             control={form.control}
@@ -182,93 +181,120 @@ export default function LoginForm({ redirect, onLogin }: LoginFormProps) {
                                 </Link>
                                 </Link>
                             </div>
                             </div>
                         </div>
                         </div>
-
-                        {error && (
-                            <Alert variant="destructive">
-                                <AlertDescription>{error}</AlertDescription>
-                            </Alert>
-                        )}
-                        <Button
-                            type="submit"
-                            className="w-full"
-                            loading={loading}
-                        >
-                            <LockIcon className="w-4 h-4 mr-2" />
-                            Login
-                        </Button>
                     </form>
                     </form>
                 </Form>
                 </Form>
             )}
             )}
 
 
             {mfaRequested && (
             {mfaRequested && (
-                <Form {...mfaForm}>
-                    <form
-                        onSubmit={mfaForm.handleSubmit(onSubmit)}
-                        className="space-y-4"
-                    >
-                        <FormField
-                            control={mfaForm.control}
-                            name="code"
-                            render={({ field }) => (
-                                <FormItem>
-                                    <FormLabel>Authenticator Code</FormLabel>
-                                    <FormControl>
-                                        <div className="flex justify-center">
-                                            <InputOTP
-                                                maxLength={6}
-                                                {...field}
-                                                pattern={
-                                                    REGEXP_ONLY_DIGITS_AND_CHARS
-                                                }
-                                            >
-                                                <InputOTPGroup>
-                                                    <InputOTPSlot index={0} />
-                                                    <InputOTPSlot index={1} />
-                                                    <InputOTPSlot index={2} />
-                                                </InputOTPGroup>
-                                                <InputOTPSeparator />
-                                                <InputOTPGroup>
-                                                    <InputOTPSlot index={3} />
-                                                    <InputOTPSlot index={4} />
-                                                    <InputOTPSlot index={5} />
-                                                </InputOTPGroup>
-                                            </InputOTP>
-                                        </div>
-                                    </FormControl>
-                                    <FormMessage />
-                                </FormItem>
-                            )}
-                        />
-                        {error && (
-                            <Alert variant="destructive">
-                                <AlertDescription>{error}</AlertDescription>
-                            </Alert>
-                        )}
+                <>
+                    <div className="text-center">
+                        <h3 className="text-lg font-medium">
+                            Two-Factor Authentication
+                        </h3>
+                        <p className="text-sm text-muted-foreground">
+                            Enter the code from your authenticator app.
+                        </p>
+                    </div>
+                    <Form {...mfaForm}>
+                        <form
+                            onSubmit={mfaForm.handleSubmit(onSubmit)}
+                            className="space-y-4"
+                            id="form"
+                        >
+                            <FormField
+                                control={mfaForm.control}
+                                name="code"
+                                render={({ field }) => (
+                                    <FormItem>
+                                        <FormControl>
+                                            <div className="flex justify-center">
+                                                <InputOTP
+                                                    maxLength={6}
+                                                    {...field}
+                                                    pattern={
+                                                        REGEXP_ONLY_DIGITS_AND_CHARS
+                                                    }
+                                                >
+                                                    <InputOTPGroup>
+                                                        <InputOTPSlot
+                                                            index={0}
+                                                        />
+                                                        <InputOTPSlot
+                                                            index={1}
+                                                        />
+                                                        <InputOTPSlot
+                                                            index={2}
+                                                        />
+                                                    </InputOTPGroup>
+                                                    <InputOTPGroup>
+                                                        <InputOTPSlot
+                                                            index={3}
+                                                        />
+                                                        <InputOTPSlot
+                                                            index={4}
+                                                        />
+                                                        <InputOTPSlot
+                                                            index={5}
+                                                        />
+                                                    </InputOTPGroup>
+                                                </InputOTP>
+                                            </div>
+                                        </FormControl>
+                                        <FormMessage />
+                                    </FormItem>
+                                )}
+                            />
+                        </form>
+                    </Form>
+                </>
+            )}
 
 
-                        <div className="space-y-4">
-                            <Button
-                                type="submit"
-                                className="w-full"
-                                loading={loading}
-                            >
-                                <LockIcon className="w-4 h-4 mr-2" />
-                                Submit Code
-                            </Button>
-                            <Button
-                                type="button"
-                                className="w-full"
-                                variant="outline"
-                                onClick={() => {
-                                    setMfaRequested(false);
-                                    mfaForm.reset();
-                                }}
-                            >
-                                Back to Login
-                            </Button>
-                        </div>
-                    </form>
-                </Form>
+            {error && (
+                <Alert variant="destructive">
+                    <AlertDescription>{error}</AlertDescription>
+                </Alert>
             )}
             )}
+
+            <div className="space-y-4">
+                {mfaRequested && (
+                    <Button
+                        type="submit"
+                        form="form"
+                        className="w-full"
+                        loading={loading}
+                        disabled={loading}
+                    >
+                        Submit Code
+                    </Button>
+                )}
+
+                {!mfaRequested && (
+                    <Button
+                        type="submit"
+                        form="form"
+                        className="w-full"
+                        loading={loading}
+                        disabled={loading}
+                    >
+                        <LockIcon className="w-4 h-4 mr-2" />
+                        Login
+                    </Button>
+                )}
+
+                {mfaRequested && (
+                    <Button
+                        type="button"
+                        className="w-full"
+                        variant="outline"
+                        onClick={() => {
+                            setMfaRequested(false);
+                            mfaForm.reset();
+                        }}
+                    >
+                        Back to Login
+                    </Button>
+                )}
+            </div>
         </div>
         </div>
     );
     );
 }
 }

+ 25 - 25
src/components/ui/input.tsx

@@ -10,18 +10,10 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
         const [showPassword, setShowPassword] = React.useState(false);
         const [showPassword, setShowPassword] = React.useState(false);
         const togglePasswordVisibility = () => setShowPassword(!showPassword);
         const togglePasswordVisibility = () => setShowPassword(!showPassword);
 
 
-        console.log("type", type);
-
-        return (
+        return type === "password" ? (
             <div className="relative">
             <div className="relative">
                 <input
                 <input
-                    type={
-                        type === "password"
-                            ? showPassword
-                                ? "text"
-                                : "password"
-                            : type
-                    }
+                    type={showPassword ? "text" : "password"}
                     className={cn(
                     className={cn(
                         "flex h-9 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
                         "flex h-9 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
                         className
                         className
@@ -29,22 +21,30 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
                     ref={ref}
                     ref={ref}
                     {...props}
                     {...props}
                 />
                 />
-                {type === "password" && (
-                    <div className="absolute inset-y-0 right-0 flex cursor-pointer items-center pr-3 text-gray-400">
-                        {showPassword ? (
-                            <EyeOff
-                                className="h-4 w-4"
-                                onClick={togglePasswordVisibility}
-                            />
-                        ) : (
-                            <Eye
-                                className="h-4 w-4"
-                                onClick={togglePasswordVisibility}
-                            />
-                        )}
-                    </div>
-                )}
+                <div className="absolute inset-y-0 right-0 flex cursor-pointer items-center pr-3 text-gray-400">
+                    {showPassword ? (
+                        <EyeOff
+                            className="h-4 w-4"
+                            onClick={togglePasswordVisibility}
+                        />
+                    ) : (
+                        <Eye
+                            className="h-4 w-4"
+                            onClick={togglePasswordVisibility}
+                        />
+                    )}
+                </div>
             </div>
             </div>
+        ) : (
+            <input
+                type={type}
+                className={cn(
+                    "flex h-9 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
+                    className
+                )}
+                ref={ref}
+                {...props}
+            />
         );
         );
     }
     }
 );
 );

+ 1 - 1
src/hooks/useUserContext.ts

@@ -1,7 +1,7 @@
 import UserContext from "@app/contexts/userContext";
 import UserContext from "@app/contexts/userContext";
 import { useContext } from "react";
 import { useContext } from "react";
 
 
-export function userUserContext() {
+export function useUserContext() {
     const context = useContext(UserContext);
     const context = useContext(UserContext);
     if (context === undefined) {
     if (context === undefined) {
         throw new Error("useUserContext must be used within a UserProvider");
         throw new Error("useUserContext must be used within a UserProvider");