1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-21 10:43:35 +00:00

[PM-6688] Use AccountService as account source (#8893)

* Use account service to track accounts and active account

* Remove state service active account Observables.

* Add email verified to account service

* Do not store account info on logged out accounts

* Add account activity tracking to account service

* Use last account activity from account service

* migrate or replicate account service data

* Add `AccountActivityService` that handles storing account last active data

* Move active and next active user to account service

* Remove authenticated accounts from state object

* Fold account activity into account service

* Fix builds

* Fix desktop app switch

* Fix logging out non active user

* Expand helper to handle new authenticated accounts location

* Prefer view observable to tons of async pipes

* Fix `npm run test:types`

* Correct user activity sorting test

* Be more precise about log out messaging

* Fix dev compare errors

All stored values are serializable, the next step wasn't necessary and was erroring on some types that lack `toString`.

* If the account in unlocked on load of lock component, navigate away from lock screen

* Handle no users case for auth service statuses

* Specify account to switch to

* Filter active account out of inactive accounts

* Prefer constructor init

* Improve comparator

* Use helper methods internally

* Fixup component tests

* Clarify name

* Ensure accounts object has only valid userIds

* Capitalize const values

* Prefer descriptive, single-responsibility guards

* Update libs/common/src/state-migrations/migrate.ts

Co-authored-by: Justin Baur <19896123+justindbaur@users.noreply.github.com>

* Fix merge

* Add user Id validation

activity for undefined was being set, which was resulting in requests for the auth status of `"undefined"` (string) userId, due to key enumeration. These changes stop that at both locations, as well as account add for good measure.

---------

Co-authored-by: Justin Baur <19896123+justindbaur@users.noreply.github.com>
This commit is contained in:
Matt Gibson
2024-04-30 09:13:02 -04:00
committed by GitHub
parent 61d079cc34
commit c70a5aa024
67 changed files with 1380 additions and 618 deletions

View File

@@ -0,0 +1,145 @@
import { MockProxy } from "jest-mock-extended";
import { MigrationHelper } from "../migration-helper";
import { mockMigrationHelper } from "../migration-helper.spec";
import {
ACCOUNT_ACCOUNTS,
ACCOUNT_ACTIVE_ACCOUNT_ID,
ACCOUNT_ACTIVITY,
KnownAccountsMigrator,
} from "./60-known-accounts";
const migrateJson = () => {
return {
authenticatedAccounts: ["user1", "user2"],
activeUserId: "user1",
user1: {
profile: {
email: "user1",
name: "User 1",
emailVerified: true,
},
},
user2: {
profile: {
email: "",
emailVerified: false,
},
},
accountActivity: {
user1: 1609459200000, // 2021-01-01
user2: 1609545600000, // 2021-01-02
},
};
};
const rollbackJson = () => {
return {
user1: {
profile: {
email: "user1",
name: "User 1",
emailVerified: true,
},
},
user2: {
profile: {
email: "",
emailVerified: false,
},
},
global_account_accounts: {
user1: {
profile: {
email: "user1",
name: "User 1",
emailVerified: true,
},
},
user2: {
profile: {
email: "",
emailVerified: false,
},
},
},
global_account_activeAccountId: "user1",
global_account_activity: {
user1: "2021-01-01T00:00:00.000Z",
user2: "2021-01-02T00:00:00.000Z",
},
};
};
describe("ReplicateKnownAccounts", () => {
let helper: MockProxy<MigrationHelper>;
let sut: KnownAccountsMigrator;
describe("migrate", () => {
beforeEach(() => {
helper = mockMigrationHelper(migrateJson(), 59);
sut = new KnownAccountsMigrator(59, 60);
});
it("migrates accounts", async () => {
await sut.migrate(helper);
expect(helper.setToGlobal).toHaveBeenCalledWith(ACCOUNT_ACCOUNTS, {
user1: {
email: "user1",
name: "User 1",
emailVerified: true,
},
user2: {
email: "",
emailVerified: false,
name: undefined,
},
});
expect(helper.remove).toHaveBeenCalledWith("authenticatedAccounts");
});
it("migrates active account it", async () => {
await sut.migrate(helper);
expect(helper.setToGlobal).toHaveBeenCalledWith(ACCOUNT_ACTIVE_ACCOUNT_ID, "user1");
expect(helper.remove).toHaveBeenCalledWith("activeUserId");
});
it("migrates account activity", async () => {
await sut.migrate(helper);
expect(helper.setToGlobal).toHaveBeenCalledWith(ACCOUNT_ACTIVITY, {
user1: '"2021-01-01T00:00:00.000Z"',
user2: '"2021-01-02T00:00:00.000Z"',
});
expect(helper.remove).toHaveBeenCalledWith("accountActivity");
});
});
describe("rollback", () => {
beforeEach(() => {
helper = mockMigrationHelper(rollbackJson(), 60);
sut = new KnownAccountsMigrator(59, 60);
});
it("rolls back authenticated accounts", async () => {
await sut.rollback(helper);
expect(helper.set).toHaveBeenCalledWith("authenticatedAccounts", ["user1", "user2"]);
expect(helper.removeFromGlobal).toHaveBeenCalledWith(ACCOUNT_ACCOUNTS);
});
it("rolls back active account id", async () => {
await sut.rollback(helper);
expect(helper.set).toHaveBeenCalledWith("activeUserId", "user1");
expect(helper.removeFromGlobal).toHaveBeenCalledWith(ACCOUNT_ACTIVE_ACCOUNT_ID);
});
it("rolls back account activity", async () => {
await sut.rollback(helper);
expect(helper.set).toHaveBeenCalledWith("accountActivity", {
user1: 1609459200000,
user2: 1609545600000,
});
expect(helper.removeFromGlobal).toHaveBeenCalledWith(ACCOUNT_ACTIVITY);
});
});
});

View File

@@ -0,0 +1,111 @@
import { KeyDefinitionLike, MigrationHelper } from "../migration-helper";
import { Migrator } from "../migrator";
export const ACCOUNT_ACCOUNTS: KeyDefinitionLike = {
stateDefinition: {
name: "account",
},
key: "accounts",
};
export const ACCOUNT_ACTIVE_ACCOUNT_ID: KeyDefinitionLike = {
stateDefinition: {
name: "account",
},
key: "activeAccountId",
};
export const ACCOUNT_ACTIVITY: KeyDefinitionLike = {
stateDefinition: {
name: "account",
},
key: "activity",
};
type ExpectedAccountType = {
profile?: {
email?: string;
name?: string;
emailVerified?: boolean;
};
};
export class KnownAccountsMigrator extends Migrator<59, 60> {
async migrate(helper: MigrationHelper): Promise<void> {
await this.migrateAuthenticatedAccounts(helper);
await this.migrateActiveAccountId(helper);
await this.migrateAccountActivity(helper);
}
async rollback(helper: MigrationHelper): Promise<void> {
// authenticated account are removed, but the accounts record also contains logged out accounts. Best we can do is to add them all back
const accounts = (await helper.getFromGlobal<Record<string, unknown>>(ACCOUNT_ACCOUNTS)) ?? {};
await helper.set("authenticatedAccounts", Object.keys(accounts));
await helper.removeFromGlobal(ACCOUNT_ACCOUNTS);
// Active Account Id
const activeAccountId = await helper.getFromGlobal<string>(ACCOUNT_ACTIVE_ACCOUNT_ID);
if (activeAccountId) {
await helper.set("activeUserId", activeAccountId);
}
await helper.removeFromGlobal(ACCOUNT_ACTIVE_ACCOUNT_ID);
// Account Activity
const accountActivity = await helper.getFromGlobal<Record<string, string>>(ACCOUNT_ACTIVITY);
if (accountActivity) {
const toStore = Object.entries(accountActivity).reduce(
(agg, [userId, dateString]) => {
agg[userId] = new Date(dateString).getTime();
return agg;
},
{} as Record<string, number>,
);
await helper.set("accountActivity", toStore);
}
await helper.removeFromGlobal(ACCOUNT_ACTIVITY);
}
private async migrateAuthenticatedAccounts(helper: MigrationHelper) {
const authenticatedAccounts = (await helper.get<string[]>("authenticatedAccounts")) ?? [];
const accounts = await Promise.all(
authenticatedAccounts.map(async (userId) => {
const account = await helper.get<ExpectedAccountType>(userId);
return { userId, account };
}),
);
const accountsToStore = accounts.reduce(
(agg, { userId, account }) => {
if (account?.profile) {
agg[userId] = {
email: account.profile.email ?? "",
emailVerified: account.profile.emailVerified ?? false,
name: account.profile.name,
};
}
return agg;
},
{} as Record<string, { email: string; emailVerified: boolean; name: string | undefined }>,
);
await helper.setToGlobal(ACCOUNT_ACCOUNTS, accountsToStore);
await helper.remove("authenticatedAccounts");
}
private async migrateAccountActivity(helper: MigrationHelper) {
const stored = await helper.get<Record<string, Date>>("accountActivity");
const accountActivity = Object.entries(stored ?? {}).reduce(
(agg, [userId, dateMs]) => {
agg[userId] = JSON.stringify(new Date(dateMs));
return agg;
},
{} as Record<string, string>,
);
await helper.setToGlobal(ACCOUNT_ACTIVITY, accountActivity);
await helper.remove("accountActivity");
}
private async migrateActiveAccountId(helper: MigrationHelper) {
const activeAccountId = await helper.get<string>("activeUserId");
await helper.setToGlobal(ACCOUNT_ACTIVE_ACCOUNT_ID, activeAccountId);
await helper.remove("activeUserId");
}
}