1
0
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:
Eugene Fox
2024-09-25 16:19:12 +03:00
committed by GitHub
parent f2683e37b2
commit 3ecb6c4a31
71 changed files with 14338 additions and 7531 deletions
+35
View File
@@ -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]
};
+128
View File
@@ -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("");
}
+42
View File
@@ -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";
};
+6
View File
@@ -0,0 +1,6 @@
export default class ExtensionOptions
{
public MinLength: number = 4;
public MaxLength: number = 32;
public ContextMenu: boolean = true;
}
+14
View File
@@ -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;
}
+53
View File
@@ -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
+5
View File
@@ -0,0 +1,5 @@
import ExtensionOptions from "./ExtensionOptions";
import GeneratorOptions from "./GeneratorOptions";
export { useStorage, StorageProvider } from "./Storage";
export { ExtensionOptions, GeneratorOptions };
+21
View File
@@ -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;
}
+14
View File
@@ -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 };
}