mirror of
https://github.com/bitwarden/browser
synced 2025-12-19 17:53:39 +00:00
[PM-28264] Consolidate and update the UI for key connector migration/confirmation (#17642)
* Consolidate the RemovePasswordComponent * Add getting confirmation details for confirm key connector * Add missing message
This commit is contained in:
@@ -184,7 +184,9 @@ import { DefaultChangeKdfApiService } from "@bitwarden/common/key-management/kdf
|
||||
import { ChangeKdfApiService } from "@bitwarden/common/key-management/kdf/change-kdf-api.service.abstraction";
|
||||
import { DefaultChangeKdfService } from "@bitwarden/common/key-management/kdf/change-kdf.service";
|
||||
import { ChangeKdfService } from "@bitwarden/common/key-management/kdf/change-kdf.service.abstraction";
|
||||
import { KeyConnectorApiService } from "@bitwarden/common/key-management/key-connector/abstractions/key-connector-api.service";
|
||||
import { KeyConnectorService as KeyConnectorServiceAbstraction } from "@bitwarden/common/key-management/key-connector/abstractions/key-connector.service";
|
||||
import { DefaultKeyConnectorApiService } from "@bitwarden/common/key-management/key-connector/services/default-key-connector-api.service";
|
||||
import { KeyConnectorService } from "@bitwarden/common/key-management/key-connector/services/key-connector.service";
|
||||
import { KeyApiService } from "@bitwarden/common/key-management/keys/services/abstractions/key-api-service.abstraction";
|
||||
import { RotateableKeySetService } from "@bitwarden/common/key-management/keys/services/abstractions/rotateable-key-set.service";
|
||||
@@ -1835,6 +1837,11 @@ const safeProviders: SafeProvider[] = [
|
||||
useClass: IpcSessionRepository,
|
||||
deps: [StateProvider],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: KeyConnectorApiService,
|
||||
useClass: DefaultKeyConnectorApiService,
|
||||
deps: [ApiServiceAbstraction],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: PremiumInterestStateService,
|
||||
useClass: NoopPremiumInterestStateService,
|
||||
|
||||
@@ -421,6 +421,7 @@ describe("TwoFactorAuthComponent", () => {
|
||||
keyConnectorUrl:
|
||||
mockUserDecryptionOpts.noMasterPasswordWithKeyConnector.keyConnectorOption!
|
||||
.keyConnectorUrl,
|
||||
organizationSsoIdentifier: "test-sso-id",
|
||||
}),
|
||||
);
|
||||
const authResult = new AuthResult();
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
import { KeyConnectorConfirmationDetailsResponse } from "../models/response/key-connector-confirmation-details.response";
|
||||
|
||||
export abstract class KeyConnectorApiService {
|
||||
abstract getConfirmationDetails(
|
||||
orgSsoIdentifier: string,
|
||||
): Promise<KeyConnectorConfirmationDetailsResponse>;
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
export interface KeyConnectorDomainConfirmation {
|
||||
keyConnectorUrl: string;
|
||||
organizationSsoIdentifier: string;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
import { BaseResponse } from "../../../../models/response/base.response";
|
||||
|
||||
export class KeyConnectorConfirmationDetailsResponse extends BaseResponse {
|
||||
organizationName: string;
|
||||
|
||||
constructor(response: any) {
|
||||
super(response);
|
||||
this.organizationName = this.getResponseProperty("OrganizationName");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
import { mock, MockProxy } from "jest-mock-extended";
|
||||
|
||||
import { ApiService } from "../../../abstractions/api.service";
|
||||
import { KeyConnectorConfirmationDetailsResponse } from "../models/response/key-connector-confirmation-details.response";
|
||||
|
||||
import { DefaultKeyConnectorApiService } from "./default-key-connector-api.service";
|
||||
|
||||
describe("DefaultKeyConnectorApiService", () => {
|
||||
let apiService: MockProxy<ApiService>;
|
||||
let sut: DefaultKeyConnectorApiService;
|
||||
|
||||
beforeEach(() => {
|
||||
apiService = mock<ApiService>();
|
||||
sut = new DefaultKeyConnectorApiService(apiService);
|
||||
});
|
||||
|
||||
describe("getConfirmationDetails", () => {
|
||||
it("encodes orgSsoIdentifier in URL", async () => {
|
||||
const orgSsoIdentifier = "test org/with special@chars";
|
||||
const expectedEncodedIdentifier = encodeURIComponent(orgSsoIdentifier);
|
||||
const mockResponse = {};
|
||||
apiService.send.mockResolvedValue(mockResponse);
|
||||
|
||||
await sut.getConfirmationDetails(orgSsoIdentifier);
|
||||
|
||||
expect(apiService.send).toHaveBeenCalledWith(
|
||||
"GET",
|
||||
`/accounts/key-connector/confirmation-details/${expectedEncodedIdentifier}`,
|
||||
null,
|
||||
true,
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
it("returns expected response", async () => {
|
||||
const orgSsoIdentifier = "test-org";
|
||||
const expectedOrgName = "example";
|
||||
const mockResponse = { OrganizationName: expectedOrgName };
|
||||
apiService.send.mockResolvedValue(mockResponse);
|
||||
|
||||
const result = await sut.getConfirmationDetails(orgSsoIdentifier);
|
||||
|
||||
expect(result).toBeInstanceOf(KeyConnectorConfirmationDetailsResponse);
|
||||
expect(result.organizationName).toBe(expectedOrgName);
|
||||
expect(apiService.send).toHaveBeenCalledWith(
|
||||
"GET",
|
||||
"/accounts/key-connector/confirmation-details/test-org",
|
||||
null,
|
||||
true,
|
||||
true,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,20 @@
|
||||
import { ApiService } from "../../../abstractions/api.service";
|
||||
import { KeyConnectorApiService } from "../abstractions/key-connector-api.service";
|
||||
import { KeyConnectorConfirmationDetailsResponse } from "../models/response/key-connector-confirmation-details.response";
|
||||
|
||||
export class DefaultKeyConnectorApiService implements KeyConnectorApiService {
|
||||
constructor(private apiService: ApiService) {}
|
||||
|
||||
async getConfirmationDetails(
|
||||
orgSsoIdentifier: string,
|
||||
): Promise<KeyConnectorConfirmationDetailsResponse> {
|
||||
const r = await this.apiService.send(
|
||||
"GET",
|
||||
"/accounts/key-connector/confirmation-details/" + encodeURIComponent(orgSsoIdentifier),
|
||||
null,
|
||||
true,
|
||||
true,
|
||||
);
|
||||
return new KeyConnectorConfirmationDetailsResponse(r);
|
||||
}
|
||||
}
|
||||
@@ -603,7 +603,10 @@ describe("KeyConnectorService", () => {
|
||||
const data$ = keyConnectorService.requiresDomainConfirmation$(mockUserId);
|
||||
const data = await firstValueFrom(data$);
|
||||
|
||||
expect(data).toEqual({ keyConnectorUrl: conversion.keyConnectorUrl });
|
||||
expect(data).toEqual({
|
||||
keyConnectorUrl: conversion.keyConnectorUrl,
|
||||
organizationSsoIdentifier: conversion.organizationId,
|
||||
});
|
||||
});
|
||||
|
||||
it("should return observable of null value when no data is set", async () => {
|
||||
|
||||
@@ -202,9 +202,16 @@ export class KeyConnectorService implements KeyConnectorServiceAbstraction {
|
||||
}
|
||||
|
||||
requiresDomainConfirmation$(userId: UserId): Observable<KeyConnectorDomainConfirmation | null> {
|
||||
return this.stateProvider
|
||||
.getUserState$(NEW_SSO_USER_KEY_CONNECTOR_CONVERSION, userId)
|
||||
.pipe(map((data) => (data != null ? { keyConnectorUrl: data.keyConnectorUrl } : null)));
|
||||
return this.stateProvider.getUserState$(NEW_SSO_USER_KEY_CONNECTOR_CONVERSION, userId).pipe(
|
||||
map((data) =>
|
||||
data != null
|
||||
? {
|
||||
keyConnectorUrl: data.keyConnectorUrl,
|
||||
organizationSsoIdentifier: data.organizationId,
|
||||
}
|
||||
: null,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
private handleKeyConnectorError(e: any) {
|
||||
|
||||
@@ -8,17 +8,34 @@
|
||||
<span class="tw-sr-only">{{ "loading" | i18n }}</span>
|
||||
</div>
|
||||
} @else {
|
||||
<div class="tw-mb-4">
|
||||
<p class="tw-mb-1 tw-text-sm tw-font-medium">{{ "keyConnectorDomain" | i18n }}:</p>
|
||||
<p class="tw-text-muted tw-break-all">{{ keyConnectorUrl }}</p>
|
||||
</div>
|
||||
@if (organizationName) {
|
||||
<p>{{ "confirmKeyConnectorOrganizationUserDescription" | i18n }}</p>
|
||||
|
||||
<p class="tw-mb-0 tw-font-bold">{{ "organization" | i18n }}</p>
|
||||
<p class="tw-mb-6">{{ organizationName }}</p>
|
||||
} @else {
|
||||
<p>{{ "verifyYourDomainDescription" | i18n }}</p>
|
||||
}
|
||||
|
||||
<p class="tw-mb-0 tw-font-bold tw-inline-flex tw-items-center">
|
||||
{{ "domain" | i18n }}
|
||||
<button
|
||||
type="button"
|
||||
[label]="'keyConnectorDomainTooltip' | i18n"
|
||||
tooltipPosition="above-center"
|
||||
bitIconButton="bwi-info-circle"
|
||||
size="small"
|
||||
></button>
|
||||
</p>
|
||||
<p class="tw-mb-6 tw-font-mono">{{ keyConnectorHostName }}</p>
|
||||
|
||||
<div class="tw-flex tw-flex-col tw-gap-2">
|
||||
<button bitButton type="button" buttonType="primary" [bitAction]="confirm" [block]="true">
|
||||
{{ "confirm" | i18n }}
|
||||
{{ "continueWithLogIn" | i18n }}
|
||||
</button>
|
||||
|
||||
<button bitButton type="button" buttonType="secondary" [bitAction]="cancel" [block]="true">
|
||||
{{ "cancel" | i18n }}
|
||||
{{ "doNotContinue" | i18n }}
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -2,13 +2,17 @@ import { Router } from "@angular/router";
|
||||
import { mock } from "jest-mock-extended";
|
||||
import { of } from "rxjs";
|
||||
|
||||
import { KeyConnectorApiService } from "@bitwarden/common/key-management/key-connector/abstractions/key-connector-api.service";
|
||||
import { KeyConnectorService } from "@bitwarden/common/key-management/key-connector/abstractions/key-connector.service";
|
||||
import { KeyConnectorDomainConfirmation } from "@bitwarden/common/key-management/key-connector/models/key-connector-domain-confirmation";
|
||||
import { KeyConnectorConfirmationDetailsResponse } from "@bitwarden/common/key-management/key-connector/models/response/key-connector-confirmation-details.response";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||
import { SyncService } from "@bitwarden/common/platform/sync";
|
||||
import { mockAccountServiceWith } from "@bitwarden/common/spec";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { AnonLayoutWrapperDataService, ToastService } from "@bitwarden/components";
|
||||
|
||||
import { ConfirmKeyConnectorDomainComponent } from "./confirm-key-connector-domain.component";
|
||||
|
||||
@@ -16,8 +20,10 @@ describe("ConfirmKeyConnectorDomainComponent", () => {
|
||||
let component: ConfirmKeyConnectorDomainComponent;
|
||||
|
||||
const userId = "test-user-id" as UserId;
|
||||
const expectedHostName = "key-connector-url.com";
|
||||
const confirmation: KeyConnectorDomainConfirmation = {
|
||||
keyConnectorUrl: "https://key-connector-url.com",
|
||||
organizationSsoIdentifier: "org-sso-identifier",
|
||||
};
|
||||
|
||||
const mockRouter = mock<Router>();
|
||||
@@ -25,6 +31,10 @@ describe("ConfirmKeyConnectorDomainComponent", () => {
|
||||
const mockKeyConnectorService = mock<KeyConnectorService>();
|
||||
const mockLogService = mock<LogService>();
|
||||
const mockMessagingService = mock<MessagingService>();
|
||||
const mockKeyConnectorApiService = mock<KeyConnectorApiService>();
|
||||
const mockToastService = mock<ToastService>();
|
||||
const mockI18nService = mock<I18nService>();
|
||||
const mockAnonLayoutWrapperDataService = mock<AnonLayoutWrapperDataService>();
|
||||
let mockAccountService = mockAccountServiceWith(userId);
|
||||
const onBeforeNavigation = jest.fn();
|
||||
|
||||
@@ -33,6 +43,8 @@ describe("ConfirmKeyConnectorDomainComponent", () => {
|
||||
|
||||
mockAccountService = mockAccountServiceWith(userId);
|
||||
|
||||
mockI18nService.t.mockImplementation((key) => `${key}-used-i18n`);
|
||||
|
||||
component = new ConfirmKeyConnectorDomainComponent(
|
||||
mockRouter,
|
||||
mockLogService,
|
||||
@@ -40,6 +52,10 @@ describe("ConfirmKeyConnectorDomainComponent", () => {
|
||||
mockMessagingService,
|
||||
mockSyncService,
|
||||
mockAccountService,
|
||||
mockKeyConnectorApiService,
|
||||
mockToastService,
|
||||
mockI18nService,
|
||||
mockAnonLayoutWrapperDataService,
|
||||
);
|
||||
|
||||
jest.spyOn(component, "onBeforeNavigation").mockImplementation(onBeforeNavigation);
|
||||
@@ -67,17 +83,41 @@ describe("ConfirmKeyConnectorDomainComponent", () => {
|
||||
expect(component.loading).toEqual(true);
|
||||
});
|
||||
|
||||
it("sets organization name to undefined when getOrganizationName throws error", async () => {
|
||||
mockKeyConnectorApiService.getConfirmationDetails.mockRejectedValue(new Error("API error"));
|
||||
|
||||
await component.ngOnInit();
|
||||
|
||||
expect(component.organizationName).toBeUndefined();
|
||||
expect(component.userId).toEqual(userId);
|
||||
expect(component.keyConnectorUrl).toEqual(confirmation.keyConnectorUrl);
|
||||
expect(component.keyConnectorHostName).toEqual(expectedHostName);
|
||||
expect(component.loading).toEqual(false);
|
||||
expect(mockAnonLayoutWrapperDataService.setAnonLayoutWrapperData).toHaveBeenCalledWith({
|
||||
pageTitle: { key: "verifyYourDomainToLogin" },
|
||||
});
|
||||
});
|
||||
|
||||
it("should set component properties correctly", async () => {
|
||||
const expectedOrgName = "Test Organization";
|
||||
mockKeyConnectorApiService.getConfirmationDetails.mockResolvedValue({
|
||||
organizationName: expectedOrgName,
|
||||
} as KeyConnectorConfirmationDetailsResponse);
|
||||
|
||||
await component.ngOnInit();
|
||||
|
||||
expect(component.userId).toEqual(userId);
|
||||
expect(component.organizationName).toEqual(expectedOrgName);
|
||||
expect(component.keyConnectorUrl).toEqual(confirmation.keyConnectorUrl);
|
||||
expect(component.keyConnectorHostName).toEqual(expectedHostName);
|
||||
expect(component.loading).toEqual(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("confirm", () => {
|
||||
it("should call keyConnectorService.convertNewSsoUserToKeyConnector with full sync and navigation to home page", async () => {
|
||||
it("calls domain verified toast when organization name is not set", async () => {
|
||||
mockKeyConnectorApiService.getConfirmationDetails.mockRejectedValue(new Error("API error"));
|
||||
|
||||
await component.ngOnInit();
|
||||
|
||||
await component.confirm();
|
||||
@@ -94,6 +134,43 @@ describe("ConfirmKeyConnectorDomainComponent", () => {
|
||||
expect(mockSyncService.fullSync.mock.invocationCallOrder[0]).toBeLessThan(
|
||||
mockMessagingService.send.mock.invocationCallOrder[0],
|
||||
);
|
||||
expect(mockToastService.showToast).toHaveBeenCalledWith({
|
||||
variant: "success",
|
||||
message: "domainVerified-used-i18n",
|
||||
});
|
||||
expect(mockMessagingService.send.mock.invocationCallOrder[0]).toBeLessThan(
|
||||
onBeforeNavigation.mock.invocationCallOrder[0],
|
||||
);
|
||||
expect(onBeforeNavigation.mock.invocationCallOrder[0]).toBeLessThan(
|
||||
mockRouter.navigate.mock.invocationCallOrder[0],
|
||||
);
|
||||
});
|
||||
|
||||
it("should call keyConnectorService.convertNewSsoUserToKeyConnector with full sync and navigation to home page", async () => {
|
||||
mockKeyConnectorApiService.getConfirmationDetails.mockResolvedValue({
|
||||
organizationName: "Test Org Name",
|
||||
} as KeyConnectorConfirmationDetailsResponse);
|
||||
|
||||
await component.ngOnInit();
|
||||
|
||||
await component.confirm();
|
||||
|
||||
expect(mockKeyConnectorService.convertNewSsoUserToKeyConnector).toHaveBeenCalledWith(userId);
|
||||
expect(mockSyncService.fullSync).toHaveBeenCalledWith(true);
|
||||
expect(mockRouter.navigate).toHaveBeenCalledWith(["/"]);
|
||||
expect(mockMessagingService.send).toHaveBeenCalledWith("loggedIn");
|
||||
expect(onBeforeNavigation).toHaveBeenCalled();
|
||||
|
||||
expect(
|
||||
mockKeyConnectorService.convertNewSsoUserToKeyConnector.mock.invocationCallOrder[0],
|
||||
).toBeLessThan(mockSyncService.fullSync.mock.invocationCallOrder[0]);
|
||||
expect(mockSyncService.fullSync.mock.invocationCallOrder[0]).toBeLessThan(
|
||||
mockMessagingService.send.mock.invocationCallOrder[0],
|
||||
);
|
||||
expect(mockToastService.showToast).toHaveBeenCalledWith({
|
||||
variant: "success",
|
||||
message: "organizationVerified-used-i18n",
|
||||
});
|
||||
expect(mockMessagingService.send.mock.invocationCallOrder[0]).toBeLessThan(
|
||||
onBeforeNavigation.mock.invocationCallOrder[0],
|
||||
);
|
||||
|
||||
@@ -5,12 +5,21 @@ import { firstValueFrom } from "rxjs";
|
||||
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import { KeyConnectorApiService } from "@bitwarden/common/key-management/key-connector/abstractions/key-connector-api.service";
|
||||
import { KeyConnectorService } from "@bitwarden/common/key-management/key-connector/abstractions/key-connector.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { SyncService } from "@bitwarden/common/platform/sync";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { BitActionDirective, ButtonModule } from "@bitwarden/components";
|
||||
import {
|
||||
AnonLayoutWrapperDataService,
|
||||
BitActionDirective,
|
||||
ButtonModule,
|
||||
IconButtonModule,
|
||||
ToastService,
|
||||
} from "@bitwarden/components";
|
||||
import { I18nPipe } from "@bitwarden/ui-common";
|
||||
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
|
||||
@@ -19,11 +28,13 @@ import { I18nPipe } from "@bitwarden/ui-common";
|
||||
selector: "confirm-key-connector-domain",
|
||||
templateUrl: "confirm-key-connector-domain.component.html",
|
||||
standalone: true,
|
||||
imports: [CommonModule, ButtonModule, I18nPipe, BitActionDirective],
|
||||
imports: [CommonModule, ButtonModule, I18nPipe, BitActionDirective, IconButtonModule],
|
||||
})
|
||||
export class ConfirmKeyConnectorDomainComponent implements OnInit {
|
||||
loading = true;
|
||||
keyConnectorUrl!: string;
|
||||
keyConnectorHostName!: string;
|
||||
organizationName: string | undefined;
|
||||
userId!: UserId;
|
||||
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
||||
@@ -37,6 +48,10 @@ export class ConfirmKeyConnectorDomainComponent implements OnInit {
|
||||
private messagingService: MessagingService,
|
||||
private syncService: SyncService,
|
||||
private accountService: AccountService,
|
||||
private keyConnectorApiService: KeyConnectorApiService,
|
||||
private toastService: ToastService,
|
||||
private i18nService: I18nService,
|
||||
private anonLayoutWrapperDataService: AnonLayoutWrapperDataService,
|
||||
) {}
|
||||
|
||||
async ngOnInit() {
|
||||
@@ -57,14 +72,36 @@ export class ConfirmKeyConnectorDomainComponent implements OnInit {
|
||||
return;
|
||||
}
|
||||
|
||||
this.keyConnectorUrl = confirmation.keyConnectorUrl;
|
||||
this.organizationName = await this.getOrganizationName(confirmation.organizationSsoIdentifier);
|
||||
|
||||
// PM-29133 Remove during cleanup.
|
||||
if (this.organizationName == undefined) {
|
||||
this.anonLayoutWrapperDataService.setAnonLayoutWrapperData({
|
||||
pageTitle: { key: "verifyYourDomainToLogin" },
|
||||
});
|
||||
}
|
||||
|
||||
this.keyConnectorUrl = confirmation.keyConnectorUrl;
|
||||
this.keyConnectorHostName = Utils.getHostname(confirmation.keyConnectorUrl);
|
||||
this.loading = false;
|
||||
}
|
||||
|
||||
confirm = async () => {
|
||||
await this.keyConnectorService.convertNewSsoUserToKeyConnector(this.userId);
|
||||
|
||||
if (this.organizationName) {
|
||||
this.toastService.showToast({
|
||||
variant: "success",
|
||||
message: this.i18nService.t("organizationVerified"),
|
||||
});
|
||||
} else {
|
||||
// PM-29133 Remove during cleanup.
|
||||
this.toastService.showToast({
|
||||
variant: "success",
|
||||
message: this.i18nService.t("domainVerified"),
|
||||
});
|
||||
}
|
||||
|
||||
await this.syncService.fullSync(true);
|
||||
|
||||
this.messagingService.send("loggedIn");
|
||||
@@ -77,4 +114,22 @@ export class ConfirmKeyConnectorDomainComponent implements OnInit {
|
||||
cancel = async () => {
|
||||
this.messagingService.send("logout");
|
||||
};
|
||||
|
||||
private async getOrganizationName(
|
||||
organizationSsoIdentifier: string,
|
||||
): Promise<string | undefined> {
|
||||
try {
|
||||
const details =
|
||||
await this.keyConnectorApiService.getConfirmationDetails(organizationSsoIdentifier);
|
||||
return details.organizationName;
|
||||
} catch (error) {
|
||||
// PM-29133 Remove during cleanup.
|
||||
// Old self hosted servers may not have this endpoint yet. On error log a warning and continue without organization name.
|
||||
this.logService.warning(
|
||||
`[ConfirmKeyConnectorDomainComponent] Unable to get key connector confirmation details for organizationSsoIdentifier ${organizationSsoIdentifier}:`,
|
||||
error,
|
||||
);
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
@if (loading) {
|
||||
<div class="tw-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>
|
||||
} @else {
|
||||
<p>{{ "removeMasterPasswordForOrgUserKeyConnector" | i18n }}</p>
|
||||
<p class="tw-mb-0 tw-font-bold">{{ "organization" | i18n }}</p>
|
||||
<p class="tw-mb-6">{{ organization.name }}</p>
|
||||
<p class="tw-mb-0 tw-font-bold tw-inline-flex tw-items-center">
|
||||
{{ "domain" | i18n }}
|
||||
<button
|
||||
type="button"
|
||||
[label]="'keyConnectorDomainTooltip' | i18n"
|
||||
tooltipPosition="above-center"
|
||||
bitIconButton="bwi-info-circle"
|
||||
size="small"
|
||||
></button>
|
||||
</p>
|
||||
<p class="tw-mb-6 tw-font-mono">{{ keyConnectorHostName }}</p>
|
||||
|
||||
<div class="tw-flex tw-flex-col tw-gap-2">
|
||||
<button bitButton type="button" buttonType="primary" [block]="true" [bitAction]="convert">
|
||||
{{ "continueWithLogIn" | i18n }}
|
||||
</button>
|
||||
|
||||
<button bitButton type="button" buttonType="secondary" [block]="true" [bitAction]="leave">
|
||||
{{ "doNotContinue" | i18n }}
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
@@ -19,6 +19,7 @@ describe("RemovePasswordComponent", () => {
|
||||
let component: RemovePasswordComponent;
|
||||
|
||||
const userId = "test-user-id" as UserId;
|
||||
const expectedHostName = "key-connector-url.com";
|
||||
const organization = {
|
||||
id: "test-organization-id",
|
||||
name: "test-organization-name",
|
||||
@@ -62,6 +63,7 @@ describe("RemovePasswordComponent", () => {
|
||||
expect(component["activeUserId"]).toBe("test-user-id");
|
||||
expect(component.organization).toEqual(organization);
|
||||
expect(component.loading).toEqual(false);
|
||||
expect(component.keyConnectorHostName).toBe(expectedHostName);
|
||||
|
||||
expect(mockKeyConnectorService.getManagingOrganization).toHaveBeenCalledWith(userId);
|
||||
expect(mockSyncService.fullSync).toHaveBeenCalledWith(false);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Directive, OnInit } from "@angular/core";
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { Component, OnInit } from "@angular/core";
|
||||
import { Router } from "@angular/router";
|
||||
import { firstValueFrom } from "rxjs";
|
||||
|
||||
@@ -9,17 +10,33 @@ import { KeyConnectorService } from "@bitwarden/common/key-management/key-connec
|
||||
import { ErrorResponse } from "@bitwarden/common/models/response/error.response";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
|
||||
import { DialogService, ToastService } from "@bitwarden/components";
|
||||
import {
|
||||
DialogService,
|
||||
ToastService,
|
||||
ButtonModule,
|
||||
BitActionDirective,
|
||||
IconButtonModule,
|
||||
} from "@bitwarden/components";
|
||||
import { I18nPipe } from "@bitwarden/ui-common";
|
||||
|
||||
@Directive()
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
|
||||
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection
|
||||
@Component({
|
||||
selector: "km-ui-remove-password",
|
||||
templateUrl: "remove-password.component.html",
|
||||
standalone: true,
|
||||
imports: [CommonModule, ButtonModule, I18nPipe, BitActionDirective, IconButtonModule],
|
||||
})
|
||||
export class RemovePasswordComponent implements OnInit {
|
||||
continuing = false;
|
||||
leaving = false;
|
||||
|
||||
loading = true;
|
||||
organization!: Organization;
|
||||
keyConnectorHostName!: string;
|
||||
private activeUserId!: UserId;
|
||||
|
||||
constructor(
|
||||
@@ -55,6 +72,7 @@ export class RemovePasswordComponent implements OnInit {
|
||||
await this.router.navigate(["/"]);
|
||||
return;
|
||||
}
|
||||
this.keyConnectorHostName = Utils.getHostname(this.organization.keyConnectorUrl);
|
||||
this.loading = false;
|
||||
}
|
||||
|
||||
@@ -73,7 +91,7 @@ export class RemovePasswordComponent implements OnInit {
|
||||
|
||||
this.toastService.showToast({
|
||||
variant: "success",
|
||||
message: this.i18nService.t("removedMasterPassword"),
|
||||
message: this.i18nService.t("organizationVerified"),
|
||||
});
|
||||
|
||||
await this.router.navigate(["/"]);
|
||||
@@ -86,9 +104,11 @@ export class RemovePasswordComponent implements OnInit {
|
||||
|
||||
leave = async () => {
|
||||
const confirmed = await this.dialogService.openSimpleDialog({
|
||||
title: this.organization.name,
|
||||
content: { key: "leaveOrganizationConfirmation" },
|
||||
title: { key: "leaveOrganization" },
|
||||
content: { key: "leaveOrganizationContent" },
|
||||
type: "warning",
|
||||
acceptButtonText: { key: "leaveNow" },
|
||||
cancelButtonText: { key: "cancel" },
|
||||
});
|
||||
|
||||
if (!confirmed) {
|
||||
|
||||
Reference in New Issue
Block a user