1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-12 22:33:35 +00:00

feat(extension-login-approvals): [Auth/PM-14939] devices list view for browser (#14620)

Creates a new `DeviceManagementComponent` that fetches devices and formats them before handing them off to a view component for display.

View components:

- `DeviceManagementTableComponent` - displays on medium to large screens
- `DeviceManagementItemGroupComponent` - displays on small screens

Feature flag: `PM14938_BrowserExtensionLoginApproval`
This commit is contained in:
Alec Rippberger
2025-07-17 12:43:49 -05:00
committed by GitHub
parent 250e46ee70
commit 00b6b0224e
28 changed files with 872 additions and 26 deletions

View File

@@ -3460,6 +3460,22 @@
"logInRequestSent": {
"message": "Request sent"
},
"loginRequestApprovedForEmailOnDevice": {
"message": "Login request approved for $EMAIL$ on $DEVICE$",
"placeholders": {
"email": {
"content": "$1",
"example": "name@example.com"
},
"device": {
"content": "$2",
"example": "Web app - Chrome"
}
}
},
"youDeniedLoginAttemptFromAnotherDevice": {
"message": "You denied a login attempt from another device. If this was you, try to log in with the device again."
},
"masterPasswordChanged": {
"message": "Master password saved"
},
@@ -3556,6 +3572,88 @@
"rememberThisDeviceToMakeFutureLoginsSeamless": {
"message": "Remember this device to make future logins seamless"
},
"manageDevices": {
"message": "Manage devices"
},
"currentSession": {
"message": "Current session"
},
"mobile": {
"message": "Mobile",
"description": "Mobile app"
},
"extension": {
"message": "Extension",
"description": "Browser extension/addon"
},
"desktop": {
"message": "Desktop",
"description": "Desktop app"
},
"webVault": {
"message": "Web vault"
},
"webApp": {
"message": "Web app"
},
"cli": {
"message": "CLI"
},
"sdk": {
"message": "SDK",
"description": "Software Development Kit"
},
"requestPending": {
"message": "Request pending"
},
"firstLogin": {
"message": "First login"
},
"trusted": {
"message": "Trusted"
},
"needsApproval": {
"message": "Needs approval"
},
"devices": {
"message": "Devices"
},
"accessAttemptBy": {
"message": "Access attempt by $EMAIL$",
"placeholders": {
"email": {
"content": "$1",
"example": "name@example.com"
}
}
},
"confirmAccess": {
"message": "Confirm access"
},
"denyAccess": {
"message": "Deny access"
},
"time": {
"message": "Time"
},
"deviceType": {
"message": "Device Type"
},
"loginRequest": {
"message": "Login request"
},
"justNow": {
"message": "Just now"
},
"requestedXMinutesAgo": {
"message": "Requested $MINUTES$ minutes ago",
"placeholders": {
"minutes": {
"content": "$1",
"example": "5"
}
}
},
"deviceApprovalRequired": {
"message": "Device approval required. Select an approval option below:"
},

View File

@@ -102,6 +102,18 @@
</bit-card>
</bit-section>
<bit-section *ngIf="extensionLoginApprovalFlagEnabled">
<bit-section-header>
<h2 bitTypography="h6">{{ "manageDevices" | i18n }}</h2>
</bit-section-header>
<bit-item>
<button bit-item-content type="button" appStopClick routerLink="/device-management">
{{ "devices" | i18n }}
<i slot="end" class="bwi bwi-chevron-right" aria-hidden="true"></i>
</button>
</bit-item>
</bit-section>
<bit-section disableMargin>
<bit-section-header>
<h2 bitTypography="h6">{{ "otherOptions" | i18n }}</h2>

View File

@@ -32,6 +32,7 @@ import { getFirstPolicy } from "@bitwarden/common/admin-console/services/policy/
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
import { getUserId } from "@bitwarden/common/auth/services/account.service";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import {
VaultTimeout,
VaultTimeoutAction,
@@ -40,6 +41,7 @@ import {
VaultTimeoutSettingsService,
VaultTimeoutStringType,
} from "@bitwarden/common/key-management/vault-timeout";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
@@ -113,6 +115,7 @@ export class AccountSecurityComponent implements OnInit, OnDestroy {
biometricUnavailabilityReason: string;
showChangeMasterPass = true;
pinEnabled$: Observable<boolean> = of(true);
extensionLoginApprovalFlagEnabled = false;
form = this.formBuilder.group({
vaultTimeout: [null as VaultTimeout | null],
@@ -155,6 +158,7 @@ export class AccountSecurityComponent implements OnInit, OnDestroy {
private biometricsService: BiometricsService,
private vaultNudgesService: NudgesService,
private validationService: ValidationService,
private configService: ConfigService,
) {}
async ngOnInit() {
@@ -235,6 +239,10 @@ export class AccountSecurityComponent implements OnInit, OnDestroy {
};
this.form.patchValue(initialValues, { emitEvent: false });
this.extensionLoginApprovalFlagEnabled = await this.configService.getFeatureFlag(
FeatureFlag.PM14938_BrowserExtensionLoginApproval,
);
timer(0, 1000)
.pipe(
switchMap(async () => {

View File

@@ -0,0 +1,11 @@
<popup-page>
<popup-header slot="header" pageTitle="{{ 'devices' | i18n }}" showBackButton>
<ng-container slot="end">
<app-pop-out></app-pop-out>
</ng-container>
</popup-header>
<div class="tw-bg-background-alt">
<auth-device-management></auth-device-management>
</div>
</popup-page>

View File

@@ -0,0 +1,22 @@
import { Component } from "@angular/core";
import { DeviceManagementComponent } from "@bitwarden/angular/auth/device-management/device-management.component";
import { I18nPipe } from "@bitwarden/ui-common";
import { PopOutComponent } from "../../../platform/popup/components/pop-out.component";
import { PopupHeaderComponent } from "../../../platform/popup/layout/popup-header.component";
import { PopupPageComponent } from "../../../platform/popup/layout/popup-page.component";
@Component({
standalone: true,
selector: "extension-device-management",
templateUrl: "extension-device-management.component.html",
imports: [
DeviceManagementComponent,
I18nPipe,
PopOutComponent,
PopupHeaderComponent,
PopupPageComponent,
],
})
export class ExtensionDeviceManagementComponent {}

View File

@@ -0,0 +1,15 @@
import { DeviceManagementComponentServiceAbstraction } from "@bitwarden/angular/auth/device-management/device-management-component.service.abstraction";
/**
* Browser extension implementation of the device management component service
*/
export class ExtensionDeviceManagementComponentService
implements DeviceManagementComponentServiceAbstraction
{
/**
* Don't show header information in browser extension client
*/
showHeaderInformation(): boolean {
return false;
}
}

View File

@@ -814,8 +814,9 @@ export default class MainBackground {
);
this.devicesService = new DevicesServiceImplementation(
this.devicesApiService,
this.appIdService,
this.devicesApiService,
this.i18nService,
);
this.authRequestApiService = new DefaultAuthRequestApiService(this.apiService, this.logService);

View File

@@ -49,6 +49,7 @@ import { AccountSwitcherComponent } from "../auth/popup/account-switching/accoun
import { fido2AuthGuard } from "../auth/popup/guards/fido2-auth.guard";
import { SetPasswordComponent } from "../auth/popup/set-password.component";
import { AccountSecurityComponent } from "../auth/popup/settings/account-security.component";
import { ExtensionDeviceManagementComponent } from "../auth/popup/settings/extension-device-management.component";
import { UpdateTempPasswordComponent } from "../auth/popup/update-temp-password.component";
import { Fido2Component } from "../autofill/popup/fido2/fido2.component";
import { AutofillComponent } from "../autofill/popup/settings/autofill.component";
@@ -263,6 +264,12 @@ const routes: Routes = [
canActivate: [authGuard],
data: { elevation: 1 } satisfies RouteDataProperties,
},
{
path: "device-management",
component: ExtensionDeviceManagementComponent,
canActivate: [authGuard],
data: { elevation: 1 } satisfies RouteDataProperties,
},
{
path: "notifications",
component: NotificationsSettingsComponent,

View File

@@ -4,6 +4,7 @@ import { APP_INITIALIZER, NgModule, NgZone } from "@angular/core";
import { merge, of, Subject } from "rxjs";
import { CollectionService } from "@bitwarden/admin-console/common";
import { DeviceManagementComponentServiceAbstraction } from "@bitwarden/angular/auth/device-management/device-management-component.service.abstraction";
import { AngularThemingService } from "@bitwarden/angular/platform/services/theming/angular-theming.service";
import { SafeProvider, safeProvider } from "@bitwarden/angular/platform/utils/safe-provider";
import { ViewCacheService } from "@bitwarden/angular/platform/view-cache";
@@ -145,6 +146,7 @@ import { ForegroundLockService } from "../../auth/popup/accounts/foreground-lock
import { ExtensionLoginComponentService } from "../../auth/popup/login/extension-login-component.service";
import { ExtensionSsoComponentService } from "../../auth/popup/login/extension-sso-component.service";
import { ExtensionLogoutService } from "../../auth/popup/logout/extension-logout.service";
import { ExtensionDeviceManagementComponentService } from "../../auth/services/extension-device-management-component.service";
import { ExtensionTwoFactorAuthComponentService } from "../../auth/services/extension-two-factor-auth-component.service";
import { ExtensionTwoFactorAuthDuoComponentService } from "../../auth/services/extension-two-factor-auth-duo-component.service";
import { ExtensionTwoFactorAuthWebAuthnComponentService } from "../../auth/services/extension-two-factor-auth-webauthn-component.service";
@@ -667,6 +669,11 @@ const safeProviders: SafeProvider[] = [
useClass: ForegroundNotificationsService,
deps: [LogService],
}),
safeProvider({
provide: DeviceManagementComponentServiceAbstraction,
useClass: ExtensionDeviceManagementComponentService,
deps: [],
}),
];
@NgModule({

View File

@@ -20,7 +20,7 @@ import {
import { SharedModule } from "../../../shared";
import { VaultBannersService } from "../../../vault/individual-vault/vault-banners/services/vault-banners.service";
import { DeviceManagementComponent } from "./device-management.component";
import { DeviceManagementOldComponent } from "./device-management-old.component";
class MockResizeObserver {
observe = jest.fn();
@@ -35,8 +35,8 @@ interface Message {
notificationId?: string;
}
describe("DeviceManagementComponent", () => {
let fixture: ComponentFixture<DeviceManagementComponent>;
describe("DeviceManagementOldComponent", () => {
let fixture: ComponentFixture<DeviceManagementOldComponent>;
let messageSubject: Subject<Message>;
let mockDevices: DeviceView[];
let vaultBannersService: VaultBannersService;
@@ -66,7 +66,7 @@ describe("DeviceManagementComponent", () => {
SharedModule,
TableModule,
PopoverModule,
DeviceManagementComponent,
DeviceManagementOldComponent,
],
providers: [
{
@@ -130,7 +130,7 @@ describe("DeviceManagementComponent", () => {
],
}).compileComponents();
fixture = TestBed.createComponent(DeviceManagementComponent);
fixture = TestBed.createComponent(DeviceManagementOldComponent);
vaultBannersService = TestBed.inject(VaultBannersService);
});

View File

@@ -45,10 +45,10 @@ interface DeviceTableData {
*/
@Component({
selector: "app-device-management",
templateUrl: "./device-management.component.html",
templateUrl: "./device-management-old.component.html",
imports: [CommonModule, SharedModule, TableModule, PopoverModule],
})
export class DeviceManagementComponent {
export class DeviceManagementOldComponent {
protected dataSource = new TableDataSource<DeviceTableData>();
protected currentDevice: DeviceView | undefined;
protected loading = true;

View File

@@ -1,13 +1,15 @@
import { NgModule } from "@angular/core";
import { RouterModule, Routes } from "@angular/router";
import { DeviceManagementComponent } from "@bitwarden/angular/auth/device-management/device-management.component";
import { canAccessFeature } from "@bitwarden/angular/platform/guard/feature-flag.guard";
import { featureFlaggedRoute } from "@bitwarden/angular/platform/utils/feature-flagged-route";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ChangePasswordComponent } from "../change-password.component";
import { TwoFactorSetupComponent } from "../two-factor/two-factor-setup.component";
import { DeviceManagementComponent } from "./device-management.component";
import { DeviceManagementOldComponent } from "./device-management-old.component";
import { PasswordSettingsComponent } from "./password-settings/password-settings.component";
import { SecurityKeysComponent } from "./security-keys.component";
import { SecurityComponent } from "./security.component";
@@ -55,11 +57,15 @@ const routes: Routes = [
component: SecurityKeysComponent,
data: { titleId: "keys" },
},
{
path: "device-management",
component: DeviceManagementComponent,
data: { titleId: "devices" },
},
...featureFlaggedRoute({
defaultComponent: DeviceManagementOldComponent,
flaggedComponent: DeviceManagementComponent,
featureFlag: FeatureFlag.PM14938_BrowserExtensionLoginApproval,
routeOptions: {
path: "device-management",
data: { titleId: "devices" },
},
}),
],
},
];

View File

@@ -10,6 +10,8 @@ import {
OrganizationUserApiService,
CollectionService,
} from "@bitwarden/admin-console/common";
import { DefaultDeviceManagementComponentService } from "@bitwarden/angular/auth/device-management/default-device-management-component.service";
import { DeviceManagementComponentServiceAbstraction } from "@bitwarden/angular/auth/device-management/device-management-component.service.abstraction";
import { ChangePasswordService } from "@bitwarden/angular/auth/password-management/change-password";
import { SetInitialPasswordService } from "@bitwarden/angular/auth/password-management/set-initial-password/set-initial-password.service.abstraction";
import { SafeProvider, safeProvider } from "@bitwarden/angular/platform/utils/safe-provider";
@@ -406,6 +408,11 @@ const safeProviders: SafeProvider[] = [
RouterService,
],
}),
safeProvider({
provide: DeviceManagementComponentServiceAbstraction,
useClass: DefaultDeviceManagementComponentService,
deps: [],
}),
];
@NgModule({

View File

@@ -3489,6 +3489,9 @@
"webVault": {
"message": "Web vault"
},
"webApp": {
"message": "Web app"
},
"cli": {
"message": "CLI"
},
@@ -3971,6 +3974,22 @@
"youDeniedALogInAttemptFromAnotherDevice": {
"message": "You denied a login attempt from another device. If this really was you, try to log in with the device again."
},
"loginRequestApprovedForEmailOnDevice": {
"message": "Login request approved for $EMAIL$ on $DEVICE$",
"placeholders": {
"email": {
"content": "$1",
"example": "name@example.com"
},
"device": {
"content": "$2",
"example": "Web app - Chrome"
}
}
},
"youDeniedLoginAttemptFromAnotherDevice": {
"message": "You denied a login attempt from another device. If this was you, try to log in with the device again."
},
"loginRequestHasAlreadyExpired": {
"message": "Login request has already expired."
},
@@ -4115,6 +4134,9 @@
"reviewLoginRequest": {
"message": "Review login request"
},
"loginRequest": {
"message": "Login request"
},
"freeTrialEndPromptCount": {
"message": "Your free trial ends in $COUNT$ days.",
"placeholders": {