mirror of
https://github.com/iib0011/omni-tools.git
synced 2025-09-26 09:29:30 +02:00
feat: password generator to test translation
This commit is contained in:
@@ -17,6 +17,7 @@ import { tool as stringTruncate } from './truncate/meta';
|
||||
import { tool as stringBase64 } from './base64/meta';
|
||||
import { tool as stringStatistic } from './statistic/meta';
|
||||
import { tool as stringCensor } from './censor/meta';
|
||||
import { tool as stringPasswordGenerator } from './password-generator/meta';
|
||||
|
||||
export const stringTools = [
|
||||
stringSplit,
|
||||
@@ -37,5 +38,6 @@ export const stringTools = [
|
||||
stringRot13,
|
||||
stringBase64,
|
||||
stringStatistic,
|
||||
stringCensor
|
||||
stringCensor,
|
||||
stringPasswordGenerator
|
||||
];
|
||||
|
165
src/pages/tools/string/password-generator/index.tsx
Normal file
165
src/pages/tools/string/password-generator/index.tsx
Normal file
@@ -0,0 +1,165 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Box, Checkbox, FormControlLabel, FormGroup } from '@mui/material';
|
||||
import { generatePassword } from './service';
|
||||
import { initialValues, InitialValuesType } from './initialValues';
|
||||
import ToolContent from '@components/ToolContent';
|
||||
import ToolTextResult from '@components/result/ToolTextResult';
|
||||
import TextFieldWithDesc from '@components/options/TextFieldWithDesc';
|
||||
import { ToolComponentProps } from '@tools/defineTool';
|
||||
import { GetGroupsType } from '@components/options/ToolOptions';
|
||||
import { CardExampleType } from '@components/examples/ToolExamples';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const exampleCards: CardExampleType<InitialValuesType>[] = [
|
||||
{
|
||||
title: 'Strong Password (12 characters)',
|
||||
description:
|
||||
'Generate a secure password with all character types including symbols.',
|
||||
sampleText: '',
|
||||
sampleResult: 'A7#mK9$pL2@x',
|
||||
sampleOptions: {
|
||||
length: '12',
|
||||
includeLowercase: true,
|
||||
includeUppercase: true,
|
||||
includeNumbers: true,
|
||||
includeSymbols: true,
|
||||
avoidAmbiguous: false
|
||||
}
|
||||
},
|
||||
{
|
||||
title: 'Simple Password (8 characters)',
|
||||
description: 'Generate a basic password with letters and numbers only.',
|
||||
sampleText: '',
|
||||
sampleResult: 'Ab3mK9pL',
|
||||
sampleOptions: {
|
||||
length: '8',
|
||||
includeLowercase: true,
|
||||
includeUppercase: true,
|
||||
includeNumbers: true,
|
||||
includeSymbols: false,
|
||||
avoidAmbiguous: false
|
||||
}
|
||||
},
|
||||
{
|
||||
title: 'Clear Password (No ambiguous)',
|
||||
description:
|
||||
'Generate a password without ambiguous characters (i, I, l, 0, O).',
|
||||
sampleText: '',
|
||||
sampleResult: 'A7#mK9$pL2@x',
|
||||
sampleOptions: {
|
||||
length: '12',
|
||||
includeLowercase: true,
|
||||
includeUppercase: true,
|
||||
includeNumbers: true,
|
||||
includeSymbols: true,
|
||||
avoidAmbiguous: true
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
export default function PasswordGenerator({ title }: ToolComponentProps) {
|
||||
const { t } = useTranslation('string');
|
||||
const [result, setResult] = useState<string>('');
|
||||
|
||||
function compute(values: InitialValuesType) {
|
||||
setResult(generatePassword(values));
|
||||
}
|
||||
|
||||
const getGroups: GetGroupsType<InitialValuesType> = ({
|
||||
values,
|
||||
updateField
|
||||
}) => [
|
||||
{
|
||||
title: t('passwordGenerator.optionsTitle'),
|
||||
component: (
|
||||
<Box sx={{ display: 'flex', gap: 2, flexDirection: 'column' }}>
|
||||
<TextFieldWithDesc
|
||||
description={t('passwordGenerator.lengthDesc')}
|
||||
placeholder={t('passwordGenerator.lengthPlaceholder')}
|
||||
value={values.length}
|
||||
onOwnChange={(val) => updateField('length', val)}
|
||||
type="number"
|
||||
/>
|
||||
|
||||
<FormGroup>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
checked={values.includeLowercase}
|
||||
onChange={(e) =>
|
||||
updateField('includeLowercase', e.target.checked)
|
||||
}
|
||||
/>
|
||||
}
|
||||
label={t('passwordGenerator.includeLowercase')}
|
||||
/>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
checked={values.includeUppercase}
|
||||
onChange={(e) =>
|
||||
updateField('includeUppercase', e.target.checked)
|
||||
}
|
||||
/>
|
||||
}
|
||||
label={t('passwordGenerator.includeUppercase')}
|
||||
/>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
checked={values.includeNumbers}
|
||||
onChange={(e) =>
|
||||
updateField('includeNumbers', e.target.checked)
|
||||
}
|
||||
/>
|
||||
}
|
||||
label={t('passwordGenerator.includeNumbers')}
|
||||
/>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
checked={values.includeSymbols}
|
||||
onChange={(e) =>
|
||||
updateField('includeSymbols', e.target.checked)
|
||||
}
|
||||
/>
|
||||
}
|
||||
label={t('passwordGenerator.includeSymbols')}
|
||||
/>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
checked={values.avoidAmbiguous}
|
||||
onChange={(e) =>
|
||||
updateField('avoidAmbiguous', e.target.checked)
|
||||
}
|
||||
/>
|
||||
}
|
||||
label={t('passwordGenerator.avoidAmbiguous')}
|
||||
/>
|
||||
</FormGroup>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
];
|
||||
|
||||
return (
|
||||
<ToolContent
|
||||
title={title}
|
||||
initialValues={initialValues}
|
||||
getGroups={getGroups}
|
||||
compute={compute}
|
||||
resultComponent={
|
||||
<ToolTextResult
|
||||
title={t('passwordGenerator.resultTitle')}
|
||||
value={result}
|
||||
/>
|
||||
}
|
||||
toolInfo={{
|
||||
title: t('passwordGenerator.toolInfo.title'),
|
||||
description: t('passwordGenerator.toolInfo.description')
|
||||
}}
|
||||
exampleCards={exampleCards}
|
||||
/>
|
||||
);
|
||||
}
|
17
src/pages/tools/string/password-generator/initialValues.ts
Normal file
17
src/pages/tools/string/password-generator/initialValues.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
export type InitialValuesType = {
|
||||
length: string; // user enters a number here
|
||||
includeLowercase: boolean;
|
||||
includeUppercase: boolean;
|
||||
includeNumbers: boolean;
|
||||
includeSymbols: boolean;
|
||||
avoidAmbiguous: boolean;
|
||||
};
|
||||
|
||||
export const initialValues: InitialValuesType = {
|
||||
length: '12',
|
||||
includeLowercase: true,
|
||||
includeUppercase: true,
|
||||
includeNumbers: true,
|
||||
includeSymbols: true,
|
||||
avoidAmbiguous: false
|
||||
};
|
14
src/pages/tools/string/password-generator/meta.ts
Normal file
14
src/pages/tools/string/password-generator/meta.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { defineTool } from '@tools/defineTool';
|
||||
import { lazy } from 'react';
|
||||
|
||||
export const tool = defineTool('string', {
|
||||
path: 'password-generator',
|
||||
icon: 'material-symbols:key',
|
||||
keywords: ['password', 'generator', 'random', 'secure'],
|
||||
component: lazy(() => import('./index')),
|
||||
i18n: {
|
||||
name: 'string:passwordGenerator.title',
|
||||
description: 'string:passwordGenerator.description',
|
||||
shortDescription: 'string:passwordGenerator.shortDescription'
|
||||
}
|
||||
});
|
@@ -0,0 +1,143 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { generatePassword } from './service';
|
||||
import { initialValues } from './initialValues';
|
||||
|
||||
describe('generatePassword', () => {
|
||||
it('should generate a password with the specified length', () => {
|
||||
const options = { ...initialValues, length: '10' };
|
||||
const result = generatePassword(options);
|
||||
expect(result).toHaveLength(10);
|
||||
});
|
||||
|
||||
it('should return empty string for invalid length', () => {
|
||||
const options = { ...initialValues, length: '0' };
|
||||
const result = generatePassword(options);
|
||||
expect(result).toBe('');
|
||||
});
|
||||
|
||||
it('should return empty string for non-numeric length', () => {
|
||||
const options = { ...initialValues, length: 'abc' };
|
||||
const result = generatePassword(options);
|
||||
expect(result).toBe('');
|
||||
});
|
||||
|
||||
it('should return empty string when no character types are selected', () => {
|
||||
const options = {
|
||||
...initialValues,
|
||||
includeLowercase: false,
|
||||
includeUppercase: false,
|
||||
includeNumbers: false,
|
||||
includeSymbols: false
|
||||
};
|
||||
const result = generatePassword(options);
|
||||
expect(result).toBe('');
|
||||
});
|
||||
|
||||
it('should only include lowercase letters when only lowercase is selected', () => {
|
||||
const options = {
|
||||
...initialValues,
|
||||
length: '20',
|
||||
includeLowercase: true,
|
||||
includeUppercase: false,
|
||||
includeNumbers: false,
|
||||
includeSymbols: false
|
||||
};
|
||||
const result = generatePassword(options);
|
||||
expect(result).toMatch(/^[a-z]+$/);
|
||||
expect(result).toHaveLength(20);
|
||||
});
|
||||
|
||||
it('should only include uppercase letters when only uppercase is selected', () => {
|
||||
const options = {
|
||||
...initialValues,
|
||||
length: '15',
|
||||
includeLowercase: false,
|
||||
includeUppercase: true,
|
||||
includeNumbers: false,
|
||||
includeSymbols: false
|
||||
};
|
||||
const result = generatePassword(options);
|
||||
expect(result).toMatch(/^[A-Z]+$/);
|
||||
expect(result).toHaveLength(15);
|
||||
});
|
||||
|
||||
it('should only include numbers when only numbers is selected', () => {
|
||||
const options = {
|
||||
...initialValues,
|
||||
length: '8',
|
||||
includeLowercase: false,
|
||||
includeUppercase: false,
|
||||
includeNumbers: true,
|
||||
includeSymbols: false
|
||||
};
|
||||
const result = generatePassword(options);
|
||||
expect(result).toMatch(/^[0-9]+$/);
|
||||
expect(result).toHaveLength(8);
|
||||
});
|
||||
|
||||
it('should include mixed character types when multiple are selected', () => {
|
||||
const options = {
|
||||
...initialValues,
|
||||
length: '100', // larger sample for better testing
|
||||
includeLowercase: true,
|
||||
includeUppercase: true,
|
||||
includeNumbers: true,
|
||||
includeSymbols: false
|
||||
};
|
||||
const result = generatePassword(options);
|
||||
expect(result).toMatch(/^[a-zA-Z0-9]+$/);
|
||||
expect(result).toHaveLength(100);
|
||||
});
|
||||
|
||||
it('should exclude ambiguous characters when avoidAmbiguous is true', () => {
|
||||
const options = {
|
||||
...initialValues,
|
||||
length: '50',
|
||||
avoidAmbiguous: true
|
||||
};
|
||||
const result = generatePassword(options);
|
||||
expect(result).not.toMatch(/[iIl0O]/);
|
||||
expect(result).toHaveLength(50);
|
||||
});
|
||||
|
||||
it('should include symbols when includeSymbols is true', () => {
|
||||
const options = {
|
||||
...initialValues,
|
||||
length: '30',
|
||||
includeLowercase: false,
|
||||
includeUppercase: false,
|
||||
includeNumbers: false,
|
||||
includeSymbols: true
|
||||
};
|
||||
const result = generatePassword(options);
|
||||
expect(result).toMatch(/^[!@#$%^&*()_+~`|}{[\]:;?><,./-=]+$/);
|
||||
expect(result).toHaveLength(30);
|
||||
});
|
||||
|
||||
it('should exclude ambiguous characters from symbols too', () => {
|
||||
const options = {
|
||||
...initialValues,
|
||||
length: '50',
|
||||
includeLowercase: false,
|
||||
includeUppercase: false,
|
||||
includeNumbers: true,
|
||||
includeSymbols: true,
|
||||
avoidAmbiguous: true
|
||||
};
|
||||
const result = generatePassword(options);
|
||||
expect(result).not.toMatch(/[iIl0O]/);
|
||||
expect(result).toHaveLength(50);
|
||||
});
|
||||
|
||||
it('should handle edge case with very short length', () => {
|
||||
const options = { ...initialValues, length: '1' };
|
||||
const result = generatePassword(options);
|
||||
expect(result).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('should handle negative length', () => {
|
||||
const options = { ...initialValues, length: '-5' };
|
||||
const result = generatePassword(options);
|
||||
expect(result).toBe('');
|
||||
});
|
||||
});
|
38
src/pages/tools/string/password-generator/service.ts
Normal file
38
src/pages/tools/string/password-generator/service.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import type { InitialValuesType } from './initialValues';
|
||||
|
||||
export function generatePassword(options: InitialValuesType): string {
|
||||
const length = parseInt(options.length || '', 10);
|
||||
if (isNaN(length) || length <= 0) {
|
||||
return '';
|
||||
}
|
||||
|
||||
let charset = '';
|
||||
const lower = 'abcdefghijklmnopqrstuvwxyz';
|
||||
const upper = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
|
||||
const numbers = '0123456789';
|
||||
const symbols = '!@#$%^&*()_+~`|}{[]:;?><,./-=';
|
||||
|
||||
if (options.includeLowercase) charset += lower;
|
||||
if (options.includeUppercase) charset += upper;
|
||||
if (options.includeNumbers) charset += numbers;
|
||||
if (options.includeSymbols) charset += symbols;
|
||||
|
||||
if (options.avoidAmbiguous) {
|
||||
// ambiguous set = i, I, l, 0, O
|
||||
const ambig = new Set(['i', 'I', 'l', '0', 'O']);
|
||||
charset = Array.from(charset)
|
||||
.filter((c) => !ambig.has(c))
|
||||
.join('');
|
||||
}
|
||||
|
||||
if (!charset) {
|
||||
return ''; // nothing to pick from
|
||||
}
|
||||
|
||||
let pwd = '';
|
||||
for (let i = 0; i < length; i++) {
|
||||
const idx = Math.floor(Math.random() * charset.length);
|
||||
pwd += charset[idx];
|
||||
}
|
||||
return pwd;
|
||||
}
|
Reference in New Issue
Block a user