Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ dist
node_modules
docs
*.html
coverage
21 changes: 9 additions & 12 deletions eslint.config.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import js from '@eslint/js'
import globals from 'globals'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'
import tseslint from 'typescript-eslint'
import js from '@eslint/js';
import globals from 'globals';
import reactHooks from 'eslint-plugin-react-hooks';
import reactRefresh from 'eslint-plugin-react-refresh';
import tseslint from 'typescript-eslint';

export default tseslint.config(
{ ignores: ['dist'] },
{ ignores: ['dist', 'coverage'] },
{
extends: [js.configs.recommended, ...tseslint.configs.recommended],
files: ['**/*.{ts,tsx}'],
Expand All @@ -19,10 +19,7 @@ export default tseslint.config(
},
rules: {
...reactHooks.configs.recommended.rules,
'react-refresh/only-export-components': [
'warn',
{ allowConstantExport: true },
],
'react-refresh/only-export-components': ['warn', { allowConstantExport: true }],
},
},
)
}
);
67 changes: 52 additions & 15 deletions src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useMemo, useState } from 'react';
import { useCallback, useEffect, useMemo, useState } from 'react';
import {
Box,
Button,
Expand All @@ -21,12 +21,16 @@ interface Mark {
label: string;
}

const gcd = (a: number, b: number): number => {
if (b === 0) return a;
return gcd(b, a % b);
};

function App() {
const defaultLength = 16;
const minLength = 4;
const maxLength = 64;

const [toggle, setToggle] = useState<boolean>(false);
const [copied, setCopied] = useState<boolean>(false);

const [length, setLength] = useState<number>(defaultLength);
Expand All @@ -36,9 +40,23 @@ function App() {
const [special, setSpecial] = useState<boolean>(false);
const [excludeAmbiguous, setExcludeAmbiguous] = useState<boolean>(true);

const password = useMemo<string>(() => {
return genPassword(length, lowercase, uppercase, digits, special, excludeAmbiguous);
}, [toggle, length, lowercase, uppercase, digits, special, excludeAmbiguous]);
const enabledCharSetsCount =
Number(lowercase) + Number(uppercase) + Number(digits) + Number(special);

const isLastEnabledCharSet = enabledCharSetsCount === 1;

const [password, setPassword] = useState<string>(() =>
genPassword(defaultLength, true, true, true, false, true)
);

const regeneratePassword = useCallback(() => {
setPassword(genPassword(length, lowercase, uppercase, digits, special, excludeAmbiguous));
}, [length, lowercase, uppercase, digits, special, excludeAmbiguous]);

// Regenerate whenever inputs change
useEffect(() => {
regeneratePassword();
}, [regeneratePassword]);

const handleSliderChange = (_event: Event, newValue: number | number[]) => {
if (Array.isArray(newValue)) {
Expand Down Expand Up @@ -74,11 +92,6 @@ function App() {
});
};

const gcd = (a: number, b: number): number => {
if (b === 0) return a;
return gcd(b, a % b);
};

const marks = useMemo<Mark[]>(() => {
const result: Mark[] = [];

Expand Down Expand Up @@ -106,19 +119,43 @@ function App() {
<Divider />
<Stack>
<FormControlLabel
control={<Switch checked={lowercase} onChange={() => setLowercase(!lowercase)} />}
control={
<Switch
checked={lowercase}
onChange={() => setLowercase(!lowercase)}
disabled={isLastEnabledCharSet && lowercase}
/>
}
label="Lowercase"
/>
<FormControlLabel
control={<Switch checked={uppercase} onChange={() => setUppercase(!uppercase)} />}
control={
<Switch
checked={uppercase}
onChange={() => setUppercase(!uppercase)}
disabled={isLastEnabledCharSet && uppercase}
/>
}
label="Uppercase"
/>
<FormControlLabel
control={<Switch checked={digits} onChange={() => setDigits(!digits)} />}
control={
<Switch
checked={digits}
onChange={() => setDigits(!digits)}
disabled={isLastEnabledCharSet && digits}
/>
}
label="Digits"
/>
<FormControlLabel
control={<Switch checked={special} onChange={() => setSpecial(!special)} />}
control={
<Switch
checked={special}
onChange={() => setSpecial(!special)}
disabled={isLastEnabledCharSet && special}
/>
}
label="Special"
/>
<FormControlLabel
Expand Down Expand Up @@ -175,7 +212,7 @@ function App() {
<Button
variant="contained"
size="large"
onClick={() => setToggle(!toggle)}
onClick={regeneratePassword}
endIcon={<RunCircleIcon />}
>
Generate
Expand Down
153 changes: 79 additions & 74 deletions src/genPassword.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,87 +14,92 @@ const random = (min: number, max: number): number => {
};

const genPassword = (
length: number = 32,
useLowercase: boolean = true,
useUppercase: boolean = false,
useDigits: boolean = false,
useSpecial: boolean = false,
excludeAmbiguous: boolean = false
length: number = 32,
useLowercase: boolean = true,
useUppercase: boolean = false,
useDigits: boolean = false,
useSpecial: boolean = false,
excludeAmbiguous: boolean = false
): string => {
// Define ambiguous characters to exclude
// 0O (zero/O), 1lI (one/l/I), 5S (five/S), 8B (eight/B), 2Z (two/Z), 6b (six/b), 9q (nine/q)
const ambiguousChars = '0O1lIo5S8B2Z6b9q';

// Filter character sets if excludeAmbiguous is enabled
const filterAmbiguous = (chars: string): string => {
if (!excludeAmbiguous) return chars;
return chars.split('').filter(char => !ambiguousChars.includes(char)).join('');
};

const charSets: string[] = [];
let charPool = '';

if (useLowercase) {
const filteredLowercase = filterAmbiguous(lowercase);
if (filteredLowercase.length > 0) {
charSets.push(filteredLowercase);
charPool += filteredLowercase;
}
// Define ambiguous characters to exclude
// 0O (zero/O), 1lI (one/l/I), 5S (five/S), 8B (eight/B), 2Z (two/Z), 6b (six/b), 9q (nine/q)
const ambiguousChars = '0O1lIo5S8B2Z6b9q';

// Filter character sets if excludeAmbiguous is enabled
const filterAmbiguous = (chars: string): string => {
if (!excludeAmbiguous) return chars;
return chars
.split('')
.filter((char) => !ambiguousChars.includes(char))
.join('');
};

const charSets: string[] = [];
let charPool = '';

if (useLowercase) {
const filteredLowercase = filterAmbiguous(lowercase);
if (filteredLowercase.length > 0) {
charSets.push(filteredLowercase);
charPool += filteredLowercase;
}
}

if (useUppercase) {
const filteredUppercase = filterAmbiguous(uppercase);
if (filteredUppercase.length > 0) {
charSets.push(filteredUppercase);
charPool += filteredUppercase;
}
if (useUppercase) {
const filteredUppercase = filterAmbiguous(uppercase);
if (filteredUppercase.length > 0) {
charSets.push(filteredUppercase);
charPool += filteredUppercase;
}
}

if (useDigits) {
const filteredDigits = filterAmbiguous(digits);
if (filteredDigits.length > 0) {
charSets.push(filteredDigits);
charPool += filteredDigits;
}
if (useDigits) {
const filteredDigits = filterAmbiguous(digits);
if (filteredDigits.length > 0) {
charSets.push(filteredDigits);
charPool += filteredDigits;
}
}

if (useSpecial) {
const filteredSpecial = filterAmbiguous(special);
if (filteredSpecial.length > 0) {
charSets.push(filteredSpecial);
charPool += filteredSpecial;
}
if (useSpecial) {
const filteredSpecial = filterAmbiguous(special);
if (filteredSpecial.length > 0) {
charSets.push(filteredSpecial);
charPool += filteredSpecial;
}

if (charPool.length === 0) {
throw new Error('At least one character type must be selected');
}

if (length < charSets.length) {
throw new Error(`Password length must be at least ${charSets.length} to include all selected character types`);
}

const result: string[] = [];

// Step 1: Guarantee at least one character from each selected set
for (const set of charSets) {
const randomIndex = random(0, set.length);
result.push(set[randomIndex]);
}

// Step 2: Fill remaining positions with random characters from the full pool
for (let i = charSets.length; i < length; i++) {
const randomIndex = random(0, charPool.length);
result.push(charPool[randomIndex]);
}

// Step 3: Shuffle to avoid predictable patterns (e.g., always lowercase first)
for (let i = result.length - 1; i > 0; i--) {
const j = random(0, i + 1);
[result[i], result[j]] = [result[j], result[i]];
}

return result.join('');
}
}

if (charPool.length === 0) {
throw new Error('At least one character type must be selected');
}

if (length < charSets.length) {
throw new Error(
`Password length must be at least ${charSets.length} to include all selected character types`
);
}

const result: string[] = [];

// Step 1: Guarantee at least one character from each selected set
for (const set of charSets) {
const randomIndex = random(0, set.length);
result.push(set[randomIndex]);
}

// Step 2: Fill remaining positions with random characters from the full pool
for (let i = charSets.length; i < length; i++) {
const randomIndex = random(0, charPool.length);
result.push(charPool[randomIndex]);
}

// Step 3: Shuffle to avoid predictable patterns (e.g., always lowercase first)
for (let i = result.length - 1; i > 0; i--) {
const j = random(0, i + 1);
[result[i], result[j]] = [result[j], result[i]];
}

return result.join('');
};

export default genPassword;