Skip to content

Commit

Permalink
⚙️ fix: Ensure backup codes are validated as an array before usage in…
Browse files Browse the repository at this point in the history
… 2FA components
  • Loading branch information
rubentalstra committed Feb 17, 2025
1 parent 4ad677f commit 9726d6e
Show file tree
Hide file tree
Showing 6 changed files with 113 additions and 112 deletions.
2 changes: 1 addition & 1 deletion client/src/components/Auth/TwoFactorScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ import React, { useState, useCallback } from 'react';
import { useSearchParams } from 'react-router-dom';
import { useForm, Controller } from 'react-hook-form';
import { REGEXP_ONLY_DIGITS, REGEXP_ONLY_DIGITS_AND_CHARS } from 'input-otp';
import { useVerifyTwoFactorTempMutation } from 'librechat-data-provider/react-query';
import { InputOTP, InputOTPGroup, InputOTPSeparator, InputOTPSlot, Label } from '~/components';
import { useLocalize } from '~/hooks';
import { useVerifyTwoFactorTempMutation } from '~/data-provider';

interface VerifyPayload {
tempToken: string;
Expand Down
2 changes: 1 addition & 1 deletion client/src/components/Nav/SettingsTabs/Account/Account.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ function Account() {
<div className="pb-3">
<EnableTwoFactorItem />
</div>
{user?.user?.backupCodes.length > 0 && (
{Array.isArray(user.user?.backupCodes) && user.user?.backupCodes.length > 0 && (
<div className="pb-3">
<BackupCodesItem />
</div>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import React, { useState } from 'react';
import { RefreshCcw, ShieldX } from 'lucide-react';
import { motion, AnimatePresence } from 'framer-motion';
import { useRegenerateBackupCodesMutation } from 'librechat-data-provider/react-query';
import { TBackupCode, TRegenerateBackupCodesResponse, type TUser } from 'librechat-data-provider';
import {
OGDialog,
Expand All @@ -13,6 +12,7 @@ import {
Spinner,
TooltipAnchor,
} from '~/components';
import { useRegenerateBackupCodesMutation } from '~/data-provider';
import { useAuthContext, useLocalize } from '~/hooks';
import { useToastContext } from '~/Providers';
import { useSetRecoilState } from 'recoil';
Expand Down Expand Up @@ -91,10 +91,10 @@ const BackupCodesItem: React.FC = () => {
exit={{ opacity: 0, y: -20 }}
className="mt-4"
>
{user.backupCodes?.length ? (
{Array.isArray(user?.backupCodes) && user?.backupCodes.length > 0 ? (
<>
<div className="grid grid-cols-2 gap-4">
{user.backupCodes.map((code, index) => {
{user?.backupCodes.map((code, index) => {
const isUsed = code.used;
const description = `Backup code number ${index + 1}, ${
isUsed
Expand Down
Original file line number Diff line number Diff line change
@@ -1,20 +1,20 @@
import React, { useCallback, useState, useEffect } from 'react';
import React, { useCallback, useState } from 'react';
import { useSetRecoilState } from 'recoil';
import { SmartphoneIcon } from 'lucide-react';
import { motion, AnimatePresence } from 'framer-motion';
import {
useEnableTwoFactorMutation,
useVerifyTwoFactorMutation,
useConfirmTwoFactorMutation,
useDisableTwoFactorMutation,
} from 'librechat-data-provider/react-query';
import type { TUser, TVerify2FARequest } from 'librechat-data-provider';
import { OGDialog, OGDialogContent, OGDialogHeader, OGDialogTitle, Progress } from '~/components';
import { SetupPhase, QRPhase, VerifyPhase, BackupPhase, DisablePhase } from './TwoFactorPhases';
import { DisableTwoFactorToggle } from './DisableTwoFactorToggle';
import { useAuthContext, useLocalize } from '~/hooks';
import { useToastContext } from '~/Providers';
import store from '~/store';
import {
useConfirmTwoFactorMutation,
useDisableTwoFactorMutation,
useEnableTwoFactorMutation,
useVerifyTwoFactorMutation,
} from '~/data-provider';

export type Phase = 'setup' | 'qr' | 'verify' | 'backup' | 'disable';

Expand All @@ -37,7 +37,7 @@ const TwoFactorAuthentication: React.FC = () => {
const [backupCodes, setBackupCodes] = useState<string[]>([]);
const [isDialogOpen, setDialogOpen] = useState<boolean>(false);
const [verificationToken, setVerificationToken] = useState<string>('');
const [phase, setPhase] = useState<Phase>(user?.backupCodes?.length > 0 ? 'disable' : 'setup');
const [phase, setPhase] = useState<Phase>(Array.isArray(user?.backupCodes) && user?.backupCodes.length > 0 ? 'disable' : 'setup');

const { mutate: confirm2FAMutate } = useConfirmTwoFactorMutation();
const { mutate: enable2FAMutate, isLoading: isGenerating } = useEnableTwoFactorMutation();
Expand Down Expand Up @@ -68,15 +68,14 @@ const TwoFactorAuthentication: React.FC = () => {
setBackupCodes([]);
setVerificationToken('');
setDisableToken('');
setPhase(user?.backupCodes?.length > 0 ? 'disable' : 'setup');
setPhase(Array.isArray(user?.backupCodes) && user?.backupCodes.length > 0 ? 'disable' : 'setup');
setDownloaded(false);
}, [user, otpauthUrl, disable2FAMutate, localize, showToast]);

const handleGenerateQRCode = useCallback(() => {
enable2FAMutate(undefined, {
onSuccess: ({ otpauthUrl, backupCodes }) => {
setOtpauthUrl(otpauthUrl);
// Extract secret from the otpauth URL (assumes the secret is present)
setSecret(otpauthUrl.split('secret=')[1].split('&')[0]);
setBackupCodes(backupCodes);
setPhase('qr');
Expand Down Expand Up @@ -198,7 +197,7 @@ const TwoFactorAuthentication: React.FC = () => {
}}
>
<DisableTwoFactorToggle
enabled={user?.backupCodes?.length > 0}
enabled={Array.isArray(user?.backupCodes) && user?.backupCodes.length > 0}
onChange={() => setDialogOpen(true)}
disabled={isVerifying || isDisabling || isGenerating}
/>
Expand All @@ -216,9 +215,9 @@ const TwoFactorAuthentication: React.FC = () => {
<OGDialogHeader>
<OGDialogTitle className="mb-2 flex items-center gap-3 text-2xl font-bold">
<SmartphoneIcon className="h-6 w-6 text-primary" />
{user?.backupCodes?.length > 0 ? localize('com_ui_2fa_disable') : localize('com_ui_2fa_setup')}
{Array.isArray(user?.backupCodes) && user?.backupCodes.length > 0 ? localize('com_ui_2fa_disable') : localize('com_ui_2fa_setup')}
</OGDialogTitle>
{user?.backupCodes?.length > 0 && phase !== 'disable' && (
{Array.isArray(user?.backupCodes) && user?.backupCodes.length > 0 && phase !== 'disable' && (
<div className="mt-4 space-y-3">
<Progress
value={(steps.indexOf(phasesLabel[phase]) / (steps.length - 1)) * 100}
Expand Down
97 changes: 96 additions & 1 deletion client/src/data-provider/Auth/mutations.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { useResetRecoilState, useSetRecoilState } from 'recoil';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { MutationKeys, dataService, request } from 'librechat-data-provider';
import { MutationKeys, QueryKeys, dataService, request } from 'librechat-data-provider';
import type { UseMutationResult } from '@tanstack/react-query';
import type * as t from 'librechat-data-provider';
import useClearStates from '~/hooks/Config/useClearStates';
Expand Down Expand Up @@ -84,3 +84,98 @@ export const useDeleteUserMutation = (
},
});
};

// Array.isArray(user?.backupCodes) && user?.backupCodes.length > 0

export const useEnableTwoFactorMutation = (): UseMutationResult<
t.TEnable2FAResponse,
unknown,
void,
unknown
> => {
const queryClient = useQueryClient();
return useMutation(() => dataService.enableTwoFactor(), {
onSuccess: (data) => {
queryClient.setQueryData([QueryKeys.user, '2fa'], data);
},
});
};

export const useVerifyTwoFactorMutation = (): UseMutationResult<
t.TVerify2FAResponse,
unknown,
t.TVerify2FARequest,
unknown
> => {
const queryClient = useQueryClient();
return useMutation(
(payload: t.TVerify2FARequest) => dataService.verifyTwoFactor(payload),
{
onSuccess: (data) => {
queryClient.setQueryData([QueryKeys.user, '2fa'], data);
},
},
);
};

export const useConfirmTwoFactorMutation = (): UseMutationResult<
t.TVerify2FAResponse,
unknown,
t.TVerify2FARequest,
unknown
> => {
const queryClient = useQueryClient();
return useMutation(
(payload: t.TVerify2FARequest) => dataService.confirmTwoFactor(payload),
{
onSuccess: (data) => {
queryClient.setQueryData([QueryKeys.user, '2fa'], data);
},
},
);
};

export const useDisableTwoFactorMutation = (): UseMutationResult<
t.TDisable2FAResponse,
unknown,
void,
unknown
> => {
const queryClient = useQueryClient();
return useMutation(() => dataService.disableTwoFactor(), {
onSuccess: (data) => {
queryClient.setQueryData([QueryKeys.user, '2fa'], null);
},
});
};

export const useRegenerateBackupCodesMutation = (): UseMutationResult<
t.TRegenerateBackupCodesResponse,
unknown,
void,
unknown
> => {
const queryClient = useQueryClient();
return useMutation(() => dataService.regenerateBackupCodes(), {
onSuccess: (data) => {
queryClient.setQueryData([QueryKeys.user, '2fa', 'backup'], data);
},
});
};

export const useVerifyTwoFactorTempMutation = (): UseMutationResult<
t.TVerify2FATempResponse,
unknown,
t.TVerify2FATempRequest,
unknown
> => {
const queryClient = useQueryClient();
return useMutation(
(payload: t.TVerify2FATempRequest) => dataService.verifyTwoFactorTemp(payload),
{
onSuccess: (data) => {
queryClient.setQueryData([QueryKeys.user, '2fa'], data);
},
},
);
};
93 changes: 0 additions & 93 deletions packages/data-provider/src/react-query/react-query-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -376,96 +376,3 @@ export const useGetCustomConfigSpeechQuery = (
},
);
};

export const useEnableTwoFactorMutation = (): UseMutationResult<
t.TEnable2FAResponse,
unknown,
void,
unknown
> => {
const queryClient = useQueryClient();
return useMutation(() => dataService.enableTwoFactor(), {
onSuccess: (data) => {
queryClient.setQueryData([QueryKeys.user, '2fa'], data);
},
});
};

export const useVerifyTwoFactorMutation = (): UseMutationResult<
t.TVerify2FAResponse,
unknown,
t.TVerify2FARequest,
unknown
> => {
const queryClient = useQueryClient();
return useMutation(
(payload: t.TVerify2FARequest) => dataService.verifyTwoFactor(payload),
{
onSuccess: (data) => {
queryClient.setQueryData([QueryKeys.user, '2fa'], data);
},
},
);
};

export const useConfirmTwoFactorMutation = (): UseMutationResult<
t.TVerify2FAResponse,
unknown,
t.TVerify2FARequest,
unknown
> => {
const queryClient = useQueryClient();
return useMutation(
(payload: t.TVerify2FARequest) => dataService.confirmTwoFactor(payload),
{
onSuccess: (data) => {
queryClient.setQueryData([QueryKeys.user, '2fa'], data);
},
},
);
};

export const useDisableTwoFactorMutation = (): UseMutationResult<
t.TDisable2FAResponse,
unknown,
void,
unknown
> => {
const queryClient = useQueryClient();
return useMutation(() => dataService.disableTwoFactor(), {
onSuccess: (data) => {
queryClient.setQueryData([QueryKeys.user, '2fa'], null);
},
});
};

export const useRegenerateBackupCodesMutation = (): UseMutationResult<
t.TRegenerateBackupCodesResponse,
unknown,
void,
unknown
> => {
const queryClient = useQueryClient();
return useMutation(() => dataService.regenerateBackupCodes(), {
onSuccess: (data) => {
queryClient.setQueryData([QueryKeys.user, '2fa', 'backup'], data);
},
});
};

export const useVerifyTwoFactorTempMutation = (): UseMutationResult<
t.TVerify2FATempResponse,
unknown,
t.TVerify2FATempRequest,
unknown
> => {
const queryClient = useQueryClient();
return useMutation(
(payload: t.TVerify2FATempRequest) => dataService.verifyTwoFactorTemp(payload),
{
onSuccess: (data) => {
queryClient.setQueryData([QueryKeys.user, '2fa'], data);
},
},
);
};

0 comments on commit 9726d6e

Please sign in to comment.