mirror of
https://github.com/bitwarden/browser
synced 2026-02-11 05:53:42 +00:00
Merge branch 'main' into handle-unix-not-found-error
This commit is contained in:
@@ -109,6 +109,7 @@ import { Account } from "@bitwarden/common/platform/models/domain/account";
|
||||
import { GlobalState } from "@bitwarden/common/platform/models/domain/global-state";
|
||||
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
||||
import { NotificationsService } from "@bitwarden/common/platform/notifications";
|
||||
// eslint-disable-next-line no-restricted-imports -- Needed for service creation
|
||||
import {
|
||||
DefaultNotificationsService,
|
||||
WorkerWebPushConnectionService,
|
||||
@@ -560,6 +561,7 @@ export default class MainBackground {
|
||||
this.messagingService,
|
||||
this.logService,
|
||||
this.globalStateProvider,
|
||||
this.singleUserStateProvider,
|
||||
);
|
||||
this.activeUserStateProvider = new DefaultActiveUserStateProvider(
|
||||
this.accountService,
|
||||
|
||||
@@ -355,6 +355,7 @@ export class ServiceContainer {
|
||||
this.messagingService,
|
||||
this.logService,
|
||||
this.globalStateProvider,
|
||||
this.singleUserStateProvider,
|
||||
);
|
||||
|
||||
this.activeUserStateProvider = new DefaultActiveUserStateProvider(
|
||||
|
||||
@@ -133,12 +133,6 @@ export class Main {
|
||||
this.mainCryptoFunctionService = new MainCryptoFunctionService();
|
||||
this.mainCryptoFunctionService.init();
|
||||
|
||||
const accountService = new AccountServiceImplementation(
|
||||
MessageSender.EMPTY,
|
||||
this.logService,
|
||||
globalStateProvider,
|
||||
);
|
||||
|
||||
const stateEventRegistrarService = new StateEventRegistrarService(
|
||||
globalStateProvider,
|
||||
storageServiceProvider,
|
||||
@@ -150,6 +144,13 @@ export class Main {
|
||||
this.logService,
|
||||
);
|
||||
|
||||
const accountService = new AccountServiceImplementation(
|
||||
MessageSender.EMPTY,
|
||||
this.logService,
|
||||
globalStateProvider,
|
||||
singleUserStateProvider,
|
||||
);
|
||||
|
||||
const activeUserStateProvider = new DefaultActiveUserStateProvider(
|
||||
accountService,
|
||||
singleUserStateProvider,
|
||||
|
||||
@@ -23,17 +23,27 @@ import { ToastService } from "@bitwarden/components";
|
||||
|
||||
import { organizationPermissionsGuard } from "./org-permissions.guard";
|
||||
|
||||
// Returns a test organization with the specified props.
|
||||
const orgFactory = (props: Partial<Organization> = {}) =>
|
||||
Object.assign(
|
||||
new Organization(),
|
||||
{
|
||||
id: "myOrgId",
|
||||
enabled: true,
|
||||
type: OrganizationUserType.Admin,
|
||||
},
|
||||
props,
|
||||
);
|
||||
|
||||
const targetOrgId = "myOrgId";
|
||||
|
||||
// Returns an array of test organizations with the target organization in the middle.
|
||||
// This more accurately tests the return value of OrganizationService.
|
||||
const orgStateFactory = (targetOrgProps: Partial<Organization> = {}) => [
|
||||
orgFactory({ id: "anotherOrg" }),
|
||||
orgFactory({ id: targetOrgId, ...targetOrgProps }), // target org intentionally nestled in the middle
|
||||
orgFactory({ id: "andAnotherOrg" }),
|
||||
];
|
||||
|
||||
describe("Organization Permissions Guard", () => {
|
||||
let router: MockProxy<Router>;
|
||||
let organizationService: MockProxy<OrganizationService>;
|
||||
@@ -49,7 +59,7 @@ describe("Organization Permissions Guard", () => {
|
||||
state = mock<RouterStateSnapshot>();
|
||||
route = mock<ActivatedRouteSnapshot>({
|
||||
params: {
|
||||
organizationId: orgFactory().id,
|
||||
organizationId: targetOrgId,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -75,82 +85,79 @@ describe("Organization Permissions Guard", () => {
|
||||
expect(actual).not.toBe(true);
|
||||
});
|
||||
|
||||
it("permits navigation if no permissions are specified", async () => {
|
||||
const org = orgFactory();
|
||||
organizationService.organizations$.calledWith(userId).mockReturnValue(of([org]));
|
||||
describe("given an enabled organization", () => {
|
||||
beforeEach(() => {
|
||||
organizationService.organizations$.calledWith(userId).mockReturnValue(of(orgStateFactory()));
|
||||
});
|
||||
|
||||
const actual = await TestBed.runInInjectionContext(async () =>
|
||||
organizationPermissionsGuard()(route, state),
|
||||
);
|
||||
it("permits navigation if no permissions are specified", async () => {
|
||||
const actual = await TestBed.runInInjectionContext(async () =>
|
||||
organizationPermissionsGuard()(route, state),
|
||||
);
|
||||
|
||||
expect(actual).toBe(true);
|
||||
});
|
||||
expect(actual).toBe(true);
|
||||
});
|
||||
|
||||
it("permits navigation if the user has permissions", async () => {
|
||||
const permissionsCallback = jest.fn();
|
||||
permissionsCallback.mockImplementation((_org) => true);
|
||||
const org = orgFactory();
|
||||
organizationService.organizations$.calledWith(userId).mockReturnValue(of([org]));
|
||||
|
||||
const actual = await TestBed.runInInjectionContext(
|
||||
async () => await organizationPermissionsGuard(permissionsCallback)(route, state),
|
||||
);
|
||||
|
||||
expect(permissionsCallback).toHaveBeenCalled();
|
||||
expect(actual).toBe(true);
|
||||
});
|
||||
|
||||
describe("if the user does not have permissions", () => {
|
||||
it("and there is no Item ID, block navigation", async () => {
|
||||
it("permits navigation if the user has permissions", async () => {
|
||||
const permissionsCallback = jest.fn();
|
||||
permissionsCallback.mockImplementation((_org) => false);
|
||||
|
||||
state = mock<RouterStateSnapshot>({
|
||||
root: mock<ActivatedRouteSnapshot>({
|
||||
queryParamMap: convertToParamMap({}),
|
||||
}),
|
||||
});
|
||||
|
||||
const org = orgFactory();
|
||||
organizationService.organizations$.calledWith(userId).mockReturnValue(of([org]));
|
||||
permissionsCallback.mockImplementation((_org) => true);
|
||||
|
||||
const actual = await TestBed.runInInjectionContext(
|
||||
async () => await organizationPermissionsGuard(permissionsCallback)(route, state),
|
||||
);
|
||||
|
||||
expect(permissionsCallback).toHaveBeenCalled();
|
||||
expect(actual).not.toBe(true);
|
||||
expect(permissionsCallback).toHaveBeenCalledWith(orgFactory({ id: targetOrgId }));
|
||||
expect(actual).toBe(true);
|
||||
});
|
||||
|
||||
it("and there is an Item ID, redirect to the item in the individual vault", async () => {
|
||||
state = mock<RouterStateSnapshot>({
|
||||
root: mock<ActivatedRouteSnapshot>({
|
||||
queryParamMap: convertToParamMap({
|
||||
itemId: "myItemId",
|
||||
describe("if the user does not have permissions", () => {
|
||||
it("and there is no Item ID, block navigation", async () => {
|
||||
const permissionsCallback = jest.fn();
|
||||
permissionsCallback.mockImplementation((_org) => false);
|
||||
|
||||
state = mock<RouterStateSnapshot>({
|
||||
root: mock<ActivatedRouteSnapshot>({
|
||||
queryParamMap: convertToParamMap({}),
|
||||
}),
|
||||
}),
|
||||
});
|
||||
const org = orgFactory();
|
||||
organizationService.organizations$.calledWith(userId).mockReturnValue(of([org]));
|
||||
});
|
||||
|
||||
const actual = await TestBed.runInInjectionContext(
|
||||
async () => await organizationPermissionsGuard((_org: Organization) => false)(route, state),
|
||||
);
|
||||
const actual = await TestBed.runInInjectionContext(
|
||||
async () => await organizationPermissionsGuard(permissionsCallback)(route, state),
|
||||
);
|
||||
|
||||
expect(router.createUrlTree).toHaveBeenCalledWith(["/vault"], {
|
||||
queryParams: { itemId: "myItemId" },
|
||||
expect(permissionsCallback).toHaveBeenCalledWith(orgFactory({ id: targetOrgId }));
|
||||
expect(actual).not.toBe(true);
|
||||
});
|
||||
|
||||
it("and there is an Item ID, redirect to the item in the individual vault", async () => {
|
||||
state = mock<RouterStateSnapshot>({
|
||||
root: mock<ActivatedRouteSnapshot>({
|
||||
queryParamMap: convertToParamMap({
|
||||
itemId: "myItemId",
|
||||
}),
|
||||
}),
|
||||
});
|
||||
|
||||
const actual = await TestBed.runInInjectionContext(
|
||||
async () =>
|
||||
await organizationPermissionsGuard((_org: Organization) => false)(route, state),
|
||||
);
|
||||
|
||||
expect(router.createUrlTree).toHaveBeenCalledWith(["/vault"], {
|
||||
queryParams: { itemId: "myItemId" },
|
||||
});
|
||||
expect(actual).not.toBe(true);
|
||||
});
|
||||
expect(actual).not.toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("given a disabled organization", () => {
|
||||
it("blocks navigation if user is not an owner", async () => {
|
||||
const org = orgFactory({
|
||||
const orgs = orgStateFactory({
|
||||
type: OrganizationUserType.Admin,
|
||||
enabled: false,
|
||||
});
|
||||
organizationService.organizations$.calledWith(userId).mockReturnValue(of([org]));
|
||||
organizationService.organizations$.calledWith(userId).mockReturnValue(of(orgs));
|
||||
|
||||
const actual = await TestBed.runInInjectionContext(
|
||||
async () => await organizationPermissionsGuard()(route, state),
|
||||
@@ -160,11 +167,12 @@ describe("Organization Permissions Guard", () => {
|
||||
});
|
||||
|
||||
it("permits navigation if user is an owner", async () => {
|
||||
const org = orgFactory({
|
||||
const orgs = orgStateFactory({
|
||||
type: OrganizationUserType.Owner,
|
||||
enabled: false,
|
||||
});
|
||||
organizationService.organizations$.calledWith(userId).mockReturnValue(of([org]));
|
||||
|
||||
organizationService.organizations$.calledWith(userId).mockReturnValue(of(orgs));
|
||||
|
||||
const actual = await TestBed.runInInjectionContext(
|
||||
async () => await organizationPermissionsGuard()(route, state),
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
Router,
|
||||
RouterStateSnapshot,
|
||||
} from "@angular/router";
|
||||
import { firstValueFrom, map } from "rxjs";
|
||||
import { firstValueFrom, switchMap } from "rxjs";
|
||||
|
||||
import {
|
||||
canAccessOrgAdmin,
|
||||
@@ -15,7 +15,9 @@ import {
|
||||
} from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { getById } from "@bitwarden/common/platform/misc";
|
||||
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
|
||||
import { ToastService } from "@bitwarden/components";
|
||||
|
||||
@@ -55,12 +57,12 @@ export function organizationPermissionsGuard(
|
||||
await syncService.fullSync(false);
|
||||
}
|
||||
|
||||
const userId = await firstValueFrom(accountService.activeAccount$.pipe(map((a) => a?.id)));
|
||||
|
||||
const org = await firstValueFrom(
|
||||
organizationService
|
||||
.organizations$(userId)
|
||||
.pipe(map((organizations) => organizations.find((org) => route.params.organizationId))),
|
||||
accountService.activeAccount$.pipe(
|
||||
getUserId,
|
||||
switchMap((userId) => organizationService.organizations$(userId)),
|
||||
getById(route.params.organizationId),
|
||||
),
|
||||
);
|
||||
|
||||
if (org == null) {
|
||||
|
||||
@@ -9,6 +9,26 @@
|
||||
</div>
|
||||
|
||||
<app-danger-zone>
|
||||
<ng-container *ngIf="showSetNewDeviceLoginProtection$ | async">
|
||||
<button
|
||||
*ngIf="verifyNewDeviceLogin"
|
||||
type="button"
|
||||
bitButton
|
||||
buttonType="danger"
|
||||
[bitAction]="setNewDeviceLoginProtection"
|
||||
>
|
||||
{{ "turnOffNewDeviceLoginProtection" | i18n }}
|
||||
</button>
|
||||
<button
|
||||
*ngIf="!verifyNewDeviceLogin"
|
||||
type="button"
|
||||
bitButton
|
||||
buttonType="secondary"
|
||||
[bitAction]="setNewDeviceLoginProtection"
|
||||
>
|
||||
{{ "turnOnNewDeviceLoginProtection" | i18n }}
|
||||
</button>
|
||||
</ng-container>
|
||||
<button type="button" bitButton buttonType="danger" (click)="deauthorizeSessions()">
|
||||
{{ "deauthorizeSessions" | i18n }}
|
||||
</button>
|
||||
@@ -32,7 +52,6 @@
|
||||
</button>
|
||||
</app-danger-zone>
|
||||
|
||||
<ng-template #deauthorizeSessionsTemplate></ng-template>
|
||||
<ng-template #viewUserApiKeyTemplate></ng-template>
|
||||
<ng-template #rotateUserApiKeyTemplate></ng-template>
|
||||
</bit-container>
|
||||
|
||||
@@ -1,9 +1,15 @@
|
||||
// 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, firstValueFrom, from, lastValueFrom, map, Observable } from "rxjs";
|
||||
import { Component, OnInit, OnDestroy } from "@angular/core";
|
||||
import {
|
||||
combineLatest,
|
||||
firstValueFrom,
|
||||
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";
|
||||
@@ -16,31 +22,35 @@ 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 {
|
||||
@ViewChild("deauthorizeSessionsTemplate", { read: ViewContainerRef, static: true })
|
||||
deauthModalRef: ViewContainerRef;
|
||||
export class AccountComponent implements OnInit, OnDestroy {
|
||||
private destroy$ = new Subject<void>();
|
||||
|
||||
showChangeEmail$: Observable<boolean>;
|
||||
showPurgeVault$: Observable<boolean>;
|
||||
showDeleteAccount$: Observable<boolean>;
|
||||
showChangeEmail$: Observable<boolean> = new Observable();
|
||||
showPurgeVault$: Observable<boolean> = new Observable();
|
||||
showDeleteAccount$: Observable<boolean> = new Observable();
|
||||
showSetNewDeviceLoginProtection$: Observable<boolean> = new Observable();
|
||||
verifyNewDeviceLogin: boolean = true;
|
||||
|
||||
constructor(
|
||||
private modalService: ModalService,
|
||||
private accountService: AccountService,
|
||||
private dialogService: DialogService,
|
||||
private userVerificationService: UserVerificationService,
|
||||
private configService: ConfigService,
|
||||
private organizationService: OrganizationService,
|
||||
private accountService: AccountService,
|
||||
) {}
|
||||
|
||||
async ngOnInit() {
|
||||
const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$));
|
||||
|
||||
this.showSetNewDeviceLoginProtection$ = this.configService.getFeatureFlag$(
|
||||
FeatureFlag.NewDeviceVerification,
|
||||
);
|
||||
const isAccountDeprovisioningEnabled$ = this.configService.getFeatureFlag$(
|
||||
FeatureFlag.AccountDeprovisioning,
|
||||
);
|
||||
@@ -83,11 +93,17 @@ export class AccountComponent implements OnInit {
|
||||
!isAccountDeprovisioningEnabled || !userIsManagedByOrganization,
|
||||
),
|
||||
);
|
||||
this.accountService.accountVerifyNewDeviceLogin$
|
||||
.pipe(takeUntil(this.destroy$))
|
||||
.subscribe((verifyDevices) => {
|
||||
this.verifyNewDeviceLogin = verifyDevices;
|
||||
});
|
||||
}
|
||||
|
||||
async deauthorizeSessions() {
|
||||
await this.modalService.openViewRef(DeauthorizeSessionsComponent, this.deauthModalRef);
|
||||
}
|
||||
deauthorizeSessions = async () => {
|
||||
const dialogRef = DeauthorizeSessionsComponent.open(this.dialogService);
|
||||
await lastValueFrom(dialogRef.closed);
|
||||
};
|
||||
|
||||
purgeVault = async () => {
|
||||
const dialogRef = PurgeVaultComponent.open(this.dialogService);
|
||||
@@ -98,4 +114,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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -1,38 +1,21 @@
|
||||
<div class="modal fade" role="dialog" aria-modal="true" aria-labelledby="deAuthTitle">
|
||||
<div class="modal-dialog modal-dialog-scrollable" role="document">
|
||||
<form
|
||||
class="modal-content"
|
||||
#form
|
||||
(ngSubmit)="submit()"
|
||||
[appApiAction]="formPromise"
|
||||
ngNativeValidate
|
||||
>
|
||||
<div class="modal-header">
|
||||
<h1 class="modal-title" id="deAuthTitle">{{ "deauthorizeSessions" | i18n }}</h1>
|
||||
<button
|
||||
type="button"
|
||||
class="close"
|
||||
data-dismiss="modal"
|
||||
appA11yTitle="{{ 'close' | i18n }}"
|
||||
>
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>{{ "deauthorizeSessionsDesc" | i18n }}</p>
|
||||
<bit-callout type="warning">{{ "deauthorizeSessionsWarning" | i18n }}</bit-callout>
|
||||
<app-user-verification [(ngModel)]="masterPassword" ngDefaultControl name="secret">
|
||||
</app-user-verification>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="submit" class="btn btn-danger btn-submit" [disabled]="form.loading">
|
||||
<i class="bwi bwi-spinner bwi-spin" title="{{ 'loading' | i18n }}" aria-hidden="true"></i>
|
||||
<span>{{ "deauthorizeSessions" | i18n }}</span>
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-secondary" data-dismiss="modal">
|
||||
{{ "close" | i18n }}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<form [formGroup]="deauthForm" [bitSubmit]="submit">
|
||||
<bit-dialog dialogSize="default" [title]="'deauthorizeSessions' | i18n">
|
||||
<ng-container bitDialogContent>
|
||||
<p bitTypography="body1">{{ "deauthorizeSessionsDesc" | i18n }}</p>
|
||||
<bit-callout type="warning">{{ "deauthorizeSessionsWarning" | i18n }}</bit-callout>
|
||||
<app-user-verification-form-input
|
||||
formControlName="verification"
|
||||
name="verification"
|
||||
[(invalidSecret)]="invalidSecret"
|
||||
></app-user-verification-form-input>
|
||||
</ng-container>
|
||||
<ng-container bitDialogFooter>
|
||||
<button bitButton bitFormButton type="submit" buttonType="danger">
|
||||
{{ "deauthorizeSessions" | i18n }}
|
||||
</button>
|
||||
<button bitButton bitFormButton type="button" buttonType="secondary" bitDialogClose>
|
||||
{{ "close" | i18n }}
|
||||
</button>
|
||||
</ng-container>
|
||||
</bit-dialog>
|
||||
</form>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Component } from "@angular/core";
|
||||
import { FormBuilder } from "@angular/forms";
|
||||
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
|
||||
@@ -8,33 +7,33 @@ import { Verification } from "@bitwarden/common/auth/types/verification";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { ToastService } from "@bitwarden/components";
|
||||
import { DialogService, ToastService } from "@bitwarden/components";
|
||||
|
||||
@Component({
|
||||
selector: "app-deauthorize-sessions",
|
||||
templateUrl: "deauthorize-sessions.component.html",
|
||||
})
|
||||
export class DeauthorizeSessionsComponent {
|
||||
masterPassword: Verification;
|
||||
formPromise: Promise<unknown>;
|
||||
deauthForm = this.formBuilder.group({
|
||||
verification: undefined as Verification | undefined,
|
||||
});
|
||||
invalidSecret: boolean = false;
|
||||
|
||||
constructor(
|
||||
private apiService: ApiService,
|
||||
private i18nService: I18nService,
|
||||
private platformUtilsService: PlatformUtilsService,
|
||||
private formBuilder: FormBuilder,
|
||||
private userVerificationService: UserVerificationService,
|
||||
private messagingService: MessagingService,
|
||||
private logService: LogService,
|
||||
private toastService: ToastService,
|
||||
) {}
|
||||
|
||||
async submit() {
|
||||
submit = async () => {
|
||||
try {
|
||||
this.formPromise = this.userVerificationService
|
||||
.buildRequest(this.masterPassword)
|
||||
.then((request) => this.apiService.postSecurityStamp(request));
|
||||
await this.formPromise;
|
||||
const verification: Verification = this.deauthForm.value.verification!;
|
||||
const request = await this.userVerificationService.buildRequest(verification);
|
||||
await this.apiService.postSecurityStamp(request);
|
||||
this.toastService.showToast({
|
||||
variant: "success",
|
||||
title: this.i18nService.t("sessionsDeauthorized"),
|
||||
@@ -44,5 +43,9 @@ export class DeauthorizeSessionsComponent {
|
||||
} catch (e) {
|
||||
this.logService.error(e);
|
||||
}
|
||||
};
|
||||
|
||||
static open(dialogService: DialogService) {
|
||||
return dialogService.open(DeauthorizeSessionsComponent);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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({
|
||||
@@ -22,7 +21,6 @@ export class DeleteAccountDialogComponent {
|
||||
|
||||
constructor(
|
||||
private i18nService: I18nService,
|
||||
private platformUtilsService: PlatformUtilsService,
|
||||
private formBuilder: FormBuilder,
|
||||
private accountApiService: AccountApiService,
|
||||
private dialogRef: DialogRef,
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
<form [formGroup]="setVerifyDevicesForm" [bitSubmit]="submit">
|
||||
<bit-dialog dialogSize="default" [title]="'newDeviceLoginProtection' | i18n">
|
||||
<ng-container bitDialogContent>
|
||||
<p *ngIf="verifyNewDeviceLogin" bitTypography="body1">
|
||||
{{ "turnOffNewDeviceLoginProtectionModalDesc" | i18n }}
|
||||
</p>
|
||||
<p *ngIf="!verifyNewDeviceLogin" bitTypography="body1">
|
||||
{{ "turnOnNewDeviceLoginProtectionModalDesc" | i18n }}
|
||||
</p>
|
||||
<bit-callout *ngIf="verifyNewDeviceLogin && !has2faConfigured" type="warning">{{
|
||||
"turnOffNewDeviceLoginProtectionWarning" | i18n
|
||||
}}</bit-callout>
|
||||
<app-user-verification-form-input
|
||||
formControlName="verification"
|
||||
name="verification"
|
||||
[(invalidSecret)]="invalidSecret"
|
||||
></app-user-verification-form-input>
|
||||
</ng-container>
|
||||
<ng-container bitDialogFooter>
|
||||
<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 }}
|
||||
</button>
|
||||
</ng-container>
|
||||
</bit-dialog>
|
||||
</form>
|
||||
@@ -0,0 +1,122 @@
|
||||
import { DialogRef } from "@angular/cdk/dialog";
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { Component, OnDestroy, OnInit } from "@angular/core";
|
||||
import { FormBuilder, ReactiveFormsModule } from "@angular/forms";
|
||||
import { firstValueFrom, Subject, takeUntil } from "rxjs";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { UserVerificationFormInputComponent } from "@bitwarden/auth/angular";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
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 {
|
||||
AsyncActionsModule,
|
||||
ButtonModule,
|
||||
CalloutModule,
|
||||
DialogModule,
|
||||
DialogService,
|
||||
FormFieldModule,
|
||||
IconButtonModule,
|
||||
RadioButtonModule,
|
||||
SelectModule,
|
||||
ToastService,
|
||||
} from "@bitwarden/components";
|
||||
|
||||
@Component({
|
||||
templateUrl: "./set-account-verify-devices-dialog.component.html",
|
||||
standalone: true,
|
||||
imports: [
|
||||
CommonModule,
|
||||
ReactiveFormsModule,
|
||||
JslibModule,
|
||||
FormFieldModule,
|
||||
AsyncActionsModule,
|
||||
ButtonModule,
|
||||
IconButtonModule,
|
||||
SelectModule,
|
||||
CalloutModule,
|
||||
RadioButtonModule,
|
||||
DialogModule,
|
||||
UserVerificationFormInputComponent,
|
||||
],
|
||||
})
|
||||
export class SetAccountVerifyDevicesDialogComponent implements OnInit, 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;
|
||||
has2faConfigured: boolean = false;
|
||||
|
||||
setVerifyDevicesForm = this.formBuilder.group({
|
||||
verification: undefined as Verification | undefined,
|
||||
});
|
||||
invalidSecret: boolean = false;
|
||||
|
||||
constructor(
|
||||
private i18nService: I18nService,
|
||||
private formBuilder: FormBuilder,
|
||||
private accountApiService: AccountApiService,
|
||||
private accountService: AccountService,
|
||||
private userVerificationService: UserVerificationService,
|
||||
private dialogRef: DialogRef,
|
||||
private toastService: ToastService,
|
||||
private apiService: ApiService,
|
||||
) {
|
||||
this.accountService.accountVerifyNewDeviceLogin$
|
||||
.pipe(takeUntil(this.destroy$))
|
||||
.subscribe((verifyDevices: boolean) => {
|
||||
this.verifyNewDeviceLogin = verifyDevices;
|
||||
});
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
const twoFactorProviders = await this.apiService.getTwoFactorProviders();
|
||||
this.has2faConfigured = twoFactorProviders.data.length > 0;
|
||||
}
|
||||
|
||||
submit = async () => {
|
||||
try {
|
||||
const activeAccount = await firstValueFrom(
|
||||
this.accountService.activeAccount$.pipe(takeUntil(this.destroy$)),
|
||||
);
|
||||
const verification: Verification = this.setVerifyDevicesForm.value.verification!;
|
||||
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.setAccountVerifyNewDeviceLogin(
|
||||
activeAccount!.id,
|
||||
request.verifyDevices,
|
||||
);
|
||||
this.dialogRef.close();
|
||||
this.toastService.showToast({
|
||||
variant: "success",
|
||||
title: "",
|
||||
message: this.i18nService.t("accountNewDeviceLoginProtectionSaved"),
|
||||
});
|
||||
} catch (e) {
|
||||
if (e instanceof ErrorResponse && e.statusCode === 400) {
|
||||
this.invalidSecret = true;
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
|
||||
static open(dialogService: DialogService) {
|
||||
return dialogService.open(SetAccountVerifyDevicesDialogComponent);
|
||||
}
|
||||
|
||||
// closes subscription leaks
|
||||
ngOnDestroy() {
|
||||
this.destroy$.next();
|
||||
this.destroy$.complete();
|
||||
}
|
||||
}
|
||||
@@ -69,6 +69,7 @@ import { SdkClientFactory } from "@bitwarden/common/platform/abstractions/sdk/sd
|
||||
import { SdkLoadService } from "@bitwarden/common/platform/abstractions/sdk/sdk-load.service";
|
||||
import { AbstractStorageService } from "@bitwarden/common/platform/abstractions/storage.service";
|
||||
import { ThemeType } from "@bitwarden/common/platform/enums";
|
||||
// eslint-disable-next-line no-restricted-imports -- Needed for DI
|
||||
import {
|
||||
UnsupportedWebPushConnectionService,
|
||||
WebPushConnectionService,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { concat, defer, fromEvent, map, Observable, of, switchMap } from "rxjs";
|
||||
|
||||
import { SupportStatus } from "@bitwarden/common/platform/misc/support-status";
|
||||
// eslint-disable-next-line no-restricted-imports -- In platform owned code.
|
||||
import {
|
||||
WebPushConnector,
|
||||
WorkerWebPushConnectionService,
|
||||
|
||||
@@ -1792,7 +1792,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."
|
||||
},
|
||||
@@ -1860,12 +1860,6 @@
|
||||
"dangerZone": {
|
||||
"message": "Danger zone"
|
||||
},
|
||||
"dangerZoneDesc": {
|
||||
"message": "Careful, these actions are not reversible!"
|
||||
},
|
||||
"dangerZoneDescSingular": {
|
||||
"message": "Careful, this action is not reversible!"
|
||||
},
|
||||
"deauthorizeSessions": {
|
||||
"message": "Deauthorize sessions"
|
||||
},
|
||||
@@ -1875,6 +1869,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"
|
||||
},
|
||||
|
||||
@@ -4,7 +4,11 @@
|
||||
</button>
|
||||
</app-header>
|
||||
|
||||
<p class="tw-text-muted tw-w-2/5" *ngIf="accountDeprovisioningEnabled$ | async">
|
||||
<p
|
||||
bitTypography="body1"
|
||||
class="tw-text-main tw-w-2/5"
|
||||
*ngIf="accountDeprovisioningEnabled$ | async"
|
||||
>
|
||||
{{ "claimedDomainsDesc" | i18n }}
|
||||
<a
|
||||
bitLink
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
<div class="tw-mt-4 tw-flex tw-flex-col" *ngIf="!loading && dataSource.data.length">
|
||||
<div class="tw-flex tw-justify-between tw-mb-4">
|
||||
<h2 bitTypography="h2">{{ "criticalApplications" | i18n }}</h2>
|
||||
<button bitButton buttonType="primary" type="button">
|
||||
<button *ngIf="isNotificationsFeatureEnabled" bitButton buttonType="primary" type="button">
|
||||
<i class="bwi bwi-envelope tw-mr-2"></i>
|
||||
{{ "requestPasswordChange" | i18n }}
|
||||
</button>
|
||||
|
||||
@@ -15,6 +15,8 @@ import {
|
||||
ApplicationHealthReportDetailWithCriticalFlag,
|
||||
ApplicationHealthReportSummary,
|
||||
} from "@bitwarden/bit-common/tools/reports/risk-insights/models/password-health";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { OrganizationId } from "@bitwarden/common/types/guid";
|
||||
import {
|
||||
@@ -47,8 +49,12 @@ export class CriticalApplicationsComponent implements OnInit {
|
||||
protected organizationId: string;
|
||||
protected applicationSummary = {} as ApplicationHealthReportSummary;
|
||||
noItemsIcon = Icons.Security;
|
||||
isNotificationsFeatureEnabled: boolean = false;
|
||||
|
||||
ngOnInit() {
|
||||
async ngOnInit() {
|
||||
this.isNotificationsFeatureEnabled = await this.configService.getFeatureFlag(
|
||||
FeatureFlag.EnableRiskInsightsNotifications,
|
||||
);
|
||||
this.organizationId = this.activatedRoute.snapshot.paramMap.get("organizationId") ?? "";
|
||||
combineLatest([
|
||||
this.dataService.applications$,
|
||||
@@ -111,6 +117,7 @@ export class CriticalApplicationsComponent implements OnInit {
|
||||
protected criticalAppsService: CriticalAppsService,
|
||||
protected reportService: RiskInsightsReportService,
|
||||
protected i18nService: I18nService,
|
||||
private configService: ConfigService,
|
||||
) {
|
||||
this.searchControl.valueChanges
|
||||
.pipe(debounceTime(200), takeUntilDestroyed())
|
||||
|
||||
@@ -563,7 +563,7 @@ const safeProviders: SafeProvider[] = [
|
||||
safeProvider({
|
||||
provide: InternalAccountService,
|
||||
useClass: AccountServiceImplementation,
|
||||
deps: [MessagingServiceAbstraction, LogService, GlobalStateProvider],
|
||||
deps: [MessagingServiceAbstraction, LogService, GlobalStateProvider, SingleUserStateProvider],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: AccountServiceAbstraction,
|
||||
|
||||
@@ -35,6 +35,8 @@ export class FakeAccountService implements AccountService {
|
||||
activeAccountSubject = new ReplaySubject<Account | null>(1);
|
||||
// eslint-disable-next-line rxjs/no-exposed-subjects -- test class
|
||||
accountActivitySubject = new ReplaySubject<Record<UserId, Date>>(1);
|
||||
// eslint-disable-next-line rxjs/no-exposed-subjects -- test class
|
||||
accountVerifyDevicesSubject = new ReplaySubject<boolean>(1);
|
||||
private _activeUserId: UserId;
|
||||
get activeUserId() {
|
||||
return this._activeUserId;
|
||||
@@ -42,6 +44,7 @@ export class FakeAccountService implements AccountService {
|
||||
accounts$ = this.accountsSubject.asObservable();
|
||||
activeAccount$ = this.activeAccountSubject.asObservable();
|
||||
accountActivity$ = this.accountActivitySubject.asObservable();
|
||||
accountVerifyNewDeviceLogin$ = this.accountVerifyDevicesSubject.asObservable();
|
||||
get sortedUserIds$() {
|
||||
return this.accountActivity$.pipe(
|
||||
map((activity) => {
|
||||
@@ -67,6 +70,11 @@ export class FakeAccountService implements AccountService {
|
||||
this.activeAccountSubject.next(null);
|
||||
this.accountActivitySubject.next(accountActivity);
|
||||
}
|
||||
|
||||
setAccountVerifyNewDeviceLogin(userId: UserId, verifyNewDeviceLogin: boolean): Promise<void> {
|
||||
return this.mock.setAccountVerifyNewDeviceLogin(userId, verifyNewDeviceLogin);
|
||||
}
|
||||
|
||||
setAccountActivity(userId: UserId, lastActivity: Date): Promise<void> {
|
||||
this.accountActivitySubject.next({
|
||||
...this.accountActivitySubject["_buffer"][0],
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { RegisterFinishRequest } from "../models/request/registration/register-finish.request";
|
||||
import { RegisterSendVerificationEmailRequest } from "../models/request/registration/register-send-verification-email.request";
|
||||
import { RegisterVerificationEmailClickedRequest } from "../models/request/registration/register-verification-email-clicked.request";
|
||||
import { SetVerifyDevicesRequest } from "../models/request/set-verify-devices.request";
|
||||
import { Verification } from "../types/verification";
|
||||
|
||||
export abstract class AccountApiService {
|
||||
@@ -18,7 +19,7 @@ export abstract class AccountApiService {
|
||||
*
|
||||
* @param request - The request object containing
|
||||
* information needed to send the verification email, such as the user's email address.
|
||||
* @returns A promise that resolves to a string tokencontaining the user's encrypted
|
||||
* @returns A promise that resolves to a string token containing the user's encrypted
|
||||
* information which must be submitted to complete registration or `null` if
|
||||
* email verification is enabled (users must get the token by clicking a
|
||||
* link in the email that will be sent to them).
|
||||
@@ -33,7 +34,7 @@ export abstract class AccountApiService {
|
||||
*
|
||||
* @param request - The request object containing the email verification token and the
|
||||
* user's email address (which is required to validate the token)
|
||||
* @returns A promise that resolves when the event is logged on the server succcessfully or a bad
|
||||
* @returns A promise that resolves when the event is logged on the server successfully or a bad
|
||||
* request if the token is invalid for any reason.
|
||||
*/
|
||||
abstract registerVerificationEmailClicked(
|
||||
@@ -50,4 +51,15 @@ export abstract class AccountApiService {
|
||||
* registration process is successfully completed.
|
||||
*/
|
||||
abstract registerFinish(request: RegisterFinishRequest): Promise<string>;
|
||||
|
||||
/**
|
||||
* Sets the [dbo].[User].[VerifyDevices] flag to true or false.
|
||||
*
|
||||
* @param request - The request object is a SecretVerificationRequest extension
|
||||
* that also contains the boolean value that the VerifyDevices property is being
|
||||
* set to.
|
||||
* @returns A promise that resolves when the process is successfully completed or
|
||||
* a bad request if secret verification fails.
|
||||
*/
|
||||
abstract setVerifyDevices(request: SetVerifyDevicesRequest): Promise<string>;
|
||||
}
|
||||
|
||||
@@ -43,6 +43,8 @@ export abstract class AccountService {
|
||||
* Observable of the last activity time for each account.
|
||||
*/
|
||||
accountActivity$: Observable<Record<UserId, Date>>;
|
||||
/** Observable of the new device login verification property for the account. */
|
||||
accountVerifyNewDeviceLogin$: Observable<boolean>;
|
||||
/** Account list in order of descending recency */
|
||||
sortedUserIds$: Observable<UserId[]>;
|
||||
/** Next account that is not the current active account */
|
||||
@@ -73,6 +75,15 @@ export abstract class AccountService {
|
||||
* @param emailVerified
|
||||
*/
|
||||
abstract setAccountEmailVerified(userId: UserId, emailVerified: boolean): Promise<void>;
|
||||
/**
|
||||
* updates the `accounts$` observable with the new VerifyNewDeviceLogin property for the account.
|
||||
* @param userId
|
||||
* @param VerifyNewDeviceLogin
|
||||
*/
|
||||
abstract setAccountVerifyNewDeviceLogin(
|
||||
userId: UserId,
|
||||
verifyNewDeviceLogin: boolean,
|
||||
): Promise<void>;
|
||||
/**
|
||||
* Updates the `activeAccount$` observable with the new active account.
|
||||
* @param userId
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
import { SecretVerificationRequest } from "./secret-verification.request";
|
||||
|
||||
export class SetVerifyDevicesRequest extends SecretVerificationRequest {
|
||||
/**
|
||||
* This is the input for a user update that controls [dbo].[Users].[VerifyDevices]
|
||||
*/
|
||||
verifyDevices!: boolean;
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import { UserVerificationService } from "../abstractions/user-verification/user-
|
||||
import { RegisterFinishRequest } from "../models/request/registration/register-finish.request";
|
||||
import { RegisterSendVerificationEmailRequest } from "../models/request/registration/register-send-verification-email.request";
|
||||
import { RegisterVerificationEmailClickedRequest } from "../models/request/registration/register-verification-email-clicked.request";
|
||||
import { SetVerifyDevicesRequest } from "../models/request/set-verify-devices.request";
|
||||
import { Verification } from "../types/verification";
|
||||
|
||||
export class AccountApiServiceImplementation implements AccountApiService {
|
||||
@@ -102,4 +103,21 @@ export class AccountApiServiceImplementation implements AccountApiService {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
async setVerifyDevices(request: SetVerifyDevicesRequest): Promise<string> {
|
||||
try {
|
||||
const response = await this.apiService.send(
|
||||
"POST",
|
||||
"/accounts/verify-devices",
|
||||
request,
|
||||
true,
|
||||
true,
|
||||
);
|
||||
|
||||
return response;
|
||||
} catch (e: unknown) {
|
||||
this.logService.error(e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,10 @@ import { MockProxy, mock } from "jest-mock-extended";
|
||||
import { firstValueFrom } from "rxjs";
|
||||
|
||||
import { FakeGlobalState } from "../../../spec/fake-state";
|
||||
import { FakeGlobalStateProvider } from "../../../spec/fake-state-provider";
|
||||
import {
|
||||
FakeGlobalStateProvider,
|
||||
FakeSingleUserStateProvider,
|
||||
} from "../../../spec/fake-state-provider";
|
||||
import { trackEmissions } from "../../../spec/utils";
|
||||
import { LogService } from "../../platform/abstractions/log.service";
|
||||
import { MessagingService } from "../../platform/abstractions/messaging.service";
|
||||
@@ -19,6 +22,7 @@ import {
|
||||
ACCOUNT_ACCOUNTS,
|
||||
ACCOUNT_ACTIVE_ACCOUNT_ID,
|
||||
ACCOUNT_ACTIVITY,
|
||||
ACCOUNT_VERIFY_NEW_DEVICE_LOGIN,
|
||||
AccountServiceImplementation,
|
||||
} from "./account.service";
|
||||
|
||||
@@ -66,6 +70,7 @@ describe("accountService", () => {
|
||||
let messagingService: MockProxy<MessagingService>;
|
||||
let logService: MockProxy<LogService>;
|
||||
let globalStateProvider: FakeGlobalStateProvider;
|
||||
let singleUserStateProvider: FakeSingleUserStateProvider;
|
||||
let sut: AccountServiceImplementation;
|
||||
let accountsState: FakeGlobalState<Record<UserId, AccountInfo>>;
|
||||
let activeAccountIdState: FakeGlobalState<UserId>;
|
||||
@@ -77,8 +82,14 @@ describe("accountService", () => {
|
||||
messagingService = mock();
|
||||
logService = mock();
|
||||
globalStateProvider = new FakeGlobalStateProvider();
|
||||
singleUserStateProvider = new FakeSingleUserStateProvider();
|
||||
|
||||
sut = new AccountServiceImplementation(messagingService, logService, globalStateProvider);
|
||||
sut = new AccountServiceImplementation(
|
||||
messagingService,
|
||||
logService,
|
||||
globalStateProvider,
|
||||
singleUserStateProvider,
|
||||
);
|
||||
|
||||
accountsState = globalStateProvider.getFake(ACCOUNT_ACCOUNTS);
|
||||
activeAccountIdState = globalStateProvider.getFake(ACCOUNT_ACTIVE_ACCOUNT_ID);
|
||||
@@ -128,6 +139,22 @@ describe("accountService", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("accountsVerifyNewDeviceLogin$", () => {
|
||||
it("returns expected value", async () => {
|
||||
// Arrange
|
||||
const expected = true;
|
||||
// we need to set this state since it is how we initialize the VerifyNewDeviceLogin$
|
||||
activeAccountIdState.stateSubject.next(userId);
|
||||
singleUserStateProvider.getFake(userId, ACCOUNT_VERIFY_NEW_DEVICE_LOGIN).nextState(expected);
|
||||
|
||||
// Act
|
||||
const result = await firstValueFrom(sut.accountVerifyNewDeviceLogin$);
|
||||
|
||||
// Assert
|
||||
expect(result).toEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe("addAccount", () => {
|
||||
it("should emit the new account", async () => {
|
||||
await sut.addAccount(userId, userInfo);
|
||||
@@ -226,6 +253,33 @@ describe("accountService", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("setAccountVerifyNewDeviceLogin", () => {
|
||||
const initialState = true;
|
||||
beforeEach(() => {
|
||||
activeAccountIdState.stateSubject.next(userId);
|
||||
singleUserStateProvider
|
||||
.getFake(userId, ACCOUNT_VERIFY_NEW_DEVICE_LOGIN)
|
||||
.nextState(initialState);
|
||||
});
|
||||
|
||||
it("should update the VerifyNewDeviceLogin", async () => {
|
||||
const expected = false;
|
||||
expect(await firstValueFrom(sut.accountVerifyNewDeviceLogin$)).toEqual(initialState);
|
||||
|
||||
await sut.setAccountVerifyNewDeviceLogin(userId, expected);
|
||||
const currentState = await firstValueFrom(sut.accountVerifyNewDeviceLogin$);
|
||||
|
||||
expect(currentState).toEqual(expected);
|
||||
});
|
||||
|
||||
it("should NOT update VerifyNewDeviceLogin when userId is null", async () => {
|
||||
await sut.setAccountVerifyNewDeviceLogin(null, false);
|
||||
const currentState = await firstValueFrom(sut.accountVerifyNewDeviceLogin$);
|
||||
|
||||
expect(currentState).toEqual(initialState);
|
||||
});
|
||||
});
|
||||
|
||||
describe("clean", () => {
|
||||
beforeEach(() => {
|
||||
accountsState.stateSubject.next({ [userId]: userInfo });
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
shareReplay,
|
||||
combineLatest,
|
||||
Observable,
|
||||
switchMap,
|
||||
filter,
|
||||
timeout,
|
||||
of,
|
||||
@@ -26,6 +27,8 @@ import {
|
||||
GlobalState,
|
||||
GlobalStateProvider,
|
||||
KeyDefinition,
|
||||
SingleUserStateProvider,
|
||||
UserKeyDefinition,
|
||||
} from "../../platform/state";
|
||||
import { UserId } from "../../types/guid";
|
||||
|
||||
@@ -45,6 +48,15 @@ export const ACCOUNT_ACTIVITY = KeyDefinition.record<Date, UserId>(ACCOUNT_DISK,
|
||||
deserializer: (activity) => new Date(activity),
|
||||
});
|
||||
|
||||
export const ACCOUNT_VERIFY_NEW_DEVICE_LOGIN = new UserKeyDefinition<boolean>(
|
||||
ACCOUNT_DISK,
|
||||
"verifyNewDeviceLogin",
|
||||
{
|
||||
deserializer: (verifyDevices) => verifyDevices,
|
||||
clearOn: ["logout"],
|
||||
},
|
||||
);
|
||||
|
||||
const LOGGED_OUT_INFO: AccountInfo = {
|
||||
email: "",
|
||||
emailVerified: false,
|
||||
@@ -76,6 +88,7 @@ export class AccountServiceImplementation implements InternalAccountService {
|
||||
accounts$: Observable<Record<UserId, AccountInfo>>;
|
||||
activeAccount$: Observable<Account | null>;
|
||||
accountActivity$: Observable<Record<UserId, Date>>;
|
||||
accountVerifyNewDeviceLogin$: Observable<boolean>;
|
||||
sortedUserIds$: Observable<UserId[]>;
|
||||
nextUpAccount$: Observable<Account>;
|
||||
|
||||
@@ -83,6 +96,7 @@ export class AccountServiceImplementation implements InternalAccountService {
|
||||
private messagingService: MessagingService,
|
||||
private logService: LogService,
|
||||
private globalStateProvider: GlobalStateProvider,
|
||||
private singleUserStateProvider: SingleUserStateProvider,
|
||||
) {
|
||||
this.accountsState = this.globalStateProvider.get(ACCOUNT_ACCOUNTS);
|
||||
this.activeAccountIdState = this.globalStateProvider.get(ACCOUNT_ACTIVE_ACCOUNT_ID);
|
||||
@@ -117,6 +131,12 @@ export class AccountServiceImplementation implements InternalAccountService {
|
||||
return nextId ? { id: nextId, ...accounts[nextId] } : null;
|
||||
}),
|
||||
);
|
||||
this.accountVerifyNewDeviceLogin$ = this.activeAccountIdState.state$.pipe(
|
||||
switchMap(
|
||||
(userId) =>
|
||||
this.singleUserStateProvider.get(userId, ACCOUNT_VERIFY_NEW_DEVICE_LOGIN).state$,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
async addAccount(userId: UserId, accountData: AccountInfo): Promise<void> {
|
||||
@@ -203,6 +223,20 @@ export class AccountServiceImplementation implements InternalAccountService {
|
||||
);
|
||||
}
|
||||
|
||||
async setAccountVerifyNewDeviceLogin(
|
||||
userId: UserId,
|
||||
setVerifyNewDeviceLogin: boolean,
|
||||
): Promise<void> {
|
||||
if (!Utils.isGuid(userId)) {
|
||||
// only store for valid userIds
|
||||
return;
|
||||
}
|
||||
|
||||
await this.singleUserStateProvider.get(userId, ACCOUNT_VERIFY_NEW_DEVICE_LOGIN).update(() => {
|
||||
return setVerifyNewDeviceLogin;
|
||||
});
|
||||
}
|
||||
|
||||
async removeAccountActivity(userId: UserId): Promise<void> {
|
||||
await this.globalStateProvider.get(ACCOUNT_ACTIVITY).update(
|
||||
(activity) => {
|
||||
|
||||
@@ -47,6 +47,7 @@ export enum FeatureFlag {
|
||||
PrivateKeyRegeneration = "pm-12241-private-key-regeneration",
|
||||
ResellerManagedOrgAlert = "PM-15814-alert-owners-of-reseller-managed-orgs",
|
||||
NewDeviceVerification = "new-device-verification",
|
||||
EnableRiskInsightsNotifications = "enable-risk-insights-notifications",
|
||||
}
|
||||
|
||||
export type AllowedFeatureFlagTypes = boolean | number | string;
|
||||
@@ -104,6 +105,7 @@ export const DefaultFeatureFlagValue = {
|
||||
[FeatureFlag.PrivateKeyRegeneration]: FALSE,
|
||||
[FeatureFlag.ResellerManagedOrgAlert]: FALSE,
|
||||
[FeatureFlag.NewDeviceVerification]: FALSE,
|
||||
[FeatureFlag.EnableRiskInsightsNotifications]: FALSE,
|
||||
} satisfies Record<FeatureFlag, AllowedFeatureFlagTypes>;
|
||||
|
||||
export type DefaultFeatureFlagValueType = typeof DefaultFeatureFlagValue;
|
||||
|
||||
@@ -21,6 +21,7 @@ export class ProfileResponse extends BaseResponse {
|
||||
securityStamp: string;
|
||||
forcePasswordReset: boolean;
|
||||
usesKeyConnector: boolean;
|
||||
verifyDevices: boolean;
|
||||
organizations: ProfileOrganizationResponse[] = [];
|
||||
providers: ProfileProviderResponse[] = [];
|
||||
providerOrganizations: ProfileProviderOrganizationResponse[] = [];
|
||||
@@ -42,6 +43,7 @@ export class ProfileResponse extends BaseResponse {
|
||||
this.securityStamp = this.getResponseProperty("SecurityStamp");
|
||||
this.forcePasswordReset = this.getResponseProperty("ForcePasswordReset") ?? false;
|
||||
this.usesKeyConnector = this.getResponseProperty("UsesKeyConnector") ?? false;
|
||||
this.verifyDevices = this.getResponseProperty("VerifyDevices") ?? true;
|
||||
|
||||
const organizations = this.getResponseProperty("Organizations");
|
||||
if (organizations != null) {
|
||||
|
||||
@@ -197,6 +197,7 @@ export class DefaultSyncService extends CoreSyncService {
|
||||
await this.avatarService.setSyncAvatarColor(response.id, response.avatarColor);
|
||||
await this.tokenService.setSecurityStamp(response.securityStamp, response.id);
|
||||
await this.accountService.setAccountEmailVerified(response.id, response.emailVerified);
|
||||
await this.accountService.setAccountVerifyNewDeviceLogin(response.id, response.verifyDevices);
|
||||
|
||||
await this.billingAccountProfileStateService.setHasPremium(
|
||||
response.premiumPersonally,
|
||||
|
||||
824
package-lock.json
generated
824
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
20
package.json
20
package.json
@@ -46,16 +46,16 @@
|
||||
"@electron/notarize": "2.5.0",
|
||||
"@electron/rebuild": "3.7.1",
|
||||
"@ngtools/webpack": "18.2.12",
|
||||
"@storybook/addon-a11y": "8.4.7",
|
||||
"@storybook/addon-actions": "8.4.7",
|
||||
"@storybook/addon-a11y": "8.5.2",
|
||||
"@storybook/addon-actions": "8.5.2",
|
||||
"@storybook/addon-designs": "8.0.4",
|
||||
"@storybook/addon-essentials": "8.4.7",
|
||||
"@storybook/addon-interactions": "8.4.7",
|
||||
"@storybook/addon-links": "8.4.7",
|
||||
"@storybook/angular": "8.4.7",
|
||||
"@storybook/manager-api": "8.4.7",
|
||||
"@storybook/theming": "8.4.7",
|
||||
"@storybook/web-components-webpack5": "8.4.7",
|
||||
"@storybook/addon-essentials": "8.5.2",
|
||||
"@storybook/addon-interactions": "8.5.2",
|
||||
"@storybook/addon-links": "8.5.2",
|
||||
"@storybook/angular": "8.5.2",
|
||||
"@storybook/manager-api": "8.5.2",
|
||||
"@storybook/theming": "8.5.2",
|
||||
"@storybook/web-components-webpack5": "8.5.2",
|
||||
"@types/argon2-browser": "1.18.4",
|
||||
"@types/chrome": "0.0.280",
|
||||
"@types/firefox-webext-browser": "120.0.4",
|
||||
@@ -124,7 +124,7 @@
|
||||
"rimraf": "6.0.1",
|
||||
"sass": "1.83.4",
|
||||
"sass-loader": "16.0.4",
|
||||
"storybook": "8.4.7",
|
||||
"storybook": "8.5.2",
|
||||
"style-loader": "4.0.0",
|
||||
"tailwindcss": "3.4.17",
|
||||
"ts-jest": "29.2.2",
|
||||
|
||||
Reference in New Issue
Block a user