disable 2fa and end email notifications

This commit is contained in:
Milo Schwartz 2024-12-24 15:36:55 -05:00
parent ccc2e3358c
commit cf75be5a6c
No known key found for this signature in database
14 changed files with 555 additions and 173 deletions

View file

@ -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;

View file

@ -11,11 +11,16 @@ 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.object({ export const disable2faBody = z
password: z.string(), .object({
code: z.string().optional(), password: z.string(),
}).strict(); code: z.string().optional()
})
.strict();
export type Disable2faBody = z.infer<typeof disable2faBody>; export type Disable2faBody = z.infer<typeof disable2faBody>;
@ -26,7 +31,7 @@ export type Disable2faResponse = {
export async function disable2fa( export async function disable2fa(
req: Request, req: Request,
res: Response, res: Response,
next: NextFunction, next: NextFunction
): Promise<any> { ): Promise<any> {
const parsedBody = disable2faBody.safeParse(req.body); const parsedBody = disable2faBody.safeParse(req.body);
@ -34,8 +39,8 @@ export async function disable2fa(
return next( return next(
createHttpError( createHttpError(
HttpCode.BAD_REQUEST, HttpCode.BAD_REQUEST,
fromError(parsedBody.error).toString(), fromError(parsedBody.error).toString()
), )
); );
} }
@ -52,8 +57,8 @@ export async function disable2fa(
return next( return next(
createHttpError( createHttpError(
HttpCode.BAD_REQUEST, HttpCode.BAD_REQUEST,
"Two-factor authentication is already disabled", "Two-factor authentication is already disabled"
), )
); );
} else { } else {
if (!code) { if (!code) {
@ -62,7 +67,7 @@ export async function disable2fa(
success: true, success: true,
error: false, error: false,
message: "Two-factor authentication required", message: "Two-factor authentication required",
status: HttpCode.ACCEPTED, status: HttpCode.ACCEPTED
}); });
} }
} }
@ -70,15 +75,15 @@ export async function disable2fa(
const validOTP = await verifyTotpCode( const validOTP = await verifyTotpCode(
code, code,
user.twoFactorSecret!, user.twoFactorSecret!,
user.userId, user.userId
); );
if (!validOTP) { if (!validOTP) {
return next( return next(
createHttpError( createHttpError(
HttpCode.BAD_REQUEST, HttpCode.BAD_REQUEST,
"The two-factor code you entered is incorrect", "The two-factor code you entered is incorrect"
), )
); );
} }
@ -91,22 +96,32 @@ export async function disable2fa(
.delete(twoFactorBackupCodes) .delete(twoFactorBackupCodes)
.where(eq(twoFactorBackupCodes.userId, user.userId)); .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,
success: true, success: true,
error: false, error: false,
message: "Two-factor authentication disabled", message: "Two-factor authentication disabled",
status: HttpCode.OK, status: HttpCode.OK
}); });
} catch (error) { } catch (error) {
logger.error(error); logger.error(error);
return next( return next(
createHttpError( createHttpError(
HttpCode.INTERNAL_SERVER_ERROR, HttpCode.INTERNAL_SERVER_ERROR,
"Failed to disable two-factor authentication", "Failed to disable two-factor authentication"
), )
); );
} }
} }

View file

@ -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,

View file

@ -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({
@ -90,8 +93,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(
@ -101,6 +102,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,

View file

@ -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();

View file

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

View file

@ -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} /> */}
</>
);
}

View file

@ -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()}

View file

@ -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>
);
}

View file

@ -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"> <div className="w-64 h-64 mx-auto flex items-center justify-center">
<qrcodesvg value={secretkey} size={256} /> <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

View file

@ -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>
)} )}

View file

@ -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 <div className="text-center">
onSubmit={mfaForm.handleSubmit(onSubmit)} <h3 className="text-lg font-medium">
className="space-y-4" Two-Factor Authentication
> </h3>
<FormField <p className="text-sm text-muted-foreground">
control={mfaForm.control} Enter the code from your authenticator app.
name="code" </p>
render={({ field }) => ( </div>
<FormItem> <Form {...mfaForm}>
<FormLabel>Authenticator Code</FormLabel> <form
<FormControl> onSubmit={mfaForm.handleSubmit(onSubmit)}
<div className="flex justify-center"> className="space-y-4"
<InputOTP id="form"
maxLength={6} >
{...field} <FormField
pattern={ control={mfaForm.control}
REGEXP_ONLY_DIGITS_AND_CHARS name="code"
} render={({ field }) => (
> <FormItem>
<InputOTPGroup> <FormControl>
<InputOTPSlot index={0} /> <div className="flex justify-center">
<InputOTPSlot index={1} /> <InputOTP
<InputOTPSlot index={2} /> maxLength={6}
</InputOTPGroup> {...field}
<InputOTPSeparator /> pattern={
<InputOTPGroup> REGEXP_ONLY_DIGITS_AND_CHARS
<InputOTPSlot index={3} /> }
<InputOTPSlot index={4} /> >
<InputOTPSlot index={5} /> <InputOTPGroup>
</InputOTPGroup> <InputOTPSlot
</InputOTP> index={0}
</div> />
</FormControl> <InputOTPSlot
<FormMessage /> index={1}
</FormItem> />
)} <InputOTPSlot
/> index={2}
{error && ( />
<Alert variant="destructive"> </InputOTPGroup>
<AlertDescription>{error}</AlertDescription> <InputOTPGroup>
</Alert> <InputOTPSlot
)} index={3}
/>
<div className="space-y-4"> <InputOTPSlot
<Button index={4}
type="submit" />
className="w-full" <InputOTPSlot
loading={loading} index={5}
> />
<LockIcon className="w-4 h-4 mr-2" /> </InputOTPGroup>
Submit Code </InputOTP>
</Button> </div>
<Button </FormControl>
type="button" <FormMessage />
className="w-full" </FormItem>
variant="outline" )}
onClick={() => { />
setMfaRequested(false); </form>
mfaForm.reset(); </Form>
}} </>
>
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>
); );
} }

View file

@ -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 type === "password" ? (
return (
<div className="relative"> <div className="relative">
<input <input
type={ type={showPassword ? "text" : "password"}
type === "password"
? showPassword
? "text"
: "password"
: type
}
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">
<div className="absolute inset-y-0 right-0 flex cursor-pointer items-center pr-3 text-gray-400"> {showPassword ? (
{showPassword ? ( <EyeOff
<EyeOff className="h-4 w-4"
className="h-4 w-4" onClick={togglePasswordVisibility}
onClick={togglePasswordVisibility} />
/> ) : (
) : ( <Eye
<Eye className="h-4 w-4"
className="h-4 w-4" onClick={togglePasswordVisibility}
onClick={togglePasswordVisibility} />
/> )}
)} </div>
</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}
/>
); );
} }
); );

View file

@ -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");