Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weโ€™ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[PM-1214] device management screen #12455

Merged
merged 43 commits into from
Jan 7, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
79327d8
Add basic device management screen
alec-livefront Dec 7, 2024
d4c0091
Add table styling
alec-livefront Dec 9, 2024
efda9ae
Add options button
alec-livefront Dec 9, 2024
a4d1740
Merge branch 'main' into auth/pm-1214/device-management-screen
alec-livefront Dec 16, 2024
dc2596e
Fix bad merge
alec-livefront Dec 16, 2024
4c49954
Merge branch 'main' into auth/pm-1214/device-management-screen
alec-livefront Dec 17, 2024
5eb9955
Fix icons, trusted status, login status and last login
alec-livefront Dec 17, 2024
99f6af0
Add sorting and table description.
alec-livefront Dec 17, 2024
f1a59e9
Add table button functionality
alec-livefront Dec 17, 2024
ebddea0
Add device deactivation
alec-livefront Dec 17, 2024
b078c74
Add strict typing and other cleanup
alec-livefront Dec 17, 2024
660aba2
Type fixes and message update
alec-livefront Dec 17, 2024
80760c8
Update login status column with pending auth request
alec-livefront Dec 18, 2024
278fa9b
Update table styling
alec-livefront Dec 18, 2024
eef346a
Merge branch 'main' into auth/pm-1214/device-management-screen
alec-livefront Dec 18, 2024
74363fb
Merge branch 'main' into auth/pm-1214/device-management-screen
alec-livefront Dec 18, 2024
bcd847d
Add virtual scroll to table
alec-livefront Dec 19, 2024
0561cd8
Merge branch 'main' into auth/pm-1214/device-management-screen
alec-livefront Dec 19, 2024
f336afc
Remove approve device and log out menu options
alec-livefront Dec 19, 2024
82dbc98
Merge branch 'auth/pm-1214/device-management-screen' of https://githuโ€ฆ
alec-livefront Dec 19, 2024
d995fe5
Remove device when successfully deactivated
alec-livefront Dec 19, 2024
3eb0ef7
Update row size
alec-livefront Dec 19, 2024
c3c73cc
Add service tests
alec-livefront Dec 19, 2024
6ca912a
Merge branch 'main' into auth/pm-1214/device-management-screen
alec-livefront Dec 20, 2024
99a647c
Move getCurrentDevice$ to DevicesService
alec-livefront Dec 20, 2024
d01ca4d
Update "Remove" to "Remove device"
alec-livefront Dec 20, 2024
0932e18
Add loading spinners
alec-livefront Dec 20, 2024
3934101
Use ValidationService to handle error
alec-livefront Dec 20, 2024
4bb0459
Fix platform translation in device management component to handle "Unโ€ฆ
alec-livefront Dec 23, 2024
c8dc459
Merge branch 'main' into auth/pm-1214/device-management-screen
alec-livefront Dec 23, 2024
66e497c
Add DeviceManagement feature flag
alec-livefront Dec 26, 2024
59cae5b
Merge branch 'main' into auth/pm-1214/device-management-screen
alec-livefront Dec 27, 2024
6e4a873
Merge branch 'main' into auth/pm-1214/device-management-screen
JaredSnider-Bitwarden Dec 30, 2024
3bebfab
Clean up device management header to look more like other tabs
alec-livefront Dec 30, 2024
fa51b93
Fix default column sort
alec-livefront Dec 30, 2024
f9f048a
Merge branch 'main' into auth/pm-1214/device-management-screen
alec-livefront Dec 30, 2024
287e9e6
Merge branch 'main' into auth/pm-1214/device-management-screen
alec-livefront Dec 31, 2024
8c79141
Merge branch 'main' into auth/pm-1214/device-management-screen
alec-livefront Jan 1, 2025
ea2a96d
Merge branch 'main' into auth/pm-1214/device-management-screen
alec-livefront Jan 2, 2025
3ed12c6
Remove DeviceManagement feature flag
alec-livefront Jan 2, 2025
f017b27
Add popover content
alec-livefront Jan 3, 2025
78ac02d
Merge branch 'main' into auth/pm-1214/device-management-screen
alec-livefront Jan 3, 2025
ca0cbf6
Merge branch 'main' into auth/pm-1214/device-management-screen
alec-livefront Jan 4, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion apps/browser/src/background/main.background.ts
Original file line number Diff line number Diff line change
Expand Up @@ -770,7 +770,10 @@
this.configService,
);

this.devicesService = new DevicesServiceImplementation(this.devicesApiService);
this.devicesService = new DevicesServiceImplementation(

Check warning on line 773 in apps/browser/src/background/main.background.ts

View check run for this annotation

Codecov / codecov/patch

apps/browser/src/background/main.background.ts#L773

Added line #L773 was not covered by tests
this.devicesApiService,
this.appIdService,
);

this.authRequestService = new AuthRequestService(
this.appIdService,
Expand Down
JaredSnider-Bitwarden marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
<bit-container>
<div class="tabbed-header">
<div class="tw-flex tw-items-center tw-gap-2">
<h1>{{ "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-h-4 tw-w-4"
[position]="'right-start'"
>
<i class="bwi bwi-question-circle" aria-hidden="true"></i>
</button>
<bit-popover [title]="'whatIsADevice' | i18n" #infoPopover>
<p>{{ "aDeviceIs" | i18n }}</p>
</bit-popover>
<i
*ngIf="asyncActionLoading"
class="bwi bwi-spinner bwi-spin tw-flex tw-items-center tw-h-4 tw-w-4"
aria-hidden="true"
></i>
</div>
</div>

<p>{{ "deviceListDescription" | 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"
JaredSnider-Bitwarden marked this conversation as resolved.
Show resolved Hide resolved
[default]="col.name === 'loginStatus' ? 'desc' : null"
scope="col"
role="columnheader"
>
{{ col.title }}
</th>
<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>
{{ row.displayName }}
<span *ngIf="row.trusted" class="tw-text-sm tw-text-muted tw-block">
{{ "trusted" | i18n }}
</span>
</div>
</td>
<td bitCell>
<span *ngIf="isCurrentDevice(row)" bitBadge variant="primary">{{
"currentSession" | i18n
}}</span>
<span *ngIf="hasPendingAuthRequest(row)" 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>
<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>
JaredSnider-Bitwarden marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,220 @@
import { CommonModule } from "@angular/common";
import { Component } from "@angular/core";
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
import { firstValueFrom } from "rxjs";
import { switchMap } from "rxjs/operators";

Check warning on line 5 in apps/web/src/app/auth/settings/security/device-management.component.ts

View check run for this annotation

Codecov / codecov/patch

apps/web/src/app/auth/settings/security/device-management.component.ts#L1-L5

Added lines #L1 - L5 were not covered by tests

import { DevicesServiceAbstraction } from "@bitwarden/common/auth/abstractions/devices/devices.service.abstraction";
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 {

Check warning on line 12 in apps/web/src/app/auth/settings/security/device-management.component.ts

View check run for this annotation

Codecov / codecov/patch

apps/web/src/app/auth/settings/security/device-management.component.ts#L7-L12

Added lines #L7 - L12 were not covered by tests
DialogService,
ToastService,
TableDataSource,
TableModule,
PopoverModule,
} from "@bitwarden/components";

import { SharedModule } from "../../../shared";

Check warning on line 20 in apps/web/src/app/auth/settings/security/device-management.component.ts

View check run for this annotation

Codecov / codecov/patch

apps/web/src/app/auth/settings/security/device-management.component.ts#L20

Added line #L20 was not covered by tests

interface DeviceTableData {
id: string;
type: DeviceType;
displayName: string;
loginStatus: string;
firstLogin: Date;
trusted: boolean;
devicePendingAuthRequest: object | null;
}

/**
* 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.component.html",
standalone: true,
imports: [CommonModule, SharedModule, TableModule, PopoverModule],
})
export class DeviceManagementComponent {
protected readonly tableId = "device-management-table";
protected dataSource = new TableDataSource<DeviceTableData>();

Check warning on line 43 in apps/web/src/app/auth/settings/security/device-management.component.ts

View check run for this annotation

Codecov / codecov/patch

apps/web/src/app/auth/settings/security/device-management.component.ts#L41-L43

Added lines #L41 - L43 were not covered by tests
protected currentDevice: DeviceView | undefined;
protected loading = true;
protected asyncActionLoading = false;

Check warning on line 46 in apps/web/src/app/auth/settings/security/device-management.component.ts

View check run for this annotation

Codecov / codecov/patch

apps/web/src/app/auth/settings/security/device-management.component.ts#L45-L46

Added lines #L45 - L46 were not covered by tests

constructor(
private i18nService: I18nService,
private devicesService: DevicesServiceAbstraction,
private dialogService: DialogService,
private toastService: ToastService,
private validationService: ValidationService,

Check warning on line 53 in apps/web/src/app/auth/settings/security/device-management.component.ts

View check run for this annotation

Codecov / codecov/patch

apps/web/src/app/auth/settings/security/device-management.component.ts#L53

Added line #L53 was not covered by tests
) {
this.devicesService

Check warning on line 55 in apps/web/src/app/auth/settings/security/device-management.component.ts

View check run for this annotation

Codecov / codecov/patch

apps/web/src/app/auth/settings/security/device-management.component.ts#L55

Added line #L55 was not covered by tests
.getCurrentDevice$()
.pipe(
takeUntilDestroyed(),
switchMap((currentDevice) => {
this.currentDevice = new DeviceView(currentDevice);
return this.devicesService.getDevices$();

Check warning on line 61 in apps/web/src/app/auth/settings/security/device-management.component.ts

View check run for this annotation

Codecov / codecov/patch

apps/web/src/app/auth/settings/security/device-management.component.ts#L60-L61

Added lines #L60 - L61 were not covered by tests
}),
)
.subscribe({
next: (devices) => {
this.dataSource.data = devices.map((device) => {
return {

Check warning on line 67 in apps/web/src/app/auth/settings/security/device-management.component.ts

View check run for this annotation

Codecov / codecov/patch

apps/web/src/app/auth/settings/security/device-management.component.ts#L66-L67

Added lines #L66 - L67 were not covered by tests
id: device.id,
type: device.type,
displayName: this.getHumanReadableDeviceType(device.type),
loginStatus: this.getLoginStatus(device),
devicePendingAuthRequest: device.response.devicePendingAuthRequest,
firstLogin: new Date(device.creationDate),
trusted: device.response.isTrusted,
};
});
this.loading = false;

Check warning on line 77 in apps/web/src/app/auth/settings/security/device-management.component.ts

View check run for this annotation

Codecov / codecov/patch

apps/web/src/app/auth/settings/security/device-management.component.ts#L77

Added line #L77 was not covered by tests
},
error: () => {
this.loading = false;

Check warning on line 80 in apps/web/src/app/auth/settings/security/device-management.component.ts

View check run for this annotation

Codecov / codecov/patch

apps/web/src/app/auth/settings/security/device-management.component.ts#L80

Added line #L80 was not covered by tests
},
});
}

/**
* Column configuration for the table
*/
protected readonly columnConfig = [

Check warning on line 88 in apps/web/src/app/auth/settings/security/device-management.component.ts

View check run for this annotation

Codecov / codecov/patch

apps/web/src/app/auth/settings/security/device-management.component.ts#L88

Added line #L88 was not covered by tests
{
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> = {

Check warning on line 116 in apps/web/src/app/auth/settings/security/device-management.component.ts

View check run for this annotation

Codecov / codecov/patch

apps/web/src/app/auth/settings/security/device-management.component.ts#L115-L116

Added lines #L115 - L116 were not covered by tests
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];

Check warning on line 125 in apps/web/src/app/auth/settings/security/device-management.component.ts

View check run for this annotation

Codecov / codecov/patch

apps/web/src/app/auth/settings/security/device-management.component.ts#L125

Added line #L125 was not covered by tests
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");

Check warning on line 138 in apps/web/src/app/auth/settings/security/device-management.component.ts

View check run for this annotation

Codecov / codecov/patch

apps/web/src/app/auth/settings/security/device-management.component.ts#L138

Added line #L138 was not covered by tests
}

if (device.response.devicePendingAuthRequest?.creationDate) {
return this.i18nService.t("requestPending");

Check warning on line 142 in apps/web/src/app/auth/settings/security/device-management.component.ts

View check run for this annotation

Codecov / codecov/patch

apps/web/src/app/auth/settings/security/device-management.component.ts#L142

Added line #L142 was not covered by tests
}

return "";

Check warning on line 145 in apps/web/src/app/auth/settings/security/device-management.component.ts

View check run for this annotation

Codecov / codecov/patch

apps/web/src/app/auth/settings/security/device-management.component.ts#L145

Added line #L145 was not covered by tests
}

/**
* 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];

Check warning on line 154 in apps/web/src/app/auth/settings/security/device-management.component.ts

View check run for this annotation

Codecov / codecov/patch

apps/web/src/app/auth/settings/security/device-management.component.ts#L154

Added line #L154 was not covered by tests
if (!metadata) {
return this.i18nService.t("unknownDevice");

Check warning on line 156 in apps/web/src/app/auth/settings/security/device-management.component.ts

View check run for this annotation

Codecov / codecov/patch

apps/web/src/app/auth/settings/security/device-management.component.ts#L156

Added line #L156 was not covered by tests
}

// 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);

Check warning on line 162 in apps/web/src/app/auth/settings/security/device-management.component.ts

View check run for this annotation

Codecov / codecov/patch

apps/web/src/app/auth/settings/security/device-management.component.ts#L162

Added line #L162 was not covered by tests
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
* @returns True if the device has a pending auth request, false otherwise
*/
protected hasPendingAuthRequest(device: DeviceTableData): boolean {
return (

Check warning on line 183 in apps/web/src/app/auth/settings/security/device-management.component.ts

View check run for this annotation

Codecov / codecov/patch

apps/web/src/app/auth/settings/security/device-management.component.ts#L183

Added line #L183 was not covered by tests
device.devicePendingAuthRequest !== undefined && device.devicePendingAuthRequest !== null
);
}

/**
* Remove a device
* @param device - The device
*/
protected async removeDevice(device: DeviceTableData) {
const confirmed = await this.dialogService.openSimpleDialog({

Check warning on line 193 in apps/web/src/app/auth/settings/security/device-management.component.ts

View check run for this annotation

Codecov / codecov/patch

apps/web/src/app/auth/settings/security/device-management.component.ts#L193

Added line #L193 was not covered by tests
title: { key: "removeDevice" },
content: { key: "removeDeviceConfirmation" },
type: "warning",
});

if (!confirmed) {
return;

Check warning on line 200 in apps/web/src/app/auth/settings/security/device-management.component.ts

View check run for this annotation

Codecov / codecov/patch

apps/web/src/app/auth/settings/security/device-management.component.ts#L200

Added line #L200 was not covered by tests
}

try {
this.asyncActionLoading = true;
await firstValueFrom(this.devicesService.deactivateDevice$(device.id));
this.asyncActionLoading = false;

Check warning on line 206 in apps/web/src/app/auth/settings/security/device-management.component.ts

View check run for this annotation

Codecov / codecov/patch

apps/web/src/app/auth/settings/security/device-management.component.ts#L203-L206

Added lines #L203 - L206 were not covered by tests

// Remove the device from the data source
this.dataSource.data = this.dataSource.data.filter((d) => d.id !== device.id);

Check warning on line 209 in apps/web/src/app/auth/settings/security/device-management.component.ts

View check run for this annotation

Codecov / codecov/patch

apps/web/src/app/auth/settings/security/device-management.component.ts#L209

Added line #L209 was not covered by tests

this.toastService.showToast({

Check warning on line 211 in apps/web/src/app/auth/settings/security/device-management.component.ts

View check run for this annotation

Codecov / codecov/patch

apps/web/src/app/auth/settings/security/device-management.component.ts#L211

Added line #L211 was not covered by tests
title: "",
message: this.i18nService.t("deviceRemoved"),
variant: "success",
});
} catch (error) {
this.validationService.showError(error);

Check warning on line 217 in apps/web/src/app/auth/settings/security/device-management.component.ts

View check run for this annotation

Codecov / codecov/patch

apps/web/src/app/auth/settings/security/device-management.component.ts#L217

Added line #L217 was not covered by tests
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import { ChangePasswordComponent } from "../change-password.component";
import { TwoFactorSetupComponent } from "../two-factor/two-factor-setup.component";

import { DeviceManagementComponent } from "./device-management.component";

Check warning on line 7 in apps/web/src/app/auth/settings/security/security-routing.module.ts

View check run for this annotation

Codecov / codecov/patch

apps/web/src/app/auth/settings/security/security-routing.module.ts#L7

Added line #L7 was not covered by tests
import { SecurityKeysComponent } from "./security-keys.component";
import { SecurityComponent } from "./security.component";

Expand All @@ -29,6 +30,11 @@
component: SecurityKeysComponent,
data: { titleId: "keys" },
},
{
path: "device-management",
component: DeviceManagementComponent,
data: { titleId: "devices" },
},
JaredSnider-Bitwarden marked this conversation as resolved.
Show resolved Hide resolved
],
},
];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
<bit-tab-link route="change-password">{{ "masterPassword" | i18n }}</bit-tab-link>
</ng-container>
<bit-tab-link route="two-factor">{{ "twoStepLogin" | i18n }}</bit-tab-link>
<bit-tab-link route="device-management">{{ "devices" | i18n }}</bit-tab-link>
<bit-tab-link route="security-keys">{{ "keys" | i18n }}</bit-tab-link>
</bit-tab-nav-bar>
</app-header>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Component, OnInit } from "@angular/core";

import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";

@Component({
selector: "app-security",
Expand All @@ -9,7 +10,10 @@
export class SecurityComponent implements OnInit {
showChangePassword = true;

constructor(private userVerificationService: UserVerificationService) {}
constructor(
private userVerificationService: UserVerificationService,
private configService: ConfigService,

Check warning on line 15 in apps/web/src/app/auth/settings/security/security.component.ts

View check run for this annotation

Codecov / codecov/patch

apps/web/src/app/auth/settings/security/security.component.ts#L15

Added line #L15 was not covered by tests
) {}

async ngOnInit() {
this.showChangePassword = await this.userVerificationService.hasMasterPassword();
Expand Down
Loading
Loading