mirror of
https://github.com/bitwarden/browser
synced 2025-12-30 15:13:32 +00:00
[PM-6819] Credential generator MV3 integration (#8998)
* replace `PasswordGeneratorService` with `legacyPasswordGenerationServiceFactory` * replace `UsernameGeneratorService` with `legacyUsernameGenerationServiceFactory` * migrate generator options and history * apply policy immediately once available * suppress duplicate policy emissions * run password generation response code in `ngZone`
This commit is contained in:
@@ -60,13 +60,16 @@ import { RemoveLegacyEtmKeyMigrator } from "./migrations/6-remove-legacy-etm-key
|
||||
import { KnownAccountsMigrator } from "./migrations/60-known-accounts";
|
||||
import { PinStateMigrator } from "./migrations/61-move-pin-state-to-providers";
|
||||
import { VaultTimeoutSettingsServiceStateProviderMigrator } from "./migrations/62-migrate-vault-timeout-settings-svc-to-state-provider";
|
||||
import { PasswordOptionsMigrator } from "./migrations/63-migrate-password-settings";
|
||||
import { GeneratorHistoryMigrator } from "./migrations/64-migrate-generator-history";
|
||||
import { ForwarderOptionsMigrator } from "./migrations/65-migrate-forwarder-settings";
|
||||
import { MoveBiometricAutoPromptToAccount } from "./migrations/7-move-biometric-auto-prompt-to-account";
|
||||
import { MoveStateVersionMigrator } from "./migrations/8-move-state-version";
|
||||
import { MoveBrowserSettingsToGlobal } from "./migrations/9-move-browser-settings-to-global";
|
||||
import { MinVersionMigrator } from "./migrations/min-version";
|
||||
|
||||
export const MIN_VERSION = 3;
|
||||
export const CURRENT_VERSION = 62;
|
||||
export const CURRENT_VERSION = 65;
|
||||
export type MinVersion = typeof MIN_VERSION;
|
||||
|
||||
export function createMigrationBuilder() {
|
||||
@@ -130,7 +133,10 @@ export function createMigrationBuilder() {
|
||||
.with(KdfConfigMigrator, 58, 59)
|
||||
.with(KnownAccountsMigrator, 59, 60)
|
||||
.with(PinStateMigrator, 60, 61)
|
||||
.with(VaultTimeoutSettingsServiceStateProviderMigrator, 61, CURRENT_VERSION);
|
||||
.with(VaultTimeoutSettingsServiceStateProviderMigrator, 61, 62)
|
||||
.with(PasswordOptionsMigrator, 62, 63)
|
||||
.with(GeneratorHistoryMigrator, 63, 64)
|
||||
.with(ForwarderOptionsMigrator, 64, CURRENT_VERSION);
|
||||
}
|
||||
|
||||
export async function currentVersion(
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
import { MigrationHelper } from "../migration-helper";
|
||||
import { mockMigrationHelper } from "../migration-helper.spec";
|
||||
|
||||
import {
|
||||
ExpectedOptions,
|
||||
PasswordOptionsMigrator,
|
||||
NAVIGATION,
|
||||
PASSWORD,
|
||||
PASSPHRASE,
|
||||
} from "./63-migrate-password-settings";
|
||||
|
||||
function migrationHelper(passwordGenerationOptions: ExpectedOptions) {
|
||||
const helper = mockMigrationHelper(
|
||||
{
|
||||
global_account_accounts: {
|
||||
SomeAccount: {
|
||||
email: "SomeAccount",
|
||||
name: "SomeAccount",
|
||||
emailVerified: true,
|
||||
},
|
||||
},
|
||||
SomeAccount: {
|
||||
settings: {
|
||||
passwordGenerationOptions,
|
||||
this: {
|
||||
looks: "important",
|
||||
},
|
||||
},
|
||||
cant: {
|
||||
touch: "this",
|
||||
},
|
||||
},
|
||||
},
|
||||
62,
|
||||
);
|
||||
|
||||
return helper;
|
||||
}
|
||||
|
||||
function expectOtherSettingsRemain(helper: MigrationHelper) {
|
||||
expect(helper.set).toHaveBeenCalledWith("SomeAccount", {
|
||||
settings: {
|
||||
this: {
|
||||
looks: "important",
|
||||
},
|
||||
},
|
||||
cant: {
|
||||
touch: "this",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
describe("PasswordOptionsMigrator", () => {
|
||||
describe("migrate", () => {
|
||||
it("migrates generator type", async () => {
|
||||
const helper = migrationHelper({
|
||||
type: "password",
|
||||
});
|
||||
helper.getFromUser.mockResolvedValue({ some: { other: "data" } });
|
||||
const migrator = new PasswordOptionsMigrator(62, 63);
|
||||
|
||||
await migrator.migrate(helper);
|
||||
|
||||
expect(helper.setToUser).toHaveBeenCalledWith("SomeAccount", NAVIGATION, {
|
||||
type: "password",
|
||||
some: { other: "data" },
|
||||
});
|
||||
expectOtherSettingsRemain(helper);
|
||||
});
|
||||
|
||||
it("migrates password settings", async () => {
|
||||
const helper = migrationHelper({
|
||||
length: 20,
|
||||
ambiguous: true,
|
||||
uppercase: false,
|
||||
minUppercase: 4,
|
||||
lowercase: true,
|
||||
minLowercase: 3,
|
||||
number: false,
|
||||
minNumber: 2,
|
||||
special: true,
|
||||
minSpecial: 1,
|
||||
});
|
||||
const migrator = new PasswordOptionsMigrator(62, 63);
|
||||
|
||||
await migrator.migrate(helper);
|
||||
|
||||
expect(helper.setToUser).toHaveBeenCalledWith("SomeAccount", PASSWORD, {
|
||||
length: 20,
|
||||
ambiguous: true,
|
||||
uppercase: false,
|
||||
minUppercase: 4,
|
||||
lowercase: true,
|
||||
minLowercase: 3,
|
||||
number: false,
|
||||
minNumber: 2,
|
||||
special: true,
|
||||
minSpecial: 1,
|
||||
});
|
||||
expectOtherSettingsRemain(helper);
|
||||
});
|
||||
|
||||
it("migrates passphrase settings", async () => {
|
||||
const helper = migrationHelper({
|
||||
numWords: 5,
|
||||
wordSeparator: "4",
|
||||
capitalize: true,
|
||||
includeNumber: false,
|
||||
});
|
||||
const migrator = new PasswordOptionsMigrator(62, 63);
|
||||
|
||||
await migrator.migrate(helper);
|
||||
|
||||
expect(helper.setToUser).toHaveBeenCalledWith("SomeAccount", PASSPHRASE, {
|
||||
numWords: 5,
|
||||
wordSeparator: "4",
|
||||
capitalize: true,
|
||||
includeNumber: false,
|
||||
});
|
||||
expectOtherSettingsRemain(helper);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,150 @@
|
||||
import { KeyDefinitionLike, MigrationHelper } from "../migration-helper";
|
||||
import { Migrator } from "../migrator";
|
||||
|
||||
/** settings targeted by migrator */
|
||||
export type AccountType = {
|
||||
settings?: {
|
||||
passwordGenerationOptions?: ExpectedOptions;
|
||||
};
|
||||
};
|
||||
|
||||
export type GeneratorType = "password" | "passphrase" | "username";
|
||||
|
||||
/** username generation options prior to refactoring */
|
||||
export type ExpectedOptions = {
|
||||
type?: GeneratorType;
|
||||
length?: number;
|
||||
minLength?: number;
|
||||
ambiguous?: boolean;
|
||||
uppercase?: boolean;
|
||||
minUppercase?: number;
|
||||
lowercase?: boolean;
|
||||
minLowercase?: number;
|
||||
number?: boolean;
|
||||
minNumber?: number;
|
||||
special?: boolean;
|
||||
minSpecial?: number;
|
||||
numWords?: number;
|
||||
wordSeparator?: string;
|
||||
capitalize?: boolean;
|
||||
includeNumber?: boolean;
|
||||
};
|
||||
|
||||
/** username generation options after refactoring */
|
||||
type ConvertedOptions = {
|
||||
generator: GeneratorNavigation;
|
||||
password: PasswordGenerationOptions;
|
||||
passphrase: PassphraseGenerationOptions;
|
||||
};
|
||||
|
||||
export const NAVIGATION: KeyDefinitionLike = {
|
||||
stateDefinition: {
|
||||
name: "generator",
|
||||
},
|
||||
key: "generatorSettings",
|
||||
};
|
||||
|
||||
export const PASSWORD: KeyDefinitionLike = {
|
||||
stateDefinition: {
|
||||
name: "generator",
|
||||
},
|
||||
key: "passwordGeneratorSettings",
|
||||
};
|
||||
|
||||
export const PASSPHRASE: KeyDefinitionLike = {
|
||||
stateDefinition: {
|
||||
name: "generator",
|
||||
},
|
||||
key: "passphraseGeneratorSettings",
|
||||
};
|
||||
|
||||
export type GeneratorNavigation = {
|
||||
type?: string;
|
||||
};
|
||||
|
||||
export type PassphraseGenerationOptions = {
|
||||
numWords?: number;
|
||||
wordSeparator?: string;
|
||||
capitalize?: boolean;
|
||||
includeNumber?: boolean;
|
||||
};
|
||||
|
||||
export type PasswordGenerationOptions = {
|
||||
length?: number;
|
||||
minLength?: number;
|
||||
ambiguous?: boolean;
|
||||
uppercase?: boolean;
|
||||
minUppercase?: number;
|
||||
lowercase?: boolean;
|
||||
minLowercase?: number;
|
||||
number?: boolean;
|
||||
minNumber?: number;
|
||||
special?: boolean;
|
||||
minSpecial?: number;
|
||||
};
|
||||
|
||||
export class PasswordOptionsMigrator extends Migrator<62, 63> {
|
||||
async migrate(helper: MigrationHelper): Promise<void> {
|
||||
const accounts = await helper.getAccounts<AccountType>();
|
||||
|
||||
async function migrateAccount(userId: string, account: AccountType) {
|
||||
const legacyOptions = account?.settings?.passwordGenerationOptions;
|
||||
|
||||
if (legacyOptions) {
|
||||
const converted = convertSettings(legacyOptions);
|
||||
await storeSettings(helper, userId, converted);
|
||||
await deleteSettings(helper, userId, account);
|
||||
}
|
||||
}
|
||||
|
||||
await Promise.all([...accounts.map(({ userId, account }) => migrateAccount(userId, account))]);
|
||||
}
|
||||
|
||||
async rollback(helper: MigrationHelper): Promise<void> {
|
||||
// not supported
|
||||
}
|
||||
}
|
||||
|
||||
function convertSettings(options: ExpectedOptions): ConvertedOptions {
|
||||
const password = {
|
||||
length: options.length,
|
||||
ambiguous: options.ambiguous,
|
||||
uppercase: options.uppercase,
|
||||
minUppercase: options.minUppercase,
|
||||
lowercase: options.lowercase,
|
||||
minLowercase: options.minLowercase,
|
||||
number: options.number,
|
||||
minNumber: options.minNumber,
|
||||
special: options.special,
|
||||
minSpecial: options.minSpecial,
|
||||
};
|
||||
|
||||
const generator = {
|
||||
type: options.type,
|
||||
};
|
||||
|
||||
const passphrase = {
|
||||
numWords: options.numWords,
|
||||
wordSeparator: options.wordSeparator,
|
||||
capitalize: options.capitalize,
|
||||
includeNumber: options.includeNumber,
|
||||
};
|
||||
|
||||
return { generator, password, passphrase };
|
||||
}
|
||||
|
||||
async function storeSettings(helper: MigrationHelper, userId: string, converted: ConvertedOptions) {
|
||||
const existing = (await helper.getFromUser(userId, NAVIGATION)) ?? {};
|
||||
const updated = Object.assign(existing, converted.generator);
|
||||
|
||||
await Promise.all([
|
||||
helper.setToUser(userId, NAVIGATION, updated),
|
||||
helper.setToUser(userId, PASSPHRASE, converted.passphrase),
|
||||
helper.setToUser(userId, PASSWORD, converted.password),
|
||||
]);
|
||||
}
|
||||
|
||||
async function deleteSettings(helper: MigrationHelper, userId: string, account: AccountType) {
|
||||
delete account?.settings?.passwordGenerationOptions;
|
||||
await helper.set(userId, account);
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
import { MigrationHelper } from "../migration-helper";
|
||||
import { mockMigrationHelper } from "../migration-helper.spec";
|
||||
|
||||
import {
|
||||
EncryptedHistory,
|
||||
GeneratorHistoryMigrator,
|
||||
HISTORY,
|
||||
} from "./64-migrate-generator-history";
|
||||
|
||||
function migrationHelper(encrypted: EncryptedHistory) {
|
||||
const helper = mockMigrationHelper(
|
||||
{
|
||||
global_account_accounts: {
|
||||
SomeAccount: {
|
||||
email: "SomeAccount",
|
||||
name: "SomeAccount",
|
||||
emailVerified: true,
|
||||
},
|
||||
},
|
||||
SomeAccount: {
|
||||
data: {
|
||||
passwordGenerationHistory: {
|
||||
encrypted,
|
||||
},
|
||||
this: {
|
||||
looks: "important",
|
||||
},
|
||||
},
|
||||
cant: {
|
||||
touch: "this",
|
||||
},
|
||||
},
|
||||
},
|
||||
63,
|
||||
);
|
||||
|
||||
return helper;
|
||||
}
|
||||
|
||||
function expectOtherSettingsRemain(helper: MigrationHelper) {
|
||||
expect(helper.set).toHaveBeenCalledWith("SomeAccount", {
|
||||
data: {
|
||||
this: {
|
||||
looks: "important",
|
||||
},
|
||||
},
|
||||
cant: {
|
||||
touch: "this",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
describe("PasswordOptionsMigrator", () => {
|
||||
describe("migrate", () => {
|
||||
it("migrates generator type", async () => {
|
||||
const helper = migrationHelper([{ this: "should be copied" }, { this: "too" }]);
|
||||
const migrator = new GeneratorHistoryMigrator(63, 64);
|
||||
|
||||
await migrator.migrate(helper);
|
||||
|
||||
expect(helper.setToUser).toHaveBeenCalledWith("SomeAccount", HISTORY, [
|
||||
{ this: "should be copied" },
|
||||
{ this: "too" },
|
||||
]);
|
||||
expectOtherSettingsRemain(helper);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,42 @@
|
||||
import { KeyDefinitionLike, MigrationHelper } from "../migration-helper";
|
||||
import { Migrator } from "../migrator";
|
||||
|
||||
/** settings targeted by migrator */
|
||||
export type AccountType = {
|
||||
data?: {
|
||||
passwordGenerationHistory?: {
|
||||
encrypted: EncryptedHistory;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
/** the actual data stored in the history is opaque to the migrator */
|
||||
export type EncryptedHistory = Array<unknown>;
|
||||
|
||||
export const HISTORY: KeyDefinitionLike = {
|
||||
stateDefinition: {
|
||||
name: "generator",
|
||||
},
|
||||
key: "localGeneratorHistoryBuffer",
|
||||
};
|
||||
|
||||
export class GeneratorHistoryMigrator extends Migrator<63, 64> {
|
||||
async migrate(helper: MigrationHelper): Promise<void> {
|
||||
const accounts = await helper.getAccounts<AccountType>();
|
||||
|
||||
async function migrateAccount(userId: string, account: AccountType) {
|
||||
const data = account?.data?.passwordGenerationHistory;
|
||||
if (data && data.encrypted) {
|
||||
await helper.setToUser(userId, HISTORY, data.encrypted);
|
||||
delete account.data.passwordGenerationHistory;
|
||||
await helper.set(userId, account);
|
||||
}
|
||||
}
|
||||
|
||||
await Promise.all([...accounts.map(({ userId, account }) => migrateAccount(userId, account))]);
|
||||
}
|
||||
|
||||
async rollback(helper: MigrationHelper): Promise<void> {
|
||||
// not supported
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,218 @@
|
||||
import { MigrationHelper } from "../migration-helper";
|
||||
import { mockMigrationHelper } from "../migration-helper.spec";
|
||||
|
||||
import {
|
||||
ADDY_IO,
|
||||
CATCHALL,
|
||||
DUCK_DUCK_GO,
|
||||
EFF_USERNAME,
|
||||
ExpectedOptions,
|
||||
FASTMAIL,
|
||||
FIREFOX_RELAY,
|
||||
FORWARD_EMAIL,
|
||||
ForwarderOptionsMigrator,
|
||||
NAVIGATION,
|
||||
SIMPLE_LOGIN,
|
||||
SUBADDRESS,
|
||||
} from "./65-migrate-forwarder-settings";
|
||||
|
||||
function migrationHelper(usernameGenerationOptions: ExpectedOptions) {
|
||||
const helper = mockMigrationHelper(
|
||||
{
|
||||
global_account_accounts: {
|
||||
SomeAccount: {
|
||||
email: "SomeAccount",
|
||||
name: "SomeAccount",
|
||||
emailVerified: true,
|
||||
},
|
||||
},
|
||||
SomeAccount: {
|
||||
settings: {
|
||||
usernameGenerationOptions,
|
||||
this: {
|
||||
looks: "important",
|
||||
},
|
||||
},
|
||||
cant: {
|
||||
touch: "this",
|
||||
},
|
||||
},
|
||||
},
|
||||
64,
|
||||
);
|
||||
|
||||
return helper;
|
||||
}
|
||||
|
||||
function expectOtherSettingsRemain(helper: MigrationHelper) {
|
||||
expect(helper.set).toHaveBeenCalledWith("SomeAccount", {
|
||||
settings: {
|
||||
this: {
|
||||
looks: "important",
|
||||
},
|
||||
},
|
||||
cant: {
|
||||
touch: "this",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
describe("ForwarderOptionsMigrator", () => {
|
||||
describe("migrate", () => {
|
||||
it("migrates generator settings", async () => {
|
||||
const helper = migrationHelper({
|
||||
type: "catchall",
|
||||
forwardedService: "simplelogin",
|
||||
});
|
||||
const migrator = new ForwarderOptionsMigrator(64, 65);
|
||||
|
||||
await migrator.migrate(helper);
|
||||
|
||||
expect(helper.setToUser).toHaveBeenCalledWith("SomeAccount", NAVIGATION, {
|
||||
username: "catchall",
|
||||
forwarder: "simplelogin",
|
||||
});
|
||||
expectOtherSettingsRemain(helper);
|
||||
});
|
||||
|
||||
it("migrates catchall settings", async () => {
|
||||
const helper = migrationHelper({
|
||||
catchallType: "random",
|
||||
catchallDomain: "example.com",
|
||||
});
|
||||
const migrator = new ForwarderOptionsMigrator(64, 65);
|
||||
|
||||
await migrator.migrate(helper);
|
||||
|
||||
expect(helper.setToUser).toHaveBeenCalledWith("SomeAccount", CATCHALL, {
|
||||
catchallType: "random",
|
||||
catchallDomain: "example.com",
|
||||
});
|
||||
expectOtherSettingsRemain(helper);
|
||||
});
|
||||
|
||||
it("migrates EFF username settings", async () => {
|
||||
const helper = migrationHelper({
|
||||
wordCapitalize: true,
|
||||
wordIncludeNumber: false,
|
||||
});
|
||||
const migrator = new ForwarderOptionsMigrator(64, 65);
|
||||
|
||||
await migrator.migrate(helper);
|
||||
|
||||
expect(helper.setToUser).toHaveBeenCalledWith("SomeAccount", EFF_USERNAME, {
|
||||
wordCapitalize: true,
|
||||
wordIncludeNumber: false,
|
||||
});
|
||||
expectOtherSettingsRemain(helper);
|
||||
});
|
||||
|
||||
it("migrates subaddress settings", async () => {
|
||||
const helper = migrationHelper({
|
||||
subaddressType: "random",
|
||||
subaddressEmail: "j.d@example.com",
|
||||
});
|
||||
const migrator = new ForwarderOptionsMigrator(64, 65);
|
||||
|
||||
await migrator.migrate(helper);
|
||||
|
||||
expect(helper.setToUser).toHaveBeenCalledWith("SomeAccount", SUBADDRESS, {
|
||||
subaddressType: "random",
|
||||
subaddressEmail: "j.d@example.com",
|
||||
});
|
||||
expectOtherSettingsRemain(helper);
|
||||
});
|
||||
|
||||
it("migrates addyIo settings", async () => {
|
||||
const helper = migrationHelper({
|
||||
forwardedAnonAddyBaseUrl: "some_addyio_base",
|
||||
forwardedAnonAddyApiToken: "some_addyio_token",
|
||||
forwardedAnonAddyDomain: "some_addyio_domain",
|
||||
});
|
||||
const migrator = new ForwarderOptionsMigrator(64, 65);
|
||||
|
||||
await migrator.migrate(helper);
|
||||
|
||||
expect(helper.setToUser).toHaveBeenCalledWith("SomeAccount", ADDY_IO, {
|
||||
baseUrl: "some_addyio_base",
|
||||
token: "some_addyio_token",
|
||||
domain: "some_addyio_domain",
|
||||
});
|
||||
expectOtherSettingsRemain(helper);
|
||||
});
|
||||
|
||||
it("migrates DuckDuckGo settings", async () => {
|
||||
const helper = migrationHelper({
|
||||
forwardedDuckDuckGoToken: "some_duckduckgo_token",
|
||||
});
|
||||
const migrator = new ForwarderOptionsMigrator(64, 65);
|
||||
|
||||
await migrator.migrate(helper);
|
||||
|
||||
expect(helper.setToUser).toHaveBeenCalledWith("SomeAccount", DUCK_DUCK_GO, {
|
||||
token: "some_duckduckgo_token",
|
||||
});
|
||||
expectOtherSettingsRemain(helper);
|
||||
});
|
||||
|
||||
it("migrates Firefox Relay settings", async () => {
|
||||
const helper = migrationHelper({
|
||||
forwardedFirefoxApiToken: "some_firefox_token",
|
||||
});
|
||||
const migrator = new ForwarderOptionsMigrator(64, 65);
|
||||
|
||||
await migrator.migrate(helper);
|
||||
|
||||
expect(helper.setToUser).toHaveBeenCalledWith("SomeAccount", FIREFOX_RELAY, {
|
||||
token: "some_firefox_token",
|
||||
});
|
||||
expectOtherSettingsRemain(helper);
|
||||
});
|
||||
|
||||
it("migrates Fastmail settings", async () => {
|
||||
const helper = migrationHelper({
|
||||
forwardedFastmailApiToken: "some_fastmail_token",
|
||||
});
|
||||
const migrator = new ForwarderOptionsMigrator(64, 65);
|
||||
|
||||
await migrator.migrate(helper);
|
||||
|
||||
expect(helper.setToUser).toHaveBeenCalledWith("SomeAccount", FASTMAIL, {
|
||||
token: "some_fastmail_token",
|
||||
});
|
||||
expectOtherSettingsRemain(helper);
|
||||
});
|
||||
|
||||
it("migrates ForwardEmail settings", async () => {
|
||||
const helper = migrationHelper({
|
||||
forwardedForwardEmailApiToken: "some_forwardemail_token",
|
||||
forwardedForwardEmailDomain: "some_forwardemail_domain",
|
||||
});
|
||||
const migrator = new ForwarderOptionsMigrator(64, 65);
|
||||
|
||||
await migrator.migrate(helper);
|
||||
|
||||
expect(helper.setToUser).toHaveBeenCalledWith("SomeAccount", FORWARD_EMAIL, {
|
||||
token: "some_forwardemail_token",
|
||||
domain: "some_forwardemail_domain",
|
||||
});
|
||||
expectOtherSettingsRemain(helper);
|
||||
});
|
||||
|
||||
it("migrates SimpleLogin settings", async () => {
|
||||
const helper = migrationHelper({
|
||||
forwardedSimpleLoginApiKey: "some_simplelogin_token",
|
||||
forwardedSimpleLoginBaseUrl: "some_simplelogin_baseurl",
|
||||
});
|
||||
const migrator = new ForwarderOptionsMigrator(64, 65);
|
||||
|
||||
await migrator.migrate(helper);
|
||||
|
||||
expect(helper.setToUser).toHaveBeenCalledWith("SomeAccount", SIMPLE_LOGIN, {
|
||||
token: "some_simplelogin_token",
|
||||
baseUrl: "some_simplelogin_baseurl",
|
||||
});
|
||||
expectOtherSettingsRemain(helper);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,245 @@
|
||||
import { KeyDefinitionLike, MigrationHelper } from "../migration-helper";
|
||||
import { Migrator } from "../migrator";
|
||||
|
||||
/** settings targeted by migrator */
|
||||
export type AccountType = {
|
||||
settings?: {
|
||||
usernameGenerationOptions?: ExpectedOptions;
|
||||
};
|
||||
};
|
||||
|
||||
/** username generation options prior to refactoring */
|
||||
export type ExpectedOptions = {
|
||||
type?: "word" | "subaddress" | "catchall" | "forwarded";
|
||||
wordCapitalize?: boolean;
|
||||
wordIncludeNumber?: boolean;
|
||||
subaddressType?: "random" | "website-name";
|
||||
subaddressEmail?: string;
|
||||
catchallType?: "random" | "website-name";
|
||||
catchallDomain?: string;
|
||||
forwardedService?: string;
|
||||
forwardedAnonAddyApiToken?: string;
|
||||
forwardedAnonAddyDomain?: string;
|
||||
forwardedAnonAddyBaseUrl?: string;
|
||||
forwardedDuckDuckGoToken?: string;
|
||||
forwardedFirefoxApiToken?: string;
|
||||
forwardedFastmailApiToken?: string;
|
||||
forwardedForwardEmailApiToken?: string;
|
||||
forwardedForwardEmailDomain?: string;
|
||||
forwardedSimpleLoginApiKey?: string;
|
||||
forwardedSimpleLoginBaseUrl?: string;
|
||||
};
|
||||
|
||||
/** username generation options after refactoring */
|
||||
type ConvertedOptions = {
|
||||
generator: GeneratorNavigation;
|
||||
algorithms: {
|
||||
catchall: CatchallGenerationOptions;
|
||||
effUsername: EffUsernameGenerationOptions;
|
||||
subaddress: SubaddressGenerationOptions;
|
||||
};
|
||||
forwarders: {
|
||||
addyIo: SelfHostedApiOptions & EmailDomainOptions;
|
||||
duckDuckGo: ApiOptions;
|
||||
fastmail: ApiOptions;
|
||||
firefoxRelay: ApiOptions;
|
||||
forwardEmail: ApiOptions & EmailDomainOptions;
|
||||
simpleLogin: SelfHostedApiOptions;
|
||||
};
|
||||
};
|
||||
|
||||
export const NAVIGATION: KeyDefinitionLike = {
|
||||
stateDefinition: {
|
||||
name: "generator",
|
||||
},
|
||||
key: "generatorSettings",
|
||||
};
|
||||
|
||||
export const CATCHALL: KeyDefinitionLike = {
|
||||
stateDefinition: {
|
||||
name: "generator",
|
||||
},
|
||||
key: "catchallGeneratorSettings",
|
||||
};
|
||||
|
||||
export const EFF_USERNAME: KeyDefinitionLike = {
|
||||
stateDefinition: {
|
||||
name: "generator",
|
||||
},
|
||||
key: "effUsernameGeneratorSettings",
|
||||
};
|
||||
|
||||
export const SUBADDRESS: KeyDefinitionLike = {
|
||||
stateDefinition: {
|
||||
name: "generator",
|
||||
},
|
||||
key: "subaddressGeneratorSettings",
|
||||
};
|
||||
|
||||
export const ADDY_IO: KeyDefinitionLike = {
|
||||
stateDefinition: {
|
||||
name: "generator",
|
||||
},
|
||||
key: "addyIoBuffer",
|
||||
};
|
||||
|
||||
export const DUCK_DUCK_GO: KeyDefinitionLike = {
|
||||
stateDefinition: {
|
||||
name: "generator",
|
||||
},
|
||||
key: "duckDuckGoBuffer",
|
||||
};
|
||||
|
||||
export const FASTMAIL: KeyDefinitionLike = {
|
||||
stateDefinition: {
|
||||
name: "generator",
|
||||
},
|
||||
key: "fastmailBuffer",
|
||||
};
|
||||
|
||||
export const FIREFOX_RELAY: KeyDefinitionLike = {
|
||||
stateDefinition: {
|
||||
name: "generator",
|
||||
},
|
||||
key: "firefoxRelayBuffer",
|
||||
};
|
||||
|
||||
export const FORWARD_EMAIL: KeyDefinitionLike = {
|
||||
stateDefinition: {
|
||||
name: "generator",
|
||||
},
|
||||
key: "forwardEmailBuffer",
|
||||
};
|
||||
|
||||
export const SIMPLE_LOGIN: KeyDefinitionLike = {
|
||||
stateDefinition: {
|
||||
name: "generator",
|
||||
},
|
||||
key: "simpleLoginBuffer",
|
||||
};
|
||||
|
||||
export type GeneratorNavigation = {
|
||||
type?: string;
|
||||
username?: string;
|
||||
forwarder?: string;
|
||||
};
|
||||
|
||||
type UsernameGenerationMode = "random" | "website-name";
|
||||
|
||||
type CatchallGenerationOptions = {
|
||||
catchallType?: UsernameGenerationMode;
|
||||
catchallDomain?: string;
|
||||
};
|
||||
|
||||
type EffUsernameGenerationOptions = {
|
||||
wordCapitalize?: boolean;
|
||||
wordIncludeNumber?: boolean;
|
||||
};
|
||||
|
||||
type SubaddressGenerationOptions = {
|
||||
subaddressType?: UsernameGenerationMode;
|
||||
subaddressEmail?: string;
|
||||
};
|
||||
|
||||
type ApiOptions = {
|
||||
token?: string;
|
||||
};
|
||||
|
||||
type SelfHostedApiOptions = ApiOptions & {
|
||||
baseUrl: string;
|
||||
};
|
||||
|
||||
type EmailDomainOptions = {
|
||||
domain: string;
|
||||
};
|
||||
|
||||
export class ForwarderOptionsMigrator extends Migrator<64, 65> {
|
||||
async migrate(helper: MigrationHelper): Promise<void> {
|
||||
const accounts = await helper.getAccounts<AccountType>();
|
||||
|
||||
async function migrateAccount(userId: string, account: AccountType) {
|
||||
const legacyOptions = account?.settings?.usernameGenerationOptions;
|
||||
|
||||
if (legacyOptions) {
|
||||
const converted = convertSettings(legacyOptions);
|
||||
await storeSettings(helper, userId, converted);
|
||||
await deleteSettings(helper, userId, account);
|
||||
}
|
||||
}
|
||||
|
||||
await Promise.all([...accounts.map(({ userId, account }) => migrateAccount(userId, account))]);
|
||||
}
|
||||
|
||||
async rollback(helper: MigrationHelper): Promise<void> {
|
||||
// not supported
|
||||
}
|
||||
}
|
||||
|
||||
function convertSettings(options: ExpectedOptions): ConvertedOptions {
|
||||
const forwarders = {
|
||||
addyIo: {
|
||||
baseUrl: options.forwardedAnonAddyBaseUrl,
|
||||
token: options.forwardedAnonAddyApiToken,
|
||||
domain: options.forwardedAnonAddyDomain,
|
||||
},
|
||||
duckDuckGo: {
|
||||
token: options.forwardedDuckDuckGoToken,
|
||||
},
|
||||
fastmail: {
|
||||
token: options.forwardedFastmailApiToken,
|
||||
},
|
||||
firefoxRelay: {
|
||||
token: options.forwardedFirefoxApiToken,
|
||||
},
|
||||
forwardEmail: {
|
||||
token: options.forwardedForwardEmailApiToken,
|
||||
domain: options.forwardedForwardEmailDomain,
|
||||
},
|
||||
simpleLogin: {
|
||||
token: options.forwardedSimpleLoginApiKey,
|
||||
baseUrl: options.forwardedSimpleLoginBaseUrl,
|
||||
},
|
||||
};
|
||||
|
||||
const generator = {
|
||||
username: options.type,
|
||||
forwarder: options.forwardedService,
|
||||
};
|
||||
|
||||
const algorithms = {
|
||||
effUsername: {
|
||||
wordCapitalize: options.wordCapitalize,
|
||||
wordIncludeNumber: options.wordIncludeNumber,
|
||||
},
|
||||
subaddress: {
|
||||
subaddressType: options.subaddressType,
|
||||
subaddressEmail: options.subaddressEmail,
|
||||
},
|
||||
catchall: {
|
||||
catchallType: options.catchallType,
|
||||
catchallDomain: options.catchallDomain,
|
||||
},
|
||||
};
|
||||
|
||||
return { generator, algorithms, forwarders };
|
||||
}
|
||||
|
||||
async function storeSettings(helper: MigrationHelper, userId: string, converted: ConvertedOptions) {
|
||||
await Promise.all([
|
||||
helper.setToUser(userId, NAVIGATION, converted.generator),
|
||||
helper.setToUser(userId, CATCHALL, converted.algorithms.catchall),
|
||||
helper.setToUser(userId, EFF_USERNAME, converted.algorithms.effUsername),
|
||||
helper.setToUser(userId, SUBADDRESS, converted.algorithms.subaddress),
|
||||
helper.setToUser(userId, ADDY_IO, converted.forwarders.addyIo),
|
||||
helper.setToUser(userId, DUCK_DUCK_GO, converted.forwarders.duckDuckGo),
|
||||
helper.setToUser(userId, FASTMAIL, converted.forwarders.fastmail),
|
||||
helper.setToUser(userId, FIREFOX_RELAY, converted.forwarders.firefoxRelay),
|
||||
helper.setToUser(userId, FORWARD_EMAIL, converted.forwarders.forwardEmail),
|
||||
helper.setToUser(userId, SIMPLE_LOGIN, converted.forwarders.simpleLogin),
|
||||
]);
|
||||
}
|
||||
|
||||
async function deleteSettings(helper: MigrationHelper, userId: string, account: AccountType) {
|
||||
delete account?.settings?.usernameGenerationOptions;
|
||||
await helper.set(userId, account);
|
||||
}
|
||||
Reference in New Issue
Block a user