mirror of
https://github.com/XFox111/PasswordGeneratorExtension.git
synced 2026-04-22 08:08:01 +03:00
Major 4.0 (#380)
- Migrated to WXT - Migrated to NPM - Added Insert & copy action - Added ESLint
This commit is contained in:
@@ -0,0 +1,35 @@
|
||||
import { BrandVariants, Theme, createDarkTheme, createLightTheme } from "@fluentui/react-components";
|
||||
|
||||
const bmcBrandRamp: BrandVariants =
|
||||
{
|
||||
"10": "#050201",
|
||||
"20": "#20140C",
|
||||
"30": "#372014",
|
||||
"40": "#492918",
|
||||
"50": "#5C321D",
|
||||
"60": "#6F3C21",
|
||||
"70": "#834525",
|
||||
"80": "#984F2A",
|
||||
"90": "#AD5A2E",
|
||||
"100": "#C36433",
|
||||
"110": "#D96E37",
|
||||
"120": "#EF793C",
|
||||
"130": "#FF884A",
|
||||
"140": "#FFA170",
|
||||
"150": "#FFB792",
|
||||
"160": "#FFCCB3"
|
||||
};
|
||||
|
||||
export const bmcLightTheme: Theme =
|
||||
{
|
||||
...createLightTheme(bmcBrandRamp),
|
||||
colorBrandBackground: bmcBrandRamp[110],
|
||||
};
|
||||
|
||||
export const bmcDarkTheme: Theme =
|
||||
{
|
||||
...createDarkTheme(bmcBrandRamp),
|
||||
colorBrandBackground: bmcBrandRamp[120],
|
||||
colorBrandForeground1: bmcBrandRamp[110],
|
||||
colorBrandForeground2: bmcBrandRamp[120]
|
||||
};
|
||||
@@ -0,0 +1,128 @@
|
||||
import { GeneratorOptions } from "./storage";
|
||||
|
||||
const Characters =
|
||||
{
|
||||
Uppercase: "ABCDEFGHIJKLMNOPQRSTUVWXYZ",
|
||||
Lowercase: "abcdefghijklmnopqrstuvwxyz",
|
||||
Numeric: "1234567890",
|
||||
Special: "!#$%&*+-=?@^_"
|
||||
};
|
||||
|
||||
const Similar: string = "iIl1Lo0O";
|
||||
const Ambiguous: string = "{}[]()/\\'\"`~,;:.<>";
|
||||
|
||||
export const CharacterHints = { ...Characters, Similar, Ambiguous };
|
||||
|
||||
/**
|
||||
* Generates a random password
|
||||
* @param options Options for password generation
|
||||
* @returns Randomly generated password
|
||||
* @throws Error if options are invalid
|
||||
*/
|
||||
export function GeneratePassword(options: GeneratorOptions): string
|
||||
{
|
||||
ValidateOptions(options);
|
||||
|
||||
let password: string = GetRequiredCharacters(options);
|
||||
const availableCharacters: string = GetAvailableCharacters(options);
|
||||
|
||||
for (let i = password.length; i < options.Length; i++)
|
||||
{
|
||||
const character: string = PickRandomFromArray(availableCharacters);
|
||||
|
||||
if (options.ExcludeRepeating && password.includes(character))
|
||||
i--;
|
||||
else
|
||||
password += character;
|
||||
}
|
||||
|
||||
password = ShuffleString(password);
|
||||
return password;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates options for password generation
|
||||
* @param options Options for password generation
|
||||
* @throws Error if options are invalid
|
||||
*/
|
||||
export function ValidateOptions(options: GeneratorOptions): void
|
||||
{
|
||||
if (options.Length < 4)
|
||||
throw new Error(i18n.t("errors.too_short"));
|
||||
|
||||
const availableCharacters: string = GetAvailableCharacters(options);
|
||||
|
||||
if (availableCharacters.length < 1)
|
||||
throw new Error(i18n.t("errors.no_characters"));
|
||||
|
||||
if (options.ExcludeRepeating && options.Length > availableCharacters.length)
|
||||
throw new Error(i18n.t("errors.too_long"));
|
||||
}
|
||||
|
||||
// Returns a string containing all characters that are available for password generation
|
||||
function GetAvailableCharacters(options: GeneratorOptions): string
|
||||
{
|
||||
let availableCharacters: string = "";
|
||||
|
||||
for (const [key, value] of Object.entries(Characters))
|
||||
if (options[key as keyof GeneratorOptions])
|
||||
availableCharacters += value;
|
||||
|
||||
if (options.ExcludeSimilar)
|
||||
availableCharacters = availableCharacters.replace(new RegExp(`[${Similar}]`, "g"), "");
|
||||
|
||||
if (options.Special && !options.ExcludeAmbiguous)
|
||||
availableCharacters += Ambiguous;
|
||||
|
||||
return availableCharacters;
|
||||
}
|
||||
|
||||
// Returns a string containing all characters from every available set that are required for password generation
|
||||
function GetRequiredCharacters(options: GeneratorOptions): string
|
||||
{
|
||||
let result: string = "";
|
||||
const characters: Record<string, string> = Object.assign({}, Characters);
|
||||
|
||||
if (!options.ExcludeAmbiguous)
|
||||
characters.Special += Ambiguous;
|
||||
|
||||
if (options.ExcludeSimilar)
|
||||
for (const key of Object.keys(characters))
|
||||
characters[key] = characters[key].replace(new RegExp(`[${Similar}]`, "g"), "");
|
||||
|
||||
for (const [key, value] of Object.entries(characters))
|
||||
if (options[key as keyof GeneratorOptions])
|
||||
result += PickRandomFromArray(value);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// Picks a random character from a string
|
||||
function PickRandomFromArray(array: string): string
|
||||
{
|
||||
return array[GetRandomInt(0, array.length)];
|
||||
}
|
||||
|
||||
// See https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Math/random
|
||||
// min is inclusive, max is exclusive
|
||||
function GetRandomInt(min: number, max: number): number
|
||||
{
|
||||
const arr = new Uint16Array(1);
|
||||
crypto.getRandomValues(arr); // Using crypto instead of Math.random() as a CSPRNG
|
||||
return Math.floor((arr[0] / 65_536) * (max - min)) + min;
|
||||
}
|
||||
|
||||
// Shuffles a string using Fisher-Yates algorithm and CSPRNG
|
||||
// See https://en.wikipedia.org/wiki/Fisher%E2%80%93Yates_shuffle
|
||||
function ShuffleString(str: string): string
|
||||
{
|
||||
const arr = str.split("");
|
||||
|
||||
for (let i = arr.length - 1; i > 0; i--)
|
||||
{
|
||||
const j = GetRandomInt(0, i + 1);
|
||||
[arr[i], arr[j]] = [arr[j], arr[i]];
|
||||
}
|
||||
|
||||
return arr.join("");
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import { Manifest } from "webextension-polyfill";
|
||||
|
||||
export const personalLinks =
|
||||
{
|
||||
website: "https://xfox111.net",
|
||||
twitter: "https://twitter.com/xfox111",
|
||||
donation: "https://buymeacoffee.com/xfox111"
|
||||
};
|
||||
|
||||
export const storeLinks =
|
||||
{
|
||||
chrome: "https://chrome.google.com/webstore/detail/password-generator/jnjobgjobffgmgfnkpkjfjkkfhfikmfl",
|
||||
edge: "https://microsoftedge.microsoft.com/addons/detail/password-generator/manimdhobjbkfpeeehlhhneookiokpbj",
|
||||
firefox: "https://addons.mozilla.org/firefox/addon/easy-password-generator"
|
||||
};
|
||||
|
||||
const getGithub = (path?: string): string =>
|
||||
new URL(path ?? "", "https://github.com/xfox111/PasswordGeneratorExtension/").href;
|
||||
|
||||
export const githubLinks =
|
||||
{
|
||||
repository: getGithub(),
|
||||
changelog: getGithub("releases/latest"),
|
||||
translationGuide: getGithub("wiki/Contribution-Guidelines#contributing-to-translations"),
|
||||
license: getGithub("blob/main/LICENSE")
|
||||
};
|
||||
|
||||
export const getFeedbackLink = () =>
|
||||
{
|
||||
if (import.meta.env.FIREFOX)
|
||||
return storeLinks.firefox;
|
||||
|
||||
const manifest: Manifest.WebExtensionManifest = browser.runtime.getManifest();
|
||||
const updateUrl: URL = new URL((manifest as unknown as Record<string, unknown>).update_url as string ?? "about:blank");
|
||||
|
||||
if (updateUrl.host === "edge.microsoft.com")
|
||||
return storeLinks.edge;
|
||||
if (updateUrl.host === "clients2.google.com")
|
||||
return storeLinks.chrome;
|
||||
|
||||
return "mailto:feedback@xfox111.net";
|
||||
};
|
||||
@@ -0,0 +1,6 @@
|
||||
export default class ExtensionOptions
|
||||
{
|
||||
public MinLength: number = 4;
|
||||
public MaxLength: number = 32;
|
||||
public ContextMenu: boolean = true;
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
export default class GeneratorOptions
|
||||
{
|
||||
public Length: number = 8;
|
||||
|
||||
public Special: boolean = true;
|
||||
public Numeric: boolean = true;
|
||||
public Lowercase: boolean = true;
|
||||
public Uppercase: boolean = true;
|
||||
|
||||
public ExcludeSimilar: boolean = true;
|
||||
public ExcludeAmbiguous: boolean = true;
|
||||
|
||||
public ExcludeRepeating: boolean = false;
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
import React, { createContext, useContext, useEffect, useState } from "react";
|
||||
import ExtensionOptions from "./ExtensionOptions";
|
||||
import GeneratorOptions from "./GeneratorOptions";
|
||||
|
||||
const defaultOptions: IStorage =
|
||||
{
|
||||
generatorOptions: new GeneratorOptions(),
|
||||
extOptions: new ExtensionOptions(),
|
||||
updateStorage: async () => { }
|
||||
};
|
||||
|
||||
const Storage = createContext<IStorage>(defaultOptions);
|
||||
|
||||
export const useStorage = () => useContext<IStorage>(Storage);
|
||||
|
||||
export const StorageProvider: React.FC<IStorageProviderProps> = (props) =>
|
||||
{
|
||||
const [storage, setStorage] = useState<IStorage | null>(null);
|
||||
|
||||
const getStorage = async () =>
|
||||
setStorage({
|
||||
extOptions: await browser.storage.sync.get(defaultOptions.extOptions) as ExtensionOptions,
|
||||
generatorOptions: await browser.storage.sync.get(defaultOptions.generatorOptions) as GeneratorOptions,
|
||||
updateStorage: async (options) => await browser.storage.sync.set(options)
|
||||
});
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
getStorage();
|
||||
browser.storage.sync.onChanged.addListener(getStorage);
|
||||
return () => browser.storage.sync.onChanged.removeListener(getStorage);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Storage.Provider value={ storage ?? defaultOptions }>
|
||||
{ storage ? props.children : props.loader }
|
||||
</Storage.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
// #region Types
|
||||
interface IStorage
|
||||
{
|
||||
generatorOptions: GeneratorOptions;
|
||||
extOptions: ExtensionOptions;
|
||||
updateStorage: (options: Partial<GeneratorOptions | ExtensionOptions>) => Promise<void>;
|
||||
}
|
||||
|
||||
interface IStorageProviderProps extends React.PropsWithChildren
|
||||
{
|
||||
loader?: JSX.Element;
|
||||
}
|
||||
// #endregion
|
||||
@@ -0,0 +1,5 @@
|
||||
import ExtensionOptions from "./ExtensionOptions";
|
||||
import GeneratorOptions from "./GeneratorOptions";
|
||||
|
||||
export { useStorage, StorageProvider } from "./Storage";
|
||||
export { ExtensionOptions, GeneratorOptions };
|
||||
@@ -0,0 +1,21 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { Theme, webDarkTheme, webLightTheme } from "@fluentui/react-components";
|
||||
|
||||
export function useTheme(lightTheme?: Theme, darkTheme?: Theme): Theme
|
||||
{
|
||||
const media = window.matchMedia("(prefers-color-scheme: dark)");
|
||||
|
||||
const getTheme = (isDark: boolean) =>
|
||||
isDark ? (darkTheme ?? webDarkTheme) : (lightTheme ?? webLightTheme);
|
||||
|
||||
const [theme, setTheme] = useState<Theme>(getTheme(media.matches));
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
const updateTheme = (args: MediaQueryListEvent) => setTheme(getTheme(args.matches));
|
||||
media.addEventListener("change", updateTheme);
|
||||
return () => media.removeEventListener("change", updateTheme);
|
||||
}, []);
|
||||
|
||||
return theme;
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { useState } from "react";
|
||||
|
||||
export default function useTimeout(timeout: number)
|
||||
{
|
||||
const [isActive, setActive] = useState<boolean>(false);
|
||||
|
||||
const trigger = () =>
|
||||
{
|
||||
setActive(true);
|
||||
setTimeout(() => setActive(false), timeout);
|
||||
};
|
||||
|
||||
return { isActive, trigger };
|
||||
}
|
||||
Reference in New Issue
Block a user