1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-18 01:03:35 +00:00

Merge master into merge/feature/org-admin-refresh (using imerge)

This commit is contained in:
Shane Melton
2022-12-13 07:31:22 -08:00
721 changed files with 18307 additions and 5660 deletions

View File

@@ -241,4 +241,72 @@ describe("Utils Service", () => {
expect(Utils.fromByteStringToArray(null)).toEqual(null);
});
});
describe("mapToRecord", () => {
it("should handle null", () => {
expect(Utils.mapToRecord(null)).toEqual(null);
});
it("should handle empty map", () => {
expect(Utils.mapToRecord(new Map())).toEqual({});
});
it("should handle convert a Map to a Record", () => {
const map = new Map([
["key1", "value1"],
["key2", "value2"],
]);
expect(Utils.mapToRecord(map)).toEqual({ key1: "value1", key2: "value2" });
});
it("should handle convert a Map to a Record with non-string keys", () => {
const map = new Map([
[1, "value1"],
[2, "value2"],
]);
const result = Utils.mapToRecord(map);
expect(result).toEqual({ 1: "value1", 2: "value2" });
expect(Utils.recordToMap(result)).toEqual(map);
});
it("should not convert an object if it's not a map", () => {
const obj = { key1: "value1", key2: "value2" };
expect(Utils.mapToRecord(obj as any)).toEqual(obj);
});
});
describe("recordToMap", () => {
it("should handle null", () => {
expect(Utils.recordToMap(null)).toEqual(null);
});
it("should handle empty record", () => {
expect(Utils.recordToMap({})).toEqual(new Map());
});
it("should handle convert a Record to a Map", () => {
const record = { key1: "value1", key2: "value2" };
expect(Utils.recordToMap(record)).toEqual(new Map(Object.entries(record)));
});
it("should handle convert a Record to a Map with non-string keys", () => {
const record = { 1: "value1", 2: "value2" };
const result = Utils.recordToMap(record);
expect(result).toEqual(
new Map([
[1, "value1"],
[2, "value2"],
])
);
expect(Utils.mapToRecord(result)).toEqual(record);
});
it("should not convert an object if already a map", () => {
const map = new Map([
["key1", "value1"],
["key2", "value2"],
]);
expect(Utils.recordToMap(map as any)).toEqual(map);
});
});
});

View File

@@ -1,12 +1,9 @@
import { MockProxy, mock, any, mockClear, matches } from "jest-mock-extended";
import { BehaviorSubject, firstValueFrom, Subject } from "rxjs";
import { MockProxy, mock, any, mockClear } from "jest-mock-extended";
import { BehaviorSubject, firstValueFrom } from "rxjs";
import { StateService } from "@bitwarden/common/abstractions/state.service";
import { SyncNotifierService } from "@bitwarden/common/abstractions/sync/syncNotifier.service.abstraction";
import { OrganizationData } from "@bitwarden/common/models/data/organization.data";
import { SyncResponse } from "@bitwarden/common/models/response/sync.response";
import { OrganizationService } from "@bitwarden/common/services/organization/organization.service";
import { SyncEventArgs } from "@bitwarden/common/types/syncEventArgs";
describe("Organization Service", () => {
let organizationService: OrganizationService;
@@ -14,8 +11,6 @@ describe("Organization Service", () => {
let stateService: MockProxy<StateService>;
let activeAccount: BehaviorSubject<string>;
let activeAccountUnlocked: BehaviorSubject<boolean>;
let syncNotifierService: MockProxy<SyncNotifierService>;
let sync: Subject<SyncEventArgs>;
const resetStateService = async (
customizeStateService: (stateService: MockProxy<StateService>) => void
@@ -25,7 +20,7 @@ describe("Organization Service", () => {
stateService.activeAccount$ = activeAccount;
stateService.activeAccountUnlocked$ = activeAccountUnlocked;
customizeStateService(stateService);
organizationService = new OrganizationService(stateService, syncNotifierService);
organizationService = new OrganizationService(stateService);
await new Promise((r) => setTimeout(r, 50));
};
@@ -41,12 +36,7 @@ describe("Organization Service", () => {
"1": organizationData("1", "Test Org"),
});
sync = new Subject<SyncEventArgs>();
syncNotifierService = mock<SyncNotifierService>();
syncNotifierService.sync$ = sync;
organizationService = new OrganizationService(stateService, syncNotifierService);
organizationService = new OrganizationService(stateService);
});
afterEach(() => {
@@ -169,36 +159,6 @@ describe("Organization Service", () => {
});
});
describe("syncEvent works", () => {
it("Complete event updates data", async () => {
sync.next({
status: "Completed",
successfully: true,
data: new SyncResponse({
profile: {
organizations: [
{
id: "1",
name: "Updated Name",
},
],
},
}),
});
await new Promise((r) => setTimeout(r, 500));
expect(stateService.setOrganizations).toHaveBeenCalledTimes(1);
expect(stateService.setOrganizations).toHaveBeenLastCalledWith(
matches((organizationData: { [id: string]: OrganizationData }) => {
const organization = organizationData["1"];
return organization?.name === "Updated Name";
})
);
});
});
function organizationData(id: string, name: string) {
const data = new OrganizationData({} as any);
data.id = id;

View File

@@ -161,6 +161,11 @@ import { TwoFactorYubiKeyResponse } from "../models/response/two-factor-yubi-key
import { UserKeyResponse } from "../models/response/user-key.response";
import { SendAccessView } from "../models/view/send-access.view";
/**
* @deprecated The `ApiService` class is deprecated and calls should be extracted into individual
* api services. The `send` method is still allowed to be used within api services. For background
* of this decision please read https://contributing.bitwarden.com/architecture/adr/refactor-api-service.
*/
export abstract class ApiService {
send: (
method: "GET" | "POST" | "PUT" | "DELETE",

View File

@@ -1,12 +0,0 @@
import { EventType } from "../enums/eventType";
export abstract class EventService {
collect: (
eventType: EventType,
cipherId?: string,
uploadImmediately?: boolean,
organizationId?: string
) => Promise<any>;
uploadEvents: (userId?: string) => Promise<any>;
clearEvents: (userId?: string) => Promise<any>;
}

View File

@@ -0,0 +1,10 @@
import { EventType } from "../../enums/eventType";
export abstract class EventCollectionService {
collect: (
eventType: EventType,
cipherId?: string,
uploadImmediately?: boolean,
organizationId?: string
) => Promise<any>;
}

View File

@@ -0,0 +1,3 @@
export abstract class EventUploadService {
uploadEvents: (userId?: string) => Promise<void>;
}

View File

@@ -6,6 +6,6 @@ export abstract class I18nService {
translationLocale: string;
collator: Intl.Collator;
localeNames: Map<string, string>;
t: (id: string, p1?: string, p2?: string, p3?: string) => string;
t: (id: string, p1?: string | number, p2?: string | number, p3?: string | number) => string;
translate: (id: string, p1?: string, p2?: string, p3?: string) => string;
}

View File

@@ -1,6 +1,7 @@
import { map, Observable } from "rxjs";
import { Utils } from "../../misc/utils";
import { OrganizationData } from "../../models/data/organization.data";
import { Organization } from "../../models/domain/organization";
import { I18nService } from "../i18n.service";
@@ -55,6 +56,10 @@ export function canAccessAdmin(i18nService: I18nService) {
);
}
export function isNotProviderUser(org: Organization): boolean {
return !org.isProviderUser;
}
export abstract class OrganizationService {
organizations$: Observable<Organization[]>;
@@ -69,3 +74,7 @@ export abstract class OrganizationService {
canManageSponsorships: () => Promise<boolean>;
hasOrganizations: () => boolean;
}
export abstract class InternalOrganizationService extends OrganizationService {
replace: (organizations: { [id: string]: OrganizationData }) => Promise<void>;
}

View File

@@ -19,6 +19,7 @@ export abstract class PlatformUtilsService {
isViewOpen: () => Promise<boolean>;
launchUri: (uri: string, options?: any) => void;
getApplicationVersion: () => Promise<string>;
getApplicationVersionNumber: () => Promise<string>;
supportsWebAuthn: (win: Window) => boolean;
supportsDuo: () => boolean;
showToast: (

View File

@@ -1,7 +1,7 @@
import { ApiService } from "../abstractions/api.service";
import { Forwarder } from "./forwarder";
import { ForwarderOptions } from "./forwarderOptions";
import { ForwarderOptions } from "./forwarder-options";
export class AnonAddyForwarder implements Forwarder {
async generate(apiService: ApiService, options: ForwarderOptions): Promise<string> {

View File

@@ -1,7 +1,7 @@
import { ApiService } from "../abstractions/api.service";
import { Forwarder } from "./forwarder";
import { ForwarderOptions } from "./forwarderOptions";
import { ForwarderOptions } from "./forwarder-options";
export class DuckDuckGoForwarder implements Forwarder {
async generate(apiService: ApiService, options: ForwarderOptions): Promise<string> {

View File

@@ -1,7 +1,7 @@
import { ApiService } from "../abstractions/api.service";
import { Forwarder } from "./forwarder";
import { ForwarderOptions } from "./forwarderOptions";
import { ForwarderOptions } from "./forwarder-options";
export class FastmailForwarder implements Forwarder {
async generate(apiService: ApiService, options: ForwarderOptions): Promise<string> {

View File

@@ -1,7 +1,7 @@
import { ApiService } from "../abstractions/api.service";
import { Forwarder } from "./forwarder";
import { ForwarderOptions } from "./forwarderOptions";
import { ForwarderOptions } from "./forwarder-options";
export class FirefoxRelayForwarder implements Forwarder {
async generate(apiService: ApiService, options: ForwarderOptions): Promise<string> {

View File

@@ -1,6 +1,6 @@
import { ApiService } from "../abstractions/api.service";
import { ForwarderOptions } from "./forwarderOptions";
import { ForwarderOptions } from "./forwarder-options";
export interface Forwarder {
generate(apiService: ApiService, options: ForwarderOptions): Promise<string>;

View File

@@ -1,7 +1,7 @@
import { ApiService } from "../abstractions/api.service";
import { Forwarder } from "./forwarder";
import { ForwarderOptions } from "./forwarderOptions";
import { ForwarderOptions } from "./forwarder-options";
export class SimpleLoginForwarder implements Forwarder {
async generate(apiService: ApiService, options: ForwarderOptions): Promise<string> {

View File

@@ -1,5 +1,6 @@
/* eslint-disable no-useless-escape */
import { getHostname, parse } from "tldts";
import { Merge } from "type-fest";
import { CryptoService } from "../abstractions/crypto.service";
import { EncryptService } from "../abstractions/encrypt.service";
@@ -55,6 +56,10 @@ export class Utils {
}
static fromB64ToArray(str: string): Uint8Array {
if (str == null) {
return null;
}
if (Utils.isNode) {
return new Uint8Array(Buffer.from(str, "base64"));
} else {
@@ -108,6 +113,9 @@ export class Utils {
}
static fromBufferToB64(buffer: ArrayBuffer): string {
if (buffer == null) {
return null;
}
if (Utils.isNode) {
return Buffer.from(buffer).toString("base64");
} else {
@@ -423,6 +431,57 @@ export class Utils {
return this.global.bitwardenContainerService;
}
/**
* Converts map to a Record<string, V> with the same data. Inverse of recordToMap
* Useful in toJSON methods, since Maps are not serializable
* @param map
* @returns
*/
static mapToRecord<K extends string | number, V>(map: Map<K, V>): Record<string, V> {
if (map == null) {
return null;
}
if (!(map instanceof Map)) {
return map;
}
return Object.fromEntries(map);
}
/**
* Converts record to a Map<string, V> with the same data. Inverse of mapToRecord
* Useful in fromJSON methods, since Maps are not serializable
*
* Warning: If the record has string keys that are numbers, they will be converted to numbers in the map
* @param record
* @returns
*/
static recordToMap<K extends string | number, V>(record: Record<K, V>): Map<K, V> {
if (record == null) {
return null;
} else if (record instanceof Map) {
return record;
}
const entries = Object.entries(record);
if (entries.length === 0) {
return new Map();
}
if (isNaN(Number(entries[0][0]))) {
return new Map(entries) as Map<K, V>;
} else {
return new Map(entries.map((e) => [Number(e[0]), e[1]])) as Map<K, V>;
}
}
/** Applies Object.assign, but converts the type nicely using Type-Fest Merge<Destination, Source> */
static merge<Destination, Source>(
destination: Destination,
source: Source
): Merge<Destination, Source> {
return Object.assign(destination, source) as unknown as Merge<Destination, Source>;
}
private static isMobile(win: Window) {
let mobile = false;
((a) => {

View File

@@ -20,7 +20,9 @@ export class OrganizationData {
useSso: boolean;
useKeyConnector: boolean;
useScim: boolean;
useCustomPermissions: boolean;
useResetPassword: boolean;
useSecretsManager: boolean;
selfHost: boolean;
usersGetPremium: boolean;
seats: number;
@@ -60,7 +62,9 @@ export class OrganizationData {
this.useSso = response.useSso;
this.useKeyConnector = response.useKeyConnector;
this.useScim = response.useScim;
this.useCustomPermissions = response.useCustomPermissions;
this.useResetPassword = response.useResetPassword;
this.useSecretsManager = response.useSecretsManager;
this.selfHost = response.selfHost;
this.usersGetPremium = response.usersGetPremium;
this.seats = response.seats;

View File

@@ -1,4 +1,4 @@
import { Except, Jsonify } from "type-fest";
import { Jsonify } from "type-fest";
import { AuthenticationStatus } from "../../enums/authenticationStatus";
import { KdfType } from "../../enums/kdfType";
@@ -40,7 +40,7 @@ export class EncryptionPair<TEncrypted, TDecrypted> {
}
static fromJSON<TEncrypted, TDecrypted>(
obj: Jsonify<EncryptionPair<Jsonify<TEncrypted>, Jsonify<TDecrypted>>>,
obj: { encrypted?: Jsonify<TEncrypted>; decrypted?: string | Jsonify<TDecrypted> },
decryptedFromJson?: (decObj: Jsonify<TDecrypted> | string) => TDecrypted,
encryptedFromJson?: (encObj: Jsonify<TEncrypted>) => TEncrypted
) {
@@ -123,7 +123,7 @@ export class AccountKeys {
apiKeyClientSecret?: string;
toJSON() {
return Object.assign(this as Except<AccountKeys, "publicKey">, {
return Utils.merge(this, {
publicKey: Utils.fromBufferToByteString(this.publicKey),
});
}
@@ -251,7 +251,7 @@ export class AccountSettings {
}
export type AccountSettingsSettings = {
equivalentDomains?: { [id: string]: any };
equivalentDomains?: string[][];
};
export class AccountTokens {

View File

@@ -1,3 +1,5 @@
import { Jsonify } from "type-fest";
import { OrganizationUserStatusType } from "../../enums/organizationUserStatusType";
import { OrganizationUserType } from "../../enums/organizationUserType";
import { ProductType } from "../../enums/productType";
@@ -20,7 +22,9 @@ export class Organization {
useSso: boolean;
useKeyConnector: boolean;
useScim: boolean;
useCustomPermissions: boolean;
useResetPassword: boolean;
useSecretsManager: boolean;
selfHost: boolean;
usersGetPremium: boolean;
seats: number;
@@ -64,7 +68,9 @@ export class Organization {
this.useSso = obj.useSso;
this.useKeyConnector = obj.useKeyConnector;
this.useScim = obj.useScim;
this.useCustomPermissions = obj.useCustomPermissions;
this.useResetPassword = obj.useResetPassword;
this.useSecretsManager = obj.useSecretsManager;
this.selfHost = obj.selfHost;
this.usersGetPremium = obj.usersGetPremium;
this.seats = obj.seats;
@@ -205,4 +211,19 @@ export class Organization {
get hasProvider() {
return this.providerId != null || this.providerName != null;
}
get canAccessSecretsManager() {
return this.useSecretsManager;
}
static fromJSON(json: Jsonify<Organization>) {
if (json == null) {
return null;
}
return Object.assign(new Organization(), json, {
familySponsorshipLastSyncDate: new Date(json.familySponsorshipLastSyncDate),
familySponsorshipValidUntil: new Date(json.familySponsorshipValidUntil),
});
}
}

View File

@@ -4,22 +4,25 @@ import { State } from "./state";
describe("state", () => {
describe("fromJSON", () => {
it("should deserialize to an instance of itself", () => {
expect(State.fromJSON({})).toBeInstanceOf(State);
expect(State.fromJSON({}, () => new Account({}))).toBeInstanceOf(State);
});
it("should always assign an object to accounts", () => {
const state = State.fromJSON({});
const state = State.fromJSON({}, () => new Account({}));
expect(state.accounts).not.toBeNull();
expect(state.accounts).toEqual({});
});
it("should build an account map", () => {
const accountsSpy = jest.spyOn(Account, "fromJSON");
const state = State.fromJSON({
accounts: {
userId: {},
const state = State.fromJSON(
{
accounts: {
userId: {},
},
},
});
Account.fromJSON
);
expect(state.accounts["userId"]).toBeInstanceOf(Account);
expect(accountsSpy).toHaveBeenCalled();

View File

@@ -19,26 +19,28 @@ export class State<
// TODO, make Jsonify<State,TGlobalState,TAccount> work. It currently doesn't because Globals doesn't implement Jsonify.
static fromJSON<TGlobalState extends GlobalState, TAccount extends Account>(
obj: any
obj: any,
accountDeserializer: (json: Jsonify<TAccount>) => TAccount
): State<TGlobalState, TAccount> {
if (obj == null) {
return null;
}
return Object.assign(new State(null), obj, {
accounts: State.buildAccountMapFromJSON(obj?.accounts),
accounts: State.buildAccountMapFromJSON(obj?.accounts, accountDeserializer),
});
}
private static buildAccountMapFromJSON(
jsonAccounts: Jsonify<{ [userId: string]: Jsonify<Account> }>
private static buildAccountMapFromJSON<TAccount extends Account>(
jsonAccounts: { [userId: string]: Jsonify<TAccount> },
accountDeserializer: (json: Jsonify<TAccount>) => TAccount
) {
if (!jsonAccounts) {
return {};
}
const accounts: { [userId: string]: Account } = {};
const accounts: { [userId: string]: TAccount } = {};
for (const userId in jsonAccounts) {
accounts[userId] = Account.fromJSON(jsonAccounts[userId]);
accounts[userId] = accountDeserializer(jsonAccounts[userId]);
}
return accounts;
}

View File

@@ -18,7 +18,9 @@ export class ProfileOrganizationResponse extends BaseResponse {
useSso: boolean;
useKeyConnector: boolean;
useScim: boolean;
useCustomPermissions: boolean;
useResetPassword: boolean;
useSecretsManager: boolean;
selfHost: boolean;
usersGetPremium: boolean;
seats: number;
@@ -59,7 +61,9 @@ export class ProfileOrganizationResponse extends BaseResponse {
this.useSso = this.getResponseProperty("UseSso");
this.useKeyConnector = this.getResponseProperty("UseKeyConnector") ?? false;
this.useScim = this.getResponseProperty("UseScim") ?? false;
this.useCustomPermissions = this.getResponseProperty("UseCustomPermissions") ?? false;
this.useResetPassword = this.getResponseProperty("UseResetPassword");
this.useSecretsManager = this.getResponseProperty("UseSecretsManager");
this.selfHost = this.getResponseProperty("SelfHost");
this.usersGetPremium = this.getResponseProperty("UsersGetPremium");
this.seats = this.getResponseProperty("Seats");

View File

@@ -1,3 +1,4 @@
import { DeepJsonify } from "../../types/deep-jsonify";
import { SendFile } from "../domain/send-file";
import { View } from "./view";
@@ -28,4 +29,12 @@ export class SendFileView implements View {
}
return 0;
}
static fromJSON(json: DeepJsonify<SendFileView>) {
if (json == null) {
return null;
}
return Object.assign(new SendFileView(), json);
}
}

View File

@@ -1,3 +1,4 @@
import { DeepJsonify } from "../../types/deep-jsonify";
import { SendText } from "../domain/send-text";
import { View } from "./view";
@@ -17,4 +18,12 @@ export class SendTextView implements View {
get maskedText(): string {
return this.text != null ? "••••••••" : null;
}
static fromJSON(json: DeepJsonify<SendTextView>) {
if (json == null) {
return null;
}
return Object.assign(new SendTextView(), json);
}
}

View File

@@ -1,5 +1,6 @@
import { SendType } from "../../enums/sendType";
import { Utils } from "../../misc/utils";
import { DeepJsonify } from "../../types/deep-jsonify";
import { Send } from "../domain/send";
import { SymmetricCryptoKey } from "../domain/symmetric-crypto-key";
@@ -65,4 +66,26 @@ export class SendView implements View {
get pendingDelete(): boolean {
return this.deletionDate <= new Date();
}
toJSON() {
return Utils.merge(this, {
key: Utils.fromBufferToB64(this.key),
});
}
static fromJSON(json: DeepJsonify<SendView>) {
if (json == null) {
return null;
}
return Object.assign(new SendView(), json, {
key: Utils.fromB64ToArray(json.key)?.buffer,
cryptoKey: SymmetricCryptoKey.fromJSON(json.cryptoKey),
text: SendTextView.fromJSON(json.text),
file: SendFileView.fromJSON(json.file),
revisionDate: json.revisionDate == null ? null : new Date(json.revisionDate),
deletionDate: json.deletionDate == null ? null : new Date(json.deletionDate),
expirationDate: json.expirationDate == null ? null : new Date(json.expirationDate),
});
}
}

View File

@@ -170,6 +170,11 @@ import { TwoFactorYubiKeyResponse } from "../models/response/two-factor-yubi-key
import { UserKeyResponse } from "../models/response/user-key.response";
import { SendAccessView } from "../models/view/send-access.view";
/**
* @deprecated The `ApiService` class is deprecated and calls should be extracted into individual
* api services. The `send` method is still allowed to be used within api services. For background
* of this decision please read https://contributing.bitwarden.com/architecture/adr/refactor-api-service.
*/
export class ApiService implements ApiServiceAbstraction {
private device: DeviceType;
private deviceType: string;
@@ -2021,7 +2026,7 @@ export class ApiService implements ApiServiceAbstraction {
request.headers.set("Bitwarden-Client-Name", this.platformUtilsService.getClientType());
request.headers.set(
"Bitwarden-Client-Version",
await this.platformUtilsService.getApplicationVersion()
await this.platformUtilsService.getApplicationVersionNumber()
);
return this.nativeFetch(request);
}

View File

@@ -412,7 +412,7 @@ export class CipherService implements CipherServiceAbstraction {
: firstValueFrom(this.settingsService.settings$).then(
(settings: AccountSettingsSettings) => {
let matches: any[] = [];
settings.equivalentDomains?.forEach((eqDomain: any) => {
settings?.equivalentDomains?.forEach((eqDomain: any) => {
if (eqDomain.length && eqDomain.indexOf(domain) >= 0) {
matches = matches.concat(eqDomain);
}

View File

@@ -1,107 +0,0 @@
import { ApiService } from "../abstractions/api.service";
import { CipherService } from "../abstractions/cipher.service";
import { EventService as EventServiceAbstraction } from "../abstractions/event.service";
import { LogService } from "../abstractions/log.service";
import { OrganizationService } from "../abstractions/organization/organization.service.abstraction";
import { StateService } from "../abstractions/state.service";
import { EventType } from "../enums/eventType";
import { EventData } from "../models/data/event.data";
import { EventRequest } from "../models/request/event.request";
export class EventService implements EventServiceAbstraction {
private inited = false;
constructor(
private apiService: ApiService,
private cipherService: CipherService,
private stateService: StateService,
private logService: LogService,
private organizationService: OrganizationService
) {}
init(checkOnInterval: boolean) {
if (this.inited) {
return;
}
this.inited = true;
if (checkOnInterval) {
this.uploadEvents();
setInterval(() => this.uploadEvents(), 60 * 1000); // check every 60 seconds
}
}
async collect(
eventType: EventType,
cipherId: string = null,
uploadImmediately = false,
organizationId: string = null
): Promise<any> {
const authed = await this.stateService.getIsAuthenticated();
if (!authed) {
return;
}
const organizations = await this.organizationService.getAll();
if (organizations == null) {
return;
}
const orgIds = new Set<string>(organizations.filter((o) => o.useEvents).map((o) => o.id));
if (orgIds.size === 0) {
return;
}
if (cipherId != null) {
const cipher = await this.cipherService.get(cipherId);
if (cipher == null || cipher.organizationId == null || !orgIds.has(cipher.organizationId)) {
return;
}
}
if (organizationId != null) {
if (!orgIds.has(organizationId)) {
return;
}
}
let eventCollection = await this.stateService.getEventCollection();
if (eventCollection == null) {
eventCollection = [];
}
const event = new EventData();
event.type = eventType;
event.cipherId = cipherId;
event.date = new Date().toISOString();
event.organizationId = organizationId;
eventCollection.push(event);
await this.stateService.setEventCollection(eventCollection);
if (uploadImmediately) {
await this.uploadEvents();
}
}
async uploadEvents(userId?: string): Promise<any> {
const authed = await this.stateService.getIsAuthenticated({ userId: userId });
if (!authed) {
return;
}
const eventCollection = await this.stateService.getEventCollection({ userId: userId });
if (eventCollection == null || eventCollection.length === 0) {
return;
}
const request = eventCollection.map((e) => {
const req = new EventRequest();
req.type = e.type;
req.cipherId = e.cipherId;
req.date = e.date;
req.organizationId = e.organizationId;
return req;
});
try {
await this.apiService.postEventsCollect(request);
this.clearEvents(userId);
} catch (e) {
this.logService.error(e);
}
}
async clearEvents(userId?: string): Promise<any> {
await this.stateService.setEventCollection(null, { userId: userId });
}
}

View File

@@ -0,0 +1,61 @@
import { CipherService } from "../../abstractions/cipher.service";
import { EventCollectionService as EventCollectionServiceAbstraction } from "../../abstractions/event/event-collection.service";
import { EventUploadService } from "../../abstractions/event/event-upload.service";
import { OrganizationService } from "../../abstractions/organization/organization.service.abstraction";
import { StateService } from "../../abstractions/state.service";
import { EventType } from "../../enums/eventType";
import { EventData } from "../../models/data/event.data";
export class EventCollectionService implements EventCollectionServiceAbstraction {
constructor(
private cipherService: CipherService,
private stateService: StateService,
private organizationService: OrganizationService,
private eventUploadService: EventUploadService
) {}
async collect(
eventType: EventType,
cipherId: string = null,
uploadImmediately = false,
organizationId: string = null
): Promise<any> {
const authed = await this.stateService.getIsAuthenticated();
if (!authed) {
return;
}
const organizations = await this.organizationService.getAll();
if (organizations == null) {
return;
}
const orgIds = new Set<string>(organizations.filter((o) => o.useEvents).map((o) => o.id));
if (orgIds.size === 0) {
return;
}
if (cipherId != null) {
const cipher = await this.cipherService.get(cipherId);
if (cipher == null || cipher.organizationId == null || !orgIds.has(cipher.organizationId)) {
return;
}
}
if (organizationId != null) {
if (!orgIds.has(organizationId)) {
return;
}
}
let eventCollection = await this.stateService.getEventCollection();
if (eventCollection == null) {
eventCollection = [];
}
const event = new EventData();
event.type = eventType;
event.cipherId = cipherId;
event.date = new Date().toISOString();
event.organizationId = organizationId;
eventCollection.push(event);
await this.stateService.setEventCollection(eventCollection);
if (uploadImmediately) {
await this.eventUploadService.uploadEvents();
}
}
}

View File

@@ -0,0 +1,55 @@
import { ApiService } from "../../abstractions/api.service";
import { EventUploadService as EventUploadServiceAbstraction } from "../../abstractions/event/event-upload.service";
import { LogService } from "../../abstractions/log.service";
import { StateService } from "../../abstractions/state.service";
import { EventRequest } from "../../models/request/event.request";
export class EventUploadService implements EventUploadServiceAbstraction {
private inited = false;
constructor(
private apiService: ApiService,
private stateService: StateService,
private logService: LogService
) {}
init(checkOnInterval: boolean) {
if (this.inited) {
return;
}
this.inited = true;
if (checkOnInterval) {
this.uploadEvents();
setInterval(() => this.uploadEvents(), 60 * 1000); // check every 60 seconds
}
}
async uploadEvents(userId?: string): Promise<void> {
const authed = await this.stateService.getIsAuthenticated({ userId: userId });
if (!authed) {
return;
}
const eventCollection = await this.stateService.getEventCollection({ userId: userId });
if (eventCollection == null || eventCollection.length === 0) {
return;
}
const request = eventCollection.map((e) => {
const req = new EventRequest();
req.type = e.type;
req.cipherId = e.cipherId;
req.date = e.date;
req.organizationId = e.organizationId;
return req;
});
try {
await this.apiService.postEventsCollect(request);
this.clearEvents(userId);
} catch (e) {
this.logService.error(e);
}
}
private async clearEvents(userId?: string): Promise<any> {
await this.stateService.setEventCollection(null, { userId: userId });
}
}

View File

@@ -122,7 +122,7 @@ export class I18nService implements I18nServiceAbstraction {
return this.translate(id, p1, p2, p3);
}
translate(id: string, p1?: string, p2?: string, p3?: string): string {
translate(id: string, p1?: string | number, p2?: string | number, p3?: string | number): string {
let result: string;
// eslint-disable-next-line
if (this.localeMessages.hasOwnProperty(id) && this.localeMessages[id]) {
@@ -136,13 +136,13 @@ export class I18nService implements I18nServiceAbstraction {
if (result !== "") {
if (p1 != null) {
result = result.split("__$1__").join(p1);
result = result.split("__$1__").join(p1.toString());
}
if (p2 != null) {
result = result.split("__$2__").join(p2);
result = result.split("__$2__").join(p2.toString());
}
if (p3 != null) {
result = result.split("__$3__").join(p3);
result = result.split("__$3__").join(p3.toString());
}
}

View File

@@ -18,7 +18,7 @@ export class MemoryStorageService
}
async has(key: string): Promise<boolean> {
return this.get(key) != null;
return (await this.get(key)) != null;
}
save(key: string, obj: any): Promise<any> {

View File

@@ -1,21 +1,16 @@
import { BehaviorSubject, concatMap, filter } from "rxjs";
import { BehaviorSubject, concatMap } from "rxjs";
import { OrganizationService as OrganizationServiceAbstraction } from "../../abstractions/organization/organization.service.abstraction";
import { InternalOrganizationService as InternalOrganizationServiceAbstraction } from "../../abstractions/organization/organization.service.abstraction";
import { StateService } from "../../abstractions/state.service";
import { SyncNotifierService } from "../../abstractions/sync/syncNotifier.service.abstraction";
import { OrganizationData } from "../../models/data/organization.data";
import { Organization } from "../../models/domain/organization";
import { isSuccessfullyCompleted } from "../../types/syncEventArgs";
export class OrganizationService implements OrganizationServiceAbstraction {
private _organizations = new BehaviorSubject<Organization[]>([]);
export class OrganizationService implements InternalOrganizationServiceAbstraction {
protected _organizations = new BehaviorSubject<Organization[]>([]);
organizations$ = this._organizations.asObservable();
constructor(
private stateService: StateService,
private syncNotifierService: SyncNotifierService
) {
constructor(private stateService: StateService) {
this.stateService.activeAccountUnlocked$
.pipe(
concatMap(async (unlocked) => {
@@ -29,28 +24,6 @@ export class OrganizationService implements OrganizationServiceAbstraction {
})
)
.subscribe();
this.syncNotifierService.sync$
.pipe(
filter(isSuccessfullyCompleted),
concatMap(async ({ data }) => {
const { profile } = data;
const organizations: { [id: string]: OrganizationData } = {};
profile.organizations.forEach((o) => {
organizations[o.id] = new OrganizationData(o);
});
profile.providerOrganizations.forEach((o) => {
if (organizations[o.id] == null) {
organizations[o.id] = new OrganizationData(o);
organizations[o.id].isProviderUser = true;
}
});
await this.updateStateAndObservables(organizations);
})
)
.subscribe();
}
async getAll(userId?: string): Promise<Organization[]> {
@@ -78,7 +51,7 @@ export class OrganizationService implements OrganizationServiceAbstraction {
organizations[organization.id] = organization;
await this.updateStateAndObservables(organizations);
await this.replace(organizations);
}
async delete(id: string): Promise<void> {
@@ -92,7 +65,7 @@ export class OrganizationService implements OrganizationServiceAbstraction {
}
delete organizations[id];
await this.updateStateAndObservables(organizations);
await this.replace(organizations);
}
get(id: string): Organization {
@@ -121,9 +94,9 @@ export class OrganizationService implements OrganizationServiceAbstraction {
return organizations.find((organization) => organization.identifier === identifier);
}
private async updateStateAndObservables(organizationsMap: { [id: string]: OrganizationData }) {
await this.stateService.setOrganizations(organizationsMap);
this.updateObservables(organizationsMap);
async replace(organizations: { [id: string]: OrganizationData }) {
await this.stateService.setOrganizations(organizations);
this.updateObservables(organizations);
}
private updateObservables(organizationsMap: { [id: string]: OrganizationData }) {

View File

@@ -16,7 +16,7 @@ import { ListResponse } from "../../models/response/list.response";
import { PolicyResponse } from "../../models/response/policy.response";
export class PolicyService implements InternalPolicyServiceAbstraction {
private _policies: BehaviorSubject<Policy[]> = new BehaviorSubject([]);
protected _policies: BehaviorSubject<Policy[]> = new BehaviorSubject([]);
policies$ = this._policies.asObservable();

View File

@@ -6,7 +6,7 @@ import { Utils } from "../misc/utils";
import { AccountSettingsSettings } from "../models/domain/account";
export class SettingsService implements SettingsServiceAbstraction {
private _settings: BehaviorSubject<AccountSettingsSettings> = new BehaviorSubject({});
protected _settings: BehaviorSubject<AccountSettingsSettings> = new BehaviorSubject({});
settings$ = this._settings.asObservable();

View File

@@ -1,4 +1,5 @@
import { BehaviorSubject, concatMap } from "rxjs";
import { Jsonify } from "type-fest";
import { LogService } from "../abstractions/log.service";
import { StateService as StateServiceAbstraction } from "../abstractions/state.service";
@@ -13,6 +14,7 @@ import { StorageLocation } from "../enums/storageLocation";
import { ThemeType } from "../enums/themeType";
import { UriMatchType } from "../enums/uriMatchType";
import { StateFactory } from "../factories/stateFactory";
import { Utils } from "../misc/utils";
import { CipherData } from "../models/data/cipher.data";
import { CollectionData } from "../models/data/collection.data";
import { EncryptedOrganizationKeyData } from "../models/data/encrypted-organization-key.data";
@@ -65,19 +67,22 @@ export class StateService<
TAccount extends Account = Account
> implements StateServiceAbstraction<TAccount>
{
private accountsSubject = new BehaviorSubject<{ [userId: string]: TAccount }>({});
protected accountsSubject = new BehaviorSubject<{ [userId: string]: TAccount }>({});
accounts$ = this.accountsSubject.asObservable();
private activeAccountSubject = new BehaviorSubject<string | null>(null);
protected activeAccountSubject = new BehaviorSubject<string | null>(null);
activeAccount$ = this.activeAccountSubject.asObservable();
private activeAccountUnlockedSubject = new BehaviorSubject<boolean>(false);
protected activeAccountUnlockedSubject = new BehaviorSubject<boolean>(false);
activeAccountUnlocked$ = this.activeAccountUnlockedSubject.asObservable();
private hasBeenInited = false;
private isRecoveredSession = false;
private accountDiskCache = new Map<string, TAccount>();
protected accountDiskCache = new BehaviorSubject<Record<string, TAccount>>({});
// default account serializer, must be overridden by child class
protected accountDeserializer = Account.fromJSON as (json: Jsonify<TAccount>) => TAccount;
constructor(
protected storageService: AbstractStorageService,
@@ -676,7 +681,7 @@ export class StateService<
const account = await this.getAccount(
this.reconcileOptions(options, await this.defaultInMemoryOptions())
);
return this.recordToMap(account?.keys?.organizationKeys?.decrypted);
return Utils.recordToMap(account?.keys?.organizationKeys?.decrypted);
}
async setDecryptedOrganizationKeys(
@@ -686,7 +691,7 @@ export class StateService<
const account = await this.getAccount(
this.reconcileOptions(options, await this.defaultInMemoryOptions())
);
account.keys.organizationKeys.decrypted = this.mapToRecord(value);
account.keys.organizationKeys.decrypted = Utils.mapToRecord(value);
await this.saveAccount(
account,
this.reconcileOptions(options, await this.defaultInMemoryOptions())
@@ -774,7 +779,7 @@ export class StateService<
const account = await this.getAccount(
this.reconcileOptions(options, await this.defaultInMemoryOptions())
);
return this.recordToMap(account?.keys?.providerKeys?.decrypted);
return Utils.recordToMap(account?.keys?.providerKeys?.decrypted);
}
async setDecryptedProviderKeys(
@@ -784,7 +789,7 @@ export class StateService<
const account = await this.getAccount(
this.reconcileOptions(options, await this.defaultInMemoryOptions())
);
account.keys.providerKeys.decrypted = this.mapToRecord(value);
account.keys.providerKeys.decrypted = Utils.mapToRecord(value);
await this.saveAccount(
account,
this.reconcileOptions(options, await this.defaultInMemoryOptions())
@@ -2378,7 +2383,7 @@ export class StateService<
}
if (this.useAccountCache) {
const cachedAccount = this.accountDiskCache.get(options.userId);
const cachedAccount = this.accountDiskCache.value[options.userId];
if (cachedAccount != null) {
return cachedAccount;
}
@@ -2392,9 +2397,7 @@ export class StateService<
))
: await this.storageService.get<TAccount>(options.userId, options);
if (this.useAccountCache) {
this.accountDiskCache.set(options.userId, account);
}
this.setDiskCache(options.userId, account);
return account;
}
@@ -2425,9 +2428,7 @@ export class StateService<
await storageLocation.save(`${options.userId}`, account, options);
if (this.useAccountCache) {
this.accountDiskCache.delete(options.userId);
}
this.deleteDiskCache(options.userId);
}
protected async saveAccountToMemory(account: TAccount): Promise<void> {
@@ -2638,9 +2639,7 @@ export class StateService<
userId = userId ?? state.activeUserId;
delete state.accounts[userId];
if (this.useAccountCache) {
this.accountDiskCache.delete(userId);
}
this.deleteDiskCache(userId);
return state;
});
@@ -2744,7 +2743,7 @@ export class StateService<
protected async state(): Promise<State<TGlobalState, TAccount>> {
const state = await this.memoryStorageService.get<State<TGlobalState, TAccount>>(keys.state, {
deserializer: (s) => State.fromJSON(s),
deserializer: (s) => State.fromJSON(s, this.accountDeserializer),
});
return state;
}
@@ -2766,51 +2765,21 @@ export class StateService<
});
}
private mapToRecord<V>(map: Map<string, V>): Record<string, V> {
return map == null ? null : Object.fromEntries(map);
private setDiskCache(key: string, value: TAccount, options?: StorageOptions) {
if (this.useAccountCache) {
this.accountDiskCache.value[key] = value;
this.accountDiskCache.next(this.accountDiskCache.value);
}
}
private recordToMap<V>(record: Record<string, V>): Map<string, V> {
return record == null ? null : new Map(Object.entries(record));
private deleteDiskCache(key: string) {
if (this.useAccountCache) {
delete this.accountDiskCache.value[key];
this.accountDiskCache.next(this.accountDiskCache.value);
}
}
}
export function withPrototype<T>(
constructor: new (...args: any[]) => T,
converter: (input: any) => T = (i) => i
): (
target: any,
propertyKey: string | symbol,
descriptor: PropertyDescriptor
) => { value: (...args: any[]) => Promise<T> } {
return (target: any, propertyKey: string | symbol, descriptor: PropertyDescriptor) => {
const originalMethod = descriptor.value;
return {
value: function (...args: any[]) {
const originalResult: Promise<T> = originalMethod.apply(this, args);
if (!(originalResult instanceof Promise)) {
throw new Error(
`Error applying prototype to stored value -- result is not a promise for method ${String(
propertyKey
)}`
);
}
return originalResult.then((result) => {
return result == null ||
result.constructor.name === constructor.prototype.constructor.name
? converter(result as T)
: converter(
Object.create(constructor.prototype, Object.getOwnPropertyDescriptors(result)) as T
);
});
},
};
};
}
function withPrototypeForArrayMembers<T>(
memberConstructor: new (...args: any[]) => T,
memberConverter: (input: any) => T = (i) => i
@@ -2847,7 +2816,7 @@ function withPrototypeForArrayMembers<T>(
return result.map((r) => {
return r == null ||
r.constructor.name === memberConstructor.prototype.constructor.name
? memberConverter(r)
? r
: memberConverter(
Object.create(memberConstructor.prototype, Object.getOwnPropertyDescriptors(r))
);

View File

@@ -7,17 +7,18 @@ import { InternalFolderService } from "../../abstractions/folder/folder.service.
import { KeyConnectorService } from "../../abstractions/keyConnector.service";
import { LogService } from "../../abstractions/log.service";
import { MessagingService } from "../../abstractions/messaging.service";
import { InternalOrganizationService } from "../../abstractions/organization/organization.service.abstraction";
import { InternalPolicyService } from "../../abstractions/policy/policy.service.abstraction";
import { ProviderService } from "../../abstractions/provider.service";
import { SendService } from "../../abstractions/send.service";
import { SettingsService } from "../../abstractions/settings.service";
import { StateService } from "../../abstractions/state.service";
import { SyncService as SyncServiceAbstraction } from "../../abstractions/sync/sync.service.abstraction";
import { SyncNotifierService } from "../../abstractions/sync/syncNotifier.service.abstraction";
import { sequentialize } from "../../misc/sequentialize";
import { CipherData } from "../../models/data/cipher.data";
import { CollectionData } from "../../models/data/collection.data";
import { FolderData } from "../../models/data/folder.data";
import { OrganizationData } from "../../models/data/organization.data";
import { PolicyData } from "../../models/data/policy.data";
import { ProviderData } from "../../models/data/provider.data";
import { SendData } from "../../models/data/send.data";
@@ -52,7 +53,7 @@ export class SyncService implements SyncServiceAbstraction {
private stateService: StateService,
private providerService: ProviderService,
private folderApiService: FolderApiServiceAbstraction,
private syncNotifierService: SyncNotifierService,
private organizationService: InternalOrganizationService,
private logoutCallback: (expired: boolean) => Promise<void>
) {}
@@ -76,10 +77,8 @@ export class SyncService implements SyncServiceAbstraction {
@sequentialize(() => "fullSync")
async fullSync(forceSync: boolean, allowThrowOnError = false): Promise<boolean> {
this.syncStarted();
this.syncNotifierService.next({ status: "Started" });
const isAuthenticated = await this.stateService.getIsAuthenticated();
if (!isAuthenticated) {
this.syncNotifierService.next({ status: "Completed", successfully: false });
return this.syncCompleted(false);
}
@@ -95,7 +94,6 @@ export class SyncService implements SyncServiceAbstraction {
if (!needsSync) {
await this.setLastSync(now);
this.syncNotifierService.next({ status: "Completed", successfully: false });
return this.syncCompleted(false);
}
@@ -112,13 +110,11 @@ export class SyncService implements SyncServiceAbstraction {
await this.syncPolicies(response.policies);
await this.setLastSync(now);
this.syncNotifierService.next({ status: "Completed", successfully: true, data: response });
return this.syncCompleted(true);
} catch (e) {
if (allowThrowOnError) {
throw e;
} else {
this.syncNotifierService.next({ status: "Completed", successfully: false });
return this.syncCompleted(false);
}
}
@@ -315,11 +311,24 @@ export class SyncService implements SyncServiceAbstraction {
await this.stateService.setForcePasswordReset(response.forcePasswordReset);
await this.keyConnectorService.setUsesKeyConnector(response.usesKeyConnector);
const organizations: { [id: string]: OrganizationData } = {};
response.organizations.forEach((o) => {
organizations[o.id] = new OrganizationData(o);
});
const providers: { [id: string]: ProviderData } = {};
response.providers.forEach((p) => {
providers[p.id] = new ProviderData(p);
});
response.providerOrganizations.forEach((o) => {
if (organizations[o.id] == null) {
organizations[o.id] = new OrganizationData(o);
organizations[o.id].isProviderUser = true;
}
});
await this.organizationService.replace(organizations);
await this.providerService.save(providers);
if (await this.keyConnectorService.userNeedsMigration()) {

View File

@@ -2,13 +2,13 @@ import { ApiService } from "../abstractions/api.service";
import { CryptoService } from "../abstractions/crypto.service";
import { StateService } from "../abstractions/state.service";
import { UsernameGenerationService as BaseUsernameGenerationService } from "../abstractions/usernameGeneration.service";
import { AnonAddyForwarder } from "../emailForwarders/anonAddyForwarder";
import { DuckDuckGoForwarder } from "../emailForwarders/duckDuckGoForwarder";
import { FastmailForwarder } from "../emailForwarders/fastmailForwarder";
import { FirefoxRelayForwarder } from "../emailForwarders/firefoxRelayForwarder";
import { Forwarder } from "../emailForwarders/forwarder";
import { ForwarderOptions } from "../emailForwarders/forwarderOptions";
import { SimpleLoginForwarder } from "../emailForwarders/simpleLoginForwarder";
import { AnonAddyForwarder } from "../email-forwarders/anon-addy-forwarder";
import { DuckDuckGoForwarder } from "../email-forwarders/duck-duck-go-forwarder";
import { FastmailForwarder } from "../email-forwarders/fastmail-forwarder";
import { FirefoxRelayForwarder } from "../email-forwarders/firefox-relay-forwarder";
import { Forwarder } from "../email-forwarders/forwarder";
import { ForwarderOptions } from "../email-forwarders/forwarder-options";
import { SimpleLoginForwarder } from "../email-forwarders/simple-login-forwarder";
import { EFFLongWordList } from "../misc/wordlist";
const DefaultOptions = {