Skip to main content
Glama
index.tsx9.18 kB
'use client'; import { Button, Form, Modal, Tag, useForm } from '@intlayer/design-system'; import { useDisableTwoFactor, useEnableTwoFactor, useSession, useVerifyTotp, } from '@intlayer/design-system/hooks'; import { useIntlayer } from 'next-intlayer'; import type { FC } from 'react'; import { useState } from 'react'; import QRCode from 'react-qr-code'; import { type TwoFactorAuthForm, type TwoFactorAuthOTPSchema, useTwoFactorAuthOTPSchema, useTwoFactorAuthSchema, } from './useTwoFactorAuthSchema'; type ModalType = 'enable' | 'disable' | 'qrcode' | 'backupCodes' | null; type TwoFactorData = { totpURI: string; backupCodes: string[]; }; export const TwoFactorAuth: FC = () => { const { state, enableButton, disableButton, modal, qrCode, backupCodes } = useIntlayer('two-factor-auth'); const { session, revalidateSession } = useSession(); const { user } = session ?? {}; const { mutate: enableTwoFactor, isPending: isEnablingTwoFactor } = useEnableTwoFactor(); const { mutate: disableTwoFactor, isPending: isDisablingTwoFactor } = useDisableTwoFactor(); const { mutate: verifyTotp, isPending: isVerifyingTotp } = useVerifyTotp(); const [modalType, setModalType] = useState<ModalType>(null); const [twoFactorData, setTwoFactorData] = useState<TwoFactorData | null>( null ); const TwoFactorAuthSchema = useTwoFactorAuthSchema(); const { form, isSubmitting } = useForm(TwoFactorAuthSchema, { defaultValues: { password: '', }, }); // @ts-ignore - twoFactorEnabled is added by better-auth plugin but not in types const isEnabled = Boolean(user?.twoFactorEnabled); const isModalOpen = modalType !== null; const isPending = isEnablingTwoFactor || isDisablingTwoFactor; const handleOpenModal = (type: 'enable' | 'disable') => { setModalType(type); form.reset(); }; const handleCloseModal = () => { setModalType(null); form.reset(); }; const handleSubmit = async (data: TwoFactorAuthForm) => { if (modalType === 'enable') { return new Promise<void>((resolve, reject) => { enableTwoFactor( { password: data.password }, { onSuccess: (result) => { // Show QR code modal with the data if (result.data) { setTwoFactorData({ totpURI: result.data.totpURI, backupCodes: result.data.backupCodes, }); setModalType('qrcode'); form.reset(); resolve(); } else { reject(); } }, onError: () => { reject(); }, } ); }); } else if (modalType === 'disable') { return new Promise<void>((resolve, reject) => { disableTwoFactor( { password: data.password }, { onSuccess: () => { handleCloseModal(); revalidateSession(); resolve(); }, onError: () => { reject(); }, } ); }); } }; const handleVerifyCode = (code: string) => { verifyTotp( { code }, { onSuccess: () => { setModalType('backupCodes'); revalidateSession(); }, } ); }; const handleBackupCodesDone = () => { setTwoFactorData(null); handleCloseModal(); }; const modalContent = modalType && (modalType === 'enable' || modalType === 'disable') ? modal[modalType] : null; const renderModalContent = () => { if (modalType === 'qrcode' && twoFactorData) { return ( <QRCodeVerification data={twoFactorData} onVerify={handleVerifyCode} isVerifying={isVerifyingTotp} /> ); } if (modalType === 'backupCodes' && twoFactorData) { return ( <BackupCodesDisplay backupCodes={twoFactorData.backupCodes} onDone={handleBackupCodesDone} /> ); } if (modalContent) { return ( <div className="mt-6 flex w-full flex-col gap-6"> <p className="text-neutral text-sm">{modalContent.description}</p> <Form schema={TwoFactorAuthSchema} onSubmitSuccess={handleSubmit} className="mt-4 flex flex-col gap-4" {...form} > <Form.InputPassword name="password" id="two-factor-auth-password" autoComplete="current-password" placeholder={modal.passwordPlaceholder.value} isRequired autoFocus /> <div className="flex gap-3"> <Button onClick={handleCloseModal} color="text" variant="outline" disabled={isPending || isSubmitting} label={modal.cancelButton.value} className="flex-1" > {modal.cancelButton} </Button> <Form.Button type="submit" color="text" isLoading={isPending || isSubmitting} disabled={isPending || isSubmitting} label={modal.confirmButton.value} className="flex-1" > {modal.confirmButton} </Form.Button> </div> </Form> </div> ); } return null; }; const getModalTitle = () => { if (modalType === 'qrcode') return qrCode.title.value; if (modalType === 'backupCodes') return backupCodes.title.value; return modalContent?.title.value; }; return ( <div className="flex flex-col gap-10"> <Tag className="ml-auto" size="sm" color={isEnabled ? 'success' : 'text'}> {state(isEnabled)} </Tag> {isEnabled ? ( <Button onClick={() => handleOpenModal('disable')} color="text" label={disableButton.ariaLabel.value} > {disableButton.text} </Button> ) : ( <Button onClick={() => handleOpenModal('enable')} color="text" label={enableButton.ariaLabel.value} > {enableButton.text} </Button> )} <Modal isOpen={isModalOpen} onClose={handleCloseModal} title={getModalTitle()} hasCloseButton={modalType !== 'backupCodes'} padding="lg" className="max-h-[80vh]" > {renderModalContent()} </Modal> </div> ); }; // QR Code Verification Component const QRCodeVerification: FC<{ data: TwoFactorData; onVerify: (code: string) => void; isVerifying: boolean; }> = ({ data, onVerify, isVerifying }) => { const { qrCode } = useIntlayer('two-factor-auth'); const TwoFactorAuthOTPSchema = useTwoFactorAuthOTPSchema(); const { form } = useForm(TwoFactorAuthOTPSchema, { defaultValues: { code: '', }, }); const handleSubmit = (data: TwoFactorAuthOTPSchema) => { onVerify(data.code); }; return ( <div className="mt-6 flex w-full flex-col gap-6"> <p className="m-auto max-w-sm text-neutral text-sm"> {qrCode.description} </p> <Form {...form} schema={TwoFactorAuthOTPSchema} onSubmitSuccess={handleSubmit} className="flex flex-col gap-4" > <div className="mx-auto w-fit items-center justify-center rounded-lg bg-white p-4"> <QRCode size={256} value={data.totpURI} /> </div> <div className="m-auto my-6 flex max-w-sm flex-col gap-2"> <label htmlFor="verification-code" className="font-medium text-sm"> {qrCode.codeLabel} </label> <Form.OTP id="verification-code" name="code" type="text" placeholder={qrCode.codePlaceholder.value} maxLength={6} /> </div> <Button type="submit" color="text" isLoading={isVerifying} label={qrCode.submitButton.value} className="w-full" > {qrCode.submitButton} </Button> </Form> </div> ); }; // Backup Codes Display Component const BackupCodesDisplay: FC<{ backupCodes: string[]; onDone: () => void; }> = ({ backupCodes: codes, onDone }) => { const { backupCodes } = useIntlayer('two-factor-auth'); return ( <div className="mt-6 flex w-full flex-col gap-6"> <p className="text-neutral text-sm">{backupCodes.description}</p> <div className="m-auto grid max-w-sm grid-cols-2 gap-3 rounded-lg p-4"> {codes.map((code) => ( <div key={code} className="rounded-md bg-neutral/20 px-3 py-2 font-mono text-sm" > {code} </div> ))} </div> <Button onClick={onDone} color="text" label={backupCodes.doneButton.value} className="w-full" > {backupCodes.doneButton} </Button> </div> ); };

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/aymericzip/intlayer'

If you have feedback or need assistance with the MCP directory API, please join our Discord server