mirror of
https://github.com/bitwarden/browser
synced 2025-12-14 23:33:31 +00:00
Assign ownership to many libs files (#6928)
Assign ownership to many of the remaining libs/common files. Criteria for ownership: * Files used by a single team, is now owned by that team. * Files related to a domain owned by a team is now owned by that team. * Where ownership is unclear the "lowest level" service takes ownership.
This commit is contained in:
152
libs/angular/src/platform/guard/feature-flag.guard.spec.ts
Normal file
152
libs/angular/src/platform/guard/feature-flag.guard.spec.ts
Normal file
@@ -0,0 +1,152 @@
|
||||
import { Component } from "@angular/core";
|
||||
import { TestBed } from "@angular/core/testing";
|
||||
import { CanActivateFn, Router } from "@angular/router";
|
||||
import { RouterTestingModule } from "@angular/router/testing";
|
||||
import { mock, MockProxy } from "jest-mock-extended";
|
||||
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { ConfigServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config.service.abstraction";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { I18nMockService } from "@bitwarden/components/src";
|
||||
|
||||
import { canAccessFeature } from "./feature-flag.guard";
|
||||
|
||||
@Component({ template: "" })
|
||||
export class EmptyComponent {}
|
||||
|
||||
describe("canAccessFeature", () => {
|
||||
const testFlag: FeatureFlag = "test-flag" as FeatureFlag;
|
||||
const featureRoute = "enabled-feature";
|
||||
const redirectRoute = "redirect";
|
||||
|
||||
let mockConfigService: MockProxy<ConfigServiceAbstraction>;
|
||||
let mockPlatformUtilsService: MockProxy<PlatformUtilsService>;
|
||||
|
||||
const setup = (featureGuard: CanActivateFn, flagValue: any) => {
|
||||
mockConfigService = mock<ConfigServiceAbstraction>();
|
||||
mockPlatformUtilsService = mock<PlatformUtilsService>();
|
||||
|
||||
// Mock the correct getter based on the type of flagValue; also mock default values if one is not provided
|
||||
if (typeof flagValue === "boolean") {
|
||||
mockConfigService.getFeatureFlag.mockImplementation((flag, defaultValue = false) =>
|
||||
flag == testFlag ? Promise.resolve(flagValue) : Promise.resolve(defaultValue)
|
||||
);
|
||||
} else if (typeof flagValue === "string") {
|
||||
mockConfigService.getFeatureFlag.mockImplementation((flag, defaultValue = "") =>
|
||||
flag == testFlag ? Promise.resolve(flagValue) : Promise.resolve(defaultValue)
|
||||
);
|
||||
} else if (typeof flagValue === "number") {
|
||||
mockConfigService.getFeatureFlag.mockImplementation((flag, defaultValue = 0) =>
|
||||
flag == testFlag ? Promise.resolve(flagValue) : Promise.resolve(defaultValue)
|
||||
);
|
||||
}
|
||||
|
||||
const testBed = TestBed.configureTestingModule({
|
||||
imports: [
|
||||
RouterTestingModule.withRoutes([
|
||||
{ path: "", component: EmptyComponent },
|
||||
{
|
||||
path: featureRoute,
|
||||
component: EmptyComponent,
|
||||
canActivate: [featureGuard],
|
||||
},
|
||||
{ path: redirectRoute, component: EmptyComponent },
|
||||
]),
|
||||
],
|
||||
providers: [
|
||||
{ provide: ConfigServiceAbstraction, useValue: mockConfigService },
|
||||
{ provide: PlatformUtilsService, useValue: mockPlatformUtilsService },
|
||||
{ provide: LogService, useValue: mock<LogService>() },
|
||||
{
|
||||
provide: I18nService,
|
||||
useValue: new I18nMockService({
|
||||
accessDenied: "Access Denied!",
|
||||
}),
|
||||
},
|
||||
],
|
||||
});
|
||||
return {
|
||||
router: testBed.inject(Router),
|
||||
};
|
||||
};
|
||||
|
||||
it("successfully navigates when the feature flag is enabled", async () => {
|
||||
const { router } = setup(canAccessFeature(testFlag), true);
|
||||
|
||||
await router.navigate([featureRoute]);
|
||||
|
||||
expect(router.url).toBe(`/${featureRoute}`);
|
||||
});
|
||||
|
||||
it("successfully navigates when the feature flag value matches the required value", async () => {
|
||||
const { router } = setup(canAccessFeature(testFlag, "some-value"), "some-value");
|
||||
|
||||
await router.navigate([featureRoute]);
|
||||
|
||||
expect(router.url).toBe(`/${featureRoute}`);
|
||||
});
|
||||
|
||||
it("fails to navigate when the feature flag is disabled", async () => {
|
||||
const { router } = setup(canAccessFeature(testFlag), false);
|
||||
|
||||
await router.navigate([featureRoute]);
|
||||
|
||||
expect(router.url).toBe("/");
|
||||
});
|
||||
|
||||
it("fails to navigate when the feature flag value does not match the required value", async () => {
|
||||
const { router } = setup(canAccessFeature(testFlag, "some-value"), "some-wrong-value");
|
||||
|
||||
await router.navigate([featureRoute]);
|
||||
|
||||
expect(router.url).toBe("/");
|
||||
});
|
||||
|
||||
it("fails to navigate when the feature flag does not exist", async () => {
|
||||
const { router } = setup(canAccessFeature("missing-flag" as FeatureFlag), true);
|
||||
|
||||
await router.navigate([featureRoute]);
|
||||
|
||||
expect(router.url).toBe("/");
|
||||
});
|
||||
|
||||
it("shows an error toast when the feature flag is disabled", async () => {
|
||||
const { router } = setup(canAccessFeature(testFlag), false);
|
||||
|
||||
await router.navigate([featureRoute]);
|
||||
|
||||
expect(mockPlatformUtilsService.showToast).toHaveBeenCalledWith(
|
||||
"error",
|
||||
null,
|
||||
"Access Denied!"
|
||||
);
|
||||
});
|
||||
|
||||
it("does not show an error toast when the feature flag is enabled", async () => {
|
||||
const { router } = setup(canAccessFeature(testFlag), true);
|
||||
|
||||
await router.navigate([featureRoute]);
|
||||
|
||||
expect(mockPlatformUtilsService.showToast).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("redirects to the specified redirect url when the feature flag is disabled", async () => {
|
||||
const { router } = setup(canAccessFeature(testFlag, true, redirectRoute), false);
|
||||
|
||||
await router.navigate([featureRoute]);
|
||||
|
||||
expect(router.url).toBe(`/${redirectRoute}`);
|
||||
});
|
||||
|
||||
it("fails to navigate when the config service throws an unexpected exception", async () => {
|
||||
const { router } = setup(canAccessFeature(testFlag), true);
|
||||
|
||||
mockConfigService.getFeatureFlag.mockImplementation(() => Promise.reject("Some error"));
|
||||
|
||||
await router.navigate([featureRoute]);
|
||||
|
||||
expect(router.url).toBe("/");
|
||||
});
|
||||
});
|
||||
50
libs/angular/src/platform/guard/feature-flag.guard.ts
Normal file
50
libs/angular/src/platform/guard/feature-flag.guard.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import { inject } from "@angular/core";
|
||||
import { CanActivateFn, Router } from "@angular/router";
|
||||
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { ConfigServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config.service.abstraction";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
|
||||
// Replace this with a type safe lookup of the feature flag values in PM-2282
|
||||
type FlagValue = boolean | number | string;
|
||||
|
||||
/**
|
||||
* Returns a CanActivateFn that checks if the feature flag is enabled. If not, it shows an "Access Denied!"
|
||||
* toast and optionally redirects to the specified url.
|
||||
* @param featureFlag - The feature flag to check
|
||||
* @param requiredFlagValue - Optional value to the feature flag must be equal to, defaults to true
|
||||
* @param redirectUrlOnDisabled - Optional url to redirect to if the feature flag is disabled
|
||||
*/
|
||||
export const canAccessFeature = (
|
||||
featureFlag: FeatureFlag,
|
||||
requiredFlagValue: FlagValue = true,
|
||||
redirectUrlOnDisabled?: string
|
||||
): CanActivateFn => {
|
||||
return async () => {
|
||||
const configService = inject(ConfigServiceAbstraction);
|
||||
const platformUtilsService = inject(PlatformUtilsService);
|
||||
const router = inject(Router);
|
||||
const i18nService = inject(I18nService);
|
||||
const logService = inject(LogService);
|
||||
|
||||
try {
|
||||
const flagValue = await configService.getFeatureFlag(featureFlag);
|
||||
|
||||
if (flagValue === requiredFlagValue) {
|
||||
return true;
|
||||
}
|
||||
|
||||
platformUtilsService.showToast("error", null, i18nService.t("accessDenied"));
|
||||
|
||||
if (redirectUrlOnDisabled != null) {
|
||||
return router.createUrlTree([redirectUrlOnDisabled]);
|
||||
}
|
||||
return false;
|
||||
} catch (e) {
|
||||
logService.error(e);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
};
|
||||
19
libs/angular/src/platform/services/theming/theme-builder.ts
Normal file
19
libs/angular/src/platform/services/theming/theme-builder.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { ThemeType } from "@bitwarden/common/platform/enums";
|
||||
|
||||
import { Theme } from "./theme";
|
||||
|
||||
export class ThemeBuilder implements Theme {
|
||||
get effectiveTheme(): ThemeType {
|
||||
return this.configuredTheme != ThemeType.System ? this.configuredTheme : this.systemTheme;
|
||||
}
|
||||
|
||||
constructor(readonly configuredTheme: ThemeType, readonly systemTheme: ThemeType) {}
|
||||
|
||||
updateSystemTheme(systemTheme: ThemeType): ThemeBuilder {
|
||||
return new ThemeBuilder(this.configuredTheme, systemTheme);
|
||||
}
|
||||
|
||||
updateConfiguredTheme(configuredTheme: ThemeType): ThemeBuilder {
|
||||
return new ThemeBuilder(configuredTheme, this.systemTheme);
|
||||
}
|
||||
}
|
||||
6
libs/angular/src/platform/services/theming/theme.ts
Normal file
6
libs/angular/src/platform/services/theming/theme.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { ThemeType } from "@bitwarden/common/platform/enums";
|
||||
|
||||
export interface Theme {
|
||||
configuredTheme: ThemeType;
|
||||
effectiveTheme: ThemeType;
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import { Observable } from "rxjs";
|
||||
|
||||
import { ThemeType } from "@bitwarden/common/platform/enums";
|
||||
|
||||
import { Theme } from "./theme";
|
||||
|
||||
export abstract class AbstractThemingService {
|
||||
theme$: Observable<Theme>;
|
||||
monitorThemeChanges: () => Promise<void>;
|
||||
updateSystemTheme: (systemTheme: ThemeType) => void;
|
||||
updateConfiguredTheme: (theme: ThemeType) => Promise<void>;
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
import { DOCUMENT } from "@angular/common";
|
||||
import { Inject, Injectable } from "@angular/core";
|
||||
import { BehaviorSubject, filter, fromEvent, Observable } from "rxjs";
|
||||
|
||||
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||
import { ThemeType } from "@bitwarden/common/platform/enums";
|
||||
|
||||
import { WINDOW } from "../../../services/injection-tokens";
|
||||
|
||||
import { Theme } from "./theme";
|
||||
import { ThemeBuilder } from "./theme-builder";
|
||||
import { AbstractThemingService } from "./theming.service.abstraction";
|
||||
|
||||
@Injectable()
|
||||
export class ThemingService implements AbstractThemingService {
|
||||
private _theme = new BehaviorSubject<ThemeBuilder | null>(null);
|
||||
theme$: Observable<Theme> = this._theme.pipe(filter((x) => x !== null));
|
||||
|
||||
constructor(
|
||||
private stateService: StateService,
|
||||
@Inject(WINDOW) private window: Window,
|
||||
@Inject(DOCUMENT) private document: Document
|
||||
) {
|
||||
this.monitorThemeChanges();
|
||||
}
|
||||
|
||||
async monitorThemeChanges(): Promise<void> {
|
||||
this._theme.next(
|
||||
new ThemeBuilder(await this.stateService.getTheme(), await this.getSystemTheme())
|
||||
);
|
||||
this.monitorConfiguredThemeChanges();
|
||||
this.monitorSystemThemeChanges();
|
||||
}
|
||||
|
||||
updateSystemTheme(systemTheme: ThemeType): void {
|
||||
this._theme.next(this._theme.getValue().updateSystemTheme(systemTheme));
|
||||
}
|
||||
|
||||
async updateConfiguredTheme(theme: ThemeType): Promise<void> {
|
||||
await this.stateService.setTheme(theme);
|
||||
this._theme.next(this._theme.getValue().updateConfiguredTheme(theme));
|
||||
}
|
||||
|
||||
protected monitorConfiguredThemeChanges(): void {
|
||||
this.theme$.subscribe((theme: Theme) => {
|
||||
this.document.documentElement.classList.remove(
|
||||
"theme_" + ThemeType.Light,
|
||||
"theme_" + ThemeType.Dark,
|
||||
"theme_" + ThemeType.Nord,
|
||||
"theme_" + ThemeType.SolarizedDark
|
||||
);
|
||||
this.document.documentElement.classList.add("theme_" + theme.effectiveTheme);
|
||||
});
|
||||
}
|
||||
|
||||
// We use a media match query for monitoring the system theme on web and browser, but this doesn't work for electron apps on Linux.
|
||||
// In desktop we override these methods to track systemTheme with the electron renderer instead, which works for all OSs.
|
||||
protected async getSystemTheme(): Promise<ThemeType> {
|
||||
return this.window.matchMedia("(prefers-color-scheme: dark)").matches
|
||||
? ThemeType.Dark
|
||||
: ThemeType.Light;
|
||||
}
|
||||
|
||||
protected monitorSystemThemeChanges(): void {
|
||||
fromEvent<MediaQueryListEvent>(
|
||||
window.matchMedia("(prefers-color-scheme: dark)"),
|
||||
"change"
|
||||
).subscribe((event) => {
|
||||
this.updateSystemTheme(event.matches ? ThemeType.Dark : ThemeType.Light);
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user