diff --git a/apps/browser-extension/src/entrypoints/popup/components/FormInput.tsx b/apps/browser-extension/src/entrypoints/popup/components/FormInput.tsx index 12dc63ac7..3c15bb392 100644 --- a/apps/browser-extension/src/entrypoints/popup/components/FormInput.tsx +++ b/apps/browser-extension/src/entrypoints/popup/components/FormInput.tsx @@ -37,6 +37,13 @@ const Icon: React.FC<{ name: string }> = ({ name }) => { ); + case 'settings': + return ( + <> + + + + ); default: return null; } diff --git a/apps/browser-extension/src/entrypoints/popup/components/PasswordConfigDialog.tsx b/apps/browser-extension/src/entrypoints/popup/components/PasswordConfigDialog.tsx new file mode 100644 index 000000000..542b9dbea --- /dev/null +++ b/apps/browser-extension/src/entrypoints/popup/components/PasswordConfigDialog.tsx @@ -0,0 +1,218 @@ +import React, { useState, useEffect, useCallback } from 'react'; +import { useTranslation } from 'react-i18next'; + +import type { PasswordSettings } from '@/utils/dist/shared/models/vault'; +import { CreatePasswordGenerator } from '@/utils/dist/shared/password-generator'; + +interface IPasswordConfigDialogProps { + isOpen: boolean; + onClose: () => void; + onSave: (password: string) => void; + onSettingsChange?: (settings: PasswordSettings) => void; + initialSettings: PasswordSettings; +} + +/** + * Password configuration dialog component. + */ +const PasswordConfigDialog: React.FC = ({ + isOpen, + onClose, + onSave, + onSettingsChange, + initialSettings +}) => { + const { t } = useTranslation(); + const [settings, setSettings] = useState(initialSettings); + const [previewPassword, setPreviewPassword] = useState(''); + + const generatePreview = useCallback((currentSettings: PasswordSettings) => { + try { + const passwordGenerator = CreatePasswordGenerator(currentSettings); + const password = passwordGenerator.generateRandomPassword(); + setPreviewPassword(password); + } catch (error) { + console.error('Error generating preview password:', error); + setPreviewPassword(''); + } + }, []); + + // Initialize settings when dialog opens + useEffect(() => { + if (isOpen) { + setSettings({ ...initialSettings }); + generatePreview({ ...initialSettings }); + } + }, [isOpen, initialSettings, generatePreview]); + + const handleSettingChange = useCallback((key: keyof PasswordSettings, value: boolean | number) => { + const newSettings = { ...settings, [key]: value }; + setSettings(newSettings); + generatePreview(newSettings); + onSettingsChange?.(newSettings); + }, [settings, generatePreview, onSettingsChange]); + + const handleRefreshPreview = useCallback(() => { + generatePreview(settings); + }, [settings, generatePreview]); + + const handleSave = useCallback(() => { + onSave(previewPassword); + onClose(); + }, [previewPassword, onSave, onClose]); + + const handleCancel = useCallback(() => { + onClose(); + }, [onClose]); + + if (!isOpen) { + return null; + } + + return ( +
+ {/* Backdrop */} +
+ + {/* Modal */} +
+
+ {/* Close button */} + + + {/* Modal content */} +
+
+

+ {t('credentials.advancedPasswordOptions')} +

+ +
+ {/* Character Type Checkboxes */} +
+
+ handleSettingChange('UseLowercase', e.target.checked)} + className="h-4 w-4 text-primary-600 focus:ring-primary-500 border-gray-300 rounded dark:bg-gray-700 dark:border-gray-600" + /> + +
+ +
+ handleSettingChange('UseUppercase', e.target.checked)} + className="h-4 w-4 text-primary-600 focus:ring-primary-500 border-gray-300 rounded dark:bg-gray-700 dark:border-gray-600" + /> + +
+ +
+ handleSettingChange('UseNumbers', e.target.checked)} + className="h-4 w-4 text-primary-600 focus:ring-primary-500 border-gray-300 rounded dark:bg-gray-700 dark:border-gray-600" + /> + +
+ +
+ handleSettingChange('UseSpecialChars', e.target.checked)} + className="h-4 w-4 text-primary-600 focus:ring-primary-500 border-gray-300 rounded dark:bg-gray-700 dark:border-gray-600" + /> + +
+ +
+ handleSettingChange('UseNonAmbiguousChars', e.target.checked)} + className="h-4 w-4 text-primary-600 focus:ring-primary-500 border-gray-300 rounded dark:bg-gray-700 dark:border-gray-600" + /> + +
+
+ + {/* Password Preview */} +
+ +
+ + +
+
+
+ + {/* Action buttons */} +
+ + +
+
+
+
+
+
+ ); +}; + +export default PasswordConfigDialog; \ No newline at end of file diff --git a/apps/browser-extension/src/entrypoints/popup/components/PasswordField.tsx b/apps/browser-extension/src/entrypoints/popup/components/PasswordField.tsx new file mode 100644 index 000000000..8d5d892e9 --- /dev/null +++ b/apps/browser-extension/src/entrypoints/popup/components/PasswordField.tsx @@ -0,0 +1,204 @@ +import React, { useState, useEffect, useCallback } from 'react'; +import { useTranslation } from 'react-i18next'; + +import type { PasswordSettings } from '@/utils/dist/shared/models/vault'; +import { CreatePasswordGenerator } from '@/utils/dist/shared/password-generator'; + +import PasswordConfigDialog from './PasswordConfigDialog'; + +interface IPasswordFieldProps { + id: string; + label: string; + value: string; + onChange: (value: string) => void; + placeholder?: string; + error?: string; + showPassword?: boolean; + onShowPasswordChange?: (show: boolean) => void; + initialSettings: PasswordSettings; +} + +/** + * Password field component with inline length slider and advanced configuration. + */ +const PasswordField: React.FC = ({ + id, + label, + value, + onChange, + placeholder, + error, + showPassword: controlledShowPassword, + onShowPasswordChange, + initialSettings +}) => { + const { t } = useTranslation(); + const [internalShowPassword, setInternalShowPassword] = useState(false); + const [showConfigDialog, setShowConfigDialog] = useState(false); + const [currentSettings, setCurrentSettings] = useState(initialSettings); + + // Use controlled or uncontrolled showPassword state + const showPassword = controlledShowPassword !== undefined ? controlledShowPassword : internalShowPassword; + + /** + * Set the showPassword state. + */ + const setShowPassword = useCallback((show: boolean): void => { + if (controlledShowPassword !== undefined) { + onShowPasswordChange?.(show); + } else { + setInternalShowPassword(show); + } + }, [controlledShowPassword, onShowPasswordChange]); + + // Update settings when initial settings change + useEffect(() => { + setCurrentSettings({ ...initialSettings }); + }, [initialSettings]); + + const generatePassword = useCallback((settings: PasswordSettings) => { + try { + const passwordGenerator = CreatePasswordGenerator(settings); + const password = passwordGenerator.generateRandomPassword(); + onChange(password); + setShowPassword(true); + } catch (error) { + console.error('Error generating password:', error); + } + }, [onChange, setShowPassword]); + + const handleLengthChange = useCallback((e: React.ChangeEvent) => { + const length = parseInt(e.target.value, 10); + const newSettings = { ...currentSettings, Length: length }; + setCurrentSettings(newSettings); + + // If there's already a password, regenerate it with the new length + if (value) { + generatePassword(newSettings); + } + }, [currentSettings, value, generatePassword]); + + const handleRegeneratePassword = useCallback(() => { + generatePassword(currentSettings); + }, [generatePassword, currentSettings]); + + const handleConfiguredPassword = useCallback((password: string) => { + onChange(password); + setShowPassword(true); + }, [onChange, setShowPassword]); + + const handleAdvancedSettingsChange = useCallback((newSettings: PasswordSettings) => { + setCurrentSettings(newSettings); + }, []); + + const togglePasswordVisibility = useCallback(() => { + setShowPassword(!showPassword); + }, [showPassword, setShowPassword]); + + const openConfigDialog = useCallback(() => { + setShowConfigDialog(true); + }, []); + + return ( +
+ {/* Label */} + + + {/* Password Input with Buttons */} +
+
+ onChange(e.target.value)} + placeholder={placeholder} + className="outline-0 shadow-sm bg-gray-50 border border-gray-300 text-gray-900 sm:text-sm rounded-l-lg block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white" + /> +
+
+ {/* Show/Hide Password Button */} + + + {/* Generate Password Button */} + +
+
+ + {/* Inline Password Length Slider */} +
+
+ + + {currentSettings.Length} + +
+ + + {/* Advanced Options Link */} +
+ +
+
+ + {/* Error Message */} + {error && ( +

{error}

+ )} + + {/* Advanced Configuration Dialog */} + setShowConfigDialog(false)} + onSave={handleConfiguredPassword} + onSettingsChange={handleAdvancedSettingsChange} + initialSettings={currentSettings} + /> +
+ ); +}; + +export default PasswordField; \ No newline at end of file diff --git a/apps/browser-extension/src/entrypoints/popup/pages/CredentialAddEdit.tsx b/apps/browser-extension/src/entrypoints/popup/pages/CredentialAddEdit.tsx index 9ffbafe7a..25bc843c0 100644 --- a/apps/browser-extension/src/entrypoints/popup/pages/CredentialAddEdit.tsx +++ b/apps/browser-extension/src/entrypoints/popup/pages/CredentialAddEdit.tsx @@ -14,6 +14,8 @@ import HeaderButton from '@/entrypoints/popup/components/HeaderButton'; import { HeaderIconType } from '@/entrypoints/popup/components/Icons/HeaderIcons'; import LoadingSpinner from '@/entrypoints/popup/components/LoadingSpinner'; import Modal from '@/entrypoints/popup/components/Modal'; +import PasswordField from '@/entrypoints/popup/components/PasswordField'; +import UsernameField from '@/entrypoints/popup/components/UsernameField'; import { useDb } from '@/entrypoints/popup/context/DbContext'; import { useHeaderButtons } from '@/entrypoints/popup/context/HeaderButtonsContext'; import { useLoading } from '@/entrypoints/popup/context/LoadingContext'; @@ -377,16 +379,9 @@ const CredentialAddEdit: React.FC = () => { } }, [setValue, watch]); - const generateRandomPassword = useCallback(async () => { - try { - const { passwordGenerator } = await initializeGenerators(); - const password = passwordGenerator.generateRandomPassword(); - setValue('Password', password); - setShowPassword(true); - } catch (error) { - console.error('Error generating random password:', error); - } - }, [initializeGenerators, setValue]); + const getCurrentPasswordSettings = useCallback(() => { + return dbContext.sqliteClient!.getPasswordSettings(); + }, [dbContext.sqliteClient]); /** * Handle form submission. @@ -596,30 +591,16 @@ const CredentialAddEdit: React.FC = () => { } ]} /> - setValue('Password', value)} error={errors.Password?.message} showPassword={showPassword} onShowPasswordChange={setShowPassword} - buttons={[ - { - icon: 'refresh', - onClick: generateRandomPassword, - title: t('credentials.generateRandomPassword') - } - ]} + initialSettings={getCurrentPasswordSettings()} /> - {

{t('credentials.alias')}

+