mirror of
https://github.com/bitwarden/browser
synced 2025-12-12 14:23:32 +00:00
feat(auth): [PM-9693] Refresh LoginDecryptionOptionsComponent (#11782)
Creates a refreshed and consolidated `LoginDecryptionOptionsComponent` for use on all visual clients, which will be used when the `UnauthenticatedExtensionUIRefresh` feature flag is on.
This commit is contained in:
@@ -3287,9 +3287,18 @@
|
|||||||
"opensInANewWindow": {
|
"opensInANewWindow": {
|
||||||
"message": "Opens in a new window"
|
"message": "Opens in a new window"
|
||||||
},
|
},
|
||||||
|
"rememberThisDeviceToMakeFutureLoginsSeamless": {
|
||||||
|
"message": "Remember this device to make future logins seamless"
|
||||||
|
},
|
||||||
"deviceApprovalRequired": {
|
"deviceApprovalRequired": {
|
||||||
"message": "Device approval required. Select an approval option below:"
|
"message": "Device approval required. Select an approval option below:"
|
||||||
},
|
},
|
||||||
|
"deviceApprovalRequiredV2": {
|
||||||
|
"message": "Device approval required"
|
||||||
|
},
|
||||||
|
"selectAnApprovalOptionBelow": {
|
||||||
|
"message": "Select an approval option below"
|
||||||
|
},
|
||||||
"rememberThisDevice": {
|
"rememberThisDevice": {
|
||||||
"message": "Remember this device"
|
"message": "Remember this device"
|
||||||
},
|
},
|
||||||
@@ -3363,6 +3372,9 @@
|
|||||||
"userEmailMissing": {
|
"userEmailMissing": {
|
||||||
"message": "User email missing"
|
"message": "User email missing"
|
||||||
},
|
},
|
||||||
|
"activeUserEmailNotFoundLoggingYouOut": {
|
||||||
|
"message": "Active user email not found. Logging you out."
|
||||||
|
},
|
||||||
"deviceTrusted": {
|
"deviceTrusted": {
|
||||||
"message": "Device trusted"
|
"message": "Device trusted"
|
||||||
},
|
},
|
||||||
@@ -3799,6 +3811,9 @@
|
|||||||
"accessing": {
|
"accessing": {
|
||||||
"message": "Accessing"
|
"message": "Accessing"
|
||||||
},
|
},
|
||||||
|
"loggedInExclamation": {
|
||||||
|
"message": "Logged in!"
|
||||||
|
},
|
||||||
"passkeyNotCopied": {
|
"passkeyNotCopied": {
|
||||||
"message": "Passkey will not be copied"
|
"message": "Passkey will not be copied"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -0,0 +1,64 @@
|
|||||||
|
import { Router } from "@angular/router";
|
||||||
|
import { MockProxy, mock } from "jest-mock-extended";
|
||||||
|
import { BehaviorSubject } from "rxjs";
|
||||||
|
|
||||||
|
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||||
|
|
||||||
|
import { postLogoutMessageListener$ } from "../utils/post-logout-message-listener";
|
||||||
|
|
||||||
|
import { ExtensionLoginDecryptionOptionsService } from "./extension-login-decryption-options.service";
|
||||||
|
|
||||||
|
// Mock the module providing postLogoutMessageListener$
|
||||||
|
jest.mock("../utils/post-logout-message-listener", () => {
|
||||||
|
return {
|
||||||
|
postLogoutMessageListener$: new BehaviorSubject<string>(""), // Replace with mock subject
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("ExtensionLoginDecryptionOptionsService", () => {
|
||||||
|
let service: ExtensionLoginDecryptionOptionsService;
|
||||||
|
|
||||||
|
let messagingService: MockProxy<MessagingService>;
|
||||||
|
let router: MockProxy<Router>;
|
||||||
|
let postLogoutMessageSubject: BehaviorSubject<string>;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
messagingService = mock<MessagingService>();
|
||||||
|
router = mock<Router>();
|
||||||
|
|
||||||
|
// Cast postLogoutMessageListener$ to BehaviorSubject for dynamic control
|
||||||
|
postLogoutMessageSubject = postLogoutMessageListener$ as BehaviorSubject<string>;
|
||||||
|
|
||||||
|
service = new ExtensionLoginDecryptionOptionsService(messagingService, router);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should instantiate the service", () => {
|
||||||
|
expect(service).not.toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("logOut()", () => {
|
||||||
|
it("should send a logout message", async () => {
|
||||||
|
postLogoutMessageSubject.next("switchAccountFinish");
|
||||||
|
|
||||||
|
await service.logOut();
|
||||||
|
|
||||||
|
expect(messagingService.send).toHaveBeenCalledWith("logout");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should navigate to root on 'switchAccountFinish'", async () => {
|
||||||
|
postLogoutMessageSubject.next("switchAccountFinish");
|
||||||
|
|
||||||
|
await service.logOut();
|
||||||
|
|
||||||
|
expect(router.navigate).toHaveBeenCalledWith(["/"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not navigate for 'doneLoggingOut'", async () => {
|
||||||
|
postLogoutMessageSubject.next("doneLoggingOut");
|
||||||
|
|
||||||
|
await service.logOut();
|
||||||
|
|
||||||
|
expect(router.navigate).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
import { Router } from "@angular/router";
|
||||||
|
import { firstValueFrom } from "rxjs";
|
||||||
|
|
||||||
|
import {
|
||||||
|
DefaultLoginDecryptionOptionsService,
|
||||||
|
LoginDecryptionOptionsService,
|
||||||
|
} from "@bitwarden/auth/angular";
|
||||||
|
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||||
|
|
||||||
|
import { postLogoutMessageListener$ } from "../utils/post-logout-message-listener";
|
||||||
|
|
||||||
|
export class ExtensionLoginDecryptionOptionsService
|
||||||
|
extends DefaultLoginDecryptionOptionsService
|
||||||
|
implements LoginDecryptionOptionsService
|
||||||
|
{
|
||||||
|
constructor(
|
||||||
|
protected messagingService: MessagingService,
|
||||||
|
private router: Router,
|
||||||
|
) {
|
||||||
|
super(messagingService);
|
||||||
|
}
|
||||||
|
|
||||||
|
override async logOut(): Promise<void> {
|
||||||
|
// start listening for "switchAccountFinish" or "doneLoggingOut"
|
||||||
|
const messagePromise = firstValueFrom(postLogoutMessageListener$);
|
||||||
|
|
||||||
|
super.logOut();
|
||||||
|
|
||||||
|
// wait for messages
|
||||||
|
const command = await messagePromise;
|
||||||
|
|
||||||
|
// doneLoggingOut already has a message handler that will navigate us
|
||||||
|
if (command === "switchAccountFinish") {
|
||||||
|
await this.router.navigate(["/"]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,15 +1,15 @@
|
|||||||
import { Component } from "@angular/core";
|
import { Component } from "@angular/core";
|
||||||
import { firstValueFrom } from "rxjs";
|
import { firstValueFrom } from "rxjs";
|
||||||
|
|
||||||
import { BaseLoginDecryptionOptionsComponent } from "@bitwarden/angular/auth/components/base-login-decryption-options.component";
|
import { BaseLoginDecryptionOptionsComponentV1 } from "@bitwarden/angular/auth/components/base-login-decryption-options-v1.component";
|
||||||
|
|
||||||
import { postLogoutMessageListener$ } from "../utils/post-logout-message-listener";
|
import { postLogoutMessageListener$ } from "../utils/post-logout-message-listener";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: "browser-login-decryption-options",
|
selector: "browser-login-decryption-options",
|
||||||
templateUrl: "login-decryption-options.component.html",
|
templateUrl: "login-decryption-options-v1.component.html",
|
||||||
})
|
})
|
||||||
export class LoginDecryptionOptionsComponent extends BaseLoginDecryptionOptionsComponent {
|
export class LoginDecryptionOptionsComponentV1 extends BaseLoginDecryptionOptionsComponentV1 {
|
||||||
override async createUser(): Promise<void> {
|
override async createUser(): Promise<void> {
|
||||||
try {
|
try {
|
||||||
await super.createUser();
|
await super.createUser();
|
||||||
@@ -21,7 +21,6 @@ import { extensionRefreshSwap } from "@bitwarden/angular/utils/extension-refresh
|
|||||||
import {
|
import {
|
||||||
AnonLayoutWrapperComponent,
|
AnonLayoutWrapperComponent,
|
||||||
AnonLayoutWrapperData,
|
AnonLayoutWrapperData,
|
||||||
DevicesIcon,
|
|
||||||
LoginComponent,
|
LoginComponent,
|
||||||
LoginSecondaryContentComponent,
|
LoginSecondaryContentComponent,
|
||||||
LockIcon,
|
LockIcon,
|
||||||
@@ -37,6 +36,8 @@ import {
|
|||||||
SetPasswordJitComponent,
|
SetPasswordJitComponent,
|
||||||
UserLockIcon,
|
UserLockIcon,
|
||||||
VaultIcon,
|
VaultIcon,
|
||||||
|
LoginDecryptionOptionsComponent,
|
||||||
|
DevicesIcon,
|
||||||
} from "@bitwarden/auth/angular";
|
} from "@bitwarden/auth/angular";
|
||||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||||
|
|
||||||
@@ -51,7 +52,7 @@ import {
|
|||||||
import { HintComponent } from "../auth/popup/hint.component";
|
import { HintComponent } from "../auth/popup/hint.component";
|
||||||
import { HomeComponent } from "../auth/popup/home.component";
|
import { HomeComponent } from "../auth/popup/home.component";
|
||||||
import { LockComponent } from "../auth/popup/lock.component";
|
import { LockComponent } from "../auth/popup/lock.component";
|
||||||
import { LoginDecryptionOptionsComponent } from "../auth/popup/login-decryption-options/login-decryption-options.component";
|
import { LoginDecryptionOptionsComponentV1 } from "../auth/popup/login-decryption-options/login-decryption-options-v1.component";
|
||||||
import { LoginComponentV1 } from "../auth/popup/login-v1.component";
|
import { LoginComponentV1 } from "../auth/popup/login-v1.component";
|
||||||
import { LoginViaAuthRequestComponentV1 } from "../auth/popup/login-via-auth-request-v1.component";
|
import { LoginViaAuthRequestComponentV1 } from "../auth/popup/login-via-auth-request-v1.component";
|
||||||
import { RegisterComponent } from "../auth/popup/register.component";
|
import { RegisterComponent } from "../auth/popup/register.component";
|
||||||
@@ -206,12 +207,6 @@ const routes: Routes = [
|
|||||||
canActivate: [unauthGuardFn(unauthRouteOverrides)],
|
canActivate: [unauthGuardFn(unauthRouteOverrides)],
|
||||||
data: { state: "2fa-options" } satisfies RouteDataProperties,
|
data: { state: "2fa-options" } satisfies RouteDataProperties,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
path: "login-initiated",
|
|
||||||
component: LoginDecryptionOptionsComponent,
|
|
||||||
canActivate: [tdeDecryptionRequiredGuard()],
|
|
||||||
data: { state: "login-initiated" } satisfies RouteDataProperties,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
path: "sso",
|
path: "sso",
|
||||||
component: SsoComponent,
|
component: SsoComponent,
|
||||||
@@ -534,6 +529,23 @@ const routes: Routes = [
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
...unauthUiRefreshSwap(
|
||||||
|
LoginDecryptionOptionsComponentV1,
|
||||||
|
ExtensionAnonLayoutWrapperComponent,
|
||||||
|
{
|
||||||
|
path: "login-initiated",
|
||||||
|
canActivate: [tdeDecryptionRequiredGuard()],
|
||||||
|
data: { state: "login-initiated" } satisfies RouteDataProperties,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "login-initiated",
|
||||||
|
canActivate: [tdeDecryptionRequiredGuard()],
|
||||||
|
data: {
|
||||||
|
pageIcon: DevicesIcon,
|
||||||
|
},
|
||||||
|
children: [{ path: "", component: LoginDecryptionOptionsComponent }],
|
||||||
|
},
|
||||||
|
),
|
||||||
{
|
{
|
||||||
path: "",
|
path: "",
|
||||||
component: ExtensionAnonLayoutWrapperComponent,
|
component: ExtensionAnonLayoutWrapperComponent,
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ import { ExtensionAnonLayoutWrapperComponent } from "../auth/popup/extension-ano
|
|||||||
import { HintComponent } from "../auth/popup/hint.component";
|
import { HintComponent } from "../auth/popup/hint.component";
|
||||||
import { HomeComponent } from "../auth/popup/home.component";
|
import { HomeComponent } from "../auth/popup/home.component";
|
||||||
import { LockComponent } from "../auth/popup/lock.component";
|
import { LockComponent } from "../auth/popup/lock.component";
|
||||||
import { LoginDecryptionOptionsComponent } from "../auth/popup/login-decryption-options/login-decryption-options.component";
|
import { LoginDecryptionOptionsComponentV1 } from "../auth/popup/login-decryption-options/login-decryption-options-v1.component";
|
||||||
import { LoginComponentV1 } from "../auth/popup/login-v1.component";
|
import { LoginComponentV1 } from "../auth/popup/login-v1.component";
|
||||||
import { LoginViaAuthRequestComponentV1 } from "../auth/popup/login-via-auth-request-v1.component";
|
import { LoginViaAuthRequestComponentV1 } from "../auth/popup/login-via-auth-request-v1.component";
|
||||||
import { RegisterComponent } from "../auth/popup/register.component";
|
import { RegisterComponent } from "../auth/popup/register.component";
|
||||||
@@ -161,7 +161,7 @@ import "../platform/popup/locales";
|
|||||||
LockComponent,
|
LockComponent,
|
||||||
LoginViaAuthRequestComponentV1,
|
LoginViaAuthRequestComponentV1,
|
||||||
LoginComponentV1,
|
LoginComponentV1,
|
||||||
LoginDecryptionOptionsComponent,
|
LoginDecryptionOptionsComponentV1,
|
||||||
NotificationsSettingsV1Component,
|
NotificationsSettingsV1Component,
|
||||||
AppearanceComponent,
|
AppearanceComponent,
|
||||||
GeneratorComponent,
|
GeneratorComponent,
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { APP_INITIALIZER, NgModule, NgZone } from "@angular/core";
|
import { APP_INITIALIZER, NgModule, NgZone } from "@angular/core";
|
||||||
|
import { Router } from "@angular/router";
|
||||||
import { Subject, merge, of } from "rxjs";
|
import { Subject, merge, of } from "rxjs";
|
||||||
|
|
||||||
import { CollectionService } from "@bitwarden/admin-console/common";
|
import { CollectionService } from "@bitwarden/admin-console/common";
|
||||||
@@ -22,6 +23,7 @@ import {
|
|||||||
AnonLayoutWrapperDataService,
|
AnonLayoutWrapperDataService,
|
||||||
LoginComponentService,
|
LoginComponentService,
|
||||||
LockComponentService,
|
LockComponentService,
|
||||||
|
LoginDecryptionOptionsService,
|
||||||
} from "@bitwarden/auth/angular";
|
} from "@bitwarden/auth/angular";
|
||||||
import { LockService, LoginEmailService, PinServiceAbstraction } from "@bitwarden/auth/common";
|
import { LockService, LoginEmailService, PinServiceAbstraction } from "@bitwarden/auth/common";
|
||||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||||
@@ -115,6 +117,7 @@ import { PasswordRepromptService } from "@bitwarden/vault";
|
|||||||
import { ForegroundLockService } from "../../auth/popup/accounts/foreground-lock.service";
|
import { ForegroundLockService } from "../../auth/popup/accounts/foreground-lock.service";
|
||||||
import { ExtensionAnonLayoutWrapperDataService } from "../../auth/popup/extension-anon-layout-wrapper/extension-anon-layout-wrapper-data.service";
|
import { ExtensionAnonLayoutWrapperDataService } from "../../auth/popup/extension-anon-layout-wrapper/extension-anon-layout-wrapper-data.service";
|
||||||
import { ExtensionLoginComponentService } from "../../auth/popup/login/extension-login-component.service";
|
import { ExtensionLoginComponentService } from "../../auth/popup/login/extension-login-component.service";
|
||||||
|
import { ExtensionLoginDecryptionOptionsService } from "../../auth/popup/login-decryption-options/extension-login-decryption-options.service";
|
||||||
import { AutofillService as AutofillServiceAbstraction } from "../../autofill/services/abstractions/autofill.service";
|
import { AutofillService as AutofillServiceAbstraction } from "../../autofill/services/abstractions/autofill.service";
|
||||||
import AutofillService from "../../autofill/services/autofill.service";
|
import AutofillService from "../../autofill/services/autofill.service";
|
||||||
import { InlineMenuFieldQualificationService } from "../../autofill/services/inline-menu-field-qualification.service";
|
import { InlineMenuFieldQualificationService } from "../../autofill/services/inline-menu-field-qualification.service";
|
||||||
@@ -591,6 +594,11 @@ const safeProviders: SafeProvider[] = [
|
|||||||
useExisting: PopupCompactModeService,
|
useExisting: PopupCompactModeService,
|
||||||
deps: [],
|
deps: [],
|
||||||
}),
|
}),
|
||||||
|
safeProvider({
|
||||||
|
provide: LoginDecryptionOptionsService,
|
||||||
|
useClass: ExtensionLoginDecryptionOptionsService,
|
||||||
|
deps: [MessagingServiceAbstraction, Router],
|
||||||
|
}),
|
||||||
];
|
];
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ import { extensionRefreshRedirect } from "@bitwarden/angular/utils/extension-ref
|
|||||||
import {
|
import {
|
||||||
AnonLayoutWrapperComponent,
|
AnonLayoutWrapperComponent,
|
||||||
AnonLayoutWrapperData,
|
AnonLayoutWrapperData,
|
||||||
DevicesIcon,
|
|
||||||
LoginComponent,
|
LoginComponent,
|
||||||
LoginSecondaryContentComponent,
|
LoginSecondaryContentComponent,
|
||||||
LockIcon,
|
LockIcon,
|
||||||
@@ -34,6 +33,8 @@ import {
|
|||||||
SetPasswordJitComponent,
|
SetPasswordJitComponent,
|
||||||
UserLockIcon,
|
UserLockIcon,
|
||||||
VaultIcon,
|
VaultIcon,
|
||||||
|
LoginDecryptionOptionsComponent,
|
||||||
|
DevicesIcon,
|
||||||
} from "@bitwarden/auth/angular";
|
} from "@bitwarden/auth/angular";
|
||||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||||
|
|
||||||
@@ -42,7 +43,7 @@ import { AccessibilityCookieComponent } from "../auth/accessibility-cookie.compo
|
|||||||
import { maxAccountsGuardFn } from "../auth/guards/max-accounts.guard";
|
import { maxAccountsGuardFn } from "../auth/guards/max-accounts.guard";
|
||||||
import { HintComponent } from "../auth/hint.component";
|
import { HintComponent } from "../auth/hint.component";
|
||||||
import { LockComponent } from "../auth/lock.component";
|
import { LockComponent } from "../auth/lock.component";
|
||||||
import { LoginDecryptionOptionsComponent } from "../auth/login/login-decryption-options/login-decryption-options.component";
|
import { LoginDecryptionOptionsComponentV1 } from "../auth/login/login-decryption-options/login-decryption-options-v1.component";
|
||||||
import { LoginComponentV1 } from "../auth/login/login-v1.component";
|
import { LoginComponentV1 } from "../auth/login/login-v1.component";
|
||||||
import { LoginViaAuthRequestComponentV1 } from "../auth/login/login-via-auth-request-v1.component";
|
import { LoginViaAuthRequestComponentV1 } from "../auth/login/login-via-auth-request-v1.component";
|
||||||
import { RegisterComponent } from "../auth/register.component";
|
import { RegisterComponent } from "../auth/register.component";
|
||||||
@@ -95,11 +96,6 @@ const routes: Routes = [
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
{
|
|
||||||
path: "login-initiated",
|
|
||||||
component: LoginDecryptionOptionsComponent,
|
|
||||||
canActivate: [tdeDecryptionRequiredGuard()],
|
|
||||||
},
|
|
||||||
{ path: "register", component: RegisterComponent },
|
{ path: "register", component: RegisterComponent },
|
||||||
{
|
{
|
||||||
path: "vault",
|
path: "vault",
|
||||||
@@ -241,6 +237,22 @@ const routes: Routes = [
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
...unauthUiRefreshSwap(
|
||||||
|
LoginDecryptionOptionsComponentV1,
|
||||||
|
AnonLayoutWrapperComponent,
|
||||||
|
{
|
||||||
|
path: "login-initiated",
|
||||||
|
canActivate: [tdeDecryptionRequiredGuard()],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "login-initiated",
|
||||||
|
canActivate: [tdeDecryptionRequiredGuard()],
|
||||||
|
data: {
|
||||||
|
pageIcon: DevicesIcon,
|
||||||
|
},
|
||||||
|
children: [{ path: "", component: LoginDecryptionOptionsComponent }],
|
||||||
|
},
|
||||||
|
),
|
||||||
{
|
{
|
||||||
path: "",
|
path: "",
|
||||||
component: AnonLayoutWrapperComponent,
|
component: AnonLayoutWrapperComponent,
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import { Component } from "@angular/core";
|
import { Component } from "@angular/core";
|
||||||
|
|
||||||
import { BaseLoginDecryptionOptionsComponent } from "@bitwarden/angular/auth/components/base-login-decryption-options.component";
|
import { BaseLoginDecryptionOptionsComponentV1 } from "@bitwarden/angular/auth/components/base-login-decryption-options-v1.component";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: "desktop-login-decryption-options",
|
selector: "desktop-login-decryption-options",
|
||||||
templateUrl: "login-decryption-options.component.html",
|
templateUrl: "login-decryption-options-v1.component.html",
|
||||||
})
|
})
|
||||||
export class LoginDecryptionOptionsComponent extends BaseLoginDecryptionOptionsComponent {
|
export class LoginDecryptionOptionsComponentV1 extends BaseLoginDecryptionOptionsComponentV1 {
|
||||||
override async createUser(): Promise<void> {
|
override async createUser(): Promise<void> {
|
||||||
try {
|
try {
|
||||||
await super.createUser();
|
await super.createUser();
|
||||||
@@ -5,7 +5,7 @@ import { EnvironmentSelectorComponent } from "@bitwarden/angular/auth/components
|
|||||||
|
|
||||||
import { SharedModule } from "../../app/shared/shared.module";
|
import { SharedModule } from "../../app/shared/shared.module";
|
||||||
|
|
||||||
import { LoginDecryptionOptionsComponent } from "./login-decryption-options/login-decryption-options.component";
|
import { LoginDecryptionOptionsComponentV1 } from "./login-decryption-options/login-decryption-options-v1.component";
|
||||||
import { LoginComponentV1 } from "./login-v1.component";
|
import { LoginComponentV1 } from "./login-v1.component";
|
||||||
import { LoginViaAuthRequestComponentV1 } from "./login-via-auth-request-v1.component";
|
import { LoginViaAuthRequestComponentV1 } from "./login-via-auth-request-v1.component";
|
||||||
|
|
||||||
@@ -15,7 +15,7 @@ import { LoginViaAuthRequestComponentV1 } from "./login-via-auth-request-v1.comp
|
|||||||
LoginComponentV1,
|
LoginComponentV1,
|
||||||
LoginViaAuthRequestComponentV1,
|
LoginViaAuthRequestComponentV1,
|
||||||
EnvironmentSelectorComponent,
|
EnvironmentSelectorComponent,
|
||||||
LoginDecryptionOptionsComponent,
|
LoginDecryptionOptionsComponentV1,
|
||||||
],
|
],
|
||||||
exports: [LoginComponentV1, LoginViaAuthRequestComponentV1],
|
exports: [LoginComponentV1, LoginViaAuthRequestComponentV1],
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -2832,6 +2832,9 @@
|
|||||||
"checkForBreaches": {
|
"checkForBreaches": {
|
||||||
"message": "Check known data breaches for this password"
|
"message": "Check known data breaches for this password"
|
||||||
},
|
},
|
||||||
|
"loggedInExclamation": {
|
||||||
|
"message": "Logged in!"
|
||||||
|
},
|
||||||
"important": {
|
"important": {
|
||||||
"message": "Important:"
|
"message": "Important:"
|
||||||
},
|
},
|
||||||
@@ -2862,9 +2865,18 @@
|
|||||||
"windowsBiometricUpdateWarningTitle": {
|
"windowsBiometricUpdateWarningTitle": {
|
||||||
"message": "Recommended Settings Update"
|
"message": "Recommended Settings Update"
|
||||||
},
|
},
|
||||||
|
"rememberThisDeviceToMakeFutureLoginsSeamless": {
|
||||||
|
"message": "Remember this device to make future logins seamless"
|
||||||
|
},
|
||||||
"deviceApprovalRequired": {
|
"deviceApprovalRequired": {
|
||||||
"message": "Device approval required. Select an approval option below:"
|
"message": "Device approval required. Select an approval option below:"
|
||||||
},
|
},
|
||||||
|
"deviceApprovalRequiredV2": {
|
||||||
|
"message": "Device approval required"
|
||||||
|
},
|
||||||
|
"selectAnApprovalOptionBelow": {
|
||||||
|
"message": "Select an approval option below"
|
||||||
|
},
|
||||||
"rememberThisDevice": {
|
"rememberThisDevice": {
|
||||||
"message": "Remember this device"
|
"message": "Remember this device"
|
||||||
},
|
},
|
||||||
@@ -2917,6 +2929,9 @@
|
|||||||
"userEmailMissing": {
|
"userEmailMissing": {
|
||||||
"message": "User email missing"
|
"message": "User email missing"
|
||||||
},
|
},
|
||||||
|
"activeUserEmailNotFoundLoggingYouOut": {
|
||||||
|
"message": "Active user email not found. Logging you out."
|
||||||
|
},
|
||||||
"deviceTrusted": {
|
"deviceTrusted": {
|
||||||
"message": "Device trusted"
|
"message": "Device trusted"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
export * from "./login";
|
export * from "./login";
|
||||||
|
export * from "./login-decryption-options";
|
||||||
export * from "./webauthn-login";
|
export * from "./webauthn-login";
|
||||||
export * from "./set-password-jit";
|
export * from "./set-password-jit";
|
||||||
export * from "./registration";
|
export * from "./registration";
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
export * from "./web-login-decryption-options.service";
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
import { MockProxy, mock } from "jest-mock-extended";
|
||||||
|
|
||||||
|
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||||
|
|
||||||
|
import { RouterService } from "../../../../core/router.service";
|
||||||
|
import { AcceptOrganizationInviteService } from "../../../organization-invite/accept-organization.service";
|
||||||
|
|
||||||
|
import { WebLoginDecryptionOptionsService } from "./web-login-decryption-options.service";
|
||||||
|
|
||||||
|
describe("WebLoginDecryptionOptionsService", () => {
|
||||||
|
let service: WebLoginDecryptionOptionsService;
|
||||||
|
|
||||||
|
let messagingService: MockProxy<MessagingService>;
|
||||||
|
let routerService: MockProxy<RouterService>;
|
||||||
|
let acceptOrganizationInviteService: MockProxy<AcceptOrganizationInviteService>;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
messagingService = mock<MessagingService>();
|
||||||
|
routerService = mock<RouterService>();
|
||||||
|
acceptOrganizationInviteService = mock<AcceptOrganizationInviteService>();
|
||||||
|
|
||||||
|
service = new WebLoginDecryptionOptionsService(
|
||||||
|
messagingService,
|
||||||
|
routerService,
|
||||||
|
acceptOrganizationInviteService,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should instantiate the service", () => {
|
||||||
|
expect(service).not.toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("handleCreateUserSuccess()", () => {
|
||||||
|
it("should clear the redirect URL and the org invite", async () => {
|
||||||
|
await service.handleCreateUserSuccess();
|
||||||
|
|
||||||
|
expect(routerService.getAndClearLoginRedirectUrl).toHaveBeenCalled();
|
||||||
|
expect(acceptOrganizationInviteService.clearOrganizationInvitation).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
import {
|
||||||
|
LoginDecryptionOptionsService,
|
||||||
|
DefaultLoginDecryptionOptionsService,
|
||||||
|
} from "@bitwarden/auth/angular";
|
||||||
|
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||||
|
|
||||||
|
import { RouterService } from "../../../../core/router.service";
|
||||||
|
import { AcceptOrganizationInviteService } from "../../../organization-invite/accept-organization.service";
|
||||||
|
|
||||||
|
export class WebLoginDecryptionOptionsService
|
||||||
|
extends DefaultLoginDecryptionOptionsService
|
||||||
|
implements LoginDecryptionOptionsService
|
||||||
|
{
|
||||||
|
constructor(
|
||||||
|
protected messagingService: MessagingService,
|
||||||
|
private routerService: RouterService,
|
||||||
|
private acceptOrganizationInviteService: AcceptOrganizationInviteService,
|
||||||
|
) {
|
||||||
|
super(messagingService);
|
||||||
|
}
|
||||||
|
|
||||||
|
override async handleCreateUserSuccess(): Promise<void> {
|
||||||
|
try {
|
||||||
|
// Invites from TDE orgs go through here, but the invite is
|
||||||
|
// accepted while being enrolled in admin recovery. So we need to clear
|
||||||
|
// the redirect and stored org invite.
|
||||||
|
await this.routerService.getAndClearLoginRedirectUrl();
|
||||||
|
await this.acceptOrganizationInviteService.clearOrganizationInvitation();
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,14 +1,14 @@
|
|||||||
import { Component, inject } from "@angular/core";
|
import { Component, inject } from "@angular/core";
|
||||||
|
|
||||||
import { BaseLoginDecryptionOptionsComponent } from "@bitwarden/angular/auth/components/base-login-decryption-options.component";
|
import { BaseLoginDecryptionOptionsComponentV1 } from "@bitwarden/angular/auth/components/base-login-decryption-options-v1.component";
|
||||||
|
|
||||||
import { RouterService } from "../../../core";
|
import { RouterService } from "../../../core";
|
||||||
import { AcceptOrganizationInviteService } from "../../organization-invite/accept-organization.service";
|
import { AcceptOrganizationInviteService } from "../../organization-invite/accept-organization.service";
|
||||||
@Component({
|
@Component({
|
||||||
selector: "web-login-decryption-options",
|
selector: "web-login-decryption-options",
|
||||||
templateUrl: "login-decryption-options.component.html",
|
templateUrl: "login-decryption-options-v1.component.html",
|
||||||
})
|
})
|
||||||
export class LoginDecryptionOptionsComponent extends BaseLoginDecryptionOptionsComponent {
|
export class LoginDecryptionOptionsComponentV1 extends BaseLoginDecryptionOptionsComponentV1 {
|
||||||
protected routerService = inject(RouterService);
|
protected routerService = inject(RouterService);
|
||||||
protected acceptOrganizationInviteService = inject(AcceptOrganizationInviteService);
|
protected acceptOrganizationInviteService = inject(AcceptOrganizationInviteService);
|
||||||
|
|
||||||
@@ -4,7 +4,7 @@ import { CheckboxModule } from "@bitwarden/components";
|
|||||||
|
|
||||||
import { SharedModule } from "../../../app/shared";
|
import { SharedModule } from "../../../app/shared";
|
||||||
|
|
||||||
import { LoginDecryptionOptionsComponent } from "./login-decryption-options/login-decryption-options.component";
|
import { LoginDecryptionOptionsComponentV1 } from "./login-decryption-options/login-decryption-options-v1.component";
|
||||||
import { LoginComponentV1 } from "./login-v1.component";
|
import { LoginComponentV1 } from "./login-v1.component";
|
||||||
import { LoginViaAuthRequestComponentV1 } from "./login-via-auth-request-v1.component";
|
import { LoginViaAuthRequestComponentV1 } from "./login-via-auth-request-v1.component";
|
||||||
import { LoginViaWebAuthnComponent } from "./login-via-webauthn/login-via-webauthn.component";
|
import { LoginViaWebAuthnComponent } from "./login-via-webauthn/login-via-webauthn.component";
|
||||||
@@ -14,13 +14,13 @@ import { LoginViaWebAuthnComponent } from "./login-via-webauthn/login-via-webaut
|
|||||||
declarations: [
|
declarations: [
|
||||||
LoginComponentV1,
|
LoginComponentV1,
|
||||||
LoginViaAuthRequestComponentV1,
|
LoginViaAuthRequestComponentV1,
|
||||||
LoginDecryptionOptionsComponent,
|
LoginDecryptionOptionsComponentV1,
|
||||||
LoginViaWebAuthnComponent,
|
LoginViaWebAuthnComponent,
|
||||||
],
|
],
|
||||||
exports: [
|
exports: [
|
||||||
LoginComponentV1,
|
LoginComponentV1,
|
||||||
LoginViaAuthRequestComponentV1,
|
LoginViaAuthRequestComponentV1,
|
||||||
LoginDecryptionOptionsComponent,
|
LoginDecryptionOptionsComponentV1,
|
||||||
LoginViaWebAuthnComponent,
|
LoginViaWebAuthnComponent,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ import {
|
|||||||
LoginComponentService,
|
LoginComponentService,
|
||||||
LockComponentService,
|
LockComponentService,
|
||||||
SetPasswordJitService,
|
SetPasswordJitService,
|
||||||
|
LoginDecryptionOptionsService,
|
||||||
} from "@bitwarden/auth/angular";
|
} from "@bitwarden/auth/angular";
|
||||||
import {
|
import {
|
||||||
InternalUserDecryptionOptionsServiceAbstraction,
|
InternalUserDecryptionOptionsServiceAbstraction,
|
||||||
@@ -60,6 +61,7 @@ import {
|
|||||||
import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service";
|
import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service";
|
||||||
import { I18nService as I18nServiceAbstraction } from "@bitwarden/common/platform/abstractions/i18n.service";
|
import { I18nService as I18nServiceAbstraction } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.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 { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||||
import { SdkClientFactory } from "@bitwarden/common/platform/abstractions/sdk/sdk-client-factory";
|
import { SdkClientFactory } from "@bitwarden/common/platform/abstractions/sdk/sdk-client-factory";
|
||||||
import { AbstractStorageService } from "@bitwarden/common/platform/abstractions/storage.service";
|
import { AbstractStorageService } from "@bitwarden/common/platform/abstractions/storage.service";
|
||||||
@@ -95,6 +97,7 @@ import {
|
|||||||
WebRegistrationFinishService,
|
WebRegistrationFinishService,
|
||||||
WebLoginComponentService,
|
WebLoginComponentService,
|
||||||
WebLockComponentService,
|
WebLockComponentService,
|
||||||
|
WebLoginDecryptionOptionsService,
|
||||||
} from "../auth";
|
} from "../auth";
|
||||||
import { AcceptOrganizationInviteService } from "../auth/organization-invite/accept-organization.service";
|
import { AcceptOrganizationInviteService } from "../auth/organization-invite/accept-organization.service";
|
||||||
import { HtmlStorageService } from "../core/html-storage.service";
|
import { HtmlStorageService } from "../core/html-storage.service";
|
||||||
@@ -296,6 +299,11 @@ const safeProviders: SafeProvider[] = [
|
|||||||
useClass: LoginEmailService,
|
useClass: LoginEmailService,
|
||||||
deps: [AccountService, AuthService, StateProvider],
|
deps: [AccountService, AuthService, StateProvider],
|
||||||
}),
|
}),
|
||||||
|
safeProvider({
|
||||||
|
provide: LoginDecryptionOptionsService,
|
||||||
|
useClass: WebLoginDecryptionOptionsService,
|
||||||
|
deps: [MessagingService, RouterService, AcceptOrganizationInviteService],
|
||||||
|
}),
|
||||||
];
|
];
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ import {
|
|||||||
RegistrationLockAltIcon,
|
RegistrationLockAltIcon,
|
||||||
RegistrationExpiredLinkIcon,
|
RegistrationExpiredLinkIcon,
|
||||||
VaultIcon,
|
VaultIcon,
|
||||||
|
LoginDecryptionOptionsComponent,
|
||||||
} from "@bitwarden/auth/angular";
|
} from "@bitwarden/auth/angular";
|
||||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||||
|
|
||||||
@@ -46,7 +47,7 @@ import { CreateOrganizationComponent } from "./admin-console/settings/create-org
|
|||||||
import { deepLinkGuard } from "./auth/guards/deep-link.guard";
|
import { deepLinkGuard } from "./auth/guards/deep-link.guard";
|
||||||
import { HintComponent } from "./auth/hint.component";
|
import { HintComponent } from "./auth/hint.component";
|
||||||
import { LockComponent } from "./auth/lock.component";
|
import { LockComponent } from "./auth/lock.component";
|
||||||
import { LoginDecryptionOptionsComponent } from "./auth/login/login-decryption-options/login-decryption-options.component";
|
import { LoginDecryptionOptionsComponentV1 } from "./auth/login/login-decryption-options/login-decryption-options-v1.component";
|
||||||
import { LoginComponentV1 } from "./auth/login/login-v1.component";
|
import { LoginComponentV1 } from "./auth/login/login-v1.component";
|
||||||
import { LoginViaAuthRequestComponentV1 } from "./auth/login/login-via-auth-request-v1.component";
|
import { LoginViaAuthRequestComponentV1 } from "./auth/login/login-via-auth-request-v1.component";
|
||||||
import { LoginViaWebAuthnComponent } from "./auth/login/login-via-webauthn/login-via-webauthn.component";
|
import { LoginViaWebAuthnComponent } from "./auth/login/login-via-webauthn/login-via-webauthn.component";
|
||||||
@@ -103,11 +104,6 @@ const routes: Routes = [
|
|||||||
component: LoginViaWebAuthnComponent,
|
component: LoginViaWebAuthnComponent,
|
||||||
data: { titleId: "logInWithPasskey" } satisfies RouteDataProperties,
|
data: { titleId: "logInWithPasskey" } satisfies RouteDataProperties,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
path: "login-initiated",
|
|
||||||
component: LoginDecryptionOptionsComponent,
|
|
||||||
canActivate: [tdeDecryptionRequiredGuard()],
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
path: "register",
|
path: "register",
|
||||||
component: TrialInitiationComponent,
|
component: TrialInitiationComponent,
|
||||||
@@ -272,6 +268,22 @@ const routes: Routes = [
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
...unauthUiRefreshSwap(
|
||||||
|
LoginDecryptionOptionsComponentV1,
|
||||||
|
AnonLayoutWrapperComponent,
|
||||||
|
{
|
||||||
|
path: "login-initiated",
|
||||||
|
canActivate: [tdeDecryptionRequiredGuard()],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "login-initiated",
|
||||||
|
canActivate: [tdeDecryptionRequiredGuard()],
|
||||||
|
data: {
|
||||||
|
pageIcon: DevicesIcon,
|
||||||
|
},
|
||||||
|
children: [{ path: "", component: LoginDecryptionOptionsComponent }],
|
||||||
|
},
|
||||||
|
),
|
||||||
...unauthUiRefreshSwap(
|
...unauthUiRefreshSwap(
|
||||||
AnonLayoutWrapperComponent,
|
AnonLayoutWrapperComponent,
|
||||||
AnonLayoutWrapperComponent,
|
AnonLayoutWrapperComponent,
|
||||||
|
|||||||
@@ -8051,9 +8051,18 @@
|
|||||||
"loginInitiated": {
|
"loginInitiated": {
|
||||||
"message": "Login initiated"
|
"message": "Login initiated"
|
||||||
},
|
},
|
||||||
|
"rememberThisDeviceToMakeFutureLoginsSeamless": {
|
||||||
|
"message": "Remember this device to make future logins seamless"
|
||||||
|
},
|
||||||
"deviceApprovalRequired": {
|
"deviceApprovalRequired": {
|
||||||
"message": "Device approval required. Select an approval option below:"
|
"message": "Device approval required. Select an approval option below:"
|
||||||
},
|
},
|
||||||
|
"deviceApprovalRequiredV2": {
|
||||||
|
"message": "Device approval required"
|
||||||
|
},
|
||||||
|
"selectAnApprovalOptionBelow": {
|
||||||
|
"message": "Select an approval option below"
|
||||||
|
},
|
||||||
"rememberThisDevice": {
|
"rememberThisDevice": {
|
||||||
"message": "Remember this device"
|
"message": "Remember this device"
|
||||||
},
|
},
|
||||||
@@ -8283,6 +8292,9 @@
|
|||||||
"userEmailMissing": {
|
"userEmailMissing": {
|
||||||
"message": "User email missing"
|
"message": "User email missing"
|
||||||
},
|
},
|
||||||
|
"activeUserEmailNotFoundLoggingYouOut": {
|
||||||
|
"message": "Active user email not found. Logging you out."
|
||||||
|
},
|
||||||
"deviceTrusted": {
|
"deviceTrusted": {
|
||||||
"message": "Device trusted"
|
"message": "Device trusted"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ type ExistingUserUntrustedDeviceData = {
|
|||||||
type Data = NewUserData | ExistingUserUntrustedDeviceData;
|
type Data = NewUserData | ExistingUserUntrustedDeviceData;
|
||||||
|
|
||||||
@Directive()
|
@Directive()
|
||||||
export class BaseLoginDecryptionOptionsComponent implements OnInit, OnDestroy {
|
export class BaseLoginDecryptionOptionsComponentV1 implements OnInit, OnDestroy {
|
||||||
private destroy$ = new Subject<void>();
|
private destroy$ = new Subject<void>();
|
||||||
|
|
||||||
protected State = State;
|
protected State = State;
|
||||||
@@ -16,6 +16,8 @@ import {
|
|||||||
DefaultAnonLayoutWrapperDataService,
|
DefaultAnonLayoutWrapperDataService,
|
||||||
LoginComponentService,
|
LoginComponentService,
|
||||||
DefaultLoginComponentService,
|
DefaultLoginComponentService,
|
||||||
|
LoginDecryptionOptionsService,
|
||||||
|
DefaultLoginDecryptionOptionsService,
|
||||||
} from "@bitwarden/auth/angular";
|
} from "@bitwarden/auth/angular";
|
||||||
import {
|
import {
|
||||||
AuthRequestServiceAbstraction,
|
AuthRequestServiceAbstraction,
|
||||||
@@ -1384,6 +1386,11 @@ const safeProviders: SafeProvider[] = [
|
|||||||
useClass: DefaultAuthRequestApiService,
|
useClass: DefaultAuthRequestApiService,
|
||||||
deps: [ApiServiceAbstraction, LogService],
|
deps: [ApiServiceAbstraction, LogService],
|
||||||
}),
|
}),
|
||||||
|
safeProvider({
|
||||||
|
provide: LoginDecryptionOptionsService,
|
||||||
|
useClass: DefaultLoginDecryptionOptionsService,
|
||||||
|
deps: [MessagingServiceAbstraction],
|
||||||
|
}),
|
||||||
];
|
];
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
|
|||||||
@@ -24,6 +24,11 @@ export * from "./login/login-secondary-content.component";
|
|||||||
export * from "./login/login-component.service";
|
export * from "./login/login-component.service";
|
||||||
export * from "./login/default-login-component.service";
|
export * from "./login/default-login-component.service";
|
||||||
|
|
||||||
|
// login decryption options
|
||||||
|
export * from "./login-decryption-options/login-decryption-options.component";
|
||||||
|
export * from "./login-decryption-options/login-decryption-options.service";
|
||||||
|
export * from "./login-decryption-options/default-login-decryption-options.service";
|
||||||
|
|
||||||
// login via auth request
|
// login via auth request
|
||||||
export * from "./login-via-auth-request/login-via-auth-request.component";
|
export * from "./login-via-auth-request/login-via-auth-request.component";
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
import { MockProxy, mock } from "jest-mock-extended";
|
||||||
|
|
||||||
|
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||||
|
|
||||||
|
import { DefaultLoginDecryptionOptionsService } from "./default-login-decryption-options.service";
|
||||||
|
|
||||||
|
describe("DefaultLoginDecryptionOptionsService", () => {
|
||||||
|
let service: DefaultLoginDecryptionOptionsService;
|
||||||
|
|
||||||
|
let messagingService: MockProxy<MessagingService>;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
messagingService = mock<MessagingService>();
|
||||||
|
|
||||||
|
service = new DefaultLoginDecryptionOptionsService(messagingService);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should instantiate the service", () => {
|
||||||
|
expect(service).not.toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("handleCreateUserSuccess()", () => {
|
||||||
|
it("should return null", async () => {
|
||||||
|
const result = await service.handleCreateUserSuccess();
|
||||||
|
|
||||||
|
expect(result).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("logOut()", () => {
|
||||||
|
it("should send a logout message", async () => {
|
||||||
|
await service.logOut();
|
||||||
|
|
||||||
|
expect(messagingService.send).toHaveBeenCalledWith("logout");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||||
|
|
||||||
|
import { LoginDecryptionOptionsService } from "./login-decryption-options.service";
|
||||||
|
|
||||||
|
export class DefaultLoginDecryptionOptionsService implements LoginDecryptionOptionsService {
|
||||||
|
constructor(protected messagingService: MessagingService) {}
|
||||||
|
|
||||||
|
handleCreateUserSuccess(): Promise<void | null> {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async logOut(): Promise<void> {
|
||||||
|
this.messagingService.send("logout");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
<ng-container *ngIf="loading">
|
||||||
|
<div class="text-center">
|
||||||
|
<i
|
||||||
|
class="bwi bwi-spinner bwi-spin bwi-2x tw-text-muted"
|
||||||
|
title="{{ 'loading' | i18n }}"
|
||||||
|
aria-hidden="true"
|
||||||
|
></i>
|
||||||
|
<span class="tw-sr-only">{{ "loading" | i18n }}</span>
|
||||||
|
</div>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<form *ngIf="!loading" [formGroup]="formGroup">
|
||||||
|
<bit-form-control>
|
||||||
|
<input type="checkbox" bitCheckbox formControlName="rememberDevice" />
|
||||||
|
<bit-label>{{ "rememberThisDevice" | i18n }}</bit-label>
|
||||||
|
<bit-hint bitTypography="body2">{{ "uncheckIfPublicDevice" | i18n }}</bit-hint>
|
||||||
|
</bit-form-control>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<ng-container *ngIf="state === State.NewUser">
|
||||||
|
<button type="button" bitButton block buttonType="primary" [bitAction]="createUser">
|
||||||
|
{{ "continue" | i18n }}
|
||||||
|
</button>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<ng-container *ngIf="state === State.ExistingUserUntrustedDevice">
|
||||||
|
<div class="tw-grid tw-gap-3">
|
||||||
|
<ng-container *ngIf="canApproveFromOtherDevice">
|
||||||
|
<button type="button" bitButton block buttonType="primary" (click)="approveFromOtherDevice()">
|
||||||
|
{{ "approveFromYourOtherDevice" | i18n }}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div *ngIf="canApproveWithMasterPassword || canRequestAdminApproval" class="tw-text-center">
|
||||||
|
{{ "or" | i18n }}
|
||||||
|
</div>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<button
|
||||||
|
*ngIf="canApproveWithMasterPassword"
|
||||||
|
type="button"
|
||||||
|
bitButton
|
||||||
|
block
|
||||||
|
buttonType="secondary"
|
||||||
|
(click)="approveWithMasterPassword()"
|
||||||
|
>
|
||||||
|
{{ "useMasterPassword" | i18n }}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
*ngIf="canRequestAdminApproval"
|
||||||
|
type="button"
|
||||||
|
bitButton
|
||||||
|
block
|
||||||
|
buttonType="secondary"
|
||||||
|
(click)="requestAdminApproval()"
|
||||||
|
>
|
||||||
|
{{ "requestAdminApproval" | i18n }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</ng-container>
|
||||||
@@ -0,0 +1,299 @@
|
|||||||
|
import { CommonModule } from "@angular/common";
|
||||||
|
import { Component, DestroyRef, OnInit } from "@angular/core";
|
||||||
|
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
|
||||||
|
import { FormBuilder, FormControl, ReactiveFormsModule } from "@angular/forms";
|
||||||
|
import { Router } from "@angular/router";
|
||||||
|
import { catchError, defer, firstValueFrom, from, map, of, switchMap, throwError } from "rxjs";
|
||||||
|
|
||||||
|
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||||
|
import {
|
||||||
|
LoginEmailServiceAbstraction,
|
||||||
|
UserDecryptionOptions,
|
||||||
|
UserDecryptionOptionsServiceAbstraction,
|
||||||
|
} from "@bitwarden/auth/common";
|
||||||
|
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||||
|
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction";
|
||||||
|
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||||
|
import { DeviceTrustServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust.service.abstraction";
|
||||||
|
import { PasswordResetEnrollmentServiceAbstraction } from "@bitwarden/common/auth/abstractions/password-reset-enrollment.service.abstraction";
|
||||||
|
import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction";
|
||||||
|
import { ClientType } from "@bitwarden/common/enums";
|
||||||
|
import { KeysRequest } from "@bitwarden/common/models/request/keys.request";
|
||||||
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
|
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||||
|
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||||
|
import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service";
|
||||||
|
import { UserId } from "@bitwarden/common/types/guid";
|
||||||
|
import {
|
||||||
|
AsyncActionsModule,
|
||||||
|
ButtonModule,
|
||||||
|
CheckboxModule,
|
||||||
|
FormFieldModule,
|
||||||
|
ToastService,
|
||||||
|
TypographyModule,
|
||||||
|
} from "@bitwarden/components";
|
||||||
|
import { KeyService } from "@bitwarden/key-management";
|
||||||
|
|
||||||
|
import { AnonLayoutWrapperDataService } from "../anon-layout/anon-layout-wrapper-data.service";
|
||||||
|
|
||||||
|
import { LoginDecryptionOptionsService } from "./login-decryption-options.service";
|
||||||
|
|
||||||
|
enum State {
|
||||||
|
NewUser,
|
||||||
|
ExistingUserUntrustedDevice,
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
standalone: true,
|
||||||
|
templateUrl: "./login-decryption-options.component.html",
|
||||||
|
imports: [
|
||||||
|
AsyncActionsModule,
|
||||||
|
ButtonModule,
|
||||||
|
CheckboxModule,
|
||||||
|
CommonModule,
|
||||||
|
FormFieldModule,
|
||||||
|
JslibModule,
|
||||||
|
ReactiveFormsModule,
|
||||||
|
TypographyModule,
|
||||||
|
],
|
||||||
|
})
|
||||||
|
export class LoginDecryptionOptionsComponent implements OnInit {
|
||||||
|
private activeAccountId: UserId;
|
||||||
|
private clientType: ClientType;
|
||||||
|
private email: string;
|
||||||
|
|
||||||
|
protected loading = false;
|
||||||
|
protected state: State;
|
||||||
|
protected State = State;
|
||||||
|
|
||||||
|
protected formGroup = this.formBuilder.group({
|
||||||
|
rememberDevice: [true], // Remember device means for the user to trust the device
|
||||||
|
});
|
||||||
|
|
||||||
|
private get rememberDeviceControl(): FormControl<boolean> {
|
||||||
|
return this.formGroup.controls.rememberDevice;
|
||||||
|
}
|
||||||
|
|
||||||
|
// New User Properties
|
||||||
|
private newUserOrgId: string;
|
||||||
|
|
||||||
|
// Existing User Untrusted Device Properties
|
||||||
|
protected canApproveFromOtherDevice = false;
|
||||||
|
protected canRequestAdminApproval = false;
|
||||||
|
protected canApproveWithMasterPassword = false;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private accountService: AccountService,
|
||||||
|
private anonLayoutWrapperDataService: AnonLayoutWrapperDataService,
|
||||||
|
private apiService: ApiService,
|
||||||
|
private destroyRef: DestroyRef,
|
||||||
|
private deviceTrustService: DeviceTrustServiceAbstraction,
|
||||||
|
private formBuilder: FormBuilder,
|
||||||
|
private i18nService: I18nService,
|
||||||
|
private keyService: KeyService,
|
||||||
|
private loginDecryptionOptionsService: LoginDecryptionOptionsService,
|
||||||
|
private loginEmailService: LoginEmailServiceAbstraction,
|
||||||
|
private messagingService: MessagingService,
|
||||||
|
private organizationApiService: OrganizationApiServiceAbstraction,
|
||||||
|
private passwordResetEnrollmentService: PasswordResetEnrollmentServiceAbstraction,
|
||||||
|
private platformUtilsService: PlatformUtilsService,
|
||||||
|
private router: Router,
|
||||||
|
private ssoLoginService: SsoLoginServiceAbstraction,
|
||||||
|
private toastService: ToastService,
|
||||||
|
private userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction,
|
||||||
|
private validationService: ValidationService,
|
||||||
|
) {
|
||||||
|
this.clientType === this.platformUtilsService.getClientType();
|
||||||
|
}
|
||||||
|
|
||||||
|
async ngOnInit() {
|
||||||
|
this.loading = true;
|
||||||
|
|
||||||
|
this.activeAccountId = (await firstValueFrom(this.accountService.activeAccount$))?.id;
|
||||||
|
|
||||||
|
this.email = await firstValueFrom(
|
||||||
|
this.accountService.activeAccount$.pipe(map((a) => a?.email)),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!this.email) {
|
||||||
|
await this.handleMissingEmail();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.observeAndPersistRememberDeviceValueChanges();
|
||||||
|
await this.setRememberDeviceDefaultValueFromState();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const userDecryptionOptions = await firstValueFrom(
|
||||||
|
this.userDecryptionOptionsService.userDecryptionOptions$,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (
|
||||||
|
!userDecryptionOptions?.trustedDeviceOption?.hasAdminApproval &&
|
||||||
|
!userDecryptionOptions?.hasMasterPassword
|
||||||
|
) {
|
||||||
|
/**
|
||||||
|
* We are dealing with a new account if both are true:
|
||||||
|
* - User does NOT have admin approval (i.e. has not enrolled in admin reset)
|
||||||
|
* - User does NOT have a master password
|
||||||
|
*/
|
||||||
|
await this.loadNewUserData();
|
||||||
|
} else {
|
||||||
|
this.loadExistingUserUntrustedDeviceData(userDecryptionOptions);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
this.validationService.showError(err);
|
||||||
|
} finally {
|
||||||
|
this.loading = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async handleMissingEmail() {
|
||||||
|
this.toastService.showToast({
|
||||||
|
variant: "error",
|
||||||
|
title: null,
|
||||||
|
message: this.i18nService.t("activeUserEmailNotFoundLoggingYouOut"),
|
||||||
|
});
|
||||||
|
|
||||||
|
setTimeout(async () => {
|
||||||
|
// We can't simply redirect to `/login` because the user is authed and the unauthGuard
|
||||||
|
// will prevent navigation. We must logout the user first via messagingService, which
|
||||||
|
// redirects to `/`, which will be handled by the redirectGuard to navigate the user to `/login`.
|
||||||
|
// The timeout just gives the user a chance to see the error toast before process reload runs on logout.
|
||||||
|
await this.loginDecryptionOptionsService.logOut();
|
||||||
|
}, 5000);
|
||||||
|
}
|
||||||
|
|
||||||
|
private observeAndPersistRememberDeviceValueChanges() {
|
||||||
|
this.rememberDeviceControl.valueChanges
|
||||||
|
.pipe(
|
||||||
|
takeUntilDestroyed(this.destroyRef),
|
||||||
|
switchMap((value) =>
|
||||||
|
defer(() => this.deviceTrustService.setShouldTrustDevice(this.activeAccountId, value)),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.subscribe();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async setRememberDeviceDefaultValueFromState() {
|
||||||
|
const rememberDeviceFromState = await this.deviceTrustService.getShouldTrustDevice(
|
||||||
|
this.activeAccountId,
|
||||||
|
);
|
||||||
|
|
||||||
|
const rememberDevice = rememberDeviceFromState ?? true;
|
||||||
|
|
||||||
|
this.rememberDeviceControl.setValue(rememberDevice);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async loadNewUserData() {
|
||||||
|
this.state = State.NewUser;
|
||||||
|
|
||||||
|
this.anonLayoutWrapperDataService.setAnonLayoutWrapperData({
|
||||||
|
pageTitle: {
|
||||||
|
key: "loggedInExclamation",
|
||||||
|
},
|
||||||
|
pageSubtitle: {
|
||||||
|
key: "rememberThisDeviceToMakeFutureLoginsSeamless",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const autoEnrollStatus$ = defer(() =>
|
||||||
|
this.ssoLoginService.getActiveUserOrganizationSsoIdentifier(),
|
||||||
|
).pipe(
|
||||||
|
switchMap((organizationIdentifier) => {
|
||||||
|
if (organizationIdentifier == undefined) {
|
||||||
|
return throwError(() => new Error(this.i18nService.t("ssoIdentifierRequired")));
|
||||||
|
}
|
||||||
|
|
||||||
|
return from(this.organizationApiService.getAutoEnrollStatus(organizationIdentifier));
|
||||||
|
}),
|
||||||
|
catchError((err: unknown) => {
|
||||||
|
this.validationService.showError(err);
|
||||||
|
return of(undefined);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
const autoEnrollStatus = await firstValueFrom(autoEnrollStatus$);
|
||||||
|
|
||||||
|
this.newUserOrgId = autoEnrollStatus.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
private loadExistingUserUntrustedDeviceData(userDecryptionOptions: UserDecryptionOptions) {
|
||||||
|
this.state = State.ExistingUserUntrustedDevice;
|
||||||
|
|
||||||
|
this.anonLayoutWrapperDataService.setAnonLayoutWrapperData({
|
||||||
|
pageTitle: {
|
||||||
|
key: "deviceApprovalRequiredV2",
|
||||||
|
},
|
||||||
|
pageSubtitle: {
|
||||||
|
key: "selectAnApprovalOptionBelow",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
this.canApproveFromOtherDevice =
|
||||||
|
userDecryptionOptions?.trustedDeviceOption?.hasLoginApprovingDevice || false;
|
||||||
|
this.canRequestAdminApproval =
|
||||||
|
userDecryptionOptions?.trustedDeviceOption?.hasAdminApproval || false;
|
||||||
|
this.canApproveWithMasterPassword = userDecryptionOptions?.hasMasterPassword || false;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected createUser = async () => {
|
||||||
|
if (this.state !== State.NewUser) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { publicKey, privateKey } = await this.keyService.initAccount();
|
||||||
|
const keysRequest = new KeysRequest(publicKey, privateKey.encryptedString);
|
||||||
|
await this.apiService.postAccountKeys(keysRequest);
|
||||||
|
|
||||||
|
this.toastService.showToast({
|
||||||
|
variant: "success",
|
||||||
|
title: null,
|
||||||
|
message: this.i18nService.t("accountSuccessfullyCreated"),
|
||||||
|
});
|
||||||
|
|
||||||
|
await this.passwordResetEnrollmentService.enroll(this.newUserOrgId);
|
||||||
|
|
||||||
|
if (this.formGroup.value.rememberDevice) {
|
||||||
|
await this.deviceTrustService.trustDevice(this.activeAccountId);
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.loginDecryptionOptionsService.handleCreateUserSuccess();
|
||||||
|
|
||||||
|
if (this.clientType === ClientType.Desktop) {
|
||||||
|
this.messagingService.send("redrawMenu");
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.handleCreateUserSuccessNavigation();
|
||||||
|
} catch (err) {
|
||||||
|
this.validationService.showError(err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private async handleCreateUserSuccessNavigation() {
|
||||||
|
if (this.clientType === ClientType.Browser) {
|
||||||
|
await this.router.navigate(["/tabs/vault"]);
|
||||||
|
} else {
|
||||||
|
await this.router.navigate(["/vault"]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async approveFromOtherDevice() {
|
||||||
|
this.loginEmailService.setLoginEmail(this.email);
|
||||||
|
await this.router.navigate(["/login-with-device"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async approveWithMasterPassword() {
|
||||||
|
await this.router.navigate(["/lock"], {
|
||||||
|
queryParams: {
|
||||||
|
from: "login-initiated",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async requestAdminApproval() {
|
||||||
|
this.loginEmailService.setLoginEmail(this.email);
|
||||||
|
await this.router.navigate(["/admin-approval-requested"]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
export abstract class LoginDecryptionOptionsService {
|
||||||
|
/**
|
||||||
|
* Handles client-specific logic that runs after a user was successfully created
|
||||||
|
*/
|
||||||
|
abstract handleCreateUserSuccess(): Promise<void | null>;
|
||||||
|
/**
|
||||||
|
* Logs the user out
|
||||||
|
*/
|
||||||
|
abstract logOut(): Promise<void>;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user