mirror of
https://github.com/bitwarden/browser
synced 2026-01-31 16:53:27 +00:00
Merge branch 'main' into PM-7853-Clients-Hide-Send-from-navigation-when-user-is-subject-to-the-disable-Send-policy
This commit is contained in:
2
.github/workflows/lint.yml
vendored
2
.github/workflows/lint.yml
vendored
@@ -144,7 +144,7 @@ jobs:
|
||||
- name: Install cargo-deny
|
||||
uses: taiki-e/install-action@073d46cba2cde38f6698c798566c1b3e24feeb44 # v2.62.67
|
||||
with:
|
||||
tool: cargo-deny@0.18.5
|
||||
tool: cargo-deny@0.18.6
|
||||
|
||||
- name: Run cargo deny
|
||||
working-directory: ./apps/desktop/desktop_native
|
||||
|
||||
@@ -1323,19 +1323,19 @@
|
||||
"message": "Buradan xaricə köçür"
|
||||
},
|
||||
"exportVerb": {
|
||||
"message": "Export",
|
||||
"message": "Xaricə köçür",
|
||||
"description": "The verb form of the word Export"
|
||||
},
|
||||
"exportNoun": {
|
||||
"message": "Export",
|
||||
"message": "Xaricə köçürmə",
|
||||
"description": "The noun form of the word Export"
|
||||
},
|
||||
"importNoun": {
|
||||
"message": "Import",
|
||||
"message": "Daxilə köçürmə",
|
||||
"description": "The noun form of the word Import"
|
||||
},
|
||||
"importVerb": {
|
||||
"message": "Import",
|
||||
"message": "Daxilə köçür",
|
||||
"description": "The verb form of the word Import"
|
||||
},
|
||||
"fileFormat": {
|
||||
|
||||
@@ -1486,7 +1486,7 @@
|
||||
"message": "Vyberte súbor"
|
||||
},
|
||||
"itemsTransferred": {
|
||||
"message": "Items transferred"
|
||||
"message": "Položky boli prenesené"
|
||||
},
|
||||
"maxFileSize": {
|
||||
"message": "Maximálna veľkosť súboru je 500 MB."
|
||||
@@ -3408,10 +3408,10 @@
|
||||
"message": "Použiť možnosti subadresovania svojho poskytovateľa e-mailu."
|
||||
},
|
||||
"catchallEmail": {
|
||||
"message": "Catch-all e-mail"
|
||||
"message": "Doménový kôš"
|
||||
},
|
||||
"catchallEmailDesc": {
|
||||
"message": "Použiť doručenú poštu typu catch-all nastavenú na doméne."
|
||||
"message": "Použiť nastavený doménový kôš."
|
||||
},
|
||||
"random": {
|
||||
"message": "Náhodné"
|
||||
|
||||
@@ -1323,19 +1323,19 @@
|
||||
"message": "ส่งออกจาก"
|
||||
},
|
||||
"exportVerb": {
|
||||
"message": "Export",
|
||||
"message": "ส่งออก",
|
||||
"description": "The verb form of the word Export"
|
||||
},
|
||||
"exportNoun": {
|
||||
"message": "Export",
|
||||
"message": "ส่งออก",
|
||||
"description": "The noun form of the word Export"
|
||||
},
|
||||
"importNoun": {
|
||||
"message": "Import",
|
||||
"message": "นำเข้า",
|
||||
"description": "The noun form of the word Import"
|
||||
},
|
||||
"importVerb": {
|
||||
"message": "Import",
|
||||
"message": "นำเข้า",
|
||||
"description": "The verb form of the word Import"
|
||||
},
|
||||
"fileFormat": {
|
||||
|
||||
@@ -1323,19 +1323,19 @@
|
||||
"message": "导出自"
|
||||
},
|
||||
"exportVerb": {
|
||||
"message": "Export",
|
||||
"message": "导出",
|
||||
"description": "The verb form of the word Export"
|
||||
},
|
||||
"exportNoun": {
|
||||
"message": "Export",
|
||||
"message": "导出",
|
||||
"description": "The noun form of the word Export"
|
||||
},
|
||||
"importNoun": {
|
||||
"message": "Import",
|
||||
"message": "导入",
|
||||
"description": "The noun form of the word Import"
|
||||
},
|
||||
"importVerb": {
|
||||
"message": "Import",
|
||||
"message": "导入",
|
||||
"description": "The verb form of the word Import"
|
||||
},
|
||||
"fileFormat": {
|
||||
|
||||
@@ -437,7 +437,7 @@
|
||||
"message": "同步"
|
||||
},
|
||||
"syncNow": {
|
||||
"message": "Sync now"
|
||||
"message": "立即同步"
|
||||
},
|
||||
"lastSync": {
|
||||
"message": "上次同步於:"
|
||||
@@ -1323,19 +1323,19 @@
|
||||
"message": "匯出自"
|
||||
},
|
||||
"exportVerb": {
|
||||
"message": "Export",
|
||||
"message": "匯出",
|
||||
"description": "The verb form of the word Export"
|
||||
},
|
||||
"exportNoun": {
|
||||
"message": "Export",
|
||||
"message": "匯出",
|
||||
"description": "The noun form of the word Export"
|
||||
},
|
||||
"importNoun": {
|
||||
"message": "Import",
|
||||
"message": "匯入",
|
||||
"description": "The noun form of the word Import"
|
||||
},
|
||||
"importVerb": {
|
||||
"message": "Import",
|
||||
"message": "匯入",
|
||||
"description": "The verb form of the word Import"
|
||||
},
|
||||
"fileFormat": {
|
||||
@@ -1486,7 +1486,7 @@
|
||||
"message": "選取檔案"
|
||||
},
|
||||
"itemsTransferred": {
|
||||
"message": "Items transferred"
|
||||
"message": "項目已轉移"
|
||||
},
|
||||
"maxFileSize": {
|
||||
"message": "檔案最大為 500MB。"
|
||||
@@ -4812,13 +4812,13 @@
|
||||
"message": "帳戶安全性"
|
||||
},
|
||||
"phishingBlocker": {
|
||||
"message": "Phishing Blocker"
|
||||
"message": "釣魚封鎖器"
|
||||
},
|
||||
"enablePhishingDetection": {
|
||||
"message": "Phishing detection"
|
||||
"message": "釣魚偵測"
|
||||
},
|
||||
"enablePhishingDetectionDesc": {
|
||||
"message": "Display warning before accessing suspected phishing sites"
|
||||
"message": "在存取疑似釣魚網站前顯示警告"
|
||||
},
|
||||
"notifications": {
|
||||
"message": "通知"
|
||||
@@ -5904,43 +5904,43 @@
|
||||
"message": "支付卡號碼"
|
||||
},
|
||||
"removeMasterPasswordForOrgUserKeyConnector": {
|
||||
"message": "Your organization is no longer using master passwords to log into Bitwarden. To continue, verify the organization and domain."
|
||||
"message": "您的組織已不再使用主密碼登入 Bitwarden。若要繼續,請驗證組織與網域。"
|
||||
},
|
||||
"continueWithLogIn": {
|
||||
"message": "Continue with log in"
|
||||
"message": "繼續登入"
|
||||
},
|
||||
"doNotContinue": {
|
||||
"message": "Do not continue"
|
||||
"message": "不要繼續"
|
||||
},
|
||||
"domain": {
|
||||
"message": "Domain"
|
||||
"message": "網域"
|
||||
},
|
||||
"keyConnectorDomainTooltip": {
|
||||
"message": "This domain will store your account encryption keys, so make sure you trust it. If you're not sure, check with your admin."
|
||||
"message": "此網域將儲存您帳號的加密金鑰,請確認您信任它。若不確定,請洽詢您的管理員。"
|
||||
},
|
||||
"verifyYourOrganization": {
|
||||
"message": "Verify your organization to log in"
|
||||
"message": "驗證您的組織以登入"
|
||||
},
|
||||
"organizationVerified": {
|
||||
"message": "Organization verified"
|
||||
"message": "組織已驗證"
|
||||
},
|
||||
"domainVerified": {
|
||||
"message": "Domain verified"
|
||||
"message": "已驗證網域"
|
||||
},
|
||||
"leaveOrganizationContent": {
|
||||
"message": "If you don't verify your organization, your access to the organization will be revoked."
|
||||
"message": "若您未驗證組織,將會被撤銷對該組織的存取權限。"
|
||||
},
|
||||
"leaveNow": {
|
||||
"message": "Leave now"
|
||||
"message": "立即離開"
|
||||
},
|
||||
"verifyYourDomainToLogin": {
|
||||
"message": "Verify your domain to log in"
|
||||
"message": "驗證您的網域以登入"
|
||||
},
|
||||
"verifyYourDomainDescription": {
|
||||
"message": "To continue with log in, verify this domain."
|
||||
"message": "若要繼續登入,請驗證此網域。"
|
||||
},
|
||||
"confirmKeyConnectorOrganizationUserDescription": {
|
||||
"message": "To continue with log in, verify the organization and domain."
|
||||
"message": "若要繼續登入,請驗證組織與網域。"
|
||||
},
|
||||
"sessionTimeoutSettingsAction": {
|
||||
"message": "逾時後動作"
|
||||
@@ -5990,19 +5990,19 @@
|
||||
"message": "設定一個解鎖方式來變更您的密碼庫逾時動作。"
|
||||
},
|
||||
"upgrade": {
|
||||
"message": "Upgrade"
|
||||
"message": "升級"
|
||||
},
|
||||
"leaveConfirmationDialogTitle": {
|
||||
"message": "Are you sure you want to leave?"
|
||||
"message": "確定要離開嗎?"
|
||||
},
|
||||
"leaveConfirmationDialogContentOne": {
|
||||
"message": "By declining, your personal items will stay in your account, but you'll lose access to shared items and organization features."
|
||||
"message": "若選擇拒絕,您的個人項目將保留在帳號中,但您將失去對共用項目與組織功能的存取權。"
|
||||
},
|
||||
"leaveConfirmationDialogContentTwo": {
|
||||
"message": "Contact your admin to regain access."
|
||||
"message": "請聯絡您的管理員以重新取得存取權限。"
|
||||
},
|
||||
"leaveConfirmationDialogConfirmButton": {
|
||||
"message": "Leave $ORGANIZATION$",
|
||||
"message": "離開 $ORGANIZATION$",
|
||||
"placeholders": {
|
||||
"organization": {
|
||||
"content": "$1",
|
||||
@@ -6011,10 +6011,10 @@
|
||||
}
|
||||
},
|
||||
"howToManageMyVault": {
|
||||
"message": "How do I manage my vault?"
|
||||
"message": "我要如何管理我的密碼庫?"
|
||||
},
|
||||
"transferItemsToOrganizationTitle": {
|
||||
"message": "Transfer items to $ORGANIZATION$",
|
||||
"message": "將項目轉移至 $ORGANIZATION$",
|
||||
"placeholders": {
|
||||
"organization": {
|
||||
"content": "$1",
|
||||
@@ -6023,7 +6023,7 @@
|
||||
}
|
||||
},
|
||||
"transferItemsToOrganizationContent": {
|
||||
"message": "$ORGANIZATION$ is requiring all items to be owned by the organization for security and compliance. Click accept to transfer ownership of your items.",
|
||||
"message": "$ORGANIZATION$ 為了安全性與合規性,要求所有項目皆由組織擁有。點擊接受即可轉移您項目的擁有權。",
|
||||
"placeholders": {
|
||||
"organization": {
|
||||
"content": "$1",
|
||||
@@ -6032,12 +6032,12 @@
|
||||
}
|
||||
},
|
||||
"acceptTransfer": {
|
||||
"message": "Accept transfer"
|
||||
"message": "同意轉移"
|
||||
},
|
||||
"declineAndLeave": {
|
||||
"message": "Decline and leave"
|
||||
"message": "拒絕並離開"
|
||||
},
|
||||
"whyAmISeeingThis": {
|
||||
"message": "Why am I seeing this?"
|
||||
"message": "為什麼我會看到此訊息?"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,39 +1,37 @@
|
||||
<main class="tw-top-0">
|
||||
<popup-page>
|
||||
<popup-header
|
||||
slot="header"
|
||||
[pageTitle]="'createdSend' | i18n"
|
||||
showBackButton
|
||||
[backAction]="goToEditSend.bind(this)"
|
||||
>
|
||||
<ng-container slot="end">
|
||||
<app-pop-out></app-pop-out>
|
||||
</ng-container>
|
||||
</popup-header>
|
||||
<popup-page>
|
||||
<popup-header
|
||||
slot="header"
|
||||
[pageTitle]="'createdSend' | i18n"
|
||||
showBackButton
|
||||
[backAction]="goToEditSend.bind(this)"
|
||||
>
|
||||
<ng-container slot="end">
|
||||
<app-pop-out></app-pop-out>
|
||||
</ng-container>
|
||||
</popup-header>
|
||||
|
||||
<div
|
||||
class="tw-flex tw-bg-background-alt tw-flex-col tw-justify-center tw-items-center tw-gap-2 tw-h-full tw-px-5"
|
||||
>
|
||||
<div class="tw-size-[95px] tw-content-center">
|
||||
<bit-icon [icon]="sendCreatedIcon"></bit-icon>
|
||||
</div>
|
||||
<h3 tabindex="0" appAutofocus class="tw-font-medium">
|
||||
{{ "createdSendSuccessfully" | i18n }}
|
||||
</h3>
|
||||
<p class="tw-text-center">
|
||||
{{ formatExpirationDate() }}
|
||||
</p>
|
||||
<button bitButton type="button" buttonType="primary" (click)="copyLink()">
|
||||
<b>{{ "copyLink" | i18n }}</b>
|
||||
</button>
|
||||
<div
|
||||
class="tw-flex tw-bg-background-alt tw-flex-col tw-justify-center tw-items-center tw-gap-2 tw-h-full tw-px-5"
|
||||
>
|
||||
<div class="tw-size-[95px] tw-content-center">
|
||||
<bit-icon [icon]="sendCreatedIcon"></bit-icon>
|
||||
</div>
|
||||
<popup-footer slot="footer">
|
||||
<button bitButton type="button" buttonType="primary" (click)="copyLink()">
|
||||
<b>{{ "copyLink" | i18n }}</b>
|
||||
</button>
|
||||
<button bitButton type="button" buttonType="secondary" (click)="goBack()">
|
||||
{{ "close" | i18n }}
|
||||
</button>
|
||||
</popup-footer>
|
||||
</popup-page>
|
||||
</main>
|
||||
<h3 tabindex="0" appAutofocus class="tw-font-medium">
|
||||
{{ "createdSendSuccessfully" | i18n }}
|
||||
</h3>
|
||||
<p class="tw-text-center">
|
||||
{{ formatExpirationDate() }}
|
||||
</p>
|
||||
<button bitButton type="button" buttonType="primary" (click)="copyLink()">
|
||||
<b>{{ "copyLink" | i18n }}</b>
|
||||
</button>
|
||||
</div>
|
||||
<popup-footer slot="footer">
|
||||
<button bitButton type="button" buttonType="primary" (click)="copyLink()">
|
||||
<b>{{ "copyLink" | i18n }}</b>
|
||||
</button>
|
||||
<button bitButton type="button" buttonType="secondary" (click)="goBack()">
|
||||
{{ "close" | i18n }}
|
||||
</button>
|
||||
</popup-footer>
|
||||
</popup-page>
|
||||
|
||||
@@ -15,8 +15,8 @@
|
||||
}
|
||||
@if (savedUrls().length > 1) {
|
||||
<div class="tw-flex tw-justify-between tw-items-center tw-mt-4 tw-mb-1 tw-pt-2">
|
||||
<p class="tw-text-muted tw-text-xs tw-uppercase tw-font-medium">
|
||||
{{ "savedWebsites" | i18n: savedUrls().length }}
|
||||
<p class="tw-text-muted tw-text-xs tw-uppercase tw-font-medium tw-mb-0">
|
||||
{{ "savedWebsites" | i18n: savedUrls().length.toString() }}
|
||||
</p>
|
||||
<button
|
||||
type="button"
|
||||
|
||||
@@ -13,17 +13,6 @@
|
||||
<button type="button" bitMenuItem (click)="doAutofill()">
|
||||
{{ "autofill" | i18n }}
|
||||
</button>
|
||||
<!-- Autofill confirmation handles both 'autofill' and 'autofill and save' so no need to show both -->
|
||||
@if (!(autofillConfirmationFlagEnabled$ | async)) {
|
||||
<button
|
||||
type="button"
|
||||
bitMenuItem
|
||||
*ngIf="canEdit && isLogin"
|
||||
(click)="doAutofillAndSave()"
|
||||
>
|
||||
{{ "fillAndSave" | i18n }}
|
||||
</button>
|
||||
}
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="showViewOption">
|
||||
|
||||
@@ -13,7 +13,6 @@ import {
|
||||
UriMatchStrategy,
|
||||
UriMatchStrategySetting,
|
||||
} from "@bitwarden/common/models/domain/domain-service";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { CipherArchiveService } from "@bitwarden/common/vault/abstractions/cipher-archive.service";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
@@ -40,10 +39,6 @@ describe("ItemMoreOptionsComponent", () => {
|
||||
openSimpleDialog: jest.fn().mockResolvedValue(true),
|
||||
open: jest.fn(),
|
||||
};
|
||||
const featureFlag$ = new BehaviorSubject<boolean>(false);
|
||||
const configService = {
|
||||
getFeatureFlag$: jest.fn().mockImplementation(() => featureFlag$.asObservable()),
|
||||
};
|
||||
const cipherService = {
|
||||
getFullCipherView: jest.fn(),
|
||||
encrypt: jest.fn(),
|
||||
@@ -93,7 +88,6 @@ describe("ItemMoreOptionsComponent", () => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [ItemMoreOptionsComponent, NoopAnimationsModule],
|
||||
providers: [
|
||||
{ provide: ConfigService, useValue: configService },
|
||||
{ provide: CipherService, useValue: cipherService },
|
||||
{ provide: VaultPopupAutofillService, useValue: autofillSvc },
|
||||
|
||||
@@ -152,22 +146,6 @@ describe("ItemMoreOptionsComponent", () => {
|
||||
expect(passwordRepromptService.passwordRepromptCheck).toHaveBeenCalledWith(baseCipher);
|
||||
});
|
||||
|
||||
it("calls the autofill service to autofill without showing the confirmation dialog when the feature flag is disabled", async () => {
|
||||
autofillSvc.currentAutofillTab$.next({ url: "https://page.example.com" });
|
||||
|
||||
await component.doAutofill();
|
||||
|
||||
expect(cipherService.getFullCipherView).toHaveBeenCalled();
|
||||
expect(autofillSvc.doAutofill).toHaveBeenCalledTimes(1);
|
||||
expect(autofillSvc.doAutofill).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ id: "cipher-1" }),
|
||||
true,
|
||||
true,
|
||||
);
|
||||
expect(autofillSvc.doAutofillAndSave).not.toHaveBeenCalled();
|
||||
expect(dialogService.openSimpleDialog).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does nothing if the user fails master password reprompt", async () => {
|
||||
baseCipher.reprompt = 2; // Master Password reprompt enabled
|
||||
autofillSvc.currentAutofillTab$.next({ url: "https://page.example.com" });
|
||||
@@ -181,7 +159,6 @@ describe("ItemMoreOptionsComponent", () => {
|
||||
});
|
||||
|
||||
it("does not show the exact match dialog when the default match strategy is Exact and autofill confirmation is not to be shown", async () => {
|
||||
// autofill confirmation dialog is not shown when either the feature flag is disabled
|
||||
uriMatchStrategy$.next(UriMatchStrategy.Exact);
|
||||
autofillSvc.currentAutofillTab$.next({ url: "https://page.example.com/path" });
|
||||
await component.doAutofill();
|
||||
@@ -191,8 +168,6 @@ describe("ItemMoreOptionsComponent", () => {
|
||||
|
||||
describe("autofill confirmation dialog", () => {
|
||||
beforeEach(() => {
|
||||
// autofill confirmation dialog is shown when feature flag is enabled
|
||||
featureFlag$.next(true);
|
||||
uriMatchStrategy$.next(UriMatchStrategy.Domain);
|
||||
passwordRepromptService.passwordRepromptCheck.mockResolvedValue(true);
|
||||
});
|
||||
@@ -206,7 +181,7 @@ describe("ItemMoreOptionsComponent", () => {
|
||||
expect(passwordRepromptService.passwordRepromptCheck).toHaveBeenCalledWith(baseCipher);
|
||||
});
|
||||
|
||||
it("opens the autofill confirmation dialog with filtered saved URLs when the feature flag is enabled", async () => {
|
||||
it("opens the autofill confirmation dialog with filtered saved URLs", async () => {
|
||||
autofillSvc.currentAutofillTab$.next({ url: "https://page.example.com/path" });
|
||||
const openSpy = mockConfirmDialogResult(AutofillConfirmationDialogResult.Canceled);
|
||||
|
||||
|
||||
@@ -11,9 +11,7 @@ import { OrganizationService } from "@bitwarden/common/admin-console/abstraction
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { UriMatchStrategy } from "@bitwarden/common/models/domain/domain-service";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { CipherId, UserId } from "@bitwarden/common/types/guid";
|
||||
import { CipherArchiveService } from "@bitwarden/common/vault/abstractions/cipher-archive.service";
|
||||
@@ -37,7 +35,6 @@ import { PasswordRepromptService } from "@bitwarden/vault";
|
||||
|
||||
import { BrowserPremiumUpgradePromptService } from "../../../services/browser-premium-upgrade-prompt.service";
|
||||
import { VaultPopupAutofillService } from "../../../services/vault-popup-autofill.service";
|
||||
import { VaultPopupItemsService } from "../../../services/vault-popup-items.service";
|
||||
import { AddEditQueryParams } from "../add-edit/add-edit-v2.component";
|
||||
import {
|
||||
AutofillConfirmationDialogComponent,
|
||||
@@ -98,10 +95,6 @@ export class ItemMoreOptionsComponent {
|
||||
|
||||
protected autofillAllowed$ = this.vaultPopupAutofillService.autofillAllowed$;
|
||||
|
||||
protected autofillConfirmationFlagEnabled$ = this.configService
|
||||
.getFeatureFlag$(FeatureFlag.AutofillConfirmation)
|
||||
.pipe(map((isFeatureFlagEnabled) => isFeatureFlagEnabled));
|
||||
|
||||
protected uriMatchStrategy$ = this.domainSettingsService.resolvedDefaultUriMatchStrategy$;
|
||||
|
||||
/**
|
||||
@@ -166,8 +159,6 @@ export class ItemMoreOptionsComponent {
|
||||
private collectionService: CollectionService,
|
||||
private restrictedItemTypesService: RestrictedItemTypesService,
|
||||
private cipherArchiveService: CipherArchiveService,
|
||||
private configService: ConfigService,
|
||||
private vaultPopupItemsService: VaultPopupItemsService,
|
||||
private domainSettingsService: DomainSettingsService,
|
||||
) {}
|
||||
|
||||
@@ -216,13 +207,9 @@ export class ItemMoreOptionsComponent {
|
||||
const cipherHasAllExactMatchLoginUris =
|
||||
uris.length > 0 && uris.every((u) => u.uri && u.match === UriMatchStrategy.Exact);
|
||||
|
||||
const showAutofillConfirmation = await firstValueFrom(this.autofillConfirmationFlagEnabled$);
|
||||
const uriMatchStrategy = await firstValueFrom(this.uriMatchStrategy$);
|
||||
|
||||
if (
|
||||
showAutofillConfirmation &&
|
||||
(cipherHasAllExactMatchLoginUris || uriMatchStrategy === UriMatchStrategy.Exact)
|
||||
) {
|
||||
if (cipherHasAllExactMatchLoginUris || uriMatchStrategy === UriMatchStrategy.Exact) {
|
||||
await this.dialogService.openSimpleDialog({
|
||||
title: { key: "cannotAutofill" },
|
||||
content: { key: "cannotAutofillExactMatch" },
|
||||
@@ -233,11 +220,6 @@ export class ItemMoreOptionsComponent {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!showAutofillConfirmation) {
|
||||
await this.vaultPopupAutofillService.doAutofill(cipher, true, true);
|
||||
return;
|
||||
}
|
||||
|
||||
const currentTab = await firstValueFrom(this.vaultPopupAutofillService.currentAutofillTab$);
|
||||
|
||||
if (!currentTab?.url) {
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
"**/node_modules/@bitwarden/desktop-napi/index.js",
|
||||
"**/node_modules/@bitwarden/desktop-napi/desktop_napi.${platform}-${arch}*.node"
|
||||
],
|
||||
"electronVersion": "37.7.0",
|
||||
"electronVersion": "39.2.6",
|
||||
"generateUpdatesFilesForAllChannels": true,
|
||||
"publish": {
|
||||
"provider": "generic",
|
||||
|
||||
@@ -1776,19 +1776,19 @@
|
||||
"message": "Buradan xaricə köçür"
|
||||
},
|
||||
"exportNoun": {
|
||||
"message": "Export",
|
||||
"message": "Xaricə köçürmə",
|
||||
"description": "The noun form of the word Export"
|
||||
},
|
||||
"exportVerb": {
|
||||
"message": "Export",
|
||||
"message": "Xaricə köçür",
|
||||
"description": "The verb form of the word Export"
|
||||
},
|
||||
"importNoun": {
|
||||
"message": "Import",
|
||||
"message": "Daxilə köçürmə",
|
||||
"description": "The noun form of the word Import"
|
||||
},
|
||||
"importVerb": {
|
||||
"message": "Import",
|
||||
"message": "Daxilə köçür",
|
||||
"description": "The verb form of the word Import"
|
||||
},
|
||||
"fileFormat": {
|
||||
|
||||
@@ -1414,7 +1414,7 @@
|
||||
"message": "Zobraziť Bitwarden v Docku aj keď je minimalizovaný na panel úloh."
|
||||
},
|
||||
"confirmTrayTitle": {
|
||||
"message": "Potvrdiť vypnutie systémovej lišty"
|
||||
"message": "Potvrdiť skrývanie systémovej lišty"
|
||||
},
|
||||
"confirmTrayDesc": {
|
||||
"message": "Vypnutím tohto nastavenia vypnete aj ostatné nastavenia súvisiace so systémovou lištou."
|
||||
@@ -2849,10 +2849,10 @@
|
||||
"message": "Použiť možnosti subadresovania svojho poskytovateľa e-mailu."
|
||||
},
|
||||
"catchallEmail": {
|
||||
"message": "E-mail Catch-all"
|
||||
"message": "Doménový kôš"
|
||||
},
|
||||
"catchallEmailDesc": {
|
||||
"message": "Použiť doručenú poštu typu catch-all nastavenú na doméne."
|
||||
"message": "Použiť nastavený doménový kôš."
|
||||
},
|
||||
"useThisEmail": {
|
||||
"message": "Použiť tento e-mail"
|
||||
|
||||
@@ -1776,19 +1776,19 @@
|
||||
"message": "导出自"
|
||||
},
|
||||
"exportNoun": {
|
||||
"message": "Export",
|
||||
"message": "导出",
|
||||
"description": "The noun form of the word Export"
|
||||
},
|
||||
"exportVerb": {
|
||||
"message": "Export",
|
||||
"message": "导出",
|
||||
"description": "The verb form of the word Export"
|
||||
},
|
||||
"importNoun": {
|
||||
"message": "Import",
|
||||
"message": "导入",
|
||||
"description": "The noun form of the word Import"
|
||||
},
|
||||
"importVerb": {
|
||||
"message": "Import",
|
||||
"message": "导入",
|
||||
"description": "The verb form of the word Import"
|
||||
},
|
||||
"fileFormat": {
|
||||
|
||||
@@ -709,7 +709,7 @@
|
||||
"message": "新增附件"
|
||||
},
|
||||
"itemsTransferred": {
|
||||
"message": "Items transferred"
|
||||
"message": "項目已轉移"
|
||||
},
|
||||
"fixEncryption": {
|
||||
"message": "修正加密"
|
||||
@@ -1199,7 +1199,7 @@
|
||||
"message": "關注我們"
|
||||
},
|
||||
"syncNow": {
|
||||
"message": "Sync now"
|
||||
"message": "立即同步"
|
||||
},
|
||||
"changeMasterPass": {
|
||||
"message": "變更主密碼"
|
||||
@@ -1776,19 +1776,19 @@
|
||||
"message": "匯出自"
|
||||
},
|
||||
"exportNoun": {
|
||||
"message": "Export",
|
||||
"message": "匯出",
|
||||
"description": "The noun form of the word Export"
|
||||
},
|
||||
"exportVerb": {
|
||||
"message": "Export",
|
||||
"message": "匯出",
|
||||
"description": "The verb form of the word Export"
|
||||
},
|
||||
"importNoun": {
|
||||
"message": "Import",
|
||||
"message": "匯入",
|
||||
"description": "The noun form of the word Import"
|
||||
},
|
||||
"importVerb": {
|
||||
"message": "Import",
|
||||
"message": "匯入",
|
||||
"description": "The verb form of the word Import"
|
||||
},
|
||||
"fileFormat": {
|
||||
@@ -4344,43 +4344,43 @@
|
||||
"message": "升級到 Premium"
|
||||
},
|
||||
"removeMasterPasswordForOrgUserKeyConnector": {
|
||||
"message": "Your organization is no longer using master passwords to log into Bitwarden. To continue, verify the organization and domain."
|
||||
"message": "您的組織已不再使用主密碼登入 Bitwarden。若要繼續,請驗證組織與網域。"
|
||||
},
|
||||
"continueWithLogIn": {
|
||||
"message": "Continue with log in"
|
||||
"message": "繼續登入"
|
||||
},
|
||||
"doNotContinue": {
|
||||
"message": "Do not continue"
|
||||
"message": "不要繼續"
|
||||
},
|
||||
"domain": {
|
||||
"message": "Domain"
|
||||
"message": "網域"
|
||||
},
|
||||
"keyConnectorDomainTooltip": {
|
||||
"message": "This domain will store your account encryption keys, so make sure you trust it. If you're not sure, check with your admin."
|
||||
"message": "此網域將儲存您帳號的加密金鑰,請確認您信任它。若不確定,請洽詢您的管理員。"
|
||||
},
|
||||
"verifyYourOrganization": {
|
||||
"message": "Verify your organization to log in"
|
||||
"message": "驗證您的組織以登入"
|
||||
},
|
||||
"organizationVerified": {
|
||||
"message": "Organization verified"
|
||||
"message": "組織已驗證"
|
||||
},
|
||||
"domainVerified": {
|
||||
"message": "Domain verified"
|
||||
"message": "已驗證網域"
|
||||
},
|
||||
"leaveOrganizationContent": {
|
||||
"message": "If you don't verify your organization, your access to the organization will be revoked."
|
||||
"message": "若您未驗證組織,將會被撤銷對該組織的存取權限。"
|
||||
},
|
||||
"leaveNow": {
|
||||
"message": "Leave now"
|
||||
"message": "立即離開"
|
||||
},
|
||||
"verifyYourDomainToLogin": {
|
||||
"message": "Verify your domain to log in"
|
||||
"message": "驗證您的網域以登入"
|
||||
},
|
||||
"verifyYourDomainDescription": {
|
||||
"message": "To continue with log in, verify this domain."
|
||||
"message": "若要繼續登入,請驗證此網域。"
|
||||
},
|
||||
"confirmKeyConnectorOrganizationUserDescription": {
|
||||
"message": "To continue with log in, verify the organization and domain."
|
||||
"message": "若要繼續登入,請驗證組織與網域。"
|
||||
},
|
||||
"sessionTimeoutSettingsAction": {
|
||||
"message": "逾時後動作"
|
||||
@@ -4430,19 +4430,19 @@
|
||||
"message": "設定一個解鎖方式來變更您的密碼庫逾時動作。"
|
||||
},
|
||||
"upgrade": {
|
||||
"message": "Upgrade"
|
||||
"message": "升級"
|
||||
},
|
||||
"leaveConfirmationDialogTitle": {
|
||||
"message": "Are you sure you want to leave?"
|
||||
"message": "確定要離開嗎?"
|
||||
},
|
||||
"leaveConfirmationDialogContentOne": {
|
||||
"message": "By declining, your personal items will stay in your account, but you'll lose access to shared items and organization features."
|
||||
"message": "若選擇拒絕,您的個人項目將保留在帳號中,但您將失去對共用項目與組織功能的存取權。"
|
||||
},
|
||||
"leaveConfirmationDialogContentTwo": {
|
||||
"message": "Contact your admin to regain access."
|
||||
"message": "請聯絡您的管理員以重新取得存取權限。"
|
||||
},
|
||||
"leaveConfirmationDialogConfirmButton": {
|
||||
"message": "Leave $ORGANIZATION$",
|
||||
"message": "離開 $ORGANIZATION$",
|
||||
"placeholders": {
|
||||
"organization": {
|
||||
"content": "$1",
|
||||
@@ -4451,10 +4451,10 @@
|
||||
}
|
||||
},
|
||||
"howToManageMyVault": {
|
||||
"message": "How do I manage my vault?"
|
||||
"message": "我要如何管理我的密碼庫?"
|
||||
},
|
||||
"transferItemsToOrganizationTitle": {
|
||||
"message": "Transfer items to $ORGANIZATION$",
|
||||
"message": "將項目轉移至 $ORGANIZATION$",
|
||||
"placeholders": {
|
||||
"organization": {
|
||||
"content": "$1",
|
||||
@@ -4463,7 +4463,7 @@
|
||||
}
|
||||
},
|
||||
"transferItemsToOrganizationContent": {
|
||||
"message": "$ORGANIZATION$ is requiring all items to be owned by the organization for security and compliance. Click accept to transfer ownership of your items.",
|
||||
"message": "$ORGANIZATION$ 為了安全性與合規性,要求所有項目皆由組織擁有。點擊接受即可轉移您項目的擁有權。",
|
||||
"placeholders": {
|
||||
"organization": {
|
||||
"content": "$1",
|
||||
@@ -4472,12 +4472,12 @@
|
||||
}
|
||||
},
|
||||
"acceptTransfer": {
|
||||
"message": "Accept transfer"
|
||||
"message": "同意轉移"
|
||||
},
|
||||
"declineAndLeave": {
|
||||
"message": "Decline and leave"
|
||||
"message": "拒絕並離開"
|
||||
},
|
||||
"whyAmISeeingThis": {
|
||||
"message": "Why am I seeing this?"
|
||||
"message": "為什麼我會看到此訊息?"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -587,6 +587,9 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||
queryParams: { search: Utils.isNullOrEmpty(searchText) ? null : searchText },
|
||||
queryParamsHandling: "merge",
|
||||
replaceUrl: true,
|
||||
state: {
|
||||
focusMainAfterNav: false,
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
<app-display-billing-address
|
||||
[subscriber]="view.organization"
|
||||
[billingAddress]="view.billingAddress"
|
||||
[taxIdWarning]="enableTaxIdWarning ? view.taxIdWarning : null"
|
||||
[taxIdWarning]="view.taxIdWarning"
|
||||
(updated)="setBillingAddress($event)"
|
||||
></app-display-billing-address>
|
||||
|
||||
|
||||
@@ -22,8 +22,6 @@ import { OrganizationService } from "@bitwarden/common/admin-console/abstraction
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { getById } from "@bitwarden/common/platform/misc";
|
||||
import { DialogService } from "@bitwarden/components";
|
||||
import { CommandDefinition, MessageListener } from "@bitwarden/messaging";
|
||||
@@ -118,12 +116,9 @@ export class OrganizationPaymentDetailsComponent implements OnInit, OnDestroy {
|
||||
|
||||
private destroy$ = new Subject<void>();
|
||||
|
||||
protected enableTaxIdWarning!: boolean;
|
||||
|
||||
constructor(
|
||||
private accountService: AccountService,
|
||||
private activatedRoute: ActivatedRoute,
|
||||
private configService: ConfigService,
|
||||
private dialogService: DialogService,
|
||||
private messageListener: MessageListener,
|
||||
private organizationService: OrganizationService,
|
||||
@@ -140,36 +135,30 @@ export class OrganizationPaymentDetailsComponent implements OnInit, OnDestroy {
|
||||
await this.changePaymentMethod();
|
||||
}
|
||||
|
||||
this.enableTaxIdWarning = await this.configService.getFeatureFlag(
|
||||
FeatureFlag.PM22415_TaxIDWarnings,
|
||||
);
|
||||
|
||||
if (this.enableTaxIdWarning) {
|
||||
this.organizationWarningsService.taxIdWarningRefreshed$
|
||||
.pipe(
|
||||
switchMap((warning) =>
|
||||
combineLatest([
|
||||
of(warning),
|
||||
this.organization$.pipe(take(1)).pipe(
|
||||
mapOrganizationToSubscriber,
|
||||
switchMap((organization) =>
|
||||
this.subscriberBillingClient.getBillingAddress(organization),
|
||||
),
|
||||
this.organizationWarningsService.taxIdWarningRefreshed$
|
||||
.pipe(
|
||||
switchMap((warning) =>
|
||||
combineLatest([
|
||||
of(warning),
|
||||
this.organization$.pipe(take(1)).pipe(
|
||||
mapOrganizationToSubscriber,
|
||||
switchMap((organization) =>
|
||||
this.subscriberBillingClient.getBillingAddress(organization),
|
||||
),
|
||||
]),
|
||||
),
|
||||
takeUntil(this.destroy$),
|
||||
)
|
||||
.subscribe(([taxIdWarning, billingAddress]) => {
|
||||
if (this.viewState$.value) {
|
||||
this.viewState$.next({
|
||||
...this.viewState$.value,
|
||||
taxIdWarning,
|
||||
billingAddress,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
),
|
||||
]),
|
||||
),
|
||||
takeUntil(this.destroy$),
|
||||
)
|
||||
.subscribe(([taxIdWarning, billingAddress]) => {
|
||||
if (this.viewState$.value) {
|
||||
this.viewState$.next({
|
||||
...this.viewState$.value,
|
||||
taxIdWarning,
|
||||
billingAddress,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
this.messageListener
|
||||
.messages$(BANK_ACCOUNT_VERIFIED_COMMAND)
|
||||
@@ -216,10 +205,7 @@ export class OrganizationPaymentDetailsComponent implements OnInit, OnDestroy {
|
||||
|
||||
setBillingAddress = (billingAddress: BillingAddress) => {
|
||||
if (this.viewState$.value) {
|
||||
if (
|
||||
this.enableTaxIdWarning &&
|
||||
this.viewState$.value.billingAddress?.taxId !== billingAddress.taxId
|
||||
) {
|
||||
if (this.viewState$.value.billingAddress?.taxId !== billingAddress.taxId) {
|
||||
this.organizationWarningsService.refreshTaxIdWarning();
|
||||
}
|
||||
this.viewState$.next({
|
||||
|
||||
@@ -12,8 +12,6 @@ import {
|
||||
|
||||
import { Account, AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { getUserId } from "@bitwarden/common/auth/services/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 { BannerModule, DialogService } from "@bitwarden/components";
|
||||
import { BILLING_DISK, StateProvider, UserKeyDefinition } from "@bitwarden/state";
|
||||
@@ -88,23 +86,21 @@ type GetWarning$ = () => Observable<TaxIdWarningType | null>;
|
||||
@Component({
|
||||
selector: "app-tax-id-warning",
|
||||
template: `
|
||||
@if (enableTaxIdWarning$ | async) {
|
||||
@let view = view$ | async;
|
||||
@let view = view$ | async;
|
||||
|
||||
@if (view) {
|
||||
<bit-banner id="tax-id-warning-banner" bannerType="warning" (onClose)="trackDismissal()">
|
||||
{{ view.message }}
|
||||
<a
|
||||
bitLink
|
||||
linkType="secondary"
|
||||
(click)="editBillingAddress()"
|
||||
class="tw-cursor-pointer"
|
||||
rel="noreferrer noopener"
|
||||
>
|
||||
{{ view.callToAction }}
|
||||
</a>
|
||||
</bit-banner>
|
||||
}
|
||||
@if (view) {
|
||||
<bit-banner id="tax-id-warning-banner" bannerType="warning" (onClose)="trackDismissal()">
|
||||
{{ view.message }}
|
||||
<a
|
||||
bitLink
|
||||
linkType="secondary"
|
||||
(click)="editBillingAddress()"
|
||||
class="tw-cursor-pointer"
|
||||
rel="noreferrer noopener"
|
||||
>
|
||||
{{ view.callToAction }}
|
||||
</a>
|
||||
</bit-banner>
|
||||
}
|
||||
`,
|
||||
imports: [BannerModule, SharedModule],
|
||||
@@ -120,10 +116,6 @@ export class TaxIdWarningComponent implements OnInit {
|
||||
// eslint-disable-next-line @angular-eslint/prefer-output-emitter-ref
|
||||
@Output() billingAddressUpdated = new EventEmitter<void>();
|
||||
|
||||
protected enableTaxIdWarning$ = this.configService.getFeatureFlag$(
|
||||
FeatureFlag.PM22415_TaxIDWarnings,
|
||||
);
|
||||
|
||||
protected userId$ = this.accountService.activeAccount$.pipe(
|
||||
filter((account): account is Account => account !== null),
|
||||
getUserId,
|
||||
@@ -209,7 +201,6 @@ export class TaxIdWarningComponent implements OnInit {
|
||||
|
||||
constructor(
|
||||
private accountService: AccountService,
|
||||
private configService: ConfigService,
|
||||
private dialogService: DialogService,
|
||||
private i18nService: I18nService,
|
||||
private subscriberBillingClient: SubscriberBillingClient,
|
||||
|
||||
@@ -0,0 +1,241 @@
|
||||
import { mock, MockProxy } from "jest-mock-extended";
|
||||
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { CipherEncryptionService } from "@bitwarden/common/vault/abstractions/cipher-encryption.service";
|
||||
import { Cipher } from "@bitwarden/common/vault/models/domain/cipher";
|
||||
import { DialogService } from "@bitwarden/components";
|
||||
import { UserId } from "@bitwarden/user-core";
|
||||
|
||||
import { LogRecorder } from "../log-recorder";
|
||||
|
||||
import { CipherStep } from "./cipher-step";
|
||||
import { RecoveryWorkingData } from "./recovery-step";
|
||||
|
||||
describe("CipherStep", () => {
|
||||
let cipherStep: CipherStep;
|
||||
let apiService: MockProxy<ApiService>;
|
||||
let cipherEncryptionService: MockProxy<CipherEncryptionService>;
|
||||
let dialogService: MockProxy<DialogService>;
|
||||
let logger: MockProxy<LogRecorder>;
|
||||
|
||||
beforeEach(() => {
|
||||
apiService = mock<ApiService>();
|
||||
cipherEncryptionService = mock<CipherEncryptionService>();
|
||||
dialogService = mock<DialogService>();
|
||||
logger = mock<LogRecorder>();
|
||||
|
||||
cipherStep = new CipherStep(apiService, cipherEncryptionService, dialogService);
|
||||
});
|
||||
|
||||
describe("runDiagnostics", () => {
|
||||
it("returns false and logs error when userId is missing", async () => {
|
||||
const workingData: RecoveryWorkingData = {
|
||||
userId: null,
|
||||
userKey: null,
|
||||
encryptedPrivateKey: null,
|
||||
isPrivateKeyCorrupt: false,
|
||||
ciphers: [],
|
||||
folders: [],
|
||||
};
|
||||
|
||||
const result = await cipherStep.runDiagnostics(workingData, logger);
|
||||
|
||||
expect(result).toBe(false);
|
||||
expect(logger.record).toHaveBeenCalledWith("Missing user ID");
|
||||
});
|
||||
|
||||
it("returns true when all user ciphers are decryptable", async () => {
|
||||
const userId = "user-id" as UserId;
|
||||
const cipher1 = { id: "cipher-1", organizationId: null } as Cipher;
|
||||
const cipher2 = { id: "cipher-2", organizationId: null } as Cipher;
|
||||
|
||||
const workingData: RecoveryWorkingData = {
|
||||
userId,
|
||||
userKey: null,
|
||||
encryptedPrivateKey: null,
|
||||
isPrivateKeyCorrupt: false,
|
||||
ciphers: [cipher1, cipher2],
|
||||
folders: [],
|
||||
};
|
||||
|
||||
cipherEncryptionService.decrypt.mockResolvedValue({} as any);
|
||||
|
||||
const result = await cipherStep.runDiagnostics(workingData, logger);
|
||||
|
||||
expect(result).toBe(true);
|
||||
expect(cipherEncryptionService.decrypt).toHaveBeenCalledWith(cipher1, userId);
|
||||
expect(cipherEncryptionService.decrypt).toHaveBeenCalledWith(cipher2, userId);
|
||||
});
|
||||
|
||||
it("filters out organization ciphers (organizationId !== null) and only processes user ciphers", async () => {
|
||||
const userId = "user-id" as UserId;
|
||||
const userCipher = { id: "user-cipher", organizationId: null } as Cipher;
|
||||
const orgCipher1 = { id: "org-cipher-1", organizationId: "org-1" } as Cipher;
|
||||
const orgCipher2 = { id: "org-cipher-2", organizationId: "org-2" } as Cipher;
|
||||
|
||||
const workingData: RecoveryWorkingData = {
|
||||
userId,
|
||||
userKey: null,
|
||||
encryptedPrivateKey: null,
|
||||
isPrivateKeyCorrupt: false,
|
||||
ciphers: [userCipher, orgCipher1, orgCipher2],
|
||||
folders: [],
|
||||
};
|
||||
|
||||
cipherEncryptionService.decrypt.mockResolvedValue({} as any);
|
||||
|
||||
const result = await cipherStep.runDiagnostics(workingData, logger);
|
||||
|
||||
expect(result).toBe(true);
|
||||
// Only user cipher should be processed
|
||||
expect(cipherEncryptionService.decrypt).toHaveBeenCalledTimes(1);
|
||||
expect(cipherEncryptionService.decrypt).toHaveBeenCalledWith(userCipher, userId);
|
||||
// Organization ciphers should not be processed
|
||||
expect(cipherEncryptionService.decrypt).not.toHaveBeenCalledWith(orgCipher1, userId);
|
||||
expect(cipherEncryptionService.decrypt).not.toHaveBeenCalledWith(orgCipher2, userId);
|
||||
});
|
||||
|
||||
it("returns false and records undecryptable user ciphers", async () => {
|
||||
const userId = "user-id" as UserId;
|
||||
const cipher1 = { id: "cipher-1", organizationId: null } as Cipher;
|
||||
const cipher2 = { id: "cipher-2", organizationId: null } as Cipher;
|
||||
const cipher3 = { id: "cipher-3", organizationId: null } as Cipher;
|
||||
|
||||
const workingData: RecoveryWorkingData = {
|
||||
userId,
|
||||
userKey: null,
|
||||
encryptedPrivateKey: null,
|
||||
isPrivateKeyCorrupt: false,
|
||||
ciphers: [cipher1, cipher2, cipher3],
|
||||
folders: [],
|
||||
};
|
||||
|
||||
cipherEncryptionService.decrypt
|
||||
.mockResolvedValueOnce({} as any) // cipher1 succeeds
|
||||
.mockRejectedValueOnce(new Error("Decryption failed")) // cipher2 fails
|
||||
.mockRejectedValueOnce(new Error("Decryption failed")); // cipher3 fails
|
||||
|
||||
const result = await cipherStep.runDiagnostics(workingData, logger);
|
||||
|
||||
expect(result).toBe(false);
|
||||
expect(logger.record).toHaveBeenCalledWith("Cipher ID cipher-2 was undecryptable");
|
||||
expect(logger.record).toHaveBeenCalledWith("Cipher ID cipher-3 was undecryptable");
|
||||
expect(logger.record).toHaveBeenCalledWith("Found 2 undecryptable ciphers");
|
||||
});
|
||||
});
|
||||
|
||||
describe("canRecover", () => {
|
||||
it("returns false when there are no undecryptable ciphers", async () => {
|
||||
const userId = "user-id" as UserId;
|
||||
const workingData: RecoveryWorkingData = {
|
||||
userId,
|
||||
userKey: null,
|
||||
encryptedPrivateKey: null,
|
||||
isPrivateKeyCorrupt: false,
|
||||
ciphers: [{ id: "cipher-1", organizationId: null } as Cipher],
|
||||
folders: [],
|
||||
};
|
||||
|
||||
cipherEncryptionService.decrypt.mockResolvedValue({} as any);
|
||||
|
||||
await cipherStep.runDiagnostics(workingData, logger);
|
||||
const result = cipherStep.canRecover(workingData);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it("returns true when there are undecryptable ciphers", async () => {
|
||||
const userId = "user-id" as UserId;
|
||||
const workingData: RecoveryWorkingData = {
|
||||
userId,
|
||||
userKey: null,
|
||||
encryptedPrivateKey: null,
|
||||
isPrivateKeyCorrupt: false,
|
||||
ciphers: [{ id: "cipher-1", organizationId: null } as Cipher],
|
||||
folders: [],
|
||||
};
|
||||
|
||||
cipherEncryptionService.decrypt.mockRejectedValue(new Error("Decryption failed"));
|
||||
|
||||
await cipherStep.runDiagnostics(workingData, logger);
|
||||
const result = cipherStep.canRecover(workingData);
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("runRecovery", () => {
|
||||
it("logs and returns early when there are no undecryptable ciphers", async () => {
|
||||
const workingData: RecoveryWorkingData = {
|
||||
userId: "user-id" as UserId,
|
||||
userKey: null,
|
||||
encryptedPrivateKey: null,
|
||||
isPrivateKeyCorrupt: false,
|
||||
ciphers: [],
|
||||
folders: [],
|
||||
};
|
||||
|
||||
await cipherStep.runRecovery(workingData, logger);
|
||||
|
||||
expect(logger.record).toHaveBeenCalledWith("No undecryptable ciphers to recover");
|
||||
expect(dialogService.openSimpleDialog).not.toHaveBeenCalled();
|
||||
expect(apiService.deleteCipher).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("throws error when user cancels deletion", async () => {
|
||||
const userId = "user-id" as UserId;
|
||||
const workingData: RecoveryWorkingData = {
|
||||
userId,
|
||||
userKey: null,
|
||||
encryptedPrivateKey: null,
|
||||
isPrivateKeyCorrupt: false,
|
||||
ciphers: [{ id: "cipher-1", organizationId: null } as Cipher],
|
||||
folders: [],
|
||||
};
|
||||
|
||||
cipherEncryptionService.decrypt.mockRejectedValue(new Error("Decryption failed"));
|
||||
await cipherStep.runDiagnostics(workingData, logger);
|
||||
|
||||
dialogService.openSimpleDialog.mockResolvedValue(false);
|
||||
|
||||
await expect(cipherStep.runRecovery(workingData, logger)).rejects.toThrow(
|
||||
"Cipher recovery cancelled by user",
|
||||
);
|
||||
|
||||
expect(logger.record).toHaveBeenCalledWith("Showing confirmation dialog for 1 ciphers");
|
||||
expect(logger.record).toHaveBeenCalledWith("User cancelled cipher deletion");
|
||||
expect(apiService.deleteCipher).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("deletes undecryptable ciphers when user confirms", async () => {
|
||||
const userId = "user-id" as UserId;
|
||||
const cipher1 = { id: "cipher-1", organizationId: null } as Cipher;
|
||||
const cipher2 = { id: "cipher-2", organizationId: null } as Cipher;
|
||||
|
||||
const workingData: RecoveryWorkingData = {
|
||||
userId,
|
||||
userKey: null,
|
||||
encryptedPrivateKey: null,
|
||||
isPrivateKeyCorrupt: false,
|
||||
ciphers: [cipher1, cipher2],
|
||||
folders: [],
|
||||
};
|
||||
|
||||
cipherEncryptionService.decrypt.mockRejectedValue(new Error("Decryption failed"));
|
||||
await cipherStep.runDiagnostics(workingData, logger);
|
||||
|
||||
dialogService.openSimpleDialog.mockResolvedValue(true);
|
||||
apiService.deleteCipher.mockResolvedValue(undefined);
|
||||
|
||||
await cipherStep.runRecovery(workingData, logger);
|
||||
|
||||
expect(logger.record).toHaveBeenCalledWith("Showing confirmation dialog for 2 ciphers");
|
||||
expect(logger.record).toHaveBeenCalledWith("Deleting 2 ciphers");
|
||||
expect(apiService.deleteCipher).toHaveBeenCalledWith("cipher-1");
|
||||
expect(apiService.deleteCipher).toHaveBeenCalledWith("cipher-2");
|
||||
expect(logger.record).toHaveBeenCalledWith("Deleted cipher cipher-1");
|
||||
expect(logger.record).toHaveBeenCalledWith("Deleted cipher cipher-2");
|
||||
expect(logger.record).toHaveBeenCalledWith("Successfully deleted 2 ciphers");
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -24,7 +24,11 @@ export class CipherStep implements RecoveryStep {
|
||||
}
|
||||
|
||||
this.undecryptableCipherIds = [];
|
||||
for (const cipher of workingData.ciphers) {
|
||||
// The tool is currently only implemented to handle ciphers that are corrupt for a user. For an organization, the case of
|
||||
// local user not having access to the organization key is not properly handled here, and should be implemented separately.
|
||||
// For now, this just filters out and does not consider corrupt organization ciphers.
|
||||
const userCiphers = workingData.ciphers.filter((c) => c.organizationId == null);
|
||||
for (const cipher of userCiphers) {
|
||||
try {
|
||||
await this.cipherService.decrypt(cipher, workingData.userId);
|
||||
} catch {
|
||||
|
||||
@@ -74,6 +74,9 @@ export class RoutedVaultFilterService implements OnDestroy {
|
||||
type: filter.type ?? null,
|
||||
},
|
||||
queryParamsHandling: "merge",
|
||||
state: {
|
||||
focusMainAfterNav: false,
|
||||
},
|
||||
};
|
||||
return [commands, extras];
|
||||
}
|
||||
|
||||
@@ -424,6 +424,9 @@ export class VaultComponent<C extends CipherViewLike> implements OnInit, OnDestr
|
||||
queryParams: { search: Utils.isNullOrEmpty(searchText) ? null : searchText },
|
||||
queryParamsHandling: "merge",
|
||||
replaceUrl: true,
|
||||
state: {
|
||||
focusMainAfterNav: false,
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
|
||||
@@ -392,7 +392,7 @@
|
||||
"message": "Yeni tətbiqlər incələ"
|
||||
},
|
||||
"reviewNewAppsDescription": {
|
||||
"message": "Review new applications with vulnerable items and mark those you’d like to monitor closely as critical. Then, you’ll be able to assign security tasks to members to remove risks."
|
||||
"message": "Həssas elementlərə sahib yeni tətbiqləri incələyin və diqqətlə izləmək istədiklərinizi kritik olaraq işarələyin. Sonra, riskləri xaric etmək üçün üzvlərə təhlükəsizlik tapşırıqları təyin edə biləcəksiniz."
|
||||
},
|
||||
"clickIconToMarkAppAsCritical": {
|
||||
"message": "Bir tətbiqi kritik olaraq işarələmək üçün ulduz ikonuna klikləyin"
|
||||
@@ -1970,11 +1970,11 @@
|
||||
"message": "Hesab şifrələmə açarları, hər Bitwarden istifadəçi hesabı üçün unikaldır, buna görə də şifrələnmiş bir ixracı, fərqli bir hesaba idxal edə bilməzsiniz."
|
||||
},
|
||||
"exportNoun": {
|
||||
"message": "Export",
|
||||
"message": "Xaricə köçürmə",
|
||||
"description": "The noun form of the word Export"
|
||||
},
|
||||
"exportVerb": {
|
||||
"message": "Export",
|
||||
"message": "Xaricə köçür",
|
||||
"description": "The verb form of the word Export"
|
||||
},
|
||||
"exportFrom": {
|
||||
@@ -2303,11 +2303,11 @@
|
||||
"message": "Alətlər"
|
||||
},
|
||||
"importNoun": {
|
||||
"message": "Import",
|
||||
"message": "Daxilə köçürmə",
|
||||
"description": "The noun form of the word Import"
|
||||
},
|
||||
"importVerb": {
|
||||
"message": "Import",
|
||||
"message": "Daxilə köçür",
|
||||
"description": "The verb form of the word Import"
|
||||
},
|
||||
"importData": {
|
||||
@@ -3294,7 +3294,7 @@
|
||||
"message": "Bulud Abunəliyini Başlat"
|
||||
},
|
||||
"launchCloudSubscriptionSentenceCase": {
|
||||
"message": "Launch cloud subscription"
|
||||
"message": "Bulud abunəliyini başlat"
|
||||
},
|
||||
"storage": {
|
||||
"message": "Saxlama"
|
||||
@@ -4212,10 +4212,10 @@
|
||||
}
|
||||
},
|
||||
"userAcceptedTransfer": {
|
||||
"message": "Accepted transfer to organization ownership."
|
||||
"message": "Təşkilatın sahibliyinə ötürülmə qəbul edildi."
|
||||
},
|
||||
"userDeclinedTransfer": {
|
||||
"message": "Revoked for declining transfer to organization ownership."
|
||||
"message": "Təşkilatın sahibliyinə ötürülməyə rədd cavabı verildiyi üçün ləğv edildi."
|
||||
},
|
||||
"invitedUserId": {
|
||||
"message": "$ID$ istifadəçisi dəvət edildi.",
|
||||
@@ -6758,10 +6758,10 @@
|
||||
"message": "Cihaz mühafizəsi barədə daha ətraflı"
|
||||
},
|
||||
"sessionTimeoutConfirmationOnSystemLockTitle": {
|
||||
"message": "\"System lock\" will only apply to the browser and desktop app"
|
||||
"message": "\"Sistem kilidi\", yalnız brauzer və masaüstü tətbiqi üçün qüvvəyə minəcək"
|
||||
},
|
||||
"sessionTimeoutConfirmationOnSystemLockDescription": {
|
||||
"message": "The mobile and web app will use \"on app restart\" as their maximum allowed timeout, since the option is not supported."
|
||||
"message": "Mobil və veb tətbiqi, dəstəklənməyən bir seçim olduğu üçün icazə verilən maksimum bitmə vaxtı olaraq \"tətbiq başladılanda\"nı istifadə edəcək. "
|
||||
},
|
||||
"vaultTimeoutPolicyInEffect": {
|
||||
"message": "Təşkilatınızın siyasətləri, icazə verilən maksimum seyf bitmə vaxtını $HOURS$ saat $MINUTES$ dəqiqə olaraq ayarladı.",
|
||||
@@ -9905,11 +9905,11 @@
|
||||
"description": "An option for the offboarding survey shown when a user cancels their subscription."
|
||||
},
|
||||
"switchToFreePlan": {
|
||||
"message": "Switching to free plan",
|
||||
"message": "Ödənişsiz plana keçilir",
|
||||
"description": "An option for the offboarding survey shown when a user cancels their subscription."
|
||||
},
|
||||
"switchToFreeOrg": {
|
||||
"message": "Switching to free organization",
|
||||
"message": "Ödənişsiz təşkilata keçilir",
|
||||
"description": "An option for the offboarding survey shown when a user cancels their subscription."
|
||||
},
|
||||
"freeForOneYear": {
|
||||
@@ -9943,7 +9943,7 @@
|
||||
"message": "Tapşırıq təyin et"
|
||||
},
|
||||
"assignSecurityTasksToMembers": {
|
||||
"message": "Send notifications to change passwords"
|
||||
"message": "Parol dəyişdirmə bildirişlərini göndər"
|
||||
},
|
||||
"assignToCollections": {
|
||||
"message": "Kolleksiyalara təyin et"
|
||||
@@ -12208,13 +12208,13 @@
|
||||
"message": "Ödənişsiz Ailələr sınağını başlat"
|
||||
},
|
||||
"blockClaimedDomainAccountCreation": {
|
||||
"message": "Block account creation for claimed domains"
|
||||
"message": "Götürülmüş domenlər üçün hesab yaradılmasını əngəllə"
|
||||
},
|
||||
"blockClaimedDomainAccountCreationDesc": {
|
||||
"message": "Prevent users from creating accounts outside of your organization using email addresses from claimed domains."
|
||||
"message": "İstifadəçilərin, götürülmüş domenlərə aid e-poçt ünvanlarını istifadə edərək təşkilatınızın xaricində hesab yaratmasını önləyin."
|
||||
},
|
||||
"blockClaimedDomainAccountCreationPrerequisite": {
|
||||
"message": "A domain must be claimed before activating this policy."
|
||||
"message": "Bu siyasət aktivləşdirilməzdən əvvəl bir domen götürülməlidir."
|
||||
},
|
||||
"unlockMethodNeededToChangeTimeoutActionDesc": {
|
||||
"message": "Seyf vaxt bitmə əməliyyatınızı dəyişdirmək üçün bir kilid açma üsulu qurun."
|
||||
@@ -12433,13 +12433,13 @@
|
||||
"message": "Bunu niyə görürəm?"
|
||||
},
|
||||
"youHaveBitwardenPremium": {
|
||||
"message": "You have Bitwarden Premium"
|
||||
"message": "Sizdə Bitwarden Premium var"
|
||||
},
|
||||
"viewAndManagePremiumSubscription": {
|
||||
"message": "View and manage your Premium subscription"
|
||||
"message": "Premium abunəliyinizi görün və idarə edin"
|
||||
},
|
||||
"youNeedToUpdateLicenseFile": {
|
||||
"message": "You'll need to update your license file"
|
||||
"message": "Lisenziya faylınızı güncəlləməlisiniz"
|
||||
},
|
||||
"youNeedToUpdateLicenseFileDate": {
|
||||
"message": "$DATE$.",
|
||||
@@ -12451,16 +12451,16 @@
|
||||
}
|
||||
},
|
||||
"uploadLicenseFile": {
|
||||
"message": "Upload license file"
|
||||
"message": "Lisenziya faylını yüklə"
|
||||
},
|
||||
"uploadYourLicenseFile": {
|
||||
"message": "Upload your license file"
|
||||
"message": "Lisenziya faylınızı yükləyin"
|
||||
},
|
||||
"uploadYourPremiumLicenseFile": {
|
||||
"message": "Upload your Premium license file"
|
||||
"message": "Premium lisenziya faylınızı yükləyin"
|
||||
},
|
||||
"uploadLicenseFileDesc": {
|
||||
"message": "Your license file name will be similar to: $FILE_NAME$",
|
||||
"message": "Lisenziya faylınızın adı $FILE_NAME$ faylı ilə oxşardır",
|
||||
"placeholders": {
|
||||
"file_name": {
|
||||
"content": "$1",
|
||||
@@ -12469,15 +12469,15 @@
|
||||
}
|
||||
},
|
||||
"alreadyHaveSubscriptionQuestion": {
|
||||
"message": "Already have a subscription?"
|
||||
"message": "Artıq abunəliyiniz var?"
|
||||
},
|
||||
"alreadyHaveSubscriptionSelfHostedMessage": {
|
||||
"message": "Open the subscription page on your Bitwarden cloud account and download your license file. Then return to this screen and upload it below."
|
||||
"message": "Bitwarden bulud hesabınızdakı abunəlik səhifəsini açın və lisenziya faylınızı endirin. Sonra bu ekrana qayıdın və aşağıda yükləyin."
|
||||
},
|
||||
"viewAllPlans": {
|
||||
"message": "View all plans"
|
||||
"message": "Bütün planlara bax"
|
||||
},
|
||||
"planDescPremium": {
|
||||
"message": "Complete online security"
|
||||
"message": "Tam onlayn təhlükəsizlik"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3294,7 +3294,7 @@
|
||||
"message": "Cloud-Abonnement starten"
|
||||
},
|
||||
"launchCloudSubscriptionSentenceCase": {
|
||||
"message": "Launch cloud subscription"
|
||||
"message": "Cloud-Abonnement starten"
|
||||
},
|
||||
"storage": {
|
||||
"message": "Speicher"
|
||||
@@ -12433,13 +12433,13 @@
|
||||
"message": "Warum wird mir das angezeigt?"
|
||||
},
|
||||
"youHaveBitwardenPremium": {
|
||||
"message": "You have Bitwarden Premium"
|
||||
"message": "Du hast Bitwarden Premium"
|
||||
},
|
||||
"viewAndManagePremiumSubscription": {
|
||||
"message": "View and manage your Premium subscription"
|
||||
},
|
||||
"youNeedToUpdateLicenseFile": {
|
||||
"message": "You'll need to update your license file"
|
||||
"message": "Du musst deine Lizenzdatei aktualisieren"
|
||||
},
|
||||
"youNeedToUpdateLicenseFileDate": {
|
||||
"message": "$DATE$.",
|
||||
@@ -12457,10 +12457,10 @@
|
||||
"message": "Lade deine Lizenzdatei hoch"
|
||||
},
|
||||
"uploadYourPremiumLicenseFile": {
|
||||
"message": "Upload your Premium license file"
|
||||
"message": "Lade deine Premium-Lizenzdatei hoch"
|
||||
},
|
||||
"uploadLicenseFileDesc": {
|
||||
"message": "Your license file name will be similar to: $FILE_NAME$",
|
||||
"message": "Dein Lizenzdateiname wird in etwa so aussehen: $FILE_NAME$",
|
||||
"placeholders": {
|
||||
"file_name": {
|
||||
"content": "$1",
|
||||
@@ -12469,13 +12469,13 @@
|
||||
}
|
||||
},
|
||||
"alreadyHaveSubscriptionQuestion": {
|
||||
"message": "Already have a subscription?"
|
||||
"message": "Du hast bereits ein Abonnement?"
|
||||
},
|
||||
"alreadyHaveSubscriptionSelfHostedMessage": {
|
||||
"message": "Open the subscription page on your Bitwarden cloud account and download your license file. Then return to this screen and upload it below."
|
||||
},
|
||||
"viewAllPlans": {
|
||||
"message": "View all plans"
|
||||
"message": "Alle Tarife anzeigen"
|
||||
},
|
||||
"planDescPremium": {
|
||||
"message": "Umfassende Online-Sicherheit"
|
||||
|
||||
@@ -4212,10 +4212,10 @@
|
||||
}
|
||||
},
|
||||
"userAcceptedTransfer": {
|
||||
"message": "Accepted transfer to organization ownership."
|
||||
"message": "Az átruházás a szervezet tulajdonába elfogadásra került."
|
||||
},
|
||||
"userDeclinedTransfer": {
|
||||
"message": "Revoked for declining transfer to organization ownership."
|
||||
"message": "A szervezet tulajdonába átruházás visszavonásra került elutasítás miatt."
|
||||
},
|
||||
"invitedUserId": {
|
||||
"message": "$ID$ azonosítójú felhasználó meghívásra került.",
|
||||
|
||||
@@ -4212,10 +4212,10 @@
|
||||
}
|
||||
},
|
||||
"userAcceptedTransfer": {
|
||||
"message": "Accepted transfer to organization ownership."
|
||||
"message": "Pieņemta īpašumtiesību nodošana apvienībai."
|
||||
},
|
||||
"userDeclinedTransfer": {
|
||||
"message": "Revoked for declining transfer to organization ownership."
|
||||
"message": "Atsaukts īpašumtiesību nodošanas apvienībai noraidīšanas dēļ."
|
||||
},
|
||||
"invitedUserId": {
|
||||
"message": "Uzaicināts lietotājs $ID$.",
|
||||
@@ -12460,7 +12460,7 @@
|
||||
"message": "Jāaugšupielādē sava Premium licences datne"
|
||||
},
|
||||
"uploadLicenseFileDesc": {
|
||||
"message": "Your license file name will be similar to: $FILE_NAME$",
|
||||
"message": "Licences datnes nosaukums būs līdzīgs šim: $FILE_NAME$",
|
||||
"placeholders": {
|
||||
"file_name": {
|
||||
"content": "$1",
|
||||
@@ -12469,15 +12469,15 @@
|
||||
}
|
||||
},
|
||||
"alreadyHaveSubscriptionQuestion": {
|
||||
"message": "Already have a subscription?"
|
||||
"message": "Jau ir abonements?"
|
||||
},
|
||||
"alreadyHaveSubscriptionSelfHostedMessage": {
|
||||
"message": "Open the subscription page on your Bitwarden cloud account and download your license file. Then return to this screen and upload it below."
|
||||
"message": "Jāatver abonementu lapa Bitwarden mākoņa kontā un jālejupielādē licences datne. Tad jāatgriežas šajā skatā un zemāk jāaugšupielādē."
|
||||
},
|
||||
"viewAllPlans": {
|
||||
"message": "View all plans"
|
||||
"message": "Apskatīt visus plānus"
|
||||
},
|
||||
"planDescPremium": {
|
||||
"message": "Complete online security"
|
||||
"message": "Pilnīga drošība tiešsaistē"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4212,10 +4212,10 @@
|
||||
}
|
||||
},
|
||||
"userAcceptedTransfer": {
|
||||
"message": "Accepted transfer to organization ownership."
|
||||
"message": "Aceitou a transferência da propriedade da organização."
|
||||
},
|
||||
"userDeclinedTransfer": {
|
||||
"message": "Revoked for declining transfer to organization ownership."
|
||||
"message": "Não aceitou a transferência da propriedade da organização."
|
||||
},
|
||||
"invitedUserId": {
|
||||
"message": "Convidou o usuário $ID$.",
|
||||
|
||||
@@ -4212,10 +4212,10 @@
|
||||
}
|
||||
},
|
||||
"userAcceptedTransfer": {
|
||||
"message": "Accepted transfer to organization ownership."
|
||||
"message": "Transferência para propriedade da organização aceite."
|
||||
},
|
||||
"userDeclinedTransfer": {
|
||||
"message": "Revoked for declining transfer to organization ownership."
|
||||
"message": "Revogado por recusa de transferência para propriedade da organização."
|
||||
},
|
||||
"invitedUserId": {
|
||||
"message": "Utilizador $ID$ convidado.",
|
||||
|
||||
@@ -7583,10 +7583,10 @@
|
||||
"message": "Použiť možnosti subadresovania svojho poskytovateľa e-mailu."
|
||||
},
|
||||
"catchallEmail": {
|
||||
"message": "Catch-all e-mail"
|
||||
"message": "Doménový kôš"
|
||||
},
|
||||
"catchallEmailDesc": {
|
||||
"message": "Použiť doručenú poštu typu catch-all nastavenú na doméne."
|
||||
"message": "Použiť nastavený doménový kôš."
|
||||
},
|
||||
"useThisEmail": {
|
||||
"message": "Použiť tento e-mail"
|
||||
|
||||
@@ -1970,11 +1970,11 @@
|
||||
"message": "每个 Bitwarden 用户账户的账户加密密钥都是唯一的,因此您无法将加密的导出导入到另一个账户。"
|
||||
},
|
||||
"exportNoun": {
|
||||
"message": "Export",
|
||||
"message": "导出",
|
||||
"description": "The noun form of the word Export"
|
||||
},
|
||||
"exportVerb": {
|
||||
"message": "Export",
|
||||
"message": "导出",
|
||||
"description": "The verb form of the word Export"
|
||||
},
|
||||
"exportFrom": {
|
||||
@@ -2303,11 +2303,11 @@
|
||||
"message": "工具"
|
||||
},
|
||||
"importNoun": {
|
||||
"message": "Import",
|
||||
"message": "导入",
|
||||
"description": "The noun form of the word Import"
|
||||
},
|
||||
"importVerb": {
|
||||
"message": "Import",
|
||||
"message": "导入",
|
||||
"description": "The verb form of the word Import"
|
||||
},
|
||||
"importData": {
|
||||
@@ -3294,7 +3294,7 @@
|
||||
"message": "启动云订阅"
|
||||
},
|
||||
"launchCloudSubscriptionSentenceCase": {
|
||||
"message": "Launch cloud subscription"
|
||||
"message": "启动云订阅"
|
||||
},
|
||||
"storage": {
|
||||
"message": "存储"
|
||||
@@ -12433,16 +12433,16 @@
|
||||
"message": "为什么我会看到这个?"
|
||||
},
|
||||
"youHaveBitwardenPremium": {
|
||||
"message": "You have Bitwarden Premium"
|
||||
"message": "您有 Bitwarden 高级版"
|
||||
},
|
||||
"viewAndManagePremiumSubscription": {
|
||||
"message": "View and manage your Premium subscription"
|
||||
"message": "查看和管理您的高级版订阅"
|
||||
},
|
||||
"youNeedToUpdateLicenseFile": {
|
||||
"message": "You'll need to update your license file"
|
||||
"message": "您需要更新您的许可文件"
|
||||
},
|
||||
"youNeedToUpdateLicenseFileDate": {
|
||||
"message": "$DATE$.",
|
||||
"message": "$DATE$。",
|
||||
"placeholders": {
|
||||
"date": {
|
||||
"content": "$1",
|
||||
@@ -12451,16 +12451,16 @@
|
||||
}
|
||||
},
|
||||
"uploadLicenseFile": {
|
||||
"message": "Upload license file"
|
||||
"message": "上传许可证文件"
|
||||
},
|
||||
"uploadYourLicenseFile": {
|
||||
"message": "Upload your license file"
|
||||
"message": "上传您的许可证文件"
|
||||
},
|
||||
"uploadYourPremiumLicenseFile": {
|
||||
"message": "Upload your Premium license file"
|
||||
"message": "上传您的高级版许可证文件"
|
||||
},
|
||||
"uploadLicenseFileDesc": {
|
||||
"message": "Your license file name will be similar to: $FILE_NAME$",
|
||||
"message": "您的许可证文件名将类似于:$FILE_NAME$",
|
||||
"placeholders": {
|
||||
"file_name": {
|
||||
"content": "$1",
|
||||
@@ -12469,15 +12469,15 @@
|
||||
}
|
||||
},
|
||||
"alreadyHaveSubscriptionQuestion": {
|
||||
"message": "Already have a subscription?"
|
||||
"message": "已经有一个订阅?"
|
||||
},
|
||||
"alreadyHaveSubscriptionSelfHostedMessage": {
|
||||
"message": "Open the subscription page on your Bitwarden cloud account and download your license file. Then return to this screen and upload it below."
|
||||
"message": "打开您的 Bitwarden 云账户上的订阅页面并下载您的许可证文件,然后返回此屏幕并上传。"
|
||||
},
|
||||
"viewAllPlans": {
|
||||
"message": "View all plans"
|
||||
"message": "查看所有套餐"
|
||||
},
|
||||
"planDescPremium": {
|
||||
"message": "Complete online security"
|
||||
"message": "全面的在线安全防护"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1970,11 +1970,11 @@
|
||||
"message": "每個 Bitwarden 使用者帳戶的帳戶加密金鑰都不相同,因此無法將已加密匯出的檔案匯入至不同帳戶中。"
|
||||
},
|
||||
"exportNoun": {
|
||||
"message": "Export",
|
||||
"message": "匯出",
|
||||
"description": "The noun form of the word Export"
|
||||
},
|
||||
"exportVerb": {
|
||||
"message": "Export",
|
||||
"message": "匯出",
|
||||
"description": "The verb form of the word Export"
|
||||
},
|
||||
"exportFrom": {
|
||||
@@ -2303,11 +2303,11 @@
|
||||
"message": "工具"
|
||||
},
|
||||
"importNoun": {
|
||||
"message": "Import",
|
||||
"message": "匯入",
|
||||
"description": "The noun form of the word Import"
|
||||
},
|
||||
"importVerb": {
|
||||
"message": "Import",
|
||||
"message": "匯入",
|
||||
"description": "The verb form of the word Import"
|
||||
},
|
||||
"importData": {
|
||||
@@ -3294,7 +3294,7 @@
|
||||
"message": "啟動雲端訂閱"
|
||||
},
|
||||
"launchCloudSubscriptionSentenceCase": {
|
||||
"message": "Launch cloud subscription"
|
||||
"message": "啟動雲端訂閱"
|
||||
},
|
||||
"storage": {
|
||||
"message": "儲存空間"
|
||||
@@ -4212,10 +4212,10 @@
|
||||
}
|
||||
},
|
||||
"userAcceptedTransfer": {
|
||||
"message": "Accepted transfer to organization ownership."
|
||||
"message": "已接受轉移至組織擁有權。"
|
||||
},
|
||||
"userDeclinedTransfer": {
|
||||
"message": "Revoked for declining transfer to organization ownership."
|
||||
"message": "因拒絕轉移至組織擁有權而遭撤銷。"
|
||||
},
|
||||
"invitedUserId": {
|
||||
"message": "已邀請使用者 $ID$。",
|
||||
@@ -5195,7 +5195,7 @@
|
||||
"message": "需要先修正密碼庫中舊的檔案附件,然後才能輪換帳戶的加密金鑰。"
|
||||
},
|
||||
"itemsTransferred": {
|
||||
"message": "Items transferred"
|
||||
"message": "項目已轉移"
|
||||
},
|
||||
"yourAccountsFingerprint": {
|
||||
"message": "您帳戶的指紋短語",
|
||||
@@ -6825,7 +6825,7 @@
|
||||
"message": "密碼庫逾時時間不在允許的範圍內。"
|
||||
},
|
||||
"disableExport": {
|
||||
"message": "Remove export"
|
||||
"message": "移除匯出"
|
||||
},
|
||||
"disablePersonalVaultExportDescription": {
|
||||
"message": "不允許成員從其個人密碼庫匯出資料。"
|
||||
@@ -9494,7 +9494,7 @@
|
||||
"message": "需要登入 SSO"
|
||||
},
|
||||
"emailRequiredForSsoLogin": {
|
||||
"message": "Email is required for SSO"
|
||||
"message": "使用 SSO 需要電子郵件"
|
||||
},
|
||||
"selectedRegionFlag": {
|
||||
"message": "選定的區域標記"
|
||||
@@ -11607,7 +11607,7 @@
|
||||
"message": "取消封存"
|
||||
},
|
||||
"unArchiveAndSave": {
|
||||
"message": "Unarchive and save"
|
||||
"message": "取消封存並儲存"
|
||||
},
|
||||
"itemsInArchive": {
|
||||
"message": "封存中的項目"
|
||||
@@ -12251,43 +12251,43 @@
|
||||
}
|
||||
},
|
||||
"removeMasterPasswordForOrgUserKeyConnector": {
|
||||
"message": "Your organization is no longer using master passwords to log into Bitwarden. To continue, verify the organization and domain."
|
||||
"message": "您的組織已不再使用主密碼登入 Bitwarden。若要繼續,請驗證組織與網域。"
|
||||
},
|
||||
"continueWithLogIn": {
|
||||
"message": "Continue with log in"
|
||||
"message": "繼續登入"
|
||||
},
|
||||
"doNotContinue": {
|
||||
"message": "Do not continue"
|
||||
"message": "不要繼續"
|
||||
},
|
||||
"domain": {
|
||||
"message": "Domain"
|
||||
"message": "網域"
|
||||
},
|
||||
"keyConnectorDomainTooltip": {
|
||||
"message": "This domain will store your account encryption keys, so make sure you trust it. If you're not sure, check with your admin."
|
||||
"message": "此網域將儲存您帳號的加密金鑰,請確認您信任它。若不確定,請洽詢您的管理員。"
|
||||
},
|
||||
"verifyYourOrganization": {
|
||||
"message": "Verify your organization to log in"
|
||||
"message": "驗證您的組織以登入"
|
||||
},
|
||||
"organizationVerified": {
|
||||
"message": "Organization verified"
|
||||
"message": "組織已驗證"
|
||||
},
|
||||
"domainVerified": {
|
||||
"message": "Domain verified"
|
||||
"message": "已驗證網域"
|
||||
},
|
||||
"leaveOrganizationContent": {
|
||||
"message": "If you don't verify your organization, your access to the organization will be revoked."
|
||||
"message": "若您未驗證組織,將會被撤銷對該組織的存取權限。"
|
||||
},
|
||||
"leaveNow": {
|
||||
"message": "Leave now"
|
||||
"message": "立即離開"
|
||||
},
|
||||
"verifyYourDomainToLogin": {
|
||||
"message": "Verify your domain to log in"
|
||||
"message": "驗證您的網域以登入"
|
||||
},
|
||||
"verifyYourDomainDescription": {
|
||||
"message": "To continue with log in, verify this domain."
|
||||
"message": "若要繼續登入,請驗證此網域。"
|
||||
},
|
||||
"confirmKeyConnectorOrganizationUserDescription": {
|
||||
"message": "To continue with log in, verify the organization and domain."
|
||||
"message": "若要繼續登入,請驗證組織與網域。"
|
||||
},
|
||||
"confirmNoSelectedCriticalApplicationsTitle": {
|
||||
"message": "未選取任何關鍵應用程式"
|
||||
@@ -12299,52 +12299,52 @@
|
||||
"message": "使用者驗證失敗。"
|
||||
},
|
||||
"recoveryDeleteCiphersTitle": {
|
||||
"message": "Delete unrecoverable vault items"
|
||||
"message": "刪除無法復原的密碼庫項目"
|
||||
},
|
||||
"recoveryDeleteCiphersDesc": {
|
||||
"message": "Some of your vault items could not be recovered. Do you want to delete these unrecoverable items from your vault?"
|
||||
"message": "部分密碼庫項目無法復原。是否要從您的密碼庫中刪除這些無法復原的項目?"
|
||||
},
|
||||
"recoveryDeleteFoldersTitle": {
|
||||
"message": "Delete unrecoverable folders"
|
||||
"message": "刪除無法復原的資料夾"
|
||||
},
|
||||
"recoveryDeleteFoldersDesc": {
|
||||
"message": "Some of your folders could not be recovered. Do you want to delete these unrecoverable folders from your vault?"
|
||||
"message": "部分資料夾無法復原。是否要從您的密碼庫中刪除這些無法復原的資料夾?"
|
||||
},
|
||||
"recoveryReplacePrivateKeyTitle": {
|
||||
"message": "Replace encryption key"
|
||||
"message": "更換加密金鑰"
|
||||
},
|
||||
"recoveryReplacePrivateKeyDesc": {
|
||||
"message": "Your public-key encryption key pair could not be recovered. Do you want to replace your encryption key with a new key pair? This will require you to set up existing emergency-access and organization memberships again."
|
||||
"message": "您的公開金鑰加密金鑰組無法復原。是否要以新的金鑰組取代目前的加密金鑰?這將需要您重新設定現有的緊急存取與組織成員資格。"
|
||||
},
|
||||
"recoveryStepSyncTitle": {
|
||||
"message": "Synchronizing data"
|
||||
"message": "正在同步資料"
|
||||
},
|
||||
"recoveryStepPrivateKeyTitle": {
|
||||
"message": "Verifying encryption key integrity"
|
||||
"message": "正在驗證加密金鑰完整性"
|
||||
},
|
||||
"recoveryStepUserInfoTitle": {
|
||||
"message": "Verifying user information"
|
||||
"message": "正在驗證使用者資訊"
|
||||
},
|
||||
"recoveryStepCipherTitle": {
|
||||
"message": "Verifying vault item integrity"
|
||||
"message": "正在驗證密碼庫項目完整性"
|
||||
},
|
||||
"recoveryStepFoldersTitle": {
|
||||
"message": "Verifying folder integrity"
|
||||
"message": "正在驗證資料夾完整性"
|
||||
},
|
||||
"dataRecoveryTitle": {
|
||||
"message": "Data Recovery and Diagnostics"
|
||||
"message": "資料復原與診斷"
|
||||
},
|
||||
"dataRecoveryDescription": {
|
||||
"message": "Use the data recovery tool to diagnose and repair issues with your account. After running diagnostics you have the option to save diagnostic logs for support and the option to repair any detected issues."
|
||||
"message": "使用資料復原工具來診斷並修復您帳號的問題。完成診斷後,您可以選擇儲存診斷記錄以供支援使用,並修復任何偵測到的問題。"
|
||||
},
|
||||
"runDiagnostics": {
|
||||
"message": "Run Diagnostics"
|
||||
"message": "執行診斷"
|
||||
},
|
||||
"repairIssues": {
|
||||
"message": "Repair Issues"
|
||||
"message": "修復問題"
|
||||
},
|
||||
"saveDiagnosticLogs": {
|
||||
"message": "Save Diagnostic Logs"
|
||||
"message": "儲存診斷記錄"
|
||||
},
|
||||
"sessionTimeoutSettingsManagedByOrganization": {
|
||||
"message": "此設定由您的組織管理。"
|
||||
@@ -12385,16 +12385,16 @@
|
||||
"message": "設定一個解鎖方式來變更您的密碼庫逾時動作。"
|
||||
},
|
||||
"leaveConfirmationDialogTitle": {
|
||||
"message": "Are you sure you want to leave?"
|
||||
"message": "確定要離開嗎?"
|
||||
},
|
||||
"leaveConfirmationDialogContentOne": {
|
||||
"message": "By declining, your personal items will stay in your account, but you'll lose access to shared items and organization features."
|
||||
"message": "若選擇拒絕,您的個人項目將保留在帳號中,但您將失去對共用項目與組織功能的存取權。"
|
||||
},
|
||||
"leaveConfirmationDialogContentTwo": {
|
||||
"message": "Contact your admin to regain access."
|
||||
"message": "請聯絡您的管理員以重新取得存取權限。"
|
||||
},
|
||||
"leaveConfirmationDialogConfirmButton": {
|
||||
"message": "Leave $ORGANIZATION$",
|
||||
"message": "離開 $ORGANIZATION$",
|
||||
"placeholders": {
|
||||
"organization": {
|
||||
"content": "$1",
|
||||
@@ -12403,10 +12403,10 @@
|
||||
}
|
||||
},
|
||||
"howToManageMyVault": {
|
||||
"message": "How do I manage my vault?"
|
||||
"message": "我要如何管理我的密碼庫?"
|
||||
},
|
||||
"transferItemsToOrganizationTitle": {
|
||||
"message": "Transfer items to $ORGANIZATION$",
|
||||
"message": "將項目轉移至 $ORGANIZATION$",
|
||||
"placeholders": {
|
||||
"organization": {
|
||||
"content": "$1",
|
||||
@@ -12415,7 +12415,7 @@
|
||||
}
|
||||
},
|
||||
"transferItemsToOrganizationContent": {
|
||||
"message": "$ORGANIZATION$ is requiring all items to be owned by the organization for security and compliance. Click accept to transfer ownership of your items.",
|
||||
"message": "$ORGANIZATION$ 為了安全性與合規性,要求所有項目皆由組織擁有。點擊接受即可轉移您項目的擁有權。",
|
||||
"placeholders": {
|
||||
"organization": {
|
||||
"content": "$1",
|
||||
@@ -12424,25 +12424,25 @@
|
||||
}
|
||||
},
|
||||
"acceptTransfer": {
|
||||
"message": "Accept transfer"
|
||||
"message": "同意轉移"
|
||||
},
|
||||
"declineAndLeave": {
|
||||
"message": "Decline and leave"
|
||||
"message": "拒絕並離開"
|
||||
},
|
||||
"whyAmISeeingThis": {
|
||||
"message": "Why am I seeing this?"
|
||||
"message": "為什麼我會看到此訊息?"
|
||||
},
|
||||
"youHaveBitwardenPremium": {
|
||||
"message": "You have Bitwarden Premium"
|
||||
"message": "您已擁有 Bitwarden 進階版"
|
||||
},
|
||||
"viewAndManagePremiumSubscription": {
|
||||
"message": "View and manage your Premium subscription"
|
||||
"message": "檢視並管理您的進階版訂閱"
|
||||
},
|
||||
"youNeedToUpdateLicenseFile": {
|
||||
"message": "You'll need to update your license file"
|
||||
"message": "您需要更新您的授權檔案"
|
||||
},
|
||||
"youNeedToUpdateLicenseFileDate": {
|
||||
"message": "$DATE$.",
|
||||
"message": "$DATE$。",
|
||||
"placeholders": {
|
||||
"date": {
|
||||
"content": "$1",
|
||||
@@ -12451,16 +12451,16 @@
|
||||
}
|
||||
},
|
||||
"uploadLicenseFile": {
|
||||
"message": "Upload license file"
|
||||
"message": "上傳授權檔案"
|
||||
},
|
||||
"uploadYourLicenseFile": {
|
||||
"message": "Upload your license file"
|
||||
"message": "上傳您的授權檔案"
|
||||
},
|
||||
"uploadYourPremiumLicenseFile": {
|
||||
"message": "Upload your Premium license file"
|
||||
"message": "上傳您的進階版授權檔案"
|
||||
},
|
||||
"uploadLicenseFileDesc": {
|
||||
"message": "Your license file name will be similar to: $FILE_NAME$",
|
||||
"message": "您的授權檔案名稱將類似於:$FILE_NAME$",
|
||||
"placeholders": {
|
||||
"file_name": {
|
||||
"content": "$1",
|
||||
@@ -12469,15 +12469,15 @@
|
||||
}
|
||||
},
|
||||
"alreadyHaveSubscriptionQuestion": {
|
||||
"message": "Already have a subscription?"
|
||||
"message": "已經有訂閱了嗎?"
|
||||
},
|
||||
"alreadyHaveSubscriptionSelfHostedMessage": {
|
||||
"message": "Open the subscription page on your Bitwarden cloud account and download your license file. Then return to this screen and upload it below."
|
||||
"message": "請在您的 Bitwarden 雲端帳號中開啟訂閱頁面並下載授權檔案,接著返回此畫面並於下方上傳。"
|
||||
},
|
||||
"viewAllPlans": {
|
||||
"message": "View all plans"
|
||||
"message": "查看所有方案"
|
||||
},
|
||||
"planDescPremium": {
|
||||
"message": "Complete online security"
|
||||
"message": "完整的線上安全防護"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
<app-display-billing-address
|
||||
[subscriber]="view.provider"
|
||||
[billingAddress]="view.billingAddress"
|
||||
[taxIdWarning]="enableTaxIdWarning ? view.taxIdWarning : null"
|
||||
[taxIdWarning]="view.taxIdWarning"
|
||||
(updated)="setBillingAddress($event)"
|
||||
></app-display-billing-address>
|
||||
|
||||
|
||||
@@ -21,8 +21,6 @@ import { ProviderService } from "@bitwarden/common/admin-console/abstractions/pr
|
||||
import { Provider } from "@bitwarden/common/admin-console/models/domain/provider";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { CommandDefinition, MessageListener } from "@bitwarden/messaging";
|
||||
import { UserId } from "@bitwarden/user-core";
|
||||
import { SubscriberBillingClient } from "@bitwarden/web-vault/app/billing/clients";
|
||||
@@ -119,13 +117,10 @@ export class ProviderPaymentDetailsComponent implements OnInit, OnDestroy {
|
||||
|
||||
private destroy$ = new Subject<void>();
|
||||
|
||||
protected enableTaxIdWarning!: boolean;
|
||||
|
||||
constructor(
|
||||
private accountService: AccountService,
|
||||
private activatedRoute: ActivatedRoute,
|
||||
private billingClient: SubscriberBillingClient,
|
||||
private configService: ConfigService,
|
||||
private messageListener: MessageListener,
|
||||
private providerService: ProviderService,
|
||||
private providerWarningsService: ProviderWarningsService,
|
||||
@@ -133,34 +128,28 @@ export class ProviderPaymentDetailsComponent implements OnInit, OnDestroy {
|
||||
) {}
|
||||
|
||||
async ngOnInit() {
|
||||
this.enableTaxIdWarning = await this.configService.getFeatureFlag(
|
||||
FeatureFlag.PM22415_TaxIDWarnings,
|
||||
);
|
||||
|
||||
if (this.enableTaxIdWarning) {
|
||||
this.providerWarningsService.taxIdWarningRefreshed$
|
||||
.pipe(
|
||||
switchMap((warning) =>
|
||||
combineLatest([
|
||||
of(warning),
|
||||
this.provider$.pipe(take(1)).pipe(
|
||||
mapProviderToSubscriber,
|
||||
switchMap((provider) => this.subscriberBillingClient.getBillingAddress(provider)),
|
||||
),
|
||||
]),
|
||||
),
|
||||
takeUntil(this.destroy$),
|
||||
)
|
||||
.subscribe(([taxIdWarning, billingAddress]) => {
|
||||
if (this.viewState$.value) {
|
||||
this.viewState$.next({
|
||||
...this.viewState$.value,
|
||||
taxIdWarning,
|
||||
billingAddress,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
this.providerWarningsService.taxIdWarningRefreshed$
|
||||
.pipe(
|
||||
switchMap((warning) =>
|
||||
combineLatest([
|
||||
of(warning),
|
||||
this.provider$.pipe(take(1)).pipe(
|
||||
mapProviderToSubscriber,
|
||||
switchMap((provider) => this.subscriberBillingClient.getBillingAddress(provider)),
|
||||
),
|
||||
]),
|
||||
),
|
||||
takeUntil(this.destroy$),
|
||||
)
|
||||
.subscribe(([taxIdWarning, billingAddress]) => {
|
||||
if (this.viewState$.value) {
|
||||
this.viewState$.next({
|
||||
...this.viewState$.value,
|
||||
taxIdWarning,
|
||||
billingAddress,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
this.messageListener
|
||||
.messages$(BANK_ACCOUNT_VERIFIED_COMMAND)
|
||||
@@ -197,10 +186,7 @@ export class ProviderPaymentDetailsComponent implements OnInit, OnDestroy {
|
||||
|
||||
setBillingAddress = (billingAddress: BillingAddress) => {
|
||||
if (this.viewState$.value) {
|
||||
if (
|
||||
this.enableTaxIdWarning &&
|
||||
this.viewState$.value.billingAddress?.taxId !== billingAddress.taxId
|
||||
) {
|
||||
if (this.viewState$.value.billingAddress?.taxId !== billingAddress.taxId) {
|
||||
this.providerWarningsService.refreshTaxIdWarning();
|
||||
}
|
||||
this.viewState$.next({
|
||||
|
||||
@@ -19,6 +19,7 @@ import { AccountCryptographicStateService } from "@bitwarden/common/key-manageme
|
||||
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||
import { EncString } from "@bitwarden/common/key-management/crypto/models/enc-string";
|
||||
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction";
|
||||
import { MasterPasswordSalt } from "@bitwarden/common/key-management/master-password/types/master-password.types";
|
||||
import { KeysRequest } from "@bitwarden/common/models/request/keys.request";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
@@ -62,6 +63,8 @@ export class DefaultSetInitialPasswordService implements SetInitialPasswordServi
|
||||
orgSsoIdentifier,
|
||||
orgId,
|
||||
resetPasswordAutoEnroll,
|
||||
newPassword,
|
||||
salt,
|
||||
} = credentials;
|
||||
|
||||
for (const [key, value] of Object.entries(credentials)) {
|
||||
@@ -155,6 +158,20 @@ export class DefaultSetInitialPasswordService implements SetInitialPasswordServi
|
||||
userId,
|
||||
);
|
||||
|
||||
// Set master password unlock data for unlock path pointed to with
|
||||
// MasterPasswordUnlockData feature development
|
||||
// (requires: password, salt, kdf, userKey).
|
||||
// As migration to this strategy continues, both unlock paths need supported.
|
||||
// Several invocations in this file become redundant and can be removed once
|
||||
// the feature is enshrined/unwound. These are marked with [PM-23246] below.
|
||||
await this.setMasterPasswordUnlockData(
|
||||
newPassword,
|
||||
salt,
|
||||
kdfConfig,
|
||||
masterKeyEncryptedUserKey[0],
|
||||
userId,
|
||||
);
|
||||
|
||||
/**
|
||||
* Set the private key only for new JIT provisioned users in MP encryption orgs.
|
||||
* (Existing TDE users will have their private key set on sync or on login.)
|
||||
@@ -174,6 +191,7 @@ export class DefaultSetInitialPasswordService implements SetInitialPasswordServi
|
||||
);
|
||||
}
|
||||
|
||||
// [PM-23246] "Legacy" master key setting path - to be removed once unlock path migration is complete
|
||||
await this.masterPasswordService.setMasterKeyHash(newLocalMasterKeyHash, userId);
|
||||
|
||||
if (resetPasswordAutoEnroll) {
|
||||
@@ -216,10 +234,40 @@ export class DefaultSetInitialPasswordService implements SetInitialPasswordServi
|
||||
userDecryptionOpts,
|
||||
);
|
||||
await this.kdfConfigService.setKdfConfig(userId, kdfConfig);
|
||||
// [PM-23246] "Legacy" master key setting path - to be removed once unlock path migration is complete
|
||||
await this.masterPasswordService.setMasterKey(masterKey, userId);
|
||||
// [PM-23246] "Legacy" master key setting path - to be removed once unlock path migration is complete
|
||||
await this.masterPasswordService.setMasterKeyEncryptedUserKey(
|
||||
masterKeyEncryptedUserKey[1],
|
||||
userId,
|
||||
);
|
||||
await this.keyService.setUserKey(masterKeyEncryptedUserKey[0], userId);
|
||||
}
|
||||
|
||||
/**
|
||||
* As part of [PM-28494], adding this setting path to accommodate the changes that are
|
||||
* emerging with pm-23246-unlock-with-master-password-unlock-data.
|
||||
* Without this, immediately locking/unlocking the vault with the new password _may_ still fail
|
||||
* if sync has not completed. Sync will eventually set this data, but we want to ensure it's
|
||||
* set right away here to prevent a race condition UX issue that prevents immediate unlock.
|
||||
*/
|
||||
private async setMasterPasswordUnlockData(
|
||||
password: string,
|
||||
salt: MasterPasswordSalt,
|
||||
kdfConfig: KdfConfig,
|
||||
userKey: UserKey,
|
||||
userId: UserId,
|
||||
): Promise<void> {
|
||||
const masterPasswordUnlockData = await this.masterPasswordService.makeMasterPasswordUnlockData(
|
||||
password,
|
||||
kdfConfig,
|
||||
salt,
|
||||
userKey,
|
||||
);
|
||||
|
||||
await this.masterPasswordService.setMasterPasswordUnlockData(masterPasswordUnlockData, userId);
|
||||
}
|
||||
|
||||
private async handleResetPasswordAutoEnroll(
|
||||
masterKeyHash: string,
|
||||
orgId: string,
|
||||
|
||||
@@ -134,6 +134,8 @@ describe("DefaultSetInitialPasswordService", () => {
|
||||
orgSsoIdentifier: "orgSsoIdentifier",
|
||||
orgId: "orgId",
|
||||
resetPasswordAutoEnroll: false,
|
||||
newPassword: "Test@Password123!",
|
||||
salt: "user@example.com" as any,
|
||||
};
|
||||
userType = SetInitialPasswordUserType.JIT_PROVISIONED_MP_ORG_USER;
|
||||
|
||||
@@ -226,6 +228,8 @@ describe("DefaultSetInitialPasswordService", () => {
|
||||
"orgSsoIdentifier",
|
||||
"orgId",
|
||||
"resetPasswordAutoEnroll",
|
||||
"newPassword",
|
||||
"salt",
|
||||
].forEach((key) => {
|
||||
it(`should throw if ${key} is not provided on the SetInitialPasswordCredentials object`, async () => {
|
||||
// Arrange
|
||||
@@ -357,6 +361,10 @@ describe("DefaultSetInitialPasswordService", () => {
|
||||
ForceSetPasswordReason.None,
|
||||
userId,
|
||||
);
|
||||
expect(masterPasswordService.setMasterKeyEncryptedUserKey).toHaveBeenCalledWith(
|
||||
masterKeyEncryptedUserKey[1],
|
||||
userId,
|
||||
);
|
||||
});
|
||||
|
||||
it("should update account decryption properties", async () => {
|
||||
@@ -417,6 +425,36 @@ describe("DefaultSetInitialPasswordService", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("should create and set master password unlock data to prevent race condition with sync", async () => {
|
||||
// Arrange
|
||||
setupMocks();
|
||||
|
||||
const mockUnlockData = {
|
||||
salt: credentials.salt,
|
||||
kdf: credentials.kdfConfig,
|
||||
masterKeyWrappedUserKey: "wrapped_key_string",
|
||||
};
|
||||
|
||||
masterPasswordService.makeMasterPasswordUnlockData.mockResolvedValue(
|
||||
mockUnlockData as any,
|
||||
);
|
||||
|
||||
// Act
|
||||
await sut.setInitialPassword(credentials, userType, userId);
|
||||
|
||||
// Assert
|
||||
expect(masterPasswordService.makeMasterPasswordUnlockData).toHaveBeenCalledWith(
|
||||
credentials.newPassword,
|
||||
credentials.kdfConfig,
|
||||
credentials.salt,
|
||||
masterKeyEncryptedUserKey[0],
|
||||
);
|
||||
expect(masterPasswordService.setMasterPasswordUnlockData).toHaveBeenCalledWith(
|
||||
mockUnlockData,
|
||||
userId,
|
||||
);
|
||||
});
|
||||
|
||||
describe("given resetPasswordAutoEnroll is true", () => {
|
||||
it(`should handle reset password (account recovery) auto enroll`, async () => {
|
||||
// Arrange
|
||||
@@ -586,6 +624,10 @@ describe("DefaultSetInitialPasswordService", () => {
|
||||
credentials.newMasterKey,
|
||||
userId,
|
||||
);
|
||||
expect(masterPasswordService.setMasterKeyEncryptedUserKey).toHaveBeenCalledWith(
|
||||
masterKeyEncryptedUserKey[1],
|
||||
userId,
|
||||
);
|
||||
expect(keyService.setUserKey).toHaveBeenCalledWith(masterKeyEncryptedUserKey[0], userId);
|
||||
});
|
||||
|
||||
@@ -616,6 +658,36 @@ describe("DefaultSetInitialPasswordService", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("should create and set master password unlock data to prevent race condition with sync", async () => {
|
||||
// Arrange
|
||||
setupMocks({ ...defaultMockConfig, userType });
|
||||
|
||||
const mockUnlockData = {
|
||||
salt: credentials.salt,
|
||||
kdf: credentials.kdfConfig,
|
||||
masterKeyWrappedUserKey: "wrapped_key_string",
|
||||
};
|
||||
|
||||
masterPasswordService.makeMasterPasswordUnlockData.mockResolvedValue(
|
||||
mockUnlockData as any,
|
||||
);
|
||||
|
||||
// Act
|
||||
await sut.setInitialPassword(credentials, userType, userId);
|
||||
|
||||
// Assert
|
||||
expect(masterPasswordService.makeMasterPasswordUnlockData).toHaveBeenCalledWith(
|
||||
credentials.newPassword,
|
||||
credentials.kdfConfig,
|
||||
credentials.salt,
|
||||
masterKeyEncryptedUserKey[0],
|
||||
);
|
||||
expect(masterPasswordService.setMasterPasswordUnlockData).toHaveBeenCalledWith(
|
||||
mockUnlockData,
|
||||
userId,
|
||||
);
|
||||
});
|
||||
|
||||
describe("given resetPasswordAutoEnroll is true", () => {
|
||||
it(`should handle reset password (account recovery) auto enroll`, async () => {
|
||||
// Arrange
|
||||
|
||||
@@ -214,6 +214,8 @@ export class SetInitialPasswordComponent implements OnInit {
|
||||
assertTruthy(passwordInputResult.newServerMasterKeyHash, "newServerMasterKeyHash", ctx);
|
||||
assertTruthy(passwordInputResult.newLocalMasterKeyHash, "newLocalMasterKeyHash", ctx);
|
||||
assertTruthy(passwordInputResult.kdfConfig, "kdfConfig", ctx);
|
||||
assertTruthy(passwordInputResult.newPassword, "newPassword", ctx);
|
||||
assertTruthy(passwordInputResult.salt, "salt", ctx);
|
||||
assertTruthy(this.orgSsoIdentifier, "orgSsoIdentifier", ctx);
|
||||
assertTruthy(this.orgId, "orgId", ctx);
|
||||
assertTruthy(this.userType, "userType", ctx);
|
||||
@@ -231,6 +233,8 @@ export class SetInitialPasswordComponent implements OnInit {
|
||||
orgSsoIdentifier: this.orgSsoIdentifier,
|
||||
orgId: this.orgId,
|
||||
resetPasswordAutoEnroll: this.resetPasswordAutoEnroll,
|
||||
newPassword: passwordInputResult.newPassword,
|
||||
salt: passwordInputResult.salt,
|
||||
};
|
||||
|
||||
await this.setInitialPasswordService.setInitialPassword(
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { MasterPasswordSalt } from "@bitwarden/common/key-management/master-password/types/master-password.types";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { MasterKey } from "@bitwarden/common/types/key";
|
||||
import { KdfConfig } from "@bitwarden/key-management";
|
||||
@@ -50,6 +51,8 @@ export interface SetInitialPasswordCredentials {
|
||||
orgSsoIdentifier: string;
|
||||
orgId: string;
|
||||
resetPasswordAutoEnroll: boolean;
|
||||
newPassword: string;
|
||||
salt: MasterPasswordSalt;
|
||||
}
|
||||
|
||||
export interface SetInitialPasswordTdeOffboardingCredentials {
|
||||
|
||||
@@ -0,0 +1,377 @@
|
||||
// Mock asUuid to return the input value for test consistency
|
||||
jest.mock("@bitwarden/common/platform/abstractions/sdk/sdk.service", () => ({
|
||||
asUuid: (x: any) => x,
|
||||
}));
|
||||
|
||||
import { DestroyRef } from "@angular/core";
|
||||
import { FormBuilder } from "@angular/forms";
|
||||
import { Router } from "@angular/router";
|
||||
import { mock, MockProxy } from "jest-mock-extended";
|
||||
import { BehaviorSubject, of } from "rxjs";
|
||||
|
||||
import {
|
||||
LoginEmailServiceAbstraction,
|
||||
LogoutService,
|
||||
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 { 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 { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { DeviceTrustServiceAbstraction } from "@bitwarden/common/key-management/device-trust/abstractions/device-trust.service.abstraction";
|
||||
import { SecurityStateService } from "@bitwarden/common/key-management/security-state/abstractions/security-state.service";
|
||||
import { SignedSecurityState } from "@bitwarden/common/key-management/types";
|
||||
import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
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 { RegisterSdkService } from "@bitwarden/common/platform/abstractions/sdk/register-sdk.service";
|
||||
import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service";
|
||||
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { AnonLayoutWrapperDataService, DialogService, ToastService } from "@bitwarden/components";
|
||||
import { KeyService } from "@bitwarden/key-management";
|
||||
|
||||
import { LoginDecryptionOptionsComponent } from "./login-decryption-options.component";
|
||||
import { LoginDecryptionOptionsService } from "./login-decryption-options.service";
|
||||
|
||||
describe("LoginDecryptionOptionsComponent", () => {
|
||||
let component: LoginDecryptionOptionsComponent;
|
||||
let accountService: MockProxy<AccountService>;
|
||||
let anonLayoutWrapperDataService: MockProxy<AnonLayoutWrapperDataService>;
|
||||
let apiService: MockProxy<ApiService>;
|
||||
let destroyRef: MockProxy<DestroyRef>;
|
||||
let deviceTrustService: MockProxy<DeviceTrustServiceAbstraction>;
|
||||
let dialogService: MockProxy<DialogService>;
|
||||
let formBuilder: FormBuilder;
|
||||
let i18nService: MockProxy<I18nService>;
|
||||
let keyService: MockProxy<KeyService>;
|
||||
let loginDecryptionOptionsService: MockProxy<LoginDecryptionOptionsService>;
|
||||
let loginEmailService: MockProxy<LoginEmailServiceAbstraction>;
|
||||
let messagingService: MockProxy<MessagingService>;
|
||||
let organizationApiService: MockProxy<OrganizationApiServiceAbstraction>;
|
||||
let passwordResetEnrollmentService: MockProxy<PasswordResetEnrollmentServiceAbstraction>;
|
||||
let platformUtilsService: MockProxy<PlatformUtilsService>;
|
||||
let router: MockProxy<Router>;
|
||||
let ssoLoginService: MockProxy<SsoLoginServiceAbstraction>;
|
||||
let toastService: MockProxy<ToastService>;
|
||||
let userDecryptionOptionsService: MockProxy<UserDecryptionOptionsServiceAbstraction>;
|
||||
let validationService: MockProxy<ValidationService>;
|
||||
let logoutService: MockProxy<LogoutService>;
|
||||
let registerSdkService: MockProxy<RegisterSdkService>;
|
||||
let securityStateService: MockProxy<SecurityStateService>;
|
||||
let appIdService: MockProxy<AppIdService>;
|
||||
let configService: MockProxy<ConfigService>;
|
||||
let accountCryptographicStateService: MockProxy<any>;
|
||||
|
||||
const mockUserId = "user-id-123" as UserId;
|
||||
const mockEmail = "test@example.com";
|
||||
const mockOrgId = "org-id-456";
|
||||
|
||||
beforeEach(() => {
|
||||
accountService = mock<AccountService>();
|
||||
anonLayoutWrapperDataService = mock<AnonLayoutWrapperDataService>();
|
||||
apiService = mock<ApiService>();
|
||||
destroyRef = mock<DestroyRef>();
|
||||
deviceTrustService = mock<DeviceTrustServiceAbstraction>();
|
||||
dialogService = mock<DialogService>();
|
||||
formBuilder = new FormBuilder();
|
||||
i18nService = mock<I18nService>();
|
||||
keyService = mock<KeyService>();
|
||||
loginDecryptionOptionsService = mock<LoginDecryptionOptionsService>();
|
||||
loginEmailService = mock<LoginEmailServiceAbstraction>();
|
||||
messagingService = mock<MessagingService>();
|
||||
organizationApiService = mock<OrganizationApiServiceAbstraction>();
|
||||
passwordResetEnrollmentService = mock<PasswordResetEnrollmentServiceAbstraction>();
|
||||
platformUtilsService = mock<PlatformUtilsService>();
|
||||
router = mock<Router>();
|
||||
ssoLoginService = mock<SsoLoginServiceAbstraction>();
|
||||
toastService = mock<ToastService>();
|
||||
userDecryptionOptionsService = mock<UserDecryptionOptionsServiceAbstraction>();
|
||||
validationService = mock<ValidationService>();
|
||||
logoutService = mock<LogoutService>();
|
||||
registerSdkService = mock<RegisterSdkService>();
|
||||
securityStateService = mock<SecurityStateService>();
|
||||
appIdService = mock<AppIdService>();
|
||||
configService = mock<ConfigService>();
|
||||
accountCryptographicStateService = mock();
|
||||
|
||||
// Setup default mocks
|
||||
accountService.activeAccount$ = new BehaviorSubject({
|
||||
id: mockUserId,
|
||||
email: mockEmail,
|
||||
name: "Test User",
|
||||
emailVerified: true,
|
||||
creationDate: new Date().toISOString(),
|
||||
});
|
||||
platformUtilsService.getClientType.mockReturnValue(ClientType.Browser);
|
||||
deviceTrustService.getShouldTrustDevice.mockResolvedValue(true);
|
||||
i18nService.t.mockImplementation((key: string) => key);
|
||||
|
||||
component = new LoginDecryptionOptionsComponent(
|
||||
accountService,
|
||||
anonLayoutWrapperDataService,
|
||||
apiService,
|
||||
destroyRef,
|
||||
deviceTrustService,
|
||||
dialogService,
|
||||
formBuilder,
|
||||
i18nService,
|
||||
keyService,
|
||||
loginDecryptionOptionsService,
|
||||
loginEmailService,
|
||||
messagingService,
|
||||
organizationApiService,
|
||||
passwordResetEnrollmentService,
|
||||
platformUtilsService,
|
||||
router,
|
||||
ssoLoginService,
|
||||
toastService,
|
||||
userDecryptionOptionsService,
|
||||
validationService,
|
||||
logoutService,
|
||||
registerSdkService,
|
||||
securityStateService,
|
||||
appIdService,
|
||||
configService,
|
||||
accountCryptographicStateService,
|
||||
);
|
||||
});
|
||||
|
||||
describe("createUser with feature flag enabled", () => {
|
||||
let mockPostKeysForTdeRegistration: jest.Mock;
|
||||
let mockRegistration: any;
|
||||
let mockAuth: any;
|
||||
let mockSdkValue: any;
|
||||
let mockSdkRef: any;
|
||||
let mockSdk: any;
|
||||
let mockDeviceKey: string;
|
||||
let mockDeviceKeyObj: SymmetricCryptoKey;
|
||||
let mockUserKeyBytes: Uint8Array;
|
||||
let mockPrivateKey: string;
|
||||
let mockSignedPublicKey: string;
|
||||
let mockSigningKey: string;
|
||||
let mockSecurityState: SignedSecurityState;
|
||||
|
||||
beforeEach(async () => {
|
||||
// Mock asUuid to return the input value for test consistency
|
||||
jest.mock("@bitwarden/common/platform/abstractions/sdk/sdk.service", () => ({
|
||||
asUuid: (x: any) => x,
|
||||
}));
|
||||
(Symbol as any).dispose = Symbol("dispose");
|
||||
|
||||
mockPrivateKey = "mock-private-key";
|
||||
mockSignedPublicKey = "mock-signed-public-key";
|
||||
mockSigningKey = "mock-signing-key";
|
||||
mockSecurityState = {
|
||||
signature: "mock-signature",
|
||||
payload: {
|
||||
version: 2,
|
||||
timestamp: Date.now(),
|
||||
privateKeyHash: "mock-hash",
|
||||
},
|
||||
} as any;
|
||||
const deviceKeyBytes = new Uint8Array(32).fill(5);
|
||||
mockDeviceKey = Buffer.from(deviceKeyBytes).toString("base64");
|
||||
mockDeviceKeyObj = SymmetricCryptoKey.fromString(mockDeviceKey);
|
||||
mockUserKeyBytes = new Uint8Array(64);
|
||||
|
||||
mockPostKeysForTdeRegistration = jest.fn().mockResolvedValue({
|
||||
account_cryptographic_state: {
|
||||
V2: {
|
||||
private_key: mockPrivateKey,
|
||||
signed_public_key: mockSignedPublicKey,
|
||||
signing_key: mockSigningKey,
|
||||
security_state: mockSecurityState,
|
||||
},
|
||||
},
|
||||
device_key: mockDeviceKey,
|
||||
user_key: mockUserKeyBytes,
|
||||
});
|
||||
|
||||
mockRegistration = {
|
||||
post_keys_for_tde_registration: mockPostKeysForTdeRegistration,
|
||||
};
|
||||
|
||||
mockAuth = {
|
||||
registration: jest.fn().mockReturnValue(mockRegistration),
|
||||
};
|
||||
|
||||
mockSdkValue = {
|
||||
auth: jest.fn().mockReturnValue(mockAuth),
|
||||
};
|
||||
|
||||
mockSdkRef = {
|
||||
value: mockSdkValue,
|
||||
[Symbol.dispose]: jest.fn(),
|
||||
};
|
||||
|
||||
mockSdk = {
|
||||
take: jest.fn().mockReturnValue(mockSdkRef),
|
||||
};
|
||||
|
||||
registerSdkService.registerClient$ = jest.fn((userId: UserId) => of(mockSdk)) as any;
|
||||
|
||||
// Setup for new user state
|
||||
userDecryptionOptionsService.userDecryptionOptionsById$.mockReturnValue(
|
||||
of({
|
||||
trustedDeviceOption: {
|
||||
hasAdminApproval: false,
|
||||
hasLoginApprovingDevice: false,
|
||||
hasManageResetPasswordPermission: false,
|
||||
isTdeOffboarding: false,
|
||||
},
|
||||
hasMasterPassword: false,
|
||||
keyConnectorOption: undefined,
|
||||
}),
|
||||
);
|
||||
|
||||
ssoLoginService.getActiveUserOrganizationSsoIdentifier.mockResolvedValue("org-identifier");
|
||||
organizationApiService.getAutoEnrollStatus.mockResolvedValue({
|
||||
id: mockOrgId,
|
||||
resetPasswordEnabled: true,
|
||||
} as any);
|
||||
|
||||
// Initialize component to set up new user state
|
||||
await component.ngOnInit();
|
||||
});
|
||||
|
||||
it("should use SDK v2 registration when feature flag is enabled", async () => {
|
||||
// Arrange
|
||||
configService.getFeatureFlag.mockResolvedValue(true);
|
||||
loginDecryptionOptionsService.handleCreateUserSuccess.mockResolvedValue(undefined);
|
||||
router.navigate.mockResolvedValue(true);
|
||||
appIdService.getAppId.mockResolvedValue("mock-app-id");
|
||||
organizationApiService.getKeys.mockResolvedValue({
|
||||
publicKey: "mock-org-public-key",
|
||||
privateKey: "mock-org-private-key",
|
||||
} as any);
|
||||
|
||||
// Act
|
||||
await component["createUser"]();
|
||||
|
||||
// Assert
|
||||
expect(configService.getFeatureFlag).toHaveBeenCalledWith(
|
||||
FeatureFlag.PM27279_V2RegistrationTdeJit,
|
||||
);
|
||||
expect(appIdService.getAppId).toHaveBeenCalled();
|
||||
expect(organizationApiService.getKeys).toHaveBeenCalledWith(mockOrgId);
|
||||
expect(registerSdkService.registerClient$).toHaveBeenCalledWith(mockUserId);
|
||||
|
||||
// Verify SDK registration was called with correct parameters
|
||||
expect(mockSdkValue.auth).toHaveBeenCalled();
|
||||
expect(mockAuth.registration).toHaveBeenCalled();
|
||||
expect(mockPostKeysForTdeRegistration).toHaveBeenCalledWith({
|
||||
org_id: mockOrgId,
|
||||
org_public_key: "mock-org-public-key",
|
||||
user_id: mockUserId,
|
||||
device_identifier: "mock-app-id",
|
||||
trust_device: true,
|
||||
});
|
||||
|
||||
const expectedDeviceKey = mockDeviceKeyObj;
|
||||
const expectedUserKey = new SymmetricCryptoKey(new Uint8Array(mockUserKeyBytes));
|
||||
|
||||
// Verify keys were set
|
||||
expect(keyService.setPrivateKey).toHaveBeenCalledWith(mockPrivateKey, mockUserId);
|
||||
expect(keyService.setSignedPublicKey).toHaveBeenCalledWith(mockSignedPublicKey, mockUserId);
|
||||
expect(keyService.setUserSigningKey).toHaveBeenCalledWith(mockSigningKey, mockUserId);
|
||||
expect(securityStateService.setAccountSecurityState).toHaveBeenCalledWith(
|
||||
mockSecurityState,
|
||||
mockUserId,
|
||||
);
|
||||
expect(accountCryptographicStateService.setAccountCryptographicState).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
V2: {
|
||||
private_key: mockPrivateKey,
|
||||
signed_public_key: mockSignedPublicKey,
|
||||
signing_key: mockSigningKey,
|
||||
security_state: mockSecurityState,
|
||||
},
|
||||
}),
|
||||
mockUserId,
|
||||
);
|
||||
|
||||
expect(validationService.showError).not.toHaveBeenCalled();
|
||||
|
||||
// Verify device and user keys were persisted
|
||||
expect(deviceTrustService.setDeviceKey).toHaveBeenCalledWith(
|
||||
mockUserId,
|
||||
expect.any(SymmetricCryptoKey),
|
||||
);
|
||||
expect(keyService.setUserKey).toHaveBeenCalledWith(
|
||||
expect.any(SymmetricCryptoKey),
|
||||
mockUserId,
|
||||
);
|
||||
|
||||
const [, deviceKeyArg] = deviceTrustService.setDeviceKey.mock.calls[0];
|
||||
const [userKeyArg] = keyService.setUserKey.mock.calls[0];
|
||||
|
||||
expect((deviceKeyArg as SymmetricCryptoKey).keyB64).toBe(expectedDeviceKey.keyB64);
|
||||
expect((userKeyArg as SymmetricCryptoKey).keyB64).toBe(expectedUserKey.keyB64);
|
||||
|
||||
// Verify success toast and navigation
|
||||
expect(toastService.showToast).toHaveBeenCalledWith({
|
||||
variant: "success",
|
||||
title: null,
|
||||
message: "accountSuccessfullyCreated",
|
||||
});
|
||||
expect(loginDecryptionOptionsService.handleCreateUserSuccess).toHaveBeenCalled();
|
||||
expect(router.navigate).toHaveBeenCalledWith(["/tabs/vault"]);
|
||||
});
|
||||
|
||||
it("should use legacy registration when feature flag is disabled", async () => {
|
||||
// Arrange
|
||||
configService.getFeatureFlag.mockResolvedValue(false);
|
||||
|
||||
const mockPublicKey = "mock-public-key";
|
||||
const mockPrivateKey = {
|
||||
encryptedString: "mock-encrypted-private-key",
|
||||
} as any;
|
||||
|
||||
keyService.initAccount.mockResolvedValue({
|
||||
publicKey: mockPublicKey,
|
||||
privateKey: mockPrivateKey,
|
||||
} as any);
|
||||
|
||||
apiService.postAccountKeys.mockResolvedValue(undefined);
|
||||
passwordResetEnrollmentService.enroll.mockResolvedValue(undefined);
|
||||
deviceTrustService.trustDevice.mockResolvedValue(undefined);
|
||||
loginDecryptionOptionsService.handleCreateUserSuccess.mockResolvedValue(undefined);
|
||||
router.navigate.mockResolvedValue(true);
|
||||
|
||||
// Act
|
||||
await component["createUser"]();
|
||||
|
||||
// Assert
|
||||
expect(configService.getFeatureFlag).toHaveBeenCalledWith(
|
||||
FeatureFlag.PM27279_V2RegistrationTdeJit,
|
||||
);
|
||||
expect(keyService.initAccount).toHaveBeenCalledWith(mockUserId);
|
||||
expect(apiService.postAccountKeys).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
publicKey: mockPublicKey,
|
||||
encryptedPrivateKey: mockPrivateKey.encryptedString,
|
||||
}),
|
||||
);
|
||||
expect(passwordResetEnrollmentService.enroll).toHaveBeenCalledWith(mockOrgId);
|
||||
expect(deviceTrustService.trustDevice).toHaveBeenCalledWith(mockUserId);
|
||||
|
||||
// Verify success toast
|
||||
expect(toastService.showToast).toHaveBeenCalledWith({
|
||||
variant: "success",
|
||||
title: null,
|
||||
message: "accountSuccessfullyCreated",
|
||||
});
|
||||
|
||||
// Verify navigation
|
||||
expect(loginDecryptionOptionsService.handleCreateUserSuccess).toHaveBeenCalled();
|
||||
expect(router.navigate).toHaveBeenCalledWith(["/tabs/vault"]);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -5,7 +5,17 @@ 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 {
|
||||
catchError,
|
||||
concatMap,
|
||||
defer,
|
||||
firstValueFrom,
|
||||
from,
|
||||
map,
|
||||
of,
|
||||
switchMap,
|
||||
throwError,
|
||||
} from "rxjs";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import {
|
||||
@@ -20,13 +30,27 @@ import { AccountService } from "@bitwarden/common/auth/abstractions/account.serv
|
||||
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 { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { AccountCryptographicStateService } from "@bitwarden/common/key-management/account-cryptography/account-cryptographic-state.service";
|
||||
import { DeviceTrustServiceAbstraction } from "@bitwarden/common/key-management/device-trust/abstractions/device-trust.service.abstraction";
|
||||
import { SecurityStateService } from "@bitwarden/common/key-management/security-state/abstractions/security-state.service";
|
||||
import {
|
||||
SignedPublicKey,
|
||||
SignedSecurityState,
|
||||
WrappedSigningKey,
|
||||
} from "@bitwarden/common/key-management/types";
|
||||
import { KeysRequest } from "@bitwarden/common/models/request/keys.request";
|
||||
import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
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 { RegisterSdkService } from "@bitwarden/common/platform/abstractions/sdk/register-sdk.service";
|
||||
import { asUuid } from "@bitwarden/common/platform/abstractions/sdk/sdk.service";
|
||||
import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service";
|
||||
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { DeviceKey, UserKey } from "@bitwarden/common/types/key";
|
||||
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop.
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import {
|
||||
@@ -40,6 +64,7 @@ import {
|
||||
TypographyModule,
|
||||
} from "@bitwarden/components";
|
||||
import { KeyService } from "@bitwarden/key-management";
|
||||
import { OrganizationId as SdkOrganizationId, UserId as SdkUserId } from "@bitwarden/sdk-internal";
|
||||
|
||||
import { LoginDecryptionOptionsService } from "./login-decryption-options.service";
|
||||
|
||||
@@ -112,6 +137,11 @@ export class LoginDecryptionOptionsComponent implements OnInit {
|
||||
private userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction,
|
||||
private validationService: ValidationService,
|
||||
private logoutService: LogoutService,
|
||||
private registerSdkService: RegisterSdkService,
|
||||
private securityStateService: SecurityStateService,
|
||||
private appIdService: AppIdService,
|
||||
private configService: ConfigService,
|
||||
private accountCryptographicStateService: AccountCryptographicStateService,
|
||||
) {
|
||||
this.clientType = this.platformUtilsService.getClientType();
|
||||
}
|
||||
@@ -251,9 +281,85 @@ export class LoginDecryptionOptionsComponent implements OnInit {
|
||||
}
|
||||
|
||||
try {
|
||||
const { publicKey, privateKey } = await this.keyService.initAccount(this.activeAccountId);
|
||||
const keysRequest = new KeysRequest(publicKey, privateKey.encryptedString);
|
||||
await this.apiService.postAccountKeys(keysRequest);
|
||||
const useSdkV2Creation = await this.configService.getFeatureFlag(
|
||||
FeatureFlag.PM27279_V2RegistrationTdeJit,
|
||||
);
|
||||
if (useSdkV2Creation) {
|
||||
const deviceIdentifier = await this.appIdService.getAppId();
|
||||
const userId = this.activeAccountId;
|
||||
const organizationId = this.newUserOrgId;
|
||||
|
||||
const orgKeyResponse = await this.organizationApiService.getKeys(organizationId);
|
||||
const register_result = await firstValueFrom(
|
||||
this.registerSdkService.registerClient$(userId).pipe(
|
||||
concatMap(async (sdk) => {
|
||||
if (!sdk) {
|
||||
throw new Error("SDK not available");
|
||||
}
|
||||
|
||||
using ref = sdk.take();
|
||||
return await ref.value
|
||||
.auth()
|
||||
.registration()
|
||||
.post_keys_for_tde_registration({
|
||||
org_id: asUuid<SdkOrganizationId>(organizationId),
|
||||
org_public_key: orgKeyResponse.publicKey,
|
||||
user_id: asUuid<SdkUserId>(userId),
|
||||
device_identifier: deviceIdentifier,
|
||||
trust_device: this.formGroup.value.rememberDevice,
|
||||
});
|
||||
}),
|
||||
),
|
||||
);
|
||||
// The keys returned here can only be v2 keys, since the SDK only implements returning V2 keys.
|
||||
if ("V1" in register_result.account_cryptographic_state) {
|
||||
throw new Error("Unexpected V1 account cryptographic state");
|
||||
}
|
||||
|
||||
// Note: When SDK state management matures, these should be moved into post_keys_for_tde_registration
|
||||
// Set account cryptography state
|
||||
await this.accountCryptographicStateService.setAccountCryptographicState(
|
||||
register_result.account_cryptographic_state,
|
||||
userId,
|
||||
);
|
||||
// Legacy individual states
|
||||
await this.keyService.setPrivateKey(
|
||||
register_result.account_cryptographic_state.V2.private_key,
|
||||
userId,
|
||||
);
|
||||
await this.keyService.setSignedPublicKey(
|
||||
register_result.account_cryptographic_state.V2.signed_public_key as SignedPublicKey,
|
||||
userId,
|
||||
);
|
||||
await this.keyService.setUserSigningKey(
|
||||
register_result.account_cryptographic_state.V2.signing_key as WrappedSigningKey,
|
||||
userId,
|
||||
);
|
||||
await this.securityStateService.setAccountSecurityState(
|
||||
register_result.account_cryptographic_state.V2.security_state as SignedSecurityState,
|
||||
userId,
|
||||
);
|
||||
|
||||
// TDE unlock
|
||||
await this.deviceTrustService.setDeviceKey(
|
||||
userId,
|
||||
SymmetricCryptoKey.fromString(register_result.device_key) as DeviceKey,
|
||||
);
|
||||
|
||||
// Set user key - user is now unlocked
|
||||
await this.keyService.setUserKey(
|
||||
SymmetricCryptoKey.fromString(register_result.user_key) as UserKey,
|
||||
userId,
|
||||
);
|
||||
} else {
|
||||
const { publicKey, privateKey } = await this.keyService.initAccount(this.activeAccountId);
|
||||
const keysRequest = new KeysRequest(publicKey, privateKey.encryptedString);
|
||||
await this.apiService.postAccountKeys(keysRequest);
|
||||
await this.passwordResetEnrollmentService.enroll(this.newUserOrgId);
|
||||
if (this.formGroup.value.rememberDevice) {
|
||||
await this.deviceTrustService.trustDevice(this.activeAccountId);
|
||||
}
|
||||
}
|
||||
|
||||
this.toastService.showToast({
|
||||
variant: "success",
|
||||
@@ -261,12 +367,6 @@ export class LoginDecryptionOptionsComponent implements OnInit {
|
||||
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) {
|
||||
|
||||
@@ -19,9 +19,21 @@ export class IdentityTokenResponse extends BaseResponse {
|
||||
tokenType: string;
|
||||
|
||||
// Decryption Information
|
||||
privateKey: string; // userKeyEncryptedPrivateKey
|
||||
|
||||
/**
|
||||
* privateKey is actually userKeyEncryptedPrivateKey
|
||||
* @deprecated Use {@link accountKeysResponseModel} instead
|
||||
*/
|
||||
privateKey: string;
|
||||
|
||||
// TODO: https://bitwarden.atlassian.net/browse/PM-30124 - Rename to just accountKeys
|
||||
accountKeysResponseModel: PrivateKeysResponseModel | null = null;
|
||||
key?: EncString; // masterKeyEncryptedUserKey
|
||||
|
||||
/**
|
||||
* key is actually masterKeyEncryptedUserKey
|
||||
* @deprecated Use {@link userDecryptionOptions.masterPasswordUnlock.masterKeyWrappedUserKey} instead
|
||||
*/
|
||||
key?: EncString;
|
||||
twoFactorToken: string;
|
||||
kdfConfig: KdfConfig;
|
||||
forcePasswordReset: boolean;
|
||||
|
||||
@@ -26,7 +26,6 @@ export enum FeatureFlag {
|
||||
|
||||
/* Billing */
|
||||
TrialPaymentOptional = "PM-8163-trial-payment",
|
||||
PM22415_TaxIDWarnings = "pm-22415-tax-id-warnings",
|
||||
PM24032_NewNavigationPremiumUpgradeButton = "pm-24032-new-navigation-premium-upgrade-button",
|
||||
PM25379_UseNewOrganizationMetadataStructure = "pm-25379-use-new-organization-metadata-structure",
|
||||
PM24996_ImplementUpgradeFromFreeDialog = "pm-24996-implement-upgrade-from-free-dialog",
|
||||
@@ -45,6 +44,7 @@ export enum FeatureFlag {
|
||||
NoLogoutOnKdfChange = "pm-23995-no-logout-on-kdf-change",
|
||||
DataRecoveryTool = "pm-28813-data-recovery-tool",
|
||||
ConsolidatedSessionTimeoutComponent = "pm-26056-consolidated-session-timeout-component",
|
||||
PM27279_V2RegistrationTdeJit = "pm-27279-v2-registration-tde-jit",
|
||||
|
||||
/* Tools */
|
||||
DesktopSendUIRefresh = "desktop-send-ui-refresh",
|
||||
@@ -63,7 +63,6 @@ export enum FeatureFlag {
|
||||
PM22134SdkCipherListView = "pm-22134-sdk-cipher-list-view",
|
||||
PM22136_SdkCipherEncryption = "pm-22136-sdk-cipher-encryption",
|
||||
CipherKeyEncryption = "cipher-key-encryption",
|
||||
AutofillConfirmation = "pm-25083-autofill-confirm-from-search",
|
||||
RiskInsightsForPremium = "pm-23904-risk-insights-for-premium",
|
||||
VaultLoadingSkeletons = "pm-25081-vault-skeleton-loaders",
|
||||
BrowserPremiumSpotlight = "pm-23384-browser-premium-spotlight",
|
||||
@@ -126,7 +125,6 @@ export const DefaultFeatureFlagValue = {
|
||||
[FeatureFlag.PM19941MigrateCipherDomainToSdk]: FALSE,
|
||||
[FeatureFlag.PM22134SdkCipherListView]: FALSE,
|
||||
[FeatureFlag.PM22136_SdkCipherEncryption]: FALSE,
|
||||
[FeatureFlag.AutofillConfirmation]: FALSE,
|
||||
[FeatureFlag.RiskInsightsForPremium]: FALSE,
|
||||
[FeatureFlag.VaultLoadingSkeletons]: FALSE,
|
||||
[FeatureFlag.BrowserPremiumSpotlight]: FALSE,
|
||||
@@ -137,7 +135,6 @@ export const DefaultFeatureFlagValue = {
|
||||
|
||||
/* Billing */
|
||||
[FeatureFlag.TrialPaymentOptional]: FALSE,
|
||||
[FeatureFlag.PM22415_TaxIDWarnings]: FALSE,
|
||||
[FeatureFlag.PM24032_NewNavigationPremiumUpgradeButton]: FALSE,
|
||||
[FeatureFlag.PM25379_UseNewOrganizationMetadataStructure]: FALSE,
|
||||
[FeatureFlag.PM24996_ImplementUpgradeFromFreeDialog]: FALSE,
|
||||
@@ -156,6 +153,7 @@ export const DefaultFeatureFlagValue = {
|
||||
[FeatureFlag.NoLogoutOnKdfChange]: FALSE,
|
||||
[FeatureFlag.DataRecoveryTool]: FALSE,
|
||||
[FeatureFlag.ConsolidatedSessionTimeoutComponent]: FALSE,
|
||||
[FeatureFlag.PM27279_V2RegistrationTdeJit]: FALSE,
|
||||
|
||||
/* Platform */
|
||||
[FeatureFlag.IpcChannelFramework]: FALSE,
|
||||
|
||||
@@ -39,6 +39,7 @@ export abstract class DeviceTrustServiceAbstraction {
|
||||
|
||||
/** Retrieves the device key if it exists from state or secure storage if supported for the active user. */
|
||||
abstract getDeviceKey(userId: UserId): Promise<DeviceKey | null>;
|
||||
abstract setDeviceKey(userId: UserId, deviceKey: DeviceKey | null): Promise<void>;
|
||||
abstract decryptUserKeyWithDeviceKey(
|
||||
userId: UserId,
|
||||
encryptedDevicePrivateKey: EncString,
|
||||
|
||||
@@ -356,7 +356,7 @@ export class DeviceTrustService implements DeviceTrustServiceAbstraction {
|
||||
}
|
||||
}
|
||||
|
||||
private async setDeviceKey(userId: UserId, deviceKey: DeviceKey | null): Promise<void> {
|
||||
async setDeviceKey(userId: UserId, deviceKey: DeviceKey | null): Promise<void> {
|
||||
if (!userId) {
|
||||
throw new Error("UserId is required. Cannot set device key.");
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { inject, Injectable } from "@angular/core";
|
||||
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
|
||||
import { NavigationEnd, Router } from "@angular/router";
|
||||
import { skip, filter, map, combineLatestWith, tap } from "rxjs";
|
||||
import { skip, filter, combineLatestWith, tap } from "rxjs";
|
||||
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
@@ -19,8 +19,10 @@ export class RouterFocusManagerService {
|
||||
*
|
||||
* By default, we focus the `main` after an internal route navigation.
|
||||
*
|
||||
* Consumers can opt out of the passing the following to the `info` input:
|
||||
* `<a [routerLink]="route()" [info]="{ focusMainAfterNav: false }"></a>`
|
||||
* Consumers can opt out of the passing the following to the `state` input. Using `state`
|
||||
* allows us to access the value between browser back/forward arrows.
|
||||
* In template: `<a [routerLink]="route()" [state]="{ focusMainAfterNav: false }"></a>`
|
||||
* In typescript: `this.router.navigate([], { state: { focusMainAfterNav: false }})`
|
||||
*
|
||||
* Or, consumers can use the autofocus directive on an applicable interactive element.
|
||||
* The autofocus directive will take precedence over this route focus pipeline.
|
||||
@@ -44,15 +46,12 @@ export class RouterFocusManagerService {
|
||||
skip(1),
|
||||
combineLatestWith(this.configService.getFeatureFlag$(FeatureFlag.RouterFocusManagement)),
|
||||
filter(([_navEvent, flagEnabled]) => flagEnabled),
|
||||
map(() => {
|
||||
const currentNavData = this.router.getCurrentNavigation()?.extras;
|
||||
filter(() => {
|
||||
const currentNavExtras = this.router.currentNavigation()?.extras;
|
||||
|
||||
const info = currentNavData?.info as { focusMainAfterNav?: boolean } | undefined;
|
||||
const focusMainAfterNav: boolean | undefined = currentNavExtras?.state?.focusMainAfterNav;
|
||||
|
||||
return info;
|
||||
}),
|
||||
filter((currentNavInfo) => {
|
||||
return currentNavInfo === undefined ? true : currentNavInfo?.focusMainAfterNav !== false;
|
||||
return focusMainAfterNav !== false;
|
||||
}),
|
||||
tap(() => {
|
||||
const mainEl = document.querySelector<HTMLElement>("main");
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
[routerLinkActiveOptions]="routerLinkMatchOptions"
|
||||
#rla="routerLinkActive"
|
||||
[active]="rla.isActive"
|
||||
[info]="{ focusMainAfterNav: false }"
|
||||
[state]="{ focusMainAfterNav: false }"
|
||||
[disabled]="disabled"
|
||||
[attr.aria-disabled]="disabled"
|
||||
ariaCurrentWhenActive="page"
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
import { ComponentFixture, TestBed } from "@angular/core/testing";
|
||||
import { mock } from "jest-mock-extended";
|
||||
import { of } from "rxjs";
|
||||
|
||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { Account, AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { SendType } from "@bitwarden/common/tools/send/enums/send-type";
|
||||
import { SendView } from "@bitwarden/common/tools/send/models/view/send.view";
|
||||
import { SendApiService } from "@bitwarden/common/tools/send/services/send-api.service.abstraction";
|
||||
import { DialogService, ToastService } from "@bitwarden/components";
|
||||
import { CredentialGeneratorService } from "@bitwarden/generator-core";
|
||||
|
||||
import { SendFormContainer } from "../../send-form-container";
|
||||
|
||||
import { SendOptionsComponent } from "./send-options.component";
|
||||
|
||||
describe("SendOptionsComponent", () => {
|
||||
let component: SendOptionsComponent;
|
||||
let fixture: ComponentFixture<SendOptionsComponent>;
|
||||
const mockSendFormContainer = mock<SendFormContainer>();
|
||||
const mockAccountService = mock<AccountService>();
|
||||
|
||||
beforeAll(() => {
|
||||
mockAccountService.activeAccount$ = of({ id: "myTestAccount" } as Account);
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [SendOptionsComponent],
|
||||
declarations: [],
|
||||
providers: [
|
||||
{ provide: SendFormContainer, useValue: mockSendFormContainer },
|
||||
{ provide: DialogService, useValue: mock<DialogService>() },
|
||||
{ provide: SendApiService, useValue: mock<SendApiService>() },
|
||||
{ provide: PolicyService, useValue: mock<PolicyService>() },
|
||||
{ provide: I18nService, useValue: mock<I18nService>() },
|
||||
{ provide: ToastService, useValue: mock<ToastService>() },
|
||||
{ provide: CredentialGeneratorService, useValue: mock<CredentialGeneratorService>() },
|
||||
{ provide: AccountService, useValue: mockAccountService },
|
||||
{ provide: PlatformUtilsService, useValue: mock<PlatformUtilsService>() },
|
||||
],
|
||||
}).compileComponents();
|
||||
fixture = TestBed.createComponent(SendOptionsComponent);
|
||||
component = fixture.componentInstance;
|
||||
component.config = { areSendsAllowed: true, mode: "add", sendType: SendType.Text };
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
it("should create", () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
|
||||
it("should emit a null password when password textbox is empty", async () => {
|
||||
const newSend = {} as SendView;
|
||||
mockSendFormContainer.patchSend.mockImplementation((updateFn) => updateFn(newSend));
|
||||
component.sendOptionsForm.patchValue({ password: "testing" });
|
||||
expect(newSend.password).toBe("testing");
|
||||
component.sendOptionsForm.patchValue({ password: "" });
|
||||
expect(newSend.password).toBe(null);
|
||||
});
|
||||
});
|
||||
@@ -4,7 +4,7 @@ import { CommonModule } from "@angular/common";
|
||||
import { Component, Input, OnInit } from "@angular/core";
|
||||
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
|
||||
import { FormBuilder, ReactiveFormsModule } from "@angular/forms";
|
||||
import { BehaviorSubject, firstValueFrom, map, switchMap } from "rxjs";
|
||||
import { BehaviorSubject, firstValueFrom, map, switchMap, tap } from "rxjs";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
@@ -12,6 +12,7 @@ import { PolicyType } from "@bitwarden/common/admin-console/enums";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { pin } from "@bitwarden/common/tools/rx";
|
||||
import { SendView } from "@bitwarden/common/tools/send/models/view/send.view";
|
||||
import { SendApiService } from "@bitwarden/common/tools/send/services/send-api.service.abstraction";
|
||||
@@ -112,18 +113,27 @@ export class SendOptionsComponent implements OnInit {
|
||||
this.disableHideEmail = disableHideEmail;
|
||||
});
|
||||
|
||||
this.sendOptionsForm.valueChanges.pipe(takeUntilDestroyed()).subscribe((value) => {
|
||||
this.sendFormContainer.patchSend((send) => {
|
||||
Object.assign(send, {
|
||||
maxAccessCount: value.maxAccessCount,
|
||||
accessCount: value.accessCount,
|
||||
password: value.password,
|
||||
hideEmail: value.hideEmail,
|
||||
notes: value.notes,
|
||||
this.sendOptionsForm.valueChanges
|
||||
.pipe(
|
||||
tap((value) => {
|
||||
if (Utils.isNullOrWhitespace(value.password)) {
|
||||
value.password = null;
|
||||
}
|
||||
}),
|
||||
takeUntilDestroyed(),
|
||||
)
|
||||
.subscribe((value) => {
|
||||
this.sendFormContainer.patchSend((send) => {
|
||||
Object.assign(send, {
|
||||
maxAccessCount: value.maxAccessCount,
|
||||
accessCount: value.accessCount,
|
||||
password: value.password,
|
||||
hideEmail: value.hideEmail,
|
||||
notes: value.notes,
|
||||
});
|
||||
return send;
|
||||
});
|
||||
return send;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
generatePassword = async () => {
|
||||
|
||||
@@ -18,7 +18,6 @@ import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
|
||||
import { FormBuilder, ReactiveFormsModule } from "@angular/forms";
|
||||
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { SendType } from "@bitwarden/common/tools/send/enums/send-type";
|
||||
import { SendView } from "@bitwarden/common/tools/send/models/view/send.view";
|
||||
import {
|
||||
@@ -227,10 +226,6 @@ export class SendFormComponent implements AfterViewInit, OnInit, OnChanges, Send
|
||||
return;
|
||||
}
|
||||
|
||||
if (Utils.isNullOrWhitespace(this.updatedSendView.password)) {
|
||||
this.updatedSendView.password = null;
|
||||
}
|
||||
|
||||
this.toastService.showToast({
|
||||
variant: "success",
|
||||
title: null,
|
||||
|
||||
1023
package-lock.json
generated
1023
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
16
package.json
16
package.json
@@ -50,6 +50,11 @@
|
||||
"@eslint/compat": "2.0.0",
|
||||
"@lit-labs/signals": "0.1.3",
|
||||
"@ngtools/webpack": "20.3.12",
|
||||
"@nx/devkit": "21.6.10",
|
||||
"@nx/eslint": "21.6.10",
|
||||
"@nx/jest": "21.6.10",
|
||||
"@nx/js": "21.6.10",
|
||||
"@nx/webpack": "21.6.10",
|
||||
"@storybook/addon-a11y": "9.1.16",
|
||||
"@storybook/addon-designs": "9.0.0-next.3",
|
||||
"@storybook/addon-docs": "9.1.16",
|
||||
@@ -93,7 +98,7 @@
|
||||
"copy-webpack-plugin": "13.0.1",
|
||||
"cross-env": "10.1.0",
|
||||
"css-loader": "7.1.2",
|
||||
"electron": "37.7.0",
|
||||
"electron": "39.2.6",
|
||||
"electron-builder": "26.0.12",
|
||||
"electron-log": "5.4.3",
|
||||
"electron-reload": "2.0.0-alpha.1",
|
||||
@@ -157,8 +162,8 @@
|
||||
"@angular/platform-browser": "20.3.15",
|
||||
"@angular/platform-browser-dynamic": "20.3.15",
|
||||
"@angular/router": "20.3.15",
|
||||
"@bitwarden/sdk-internal": "0.2.0-main.433",
|
||||
"@bitwarden/commercial-sdk-internal": "0.2.0-main.433",
|
||||
"@bitwarden/sdk-internal": "0.2.0-main.439",
|
||||
"@bitwarden/commercial-sdk-internal": "0.2.0-main.439",
|
||||
"@electron/fuses": "1.8.0",
|
||||
"@emotion/css": "11.13.5",
|
||||
"@koa/multer": "4.0.0",
|
||||
@@ -166,11 +171,6 @@
|
||||
"@microsoft/signalr": "8.0.7",
|
||||
"@microsoft/signalr-protocol-msgpack": "8.0.7",
|
||||
"@ng-select/ng-select": "20.7.0",
|
||||
"@nx/devkit": "21.6.10",
|
||||
"@nx/eslint": "21.6.10",
|
||||
"@nx/jest": "21.6.10",
|
||||
"@nx/js": "21.6.10",
|
||||
"@nx/webpack": "21.6.10",
|
||||
"big-integer": "1.6.52",
|
||||
"braintree-web-drop-in": "1.46.0",
|
||||
"buffer": "6.0.3",
|
||||
|
||||
Reference in New Issue
Block a user