mirror of
https://github.com/bitwarden/browser
synced 2025-12-11 13:53:34 +00:00
refactor(device-management): Auth/[PM-19823] Cleanup DeviceManagementOldComponent (#16541)
This commit is contained in:
@@ -1,104 +0,0 @@
|
|||||||
<bit-container>
|
|
||||||
<div class="tw-mt-6 tw-mb-2 tw-pb-2.5">
|
|
||||||
<div class="tw-flex tw-items-center tw-gap-2">
|
|
||||||
<h1 class="tw-m-0">{{ "devices" | i18n }}</h1>
|
|
||||||
<button
|
|
||||||
[bitPopoverTriggerFor]="infoPopover"
|
|
||||||
type="button"
|
|
||||||
class="tw-border-none tw-bg-transparent tw-text-primary-600 tw-flex tw-items-center tw-size-4"
|
|
||||||
[position]="'right-start'"
|
|
||||||
>
|
|
||||||
<i class="bwi bwi-question-circle" aria-hidden="true"></i>
|
|
||||||
</button>
|
|
||||||
<bit-popover [title]="'whatIsADevice' | i18n" #infoPopover>
|
|
||||||
<p class="tw-mb-0">{{ "aDeviceIs" | i18n }}</p>
|
|
||||||
</bit-popover>
|
|
||||||
<i
|
|
||||||
*ngIf="asyncActionLoading"
|
|
||||||
class="bwi bwi-spinner bwi-spin tw-flex tw-items-center tw-size-4"
|
|
||||||
aria-hidden="true"
|
|
||||||
></i>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<p>{{ "deviceListDescriptionTemp" | i18n }}</p>
|
|
||||||
|
|
||||||
<div *ngIf="loading" class="tw-flex tw-justify-center tw-items-center tw-p-4">
|
|
||||||
<i class="bwi bwi-spinner bwi-spin tw-text-2xl" aria-hidden="true"></i>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<bit-table-scroll *ngIf="!loading" [dataSource]="dataSource" [rowSize]="50">
|
|
||||||
<ng-container header>
|
|
||||||
<th
|
|
||||||
*ngFor="let col of columnConfig"
|
|
||||||
[class]="col.headerClass"
|
|
||||||
bitCell
|
|
||||||
[bitSortable]="col.sortable ? col.name : null"
|
|
||||||
[default]="col.name === 'loginStatus' ? 'desc' : null"
|
|
||||||
scope="col"
|
|
||||||
role="columnheader"
|
|
||||||
>
|
|
||||||
{{ col.title }}
|
|
||||||
</th>
|
|
||||||
<!-- TODO: Add a column for the device actions when available -->
|
|
||||||
<!-- <th bitCell scope="col" role="columnheader"></th> -->
|
|
||||||
</ng-container>
|
|
||||||
<ng-template bitRowDef let-row>
|
|
||||||
<td bitCell class="tw-flex tw-gap-2">
|
|
||||||
<div class="tw-flex tw-items-center tw-justify-center tw-w-10">
|
|
||||||
<i [class]="getDeviceIcon(row.type)" class="bwi-lg" aria-hidden="true"></i>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<ng-container *ngIf="row.hasPendingAuthRequest">
|
|
||||||
<a bitLink href="#" appStopClick (click)="managePendingAuthRequest(row)">
|
|
||||||
{{ row.displayName }}
|
|
||||||
</a>
|
|
||||||
|
|
||||||
<span class="tw-text-sm tw-text-muted tw-block">
|
|
||||||
{{ "needsApproval" | i18n }}
|
|
||||||
</span>
|
|
||||||
</ng-container>
|
|
||||||
<ng-container *ngIf="!row.hasPendingAuthRequest">
|
|
||||||
{{ row.displayName }}
|
|
||||||
<span
|
|
||||||
*ngIf="row.trusted && !row.hasPendingAuthRequest"
|
|
||||||
class="tw-text-sm tw-text-muted tw-block"
|
|
||||||
>
|
|
||||||
{{ "trusted" | i18n }}
|
|
||||||
</span>
|
|
||||||
</ng-container>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td bitCell>
|
|
||||||
<span *ngIf="isCurrentDevice(row)" bitBadge variant="primary">{{
|
|
||||||
"currentSession" | i18n
|
|
||||||
}}</span>
|
|
||||||
<span *ngIf="row.hasPendingAuthRequest" bitBadge variant="warning">{{
|
|
||||||
"requestPending" | i18n
|
|
||||||
}}</span>
|
|
||||||
</td>
|
|
||||||
<td bitCell>{{ row.firstLogin | date: "medium" }}</td>
|
|
||||||
<!-- <td bitCell>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
bitIconButton="bwi-ellipsis-v"
|
|
||||||
[bitMenuTriggerFor]="optionsMenu"
|
|
||||||
></button>
|
|
||||||
<bit-menu #optionsMenu>
|
|
||||||
Remove device button to be re-added later when we have per device session de-authentication.
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
bitMenuItem
|
|
||||||
(click)="removeDevice(row)"
|
|
||||||
[disabled]="isCurrentDevice(row)"
|
|
||||||
>
|
|
||||||
<span [class]="isCurrentDevice(row) ? 'tw-text-muted' : 'tw-text-danger'">
|
|
||||||
<i class="bwi bwi-trash" aria-hidden="true"></i>
|
|
||||||
{{ "removeDevice" | i18n }}
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
</bit-menu>
|
|
||||||
</td> -->
|
|
||||||
</ng-template>
|
|
||||||
</bit-table-scroll>
|
|
||||||
</bit-container>
|
|
||||||
@@ -1,193 +0,0 @@
|
|||||||
import { ComponentFixture, TestBed } from "@angular/core/testing";
|
|
||||||
import { RouterTestingModule } from "@angular/router/testing";
|
|
||||||
import { of, Subject } from "rxjs";
|
|
||||||
|
|
||||||
import { AuthRequestApiServiceAbstraction } 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,
|
|
||||||
LayoutComponent,
|
|
||||||
} from "@bitwarden/components";
|
|
||||||
|
|
||||||
import { SharedModule } from "../../../shared";
|
|
||||||
import { VaultBannersService } from "../../../vault/individual-vault/vault-banners/services/vault-banners.service";
|
|
||||||
|
|
||||||
import { DeviceManagementOldComponent } from "./device-management-old.component";
|
|
||||||
|
|
||||||
class MockResizeObserver {
|
|
||||||
observe = jest.fn();
|
|
||||||
unobserve = jest.fn();
|
|
||||||
disconnect = jest.fn();
|
|
||||||
}
|
|
||||||
|
|
||||||
global.ResizeObserver = MockResizeObserver;
|
|
||||||
|
|
||||||
interface Message {
|
|
||||||
command: string;
|
|
||||||
notificationId?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
describe("DeviceManagementOldComponent", () => {
|
|
||||||
let fixture: ComponentFixture<DeviceManagementOldComponent>;
|
|
||||||
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,
|
|
||||||
DeviceManagementOldComponent,
|
|
||||||
],
|
|
||||||
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: AuthRequestApiServiceAbstraction,
|
|
||||||
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(),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
provide: LayoutComponent,
|
|
||||||
useValue: {
|
|
||||||
mainContent: jest.fn(),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}).compileComponents();
|
|
||||||
|
|
||||||
fixture = TestBed.createComponent(DeviceManagementOldComponent);
|
|
||||||
|
|
||||||
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,373 +0,0 @@
|
|||||||
import { CommonModule } from "@angular/common";
|
|
||||||
import { Component, DestroyRef } from "@angular/core";
|
|
||||||
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
|
|
||||||
import { firstValueFrom } from "rxjs";
|
|
||||||
|
|
||||||
import { LoginApprovalDialogComponent } from "@bitwarden/angular/auth/login-approval";
|
|
||||||
import { AuthRequestApiServiceAbstraction } from "@bitwarden/auth/common";
|
|
||||||
import { DevicesServiceAbstraction } from "@bitwarden/common/auth/abstractions/devices/devices.service.abstraction";
|
|
||||||
import {
|
|
||||||
DevicePendingAuthRequest,
|
|
||||||
DeviceResponse,
|
|
||||||
} from "@bitwarden/common/auth/abstractions/devices/responses/device.response";
|
|
||||||
import { DeviceView } from "@bitwarden/common/auth/abstractions/devices/views/device.view";
|
|
||||||
import { DeviceType, DeviceTypeMetadata } 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,
|
|
||||||
TableDataSource,
|
|
||||||
TableModule,
|
|
||||||
PopoverModule,
|
|
||||||
} from "@bitwarden/components";
|
|
||||||
|
|
||||||
import { SharedModule } from "../../../shared";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Interface representing a row in the device management table
|
|
||||||
*/
|
|
||||||
interface DeviceTableData {
|
|
||||||
id: string;
|
|
||||||
type: DeviceType;
|
|
||||||
displayName: string;
|
|
||||||
loginStatus: string;
|
|
||||||
firstLogin: Date;
|
|
||||||
trusted: boolean;
|
|
||||||
devicePendingAuthRequest: DevicePendingAuthRequest | null;
|
|
||||||
hasPendingAuthRequest: boolean;
|
|
||||||
identifier: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Provides a table of devices and allows the user to log out, approve or remove a device
|
|
||||||
*/
|
|
||||||
@Component({
|
|
||||||
selector: "app-device-management",
|
|
||||||
templateUrl: "./device-management-old.component.html",
|
|
||||||
imports: [CommonModule, SharedModule, TableModule, PopoverModule],
|
|
||||||
})
|
|
||||||
export class DeviceManagementOldComponent {
|
|
||||||
protected dataSource = new TableDataSource<DeviceTableData>();
|
|
||||||
protected currentDevice: DeviceView | undefined;
|
|
||||||
protected loading = true;
|
|
||||||
protected asyncActionLoading = false;
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
private i18nService: I18nService,
|
|
||||||
private devicesService: DevicesServiceAbstraction,
|
|
||||||
private dialogService: DialogService,
|
|
||||||
private toastService: ToastService,
|
|
||||||
private validationService: ValidationService,
|
|
||||||
private messageListener: MessageListener,
|
|
||||||
private authRequestApiService: AuthRequestApiServiceAbstraction,
|
|
||||||
private destroyRef: DestroyRef,
|
|
||||||
) {
|
|
||||||
void this.initializeDevices();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initialize the devices list and set up the message listener
|
|
||||||
*/
|
|
||||||
private async initializeDevices(): Promise<void> {
|
|
||||||
try {
|
|
||||||
await this.loadDevices();
|
|
||||||
|
|
||||||
this.messageListener.allMessages$
|
|
||||||
.pipe(takeUntilDestroyed(this.destroyRef))
|
|
||||||
.subscribe((message) => {
|
|
||||||
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) {
|
|
||||||
this.validationService.showError(new Error(this.i18nService.t("deviceIdMissing")));
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (device.type == undefined) {
|
|
||||||
this.validationService.showError(new Error(this.i18nService.t("deviceTypeMissing")));
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!device.creationDate) {
|
|
||||||
this.validationService.showError(
|
|
||||||
new Error(this.i18nService.t("deviceCreationDateMissing")),
|
|
||||||
);
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Column configuration for the table
|
|
||||||
*/
|
|
||||||
protected readonly columnConfig = [
|
|
||||||
{
|
|
||||||
name: "displayName",
|
|
||||||
title: this.i18nService.t("device"),
|
|
||||||
headerClass: "tw-w-1/3",
|
|
||||||
sortable: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "loginStatus",
|
|
||||||
title: this.i18nService.t("loginStatus"),
|
|
||||||
headerClass: "tw-w-1/3",
|
|
||||||
sortable: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "firstLogin",
|
|
||||||
title: this.i18nService.t("firstLogin"),
|
|
||||||
headerClass: "tw-w-1/3",
|
|
||||||
sortable: true,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the icon for a device type
|
|
||||||
* @param type - The device type
|
|
||||||
* @returns The icon for the device type
|
|
||||||
*/
|
|
||||||
getDeviceIcon(type: DeviceType): string {
|
|
||||||
const defaultIcon = "bwi bwi-desktop";
|
|
||||||
const categoryIconMap: Record<string, string> = {
|
|
||||||
webVault: "bwi bwi-browser",
|
|
||||||
desktop: "bwi bwi-desktop",
|
|
||||||
mobile: "bwi bwi-mobile",
|
|
||||||
cli: "bwi bwi-cli",
|
|
||||||
extension: "bwi bwi-puzzle",
|
|
||||||
sdk: "bwi bwi-desktop",
|
|
||||||
};
|
|
||||||
|
|
||||||
const metadata = DeviceTypeMetadata[type];
|
|
||||||
return metadata ? (categoryIconMap[metadata.category] ?? defaultIcon) : defaultIcon;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the login status of a device
|
|
||||||
* It will return the current session if the device is the current device
|
|
||||||
* It will return the date of the pending auth request when available
|
|
||||||
* @param device - The device
|
|
||||||
* @returns The login status
|
|
||||||
*/
|
|
||||||
private getLoginStatus(device: DeviceView): string {
|
|
||||||
if (this.isCurrentDevice(device)) {
|
|
||||||
return this.i18nService.t("currentSession");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (device?.response?.devicePendingAuthRequest?.creationDate) {
|
|
||||||
return this.i18nService.t("requestPending");
|
|
||||||
}
|
|
||||||
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get a human readable device type from the DeviceType enum
|
|
||||||
* @param type - The device type
|
|
||||||
* @returns The human readable device type
|
|
||||||
*/
|
|
||||||
private getHumanReadableDeviceType(type: DeviceType): string {
|
|
||||||
const metadata = DeviceTypeMetadata[type];
|
|
||||||
if (!metadata) {
|
|
||||||
return this.i18nService.t("unknownDevice");
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the platform is "Unknown" translate it since it is not a proper noun
|
|
||||||
const platform =
|
|
||||||
metadata.platform === "Unknown" ? this.i18nService.t("unknown") : metadata.platform;
|
|
||||||
const category = this.i18nService.t(metadata.category);
|
|
||||||
return platform ? `${category} - ${platform}` : category;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if a device is the current device
|
|
||||||
* @param device - The device or device table data
|
|
||||||
* @returns True if the device is the current device, false otherwise
|
|
||||||
*/
|
|
||||||
protected isCurrentDevice(device: DeviceView | DeviceTableData): boolean {
|
|
||||||
return "response" in device
|
|
||||||
? device.id === this.currentDevice?.id
|
|
||||||
: device.id === this.currentDevice?.id;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if a device has a pending auth request
|
|
||||||
* @param device - The device response
|
|
||||||
* @returns True if the device has a pending auth request, false otherwise
|
|
||||||
*/
|
|
||||||
private hasPendingAuthRequest(device: DeviceResponse): boolean {
|
|
||||||
return (
|
|
||||||
device.devicePendingAuthRequest !== undefined && device.devicePendingAuthRequest !== null
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Open a dialog to approve or deny a pending auth request for a device
|
|
||||||
*/
|
|
||||||
async managePendingAuthRequest(device: DeviceTableData) {
|
|
||||||
if (device.devicePendingAuthRequest === undefined || device.devicePendingAuthRequest === null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const dialogRef = LoginApprovalDialogComponent.open(this.dialogService, {
|
|
||||||
notificationId: device.devicePendingAuthRequest.id,
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await firstValueFrom(dialogRef.closed);
|
|
||||||
|
|
||||||
if (result !== undefined && typeof result === "boolean") {
|
|
||||||
// auth request approved or denied so reset
|
|
||||||
device.devicePendingAuthRequest = null;
|
|
||||||
device.hasPendingAuthRequest = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Remove a device
|
|
||||||
* @param device - The device
|
|
||||||
*/
|
|
||||||
protected async removeDevice(device: DeviceTableData) {
|
|
||||||
const confirmed = await this.dialogService.openSimpleDialog({
|
|
||||||
title: { key: "removeDevice" },
|
|
||||||
content: { key: "removeDeviceConfirmation" },
|
|
||||||
type: "warning",
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!confirmed) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
this.asyncActionLoading = true;
|
|
||||||
await firstValueFrom(this.devicesService.deactivateDevice$(device.id));
|
|
||||||
this.asyncActionLoading = false;
|
|
||||||
|
|
||||||
// Remove the device from the data source
|
|
||||||
this.dataSource.data = this.dataSource.data.filter((d) => d.id !== device.id);
|
|
||||||
|
|
||||||
this.toastService.showToast({
|
|
||||||
title: "",
|
|
||||||
message: this.i18nService.t("deviceRemoved"),
|
|
||||||
variant: "success",
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
this.validationService.showError(error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -2,12 +2,9 @@ import { NgModule } from "@angular/core";
|
|||||||
import { RouterModule, Routes } from "@angular/router";
|
import { RouterModule, Routes } from "@angular/router";
|
||||||
|
|
||||||
import { DeviceManagementComponent } from "@bitwarden/angular/auth/device-management/device-management.component";
|
import { DeviceManagementComponent } from "@bitwarden/angular/auth/device-management/device-management.component";
|
||||||
import { featureFlaggedRoute } from "@bitwarden/angular/platform/utils/feature-flagged-route";
|
|
||||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
|
||||||
|
|
||||||
import { TwoFactorSetupComponent } from "../two-factor/two-factor-setup.component";
|
import { TwoFactorSetupComponent } from "../two-factor/two-factor-setup.component";
|
||||||
|
|
||||||
import { DeviceManagementOldComponent } from "./device-management-old.component";
|
|
||||||
import { PasswordSettingsComponent } from "./password-settings/password-settings.component";
|
import { PasswordSettingsComponent } from "./password-settings/password-settings.component";
|
||||||
import { SecurityKeysComponent } from "./security-keys.component";
|
import { SecurityKeysComponent } from "./security-keys.component";
|
||||||
import { SecurityComponent } from "./security.component";
|
import { SecurityComponent } from "./security.component";
|
||||||
@@ -34,15 +31,11 @@ const routes: Routes = [
|
|||||||
component: SecurityKeysComponent,
|
component: SecurityKeysComponent,
|
||||||
data: { titleId: "keys" },
|
data: { titleId: "keys" },
|
||||||
},
|
},
|
||||||
...featureFlaggedRoute({
|
{
|
||||||
defaultComponent: DeviceManagementOldComponent,
|
|
||||||
flaggedComponent: DeviceManagementComponent,
|
|
||||||
featureFlag: FeatureFlag.PM14938_BrowserExtensionLoginApproval,
|
|
||||||
routeOptions: {
|
|
||||||
path: "device-management",
|
path: "device-management",
|
||||||
|
component: DeviceManagementComponent,
|
||||||
data: { titleId: "devices" },
|
data: { titleId: "devices" },
|
||||||
},
|
},
|
||||||
}),
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
Reference in New Issue
Block a user