Skip to content

Commit

Permalink
feat(newDeviceVerification) : added visual elements for opting out of…
Browse files Browse the repository at this point in the history
… new device verification.
  • Loading branch information
ike-kottlowski committed Jan 15, 2025
1 parent d900890 commit ed61bfd
Show file tree
Hide file tree
Showing 10 changed files with 144 additions and 54 deletions.
18 changes: 18 additions & 0 deletions apps/web/src/app/auth/settings/account/account.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,24 @@ <h1 bitTypography="h1">{{ "changeEmail" | i18n }}</h1>
</div>

<app-danger-zone>
<button
*ngIf="verifyNewDeviceLogin && showSetNewDeviceLoginProtection$ | async"
type="button"
bitButton
buttonType="danger"
[bitAction]="setNewDeviceLoginProtection"
>
{{ "turnOffNewDeviceLoginProtection" | i18n }}
</button>
<button
*ngIf="!verifyNewDeviceLogin && showSetNewDeviceLoginProtection$ | async"
type="button"
bitButton
buttonType="secondary"
[bitAction]="setNewDeviceLoginProtection"
>
{{ "turnOnNewDeviceLoginProtection" | i18n }}
</button>
<button type="button" bitButton buttonType="danger" (click)="deauthorizeSessions()">
{{ "deauthorizeSessions" | i18n }}
</button>
Expand Down
33 changes: 28 additions & 5 deletions apps/web/src/app/auth/settings/account/account.component.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { Component, OnInit, ViewChild, ViewContainerRef } from "@angular/core";
import { combineLatest, from, lastValueFrom, map, Observable } from "rxjs";
import { Component, OnInit, ViewChild, ViewContainerRef, OnDestroy } from "@angular/core";
import { combineLatest, from, lastValueFrom, map, Observable, Subject, takeUntil } from "rxjs";

import { ModalService } from "@bitwarden/angular/services/modal.service";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
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";
Expand All @@ -14,28 +13,37 @@ import { PurgeVaultComponent } from "../../../vault/settings/purge-vault.compone

import { DeauthorizeSessionsComponent } from "./deauthorize-sessions.component";
import { DeleteAccountDialogComponent } from "./delete-account-dialog.component";
import { SetAccountVerifyDevicesDialogComponent } from "./set-account-verify-devices-dialog.component";

@Component({
selector: "app-account",
templateUrl: "account.component.html",
})
export class AccountComponent implements OnInit {
export class AccountComponent implements OnInit, OnDestroy {
@ViewChild("deauthorizeSessionsTemplate", { read: ViewContainerRef, static: true })
deauthModalRef: ViewContainerRef;
private destroy$ = new Subject<void>();

showChangeEmail$: Observable<boolean>;
showPurgeVault$: Observable<boolean>;
showDeleteAccount$: Observable<boolean>;
showSetNewDeviceLoginProtection$: Observable<boolean>;
verifyNewDeviceLogin: boolean;

constructor(
private modalService: ModalService,
private accountService: AccountService,
private dialogService: DialogService,
private userVerificationService: UserVerificationService,
private configService: ConfigService,
private organizationService: OrganizationService,
) {}

async ngOnInit() {
this.showSetNewDeviceLoginProtection$ = this.configService.getFeatureFlag$(
FeatureFlag.NewDeviceVerification,
);

const isAccountDeprovisioningEnabled$ = this.configService.getFeatureFlag$(
FeatureFlag.AccountDeprovisioning,
);
Expand Down Expand Up @@ -76,6 +84,11 @@ export class AccountComponent implements OnInit {
!isAccountDeprovisioningEnabled || !userIsManagedByOrganization,
),
);
this.accountService.accountVerifyDevices$
.pipe(takeUntil(this.destroy$))
.subscribe((verifyDevices) => {
this.verifyNewDeviceLogin = verifyDevices;
});
}

async deauthorizeSessions() {
Expand All @@ -91,4 +104,14 @@ export class AccountComponent implements OnInit {
const dialogRef = DeleteAccountDialogComponent.open(this.dialogService);
await lastValueFrom(dialogRef.closed);
};

setNewDeviceLoginProtection = async () => {
const dialogRef = SetAccountVerifyDevicesDialogComponent.open(this.dialogService);
await lastValueFrom(dialogRef.closed);
};

ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
}
}
Original file line number Diff line number Diff line change
@@ -1,14 +1,6 @@
<h1 bitTypography="h1" class="tw-mt-16 tw-pb-2.5 !tw-text-danger">{{ "dangerZone" | i18n }}</h1>

<div class="tw-rounded tw-border tw-border-solid tw-border-danger-600 tw-p-5">
<p>
{{
(accountDeprovisioningEnabled$ | async) && content.children.length === 1
? ("dangerZoneDescSingular" | i18n)
: ("dangerZoneDesc" | i18n)
}}
</p>

<div #content class="tw-flex tw-flex-row tw-gap-2">
<ng-content></ng-content>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import { AccountApiService } from "@bitwarden/common/auth/abstractions/account-a
import { Verification } from "@bitwarden/common/auth/types/verification";
import { ErrorResponse } from "@bitwarden/common/models/response/error.response";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { DialogService, ToastService } from "@bitwarden/components";

@Component({
Expand All @@ -22,7 +21,6 @@ export class DeleteAccountDialogComponent {

constructor(
private i18nService: I18nService,
private platformUtilsService: PlatformUtilsService,
private formBuilder: FormBuilder,
private accountApiService: AccountApiService,
private dialogRef: DialogRef,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
<form [formGroup]="setVerifyDevicesForm" [bitSubmit]="submit">
<bit-dialog dialogSize="default" [title]="'newDeviceLoginProtection' | i18n">
<ng-container bitDialogContent>
<p bitTypography="body1">{{ dialogDesc }}</p>
<bit-callout type="warning">{{
<p *ngIf="verifyNewDeviceLogin" bitTypography="body1">
{{ "turnOffNewDeviceLoginProtectionModalDesc" | i18n }}
</p>
<p *ngIf="!verifyNewDeviceLogin" bitTypography="body1">
{{ "turnOnNewDeviceLoginProtectionModalDesc" | i18n }}
</p>
<bit-callout *ngIf="verifyNewDeviceLogin" type="warning">{{
"turnOffNewDeviceLoginProtectionWarning" | i18n
}}</bit-callout>
<app-user-verification-form-input
Expand All @@ -12,8 +17,23 @@
></app-user-verification-form-input>
</ng-container>
<ng-container bitDialogFooter>
<button bitButton bitFormButton type="submit" buttonType="danger">
{{ dialogSubmitButtonDesc }}
<button
bitButton
*ngIf="verifyNewDeviceLogin"
bitFormButton
type="submit"
buttonType="danger"
>
{{ "disable" | i18n }}
</button>
<button
bitButton
*ngIf="!verifyNewDeviceLogin"
bitFormButton
type="submit"
buttonType="primary"
>
{{ "enable" | i18n }}
</button>
<button bitButton bitFormButton type="button" buttonType="secondary" bitDialogClose>
{{ "close" | i18n }}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,57 +1,70 @@
import { DialogRef } from "@angular/cdk/dialog";
import { Component } from "@angular/core";
import { Component, OnDestroy } from "@angular/core";
import { FormBuilder } from "@angular/forms";
import { Subject, takeUntil } from "rxjs";

import { AccountApiService } from "@bitwarden/common/auth/abstractions/account-api.service";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
import { SetVerifyDevicesRequest } from "@bitwarden/common/auth/models/request/set-verify-devices.request";
import { Verification } from "@bitwarden/common/auth/types/verification";
import { ErrorResponse } from "@bitwarden/common/models/response/error.response";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { UserId } from "@bitwarden/common/types/guid";
import { DialogService, ToastService } from "@bitwarden/components";

@Component({
templateUrl: "./set-account-verify-devices-dialog.component.html",
})
export class SetAccountVerifyDevicesDialogComponent {
export class SetAccountVerifyDevicesDialogComponent implements OnDestroy {
// use this subject for all subscriptions to ensure all subscripts are completed
private destroy$ = new Subject<void>();
// the default for new device verification is true
verifyNewDeviceLogin: boolean = true;
activeUserId: UserId;

setVerifyDevicesForm = this.formBuilder.group({
verification: undefined as Verification | undefined,
});
invalidSecret: boolean = false;
verifyNewDeviceLogin: boolean = true;
dialogDesc: string = ""; // on or off
dialogSubmitButtonDesc: string = ""; // on or off
dialogBodyDesc: string = ""; // turn on or off

constructor(
private i18nService: I18nService,
private formBuilder: FormBuilder,
private accountApiService: AccountApiService,
private accountService: AccountService,
private userVerificationService: UserVerificationService,
private dialogRef: DialogRef,
private toastService: ToastService,
) {
//todo set dialog text based on account information
this.verifyNewDeviceLogin = getVerifyDevices;
this.accountService.activeAccount$.pipe(
(a) => (this.verifyNewDeviceLogin = a?.verifyDevices ?? true),
);
this.dialogDesc = this.i18nService.t("accountNewDeviceLoginProtection");
this.dialogSubmitButtonDesc = this.i18nService.t("accountNewDeviceLoginProtectionSave");
this.dialogBodyDesc = this.i18nService.t("accountNewDeviceLoginProtectionDesc");
this.accountService.accountVerifyDevices$
.pipe(takeUntil(this.destroy$))
.subscribe((verifyDevices) => {
this.verifyNewDeviceLogin = verifyDevices;
});
this.accountService.activeAccount$
.pipe(takeUntil(this.destroy$))
.subscribe((account) => (this.activeUserId = account.id));
}

submit = async () => {
try {
// const verification = this.setVerifyDevicesForm.get("verification").value;
//todo create request object
const request: SetVerifyDevicesRequest = null;
const verification = this.setVerifyDevicesForm.get("verification").value;
const request: SetVerifyDevicesRequest = await this.userVerificationService.buildRequest(
verification,
SetVerifyDevicesRequest,
);
// set verify device opposite what is currently is.
request.verifyDevices = !this.verifyNewDeviceLogin;

await this.accountApiService.setVerifyDevices(request);
await this.accountService.setAccountVerifyDevices(this.activeUserId, request.verifyDevices);
this.dialogRef.close();
this.toastService.showToast({
variant: "success",
title: this.i18nService.t("accountNewDeviceLoginProtectionSaved"),
message: this.i18nService.t("accountNewDeviceLoginProtectionSavedDesc"),
title: null,
message: this.i18nService.t("accountNewDeviceLoginProtectionSaved"),
});
} catch (e) {
if (e instanceof ErrorResponse && e.statusCode === 400) {
Expand All @@ -64,4 +77,10 @@ export class SetAccountVerifyDevicesDialogComponent {
static open(dialogService: DialogService) {
return dialogService.open(SetAccountVerifyDevicesDialogComponent);
}

// closes subscription leaks
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
}
}
2 changes: 2 additions & 0 deletions apps/web/src/app/shared/loose-components.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import { DangerZoneComponent } from "../auth/settings/account/danger-zone.compon
import { DeauthorizeSessionsComponent } from "../auth/settings/account/deauthorize-sessions.component";
import { DeleteAccountDialogComponent } from "../auth/settings/account/delete-account-dialog.component";
import { ProfileComponent } from "../auth/settings/account/profile.component";
import { SetAccountVerifyDevicesDialogComponent } from "../auth/settings/account/set-account-verify-devices-dialog.component";
import { EmergencyAccessAttachmentsComponent } from "../auth/settings/emergency-access/attachments/emergency-access-attachments.component";
import { EmergencyAccessConfirmComponent } from "../auth/settings/emergency-access/confirm/emergency-access-confirm.component";
import { EmergencyAccessAddEditComponent } from "../auth/settings/emergency-access/emergency-access-add-edit.component";
Expand Down Expand Up @@ -155,6 +156,7 @@ import { SharedModule } from "./shared.module";
SecurityKeysComponent,
SelectableAvatarComponent,
SendAddEditComponent,
SetAccountVerifyDevicesDialogComponent,
SetPasswordComponent,
SponsoredFamiliesComponent,
SponsoringOrgRowComponent,
Expand Down
23 changes: 22 additions & 1 deletion apps/web/src/locales/en/messages.json
Original file line number Diff line number Diff line change
Expand Up @@ -1726,7 +1726,7 @@
},
"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."
},
Expand Down Expand Up @@ -1809,6 +1809,27 @@
"deauthorizeSessionsWarning": {
"message": "Proceeding will also log you out of your current session, requiring you to log back in. You will also be prompted for two-step login again, if set up. Active sessions on other devices may continue to remain active for up to one hour."
},
"newDeviceLoginProtection": {
"message":"New device login"
},
"turnOffNewDeviceLoginProtection": {
"message":"Turn off new device login protection"
},
"turnOnNewDeviceLoginProtection": {
"message":"Turn on new device login protection"
},
"turnOffNewDeviceLoginProtectionModalDesc": {
"message":"Proceed below to turn off the verification emails bitwarden sends when you login from a new device."
},
"turnOnNewDeviceLoginProtectionModalDesc": {
"message":"Proceed below to have bitwarden send you verification emails when you login from a new device."
},
"turnOffNewDeviceLoginProtectionWarning": {
"message":"With new device login protection turned off, anyone with your master password can access your account from any device. To protect your account without verification emails, set up two-step login."
},
"accountNewDeviceLoginProtectionSaved": {
"message": "New device login protection changes saved"
},
"sessionsDeauthorized": {
"message": "All sessions deauthorized"
},
Expand Down
2 changes: 1 addition & 1 deletion libs/angular/src/services/jslib-services.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -551,7 +551,7 @@ const safeProviders: SafeProvider[] = [
safeProvider({
provide: InternalAccountService,
useClass: AccountServiceImplementation,
deps: [MessagingServiceAbstraction, LogService, GlobalStateProvider],
deps: [MessagingServiceAbstraction, LogService, GlobalStateProvider, SingleUserStateProvider],
}),
safeProvider({
provide: AccountServiceAbstraction,
Expand Down
Loading

0 comments on commit ed61bfd

Please sign in to comment.