mirror of
https://github.com/bitwarden/browser
synced 2026-02-22 12:24:01 +00:00
Merge branch 'auth/pm-19877/notification-processing' into auth/pm-23620/auth-request-answering-service
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { SystemNotificationEvent } from "@bitwarden/common/platform/notifications/system-notifications.service";
|
||||
import { SystemNotificationEvent } from "@bitwarden/common/platform/system-notifications/system-notifications.service";
|
||||
import { UserId } from "@bitwarden/user-core";
|
||||
|
||||
export abstract class AuthRequestAnsweringServiceAbstraction {
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
ButtonLocation,
|
||||
SystemNotificationEvent,
|
||||
SystemNotificationsService,
|
||||
} from "@bitwarden/common/platform/notifications/system-notifications.service";
|
||||
} from "@bitwarden/common/platform/system-notifications/system-notifications.service";
|
||||
import { UserId } from "@bitwarden/user-core";
|
||||
|
||||
import { AuthRequestAnsweringServiceAbstraction } from "../../abstractions/auth-request-answering/auth-request-answering.service.abstraction";
|
||||
|
||||
@@ -5,11 +5,11 @@
|
||||
// eslint-disable-next-line @bitwarden/platform/no-enums
|
||||
export enum PushTechnology {
|
||||
/**
|
||||
* Indicates that we should use SignalR over web sockets to receive push notifications from the server.
|
||||
* Indicates that we should use SignalR over web sockets to receive push server notifications from the server.
|
||||
*/
|
||||
SignalR = 0,
|
||||
/**
|
||||
* Indicatates that we should use WebPush to receive push notifications from the server.
|
||||
* Indicates that we should use WebPush to receive push server notifications from the server.
|
||||
*/
|
||||
WebPush = 1,
|
||||
}
|
||||
|
||||
@@ -5,5 +5,5 @@ export abstract class ConfigApiServiceAbstraction {
|
||||
/**
|
||||
* Fetches the server configuration for the given user. If no user is provided, the configuration will not contain user-specific context.
|
||||
*/
|
||||
abstract get(userId: UserId | undefined): Promise<ServerConfigResponse>;
|
||||
abstract get(userId: UserId | null): Promise<ServerConfigResponse>;
|
||||
}
|
||||
|
||||
@@ -95,6 +95,13 @@ export interface Environment {
|
||||
*/
|
||||
export abstract class EnvironmentService {
|
||||
abstract environment$: Observable<Environment>;
|
||||
|
||||
/**
|
||||
* The environment stored in global state, when a user signs in the state stored here will become
|
||||
* their user environment.
|
||||
*/
|
||||
abstract globalEnvironment$: Observable<Environment>;
|
||||
|
||||
abstract cloudWebVaultUrl$: Observable<string>;
|
||||
|
||||
/**
|
||||
@@ -125,12 +132,12 @@ export abstract class EnvironmentService {
|
||||
* @param userId - The user id to set the cloud web vault app URL for. If null or undefined the global environment is set.
|
||||
* @param region - The region of the cloud web vault app.
|
||||
*/
|
||||
abstract setCloudRegion(userId: UserId, region: Region): Promise<void>;
|
||||
abstract setCloudRegion(userId: UserId | null, region: Region): Promise<void>;
|
||||
|
||||
/**
|
||||
* Get the environment from state. Useful if you need to get the environment for another user.
|
||||
*/
|
||||
abstract getEnvironment$(userId: UserId): Observable<Environment | undefined>;
|
||||
abstract getEnvironment$(userId: UserId): Observable<Environment>;
|
||||
|
||||
/**
|
||||
* @deprecated Use {@link getEnvironment$} instead.
|
||||
|
||||
@@ -28,7 +28,7 @@ The `openPopup()` method has limitations in some environments due to browser-spe
|
||||
|
||||
- **Safari**: Only works when `openPopup()` is triggered from a window context. Attempts from background service workers fail.
|
||||
- **Firefox**: Does not appear to support `openPopup()` in either context.
|
||||
- **Chrome**: Fully functional in both contexts.
|
||||
- **Chrome**: Fully functional in both contexts, but only on Mac. Windows it does not work in.
|
||||
- **Edge**: Behavior has not been tested.
|
||||
- **Vivaldi**: `openPopup()` results in an error that _might_ be related to running in a background context, but the cause is currently unclear.
|
||||
- **Opera**: Works from window context. Background calls fail silently with no error message.
|
||||
|
||||
@@ -137,7 +137,7 @@ describe("NotificationsService", () => {
|
||||
expect(actualNotification.type).toBe(expectedType);
|
||||
};
|
||||
|
||||
it("emits notifications through WebPush when supported", async () => {
|
||||
it("emits server notifications through WebPush when supported", async () => {
|
||||
const notificationsPromise = firstValueFrom(sut.notifications$.pipe(bufferCount(2)));
|
||||
|
||||
emitActiveUser(mockUser1);
|
||||
@@ -230,7 +230,7 @@ describe("NotificationsService", () => {
|
||||
});
|
||||
|
||||
it.each([
|
||||
// Temporarily rolling back notifications being connected while locked
|
||||
// Temporarily rolling back server notifications being connected while locked
|
||||
// { initialStatus: AuthenticationStatus.Locked, updatedStatus: AuthenticationStatus.Unlocked },
|
||||
// { initialStatus: AuthenticationStatus.Unlocked, updatedStatus: AuthenticationStatus.Locked },
|
||||
// { initialStatus: AuthenticationStatus.Locked, updatedStatus: AuthenticationStatus.Locked },
|
||||
@@ -259,7 +259,7 @@ describe("NotificationsService", () => {
|
||||
);
|
||||
|
||||
it.each([
|
||||
// Temporarily disabling notifications connecting while in a locked state
|
||||
// Temporarily disabling server notifications connecting while in a locked state
|
||||
// AuthenticationStatus.Locked,
|
||||
AuthenticationStatus.Unlocked,
|
||||
])(
|
||||
@@ -285,7 +285,7 @@ describe("NotificationsService", () => {
|
||||
},
|
||||
);
|
||||
|
||||
it("does not connect to any notification stream when notifications are disabled through special url", () => {
|
||||
it("does not connect to any notification stream when server notifications are disabled through special url", () => {
|
||||
const subscription = sut.notifications$.subscribe();
|
||||
emitActiveUser(mockUser1);
|
||||
emitNotificationUrl(DISABLED_NOTIFICATIONS_URL);
|
||||
@@ -63,7 +63,7 @@ export class DefaultServerNotificationsService implements ServerNotificationsSer
|
||||
distinctUntilChanged(),
|
||||
switchMap((activeAccountId) => {
|
||||
if (activeAccountId == null) {
|
||||
// We don't emit notifications for inactive accounts currently
|
||||
// We don't emit server-notifications for inactive accounts currently
|
||||
return EMPTY;
|
||||
}
|
||||
|
||||
@@ -76,8 +76,8 @@ export class DefaultServerNotificationsService implements ServerNotificationsSer
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a stream of push notifications for the given user.
|
||||
* @param userId The user id of the user to get the push notifications for.
|
||||
* Retrieves a stream of push server notifications for the given user.
|
||||
* @param userId The user id of the user to get the push server notifications for.
|
||||
*/
|
||||
private userNotifications$(userId: UserId) {
|
||||
return this.environmentService.environment$.pipe(
|
||||
@@ -111,7 +111,7 @@ export class DefaultServerNotificationsService implements ServerNotificationsSer
|
||||
}),
|
||||
supportSwitch({
|
||||
supported: (service) => {
|
||||
this.logService.info("Using WebPush for notifications");
|
||||
this.logService.info("Using WebPush for server notifications");
|
||||
return service.notifications$.pipe(
|
||||
catchError((err: unknown) => {
|
||||
this.logService.warning("Issue with web push, falling back to SignalR", err);
|
||||
@@ -120,7 +120,7 @@ export class DefaultServerNotificationsService implements ServerNotificationsSer
|
||||
);
|
||||
},
|
||||
notSupported: () => {
|
||||
this.logService.info("Using SignalR for notifications");
|
||||
this.logService.info("Using SignalR for server notifications");
|
||||
return this.connectSignalR$(userId, notificationsUrl);
|
||||
},
|
||||
}),
|
||||
@@ -240,7 +240,8 @@ export class DefaultServerNotificationsService implements ServerNotificationsSer
|
||||
mergeMap(async ([notification, userId]) => this.processNotification(notification, userId)),
|
||||
)
|
||||
.subscribe({
|
||||
error: (e: unknown) => this.logService.warning("Error in notifications$ observable", e),
|
||||
error: (e: unknown) =>
|
||||
this.logService.warning("Error in server notifications$ observable", e),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
export * from "./worker-webpush-connection.service";
|
||||
export * from "./signalr-connection.service";
|
||||
export * from "./default-server-notifications.service";
|
||||
export * from "./unsupported-server-notifications.service";
|
||||
export * from "./noop-server-notifications.service";
|
||||
export * from "./unsupported-webpush-connection.service";
|
||||
export * from "./webpush-connection.service";
|
||||
export * from "./websocket-webpush-connection.service";
|
||||
@@ -6,14 +6,14 @@ import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { LogService } from "../../abstractions/log.service";
|
||||
import { ServerNotificationsService } from "../server-notifications.service";
|
||||
|
||||
export class UnsupportedServerNotificationsService implements ServerNotificationsService {
|
||||
export class NoopServerNotificationsService implements ServerNotificationsService {
|
||||
notifications$: Observable<readonly [NotificationResponse, UserId]> = new Subject();
|
||||
|
||||
constructor(private logService: LogService) {}
|
||||
|
||||
startListening(): Subscription {
|
||||
this.logService.info(
|
||||
"Initializing no-op notification service, no push notifications will be received",
|
||||
"Initializing no-op notification service, no push server notifications will be received",
|
||||
);
|
||||
return Subscription.EMPTY;
|
||||
}
|
||||
@@ -10,7 +10,7 @@ export class WebPushNotificationsApiService {
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Posts a device-user association to the server and ensures it's installed for push notifications
|
||||
* Posts a device-user association to the server and ensures it's installed for push server notifications
|
||||
*/
|
||||
async putSubscription(pushSubscription: PushSubscriptionJSON): Promise<void> {
|
||||
const request = WebPushRequest.from(pushSubscription);
|
||||
@@ -40,7 +40,7 @@ interface PushEvent {
|
||||
}
|
||||
|
||||
/**
|
||||
* An implementation for connecting to web push based notifications running in a Worker.
|
||||
* An implementation for connecting to web push based server notifications running in a Worker.
|
||||
*/
|
||||
export class WorkerWebPushConnectionService implements WebPushConnectionService {
|
||||
private pushEvent = new Subject<PushEvent>();
|
||||
@@ -75,7 +75,7 @@ export class WorkerWebPushConnectionService implements WebPushConnectionService
|
||||
}
|
||||
|
||||
supportStatus$(userId: UserId): Observable<SupportStatus<WebPushConnector>> {
|
||||
// Check the server config to see if it supports sending WebPush notifications
|
||||
// Check the server config to see if it supports sending WebPush server notifications
|
||||
// FIXME: get config of server for the specified userId, once ConfigService supports it
|
||||
return this.configService.serverConfig$.pipe(
|
||||
map((config) =>
|
||||
@@ -13,11 +13,11 @@ export abstract class ServerNotificationsService {
|
||||
/**
|
||||
* @deprecated This method should not be consumed, an observable to listen to server
|
||||
* notifications will be available one day but it is not ready to be consumed generally.
|
||||
* Please add code reacting to notifications in {@link DefaultServerNotificationsService.processNotification}
|
||||
* Please add code reacting to server notifications in {@link DefaultServerNotificationsService.processNotification}
|
||||
*/
|
||||
abstract notifications$: Observable<readonly [NotificationResponse, UserId]>;
|
||||
/**
|
||||
* Starts automatic listening and processing of notifications, should only be called once per application,
|
||||
* Starts automatic listening and processing of server notifications, should only be called once per application,
|
||||
* or you will risk notifications being processed multiple times.
|
||||
*/
|
||||
abstract startListening(): Subscription;
|
||||
@@ -10,7 +10,7 @@ export class ConfigApiService implements ConfigApiServiceAbstraction {
|
||||
private tokenService: TokenService,
|
||||
) {}
|
||||
|
||||
async get(userId: UserId | undefined): Promise<ServerConfigResponse> {
|
||||
async get(userId: UserId | null): Promise<ServerConfigResponse> {
|
||||
// Authentication adds extra context to config responses, if the user has an access token, we want to use it
|
||||
// We don't particularly care about ensuring the token is valid and not expired, just that it exists
|
||||
const authed: boolean =
|
||||
|
||||
@@ -10,9 +10,9 @@ import {
|
||||
FakeGlobalState,
|
||||
FakeSingleUserState,
|
||||
FakeStateProvider,
|
||||
awaitAsync,
|
||||
mockAccountServiceWith,
|
||||
} from "../../../../spec";
|
||||
import { Matrix } from "../../../../spec/matrix";
|
||||
import { subscribeTo } from "../../../../spec/observable-tracker";
|
||||
import { AuthService } from "../../../auth/abstractions/auth.service";
|
||||
import { AuthenticationStatus } from "../../../auth/enums/authentication-status";
|
||||
@@ -74,7 +74,8 @@ describe("ConfigService", () => {
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
environmentService.environment$ = environmentSubject;
|
||||
Matrix.autoMockMethod(environmentService.getEnvironment$, () => environmentSubject);
|
||||
environmentService.globalEnvironment$ = environmentSubject;
|
||||
sut = new DefaultConfigService(
|
||||
configApiService,
|
||||
environmentService,
|
||||
@@ -98,9 +99,12 @@ describe("ConfigService", () => {
|
||||
: serverConfigFactory(activeApiUrl + userId, tooOld);
|
||||
const globalStored =
|
||||
configStateDescription === "missing"
|
||||
? {}
|
||||
? {
|
||||
[activeApiUrl]: null,
|
||||
}
|
||||
: {
|
||||
[activeApiUrl]: serverConfigFactory(activeApiUrl, tooOld),
|
||||
[activeApiUrl + "0"]: serverConfigFactory(activeApiUrl + userId, tooOld),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -108,11 +112,6 @@ describe("ConfigService", () => {
|
||||
userState.nextState(userStored);
|
||||
});
|
||||
|
||||
// sanity check
|
||||
test("authed and unauthorized state are different", () => {
|
||||
expect(globalStored[activeApiUrl]).not.toEqual(userStored);
|
||||
});
|
||||
|
||||
describe("fail to fetch", () => {
|
||||
beforeEach(() => {
|
||||
configApiService.get.mockRejectedValue(new Error("Unable to fetch"));
|
||||
@@ -178,6 +177,7 @@ describe("ConfigService", () => {
|
||||
beforeEach(() => {
|
||||
globalState.stateSubject.next(globalStored);
|
||||
userState.nextState(userStored);
|
||||
Matrix.autoMockMethod(environmentService.getEnvironment$, () => environmentSubject);
|
||||
});
|
||||
it("does not fetch from server", async () => {
|
||||
await firstValueFrom(sut.serverConfig$);
|
||||
@@ -189,21 +189,13 @@ describe("ConfigService", () => {
|
||||
const actual = await firstValueFrom(sut.serverConfig$);
|
||||
expect(actual).toEqual(activeUserId ? userStored : globalStored[activeApiUrl]);
|
||||
});
|
||||
|
||||
it("does not complete after emit", async () => {
|
||||
const emissions = [];
|
||||
const subscription = sut.serverConfig$.subscribe((v) => emissions.push(v));
|
||||
await awaitAsync();
|
||||
expect(emissions.length).toBe(1);
|
||||
expect(subscription.closed).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("gets global config when there is an locked active user", async () => {
|
||||
await accountService.switchAccount(userId);
|
||||
environmentService.environment$ = of(environmentFactory(activeApiUrl));
|
||||
environmentService.globalEnvironment$ = of(environmentFactory(activeApiUrl));
|
||||
|
||||
globalState.stateSubject.next({
|
||||
[activeApiUrl]: serverConfigFactory(activeApiUrl + "global"),
|
||||
@@ -236,7 +228,8 @@ describe("ConfigService", () => {
|
||||
|
||||
beforeEach(() => {
|
||||
environmentSubject = new Subject<Environment>();
|
||||
environmentService.environment$ = environmentSubject;
|
||||
environmentService.globalEnvironment$ = environmentSubject;
|
||||
Matrix.autoMockMethod(environmentService.getEnvironment$, () => environmentSubject);
|
||||
sut = new DefaultConfigService(
|
||||
configApiService,
|
||||
environmentService,
|
||||
@@ -327,7 +320,8 @@ describe("ConfigService", () => {
|
||||
|
||||
beforeEach(async () => {
|
||||
const config = serverConfigFactory("existing-data", tooOld);
|
||||
environmentService.environment$ = environmentSubject;
|
||||
environmentService.globalEnvironment$ = environmentSubject;
|
||||
Matrix.autoMockMethod(environmentService.getEnvironment$, () => environmentSubject);
|
||||
|
||||
globalState.stateSubject.next({ [apiUrl(0)]: config });
|
||||
userState.stateSubject.next({
|
||||
|
||||
@@ -1,17 +1,18 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import {
|
||||
combineLatest,
|
||||
distinctUntilChanged,
|
||||
firstValueFrom,
|
||||
map,
|
||||
mergeWith,
|
||||
NEVER,
|
||||
Observable,
|
||||
of,
|
||||
shareReplay,
|
||||
ReplaySubject,
|
||||
share,
|
||||
Subject,
|
||||
switchMap,
|
||||
tap,
|
||||
timer,
|
||||
} from "rxjs";
|
||||
import { SemVer } from "semver";
|
||||
|
||||
@@ -50,11 +51,15 @@ export const GLOBAL_SERVER_CONFIGURATIONS = KeyDefinition.record<ServerConfig, A
|
||||
},
|
||||
);
|
||||
|
||||
const environmentComparer = (previous: Environment, current: Environment) => {
|
||||
return previous.getApiUrl() === current.getApiUrl();
|
||||
};
|
||||
|
||||
// FIXME: currently we are limited to api requests for active users. Update to accept a UserId and APIUrl once ApiService supports it.
|
||||
export class DefaultConfigService implements ConfigService {
|
||||
private failedFetchFallbackSubject = new Subject<ServerConfig>();
|
||||
private failedFetchFallbackSubject = new Subject<ServerConfig | null>();
|
||||
|
||||
serverConfig$: Observable<ServerConfig>;
|
||||
serverConfig$: Observable<ServerConfig | null>;
|
||||
|
||||
serverSettings$: Observable<ServerSettings>;
|
||||
|
||||
@@ -67,32 +72,61 @@ export class DefaultConfigService implements ConfigService {
|
||||
private stateProvider: StateProvider,
|
||||
private authService: AuthService,
|
||||
) {
|
||||
const userId$ = this.stateProvider.activeUserId$;
|
||||
const authStatus$ = userId$.pipe(
|
||||
switchMap((userId) => (userId == null ? of(null) : this.authService.authStatusFor$(userId))),
|
||||
const globalConfig$ = this.environmentService.globalEnvironment$.pipe(
|
||||
distinctUntilChanged(environmentComparer),
|
||||
switchMap((environment) =>
|
||||
this.globalConfigFor$(environment.getApiUrl()).pipe(
|
||||
map((config) => {
|
||||
return [config, null as UserId | null, environment, config] as const;
|
||||
}),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
this.serverConfig$ = combineLatest([
|
||||
userId$,
|
||||
this.environmentService.environment$,
|
||||
authStatus$,
|
||||
]).pipe(
|
||||
switchMap(([userId, environment, authStatus]) => {
|
||||
if (userId == null || authStatus !== AuthenticationStatus.Unlocked) {
|
||||
return this.globalConfigFor$(environment.getApiUrl()).pipe(
|
||||
map((config) => [config, null, environment] as const),
|
||||
);
|
||||
this.serverConfig$ = this.stateProvider.activeUserId$.pipe(
|
||||
distinctUntilChanged(),
|
||||
switchMap((userId) => {
|
||||
if (userId == null) {
|
||||
// Global
|
||||
return globalConfig$;
|
||||
}
|
||||
|
||||
return this.userConfigFor$(userId).pipe(
|
||||
map((config) => [config, userId, environment] as const),
|
||||
return this.authService.authStatusFor$(userId).pipe(
|
||||
map((authStatus) => authStatus === AuthenticationStatus.Unlocked),
|
||||
distinctUntilChanged(),
|
||||
switchMap((isUnlocked) => {
|
||||
if (!isUnlocked) {
|
||||
return globalConfig$;
|
||||
}
|
||||
|
||||
return combineLatest([
|
||||
this.environmentService
|
||||
.getEnvironment$(userId)
|
||||
.pipe(distinctUntilChanged(environmentComparer)),
|
||||
this.userConfigFor$(userId),
|
||||
]).pipe(
|
||||
switchMap(([environment, config]) => {
|
||||
if (config == null) {
|
||||
// If the user doesn't have any config yet, use the global config for that url as the fallback
|
||||
return this.globalConfigFor$(environment.getApiUrl()).pipe(
|
||||
map(
|
||||
(globalConfig) =>
|
||||
[null as ServerConfig | null, userId, environment, globalConfig] as const,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return of([config, userId, environment, config] as const);
|
||||
}),
|
||||
);
|
||||
}),
|
||||
);
|
||||
}),
|
||||
tap(async (rec) => {
|
||||
const [existingConfig, userId, environment] = rec;
|
||||
const [existingConfig, userId, environment, fallbackConfig] = rec;
|
||||
// Grab new config if older retrieval interval
|
||||
if (!existingConfig || this.olderThanRetrievalInterval(existingConfig.utcDate)) {
|
||||
await this.renewConfig(existingConfig, userId, environment);
|
||||
await this.renewConfig(existingConfig, userId, environment, fallbackConfig);
|
||||
}
|
||||
}),
|
||||
switchMap(([existingConfig]) => {
|
||||
@@ -106,7 +140,7 @@ export class DefaultConfigService implements ConfigService {
|
||||
}),
|
||||
// If fetch fails, we'll emit on this subject to fallback to the existing config
|
||||
mergeWith(this.failedFetchFallbackSubject),
|
||||
shareReplay({ refCount: true, bufferSize: 1 }),
|
||||
share({ connector: () => new ReplaySubject(1), resetOnRefCountZero: () => timer(1000) }),
|
||||
);
|
||||
|
||||
this.cloudRegion$ = this.serverConfig$.pipe(
|
||||
@@ -155,19 +189,18 @@ export class DefaultConfigService implements ConfigService {
|
||||
|
||||
// Updates the on-disk configuration with a newly retrieved configuration
|
||||
private async renewConfig(
|
||||
existingConfig: ServerConfig,
|
||||
userId: UserId,
|
||||
existingConfig: ServerConfig | null,
|
||||
userId: UserId | null,
|
||||
environment: Environment,
|
||||
fallbackConfig: ServerConfig | null,
|
||||
): Promise<void> {
|
||||
try {
|
||||
// Feature flags often have a big impact on user experience, lets ensure we return some value
|
||||
// somewhat quickly even though it may not be accurate, we won't cancel the HTTP request
|
||||
// though so that hopefully it can have finished and hydrated a more accurate value.
|
||||
const handle = setTimeout(() => {
|
||||
this.logService.info(
|
||||
"Self-host environment did not respond in time, emitting previous config.",
|
||||
);
|
||||
this.failedFetchFallbackSubject.next(existingConfig);
|
||||
this.logService.info("Environment did not respond in time, emitting previous config.");
|
||||
this.failedFetchFallbackSubject.next(fallbackConfig);
|
||||
}, SLOW_EMISSION_GUARD);
|
||||
const response = await this.configApiService.get(userId);
|
||||
clearTimeout(handle);
|
||||
@@ -195,17 +228,17 @@ export class DefaultConfigService implements ConfigService {
|
||||
// mutate error to be handled by catchError
|
||||
this.logService.error(`Unable to fetch ServerConfig from ${environment.getApiUrl()}`, e);
|
||||
// Emit the existing config
|
||||
this.failedFetchFallbackSubject.next(existingConfig);
|
||||
this.failedFetchFallbackSubject.next(fallbackConfig);
|
||||
}
|
||||
}
|
||||
|
||||
private globalConfigFor$(apiUrl: string): Observable<ServerConfig> {
|
||||
private globalConfigFor$(apiUrl: string): Observable<ServerConfig | null> {
|
||||
return this.stateProvider
|
||||
.getGlobal(GLOBAL_SERVER_CONFIGURATIONS)
|
||||
.state$.pipe(map((configs) => configs?.[apiUrl]));
|
||||
.state$.pipe(map((configs) => configs?.[apiUrl] ?? null));
|
||||
}
|
||||
|
||||
private userConfigFor$(userId: UserId): Observable<ServerConfig> {
|
||||
private userConfigFor$(userId: UserId): Observable<ServerConfig | null> {
|
||||
return this.stateProvider.getUser(userId, USER_SERVER_CONFIG).state$;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -133,6 +133,7 @@ export class DefaultEnvironmentService implements EnvironmentService {
|
||||
);
|
||||
|
||||
environment$: Observable<Environment>;
|
||||
globalEnvironment$: Observable<Environment>;
|
||||
cloudWebVaultUrl$: Observable<string>;
|
||||
|
||||
constructor(
|
||||
@@ -148,6 +149,10 @@ export class DefaultEnvironmentService implements EnvironmentService {
|
||||
distinctUntilChanged((oldUserId: UserId, newUserId: UserId) => oldUserId == newUserId),
|
||||
);
|
||||
|
||||
this.globalEnvironment$ = this.stateProvider
|
||||
.getGlobal(GLOBAL_ENVIRONMENT_KEY)
|
||||
.state$.pipe(map((state) => this.buildEnvironment(state?.region, state?.urls)));
|
||||
|
||||
this.environment$ = account$.pipe(
|
||||
switchMap((userId) => {
|
||||
const t = userId
|
||||
@@ -263,7 +268,7 @@ export class DefaultEnvironmentService implements EnvironmentService {
|
||||
return new SelfHostedEnvironment(urls);
|
||||
}
|
||||
|
||||
async setCloudRegion(userId: UserId, region: CloudRegion) {
|
||||
async setCloudRegion(userId: UserId | null, region: CloudRegion) {
|
||||
if (userId == null) {
|
||||
await this.globalCloudRegionState.update(() => region);
|
||||
} else {
|
||||
@@ -271,7 +276,7 @@ export class DefaultEnvironmentService implements EnvironmentService {
|
||||
}
|
||||
}
|
||||
|
||||
getEnvironment$(userId: UserId): Observable<Environment | undefined> {
|
||||
getEnvironment$(userId: UserId): Observable<Environment> {
|
||||
return this.stateProvider.getUser(userId, USER_ENVIRONMENT_KEY).state$.pipe(
|
||||
map((state) => {
|
||||
return this.buildEnvironment(state?.region, state?.urls);
|
||||
|
||||
@@ -230,7 +230,7 @@ export abstract class CoreSyncService implements SyncService {
|
||||
}),
|
||||
),
|
||||
);
|
||||
// Process only notifications for currently active user when user is not logged out
|
||||
// Process only server notifications for currently active user when user is not logged out
|
||||
// TODO: once send service allows data manipulation of non-active users, this should process any received notification
|
||||
if (activeUserId === notification.userId && status !== AuthenticationStatus.LoggedOut) {
|
||||
try {
|
||||
|
||||
1
libs/common/src/platform/system-notifications/index.ts
Normal file
1
libs/common/src/platform/system-notifications/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { SystemNotificationsService } from "./system-notifications.service";
|
||||
@@ -32,7 +32,7 @@ export type SystemNotificationEvent = {
|
||||
};
|
||||
|
||||
/**
|
||||
* A service responsible for displaying operating system level notifications.
|
||||
* A service responsible for displaying operating system level server notifications.
|
||||
*/
|
||||
export abstract class SystemNotificationsService {
|
||||
abstract notificationClicked$: Observable<SystemNotificationEvent>;
|
||||
@@ -43,7 +43,7 @@ export abstract class SystemNotificationsService {
|
||||
* @returns If a notification is successfully created it will respond back with an
|
||||
* id that refers to a notification.
|
||||
*/
|
||||
abstract create(createInfo: SystemNotificationCreateInfo): Promise<string | undefined>;
|
||||
abstract create(createInfo: SystemNotificationCreateInfo): Promise<string>;
|
||||
|
||||
/**
|
||||
* Clears a notification.
|
||||
@@ -52,7 +52,7 @@ export abstract class SystemNotificationsService {
|
||||
abstract clear(clearInfo: SystemNotificationClearInfo): Promise<void>;
|
||||
|
||||
/**
|
||||
* Used to know if a given platform supports notifications.
|
||||
* Used to know if a given platform supports server notifications.
|
||||
*/
|
||||
abstract isSupported(): boolean;
|
||||
}
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
export class UnsupportedSystemNotificationsService implements SystemNotificationsService {
|
||||
notificationClicked$ = throwError(() => new Error("Notification clicked is not supported."));
|
||||
|
||||
async create(createInfo: SystemNotificationCreateInfo): Promise<undefined> {
|
||||
async create(createInfo: SystemNotificationCreateInfo): Promise<string> {
|
||||
throw new Error("Create OS Notification unsupported.");
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import { firstValueFrom, of } from "rxjs";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { ServerNotificationsService } from "@bitwarden/common/platform/notifications";
|
||||
import { ServerNotificationsService } from "@bitwarden/common/platform/server-notifications";
|
||||
import { StateProvider } from "@bitwarden/common/platform/state";
|
||||
import { NotificationId, UserId } from "@bitwarden/common/types/guid";
|
||||
|
||||
@@ -48,7 +48,7 @@ describe("End User Notification Center Service", () => {
|
||||
});
|
||||
|
||||
describe("notifications$", () => {
|
||||
it("should return notifications from state when not null", async () => {
|
||||
it("should return server notifications from state when not null", async () => {
|
||||
fakeStateProvider.singleUser.mockFor("user-id" as UserId, NOTIFICATIONS, [
|
||||
{
|
||||
id: "notification-id" as NotificationId,
|
||||
@@ -62,7 +62,7 @@ describe("End User Notification Center Service", () => {
|
||||
expect(mockLogService.warning).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should return notifications API when state is null", async () => {
|
||||
it("should return server notifications API when state is null", async () => {
|
||||
mockApiService.send.mockResolvedValue({
|
||||
data: [
|
||||
{
|
||||
@@ -86,7 +86,7 @@ describe("End User Notification Center Service", () => {
|
||||
expect(mockLogService.warning).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should log a warning if there are more notifications available", async () => {
|
||||
it("should log a warning if there are more server notifications available", async () => {
|
||||
mockApiService.send.mockResolvedValue({
|
||||
data: [
|
||||
...new Array(DEFAULT_NOTIFICATION_PAGE_SIZE + 1).fill({ id: "notification-id" }),
|
||||
@@ -120,7 +120,7 @@ describe("End User Notification Center Service", () => {
|
||||
});
|
||||
|
||||
describe("unreadNotifications$", () => {
|
||||
it("should return unread notifications from state when read value is null", async () => {
|
||||
it("should return unread server notifications from state when read value is null", async () => {
|
||||
fakeStateProvider.singleUser.mockFor("user-id" as UserId, NOTIFICATIONS, [
|
||||
{
|
||||
id: "notification-id" as NotificationId,
|
||||
@@ -136,7 +136,7 @@ describe("End User Notification Center Service", () => {
|
||||
});
|
||||
|
||||
describe("getNotifications", () => {
|
||||
it("should call getNotifications returning notifications from API", async () => {
|
||||
it("should call getNotifications returning server notifications from API", async () => {
|
||||
mockApiService.send.mockResolvedValue({
|
||||
data: [
|
||||
{
|
||||
@@ -156,7 +156,7 @@ describe("End User Notification Center Service", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("should update local state when notifications are updated", async () => {
|
||||
it("should update local state when server notifications are updated", async () => {
|
||||
mockApiService.send.mockResolvedValue({
|
||||
data: [
|
||||
{
|
||||
|
||||
@@ -6,7 +6,7 @@ import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authenticatio
|
||||
import { NotificationType } from "@bitwarden/common/enums";
|
||||
import { ListResponse } from "@bitwarden/common/models/response/list.response";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { ServerNotificationsService } from "@bitwarden/common/platform/notifications";
|
||||
import { ServerNotificationsService } from "@bitwarden/common/platform/server-notifications";
|
||||
import { StateProvider } from "@bitwarden/common/platform/state";
|
||||
import { NotificationId, UserId } from "@bitwarden/common/types/guid";
|
||||
import {
|
||||
|
||||
@@ -8,7 +8,7 @@ import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authenticatio
|
||||
import { NotificationType } from "@bitwarden/common/enums";
|
||||
import { NotificationResponse } from "@bitwarden/common/models/response/notification.response";
|
||||
import { Message, MessageListener } from "@bitwarden/common/platform/messaging";
|
||||
import { ServerNotificationsService } from "@bitwarden/common/platform/notifications";
|
||||
import { ServerNotificationsService } from "@bitwarden/common/platform/server-notifications";
|
||||
import { SecurityTaskId, UserId } from "@bitwarden/common/types/guid";
|
||||
|
||||
import { FakeStateProvider, mockAccountServiceWith } from "../../../../spec";
|
||||
|
||||
@@ -17,7 +17,7 @@ import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authenticatio
|
||||
import { NotificationType } from "@bitwarden/common/enums";
|
||||
import { ListResponse } from "@bitwarden/common/models/response/list.response";
|
||||
import { MessageListener } from "@bitwarden/common/platform/messaging";
|
||||
import { ServerNotificationsService } from "@bitwarden/common/platform/notifications";
|
||||
import { ServerNotificationsService } from "@bitwarden/common/platform/server-notifications";
|
||||
import { StateProvider } from "@bitwarden/common/platform/state";
|
||||
import { SecurityTaskId, UserId } from "@bitwarden/common/types/guid";
|
||||
import {
|
||||
@@ -171,7 +171,7 @@ export class DefaultTaskService implements TaskService {
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a subscription for pending security task notifications or completed syncs for unlocked users.
|
||||
* Creates a subscription for pending security task server notifications or completed syncs for unlocked users.
|
||||
*/
|
||||
listenForTaskNotifications(): Subscription {
|
||||
return this.authService.authStatuses$
|
||||
|
||||
Reference in New Issue
Block a user