diff --git a/apps/web/middleware.ts b/apps/web/middleware.ts index c1a93ba82..d75fa085f 100644 --- a/apps/web/middleware.ts +++ b/apps/web/middleware.ts @@ -128,9 +128,10 @@ function getPatterns() { handler: async (req: NextRequest, res: NextResponse) => { const supabase = createMiddlewareClient(req, res); const { data } = await supabase.auth.getSession(); + const isVerifyMfa = req.nextUrl.pathname === pathsConfig.auth.verifyMfa; // If user is logged in, redirect to home page. - if (data.session) { + if (data.session && !isVerifyMfa) { return NextResponse.redirect( new URL(pathsConfig.app.home, req.nextUrl.origin).href, ); @@ -138,7 +139,7 @@ function getPatterns() { }, }, { - pattern: new URLPattern({ pathname: '/home/*' }), + pattern: new URLPattern({ pathname: '/home*' }), handler: async (req: NextRequest, res: NextResponse) => { const supabase = createMiddlewareClient(req, res); const { data, error } = await supabase.auth.getSession(); @@ -156,6 +157,10 @@ function getPatterns() { const requiresMultiFactorAuthentication = await checkRequiresMultiFactorAuthentication(supabase); + console.log({ + requiresMultiFactorAuthentication, + }); + // If user requires multi-factor authentication, redirect to MFA page. if (requiresMultiFactorAuthentication) { return NextResponse.redirect( diff --git a/apps/web/package.json b/apps/web/package.json index 5f7f42b90..6a51a6714 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -41,7 +41,7 @@ "edge-csrf": "^1.0.9", "i18next": "^23.10.1", "i18next-resources-to-backend": "^1.2.0", - "next": "canary", + "next": "14.2.0-canary.44", "next-contentlayer": "0.3.4", "next-sitemap": "^4.2.3", "next-themes": "^0.2.1", @@ -61,7 +61,7 @@ "@kit/prettier-config": "^0.1.0", "@kit/tailwind-config": "^0.1.0", "@kit/tsconfig": "^0.1.0", - "@next/bundle-analyzer": "canary", + "@next/bundle-analyzer": "14.2.0-canary.44", "@types/mdx": "^2.0.10", "@types/node": "^20.11.5", "@types/react": "^18.2.48", diff --git a/apps/web/public/locales/en/account.json b/apps/web/public/locales/en/account.json index f01832124..feca5d60e 100644 --- a/apps/web/public/locales/en/account.json +++ b/apps/web/public/locales/en/account.json @@ -10,7 +10,7 @@ "connectedAccounts": "Connected Accounts", "authenticationTab": "Authentication", "multiFactorAuth": "Multi-Factor Authentication", - "multiFactorAuthSubheading": "Set up a MFA method to secure your account", + "multiFactorAuthDescription": "Set up Multi-Factor Authentication method to further secure your account", "connectedAccountsSubheading": "Below are the accounts linked to your profile", "availableProviders": "Available Providers", "availableProvidersSubheading": "Click on the providers below to link your profile to the provider", @@ -66,10 +66,10 @@ "connectWithProvider": "Connect with {{ provider }}", "connectedWithProvider": "Connected with {{ provider }}", "setupMfaButtonLabel": "Setup a new Factor", - "multiFactorSetupError": "Sorry, there was an error while setting up your factor. Please try again.", + "multiFactorSetupErrorHeading": "Setup Failed", + "multiFactorSetupErrorDescription": "Sorry, there was an error while setting up your factor. Please try again.", "multiFactorAuthHeading": "Secure your account with Multi-Factor Authentication", - "multiFactorAuthDescription": "Enable Multi-Factor Authentication to verify your identity for an extra layer of security to your account in case your password is stolen. In addition to entering your password, it requires you confirm your identity via SMS.", - "multiFactorModalHeading": "Use your phone to scan the QR code below. Then enter the code generated.", + "multiFactorModalHeading": "Use your authenticator app to scan the QR code below. Then enter the code generated.", "factorNameLabel": "A memorable name to identify this factor", "factorNameHint": "Use an easy-to-remember name to easily identify this factor in the future. Ex. iPhone 14", "factorNameSubmitLabel": "Set factor name", @@ -78,6 +78,7 @@ "unenrollFactorSuccess": "Factor successfully unenrolled", "unenrollFactorError": "Unenrolling factor failed", "factorsListError": "Error loading factors list", + "factorsListErrorDescription": "Sorry, we couldn't load the factors list. Please try again.", "factorName": "Factor Name", "factorType": "Type", "factorStatus": "Status", @@ -99,13 +100,16 @@ "loadingFactors": "Loading factors...", "enableMfaFactor": "Enable Factor", "disableMfaFactor": "Disable Factor", - "qrCodeError": "Sorry, we weren't able to generate the QR code", + "qrCodeErrorHeading": "QR Code Error", + "qrCodeErrorDescription": "Sorry, we weren't able to generate the QR code", "multiFactorSetupSuccess": "Factor successfully enrolled", "submitVerificationCode": "Submit Verification Code", "mfaEnabledSuccessAlert": "Multi-Factor authentication is enabled", "verifyingCode": "Verifying code...", - "invalidVerificationCode": "Invalid verification code. Please try again", + "invalidVerificationCodeHeading": "Invalid Verification Code", + "invalidVerificationCodeDescription": "The verification code you entered is invalid. Please try again.", "unenrollFactorModalHeading": "Unenroll Factor", + "unenrollFactorModalDescription": "You're about to unenroll this factor. You will not be able to use it to login to your account.", "unenrollFactorModalBody": "You're about to unenroll this factor. You will not be able to use it to login to your account.", "unenrollFactorModalButtonLabel": "Yes, unenroll factor", "selectFactor": "Choose a factor to verify your identity", diff --git a/packages/features/accounts/package.json b/packages/features/accounts/package.json index fcbfa8db7..4e6d1eedd 100644 --- a/packages/features/accounts/package.json +++ b/packages/features/accounts/package.json @@ -24,7 +24,9 @@ "@kit/tsconfig": "0.1.0", "@kit/ui": "*", "@radix-ui/react-icons": "^1.3.0", - "lucide-react": "^0.363.0" + "lucide-react": "^0.363.0", + "react-hook-form": "^7.51.2", + "zod": "^3.22.4" }, "peerDependencies": { "@kit/shared": "0.1.0", diff --git a/packages/features/accounts/src/components/personal-account-settings/account-settings-container.tsx b/packages/features/accounts/src/components/personal-account-settings/account-settings-container.tsx index 0a36497f0..38be59754 100644 --- a/packages/features/accounts/src/components/personal-account-settings/account-settings-container.tsx +++ b/packages/features/accounts/src/components/personal-account-settings/account-settings-container.tsx @@ -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( + + + + + + + + + + + + + + + + diff --git a/packages/features/accounts/src/components/personal-account-settings/update-email-form-container.tsx b/packages/features/accounts/src/components/personal-account-settings/email/update-email-form-container.tsx similarity index 100% rename from packages/features/accounts/src/components/personal-account-settings/update-email-form-container.tsx rename to packages/features/accounts/src/components/personal-account-settings/email/update-email-form-container.tsx diff --git a/packages/features/accounts/src/components/personal-account-settings/update-email-form.tsx b/packages/features/accounts/src/components/personal-account-settings/email/update-email-form.tsx similarity index 98% rename from packages/features/accounts/src/components/personal-account-settings/update-email-form.tsx rename to packages/features/accounts/src/components/personal-account-settings/email/update-email-form.tsx index aad217fbb..665a64dc3 100644 --- a/packages/features/accounts/src/components/personal-account-settings/update-email-form.tsx +++ b/packages/features/accounts/src/components/personal-account-settings/email/update-email-form.tsx @@ -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( diff --git a/packages/features/accounts/src/components/personal-account-settings/mfa/multi-factor-auth-list.tsx b/packages/features/accounts/src/components/personal-account-settings/mfa/multi-factor-auth-list.tsx new file mode 100644 index 000000000..0b6b86c4c --- /dev/null +++ b/packages/features/accounts/src/components/personal-account-settings/mfa/multi-factor-auth-list.tsx @@ -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(); + + if (isLoading) { + return ( +
+ + +
+ +
+
+ ); + } + + if (isError) { + return ( +
+ + + + + + + + + + + +
+ ); + } + + const allFactors = factors?.all ?? []; + + if (!allFactors.length) { + return ( +
+ + + + + + + + + + +
+ +
+
+ ); + } + + const canAddNewFactors = allFactors.length < MAX_FACTOR_COUNT; + + return ( +
+ + + +
+ +
+
+ + + {(factorId) => ( + setUnenrolling(undefined)} + /> + )} + +
+ ); +} + +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 ( + + + + + + + + + + + + + onUnenrollRequested(props.factorId)} + > + + + + + ); +} + +function FactorsTable({ + setUnenrolling, + factors, +}: React.PropsWithChildren<{ + setUnenrolling: (factorId: string) => void; + factors: Factor[]; +}>) { + return ( + + + + + + + + + + + + + + + + + + + {factors.map((factor) => ( + + + {factor.friendly_name} + + + + + {factor.factor_type} + + + + + + + + ))} + +
+ + {factor.status} + + + + + + + + + + + + + +
+ ); +} + +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, + }); + }, + }); +} diff --git a/packages/features/accounts/src/components/personal-account-settings/mfa/multi-factor-auth-setup-dialog.tsx b/packages/features/accounts/src/components/personal-account-settings/mfa/multi-factor-auth-setup-dialog.tsx new file mode 100644 index 000000000..fcbf33ba3 --- /dev/null +++ b/packages/features/accounts/src/components/personal-account-settings/mfa/multi-factor-auth-setup-dialog.tsx @@ -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 ( + <> + + + + e.preventDefault()} + onEscapeKeyDown={(e) => e.preventDefault()} + > + + + + + + + + + + +
+ setIsOpen(false)} + onEnrolled={onEnrollSuccess} + /> +
+
+
+ + ); +} + +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 ; + } + + return ( +
+
+ + verificationCodeForm.setValue('factorId', factorId) + } + /> +
+ + +
+ +
+ { + return ( + + + + + + + + + + + + + + + + + + + + + + + + ); + }} + name={'verificationCode'} + /> + + + + +
+
+ +
+
+ ); +} + +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 ( +
+ + + + + + + + + + + +
+ ); + } + + if (!factorName) { + return ( + { + 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 ( +
+

+ + + +

+ +
+ +
+
+ ); +} + +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 ( +
+ { + props.onSetFactorName(data.name); + })} + > +
+ { + return ( + + + + + + + + + + + + + + + + ); + }} + /> + + + + +
+
+ + ); +} + +function QrImage({ src }: { src: string }) { + return {'QR; +} + +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 ( + + + + + + + + + + + + ); +} diff --git a/packages/features/accounts/src/components/personal-account-settings/multi-factor-auth-setup-modal.tsx b/packages/features/accounts/src/components/personal-account-settings/multi-factor-auth-setup-modal.tsx deleted file mode 100644 index 8d8fa4c51..000000000 --- a/packages/features/accounts/src/components/personal-account-settings/multi-factor-auth-setup-modal.tsx +++ /dev/null @@ -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 ( - - - - - - - - - props.setIsOpen(false)} - onEnrolled={onEnrollSuccess} - /> - - - ); -} - -function MultiFactorAuthSetupForm({ - onEnrolled, - onCancel, -}: React.PropsWithChildren<{ - onCancel: () => void; - onEnrolled: () => void; -}>) { - const verifyCodeMutation = useVerifyCodeMutation(); - const [factorId, setFactorId] = useState(); - 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 ( -
- - - -
- ); - } - - return ( -
-
- -
- - -
{ - e.preventDefault(); - - return onSubmit(); - }} - className={'w-full'} - > -
- - -
- -
-
-
-
-
- ); -} - -function FactorQrCode({ - onSetFactorId, - onCancel, -}: React.PropsWithChildren<{ - onCancel: () => void; - onSetFactorId: React.Dispatch>; -}>) { - 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 ( -
- - - -
- ); - } - - if (!factorName) { - return ( - { - setFactor((factor) => ({ ...factor, name })); - }} - /> - ); - } - - return ( -
-

- - - -

- -
- -
-
- ); -} - -function FactorNameForm( - props: React.PropsWithChildren<{ - onSetFactorName: (name: string) => void; - onCancel: () => void; - }>, -) { - const inputName = 'factorName'; - - return ( -
{ - event.preventDefault(); - - const data = new FormData(event.currentTarget); - const name = data.get(inputName) as string; - - props.onSetFactorName(name); - }} - > -
- - -
- -
-
-
- ); -} - -function QrImage({ src }: { src: string }) { - return {'QR; -} - -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 }); -} diff --git a/packages/features/accounts/src/components/personal-account-settings/update-password-container.tsx b/packages/features/accounts/src/components/personal-account-settings/password/update-password-container.tsx similarity index 100% rename from packages/features/accounts/src/components/personal-account-settings/update-password-container.tsx rename to packages/features/accounts/src/components/personal-account-settings/password/update-password-container.tsx diff --git a/packages/features/accounts/src/components/personal-account-settings/update-password-form.tsx b/packages/features/accounts/src/components/personal-account-settings/password/update-password-form.tsx similarity index 98% rename from packages/features/accounts/src/components/personal-account-settings/update-password-form.tsx rename to packages/features/accounts/src/components/personal-account-settings/password/update-password-form.tsx index ba851955d..3a541157c 100644 --- a/packages/features/accounts/src/components/personal-account-settings/update-password-form.tsx +++ b/packages/features/accounts/src/components/personal-account-settings/password/update-password-form.tsx @@ -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, diff --git a/packages/features/accounts/src/server/personal-accounts-server-actions.ts b/packages/features/accounts/src/server/personal-accounts-server-actions.ts index 4bed6b678..f346ea0a7 100644 --- a/packages/features/accounts/src/server/personal-accounts-server-actions.ts +++ b/packages/features/accounts/src/server/personal-accounts-server-actions.ts @@ -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'); diff --git a/packages/features/auth/src/components/multi-factor-challenge-container.tsx b/packages/features/auth/src/components/multi-factor-challenge-container.tsx index cffcfbf17..75458552c 100644 --- a/packages/features/auth/src/components/multi-factor-challenge-container.tsx +++ b/packages/features/auth/src/components/multi-factor-challenge-container.tsx @@ -1,20 +1,35 @@ 'use client'; -import type { FormEventHandler } from 'react'; -import { useCallback, useEffect, useState } from 'react'; +import { useCallback, useEffect } from 'react'; import { useRouter } from 'next/navigation'; +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 { z } from 'zod'; import useFetchAuthFactors from '@kit/supabase/hooks/use-fetch-mfa-factors'; import { useSignOut } from '@kit/supabase/hooks/use-sign-out'; import { useSupabase } from '@kit/supabase/hooks/use-supabase'; -import { Alert, AlertDescription } from '@kit/ui/alert'; +import { Alert, AlertDescription, AlertTitle } from '@kit/ui/alert'; import { Button } from '@kit/ui/button'; -import { Heading } from '@kit/ui/heading'; +import { + Form, + FormControl, + FormDescription, + FormField, + FormItem, + FormMessage, +} from '@kit/ui/form'; import { If } from '@kit/ui/if'; -import { OtpInput } from '@kit/ui/otp-input'; +import { + InputOTP, + InputOTPGroup, + InputOTPSeparator, + InputOTPSlot, +} from '@kit/ui/input-otp'; import Spinner from '@kit/ui/spinner'; import { Trans } from '@kit/ui/trans'; @@ -26,72 +41,128 @@ export function MultiFactorChallengeContainer({ }; }>) { const router = useRouter(); - - const [factorId, setFactorId] = useState(''); - const [verifyCode, setVerifyCode] = useState(''); const verifyMFAChallenge = useVerifyMFAChallenge(); const onSuccess = useCallback(() => { router.replace(paths.redirectPath); }, [router, paths.redirectPath]); - const onSubmitClicked: FormEventHandler = useCallback( - (event) => { - void (async () => { - event.preventDefault(); - - if (!factorId || !verifyCode) { - return; - } - - await verifyMFAChallenge.mutateAsync({ - factorId, - verifyCode, - }); - - onSuccess(); - })(); + const verificationCodeForm = useForm({ + resolver: zodResolver( + z.object({ + factorId: z.string().min(1), + verificationCode: z.string().min(6).max(6), + }), + ), + defaultValues: { + factorId: '', + verificationCode: '', }, - [factorId, verifyMFAChallenge, onSuccess, verifyCode], - ); + }); + + const factorId = verificationCodeForm.watch('factorId'); if (!factorId) { return ( - + { + verificationCodeForm.setValue('factorId', factorId); + }} + onSuccess={onSuccess} + /> ); } return ( -
-
- - - + + { + await verifyMFAChallenge.mutateAsync({ + factorId, + verificationCode: data.verificationCode, + }); -
- setVerifyCode('')} - onValid={setVerifyCode} - /> + onSuccess(); + })} + > +
+ + + - - - - - - - +
+
+ + + + + + + + + + + + + + + { + return ( + + + + + + + + + + + + + + + + + + + + + + + + ); + }} + /> +
+
+ +
- - -
- + + ); } @@ -99,11 +170,12 @@ function useVerifyMFAChallenge() { const client = useSupabase(); const mutationKey = ['mfa-verify-challenge']; + const mutationFn = async (params: { factorId: string; - verifyCode: string; + verificationCode: string; }) => { - const { factorId, verifyCode: code } = params; + const { factorId, verificationCode: code } = params; const response = await client.auth.mfa.challengeAndVerify({ factorId, @@ -128,7 +200,6 @@ function FactorsListContainer({ onSelect: (factor: string) => void; }>) { const signOut = useSignOut(); - const { data: factors, isLoading, error } = useFetchAuthFactors(); const isSuccess = factors && !isLoading && !error; @@ -174,8 +245,14 @@ function FactorsListContainer({ return (
- + + + + + + +
@@ -187,22 +264,24 @@ function FactorsListContainer({ return (
- + - +
- {verifiedFactors.map((factor) => ( -
- -
- ))} +
+ {verifiedFactors.map((factor) => ( +
+ +
+ ))} +
); } diff --git a/packages/features/team-accounts/package.json b/packages/features/team-accounts/package.json index 2ecf025d0..962a7a036 100644 --- a/packages/features/team-accounts/package.json +++ b/packages/features/team-accounts/package.json @@ -22,6 +22,7 @@ "@kit/tailwind-config": "0.1.0", "@kit/tsconfig": "0.1.0", "@kit/ui": "*", + "@hookform/resolvers/zod": "1.0.0", "lucide-react": "^0.363.0" }, "peerDependencies": { diff --git a/packages/stripe/src/services/stripe-webhook-handler.service.ts b/packages/stripe/src/services/stripe-webhook-handler.service.ts index 397aa2ffa..3012fa924 100644 --- a/packages/stripe/src/services/stripe-webhook-handler.service.ts +++ b/packages/stripe/src/services/stripe-webhook-handler.service.ts @@ -118,8 +118,10 @@ export class StripeWebhookHandlerService const stripe = await this.loadStripe(); const session = event.data.object; - const subscriptionId = session.subscription as string; + // TODO: handle one-off payments + // is subscription there? + const subscriptionId = session.subscription as string; const subscription = await stripe.subscriptions.retrieve(subscriptionId); const accountId = session.client_reference_id!; diff --git a/packages/ui/package.json b/packages/ui/package.json index a74a22cbe..b5510c719 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -28,6 +28,7 @@ "@radix-ui/react-tooltip": "1.0.7", "clsx": "^2.1.0", "cmdk": "^0.2.0", + "input-otp": "1.2.3", "react-top-loading-bar": "2.3.1", "tailwind-merge": "^2.2.0" }, @@ -75,6 +76,7 @@ "prettier": "@kit/prettier-config", "exports": { "./accordion": "./src/shadcn/accordion.tsx", + "./alert-dialog": "./src/shadcn/alert-dialog.tsx", "./avatar": "./src/shadcn/avatar.tsx", "./button": "./src/shadcn/button.tsx", "./calendar": "./src/shadcn/calendar.tsx", @@ -101,6 +103,7 @@ "./badge": "./src/shadcn/badge.tsx", "./radio-group": "./src/shadcn/radio-group.tsx", "./separator": "./src/shadcn/separator.tsx", + "./input-otp": "./src/shadcn/input-otp.tsx", "./utils": "./src/utils/index.ts", "./if": "./src/makerkit/if.tsx", "./trans": "./src/makerkit/trans.tsx", @@ -113,7 +116,6 @@ "./global-loader": "./src/makerkit/global-loader.tsx", "./error-boundary": "./src/makerkit/error-boundary.tsx", "./auth-change-listener": "./src/makerkit/auth-change-listener.tsx", - "./otp-input": "./src/makerkit/otp-input.tsx", "./loading-overlay": "./src/makerkit/loading-overlay.tsx", "./profile-avatar": "./src/makerkit/profile-avatar.tsx", "./mdx": "./src/makerkit/mdx/mdx-renderer.tsx" diff --git a/packages/ui/src/makerkit/otp-input.tsx b/packages/ui/src/makerkit/otp-input.tsx deleted file mode 100644 index cb375fcc2..000000000 --- a/packages/ui/src/makerkit/otp-input.tsx +++ /dev/null @@ -1,143 +0,0 @@ -import type { FormEventHandler } from 'react'; -import { useCallback, useEffect, useMemo } from 'react'; - -import { useFieldArray, useForm } from 'react-hook-form'; - -import { Input } from '../shadcn/input'; - -const DIGITS = 6; - -export function OtpInput({ - onValid, - onInvalid, -}: React.PropsWithChildren<{ - onValid: (code: string) => void; - onInvalid: () => void; -}>) { - const digitsArray = useMemo( - () => Array.from({ length: DIGITS }, (_, i) => i), - [], - ); - - const { control, register, watch, setFocus, formState, setValue } = useForm({ - mode: 'onChange', - reValidateMode: 'onChange', - defaultValues: { - values: digitsArray.map(() => ({ value: '' })), - }, - }); - - useFieldArray({ - control, - name: 'values', - shouldUnregister: true, - }); - - const { values } = watch(); - const isFormValid = formState.isValid; - const code = (values ?? []).map(({ value }) => value).join(''); - - useEffect(() => { - if (!isFormValid) { - onInvalid(); - - return; - } - - if (code.length === DIGITS) { - onValid(code); - - return; - } - - onInvalid(); - }, [onInvalid, onValid, code, isFormValid]); - - useEffect(() => { - setFocus('values.0.value'); - }, [setFocus]); - - const onInput: FormEventHandler = useCallback( - (target) => { - const element = target.currentTarget; - const isValid = element.reportValidity(); - - if (isValid) { - const nextIndex = Number(element.dataset.index) + 1; - - if (nextIndex >= DIGITS) { - return; - } - - setFocus(`values.${nextIndex}.value`); - } - }, - [setFocus], - ); - - const onPaste = useCallback( - (event: React.ClipboardEvent) => { - const pasted = event.clipboardData.getData('text/plain'); - - // check if value is numeric - if (isNumeric(pasted)) { - const digits = getDigits(pasted, digitsArray); - - digits.forEach((value, index) => { - setValue(`values.${index}.value`, value); - setFocus(`values.${index + 1}.value`); - }); - } - }, - [digitsArray, setFocus, setValue], - ); - - const handleKeyDown = useCallback( - (event: React.KeyboardEvent) => { - if (event.key === 'Backspace') { - event.preventDefault(); - - const index = Number(event.currentTarget.dataset.inputIndex); - - setValue(`values.${index}.value`, ''); - setFocus(`values.${index - 1}.value`); - } - }, - [setFocus, setValue], - ); - - return ( -
- {digitsArray.map((digit, index) => { - const control = { ...register(`values.${digit}.value`) }; - - return ( - - ); - })} -
- ); -} - -function isNumeric(pasted: string) { - const isNumericRegExp = /^-?\d+$/; - - return isNumericRegExp.test(pasted); -} - -function getDigits(pasted: string, digitsArray: number[]) { - return pasted.split('').slice(0, digitsArray.length); -} diff --git a/packages/ui/src/shadcn/alert-dialog.tsx b/packages/ui/src/shadcn/alert-dialog.tsx index 675501858..2f7804bec 100644 --- a/packages/ui/src/shadcn/alert-dialog.tsx +++ b/packages/ui/src/shadcn/alert-dialog.tsx @@ -4,9 +4,8 @@ import * as React from 'react'; import * as AlertDialogPrimitive from '@radix-ui/react-alert-dialog'; -import { buttonVariants } from '@kit/ui/button'; - import { cn } from '../utils/cn'; +import { buttonVariants } from './button'; const AlertDialog = AlertDialogPrimitive.Root; diff --git a/packages/ui/src/shadcn/input-otp.tsx b/packages/ui/src/shadcn/input-otp.tsx new file mode 100644 index 000000000..5648462b6 --- /dev/null +++ b/packages/ui/src/shadcn/input-otp.tsx @@ -0,0 +1,72 @@ +'use client'; + +import * as React from 'react'; + +import { DashIcon } from '@radix-ui/react-icons'; +import { OTPInput, OTPInputContext } from 'input-otp'; + +import { cn } from '../utils'; + +const InputOTP = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, containerClassName, ...props }, ref) => ( + +)); +InputOTP.displayName = 'InputOTP'; + +const InputOTPGroup = React.forwardRef< + React.ElementRef<'div'>, + React.ComponentPropsWithoutRef<'div'> +>(({ className, ...props }, ref) => ( +
+)); +InputOTPGroup.displayName = 'InputOTPGroup'; + +const InputOTPSlot = React.forwardRef< + React.ElementRef<'div'>, + React.ComponentPropsWithoutRef<'div'> & { index: number } +>(({ index, className, ...props }, ref) => { + const inputOTPContext = React.useContext(OTPInputContext); + const { char, hasFakeCaret, isActive } = inputOTPContext.slots[index]; + + return ( +
+ {char} + {hasFakeCaret && ( +
+
+
+ )} +
+ ); +}); +InputOTPSlot.displayName = 'InputOTPSlot'; + +const InputOTPSeparator = React.forwardRef< + React.ElementRef<'div'>, + React.ComponentPropsWithoutRef<'div'> +>(({ ...props }, ref) => ( +
+ +
+)); +InputOTPSeparator.displayName = 'InputOTPSeparator'; + +export { InputOTP, InputOTPGroup, InputOTPSlot, InputOTPSeparator }; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b3e74fe78..3fd98c103 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -88,7 +88,7 @@ importers: version: 5.28.6(react@18.2.0) '@tanstack/react-query-next-experimental': specifier: ^5.28.6 - version: 5.28.8(@tanstack/react-query@5.28.6)(next@14.2.0-canary.43)(react@18.2.0) + version: 5.28.8(@tanstack/react-query@5.28.6)(next@14.2.0-canary.44)(react@18.2.0) '@tanstack/react-table': specifier: ^8.11.3 version: 8.15.0(react-dom@18.2.0)(react@18.2.0) @@ -100,7 +100,7 @@ importers: version: 3.6.0 edge-csrf: specifier: ^1.0.9 - version: 1.0.9(next@14.2.0-canary.43) + version: 1.0.9(next@14.2.0-canary.44) i18next: specifier: ^23.10.1 version: 23.10.1 @@ -108,17 +108,17 @@ importers: specifier: ^1.2.0 version: 1.2.0 next: - specifier: canary - version: 14.2.0-canary.43(@opentelemetry/api@1.8.0)(react-dom@18.2.0)(react@18.2.0) + specifier: 14.2.0-canary.44 + version: 14.2.0-canary.44(@opentelemetry/api@1.8.0)(react-dom@18.2.0)(react@18.2.0) next-contentlayer: specifier: 0.3.4 - version: 0.3.4(contentlayer@0.3.4)(esbuild@0.20.2)(next@14.2.0-canary.43)(react-dom@18.2.0)(react@18.2.0) + version: 0.3.4(contentlayer@0.3.4)(esbuild@0.20.2)(next@14.2.0-canary.44)(react-dom@18.2.0)(react@18.2.0) next-sitemap: specifier: ^4.2.3 - version: 4.2.3(next@14.2.0-canary.43) + version: 4.2.3(next@14.2.0-canary.44) next-themes: specifier: ^0.2.1 - version: 0.2.1(next@14.2.0-canary.43)(react-dom@18.2.0)(react@18.2.0) + version: 0.2.1(next@14.2.0-canary.44)(react-dom@18.2.0)(react@18.2.0) react: specifier: 18.2.0 version: 18.2.0 @@ -163,8 +163,8 @@ importers: specifier: ^0.1.0 version: link:../../tooling/typescript '@next/bundle-analyzer': - specifier: canary - version: 14.2.0-canary.43 + specifier: 14.2.0-canary.44 + version: 14.2.0-canary.44 '@types/mdx': specifier: ^2.0.10 version: 2.0.12 @@ -610,6 +610,9 @@ importers: cmdk: specifier: ^0.2.0 version: 0.2.1(@types/react@18.2.71)(react-dom@18.2.0)(react@18.2.0) + input-otp: + specifier: 1.2.3 + version: 1.2.3(react-dom@18.2.0)(react@18.2.0) react-top-loading-bar: specifier: 2.3.1 version: 2.3.1(react@18.2.0) @@ -1958,8 +1961,8 @@ packages: - supports-color dev: false - /@next/bundle-analyzer@14.2.0-canary.43: - resolution: {integrity: sha512-5GYBb99OLnmg5xZDrUUD0ILB/gJDN4MxJTG5fU5JQXIDc6Ew+jJgMzjdqptJduvlExorAWNNpQnjdnRlnZCQfg==} + /@next/bundle-analyzer@14.2.0-canary.44: + resolution: {integrity: sha512-713lVU5ubs+w1FFAVhH6N9QhZHRv/cvq/bohkYXm1KBX4vHS8/d+cieLT6HUw1KZj673R+rn+/wNZ4a/3QJnPA==} dependencies: webpack-bundle-analyzer: 4.10.1 transitivePeerDependencies: @@ -1975,8 +1978,8 @@ packages: resolution: {integrity: sha512-Py8zIo+02ht82brwwhTg36iogzFqGLPXlRGKQw5s+qP/kMNc4MAyDeEwBKDijk6zTIbegEgu8Qy7C1LboslQAw==} dev: false - /@next/env@14.2.0-canary.43: - resolution: {integrity: sha512-jBjfC5J053shwv+g4kplFG+iH1TqWwMtLCIpDSplOmRDLdGeai6s3oKmWIxd+MbG5ETSZOl1vCN5A3nMgGkXfg==} + /@next/env@14.2.0-canary.44: + resolution: {integrity: sha512-I4pQqivxUD8jgAy8WomJhniJmdEUmdfNPTTXunsRUPHkWU3a6rlGc90fYrb+4+Bm/szyXEhBesFaB6n27NWjEw==} dev: false /@next/eslint-plugin-next@14.1.4: @@ -2008,8 +2011,8 @@ packages: dev: false optional: true - /@next/swc-darwin-arm64@14.2.0-canary.43: - resolution: {integrity: sha512-M9Asj8J6GMVNdMRnDnR+hELiyjgaHSUYAZz4M7ro5Vd1X8wpg3jygd/RnkTv+hhHn3rqwV9jWyZ4xdyG3SORrg==} + /@next/swc-darwin-arm64@14.2.0-canary.44: + resolution: {integrity: sha512-P9gmEH5fSTL2E3pMVfsIB2o5qqVsdNSfpq8puNYcbMNvpvwhGP9mgx9gOybf8FdfxYSGCGnLjeit/3ja0LJIeQ==} engines: {node: '>= 10'} cpu: [arm64] os: [darwin] @@ -2026,8 +2029,8 @@ packages: dev: false optional: true - /@next/swc-darwin-x64@14.2.0-canary.43: - resolution: {integrity: sha512-3BQ5FirbYZgXAFOCUynDr/Sl0fcFfEiLiDVdGMaJO7754fuWJShcj5tODiFC2B7MgLsVkri/84prBzsjkg76jA==} + /@next/swc-darwin-x64@14.2.0-canary.44: + resolution: {integrity: sha512-yiakf77DTsX6uKEW1bNcV4ST654OzR9svNdhIz3gqti17SVF2LnVhJGMZ8VcQhqIvQe6zeG4m2HXvWFsxGURuQ==} engines: {node: '>= 10'} cpu: [x64] os: [darwin] @@ -2044,8 +2047,8 @@ packages: dev: false optional: true - /@next/swc-linux-arm64-gnu@14.2.0-canary.43: - resolution: {integrity: sha512-VoCLYDTD2bkLsUkT0bACplrdpTw+IBKdFr5ih85atePrujCz6dMPUxeNMwH9aYL7r3PgzH6dR30r0Y5TFwUUSg==} + /@next/swc-linux-arm64-gnu@14.2.0-canary.44: + resolution: {integrity: sha512-ZdzSCApQdooLTQOc4hQdHQLiidYv3nImyjhkZF2ol1Rb3JBLtuTVx2zg3GjBN/aypUBx67kpWZoPIPbIlYcKpA==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] @@ -2062,8 +2065,8 @@ packages: dev: false optional: true - /@next/swc-linux-arm64-musl@14.2.0-canary.43: - resolution: {integrity: sha512-8c35oylAS4Ggu155txTpOv7VG4BzG8BTluVbUZuaneZwsZi6VTbjVKMVnLYmmdcdRkkvRgPc83oUr2HGxwxFBw==} + /@next/swc-linux-arm64-musl@14.2.0-canary.44: + resolution: {integrity: sha512-hh1zk4yEKDRbLivQqH04Ve+bB+baivK5/mKnFSDAaP7fKiLu6RA71J7oh2lUksgDOVTLqpR2ApHKyHbnNKVCcQ==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] @@ -2080,8 +2083,8 @@ packages: dev: false optional: true - /@next/swc-linux-x64-gnu@14.2.0-canary.43: - resolution: {integrity: sha512-PHy7clJ+ChZzNJ3c9A2IrWJN4aNa+FZ+v39XNdcjdkdhPvwu1QSvtirWSbxqKpAqgA/3sMhAGCvwOx6yeBs4Ug==} + /@next/swc-linux-x64-gnu@14.2.0-canary.44: + resolution: {integrity: sha512-Dt7SpIQrimsTAlWNHBzb3gwG01s4X2rVtX8RCVfO3Wqb6bqb0sFHxJQ7MWDql9CtbQLvwEPvWwRz0RPtEFK5Cg==} engines: {node: '>= 10'} cpu: [x64] os: [linux] @@ -2098,8 +2101,8 @@ packages: dev: false optional: true - /@next/swc-linux-x64-musl@14.2.0-canary.43: - resolution: {integrity: sha512-pvma+GKwkDEzhQRrwl9P4oGu9A9NGJH/Za+SG/XwWph2i78+4OMDCKrmKEJ1T5BE6Bgo+Emfhdy8TmfqHPQQCg==} + /@next/swc-linux-x64-musl@14.2.0-canary.44: + resolution: {integrity: sha512-VNUjddGcYepo9Pqn6O9uENlHNdKsOV8gjqLYvGPs820NVxRUSRw4yhUUmQSzTiJT+XxYlQ1XsT8DE5aWgCoxtA==} engines: {node: '>= 10'} cpu: [x64] os: [linux] @@ -2116,8 +2119,8 @@ packages: dev: false optional: true - /@next/swc-win32-arm64-msvc@14.2.0-canary.43: - resolution: {integrity: sha512-b1npBheIu7/BgMZTCFkuNYv0Q/N9u6+7MYY5xjZDVIutW8ut2V93JZqeC2SYWFm03I+LNdYjplRhn3TVerz9Xg==} + /@next/swc-win32-arm64-msvc@14.2.0-canary.44: + resolution: {integrity: sha512-2lBxvpMqErzTsMDPdnzMw/IcRp1SYkSzL/UuScgvbE7gSDnMlur8PtAM5MMwxQFdMAEbcnefjxH4kjOuwQagyw==} engines: {node: '>= 10'} cpu: [arm64] os: [win32] @@ -2134,8 +2137,8 @@ packages: dev: false optional: true - /@next/swc-win32-ia32-msvc@14.2.0-canary.43: - resolution: {integrity: sha512-1bZDCGyQzvdRNxVUUhsjBZOzBEEoQlh1r91ifjUz9nhcFYOlmP6IplPMjaLmG+GJMUiI36j5svdPYO3LP08b8g==} + /@next/swc-win32-ia32-msvc@14.2.0-canary.44: + resolution: {integrity: sha512-yadLbdDfXvn5uPNPyxmXdHUNTEyBNLspF87kbGVHM1HsffTaqgZuQZy52E247aoATJ/g951fYAqi7pxA61rWwg==} engines: {node: '>= 10'} cpu: [ia32] os: [win32] @@ -2152,8 +2155,8 @@ packages: dev: false optional: true - /@next/swc-win32-x64-msvc@14.2.0-canary.43: - resolution: {integrity: sha512-pU9gjLmp4yjYzBqCGa5bQ0iyJ5D73IRITEUFKrjZPi0XHUbFLrhcaaCsnVgMO4xfOQJgS7ODuQB7N0iPk7/EMw==} + /@next/swc-win32-x64-msvc@14.2.0-canary.44: + resolution: {integrity: sha512-t+0EgPaqQVDmBxrbOWEi3R+WeOg9jDhBYSqtyJO1n8Fou34xApsWzdgv9vSuRsAhU4CoMJQQkzm04FMIJReHmw==} engines: {node: '>= 10'} cpu: [x64] os: [win32] @@ -4318,7 +4321,7 @@ packages: /@tanstack/query-core@5.28.6: resolution: {integrity: sha512-hnhotV+DnQtvtR3jPvbQMPNMW4KEK0J4k7c609zJ8muiNknm+yoDyMHmxTWM5ZnlZpsz0zOxYFr+mzRJNHWJsA==} - /@tanstack/react-query-next-experimental@5.28.8(@tanstack/react-query@5.28.6)(next@14.2.0-canary.43)(react@18.2.0): + /@tanstack/react-query-next-experimental@5.28.8(@tanstack/react-query@5.28.6)(next@14.2.0-canary.44)(react@18.2.0): resolution: {integrity: sha512-1JAh1SHrqX1PPfoJtEiS8ewvz7D3lkBsIvDCpE8hWB07EF4O8hxPWQiVDf/fJ7U2g6N7iARX74335BHpCg250Q==} peerDependencies: '@tanstack/react-query': ^5.28.8 @@ -4326,7 +4329,7 @@ packages: react: ^18.0.0 dependencies: '@tanstack/react-query': 5.28.6(react@18.2.0) - next: 14.2.0-canary.43(@opentelemetry/api@1.8.0)(react-dom@18.2.0)(react@18.2.0) + next: 14.2.0-canary.44(@opentelemetry/api@1.8.0)(react-dom@18.2.0)(react@18.2.0) react: 18.2.0 dev: false @@ -6179,12 +6182,12 @@ packages: /eastasianwidth@0.2.0: resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} - /edge-csrf@1.0.9(next@14.2.0-canary.43): + /edge-csrf@1.0.9(next@14.2.0-canary.44): resolution: {integrity: sha512-3F89YTh42UDdISr3s9AEcgJDLi4ysgjGfnybzF0LuZGaG2W31h1ZwgWwEQBLMj04lAklcP4XHZYi7vk9o8zcbg==} peerDependencies: next: ^13.0.0 || ^14.0.0 dependencies: - next: 14.2.0-canary.43(@opentelemetry/api@1.8.0)(react-dom@18.2.0)(react@18.2.0) + next: 14.2.0-canary.44(@opentelemetry/api@1.8.0)(react-dom@18.2.0)(react@18.2.0) dev: false /editorconfig@1.0.4: @@ -7626,6 +7629,16 @@ packages: resolution: {integrity: sha512-7NXolsK4CAS5+xvdj5OMMbI962hU/wvwoxk+LWR9Ek9bVtyuuYScDN6eS0rUm6TxApFpw7CX1o4uJzcd4AyD3Q==} dev: false + /input-otp@1.2.3(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-pxYvgnihL9KAdpcShX2+iKctdMRbDs36bIqd8uIsN3e5vv9VjMv2bhO3S5Bl1PjcDPsA/OXZe5R71n8oVtucfQ==} + peerDependencies: + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + dependencies: + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + /inquirer@7.3.3: resolution: {integrity: sha512-JG3eIAj5V9CwcGvuOmoo6LB9kbAYT8HXffUl6memuszlwDC/qvFAJw49XJ5NROSFNPxp3iQg1GqkFhaY/CR0IA==} engines: {node: '>=8.0.0'} @@ -8926,7 +8939,7 @@ packages: engines: {node: '>= 0.4.0'} dev: false - /next-contentlayer@0.3.4(contentlayer@0.3.4)(esbuild@0.20.2)(next@14.2.0-canary.43)(react-dom@18.2.0)(react@18.2.0): + /next-contentlayer@0.3.4(contentlayer@0.3.4)(esbuild@0.20.2)(next@14.2.0-canary.44)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-UtUCwgAl159KwfhNaOwyiI7Lg6sdioyKMeh+E7jxx0CJ29JuXGxBEYmCI6+72NxFGIFZKx8lvttbbQhbnYWYSw==} peerDependencies: contentlayer: 0.3.4 @@ -8937,7 +8950,7 @@ packages: '@contentlayer/core': 0.3.4(esbuild@0.20.2) '@contentlayer/utils': 0.3.4 contentlayer: 0.3.4(esbuild@0.20.2) - next: 14.2.0-canary.43(@opentelemetry/api@1.8.0)(react-dom@18.2.0)(react@18.2.0) + next: 14.2.0-canary.44(@opentelemetry/api@1.8.0)(react-dom@18.2.0)(react@18.2.0) react: 18.2.0 react-dom: 18.2.0(react@18.2.0) transitivePeerDependencies: @@ -8947,7 +8960,7 @@ packages: - supports-color dev: false - /next-sitemap@4.2.3(next@14.2.0-canary.43): + /next-sitemap@4.2.3(next@14.2.0-canary.44): resolution: {integrity: sha512-vjdCxeDuWDzldhCnyFCQipw5bfpl4HmZA7uoo3GAaYGjGgfL4Cxb1CiztPuWGmS+auYs7/8OekRS8C2cjdAsjQ==} engines: {node: '>=14.18'} hasBin: true @@ -8958,17 +8971,17 @@ packages: '@next/env': 13.5.6 fast-glob: 3.3.2 minimist: 1.2.8 - next: 14.2.0-canary.43(@opentelemetry/api@1.8.0)(react-dom@18.2.0)(react@18.2.0) + next: 14.2.0-canary.44(@opentelemetry/api@1.8.0)(react-dom@18.2.0)(react@18.2.0) dev: false - /next-themes@0.2.1(next@14.2.0-canary.43)(react-dom@18.2.0)(react@18.2.0): + /next-themes@0.2.1(next@14.2.0-canary.44)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-B+AKNfYNIzh0vqQQKqQItTS8evEouKD7H5Hj3kmuPERwddR2TxvDSFZuTj6T7Jfn1oyeUyJMydPl1Bkxkh0W7A==} peerDependencies: next: '*' react: '*' react-dom: '*' dependencies: - next: 14.2.0-canary.43(@opentelemetry/api@1.8.0)(react-dom@18.2.0)(react@18.2.0) + next: 14.2.0-canary.44(@opentelemetry/api@1.8.0)(react-dom@18.2.0)(react@18.2.0) react: 18.2.0 react-dom: 18.2.0(react@18.2.0) dev: false @@ -9012,8 +9025,8 @@ packages: - babel-plugin-macros dev: false - /next@14.2.0-canary.43(@opentelemetry/api@1.8.0)(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-tL5fxsleOuRS7Momx5wRwkCOPLybQKwgJnpzgMGVReQs+kA9lkQiBANvlYdAsrvZ3vjzx2H+9mSqKDcKaC8UXQ==} + /next@14.2.0-canary.44(@opentelemetry/api@1.8.0)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-/f26kB0PHbggCRt6WSre695wKCYzCVQed1K3UFp5830wD6OMLM6j/ZMBgHx0AXD/JPR3dq2T2slX6LLEyYdnVw==} engines: {node: '>=18.17.0'} hasBin: true peerDependencies: @@ -9030,7 +9043,7 @@ packages: sass: optional: true dependencies: - '@next/env': 14.2.0-canary.43 + '@next/env': 14.2.0-canary.44 '@opentelemetry/api': 1.8.0 '@swc/helpers': 0.5.5 busboy: 1.6.0 @@ -9041,15 +9054,15 @@ packages: react-dom: 18.2.0(react@18.2.0) styled-jsx: 5.1.1(react@18.2.0) optionalDependencies: - '@next/swc-darwin-arm64': 14.2.0-canary.43 - '@next/swc-darwin-x64': 14.2.0-canary.43 - '@next/swc-linux-arm64-gnu': 14.2.0-canary.43 - '@next/swc-linux-arm64-musl': 14.2.0-canary.43 - '@next/swc-linux-x64-gnu': 14.2.0-canary.43 - '@next/swc-linux-x64-musl': 14.2.0-canary.43 - '@next/swc-win32-arm64-msvc': 14.2.0-canary.43 - '@next/swc-win32-ia32-msvc': 14.2.0-canary.43 - '@next/swc-win32-x64-msvc': 14.2.0-canary.43 + '@next/swc-darwin-arm64': 14.2.0-canary.44 + '@next/swc-darwin-x64': 14.2.0-canary.44 + '@next/swc-linux-arm64-gnu': 14.2.0-canary.44 + '@next/swc-linux-arm64-musl': 14.2.0-canary.44 + '@next/swc-linux-x64-gnu': 14.2.0-canary.44 + '@next/swc-linux-x64-musl': 14.2.0-canary.44 + '@next/swc-win32-arm64-msvc': 14.2.0-canary.44 + '@next/swc-win32-ia32-msvc': 14.2.0-canary.44 + '@next/swc-win32-x64-msvc': 14.2.0-canary.44 transitivePeerDependencies: - '@babel/core' - babel-plugin-macros