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:
@@ -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:"
|
||||
},
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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>
|
||||
@@ -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 {}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
@@ -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;
|
||||
@@ -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" },
|
||||
},
|
||||
}),
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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": {
|
||||
|
||||
Reference in New Issue
Block a user