mirror of
https://github.com/bitwarden/browser
synced 2025-12-06 00:13:28 +00:00
[PM-18485] Remove new device verification guard (#14417)
* remove NewDeviceVerificationGuard and all associated entities. New Device verification feature has rolled out in production, this guard is no longer needed. * remove unused properties from the vault profile service
This commit is contained in:
@@ -5135,42 +5135,6 @@
|
||||
"beta": {
|
||||
"message": "Beta"
|
||||
},
|
||||
"importantNotice": {
|
||||
"message": "Important notice"
|
||||
},
|
||||
"setupTwoStepLogin": {
|
||||
"message": "Set up two-step login"
|
||||
},
|
||||
"newDeviceVerificationNoticeContentPage1": {
|
||||
"message": "Bitwarden will send a code to your account email to verify logins from new devices starting in February 2025."
|
||||
},
|
||||
"newDeviceVerificationNoticeContentPage2": {
|
||||
"message": "You can set up two-step login as an alternative way to protect your account or change your email to one you can access."
|
||||
},
|
||||
"remindMeLater": {
|
||||
"message": "Remind me later"
|
||||
},
|
||||
"newDeviceVerificationNoticePageOneFormContent": {
|
||||
"message": "Do you have reliable access to your email, $EMAIL$?",
|
||||
"placeholders": {
|
||||
"email": {
|
||||
"content": "$1",
|
||||
"example": "your_name@email.com"
|
||||
}
|
||||
}
|
||||
},
|
||||
"newDeviceVerificationNoticePageOneEmailAccessNo": {
|
||||
"message": "No, I do not"
|
||||
},
|
||||
"newDeviceVerificationNoticePageOneEmailAccessYes": {
|
||||
"message": "Yes, I can reliably access my email"
|
||||
},
|
||||
"turnOnTwoStepLogin": {
|
||||
"message": "Turn on two-step login"
|
||||
},
|
||||
"changeAcctEmail": {
|
||||
"message": "Change account email"
|
||||
},
|
||||
"extensionWidth": {
|
||||
"message": "Extension width"
|
||||
},
|
||||
|
||||
@@ -15,7 +15,6 @@ import {
|
||||
tdeDecryptionRequiredGuard,
|
||||
unauthGuardFn,
|
||||
} from "@bitwarden/angular/auth/guards";
|
||||
import { NewDeviceVerificationNoticeGuard } from "@bitwarden/angular/vault/guards";
|
||||
import {
|
||||
AnonLayoutWrapperComponent,
|
||||
AnonLayoutWrapperData,
|
||||
@@ -43,11 +42,6 @@ import {
|
||||
VaultIcon,
|
||||
} from "@bitwarden/auth/angular";
|
||||
import { LockComponent } from "@bitwarden/key-management-ui";
|
||||
import {
|
||||
NewDeviceVerificationNoticePageOneComponent,
|
||||
NewDeviceVerificationNoticePageTwoComponent,
|
||||
VaultIcons,
|
||||
} from "@bitwarden/vault";
|
||||
|
||||
import { fido2AuthGuard } from "../auth/guards/fido2-auth.guard";
|
||||
import { AccountSwitcherComponent } from "../auth/popup/account-switching/account-switcher.component";
|
||||
@@ -605,34 +599,6 @@ const routes: Routes = [
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: "new-device-notice",
|
||||
component: ExtensionAnonLayoutWrapperComponent,
|
||||
canActivate: [],
|
||||
children: [
|
||||
{
|
||||
path: "",
|
||||
component: NewDeviceVerificationNoticePageOneComponent,
|
||||
data: {
|
||||
pageIcon: VaultIcons.ExclamationTriangle,
|
||||
pageTitle: {
|
||||
key: "importantNotice",
|
||||
},
|
||||
hideFooter: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: "setup",
|
||||
component: NewDeviceVerificationNoticePageTwoComponent,
|
||||
data: {
|
||||
pageIcon: VaultIcons.UserLock,
|
||||
pageTitle: {
|
||||
key: "setupTwoStepLogin",
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: "tabs",
|
||||
component: TabsV2Component,
|
||||
@@ -650,7 +616,7 @@ const routes: Routes = [
|
||||
{
|
||||
path: "vault",
|
||||
component: VaultV2Component,
|
||||
canActivate: [authGuard, NewDeviceVerificationNoticeGuard],
|
||||
canActivate: [authGuard],
|
||||
canDeactivate: [clearVaultStateGuard],
|
||||
data: { elevation: 0 } satisfies RouteDataProperties,
|
||||
},
|
||||
|
||||
@@ -15,7 +15,6 @@ import {
|
||||
unauthGuardFn,
|
||||
} from "@bitwarden/angular/auth/guards";
|
||||
import { featureFlaggedRoute } from "@bitwarden/angular/platform/utils/feature-flagged-route";
|
||||
import { NewDeviceVerificationNoticeGuard } from "@bitwarden/angular/vault/guards";
|
||||
import {
|
||||
AnonLayoutWrapperComponent,
|
||||
AnonLayoutWrapperData,
|
||||
@@ -44,11 +43,6 @@ import {
|
||||
} from "@bitwarden/auth/angular";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { LockComponent } from "@bitwarden/key-management-ui";
|
||||
import {
|
||||
NewDeviceVerificationNoticePageOneComponent,
|
||||
NewDeviceVerificationNoticePageTwoComponent,
|
||||
VaultIcons,
|
||||
} from "@bitwarden/vault";
|
||||
|
||||
import { AccessibilityCookieComponent } from "../auth/accessibility-cookie.component";
|
||||
import { maxAccountsGuardFn } from "../auth/guards/max-accounts.guard";
|
||||
@@ -108,40 +102,13 @@ const routes: Routes = [
|
||||
},
|
||||
} satisfies RouteDataProperties & AnonLayoutWrapperData,
|
||||
},
|
||||
{
|
||||
path: "new-device-notice",
|
||||
component: AnonLayoutWrapperComponent,
|
||||
canActivate: [],
|
||||
children: [
|
||||
{
|
||||
path: "",
|
||||
component: NewDeviceVerificationNoticePageOneComponent,
|
||||
data: {
|
||||
pageIcon: VaultIcons.ExclamationTriangle,
|
||||
pageTitle: {
|
||||
key: "importantNotice",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
path: "setup",
|
||||
component: NewDeviceVerificationNoticePageTwoComponent,
|
||||
data: {
|
||||
pageIcon: VaultIcons.UserLock,
|
||||
pageTitle: {
|
||||
key: "setupTwoStepLogin",
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
...featureFlaggedRoute({
|
||||
defaultComponent: VaultComponent,
|
||||
flaggedComponent: VaultV2Component,
|
||||
featureFlag: FeatureFlag.PM18520_UpdateDesktopCipherForm,
|
||||
routeOptions: {
|
||||
path: "vault",
|
||||
canActivate: [authGuard, NewDeviceVerificationNoticeGuard],
|
||||
canActivate: [authGuard],
|
||||
},
|
||||
}),
|
||||
{ path: "accessibility-cookie", component: AccessibilityCookieComponent },
|
||||
|
||||
@@ -3689,63 +3689,6 @@
|
||||
"fileSavedToDevice": {
|
||||
"message": "File saved to device. Manage from your device downloads."
|
||||
},
|
||||
"importantNotice": {
|
||||
"message": "Important notice"
|
||||
},
|
||||
"setupTwoStepLogin": {
|
||||
"message": "Set up two-step login"
|
||||
},
|
||||
"itemDetails": {
|
||||
"message": "Item details"
|
||||
},
|
||||
"itemName": {
|
||||
"message": "Item name"
|
||||
},
|
||||
"loginCredentials": {
|
||||
"message": "Login credentials"
|
||||
},
|
||||
"additionalOptions": {
|
||||
"message": "Additional options"
|
||||
},
|
||||
"itemHistory": {
|
||||
"message": "Item history"
|
||||
},
|
||||
"lastEdited": {
|
||||
"message": "Last edited"
|
||||
},
|
||||
"upload": {
|
||||
"message": "Upload"
|
||||
},
|
||||
"newDeviceVerificationNoticeContentPage1": {
|
||||
"message": "Bitwarden will send a code to your account email to verify logins from new devices starting in February 2025."
|
||||
},
|
||||
"newDeviceVerificationNoticeContentPage2": {
|
||||
"message": "You can set up two-step login as an alternative way to protect your account or change your email to one you can access."
|
||||
},
|
||||
"remindMeLater": {
|
||||
"message": "Remind me later"
|
||||
},
|
||||
"newDeviceVerificationNoticePageOneFormContent": {
|
||||
"message": "Do you have reliable access to your email, $EMAIL$?",
|
||||
"placeholders": {
|
||||
"email": {
|
||||
"content": "$1",
|
||||
"example": "your_name@email.com"
|
||||
}
|
||||
}
|
||||
},
|
||||
"newDeviceVerificationNoticePageOneEmailAccessNo": {
|
||||
"message": "No, I do not"
|
||||
},
|
||||
"newDeviceVerificationNoticePageOneEmailAccessYes": {
|
||||
"message": "Yes, I can reliably access my email"
|
||||
},
|
||||
"turnOnTwoStepLogin": {
|
||||
"message": "Turn on two-step login"
|
||||
},
|
||||
"changeAcctEmail": {
|
||||
"message": "Change account email"
|
||||
},
|
||||
"allowScreenshots": {
|
||||
"message": "Allow screen capture"
|
||||
},
|
||||
|
||||
@@ -16,7 +16,6 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl
|
||||
import { ToastService } from "@bitwarden/components";
|
||||
import { KeyService } from "@bitwarden/key-management";
|
||||
import { I18nPipe } from "@bitwarden/ui-common";
|
||||
import { NewDeviceVerificationNoticeService } from "@bitwarden/vault";
|
||||
|
||||
import { RecoverTwoFactorComponent } from "./recover-two-factor.component";
|
||||
|
||||
@@ -35,7 +34,6 @@ describe("RecoverTwoFactorComponent", () => {
|
||||
let mockConfigService: MockProxy<ConfigService>;
|
||||
let mockLoginSuccessHandlerService: MockProxy<LoginSuccessHandlerService>;
|
||||
let mockLogService: MockProxy<LogService>;
|
||||
let mockNewDeviceVerificationNoticeService: MockProxy<NewDeviceVerificationNoticeService>;
|
||||
|
||||
beforeEach(() => {
|
||||
mockRouter = mock<Router>();
|
||||
@@ -48,7 +46,6 @@ describe("RecoverTwoFactorComponent", () => {
|
||||
mockConfigService = mock<ConfigService>();
|
||||
mockLoginSuccessHandlerService = mock<LoginSuccessHandlerService>();
|
||||
mockLogService = mock<LogService>();
|
||||
mockNewDeviceVerificationNoticeService = mock<NewDeviceVerificationNoticeService>();
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [RecoverTwoFactorComponent],
|
||||
@@ -63,10 +60,6 @@ describe("RecoverTwoFactorComponent", () => {
|
||||
{ provide: ConfigService, useValue: mockConfigService },
|
||||
{ provide: LoginSuccessHandlerService, useValue: mockLoginSuccessHandlerService },
|
||||
{ provide: LogService, useValue: mockLogService },
|
||||
{
|
||||
provide: NewDeviceVerificationNoticeService,
|
||||
useValue: mockNewDeviceVerificationNoticeService,
|
||||
},
|
||||
],
|
||||
imports: [I18nPipe],
|
||||
// FIXME(PM-18598): Replace unknownElements and unknownProperties with actual imports
|
||||
@@ -102,9 +95,6 @@ describe("RecoverTwoFactorComponent", () => {
|
||||
title: "",
|
||||
message: mockI18nService.t("youHaveBeenLoggedIn"),
|
||||
});
|
||||
expect(
|
||||
mockNewDeviceVerificationNoticeService.updateNewDeviceVerificationSkipNoticeState,
|
||||
).toHaveBeenCalledWith(authResult.userId, true);
|
||||
expect(mockRouter.navigate).toHaveBeenCalledWith(["/settings/security/two-factor"]);
|
||||
});
|
||||
|
||||
|
||||
@@ -12,7 +12,6 @@ import { TokenTwoFactorRequest } from "@bitwarden/common/auth/models/request/ide
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { ToastService } from "@bitwarden/components";
|
||||
import { NewDeviceVerificationNoticeService } from "@bitwarden/vault";
|
||||
|
||||
@Component({
|
||||
selector: "app-recover-two-factor",
|
||||
@@ -37,7 +36,6 @@ export class RecoverTwoFactorComponent implements OnInit {
|
||||
private toastService: ToastService,
|
||||
private loginSuccessHandlerService: LoginSuccessHandlerService,
|
||||
private logService: LogService,
|
||||
private newDeviceVerificationNoticeService: NewDeviceVerificationNoticeService,
|
||||
) {}
|
||||
|
||||
async ngOnInit() {
|
||||
@@ -104,13 +102,6 @@ export class RecoverTwoFactorComponent implements OnInit {
|
||||
|
||||
await this.loginSuccessHandlerService.run(authResult.userId);
|
||||
|
||||
// Before routing, set the state to skip the new device notification. This is a temporary
|
||||
// fix and will be cleaned up in PM-18485.
|
||||
await this.newDeviceVerificationNoticeService.updateNewDeviceVerificationSkipNoticeState(
|
||||
authResult.userId,
|
||||
true,
|
||||
);
|
||||
|
||||
await this.router.navigate(["/settings/security/two-factor"]);
|
||||
} catch (error) {
|
||||
// If login errors, redirect to login page per product. Don't show error
|
||||
|
||||
@@ -10,7 +10,6 @@ import {
|
||||
unauthGuardFn,
|
||||
activeAuthGuard,
|
||||
} from "@bitwarden/angular/auth/guards";
|
||||
import { NewDeviceVerificationNoticeGuard } from "@bitwarden/angular/vault/guards";
|
||||
import {
|
||||
AnonLayoutWrapperComponent,
|
||||
AnonLayoutWrapperData,
|
||||
@@ -41,11 +40,7 @@ import {
|
||||
DeviceVerificationIcon,
|
||||
} from "@bitwarden/auth/angular";
|
||||
import { LockComponent } from "@bitwarden/key-management-ui";
|
||||
import {
|
||||
NewDeviceVerificationNoticePageOneComponent,
|
||||
NewDeviceVerificationNoticePageTwoComponent,
|
||||
VaultIcons,
|
||||
} from "@bitwarden/vault";
|
||||
import { VaultIcons } from "@bitwarden/vault";
|
||||
|
||||
import { flagEnabled, Flags } from "../utils/flags";
|
||||
|
||||
@@ -586,37 +581,10 @@ const routes: Routes = [
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: "new-device-notice",
|
||||
component: AnonLayoutWrapperComponent,
|
||||
canActivate: [],
|
||||
children: [
|
||||
{
|
||||
path: "",
|
||||
component: NewDeviceVerificationNoticePageOneComponent,
|
||||
data: {
|
||||
pageIcon: VaultIcons.ExclamationTriangle,
|
||||
pageTitle: {
|
||||
key: "importantNotice",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
path: "setup",
|
||||
component: NewDeviceVerificationNoticePageTwoComponent,
|
||||
data: {
|
||||
pageIcon: VaultIcons.UserLock,
|
||||
pageTitle: {
|
||||
key: "setupTwoStepLogin",
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: "",
|
||||
component: UserLayoutComponent,
|
||||
canActivate: [deepLinkGuard(), authGuard, NewDeviceVerificationNoticeGuard],
|
||||
canActivate: [deepLinkGuard(), authGuard],
|
||||
children: [
|
||||
{
|
||||
path: "vault",
|
||||
|
||||
@@ -10246,42 +10246,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"importantNotice": {
|
||||
"message": "Important notice"
|
||||
},
|
||||
"setupTwoStepLogin": {
|
||||
"message": "Set up two-step login"
|
||||
},
|
||||
"newDeviceVerificationNoticeContentPage1": {
|
||||
"message": "Bitwarden will send a code to your account email to verify logins from new devices starting in February 2025."
|
||||
},
|
||||
"newDeviceVerificationNoticeContentPage2": {
|
||||
"message": "You can set up two-step login as an alternative way to protect your account or change your email to one you can access."
|
||||
},
|
||||
"remindMeLater": {
|
||||
"message": "Remind me later"
|
||||
},
|
||||
"newDeviceVerificationNoticePageOneFormContent": {
|
||||
"message": "Do you have reliable access to your email, $EMAIL$?",
|
||||
"placeholders": {
|
||||
"email": {
|
||||
"content": "$1",
|
||||
"example": "your_name@email.com"
|
||||
}
|
||||
}
|
||||
},
|
||||
"newDeviceVerificationNoticePageOneEmailAccessNo": {
|
||||
"message": "No, I do not"
|
||||
},
|
||||
"newDeviceVerificationNoticePageOneEmailAccessYes": {
|
||||
"message": "Yes, I can reliably access my email"
|
||||
},
|
||||
"turnOnTwoStepLogin": {
|
||||
"message": "Turn on two-step login"
|
||||
},
|
||||
"changeAcctEmail": {
|
||||
"message": "Change account email"
|
||||
},
|
||||
"removeMembers": {
|
||||
"message": "Remove members"
|
||||
},
|
||||
|
||||
@@ -312,7 +312,7 @@ import {
|
||||
UserAsymmetricKeysRegenerationService,
|
||||
} from "@bitwarden/key-management";
|
||||
import { SafeInjectionToken } from "@bitwarden/ui-common";
|
||||
import { NewDeviceVerificationNoticeService, PasswordRepromptService } from "@bitwarden/vault";
|
||||
import { PasswordRepromptService } from "@bitwarden/vault";
|
||||
import {
|
||||
IndividualVaultExportService,
|
||||
IndividualVaultExportServiceAbstraction,
|
||||
@@ -1459,7 +1459,6 @@ const safeProviders: SafeProvider[] = [
|
||||
useClass: DefaultLoginDecryptionOptionsService,
|
||||
deps: [MessagingServiceAbstraction],
|
||||
}),
|
||||
safeProvider(NewDeviceVerificationNoticeService),
|
||||
safeProvider({
|
||||
provide: UserAsymmetricKeysRegenerationApiService,
|
||||
useClass: DefaultUserAsymmetricKeysRegenerationApiService,
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
export * from "./new-device-verification-notice.guard";
|
||||
@@ -1,293 +0,0 @@
|
||||
import { TestBed } from "@angular/core/testing";
|
||||
import { ActivatedRouteSnapshot, Router, RouterStateSnapshot } from "@angular/router";
|
||||
import { BehaviorSubject } from "rxjs";
|
||||
|
||||
import { Account, 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";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { NewDeviceVerificationNoticeService } from "@bitwarden/vault";
|
||||
|
||||
import { VaultProfileService } from "../services/vault-profile.service";
|
||||
|
||||
import { NewDeviceVerificationNoticeGuard } from "./new-device-verification-notice.guard";
|
||||
|
||||
describe("NewDeviceVerificationNoticeGuard", () => {
|
||||
const _state = Object.freeze({}) as RouterStateSnapshot;
|
||||
const emptyRoute = Object.freeze({ queryParams: {} }) as ActivatedRouteSnapshot;
|
||||
const eightDaysAgo = new Date();
|
||||
eightDaysAgo.setDate(eightDaysAgo.getDate() - 8);
|
||||
|
||||
const account = {
|
||||
id: "account-id",
|
||||
} as unknown as Account;
|
||||
|
||||
const activeAccount$ = new BehaviorSubject<Account | null>(account);
|
||||
|
||||
const createUrlTree = jest.fn();
|
||||
const getFeatureFlag = jest.fn().mockImplementation((key) => {
|
||||
if (key === FeatureFlag.NewDeviceVerificationTemporaryDismiss) {
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
|
||||
return Promise.resolve(false);
|
||||
});
|
||||
const isSelfHost = jest.fn().mockReturnValue(false);
|
||||
const getProfileTwoFactorEnabled = jest.fn().mockResolvedValue(false);
|
||||
const noticeState$ = jest.fn().mockReturnValue(new BehaviorSubject(null));
|
||||
const skipState$ = jest.fn().mockReturnValue(new BehaviorSubject(null));
|
||||
const getProfileCreationDate = jest.fn().mockResolvedValue(eightDaysAgo);
|
||||
const hasMasterPasswordAndMasterKeyHash = jest.fn().mockResolvedValue(true);
|
||||
const getUserSSOBound = jest.fn().mockResolvedValue(false);
|
||||
const getUserSSOBoundAdminOwner = jest.fn().mockResolvedValue(false);
|
||||
|
||||
beforeEach(() => {
|
||||
getFeatureFlag.mockClear();
|
||||
isSelfHost.mockClear();
|
||||
getProfileCreationDate.mockClear();
|
||||
getProfileTwoFactorEnabled.mockClear();
|
||||
createUrlTree.mockClear();
|
||||
hasMasterPasswordAndMasterKeyHash.mockClear();
|
||||
getUserSSOBound.mockClear();
|
||||
getUserSSOBoundAdminOwner.mockClear();
|
||||
skipState$.mockClear();
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
{ provide: Router, useValue: { createUrlTree } },
|
||||
{ provide: ConfigService, useValue: { getFeatureFlag } },
|
||||
{ provide: NewDeviceVerificationNoticeService, useValue: { noticeState$, skipState$ } },
|
||||
{ provide: AccountService, useValue: { activeAccount$ } },
|
||||
{ provide: PlatformUtilsService, useValue: { isSelfHost } },
|
||||
{ provide: UserVerificationService, useValue: { hasMasterPasswordAndMasterKeyHash } },
|
||||
{
|
||||
provide: VaultProfileService,
|
||||
useValue: {
|
||||
getProfileCreationDate,
|
||||
getProfileTwoFactorEnabled,
|
||||
getUserSSOBound,
|
||||
getUserSSOBoundAdminOwner,
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
function newDeviceGuard(route?: ActivatedRouteSnapshot) {
|
||||
// Run the guard within injection context so `inject` works as you'd expect
|
||||
// Pass state object to make TypeScript happy
|
||||
return TestBed.runInInjectionContext(async () =>
|
||||
NewDeviceVerificationNoticeGuard(route ?? emptyRoute, _state),
|
||||
);
|
||||
}
|
||||
|
||||
describe("fromNewDeviceVerification", () => {
|
||||
const route = {
|
||||
queryParams: { fromNewDeviceVerification: "true" },
|
||||
} as unknown as ActivatedRouteSnapshot;
|
||||
|
||||
it("returns `true` when `fromNewDeviceVerification` is present", async () => {
|
||||
expect(await newDeviceGuard(route)).toBe(true);
|
||||
});
|
||||
|
||||
it("does not execute other logic", async () => {
|
||||
// `fromNewDeviceVerification` param should exit early,
|
||||
// not foolproof but a quick way to test that other logic isn't executed
|
||||
await newDeviceGuard(route);
|
||||
|
||||
expect(getFeatureFlag).not.toHaveBeenCalled();
|
||||
expect(isSelfHost).not.toHaveBeenCalled();
|
||||
expect(getProfileTwoFactorEnabled).not.toHaveBeenCalled();
|
||||
expect(getProfileCreationDate).not.toHaveBeenCalled();
|
||||
expect(hasMasterPasswordAndMasterKeyHash).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("missing current account", () => {
|
||||
afterAll(() => {
|
||||
// reset `activeAccount$` observable
|
||||
activeAccount$.next(account);
|
||||
});
|
||||
|
||||
it("redirects to login when account is missing", async () => {
|
||||
activeAccount$.next(null);
|
||||
|
||||
await newDeviceGuard();
|
||||
|
||||
expect(createUrlTree).toHaveBeenCalledWith(["/login"]);
|
||||
});
|
||||
});
|
||||
|
||||
it("returns `true` when 2FA is enabled", async () => {
|
||||
getProfileTwoFactorEnabled.mockResolvedValueOnce(true);
|
||||
|
||||
expect(await newDeviceGuard()).toBe(true);
|
||||
});
|
||||
|
||||
it("returns `true` when the user is self hosted", async () => {
|
||||
isSelfHost.mockReturnValueOnce(true);
|
||||
|
||||
expect(await newDeviceGuard()).toBe(true);
|
||||
});
|
||||
|
||||
it("returns `true` when the profile was created less than a week ago", async () => {
|
||||
const sixDaysAgo = new Date();
|
||||
sixDaysAgo.setDate(sixDaysAgo.getDate() - 6);
|
||||
|
||||
getProfileCreationDate.mockResolvedValueOnce(sixDaysAgo);
|
||||
|
||||
expect(await newDeviceGuard()).toBe(true);
|
||||
});
|
||||
|
||||
it("returns `true` when the profile service throws an error", async () => {
|
||||
getProfileCreationDate.mockRejectedValueOnce(new Error("test"));
|
||||
|
||||
expect(await newDeviceGuard()).toBe(true);
|
||||
});
|
||||
|
||||
it("returns `true` when the skip state value is set to true", async () => {
|
||||
skipState$.mockReturnValueOnce(new BehaviorSubject(true));
|
||||
|
||||
expect(await newDeviceGuard()).toBe(true);
|
||||
expect(skipState$.mock.calls[0][0]).toBe("account-id");
|
||||
expect(skipState$.mock.calls.length).toBe(1);
|
||||
});
|
||||
|
||||
describe("SSO bound", () => {
|
||||
beforeEach(() => {
|
||||
getFeatureFlag.mockImplementation((key) => {
|
||||
if (key === FeatureFlag.NewDeviceVerificationPermanentDismiss) {
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
|
||||
return Promise.resolve(false);
|
||||
});
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
getFeatureFlag.mockReturnValue(false);
|
||||
});
|
||||
|
||||
it('returns "true" when the user is SSO bound and not an admin or owner', async () => {
|
||||
getUserSSOBound.mockResolvedValueOnce(true);
|
||||
getUserSSOBoundAdminOwner.mockResolvedValueOnce(false);
|
||||
|
||||
expect(await newDeviceGuard()).toBe(true);
|
||||
});
|
||||
|
||||
it('returns "true" when the user is an admin or owner of an SSO bound organization and has not logged in with their master password', async () => {
|
||||
getUserSSOBound.mockResolvedValueOnce(true);
|
||||
getUserSSOBoundAdminOwner.mockResolvedValueOnce(true);
|
||||
hasMasterPasswordAndMasterKeyHash.mockResolvedValueOnce(false);
|
||||
|
||||
expect(await newDeviceGuard()).toBe(true);
|
||||
});
|
||||
|
||||
it("shows notice when the user is an admin or owner of an SSO bound organization and logged in with their master password", async () => {
|
||||
getUserSSOBound.mockResolvedValueOnce(true);
|
||||
getUserSSOBoundAdminOwner.mockResolvedValueOnce(true);
|
||||
hasMasterPasswordAndMasterKeyHash.mockResolvedValueOnce(true);
|
||||
|
||||
await newDeviceGuard();
|
||||
|
||||
expect(createUrlTree).toHaveBeenCalledWith(["/new-device-notice"]);
|
||||
});
|
||||
|
||||
it("shows notice when the user that is not in an SSO bound organization", async () => {
|
||||
getUserSSOBound.mockResolvedValueOnce(false);
|
||||
getUserSSOBoundAdminOwner.mockResolvedValueOnce(false);
|
||||
hasMasterPasswordAndMasterKeyHash.mockResolvedValueOnce(true);
|
||||
|
||||
await newDeviceGuard();
|
||||
|
||||
expect(createUrlTree).toHaveBeenCalledWith(["/new-device-notice"]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("temp flag", () => {
|
||||
beforeEach(() => {
|
||||
getFeatureFlag.mockImplementation((key) => {
|
||||
if (key === FeatureFlag.NewDeviceVerificationTemporaryDismiss) {
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
|
||||
return Promise.resolve(false);
|
||||
});
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
getFeatureFlag.mockReturnValue(false);
|
||||
});
|
||||
|
||||
it("redirects to notice when the user has not dismissed it", async () => {
|
||||
noticeState$.mockReturnValueOnce(new BehaviorSubject(null));
|
||||
|
||||
await newDeviceGuard();
|
||||
|
||||
expect(createUrlTree).toHaveBeenCalledWith(["/new-device-notice"]);
|
||||
expect(noticeState$).toHaveBeenCalledWith(account.id);
|
||||
});
|
||||
|
||||
it("redirects to notice when the user dismissed it more than 7 days ago", async () => {
|
||||
const eighteenDaysAgo = new Date();
|
||||
eighteenDaysAgo.setDate(eighteenDaysAgo.getDate() - 18);
|
||||
|
||||
noticeState$.mockReturnValueOnce(
|
||||
new BehaviorSubject({ last_dismissal: eighteenDaysAgo.toISOString() }),
|
||||
);
|
||||
|
||||
await newDeviceGuard();
|
||||
|
||||
expect(createUrlTree).toHaveBeenCalledWith(["/new-device-notice"]);
|
||||
});
|
||||
|
||||
it("returns true when the user dismissed less than 7 days ago", async () => {
|
||||
const fourDaysAgo = new Date();
|
||||
fourDaysAgo.setDate(fourDaysAgo.getDate() - 4);
|
||||
|
||||
noticeState$.mockReturnValueOnce(
|
||||
new BehaviorSubject({ last_dismissal: fourDaysAgo.toISOString() }),
|
||||
);
|
||||
|
||||
expect(await newDeviceGuard()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("permanent flag", () => {
|
||||
beforeEach(() => {
|
||||
getFeatureFlag.mockImplementation((key) => {
|
||||
if (key === FeatureFlag.NewDeviceVerificationPermanentDismiss) {
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
|
||||
return Promise.resolve(false);
|
||||
});
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
getFeatureFlag.mockReturnValue(false);
|
||||
});
|
||||
|
||||
it("redirects when the user has not dismissed", async () => {
|
||||
noticeState$.mockReturnValueOnce(new BehaviorSubject(null));
|
||||
|
||||
await newDeviceGuard();
|
||||
|
||||
expect(createUrlTree).toHaveBeenCalledWith(["/new-device-notice"]);
|
||||
|
||||
noticeState$.mockReturnValueOnce(new BehaviorSubject({ permanent_dismissal: null }));
|
||||
|
||||
await newDeviceGuard();
|
||||
|
||||
expect(createUrlTree).toHaveBeenCalledTimes(2);
|
||||
expect(createUrlTree).toHaveBeenCalledWith(["/new-device-notice"]);
|
||||
});
|
||||
|
||||
it("returns `true` when the user has dismissed", async () => {
|
||||
noticeState$.mockReturnValueOnce(new BehaviorSubject({ permanent_dismissal: true }));
|
||||
|
||||
expect(await newDeviceGuard()).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,166 +0,0 @@
|
||||
import { inject } from "@angular/core";
|
||||
import { ActivatedRouteSnapshot, CanActivateFn, Router } from "@angular/router";
|
||||
import { firstValueFrom, Observable } from "rxjs";
|
||||
|
||||
import { Account, 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";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { NewDeviceVerificationNoticeService } from "@bitwarden/vault";
|
||||
|
||||
import { VaultProfileService } from "../services/vault-profile.service";
|
||||
|
||||
export const NewDeviceVerificationNoticeGuard: CanActivateFn = async (
|
||||
route: ActivatedRouteSnapshot,
|
||||
) => {
|
||||
const router = inject(Router);
|
||||
const configService = inject(ConfigService);
|
||||
const newDeviceVerificationNoticeService = inject(NewDeviceVerificationNoticeService);
|
||||
const accountService = inject(AccountService);
|
||||
const platformUtilsService = inject(PlatformUtilsService);
|
||||
const vaultProfileService = inject(VaultProfileService);
|
||||
const userVerificationService = inject(UserVerificationService);
|
||||
|
||||
if (route.queryParams["fromNewDeviceVerification"]) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const tempNoticeFlag = await configService.getFeatureFlag(
|
||||
FeatureFlag.NewDeviceVerificationTemporaryDismiss,
|
||||
);
|
||||
const permNoticeFlag = await configService.getFeatureFlag(
|
||||
FeatureFlag.NewDeviceVerificationPermanentDismiss,
|
||||
);
|
||||
|
||||
if (!tempNoticeFlag && !permNoticeFlag) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const currentAcct$: Observable<Account | null> = accountService.activeAccount$;
|
||||
const currentAcct = await firstValueFrom(currentAcct$);
|
||||
|
||||
if (!currentAcct) {
|
||||
return router.createUrlTree(["/login"]);
|
||||
}
|
||||
|
||||
// Currently used by the auth recovery login flow and will get cleaned up in PM-18485.
|
||||
if (await firstValueFrom(newDeviceVerificationNoticeService.skipState$(currentAcct.id))) {
|
||||
return true;
|
||||
}
|
||||
|
||||
try {
|
||||
const isSelfHosted = platformUtilsService.isSelfHost();
|
||||
const userIsSSOUser = await ssoAppliesToUser(
|
||||
userVerificationService,
|
||||
vaultProfileService,
|
||||
currentAcct.id,
|
||||
);
|
||||
const has2FAEnabled = await hasATwoFactorProviderEnabled(vaultProfileService, currentAcct.id);
|
||||
const isProfileLessThanWeekOld = await profileIsLessThanWeekOld(
|
||||
vaultProfileService,
|
||||
currentAcct.id,
|
||||
);
|
||||
|
||||
// When any of the following are true, the device verification notice is
|
||||
// not applicable for the user. When the user has *not* logged in with their
|
||||
// master password, assume they logged in with SSO.
|
||||
if (has2FAEnabled || isSelfHosted || userIsSSOUser || isProfileLessThanWeekOld) {
|
||||
return true;
|
||||
}
|
||||
} catch {
|
||||
// Skip showing the notice if there was a problem determining applicability
|
||||
// The most likely problem to occur is the user not having a network connection
|
||||
return true;
|
||||
}
|
||||
|
||||
const userItems$ = newDeviceVerificationNoticeService.noticeState$(currentAcct.id);
|
||||
const userItems = await firstValueFrom(userItems$);
|
||||
|
||||
// Show the notice when:
|
||||
// - The temp notice flag is enabled
|
||||
// - The user hasn't dismissed the notice or the user dismissed it more than 7 days ago
|
||||
if (
|
||||
tempNoticeFlag &&
|
||||
(!userItems?.last_dismissal || isMoreThan7DaysAgo(userItems?.last_dismissal))
|
||||
) {
|
||||
return router.createUrlTree(["/new-device-notice"]);
|
||||
}
|
||||
|
||||
// Show the notice when:
|
||||
// - The permanent notice flag is enabled
|
||||
// - The user hasn't dismissed the notice
|
||||
if (permNoticeFlag && !userItems?.permanent_dismissal) {
|
||||
return router.createUrlTree(["/new-device-notice"]);
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
/** Returns true has one 2FA provider enabled */
|
||||
async function hasATwoFactorProviderEnabled(
|
||||
vaultProfileService: VaultProfileService,
|
||||
userId: string,
|
||||
): Promise<boolean> {
|
||||
return vaultProfileService.getProfileTwoFactorEnabled(userId);
|
||||
}
|
||||
|
||||
/** Returns true when the user's profile is less than a week old */
|
||||
async function profileIsLessThanWeekOld(
|
||||
vaultProfileService: VaultProfileService,
|
||||
userId: string,
|
||||
): Promise<boolean> {
|
||||
const creationDate = await vaultProfileService.getProfileCreationDate(userId);
|
||||
return !isMoreThan7DaysAgo(creationDate);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true when either:
|
||||
* - The user is SSO bound to an organization and is not an Admin or Owner
|
||||
* - The user is an Admin or Owner of an organization with SSO bound and has not logged in with their master password
|
||||
*
|
||||
* NOTE: There are edge cases where this does not satisfy the original requirement of showing the notice to
|
||||
* users who are subject to the SSO required policy. When Owners and Admins log in with their MP they will see the notice
|
||||
* when they log in with SSO they will not. This is a concession made because the original logic references policies would not work for TDE users.
|
||||
* When this guard is run for those users a sync hasn't occurred and thus the policies are not available.
|
||||
*/
|
||||
async function ssoAppliesToUser(
|
||||
userVerificationService: UserVerificationService,
|
||||
vaultProfileService: VaultProfileService,
|
||||
userId: string,
|
||||
) {
|
||||
const userSSOBound = await vaultProfileService.getUserSSOBound(userId);
|
||||
const userSSOBoundAdminOwner = await vaultProfileService.getUserSSOBoundAdminOwner(userId);
|
||||
const userLoggedInWithMP = await userLoggedInWithMasterPassword(userVerificationService, userId);
|
||||
|
||||
const nonOwnerAdminSsoUser = userSSOBound && !userSSOBoundAdminOwner;
|
||||
const ssoAdminOwnerLoggedInWithMP = userSSOBoundAdminOwner && !userLoggedInWithMP;
|
||||
|
||||
return nonOwnerAdminSsoUser || ssoAdminOwnerLoggedInWithMP;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true when the user logged in with their master password.
|
||||
*/
|
||||
async function userLoggedInWithMasterPassword(
|
||||
userVerificationService: UserVerificationService,
|
||||
userId: string,
|
||||
) {
|
||||
return userVerificationService.hasMasterPasswordAndMasterKeyHash(userId);
|
||||
}
|
||||
|
||||
/** Returns the true when the date given is older than 7 days */
|
||||
function isMoreThan7DaysAgo(date?: string | Date): boolean {
|
||||
if (!date) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const inputDate = new Date(date).getTime();
|
||||
const today = new Date().getTime();
|
||||
|
||||
const differenceInMS = today - inputDate;
|
||||
const msInADay = 1000 * 60 * 60 * 24;
|
||||
const differenceInDays = Math.round(differenceInMS / msInADay);
|
||||
|
||||
return differenceInDays > 7;
|
||||
}
|
||||
@@ -68,94 +68,4 @@ describe("VaultProfileService", () => {
|
||||
expect(getProfile).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("getProfileTwoFactorEnabled", () => {
|
||||
it("calls `getProfile` when stored 2FA property is not stored", async () => {
|
||||
expect(service["profile2FAEnabled"]).toBeNull();
|
||||
|
||||
const twoFactorEnabled = await service.getProfileTwoFactorEnabled(userId);
|
||||
|
||||
expect(twoFactorEnabled).toBe(true);
|
||||
expect(getProfile).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("calls `getProfile` when stored profile id does not match", async () => {
|
||||
service["profile2FAEnabled"] = false;
|
||||
service["userId"] = "old-user-id";
|
||||
|
||||
const twoFactorEnabled = await service.getProfileTwoFactorEnabled(userId);
|
||||
|
||||
expect(twoFactorEnabled).toBe(true);
|
||||
expect(getProfile).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does not call `getProfile` when 2FA property is already stored", async () => {
|
||||
service["profile2FAEnabled"] = false;
|
||||
|
||||
const twoFactorEnabled = await service.getProfileTwoFactorEnabled(userId);
|
||||
|
||||
expect(twoFactorEnabled).toBe(false);
|
||||
expect(getProfile).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("getUserSSOBound", () => {
|
||||
it("calls `getProfile` when stored ssoBound property is not stored", async () => {
|
||||
expect(service["userIsSsoBound"]).toBeNull();
|
||||
|
||||
const userIsSsoBound = await service.getUserSSOBound(userId);
|
||||
|
||||
expect(userIsSsoBound).toBe(true);
|
||||
expect(getProfile).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("calls `getProfile` when stored profile id does not match", async () => {
|
||||
service["userIsSsoBound"] = false;
|
||||
service["userId"] = "old-user-id";
|
||||
|
||||
const userIsSsoBound = await service.getUserSSOBound(userId);
|
||||
|
||||
expect(userIsSsoBound).toBe(true);
|
||||
expect(getProfile).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does not call `getProfile` when ssoBound property is already stored", async () => {
|
||||
service["userIsSsoBound"] = false;
|
||||
|
||||
const userIsSsoBound = await service.getUserSSOBound(userId);
|
||||
|
||||
expect(userIsSsoBound).toBe(false);
|
||||
expect(getProfile).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("getUserSSOBoundAdminOwner", () => {
|
||||
it("calls `getProfile` when stored userIsSsoBoundAdminOwner property is not stored", async () => {
|
||||
expect(service["userIsSsoBoundAdminOwner"]).toBeNull();
|
||||
|
||||
const userIsSsoBoundAdminOwner = await service.getUserSSOBoundAdminOwner(userId);
|
||||
|
||||
expect(userIsSsoBoundAdminOwner).toBe(true);
|
||||
expect(getProfile).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("calls `getProfile` when stored profile id does not match", async () => {
|
||||
service["userIsSsoBoundAdminOwner"] = false;
|
||||
service["userId"] = "old-user-id";
|
||||
|
||||
const userIsSsoBoundAdminOwner = await service.getUserSSOBoundAdminOwner(userId);
|
||||
|
||||
expect(userIsSsoBoundAdminOwner).toBe(true);
|
||||
expect(getProfile).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does not call `getProfile` when userIsSsoBoundAdminOwner property is already stored", async () => {
|
||||
service["userIsSsoBoundAdminOwner"] = false;
|
||||
|
||||
const userIsSsoBoundAdminOwner = await service.getUserSSOBoundAdminOwner(userId);
|
||||
|
||||
expect(userIsSsoBoundAdminOwner).toBe(false);
|
||||
expect(getProfile).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,18 +1,13 @@
|
||||
import { Injectable, inject } from "@angular/core";
|
||||
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { OrganizationUserType } from "@bitwarden/common/admin-console/enums";
|
||||
import { ProfileResponse } from "@bitwarden/common/models/response/profile.response";
|
||||
|
||||
@Injectable({
|
||||
providedIn: "root",
|
||||
})
|
||||
/**
|
||||
* Class to provide profile level details without having to call the API each time.
|
||||
* NOTE: This is a temporary service and can be replaced once the `UnauthenticatedExtensionUIRefresh` flag goes live.
|
||||
* The `UnauthenticatedExtensionUIRefresh` introduces a sync that takes place upon logging in. These details can then
|
||||
* be added to account object and retrieved from there.
|
||||
* TODO: PM-16202
|
||||
* Class to provide profile level details to vault entities without having to call the API each time.
|
||||
*/
|
||||
export class VaultProfileService {
|
||||
private apiService = inject(ApiService);
|
||||
@@ -22,15 +17,6 @@ export class VaultProfileService {
|
||||
/** Profile creation stored as a string. */
|
||||
private profileCreatedDate: string | null = null;
|
||||
|
||||
/** True when 2FA is enabled on the profile. */
|
||||
private profile2FAEnabled: boolean | null = null;
|
||||
|
||||
/** True when ssoBound is true for any of the users organizations */
|
||||
private userIsSsoBound: boolean | null = null;
|
||||
|
||||
/** True when the user is an admin or owner of the ssoBound organization */
|
||||
private userIsSsoBoundAdminOwner: boolean | null = null;
|
||||
|
||||
/**
|
||||
* Returns the creation date of the profile.
|
||||
* Note: `Date`s are mutable in JS, creating a new
|
||||
@@ -46,56 +32,11 @@ export class VaultProfileService {
|
||||
return new Date(profile.creationDate);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether there is a 2FA provider on the profile.
|
||||
*/
|
||||
async getProfileTwoFactorEnabled(userId: string): Promise<boolean> {
|
||||
if (this.profile2FAEnabled !== null && userId === this.userId) {
|
||||
return Promise.resolve(this.profile2FAEnabled);
|
||||
}
|
||||
|
||||
const profile = await this.fetchAndCacheProfile();
|
||||
|
||||
return profile.twoFactorEnabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the user logs in with SSO for any organization.
|
||||
*/
|
||||
async getUserSSOBound(userId: string): Promise<boolean> {
|
||||
if (this.userIsSsoBound !== null && userId === this.userId) {
|
||||
return Promise.resolve(this.userIsSsoBound);
|
||||
}
|
||||
|
||||
await this.fetchAndCacheProfile();
|
||||
|
||||
return !!this.userIsSsoBound;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true when the user is an Admin or Owner of an organization with `ssoBound` true.
|
||||
*/
|
||||
async getUserSSOBoundAdminOwner(userId: string): Promise<boolean> {
|
||||
if (this.userIsSsoBoundAdminOwner !== null && userId === this.userId) {
|
||||
return Promise.resolve(this.userIsSsoBoundAdminOwner);
|
||||
}
|
||||
|
||||
await this.fetchAndCacheProfile();
|
||||
|
||||
return !!this.userIsSsoBoundAdminOwner;
|
||||
}
|
||||
|
||||
private async fetchAndCacheProfile(): Promise<ProfileResponse> {
|
||||
const profile = await this.apiService.getProfile();
|
||||
|
||||
this.userId = profile.id;
|
||||
this.profileCreatedDate = profile.creationDate;
|
||||
this.profile2FAEnabled = profile.twoFactorEnabled;
|
||||
const ssoBoundOrg = profile.organizations.find((org) => org.ssoBound);
|
||||
this.userIsSsoBound = !!ssoBoundOrg;
|
||||
this.userIsSsoBoundAdminOwner =
|
||||
ssoBoundOrg?.type === OrganizationUserType.Admin ||
|
||||
ssoBoundOrg?.type === OrganizationUserType.Owner;
|
||||
|
||||
return profile;
|
||||
}
|
||||
|
||||
@@ -55,8 +55,7 @@ export enum FeatureFlag {
|
||||
/* Vault */
|
||||
PM8851_BrowserOnboardingNudge = "pm-8851-browser-onboarding-nudge",
|
||||
PM9111ExtensionPersistAddEditForm = "pm-9111-extension-persist-add-edit-form",
|
||||
NewDeviceVerificationTemporaryDismiss = "new-device-temporary-dismiss",
|
||||
NewDeviceVerificationPermanentDismiss = "new-device-permanent-dismiss",
|
||||
VaultBulkManagementAction = "vault-bulk-management-action",
|
||||
SecurityTasks = "security-tasks",
|
||||
CipherKeyEncryption = "cipher-key-encryption",
|
||||
PM18520_UpdateDesktopCipherForm = "pm-18520-desktop-cipher-forms",
|
||||
@@ -108,8 +107,7 @@ export const DefaultFeatureFlagValue = {
|
||||
/* Vault */
|
||||
[FeatureFlag.PM8851_BrowserOnboardingNudge]: FALSE,
|
||||
[FeatureFlag.PM9111ExtensionPersistAddEditForm]: FALSE,
|
||||
[FeatureFlag.NewDeviceVerificationTemporaryDismiss]: FALSE,
|
||||
[FeatureFlag.NewDeviceVerificationPermanentDismiss]: FALSE,
|
||||
[FeatureFlag.VaultBulkManagementAction]: FALSE,
|
||||
[FeatureFlag.SecurityTasks]: FALSE,
|
||||
[FeatureFlag.CipherKeyEncryption]: FALSE,
|
||||
[FeatureFlag.PM18520_UpdateDesktopCipherForm]: FALSE,
|
||||
|
||||
@@ -195,13 +195,6 @@ export const PREMIUM_BANNER_DISK_LOCAL = new StateDefinition("premiumBannerRepro
|
||||
web: "disk-local",
|
||||
});
|
||||
export const BANNERS_DISMISSED_DISK = new StateDefinition("bannersDismissed", "disk");
|
||||
export const NEW_DEVICE_VERIFICATION_NOTICE = new StateDefinition(
|
||||
"newDeviceVerificationNotice",
|
||||
"disk",
|
||||
{
|
||||
web: "disk-local",
|
||||
},
|
||||
);
|
||||
export const VAULT_APPEARANCE = new StateDefinition("vaultAppearance", "disk");
|
||||
export const SECURITY_TASKS_DISK = new StateDefinition("securityTasks", "disk");
|
||||
export const AT_RISK_PASSWORDS_PAGE_DISK = new StateDefinition("atRiskPasswordsPage", "disk");
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
<form [formGroup]="formGroup" [bitSubmit]="submit">
|
||||
<p class="tw-text-center" bitTypography="body1">
|
||||
{{ "newDeviceVerificationNoticeContentPage1" | i18n }}
|
||||
<a bitLink (click)="navigateToNewDeviceVerificationHelp($event)" href="#">
|
||||
{{ "learnMore" | i18n }}.
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<bit-card
|
||||
class="tw-pb-0"
|
||||
[ngClass]="{
|
||||
'tw-flex tw-flex-col tw-items-center !tw-rounded-b-none': isDesktop,
|
||||
'md:tw-flex md:tw-flex-col md:tw-items-center md:!tw-rounded-b-none': !isDesktop,
|
||||
}"
|
||||
>
|
||||
<p bitTypography="body2" class="tw-text-muted md:tw-w-9/12">
|
||||
{{ "newDeviceVerificationNoticePageOneFormContent" | i18n: this.currentEmail }}
|
||||
</p>
|
||||
|
||||
<bit-radio-group formControlName="hasEmailAccess" class="md:tw-w-9/12">
|
||||
<bit-radio-button id="option_A" [value]="0">
|
||||
<bit-label>{{ "newDeviceVerificationNoticePageOneEmailAccessNo" | i18n }}</bit-label>
|
||||
</bit-radio-button>
|
||||
<bit-radio-button id="option_B" [value]="1">
|
||||
<bit-label>{{ "newDeviceVerificationNoticePageOneEmailAccessYes" | i18n }}</bit-label>
|
||||
</bit-radio-button>
|
||||
</bit-radio-group>
|
||||
</bit-card>
|
||||
|
||||
<button bitButton type="submit" buttonType="primary" class="tw-w-full tw-mt-4">
|
||||
{{ "continue" | i18n }}
|
||||
</button>
|
||||
</form>
|
||||
@@ -1,173 +0,0 @@
|
||||
import { ComponentFixture, TestBed } from "@angular/core/testing";
|
||||
import { By } from "@angular/platform-browser";
|
||||
import { Router } from "@angular/router";
|
||||
import { BehaviorSubject } from "rxjs";
|
||||
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
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 { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
|
||||
import { NewDeviceVerificationNoticeService } from "../../services/new-device-verification-notice.service";
|
||||
|
||||
import { NewDeviceVerificationNoticePageOneComponent } from "./new-device-verification-notice-page-one.component";
|
||||
|
||||
describe("NewDeviceVerificationNoticePageOneComponent", () => {
|
||||
let component: NewDeviceVerificationNoticePageOneComponent;
|
||||
let fixture: ComponentFixture<NewDeviceVerificationNoticePageOneComponent>;
|
||||
|
||||
const activeAccount$ = new BehaviorSubject({ email: "test@example.com", id: "acct-1" });
|
||||
const navigate = jest.fn().mockResolvedValue(null);
|
||||
const updateNewDeviceVerificationNoticeState = jest.fn().mockResolvedValue(null);
|
||||
const getFeatureFlag = jest.fn().mockResolvedValue(null);
|
||||
|
||||
beforeEach(async () => {
|
||||
navigate.mockClear();
|
||||
updateNewDeviceVerificationNoticeState.mockClear();
|
||||
getFeatureFlag.mockClear();
|
||||
|
||||
await TestBed.configureTestingModule({
|
||||
providers: [
|
||||
{ provide: I18nService, useValue: { t: (...key: string[]) => key.join(" ") } },
|
||||
{ provide: Router, useValue: { navigate } },
|
||||
{ provide: AccountService, useValue: { activeAccount$ } },
|
||||
{
|
||||
provide: NewDeviceVerificationNoticeService,
|
||||
useValue: { updateNewDeviceVerificationNoticeState },
|
||||
},
|
||||
{ provide: PlatformUtilsService, useValue: { getClientType: () => false } },
|
||||
{ provide: ConfigService, useValue: { getFeatureFlag } },
|
||||
],
|
||||
}).compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(NewDeviceVerificationNoticePageOneComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it("sets initial properties", () => {
|
||||
expect(component["currentEmail"]).toBe("test@example.com");
|
||||
expect(component["currentUserId"]).toBe("acct-1");
|
||||
});
|
||||
|
||||
describe("temporary flag submission", () => {
|
||||
beforeEach(() => {
|
||||
getFeatureFlag.mockImplementation((key) => {
|
||||
if (key === FeatureFlag.NewDeviceVerificationTemporaryDismiss) {
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
|
||||
return Promise.resolve(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("no email access", () => {
|
||||
beforeEach(() => {
|
||||
component["formGroup"].controls.hasEmailAccess.setValue(0);
|
||||
fixture.detectChanges();
|
||||
|
||||
const submit = fixture.debugElement.query(By.css('button[type="submit"]'));
|
||||
submit.nativeElement.click();
|
||||
});
|
||||
|
||||
it("redirects to step two ", () => {
|
||||
expect(navigate).toHaveBeenCalledTimes(1);
|
||||
expect(navigate).toHaveBeenCalledWith(["new-device-notice/setup"]);
|
||||
});
|
||||
|
||||
it("does not update notice state", () => {
|
||||
expect(getFeatureFlag).not.toHaveBeenCalled();
|
||||
expect(updateNewDeviceVerificationNoticeState).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("has email access", () => {
|
||||
beforeEach(() => {
|
||||
component["formGroup"].controls.hasEmailAccess.setValue(1);
|
||||
fixture.detectChanges();
|
||||
|
||||
jest.useFakeTimers();
|
||||
jest.setSystemTime(new Date("2024-03-03T00:00:00.000Z"));
|
||||
const submit = fixture.debugElement.query(By.css('button[type="submit"]'));
|
||||
submit.nativeElement.click();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
it("redirects to the vault", () => {
|
||||
expect(navigate).toHaveBeenCalledTimes(1);
|
||||
expect(navigate).toHaveBeenCalledWith(["/vault"]);
|
||||
});
|
||||
|
||||
it("updates notice state with a new date", () => {
|
||||
expect(updateNewDeviceVerificationNoticeState).toHaveBeenCalledWith("acct-1", {
|
||||
last_dismissal: new Date("2024-03-03T00:00:00.000Z"),
|
||||
permanent_dismissal: false,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("permanent flag submission", () => {
|
||||
beforeEach(() => {
|
||||
getFeatureFlag.mockImplementation((key) => {
|
||||
if (key === FeatureFlag.NewDeviceVerificationPermanentDismiss) {
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
|
||||
return Promise.resolve(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("no email access", () => {
|
||||
beforeEach(() => {
|
||||
component["formGroup"].controls.hasEmailAccess.setValue(0);
|
||||
fixture.detectChanges();
|
||||
|
||||
const submit = fixture.debugElement.query(By.css('button[type="submit"]'));
|
||||
submit.nativeElement.click();
|
||||
});
|
||||
|
||||
it("redirects to step two", () => {
|
||||
expect(navigate).toHaveBeenCalledTimes(1);
|
||||
expect(navigate).toHaveBeenCalledWith(["new-device-notice/setup"]);
|
||||
});
|
||||
|
||||
it("does not update notice state", () => {
|
||||
expect(getFeatureFlag).not.toHaveBeenCalled();
|
||||
expect(updateNewDeviceVerificationNoticeState).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("has email access", () => {
|
||||
beforeEach(() => {
|
||||
component["formGroup"].controls.hasEmailAccess.setValue(1);
|
||||
fixture.detectChanges();
|
||||
|
||||
jest.useFakeTimers();
|
||||
jest.setSystemTime(new Date("2024-04-04T00:00:00.000Z"));
|
||||
const submit = fixture.debugElement.query(By.css('button[type="submit"]'));
|
||||
submit.nativeElement.click();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
it("redirects to the vault ", () => {
|
||||
expect(navigate).toHaveBeenCalledTimes(1);
|
||||
expect(navigate).toHaveBeenCalledWith(["/vault"]);
|
||||
});
|
||||
|
||||
it("updates notice state with a new date", () => {
|
||||
expect(updateNewDeviceVerificationNoticeState).toHaveBeenCalledWith("acct-1", {
|
||||
last_dismissal: new Date("2024-04-04T00:00:00.000Z"),
|
||||
permanent_dismissal: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,130 +0,0 @@
|
||||
import { LiveAnnouncer } from "@angular/cdk/a11y";
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { AfterViewInit, Component, OnInit } from "@angular/core";
|
||||
import { FormBuilder, FormControl, ReactiveFormsModule } from "@angular/forms";
|
||||
import { Router } from "@angular/router";
|
||||
import { firstValueFrom, Observable } from "rxjs";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { Account, AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { ClientType } from "@bitwarden/common/enums";
|
||||
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 { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import {
|
||||
AsyncActionsModule,
|
||||
ButtonModule,
|
||||
CardComponent,
|
||||
FormFieldModule,
|
||||
RadioButtonModule,
|
||||
TypographyModule,
|
||||
LinkModule,
|
||||
} from "@bitwarden/components";
|
||||
|
||||
import {
|
||||
NewDeviceVerificationNotice,
|
||||
NewDeviceVerificationNoticeService,
|
||||
} from "./../../services/new-device-verification-notice.service";
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
selector: "app-new-device-verification-notice-page-one",
|
||||
templateUrl: "./new-device-verification-notice-page-one.component.html",
|
||||
imports: [
|
||||
CardComponent,
|
||||
CommonModule,
|
||||
JslibModule,
|
||||
TypographyModule,
|
||||
ButtonModule,
|
||||
RadioButtonModule,
|
||||
FormFieldModule,
|
||||
AsyncActionsModule,
|
||||
ReactiveFormsModule,
|
||||
LinkModule,
|
||||
],
|
||||
})
|
||||
export class NewDeviceVerificationNoticePageOneComponent implements OnInit, AfterViewInit {
|
||||
protected formGroup = this.formBuilder.group({
|
||||
hasEmailAccess: new FormControl(0),
|
||||
});
|
||||
protected isDesktop: boolean;
|
||||
readonly currentAcct$: Observable<Account | null> = this.accountService.activeAccount$;
|
||||
protected currentEmail: string = "";
|
||||
private currentUserId: UserId | null = null;
|
||||
|
||||
constructor(
|
||||
private formBuilder: FormBuilder,
|
||||
private router: Router,
|
||||
private accountService: AccountService,
|
||||
private newDeviceVerificationNoticeService: NewDeviceVerificationNoticeService,
|
||||
private platformUtilsService: PlatformUtilsService,
|
||||
private configService: ConfigService,
|
||||
private liveAnnouncer: LiveAnnouncer,
|
||||
private i18nService: I18nService,
|
||||
) {
|
||||
this.isDesktop = this.platformUtilsService.getClientType() === ClientType.Desktop;
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
const currentAcct = await firstValueFrom(this.currentAcct$);
|
||||
if (!currentAcct) {
|
||||
return;
|
||||
}
|
||||
this.currentEmail = currentAcct.email;
|
||||
this.currentUserId = currentAcct.id;
|
||||
}
|
||||
|
||||
ngAfterViewInit() {
|
||||
void this.liveAnnouncer.announce(this.i18nService.t("importantNotice"), "polite");
|
||||
}
|
||||
|
||||
submit = async () => {
|
||||
const doesNotHaveEmailAccess = this.formGroup.controls.hasEmailAccess.value === 0;
|
||||
|
||||
if (doesNotHaveEmailAccess) {
|
||||
await this.router.navigate(["new-device-notice/setup"]);
|
||||
return;
|
||||
}
|
||||
|
||||
const tempNoticeFlag = await this.configService.getFeatureFlag(
|
||||
FeatureFlag.NewDeviceVerificationTemporaryDismiss,
|
||||
);
|
||||
const permNoticeFlag = await this.configService.getFeatureFlag(
|
||||
FeatureFlag.NewDeviceVerificationPermanentDismiss,
|
||||
);
|
||||
|
||||
let newNoticeState: NewDeviceVerificationNotice | null = null;
|
||||
|
||||
// When the temporary flag is enabled, only update the `last_dismissal`
|
||||
if (tempNoticeFlag) {
|
||||
newNoticeState = {
|
||||
last_dismissal: new Date(),
|
||||
permanent_dismissal: false,
|
||||
};
|
||||
} else if (permNoticeFlag) {
|
||||
// When the per flag is enabled, only update the `last_dismissal`
|
||||
newNoticeState = {
|
||||
last_dismissal: new Date(),
|
||||
permanent_dismissal: true,
|
||||
};
|
||||
}
|
||||
|
||||
// This shouldn't occur as the user shouldn't get here unless one of the flags is active.
|
||||
if (newNoticeState) {
|
||||
await this.newDeviceVerificationNoticeService.updateNewDeviceVerificationNoticeState(
|
||||
this.currentUserId!,
|
||||
newNoticeState,
|
||||
);
|
||||
}
|
||||
|
||||
await this.router.navigate(["/vault"]);
|
||||
};
|
||||
|
||||
navigateToNewDeviceVerificationHelp(event: Event) {
|
||||
event.preventDefault();
|
||||
|
||||
this.platformUtilsService.launchUri("https://bitwarden.com/help/new-device-verification/");
|
||||
}
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
<p class="tw-text-center" bitTypography="body1">
|
||||
{{ "newDeviceVerificationNoticeContentPage2" | i18n }}
|
||||
</p>
|
||||
|
||||
<a
|
||||
href="#"
|
||||
bitButton
|
||||
(click)="navigateToTwoStepLogin($event)"
|
||||
buttonType="primary"
|
||||
class="tw-w-full tw-mt-4"
|
||||
data-testid="two-factor"
|
||||
>
|
||||
{{ "turnOnTwoStepLogin" | i18n }}
|
||||
<i
|
||||
class="bwi bwi-external-link bwi-lg bwi-fw"
|
||||
aria-hidden="true"
|
||||
[ngClass]="{ 'md:tw-hidden': !isDesktop }"
|
||||
>
|
||||
</i>
|
||||
</a>
|
||||
<a
|
||||
href="#"
|
||||
bitButton
|
||||
(click)="navigateToChangeAcctEmail($event)"
|
||||
buttonType="secondary"
|
||||
class="tw-w-full tw-mt-4"
|
||||
data-testid="change-email"
|
||||
>
|
||||
{{ "changeAcctEmail" | i18n }}
|
||||
<i
|
||||
class="bwi bwi-external-link bwi-lg bwi-fw"
|
||||
aria-hidden="true"
|
||||
[ngClass]="{ 'md:tw-hidden': !isDesktop }"
|
||||
></i>
|
||||
</a>
|
||||
|
||||
<div class="tw-flex tw-justify-center tw-mt-6" *ngIf="!permanentFlagEnabled">
|
||||
<a
|
||||
bitLink
|
||||
linkType="primary"
|
||||
(click)="remindMeLaterSelect()"
|
||||
data-testid="remind-me-later"
|
||||
href="#"
|
||||
appStopClick
|
||||
>
|
||||
{{ "remindMeLater" | i18n }}
|
||||
</a>
|
||||
</div>
|
||||
@@ -1,175 +0,0 @@
|
||||
import { ComponentFixture, TestBed } from "@angular/core/testing";
|
||||
import { By } from "@angular/platform-browser";
|
||||
import { Router } from "@angular/router";
|
||||
import { BehaviorSubject } from "rxjs";
|
||||
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { ClientType } from "@bitwarden/common/enums";
|
||||
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 { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
|
||||
import { NewDeviceVerificationNoticeService } from "../../services/new-device-verification-notice.service";
|
||||
|
||||
import { NewDeviceVerificationNoticePageTwoComponent } from "./new-device-verification-notice-page-two.component";
|
||||
|
||||
describe("NewDeviceVerificationNoticePageTwoComponent", () => {
|
||||
let component: NewDeviceVerificationNoticePageTwoComponent;
|
||||
let fixture: ComponentFixture<NewDeviceVerificationNoticePageTwoComponent>;
|
||||
|
||||
const activeAccount$ = new BehaviorSubject({ email: "test@example.com", id: "acct-1" });
|
||||
const environment$ = new BehaviorSubject({ getWebVaultUrl: () => "vault.bitwarden.com" });
|
||||
const navigate = jest.fn().mockResolvedValue(null);
|
||||
const updateNewDeviceVerificationNoticeState = jest.fn().mockResolvedValue(null);
|
||||
const getFeatureFlag = jest.fn().mockResolvedValue(false);
|
||||
const getClientType = jest.fn().mockReturnValue(ClientType.Browser);
|
||||
const launchUri = jest.fn();
|
||||
|
||||
beforeEach(async () => {
|
||||
navigate.mockClear();
|
||||
updateNewDeviceVerificationNoticeState.mockClear();
|
||||
getFeatureFlag.mockClear();
|
||||
getClientType.mockClear();
|
||||
launchUri.mockClear();
|
||||
|
||||
await TestBed.configureTestingModule({
|
||||
providers: [
|
||||
{ provide: I18nService, useValue: { t: (...key: string[]) => key.join(" ") } },
|
||||
{ provide: Router, useValue: { navigate } },
|
||||
{ provide: AccountService, useValue: { activeAccount$ } },
|
||||
{ provide: EnvironmentService, useValue: { environment$ } },
|
||||
{
|
||||
provide: NewDeviceVerificationNoticeService,
|
||||
useValue: { updateNewDeviceVerificationNoticeState },
|
||||
},
|
||||
{ provide: PlatformUtilsService, useValue: { getClientType, launchUri } },
|
||||
{ provide: ConfigService, useValue: { getFeatureFlag } },
|
||||
],
|
||||
}).compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(NewDeviceVerificationNoticePageTwoComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it("sets initial properties", () => {
|
||||
expect(component["currentUserId"]).toBe("acct-1");
|
||||
expect(component["permanentFlagEnabled"]).toBe(false);
|
||||
});
|
||||
|
||||
describe("change email", () => {
|
||||
const changeEmailButton = () =>
|
||||
fixture.debugElement.query(By.css('[data-testid="change-email"]'));
|
||||
|
||||
describe("web", () => {
|
||||
beforeEach(() => {
|
||||
component["isWeb"] = true;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it("navigates to settings", () => {
|
||||
changeEmailButton().nativeElement.click();
|
||||
|
||||
expect(navigate).toHaveBeenCalledTimes(1);
|
||||
expect(navigate).toHaveBeenCalledWith(["/settings/account"], {
|
||||
queryParams: { fromNewDeviceVerification: true },
|
||||
});
|
||||
expect(launchUri).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("browser/desktop", () => {
|
||||
beforeEach(() => {
|
||||
component["isWeb"] = false;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it("launches to settings", () => {
|
||||
changeEmailButton().nativeElement.click();
|
||||
|
||||
expect(navigate).not.toHaveBeenCalled();
|
||||
expect(launchUri).toHaveBeenCalledWith(
|
||||
"vault.bitwarden.com/#/settings/account/?fromNewDeviceVerification=true",
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("enable 2fa", () => {
|
||||
const changeEmailButton = () =>
|
||||
fixture.debugElement.query(By.css('[data-testid="two-factor"]'));
|
||||
|
||||
describe("web", () => {
|
||||
beforeEach(() => {
|
||||
component["isWeb"] = true;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it("navigates to two factor settings", () => {
|
||||
changeEmailButton().nativeElement.click();
|
||||
|
||||
expect(navigate).toHaveBeenCalledTimes(1);
|
||||
expect(navigate).toHaveBeenCalledWith(["/settings/security/two-factor"], {
|
||||
queryParams: { fromNewDeviceVerification: true },
|
||||
});
|
||||
expect(launchUri).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("browser/desktop", () => {
|
||||
beforeEach(() => {
|
||||
component["isWeb"] = false;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it("launches to two factor settings", () => {
|
||||
changeEmailButton().nativeElement.click();
|
||||
|
||||
expect(navigate).not.toHaveBeenCalled();
|
||||
expect(launchUri).toHaveBeenCalledWith(
|
||||
"vault.bitwarden.com/#/settings/security/two-factor/?fromNewDeviceVerification=true",
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("remind me later", () => {
|
||||
const remindMeLater = () =>
|
||||
fixture.debugElement.query(By.css('[data-testid="remind-me-later"]'));
|
||||
|
||||
beforeEach(() => {
|
||||
jest.useFakeTimers();
|
||||
jest.setSystemTime(new Date("2024-02-02T00:00:00.000Z"));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
it("navigates to the vault", () => {
|
||||
remindMeLater().nativeElement.click();
|
||||
|
||||
expect(navigate).toHaveBeenCalledTimes(1);
|
||||
expect(navigate).toHaveBeenCalledWith(["/vault"]);
|
||||
});
|
||||
|
||||
it("updates notice state", () => {
|
||||
remindMeLater().nativeElement.click();
|
||||
|
||||
expect(updateNewDeviceVerificationNoticeState).toHaveBeenCalledTimes(1);
|
||||
expect(updateNewDeviceVerificationNoticeState).toHaveBeenCalledWith("acct-1", {
|
||||
last_dismissal: new Date("2024-02-02T00:00:00.000Z"),
|
||||
permanent_dismissal: false,
|
||||
});
|
||||
});
|
||||
|
||||
it("is hidden when the permanent flag is enabled", async () => {
|
||||
getFeatureFlag.mockResolvedValueOnce(true);
|
||||
await component.ngOnInit();
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(remindMeLater()).toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,111 +0,0 @@
|
||||
import { LiveAnnouncer } from "@angular/cdk/a11y";
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { AfterViewInit, Component, OnInit } from "@angular/core";
|
||||
import { Router } from "@angular/router";
|
||||
import { firstValueFrom, Observable } from "rxjs";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { Account, AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { ClientType } from "@bitwarden/common/enums";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import {
|
||||
Environment,
|
||||
EnvironmentService,
|
||||
} from "@bitwarden/common/platform/abstractions/environment.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { ButtonModule, LinkModule, TypographyModule } from "@bitwarden/components";
|
||||
|
||||
import { NewDeviceVerificationNoticeService } from "../../services/new-device-verification-notice.service";
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
selector: "app-new-device-verification-notice-page-two",
|
||||
templateUrl: "./new-device-verification-notice-page-two.component.html",
|
||||
imports: [CommonModule, JslibModule, TypographyModule, ButtonModule, LinkModule],
|
||||
})
|
||||
export class NewDeviceVerificationNoticePageTwoComponent implements OnInit, AfterViewInit {
|
||||
protected isWeb: boolean;
|
||||
protected isDesktop: boolean;
|
||||
protected permanentFlagEnabled = false;
|
||||
readonly currentAcct$: Observable<Account | null> = this.accountService.activeAccount$;
|
||||
private currentUserId: UserId | null = null;
|
||||
private env$: Observable<Environment> = this.environmentService.environment$;
|
||||
|
||||
constructor(
|
||||
private newDeviceVerificationNoticeService: NewDeviceVerificationNoticeService,
|
||||
private router: Router,
|
||||
private accountService: AccountService,
|
||||
private platformUtilsService: PlatformUtilsService,
|
||||
private environmentService: EnvironmentService,
|
||||
private configService: ConfigService,
|
||||
private liveAnnouncer: LiveAnnouncer,
|
||||
private i18nService: I18nService,
|
||||
) {
|
||||
this.isWeb = this.platformUtilsService.getClientType() === ClientType.Web;
|
||||
this.isDesktop = this.platformUtilsService.getClientType() === ClientType.Desktop;
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
this.permanentFlagEnabled = await this.configService.getFeatureFlag(
|
||||
FeatureFlag.NewDeviceVerificationPermanentDismiss,
|
||||
);
|
||||
|
||||
const currentAcct = await firstValueFrom(this.currentAcct$);
|
||||
if (!currentAcct) {
|
||||
return;
|
||||
}
|
||||
this.currentUserId = currentAcct.id;
|
||||
}
|
||||
|
||||
ngAfterViewInit() {
|
||||
void this.liveAnnouncer.announce(this.i18nService.t("setupTwoStepLogin"), "polite");
|
||||
}
|
||||
|
||||
async navigateToTwoStepLogin(event: Event) {
|
||||
event.preventDefault();
|
||||
|
||||
const env = await firstValueFrom(this.env$);
|
||||
const url = env.getWebVaultUrl();
|
||||
|
||||
if (this.isWeb) {
|
||||
await this.router.navigate(["/settings/security/two-factor"], {
|
||||
queryParams: { fromNewDeviceVerification: true },
|
||||
});
|
||||
} else {
|
||||
this.platformUtilsService.launchUri(
|
||||
url + "/#/settings/security/two-factor/?fromNewDeviceVerification=true",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async navigateToChangeAcctEmail(event: Event) {
|
||||
event.preventDefault();
|
||||
|
||||
const env = await firstValueFrom(this.env$);
|
||||
const url = env.getWebVaultUrl();
|
||||
if (this.isWeb) {
|
||||
await this.router.navigate(["/settings/account"], {
|
||||
queryParams: { fromNewDeviceVerification: true },
|
||||
});
|
||||
} else {
|
||||
this.platformUtilsService.launchUri(
|
||||
url + "/#/settings/account/?fromNewDeviceVerification=true",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async remindMeLaterSelect() {
|
||||
await this.newDeviceVerificationNoticeService.updateNewDeviceVerificationNoticeState(
|
||||
this.currentUserId!,
|
||||
{
|
||||
last_dismissal: new Date(),
|
||||
permanent_dismissal: false,
|
||||
},
|
||||
);
|
||||
|
||||
await this.router.navigate(["/vault"]);
|
||||
}
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
import { svgIcon } from "@bitwarden/components";
|
||||
|
||||
export const ExclamationTriangle = svgIcon`
|
||||
<svg width="120" height="100" viewBox="0 0 120 100" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M91.0871 85.1224H28.913C27.5592 85.1349 26.2271 84.7737 25.0549 84.0713C23.8828 83.3688 22.914 82.3578 22.248 81.1386C21.5868 79.9571 21.2405 78.6149 21.2502 77.2502C21.2599 75.8855 21.6207 74.5484 22.2964 73.3768L53.3835 18.7683C54.0665 17.5817 55.0352 16.6008 56.1953 15.9184C57.3554 15.2361 58.6656 14.8773 60 14.8773C61.3345 14.8773 62.6447 15.2361 63.8048 15.9184C64.9649 16.6008 65.9336 17.5817 66.6166 18.7683L97.7036 73.3768C98.3793 74.5484 98.7426 75.8855 98.7499 77.2502C98.7571 78.6149 98.4132 79.9571 97.7521 81.1386C97.0861 82.3578 96.1173 83.3713 94.9451 84.0713C93.773 84.7712 92.4409 85.1349 91.0871 85.1224ZM60 19.8972C59.5084 19.8896 59.0216 20.0176 58.5929 20.2659C58.1643 20.5143 57.8058 20.878 57.554 21.3171L26.4717 75.9256C26.2344 76.3371 26.1085 76.8087 26.1085 77.2878C26.1085 77.767 26.2344 78.2386 26.4717 78.65C26.7188 79.0991 27.0772 79.4704 27.5107 79.7263C27.9442 79.9821 28.4359 80.1126 28.9324 80.1051H91.0871C91.586 80.1126 92.0776 79.9821 92.5087 79.7263C92.9398 79.4704 93.3007 79.0991 93.5477 78.65C93.7851 78.2386 93.911 77.767 93.911 77.2878C93.911 76.8087 93.7851 76.3371 93.5477 75.9256L62.4461 21.3171C62.1943 20.878 61.8358 20.5168 61.4071 20.2659C60.9785 20.0151 60.4917 19.8896 60 19.8972ZM60 62.8705C59.3582 62.8705 58.7407 62.6071 58.2878 62.1355C57.8349 61.6639 57.5782 61.0267 57.5782 60.3619V37.4177C57.5782 36.7529 57.8325 36.1132 58.2878 35.644C58.7431 35.1749 59.3582 34.909 60 34.909C60.6418 34.909 61.2594 35.1724 61.7123 35.644C62.1652 36.1157 62.4219 36.7529 62.4219 37.4177V60.3619C62.4219 61.0267 62.1676 61.6664 61.7123 62.1355C61.257 62.6046 60.6418 62.8705 60 62.8705ZM60 75.2734C61.5864 75.2734 62.8724 73.9413 62.8724 72.2981C62.8724 70.6549 61.5864 69.3228 60 69.3228C58.4137 69.3228 57.1277 70.6549 57.1277 72.2981C57.1277 73.9413 58.4137 75.2734 60 75.2734Z" class="tw-fill-warning-600" />
|
||||
</svg>
|
||||
`;
|
||||
@@ -2,8 +2,6 @@ export * from "./deactivated-org";
|
||||
export * from "./no-folders";
|
||||
export * from "./vault";
|
||||
export * from "./empty-trash";
|
||||
export * from "./exclamation-triangle";
|
||||
export * from "./user-lock";
|
||||
export * from "./browser-extension";
|
||||
export * from "./bitwarden-icon";
|
||||
export * from "./security-handshake";
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
import { svgIcon } from "@bitwarden/components";
|
||||
|
||||
export const UserLock = svgIcon`
|
||||
<svg width="120" height="100" viewBox="0 0 120 100" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path class="tw-fill-art-primary" fill-rule="evenodd" clip-rule="evenodd" d="M0 19.6127C0 14.9806 3.71508 11.2256 8.29787 11.2256H89.5153C94.098 11.2256 97.8131 14.9806 97.8131 19.6127V28.0844H95.2599V19.6127C95.2599 16.4059 92.688 13.8062 89.5153 13.8062H8.29787C5.12517 13.8062 2.55319 16.4059 2.55319 19.6127V68.6449C2.55319 71.8518 5.12517 74.4514 8.29787 74.4514H16.2389V77.032H8.29787C3.71509 77.032 0 73.277 0 68.6449V19.6127ZM50.9015 74.4514H63.2653V77.032H50.9015V74.4514Z" />
|
||||
<path class="tw-fill-art-primary" fill-rule="evenodd" clip-rule="evenodd" d="M88.7235 60.2578C97.5366 60.2578 104.681 53.0366 104.681 44.1287C104.681 35.2209 97.5366 27.9997 88.7235 27.9997C79.9105 27.9997 72.7661 35.2209 72.7661 44.1287C72.7661 53.0366 79.9105 60.2578 88.7235 60.2578ZM88.7235 62.8384C98.9467 62.8384 107.234 54.4618 107.234 44.1287C107.234 33.7957 98.9467 25.4191 88.7235 25.4191C78.5004 25.4191 70.2129 33.7957 70.2129 44.1287C70.2129 54.4618 78.5004 62.8384 88.7235 62.8384Z" />
|
||||
<path class="tw-fill-art-accent" fill-rule="evenodd" clip-rule="evenodd" d="M33.8298 46.0642C28.7286 46.0642 24.2553 51.0589 24.2553 57.6771H21.7021C21.7021 50.0428 26.9452 43.4835 33.8298 43.4835C40.7144 43.4835 45.9575 50.0428 45.9575 57.6771H43.4043C43.4043 51.0589 38.931 46.0642 33.8298 46.0642Z" />
|
||||
<path class="tw-fill-art-primary" fill-rule="evenodd" clip-rule="evenodd" d="M60.8742 88.6448H115.299C116.596 88.6448 117.089 88.3032 117.227 88.1603C117.3 88.0845 117.558 87.7852 117.396 86.7815C115.225 73.2724 103.259 62.8383 88.7118 62.8383C74.1651 62.8383 62.1986 73.2724 60.0274 86.7815C59.9451 87.2938 60.0532 87.888 60.2785 88.2731C60.379 88.445 60.4746 88.5285 60.5381 88.5682C60.5886 88.5998 60.6806 88.6448 60.8742 88.6448ZM60.8742 91.2254H115.299C118.653 91.2254 120.416 89.4793 119.916 86.3677C117.539 71.5724 104.473 60.2577 88.7118 60.2577C72.9505 60.2577 59.8851 71.5724 57.5073 86.3677C57.1687 88.4743 58.2526 91.2254 60.8742 91.2254Z" />
|
||||
<path class="tw-fill-art-accent" fill-rule="evenodd" clip-rule="evenodd" d="M44.6808 58.9675H22.9787C20.1585 58.9675 17.8723 61.2783 17.8723 64.1288V79.6126C17.8723 82.4631 20.1585 84.7739 22.9787 84.7739H44.6808C47.501 84.7739 49.7872 82.4631 49.7872 79.6126V64.1288C49.7872 61.2783 47.501 58.9675 44.6808 58.9675ZM22.9787 56.3868C18.7484 56.3868 15.3191 59.853 15.3191 64.1288V79.6126C15.3191 83.8884 18.7484 87.3546 22.9787 87.3546H44.6808C48.9111 87.3546 52.3404 83.8884 52.3404 79.6126V64.1288C52.3404 59.853 48.9111 56.3868 44.6808 56.3868H22.9787Z"/>
|
||||
<path class="tw-fill-art-accent" fill-rule="evenodd" clip-rule="evenodd" d="M33.8301 71.8707C34.5351 71.8707 35.1067 72.4484 35.1067 73.1611L35.1067 77.6923C35.1067 78.4049 34.5351 78.9826 33.8301 78.9826C33.125 78.9826 32.5535 78.4049 32.5535 77.6923L32.5535 73.1611C32.5535 72.4484 33.125 71.8707 33.8301 71.8707Z" />
|
||||
<path class="tw-fill-art-accent" fill-rule="evenodd" clip-rule="evenodd" d="M78.315 18.3374C77.6099 18.3374 77.0384 17.7597 77.0384 17.0471V16.5436C77.0384 15.831 77.6099 15.2533 78.315 15.2533C79.02 15.2533 79.5916 15.831 79.5916 16.5436V17.0471C79.5916 17.7597 79.02 18.3374 78.315 18.3374Z" />
|
||||
<path class="tw-fill-art-accent" fill-rule="evenodd" clip-rule="evenodd" d="M33.8299 74.3097C32.4198 74.3097 31.2767 73.1543 31.2767 71.729V71.2256C31.2767 69.8003 32.4198 68.6449 33.8299 68.6449C35.24 68.6449 36.3831 69.8003 36.3831 71.2256V71.729C36.3831 73.1543 35.24 74.3097 33.8299 74.3097Z" />
|
||||
<path class="tw-fill-art-accent" fill-rule="evenodd" clip-rule="evenodd" d="M84.2903 18.3374C83.5853 18.3374 83.0137 17.7597 83.0137 17.0471V16.5436C83.0137 15.831 83.5853 15.2533 84.2903 15.2533C84.9953 15.2533 85.5669 15.831 85.5669 16.5436V17.0471C85.5669 17.7597 84.9953 18.3374 84.2903 18.3374Z" />
|
||||
<path class="tw-fill-art-accent" fill-rule="evenodd" clip-rule="evenodd" d="M90.2644 18.3374C89.5594 18.3374 88.9878 17.7597 88.9878 17.0471V16.5436C88.9878 15.831 89.5594 15.2533 90.2644 15.2533C90.9695 15.2533 91.541 15.831 91.541 16.5436V17.0471C91.541 17.7597 90.9695 18.3374 90.2644 18.3374Z"/>
|
||||
<path class="tw-fill-art-primary" fill-rule="evenodd" clip-rule="evenodd" d="M95.7422 22.0817H0.638428V20.7914H95.7422V22.0817Z" />
|
||||
</svg>
|
||||
`;
|
||||
@@ -1,5 +1,4 @@
|
||||
export { PasswordRepromptService } from "./services/password-reprompt.service";
|
||||
export { NewDeviceVerificationNoticeService } from "./services/new-device-verification-notice.service";
|
||||
export { CopyCipherFieldService, CopyAction } from "./services/copy-cipher-field.service";
|
||||
export { CopyCipherFieldDirective } from "./components/copy-cipher-field.directive";
|
||||
export { OrgIconDirective } from "./components/org-icon.directive";
|
||||
@@ -16,8 +15,6 @@ export {
|
||||
|
||||
export { DownloadAttachmentComponent } from "./components/download-attachment/download-attachment.component";
|
||||
export { PasswordHistoryViewComponent } from "./components/password-history-view/password-history-view.component";
|
||||
export { NewDeviceVerificationNoticePageOneComponent } from "./components/new-device-verification-notice/new-device-verification-notice-page-one.component";
|
||||
export { NewDeviceVerificationNoticePageTwoComponent } from "./components/new-device-verification-notice/new-device-verification-notice-page-two.component";
|
||||
export { DecryptionFailureDialogComponent } from "./components/decryption-failure-dialog/decryption-failure-dialog.component";
|
||||
export { openPasswordHistoryDialog } from "./components/password-history/password-history.component";
|
||||
export * from "./components/add-edit-folder-dialog/add-edit-folder-dialog.component";
|
||||
|
||||
@@ -1,112 +0,0 @@
|
||||
import { firstValueFrom } from "rxjs";
|
||||
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
|
||||
import {
|
||||
FakeAccountService,
|
||||
FakeSingleUserState,
|
||||
FakeStateProvider,
|
||||
mockAccountServiceWith,
|
||||
} from "../../../common/spec";
|
||||
|
||||
import {
|
||||
NewDeviceVerificationNoticeService,
|
||||
NewDeviceVerificationNotice,
|
||||
NEW_DEVICE_VERIFICATION_NOTICE_KEY,
|
||||
SKIP_NEW_DEVICE_VERIFICATION_NOTICE,
|
||||
} from "./new-device-verification-notice.service";
|
||||
|
||||
describe("New Device Verification Notice", () => {
|
||||
const sut = NEW_DEVICE_VERIFICATION_NOTICE_KEY;
|
||||
const userId = Utils.newGuid() as UserId;
|
||||
let newDeviceVerificationService: NewDeviceVerificationNoticeService;
|
||||
let mockNoticeState: FakeSingleUserState<NewDeviceVerificationNotice>;
|
||||
let mockSkipState: FakeSingleUserState<boolean>;
|
||||
let stateProvider: FakeStateProvider;
|
||||
let accountService: FakeAccountService;
|
||||
|
||||
beforeEach(() => {
|
||||
accountService = mockAccountServiceWith(userId);
|
||||
stateProvider = new FakeStateProvider(accountService);
|
||||
mockNoticeState = stateProvider.singleUser.getFake(userId, NEW_DEVICE_VERIFICATION_NOTICE_KEY);
|
||||
mockSkipState = stateProvider.singleUser.getFake(userId, SKIP_NEW_DEVICE_VERIFICATION_NOTICE);
|
||||
newDeviceVerificationService = new NewDeviceVerificationNoticeService(stateProvider);
|
||||
});
|
||||
|
||||
it("should deserialize newDeviceVerificationNotice values", async () => {
|
||||
const currentDate = new Date();
|
||||
const inputObj = {
|
||||
last_dismissal: currentDate,
|
||||
permanent_dismissal: false,
|
||||
};
|
||||
|
||||
const expectedFolderData = {
|
||||
last_dismissal: currentDate.toJSON(),
|
||||
permanent_dismissal: false,
|
||||
};
|
||||
|
||||
const result = sut.deserializer(JSON.parse(JSON.stringify(inputObj)));
|
||||
|
||||
expect(result).toEqual(expectedFolderData);
|
||||
});
|
||||
|
||||
describe("notice$", () => {
|
||||
it("emits new device verification notice state", async () => {
|
||||
const currentDate = new Date();
|
||||
const data = {
|
||||
last_dismissal: currentDate,
|
||||
permanent_dismissal: false,
|
||||
};
|
||||
await stateProvider.setUserState(NEW_DEVICE_VERIFICATION_NOTICE_KEY, data, userId);
|
||||
|
||||
const result = await firstValueFrom(newDeviceVerificationService.noticeState$(userId));
|
||||
|
||||
expect(result).toBe(data);
|
||||
});
|
||||
});
|
||||
|
||||
describe("update notice state", () => {
|
||||
it("should update the date with a new value", async () => {
|
||||
const currentDate = new Date();
|
||||
const oldDate = new Date("11-11-2011");
|
||||
const oldState = {
|
||||
last_dismissal: oldDate,
|
||||
permanent_dismissal: false,
|
||||
};
|
||||
const newState = {
|
||||
last_dismissal: currentDate,
|
||||
permanent_dismissal: true,
|
||||
};
|
||||
mockNoticeState.nextState(oldState);
|
||||
await newDeviceVerificationService.updateNewDeviceVerificationNoticeState(userId, newState);
|
||||
|
||||
const result = await firstValueFrom(newDeviceVerificationService.noticeState$(userId));
|
||||
expect(result).toEqual(newState);
|
||||
});
|
||||
});
|
||||
|
||||
describe("skipNotice state", () => {
|
||||
it("emits skip notice state", async () => {
|
||||
const shouldSkip = true;
|
||||
await stateProvider.setUserState(SKIP_NEW_DEVICE_VERIFICATION_NOTICE, shouldSkip, userId);
|
||||
|
||||
const result = await firstValueFrom(newDeviceVerificationService.skipState$(userId));
|
||||
|
||||
expect(result).toBe(shouldSkip);
|
||||
});
|
||||
|
||||
it("should update the skip notice state", async () => {
|
||||
const initialSkipState = false;
|
||||
const updatedSkipState = true;
|
||||
mockSkipState.nextState(initialSkipState);
|
||||
await newDeviceVerificationService.updateNewDeviceVerificationSkipNoticeState(
|
||||
userId,
|
||||
updatedSkipState,
|
||||
);
|
||||
|
||||
const result = await firstValueFrom(newDeviceVerificationService.skipState$(userId));
|
||||
expect(result).toBe(updatedSkipState);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,89 +0,0 @@
|
||||
import { Injectable } from "@angular/core";
|
||||
import { Observable } from "rxjs";
|
||||
import { Jsonify } from "type-fest";
|
||||
|
||||
import {
|
||||
StateProvider,
|
||||
UserKeyDefinition,
|
||||
NEW_DEVICE_VERIFICATION_NOTICE,
|
||||
SingleUserState,
|
||||
} from "@bitwarden/common/platform/state";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
|
||||
// This service checks when to show New Device Verification Notice to Users
|
||||
// It will be a two phase approach and the values below will work with two different feature flags
|
||||
// If a user dismisses the notice, use "last_dismissal" to wait 7 days before re-prompting
|
||||
// permanent_dismissal will be checked if the user should never see the notice again
|
||||
export class NewDeviceVerificationNotice {
|
||||
last_dismissal: Date | null = null;
|
||||
permanent_dismissal: boolean | null = null;
|
||||
|
||||
constructor(obj: Partial<NewDeviceVerificationNotice>) {
|
||||
if (obj == null) {
|
||||
return;
|
||||
}
|
||||
this.last_dismissal = obj.last_dismissal || null;
|
||||
this.permanent_dismissal = obj.permanent_dismissal || null;
|
||||
}
|
||||
|
||||
static fromJSON(obj: Jsonify<NewDeviceVerificationNotice>) {
|
||||
return Object.assign(new NewDeviceVerificationNotice({}), obj);
|
||||
}
|
||||
}
|
||||
|
||||
export const NEW_DEVICE_VERIFICATION_NOTICE_KEY =
|
||||
new UserKeyDefinition<NewDeviceVerificationNotice>(
|
||||
NEW_DEVICE_VERIFICATION_NOTICE,
|
||||
"noticeState",
|
||||
{
|
||||
deserializer: (obj: Jsonify<NewDeviceVerificationNotice>) =>
|
||||
NewDeviceVerificationNotice.fromJSON(obj),
|
||||
clearOn: [],
|
||||
},
|
||||
);
|
||||
|
||||
export const SKIP_NEW_DEVICE_VERIFICATION_NOTICE = new UserKeyDefinition<boolean>(
|
||||
NEW_DEVICE_VERIFICATION_NOTICE,
|
||||
"shouldSkip",
|
||||
{
|
||||
deserializer: (data: boolean) => data,
|
||||
clearOn: ["logout"],
|
||||
},
|
||||
);
|
||||
|
||||
@Injectable()
|
||||
export class NewDeviceVerificationNoticeService {
|
||||
constructor(private stateProvider: StateProvider) {}
|
||||
|
||||
private noticeState(userId: UserId): SingleUserState<NewDeviceVerificationNotice> {
|
||||
return this.stateProvider.getUser(userId, NEW_DEVICE_VERIFICATION_NOTICE_KEY);
|
||||
}
|
||||
|
||||
noticeState$(userId: UserId): Observable<NewDeviceVerificationNotice | null> {
|
||||
return this.noticeState(userId).state$;
|
||||
}
|
||||
|
||||
async updateNewDeviceVerificationNoticeState(
|
||||
userId: UserId,
|
||||
newState: NewDeviceVerificationNotice,
|
||||
): Promise<void> {
|
||||
await this.noticeState(userId).update(() => {
|
||||
return { ...newState };
|
||||
});
|
||||
}
|
||||
|
||||
private skipState(userId: UserId): SingleUserState<boolean> {
|
||||
return this.stateProvider.getUser(userId, SKIP_NEW_DEVICE_VERIFICATION_NOTICE);
|
||||
}
|
||||
|
||||
skipState$(userId: UserId): Observable<boolean | null> {
|
||||
return this.skipState(userId).state$;
|
||||
}
|
||||
|
||||
async updateNewDeviceVerificationSkipNoticeState(
|
||||
userId: UserId,
|
||||
shouldSkip: boolean,
|
||||
): Promise<void> {
|
||||
await this.skipState(userId).update(() => shouldSkip);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user