Remove multiple components related to multi-factor authentication setup

Removed personal account related multi-factor authentication setup modal and otp-input. Adjusted dependencies, exports, and imports to reflect the deletion. Various adjustments in other areas of the codebase were made to account for these deletions, including moving necessary components and adding the 'input-otp' library in the package.json under 'ui' directory.
This commit is contained in:
giancarlo
2024-03-28 01:30:43 +08:00
parent 500fea4bf8
commit 6048cc4759
21 changed files with 1078 additions and 630 deletions

View File

@@ -1,3 +1,5 @@
'use client';
import {
Card,
CardContent,
@@ -9,10 +11,11 @@ import { If } from '@kit/ui/if';
import { Trans } from '@kit/ui/trans';
import { AccountDangerZone } from './account-danger-zone';
import { UpdateEmailFormContainer } from './email/update-email-form-container';
import { MultiFactorAuthFactorsList } from './mfa/multi-factor-auth-list';
import { UpdatePasswordFormContainer } from './password/update-password-container';
import { UpdateAccountDetailsFormContainer } from './update-account-details-form-container';
import { UpdateAccountImageContainer } from './update-account-image-container';
import { UpdateEmailFormContainer } from './update-email-form-container';
import { UpdatePasswordFormContainer } from './update-password-container';
export function PersonalAccountSettingsContainer(
props: React.PropsWithChildren<{
@@ -91,6 +94,22 @@ export function PersonalAccountSettingsContainer(
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>
<Trans i18nKey={'account:multiFactorAuth'} />
</CardTitle>
<CardDescription>
<Trans i18nKey={'account:multiFactorAuthDescription'} />
</CardDescription>
</CardHeader>
<CardContent>
<MultiFactorAuthFactorsList />
</CardContent>
</Card>
<If condition={props.features.enableAccountDeletion}>
<Card className={'border-destructive border-2'}>
<CardHeader>

View File

@@ -22,7 +22,7 @@ import { If } from '@kit/ui/if';
import { Input } from '@kit/ui/input';
import { Trans } from '@kit/ui/trans';
import { UpdateEmailSchema } from '../../schema/update-email.schema';
import { UpdateEmailSchema } from '../../../schema/update-email.schema';
function createEmailResolver(currentEmail: string, errorMessage: string) {
return zodResolver(

View File

@@ -0,0 +1,279 @@
import { useCallback, useState } from 'react';
import type { Factor } from '@supabase/gotrue-js';
import { ExclamationTriangleIcon } from '@radix-ui/react-icons';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { X } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import { toast } from 'sonner';
import useFetchAuthFactors from '@kit/supabase/hooks/use-fetch-mfa-factors';
import { useSupabase } from '@kit/supabase/hooks/use-supabase';
import { useFactorsMutationKey } from '@kit/supabase/hooks/use-user-factors-mutation-key';
import { Alert, AlertDescription, AlertTitle } from '@kit/ui/alert';
import {
AlertDialog,
AlertDialogAction,
AlertDialogContent,
AlertDialogDescription,
AlertDialogHeader,
AlertDialogTitle,
} from '@kit/ui/alert-dialog';
import { Badge } from '@kit/ui/badge';
import { Button } from '@kit/ui/button';
import { If } from '@kit/ui/if';
import Spinner from '@kit/ui/spinner';
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@kit/ui/table';
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from '@kit/ui/tooltip';
import { Trans } from '@kit/ui/trans';
import { MultiFactorAuthSetupDialog } from './multi-factor-auth-setup-dialog';
const MAX_FACTOR_COUNT = 10;
export function MultiFactorAuthFactorsList() {
const { data: factors, isLoading, isError } = useFetchAuthFactors();
const [unEnrolling, setUnenrolling] = useState<string>();
if (isLoading) {
return (
<div className={'flex items-center space-x-4'}>
<Spinner />
<div>
<Trans i18nKey={'account:loadingFactors'} />
</div>
</div>
);
}
if (isError) {
return (
<div>
<Alert variant={'destructive'}>
<ExclamationTriangleIcon className={'h-4'} />
<AlertTitle>
<Trans i18nKey={'account:factorsListError'} />
</AlertTitle>
<AlertDescription>
<Trans i18nKey={'account:factorsListErrorDescription'} />
</AlertDescription>
</Alert>
</div>
);
}
const allFactors = factors?.all ?? [];
if (!allFactors.length) {
return (
<div className={'flex flex-col space-y-4'}>
<Alert variant={'info'}>
<AlertTitle>
<Trans i18nKey={'account:multiFactorAuthHeading'} />
</AlertTitle>
<AlertDescription>
<Trans i18nKey={'account:multiFactorAuthDescription'} />
</AlertDescription>
</Alert>
<div>
<MultiFactorAuthSetupDialog />
</div>
</div>
);
}
const canAddNewFactors = allFactors.length < MAX_FACTOR_COUNT;
return (
<div className={'flex flex-col space-y-4'}>
<FactorsTable factors={allFactors} setUnenrolling={setUnenrolling} />
<If condition={canAddNewFactors}>
<div>
<MultiFactorAuthSetupDialog />
</div>
</If>
<If condition={unEnrolling}>
{(factorId) => (
<ConfirmUnenrollFactorModal
factorId={factorId}
setIsModalOpen={() => setUnenrolling(undefined)}
/>
)}
</If>
</div>
);
}
function ConfirmUnenrollFactorModal(
props: React.PropsWithChildren<{
factorId: string;
setIsModalOpen: (isOpen: boolean) => void;
}>,
) {
const { t } = useTranslation();
const unEnroll = useUnenrollFactor();
const onUnenrollRequested = useCallback(
(factorId: string) => {
if (unEnroll.isPending) return;
const promise = unEnroll.mutateAsync(factorId).then(() => {
props.setIsModalOpen(false);
});
toast.promise(promise, {
loading: t(`account:unenrollingFactor`),
success: t(`account:unenrollFactorSuccess`),
error: t(`account:unenrollFactorError`),
});
},
[props, t, unEnroll],
);
return (
<AlertDialog open={!!props.factorId} onOpenChange={props.setIsModalOpen}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>
<Trans i18nKey={'account:unenrollFactorModalHeading'} />
</AlertDialogTitle>
<AlertDialogDescription>
<Trans i18nKey={'account:unenrollFactorModalDescription'} />
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogAction
className={'w-full'}
type={'button'}
disabled={unEnroll.isPending}
onClick={() => onUnenrollRequested(props.factorId)}
>
<Trans i18nKey={'account:unenrollFactorModalButtonLabel'} />
</AlertDialogAction>
</AlertDialogContent>
</AlertDialog>
);
}
function FactorsTable({
setUnenrolling,
factors,
}: React.PropsWithChildren<{
setUnenrolling: (factorId: string) => void;
factors: Factor[];
}>) {
return (
<Table>
<TableHeader>
<TableRow>
<TableHead>
<Trans i18nKey={'account:factorName'} />
</TableHead>
<TableHead>
<Trans i18nKey={'account:factorType'} />
</TableHead>
<TableHead>
<Trans i18nKey={'account:factorStatus'} />
</TableHead>
<TableHead />
</TableRow>
</TableHeader>
<TableBody>
{factors.map((factor) => (
<TableRow key={factor.id}>
<TableCell>
<span className={'block truncate'}>{factor.friendly_name}</span>
</TableCell>
<TableCell>
<Badge variant={'info'} className={'inline-flex uppercase'}>
{factor.factor_type}
</Badge>
</TableCell>
<td>
<Badge
variant={'info'}
className={'inline-flex capitalize'}
color={factor.status === 'verified' ? 'success' : 'normal'}
>
{factor.status}
</Badge>
</td>
<td className={'flex justify-end'}>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant={'ghost'}
size={'icon'}
onClick={() => setUnenrolling(factor.id)}
>
<X className={'h-4'} />
</Button>
</TooltipTrigger>
<TooltipContent>
<Trans i18nKey={'account:unenrollTooltip'} />
</TooltipContent>
</Tooltip>
</TooltipProvider>
</td>
</TableRow>
))}
</TableBody>
</Table>
);
}
function useUnenrollFactor() {
const queryClient = useQueryClient();
const client = useSupabase();
const mutationKey = useFactorsMutationKey();
const mutationFn = async (factorId: string) => {
const { data, error } = await client.auth.mfa.unenroll({
factorId,
});
if (error) {
throw error;
}
return data;
};
return useMutation({
mutationFn,
mutationKey,
onSuccess: async () => {
return queryClient.refetchQueries({
queryKey: mutationKey,
});
},
});
}

View File

@@ -0,0 +1,450 @@
'use client';
import { useCallback, useState } from 'react';
import Image from 'next/image';
import { zodResolver } from '@hookform/resolvers/zod';
import { ExclamationTriangleIcon } from '@radix-ui/react-icons';
import { useMutation } from '@tanstack/react-query';
import { useForm } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import { toast } from 'sonner';
import { z } from 'zod';
import { useSupabase } from '@kit/supabase/hooks/use-supabase';
import { useFactorsMutationKey } from '@kit/supabase/hooks/use-user-factors-mutation-key';
import { Alert, AlertDescription, AlertTitle } from '@kit/ui/alert';
import { Button } from '@kit/ui/button';
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from '@kit/ui/dialog';
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@kit/ui/form';
import { If } from '@kit/ui/if';
import { Input } from '@kit/ui/input';
import {
InputOTP,
InputOTPGroup,
InputOTPSeparator,
InputOTPSlot,
} from '@kit/ui/input-otp';
import { Trans } from '@kit/ui/trans';
import { refreshAuthSession } from '../../../server/personal-accounts-server-actions';
export function MultiFactorAuthSetupDialog() {
const { t } = useTranslation();
const [isOpen, setIsOpen] = useState(false);
const onEnrollSuccess = useCallback(() => {
setIsOpen(false);
return toast.success(t(`multiFactorSetupSuccess`));
}, [t]);
return (
<>
<Button onClick={() => setIsOpen(true)}>
<Trans i18nKey={'account:setupMfaButtonLabel'} />
</Button>
<Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogContent
onInteractOutside={(e) => e.preventDefault()}
onEscapeKeyDown={(e) => e.preventDefault()}
>
<DialogHeader>
<DialogTitle>
<Trans i18nKey={'account:setupMfaButtonLabel'} />
</DialogTitle>
<DialogDescription>
<Trans i18nKey={'account:multiFactorAuthDescription'} />
</DialogDescription>
</DialogHeader>
<div>
<MultiFactorAuthSetupForm
onCancel={() => setIsOpen(false)}
onEnrolled={onEnrollSuccess}
/>
</div>
</DialogContent>
</Dialog>
</>
);
}
function MultiFactorAuthSetupForm({
onEnrolled,
onCancel,
}: React.PropsWithChildren<{
onCancel: () => void;
onEnrolled: () => void;
}>) {
const verifyCodeMutation = useVerifyCodeMutation();
const verificationCodeForm = useForm({
resolver: zodResolver(
z.object({
factorId: z.string().min(1),
verificationCode: z.string().min(6).max(6),
}),
),
defaultValues: {
factorId: '',
verificationCode: '',
},
});
const [state, setState] = useState({
loading: false,
error: '',
});
const onSubmit = useCallback(
async ({
verificationCode,
factorId,
}: {
verificationCode: string;
factorId: string;
}) => {
setState({
loading: true,
error: '',
});
try {
await verifyCodeMutation.mutateAsync({
factorId,
code: verificationCode,
});
await refreshAuthSession();
setState({
loading: false,
error: '',
});
onEnrolled();
} catch (error) {
const message = (error as Error).message || `Unknown error`;
setState({
loading: false,
error: message,
});
}
},
[onEnrolled, verifyCodeMutation],
);
if (state.error) {
return <ErrorAlert />;
}
return (
<div className={'flex flex-col space-y-4'}>
<div className={'flex justify-center'}>
<FactorQrCode
onCancel={onCancel}
onSetFactorId={(factorId) =>
verificationCodeForm.setValue('factorId', factorId)
}
/>
</div>
<If condition={verificationCodeForm.watch('factorId')}>
<Form {...verificationCodeForm}>
<form
onSubmit={verificationCodeForm.handleSubmit(onSubmit)}
className={'w-full'}
>
<div className={'flex flex-col space-y-4'}>
<FormField
render={({ field }) => {
return (
<FormItem
className={
'mx-auto flex flex-col items-center justify-center'
}
>
<FormControl>
<InputOTP {...field} maxLength={6} minLength={6}>
<InputOTPGroup>
<InputOTPSlot index={0} />
<InputOTPSlot index={1} />
<InputOTPSlot index={2} />
</InputOTPGroup>
<InputOTPSeparator />
<InputOTPGroup>
<InputOTPSlot index={3} />
<InputOTPSlot index={4} />
<InputOTPSlot index={5} />
</InputOTPGroup>
</InputOTP>
</FormControl>
<FormDescription>
<Trans
i18nKey={'account:verifyActivationCodeDescription'}
/>
</FormDescription>
<FormMessage />
</FormItem>
);
}}
name={'verificationCode'}
/>
<Button
disabled={!verificationCodeForm.formState.isValid}
type={'submit'}
>
{state.loading ? (
<Trans i18nKey={'account:verifyingCode'} />
) : (
<Trans i18nKey={'account:enableMfaFactor'} />
)}
</Button>
<Button type={'button'} variant={'ghost'} onClick={onCancel}>
<Trans i18nKey={'common:cancel'} />
</Button>
</div>
</form>
</Form>
</If>
</div>
);
}
function FactorQrCode({
onSetFactorId,
onCancel,
}: React.PropsWithChildren<{
onCancel: () => void;
onSetFactorId: (factorId: string) => void;
}>) {
const enrollFactorMutation = useEnrollFactor();
const [error, setError] = useState(false);
const form = useForm({
resolver: zodResolver(
z.object({
factorName: z.string().min(1),
qrCode: z.string().min(1),
}),
),
defaultValues: {
factorName: '',
qrCode: '',
},
});
const factorName = form.watch('factorName');
if (error) {
return (
<div className={'flex w-full flex-col space-y-2'}>
<Alert variant={'destructive'}>
<ExclamationTriangleIcon className={'h-4'} />
<AlertTitle>
<Trans i18nKey={'account:qrCodeErrorHeading'} />
</AlertTitle>
<AlertDescription>
<Trans i18nKey={'account:qrCodeErrorDescription'} />
</AlertDescription>
</Alert>
</div>
);
}
if (!factorName) {
return (
<FactorNameForm
onCancel={onCancel}
onSetFactorName={async (name) => {
const data = await enrollFactorMutation.mutateAsync(name);
if (!data) {
return setError(true);
}
form.setValue('factorName', name);
form.setValue('qrCode', data.totp.qr_code);
// dispatch event to set factor ID
onSetFactorId(data.id);
}}
/>
);
}
return (
<div className={'flex flex-col space-y-4'}>
<p>
<span className={'text-muted-foreground text-sm'}>
<Trans i18nKey={'account:multiFactorModalHeading'} />
</span>
</p>
<div className={'flex justify-center'}>
<QrImage src={form.getValues('qrCode')} />
</div>
</div>
);
}
function FactorNameForm(
props: React.PropsWithChildren<{
onSetFactorName: (name: string) => void;
onCancel: () => void;
}>,
) {
const form = useForm({
resolver: zodResolver(
z.object({
name: z.string().min(1),
}),
),
defaultValues: {
name: '',
},
});
return (
<Form {...form}>
<form
className={'w-full'}
onSubmit={form.handleSubmit((data) => {
props.onSetFactorName(data.name);
})}
>
<div className={'flex flex-col space-y-4'}>
<FormField
name={'name'}
render={({ field }) => {
return (
<FormItem>
<FormLabel>
<Trans i18nKey={'account:factorNameLabel'} />
</FormLabel>
<FormControl>
<Input autoComplete={'off'} required {...field} />
</FormControl>
<FormDescription>
<Trans i18nKey={'account:factorNameHint'} />
</FormDescription>
<FormMessage />
</FormItem>
);
}}
/>
<Button type={'submit'}>
<Trans i18nKey={'account:factorNameSubmitLabel'} />
</Button>
<Button type={'button'} variant={'ghost'} onClick={props.onCancel}>
<Trans i18nKey={'common:cancel'} />
</Button>
</div>
</form>
</Form>
);
}
function QrImage({ src }: { src: string }) {
return <Image alt={'QR Code'} src={src} width={160} height={160} />;
}
function useEnrollFactor() {
const client = useSupabase();
const mutationKey = useFactorsMutationKey();
const mutationFn = async (factorName: string) => {
const { data, error } = await client.auth.mfa.enroll({
friendlyName: factorName,
factorType: 'totp',
});
if (error) {
throw error;
}
return data;
};
return useMutation({
mutationFn,
mutationKey,
});
}
function useVerifyCodeMutation() {
const mutationKey = useFactorsMutationKey();
const client = useSupabase();
const mutationFn = async (params: { factorId: string; code: string }) => {
const challenge = await client.auth.mfa.challenge({
factorId: params.factorId,
});
if (challenge.error) {
throw challenge.error;
}
const challengeId = challenge.data.id;
const verify = await client.auth.mfa.verify({
factorId: params.factorId,
code: params.code,
challengeId,
});
if (verify.error) {
throw verify.error;
}
return verify;
};
return useMutation({ mutationKey, mutationFn });
}
function ErrorAlert() {
return (
<Alert variant={'destructive'}>
<ExclamationTriangleIcon className={'h-4'} />
<AlertTitle>
<Trans i18nKey={'account:multiFactorSetupErrorHeading'} />
</AlertTitle>
<AlertDescription>
<Trans i18nKey={'account:multiFactorSetupErrorDescription'} />
</AlertDescription>
</Alert>
);
}

View File

@@ -1,344 +0,0 @@
import React, { useCallback, useEffect, useState } from 'react';
import Image from 'next/image';
import { useMutation } from '@tanstack/react-query';
import { useTranslation } from 'react-i18next';
import { toast } from 'sonner';
import { useSupabase } from '@kit/supabase/hooks/use-supabase';
import { useFactorsMutationKey } from '@kit/supabase/hooks/use-user-factors-mutation-key';
import { Alert } from '@kit/ui/alert';
import { Button } from '@kit/ui/button';
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
} from '@kit/ui/dialog';
import { If } from '@kit/ui/if';
import { Input } from '@kit/ui/input';
import { Label } from '@kit/ui/label';
import { OtpInput } from '@kit/ui/otp-input';
import { Trans } from '@kit/ui/trans';
function MultiFactorAuthSetupModal(
props: React.PropsWithChildren<{
isOpen: boolean;
setIsOpen: (isOpen: boolean) => void;
}>,
) {
const { t } = useTranslation();
const onEnrollSuccess = useCallback(() => {
props.setIsOpen(false);
return toast.success(t(`multiFactorSetupSuccess`));
}, [props, t]);
return (
<Dialog open={props.isOpen} onOpenChange={props.setIsOpen}>
<DialogContent>
<DialogHeader>
<DialogTitle>
<Trans i18nKey={'account:setupMfaButtonLabel'} />
</DialogTitle>
</DialogHeader>
<MultiFactorAuthSetupForm
onCancel={() => props.setIsOpen(false)}
onEnrolled={onEnrollSuccess}
/>
</DialogContent>
</Dialog>
);
}
function MultiFactorAuthSetupForm({
onEnrolled,
onCancel,
}: React.PropsWithChildren<{
onCancel: () => void;
onEnrolled: () => void;
}>) {
const verifyCodeMutation = useVerifyCodeMutation();
const [factorId, setFactorId] = useState<string | undefined>();
const [verificationCode, setVerificationCode] = useState('');
const [state, setState] = useState({
loading: false,
error: '',
});
const onSubmit = useCallback(async () => {
setState({
loading: true,
error: '',
});
if (!factorId || !verificationCode) {
return setState({
loading: false,
error: 'No factor ID or verification code found',
});
}
try {
await verifyCodeMutation.mutateAsync({
factorId,
code: verificationCode,
});
setState({
loading: false,
error: '',
});
onEnrolled();
} catch (error) {
const message = (error as Error).message || `Unknown error`;
setState({
loading: false,
error: message,
});
}
}, [onEnrolled, verifyCodeMutation, factorId, verificationCode]);
if (state.error) {
return (
<div className={'flex flex-col space-y-4'}>
<Alert variant={'destructive'}>
<Trans i18nKey={'account:multiFactorSetupError'} />
</Alert>
</div>
);
}
return (
<div className={'flex flex-col space-y-4'}>
<div className={'flex justify-center'}>
<FactorQrCode onCancel={onCancel} onSetFactorId={setFactorId} />
</div>
<If condition={factorId}>
<form
onSubmit={(e) => {
e.preventDefault();
return onSubmit();
}}
className={'w-full'}
>
<div className={'flex flex-col space-y-4'}>
<Label>
<Trans i18nKey={'account:verificationCode'} />
<OtpInput
onInvalid={() => setVerificationCode('')}
onValid={setVerificationCode}
/>
<span>
<Trans i18nKey={'account:verifyActivationCodeDescription'} />
</span>
</Label>
<div className={'flex justify-end space-x-2'}>
<Button disabled={!verificationCode} type={'submit'}>
{state.loading ? (
<Trans i18nKey={'account:verifyingCode'} />
) : (
<Trans i18nKey={'account:enableMfaFactor'} />
)}
</Button>
</div>
</div>
</form>
</If>
</div>
);
}
function FactorQrCode({
onSetFactorId,
onCancel,
}: React.PropsWithChildren<{
onCancel: () => void;
onSetFactorId: React.Dispatch<React.SetStateAction<string | undefined>>;
}>) {
const enrollFactorMutation = useEnrollFactor();
const [error, setError] = useState(false);
const [factor, setFactor] = useState({
name: '',
qrCode: '',
});
const factorName = factor.name;
useEffect(() => {
if (!factorName) {
return;
}
void (async () => {
try {
const data = await enrollFactorMutation.mutateAsync(factorName);
if (!data) {
return setError(true);
}
// set image
setFactor((factor) => {
return {
...factor,
qrCode: data.totp.qr_code,
};
});
// dispatch event to set factor ID
onSetFactorId(data.id);
} catch (e) {
setError(true);
}
})();
}, [onSetFactorId, factorName, enrollFactorMutation]);
if (error) {
return (
<div className={'flex w-full flex-col space-y-2'}>
<Alert variant={'destructive'}>
<Trans i18nKey={'account:qrCodeError'} />
</Alert>
</div>
);
}
if (!factorName) {
return (
<FactorNameForm
onCancel={onCancel}
onSetFactorName={(name) => {
setFactor((factor) => ({ ...factor, name }));
}}
/>
);
}
return (
<div className={'flex flex-col space-y-4'}>
<p>
<span className={'text-base'}>
<Trans i18nKey={'account:multiFactorModalHeading'} />
</span>
</p>
<div className={'flex justify-center'}>
<QrImage src={factor.qrCode} />
</div>
</div>
);
}
function FactorNameForm(
props: React.PropsWithChildren<{
onSetFactorName: (name: string) => void;
onCancel: () => void;
}>,
) {
const inputName = 'factorName';
return (
<form
className={'w-full'}
onSubmit={(event) => {
event.preventDefault();
const data = new FormData(event.currentTarget);
const name = data.get(inputName) as string;
props.onSetFactorName(name);
}}
>
<div className={'flex flex-col space-y-4'}>
<Label>
<Trans i18nKey={'account:factorNameLabel'} />
<Input autoComplete={'off'} required name={inputName} />
<span>
<Trans i18nKey={'account:factorNameHint'} />
</span>
</Label>
<div className={'flex justify-end space-x-2'}>
<Button type={'submit'}>
<Trans i18nKey={'account:factorNameSubmitLabel'} />
</Button>
</div>
</div>
</form>
);
}
function QrImage({ src }: { src: string }) {
return <Image alt={'QR Code'} src={src} width={160} height={160} />;
}
export default MultiFactorAuthSetupModal;
function useEnrollFactor() {
const client = useSupabase();
const mutationKey = useFactorsMutationKey();
const mutationFn = async (factorName: string) => {
const { data, error } = await client.auth.mfa.enroll({
friendlyName: factorName,
factorType: 'totp',
});
if (error) {
throw error;
}
return data;
};
return useMutation({
mutationFn,
mutationKey,
});
}
function useVerifyCodeMutation() {
const mutationKey = useFactorsMutationKey();
const client = useSupabase();
const mutationFn = async (params: { factorId: string; code: string }) => {
const challenge = await client.auth.mfa.challenge({
factorId: params.factorId,
});
if (challenge.error) {
throw challenge.error;
}
const challengeId = challenge.data.id;
const verify = await client.auth.mfa.verify({
factorId: params.factorId,
code: params.code,
challengeId,
});
if (verify.error) {
throw verify.error;
}
return verify;
};
return useMutation({ mutationKey, mutationFn });
}

View File

@@ -25,7 +25,7 @@ import { Input } from '@kit/ui/input';
import { Label } from '@kit/ui/label';
import { Trans } from '@kit/ui/trans';
import { PasswordUpdateSchema } from '../../schema/update-password.schema';
import { PasswordUpdateSchema } from '../../../schema/update-password.schema';
export const UpdatePasswordForm = ({
user,

View File

@@ -8,6 +8,14 @@ import { getSupabaseServerActionClient } from '@kit/supabase/server-actions-clie
import { PersonalAccountsService } from './services/personal-accounts.service';
export async function refreshAuthSession() {
const client = getSupabaseServerActionClient();
await client.auth.refreshSession();
return {};
}
export async function deletePersonalAccountAction(formData: FormData) {
const confirmation = formData.get('confirmation');