/* Copyright (C) 2023-2026 QuantumNous This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . For commercial licensing, please contact support@quantumnous.com */ import { useMemo } from 'react' import * as z from 'zod' import { useForm } from 'react-hook-form' import { zodResolver } from '@hookform/resolvers/zod' import { useTranslation } from 'react-i18next' import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage, } from '@/components/ui/form' import { Input } from '@/components/ui/input' import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue, } from '@/components/ui/select' import { Switch } from '@/components/ui/switch' import { Textarea } from '@/components/ui/textarea' import { SettingsForm, SettingsSwitchContent, SettingsSwitchItem, } from '../components/settings-form-layout' import { SettingsPageFormActions } from '../components/settings-page-context' import { SettingsSection } from '../components/settings-section' import { useResetForm } from '../hooks/use-reset-form' import { useUpdateOption } from '../hooks/use-update-option' const passkeySchema = z.object({ 'passkey.enabled': z.boolean(), 'passkey.rp_display_name': z.string(), 'passkey.rp_id': z.string(), 'passkey.origins': z.string(), 'passkey.allow_insecure_origin': z.boolean(), 'passkey.user_verification': z.enum(['required', 'preferred', 'discouraged']), 'passkey.attachment_preference': z.enum([ 'none', 'platform', 'cross-platform', ]), }) type PasskeyFormValues = z.infer interface PasskeySectionProps { defaultValues: PasskeyFormValues } export function PasskeySection({ defaultValues }: PasskeySectionProps) { const { t } = useTranslation() const updateOption = useUpdateOption() const formDefaults = useMemo( () => ({ ...defaultValues, 'passkey.origins': (defaultValues['passkey.origins'] as string) .split(',') .map((origin: string) => origin.trim()) .filter(Boolean) .join('\n'), 'passkey.attachment_preference': (defaultValues['passkey.attachment_preference'] as string) === '' ? 'none' : (defaultValues['passkey.attachment_preference'] as | 'platform' | 'cross-platform'), }), [defaultValues] ) const form = useForm({ resolver: zodResolver(passkeySchema), defaultValues: formDefaults, }) useResetForm(form, formDefaults) const onSubmit = async () => { const rawData = form.getValues() as Record const flattenedEntries: Array< [keyof PasskeyFormValues, PasskeyFormValues[keyof PasskeyFormValues]] > = [] Object.entries(rawData).forEach(([key, value]) => { if (key === 'passkey' && value && typeof value === 'object') { Object.entries(value as Record).forEach( ([nestedKey, nestedValue]) => { flattenedEntries.push([ `passkey.${nestedKey}` as keyof PasskeyFormValues, nestedValue as PasskeyFormValues[keyof PasskeyFormValues], ]) } ) } else { flattenedEntries.push([ key as keyof PasskeyFormValues, value as PasskeyFormValues[keyof PasskeyFormValues], ]) } }) const data = Object.fromEntries(flattenedEntries) as PasskeyFormValues const updates: Array<{ key: string; value: string | boolean }> = [] Object.entries(data).forEach(([key, value]) => { if (key === 'passkey.origins') { const processed = (value as string) .split('\n') .map((origin: string) => origin.trim()) .filter(Boolean) .join(',') const currentDefault = defaultValues['passkey.origins'] as string if (processed !== currentDefault) { updates.push({ key, value: processed }) } } else if (key === 'passkey.attachment_preference') { const attachmentPreference = value as PasskeyFormValues['passkey.attachment_preference'] const incoming = attachmentPreference === 'none' ? '' : attachmentPreference const currentDefault = defaultValues['passkey.attachment_preference'] === 'none' ? '' : defaultValues['passkey.attachment_preference'] if (incoming !== currentDefault) { updates.push({ key, value: incoming }) } } else if (value !== defaultValues[key as keyof PasskeyFormValues]) { updates.push({ key, value }) } }) for (const update of updates) { await updateOption.mutateAsync(update) } } return (
( {t('Enable Passkey')} {t( 'Allow users to register and sign in with Passkey (WebAuthn)' )} )} /> ( {t('Relying Party Display Name')} {t( 'Human-readable name shown to users during Passkey prompts.' )} )} /> ( {t('Relying Party ID')} {t( 'The effective domain for Passkey registration. Must match the current domain or be its parent domain.' )} )} /> ( {t('User Verification')} {t( 'Controls whether user verification (biometrics/PIN) is required during Passkey flows.' )} )} /> ( {t('Device Type Preference')} {t( 'Built-in: phone fingerprint/face, or Windows Hello; External: USB security key' )} )} /> ( {t('Allow Insecure Origins')} {t( 'Permit Passkey registration on non-HTTPS origins (only recommended for development)' )} )} /> ( {t('Allowed Origins')}