mirror of
https://github.com/bitwarden/browser
synced 2025-12-17 16:53:34 +00:00
[PM-5264] Implement StateProvider in LoginEmailService (#7662)
* setup StateProvider in LoginService * replace implementations * replace implementation * remove stateService * change storage location for web to 'disk-local' * implement migrate() method of Migrator * add RememberedEmailMigrator to migrate.ts * add rollback * add tests * replace implementation * replace implementation * add StateProvider to Desktop services * rename LoginService to RememberEmailService * update state definition * rename file * rename to storedEmail * rename service to EmailService to avoid confusion * add jsdocs * refactor login.component.ts * fix typos * fix test * rename to LoginEmailService * update factory * more renaming * remove duplicate logic and rename method * convert storedEmail to observable * refactor to remove setStoredEmail() method * move service to libs/auth/common * address floating promises * remove comment * remove unnecessary deps in service registration
This commit is contained in:
@@ -1,8 +0,0 @@
|
||||
export abstract class LoginService {
|
||||
getEmail: () => string;
|
||||
getRememberEmail: () => boolean;
|
||||
setEmail: (value: string) => void;
|
||||
setRememberEmail: (value: boolean) => void;
|
||||
clearValues: () => void;
|
||||
saveEmailSettings: () => Promise<void>;
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
import { StateService } from "../../platform/abstractions/state.service";
|
||||
import { LoginService as LoginServiceAbstraction } from "../abstractions/login.service";
|
||||
|
||||
export class LoginService implements LoginServiceAbstraction {
|
||||
private _email: string;
|
||||
private _rememberEmail: boolean;
|
||||
|
||||
constructor(private stateService: StateService) {}
|
||||
|
||||
getEmail() {
|
||||
return this._email;
|
||||
}
|
||||
|
||||
getRememberEmail() {
|
||||
return this._rememberEmail;
|
||||
}
|
||||
|
||||
setEmail(value: string) {
|
||||
this._email = value;
|
||||
}
|
||||
|
||||
setRememberEmail(value: boolean) {
|
||||
this._rememberEmail = value;
|
||||
}
|
||||
|
||||
clearValues() {
|
||||
this._email = null;
|
||||
this._rememberEmail = null;
|
||||
}
|
||||
|
||||
async saveEmailSettings() {
|
||||
await this.stateService.setRememberedEmail(this._rememberEmail ? this._email : null);
|
||||
this.clearValues();
|
||||
}
|
||||
}
|
||||
@@ -262,8 +262,6 @@ export abstract class StateService<T extends Account = Account> {
|
||||
* Sets the user's Pin, encrypted by the user key
|
||||
*/
|
||||
setProtectedPin: (value: string, options?: StorageOptions) => Promise<void>;
|
||||
getRememberedEmail: (options?: StorageOptions) => Promise<string>;
|
||||
setRememberedEmail: (value: string, options?: StorageOptions) => Promise<void>;
|
||||
getSecurityStamp: (options?: StorageOptions) => Promise<string>;
|
||||
setSecurityStamp: (value: string, options?: StorageOptions) => Promise<void>;
|
||||
getUserId: (options?: StorageOptions) => Promise<string>;
|
||||
|
||||
@@ -3,7 +3,6 @@ import { ThemeType } from "../../enums";
|
||||
export class GlobalState {
|
||||
installedVersion?: string;
|
||||
organizationInvitation?: any;
|
||||
rememberedEmail?: string;
|
||||
theme?: ThemeType = ThemeType.System;
|
||||
twoFactorToken?: string;
|
||||
biometricFingerprintValidated?: boolean;
|
||||
|
||||
@@ -1241,23 +1241,6 @@ export class StateService<
|
||||
);
|
||||
}
|
||||
|
||||
async getRememberedEmail(options?: StorageOptions): Promise<string> {
|
||||
return (
|
||||
await this.getGlobals(this.reconcileOptions(options, await this.defaultOnDiskLocalOptions()))
|
||||
)?.rememberedEmail;
|
||||
}
|
||||
|
||||
async setRememberedEmail(value: string, options?: StorageOptions): Promise<void> {
|
||||
const globals = await this.getGlobals(
|
||||
this.reconcileOptions(options, await this.defaultOnDiskLocalOptions()),
|
||||
);
|
||||
globals.rememberedEmail = value;
|
||||
await this.saveGlobals(
|
||||
globals,
|
||||
this.reconcileOptions(options, await this.defaultOnDiskLocalOptions()),
|
||||
);
|
||||
}
|
||||
|
||||
async getSecurityStamp(options?: StorageOptions): Promise<string> {
|
||||
return (
|
||||
await this.getAccount(this.reconcileOptions(options, await this.defaultInMemoryOptions()))
|
||||
|
||||
@@ -38,13 +38,16 @@ export const BILLING_DISK = new StateDefinition("billing", "disk");
|
||||
export const KEY_CONNECTOR_DISK = new StateDefinition("keyConnector", "disk");
|
||||
export const ACCOUNT_MEMORY = new StateDefinition("account", "memory");
|
||||
export const AVATAR_DISK = new StateDefinition("avatar", "disk", { web: "disk-local" });
|
||||
export const LOGIN_EMAIL_DISK = new StateDefinition("loginEmail", "disk", {
|
||||
web: "disk-local",
|
||||
});
|
||||
export const LOGIN_STRATEGY_MEMORY = new StateDefinition("loginStrategy", "memory");
|
||||
export const SSO_DISK = new StateDefinition("ssoLogin", "disk");
|
||||
export const TOKEN_DISK = new StateDefinition("token", "disk");
|
||||
export const TOKEN_DISK_LOCAL = new StateDefinition("tokenDiskLocal", "disk", {
|
||||
web: "disk-local",
|
||||
});
|
||||
export const TOKEN_MEMORY = new StateDefinition("token", "memory");
|
||||
export const LOGIN_STRATEGY_MEMORY = new StateDefinition("loginStrategy", "memory");
|
||||
export const USER_DECRYPTION_OPTIONS_DISK = new StateDefinition("userDecryptionOptions", "disk");
|
||||
|
||||
// Autofill
|
||||
|
||||
@@ -47,6 +47,7 @@ import { MoveDdgToStateProviderMigrator } from "./migrations/48-move-ddg-to-stat
|
||||
import { AccountServerConfigMigrator } from "./migrations/49-move-account-server-configs";
|
||||
import { AddKeyTypeToOrgKeysMigrator } from "./migrations/5-add-key-type-to-org-keys";
|
||||
import { KeyConnectorMigrator } from "./migrations/50-move-key-connector-to-state-provider";
|
||||
import { RememberedEmailMigrator } from "./migrations/51-move-remembered-email-to-state-providers";
|
||||
import { RemoveLegacyEtmKeyMigrator } from "./migrations/6-remove-legacy-etm-key";
|
||||
import { MoveBiometricAutoPromptToAccount } from "./migrations/7-move-biometric-auto-prompt-to-account";
|
||||
import { MoveStateVersionMigrator } from "./migrations/8-move-state-version";
|
||||
@@ -54,7 +55,7 @@ import { MoveBrowserSettingsToGlobal } from "./migrations/9-move-browser-setting
|
||||
import { MinVersionMigrator } from "./migrations/min-version";
|
||||
|
||||
export const MIN_VERSION = 3;
|
||||
export const CURRENT_VERSION = 50;
|
||||
export const CURRENT_VERSION = 51;
|
||||
|
||||
export type MinVersion = typeof MIN_VERSION;
|
||||
|
||||
@@ -107,7 +108,8 @@ export function createMigrationBuilder() {
|
||||
.with(MoveDesktopSettingsMigrator, 46, 47)
|
||||
.with(MoveDdgToStateProviderMigrator, 47, 48)
|
||||
.with(AccountServerConfigMigrator, 48, 49)
|
||||
.with(KeyConnectorMigrator, 49, CURRENT_VERSION);
|
||||
.with(KeyConnectorMigrator, 49, 50)
|
||||
.with(RememberedEmailMigrator, 50, CURRENT_VERSION);
|
||||
}
|
||||
|
||||
export async function currentVersion(
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
import { MockProxy } from "jest-mock-extended";
|
||||
|
||||
import { MigrationHelper } from "../migration-helper";
|
||||
import { mockMigrationHelper, runMigrator } from "../migration-helper.spec";
|
||||
|
||||
import { RememberedEmailMigrator } from "./51-move-remembered-email-to-state-providers";
|
||||
|
||||
function rollbackJSON() {
|
||||
return {
|
||||
global: {
|
||||
extra: "data",
|
||||
},
|
||||
global_loginEmail_storedEmail: "user@example.com",
|
||||
};
|
||||
}
|
||||
|
||||
describe("RememberedEmailMigrator", () => {
|
||||
const migrator = new RememberedEmailMigrator(50, 51);
|
||||
|
||||
describe("migrate", () => {
|
||||
it("should migrate the rememberedEmail property from the legacy global object to a global StorageKey as 'global_loginEmail_storedEmail'", async () => {
|
||||
const output = await runMigrator(migrator, {
|
||||
global: {
|
||||
rememberedEmail: "user@example.com",
|
||||
extra: "data", // Represents a global property that should persist after migration
|
||||
},
|
||||
});
|
||||
|
||||
expect(output).toEqual({
|
||||
global: {
|
||||
extra: "data",
|
||||
},
|
||||
global_loginEmail_storedEmail: "user@example.com",
|
||||
});
|
||||
});
|
||||
|
||||
it("should remove the rememberedEmail property from the legacy global object", async () => {
|
||||
const output = await runMigrator(migrator, {
|
||||
global: {
|
||||
rememberedEmail: "user@example.com",
|
||||
},
|
||||
});
|
||||
|
||||
expect(output.global).not.toHaveProperty("rememberedEmail");
|
||||
});
|
||||
});
|
||||
|
||||
describe("rollback", () => {
|
||||
let helper: MockProxy<MigrationHelper>;
|
||||
let sut: RememberedEmailMigrator;
|
||||
|
||||
const keyDefinitionLike = {
|
||||
key: "storedEmail",
|
||||
stateDefinition: {
|
||||
name: "loginEmail",
|
||||
},
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
helper = mockMigrationHelper(rollbackJSON(), 51);
|
||||
sut = new RememberedEmailMigrator(50, 51);
|
||||
});
|
||||
|
||||
it("should null out the storedEmail global StorageKey", async () => {
|
||||
await sut.rollback(helper);
|
||||
|
||||
expect(helper.setToGlobal).toHaveBeenCalledTimes(1);
|
||||
expect(helper.setToGlobal).toHaveBeenCalledWith(keyDefinitionLike, null);
|
||||
});
|
||||
|
||||
it("should add the rememberedEmail property back to legacy global object", async () => {
|
||||
await sut.rollback(helper);
|
||||
|
||||
expect(helper.set).toHaveBeenCalledTimes(1);
|
||||
expect(helper.set).toHaveBeenCalledWith("global", {
|
||||
rememberedEmail: "user@example.com",
|
||||
extra: "data",
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,46 @@
|
||||
import { KeyDefinitionLike, MigrationHelper, StateDefinitionLike } from "../migration-helper";
|
||||
import { Migrator } from "../migrator";
|
||||
|
||||
type ExpectedGlobalState = { rememberedEmail?: string };
|
||||
|
||||
const LOGIN_EMAIL_STATE: StateDefinitionLike = { name: "loginEmail" };
|
||||
|
||||
const STORED_EMAIL: KeyDefinitionLike = {
|
||||
key: "storedEmail",
|
||||
stateDefinition: LOGIN_EMAIL_STATE,
|
||||
};
|
||||
|
||||
export class RememberedEmailMigrator extends Migrator<50, 51> {
|
||||
async migrate(helper: MigrationHelper): Promise<void> {
|
||||
const legacyGlobal = await helper.get<ExpectedGlobalState>("global");
|
||||
|
||||
// Move global data
|
||||
if (legacyGlobal?.rememberedEmail != null) {
|
||||
await helper.setToGlobal(STORED_EMAIL, legacyGlobal.rememberedEmail);
|
||||
}
|
||||
|
||||
// Delete legacy global data
|
||||
delete legacyGlobal?.rememberedEmail;
|
||||
await helper.set("global", legacyGlobal);
|
||||
}
|
||||
|
||||
async rollback(helper: MigrationHelper): Promise<void> {
|
||||
let legacyGlobal = await helper.get<ExpectedGlobalState>("global");
|
||||
let updatedLegacyGlobal = false;
|
||||
const globalStoredEmail = await helper.getFromGlobal<string>(STORED_EMAIL);
|
||||
|
||||
if (globalStoredEmail) {
|
||||
if (!legacyGlobal) {
|
||||
legacyGlobal = {};
|
||||
}
|
||||
|
||||
updatedLegacyGlobal = true;
|
||||
legacyGlobal.rememberedEmail = globalStoredEmail;
|
||||
await helper.setToGlobal(STORED_EMAIL, null);
|
||||
}
|
||||
|
||||
if (updatedLegacyGlobal) {
|
||||
await helper.set("global", legacyGlobal);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user