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:
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
111
libs/common/src/state-migrations/migrations/60-known-accounts.ts
Normal file
111
libs/common/src/state-migrations/migrations/60-known-accounts.ts
Normal 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");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user