mirror of
https://github.com/aliasvault/aliasvault.git
synced 2026-05-15 19:05:30 -04:00
Add separate password field component with password length slider (#883)
This commit is contained in:
committed by
Leendert de Borst
parent
d43efb0273
commit
b2177f5d98
@@ -37,6 +37,13 @@ const Icon: React.FC<{ name: string }> = ({ name }) => {
|
||||
<path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15" />
|
||||
</>
|
||||
);
|
||||
case 'settings':
|
||||
return (
|
||||
<>
|
||||
<path d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" />
|
||||
<path d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||
</>
|
||||
);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -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<IPasswordConfigDialogProps> = ({
|
||||
isOpen,
|
||||
onClose,
|
||||
onSave,
|
||||
onSettingsChange,
|
||||
initialSettings
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const [settings, setSettings] = useState<PasswordSettings>(initialSettings);
|
||||
const [previewPassword, setPreviewPassword] = useState<string>('');
|
||||
|
||||
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 (
|
||||
<div className="fixed inset-0 z-50 overflow-y-auto">
|
||||
{/* Backdrop */}
|
||||
<div className="fixed inset-0 bg-black bg-opacity-60 transition-opacity" onClick={handleCancel} />
|
||||
|
||||
{/* Modal */}
|
||||
<div className="fixed inset-0 flex items-center justify-center p-4">
|
||||
<div className="relative transform overflow-hidden rounded-lg bg-white dark:bg-gray-800 px-4 pb-4 pt-5 text-left shadow-xl transition-all w-full max-w-lg">
|
||||
{/* Close button */}
|
||||
<button
|
||||
type="button"
|
||||
className="absolute right-4 top-4 text-gray-400 hover:text-gray-500 focus:outline-none"
|
||||
onClick={handleCancel}
|
||||
>
|
||||
<svg className="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
{/* Modal content */}
|
||||
<div className="sm:flex sm:items-start">
|
||||
<div className="w-full mt-3 text-center sm:ml-4 sm:mt-0 sm:text-left">
|
||||
<h3 className="text-lg font-medium leading-6 text-gray-900 dark:text-white mb-4">
|
||||
{t('credentials.advancedPasswordOptions')}
|
||||
</h3>
|
||||
|
||||
<div className="space-y-4">
|
||||
{/* Character Type Checkboxes */}
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center">
|
||||
<input
|
||||
id="use-lowercase"
|
||||
type="checkbox"
|
||||
checked={settings.UseLowercase}
|
||||
onChange={(e) => 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"
|
||||
/>
|
||||
<label htmlFor="use-lowercase" className="ml-2 text-sm text-gray-700 dark:text-gray-300">
|
||||
{t('credentials.includeLowercase')} (a-z)
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center">
|
||||
<input
|
||||
id="use-uppercase"
|
||||
type="checkbox"
|
||||
checked={settings.UseUppercase}
|
||||
onChange={(e) => 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"
|
||||
/>
|
||||
<label htmlFor="use-uppercase" className="ml-2 text-sm text-gray-700 dark:text-gray-300">
|
||||
{t('credentials.includeUppercase')} (A-Z)
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center">
|
||||
<input
|
||||
id="use-numbers"
|
||||
type="checkbox"
|
||||
checked={settings.UseNumbers}
|
||||
onChange={(e) => 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"
|
||||
/>
|
||||
<label htmlFor="use-numbers" className="ml-2 text-sm text-gray-700 dark:text-gray-300">
|
||||
{t('credentials.includeNumbers')} (0-9)
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center">
|
||||
<input
|
||||
id="use-special-chars"
|
||||
type="checkbox"
|
||||
checked={settings.UseSpecialChars}
|
||||
onChange={(e) => 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"
|
||||
/>
|
||||
<label htmlFor="use-special-chars" className="ml-2 text-sm text-gray-700 dark:text-gray-300">
|
||||
{t('credentials.includeSpecialChars')} (!@#$%^&*)
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center">
|
||||
<input
|
||||
id="use-non-ambiguous"
|
||||
type="checkbox"
|
||||
checked={settings.UseNonAmbiguousChars}
|
||||
onChange={(e) => 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"
|
||||
/>
|
||||
<label htmlFor="use-non-ambiguous" className="ml-2 text-sm text-gray-700 dark:text-gray-300">
|
||||
{t('credentials.avoidAmbiguousChars')} (avoid 0, O, l, I, etc.)
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Password Preview */}
|
||||
<div className="mt-4">
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
{t('credentials.preview')}:
|
||||
</label>
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
type="text"
|
||||
value={previewPassword}
|
||||
readOnly
|
||||
className="flex-1 bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:text-white font-mono"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleRefreshPreview}
|
||||
className="px-3 py-2 text-sm text-gray-500 dark:text-white bg-gray-200 hover:bg-gray-300 focus:ring-4 focus:outline-none focus:ring-gray-300 font-medium rounded-lg dark:bg-gray-600 dark:hover:bg-gray-700 dark:focus:ring-gray-800"
|
||||
title={t('credentials.generateNewPreview')}
|
||||
>
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Action buttons */}
|
||||
<div className="mt-5 sm:mt-6 sm:flex sm:flex-row-reverse">
|
||||
<button
|
||||
type="button"
|
||||
className="inline-flex w-full justify-center rounded-md bg-primary-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-primary-500 sm:ml-3 sm:w-auto"
|
||||
onClick={handleSave}
|
||||
>
|
||||
{t('common.use')}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="mt-3 inline-flex w-full justify-center rounded-md bg-white px-3 py-2 text-sm font-semibold text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 hover:bg-gray-50 sm:mt-0 sm:w-auto dark:bg-gray-700 dark:text-white dark:ring-gray-600 dark:hover:bg-gray-600"
|
||||
onClick={handleCancel}
|
||||
>
|
||||
{t('common.cancel')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default PasswordConfigDialog;
|
||||
@@ -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<IPasswordFieldProps> = ({
|
||||
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<PasswordSettings>(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<HTMLInputElement>) => {
|
||||
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 (
|
||||
<div className="space-y-2">
|
||||
{/* Label */}
|
||||
<label htmlFor={id} className="block text-sm font-medium text-gray-900 dark:text-white">
|
||||
{label}
|
||||
</label>
|
||||
|
||||
{/* Password Input with Buttons */}
|
||||
<div className="flex">
|
||||
<div className="relative flex-grow">
|
||||
<input
|
||||
type={showPassword ? 'text' : 'password'}
|
||||
id={id}
|
||||
value={value}
|
||||
onChange={(e) => 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"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex">
|
||||
{/* Show/Hide Password Button */}
|
||||
<button
|
||||
type="button"
|
||||
onClick={togglePasswordVisibility}
|
||||
className="px-3 text-gray-500 dark:text-white bg-gray-200 hover:bg-gray-300 focus:ring-4 focus:outline-none focus:ring-gray-300 font-medium text-sm dark:bg-gray-600 dark:hover:bg-gray-700 dark:focus:ring-gray-800"
|
||||
title={showPassword ? t('common.hidePassword') : t('common.showPassword')}
|
||||
>
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
{showPassword ? (
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.88 9.88l-3.29-3.29m7.532 7.532l3.29 3.29M3 3l3.59 3.59m0 0A9.953 9.953 0 0112 5c4.478 0 8.268 2.943 9.543 7a10.025 10.025 0 01-4.132 5.411m0 0L21 21" />
|
||||
) : (
|
||||
<>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
|
||||
</>
|
||||
)}
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
{/* Generate Password Button */}
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleRegeneratePassword}
|
||||
className="px-3 text-gray-500 dark:text-white bg-gray-200 hover:bg-gray-300 focus:ring-4 focus:outline-none focus:ring-gray-300 font-medium rounded-r-lg text-sm border-l border-gray-300 dark:border-gray-700 dark:bg-gray-600 dark:hover:bg-gray-700 dark:focus:ring-gray-800"
|
||||
title={t('credentials.generateRandomPassword')}
|
||||
>
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Inline Password Length Slider */}
|
||||
<div className="pt-2">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<label htmlFor={`${id}-length`} className="text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
{t('credentials.passwordLength')}
|
||||
</label>
|
||||
<span className="text-sm text-gray-600 dark:text-gray-400 font-mono">
|
||||
{currentSettings.Length}
|
||||
</span>
|
||||
</div>
|
||||
<input
|
||||
type="range"
|
||||
id={`${id}-length`}
|
||||
min="8"
|
||||
max="64"
|
||||
value={currentSettings.Length}
|
||||
onChange={handleLengthChange}
|
||||
className="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer dark:bg-gray-700"
|
||||
/>
|
||||
|
||||
{/* Advanced Options Link */}
|
||||
<div className="mt-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={openConfigDialog}
|
||||
className="text-sm text-primary-600 hover:text-primary-700 dark:text-primary-400 dark:hover:text-primary-300 underline cursor-pointer bg-transparent border-none p-0"
|
||||
>
|
||||
{t('credentials.changePasswordComplexity')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Error Message */}
|
||||
{error && (
|
||||
<p className="text-sm text-red-600 dark:text-red-400">{error}</p>
|
||||
)}
|
||||
|
||||
{/* Advanced Configuration Dialog */}
|
||||
<PasswordConfigDialog
|
||||
isOpen={showConfigDialog}
|
||||
onClose={() => setShowConfigDialog(false)}
|
||||
onSave={handleConfiguredPassword}
|
||||
onSettingsChange={handleAdvancedSettingsChange}
|
||||
initialSettings={currentSettings}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default PasswordField;
|
||||
@@ -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 = () => {
|
||||
}
|
||||
]}
|
||||
/>
|
||||
<FormInput
|
||||
<PasswordField
|
||||
id="password"
|
||||
label={t('common.password')}
|
||||
type="password"
|
||||
value={watch('Password') ?? ''}
|
||||
onChange={(value) => setValue('Password', value)}
|
||||
error={errors.Password?.message}
|
||||
showPassword={showPassword}
|
||||
onShowPasswordChange={setShowPassword}
|
||||
buttons={[
|
||||
{
|
||||
icon: 'refresh',
|
||||
onClick: generateRandomPassword,
|
||||
title: t('credentials.generateRandomPassword')
|
||||
}
|
||||
]}
|
||||
initialSettings={getCurrentPasswordSettings()}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleGenerateRandomAlias}
|
||||
className="w-full bg-primary-500 text-white py-2 px-4 rounded hover:bg-primary-600 focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2"
|
||||
>
|
||||
{t('credentials.generateRandomAlias')}
|
||||
</button>
|
||||
<FormInput
|
||||
id="email"
|
||||
label={t('common.email')}
|
||||
@@ -633,6 +614,13 @@ const CredentialAddEdit: React.FC = () => {
|
||||
<div className="bg-white dark:bg-gray-800 p-4 rounded-lg border border-gray-200 dark:border-gray-700">
|
||||
<h2 className="text-lg font-semibold mb-4 text-gray-900 dark:text-white">{t('credentials.alias')}</h2>
|
||||
<div className="space-y-4">
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleGenerateRandomAlias}
|
||||
className="w-full bg-primary-500 text-white py-2 px-4 rounded hover:bg-primary-600 focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2"
|
||||
>
|
||||
{t('credentials.generateRandomAlias')}
|
||||
</button>
|
||||
<FormInput
|
||||
id="firstName"
|
||||
label={t('credentials.firstName')}
|
||||
|
||||
@@ -52,6 +52,7 @@
|
||||
"error": "Error",
|
||||
"success": "Success",
|
||||
"cancel": "Cancel",
|
||||
"use": "Use",
|
||||
"delete": "Delete",
|
||||
"close": "Close",
|
||||
"copied": "Copied!",
|
||||
@@ -237,6 +238,17 @@
|
||||
"loginCredentials": "Login Credentials",
|
||||
"generateRandomUsername": "Generate random username",
|
||||
"generateRandomPassword": "Generate random password",
|
||||
"configurePassword": "Configure password",
|
||||
"changePasswordComplexity": "Change password complexity",
|
||||
"advancedPasswordOptions": "Advanced password options",
|
||||
"passwordLength": "Password length",
|
||||
"includeLowercase": "Include lowercase letters",
|
||||
"includeUppercase": "Include uppercase letters",
|
||||
"includeNumbers": "Include numbers",
|
||||
"includeSpecialChars": "Include special characters",
|
||||
"avoidAmbiguousChars": "Avoid ambiguous characters",
|
||||
"preview": "Preview",
|
||||
"generateNewPreview": "Generate new preview",
|
||||
"generateRandomAlias": "Generate Random Alias",
|
||||
"alias": "Alias",
|
||||
"firstName": "First Name",
|
||||
|
||||
Reference in New Issue
Block a user