diff --git a/README.md b/README.md index b76671c..5dbf094 100644 --- a/README.md +++ b/README.md @@ -50,6 +50,7 @@ Here is the list of all utilities: - [WebP Converter](https://jam.dev/utilities/webp-converter) - [SQL Minifer](https://jam.dev/utilities/sql-minifier) - [Internet Speed Test](https://jam.dev/utilities/internet-speed-test) +- [Password Generator](https://jam.dev/utilities/password-generator) ### Built With diff --git a/components/seo/PasswordGeneratorSEO.tsx b/components/seo/PasswordGeneratorSEO.tsx new file mode 100644 index 0000000..eb08edb --- /dev/null +++ b/components/seo/PasswordGeneratorSEO.tsx @@ -0,0 +1,92 @@ +export default function PasswordGeneratorSEO() { + return ( +
+
+

+ Generate secure passwords instantly with this free online password + generator. Whether you're protecting your accounts, apps, or + sensitive data, Jam's Password Generator creates strong, + randomized passwords to keep you safe. +

+
+ +
+

How to Use Jam's Password Generator

+

+ Quickly create a strong password tailored to your needs. Follow these + simple steps: +

+ +
+ +
+

Why Use a Password Generator?

+

+ Manually created passwords are often weak and easy to guess. A + password generator helps you avoid common mistakes and ensures your + credentials are strong. +

+ +
+ +
+

FAQs

+ +
+
+ ); +} diff --git a/components/utils/password-generator.utils.test.ts b/components/utils/password-generator.utils.test.ts new file mode 100644 index 0000000..96eefb5 --- /dev/null +++ b/components/utils/password-generator.utils.test.ts @@ -0,0 +1,52 @@ +import { PasswordBuilder } from "./password-generator.utils"; + +describe("PasswordBuilder", () => { + it("generates a character pool correctly according to options", () => { + const builder = new PasswordBuilder(true, false, false, false, 8); + const result = builder.Build(); + + expect(result).toMatch(/^[a-z]+$/); + expect(result.length).toBe(8); + }); + + it("includes at least one character from each selected category", () => { + const builder = new PasswordBuilder(true, true, true, true, 20); + const password = builder.Build(); + + expect(password).toMatch(/[a-z]/); // lowercase + expect(password).toMatch(/[A-Z]/); // uppercase + expect(password).toMatch(/[0-9]/); // numbers + expect(password).toMatch(/[!@#$%^&*()_+[\]{}|;:,.<>?/~`\-=]/); // symbols + expect(password.length).toBe(20); + }); + + it("respects the minimum and maximum length constraints", () => { + const builderMin = new PasswordBuilder(true, false, false, false, 0); + expect(builderMin.Build().length).toBeGreaterThanOrEqual(1); + + const builderMax = new PasswordBuilder(true, false, false, false, 999); + expect(builderMax.Build().length).toBeLessThanOrEqual(128); + }); + + it("handles when desired length is smaller than the number of categories", () => { + // 4 categories but desired length 2 + const builder = new PasswordBuilder(true, true, true, true, 2); + const password = builder.Build(); + + // Must include at least one char from each category + expect(password).toMatch(/[a-z]/); + expect(password).toMatch(/[A-Z]/); + expect(password).toMatch(/[0-9]/); + expect(password).toMatch(/[!@#$%^&*()_+[\]{}|;:,.<>?/~`\-=]/); + // And length must be at least 4 + expect(password.length).toBeGreaterThanOrEqual(4); + }); + + it("GetMandatoryChars returns characters from the correct categories", () => { + const builder = new PasswordBuilder(true, true, false, false, 10); + const picks = builder.GetMandatoryChars(); + expect(picks.some((c) => /[a-z]/.test(c))).toBeTruthy(); + expect(picks.some((c) => /[A-Z]/.test(c))).toBeTruthy(); + expect(picks.length).toBe(2); // two categories selected + }); +}); diff --git a/components/utils/password-generator.utils.ts b/components/utils/password-generator.utils.ts new file mode 100644 index 0000000..7f0662a --- /dev/null +++ b/components/utils/password-generator.utils.ts @@ -0,0 +1,82 @@ +export const getRandomInt = (max: number) => { + const arr = new Uint32Array(1); + window.crypto.getRandomValues(arr); + return arr[0] % max; +}; + +export class PasswordBuilder { + private LOWER = "abcdefghijklmnopqrstuvwxyz"; + private UPPER = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; + private NUMS = "0123456789"; + private SYMBOLS = "!@#$%^&*()_+[]{}|;:,.<>?/~`-="; + private characterPool = ""; + + constructor( + private includeLowercase: boolean, + private includeUppercase: boolean, + private includeNumbers: boolean, + private includeSymbols: boolean, + private desiredLength: number + ) { + let pool = ""; + if (this.includeLowercase) pool += this.LOWER; + if (this.includeUppercase) pool += this.UPPER; + if (this.includeNumbers) pool += this.NUMS; + if (this.includeSymbols) pool += this.SYMBOLS; + this.characterPool = pool; + } + + GetMandatoryChars() { + const picks: string[] = []; + + if (this.includeLowercase) { + const lowers = this.LOWER.split(""); + picks.push(lowers[getRandomInt(lowers.length)]); + } + if (this.includeUppercase) { + const uppers = this.UPPER.split(""); + picks.push(uppers[getRandomInt(uppers.length)]); + } + if (this.includeNumbers) { + const numbers = this.NUMS.split(""); + picks.push(numbers[getRandomInt(numbers.length)]); + } + if (this.includeSymbols) { + const symbols = this.SYMBOLS.split(""); + picks.push(symbols[getRandomInt(symbols.length)]); + } + + return picks; + } + + Build() { + const categoriesAmount = [ + this.includeLowercase, + this.includeUppercase, + this.includeNumbers, + this.includeSymbols, + ].filter((b) => !!b); + + const finalLen = Math.max(1, Math.min(128, this.desiredLength)); + const useLen = + finalLen < categoriesAmount.length ? categoriesAmount.length : finalLen; + + // start with selected categories mandatory characters + const guaranteed = this.GetMandatoryChars(); + const resultChars: string[] = [...guaranteed]; + + // fill the rest of the input length + for (let i = resultChars.length; i < useLen; i++) { + const idx = getRandomInt(this.characterPool.length); + resultChars.push(this.characterPool[idx]); + } + + // Fisher-Yates shuffle algorithm + for (let i = resultChars.length - 1; i > 0; i--) { + const j = getRandomInt(i + 1); + [resultChars[i], resultChars[j]] = [resultChars[j], resultChars[i]]; + } + + return resultChars.join(""); + } +} diff --git a/components/utils/tools-list.ts b/components/utils/tools-list.ts index fe66855..eacad4c 100644 --- a/components/utils/tools-list.ts +++ b/components/utils/tools-list.ts @@ -155,4 +155,10 @@ export const tools = [ "Test your internet connection speed with accurate measurements by using Cloudflare's global network.", link: "/utilities/internet-speed-test", }, + { + title: "Password Generator", + description: + "Mix uppercase, lowercase, numbers, and symbols to generate bulletproof passwords that will keep your accounts safe.", + link: "/utilities/password-generator", + }, ]; diff --git a/pages/utilities/password-generator.tsx b/pages/utilities/password-generator.tsx new file mode 100644 index 0000000..b06919b --- /dev/null +++ b/pages/utilities/password-generator.tsx @@ -0,0 +1,207 @@ +import { useCallback, useState, useRef, useMemo } from "react"; +import { Textarea } from "@/components/ds/TextareaComponent"; +import PageHeader from "@/components/PageHeader"; +import { Card } from "@/components/ds/CardComponent"; +import { Button } from "@/components/ds/ButtonComponent"; +import { Label } from "@/components/ds/LabelComponent"; +import Header from "@/components/Header"; +import { Checkbox } from "@/components/ds/CheckboxComponent"; +import { CMDK } from "@/components/CMDK"; +import { useCopyToClipboard } from "@/components/hooks/useCopyToClipboard"; +import CallToActionGrid from "@/components/CallToActionGrid"; +import Meta from "@/components/Meta"; +import { Input } from "@/components/ds/InputComponent"; +import { PasswordBuilder } from "@/components/utils/password-generator.utils"; +import PasswordGeneratorSEO from "@/components/seo/PasswordGeneratorSEO"; +import GitHubContribution from "@/components/GitHubContribution"; +import { cn } from "@/lib/utils"; + +export default function PasswordGenerator() { + const [password, setPassword] = useState(""); + const [length, setLength] = useState(16); + const [includeLowercase, setIncludeLowercase] = useState(true); + const [includeUppercase, setIncludeUppercase] = useState(true); + const [includeNumbers, setIncludeNumbers] = useState(true); + const [includeSymbols, setIncludeSymbols] = useState(true); + const { buttonText, handleCopy } = useCopyToClipboard(); + const outputRef = useRef(null); + + const generatePassword = useCallback(() => { + const builder = new PasswordBuilder( + includeLowercase, + includeUppercase, + includeNumbers, + includeSymbols, + length + ); + setPassword(builder.Build()); + }, [ + includeLowercase, + includeUppercase, + includeNumbers, + includeSymbols, + length, + ]); + + const strengthInfo = useMemo(() => { + const types = [ + includeLowercase, + includeUppercase, + includeNumbers, + includeSymbols, + ].filter(Boolean).length; + + if (length >= 20 && types >= 3) { + return { + label: "Very Strong", + className: "bg-green-600", + }; + } + if (length >= 12 && types >= 3) { + return { label: "Strong", className: "bg-green-500" }; + } + if (length >= 10 && types >= 2) { + return { label: "Medium", className: "bg-yellow-500" }; + } + return { label: "Weak", className: "bg-red-500" }; + }, [ + length, + includeLowercase, + includeUppercase, + includeNumbers, + includeSymbols, + ]); + + return ( +
+ +
+ + +
+ +
+ +
+ +
+
+ +
+ +
+ +
+ + + + + + + +
+ +
+ +
+
+ + setLength(Number(e.target.value))} + className="h-8 w-full" + /> +
+ +
+ + +
+
+ {strengthInfo.label} +
+
+
+ +
+ +
+ +
+ +
+ +
+
+ +
+ +