mirror of
https://github.com/bitwarden/browser
synced 2025-12-15 15:53:27 +00:00
feat(web): [PM-15063] add banner for pending device auth requests
Adds a banner in the web vault to notify users when they have pending device authentication requests. The banner links to the device management screen. Also implements real-time updates to the device management table when new auth requests are received. JIRA: PM-15063
This commit is contained in:
@@ -40,7 +40,8 @@
|
|||||||
>
|
>
|
||||||
{{ col.title }}
|
{{ col.title }}
|
||||||
</th>
|
</th>
|
||||||
<th bitCell scope="col" role="columnheader"></th>
|
<!-- TODO: Add a column for the device actions when available -->
|
||||||
|
<!-- <th bitCell scope="col" role="columnheader"></th> -->
|
||||||
</ng-container>
|
</ng-container>
|
||||||
<ng-template bitRowDef let-row>
|
<ng-template bitRowDef let-row>
|
||||||
<td bitCell class="tw-flex tw-gap-2">
|
<td bitCell class="tw-flex tw-gap-2">
|
||||||
|
|||||||
@@ -0,0 +1,181 @@
|
|||||||
|
import { ComponentFixture, TestBed } from "@angular/core/testing";
|
||||||
|
import { RouterTestingModule } from "@angular/router/testing";
|
||||||
|
import { of, Subject } from "rxjs";
|
||||||
|
|
||||||
|
import { AuthRequestApiService } from "@bitwarden/auth/common";
|
||||||
|
import { DevicesServiceAbstraction } from "@bitwarden/common/auth/abstractions/devices/devices.service.abstraction";
|
||||||
|
import { DeviceView } from "@bitwarden/common/auth/abstractions/devices/views/device.view";
|
||||||
|
import { DeviceType } from "@bitwarden/common/enums";
|
||||||
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
|
import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service";
|
||||||
|
import { MessageListener } from "@bitwarden/common/platform/messaging";
|
||||||
|
import { DialogService, ToastService, TableModule, PopoverModule } from "@bitwarden/components";
|
||||||
|
|
||||||
|
import { SharedModule } from "../../../shared";
|
||||||
|
import { VaultBannersService } from "../../../vault/individual-vault/vault-banners/services/vault-banners.service";
|
||||||
|
|
||||||
|
import { DeviceManagementComponent } from "./device-management.component";
|
||||||
|
|
||||||
|
class MockResizeObserver {
|
||||||
|
observe = jest.fn();
|
||||||
|
unobserve = jest.fn();
|
||||||
|
disconnect = jest.fn();
|
||||||
|
}
|
||||||
|
|
||||||
|
global.ResizeObserver = MockResizeObserver;
|
||||||
|
|
||||||
|
interface Message {
|
||||||
|
command: string;
|
||||||
|
notificationId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("DeviceManagementComponent", () => {
|
||||||
|
let fixture: ComponentFixture<DeviceManagementComponent>;
|
||||||
|
let messageSubject: Subject<Message>;
|
||||||
|
let mockDevices: DeviceView[];
|
||||||
|
let vaultBannersService: VaultBannersService;
|
||||||
|
|
||||||
|
const mockDeviceResponse = {
|
||||||
|
id: "test-id",
|
||||||
|
requestDeviceType: "test-type",
|
||||||
|
requestDeviceTypeValue: DeviceType.Android,
|
||||||
|
requestDeviceIdentifier: "test-identifier",
|
||||||
|
requestIpAddress: "127.0.0.1",
|
||||||
|
creationDate: new Date().toISOString(),
|
||||||
|
responseDate: null,
|
||||||
|
key: "test-key",
|
||||||
|
masterPasswordHash: null,
|
||||||
|
publicKey: "test-public-key",
|
||||||
|
requestApproved: false,
|
||||||
|
origin: "test-origin",
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
messageSubject = new Subject<Message>();
|
||||||
|
mockDevices = [];
|
||||||
|
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [
|
||||||
|
RouterTestingModule,
|
||||||
|
SharedModule,
|
||||||
|
TableModule,
|
||||||
|
PopoverModule,
|
||||||
|
DeviceManagementComponent,
|
||||||
|
],
|
||||||
|
providers: [
|
||||||
|
{
|
||||||
|
provide: DevicesServiceAbstraction,
|
||||||
|
useValue: {
|
||||||
|
getDevices$: jest.fn().mockReturnValue(mockDevices),
|
||||||
|
getCurrentDevice$: jest.fn().mockReturnValue(of(null)),
|
||||||
|
getDeviceByIdentifier$: jest.fn().mockReturnValue(of(null)),
|
||||||
|
updateTrustedDeviceKeys: jest.fn(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
provide: AuthRequestApiService,
|
||||||
|
useValue: {
|
||||||
|
getAuthRequest: jest.fn().mockResolvedValue(mockDeviceResponse),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
provide: MessageListener,
|
||||||
|
useValue: {
|
||||||
|
allMessages$: messageSubject.asObservable(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
provide: DialogService,
|
||||||
|
useValue: {
|
||||||
|
openSimpleDialog: jest.fn(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
provide: ToastService,
|
||||||
|
useValue: {
|
||||||
|
success: jest.fn(),
|
||||||
|
error: jest.fn(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
provide: VaultBannersService,
|
||||||
|
useValue: {
|
||||||
|
shouldShowPendingAuthRequestBanner: jest.fn(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
provide: I18nService,
|
||||||
|
useValue: {
|
||||||
|
t: jest.fn((key: string) => key),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
provide: ValidationService,
|
||||||
|
useValue: {
|
||||||
|
showError: jest.fn(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}).compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(DeviceManagementComponent);
|
||||||
|
|
||||||
|
vaultBannersService = TestBed.inject(VaultBannersService);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("message listener", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.spyOn(vaultBannersService, "shouldShowPendingAuthRequestBanner").mockResolvedValue(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("ignores other message types", async () => {
|
||||||
|
const initialDataLength = (fixture.componentInstance as any).dataSource.data.length;
|
||||||
|
const message: Message = { command: "other", notificationId: "test-id" };
|
||||||
|
messageSubject.next(message);
|
||||||
|
await fixture.whenStable();
|
||||||
|
|
||||||
|
expect((fixture.componentInstance as any).dataSource.data.length).toBe(initialDataLength);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("adds device to table when auth request message received", async () => {
|
||||||
|
const initialDataLength = (fixture.componentInstance as any).dataSource.data.length;
|
||||||
|
const message: Message = {
|
||||||
|
command: "openLoginApproval",
|
||||||
|
notificationId: "test-id",
|
||||||
|
};
|
||||||
|
|
||||||
|
messageSubject.next(message);
|
||||||
|
fixture.detectChanges();
|
||||||
|
await fixture.whenStable();
|
||||||
|
|
||||||
|
const dataSource = (fixture.componentInstance as any).dataSource;
|
||||||
|
expect(dataSource.data.length).toBe(initialDataLength + 1);
|
||||||
|
|
||||||
|
const addedDevice = dataSource.data[0];
|
||||||
|
expect(addedDevice).toEqual({
|
||||||
|
id: "",
|
||||||
|
type: mockDeviceResponse.requestDeviceTypeValue,
|
||||||
|
displayName: expect.any(String),
|
||||||
|
loginStatus: "requestPending",
|
||||||
|
firstLogin: expect.any(Date),
|
||||||
|
trusted: false,
|
||||||
|
devicePendingAuthRequest: {
|
||||||
|
id: mockDeviceResponse.id,
|
||||||
|
creationDate: mockDeviceResponse.creationDate,
|
||||||
|
},
|
||||||
|
hasPendingAuthRequest: true,
|
||||||
|
identifier: mockDeviceResponse.requestDeviceIdentifier,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("stops listening when component is destroyed", async () => {
|
||||||
|
fixture.destroy();
|
||||||
|
const message: Message = {
|
||||||
|
command: "openLoginApproval",
|
||||||
|
notificationId: "test-id",
|
||||||
|
};
|
||||||
|
messageSubject.next(message);
|
||||||
|
expect((fixture.componentInstance as any).dataSource.data.length).toBe(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,9 +1,10 @@
|
|||||||
import { CommonModule } from "@angular/common";
|
import { CommonModule } from "@angular/common";
|
||||||
import { Component } from "@angular/core";
|
import { Component, DestroyRef } from "@angular/core";
|
||||||
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
|
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
|
||||||
import { combineLatest, firstValueFrom } from "rxjs";
|
import { firstValueFrom } from "rxjs";
|
||||||
|
|
||||||
import { LoginApprovalComponent } from "@bitwarden/auth/angular";
|
import { LoginApprovalComponent } from "@bitwarden/auth/angular";
|
||||||
|
import { AuthRequestApiService } from "@bitwarden/auth/common";
|
||||||
import { DevicesServiceAbstraction } from "@bitwarden/common/auth/abstractions/devices/devices.service.abstraction";
|
import { DevicesServiceAbstraction } from "@bitwarden/common/auth/abstractions/devices/devices.service.abstraction";
|
||||||
import {
|
import {
|
||||||
DevicePendingAuthRequest,
|
DevicePendingAuthRequest,
|
||||||
@@ -13,6 +14,7 @@ import { DeviceView } from "@bitwarden/common/auth/abstractions/devices/views/de
|
|||||||
import { DeviceType, DeviceTypeMetadata } from "@bitwarden/common/enums";
|
import { DeviceType, DeviceTypeMetadata } from "@bitwarden/common/enums";
|
||||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service";
|
import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service";
|
||||||
|
import { MessageListener } from "@bitwarden/common/platform/messaging";
|
||||||
import {
|
import {
|
||||||
DialogService,
|
DialogService,
|
||||||
ToastService,
|
ToastService,
|
||||||
@@ -23,6 +25,9 @@ import {
|
|||||||
|
|
||||||
import { SharedModule } from "../../../shared";
|
import { SharedModule } from "../../../shared";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface representing a row in the device management table
|
||||||
|
*/
|
||||||
interface DeviceTableData {
|
interface DeviceTableData {
|
||||||
id: string;
|
id: string;
|
||||||
type: DeviceType;
|
type: DeviceType;
|
||||||
@@ -32,6 +37,7 @@ interface DeviceTableData {
|
|||||||
trusted: boolean;
|
trusted: boolean;
|
||||||
devicePendingAuthRequest: DevicePendingAuthRequest | null;
|
devicePendingAuthRequest: DevicePendingAuthRequest | null;
|
||||||
hasPendingAuthRequest: boolean;
|
hasPendingAuthRequest: boolean;
|
||||||
|
identifier: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -44,7 +50,6 @@ interface DeviceTableData {
|
|||||||
imports: [CommonModule, SharedModule, TableModule, PopoverModule],
|
imports: [CommonModule, SharedModule, TableModule, PopoverModule],
|
||||||
})
|
})
|
||||||
export class DeviceManagementComponent {
|
export class DeviceManagementComponent {
|
||||||
protected readonly tableId = "device-management-table";
|
|
||||||
protected dataSource = new TableDataSource<DeviceTableData>();
|
protected dataSource = new TableDataSource<DeviceTableData>();
|
||||||
protected currentDevice: DeviceView | undefined;
|
protected currentDevice: DeviceView | undefined;
|
||||||
protected loading = true;
|
protected loading = true;
|
||||||
@@ -56,32 +61,146 @@ export class DeviceManagementComponent {
|
|||||||
private dialogService: DialogService,
|
private dialogService: DialogService,
|
||||||
private toastService: ToastService,
|
private toastService: ToastService,
|
||||||
private validationService: ValidationService,
|
private validationService: ValidationService,
|
||||||
|
private messageListener: MessageListener,
|
||||||
|
private authRequestApiService: AuthRequestApiService,
|
||||||
|
private destroyRef: DestroyRef,
|
||||||
) {
|
) {
|
||||||
combineLatest([this.devicesService.getCurrentDevice$(), this.devicesService.getDevices$()])
|
void this.initializeDevices();
|
||||||
.pipe(takeUntilDestroyed())
|
}
|
||||||
.subscribe({
|
|
||||||
next: ([currentDevice, devices]: [DeviceResponse, Array<DeviceView>]) => {
|
|
||||||
this.currentDevice = new DeviceView(currentDevice);
|
|
||||||
|
|
||||||
this.dataSource.data = devices.map((device: DeviceView): DeviceTableData => {
|
/**
|
||||||
return {
|
* Initialize the devices list and set up the message listener
|
||||||
id: device.id,
|
*/
|
||||||
type: device.type,
|
private async initializeDevices(): Promise<void> {
|
||||||
displayName: this.getHumanReadableDeviceType(device.type),
|
try {
|
||||||
loginStatus: this.getLoginStatus(device),
|
await this.loadDevices();
|
||||||
firstLogin: new Date(device.creationDate),
|
|
||||||
trusted: device.response.isTrusted,
|
|
||||||
devicePendingAuthRequest: device.response.devicePendingAuthRequest,
|
|
||||||
hasPendingAuthRequest: this.hasPendingAuthRequest(device.response),
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
this.loading = false;
|
this.messageListener.allMessages$
|
||||||
},
|
.pipe(takeUntilDestroyed(this.destroyRef))
|
||||||
error: () => {
|
.subscribe((message) => {
|
||||||
this.loading = false;
|
if (message.command !== "openLoginApproval") {
|
||||||
},
|
return;
|
||||||
});
|
}
|
||||||
|
// Handle inserting a new device when an auth request is received
|
||||||
|
this.upsertDeviceWithPendingAuthRequest(
|
||||||
|
message as { command: string; notificationId: string },
|
||||||
|
).catch((error) => this.validationService.showError(error));
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
this.validationService.showError(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle inserting a new device when an auth request is received
|
||||||
|
* @param message - The auth request message
|
||||||
|
*/
|
||||||
|
private async upsertDeviceWithPendingAuthRequest(message: {
|
||||||
|
command: string;
|
||||||
|
notificationId: string;
|
||||||
|
}): Promise<void> {
|
||||||
|
const requestId = message.notificationId;
|
||||||
|
if (!requestId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const authRequestResponse = await this.authRequestApiService.getAuthRequest(requestId);
|
||||||
|
if (!authRequestResponse) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add new device to the table
|
||||||
|
const upsertDevice: DeviceTableData = {
|
||||||
|
id: "",
|
||||||
|
type: authRequestResponse.requestDeviceTypeValue,
|
||||||
|
displayName: this.getHumanReadableDeviceType(authRequestResponse.requestDeviceTypeValue),
|
||||||
|
loginStatus: this.i18nService.t("requestPending"),
|
||||||
|
firstLogin: new Date(authRequestResponse.creationDate),
|
||||||
|
trusted: false,
|
||||||
|
devicePendingAuthRequest: {
|
||||||
|
id: authRequestResponse.id,
|
||||||
|
creationDate: authRequestResponse.creationDate,
|
||||||
|
},
|
||||||
|
hasPendingAuthRequest: true,
|
||||||
|
identifier: authRequestResponse.requestDeviceIdentifier,
|
||||||
|
};
|
||||||
|
|
||||||
|
// If the device already exists in the DB, update the device id and first login date
|
||||||
|
if (authRequestResponse.requestDeviceIdentifier) {
|
||||||
|
const existingDevice = await firstValueFrom(
|
||||||
|
this.devicesService.getDeviceByIdentifier$(authRequestResponse.requestDeviceIdentifier),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (existingDevice?.id && existingDevice.creationDate) {
|
||||||
|
upsertDevice.id = existingDevice.id;
|
||||||
|
upsertDevice.firstLogin = new Date(existingDevice.creationDate);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const existingDeviceIndex = this.dataSource.data.findIndex(
|
||||||
|
(device) => device.identifier === upsertDevice.identifier,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (existingDeviceIndex >= 0) {
|
||||||
|
// Update existing device
|
||||||
|
this.dataSource.data[existingDeviceIndex] = upsertDevice;
|
||||||
|
this.dataSource.data = [...this.dataSource.data];
|
||||||
|
} else {
|
||||||
|
// Add new device
|
||||||
|
this.dataSource.data = [upsertDevice, ...this.dataSource.data];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load current device and all devices
|
||||||
|
*/
|
||||||
|
private async loadDevices(): Promise<void> {
|
||||||
|
try {
|
||||||
|
const currentDevice = await firstValueFrom(this.devicesService.getCurrentDevice$());
|
||||||
|
const devices = await firstValueFrom(this.devicesService.getDevices$());
|
||||||
|
|
||||||
|
if (!currentDevice || !devices) {
|
||||||
|
this.loading = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.currentDevice = new DeviceView(currentDevice);
|
||||||
|
this.updateDeviceTable(devices);
|
||||||
|
} catch (error) {
|
||||||
|
this.validationService.showError(error);
|
||||||
|
} finally {
|
||||||
|
this.loading = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the device table with the latest device data
|
||||||
|
* @param devices - Array of device views to display in the table
|
||||||
|
*/
|
||||||
|
private updateDeviceTable(devices: Array<DeviceView>): void {
|
||||||
|
this.dataSource.data = devices
|
||||||
|
.map((device: DeviceView): DeviceTableData | null => {
|
||||||
|
if (!device.id || !device.type || !device.creationDate) {
|
||||||
|
this.validationService.showError(new Error("Invalid device data"));
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasPendingRequest = device.response
|
||||||
|
? this.hasPendingAuthRequest(device.response)
|
||||||
|
: false;
|
||||||
|
return {
|
||||||
|
id: device.id,
|
||||||
|
type: device.type,
|
||||||
|
displayName: this.getHumanReadableDeviceType(device.type),
|
||||||
|
loginStatus: this.getLoginStatus(device),
|
||||||
|
firstLogin: new Date(device.creationDate),
|
||||||
|
trusted: device.response?.isTrusted ?? false,
|
||||||
|
devicePendingAuthRequest: device.response?.devicePendingAuthRequest ?? null,
|
||||||
|
hasPendingAuthRequest: hasPendingRequest,
|
||||||
|
identifier: device.identifier ?? "",
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.filter((device): device is DeviceTableData => device !== null);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -140,7 +259,7 @@ export class DeviceManagementComponent {
|
|||||||
return this.i18nService.t("currentSession");
|
return this.i18nService.t("currentSession");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (device.response.devicePendingAuthRequest?.creationDate) {
|
if (device?.response?.devicePendingAuthRequest?.creationDate) {
|
||||||
return this.i18nService.t("requestPending");
|
return this.i18nService.t("requestPending");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,11 @@ import {
|
|||||||
UserDecryptionOptionsServiceAbstraction,
|
UserDecryptionOptionsServiceAbstraction,
|
||||||
} from "@bitwarden/auth/common";
|
} from "@bitwarden/auth/common";
|
||||||
import { AccountInfo, AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
import { AccountInfo, AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||||
|
import { DevicesServiceAbstraction } from "@bitwarden/common/auth/abstractions/devices/devices.service.abstraction";
|
||||||
|
import { DeviceResponse } from "@bitwarden/common/auth/abstractions/devices/responses/device.response";
|
||||||
|
import { DeviceView } from "@bitwarden/common/auth/abstractions/devices/views/device.view";
|
||||||
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
|
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
|
||||||
|
import { DeviceType } from "@bitwarden/common/enums";
|
||||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||||
import { StateProvider } from "@bitwarden/common/platform/state";
|
import { StateProvider } from "@bitwarden/common/platform/state";
|
||||||
@@ -36,6 +40,7 @@ describe("VaultBannersService", () => {
|
|||||||
const accounts$ = new BehaviorSubject<Record<UserId, AccountInfo>>({
|
const accounts$ = new BehaviorSubject<Record<UserId, AccountInfo>>({
|
||||||
[userId]: { email: "test@bitwarden.com", emailVerified: true, name: "name" } as AccountInfo,
|
[userId]: { email: "test@bitwarden.com", emailVerified: true, name: "name" } as AccountInfo,
|
||||||
});
|
});
|
||||||
|
const devices$ = new BehaviorSubject<DeviceView[]>([]);
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
lastSync$.next(new Date("2024-05-14"));
|
lastSync$.next(new Date("2024-05-14"));
|
||||||
@@ -79,6 +84,10 @@ describe("VaultBannersService", () => {
|
|||||||
userDecryptionOptionsById$: () => userDecryptionOptions$,
|
userDecryptionOptionsById$: () => userDecryptionOptions$,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
provide: DevicesServiceAbstraction,
|
||||||
|
useValue: { getDevices$: () => devices$ },
|
||||||
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -274,4 +283,63 @@ describe("VaultBannersService", () => {
|
|||||||
expect(await service.shouldShowVerifyEmailBanner(userId)).toBe(false);
|
expect(await service.shouldShowVerifyEmailBanner(userId)).toBe(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("PendingAuthRequest", () => {
|
||||||
|
const now = new Date();
|
||||||
|
let deviceResponse: DeviceResponse;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
deviceResponse = new DeviceResponse({
|
||||||
|
Id: "device1",
|
||||||
|
UserId: userId,
|
||||||
|
Name: "Test Device",
|
||||||
|
Identifier: "test-device",
|
||||||
|
Type: DeviceType.Android,
|
||||||
|
CreationDate: now.toISOString(),
|
||||||
|
RevisionDate: now.toISOString(),
|
||||||
|
IsTrusted: false,
|
||||||
|
});
|
||||||
|
// Reset devices list, single user state, and active user state before each test
|
||||||
|
devices$.next([]);
|
||||||
|
fakeStateProvider.singleUser.states.clear();
|
||||||
|
fakeStateProvider.activeUser.states.clear();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("shows pending auth request banner when there is a pending request", async () => {
|
||||||
|
deviceResponse.devicePendingAuthRequest = {
|
||||||
|
id: "123",
|
||||||
|
creationDate: now.toISOString(),
|
||||||
|
};
|
||||||
|
devices$.next([new DeviceView(deviceResponse)]);
|
||||||
|
|
||||||
|
service = TestBed.inject(VaultBannersService);
|
||||||
|
|
||||||
|
expect(await service.shouldShowPendingAuthRequestBanner(userId)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not show pending auth request banner when there are no pending requests", async () => {
|
||||||
|
deviceResponse.devicePendingAuthRequest = null;
|
||||||
|
devices$.next([new DeviceView(deviceResponse)]);
|
||||||
|
|
||||||
|
service = TestBed.inject(VaultBannersService);
|
||||||
|
|
||||||
|
expect(await service.shouldShowPendingAuthRequestBanner(userId)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("dismisses pending auth request banner", async () => {
|
||||||
|
deviceResponse.devicePendingAuthRequest = {
|
||||||
|
id: "123",
|
||||||
|
creationDate: now.toISOString(),
|
||||||
|
};
|
||||||
|
devices$.next([new DeviceView(deviceResponse)]);
|
||||||
|
|
||||||
|
service = TestBed.inject(VaultBannersService);
|
||||||
|
|
||||||
|
expect(await service.shouldShowPendingAuthRequestBanner(userId)).toBe(true);
|
||||||
|
|
||||||
|
await service.dismissBanner(userId, VisibleVaultBanner.PendingAuthRequest);
|
||||||
|
|
||||||
|
expect(await service.shouldShowPendingAuthRequestBanner(userId)).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { Observable, combineLatest, firstValueFrom, map, filter, mergeMap, take
|
|||||||
|
|
||||||
import { UserDecryptionOptionsServiceAbstraction } from "@bitwarden/auth/common";
|
import { UserDecryptionOptionsServiceAbstraction } from "@bitwarden/auth/common";
|
||||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||||
|
import { DevicesServiceAbstraction } from "@bitwarden/common/auth/abstractions/devices/devices.service.abstraction";
|
||||||
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
|
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
|
||||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||||
import {
|
import {
|
||||||
@@ -21,6 +22,7 @@ export enum VisibleVaultBanner {
|
|||||||
OutdatedBrowser = "outdated-browser",
|
OutdatedBrowser = "outdated-browser",
|
||||||
Premium = "premium",
|
Premium = "premium",
|
||||||
VerifyEmail = "verify-email",
|
VerifyEmail = "verify-email",
|
||||||
|
PendingAuthRequest = "pending-auth-request",
|
||||||
}
|
}
|
||||||
|
|
||||||
type PremiumBannerReprompt = {
|
type PremiumBannerReprompt = {
|
||||||
@@ -60,8 +62,23 @@ export class VaultBannersService {
|
|||||||
private kdfConfigService: KdfConfigService,
|
private kdfConfigService: KdfConfigService,
|
||||||
private syncService: SyncService,
|
private syncService: SyncService,
|
||||||
private userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction,
|
private userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction,
|
||||||
|
private devicesService: DevicesServiceAbstraction,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
|
/** Returns true when the pending auth request banner should be shown */
|
||||||
|
async shouldShowPendingAuthRequestBanner(userId: UserId): Promise<boolean> {
|
||||||
|
const devices = await firstValueFrom(this.devicesService.getDevices$());
|
||||||
|
const hasPendingRequest = devices.some(
|
||||||
|
(device) => device.response?.devicePendingAuthRequest != null,
|
||||||
|
);
|
||||||
|
|
||||||
|
const alreadyDismissed = (await this.getBannerDismissedState(userId)).includes(
|
||||||
|
VisibleVaultBanner.PendingAuthRequest,
|
||||||
|
);
|
||||||
|
|
||||||
|
return hasPendingRequest && !alreadyDismissed;
|
||||||
|
}
|
||||||
|
|
||||||
shouldShowPremiumBanner$(userId: UserId): Observable<boolean> {
|
shouldShowPremiumBanner$(userId: UserId): Observable<boolean> {
|
||||||
const premiumBannerState = this.premiumBannerState(userId);
|
const premiumBannerState = this.premiumBannerState(userId);
|
||||||
const premiumSources$ = combineLatest([
|
const premiumSources$ = combineLatest([
|
||||||
|
|||||||
@@ -50,6 +50,20 @@
|
|||||||
</a>
|
</a>
|
||||||
</bit-banner>
|
</bit-banner>
|
||||||
|
|
||||||
|
<bit-banner
|
||||||
|
id="pending-auth-request-banner"
|
||||||
|
class="-tw-m-6 tw-flex tw-flex-col tw-pb-6"
|
||||||
|
bannerType="info"
|
||||||
|
icon="bwi-info-circle"
|
||||||
|
*ngIf="visibleBanners.includes(VisibleVaultBanner.PendingAuthRequest)"
|
||||||
|
(onClose)="dismissBanner(VisibleVaultBanner.PendingAuthRequest)"
|
||||||
|
>
|
||||||
|
{{ "youHaveAPendingLoginRequest" | i18n }}
|
||||||
|
<a bitLink linkType="secondary" routerLink="/settings/security/device-management">
|
||||||
|
{{ "reviewLoginRequest" | i18n }}
|
||||||
|
</a>
|
||||||
|
</bit-banner>
|
||||||
|
|
||||||
<app-verify-email
|
<app-verify-email
|
||||||
id="verify-email-banner"
|
id="verify-email-banner"
|
||||||
*ngIf="visibleBanners.includes(VisibleVaultBanner.VerifyEmail)"
|
*ngIf="visibleBanners.includes(VisibleVaultBanner.VerifyEmail)"
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { ComponentFixture, TestBed } from "@angular/core/testing";
|
|||||||
import { By } from "@angular/platform-browser";
|
import { By } from "@angular/platform-browser";
|
||||||
import { RouterTestingModule } from "@angular/router/testing";
|
import { RouterTestingModule } from "@angular/router/testing";
|
||||||
import { mock } from "jest-mock-extended";
|
import { mock } from "jest-mock-extended";
|
||||||
import { BehaviorSubject, Observable } from "rxjs";
|
import { BehaviorSubject, Subject } from "rxjs";
|
||||||
|
|
||||||
import { I18nPipe } from "@bitwarden/angular/platform/pipes/i18n.pipe";
|
import { I18nPipe } from "@bitwarden/angular/platform/pipes/i18n.pipe";
|
||||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||||
@@ -10,6 +10,7 @@ import { AccountService } from "@bitwarden/common/auth/abstractions/account.serv
|
|||||||
import { TokenService } from "@bitwarden/common/auth/abstractions/token.service";
|
import { TokenService } from "@bitwarden/common/auth/abstractions/token.service";
|
||||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||||
|
import { MessageListener } from "@bitwarden/common/platform/messaging";
|
||||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||||
import { FakeAccountService, mockAccountServiceWith } from "@bitwarden/common/spec";
|
import { FakeAccountService, mockAccountServiceWith } from "@bitwarden/common/spec";
|
||||||
import { UserId } from "@bitwarden/common/types/guid";
|
import { UserId } from "@bitwarden/common/types/guid";
|
||||||
@@ -24,24 +25,30 @@ import { VaultBannersComponent } from "./vault-banners.component";
|
|||||||
describe("VaultBannersComponent", () => {
|
describe("VaultBannersComponent", () => {
|
||||||
let component: VaultBannersComponent;
|
let component: VaultBannersComponent;
|
||||||
let fixture: ComponentFixture<VaultBannersComponent>;
|
let fixture: ComponentFixture<VaultBannersComponent>;
|
||||||
|
let messageSubject: Subject<{ command: string }>;
|
||||||
const premiumBanner$ = new BehaviorSubject<boolean>(false);
|
const premiumBanner$ = new BehaviorSubject<boolean>(false);
|
||||||
|
const pendingAuthRequest$ = new BehaviorSubject<boolean>(false);
|
||||||
const mockUserId = Utils.newGuid() as UserId;
|
const mockUserId = Utils.newGuid() as UserId;
|
||||||
|
|
||||||
const bannerService = mock<VaultBannersService>({
|
const bannerService = mock<VaultBannersService>({
|
||||||
shouldShowPremiumBanner$: jest.fn((userId$: Observable<UserId>) => premiumBanner$),
|
shouldShowPremiumBanner$: jest.fn((userId: UserId) => premiumBanner$),
|
||||||
shouldShowUpdateBrowserBanner: jest.fn(),
|
shouldShowUpdateBrowserBanner: jest.fn(),
|
||||||
shouldShowVerifyEmailBanner: jest.fn(),
|
shouldShowVerifyEmailBanner: jest.fn(),
|
||||||
shouldShowLowKDFBanner: jest.fn(),
|
shouldShowLowKDFBanner: jest.fn(),
|
||||||
|
shouldShowPendingAuthRequestBanner: jest.fn((userId: UserId) =>
|
||||||
|
Promise.resolve(pendingAuthRequest$.value),
|
||||||
|
),
|
||||||
dismissBanner: jest.fn(),
|
dismissBanner: jest.fn(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const accountService: FakeAccountService = mockAccountServiceWith(mockUserId);
|
const accountService: FakeAccountService = mockAccountServiceWith(mockUserId);
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
|
messageSubject = new Subject<{ command: string }>();
|
||||||
bannerService.shouldShowUpdateBrowserBanner.mockResolvedValue(false);
|
bannerService.shouldShowUpdateBrowserBanner.mockResolvedValue(false);
|
||||||
bannerService.shouldShowVerifyEmailBanner.mockResolvedValue(false);
|
bannerService.shouldShowVerifyEmailBanner.mockResolvedValue(false);
|
||||||
bannerService.shouldShowLowKDFBanner.mockResolvedValue(false);
|
bannerService.shouldShowLowKDFBanner.mockResolvedValue(false);
|
||||||
|
pendingAuthRequest$.next(false);
|
||||||
premiumBanner$.next(false);
|
premiumBanner$.next(false);
|
||||||
|
|
||||||
await TestBed.configureTestingModule({
|
await TestBed.configureTestingModule({
|
||||||
@@ -74,6 +81,12 @@ describe("VaultBannersComponent", () => {
|
|||||||
provide: AccountService,
|
provide: AccountService,
|
||||||
useValue: accountService,
|
useValue: accountService,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
provide: MessageListener,
|
||||||
|
useValue: mock<MessageListener>({
|
||||||
|
allMessages$: messageSubject.asObservable(),
|
||||||
|
}),
|
||||||
|
},
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
.overrideProvider(VaultBannersService, { useValue: bannerService })
|
.overrideProvider(VaultBannersService, { useValue: bannerService })
|
||||||
@@ -153,5 +166,76 @@ describe("VaultBannersComponent", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("PendingAuthRequest", () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
pendingAuthRequest$.next(true);
|
||||||
|
await component.ngOnInit();
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("shows pending auth request banner", async () => {
|
||||||
|
expect(component.visibleBanners).toEqual([VisibleVaultBanner.PendingAuthRequest]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("dismisses pending auth request banner", async () => {
|
||||||
|
const dismissButton = fixture.debugElement.nativeElement.querySelector(
|
||||||
|
'button[biticonbutton="bwi-close"]',
|
||||||
|
);
|
||||||
|
|
||||||
|
pendingAuthRequest$.next(false);
|
||||||
|
dismissButton.click();
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
expect(bannerService.dismissBanner).toHaveBeenCalledWith(
|
||||||
|
mockUserId,
|
||||||
|
VisibleVaultBanner.PendingAuthRequest,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Wait for async operations to complete
|
||||||
|
await fixture.whenStable();
|
||||||
|
await component.determineVisibleBanners();
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
expect(component.visibleBanners).toEqual([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("message listener", () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
bannerService.shouldShowPendingAuthRequestBanner.mockResolvedValue(true);
|
||||||
|
messageSubject.next({ command: "openLoginApproval" });
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("adds pending auth request banner when openLoginApproval message is received", async () => {
|
||||||
|
await component.ngOnInit();
|
||||||
|
messageSubject.next({ command: "openLoginApproval" });
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
expect(component.visibleBanners).toContain(VisibleVaultBanner.PendingAuthRequest);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not add duplicate pending auth request banner", async () => {
|
||||||
|
await component.ngOnInit();
|
||||||
|
messageSubject.next({ command: "openLoginApproval" });
|
||||||
|
messageSubject.next({ command: "openLoginApproval" });
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
const bannerCount = component.visibleBanners.filter(
|
||||||
|
(b) => b === VisibleVaultBanner.PendingAuthRequest,
|
||||||
|
).length;
|
||||||
|
expect(bannerCount).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("ignores other message types", async () => {
|
||||||
|
bannerService.shouldShowPendingAuthRequestBanner.mockResolvedValue(false);
|
||||||
|
await component.ngOnInit();
|
||||||
|
messageSubject.next({ command: "someOtherCommand" });
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
expect(component.visibleBanners).not.toContain(VisibleVaultBanner.PendingAuthRequest);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
// FIXME: Update this file to be type safe and remove this and next line
|
|
||||||
// @ts-strict-ignore
|
|
||||||
import { Component, Input, OnInit } from "@angular/core";
|
import { Component, Input, OnInit } from "@angular/core";
|
||||||
|
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
|
||||||
import { Router } from "@angular/router";
|
import { Router } from "@angular/router";
|
||||||
import { firstValueFrom, map, Observable, switchMap } from "rxjs";
|
import { firstValueFrom, map, Observable, switchMap, filter } from "rxjs";
|
||||||
|
|
||||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
|
import { MessageListener } from "@bitwarden/common/platform/messaging";
|
||||||
|
import { UserId } from "@bitwarden/common/types/guid";
|
||||||
import { BannerModule } from "@bitwarden/components";
|
import { BannerModule } from "@bitwarden/components";
|
||||||
|
|
||||||
import { VerifyEmailComponent } from "../../../auth/settings/verify-email.component";
|
import { VerifyEmailComponent } from "../../../auth/settings/verify-email.component";
|
||||||
@@ -34,10 +35,24 @@ export class VaultBannersComponent implements OnInit {
|
|||||||
private router: Router,
|
private router: Router,
|
||||||
private i18nService: I18nService,
|
private i18nService: I18nService,
|
||||||
private accountService: AccountService,
|
private accountService: AccountService,
|
||||||
|
private messageListener: MessageListener,
|
||||||
) {
|
) {
|
||||||
this.premiumBannerVisible$ = this.activeUserId$.pipe(
|
this.premiumBannerVisible$ = this.activeUserId$.pipe(
|
||||||
|
filter((userId): userId is UserId => userId != null),
|
||||||
switchMap((userId) => this.vaultBannerService.shouldShowPremiumBanner$(userId)),
|
switchMap((userId) => this.vaultBannerService.shouldShowPremiumBanner$(userId)),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Listen for auth request messages and show banner immediately
|
||||||
|
this.messageListener.allMessages$
|
||||||
|
.pipe(
|
||||||
|
filter((message: { command: string }) => message.command === "openLoginApproval"),
|
||||||
|
takeUntilDestroyed(),
|
||||||
|
)
|
||||||
|
.subscribe(() => {
|
||||||
|
if (!this.visibleBanners.includes(VisibleVaultBanner.PendingAuthRequest)) {
|
||||||
|
this.visibleBanners = [...this.visibleBanners, VisibleVaultBanner.PendingAuthRequest];
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async ngOnInit(): Promise<void> {
|
async ngOnInit(): Promise<void> {
|
||||||
@@ -46,8 +61,10 @@ export class VaultBannersComponent implements OnInit {
|
|||||||
|
|
||||||
async dismissBanner(banner: VisibleVaultBanner): Promise<void> {
|
async dismissBanner(banner: VisibleVaultBanner): Promise<void> {
|
||||||
const activeUserId = await firstValueFrom(this.activeUserId$);
|
const activeUserId = await firstValueFrom(this.activeUserId$);
|
||||||
|
if (!activeUserId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
await this.vaultBannerService.dismissBanner(activeUserId, banner);
|
await this.vaultBannerService.dismissBanner(activeUserId, banner);
|
||||||
|
|
||||||
await this.determineVisibleBanners();
|
await this.determineVisibleBanners();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -63,19 +80,26 @@ export class VaultBannersComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Determine which banners should be present */
|
/** Determine which banners should be present */
|
||||||
private async determineVisibleBanners(): Promise<void> {
|
async determineVisibleBanners(): Promise<void> {
|
||||||
const activeUserId = await firstValueFrom(this.activeUserId$);
|
const activeUserId = await firstValueFrom(this.activeUserId$);
|
||||||
|
|
||||||
|
if (!activeUserId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const showBrowserOutdated =
|
const showBrowserOutdated =
|
||||||
await this.vaultBannerService.shouldShowUpdateBrowserBanner(activeUserId);
|
await this.vaultBannerService.shouldShowUpdateBrowserBanner(activeUserId);
|
||||||
const showVerifyEmail = await this.vaultBannerService.shouldShowVerifyEmailBanner(activeUserId);
|
const showVerifyEmail = await this.vaultBannerService.shouldShowVerifyEmailBanner(activeUserId);
|
||||||
const showLowKdf = await this.vaultBannerService.shouldShowLowKDFBanner(activeUserId);
|
const showLowKdf = await this.vaultBannerService.shouldShowLowKDFBanner(activeUserId);
|
||||||
|
const showPendingAuthRequest =
|
||||||
|
await this.vaultBannerService.shouldShowPendingAuthRequestBanner(activeUserId);
|
||||||
|
|
||||||
this.visibleBanners = [
|
this.visibleBanners = [
|
||||||
showBrowserOutdated ? VisibleVaultBanner.OutdatedBrowser : null,
|
showBrowserOutdated ? VisibleVaultBanner.OutdatedBrowser : null,
|
||||||
showVerifyEmail ? VisibleVaultBanner.VerifyEmail : null,
|
showVerifyEmail ? VisibleVaultBanner.VerifyEmail : null,
|
||||||
showLowKdf ? VisibleVaultBanner.KDFSettings : null,
|
showLowKdf ? VisibleVaultBanner.KDFSettings : null,
|
||||||
].filter(Boolean); // remove all falsy values, i.e. null
|
showPendingAuthRequest ? VisibleVaultBanner.PendingAuthRequest : null,
|
||||||
|
].filter((banner): banner is VisibleVaultBanner => banner !== null); // ensures the filtered array contains only VisibleVaultBanner values
|
||||||
}
|
}
|
||||||
|
|
||||||
freeTrialMessage(organization: FreeTrial) {
|
freeTrialMessage(organization: FreeTrial) {
|
||||||
|
|||||||
@@ -4127,6 +4127,12 @@
|
|||||||
"updateBrowserDesc": {
|
"updateBrowserDesc": {
|
||||||
"message": "You are using an unsupported web browser. The web vault may not function properly."
|
"message": "You are using an unsupported web browser. The web vault may not function properly."
|
||||||
},
|
},
|
||||||
|
"youHaveAPendingLoginRequest": {
|
||||||
|
"message": "You have a pending login request from another device."
|
||||||
|
},
|
||||||
|
"reviewLoginRequest": {
|
||||||
|
"message": "Review login request"
|
||||||
|
},
|
||||||
"freeTrialEndPromptCount": {
|
"freeTrialEndPromptCount": {
|
||||||
"message": "Your free trial ends in $COUNT$ days.",
|
"message": "Your free trial ends in $COUNT$ days.",
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
|
|||||||
@@ -1,20 +1,19 @@
|
|||||||
// FIXME: Update this file to be type safe and remove this and next line
|
|
||||||
// @ts-strict-ignore
|
|
||||||
import { DeviceType } from "../../../../enums";
|
import { DeviceType } from "../../../../enums";
|
||||||
import { View } from "../../../../models/view/view";
|
import { View } from "../../../../models/view/view";
|
||||||
import { DeviceResponse } from "../responses/device.response";
|
import { DeviceResponse } from "../responses/device.response";
|
||||||
|
|
||||||
export class DeviceView implements View {
|
export class DeviceView implements View {
|
||||||
id: string;
|
id: string | undefined;
|
||||||
userId: string;
|
userId: string | undefined;
|
||||||
name: string;
|
name: string | undefined;
|
||||||
identifier: string;
|
identifier: string | undefined;
|
||||||
type: DeviceType;
|
type: DeviceType | undefined;
|
||||||
creationDate: string;
|
creationDate: string | undefined;
|
||||||
revisionDate: string;
|
revisionDate: string | undefined;
|
||||||
response: DeviceResponse;
|
response: DeviceResponse | undefined;
|
||||||
|
|
||||||
constructor(deviceResponse: DeviceResponse) {
|
constructor(deviceResponse: DeviceResponse) {
|
||||||
Object.assign(this, deviceResponse);
|
Object.assign(this, deviceResponse);
|
||||||
|
this.response = deviceResponse;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,9 @@ const RequestTimeOut = 60000 * 15; //15 Minutes
|
|||||||
export class AuthRequestResponse extends BaseResponse {
|
export class AuthRequestResponse extends BaseResponse {
|
||||||
id: string;
|
id: string;
|
||||||
publicKey: string;
|
publicKey: string;
|
||||||
requestDeviceType: DeviceType;
|
requestDeviceType: string;
|
||||||
|
requestDeviceTypeValue: DeviceType;
|
||||||
|
requestDeviceIdentifier: string;
|
||||||
requestIpAddress: string;
|
requestIpAddress: string;
|
||||||
key: string; // could be either an encrypted MasterKey or an encrypted UserKey
|
key: string; // could be either an encrypted MasterKey or an encrypted UserKey
|
||||||
masterPasswordHash: string; // if hash is present, the `key` above is an encrypted MasterKey (else `key` is an encrypted UserKey)
|
masterPasswordHash: string; // if hash is present, the `key` above is an encrypted MasterKey (else `key` is an encrypted UserKey)
|
||||||
@@ -21,6 +23,8 @@ export class AuthRequestResponse extends BaseResponse {
|
|||||||
this.id = this.getResponseProperty("Id");
|
this.id = this.getResponseProperty("Id");
|
||||||
this.publicKey = this.getResponseProperty("PublicKey");
|
this.publicKey = this.getResponseProperty("PublicKey");
|
||||||
this.requestDeviceType = this.getResponseProperty("RequestDeviceType");
|
this.requestDeviceType = this.getResponseProperty("RequestDeviceType");
|
||||||
|
this.requestDeviceTypeValue = this.getResponseProperty("RequestDeviceTypeValue");
|
||||||
|
this.requestDeviceIdentifier = this.getResponseProperty("RequestDeviceIdentifier");
|
||||||
this.requestIpAddress = this.getResponseProperty("RequestIpAddress");
|
this.requestIpAddress = this.getResponseProperty("RequestIpAddress");
|
||||||
this.key = this.getResponseProperty("Key");
|
this.key = this.getResponseProperty("Key");
|
||||||
this.masterPasswordHash = this.getResponseProperty("MasterPasswordHash");
|
this.masterPasswordHash = this.getResponseProperty("MasterPasswordHash");
|
||||||
|
|||||||
Reference in New Issue
Block a user