From 79327d8afb26d256adef79d17b22770afcce1d15 Mon Sep 17 00:00:00 2001 From: Alec Rippberger Date: Fri, 6 Dec 2024 21:33:00 -0600 Subject: [PATCH 01/25] Add basic device management screen --- .../security/device-management.component.html | 34 +++++++++++++++++++ .../security/device-management.component.ts | 25 ++++++++++++++ .../security/security-routing.module.ts | 6 ++++ .../settings/security/security.component.html | 1 + .../src/app/shared/loose-components.module.ts | 2 ++ apps/web/src/locales/en/messages.json | 3 ++ 6 files changed, 71 insertions(+) create mode 100644 apps/web/src/app/auth/settings/security/device-management.component.html create mode 100644 apps/web/src/app/auth/settings/security/device-management.component.ts diff --git a/apps/web/src/app/auth/settings/security/device-management.component.html b/apps/web/src/app/auth/settings/security/device-management.component.html new file mode 100644 index 00000000000..e6cdae6029d --- /dev/null +++ b/apps/web/src/app/auth/settings/security/device-management.component.html @@ -0,0 +1,34 @@ +
+

{{ "devices" | i18n }}

+

test1

+ + + + {{ "deviceType" | i18n }} + {{ "deviceName" | i18n }} + {{ "lastActive" | i18n }} + {{ "action" | i18n }} + + + + + + + {{ "desktop" | i18n }} + + Windows 10 Chrome + {{ "now" | i18n }} + + + + + + + + + +
diff --git a/apps/web/src/app/auth/settings/security/device-management.component.ts b/apps/web/src/app/auth/settings/security/device-management.component.ts new file mode 100644 index 00000000000..315e18f5b60 --- /dev/null +++ b/apps/web/src/app/auth/settings/security/device-management.component.ts @@ -0,0 +1,25 @@ +import { Component, OnInit } from "@angular/core"; + +import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { DialogService } from "@bitwarden/components"; + +@Component({ + selector: "app-device-management", + templateUrl: "./device-management.component.html", +}) +export class DeviceManagementComponent implements OnInit { + constructor( + private accountService: AccountService, + private apiService: ApiService, + private dialogService: DialogService, + ) {} + + async ngOnInit() { + // TODO: Load devices + } + + async revokeDevice() { + // TODO: Implement device revocation using dialogService for confirmation + } +} diff --git a/apps/web/src/app/auth/settings/security/security-routing.module.ts b/apps/web/src/app/auth/settings/security/security-routing.module.ts index 8af0499d05a..6ed21605184 100644 --- a/apps/web/src/app/auth/settings/security/security-routing.module.ts +++ b/apps/web/src/app/auth/settings/security/security-routing.module.ts @@ -4,6 +4,7 @@ import { RouterModule, Routes } from "@angular/router"; import { ChangePasswordComponent } from "../change-password.component"; import { TwoFactorSetupComponent } from "../two-factor/two-factor-setup.component"; +import { DeviceManagementComponent } from "./device-management.component"; import { SecurityKeysComponent } from "./security-keys.component"; import { SecurityComponent } from "./security.component"; @@ -29,6 +30,11 @@ const routes: Routes = [ component: SecurityKeysComponent, data: { titleId: "keys" }, }, + { + path: "device-management", + component: DeviceManagementComponent, + data: { titleId: "devices" }, + }, ], }, ]; diff --git a/apps/web/src/app/auth/settings/security/security.component.html b/apps/web/src/app/auth/settings/security/security.component.html index 25459faeacc..6bd7c1daf36 100644 --- a/apps/web/src/app/auth/settings/security/security.component.html +++ b/apps/web/src/app/auth/settings/security/security.component.html @@ -4,6 +4,7 @@ {{ "masterPassword" | i18n }} {{ "twoStepLogin" | i18n }} + {{ "devices" | i18n }} {{ "keys" | i18n }} diff --git a/apps/web/src/app/shared/loose-components.module.ts b/apps/web/src/app/shared/loose-components.module.ts index 15f15e2e317..a7d34424a96 100644 --- a/apps/web/src/app/shared/loose-components.module.ts +++ b/apps/web/src/app/shared/loose-components.module.ts @@ -39,6 +39,7 @@ import { EmergencyAccessViewComponent } from "../auth/settings/emergency-access/ import { EmergencyAddEditCipherComponent } from "../auth/settings/emergency-access/view/emergency-add-edit-cipher.component"; import { ApiKeyComponent } from "../auth/settings/security/api-key.component"; import { ChangeKdfModule } from "../auth/settings/security/change-kdf/change-kdf.module"; +import { DeviceManagementComponent } from "../auth/settings/security/device-management.component"; import { SecurityKeysComponent } from "../auth/settings/security/security-keys.component"; import { SecurityComponent } from "../auth/settings/security/security.component"; import { TwoFactorRecoveryComponent } from "../auth/settings/two-factor/two-factor-recovery.component"; @@ -121,6 +122,7 @@ import { SharedModule } from "./shared.module"; ChangeEmailComponent, DeauthorizeSessionsComponent, DeleteAccountDialogComponent, + DeviceManagementComponent, DomainRulesComponent, EmergencyAccessAddEditComponent, EmergencyAccessAttachmentsComponent, diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index 163c63a6d24..b11b1e1d217 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -9902,5 +9902,8 @@ }, "removeMembers": { "message": "Remove members" + }, + "devices": { + "message": "Devices" } } From d4c00910c8e01e1bc4767643829180a7bc7fd44b Mon Sep 17 00:00:00 2001 From: Alec Rippberger Date: Sun, 8 Dec 2024 21:12:46 -0600 Subject: [PATCH 02/25] Add table styling --- .../security/device-management.component.html | 53 ++++++----- .../security/device-management.component.ts | 94 ++++++++++++++++--- .../src/app/shared/loose-components.module.ts | 2 - apps/web/src/locales/en/messages.json | 9 ++ 4 files changed, 121 insertions(+), 37 deletions(-) diff --git a/apps/web/src/app/auth/settings/security/device-management.component.html b/apps/web/src/app/auth/settings/security/device-management.component.html index e6cdae6029d..8b4e1ca192d 100644 --- a/apps/web/src/app/auth/settings/security/device-management.component.html +++ b/apps/web/src/app/auth/settings/security/device-management.component.html @@ -1,33 +1,42 @@
-

{{ "devices" | i18n }}

-

test1

- +

+ {{ "devices" | i18n }} +

+

{{ "deviceListDescription" | i18n }}

+ + - {{ "deviceType" | i18n }} - {{ "deviceName" | i18n }} - {{ "lastActive" | i18n }} - {{ "action" | i18n }} + + {{ col.title }} + - - - - {{ "desktop" | i18n }} + + +
+ +
+ +
+ {{ device.deviceName }} + + {{ "trusted" | i18n }} + +
- Windows 10 Chrome - {{ "now" | i18n }} - - - - - + {{ device.loginStatus }} + + {{ device.loginStatus }} test + {{ device.firstLogin | date }}
diff --git a/apps/web/src/app/auth/settings/security/device-management.component.ts b/apps/web/src/app/auth/settings/security/device-management.component.ts index 315e18f5b60..c04155fd150 100644 --- a/apps/web/src/app/auth/settings/security/device-management.component.ts +++ b/apps/web/src/app/auth/settings/security/device-management.component.ts @@ -1,25 +1,93 @@ +import { CommonModule } from "@angular/common"; import { Component, OnInit } from "@angular/core"; -import { ApiService } from "@bitwarden/common/abstractions/api.service"; -import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; -import { DialogService } from "@bitwarden/components"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { TableDataSource, TableModule } from "@bitwarden/components"; + +import { SharedModule } from "../../../shared"; + +interface Device { + type: string; + deviceName: string; + loginStatus: string; + firstLogin: Date; + trusted: boolean; +} @Component({ selector: "app-device-management", templateUrl: "./device-management.component.html", + standalone: true, + imports: [CommonModule, SharedModule, TableModule], }) export class DeviceManagementComponent implements OnInit { - constructor( - private accountService: AccountService, - private apiService: ApiService, - private dialogService: DialogService, - ) {} - - async ngOnInit() { - // TODO: Load devices + protected readonly tableId = "device-management-table"; + protected dataSource = new TableDataSource(); + + constructor(private i18nService: I18nService) {} + + protected readonly columnConfig = [ + { + name: "deviceName", + title: this.i18nService.t("device"), + headerClass: "tw-w-1/3", + }, + { + name: "loginStatus", + title: this.i18nService.t("loginStatus"), + headerClass: "tw-w-1/4", + }, + { + name: "firstLogin", + title: this.i18nService.t("firstLogin"), + headerClass: "tw-w-1/4", + }, + ]; + + currentDeviceId = "device-1"; + + devices: Device[] = [ + { + type: "browser", + deviceName: "Web app - Chrome", + loginStatus: "Current session", + firstLogin: new Date("2026-09-20T09:25:54"), + trusted: true, + }, + { + type: "extension", + deviceName: "Extension - Chrome", + loginStatus: "This week", + firstLogin: new Date("2026-09-20T09:25:54"), + trusted: true, + }, + { + type: "cli", + deviceName: "CLI - macOS", + loginStatus: "This month", + firstLogin: new Date("2026-09-20T09:25:54"), + trusted: true, + }, + { + type: "mobile", + deviceName: "Mobile - iOS", + loginStatus: "", + firstLogin: new Date("2026-09-20T09:25:54"), + trusted: true, + }, + ]; + + ngOnInit() { + this.dataSource.data = this.devices; } - async revokeDevice() { - // TODO: Implement device revocation using dialogService for confirmation + getDeviceIcon(type: string): string { + const iconMap: { [key: string]: string } = { + browser: "bwi bwi-browser", + extension: "bwi bwi-puzzle", + cli: "bwi bwi-cli", + mobile: "bwi bwi-mobile", + }; + return iconMap[type] || "bwi bwi-device"; } } diff --git a/apps/web/src/app/shared/loose-components.module.ts b/apps/web/src/app/shared/loose-components.module.ts index a7d34424a96..15f15e2e317 100644 --- a/apps/web/src/app/shared/loose-components.module.ts +++ b/apps/web/src/app/shared/loose-components.module.ts @@ -39,7 +39,6 @@ import { EmergencyAccessViewComponent } from "../auth/settings/emergency-access/ import { EmergencyAddEditCipherComponent } from "../auth/settings/emergency-access/view/emergency-add-edit-cipher.component"; import { ApiKeyComponent } from "../auth/settings/security/api-key.component"; import { ChangeKdfModule } from "../auth/settings/security/change-kdf/change-kdf.module"; -import { DeviceManagementComponent } from "../auth/settings/security/device-management.component"; import { SecurityKeysComponent } from "../auth/settings/security/security-keys.component"; import { SecurityComponent } from "../auth/settings/security/security.component"; import { TwoFactorRecoveryComponent } from "../auth/settings/two-factor/two-factor-recovery.component"; @@ -122,7 +121,6 @@ import { SharedModule } from "./shared.module"; ChangeEmailComponent, DeauthorizeSessionsComponent, DeleteAccountDialogComponent, - DeviceManagementComponent, DomainRulesComponent, EmergencyAccessAddEditComponent, EmergencyAccessAttachmentsComponent, diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index b11b1e1d217..c3f8641f03b 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -3783,6 +3783,15 @@ "device": { "message": "Device" }, + "loginStatus": { + "message": "Login status" + }, + "firstLogin": { + "message": "First login" + }, + "trusted": { + "message": "Trusted" + }, "creatingAccountOn": { "message": "Creating account on" }, From efda9ae56eaedf0b11032b2f3c6460ae0947f543 Mon Sep 17 00:00:00 2001 From: Alec Rippberger Date: Mon, 9 Dec 2024 14:12:15 -0600 Subject: [PATCH 03/25] Add options button --- .../security/device-management.component.html | 19 ++++++++++++++++++- .../security/device-management.component.ts | 3 ++- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/apps/web/src/app/auth/settings/security/device-management.component.html b/apps/web/src/app/auth/settings/security/device-management.component.html index 8b4e1ca192d..14fed1d2aee 100644 --- a/apps/web/src/app/auth/settings/security/device-management.component.html +++ b/apps/web/src/app/auth/settings/security/device-management.component.html @@ -1,6 +1,7 @@

- {{ "devices" | i18n }} + {{ "devices" | i18n }} +

{{ "deviceListDescription" | i18n }}

@@ -13,9 +14,12 @@

bitCell scope="col" role="columnheader" + [bitSortable]="col.name" + default > {{ col.title }} + @@ -37,6 +41,19 @@

{{ device.loginStatus }} test {{ device.firstLogin | date }} + + + + Anchor link + Another link + + + + diff --git a/apps/web/src/app/auth/settings/security/device-management.component.ts b/apps/web/src/app/auth/settings/security/device-management.component.ts index c04155fd150..4828959168e 100644 --- a/apps/web/src/app/auth/settings/security/device-management.component.ts +++ b/apps/web/src/app/auth/settings/security/device-management.component.ts @@ -5,6 +5,7 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic import { TableDataSource, TableModule } from "@bitwarden/components"; import { SharedModule } from "../../../shared"; +import { TableScrollComponent } from "@bitwarden/components/src/table/table-scroll.component"; interface Device { type: string; @@ -35,7 +36,7 @@ export class DeviceManagementComponent implements OnInit { { name: "loginStatus", title: this.i18nService.t("loginStatus"), - headerClass: "tw-w-1/4", + headerClass: "tw-w-1/3", }, { name: "firstLogin", From dc2596ea8cfae04277036b8c4b9febaaff135bbe Mon Sep 17 00:00:00 2001 From: Alec Rippberger Date: Mon, 16 Dec 2024 10:58:02 -0600 Subject: [PATCH 04/25] Fix bad merge --- apps/web/src/locales/en/messages.json | 1 - 1 file changed, 1 deletion(-) diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index e21d512cf70..062cb28f907 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -9981,6 +9981,5 @@ }, "domainClaimed": { "message": "Domain claimed" ->>>>>>> main } } From 5eb99551eba7ecc64b4c8c5f03de5e176e3b5e11 Mon Sep 17 00:00:00 2001 From: Alec Rippberger Date: Mon, 16 Dec 2024 23:46:11 -0600 Subject: [PATCH 05/25] Fix icons, trusted status, login status and last login --- .../security/device-management.component.html | 12 +- .../security/device-management.component.ts | 152 ++++++++++++------ apps/web/src/locales/en/messages.json | 3 + .../device-trust.service.abstraction.ts | 4 + .../devices/responses/device.response.ts | 5 + .../abstractions/devices/views/device.view.ts | 1 + .../device-trust.service.implementation.ts | 9 +- libs/common/src/enums/device-type.enum.ts | 50 ++++-- 8 files changed, 167 insertions(+), 69 deletions(-) diff --git a/apps/web/src/app/auth/settings/security/device-management.component.html b/apps/web/src/app/auth/settings/security/device-management.component.html index 14fed1d2aee..d6282de49d4 100644 --- a/apps/web/src/app/auth/settings/security/device-management.component.html +++ b/apps/web/src/app/auth/settings/security/device-management.component.html @@ -14,7 +14,6 @@

bitCell scope="col" role="columnheader" - [bitSortable]="col.name" default > {{ col.title }} @@ -28,19 +27,18 @@

-
- {{ device.deviceName }} + {{ device.displayName }} {{ "trusted" | i18n }}
- {{ device.loginStatus }} - - {{ device.loginStatus }} test + {{ device.loginStatus }} + + {{ "currentSession" | i18n }} - {{ device.firstLogin | date }} + {{ device.firstLogin | date: "medium" }} - Anchor link - Another link - - + + + diff --git a/apps/web/src/app/auth/settings/security/device-management.component.ts b/apps/web/src/app/auth/settings/security/device-management.component.ts index 6cb5cf0b60d..d193b8a09fc 100644 --- a/apps/web/src/app/auth/settings/security/device-management.component.ts +++ b/apps/web/src/app/auth/settings/security/device-management.component.ts @@ -7,7 +7,7 @@ import { DevicesServiceAbstraction } from "@bitwarden/common/auth/abstractions/d 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 { TableDataSource, TableModule } from "@bitwarden/components"; +import { DialogService , ToastService , TableDataSource, TableModule } from "@bitwarden/components"; import { SharedModule } from "../../../shared"; @@ -35,6 +35,8 @@ export class DeviceManagementComponent implements OnInit { private i18nService: I18nService, private devicesService: DevicesServiceAbstraction, private deviceTrustService: DeviceTrustServiceAbstraction, + private dialogService: DialogService, + private toastService: ToastService, ) { // Get current device this.deviceTrustService @@ -152,4 +154,88 @@ export class DeviceManagementComponent implements OnInit { ? device.id === this.currentDevice?.id : device.id === this.currentDevice?.id; } + + protected async removeDevice(device: DeviceTableData) { + const confirmed = await this.dialogService.openSimpleDialog({ + title: { key: "removeDevice" }, + content: { key: "removeDeviceConfirmation" }, + type: "warning", + }); + + if (!confirmed) { + return; + } + + try { + // TODO: Implement actual device removal - https://bitwarden.atlassian.net/browse/PM-2133 + // await this.devicesService.removeDevice(device.id); + + this.toastService.showToast({ + title: "", + message: this.i18nService.t("deviceRemoved"), + variant: "success", + }); + } catch (e) { + this.toastService.showToast({ + title: "", + message: this.i18nService.t("errorOccurred"), + variant: "warning", + }); + } + } + + /** + * Log out a device + * @param device - The device + */ + protected async logOutDevice(device: DeviceTableData) { + const confirmed = await this.dialogService.openSimpleDialog({ + title: { key: "logOut" }, + content: { key: "logOutConfirmation" }, + type: "warning", + }); + + if (!confirmed) { + return; + } + try { + // TODO: Implement actual device approval + // await this.devicesService.approveDevice(device.id); + + this.toastService.showToast({ + title: "", + message: this.i18nService.t("deviceApproved"), + variant: "success", + }); + } catch (e) { + this.toastService.showToast({ + title: "", + message: this.i18nService.t("errorOccurred"), + variant: "warning", + }); + } + } + + /** + * Approve a device + * @param device - The device + */ + protected async approveDevice(device: DeviceTableData) { + try { + // TODO: Implement actual device approval + // await this.devicesService.approveDevice(device.id); + + this.toastService.showToast({ + title: "", + message: this.i18nService.t("deviceApproved"), + variant: "success", + }); + } catch (e) { + this.toastService.showToast({ + title: "", + message: this.i18nService.t("errorOccurred"), + variant: "warning", + }); + } + } } diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index 816655b1cad..a11afee684a 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -8245,6 +8245,15 @@ "approveRequest": { "message": "Approve request" }, + "deviceApproved": { + "message": "Device approved" + }, + "deviceRemoved": { + "message": "Device removed" + }, + "removeDeviceConfirmation": { + "message": "Are you sure you want to remove this device?" + }, "noDeviceRequests": { "message": "No device requests" }, From ebddea032c776d5fccc3fe407f1ea690bf2a9b9b Mon Sep 17 00:00:00 2001 From: Alec Rippberger Date: Tue, 17 Dec 2024 15:20:53 -0600 Subject: [PATCH 08/25] Add device deactivation --- .../security/device-management.component.html | 2 +- .../security/device-management.component.ts | 15 +++++++++------ .../devices-api.service.abstraction.ts | 6 ++++++ .../devices/devices.service.abstraction.ts | 13 ++++++------- .../devices-api.service.implementation.ts | 4 ++++ .../devices/devices.service.implementation.ts | 7 +++++++ 6 files changed, 33 insertions(+), 14 deletions(-) diff --git a/apps/web/src/app/auth/settings/security/device-management.component.html b/apps/web/src/app/auth/settings/security/device-management.component.html index b42da9a9540..d72f0cd7ee0 100644 --- a/apps/web/src/app/auth/settings/security/device-management.component.html +++ b/apps/web/src/app/auth/settings/security/device-management.component.html @@ -13,7 +13,7 @@

[class]="col.headerClass" bitCell [bitSortable]="col.sortable ? col.name : null" - [default]="col.name === 'displayName' ? 'asc' : null" + [default]="col.name === 'displayName' ? 'desc' : null" scope="col" role="columnheader" > diff --git a/apps/web/src/app/auth/settings/security/device-management.component.ts b/apps/web/src/app/auth/settings/security/device-management.component.ts index d193b8a09fc..57736465674 100644 --- a/apps/web/src/app/auth/settings/security/device-management.component.ts +++ b/apps/web/src/app/auth/settings/security/device-management.component.ts @@ -1,13 +1,14 @@ import { CommonModule } from "@angular/common"; import { Component, OnInit } from "@angular/core"; import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; +import { firstValueFrom } from "rxjs"; import { DeviceTrustServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust.service.abstraction"; 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 { DialogService , ToastService , TableDataSource, TableModule } from "@bitwarden/components"; +import { DialogService, ToastService, TableDataSource, TableModule } from "@bitwarden/components"; import { SharedModule } from "../../../shared"; @@ -20,6 +21,9 @@ interface DeviceTableData { trusted: boolean; } +/** + * 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", @@ -46,7 +50,7 @@ export class DeviceManagementComponent implements OnInit { this.currentDevice = new DeviceView(device); }); - // Get all devices and map them + // Get all devices and map them for the table this.devicesService .getDevices$() .pipe(takeUntilDestroyed()) @@ -167,8 +171,7 @@ export class DeviceManagementComponent implements OnInit { } try { - // TODO: Implement actual device removal - https://bitwarden.atlassian.net/browse/PM-2133 - // await this.devicesService.removeDevice(device.id); + await firstValueFrom(this.devicesService.deactivateDevice$(device.id)); this.toastService.showToast({ title: "", @@ -199,8 +202,8 @@ export class DeviceManagementComponent implements OnInit { return; } try { - // TODO: Implement actual device approval - // await this.devicesService.approveDevice(device.id); + // TODO: Implement actual device log out + // await this.devicesService.logOutDevice(device.id); this.toastService.showToast({ title: "", diff --git a/libs/common/src/auth/abstractions/devices-api.service.abstraction.ts b/libs/common/src/auth/abstractions/devices-api.service.abstraction.ts index 0af89928449..92f0ebf1667 100644 --- a/libs/common/src/auth/abstractions/devices-api.service.abstraction.ts +++ b/libs/common/src/auth/abstractions/devices-api.service.abstraction.ts @@ -36,4 +36,10 @@ export abstract class DevicesApiServiceAbstraction { * @param deviceIdentifier - current device identifier */ postDeviceTrustLoss: (deviceIdentifier: string) => Promise; + + /** + * Deactivates a device + * @param deviceId - The device ID + */ + deactivateDevice: (deviceId: string) => Promise; } diff --git a/libs/common/src/auth/abstractions/devices/devices.service.abstraction.ts b/libs/common/src/auth/abstractions/devices/devices.service.abstraction.ts index a02ccc64876..2439046fbc4 100644 --- a/libs/common/src/auth/abstractions/devices/devices.service.abstraction.ts +++ b/libs/common/src/auth/abstractions/devices/devices.service.abstraction.ts @@ -1,17 +1,16 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { Observable } from "rxjs"; import { DeviceView } from "./views/device.view"; export abstract class DevicesServiceAbstraction { - getDevices$: () => Observable>; - getDeviceByIdentifier$: (deviceIdentifier: string) => Observable; - isDeviceKnownForUser$: (email: string, deviceIdentifier: string) => Observable; - updateTrustedDeviceKeys$: ( + abstract getDevices$(): Observable>; + abstract getDeviceByIdentifier$(deviceIdentifier: string): Observable; + abstract isDeviceKnownForUser$(email: string, deviceIdentifier: string): Observable; + abstract updateTrustedDeviceKeys$( deviceIdentifier: string, devicePublicKeyEncryptedUserKey: string, userKeyEncryptedDevicePublicKey: string, deviceKeyEncryptedDevicePrivateKey: string, - ) => Observable; + ): Observable; + abstract deactivateDevice$(deviceId: string): Observable; } diff --git a/libs/common/src/auth/services/devices-api.service.implementation.ts b/libs/common/src/auth/services/devices-api.service.implementation.ts index 711f0bb68ec..cf760effbdf 100644 --- a/libs/common/src/auth/services/devices-api.service.implementation.ts +++ b/libs/common/src/auth/services/devices-api.service.implementation.ts @@ -117,4 +117,8 @@ export class DevicesApiServiceImplementation implements DevicesApiServiceAbstrac }, ); } + + async deactivateDevice(deviceId: string): Promise { + await this.apiService.send("POST", `/devices/${deviceId}/deactivate`, null, true, false); + } } diff --git a/libs/common/src/auth/services/devices/devices.service.implementation.ts b/libs/common/src/auth/services/devices/devices.service.implementation.ts index 6032ed66a89..052b377a0e2 100644 --- a/libs/common/src/auth/services/devices/devices.service.implementation.ts +++ b/libs/common/src/auth/services/devices/devices.service.implementation.ts @@ -65,4 +65,11 @@ export class DevicesServiceImplementation implements DevicesServiceAbstraction { ), ).pipe(map((deviceResponse: DeviceResponse) => new DeviceView(deviceResponse))); } + + /** + * @description Deactivates a device + */ + deactivateDevice$(deviceId: string): Observable { + return defer(() => this.devicesApiService.deactivateDevice(deviceId)); + } } From b078c74856c010f49b9b6b7c8207b84dcd2705ad Mon Sep 17 00:00:00 2001 From: Alec Rippberger Date: Tue, 17 Dec 2024 16:21:45 -0600 Subject: [PATCH 09/25] Add strict typing and other cleanup --- .../security/device-management.component.ts | 26 ++++++++++--------- .../request/update-devices-trust.request.ts | 4 +-- .../device-trust.service.implementation.ts | 16 ++++++------ 3 files changed, 24 insertions(+), 22 deletions(-) diff --git a/apps/web/src/app/auth/settings/security/device-management.component.ts b/apps/web/src/app/auth/settings/security/device-management.component.ts index 57736465674..376b5f536ae 100644 --- a/apps/web/src/app/auth/settings/security/device-management.component.ts +++ b/apps/web/src/app/auth/settings/security/device-management.component.ts @@ -1,5 +1,5 @@ import { CommonModule } from "@angular/common"; -import { Component, OnInit } from "@angular/core"; +import { Component } from "@angular/core"; import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; import { firstValueFrom } from "rxjs"; @@ -30,7 +30,7 @@ interface DeviceTableData { standalone: true, imports: [CommonModule, SharedModule, TableModule], }) -export class DeviceManagementComponent implements OnInit { +export class DeviceManagementComponent { protected readonly tableId = "device-management-table"; protected dataSource = new TableDataSource(); protected currentDevice: DeviceView | undefined; @@ -68,8 +68,9 @@ export class DeviceManagementComponent implements OnInit { }); } - ngOnInit(): void {} - + /** + * Column configuration for the table + */ protected readonly columnConfig = [ { name: "displayName", @@ -97,21 +98,18 @@ export class DeviceManagementComponent implements OnInit { * @returns The icon for the device type */ getDeviceIcon(type: DeviceType): string { - const metadata = DeviceTypeMetadata[type]; - if (!metadata) { - return "bwi bwi-device"; // fallback icon - } - // Map device categories to their corresponding icons - const categoryIconMap: { [key: string]: string } = { + const defaultIcon = "bwi bwi-desktop"; + const categoryIconMap: Record = { webVault: "bwi bwi-browser", desktop: "bwi bwi-desktop", mobile: "bwi bwi-mobile", cli: "bwi bwi-cli", extension: "bwi bwi-puzzle", - sdk: "bwi bwi-device", + sdk: "bwi bwi-desktop", }; - return categoryIconMap[metadata.category] || "bwi bwi-device"; + const metadata = DeviceTypeMetadata[type]; + return metadata ? (categoryIconMap[metadata.category] ?? defaultIcon) : defaultIcon; } /** @@ -159,6 +157,10 @@ export class DeviceManagementComponent implements OnInit { : device.id === this.currentDevice?.id; } + /** + * Remove a device + * @param device - The device + */ protected async removeDevice(device: DeviceTableData) { const confirmed = await this.dialogService.openSimpleDialog({ title: { key: "removeDevice" }, diff --git a/libs/common/src/auth/models/request/update-devices-trust.request.ts b/libs/common/src/auth/models/request/update-devices-trust.request.ts index 8e3ce86c1a9..21fe0f600dc 100644 --- a/libs/common/src/auth/models/request/update-devices-trust.request.ts +++ b/libs/common/src/auth/models/request/update-devices-trust.request.ts @@ -8,8 +8,8 @@ export class UpdateDevicesTrustRequest extends SecretVerificationRequest { } export class DeviceKeysUpdateRequest { - encryptedPublicKey: string; - encryptedUserKey: string; + encryptedPublicKey: string | undefined; + encryptedUserKey: string | undefined; } export class OtherDeviceKeysUpdateRequest extends DeviceKeysUpdateRequest { diff --git a/libs/common/src/auth/services/device-trust.service.implementation.ts b/libs/common/src/auth/services/device-trust.service.implementation.ts index cbad83dbdcf..3bdec71cadc 100644 --- a/libs/common/src/auth/services/device-trust.service.implementation.ts +++ b/libs/common/src/auth/services/device-trust.service.implementation.ts @@ -1,5 +1,3 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { firstValueFrom, map, Observable, defer } from "rxjs"; import { UserDecryptionOptionsServiceAbstraction } from "@bitwarden/auth/common"; @@ -159,15 +157,15 @@ export class DeviceTrustService implements DeviceTrustServiceAbstraction { const deviceIdentifier = await this.appIdService.getAppId(); const deviceResponse = await this.devicesApiService.updateTrustedDeviceKeys( deviceIdentifier, - devicePublicKeyEncryptedUserKey.encryptedString, - userKeyEncryptedDevicePublicKey.encryptedString, - deviceKeyEncryptedDevicePrivateKey.encryptedString, + devicePublicKeyEncryptedUserKey.encryptedString!, + userKeyEncryptedDevicePublicKey.encryptedString!, + deviceKeyEncryptedDevicePrivateKey.encryptedString!, ); // store device key in local/secure storage if enc keys posted to server successfully await this.setDeviceKey(userId, deviceKey); - this.platformUtilsService.showToast("success", null, this.i18nService.t("deviceTrusted")); + this.platformUtilsService.showToast("success", "", this.i18nService.t("deviceTrusted")); return deviceResponse; } @@ -266,6 +264,8 @@ export class DeviceTrustService implements DeviceTrustServiceAbstraction { } catch (e) { this.logService.error("Failed to get device key", e); } + + return null; } private async setDeviceKey(userId: UserId, deviceKey: DeviceKey | null): Promise { @@ -277,7 +277,7 @@ export class DeviceTrustService implements DeviceTrustServiceAbstraction { if (this.platformSupportsSecureStorage) { await this.secureStorageService.save( `${userId}${this.deviceKeySecureStorageKey}`, - deviceKey, + deviceKey as DeviceKey, this.getSecureStorageOptions(userId), ); return; @@ -330,7 +330,7 @@ export class DeviceTrustService implements DeviceTrustServiceAbstraction { // Attempt to decrypt encryptedUserDataKey with devicePrivateKey const userKey = await this.encryptService.rsaDecrypt( - new EncString(encryptedUserKey.encryptedString), + new EncString(encryptedUserKey.encryptedString!), devicePrivateKey, ); From 660aba2780924a0e5b6b110619c41c3126f6f36e Mon Sep 17 00:00:00 2001 From: Alec Rippberger Date: Tue, 17 Dec 2024 16:41:29 -0600 Subject: [PATCH 10/25] Type fixes and message update --- .../app/auth/settings/security/device-management.component.ts | 2 +- .../src/auth/services/device-trust.service.implementation.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/web/src/app/auth/settings/security/device-management.component.ts b/apps/web/src/app/auth/settings/security/device-management.component.ts index 376b5f536ae..7053c81c6ee 100644 --- a/apps/web/src/app/auth/settings/security/device-management.component.ts +++ b/apps/web/src/app/auth/settings/security/device-management.component.ts @@ -209,7 +209,7 @@ export class DeviceManagementComponent { this.toastService.showToast({ title: "", - message: this.i18nService.t("deviceApproved"), + message: this.i18nService.t("loggedOut"), variant: "success", }); } catch (e) { diff --git a/libs/common/src/auth/services/device-trust.service.implementation.ts b/libs/common/src/auth/services/device-trust.service.implementation.ts index 3bdec71cadc..bb28e76eee3 100644 --- a/libs/common/src/auth/services/device-trust.service.implementation.ts +++ b/libs/common/src/auth/services/device-trust.service.implementation.ts @@ -94,10 +94,10 @@ export class DeviceTrustService implements DeviceTrustServiceAbstraction { this.stateProvider.getUserState$(SHOULD_TRUST_DEVICE, userId), ); - return shouldTrustDevice; + return shouldTrustDevice ?? false; } - async setShouldTrustDevice(userId: UserId, value: boolean): Promise { + async setShouldTrustDevice(userId: UserId, value: boolean | null): Promise { if (!userId) { throw new Error("UserId is required. Cannot set should trust device."); } From 80760c87729e2515978f6b323714d0b51f725145 Mon Sep 17 00:00:00 2001 From: Alec Rippberger Date: Tue, 17 Dec 2024 21:51:34 -0600 Subject: [PATCH 11/25] Update login status column with pending auth request --- .../security/device-management.component.html | 10 +++++++--- .../security/device-management.component.ts | 15 ++++++++++++++- apps/web/src/locales/en/messages.json | 3 +++ 3 files changed, 24 insertions(+), 4 deletions(-) diff --git a/apps/web/src/app/auth/settings/security/device-management.component.html b/apps/web/src/app/auth/settings/security/device-management.component.html index d72f0cd7ee0..a2f036d27db 100644 --- a/apps/web/src/app/auth/settings/security/device-management.component.html +++ b/apps/web/src/app/auth/settings/security/device-management.component.html @@ -35,9 +35,13 @@

- {{ device.loginStatus }} - - {{ "currentSession" | i18n }} + + {{ + "currentSession" | i18n + }} + {{ + "requestPending" | i18n + }} {{ device.firstLogin | date: "medium" }} diff --git a/apps/web/src/app/auth/settings/security/device-management.component.ts b/apps/web/src/app/auth/settings/security/device-management.component.ts index 7053c81c6ee..a0d324506f4 100644 --- a/apps/web/src/app/auth/settings/security/device-management.component.ts +++ b/apps/web/src/app/auth/settings/security/device-management.component.ts @@ -19,6 +19,7 @@ interface DeviceTableData { loginStatus: string; firstLogin: Date; trusted: boolean; + devicePendingAuthRequest: object | null; } /** @@ -61,6 +62,7 @@ export class DeviceManagementComponent { 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, }; @@ -125,7 +127,7 @@ export class DeviceManagementComponent { } if (device.response.devicePendingAuthRequest?.creationDate) { - return new Date(device.response.devicePendingAuthRequest.creationDate).toLocaleDateString(); + return this.i18nService.t("requestPending"); } return ""; @@ -157,6 +159,17 @@ export class DeviceManagementComponent { : 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 ( + device.devicePendingAuthRequest !== undefined && device.devicePendingAuthRequest !== null + ); + } + /** * Remove a device * @param device - The device diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index a11afee684a..6eaa2a9e1c3 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -1718,6 +1718,9 @@ "currentSession": { "message": "Current session" }, + "requestPending": { + "message": "Request pending" + }, "logBackInOthersToo": { "message": "Please log back in. If you are using other Bitwarden applications log out and back in to those as well." }, From 278fa9b6aba5c6d758b6684431099d9970c46d60 Mon Sep 17 00:00:00 2001 From: Alec Rippberger Date: Tue, 17 Dec 2024 21:59:32 -0600 Subject: [PATCH 12/25] Update table styling --- .../app/auth/settings/security/device-management.component.html | 2 +- .../app/auth/settings/security/device-management.component.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/web/src/app/auth/settings/security/device-management.component.html b/apps/web/src/app/auth/settings/security/device-management.component.html index a2f036d27db..a5512cedcc1 100644 --- a/apps/web/src/app/auth/settings/security/device-management.component.html +++ b/apps/web/src/app/auth/settings/security/device-management.component.html @@ -75,7 +75,7 @@

(click)="removeDevice(device)" [disabled]="isCurrentDevice(device)" > - + {{ "remove" | i18n }} diff --git a/apps/web/src/app/auth/settings/security/device-management.component.ts b/apps/web/src/app/auth/settings/security/device-management.component.ts index a0d324506f4..fb1eeadce63 100644 --- a/apps/web/src/app/auth/settings/security/device-management.component.ts +++ b/apps/web/src/app/auth/settings/security/device-management.component.ts @@ -89,7 +89,7 @@ export class DeviceManagementComponent { { name: "firstLogin", title: this.i18nService.t("firstLogin"), - headerClass: "tw-w-1/4", + headerClass: "tw-w-1/3", sortable: true, }, ]; From bcd847db2b10d0fa5f9cedbe243aae7f78232d79 Mon Sep 17 00:00:00 2001 From: Alec Rippberger Date: Wed, 18 Dec 2024 21:59:17 -0600 Subject: [PATCH 13/25] Add virtual scroll to table --- .../security/device-management.component.html | 146 +++++++++--------- 1 file changed, 71 insertions(+), 75 deletions(-) diff --git a/apps/web/src/app/auth/settings/security/device-management.component.html b/apps/web/src/app/auth/settings/security/device-management.component.html index a5512cedcc1..e8c51bfb983 100644 --- a/apps/web/src/app/auth/settings/security/device-management.component.html +++ b/apps/web/src/app/auth/settings/security/device-management.component.html @@ -5,84 +5,80 @@

{{ "deviceListDescription" | i18n }}

- + - - - {{ col.title }} - - - + + {{ col.title }} + + - - - -
- -
-
- {{ device.displayName }} - - {{ "trusted" | i18n }} - -
- - - {{ - "currentSession" | i18n - }} - {{ - "requestPending" | i18n - }} - - {{ device.firstLogin | date: "medium" }} - + + +
+ +
+
+ {{ row.displayName }} + + {{ "trusted" | i18n }} + +
+ + + {{ + "currentSession" | i18n + }} + {{ + "requestPending" | i18n + }} + + {{ row.firstLogin | date: "medium" }} + + + + - - - - - - - + bitMenuItem + (click)="logOutDevice(row)" + [disabled]="isCurrentDevice(row)" + > + + {{ "logOut" | i18n }} + + + +
-
+

From f336afcf52511f8bdc6940b9c20f52b5392d7d53 Mon Sep 17 00:00:00 2001 From: Alec Rippberger Date: Wed, 18 Dec 2024 22:03:06 -0600 Subject: [PATCH 14/25] Remove approve device and log out menu options --- .../security/device-management.component.html | 18 ------ .../security/device-management.component.ts | 55 ------------------- 2 files changed, 73 deletions(-) diff --git a/apps/web/src/app/auth/settings/security/device-management.component.html b/apps/web/src/app/auth/settings/security/device-management.component.html index e8c51bfb983..acdfaa84b9a 100644 --- a/apps/web/src/app/auth/settings/security/device-management.component.html +++ b/apps/web/src/app/auth/settings/security/device-management.component.html @@ -48,24 +48,6 @@

[bitMenuTriggerFor]="optionsMenu" > - - diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index a38dc4674b2..580a0744a63 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -8257,6 +8257,9 @@ "deviceRemoved": { "message": "Device removed" }, + "removeDevice": { + "message": "Remove device" + }, "removeDeviceConfirmation": { "message": "Are you sure you want to remove this device?" }, From 0932e1886c5fe84f04ad4d67662d5508b881dd2a Mon Sep 17 00:00:00 2001 From: Alec Rippberger Date: Fri, 20 Dec 2024 16:07:18 -0600 Subject: [PATCH 20/25] Add loading spinners --- .../security/device-management.component.html | 23 +++++++++--- .../security/device-management.component.ts | 35 ++++++++++++------- 2 files changed, 40 insertions(+), 18 deletions(-) diff --git a/apps/web/src/app/auth/settings/security/device-management.component.html b/apps/web/src/app/auth/settings/security/device-management.component.html index 3ee04c8376e..dee1254316c 100644 --- a/apps/web/src/app/auth/settings/security/device-management.component.html +++ b/apps/web/src/app/auth/settings/security/device-management.component.html @@ -1,11 +1,24 @@
-

- {{ "devices" | i18n }} - -

+
+

{{ "devices" | i18n }}

+ + +
+

{{ "deviceListDescription" | i18n }}

- +
+ +
+ + (); protected currentDevice: DeviceView | undefined; + protected loading = true; + protected asyncActionLoading = false; constructor( private i18nService: I18nService, @@ -42,7 +44,6 @@ export class DeviceManagementComponent { private dialogService: DialogService, private toastService: ToastService, ) { - // Get the current device and then get all devices this.devicesService .getCurrentDevice$() .pipe( @@ -52,18 +53,24 @@ export class DeviceManagementComponent { return this.devicesService.getDevices$(); }), ) - .subscribe((devices) => { - this.dataSource.data = devices.map((device) => { - return { - 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, - }; - }); + .subscribe({ + next: (devices) => { + this.dataSource.data = devices.map((device) => { + return { + 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; + }, + error: () => { + this.loading = false; + }, }); } @@ -183,7 +190,9 @@ export class DeviceManagementComponent { } 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); From 39341019182c71afb5711ce1819f07d779569f48 Mon Sep 17 00:00:00 2001 From: Alec Rippberger Date: Fri, 20 Dec 2024 17:40:33 -0600 Subject: [PATCH 21/25] Use ValidationService to handle error --- .../settings/security/device-management.component.ts | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/apps/web/src/app/auth/settings/security/device-management.component.ts b/apps/web/src/app/auth/settings/security/device-management.component.ts index a2450fa1cd0..5a27f42416a 100644 --- a/apps/web/src/app/auth/settings/security/device-management.component.ts +++ b/apps/web/src/app/auth/settings/security/device-management.component.ts @@ -8,6 +8,7 @@ import { DevicesServiceAbstraction } from "@bitwarden/common/auth/abstractions/d 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 { DialogService, ToastService, TableDataSource, TableModule } from "@bitwarden/components"; import { SharedModule } from "../../../shared"; @@ -43,6 +44,7 @@ export class DeviceManagementComponent { private devicesService: DevicesServiceAbstraction, private dialogService: DialogService, private toastService: ToastService, + private validationService: ValidationService, ) { this.devicesService .getCurrentDevice$() @@ -202,12 +204,8 @@ export class DeviceManagementComponent { message: this.i18nService.t("deviceRemoved"), variant: "success", }); - } catch (e) { - this.toastService.showToast({ - title: "", - message: this.i18nService.t("errorOccurred"), - variant: "warning", - }); + } catch (error) { + this.validationService.showError(error); } } } From 4bb04596c27b1e588a321ffcc9e7a2ae55c3083c Mon Sep 17 00:00:00 2001 From: Alec Rippberger Date: Mon, 23 Dec 2024 11:34:48 -0600 Subject: [PATCH 22/25] Fix platform translation in device management component to handle "Unknown" platform correctly --- .../auth/settings/security/device-management.component.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/apps/web/src/app/auth/settings/security/device-management.component.ts b/apps/web/src/app/auth/settings/security/device-management.component.ts index 5a27f42416a..fabc960d574 100644 --- a/apps/web/src/app/auth/settings/security/device-management.component.ts +++ b/apps/web/src/app/auth/settings/security/device-management.component.ts @@ -150,8 +150,11 @@ export class DeviceManagementComponent { 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 metadata.platform ? `${category} - ${metadata.platform}` : category; + return platform ? `${category} - ${platform}` : category; } /** From 66e497c230d799d8c1027cff28159389597cf234 Mon Sep 17 00:00:00 2001 From: Alec Rippberger Date: Thu, 26 Dec 2024 12:39:04 -0600 Subject: [PATCH 23/25] Add DeviceManagement feature flag --- .../app/auth/settings/security/security-routing.module.ts | 4 ++++ .../app/auth/settings/security/security.component.html | 4 +++- .../src/app/auth/settings/security/security.component.ts | 8 +++++++- libs/common/src/enums/feature-flag.enum.ts | 2 ++ 4 files changed, 16 insertions(+), 2 deletions(-) diff --git a/apps/web/src/app/auth/settings/security/security-routing.module.ts b/apps/web/src/app/auth/settings/security/security-routing.module.ts index 6ed21605184..87fe1183cdd 100644 --- a/apps/web/src/app/auth/settings/security/security-routing.module.ts +++ b/apps/web/src/app/auth/settings/security/security-routing.module.ts @@ -1,6 +1,9 @@ import { NgModule } from "@angular/core"; import { RouterModule, Routes } from "@angular/router"; +import { canAccessFeature } from "@bitwarden/angular/platform/guard/feature-flag.guard"; +import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; + import { ChangePasswordComponent } from "../change-password.component"; import { TwoFactorSetupComponent } from "../two-factor/two-factor-setup.component"; @@ -34,6 +37,7 @@ const routes: Routes = [ path: "device-management", component: DeviceManagementComponent, data: { titleId: "devices" }, + canActivate: [canAccessFeature(FeatureFlag.DeviceManagement)], }, ], }, diff --git a/apps/web/src/app/auth/settings/security/security.component.html b/apps/web/src/app/auth/settings/security/security.component.html index 6bd7c1daf36..aa9ebf5840c 100644 --- a/apps/web/src/app/auth/settings/security/security.component.html +++ b/apps/web/src/app/auth/settings/security/security.component.html @@ -4,7 +4,9 @@ {{ "masterPassword" | i18n }} {{ "twoStepLogin" | i18n }} - {{ "devices" | i18n }} + + {{ "devices" | i18n }} + {{ "keys" | i18n }} diff --git a/apps/web/src/app/auth/settings/security/security.component.ts b/apps/web/src/app/auth/settings/security/security.component.ts index 1df8145a917..0a4b6d9d626 100644 --- a/apps/web/src/app/auth/settings/security/security.component.ts +++ b/apps/web/src/app/auth/settings/security/security.component.ts @@ -1,6 +1,8 @@ import { Component, OnInit } from "@angular/core"; import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; +import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; @Component({ selector: "app-security", @@ -8,8 +10,12 @@ import { UserVerificationService } from "@bitwarden/common/auth/abstractions/use }) export class SecurityComponent implements OnInit { showChangePassword = true; + deviceManagementAvailable$ = this.configService.getFeatureFlag$(FeatureFlag.DeviceManagement); - constructor(private userVerificationService: UserVerificationService) {} + constructor( + private userVerificationService: UserVerificationService, + private configService: ConfigService, + ) {} async ngOnInit() { this.showChangePassword = await this.userVerificationService.hasMasterPassword(); diff --git a/libs/common/src/enums/feature-flag.enum.ts b/libs/common/src/enums/feature-flag.enum.ts index cc2abed3ba1..2f3c193d2cc 100644 --- a/libs/common/src/enums/feature-flag.enum.ts +++ b/libs/common/src/enums/feature-flag.enum.ts @@ -43,6 +43,7 @@ export enum FeatureFlag { PM11360RemoveProviderExportPermission = "pm-11360-remove-provider-export-permission", PM12443RemovePagingLogic = "pm-12443-remove-paging-logic", PrivateKeyRegeneration = "pm-12241-private-key-regeneration", + DeviceManagement = "device-management", } export type AllowedFeatureFlagTypes = boolean | number | string; @@ -96,6 +97,7 @@ export const DefaultFeatureFlagValue = { [FeatureFlag.PM11360RemoveProviderExportPermission]: FALSE, [FeatureFlag.PM12443RemovePagingLogic]: FALSE, [FeatureFlag.PrivateKeyRegeneration]: FALSE, + [FeatureFlag.DeviceManagement]: FALSE, } satisfies Record; export type DefaultFeatureFlagValueType = typeof DefaultFeatureFlagValue; From 3bebfabc4056105c82cfb331f3a10641a3aaf859 Mon Sep 17 00:00:00 2001 From: Alec Rippberger Date: Mon, 30 Dec 2024 10:50:09 -0600 Subject: [PATCH 24/25] Clean up device management header to look more like other tabs --- .../security/device-management.component.html | 28 ++++++++++--------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/apps/web/src/app/auth/settings/security/device-management.component.html b/apps/web/src/app/auth/settings/security/device-management.component.html index dee1254316c..1023827dcf6 100644 --- a/apps/web/src/app/auth/settings/security/device-management.component.html +++ b/apps/web/src/app/auth/settings/security/device-management.component.html @@ -1,15 +1,17 @@ -
-
-

{{ "devices" | i18n }}

- - + +
+
+

{{ "devices" | i18n }}

+ + +

{{ "deviceListDescription" | i18n }}

@@ -76,4 +78,4 @@

{{ "devices" | i18n }}

-
+ From fa51b93a0f6208bd882d922afb4ac0d21a4d0706 Mon Sep 17 00:00:00 2001 From: Alec Rippberger Date: Mon, 30 Dec 2024 10:51:49 -0600 Subject: [PATCH 25/25] Fix default column sort --- .../app/auth/settings/security/device-management.component.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/web/src/app/auth/settings/security/device-management.component.html b/apps/web/src/app/auth/settings/security/device-management.component.html index 1023827dcf6..69acd255a60 100644 --- a/apps/web/src/app/auth/settings/security/device-management.component.html +++ b/apps/web/src/app/auth/settings/security/device-management.component.html @@ -27,7 +27,7 @@

{{ "devices" | i18n }}

[class]="col.headerClass" bitCell [bitSortable]="col.sortable ? col.name : null" - [default]="col.name === 'displayName' ? 'desc' : null" + [default]="col.name === 'loginStatus' ? 'desc' : null" scope="col" role="columnheader" >