mirror of
https://github.com/aliasvault/aliasvault.git
synced 2026-03-19 23:28:23 -04:00
Add (re)generate username and password controls (#900)
This commit is contained in:
committed by
Leendert de Borst
parent
a93a7f7fff
commit
3d8c2b7086
@@ -1,5 +1,46 @@
|
||||
import React, { forwardRef } from 'react';
|
||||
|
||||
/**
|
||||
* Button configuration for form input.
|
||||
*/
|
||||
type FormInputButton = {
|
||||
icon: string;
|
||||
onClick: () => void;
|
||||
title?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Icon component for form input buttons.
|
||||
*/
|
||||
const Icon: React.FC<{ name: string }> = ({ name }) => {
|
||||
switch (name) {
|
||||
case 'visibility':
|
||||
return (
|
||||
<>
|
||||
<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z" />
|
||||
<circle cx="12" cy="12" r="3" />
|
||||
</>
|
||||
);
|
||||
case 'visibility-off':
|
||||
return (
|
||||
<>
|
||||
<path d="M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19m-6.72-1.07a3 3 0 1 1-4.24-4.24" />
|
||||
<line x1="1" y1="1" x2="23" y2="23" />
|
||||
</>
|
||||
);
|
||||
case 'refresh':
|
||||
return (
|
||||
<>
|
||||
<path d="M23 4v6h-6" />
|
||||
<path d="M1 20v-6h6" />
|
||||
<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" />
|
||||
</>
|
||||
);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Form input props.
|
||||
*/
|
||||
@@ -14,6 +55,9 @@ type FormInputProps = {
|
||||
multiline?: boolean;
|
||||
rows?: number;
|
||||
error?: string;
|
||||
buttons?: FormInputButton[];
|
||||
showPassword?: boolean;
|
||||
onShowPasswordChange?: (show: boolean) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -29,13 +73,48 @@ export const FormInput = forwardRef<HTMLInputElement, FormInputProps>(({
|
||||
required = false,
|
||||
multiline = false,
|
||||
rows = 1,
|
||||
error
|
||||
error,
|
||||
buttons = [],
|
||||
showPassword: controlledShowPassword,
|
||||
onShowPasswordChange
|
||||
}, ref) => {
|
||||
const [showPassword, setShowPassword] = React.useState(false);
|
||||
const [internalShowPassword, setInternalShowPassword] = React.useState(false);
|
||||
|
||||
/**
|
||||
* Use controlled or uncontrolled showPassword state.
|
||||
* If controlledShowPassword is provided, use that value and call onShowPasswordChange.
|
||||
* Otherwise, use internal state.
|
||||
*/
|
||||
const showPassword = controlledShowPassword !== undefined ? controlledShowPassword : internalShowPassword;
|
||||
|
||||
/**
|
||||
* Set the showPassword state.
|
||||
* If controlledShowPassword is provided, use that value and call onShowPasswordChange.
|
||||
* Otherwise, use internal state.
|
||||
*/
|
||||
const setShowPassword = (value: boolean): void => {
|
||||
if (controlledShowPassword !== undefined) {
|
||||
onShowPasswordChange?.(value);
|
||||
} else {
|
||||
setInternalShowPassword(value);
|
||||
}
|
||||
};
|
||||
|
||||
const inputClasses = `mt-1 block w-full rounded-md ${
|
||||
error ? 'border-red-500' : 'border-gray-300 dark:border-gray-700'
|
||||
} text-gray-900 sm:text-sm rounded-lg shadow-sm border focus:ring-primary-500 focus:border-primary-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white dark:placeholder-gray-400 p-3`;
|
||||
} text-gray-900 sm:text-sm rounded-lg shadow-sm border focus:ring-primary-500 focus:border-primary-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white dark:placeholder-gray-400 py-2 px-3`;
|
||||
|
||||
// Add password visibility button if type is password
|
||||
const allButtons = type === 'password'
|
||||
? [...buttons, {
|
||||
icon: showPassword ? 'visibility-off' : 'visibility',
|
||||
/**
|
||||
* Toggle password visibility.
|
||||
*/
|
||||
onClick: (): void => setShowPassword(!showPassword),
|
||||
title: showPassword ? 'Hide password' : 'Show password'
|
||||
}]
|
||||
: buttons;
|
||||
|
||||
return (
|
||||
<div>
|
||||
@@ -64,14 +143,21 @@ export const FormInput = forwardRef<HTMLInputElement, FormInputProps>(({
|
||||
className={inputClasses}
|
||||
/>
|
||||
)}
|
||||
{type === 'password' && (
|
||||
<div className="absolute right-2 top-1/2 -translate-y-1/2">
|
||||
<button
|
||||
onClick={() => setShowPassword(!showPassword)}
|
||||
className="px-3 py-1 text-sm text-gray-600 hover:text-gray-900 transition-colors duration-200"
|
||||
>
|
||||
{showPassword ? 'Hide' : 'Show'}
|
||||
</button>
|
||||
{allButtons.length > 0 && (
|
||||
<div className="absolute right-2 top-1/2 -translate-y-1/2 flex items-center gap-2">
|
||||
{allButtons.map((button, index) => (
|
||||
<button
|
||||
type="button"
|
||||
key={index}
|
||||
onClick={button.onClick}
|
||||
title={button.title}
|
||||
className="p-1 text-gray-600 hover:text-gray-900 dark:text-gray-400 dark:hover:text-white transition-colors duration-200"
|
||||
>
|
||||
<svg className="w-5 h-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<Icon name={button.icon} />
|
||||
</svg>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -14,6 +14,43 @@ type FormInputCopyToClipboardProps = {
|
||||
|
||||
const clipboardService = new ClipboardCopyService();
|
||||
|
||||
/**
|
||||
* Icon component for form input buttons.
|
||||
*/
|
||||
const Icon: React.FC<{ name: string }> = ({ name }) => {
|
||||
switch (name) {
|
||||
case 'visibility':
|
||||
return (
|
||||
<>
|
||||
<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z" />
|
||||
<circle cx="12" cy="12" r="3" />
|
||||
</>
|
||||
);
|
||||
case 'visibility-off':
|
||||
return (
|
||||
<>
|
||||
<path d="M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19m-6.72-1.07a3 3 0 1 1-4.24-4.24" />
|
||||
<line x1="1" y1="1" x2="23" y2="23" />
|
||||
</>
|
||||
);
|
||||
case 'copy':
|
||||
return (
|
||||
<>
|
||||
<rect x="9" y="9" width="13" height="13" rx="2" ry="2" />
|
||||
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" />
|
||||
</>
|
||||
);
|
||||
case 'check':
|
||||
return (
|
||||
<>
|
||||
<polyline points="20 6 9 17 4 12" />
|
||||
</>
|
||||
);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Form input copy to clipboard component.
|
||||
*/
|
||||
@@ -71,17 +108,38 @@ export const FormInputCopyToClipboard: React.FC<FormInputCopyToClipboardProps> =
|
||||
} text-gray-900 sm:text-sm rounded-lg shadow-sm focus:ring-primary-500 focus:border-primary-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white dark:placeholder-gray-400`}
|
||||
/>
|
||||
<div className="absolute right-2 top-1/2 -translate-y-1/2 flex items-center gap-2">
|
||||
{copied && (
|
||||
<span className="text-green-500 dark:text-green-400">
|
||||
Copied!
|
||||
</span>
|
||||
{copied ? (
|
||||
<button
|
||||
type="button"
|
||||
className="p-1 text-green-500 dark:text-green-400 transition-colors duration-200"
|
||||
title="Copied!"
|
||||
>
|
||||
<svg className="w-5 h-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<Icon name="check" />
|
||||
</svg>
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
onClick={copyToClipboard}
|
||||
className="p-1 text-gray-600 hover:text-gray-900 dark:text-gray-400 dark:hover:text-white transition-colors duration-200"
|
||||
title="Copy to clipboard"
|
||||
>
|
||||
<svg className="w-5 h-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<Icon name="copy" />
|
||||
</svg>
|
||||
</button>
|
||||
)}
|
||||
{type === 'password' && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowPassword(!showPassword)}
|
||||
className="px-3 py-1 text-sm text-gray-600 hover:text-gray-900 dark:text-gray-400 dark:hover:text-white transition-colors duration-200"
|
||||
className="p-1 text-gray-600 hover:text-gray-900 dark:text-gray-400 dark:hover:text-white transition-colors duration-200"
|
||||
title={showPassword ? 'Hide password' : 'Show password'}
|
||||
>
|
||||
{showPassword ? 'Hide' : 'Show'}
|
||||
<svg className="w-5 h-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<Icon name={showPassword ? 'visibility-off' : 'visibility'} />
|
||||
</svg>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -14,7 +14,7 @@ import { useHeaderButtons } from '@/entrypoints/popup/context/HeaderButtonsConte
|
||||
import { useWebApi } from '@/entrypoints/popup/context/WebApiContext';
|
||||
import { useVaultMutate } from '@/entrypoints/popup/hooks/useVaultMutate';
|
||||
|
||||
import { IdentityHelperUtils, CreateIdentityGenerator } from '@/utils/shared/identity-generator';
|
||||
import { IdentityHelperUtils, CreateIdentityGenerator, CreateUsernameEmailGenerator, Identity, Gender } from '@/utils/shared/identity-generator';
|
||||
import type { Credential } from '@/utils/shared/models/vault';
|
||||
import { CreatePasswordGenerator } from '@/utils/shared/password-generator';
|
||||
|
||||
@@ -66,6 +66,7 @@ const CredentialAddEdit: React.FC = () => {
|
||||
const { setHeaderButtons } = useHeaderButtons();
|
||||
const { setIsInitialLoading } = useLoading();
|
||||
const [localLoading, setLocalLoading] = useState(false);
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
const webApi = useWebApi();
|
||||
|
||||
const serviceNameRef = useRef<HTMLInputElement>(null);
|
||||
@@ -175,7 +176,7 @@ const CredentialAddEdit: React.FC = () => {
|
||||
const identity = identityGenerator.generateRandomIdentity();
|
||||
const password = passwordGenerator.generateRandomPassword();
|
||||
|
||||
const metadata = await dbContext!.getVaultMetadata();
|
||||
const metadata = await dbContext.getVaultMetadata();
|
||||
|
||||
const privateEmailDomains = metadata?.privateEmailDomains ?? [];
|
||||
const publicEmailDomains = metadata?.publicEmailDomains ?? [];
|
||||
@@ -212,6 +213,44 @@ const CredentialAddEdit: React.FC = () => {
|
||||
void generateRandomAlias();
|
||||
}, [generateRandomAlias]);
|
||||
|
||||
const generateRandomUsername = useCallback(async () => {
|
||||
try {
|
||||
const usernameEmailGenerator = CreateUsernameEmailGenerator();
|
||||
|
||||
let gender = Gender.Other;
|
||||
try {
|
||||
gender = watch('Alias.Gender') as Gender;
|
||||
} catch {
|
||||
// Gender parsing failed, default to other.
|
||||
}
|
||||
|
||||
const identity: Identity = {
|
||||
firstName: watch('Alias.FirstName') ?? '',
|
||||
lastName: watch('Alias.LastName') ?? '',
|
||||
nickName: watch('Alias.NickName') ?? '',
|
||||
gender: gender,
|
||||
birthDate: new Date(watch('Alias.BirthDate') ?? ''),
|
||||
emailPrefix: watch('Alias.Email') ?? '',
|
||||
};
|
||||
|
||||
const username = usernameEmailGenerator.generateUsername(identity);
|
||||
setValue('Username', username);
|
||||
} catch (error) {
|
||||
console.error('Error generating random username:', error);
|
||||
}
|
||||
}, [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]);
|
||||
|
||||
/**
|
||||
* Handle form submission.
|
||||
*/
|
||||
@@ -336,7 +375,7 @@ const CredentialAddEdit: React.FC = () => {
|
||||
mode === 'random' ? 'bg-primary-500 text-white' : 'bg-gray-200 dark:bg-gray-700 text-gray-700 dark:text-gray-300'
|
||||
}`}
|
||||
>
|
||||
<svg className='w-5 h-5' viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<svg className='w-5 h-5' viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
|
||||
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"/>
|
||||
<circle cx="8" cy="8" r="1"/>
|
||||
<circle cx="16" cy="8" r="1"/>
|
||||
@@ -396,6 +435,13 @@ const CredentialAddEdit: React.FC = () => {
|
||||
value={watch('Username') ?? ''}
|
||||
onChange={(value) => setValue('Username', value)}
|
||||
error={errors.Username?.message}
|
||||
buttons={[
|
||||
{
|
||||
icon: 'refresh',
|
||||
onClick: generateRandomUsername,
|
||||
title: 'Generate random username'
|
||||
}
|
||||
]}
|
||||
/>
|
||||
<FormInput
|
||||
id="password"
|
||||
@@ -404,6 +450,15 @@ const CredentialAddEdit: React.FC = () => {
|
||||
value={watch('Password') ?? ''}
|
||||
onChange={(value) => setValue('Password', value)}
|
||||
error={errors.Password?.message}
|
||||
showPassword={showPassword}
|
||||
onShowPasswordChange={setShowPassword}
|
||||
buttons={[
|
||||
{
|
||||
icon: 'refresh',
|
||||
onClick: generateRandomPassword,
|
||||
title: 'Generate random password'
|
||||
}
|
||||
]}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
|
||||
Reference in New Issue
Block a user