mirror of
https://github.com/bitwarden/browser
synced 2026-02-10 21:50:15 +00:00
Merge branch 'main' into zhHantExtraSupport
This commit is contained in:
4
.github/CODEOWNERS
vendored
4
.github/CODEOWNERS
vendored
@@ -97,12 +97,15 @@ apps/web/src/translation-constants.ts @bitwarden/team-platform-dev
|
||||
.github/workflows/scan.yml @bitwarden/team-platform-dev
|
||||
.github/workflows/test.yml @bitwarden/team-platform-dev
|
||||
.github/workflows/version-auto-bump.yml @bitwarden/team-platform-dev
|
||||
# ESLint custom rules
|
||||
libs/eslint @bitwarden/team-platform-dev
|
||||
|
||||
## Autofill team files ##
|
||||
apps/browser/src/autofill @bitwarden/team-autofill-dev
|
||||
apps/desktop/src/autofill @bitwarden/team-autofill-dev
|
||||
libs/common/src/autofill @bitwarden/team-autofill-dev
|
||||
apps/desktop/macos/autofill-extension @bitwarden/team-autofill-dev
|
||||
apps/desktop/desktop_native/windows-plugin-authenticator @bitwarden/team-autofill-dev
|
||||
# DuckDuckGo integration
|
||||
apps/desktop/native-messaging-test-runner @bitwarden/team-autofill-dev
|
||||
apps/desktop/src/services/duckduckgo-message-handler.service.ts @bitwarden/team-autofill-dev
|
||||
@@ -128,6 +131,7 @@ apps/web/src/app/key-management @bitwarden/team-key-management-dev
|
||||
apps/browser/src/key-management @bitwarden/team-key-management-dev
|
||||
apps/cli/src/key-management @bitwarden/team-key-management-dev
|
||||
libs/key-management @bitwarden/team-key-management-dev
|
||||
libs/key-management-ui @bitwarden/team-key-management-dev
|
||||
libs/common/src/key-management @bitwarden/team-key-management-dev
|
||||
|
||||
apps/desktop/destkop_native/core/src/biometric/ @bitwarden/team-key-management-dev
|
||||
|
||||
2
.github/renovate.json
vendored
2
.github/renovate.json
vendored
@@ -211,6 +211,8 @@
|
||||
"@storybook/angular",
|
||||
"@storybook/manager-api",
|
||||
"@storybook/theming",
|
||||
"@typescript-eslint/utils",
|
||||
"@typescript-eslint/rule-tester",
|
||||
"@types/react",
|
||||
"autoprefixer",
|
||||
"bootstrap",
|
||||
|
||||
44
.github/workflows/test.yml
vendored
44
.github/workflows/test.yml
vendored
@@ -88,7 +88,7 @@ jobs:
|
||||
uses: codecov/codecov-action@1e68e06f1dbfde0e4cefc87efeba9e4643565303 # v5.1.2
|
||||
|
||||
- name: Upload results to codecov.io
|
||||
uses: codecov/test-results-action@9739113ad922ea0a9abb4b2c0f8bf6a4aa8ef820 # v1.0.1
|
||||
uses: codecov/test-results-action@4e79e65778be1cecd5df25e14af1eafb6df80ea9 # v1.0.2
|
||||
if: ${{ needs.check-test-secrets.outputs.available == 'true' }}
|
||||
env:
|
||||
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
||||
@@ -103,15 +103,15 @@ jobs:
|
||||
matrix:
|
||||
os:
|
||||
- ubuntu-22.04
|
||||
- macos-latest
|
||||
- windows-latest
|
||||
- macos-14
|
||||
- windows-2022
|
||||
|
||||
steps:
|
||||
- name: Check Rust version
|
||||
run: rustup --version
|
||||
|
||||
- name: Install gnome-keyring
|
||||
if: ${{ matrix.os=='ubuntu-latest' }}
|
||||
if: ${{ matrix.os=='ubuntu-22.04' }}
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y gnome-keyring dbus-x11
|
||||
@@ -124,7 +124,7 @@ jobs:
|
||||
run: cargo build
|
||||
|
||||
- name: Test Ubuntu
|
||||
if: ${{ matrix.os=='ubuntu-latest' }}
|
||||
if: ${{ matrix.os=='ubuntu-22.04' }}
|
||||
working-directory: ./apps/desktop/desktop_native
|
||||
run: |
|
||||
eval "$(dbus-launch --sh-syntax)"
|
||||
@@ -135,11 +135,41 @@ jobs:
|
||||
cargo test -- --test-threads=1
|
||||
|
||||
- name: Test macOS
|
||||
if: ${{ matrix.os=='macos-latest' }}
|
||||
if: ${{ matrix.os=='macos-14' }}
|
||||
working-directory: ./apps/desktop/desktop_native
|
||||
run: cargo test -- --test-threads=1
|
||||
|
||||
- name: Test Windows
|
||||
if: ${{ matrix.os=='windows-latest'}}
|
||||
if: ${{ matrix.os=='windows-2022'}}
|
||||
working-directory: ./apps/desktop/desktop_native/core
|
||||
run: cargo test -- --test-threads=1
|
||||
|
||||
rust-coverage:
|
||||
name: Rust Coverage
|
||||
runs-on: macos-14
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
- name: Install rust
|
||||
uses: dtolnay/rust-toolchain@a54c7afa936fefeb4456b2dd8068152669aa8203 # stable
|
||||
with:
|
||||
toolchain: stable
|
||||
components: llvm-tools
|
||||
|
||||
- name: Cache cargo registry
|
||||
uses: Swatinem/rust-cache@82a92a6e8fbeee089604da2575dc567ae9ddeaab # v2.7.5
|
||||
with:
|
||||
workspaces: "apps/desktop/desktop_native -> target"
|
||||
|
||||
- name: Install cargo-llvm-cov
|
||||
run: cargo install cargo-llvm-cov --version 0.6.16
|
||||
|
||||
- name: Generate coverage
|
||||
working-directory: ./apps/desktop/desktop_native
|
||||
run: cargo llvm-cov --all-features --lcov --output-path lcov.info --workspace --no-cfg-coverage
|
||||
|
||||
- name: Upload to codecov.io
|
||||
uses: codecov/codecov-action@13ce06bfc6bbe3ecf90edbbf1bc32fe5978ca1d3 # v5.3.1
|
||||
with:
|
||||
files: ./apps/desktop/desktop_native/lcov.info
|
||||
|
||||
@@ -4155,15 +4155,6 @@
|
||||
"itemName": {
|
||||
"message": "Item name"
|
||||
},
|
||||
"cannotRemoveViewOnlyCollections": {
|
||||
"message": "You cannot remove collections with View only permissions: $COLLECTIONS$",
|
||||
"placeholders": {
|
||||
"collections": {
|
||||
"content": "$1",
|
||||
"example": "Work, Personal"
|
||||
}
|
||||
}
|
||||
},
|
||||
"organizationIsDeactivated": {
|
||||
"message": "Organization is deactivated"
|
||||
},
|
||||
@@ -4896,6 +4887,15 @@
|
||||
"extraWide": {
|
||||
"message": "Extra wide"
|
||||
},
|
||||
"cannotRemoveViewOnlyCollections": {
|
||||
"message": "You cannot remove collections with View only permissions: $COLLECTIONS$",
|
||||
"placeholders": {
|
||||
"collections": {
|
||||
"content": "$1",
|
||||
"example": "Work, Personal"
|
||||
}
|
||||
}
|
||||
},
|
||||
"updateDesktopAppOrDisableFingerprintDialogTitle": {
|
||||
"message": "Please update your desktop application"
|
||||
},
|
||||
|
||||
@@ -424,6 +424,7 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ
|
||||
}
|
||||
|
||||
await this.setupSubmitListenerOnFormlessField(formFieldElement);
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -439,15 +440,16 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ
|
||||
this.formElements.add(formElement);
|
||||
formElement.addEventListener(EVENTS.SUBMIT, this.handleFormFieldSubmitEvent);
|
||||
|
||||
const closesSubmitButton = await this.findSubmitButton(formElement);
|
||||
const closestSubmitButton = await this.findSubmitButton(formElement);
|
||||
|
||||
// If we cannot find a submit button within the form, check for a submit button outside the form.
|
||||
if (!closesSubmitButton) {
|
||||
if (!closestSubmitButton) {
|
||||
await this.setupSubmitListenerOnFormlessField(formFieldElement);
|
||||
return;
|
||||
}
|
||||
|
||||
this.setupSubmitButtonEventListeners(closesSubmitButton);
|
||||
this.setupSubmitButtonEventListeners(closestSubmitButton);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -459,9 +461,11 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ
|
||||
*/
|
||||
private async setupSubmitListenerOnFormlessField(formFieldElement: FillableFormFieldElement) {
|
||||
if (formFieldElement && !this.fieldsWithSubmitElements.has(formFieldElement)) {
|
||||
const closesSubmitButton = await this.findClosestFormlessSubmitButton(formFieldElement);
|
||||
this.setupSubmitButtonEventListeners(closesSubmitButton);
|
||||
const closestSubmitButton = await this.findClosestFormlessSubmitButton(formFieldElement);
|
||||
|
||||
this.setupSubmitButtonEventListeners(closestSubmitButton);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -747,7 +747,7 @@ describe("AutofillService", () => {
|
||||
jest.spyOn(autofillService as any, "generateFillScript");
|
||||
jest.spyOn(autofillService as any, "generateLoginFillScript");
|
||||
jest.spyOn(logService, "info");
|
||||
jest.spyOn(cipherService, "updateLastUsedDate");
|
||||
jest.spyOn(chrome.runtime, "sendMessage");
|
||||
jest.spyOn(eventCollectionService, "collect");
|
||||
|
||||
const autofillResult = await autofillService.doAutoFill(autofillOptions);
|
||||
@@ -769,7 +769,10 @@ describe("AutofillService", () => {
|
||||
);
|
||||
expect(autofillService["generateLoginFillScript"]).toHaveBeenCalled();
|
||||
expect(logService.info).not.toHaveBeenCalled();
|
||||
expect(cipherService.updateLastUsedDate).toHaveBeenCalledWith(autofillOptions.cipher.id);
|
||||
expect(chrome.runtime.sendMessage).toHaveBeenCalledWith({
|
||||
cipherId: autofillOptions.cipher.id,
|
||||
command: "updateLastUsedDate",
|
||||
});
|
||||
expect(chrome.tabs.sendMessage).toHaveBeenCalledWith(
|
||||
autofillOptions.pageDetails[0].tab.id,
|
||||
{
|
||||
@@ -890,11 +893,11 @@ describe("AutofillService", () => {
|
||||
|
||||
it("skips updating the cipher's last used date if the passed options indicate that we should skip the last used cipher", async () => {
|
||||
autofillOptions.skipLastUsed = true;
|
||||
jest.spyOn(cipherService, "updateLastUsedDate");
|
||||
jest.spyOn(chrome.runtime, "sendMessage");
|
||||
|
||||
await autofillService.doAutoFill(autofillOptions);
|
||||
|
||||
expect(cipherService.updateLastUsedDate).not.toHaveBeenCalled();
|
||||
expect(chrome.runtime.sendMessage).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("returns early if the fillScript cannot be generated", async () => {
|
||||
|
||||
@@ -463,8 +463,13 @@ export default class AutofillService implements AutofillServiceInterface {
|
||||
fillScript.properties.delay_between_operations = 20;
|
||||
|
||||
didAutofill = true;
|
||||
|
||||
if (!options.skipLastUsed) {
|
||||
await this.cipherService.updateLastUsedDate(options.cipher.id);
|
||||
// In order to prevent a UI update send message to background script to update last used date
|
||||
await chrome.runtime.sendMessage({
|
||||
command: "updateLastUsedDate",
|
||||
cipherId: options.cipher.id,
|
||||
});
|
||||
}
|
||||
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
|
||||
@@ -317,6 +317,7 @@ describe("CollectAutofillContentService", () => {
|
||||
__form__0: {
|
||||
opid: "__form__0",
|
||||
htmlAction: formAction,
|
||||
htmlClass: null,
|
||||
htmlName: formName,
|
||||
htmlID: formId,
|
||||
htmlMethod: formMethod,
|
||||
@@ -544,6 +545,7 @@ describe("CollectAutofillContentService", () => {
|
||||
__form__0: {
|
||||
opid: "__form__0",
|
||||
htmlAction: formAction1,
|
||||
htmlClass: null,
|
||||
htmlName: formName1,
|
||||
htmlID: formId1,
|
||||
htmlMethod: formMethod1,
|
||||
@@ -551,6 +553,7 @@ describe("CollectAutofillContentService", () => {
|
||||
__form__1: {
|
||||
opid: "__form__1",
|
||||
htmlAction: formAction2,
|
||||
htmlClass: null,
|
||||
htmlName: formName2,
|
||||
htmlID: formId2,
|
||||
htmlMethod: formMethod2,
|
||||
|
||||
@@ -228,6 +228,7 @@ export class CollectAutofillContentService implements CollectAutofillContentServ
|
||||
opid: formElement.opid,
|
||||
htmlAction: this.getFormActionAttribute(formElement),
|
||||
htmlName: this.getPropertyOrAttribute(formElement, "name"),
|
||||
htmlClass: this.getPropertyOrAttribute(formElement, "class"),
|
||||
htmlID: this.getPropertyOrAttribute(formElement, "id"),
|
||||
htmlMethod: this.getPropertyOrAttribute(formElement, "method"),
|
||||
});
|
||||
@@ -982,8 +983,7 @@ export class CollectAutofillContentService implements CollectAutofillContentServ
|
||||
const queueLength = this.mutationsQueue.length;
|
||||
|
||||
if (!this.domQueryService.pageContainsShadowDomElements()) {
|
||||
// Checking if a page contains shadowDOM elements is a heavy operation and doesn't have to be done immediately, so we can call this within an idle moment on the event loop.
|
||||
requestIdleCallbackPolyfill(this.checkPageContainsShadowDom, { timeout: 500 });
|
||||
this.checkPageContainsShadowDom();
|
||||
}
|
||||
|
||||
for (let queueIndex = 0; queueIndex < queueLength; queueIndex++) {
|
||||
|
||||
@@ -56,6 +56,7 @@ export class InlineMenuFieldQualificationService
|
||||
"neuer benutzer",
|
||||
"neues passwort",
|
||||
"neue e-mail",
|
||||
"pwdcheck",
|
||||
];
|
||||
private updatePasswordFieldKeywords = [
|
||||
"update password",
|
||||
|
||||
@@ -809,7 +809,7 @@ export default class MainBackground {
|
||||
this.apiService,
|
||||
);
|
||||
|
||||
this.ssoLoginService = new SsoLoginService(this.stateProvider);
|
||||
this.ssoLoginService = new SsoLoginService(this.stateProvider, this.logService);
|
||||
|
||||
this.userVerificationApiService = new UserVerificationApiService(this.apiService);
|
||||
|
||||
@@ -1142,6 +1142,7 @@ export default class MainBackground {
|
||||
this.accountService,
|
||||
lockService,
|
||||
this.billingAccountProfileStateService,
|
||||
this.cipherService,
|
||||
);
|
||||
this.nativeMessagingBackground = new NativeMessagingBackground(
|
||||
this.keyService,
|
||||
|
||||
@@ -16,6 +16,7 @@ import { MessageListener, isExternalMessage } from "@bitwarden/common/platform/m
|
||||
import { devFlagEnabled } from "@bitwarden/common/platform/misc/flags";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { NotificationsService } from "@bitwarden/common/platform/notifications";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||
import { BiometricsCommands } from "@bitwarden/key-management";
|
||||
|
||||
@@ -53,6 +54,7 @@ export default class RuntimeBackground {
|
||||
private accountService: AccountService,
|
||||
private readonly lockService: LockService,
|
||||
private billingAccountProfileStateService: BillingAccountProfileStateService,
|
||||
private cipherService: CipherService,
|
||||
) {
|
||||
// onInstalled listener must be wired up before anything else, so we do it in the ctor
|
||||
chrome.runtime.onInstalled.addListener((details: any) => {
|
||||
@@ -200,6 +202,9 @@ export default class RuntimeBackground {
|
||||
case BiometricsCommands.GetBiometricsStatusForUser: {
|
||||
return await this.main.biometricsService.getBiometricsStatusForUser(msg.userId);
|
||||
}
|
||||
case "updateLastUsedDate": {
|
||||
return await this.cipherService.updateLastUsedDate(msg.cipherId);
|
||||
}
|
||||
case "getUseTreeWalkerApiForPageDetailsCollectionFeatureFlag": {
|
||||
return await this.configService.getFeatureFlag(
|
||||
FeatureFlag.UseTreeWalkerApiForPageDetailsCollection,
|
||||
|
||||
@@ -7,4 +7,5 @@
|
||||
[description]="(showEmptyAutofillTip$ | async) ? ('autofillSuggestionsTip' | i18n) : null"
|
||||
showAutofillButton
|
||||
[primaryActionAutofill]="clickItemsToAutofillVaultView"
|
||||
[groupByType]="groupByType()"
|
||||
></app-vault-list-items-container>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { Component, OnInit } from "@angular/core";
|
||||
import { toSignal } from "@angular/core/rxjs-interop";
|
||||
import { combineLatest, firstValueFrom, map, Observable } from "rxjs";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
@@ -48,6 +49,10 @@ export class AutofillVaultListItemsComponent implements OnInit {
|
||||
|
||||
clickItemsToAutofillVaultView = false;
|
||||
|
||||
protected groupByType = toSignal(
|
||||
this.vaultPopupItemsService.hasFilterApplied$.pipe(map((hasFilter) => !hasFilter)),
|
||||
);
|
||||
|
||||
/**
|
||||
* Observable that determines whether the empty autofill tip should be shown.
|
||||
* The tip is shown when there are no login ciphers to autofill, no filter is applied, and autofill is allowed in
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
<button type="button" bitMenuItem (click)="toggleFavorite()">
|
||||
{{ favoriteText | i18n }}
|
||||
</button>
|
||||
<ng-container *ngIf="canEdit">
|
||||
<ng-container *ngIf="canEdit && canViewPassword">
|
||||
<a bitMenuItem (click)="clone()" *ngIf="canClone$ | async">
|
||||
{{ "clone" | i18n }}
|
||||
</a>
|
||||
|
||||
@@ -97,6 +97,9 @@ export class ItemMoreOptionsComponent implements OnInit {
|
||||
return this.cipher.edit;
|
||||
}
|
||||
|
||||
get canViewPassword() {
|
||||
return this.cipher.viewPassword;
|
||||
}
|
||||
/**
|
||||
* Determines if the cipher can be autofilled.
|
||||
*/
|
||||
|
||||
@@ -11,11 +11,11 @@ import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { CollectionId, OrganizationId } from "@bitwarden/common/types/guid";
|
||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||
import { ButtonModule, DialogService, MenuModule, NoItemsModule } from "@bitwarden/components";
|
||||
import { AddEditFolderDialogComponent } from "@bitwarden/vault";
|
||||
|
||||
import { BrowserApi } from "../../../../../platform/browser/browser-api";
|
||||
import BrowserPopupUtils from "../../../../../platform/popup/browser-popup-utils";
|
||||
import { AddEditQueryParams } from "../add-edit/add-edit-v2.component";
|
||||
import { AddEditFolderDialogComponent } from "../add-edit-folder-dialog/add-edit-folder-dialog.component";
|
||||
|
||||
export interface NewItemInitialValues {
|
||||
folderId?: string;
|
||||
@@ -72,6 +72,6 @@ export class NewItemDropdownV2Component implements OnInit {
|
||||
}
|
||||
|
||||
openFolderDialog() {
|
||||
this.dialogService.open(AddEditFolderDialogComponent);
|
||||
AddEditFolderDialogComponent.open(this.dialogService);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,11 +6,12 @@ import { combineLatest, map, take } from "rxjs";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { DisclosureTriggerForDirective, IconButtonModule } from "@bitwarden/components";
|
||||
import {
|
||||
DisclosureComponent,
|
||||
DisclosureTriggerForDirective,
|
||||
IconButtonModule,
|
||||
} from "@bitwarden/components";
|
||||
|
||||
// FIXME: remove `src` and fix import
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { DisclosureComponent } from "../../../../../../../../libs/components/src/disclosure/disclosure.component";
|
||||
import { runInsideAngular } from "../../../../../platform/browser/run-inside-angular.operator";
|
||||
import { VaultPopupListFiltersService } from "../../../../../vault/popup/services/vault-popup-list-filters.service";
|
||||
import { VaultListFiltersComponent } from "../vault-list-filters/vault-list-filters.component";
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
<bit-section *ngIf="ciphers?.length > 0 || description" [disableMargin]="disableSectionMargin">
|
||||
<bit-section
|
||||
*ngIf="cipherGroups$().length > 0 || description"
|
||||
[disableMargin]="disableSectionMargin"
|
||||
>
|
||||
<ng-container *ngIf="collapsibleKey">
|
||||
<button
|
||||
class="tw-group/vault-section-header hover:tw-bg-secondary-100 tw-pl-1 tw-w-full tw-border-x-0 tw-border-t-0 tw-border-b tw-border-solid focus-visible:tw-outline-none focus-visible:tw-rounded-md focus-visible:tw-ring-inset focus-visible:tw-ring-2 focus-visible:tw-ring-primary-600"
|
||||
class="tw-group/vault-section-header hover:tw-bg-primary-100 tw-rounded-md tw-pl-1 tw-w-full tw-border-x-0 tw-border-t-0 tw-border-b tw-border-solid focus-visible:tw-outline-none focus-visible:tw-ring-inset focus-visible:tw-ring-2 focus-visible:tw-ring-primary-600"
|
||||
[ngClass]="{
|
||||
'tw-border-b-secondary-300': !sectionOpenState(),
|
||||
'tw-border-b-secondary-300 tw-rounded-b-none [&:is(:hover,:focus-visible)]:tw-border-b-transparent [&:is(:hover,:focus-visible)]:tw-rounded-b-md':
|
||||
!sectionOpenState(),
|
||||
'tw-border-b-transparent': sectionOpenState(),
|
||||
}"
|
||||
type="button"
|
||||
@@ -17,6 +21,7 @@
|
||||
<ng-container *ngTemplateOutlet="itemGroup"></ng-container>
|
||||
</bit-disclosure>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="!collapsibleKey">
|
||||
<div class="tw-pl-1">
|
||||
<ng-container *ngTemplateOutlet="sectionHeader"></ng-container>
|
||||
@@ -27,7 +32,7 @@
|
||||
</bit-section>
|
||||
|
||||
<ng-template #sectionHeader>
|
||||
<bit-section-header class="tw-p-0.5 -tw-mt-0.5 -tw-mx-0.5">
|
||||
<bit-section-header class="tw-p-0.5 -tw-mx-0.5">
|
||||
<h2 bitTypography="h6">
|
||||
{{ title }}
|
||||
</h2>
|
||||
@@ -47,11 +52,11 @@
|
||||
'tw-hidden': collapsibleKey && !sectionOpenState(),
|
||||
}"
|
||||
>
|
||||
{{ ciphers.length }}
|
||||
{{ ciphers().length }}
|
||||
</span>
|
||||
<span class="tw-pr-1" *ngIf="collapsibleKey">
|
||||
<i
|
||||
class="bwi"
|
||||
class="bwi tw-text-main"
|
||||
[ngClass]="{
|
||||
'bwi-angle-down tw-inline-block': !sectionOpenState(),
|
||||
'bwi-angle-up tw-hidden group-hover/vault-section-header:tw-inline-block group-focus-visible/vault-section-header:tw-inline-block':
|
||||
@@ -72,69 +77,78 @@
|
||||
|
||||
<ng-template #itemGroup>
|
||||
<bit-item-group>
|
||||
<cdk-virtual-scroll-viewport
|
||||
[itemSize]="itemHeight$ | async"
|
||||
class="tw-overflow-visible [&>.cdk-virtual-scroll-content-wrapper]:[contain:layout_style]"
|
||||
>
|
||||
<bit-item *cdkVirtualFor="let cipher of ciphers">
|
||||
<button
|
||||
bit-item-content
|
||||
type="button"
|
||||
(click)="primaryActionOnSelect(cipher)"
|
||||
(dblclick)="launchCipher(cipher)"
|
||||
[appA11yTitle]="cipherItemTitleKey | async | i18n: cipher.name"
|
||||
class="{{ itemHeightClass }}"
|
||||
>
|
||||
<div slot="start" class="tw-justify-start tw-w-7 tw-flex">
|
||||
<app-vault-icon [cipher]="cipher"></app-vault-icon>
|
||||
</div>
|
||||
<span data-testid="item-name">{{ cipher.name }}</span>
|
||||
<i
|
||||
*ngIf="cipher.organizationId"
|
||||
slot="default-trailing"
|
||||
appOrgIcon
|
||||
[tierType]="cipher.organization.productTierType"
|
||||
[size]="'small'"
|
||||
[appA11yTitle]="orgIconTooltip(cipher)"
|
||||
></i>
|
||||
<i
|
||||
*ngIf="cipher.hasAttachments"
|
||||
class="bwi bwi-paperclip bwi-sm"
|
||||
[appA11yTitle]="'attachments' | i18n"
|
||||
></i>
|
||||
<span slot="secondary">{{ cipher.subTitle }}</span>
|
||||
</button>
|
||||
<ng-container slot="end">
|
||||
<bit-item-action *ngIf="!(hideAutofillButton$ | async)">
|
||||
<button
|
||||
type="button"
|
||||
bitBadge
|
||||
variant="primary"
|
||||
(click)="doAutofill(cipher)"
|
||||
[title]="autofillShortcutTooltip() ?? ('autofillTitle' | i18n: cipher.name)"
|
||||
[attr.aria-label]="'autofillTitle' | i18n: cipher.name"
|
||||
>
|
||||
{{ "fill" | i18n }}
|
||||
</button>
|
||||
</bit-item-action>
|
||||
<bit-item-action *ngIf="!showAutofillButton && cipher.canLaunch">
|
||||
<button
|
||||
type="button"
|
||||
bitIconButton="bwi-external-link"
|
||||
size="small"
|
||||
(click)="launchCipher(cipher)"
|
||||
[attr.aria-label]="'launchWebsiteName' | i18n: cipher.name"
|
||||
[title]="'launchWebsiteName' | i18n: cipher.name"
|
||||
></button>
|
||||
</bit-item-action>
|
||||
<app-item-copy-actions [cipher]="cipher"></app-item-copy-actions>
|
||||
<app-item-more-options
|
||||
[cipher]="cipher"
|
||||
[hideAutofillOptions]="hideAutofillOptions$ | async"
|
||||
[showViewOption]="primaryActionAutofill"
|
||||
></app-item-more-options>
|
||||
</ng-container>
|
||||
</bit-item>
|
||||
</cdk-virtual-scroll-viewport>
|
||||
<ng-container *ngFor="let group of cipherGroups$()">
|
||||
<ng-container *ngIf="group.subHeaderKey">
|
||||
<h3 class="tw-text-muted tw-text-xs tw-font-semibold tw-pl-1 tw-mb-1 bit-compact:tw-m-0">
|
||||
{{ group.subHeaderKey | i18n }}
|
||||
</h3>
|
||||
</ng-container>
|
||||
|
||||
<cdk-virtual-scroll-viewport
|
||||
[itemSize]="itemHeight$ | async"
|
||||
class="tw-overflow-visible [&>.cdk-virtual-scroll-content-wrapper]:[contain:layout_style]"
|
||||
>
|
||||
<bit-item *cdkVirtualFor="let cipher of group.ciphers">
|
||||
<button
|
||||
bit-item-content
|
||||
type="button"
|
||||
(click)="primaryActionOnSelect(cipher)"
|
||||
(dblclick)="launchCipher(cipher)"
|
||||
[appA11yTitle]="cipherItemTitleKey | async | i18n: cipher.name"
|
||||
class="{{ itemHeightClass }}"
|
||||
>
|
||||
<div slot="start" class="tw-justify-start tw-w-7 tw-flex">
|
||||
<app-vault-icon [cipher]="cipher"></app-vault-icon>
|
||||
</div>
|
||||
<span data-testid="item-name">{{ cipher.name }}</span>
|
||||
<i
|
||||
*ngIf="cipher.organizationId"
|
||||
slot="default-trailing"
|
||||
appOrgIcon
|
||||
[tierType]="cipher.organization.productTierType"
|
||||
[size]="'small'"
|
||||
[appA11yTitle]="orgIconTooltip(cipher)"
|
||||
></i>
|
||||
<i
|
||||
*ngIf="cipher.hasAttachments"
|
||||
class="bwi bwi-paperclip bwi-sm"
|
||||
[appA11yTitle]="'attachments' | i18n"
|
||||
></i>
|
||||
<span slot="secondary">{{ cipher.subTitle }}</span>
|
||||
</button>
|
||||
|
||||
<ng-container slot="end">
|
||||
<bit-item-action *ngIf="!(hideAutofillButton$ | async)">
|
||||
<button
|
||||
type="button"
|
||||
bitBadge
|
||||
variant="primary"
|
||||
(click)="doAutofill(cipher)"
|
||||
[title]="autofillShortcutTooltip() ?? ('autofillTitle' | i18n: cipher.name)"
|
||||
[attr.aria-label]="'autofillTitle' | i18n: cipher.name"
|
||||
>
|
||||
{{ "fill" | i18n }}
|
||||
</button>
|
||||
</bit-item-action>
|
||||
<bit-item-action *ngIf="!showAutofillButton && cipher.canLaunch">
|
||||
<button
|
||||
type="button"
|
||||
bitIconButton="bwi-external-link"
|
||||
size="small"
|
||||
(click)="launchCipher(cipher)"
|
||||
[attr.aria-label]="'launchWebsiteName' | i18n: cipher.name"
|
||||
[title]="'launchWebsiteName' | i18n: cipher.name"
|
||||
></button>
|
||||
</bit-item-action>
|
||||
<app-item-copy-actions [cipher]="cipher"></app-item-copy-actions>
|
||||
<app-item-more-options
|
||||
[cipher]="cipher"
|
||||
[hideAutofillOptions]="hideAutofillOptions$ | async"
|
||||
[showViewOption]="primaryActionAutofill"
|
||||
></app-item-more-options>
|
||||
</ng-container>
|
||||
</bit-item>
|
||||
</cdk-virtual-scroll-viewport>
|
||||
</ng-container>
|
||||
</bit-item-group>
|
||||
</ng-template>
|
||||
|
||||
@@ -9,11 +9,14 @@ import {
|
||||
EventEmitter,
|
||||
inject,
|
||||
Input,
|
||||
OnInit,
|
||||
Output,
|
||||
Signal,
|
||||
signal,
|
||||
ViewChild,
|
||||
computed,
|
||||
OnInit,
|
||||
ChangeDetectionStrategy,
|
||||
input,
|
||||
} from "@angular/core";
|
||||
import { Router } from "@angular/router";
|
||||
import { firstValueFrom, Observable, map } from "rxjs";
|
||||
@@ -23,6 +26,7 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { CipherId } from "@bitwarden/common/types/guid";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
import {
|
||||
BadgeModule,
|
||||
@@ -73,6 +77,7 @@ import { ItemMoreOptionsComponent } from "../item-more-options/item-more-options
|
||||
selector: "app-vault-list-items-container",
|
||||
templateUrl: "vault-list-items-container.component.html",
|
||||
standalone: true,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class VaultListItemsContainerComponent implements OnInit, AfterViewInit {
|
||||
private compactModeService = inject(CompactModeService);
|
||||
@@ -110,11 +115,51 @@ export class VaultListItemsContainerComponent implements OnInit, AfterViewInit {
|
||||
*/
|
||||
private viewCipherTimeout: number | null;
|
||||
|
||||
ciphers = input<PopupCipherView[]>([]);
|
||||
|
||||
/**
|
||||
* The list of ciphers to display.
|
||||
* If true, we will group ciphers by type (Login, Card, Identity)
|
||||
* within subheadings in a single container, converted to a WritableSignal.
|
||||
*/
|
||||
@Input()
|
||||
ciphers: PopupCipherView[] = [];
|
||||
groupByType = input<boolean>(false);
|
||||
|
||||
/**
|
||||
* Computed signal for a grouped list of ciphers with an optional header
|
||||
*/
|
||||
cipherGroups$ = computed<
|
||||
{
|
||||
subHeaderKey?: string | null;
|
||||
ciphers: PopupCipherView[];
|
||||
}[]
|
||||
>(() => {
|
||||
const groups: { [key: string]: CipherView[] } = {};
|
||||
|
||||
this.ciphers().forEach((cipher) => {
|
||||
let groupKey;
|
||||
|
||||
if (this.groupByType()) {
|
||||
switch (cipher.type) {
|
||||
case CipherType.Card:
|
||||
groupKey = "cards";
|
||||
break;
|
||||
case CipherType.Identity:
|
||||
groupKey = "identities";
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!groups[groupKey]) {
|
||||
groups[groupKey] = [];
|
||||
}
|
||||
|
||||
groups[groupKey].push(cipher);
|
||||
});
|
||||
|
||||
return Object.keys(groups).map((key) => ({
|
||||
subHeaderKey: this.groupByType ? key : "",
|
||||
ciphers: groups[key],
|
||||
}));
|
||||
});
|
||||
|
||||
/**
|
||||
* Title for the vault list item section.
|
||||
|
||||
@@ -11,10 +11,8 @@ import { AccountService } from "@bitwarden/common/auth/abstractions/account.serv
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
import { PasswordHistoryViewComponent } from "@bitwarden/vault";
|
||||
|
||||
// FIXME: remove `src` and fix import
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { PasswordHistoryViewComponent } from "../../../../../../../../libs/vault/src/components/password-history-view/password-history-view.component";
|
||||
import { PopOutComponent } from "../../../../../platform/popup/components/pop-out.component";
|
||||
import { PopupHeaderComponent } from "../../../../../platform/popup/layout/popup-header.component";
|
||||
import { PopupPageComponent } from "../../../../../platform/popup/layout/popup-page.component";
|
||||
|
||||
@@ -69,13 +69,13 @@
|
||||
<app-autofill-vault-list-items></app-autofill-vault-list-items>
|
||||
<app-vault-list-items-container
|
||||
[title]="'favorites' | i18n"
|
||||
[ciphers]="favoriteCiphers$ | async"
|
||||
[ciphers]="(favoriteCiphers$ | async) || []"
|
||||
id="favorites"
|
||||
collapsibleKey="favorites"
|
||||
></app-vault-list-items-container>
|
||||
<app-vault-list-items-container
|
||||
[title]="'allItems' | i18n"
|
||||
[ciphers]="remainingCiphers$ | async"
|
||||
[ciphers]="(remainingCiphers$ | async) || []"
|
||||
id="allItems"
|
||||
disableSectionMargin
|
||||
collapsibleKey="allItems"
|
||||
|
||||
@@ -23,6 +23,7 @@ import { EventType } from "@bitwarden/common/enums";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { PremiumUpgradePromptService } from "@bitwarden/common/vault/abstractions/premium-upgrade-prompt.service";
|
||||
import { ViewPasswordHistoryService } from "@bitwarden/common/vault/abstractions/view-password-history.service";
|
||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
@@ -35,14 +36,8 @@ import {
|
||||
SearchModule,
|
||||
ToastService,
|
||||
} from "@bitwarden/components";
|
||||
import { CopyCipherFieldService } from "@bitwarden/vault";
|
||||
import { CipherViewComponent, CopyCipherFieldService } from "@bitwarden/vault";
|
||||
|
||||
// FIXME: remove `src` and fix import
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { PremiumUpgradePromptService } from "../../../../../../../../libs/common/src/vault/abstractions/premium-upgrade-prompt.service";
|
||||
// FIXME: remove `src` and fix import
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { CipherViewComponent } from "../../../../../../../../libs/vault/src/cipher-view";
|
||||
import { BrowserApi } from "../../../../../platform/browser/browser-api";
|
||||
import BrowserPopupUtils from "../../../../../platform/popup/browser-popup-utils";
|
||||
import { PopOutComponent } from "../../../../../platform/popup/components/pop-out.component";
|
||||
|
||||
@@ -21,14 +21,12 @@ import { LoginView } from "@bitwarden/common/vault/models/view/login.view";
|
||||
import { ToastService } from "@bitwarden/components";
|
||||
import { PasswordRepromptService } from "@bitwarden/vault";
|
||||
|
||||
// FIXME: remove `src` and fix import
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { InlineMenuFieldQualificationService } from "../../../../../browser/src/autofill/services/inline-menu-field-qualification.service";
|
||||
import {
|
||||
AutoFillOptions,
|
||||
AutofillService,
|
||||
PageDetail,
|
||||
} from "../../../autofill/services/abstractions/autofill.service";
|
||||
import { InlineMenuFieldQualificationService } from "../../../autofill/services/inline-menu-field-qualification.service";
|
||||
import { BrowserApi } from "../../../platform/browser/browser-api";
|
||||
import BrowserPopupUtils from "../../../platform/popup/browser-popup-utils";
|
||||
|
||||
|
||||
@@ -27,13 +27,11 @@ import { LoginUriView } from "@bitwarden/common/vault/models/view/login-uri.view
|
||||
import { ToastService } from "@bitwarden/components";
|
||||
import { PasswordRepromptService } from "@bitwarden/vault";
|
||||
|
||||
// FIXME: remove `src` and fix import
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { InlineMenuFieldQualificationService } from "../../../../../browser/src/autofill/services/inline-menu-field-qualification.service";
|
||||
import {
|
||||
AutofillService,
|
||||
PageDetail,
|
||||
} from "../../../autofill/services/abstractions/autofill.service";
|
||||
import { InlineMenuFieldQualificationService } from "../../../autofill/services/inline-menu-field-qualification.service";
|
||||
import { BrowserApi } from "../../../platform/browser/browser-api";
|
||||
import BrowserPopupUtils from "../../../platform/popup/browser-popup-utils";
|
||||
import { closeViewVaultItemPopout, VaultPopoutType } from "../utils/vault-popout-window";
|
||||
|
||||
@@ -17,9 +17,7 @@ import { VaultSettingsService } from "@bitwarden/common/vault/abstractions/vault
|
||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
|
||||
// FIXME: remove `src` and fix import
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { InlineMenuFieldQualificationService } from "../../../../../browser/src/autofill/services/inline-menu-field-qualification.service";
|
||||
import { InlineMenuFieldQualificationService } from "../../../autofill/services/inline-menu-field-qualification.service";
|
||||
import { BrowserApi } from "../../../platform/browser/browser-api";
|
||||
|
||||
import { VaultPopupAutofillService } from "./vault-popup-autofill.service";
|
||||
|
||||
@@ -214,6 +214,7 @@ export class VaultPopupItemsService {
|
||||
map(([hasSearchText, filters]) => {
|
||||
return hasSearchText || Object.values(filters).some((filter) => filter !== null);
|
||||
}),
|
||||
shareReplay({ bufferSize: 1, refCount: true }),
|
||||
);
|
||||
|
||||
/**
|
||||
|
||||
@@ -14,17 +14,15 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic
|
||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||
import { ThemeType } from "@bitwarden/common/platform/enums";
|
||||
import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service";
|
||||
import { BadgeModule, CheckboxModule, Option } from "@bitwarden/components";
|
||||
import {
|
||||
BadgeModule,
|
||||
CardComponent,
|
||||
CheckboxModule,
|
||||
FormFieldModule,
|
||||
Option,
|
||||
SelectModule,
|
||||
} from "@bitwarden/components";
|
||||
|
||||
// FIXME: remove `src` and fix import
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { CardComponent } from "../../../../../../libs/components/src/card/card.component";
|
||||
// FIXME: remove `src` and fix import
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { FormFieldModule } from "../../../../../../libs/components/src/form-field/form-field.module";
|
||||
// FIXME: remove `src` and fix import
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { SelectModule } from "../../../../../../libs/components/src/select/select.module";
|
||||
import { PopOutComponent } from "../../../platform/popup/components/pop-out.component";
|
||||
import { PopupCompactModeService } from "../../../platform/popup/layout/popup-compact-mode.service";
|
||||
import { PopupHeaderComponent } from "../../../platform/popup/layout/popup-header.component";
|
||||
|
||||
@@ -14,10 +14,10 @@ import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
|
||||
import { FolderView } from "@bitwarden/common/vault/models/view/folder.view";
|
||||
import { DialogService } from "@bitwarden/components";
|
||||
import { AddEditFolderDialogComponent } from "@bitwarden/vault";
|
||||
|
||||
import { PopupFooterComponent } from "../../../platform/popup/layout/popup-footer.component";
|
||||
import { PopupHeaderComponent } from "../../../platform/popup/layout/popup-header.component";
|
||||
import { AddEditFolderDialogComponent } from "../components/vault-v2/add-edit-folder-dialog/add-edit-folder-dialog.component";
|
||||
|
||||
import { FoldersV2Component } from "./folders-v2.component";
|
||||
|
||||
@@ -27,8 +27,8 @@ import { FoldersV2Component } from "./folders-v2.component";
|
||||
template: `<ng-content></ng-content>`,
|
||||
})
|
||||
class MockPopupHeaderComponent {
|
||||
@Input() pageTitle: string;
|
||||
@Input() backAction: () => void;
|
||||
@Input() pageTitle: string = "";
|
||||
@Input() backAction: () => void = () => {};
|
||||
}
|
||||
|
||||
@Component({
|
||||
@@ -37,14 +37,15 @@ class MockPopupHeaderComponent {
|
||||
template: `<ng-content></ng-content>`,
|
||||
})
|
||||
class MockPopupFooterComponent {
|
||||
@Input() pageTitle: string;
|
||||
@Input() pageTitle: string = "";
|
||||
}
|
||||
|
||||
describe("FoldersV2Component", () => {
|
||||
let component: FoldersV2Component;
|
||||
let fixture: ComponentFixture<FoldersV2Component>;
|
||||
const folderViews$ = new BehaviorSubject<FolderView[]>([]);
|
||||
const open = jest.fn();
|
||||
const open = jest.spyOn(AddEditFolderDialogComponent, "open");
|
||||
const mockDialogService = { open: jest.fn() };
|
||||
|
||||
beforeEach(async () => {
|
||||
open.mockClear();
|
||||
@@ -68,7 +69,7 @@ describe("FoldersV2Component", () => {
|
||||
imports: [MockPopupHeaderComponent, MockPopupFooterComponent],
|
||||
},
|
||||
})
|
||||
.overrideProvider(DialogService, { useValue: { open } })
|
||||
.overrideProvider(DialogService, { useValue: mockDialogService })
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(FoldersV2Component);
|
||||
@@ -101,9 +102,7 @@ describe("FoldersV2Component", () => {
|
||||
|
||||
editButton.triggerEventHandler("click");
|
||||
|
||||
expect(open).toHaveBeenCalledWith(AddEditFolderDialogComponent, {
|
||||
data: { editFolderConfig: { folder } },
|
||||
});
|
||||
expect(open).toHaveBeenCalledWith(mockDialogService, { editFolderConfig: { folder } });
|
||||
});
|
||||
|
||||
it("opens add dialog for new folder when there are no folders", () => {
|
||||
@@ -114,6 +113,6 @@ describe("FoldersV2Component", () => {
|
||||
|
||||
addButton.triggerEventHandler("click");
|
||||
|
||||
expect(open).toHaveBeenCalledWith(AddEditFolderDialogComponent, { data: {} });
|
||||
expect(open).toHaveBeenCalledWith(mockDialogService, {});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -12,25 +12,14 @@ import {
|
||||
ButtonModule,
|
||||
DialogService,
|
||||
IconButtonModule,
|
||||
ItemModule,
|
||||
NoItemsModule,
|
||||
} from "@bitwarden/components";
|
||||
import { VaultIcons } from "@bitwarden/vault";
|
||||
import { AddEditFolderDialogComponent, VaultIcons } from "@bitwarden/vault";
|
||||
|
||||
// FIXME: remove `src` and fix import
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { ItemGroupComponent } from "../../../../../../libs/components/src/item/item-group.component";
|
||||
// FIXME: remove `src` and fix import
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { ItemModule } from "../../../../../../libs/components/src/item/item.module";
|
||||
// FIXME: remove `src` and fix import
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { NoItemsModule } from "../../../../../../libs/components/src/no-items/no-items.module";
|
||||
import { PopOutComponent } from "../../../platform/popup/components/pop-out.component";
|
||||
import { PopupHeaderComponent } from "../../../platform/popup/layout/popup-header.component";
|
||||
import { PopupPageComponent } from "../../../platform/popup/layout/popup-page.component";
|
||||
import {
|
||||
AddEditFolderDialogComponent,
|
||||
AddEditFolderDialogData,
|
||||
} from "../components/vault-v2/add-edit-folder-dialog/add-edit-folder-dialog.component";
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
@@ -42,7 +31,6 @@ import {
|
||||
PopupPageComponent,
|
||||
PopupHeaderComponent,
|
||||
ItemModule,
|
||||
ItemGroupComponent,
|
||||
NoItemsModule,
|
||||
IconButtonModule,
|
||||
ButtonModule,
|
||||
@@ -78,8 +66,6 @@ export class FoldersV2Component {
|
||||
// If a folder is provided, the edit variant should be shown
|
||||
const editFolderConfig = folder ? { folder } : undefined;
|
||||
|
||||
this.dialogService.open<unknown, AddEditFolderDialogData>(AddEditFolderDialogComponent, {
|
||||
data: { editFolderConfig },
|
||||
});
|
||||
AddEditFolderDialogComponent.open(this.dialogService, { editFolderConfig });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -123,6 +123,9 @@ export class EditCommand {
|
||||
"Item does not belong to an organization. Consider moving it first.",
|
||||
);
|
||||
}
|
||||
if (!cipher.viewPassword) {
|
||||
return Response.noEditPermission();
|
||||
}
|
||||
|
||||
cipher.collectionIds = req;
|
||||
try {
|
||||
|
||||
@@ -39,6 +39,10 @@ export class Response {
|
||||
return Response.error("Not found.");
|
||||
}
|
||||
|
||||
static noEditPermission(): Response {
|
||||
return Response.error("You do not have permission to edit this item");
|
||||
}
|
||||
|
||||
static badRequest(message: string): Response {
|
||||
return Response.error(message);
|
||||
}
|
||||
|
||||
1
apps/desktop/desktop_native/.gitignore
vendored
1
apps/desktop/desktop_native/.gitignore
vendored
@@ -5,3 +5,4 @@ index.node
|
||||
npm-debug.log*
|
||||
*.node
|
||||
dist
|
||||
windows_pluginauthenticator_bindings.rs
|
||||
|
||||
72
apps/desktop/desktop_native/Cargo.lock
generated
72
apps/desktop/desktop_native/Cargo.lock
generated
@@ -410,6 +410,26 @@ dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bindgen"
|
||||
version = "0.71.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5f58bf3d7db68cfbac37cfc485a8d711e87e064c3d0fe0435b92f7a407f9d6b3"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"cexpr",
|
||||
"clang-sys",
|
||||
"itertools",
|
||||
"log",
|
||||
"prettyplease",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"regex",
|
||||
"rustc-hash",
|
||||
"shlex",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "2.8.0"
|
||||
@@ -553,6 +573,15 @@ dependencies = [
|
||||
"shlex",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cexpr"
|
||||
version = "0.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766"
|
||||
dependencies = [
|
||||
"nom",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
version = "1.0.0"
|
||||
@@ -593,6 +622,17 @@ dependencies = [
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clang-sys"
|
||||
version = "1.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4"
|
||||
dependencies = [
|
||||
"glob",
|
||||
"libc",
|
||||
"libloading",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "4.5.27"
|
||||
@@ -1458,6 +1498,15 @@ version = "1.70.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf"
|
||||
|
||||
[[package]]
|
||||
name = "itertools"
|
||||
version = "0.13.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186"
|
||||
dependencies = [
|
||||
"either",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "1.0.14"
|
||||
@@ -2259,6 +2308,16 @@ dependencies = [
|
||||
"zerocopy",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "prettyplease"
|
||||
version = "0.2.27"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "483f8c21f64f3ea09fe0f30f5d48c3e8eefe5dac9129f0075f76593b4c1da705"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro-crate"
|
||||
version = "3.2.0"
|
||||
@@ -2437,6 +2496,12 @@ version = "0.1.24"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f"
|
||||
|
||||
[[package]]
|
||||
name = "rustc-hash"
|
||||
version = "2.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c7fb8039b3032c191086b10f11f319a6e99e1e82889c5cc6046f515c9db1d497"
|
||||
|
||||
[[package]]
|
||||
name = "rustc_version"
|
||||
version = "0.4.1"
|
||||
@@ -3447,6 +3512,13 @@ dependencies = [
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-plugin-authenticator"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-registry"
|
||||
version = "0.4.0"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[workspace]
|
||||
resolver = "2"
|
||||
members = ["napi", "core", "proxy", "macos_provider"]
|
||||
members = ["napi", "core", "proxy", "macos_provider", "windows-plugin-authenticator"]
|
||||
|
||||
[workspace.dependencies]
|
||||
anyhow = "=1.0.94"
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
[package]
|
||||
name = "windows-plugin-authenticator"
|
||||
version = "0.0.0"
|
||||
edition = "2021"
|
||||
license = "GPL-3.0"
|
||||
publish = false
|
||||
|
||||
[target.'cfg(target_os = "windows")'.build-dependencies]
|
||||
bindgen = "0.71.1"
|
||||
@@ -0,0 +1,23 @@
|
||||
# windows-plugin-authenticator
|
||||
|
||||
This is an internal crate that's meant to be a safe abstraction layer over the generated Rust bindings for the Windows WebAuthn Plugin Authenticator API's.
|
||||
|
||||
You can find more information about the Windows WebAuthn API's [here](https://github.com/microsoft/webauthn).
|
||||
|
||||
## Building
|
||||
|
||||
To build this crate, set the following environment variables:
|
||||
|
||||
- `LIBCLANG_PATH` -> the path to the `bin` directory of your LLVM install ([more info](https://rust-lang.github.io/rust-bindgen/requirements.html?highlight=libclang_path#installing-clang))
|
||||
|
||||
### Bash Example
|
||||
|
||||
```
|
||||
export LIBCLANG_PATH='C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Tools\Llvm\x64\bin'
|
||||
```
|
||||
|
||||
### PowerShell Example
|
||||
|
||||
```
|
||||
$env:LIBCLANG_PATH = 'C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Tools\Llvm\x64\bin'
|
||||
```
|
||||
@@ -0,0 +1,22 @@
|
||||
fn main() {
|
||||
#[cfg(target_os = "windows")]
|
||||
windows();
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
fn windows() {
|
||||
let out_dir = std::env::var("OUT_DIR").expect("OUT_DIR not set");
|
||||
|
||||
let bindings = bindgen::Builder::default()
|
||||
.header("pluginauthenticator.hpp")
|
||||
.parse_callbacks(Box::new(bindgen::CargoCallbacks::new()))
|
||||
.generate()
|
||||
.expect("Unable to generate bindings.");
|
||||
|
||||
bindings
|
||||
.write_to_file(format!(
|
||||
"{}\\windows_pluginauthenticator_bindings.rs",
|
||||
out_dir
|
||||
))
|
||||
.expect("Couldn't write bindings.");
|
||||
}
|
||||
@@ -0,0 +1,231 @@
|
||||
/*
|
||||
Bitwarden's pluginauthenticator.hpp
|
||||
|
||||
Source: https://github.com/microsoft/webauthn/blob/master/experimental/pluginauthenticator.h
|
||||
|
||||
This is a C++ header file, so the extension has been manually
|
||||
changed from `.h` to `.hpp`, so bindgen will automatically
|
||||
generate the correct C++ bindings.
|
||||
|
||||
More Info: https://rust-lang.github.io/rust-bindgen/cpp.html
|
||||
*/
|
||||
|
||||
/* this ALWAYS GENERATED file contains the definitions for the interfaces */
|
||||
|
||||
/* File created by MIDL compiler version 8.01.0628 */
|
||||
/* @@MIDL_FILE_HEADING( ) */
|
||||
|
||||
/* verify that the <rpcndr.h> version is high enough to compile this file*/
|
||||
#ifndef __REQUIRED_RPCNDR_H_VERSION__
|
||||
#define __REQUIRED_RPCNDR_H_VERSION__ 501
|
||||
#endif
|
||||
|
||||
/* verify that the <rpcsal.h> version is high enough to compile this file*/
|
||||
#ifndef __REQUIRED_RPCSAL_H_VERSION__
|
||||
#define __REQUIRED_RPCSAL_H_VERSION__ 100
|
||||
#endif
|
||||
|
||||
#include "rpc.h"
|
||||
#include "rpcndr.h"
|
||||
|
||||
#ifndef __RPCNDR_H_VERSION__
|
||||
#error this stub requires an updated version of <rpcndr.h>
|
||||
#endif /* __RPCNDR_H_VERSION__ */
|
||||
|
||||
#ifndef COM_NO_WINDOWS_H
|
||||
#include "windows.h"
|
||||
#include "ole2.h"
|
||||
#endif /*COM_NO_WINDOWS_H*/
|
||||
|
||||
#ifndef __pluginauthenticator_h__
|
||||
#define __pluginauthenticator_h__
|
||||
|
||||
#if defined(_MSC_VER) && (_MSC_VER >= 1020)
|
||||
#pragma once
|
||||
#endif
|
||||
|
||||
#ifndef DECLSPEC_XFGVIRT
|
||||
#if defined(_CONTROL_FLOW_GUARD_XFG)
|
||||
#define DECLSPEC_XFGVIRT(base, func) __declspec(xfg_virtual(base, func))
|
||||
#else
|
||||
#define DECLSPEC_XFGVIRT(base, func)
|
||||
#endif
|
||||
#endif
|
||||
|
||||
/* Forward Declarations */
|
||||
|
||||
#ifndef __EXPERIMENTAL_IPluginAuthenticator_FWD_DEFINED__
|
||||
#define __EXPERIMENTAL_IPluginAuthenticator_FWD_DEFINED__
|
||||
typedef interface EXPERIMENTAL_IPluginAuthenticator EXPERIMENTAL_IPluginAuthenticator;
|
||||
|
||||
#endif /* __EXPERIMENTAL_IPluginAuthenticator_FWD_DEFINED__ */
|
||||
|
||||
/* header files for imported files */
|
||||
#include "oaidl.h"
|
||||
#include "webauthn.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C"{
|
||||
#endif
|
||||
|
||||
/* interface __MIDL_itf_pluginauthenticator_0000_0000 */
|
||||
/* [local] */
|
||||
|
||||
typedef struct _EXPERIMENTAL_WEBAUTHN_PLUGIN_OPERATION_REQUEST
|
||||
{
|
||||
HWND hWnd;
|
||||
GUID transactionId;
|
||||
DWORD cbRequestSignature;
|
||||
/* [size_is] */ byte *pbRequestSignature;
|
||||
DWORD cbEncodedRequest;
|
||||
/* [size_is] */ byte *pbEncodedRequest;
|
||||
} EXPERIMENTAL_WEBAUTHN_PLUGIN_OPERATION_REQUEST;
|
||||
|
||||
typedef struct _EXPERIMENTAL_WEBAUTHN_PLUGIN_OPERATION_REQUEST *EXPERIMENTAL_PWEBAUTHN_PLUGIN_OPERATION_REQUEST;
|
||||
|
||||
typedef const EXPERIMENTAL_WEBAUTHN_PLUGIN_OPERATION_REQUEST *EXPERIMENTAL_PCWEBAUTHN_PLUGIN_OPERATION_REQUEST;
|
||||
|
||||
typedef struct _EXPERIMENTAL_WEBAUTHN_PLUGIN_OPERATION_RESPONSE
|
||||
{
|
||||
DWORD cbEncodedResponse;
|
||||
/* [size_is] */ byte *pbEncodedResponse;
|
||||
} EXPERIMENTAL_WEBAUTHN_PLUGIN_OPERATION_RESPONSE;
|
||||
|
||||
typedef struct _EXPERIMENTAL_WEBAUTHN_PLUGIN_OPERATION_RESPONSE *EXPERIMENTAL_PWEBAUTHN_PLUGIN_OPERATION_RESPONSE;
|
||||
|
||||
typedef const EXPERIMENTAL_WEBAUTHN_PLUGIN_OPERATION_RESPONSE *EXPERIMENTAL_PCWEBAUTHN_PLUGIN_OPERATION_RESPONSE;
|
||||
|
||||
typedef struct _EXPERIMENTAL_WEBAUTHN_PLUGIN_CANCEL_OPERATION_REQUEST
|
||||
{
|
||||
GUID transactionId;
|
||||
DWORD cbRequestSignature;
|
||||
/* [size_is] */ byte *pbRequestSignature;
|
||||
} EXPERIMENTAL_WEBAUTHN_PLUGIN_CANCEL_OPERATION_REQUEST;
|
||||
|
||||
typedef struct _EXPERIMENTAL_WEBAUTHN_PLUGIN_CANCEL_OPERATION_REQUEST *EXPERIMENTAL_PWEBAUTHN_PLUGIN_CANCEL_OPERATION_REQUEST;
|
||||
|
||||
typedef const EXPERIMENTAL_WEBAUTHN_PLUGIN_CANCEL_OPERATION_REQUEST *EXPERIMENTAL_PCWEBAUTHN_PLUGIN_CANCEL_OPERATION_REQUEST;
|
||||
|
||||
extern RPC_IF_HANDLE __MIDL_itf_pluginauthenticator_0000_0000_v0_0_c_ifspec;
|
||||
extern RPC_IF_HANDLE __MIDL_itf_pluginauthenticator_0000_0000_v0_0_s_ifspec;
|
||||
|
||||
#ifndef __EXPERIMENTAL_IPluginAuthenticator_INTERFACE_DEFINED__
|
||||
#define __EXPERIMENTAL_IPluginAuthenticator_INTERFACE_DEFINED__
|
||||
|
||||
/* interface EXPERIMENTAL_IPluginAuthenticator */
|
||||
/* [unique][version][uuid][object] */
|
||||
|
||||
EXTERN_C const IID IID_EXPERIMENTAL_IPluginAuthenticator;
|
||||
|
||||
#if defined(__cplusplus) && !defined(CINTERFACE)
|
||||
|
||||
MIDL_INTERFACE("e6466e9a-b2f3-47c5-b88d-89bc14a8d998")
|
||||
EXPERIMENTAL_IPluginAuthenticator : public IUnknown
|
||||
{
|
||||
public:
|
||||
virtual HRESULT STDMETHODCALLTYPE EXPERIMENTAL_PluginMakeCredential(
|
||||
/* [in] */ __RPC__in EXPERIMENTAL_PCWEBAUTHN_PLUGIN_OPERATION_REQUEST request,
|
||||
/* [out] */ __RPC__deref_out_opt EXPERIMENTAL_PWEBAUTHN_PLUGIN_OPERATION_RESPONSE *response) = 0;
|
||||
|
||||
virtual HRESULT STDMETHODCALLTYPE EXPERIMENTAL_PluginGetAssertion(
|
||||
/* [in] */ __RPC__in EXPERIMENTAL_PCWEBAUTHN_PLUGIN_OPERATION_REQUEST request,
|
||||
/* [out] */ __RPC__deref_out_opt EXPERIMENTAL_PWEBAUTHN_PLUGIN_OPERATION_RESPONSE *response) = 0;
|
||||
|
||||
virtual HRESULT STDMETHODCALLTYPE EXPERIMENTAL_PluginCancelOperation(
|
||||
/* [in] */ __RPC__in EXPERIMENTAL_PCWEBAUTHN_PLUGIN_CANCEL_OPERATION_REQUEST request) = 0;
|
||||
|
||||
};
|
||||
|
||||
#else /* C style interface */
|
||||
|
||||
typedef struct EXPERIMENTAL_IPluginAuthenticatorVtbl
|
||||
{
|
||||
BEGIN_INTERFACE
|
||||
|
||||
DECLSPEC_XFGVIRT(IUnknown, QueryInterface)
|
||||
HRESULT ( STDMETHODCALLTYPE *QueryInterface )(
|
||||
__RPC__in EXPERIMENTAL_IPluginAuthenticator * This,
|
||||
/* [in] */ __RPC__in REFIID riid,
|
||||
/* [annotation][iid_is][out] */
|
||||
_COM_Outptr_ void **ppvObject);
|
||||
|
||||
DECLSPEC_XFGVIRT(IUnknown, AddRef)
|
||||
ULONG ( STDMETHODCALLTYPE *AddRef )(
|
||||
__RPC__in EXPERIMENTAL_IPluginAuthenticator * This);
|
||||
|
||||
DECLSPEC_XFGVIRT(IUnknown, Release)
|
||||
ULONG ( STDMETHODCALLTYPE *Release )(
|
||||
__RPC__in EXPERIMENTAL_IPluginAuthenticator * This);
|
||||
|
||||
DECLSPEC_XFGVIRT(EXPERIMENTAL_IPluginAuthenticator, EXPERIMENTAL_PluginMakeCredential)
|
||||
HRESULT ( STDMETHODCALLTYPE *EXPERIMENTAL_PluginMakeCredential )(
|
||||
__RPC__in EXPERIMENTAL_IPluginAuthenticator * This,
|
||||
/* [in] */ __RPC__in EXPERIMENTAL_PCWEBAUTHN_PLUGIN_OPERATION_REQUEST request,
|
||||
/* [out] */ __RPC__deref_out_opt EXPERIMENTAL_PWEBAUTHN_PLUGIN_OPERATION_RESPONSE *response);
|
||||
|
||||
DECLSPEC_XFGVIRT(EXPERIMENTAL_IPluginAuthenticator, EXPERIMENTAL_PluginGetAssertion)
|
||||
HRESULT ( STDMETHODCALLTYPE *EXPERIMENTAL_PluginGetAssertion )(
|
||||
__RPC__in EXPERIMENTAL_IPluginAuthenticator * This,
|
||||
/* [in] */ __RPC__in EXPERIMENTAL_PCWEBAUTHN_PLUGIN_OPERATION_REQUEST request,
|
||||
/* [out] */ __RPC__deref_out_opt EXPERIMENTAL_PWEBAUTHN_PLUGIN_OPERATION_RESPONSE *response);
|
||||
|
||||
DECLSPEC_XFGVIRT(EXPERIMENTAL_IPluginAuthenticator, EXPERIMENTAL_PluginCancelOperation)
|
||||
HRESULT ( STDMETHODCALLTYPE *EXPERIMENTAL_PluginCancelOperation )(
|
||||
__RPC__in EXPERIMENTAL_IPluginAuthenticator * This,
|
||||
/* [in] */ __RPC__in EXPERIMENTAL_PCWEBAUTHN_PLUGIN_CANCEL_OPERATION_REQUEST request);
|
||||
|
||||
END_INTERFACE
|
||||
} EXPERIMENTAL_IPluginAuthenticatorVtbl;
|
||||
|
||||
interface EXPERIMENTAL_IPluginAuthenticator
|
||||
{
|
||||
CONST_VTBL struct EXPERIMENTAL_IPluginAuthenticatorVtbl *lpVtbl;
|
||||
};
|
||||
|
||||
#ifdef COBJMACROS
|
||||
|
||||
|
||||
#define EXPERIMENTAL_IPluginAuthenticator_QueryInterface(This,riid,ppvObject) \
|
||||
( (This)->lpVtbl -> QueryInterface(This,riid,ppvObject) )
|
||||
|
||||
#define EXPERIMENTAL_IPluginAuthenticator_AddRef(This) \
|
||||
( (This)->lpVtbl -> AddRef(This) )
|
||||
|
||||
#define EXPERIMENTAL_IPluginAuthenticator_Release(This) \
|
||||
( (This)->lpVtbl -> Release(This) )
|
||||
|
||||
|
||||
#define EXPERIMENTAL_IPluginAuthenticator_EXPERIMENTAL_PluginMakeCredential(This,request,response) \
|
||||
( (This)->lpVtbl -> EXPERIMENTAL_PluginMakeCredential(This,request,response) )
|
||||
|
||||
#define EXPERIMENTAL_IPluginAuthenticator_EXPERIMENTAL_PluginGetAssertion(This,request,response) \
|
||||
( (This)->lpVtbl -> EXPERIMENTAL_PluginGetAssertion(This,request,response) )
|
||||
|
||||
#define EXPERIMENTAL_IPluginAuthenticator_EXPERIMENTAL_PluginCancelOperation(This,request) \
|
||||
( (This)->lpVtbl -> EXPERIMENTAL_PluginCancelOperation(This,request) )
|
||||
|
||||
#endif /* COBJMACROS */
|
||||
|
||||
#endif /* C style interface */
|
||||
|
||||
#endif /* __EXPERIMENTAL_IPluginAuthenticator_INTERFACE_DEFINED__ */
|
||||
|
||||
/* Additional Prototypes for ALL interfaces */
|
||||
|
||||
unsigned long __RPC_USER HWND_UserSize( __RPC__in unsigned long *, unsigned long , __RPC__in HWND * );
|
||||
unsigned char * __RPC_USER HWND_UserMarshal( __RPC__in unsigned long *, __RPC__inout_xcount(0) unsigned char *, __RPC__in HWND * );
|
||||
unsigned char * __RPC_USER HWND_UserUnmarshal(__RPC__in unsigned long *, __RPC__in_xcount(0) unsigned char *, __RPC__out HWND * );
|
||||
void __RPC_USER HWND_UserFree( __RPC__in unsigned long *, __RPC__in HWND * );
|
||||
|
||||
unsigned long __RPC_USER HWND_UserSize64( __RPC__in unsigned long *, unsigned long , __RPC__in HWND * );
|
||||
unsigned char * __RPC_USER HWND_UserMarshal64( __RPC__in unsigned long *, __RPC__inout_xcount(0) unsigned char *, __RPC__in HWND * );
|
||||
unsigned char * __RPC_USER HWND_UserUnmarshal64(__RPC__in unsigned long *, __RPC__in_xcount(0) unsigned char *, __RPC__out HWND * );
|
||||
void __RPC_USER HWND_UserFree64( __RPC__in unsigned long *, __RPC__in HWND * );
|
||||
|
||||
/* end of Additional Prototypes */
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,11 @@
|
||||
#![cfg(target_os = "windows")]
|
||||
|
||||
mod pa;
|
||||
|
||||
pub fn get_version_number() -> u64 {
|
||||
unsafe { pa::WebAuthNGetApiVersionNumber() }.into()
|
||||
}
|
||||
|
||||
pub fn add_authenticator() {
|
||||
unimplemented!();
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
/*
|
||||
The 'pa' (plugin authenticator) module will contain the generated
|
||||
bindgen code.
|
||||
|
||||
The attributes below will suppress warnings from the generated code.
|
||||
*/
|
||||
|
||||
#![cfg(target_os = "windows")]
|
||||
#![allow(clippy::all)]
|
||||
#![allow(warnings)]
|
||||
|
||||
include!(concat!(
|
||||
env!("OUT_DIR"),
|
||||
"/windows_pluginauthenticator_bindings.rs"
|
||||
));
|
||||
@@ -16,6 +16,8 @@
|
||||
<true/>
|
||||
<key>com.apple.security.files.user-selected.read-write</key>
|
||||
<true/>
|
||||
<key>com.apple.security.device.usb</key>
|
||||
<true/>
|
||||
<!--
|
||||
<key>com.apple.developer.authentication-services.autofill-credential-provider</key>
|
||||
<true/>
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
type="checkbox"
|
||||
[(ngModel)]="$any(c).checked"
|
||||
name="Collection[{{ i }}].Checked"
|
||||
[disabled]="!cipher.canAssignToCollections"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,158 +0,0 @@
|
||||
<!-- Please remove this disable statement when editing this file! -->
|
||||
<!-- eslint-disable tailwindcss/no-custom-classname -->
|
||||
<form
|
||||
#form
|
||||
(ngSubmit)="submit()"
|
||||
[appApiAction]="formPromise"
|
||||
class="tw-container tw-mx-auto"
|
||||
[formGroup]="formGroup"
|
||||
>
|
||||
<div>
|
||||
<div class="tw-mb-3">
|
||||
<bit-form-field>
|
||||
<bit-label>{{ "emailAddress" | i18n }}</bit-label>
|
||||
<input
|
||||
id="register-form_input_email"
|
||||
bitInput
|
||||
type="email"
|
||||
formControlName="email"
|
||||
[attr.readonly]="queryParamFromOrgInvite ? true : null"
|
||||
/>
|
||||
<bit-hint>{{ "emailAddressDesc" | i18n }}</bit-hint>
|
||||
</bit-form-field>
|
||||
</div>
|
||||
|
||||
<div class="tw-mb-3">
|
||||
<bit-form-field>
|
||||
<bit-label>{{ "name" | i18n }}</bit-label>
|
||||
<input id="register-form_input_name" bitInput type="text" formControlName="name" />
|
||||
<bit-hint>{{ "yourNameDesc" | i18n }}</bit-hint>
|
||||
</bit-form-field>
|
||||
</div>
|
||||
|
||||
<div class="tw-mb-3">
|
||||
<auth-password-callout [policy]="enforcedPolicyOptions" *ngIf="enforcedPolicyOptions">
|
||||
</auth-password-callout>
|
||||
<bit-form-field>
|
||||
<bit-label>{{ "masterPass" | i18n }}</bit-label>
|
||||
<input
|
||||
id="register-form_input_master-password"
|
||||
bitInput
|
||||
type="password"
|
||||
formControlName="masterPassword"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
bitSuffix
|
||||
bitIconButton
|
||||
bitPasswordInputToggle
|
||||
[(toggled)]="showPassword"
|
||||
></button>
|
||||
<bit-hint>
|
||||
<span class="tw-font-semibold">{{ "important" | i18n }}</span>
|
||||
{{ "masterPassImportant" | i18n }} {{ characterMinimumMessage }}
|
||||
</bit-hint>
|
||||
</bit-form-field>
|
||||
<app-password-strength
|
||||
[password]="formGroup.get('masterPassword')?.value"
|
||||
[email]="formGroup.get('email')?.value"
|
||||
[name]="formGroup.get('name')?.value"
|
||||
[showText]="true"
|
||||
(passwordStrengthResult)="getStrengthResult($event)"
|
||||
>
|
||||
</app-password-strength>
|
||||
</div>
|
||||
|
||||
<div class="tw-mb-3">
|
||||
<bit-form-field>
|
||||
<bit-label>{{ "reTypeMasterPass" | i18n }}</bit-label>
|
||||
<input
|
||||
id="register-form_input_confirm-master-password"
|
||||
bitInput
|
||||
type="password"
|
||||
formControlName="confirmMasterPassword"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
bitSuffix
|
||||
bitIconButton
|
||||
bitPasswordInputToggle
|
||||
[(toggled)]="showPassword"
|
||||
></button>
|
||||
</bit-form-field>
|
||||
</div>
|
||||
|
||||
<div class="tw-mb-3">
|
||||
<bit-form-field>
|
||||
<bit-label>{{ "masterPassHintLabel" | i18n }}</bit-label>
|
||||
<input id="register-form_input_hint" bitInput type="text" formControlName="hint" />
|
||||
<bit-hint>{{ "masterPassHintDesc" | i18n }}</bit-hint>
|
||||
</bit-form-field>
|
||||
</div>
|
||||
|
||||
<div [hidden]="!showCaptcha()">
|
||||
<iframe id="hcaptcha_iframe" height="80" sandbox="allow-scripts allow-same-origin"></iframe>
|
||||
</div>
|
||||
<div class="tw-mb-4 tw-flex tw-items-start">
|
||||
<input
|
||||
class="mt-1"
|
||||
type="checkbox"
|
||||
bitCheckbox
|
||||
id="checkForBreaches"
|
||||
name="CheckBreach"
|
||||
formControlName="checkForBreaches"
|
||||
/>
|
||||
<bit-label for="checkForBreaches"> {{ "checkForBreaches" | i18n }}</bit-label>
|
||||
</div>
|
||||
<div class="tw-mb-3 tw-flex tw-items-start" *ngIf="showTerms">
|
||||
<input
|
||||
class="mt-1"
|
||||
id="register-form-input-accept-policies"
|
||||
bitCheckbox
|
||||
type="checkbox"
|
||||
formControlName="acceptPolicies"
|
||||
/>
|
||||
|
||||
<bit-label for="register-form-input-accept-policies">
|
||||
{{ "acceptPolicies" | i18n }}<br />
|
||||
<a bitLink href="https://bitwarden.com/terms/" target="_blank" rel="noreferrer">{{
|
||||
"termsOfService" | i18n
|
||||
}}</a
|
||||
>,
|
||||
<a bitLink href="https://bitwarden.com/privacy/" target="_blank" rel="noreferrer">{{
|
||||
"privacyPolicy" | i18n
|
||||
}}</a>
|
||||
</bit-label>
|
||||
</div>
|
||||
|
||||
<div class="tw-space-x-2 tw-pt-2">
|
||||
<ng-container *ngIf="!accountCreated">
|
||||
<button
|
||||
[block]="true"
|
||||
type="submit"
|
||||
buttonType="primary"
|
||||
bitButton
|
||||
[loading]="form.loading"
|
||||
>
|
||||
{{ "createAccount" | i18n }}
|
||||
</button>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="accountCreated">
|
||||
<button
|
||||
[block]="true"
|
||||
type="submit"
|
||||
buttonType="primary"
|
||||
bitButton
|
||||
[loading]="form.loading"
|
||||
>
|
||||
{{ "logIn" | i18n }}
|
||||
</button>
|
||||
</ng-container>
|
||||
</div>
|
||||
<p class="tw-m-0 tw-mt-5 tw-text-sm">
|
||||
{{ "alreadyHaveAccount" | i18n }}
|
||||
<a bitLink routerLink="/login">{{ "logIn" | i18n }}</a>
|
||||
</p>
|
||||
<bit-error-summary *ngIf="showErrorSummary" [formGroup]="formGroup"></bit-error-summary>
|
||||
</div>
|
||||
</form>
|
||||
@@ -1,115 +0,0 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Component, Input, OnInit } from "@angular/core";
|
||||
import { UntypedFormBuilder } from "@angular/forms";
|
||||
import { Router } from "@angular/router";
|
||||
|
||||
import { RegisterComponent as BaseRegisterComponent } from "@bitwarden/angular/auth/components/register.component";
|
||||
import { FormValidationErrorsService } from "@bitwarden/angular/platform/abstractions/form-validation-errors.service";
|
||||
import { LoginStrategyServiceAbstraction } from "@bitwarden/auth/common";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { AuditService } from "@bitwarden/common/abstractions/audit.service";
|
||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options";
|
||||
import { ReferenceEventRequest } from "@bitwarden/common/models/request/reference-event.request";
|
||||
import { RegisterRequest } from "@bitwarden/common/models/request/register.request";
|
||||
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { DialogService, ToastService } from "@bitwarden/components";
|
||||
import { KeyService } from "@bitwarden/key-management";
|
||||
|
||||
import { AcceptOrganizationInviteService } from "../organization-invite/accept-organization.service";
|
||||
|
||||
@Component({
|
||||
selector: "app-register-form",
|
||||
templateUrl: "./register-form.component.html",
|
||||
})
|
||||
export class RegisterFormComponent extends BaseRegisterComponent implements OnInit {
|
||||
@Input() queryParamEmail: string;
|
||||
@Input() queryParamFromOrgInvite: boolean;
|
||||
@Input() enforcedPolicyOptions: MasterPasswordPolicyOptions;
|
||||
@Input() referenceDataValue: ReferenceEventRequest;
|
||||
|
||||
showErrorSummary = false;
|
||||
characterMinimumMessage: string;
|
||||
|
||||
constructor(
|
||||
formValidationErrorService: FormValidationErrorsService,
|
||||
formBuilder: UntypedFormBuilder,
|
||||
loginStrategyService: LoginStrategyServiceAbstraction,
|
||||
router: Router,
|
||||
i18nService: I18nService,
|
||||
keyService: KeyService,
|
||||
apiService: ApiService,
|
||||
platformUtilsService: PlatformUtilsService,
|
||||
private policyService: PolicyService,
|
||||
environmentService: EnvironmentService,
|
||||
logService: LogService,
|
||||
auditService: AuditService,
|
||||
dialogService: DialogService,
|
||||
acceptOrgInviteService: AcceptOrganizationInviteService,
|
||||
toastService: ToastService,
|
||||
) {
|
||||
super(
|
||||
formValidationErrorService,
|
||||
formBuilder,
|
||||
loginStrategyService,
|
||||
router,
|
||||
i18nService,
|
||||
keyService,
|
||||
apiService,
|
||||
platformUtilsService,
|
||||
environmentService,
|
||||
logService,
|
||||
auditService,
|
||||
dialogService,
|
||||
toastService,
|
||||
);
|
||||
this.modifyRegisterRequest = async (request: RegisterRequest) => {
|
||||
// Org invites are deep linked. Non-existent accounts are redirected to the register page.
|
||||
// Org user id and token are included here only for validation and two factor purposes.
|
||||
const orgInvite = await acceptOrgInviteService.getOrganizationInvite();
|
||||
if (orgInvite != null) {
|
||||
request.organizationUserId = orgInvite.organizationUserId;
|
||||
request.token = orgInvite.token;
|
||||
}
|
||||
// Invite is accepted after login (on deep link redirect).
|
||||
};
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
await super.ngOnInit();
|
||||
this.referenceData = this.referenceDataValue;
|
||||
if (this.queryParamEmail) {
|
||||
this.formGroup.get("email")?.setValue(this.queryParamEmail);
|
||||
}
|
||||
|
||||
if (this.enforcedPolicyOptions != null && this.enforcedPolicyOptions.minLength > 0) {
|
||||
this.characterMinimumMessage = "";
|
||||
} else {
|
||||
this.characterMinimumMessage = this.i18nService.t("characterMinimum", this.minimumLength);
|
||||
}
|
||||
}
|
||||
|
||||
async submit() {
|
||||
if (
|
||||
this.enforcedPolicyOptions != null &&
|
||||
!this.policyService.evaluateMasterPassword(
|
||||
this.passwordStrengthResult.score,
|
||||
this.formGroup.value.masterPassword,
|
||||
this.enforcedPolicyOptions,
|
||||
)
|
||||
) {
|
||||
this.toastService.showToast({
|
||||
variant: "error",
|
||||
title: this.i18nService.t("errorOccurred"),
|
||||
message: this.i18nService.t("masterPasswordPolicyRequirementsNotMet"),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
await super.submit(false);
|
||||
}
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
import { NgModule } from "@angular/core";
|
||||
|
||||
import { PasswordCalloutComponent } from "@bitwarden/auth/angular";
|
||||
|
||||
import { SharedModule } from "../../shared";
|
||||
|
||||
import { RegisterFormComponent } from "./register-form.component";
|
||||
|
||||
@NgModule({
|
||||
imports: [SharedModule, PasswordCalloutComponent],
|
||||
declarations: [RegisterFormComponent],
|
||||
exports: [RegisterFormComponent],
|
||||
})
|
||||
export class RegisterFormModule {}
|
||||
@@ -118,7 +118,13 @@
|
||||
) | currency: "$"
|
||||
}}
|
||||
</b>
|
||||
<span class="tw-text-xs tw-px-0"> /{{ "monthPerMember" | i18n }}</span>
|
||||
<span class="tw-text-xs tw-px-0">
|
||||
/{{
|
||||
selectableProduct.productTier === productTypes.Families
|
||||
? "month"
|
||||
: ("monthPerMember" | i18n)
|
||||
}}</span
|
||||
>
|
||||
<b class="tw-text-sm tw-font-semibold">
|
||||
<ng-container
|
||||
*ngIf="selectableProduct.PasswordManager.hasAdditionalSeatsOption"
|
||||
|
||||
@@ -1045,10 +1045,12 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy {
|
||||
this.estimatedTax = invoice.taxAmount;
|
||||
})
|
||||
.catch((error) => {
|
||||
const translatedMessage = this.i18nService.t(error.message);
|
||||
this.toastService.showToast({
|
||||
title: "",
|
||||
variant: "error",
|
||||
message: this.i18nService.t(error.message),
|
||||
message:
|
||||
!translatedMessage || translatedMessage === "" ? error.message : translatedMessage,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -433,7 +433,11 @@
|
||||
<p class="tw-text-muted tw-italic tw-mb-3 tw-block" bitTypography="body2">
|
||||
{{ paymentDesc }}
|
||||
</p>
|
||||
<app-payment *ngIf="createOrganization || upgradeRequiresPaymentMethod"></app-payment>
|
||||
<app-payment
|
||||
*ngIf="createOrganization || upgradeRequiresPaymentMethod"
|
||||
[showAccountCredit]="false"
|
||||
>
|
||||
</app-payment>
|
||||
<app-manage-tax-information
|
||||
class="tw-my-4"
|
||||
[showTaxIdField]="showTaxIdField"
|
||||
|
||||
@@ -294,6 +294,10 @@
|
||||
</ng-template>
|
||||
|
||||
<ng-template #setupSelfHost>
|
||||
<ng-container *ngIf="userOrg.hasReseller && resellerSeatsRemainingMessage">
|
||||
<h2 bitTypography="h2" class="tw-mt-7">{{ "manageSubscription" | i18n }}</h2>
|
||||
<p bitTypography="body1">{{ resellerSeatsRemainingMessage }}</p>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="showSelfHost">
|
||||
<h2 bitTypography="h2" class="tw-mt-7">
|
||||
{{ "selfHostingTitleProper" | i18n }}
|
||||
|
||||
@@ -4,13 +4,17 @@ import { Component, OnDestroy, OnInit } from "@angular/core";
|
||||
import { ActivatedRoute } from "@angular/router";
|
||||
import { firstValueFrom, lastValueFrom, Observable, Subject } from "rxjs";
|
||||
|
||||
import { OrganizationUserApiService } from "@bitwarden/admin-console/common";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction";
|
||||
import {
|
||||
getOrganizationById,
|
||||
OrganizationService,
|
||||
} from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { OrganizationApiKeyType } from "@bitwarden/common/admin-console/enums";
|
||||
import {
|
||||
OrganizationApiKeyType,
|
||||
OrganizationUserStatusType,
|
||||
} from "@bitwarden/common/admin-console/enums";
|
||||
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";
|
||||
@@ -61,12 +65,15 @@ export class OrganizationSubscriptionCloudComponent implements OnInit, OnDestroy
|
||||
showSubscription = true;
|
||||
showSelfHost = false;
|
||||
organizationIsManagedByConsolidatedBillingMSP = false;
|
||||
resellerSeatsRemainingMessage: string;
|
||||
|
||||
protected readonly subscriptionHiddenIcon = SubscriptionHiddenIcon;
|
||||
protected readonly teamsStarter = ProductTierType.TeamsStarter;
|
||||
|
||||
private destroy$ = new Subject<void>();
|
||||
|
||||
private seatsRemainingMessage: string;
|
||||
|
||||
constructor(
|
||||
private apiService: ApiService,
|
||||
private i18nService: I18nService,
|
||||
@@ -79,6 +86,7 @@ export class OrganizationSubscriptionCloudComponent implements OnInit, OnDestroy
|
||||
private configService: ConfigService,
|
||||
private toastService: ToastService,
|
||||
private billingApiService: BillingApiServiceAbstraction,
|
||||
private organizationUserApiService: OrganizationUserApiService,
|
||||
) {}
|
||||
|
||||
async ngOnInit() {
|
||||
@@ -104,6 +112,28 @@ export class OrganizationSubscriptionCloudComponent implements OnInit, OnDestroy
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (this.userOrg.hasReseller) {
|
||||
const allUsers = await this.organizationUserApiService.getAllUsers(this.userOrg.id);
|
||||
|
||||
const userCount = allUsers.data.filter((user) =>
|
||||
[
|
||||
OrganizationUserStatusType.Invited,
|
||||
OrganizationUserStatusType.Accepted,
|
||||
OrganizationUserStatusType.Confirmed,
|
||||
].includes(user.status),
|
||||
).length;
|
||||
|
||||
const remainingSeats = this.userOrg.seats - userCount;
|
||||
|
||||
const seatsRemaining = this.i18nService.t(
|
||||
"seatsRemaining",
|
||||
remainingSeats.toString(),
|
||||
this.userOrg.seats.toString(),
|
||||
);
|
||||
|
||||
this.resellerSeatsRemainingMessage = seatsRemaining;
|
||||
}
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
|
||||
@@ -1,17 +1,4 @@
|
||||
<app-vertical-stepper #stepper linear>
|
||||
<app-vertical-step
|
||||
label="{{ 'createAccount' | i18n | titlecase }}"
|
||||
[editable]="false"
|
||||
[subLabel]="subLabels.createAccount"
|
||||
[addSubLabelSpacing]="true"
|
||||
>
|
||||
<app-register-form
|
||||
[referenceDataValue]="referenceEventRequest"
|
||||
[isInTrialFlow]="true"
|
||||
(createdAccount)="accountCreated($event)"
|
||||
>
|
||||
</app-register-form>
|
||||
</app-vertical-step>
|
||||
<app-vertical-step
|
||||
label="{{ 'organizationInformation' | i18n | titlecase }}"
|
||||
[subLabel]="subLabels.organizationInfo"
|
||||
|
||||
@@ -1,17 +1,4 @@
|
||||
<app-vertical-stepper #stepper linear>
|
||||
<app-vertical-step
|
||||
label="{{ 'createAccount' | i18n | titlecase }}"
|
||||
[editable]="false"
|
||||
[subLabel]="createAccountLabel"
|
||||
[addSubLabelSpacing]="true"
|
||||
>
|
||||
<app-register-form
|
||||
[referenceDataValue]="referenceEventRequest"
|
||||
[isInTrialFlow]="true"
|
||||
(createdAccount)="accountCreated($event)"
|
||||
>
|
||||
</app-register-form>
|
||||
</app-vertical-step>
|
||||
<app-vertical-step
|
||||
label="{{ 'organizationInformation' | i18n | titlecase }}"
|
||||
[subLabel]="subLabels.organizationInfo"
|
||||
|
||||
@@ -19,7 +19,16 @@ import {
|
||||
} from "../../../billing/accounts/trial-initiation/trial-billing-step.component";
|
||||
import { VerticalStepperComponent } from "../../trial-initiation/vertical-stepper/vertical-stepper.component";
|
||||
import { SecretsManagerTrialFreeStepperComponent } from "../secrets-manager/secrets-manager-trial-free-stepper.component";
|
||||
import { ValidOrgParams } from "../trial-initiation.component";
|
||||
|
||||
export enum ValidOrgParams {
|
||||
families = "families",
|
||||
enterprise = "enterprise",
|
||||
teams = "teams",
|
||||
teamsStarter = "teamsStarter",
|
||||
individual = "individual",
|
||||
premium = "premium",
|
||||
free = "free",
|
||||
}
|
||||
|
||||
const trialFlowOrgs = [
|
||||
ValidOrgParams.teams,
|
||||
|
||||
@@ -1,149 +0,0 @@
|
||||
<!-- eslint-disable tailwindcss/no-custom-classname -->
|
||||
<app-secrets-manager-trial
|
||||
*ngIf="layout === layouts.secretsManager; else passwordManagerTrial"
|
||||
></app-secrets-manager-trial>
|
||||
<ng-template #passwordManagerTrial>
|
||||
<div *ngIf="accountCreateOnly" class="">
|
||||
<h1 class="tw-mt-12 tw-text-center tw-text-xl">{{ "createAccount" | i18n }}</h1>
|
||||
<div
|
||||
class="tw-min-w-xl tw-m-auto tw-max-w-xl tw-rounded tw-border tw-border-solid tw-border-secondary-300 tw-bg-background tw-p-8"
|
||||
>
|
||||
<app-register-form
|
||||
[queryParamEmail]="email"
|
||||
[queryParamFromOrgInvite]="fromOrgInvite"
|
||||
[enforcedPolicyOptions]="enforcedPolicyOptions"
|
||||
[referenceDataValue]="referenceData"
|
||||
></app-register-form>
|
||||
</div>
|
||||
</div>
|
||||
<div *ngIf="!accountCreateOnly">
|
||||
<div class="tw-absolute tw--z-10 tw--mt-48 tw-h-[28rem] tw-w-full tw-bg-background-alt2"></div>
|
||||
<div class="tw-min-w-4xl tw-mx-auto tw-flex tw-max-w-screen-xl tw-gap-12 tw-px-4">
|
||||
<div class="tw-w-1/2">
|
||||
<img
|
||||
alt="Bitwarden"
|
||||
style="height: 50px; width: 335px"
|
||||
class="tw-mt-6"
|
||||
src="../../images/register-layout/logo-horizontal-white.svg"
|
||||
/>
|
||||
|
||||
<div class="tw-pt-12">
|
||||
<!-- Layout params are used by marketing to determine left-hand content -->
|
||||
<app-default-content *ngIf="layout === layouts.default"></app-default-content>
|
||||
<app-teams-content *ngIf="layout === layouts.teams"></app-teams-content>
|
||||
<app-teams1-content *ngIf="layout === layouts.teams1"></app-teams1-content>
|
||||
<app-teams2-content *ngIf="layout === layouts.teams2"></app-teams2-content>
|
||||
<app-teams3-content *ngIf="layout === layouts.teams3"></app-teams3-content>
|
||||
<app-enterprise-content *ngIf="layout === layouts.enterprise"></app-enterprise-content>
|
||||
<app-enterprise1-content *ngIf="layout === layouts.enterprise1"></app-enterprise1-content>
|
||||
<app-enterprise2-content *ngIf="layout === layouts.enterprise2"></app-enterprise2-content>
|
||||
<app-cnet-enterprise-content
|
||||
*ngIf="layout === layouts.cnetcmpgnent"
|
||||
></app-cnet-enterprise-content>
|
||||
<app-cnet-individual-content
|
||||
*ngIf="layout === layouts.cnetcmpgnind"
|
||||
></app-cnet-individual-content>
|
||||
<app-cnet-teams-content
|
||||
*ngIf="layout === layouts.cnetcmpgnteams"
|
||||
></app-cnet-teams-content>
|
||||
<app-abm-enterprise-content
|
||||
*ngIf="layout === layouts.abmenterprise"
|
||||
></app-abm-enterprise-content>
|
||||
<app-abm-teams-content *ngIf="layout === layouts.abmteams"></app-abm-teams-content>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tw-w-1/2">
|
||||
<div *ngIf="!useTrialStepper">
|
||||
<div
|
||||
class="tw-min-w-xl tw-m-auto tw-mt-28 tw-max-w-xl tw-rounded tw-border tw-border-solid tw-border-secondary-300 tw-bg-background tw-p-8"
|
||||
>
|
||||
<app-register-form
|
||||
[queryParamEmail]="email"
|
||||
[enforcedPolicyOptions]="enforcedPolicyOptions"
|
||||
[referenceDataValue]="referenceData"
|
||||
></app-register-form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tw-pt-44" *ngIf="useTrialStepper">
|
||||
<div
|
||||
class="tw-rounded tw-border tw-border-solid tw-border-secondary-300 tw-bg-background"
|
||||
>
|
||||
<div class="tw-flex tw-h-auto tw-w-full tw-gap-5 tw-rounded-t tw-bg-secondary-100">
|
||||
<h2 class="tw-pb-4 tw-pl-4 tw-pt-5 tw-text-base tw-font-bold tw-uppercase">
|
||||
{{ freeTrialText }}
|
||||
</h2>
|
||||
<environment-selector
|
||||
class="tw-mr-4 tw-mt-6 tw-flex-shrink-0 tw-text-end"
|
||||
></environment-selector>
|
||||
</div>
|
||||
<app-vertical-stepper #stepper linear (selectionChange)="stepSelectionChange($event)">
|
||||
<app-vertical-step label="Create Account" [editable]="false" [subLabel]="email">
|
||||
<app-register-form
|
||||
[isInTrialFlow]="true"
|
||||
(createdAccount)="createdAccount($event)"
|
||||
[referenceDataValue]="referenceData"
|
||||
></app-register-form>
|
||||
</app-vertical-step>
|
||||
<app-vertical-step label="Organization Information" [subLabel]="orgInfoSubLabel">
|
||||
<app-org-info [nameOnly]="true" [formGroup]="orgInfoFormGroup"></app-org-info>
|
||||
<button
|
||||
type="button"
|
||||
bitButton
|
||||
buttonType="primary"
|
||||
[disabled]="orgInfoFormGroup.get('name').invalid"
|
||||
[loading]="loading"
|
||||
(click)="createOrganizationOnTrial()"
|
||||
>
|
||||
{{ (enableTrialPayment$ | async) ? ("startTrial" | i18n) : ("next" | i18n) }}
|
||||
</button>
|
||||
</app-vertical-step>
|
||||
<app-vertical-step
|
||||
label="Billing"
|
||||
[subLabel]="billingSubLabel"
|
||||
*ngIf="!(enableTrialPayment$ | async)"
|
||||
>
|
||||
<app-trial-billing-step
|
||||
*ngIf="stepper.selectedIndex === 2"
|
||||
[organizationInfo]="{
|
||||
name: orgInfoFormGroup.get('name').value,
|
||||
email: orgInfoFormGroup.get('email').value,
|
||||
type: trialOrganizationType,
|
||||
}"
|
||||
[subscriptionProduct]="SubscriptionProduct.PasswordManager"
|
||||
(steppedBack)="previousStep()"
|
||||
(organizationCreated)="createdOrganization($event)"
|
||||
>
|
||||
</app-trial-billing-step>
|
||||
</app-vertical-step>
|
||||
<app-vertical-step label="Confirmation Details" [applyBorder]="false">
|
||||
<app-trial-confirmation-details
|
||||
[email]="email"
|
||||
[orgLabel]="orgLabel"
|
||||
></app-trial-confirmation-details>
|
||||
<div class="tw-mb-3 tw-flex">
|
||||
<button
|
||||
type="button"
|
||||
bitButton
|
||||
buttonType="primary"
|
||||
(click)="navigateToOrgVault()"
|
||||
>
|
||||
{{ "getStarted" | i18n | titlecase }}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
bitButton
|
||||
buttonType="secondary"
|
||||
(click)="navigateToOrgInvite()"
|
||||
class="tw-ml-3 tw-inline-flex tw-items-center tw-px-3"
|
||||
>
|
||||
{{ "inviteUsers" | i18n }}
|
||||
</button>
|
||||
</div>
|
||||
</app-vertical-step>
|
||||
</app-vertical-stepper>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ng-template>
|
||||
@@ -1,336 +0,0 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { StepperSelectionEvent } from "@angular/cdk/stepper";
|
||||
import { TitleCasePipe } from "@angular/common";
|
||||
import { NO_ERRORS_SCHEMA } from "@angular/core";
|
||||
import { ComponentFixture, fakeAsync, TestBed, tick } from "@angular/core/testing";
|
||||
import { FormBuilder, UntypedFormBuilder } from "@angular/forms";
|
||||
import { ActivatedRoute, Router } from "@angular/router";
|
||||
import { RouterTestingModule } from "@angular/router/testing";
|
||||
import { mock, MockProxy } from "jest-mock-extended";
|
||||
import { BehaviorSubject, of } from "rxjs";
|
||||
|
||||
import { I18nPipe } from "@bitwarden/angular/platform/pipes/i18n.pipe";
|
||||
import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction";
|
||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options";
|
||||
import { Policy } from "@bitwarden/common/admin-console/models/domain/policy";
|
||||
import { OrganizationBillingServiceAbstraction as OrganizationBillingService } from "@bitwarden/common/billing/abstractions/organization-billing.service";
|
||||
import { PlanType } from "@bitwarden/common/billing/enums";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||
|
||||
import { AcceptOrganizationInviteService } from "../../auth/organization-invite/accept-organization.service";
|
||||
import { OrganizationInvite } from "../../auth/organization-invite/organization-invite";
|
||||
import { RouterService } from "../../core";
|
||||
import { SharedModule } from "../../shared";
|
||||
|
||||
import { TrialInitiationComponent } from "./trial-initiation.component";
|
||||
import { VerticalStepperComponent } from "./vertical-stepper/vertical-stepper.component";
|
||||
|
||||
describe("TrialInitiationComponent", () => {
|
||||
let component: TrialInitiationComponent;
|
||||
let fixture: ComponentFixture<TrialInitiationComponent>;
|
||||
const mockQueryParams = new BehaviorSubject<any>({ org: "enterprise" });
|
||||
const testOrgId = "91329456-5b9f-44b3-9279-6bb9ee6a0974";
|
||||
const formBuilder: FormBuilder = new FormBuilder();
|
||||
let routerSpy: jest.SpyInstance;
|
||||
|
||||
let stateServiceMock: MockProxy<StateService>;
|
||||
let policyApiServiceMock: MockProxy<PolicyApiServiceAbstraction>;
|
||||
let policyServiceMock: MockProxy<PolicyService>;
|
||||
let routerServiceMock: MockProxy<RouterService>;
|
||||
let acceptOrgInviteServiceMock: MockProxy<AcceptOrganizationInviteService>;
|
||||
let organizationBillingServiceMock: MockProxy<OrganizationBillingService>;
|
||||
let configServiceMock: MockProxy<ConfigService>;
|
||||
|
||||
beforeEach(() => {
|
||||
// only define services directly that we want to mock return values in this component
|
||||
stateServiceMock = mock<StateService>();
|
||||
policyApiServiceMock = mock<PolicyApiServiceAbstraction>();
|
||||
policyServiceMock = mock<PolicyService>();
|
||||
routerServiceMock = mock<RouterService>();
|
||||
acceptOrgInviteServiceMock = mock<AcceptOrganizationInviteService>();
|
||||
organizationBillingServiceMock = mock<OrganizationBillingService>();
|
||||
configServiceMock = mock<ConfigService>();
|
||||
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
TestBed.configureTestingModule({
|
||||
imports: [
|
||||
SharedModule,
|
||||
RouterTestingModule.withRoutes([
|
||||
{ path: "trial", component: TrialInitiationComponent },
|
||||
{
|
||||
path: `organizations/${testOrgId}/vault`,
|
||||
component: BlankComponent,
|
||||
},
|
||||
{
|
||||
path: `organizations/${testOrgId}/members`,
|
||||
component: BlankComponent,
|
||||
},
|
||||
]),
|
||||
],
|
||||
declarations: [TrialInitiationComponent, I18nPipe],
|
||||
providers: [
|
||||
UntypedFormBuilder,
|
||||
{
|
||||
provide: ActivatedRoute,
|
||||
useValue: {
|
||||
queryParams: mockQueryParams.asObservable(),
|
||||
},
|
||||
},
|
||||
{ provide: StateService, useValue: stateServiceMock },
|
||||
{ provide: PolicyService, useValue: policyServiceMock },
|
||||
{ provide: PolicyApiServiceAbstraction, useValue: policyApiServiceMock },
|
||||
{ provide: LogService, useValue: mock<LogService>() },
|
||||
{ provide: I18nService, useValue: mock<I18nService>() },
|
||||
{ provide: TitleCasePipe, useValue: mock<TitleCasePipe>() },
|
||||
{
|
||||
provide: VerticalStepperComponent,
|
||||
useClass: VerticalStepperStubComponent,
|
||||
},
|
||||
{
|
||||
provide: RouterService,
|
||||
useValue: routerServiceMock,
|
||||
},
|
||||
{
|
||||
provide: AcceptOrganizationInviteService,
|
||||
useValue: acceptOrgInviteServiceMock,
|
||||
},
|
||||
{
|
||||
provide: OrganizationBillingService,
|
||||
useValue: organizationBillingServiceMock,
|
||||
},
|
||||
{
|
||||
provide: ConfigService,
|
||||
useValue: configServiceMock,
|
||||
},
|
||||
],
|
||||
schemas: [NO_ERRORS_SCHEMA], // Allows child components to be ignored (such as register component)
|
||||
}).compileComponents();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(TrialInitiationComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it("should create", () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
|
||||
// These tests demonstrate mocking service calls
|
||||
describe("onInit() enforcedPolicyOptions", () => {
|
||||
it("should not set enforcedPolicyOptions if there isn't an org invite in deep linked url", async () => {
|
||||
acceptOrgInviteServiceMock.getOrganizationInvite.mockResolvedValueOnce(null);
|
||||
// Need to recreate component with new service mock
|
||||
fixture = TestBed.createComponent(TrialInitiationComponent);
|
||||
component = fixture.componentInstance;
|
||||
await component.ngOnInit();
|
||||
|
||||
expect(component.enforcedPolicyOptions).toBe(undefined);
|
||||
});
|
||||
it("should set enforcedPolicyOptions if the deep linked url has an org invite", async () => {
|
||||
// Set up service method mocks
|
||||
acceptOrgInviteServiceMock.getOrganizationInvite.mockResolvedValueOnce({
|
||||
organizationId: testOrgId,
|
||||
token: "token",
|
||||
email: "testEmail",
|
||||
organizationUserId: "123",
|
||||
} as OrganizationInvite);
|
||||
policyApiServiceMock.getPoliciesByToken.mockReturnValueOnce(
|
||||
Promise.resolve([
|
||||
{
|
||||
id: "345",
|
||||
organizationId: testOrgId,
|
||||
type: 1,
|
||||
data: {
|
||||
minComplexity: 4,
|
||||
minLength: 10,
|
||||
requireLower: null,
|
||||
requireNumbers: null,
|
||||
requireSpecial: null,
|
||||
requireUpper: null,
|
||||
},
|
||||
enabled: true,
|
||||
},
|
||||
] as Policy[]),
|
||||
);
|
||||
policyServiceMock.masterPasswordPolicyOptions$.mockReturnValue(
|
||||
of({
|
||||
minComplexity: 4,
|
||||
minLength: 10,
|
||||
requireLower: null,
|
||||
requireNumbers: null,
|
||||
requireSpecial: null,
|
||||
requireUpper: null,
|
||||
} as MasterPasswordPolicyOptions),
|
||||
);
|
||||
|
||||
// Need to recreate component with new service mocks
|
||||
fixture = TestBed.createComponent(TrialInitiationComponent);
|
||||
component = fixture.componentInstance;
|
||||
await component.ngOnInit();
|
||||
expect(component.enforcedPolicyOptions).toMatchObject({
|
||||
minComplexity: 4,
|
||||
minLength: 10,
|
||||
requireLower: null,
|
||||
requireNumbers: null,
|
||||
requireSpecial: null,
|
||||
requireUpper: null,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// These tests demonstrate route params
|
||||
describe("Route params", () => {
|
||||
it("should set org variable to be enterprise and plan to EnterpriseAnnually if org param is enterprise", fakeAsync(() => {
|
||||
mockQueryParams.next({ org: "enterprise" });
|
||||
tick(); // wait for resolution
|
||||
fixture = TestBed.createComponent(TrialInitiationComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
expect(component.org).toBe("enterprise");
|
||||
expect(component.plan).toBe(PlanType.EnterpriseAnnually);
|
||||
}));
|
||||
it("should not set org variable if no org param is provided", fakeAsync(() => {
|
||||
mockQueryParams.next({});
|
||||
tick(); // wait for resolution
|
||||
fixture = TestBed.createComponent(TrialInitiationComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
expect(component.org).toBe("");
|
||||
expect(component.accountCreateOnly).toBe(true);
|
||||
}));
|
||||
it("should not set the org if org param is invalid ", fakeAsync(async () => {
|
||||
mockQueryParams.next({ org: "hahahaha" });
|
||||
tick(); // wait for resolution
|
||||
fixture = TestBed.createComponent(TrialInitiationComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
expect(component.org).toBe("");
|
||||
expect(component.accountCreateOnly).toBe(true);
|
||||
}));
|
||||
it("should set the layout variable if layout param is valid ", fakeAsync(async () => {
|
||||
mockQueryParams.next({ layout: "teams1" });
|
||||
tick(); // wait for resolution
|
||||
fixture = TestBed.createComponent(TrialInitiationComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
expect(component.layout).toBe("teams1");
|
||||
expect(component.accountCreateOnly).toBe(false);
|
||||
}));
|
||||
it("should not set the layout variable and leave as 'default' if layout param is invalid ", fakeAsync(async () => {
|
||||
mockQueryParams.next({ layout: "asdfasdf" });
|
||||
tick(); // wait for resolution
|
||||
fixture = TestBed.createComponent(TrialInitiationComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
component.ngOnInit();
|
||||
expect(component.layout).toBe("default");
|
||||
expect(component.accountCreateOnly).toBe(true);
|
||||
}));
|
||||
});
|
||||
|
||||
// These tests demonstrate the use of a stub component
|
||||
describe("createAccount()", () => {
|
||||
beforeEach(() => {
|
||||
component.verticalStepper = TestBed.createComponent(VerticalStepperStubComponent)
|
||||
.componentInstance as VerticalStepperComponent;
|
||||
});
|
||||
|
||||
it("should set email and call verticalStepper.next()", fakeAsync(() => {
|
||||
const verticalStepperNext = jest.spyOn(component.verticalStepper, "next");
|
||||
component.createdAccount("test@email.com");
|
||||
expect(verticalStepperNext).toHaveBeenCalled();
|
||||
expect(component.email).toBe("test@email.com");
|
||||
}));
|
||||
});
|
||||
|
||||
describe("billingSuccess()", () => {
|
||||
beforeEach(() => {
|
||||
component.verticalStepper = TestBed.createComponent(VerticalStepperStubComponent)
|
||||
.componentInstance as VerticalStepperComponent;
|
||||
});
|
||||
|
||||
it("should set orgId and call verticalStepper.next()", () => {
|
||||
const verticalStepperNext = jest.spyOn(component.verticalStepper, "next");
|
||||
component.billingSuccess({ orgId: testOrgId });
|
||||
expect(verticalStepperNext).toHaveBeenCalled();
|
||||
expect(component.orgId).toBe(testOrgId);
|
||||
});
|
||||
});
|
||||
|
||||
describe("stepSelectionChange()", () => {
|
||||
beforeEach(() => {
|
||||
component.verticalStepper = TestBed.createComponent(VerticalStepperStubComponent)
|
||||
.componentInstance as VerticalStepperComponent;
|
||||
});
|
||||
|
||||
it("on step 2 should show organization copy text", () => {
|
||||
component.stepSelectionChange({
|
||||
selectedIndex: 1,
|
||||
previouslySelectedIndex: 0,
|
||||
} as StepperSelectionEvent);
|
||||
|
||||
expect(component.orgInfoSubLabel).toContain("Enter your");
|
||||
expect(component.orgInfoSubLabel).toContain(" organization information");
|
||||
});
|
||||
it("going from step 2 to 3 should set the orgInforSubLabel to be the Org name from orgInfoFormGroup", () => {
|
||||
component.orgInfoFormGroup = formBuilder.group({
|
||||
name: ["Hooli"],
|
||||
email: [""],
|
||||
});
|
||||
component.stepSelectionChange({
|
||||
selectedIndex: 2,
|
||||
previouslySelectedIndex: 1,
|
||||
} as StepperSelectionEvent);
|
||||
|
||||
expect(component.orgInfoSubLabel).toContain("Hooli");
|
||||
});
|
||||
});
|
||||
|
||||
describe("previousStep()", () => {
|
||||
beforeEach(() => {
|
||||
component.verticalStepper = TestBed.createComponent(VerticalStepperStubComponent)
|
||||
.componentInstance as VerticalStepperComponent;
|
||||
});
|
||||
|
||||
it("should call verticalStepper.previous()", fakeAsync(() => {
|
||||
const verticalStepperPrevious = jest.spyOn(component.verticalStepper, "previous");
|
||||
component.previousStep();
|
||||
expect(verticalStepperPrevious).toHaveBeenCalled();
|
||||
}));
|
||||
});
|
||||
|
||||
// These tests demonstrate router navigation
|
||||
describe("navigation methods", () => {
|
||||
beforeEach(() => {
|
||||
component.orgId = testOrgId;
|
||||
const router = TestBed.inject(Router);
|
||||
fixture.detectChanges();
|
||||
routerSpy = jest.spyOn(router, "navigate");
|
||||
});
|
||||
describe("navigateToOrgVault", () => {
|
||||
it("should call verticalStepper.previous()", fakeAsync(() => {
|
||||
component.navigateToOrgVault();
|
||||
expect(routerSpy).toHaveBeenCalledWith(["organizations", testOrgId, "vault"]);
|
||||
}));
|
||||
});
|
||||
describe("navigateToOrgVault", () => {
|
||||
it("should call verticalStepper.previous()", fakeAsync(() => {
|
||||
component.navigateToOrgInvite();
|
||||
expect(routerSpy).toHaveBeenCalledWith(["organizations", testOrgId, "members"]);
|
||||
}));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
export class VerticalStepperStubComponent extends VerticalStepperComponent {}
|
||||
export class BlankComponent {} // For router tests
|
||||
@@ -1,353 +0,0 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { StepperSelectionEvent } from "@angular/cdk/stepper";
|
||||
import { TitleCasePipe } from "@angular/common";
|
||||
import { Component, OnDestroy, OnInit, ViewChild } from "@angular/core";
|
||||
import { UntypedFormBuilder, Validators } from "@angular/forms";
|
||||
import { ActivatedRoute, Router } from "@angular/router";
|
||||
import { Subject, takeUntil } from "rxjs";
|
||||
|
||||
import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction";
|
||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options";
|
||||
import { Policy } from "@bitwarden/common/admin-console/models/domain/policy";
|
||||
import {
|
||||
OrganizationInformation,
|
||||
PlanInformation,
|
||||
OrganizationBillingServiceAbstraction as OrganizationBillingService,
|
||||
} from "@bitwarden/common/billing/abstractions/organization-billing.service";
|
||||
import { PlanType, ProductTierType } from "@bitwarden/common/billing/enums";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { ReferenceEventRequest } from "@bitwarden/common/models/request/reference-event.request";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
|
||||
import { AcceptOrganizationInviteService } from "../../auth/organization-invite/accept-organization.service";
|
||||
import { OrganizationInvite } from "../../auth/organization-invite/organization-invite";
|
||||
import {
|
||||
OrganizationCreatedEvent,
|
||||
SubscriptionProduct,
|
||||
TrialOrganizationType,
|
||||
} from "../../billing/accounts/trial-initiation/trial-billing-step.component";
|
||||
|
||||
import { RouterService } from "./../../core/router.service";
|
||||
import { VerticalStepperComponent } from "./vertical-stepper/vertical-stepper.component";
|
||||
|
||||
export enum ValidOrgParams {
|
||||
families = "families",
|
||||
enterprise = "enterprise",
|
||||
teams = "teams",
|
||||
teamsStarter = "teamsStarter",
|
||||
individual = "individual",
|
||||
premium = "premium",
|
||||
free = "free",
|
||||
}
|
||||
|
||||
enum ValidLayoutParams {
|
||||
default = "default",
|
||||
teams = "teams",
|
||||
teams1 = "teams1",
|
||||
teams2 = "teams2",
|
||||
teams3 = "teams3",
|
||||
enterprise = "enterprise",
|
||||
enterprise1 = "enterprise1",
|
||||
enterprise2 = "enterprise2",
|
||||
cnetcmpgnent = "cnetcmpgnent",
|
||||
cnetcmpgnind = "cnetcmpgnind",
|
||||
cnetcmpgnteams = "cnetcmpgnteams",
|
||||
abmenterprise = "abmenterprise",
|
||||
abmteams = "abmteams",
|
||||
secretsManager = "secretsManager",
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: "app-trial",
|
||||
templateUrl: "trial-initiation.component.html",
|
||||
})
|
||||
export class TrialInitiationComponent implements OnInit, OnDestroy {
|
||||
email = "";
|
||||
fromOrgInvite = false;
|
||||
org = "";
|
||||
orgInfoSubLabel = "";
|
||||
orgId = "";
|
||||
orgLabel = "";
|
||||
billingSubLabel = "";
|
||||
layout = "default";
|
||||
plan: PlanType;
|
||||
productTier: ProductTierType;
|
||||
accountCreateOnly = true;
|
||||
useTrialStepper = false;
|
||||
loading = false;
|
||||
policies: Policy[];
|
||||
enforcedPolicyOptions: MasterPasswordPolicyOptions;
|
||||
trialFlowOrgs: string[] = [
|
||||
ValidOrgParams.teams,
|
||||
ValidOrgParams.teamsStarter,
|
||||
ValidOrgParams.enterprise,
|
||||
ValidOrgParams.families,
|
||||
];
|
||||
routeFlowOrgs: string[] = [
|
||||
ValidOrgParams.free,
|
||||
ValidOrgParams.premium,
|
||||
ValidOrgParams.individual,
|
||||
];
|
||||
layouts = ValidLayoutParams;
|
||||
referenceData: ReferenceEventRequest;
|
||||
@ViewChild("stepper", { static: false }) verticalStepper: VerticalStepperComponent;
|
||||
|
||||
orgInfoFormGroup = this.formBuilder.group({
|
||||
name: ["", { validators: [Validators.required, Validators.maxLength(50)], updateOn: "change" }],
|
||||
email: [""],
|
||||
});
|
||||
|
||||
private set referenceDataId(referenceId: string) {
|
||||
if (referenceId != null) {
|
||||
this.referenceData.id = referenceId;
|
||||
} else {
|
||||
this.referenceData.id = ("; " + document.cookie)
|
||||
.split("; reference=")
|
||||
.pop()
|
||||
.split(";")
|
||||
.shift();
|
||||
}
|
||||
|
||||
if (this.referenceData.id === "") {
|
||||
this.referenceData.id = null;
|
||||
} else {
|
||||
// Matches "_ga_QBRN562QQQ=value1.value2.session" and captures values and session.
|
||||
const regex = /_ga_QBRN562QQQ=([^.]+)\.([^.]+)\.(\d+)/;
|
||||
const match = document.cookie.match(regex);
|
||||
if (match) {
|
||||
this.referenceData.session = match[3];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private destroy$ = new Subject<void>();
|
||||
protected enableTrialPayment$ = this.configService.getFeatureFlag$(
|
||||
FeatureFlag.TrialPaymentOptional,
|
||||
);
|
||||
|
||||
constructor(
|
||||
private route: ActivatedRoute,
|
||||
protected router: Router,
|
||||
private formBuilder: UntypedFormBuilder,
|
||||
private titleCasePipe: TitleCasePipe,
|
||||
private logService: LogService,
|
||||
private policyApiService: PolicyApiServiceAbstraction,
|
||||
private policyService: PolicyService,
|
||||
private i18nService: I18nService,
|
||||
private routerService: RouterService,
|
||||
private acceptOrgInviteService: AcceptOrganizationInviteService,
|
||||
private organizationBillingService: OrganizationBillingService,
|
||||
private configService: ConfigService,
|
||||
) {}
|
||||
|
||||
async ngOnInit(): Promise<void> {
|
||||
this.route.queryParams.pipe(takeUntil(this.destroy$)).subscribe((qParams) => {
|
||||
this.referenceData = new ReferenceEventRequest();
|
||||
if (qParams.email != null && qParams.email.indexOf("@") > -1) {
|
||||
this.email = qParams.email;
|
||||
this.fromOrgInvite = qParams.fromOrgInvite === "true";
|
||||
}
|
||||
|
||||
this.referenceDataId = qParams.reference;
|
||||
|
||||
if (Object.values(ValidLayoutParams).includes(qParams.layout)) {
|
||||
this.layout = qParams.layout;
|
||||
this.accountCreateOnly = false;
|
||||
}
|
||||
|
||||
if (this.trialFlowOrgs.includes(qParams.org)) {
|
||||
this.org = qParams.org;
|
||||
this.orgLabel = this.titleCasePipe.transform(this.orgDisplayName);
|
||||
this.useTrialStepper = true;
|
||||
this.referenceData.flow = qParams.org;
|
||||
|
||||
if (this.org === ValidOrgParams.families) {
|
||||
this.plan = PlanType.FamiliesAnnually;
|
||||
this.productTier = ProductTierType.Families;
|
||||
} else if (this.org === ValidOrgParams.teamsStarter) {
|
||||
this.plan = PlanType.TeamsStarter;
|
||||
this.productTier = ProductTierType.TeamsStarter;
|
||||
} else if (this.org === ValidOrgParams.teams) {
|
||||
this.plan = PlanType.TeamsAnnually;
|
||||
this.productTier = ProductTierType.Teams;
|
||||
} else if (this.org === ValidOrgParams.enterprise) {
|
||||
this.plan = PlanType.EnterpriseAnnually;
|
||||
this.productTier = ProductTierType.Enterprise;
|
||||
}
|
||||
} else if (this.routeFlowOrgs.includes(qParams.org)) {
|
||||
this.referenceData.flow = qParams.org;
|
||||
const route = this.router.createUrlTree(["create-organization"], {
|
||||
queryParams: { plan: qParams.org },
|
||||
});
|
||||
this.routerService.setPreviousUrl(route.toString());
|
||||
}
|
||||
|
||||
// Are they coming from an email for sponsoring a families organization
|
||||
// After logging in redirect them to setup the families sponsorship
|
||||
this.setupFamilySponsorship(qParams.sponsorshipToken);
|
||||
|
||||
this.referenceData.initiationPath = this.accountCreateOnly
|
||||
? "Registration form"
|
||||
: "Password Manager trial from marketing website";
|
||||
});
|
||||
|
||||
// If there's a deep linked org invite, use it to get the password policies
|
||||
const orgInvite = await this.acceptOrgInviteService.getOrganizationInvite();
|
||||
if (orgInvite != null) {
|
||||
await this.initPasswordPolicies(orgInvite);
|
||||
}
|
||||
|
||||
this.orgInfoFormGroup.controls.name.valueChanges
|
||||
.pipe(takeUntil(this.destroy$))
|
||||
.subscribe(() => {
|
||||
this.orgInfoFormGroup.controls.name.markAsTouched();
|
||||
});
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.destroy$.next();
|
||||
this.destroy$.complete();
|
||||
}
|
||||
|
||||
stepSelectionChange(event: StepperSelectionEvent) {
|
||||
// Set org info sub label
|
||||
if (event.selectedIndex === 1 && this.orgInfoFormGroup.controls.name.value === "") {
|
||||
this.orgInfoSubLabel =
|
||||
"Enter your " +
|
||||
this.titleCasePipe.transform(this.orgDisplayName) +
|
||||
" organization information";
|
||||
} else if (event.previouslySelectedIndex === 1) {
|
||||
this.orgInfoSubLabel = this.orgInfoFormGroup.controls.name.value;
|
||||
}
|
||||
|
||||
//set billing sub label
|
||||
if (event.selectedIndex === 2) {
|
||||
this.billingSubLabel = this.i18nService.t("billingTrialSubLabel");
|
||||
}
|
||||
}
|
||||
|
||||
async createOrganizationOnTrial() {
|
||||
this.loading = true;
|
||||
const organization: OrganizationInformation = {
|
||||
name: this.orgInfoFormGroup.get("name").value,
|
||||
billingEmail: this.orgInfoFormGroup.get("email").value,
|
||||
initiationPath: "Password Manager trial from marketing website",
|
||||
};
|
||||
|
||||
const plan: PlanInformation = {
|
||||
type: this.plan,
|
||||
passwordManagerSeats: 1,
|
||||
};
|
||||
|
||||
const response = await this.organizationBillingService.purchaseSubscriptionNoPaymentMethod({
|
||||
organization,
|
||||
plan,
|
||||
});
|
||||
|
||||
this.orgId = response?.id;
|
||||
this.billingSubLabel = `${this.i18nService.t("annual")} ($0/${this.i18nService.t("yr")})`;
|
||||
this.loading = false;
|
||||
this.verticalStepper.next();
|
||||
}
|
||||
|
||||
createdAccount(email: string) {
|
||||
this.email = email;
|
||||
this.orgInfoFormGroup.get("email")?.setValue(email);
|
||||
this.verticalStepper.next();
|
||||
}
|
||||
|
||||
billingSuccess(event: any) {
|
||||
this.orgId = event?.orgId;
|
||||
this.billingSubLabel = event?.subLabelText;
|
||||
this.verticalStepper.next();
|
||||
}
|
||||
|
||||
createdOrganization(event: OrganizationCreatedEvent) {
|
||||
this.orgId = event.organizationId;
|
||||
this.billingSubLabel = event.planDescription;
|
||||
this.verticalStepper.next();
|
||||
}
|
||||
|
||||
navigateToOrgVault() {
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.router.navigate(["organizations", this.orgId, "vault"]);
|
||||
}
|
||||
|
||||
navigateToOrgInvite() {
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.router.navigate(["organizations", this.orgId, "members"]);
|
||||
}
|
||||
|
||||
previousStep() {
|
||||
this.verticalStepper.previous();
|
||||
}
|
||||
|
||||
get orgDisplayName() {
|
||||
if (this.org === "teamsStarter") {
|
||||
return "Teams Starter";
|
||||
}
|
||||
|
||||
return this.org;
|
||||
}
|
||||
|
||||
get freeTrialText() {
|
||||
const translationKey =
|
||||
this.layout === this.layouts.secretsManager
|
||||
? "startYour7DayFreeTrialOfBitwardenSecretsManagerFor"
|
||||
: "startYour7DayFreeTrialOfBitwardenFor";
|
||||
|
||||
return this.i18nService.t(translationKey, this.org);
|
||||
}
|
||||
|
||||
get trialOrganizationType(): TrialOrganizationType {
|
||||
switch (this.productTier) {
|
||||
case ProductTierType.Free:
|
||||
return null;
|
||||
default:
|
||||
return this.productTier;
|
||||
}
|
||||
}
|
||||
|
||||
private setupFamilySponsorship(sponsorshipToken: string) {
|
||||
if (sponsorshipToken != null) {
|
||||
const route = this.router.createUrlTree(["setup/families-for-enterprise"], {
|
||||
queryParams: { plan: sponsorshipToken },
|
||||
});
|
||||
this.routerService.setPreviousUrl(route.toString());
|
||||
}
|
||||
}
|
||||
|
||||
private async initPasswordPolicies(invite: OrganizationInvite): Promise<void> {
|
||||
if (invite == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
this.policies = await this.policyApiService.getPoliciesByToken(
|
||||
invite.organizationId,
|
||||
invite.token,
|
||||
invite.email,
|
||||
invite.organizationUserId,
|
||||
);
|
||||
} catch (e) {
|
||||
this.logService.error(e);
|
||||
}
|
||||
|
||||
if (this.policies != null) {
|
||||
this.policyService
|
||||
.masterPasswordPolicyOptions$(this.policies)
|
||||
.pipe(takeUntil(this.destroy$))
|
||||
.subscribe((enforcedPasswordPolicyOptions) => {
|
||||
this.enforcedPolicyOptions = enforcedPasswordPolicyOptions;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
protected readonly SubscriptionProduct = SubscriptionProduct;
|
||||
}
|
||||
@@ -6,7 +6,6 @@ import { InputPasswordComponent } from "@bitwarden/auth/angular";
|
||||
import { FormFieldModule } from "@bitwarden/components";
|
||||
|
||||
import { OrganizationCreateModule } from "../../admin-console/organizations/create/organization-create.module";
|
||||
import { RegisterFormModule } from "../../auth/register-form/register-form.module";
|
||||
import { TaxInfoComponent } from "../../billing";
|
||||
import { TrialBillingStepComponent } from "../../billing/accounts/trial-initiation/trial-billing-step.component";
|
||||
import { SecretsManagerTrialFreeStepperComponent } from "../../billing/trial-initiation/secrets-manager/secrets-manager-trial-free-stepper.component";
|
||||
@@ -39,7 +38,6 @@ import { TeamsContentComponent } from "./content/teams-content.component";
|
||||
import { Teams1ContentComponent } from "./content/teams1-content.component";
|
||||
import { Teams2ContentComponent } from "./content/teams2-content.component";
|
||||
import { Teams3ContentComponent } from "./content/teams3-content.component";
|
||||
import { TrialInitiationComponent } from "./trial-initiation.component";
|
||||
import { VerticalStepperModule } from "./vertical-stepper/vertical-stepper.module";
|
||||
|
||||
@NgModule({
|
||||
@@ -48,7 +46,6 @@ import { VerticalStepperModule } from "./vertical-stepper/vertical-stepper.modul
|
||||
CdkStepperModule,
|
||||
VerticalStepperModule,
|
||||
FormFieldModule,
|
||||
RegisterFormModule,
|
||||
OrganizationCreateModule,
|
||||
EnvironmentSelectorModule,
|
||||
TaxInfoComponent,
|
||||
@@ -56,7 +53,6 @@ import { VerticalStepperModule } from "./vertical-stepper/vertical-stepper.modul
|
||||
InputPasswordComponent,
|
||||
],
|
||||
declarations: [
|
||||
TrialInitiationComponent,
|
||||
CompleteTrialInitiationComponent,
|
||||
EnterpriseContentComponent,
|
||||
TeamsContentComponent,
|
||||
@@ -87,7 +83,7 @@ import { VerticalStepperModule } from "./vertical-stepper/vertical-stepper.modul
|
||||
SecretsManagerTrialFreeStepperComponent,
|
||||
SecretsManagerTrialPaidStepperComponent,
|
||||
],
|
||||
exports: [TrialInitiationComponent, CompleteTrialInitiationComponent],
|
||||
exports: [CompleteTrialInitiationComponent],
|
||||
providers: [TitleCasePipe],
|
||||
})
|
||||
export class TrialInitiationModule {}
|
||||
|
||||
@@ -20,7 +20,6 @@ import { WeakPasswordsReportComponent as OrgWeakPasswordsReportComponent } from
|
||||
import { HintComponent } from "../auth/hint.component";
|
||||
import { RecoverDeleteComponent } from "../auth/recover-delete.component";
|
||||
import { RecoverTwoFactorComponent } from "../auth/recover-two-factor.component";
|
||||
import { RegisterFormModule } from "../auth/register-form/register-form.module";
|
||||
import { RemovePasswordComponent } from "../auth/remove-password.component";
|
||||
import { SetPasswordComponent } from "../auth/set-password.component";
|
||||
import { AccountComponent } from "../auth/settings/account/account.component";
|
||||
@@ -90,7 +89,6 @@ import { SharedModule } from "./shared.module";
|
||||
@NgModule({
|
||||
imports: [
|
||||
SharedModule,
|
||||
RegisterFormModule,
|
||||
ProductSwitcherModule,
|
||||
UserVerificationModule,
|
||||
ChangeKdfModule,
|
||||
|
||||
@@ -42,6 +42,7 @@ export class VaultCipherRowComponent implements OnInit {
|
||||
@Input() collections: CollectionView[];
|
||||
@Input() viewingOrgVault: boolean;
|
||||
@Input() canEditCipher: boolean;
|
||||
@Input() canAssignCollections: boolean;
|
||||
@Input() canManageCollection: boolean;
|
||||
|
||||
@Output() onEvent = new EventEmitter<VaultItemEvent>();
|
||||
@@ -101,7 +102,7 @@ export class VaultCipherRowComponent implements OnInit {
|
||||
}
|
||||
|
||||
protected get showAssignToCollections() {
|
||||
return this.organizations?.length && this.canEditCipher && !this.cipher.isDeleted;
|
||||
return this.organizations?.length && this.canAssignCollections && !this.cipher.isDeleted;
|
||||
}
|
||||
|
||||
protected get showClone() {
|
||||
@@ -208,6 +209,6 @@ export class VaultCipherRowComponent implements OnInit {
|
||||
return true; // Always show checkbox in individual vault or for non-org items
|
||||
}
|
||||
|
||||
return this.organization.canEditAllCiphers || this.cipher.edit;
|
||||
return this.organization.canEditAllCiphers || (this.cipher.edit && this.cipher.viewPassword);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -144,6 +144,7 @@
|
||||
[collections]="allCollections"
|
||||
[checked]="selection.isSelected(item)"
|
||||
[canEditCipher]="canEditCipher(item.cipher)"
|
||||
[canAssignCollections]="canAssignCollections(item.cipher)"
|
||||
[canManageCollection]="canManageCollection(item.cipher)"
|
||||
(checkedToggled)="selection.toggle(item)"
|
||||
(onEvent)="event($event)"
|
||||
|
||||
@@ -236,6 +236,13 @@ export class VaultItemsComponent {
|
||||
return (organization.canEditAllCiphers && this.viewingOrgVault) || cipher.edit;
|
||||
}
|
||||
|
||||
protected canAssignCollections(cipher: CipherView) {
|
||||
const organization = this.allOrganizations.find((o) => o.id === cipher.organizationId);
|
||||
return (
|
||||
(organization?.canEditAllCiphers && this.viewingOrgVault) || cipher.canAssignToCollections
|
||||
);
|
||||
}
|
||||
|
||||
protected canManageCollection(cipher: CipherView) {
|
||||
// If the cipher is not part of an organization (personal item), user can manage it
|
||||
if (cipher.organizationId == null) {
|
||||
@@ -461,7 +468,7 @@ export class VaultItemsComponent {
|
||||
private allCiphersHaveEditAccess(): boolean {
|
||||
return this.selection.selected
|
||||
.filter(({ cipher }) => cipher)
|
||||
.every(({ cipher }) => cipher?.edit);
|
||||
.every(({ cipher }) => cipher?.edit && cipher?.viewPassword);
|
||||
}
|
||||
|
||||
private getUniqueOrganizationIds(): Set<string> {
|
||||
|
||||
@@ -4,10 +4,8 @@ import { Observable } from "rxjs";
|
||||
|
||||
import { CollectionAdminView, CollectionView } from "@bitwarden/admin-console/common";
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
// FIXME: remove `src` and fix import
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { FolderView } from "@bitwarden/common/src/vault/models/view/folder.view";
|
||||
import { TreeNode } from "@bitwarden/common/vault/models/domain/tree-node";
|
||||
import { FolderView } from "@bitwarden/common/vault/models/view/folder.view";
|
||||
|
||||
import {
|
||||
CipherTypeFilter,
|
||||
|
||||
@@ -274,6 +274,7 @@ export class VaultFilterService implements VaultFilterServiceAbstraction {
|
||||
folderCopy.id = f.id;
|
||||
folderCopy.revisionDate = f.revisionDate;
|
||||
folderCopy.icon = "bwi-folder";
|
||||
folderCopy.fullName = f.name; // save full folder name before separating it into parts
|
||||
const parts = f.name != null ? f.name.replace(/^\/+|\/+$/g, "").split(NestingDelimiter) : [];
|
||||
ServiceUtils.nestedTraverse(nodes, 0, parts, folderCopy, null, NestingDelimiter);
|
||||
});
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
import { CollectionAdminView } from "@bitwarden/admin-console/common";
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
// FIXME: remove `src` and fix import
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { FolderView } from "@bitwarden/common/src/vault/models/view/folder.view";
|
||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||
import { ITreeNodeObject } from "@bitwarden/common/vault/models/domain/tree-node";
|
||||
import { FolderView } from "@bitwarden/common/vault/models/view/folder.view";
|
||||
|
||||
export type CipherStatus = "all" | "favorites" | "trash" | CipherType;
|
||||
|
||||
@@ -12,5 +10,13 @@ export type CipherTypeFilter = ITreeNodeObject & { type: CipherStatus; icon: str
|
||||
export type CollectionFilter = CollectionAdminView & {
|
||||
icon: string;
|
||||
};
|
||||
export type FolderFilter = FolderView & { icon: string };
|
||||
export type FolderFilter = FolderView & {
|
||||
icon: string;
|
||||
/**
|
||||
* Full folder name.
|
||||
*
|
||||
* Used for when the folder `name` property is be separated into parts.
|
||||
*/
|
||||
fullName?: string;
|
||||
};
|
||||
export type OrganizationFilter = Organization & { icon: string; hideOptions?: boolean };
|
||||
|
||||
@@ -77,6 +77,8 @@ import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
import { ServiceUtils } from "@bitwarden/common/vault/service-utils";
|
||||
import { DialogService, Icons, ToastService } from "@bitwarden/components";
|
||||
import {
|
||||
AddEditFolderDialogComponent,
|
||||
AddEditFolderDialogResult,
|
||||
CipherFormConfig,
|
||||
CollectionAssignmentResult,
|
||||
DecryptionFailureDialogComponent,
|
||||
@@ -118,7 +120,6 @@ import {
|
||||
BulkMoveDialogResult,
|
||||
openBulkMoveDialog,
|
||||
} from "./bulk-action-dialogs/bulk-move-dialog/bulk-move-dialog.component";
|
||||
import { FolderAddEditDialogResult, openFolderAddEditDialog } from "./folder-add-edit.component";
|
||||
import { VaultBannersComponent } from "./vault-banners/vault-banners.component";
|
||||
import { VaultFilterComponent } from "./vault-filter/components/vault-filter.component";
|
||||
import { VaultFilterService } from "./vault-filter/services/abstractions/vault-filter.service";
|
||||
@@ -607,20 +608,24 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||
await this.filterComponent.filters?.organizationFilter?.action(orgNode);
|
||||
}
|
||||
|
||||
addFolder = async (): Promise<void> => {
|
||||
openFolderAddEditDialog(this.dialogService);
|
||||
addFolder = (): void => {
|
||||
AddEditFolderDialogComponent.open(this.dialogService);
|
||||
};
|
||||
|
||||
editFolder = async (folder: FolderFilter): Promise<void> => {
|
||||
const dialog = openFolderAddEditDialog(this.dialogService, {
|
||||
data: {
|
||||
folderId: folder.id,
|
||||
const dialogRef = AddEditFolderDialogComponent.open(this.dialogService, {
|
||||
editFolderConfig: {
|
||||
// Shallow copy is used so the original folder object is not modified
|
||||
folder: {
|
||||
...folder,
|
||||
name: folder.fullName ?? folder.name, // If the filter has a fullName populated, use that as the editable name
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const result = await lastValueFrom(dialog.closed);
|
||||
const result = await lastValueFrom(dialogRef.closed);
|
||||
|
||||
if (result === FolderAddEditDialogResult.Deleted) {
|
||||
if (result === AddEditFolderDialogResult.Deleted) {
|
||||
await this.router.navigate([], {
|
||||
queryParams: { folderId: null },
|
||||
queryParamsHandling: "merge",
|
||||
|
||||
@@ -15,6 +15,7 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service"
|
||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||
import { CollectionId } from "@bitwarden/common/types/guid";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { PremiumUpgradePromptService } from "@bitwarden/common/vault/abstractions/premium-upgrade-prompt.service";
|
||||
import { ViewPasswordHistoryService } from "@bitwarden/common/vault/abstractions/view-password-history.service";
|
||||
import { CipherType } from "@bitwarden/common/vault/enums/cipher-type";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
@@ -25,13 +26,8 @@ import {
|
||||
DialogService,
|
||||
ToastService,
|
||||
} from "@bitwarden/components";
|
||||
import { CipherViewComponent } from "@bitwarden/vault";
|
||||
|
||||
// FIXME: remove `src` and fix import
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { PremiumUpgradePromptService } from "../../../../../../libs/common/src/vault/abstractions/premium-upgrade-prompt.service";
|
||||
// FIXME: remove `src` and fix import
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { CipherViewComponent } from "../../../../../../libs/vault/src/cipher-view/cipher-view.component";
|
||||
import { SharedModule } from "../../shared/shared.module";
|
||||
import { WebVaultPremiumUpgradePromptService } from "../services/web-premium-upgrade-prompt.service";
|
||||
import { WebViewPasswordHistoryService } from "../services/web-view-password-history.service";
|
||||
|
||||
@@ -7,13 +7,10 @@ import { OrganizationService } from "@bitwarden/common/admin-console/abstraction
|
||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { OrganizationUserStatusType } from "@bitwarden/common/admin-console/enums";
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { CipherId } from "@bitwarden/common/types/guid";
|
||||
import { Account, AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { CipherId, UserId } from "@bitwarden/common/types/guid";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
|
||||
// FIXME: remove `src` and fix import
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { Account } from "../../../../../../../libs/importer/src/importers/lastpass/access/models";
|
||||
import { RoutedVaultFilterService } from "../../individual-vault/vault-filter/services/routed-vault-filter.service";
|
||||
|
||||
import { AdminConsoleCipherFormConfigService } from "./admin-console-cipher-form-config.service";
|
||||
@@ -85,7 +82,14 @@ describe("AdminConsoleCipherFormConfigService", () => {
|
||||
{ provide: CipherService, useValue: { get: getCipher } },
|
||||
{
|
||||
provide: AccountService,
|
||||
useValue: { activeAccount$: new BehaviorSubject<Account>(new Account()) },
|
||||
useValue: {
|
||||
activeAccount$: new BehaviorSubject<Account>({
|
||||
id: "123-456-789" as UserId,
|
||||
email: "test@email.com",
|
||||
emailVerified: true,
|
||||
name: "Test User",
|
||||
}),
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
@@ -15,14 +15,8 @@ import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.servi
|
||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||
import { CipherData } from "@bitwarden/common/vault/models/data/cipher.data";
|
||||
import { Cipher } from "@bitwarden/common/vault/models/domain/cipher";
|
||||
import { CipherFormConfig, CipherFormConfigService, CipherFormMode } from "@bitwarden/vault";
|
||||
|
||||
// FIXME: remove `src` and fix import
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import {
|
||||
CipherFormConfig,
|
||||
CipherFormConfigService,
|
||||
CipherFormMode,
|
||||
} from "../../../../../../../libs/vault/src/cipher-form/abstractions/cipher-form-config.service";
|
||||
import { RoutedVaultFilterService } from "../../individual-vault/vault-filter/services/routed-vault-filter.service";
|
||||
|
||||
/** Admin Console implementation of the `CipherFormConfigService`. */
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
import { Injectable } from "@angular/core";
|
||||
|
||||
import { ViewPasswordHistoryService } from "@bitwarden/common/vault/abstractions/view-password-history.service";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
import { DialogService } from "@bitwarden/components";
|
||||
|
||||
// FIXME: remove `src` and fix import
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { ViewPasswordHistoryService } from "../../../../../../libs/common/src/vault/abstractions/view-password-history.service";
|
||||
import { openPasswordHistoryDialog } from "../individual-vault/password-history.component";
|
||||
|
||||
/**
|
||||
|
||||
@@ -485,6 +485,18 @@
|
||||
"editFolder": {
|
||||
"message": "Edit folder"
|
||||
},
|
||||
"newFolder": {
|
||||
"message": "New folder"
|
||||
},
|
||||
"folderName": {
|
||||
"message": "Folder name"
|
||||
},
|
||||
"folderHintText": {
|
||||
"message": "Nest a folder by adding the parent folder's name followed by a “/”. Example: Social/Forums"
|
||||
},
|
||||
"deleteFolderPermanently": {
|
||||
"message": "Are you sure you want to permanently delete this folder?"
|
||||
},
|
||||
"baseDomain": {
|
||||
"message": "Base domain",
|
||||
"description": "Domain name. Example: website.com"
|
||||
@@ -749,15 +761,6 @@
|
||||
"itemName": {
|
||||
"message": "Item name"
|
||||
},
|
||||
"cannotRemoveViewOnlyCollections": {
|
||||
"message": "You cannot remove collections with View only permissions: $COLLECTIONS$",
|
||||
"placeholders": {
|
||||
"collections": {
|
||||
"content": "$1",
|
||||
"example": "Work, Personal"
|
||||
}
|
||||
}
|
||||
},
|
||||
"ex": {
|
||||
"message": "ex.",
|
||||
"description": "Short abbreviation for 'example'."
|
||||
@@ -10143,6 +10146,15 @@
|
||||
"descriptorCode": {
|
||||
"message": "Descriptor code"
|
||||
},
|
||||
"cannotRemoveViewOnlyCollections": {
|
||||
"message": "You cannot remove collections with View only permissions: $COLLECTIONS$",
|
||||
"placeholders": {
|
||||
"collections": {
|
||||
"content": "$1",
|
||||
"example": "Work, Personal"
|
||||
}
|
||||
}
|
||||
},
|
||||
"importantNotice": {
|
||||
"message": "Important notice"
|
||||
},
|
||||
@@ -10333,5 +10345,45 @@
|
||||
"example": "Acme c"
|
||||
}
|
||||
}
|
||||
},
|
||||
"seatsRemaining": {
|
||||
"message": "You have $REMAINING$ seats remaining out of $TOTAL$ seats assigned to this organization. Contact your provider to manage your subscription.",
|
||||
"placeholders": {
|
||||
"remaining": {
|
||||
"content": "$1",
|
||||
"example": "5"
|
||||
},
|
||||
"total": {
|
||||
"content": "$2",
|
||||
"example": "10"
|
||||
}
|
||||
}
|
||||
},
|
||||
"existingOrganization": {
|
||||
"message": "Existing organization"
|
||||
},
|
||||
"selectOrganizationProviderPortal": {
|
||||
"message": "Select an organization to add to your Provider Portal."
|
||||
},
|
||||
"noOrganizations": {
|
||||
"message": "There are no organizations to list"
|
||||
},
|
||||
"yourProviderSubscriptionCredit": {
|
||||
"message": "Your provider subscription will receive a credit for any remaining time in the organization's subscription."
|
||||
},
|
||||
"doYouWantToAddThisOrg": {
|
||||
"message": "Do you want to add this organization to $PROVIDER$?",
|
||||
"placeholders": {
|
||||
"provider": {
|
||||
"content": "$1",
|
||||
"example": "Cool MSP"
|
||||
}
|
||||
}
|
||||
},
|
||||
"addedExistingOrganization": {
|
||||
"message": "Added existing organization"
|
||||
},
|
||||
"assignedExceedsAvailable": {
|
||||
"message": "Assigned seats exceed available seats."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
ProviderSubscriptionComponent,
|
||||
ProviderSubscriptionStatusComponent,
|
||||
} from "../../billing/providers";
|
||||
import { AddExistingOrganizationDialogComponent } from "../../billing/providers/clients/add-existing-organization-dialog.component";
|
||||
|
||||
import { AddOrganizationComponent } from "./clients/add-organization.component";
|
||||
import { CreateOrganizationComponent } from "./clients/create-organization.component";
|
||||
@@ -63,6 +64,7 @@ import { VerifyRecoverDeleteProviderComponent } from "./verify-recover-delete-pr
|
||||
SetupProviderComponent,
|
||||
UserAddEditComponent,
|
||||
AddEditMemberDialogComponent,
|
||||
AddExistingOrganizationDialogComponent,
|
||||
CreateClientDialogComponent,
|
||||
ManageClientNameDialogComponent,
|
||||
ManageClientSubscriptionDialogComponent,
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Injectable } from "@angular/core";
|
||||
import { firstValueFrom, map } from "rxjs";
|
||||
import { switchMap } from "rxjs/operators";
|
||||
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { ProviderApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/provider/provider-api.service.abstraction";
|
||||
import { OrganizationKeysRequest } from "@bitwarden/common/admin-console/models/request/organization-keys.request";
|
||||
import { ProviderAddOrganizationRequest } from "@bitwarden/common/admin-console/models/request/provider/provider-add-organization.request";
|
||||
import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions/billing-api.service.abstraction";
|
||||
@@ -10,6 +13,8 @@ import { PlanType } from "@bitwarden/common/billing/enums";
|
||||
import { CreateClientOrganizationRequest } from "@bitwarden/common/billing/models/request/create-client-organization.request";
|
||||
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { StateProvider } from "@bitwarden/common/platform/state";
|
||||
import { OrganizationId } from "@bitwarden/common/types/guid";
|
||||
import { OrgKey } from "@bitwarden/common/types/key";
|
||||
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
|
||||
import { KeyService } from "@bitwarden/key-management";
|
||||
@@ -23,6 +28,8 @@ export class WebProviderService {
|
||||
private i18nService: I18nService,
|
||||
private encryptService: EncryptService,
|
||||
private billingApiService: BillingApiServiceAbstraction,
|
||||
private stateProvider: StateProvider,
|
||||
private providerApiService: ProviderApiServiceAbstraction,
|
||||
) {}
|
||||
|
||||
async addOrganizationToProvider(providerId: string, organizationId: string) {
|
||||
@@ -40,6 +47,22 @@ export class WebProviderService {
|
||||
return response;
|
||||
}
|
||||
|
||||
async addOrganizationToProviderVNext(providerId: string, organizationId: string): Promise<void> {
|
||||
const orgKey = await firstValueFrom(
|
||||
this.stateProvider.activeUserId$.pipe(
|
||||
switchMap((userId) => this.keyService.orgKeys$(userId)),
|
||||
map((organizationKeysById) => organizationKeysById[organizationId as OrganizationId]),
|
||||
),
|
||||
);
|
||||
const providerKey = await this.keyService.getProviderKey(providerId);
|
||||
const encryptedOrgKey = await this.encryptService.encrypt(orgKey.key, providerKey);
|
||||
await this.providerApiService.addOrganizationToProvider(providerId, {
|
||||
key: encryptedOrgKey.encryptedString,
|
||||
organizationId,
|
||||
});
|
||||
await this.syncService.fullSync(true);
|
||||
}
|
||||
|
||||
async createClientOrganization(
|
||||
providerId: string,
|
||||
name: string,
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
<bit-dialog [loading]="loading">
|
||||
<span bitDialogTitle>
|
||||
{{ "addExistingOrganization" | i18n }}
|
||||
</span>
|
||||
<ng-container bitDialogContent>
|
||||
<ng-container *ngIf="!selectedOrganization; else organizationSelected">
|
||||
<p>{{ "selectOrganizationProviderPortal" | i18n }}</p>
|
||||
<bit-table>
|
||||
<ng-container header>
|
||||
<tr>
|
||||
<th colspan="2" bitCell>{{ "name" | i18n }}</th>
|
||||
<th bitCell>{{ "assigned" | i18n }}</th>
|
||||
</tr>
|
||||
</ng-container>
|
||||
<ng-template body>
|
||||
<tr
|
||||
bitRow
|
||||
*ngFor="let addable of addableOrganizations"
|
||||
[ngClass]="{ 'tw-text-muted': addable.disabled }"
|
||||
>
|
||||
<td bitCell class="tw-w-8">
|
||||
<bit-avatar [text]="addable.name" [id]="addable.id" size="small"></bit-avatar>
|
||||
</td>
|
||||
<td bitCell>
|
||||
{{ addable.name }}
|
||||
<div *ngIf="addable.disabled" class="tw-text-xs">
|
||||
{{ "assignedExceedsAvailable" | i18n }}
|
||||
</div>
|
||||
</td>
|
||||
<td bitCell>{{ addable.seats }}</td>
|
||||
<td bitCell class="tw-text-right">
|
||||
<button
|
||||
type="button"
|
||||
bitButton
|
||||
buttonType="secondary"
|
||||
[disabled]="addable.disabled"
|
||||
(click)="selectOrganization(addable.id)"
|
||||
>
|
||||
{{ "next" | i18n }}
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
</bit-table>
|
||||
<p *ngIf="addableOrganizations.length === 0" class="tw-text-muted tw-mt-2">
|
||||
{{ "noOrganizations" | i18n }}
|
||||
</p>
|
||||
</ng-container>
|
||||
<ng-template #organizationSelected>
|
||||
<p>{{ "yourProviderSubscriptionCredit" | i18n }}</p>
|
||||
<p>{{ "doYouWantToAddThisOrg" | i18n: dialogParams.provider.name }}</p>
|
||||
<div class="tw-flex tw-flex-col">
|
||||
<div>{{ "organization" | i18n }}: {{ selectedOrganization.name }}</div>
|
||||
<div>{{ "billingPlan" | i18n }}: {{ selectedOrganization.plan }}</div>
|
||||
<div>{{ "assignedSeats" | i18n }}: {{ selectedOrganization.seats }}</div>
|
||||
</div>
|
||||
</ng-template>
|
||||
</ng-container>
|
||||
<ng-container bitDialogFooter>
|
||||
<button
|
||||
*ngIf="selectedOrganization"
|
||||
type="button"
|
||||
bitButton
|
||||
buttonType="primary"
|
||||
[bitAction]="addExistingOrganization"
|
||||
>
|
||||
{{ "addOrganization" | i18n }}
|
||||
</button>
|
||||
<button bitButton buttonType="secondary" type="button" [bitDialogClose]="ResultType.Closed">
|
||||
{{ "close" | i18n }}
|
||||
</button>
|
||||
</ng-container>
|
||||
</bit-dialog>
|
||||
@@ -0,0 +1,82 @@
|
||||
import { DIALOG_DATA, DialogConfig, DialogRef } from "@angular/cdk/dialog";
|
||||
import { Component, Inject, OnInit } from "@angular/core";
|
||||
|
||||
import { ProviderApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/provider/provider-api.service.abstraction";
|
||||
import { Provider } from "@bitwarden/common/admin-console/models/domain/provider";
|
||||
import { AddableOrganizationResponse } from "@bitwarden/common/admin-console/models/response/addable-organization.response";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { DialogService, ToastService } from "@bitwarden/components";
|
||||
|
||||
import { WebProviderService } from "../../../admin-console/providers/services/web-provider.service";
|
||||
|
||||
export type AddExistingOrganizationDialogParams = {
|
||||
provider: Provider;
|
||||
};
|
||||
|
||||
export enum AddExistingOrganizationDialogResultType {
|
||||
Closed = "closed",
|
||||
Submitted = "submitted",
|
||||
}
|
||||
|
||||
@Component({
|
||||
templateUrl: "./add-existing-organization-dialog.component.html",
|
||||
})
|
||||
export class AddExistingOrganizationDialogComponent implements OnInit {
|
||||
protected loading: boolean = true;
|
||||
|
||||
addableOrganizations: AddableOrganizationResponse[] = [];
|
||||
selectedOrganization?: AddableOrganizationResponse;
|
||||
|
||||
protected readonly ResultType = AddExistingOrganizationDialogResultType;
|
||||
|
||||
constructor(
|
||||
@Inject(DIALOG_DATA) protected dialogParams: AddExistingOrganizationDialogParams,
|
||||
private dialogRef: DialogRef<AddExistingOrganizationDialogResultType>,
|
||||
private i18nService: I18nService,
|
||||
private providerApiService: ProviderApiServiceAbstraction,
|
||||
private toastService: ToastService,
|
||||
private webProviderService: WebProviderService,
|
||||
) {}
|
||||
|
||||
async ngOnInit() {
|
||||
this.addableOrganizations = await this.providerApiService.getProviderAddableOrganizations(
|
||||
this.dialogParams.provider.id,
|
||||
);
|
||||
this.loading = false;
|
||||
}
|
||||
|
||||
addExistingOrganization = async (): Promise<void> => {
|
||||
if (this.selectedOrganization) {
|
||||
await this.webProviderService.addOrganizationToProviderVNext(
|
||||
this.dialogParams.provider.id,
|
||||
this.selectedOrganization.id,
|
||||
);
|
||||
|
||||
this.toastService.showToast({
|
||||
variant: "success",
|
||||
title: "",
|
||||
message: this.i18nService.t("addedExistingOrganization"),
|
||||
});
|
||||
|
||||
this.dialogRef.close(this.ResultType.Submitted);
|
||||
}
|
||||
};
|
||||
|
||||
selectOrganization(organizationId: string) {
|
||||
this.selectedOrganization = this.addableOrganizations.find(
|
||||
(organization) => organization.id === organizationId,
|
||||
);
|
||||
}
|
||||
|
||||
static open = (
|
||||
dialogService: DialogService,
|
||||
dialogConfig: DialogConfig<
|
||||
AddExistingOrganizationDialogParams,
|
||||
DialogRef<AddExistingOrganizationDialogResultType>
|
||||
>,
|
||||
) =>
|
||||
dialogService.open<
|
||||
AddExistingOrganizationDialogResultType,
|
||||
AddExistingOrganizationDialogParams
|
||||
>(AddExistingOrganizationDialogComponent, dialogConfig);
|
||||
}
|
||||
@@ -1,9 +1,39 @@
|
||||
<app-header>
|
||||
<bit-search [placeholder]="'search' | i18n" [formControl]="searchControl"></bit-search>
|
||||
<a type="button" bitButton *ngIf="isProviderAdmin" buttonType="primary" (click)="createClient()">
|
||||
<i class="bwi bwi-plus bwi-fw" aria-hidden="true"></i>
|
||||
{{ "addNewOrganization" | i18n }}
|
||||
</a>
|
||||
<ng-container *ngIf="addExistingOrgsFromProviderPortal$ | async; else addExistingOrgsDisabled">
|
||||
<button
|
||||
bitButton
|
||||
buttonType="primary"
|
||||
type="button"
|
||||
[bitMenuTriggerFor]="clientMenu"
|
||||
appA11yTitle="{{ 'add' | i18n }}"
|
||||
>
|
||||
<i class="bwi bwi-plus bwi-fw" aria-hidden="true"></i>
|
||||
{{ "add" | i18n }}
|
||||
</button>
|
||||
<bit-menu #clientMenu>
|
||||
<button type="button" bitMenuItem (click)="createClient()">
|
||||
<i aria-hidden="true" class="bwi bwi-business"></i>
|
||||
{{ "newClient" | i18n }}
|
||||
</button>
|
||||
<button type="button" bitMenuItem (click)="addExistingOrganization()">
|
||||
<i aria-hidden="true" class="bwi bwi-sitemap"></i>
|
||||
{{ "existingOrganization" | i18n }}
|
||||
</button>
|
||||
</bit-menu>
|
||||
</ng-container>
|
||||
<ng-template #addExistingOrgsDisabled>
|
||||
<a
|
||||
type="button"
|
||||
bitButton
|
||||
*ngIf="isProviderAdmin"
|
||||
buttonType="primary"
|
||||
(click)="createClient()"
|
||||
>
|
||||
<i class="bwi bwi-plus bwi-fw" aria-hidden="true"></i>
|
||||
{{ "addNewOrganization" | i18n }}
|
||||
</a>
|
||||
</ng-template>
|
||||
</app-header>
|
||||
|
||||
<ng-container *ngIf="loading">
|
||||
|
||||
@@ -11,6 +11,8 @@ import { Provider } from "@bitwarden/common/admin-console/models/domain/provider
|
||||
import { ProviderOrganizationOrganizationDetailsResponse } from "@bitwarden/common/admin-console/models/response/provider/provider-organization.response";
|
||||
import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions";
|
||||
import { PlanResponse } from "@bitwarden/common/billing/models/response/plan.response";
|
||||
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 { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service";
|
||||
import {
|
||||
@@ -25,6 +27,10 @@ import { HeaderModule } from "@bitwarden/web-vault/app/layouts/header/header.mod
|
||||
|
||||
import { WebProviderService } from "../../../admin-console/providers/services/web-provider.service";
|
||||
|
||||
import {
|
||||
AddExistingOrganizationDialogComponent,
|
||||
AddExistingOrganizationDialogResultType,
|
||||
} from "./add-existing-organization-dialog.component";
|
||||
import {
|
||||
CreateClientDialogResultType,
|
||||
openCreateClientDialog,
|
||||
@@ -62,6 +68,9 @@ export class ManageClientsComponent {
|
||||
|
||||
protected searchControl = new FormControl("", { nonNullable: true });
|
||||
protected plans: PlanResponse[] = [];
|
||||
protected addExistingOrgsFromProviderPortal$ = this.configService.getFeatureFlag$(
|
||||
FeatureFlag.PM15179_AddExistingOrgsFromProviderPortal,
|
||||
);
|
||||
|
||||
constructor(
|
||||
private billingApiService: BillingApiServiceAbstraction,
|
||||
@@ -73,6 +82,7 @@ export class ManageClientsComponent {
|
||||
private toastService: ToastService,
|
||||
private validationService: ValidationService,
|
||||
private webProviderService: WebProviderService,
|
||||
private configService: ConfigService,
|
||||
) {
|
||||
this.activatedRoute.queryParams.pipe(first(), takeUntilDestroyed()).subscribe((queryParams) => {
|
||||
this.searchControl.setValue(queryParams.search);
|
||||
@@ -111,19 +121,30 @@ export class ManageClientsComponent {
|
||||
|
||||
async load() {
|
||||
this.provider = await firstValueFrom(this.providerService.get$(this.providerId));
|
||||
|
||||
this.isProviderAdmin = this.provider?.type === ProviderUserType.ProviderAdmin;
|
||||
|
||||
const clients = (await this.billingApiService.getProviderClientOrganizations(this.providerId))
|
||||
.data;
|
||||
|
||||
this.dataSource.data = clients;
|
||||
|
||||
this.dataSource.data = (
|
||||
await this.billingApiService.getProviderClientOrganizations(this.providerId)
|
||||
).data;
|
||||
this.plans = (await this.billingApiService.getPlans()).data;
|
||||
|
||||
this.loading = false;
|
||||
}
|
||||
|
||||
addExistingOrganization = async () => {
|
||||
if (this.provider) {
|
||||
const reference = AddExistingOrganizationDialogComponent.open(this.dialogService, {
|
||||
data: {
|
||||
provider: this.provider,
|
||||
},
|
||||
});
|
||||
|
||||
const result = await lastValueFrom(reference.closed);
|
||||
|
||||
if (result === AddExistingOrganizationDialogResultType.Submitted) {
|
||||
await this.load();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
createClient = async () => {
|
||||
const reference = openCreateClientDialog(this.dialogService, {
|
||||
data: {
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
import { CipherId, OrganizationId } from "@bitwarden/common/types/guid";
|
||||
import { SecurityTask, SecurityTaskStatus, SecurityTaskType } from "@bitwarden/vault";
|
||||
|
||||
/**
|
||||
* Request type for creating tasks.
|
||||
* @property cipherId - Optional. The ID of the cipher to create the task for.
|
||||
* @property type - The type of task to create. Currently defined as "updateAtRiskCredential".
|
||||
*/
|
||||
export type CreateTasksRequest = Readonly<{
|
||||
cipherId?: CipherId;
|
||||
type: SecurityTaskType.UpdateAtRiskCredential;
|
||||
}>;
|
||||
|
||||
export abstract class AdminTaskService {
|
||||
/**
|
||||
* Retrieves all tasks for a given organization.
|
||||
* @param organizationId - The ID of the organization to retrieve tasks for.
|
||||
* @param status - Optional. The status of the tasks to retrieve.
|
||||
*/
|
||||
abstract getAllTasks(
|
||||
organizationId: OrganizationId,
|
||||
status?: SecurityTaskStatus | undefined,
|
||||
): Promise<SecurityTask[]>;
|
||||
|
||||
/**
|
||||
* Creates multiple tasks for a given organization and sends out notifications to applicable users.
|
||||
* @param organizationId - The ID of the organization to create tasks for.
|
||||
* @param tasks - The tasks to create.
|
||||
*/
|
||||
abstract bulkCreateTasks(
|
||||
organizationId: OrganizationId,
|
||||
tasks: CreateTasksRequest[],
|
||||
): Promise<void>;
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
import { MockProxy, mock } from "jest-mock-extended";
|
||||
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { CipherId, OrganizationId } from "@bitwarden/common/types/guid";
|
||||
import { SecurityTaskStatus, SecurityTaskType } from "@bitwarden/vault";
|
||||
|
||||
import { CreateTasksRequest } from "./abstractions/admin-task.abstraction";
|
||||
import { DefaultAdminTaskService } from "./default-admin-task.service";
|
||||
|
||||
describe("DefaultAdminTaskService", () => {
|
||||
let defaultAdminTaskService: DefaultAdminTaskService;
|
||||
let apiService: MockProxy<ApiService>;
|
||||
|
||||
beforeEach(() => {
|
||||
apiService = mock<ApiService>();
|
||||
defaultAdminTaskService = new DefaultAdminTaskService(apiService);
|
||||
});
|
||||
|
||||
describe("getAllTasks", () => {
|
||||
it("should call the api service with the correct parameters with status", async () => {
|
||||
const organizationId = "orgId" as OrganizationId;
|
||||
const status = SecurityTaskStatus.Pending;
|
||||
const expectedUrl = `/tasks/organization?organizationId=${organizationId}&status=0`;
|
||||
|
||||
await defaultAdminTaskService.getAllTasks(organizationId, status);
|
||||
|
||||
expect(apiService.send).toHaveBeenCalledWith("GET", expectedUrl, null, true, true);
|
||||
});
|
||||
|
||||
it("should call the api service with the correct parameters without status", async () => {
|
||||
const organizationId = "orgId" as OrganizationId;
|
||||
const expectedUrl = `/tasks/organization?organizationId=${organizationId}`;
|
||||
|
||||
await defaultAdminTaskService.getAllTasks(organizationId);
|
||||
|
||||
expect(apiService.send).toHaveBeenCalledWith("GET", expectedUrl, null, true, true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("bulkCreateTasks", () => {
|
||||
it("should call the api service with the correct parameters", async () => {
|
||||
const organizationId = "orgId" as OrganizationId;
|
||||
const tasks: CreateTasksRequest[] = [
|
||||
{
|
||||
cipherId: "cipherId-1" as CipherId,
|
||||
type: SecurityTaskType.UpdateAtRiskCredential,
|
||||
},
|
||||
{
|
||||
cipherId: "cipherId-2" as CipherId,
|
||||
type: SecurityTaskType.UpdateAtRiskCredential,
|
||||
},
|
||||
];
|
||||
|
||||
await defaultAdminTaskService.bulkCreateTasks(organizationId, tasks);
|
||||
|
||||
expect(apiService.send).toHaveBeenCalledWith(
|
||||
"POST",
|
||||
`/tasks/${organizationId}/bulk-create`,
|
||||
tasks,
|
||||
true,
|
||||
true,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,48 @@
|
||||
import { Injectable } from "@angular/core";
|
||||
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { ListResponse } from "@bitwarden/common/models/response/list.response";
|
||||
import { OrganizationId } from "@bitwarden/common/types/guid";
|
||||
import {
|
||||
SecurityTask,
|
||||
SecurityTaskData,
|
||||
SecurityTaskResponse,
|
||||
SecurityTaskStatus,
|
||||
} from "@bitwarden/vault";
|
||||
|
||||
import { AdminTaskService, CreateTasksRequest } from "./abstractions/admin-task.abstraction";
|
||||
|
||||
@Injectable()
|
||||
export class DefaultAdminTaskService implements AdminTaskService {
|
||||
constructor(private apiService: ApiService) {}
|
||||
|
||||
async getAllTasks(
|
||||
organizationId: OrganizationId,
|
||||
status?: SecurityTaskStatus | undefined,
|
||||
): Promise<SecurityTask[]> {
|
||||
const queryParams = new URLSearchParams();
|
||||
|
||||
queryParams.append("organizationId", organizationId);
|
||||
if (status !== undefined) {
|
||||
queryParams.append("status", status.toString());
|
||||
}
|
||||
|
||||
const r = await this.apiService.send(
|
||||
"GET",
|
||||
`/tasks/organization?${queryParams.toString()}`,
|
||||
null,
|
||||
true,
|
||||
true,
|
||||
);
|
||||
const response = new ListResponse(r, SecurityTaskResponse);
|
||||
|
||||
return response.data.map((d) => new SecurityTask(new SecurityTaskData(d)));
|
||||
}
|
||||
|
||||
async bulkCreateTasks(
|
||||
organizationId: OrganizationId,
|
||||
tasks: CreateTasksRequest[],
|
||||
): Promise<void> {
|
||||
await this.apiService.send("POST", `/tasks/${organizationId}/bulk-create`, tasks, true, true);
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,8 @@ import rxjs from "eslint-plugin-rxjs";
|
||||
import angularRxjs from "eslint-plugin-rxjs-angular";
|
||||
import storybook from "eslint-plugin-storybook";
|
||||
|
||||
import platformPlugins from "./libs/eslint/platform/index.mjs";
|
||||
|
||||
export default tseslint.config(
|
||||
...storybook.configs["flat/recommended"],
|
||||
{
|
||||
@@ -28,6 +30,7 @@ export default tseslint.config(
|
||||
plugins: {
|
||||
rxjs: rxjs,
|
||||
"rxjs-angular": angularRxjs,
|
||||
"@bitwarden/platform": platformPlugins,
|
||||
},
|
||||
languageOptions: {
|
||||
parserOptions: {
|
||||
@@ -66,7 +69,7 @@ export default tseslint.config(
|
||||
"@angular-eslint/no-outputs-metadata-property": 0,
|
||||
"@angular-eslint/use-lifecycle-interface": "error",
|
||||
"@angular-eslint/use-pipe-transform-interface": 0,
|
||||
|
||||
"@bitwarden/platform/required-using": "error",
|
||||
"@typescript-eslint/explicit-member-accessibility": ["error", { accessibility: "no-public" }],
|
||||
"@typescript-eslint/no-explicit-any": "off", // TODO: This should be re-enabled
|
||||
"@typescript-eslint/no-floating-promises": "error",
|
||||
|
||||
@@ -30,6 +30,7 @@ module.exports = {
|
||||
"<rootDir>/libs/billing/jest.config.js",
|
||||
"<rootDir>/libs/common/jest.config.js",
|
||||
"<rootDir>/libs/components/jest.config.js",
|
||||
"<rootDir>/libs/eslint/jest.config.js",
|
||||
"<rootDir>/libs/tools/export/vault-export/vault-export-core/jest.config.js",
|
||||
"<rootDir>/libs/tools/generator/core/jest.config.js",
|
||||
"<rootDir>/libs/tools/generator/components/jest.config.js",
|
||||
|
||||
@@ -195,7 +195,7 @@ export class BaseLoginDecryptionOptionsComponentV1 implements OnInit, OnDestroy
|
||||
|
||||
async loadNewUserData() {
|
||||
const autoEnrollStatus$ = defer(() =>
|
||||
this.ssoLoginService.getActiveUserOrganizationSsoIdentifier(),
|
||||
this.ssoLoginService.getActiveUserOrganizationSsoIdentifier(this.activeAccountId),
|
||||
).pipe(
|
||||
switchMap((organizationIdentifier) => {
|
||||
if (organizationIdentifier == undefined) {
|
||||
|
||||
@@ -47,7 +47,7 @@ export class SetPasswordComponent extends BaseChangePasswordComponent implements
|
||||
resetPasswordAutoEnroll = false;
|
||||
onSuccessfulChangePassword: () => Promise<void>;
|
||||
successRoute = "vault";
|
||||
userId: UserId;
|
||||
activeUserId: UserId;
|
||||
|
||||
forceSetPasswordReason: ForceSetPasswordReason = ForceSetPasswordReason.None;
|
||||
ForceSetPasswordReason = ForceSetPasswordReason;
|
||||
@@ -96,10 +96,10 @@ export class SetPasswordComponent extends BaseChangePasswordComponent implements
|
||||
await this.syncService.fullSync(true);
|
||||
this.syncLoading = false;
|
||||
|
||||
this.userId = (await firstValueFrom(this.accountService.activeAccount$))?.id;
|
||||
this.activeUserId = (await firstValueFrom(this.accountService.activeAccount$))?.id;
|
||||
|
||||
this.forceSetPasswordReason = await firstValueFrom(
|
||||
this.masterPasswordService.forceSetPasswordReason$(this.userId),
|
||||
this.masterPasswordService.forceSetPasswordReason$(this.activeUserId),
|
||||
);
|
||||
|
||||
this.route.queryParams
|
||||
@@ -111,7 +111,7 @@ export class SetPasswordComponent extends BaseChangePasswordComponent implements
|
||||
} else {
|
||||
// Try to get orgSsoId from state as fallback
|
||||
// Note: this is primarily for the TDE user w/out MP obtains admin MP reset permission scenario.
|
||||
return this.ssoLoginService.getActiveUserOrganizationSsoIdentifier();
|
||||
return this.ssoLoginService.getActiveUserOrganizationSsoIdentifier(this.activeUserId);
|
||||
}
|
||||
}),
|
||||
filter((orgSsoId) => orgSsoId != null),
|
||||
@@ -167,10 +167,10 @@ export class SetPasswordComponent extends BaseChangePasswordComponent implements
|
||||
|
||||
// in case we have a local private key, and are not sure whether it has been posted to the server, we post the local private key instead of generating a new one
|
||||
const existingUserPrivateKey = (await firstValueFrom(
|
||||
this.keyService.userPrivateKey$(this.userId),
|
||||
this.keyService.userPrivateKey$(this.activeUserId),
|
||||
)) as Uint8Array;
|
||||
const existingUserPublicKey = await firstValueFrom(
|
||||
this.keyService.userPublicKey$(this.userId),
|
||||
this.keyService.userPublicKey$(this.activeUserId),
|
||||
);
|
||||
if (existingUserPrivateKey != null && existingUserPublicKey != null) {
|
||||
const existingUserPublicKeyB64 = Utils.fromBufferToB64(existingUserPublicKey);
|
||||
@@ -217,7 +217,7 @@ export class SetPasswordComponent extends BaseChangePasswordComponent implements
|
||||
|
||||
return this.organizationUserApiService.putOrganizationUserResetPasswordEnrollment(
|
||||
this.orgId,
|
||||
this.userId,
|
||||
this.activeUserId,
|
||||
resetRequest,
|
||||
);
|
||||
});
|
||||
@@ -260,7 +260,7 @@ export class SetPasswordComponent extends BaseChangePasswordComponent implements
|
||||
// Clear force set password reason to allow navigation back to vault.
|
||||
await this.masterPasswordService.setForceSetPasswordReason(
|
||||
ForceSetPasswordReason.None,
|
||||
this.userId,
|
||||
this.activeUserId,
|
||||
);
|
||||
|
||||
// User now has a password so update account decryption options in state
|
||||
@@ -269,9 +269,9 @@ export class SetPasswordComponent extends BaseChangePasswordComponent implements
|
||||
);
|
||||
userDecryptionOpts.hasMasterPassword = true;
|
||||
await this.userDecryptionOptionsService.setUserDecryptionOptions(userDecryptionOpts);
|
||||
await this.kdfConfigService.setKdfConfig(this.userId, this.kdfConfig);
|
||||
await this.masterPasswordService.setMasterKey(masterKey, this.userId);
|
||||
await this.keyService.setUserKey(userKey[0], this.userId);
|
||||
await this.kdfConfigService.setKdfConfig(this.activeUserId, this.kdfConfig);
|
||||
await this.masterPasswordService.setMasterKey(masterKey, this.activeUserId);
|
||||
await this.keyService.setUserKey(userKey[0], this.activeUserId);
|
||||
|
||||
// Set private key only for new JIT provisioned users in MP encryption orgs
|
||||
// Existing TDE users will have private key set on sync or on login
|
||||
@@ -280,7 +280,7 @@ export class SetPasswordComponent extends BaseChangePasswordComponent implements
|
||||
this.forceSetPasswordReason !=
|
||||
ForceSetPasswordReason.TdeUserWithoutPasswordHasPasswordResetPermission
|
||||
) {
|
||||
await this.keyService.setPrivateKey(keyPair[1].encryptedString, this.userId);
|
||||
await this.keyService.setPrivateKey(keyPair[1].encryptedString, this.activeUserId);
|
||||
}
|
||||
|
||||
const localMasterKeyHash = await this.keyService.hashMasterKey(
|
||||
@@ -288,6 +288,6 @@ export class SetPasswordComponent extends BaseChangePasswordComponent implements
|
||||
masterKey,
|
||||
HashPurpose.LocalAuthorization,
|
||||
);
|
||||
await this.masterPasswordService.setMasterKeyHash(localMasterKeyHash, this.userId);
|
||||
await this.masterPasswordService.setMasterKeyHash(localMasterKeyHash, this.activeUserId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Directive, OnInit } from "@angular/core";
|
||||
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
|
||||
import { ActivatedRoute, NavigationExtras, Router } from "@angular/router";
|
||||
import { firstValueFrom } from "rxjs";
|
||||
import { first } from "rxjs/operators";
|
||||
@@ -27,6 +28,7 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service"
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { ToastService } from "@bitwarden/components";
|
||||
import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy";
|
||||
|
||||
@@ -55,6 +57,7 @@ export class SsoComponent implements OnInit {
|
||||
protected redirectUri: string;
|
||||
protected state: string;
|
||||
protected codeChallenge: string;
|
||||
protected activeUserId: UserId;
|
||||
|
||||
constructor(
|
||||
protected ssoLoginService: SsoLoginServiceAbstraction,
|
||||
@@ -74,7 +77,11 @@ export class SsoComponent implements OnInit {
|
||||
protected masterPasswordService: InternalMasterPasswordServiceAbstraction,
|
||||
protected accountService: AccountService,
|
||||
protected toastService: ToastService,
|
||||
) {}
|
||||
) {
|
||||
this.accountService.activeAccount$.pipe(takeUntilDestroyed()).subscribe((account) => {
|
||||
this.activeUserId = account?.id;
|
||||
});
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
// eslint-disable-next-line rxjs/no-async-subscribe
|
||||
@@ -226,7 +233,10 @@ export class SsoComponent implements OnInit {
|
||||
// - TDE login decryption options component
|
||||
// - Browser SSO on extension open
|
||||
// Note: you cannot set this in state before 2FA b/c there won't be an account in state.
|
||||
await this.ssoLoginService.setActiveUserOrganizationSsoIdentifier(orgSsoIdentifier);
|
||||
await this.ssoLoginService.setActiveUserOrganizationSsoIdentifier(
|
||||
orgSsoIdentifier,
|
||||
this.activeUserId,
|
||||
);
|
||||
|
||||
// Users enrolled in admin acct recovery can be forced to set a new password after
|
||||
// having the admin set a temp password for them (affects TDE & standard users)
|
||||
|
||||
@@ -69,7 +69,7 @@
|
||||
</a>
|
||||
</div>
|
||||
<div class="text-center">
|
||||
<a bitLink href="#" appStopClick (click)="selectOtherTwofactorMethod()">{{
|
||||
<a bitLink href="#" appStopClick (click)="selectOtherTwoFactorMethod()">{{
|
||||
"useAnotherTwoStepMethod" | i18n
|
||||
}}</a>
|
||||
</div>
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// @ts-strict-ignore
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { Component, Inject, OnInit, ViewChild } from "@angular/core";
|
||||
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
|
||||
import { FormBuilder, ReactiveFormsModule, Validators } from "@angular/forms";
|
||||
import { ActivatedRoute, NavigationExtras, Router, RouterLink } from "@angular/router";
|
||||
import { Subject, takeUntil, lastValueFrom, first, firstValueFrom } from "rxjs";
|
||||
@@ -31,6 +32,7 @@ import { EnvironmentService } from "@bitwarden/common/platform/abstractions/envi
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import {
|
||||
AsyncActionsModule,
|
||||
ButtonModule,
|
||||
@@ -126,6 +128,7 @@ export class TwoFactorAuthComponent extends CaptchaProtectedComponent implements
|
||||
protected changePasswordRoute = "set-password";
|
||||
protected forcePasswordResetRoute = "update-temp-password";
|
||||
protected successRoute = "vault";
|
||||
protected activeUserId: UserId;
|
||||
|
||||
constructor(
|
||||
protected loginStrategyService: LoginStrategyServiceAbstraction,
|
||||
@@ -148,6 +151,10 @@ export class TwoFactorAuthComponent extends CaptchaProtectedComponent implements
|
||||
protected toastService: ToastService,
|
||||
) {
|
||||
super(environmentService, i18nService, platformUtilsService, toastService);
|
||||
|
||||
this.accountService.activeAccount$.pipe(takeUntilDestroyed()).subscribe((account) => {
|
||||
this.activeUserId = account?.id;
|
||||
});
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
@@ -214,7 +221,7 @@ export class TwoFactorAuthComponent extends CaptchaProtectedComponent implements
|
||||
}
|
||||
}
|
||||
|
||||
async selectOtherTwofactorMethod() {
|
||||
async selectOtherTwoFactorMethod() {
|
||||
const dialogRef = TwoFactorOptionsComponent.open(this.dialogService);
|
||||
const response: TwoFactorOptionsDialogResultType = await lastValueFrom(dialogRef.closed);
|
||||
if (response.result === TwoFactorOptionsDialogResult.Provider) {
|
||||
@@ -262,7 +269,10 @@ export class TwoFactorAuthComponent extends CaptchaProtectedComponent implements
|
||||
// Save off the OrgSsoIdentifier for use in the TDE flows
|
||||
// - TDE login decryption options component
|
||||
// - Browser SSO on extension open
|
||||
await this.ssoLoginService.setActiveUserOrganizationSsoIdentifier(this.orgIdentifier);
|
||||
await this.ssoLoginService.setActiveUserOrganizationSsoIdentifier(
|
||||
this.orgIdentifier,
|
||||
this.activeUserId,
|
||||
);
|
||||
this.loginEmailService.clearValues();
|
||||
|
||||
// note: this flow affects both TDE & standard users
|
||||
|
||||
@@ -35,6 +35,7 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { ToastService } from "@bitwarden/components";
|
||||
|
||||
import { CaptchaProtectedComponent } from "./captcha-protected.component";
|
||||
@@ -73,6 +74,8 @@ export class TwoFactorComponent extends CaptchaProtectedComponent implements OnI
|
||||
protected successRoute = "vault";
|
||||
protected twoFactorTimeoutRoute = "authentication-timeout";
|
||||
|
||||
protected activeUserId: UserId;
|
||||
|
||||
get isDuoProvider(): boolean {
|
||||
return (
|
||||
this.selectedProviderType === TwoFactorProviderType.Duo ||
|
||||
@@ -102,8 +105,13 @@ export class TwoFactorComponent extends CaptchaProtectedComponent implements OnI
|
||||
protected toastService: ToastService,
|
||||
) {
|
||||
super(environmentService, i18nService, platformUtilsService, toastService);
|
||||
|
||||
this.webAuthnSupported = this.platformUtilsService.supportsWebAuthn(win);
|
||||
|
||||
this.accountService.activeAccount$.pipe(takeUntilDestroyed()).subscribe((account) => {
|
||||
this.activeUserId = account?.id;
|
||||
});
|
||||
|
||||
// Add subscription to authenticationSessionTimeout$ and navigate to twoFactorTimeoutRoute if expired
|
||||
this.loginStrategyService.authenticationSessionTimeout$
|
||||
.pipe(takeUntilDestroyed())
|
||||
@@ -287,7 +295,10 @@ export class TwoFactorComponent extends CaptchaProtectedComponent implements OnI
|
||||
// Save off the OrgSsoIdentifier for use in the TDE flows
|
||||
// - TDE login decryption options component
|
||||
// - Browser SSO on extension open
|
||||
await this.ssoLoginService.setActiveUserOrganizationSsoIdentifier(this.orgIdentifier);
|
||||
await this.ssoLoginService.setActiveUserOrganizationSsoIdentifier(
|
||||
this.orgIdentifier,
|
||||
this.activeUserId,
|
||||
);
|
||||
this.loginEmailService.clearValues();
|
||||
|
||||
// note: this flow affects both TDE & standard users
|
||||
|
||||
@@ -296,7 +296,7 @@ import {
|
||||
DefaultUserAsymmetricKeysRegenerationApiService,
|
||||
} from "@bitwarden/key-management";
|
||||
import { SafeInjectionToken } from "@bitwarden/ui-common";
|
||||
import { PasswordRepromptService } from "@bitwarden/vault";
|
||||
import { NewDeviceVerificationNoticeService, PasswordRepromptService } from "@bitwarden/vault";
|
||||
import {
|
||||
VaultExportService,
|
||||
VaultExportServiceAbstraction,
|
||||
@@ -306,9 +306,6 @@ import {
|
||||
IndividualVaultExportServiceAbstraction,
|
||||
} from "@bitwarden/vault-export-core";
|
||||
|
||||
// FIXME: remove `src` and fix import
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { NewDeviceVerificationNoticeService } from "../../../vault/src/services/new-device-verification-notice.service";
|
||||
import { FormValidationErrorsService as FormValidationErrorsServiceAbstraction } from "../platform/abstractions/form-validation-errors.service";
|
||||
import { ViewCacheService } from "../platform/abstractions/view-cache.service";
|
||||
import { FormValidationErrorsService } from "../platform/services/form-validation-errors.service";
|
||||
@@ -799,7 +796,7 @@ const safeProviders: SafeProvider[] = [
|
||||
safeProvider({
|
||||
provide: SsoLoginServiceAbstraction,
|
||||
useClass: SsoLoginService,
|
||||
deps: [StateProvider],
|
||||
deps: [StateProvider, LogService],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: STATE_FACTORY,
|
||||
|
||||
@@ -8,10 +8,8 @@ import { Account, AccountService } from "@bitwarden/common/auth/abstractions/acc
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { NewDeviceVerificationNoticeService } from "@bitwarden/vault";
|
||||
|
||||
// FIXME: remove `src` and fix import
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { NewDeviceVerificationNoticeService } from "../../../../vault/src/services/new-device-verification-notice.service";
|
||||
import { VaultProfileService } from "../services/vault-profile.service";
|
||||
|
||||
import { NewDeviceVerificationNoticeGuard } from "./new-device-verification-notice.guard";
|
||||
|
||||
@@ -8,10 +8,8 @@ import { Account, AccountService } from "@bitwarden/common/auth/abstractions/acc
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { NewDeviceVerificationNoticeService } from "@bitwarden/vault";
|
||||
|
||||
// FIXME: remove `src` and fix import
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { NewDeviceVerificationNoticeService } from "../../../../vault/src/services/new-device-verification-notice.service";
|
||||
import { VaultProfileService } from "../services/vault-profile.service";
|
||||
|
||||
export const NewDeviceVerificationNoticeGuard: CanActivateFn = async (
|
||||
|
||||
@@ -16,14 +16,11 @@ import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folde
|
||||
import { TreeNode } from "@bitwarden/common/vault/models/domain/tree-node";
|
||||
import { FolderView } from "@bitwarden/common/vault/models/view/folder.view";
|
||||
import { ServiceUtils } from "@bitwarden/common/vault/service-utils";
|
||||
import { COLLAPSED_GROUPINGS } from "@bitwarden/common/vault/services/key-state/collapsed-groupings.state";
|
||||
|
||||
import { DeprecatedVaultFilterService as DeprecatedVaultFilterServiceAbstraction } from "../../abstractions/deprecated-vault-filter.service";
|
||||
import { DynamicTreeNode } from "../models/dynamic-tree-node.model";
|
||||
|
||||
// FIXME: remove `src` and fix import
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { COLLAPSED_GROUPINGS } from "./../../../../../common/src/vault/services/key-state/collapsed-groupings.state";
|
||||
|
||||
const NestingDelimiter = "/";
|
||||
|
||||
@Injectable()
|
||||
|
||||
@@ -202,7 +202,7 @@ export class LoginDecryptionOptionsComponent implements OnInit {
|
||||
});
|
||||
|
||||
const autoEnrollStatus$ = defer(() =>
|
||||
this.ssoLoginService.getActiveUserOrganizationSsoIdentifier(),
|
||||
this.ssoLoginService.getActiveUserOrganizationSsoIdentifier(this.activeAccountId),
|
||||
).pipe(
|
||||
switchMap((organizationIdentifier) => {
|
||||
if (organizationIdentifier == undefined) {
|
||||
|
||||
@@ -36,6 +36,7 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service"
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import {
|
||||
AsyncActionsModule,
|
||||
ButtonModule,
|
||||
@@ -89,6 +90,7 @@ export class SsoComponent implements OnInit {
|
||||
protected state: string | undefined;
|
||||
protected codeChallenge: string | undefined;
|
||||
protected clientId: SsoClientType | undefined;
|
||||
protected activeUserId: UserId | undefined;
|
||||
|
||||
formPromise: Promise<AuthResult> | undefined;
|
||||
initiateSsoFormPromise: Promise<SsoPreValidateResponse> | undefined;
|
||||
@@ -130,6 +132,8 @@ export class SsoComponent implements OnInit {
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
this.activeUserId = (await firstValueFrom(this.accountService.activeAccount$))?.id;
|
||||
|
||||
const qParams: QueryParams = await firstValueFrom(this.route.queryParams);
|
||||
|
||||
// This if statement will pass on the second portion of the SSO flow
|
||||
@@ -384,7 +388,10 @@ export class SsoComponent implements OnInit {
|
||||
// - TDE login decryption options component
|
||||
// - Browser SSO on extension open
|
||||
// Note: you cannot set this in state before 2FA b/c there won't be an account in state.
|
||||
await this.ssoLoginService.setActiveUserOrganizationSsoIdentifier(orgSsoIdentifier);
|
||||
await this.ssoLoginService.setActiveUserOrganizationSsoIdentifier(
|
||||
orgSsoIdentifier,
|
||||
this.activeUserId,
|
||||
);
|
||||
|
||||
// Users enrolled in admin acct recovery can be forced to set a new password after
|
||||
// having the admin set a temp password for them (affects TDE & standard users)
|
||||
|
||||
@@ -120,7 +120,7 @@
|
||||
<div class="tw-mb-6" *ngIf="sentInitialCode">
|
||||
{{ "enterVerificationCodeSentToEmail" | i18n }}
|
||||
|
||||
<p class="mb-0">
|
||||
<p class="tw-mb-0">
|
||||
<button bitLink type="button" linkType="primary" (click)="requestOTP()">
|
||||
{{ "resendCode" | i18n }}
|
||||
</button>
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { AddableOrganizationResponse } from "@bitwarden/common/admin-console/models/response/addable-organization.response";
|
||||
|
||||
import { ProviderSetupRequest } from "../../models/request/provider/provider-setup.request";
|
||||
import { ProviderUpdateRequest } from "../../models/request/provider/provider-update.request";
|
||||
import { ProviderVerifyRecoverDeleteRequest } from "../../models/request/provider/provider-verify-recover-delete.request";
|
||||
@@ -14,4 +16,12 @@ export class ProviderApiServiceAbstraction {
|
||||
request: ProviderVerifyRecoverDeleteRequest,
|
||||
) => Promise<any>;
|
||||
deleteProvider: (id: string) => Promise<void>;
|
||||
getProviderAddableOrganizations: (providerId: string) => Promise<AddableOrganizationResponse[]>;
|
||||
addOrganizationToProvider: (
|
||||
providerId: string,
|
||||
request: {
|
||||
key: string;
|
||||
organizationId: string;
|
||||
},
|
||||
) => Promise<void>;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import { BaseResponse } from "@bitwarden/common/models/response/base.response";
|
||||
|
||||
export class AddableOrganizationResponse extends BaseResponse {
|
||||
id: string;
|
||||
plan: string;
|
||||
name: string;
|
||||
seats: number;
|
||||
disabled: boolean;
|
||||
|
||||
constructor(response: any) {
|
||||
super(response);
|
||||
this.id = this.getResponseProperty("id");
|
||||
this.plan = this.getResponseProperty("plan");
|
||||
this.name = this.getResponseProperty("name");
|
||||
this.seats = this.getResponseProperty("seats");
|
||||
this.disabled = this.getResponseProperty("disabled");
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
import { AddableOrganizationResponse } from "@bitwarden/common/admin-console/models/response/addable-organization.response";
|
||||
|
||||
import { ApiService } from "../../../abstractions/api.service";
|
||||
import { ProviderApiServiceAbstraction } from "../../abstractions/provider/provider-api.service.abstraction";
|
||||
import { ProviderSetupRequest } from "../../models/request/provider/provider-setup.request";
|
||||
@@ -44,4 +46,34 @@ export class ProviderApiService implements ProviderApiServiceAbstraction {
|
||||
async deleteProvider(id: string): Promise<void> {
|
||||
await this.apiService.send("DELETE", "/providers/" + id, null, true, false);
|
||||
}
|
||||
|
||||
async getProviderAddableOrganizations(
|
||||
providerId: string,
|
||||
): Promise<AddableOrganizationResponse[]> {
|
||||
const response = await this.apiService.send(
|
||||
"GET",
|
||||
"/providers/" + providerId + "/clients/addable",
|
||||
null,
|
||||
true,
|
||||
true,
|
||||
);
|
||||
|
||||
return response.map((data: any) => new AddableOrganizationResponse(data));
|
||||
}
|
||||
|
||||
addOrganizationToProvider(
|
||||
providerId: string,
|
||||
request: {
|
||||
key: string;
|
||||
organizationId: string;
|
||||
},
|
||||
): Promise<void> {
|
||||
return this.apiService.send(
|
||||
"POST",
|
||||
"/providers/" + providerId + "/clients/existing",
|
||||
request,
|
||||
true,
|
||||
false,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
|
||||
export abstract class SsoLoginServiceAbstraction {
|
||||
/**
|
||||
* Gets the code verifier used for SSO.
|
||||
@@ -74,12 +76,16 @@ export abstract class SsoLoginServiceAbstraction {
|
||||
* Gets the value of the active user's organization sso identifier.
|
||||
*
|
||||
* This should only be used post successful SSO login once the user is initialized.
|
||||
* @param userId The user id for retrieving the org identifier state.
|
||||
*/
|
||||
getActiveUserOrganizationSsoIdentifier: () => Promise<string>;
|
||||
getActiveUserOrganizationSsoIdentifier: (userId: UserId) => Promise<string>;
|
||||
/**
|
||||
* Sets the value of the active user's organization sso identifier.
|
||||
*
|
||||
* This should only be used post successful SSO login once the user is initialized.
|
||||
*/
|
||||
setActiveUserOrganizationSsoIdentifier: (organizationIdentifier: string) => Promise<void>;
|
||||
setActiveUserOrganizationSsoIdentifier: (
|
||||
organizationIdentifier: string,
|
||||
userId: UserId | undefined,
|
||||
) => Promise<void>;
|
||||
}
|
||||
|
||||
94
libs/common/src/auth/services/sso-login.service.spec.ts
Normal file
94
libs/common/src/auth/services/sso-login.service.spec.ts
Normal file
@@ -0,0 +1,94 @@
|
||||
import { mock, MockProxy } from "jest-mock-extended";
|
||||
|
||||
import {
|
||||
CODE_VERIFIER,
|
||||
GLOBAL_ORGANIZATION_SSO_IDENTIFIER,
|
||||
SSO_EMAIL,
|
||||
SSO_STATE,
|
||||
SsoLoginService,
|
||||
USER_ORGANIZATION_SSO_IDENTIFIER,
|
||||
} from "@bitwarden/common/auth/services/sso-login.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
|
||||
import { FakeAccountService, FakeStateProvider, mockAccountServiceWith } from "../../../spec";
|
||||
|
||||
describe("SSOLoginService ", () => {
|
||||
let sut: SsoLoginService;
|
||||
|
||||
let accountService: FakeAccountService;
|
||||
let mockSingleUserStateProvider: FakeStateProvider;
|
||||
let mockLogService: MockProxy<LogService>;
|
||||
let userId: UserId;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
|
||||
userId = Utils.newGuid() as UserId;
|
||||
accountService = mockAccountServiceWith(userId);
|
||||
mockSingleUserStateProvider = new FakeStateProvider(accountService);
|
||||
mockLogService = mock<LogService>();
|
||||
|
||||
sut = new SsoLoginService(mockSingleUserStateProvider, mockLogService);
|
||||
});
|
||||
|
||||
it("instantiates", () => {
|
||||
expect(sut).not.toBeFalsy();
|
||||
});
|
||||
|
||||
it("gets and sets code verifier", async () => {
|
||||
const codeVerifier = "test-code-verifier";
|
||||
await sut.setCodeVerifier(codeVerifier);
|
||||
mockSingleUserStateProvider.getGlobal(CODE_VERIFIER);
|
||||
|
||||
const result = await sut.getCodeVerifier();
|
||||
expect(result).toBe(codeVerifier);
|
||||
});
|
||||
|
||||
it("gets and sets SSO state", async () => {
|
||||
const ssoState = "test-sso-state";
|
||||
await sut.setSsoState(ssoState);
|
||||
mockSingleUserStateProvider.getGlobal(SSO_STATE);
|
||||
|
||||
const result = await sut.getSsoState();
|
||||
expect(result).toBe(ssoState);
|
||||
});
|
||||
|
||||
it("gets and sets organization SSO identifier", async () => {
|
||||
const orgIdentifier = "test-org-identifier";
|
||||
await sut.setOrganizationSsoIdentifier(orgIdentifier);
|
||||
mockSingleUserStateProvider.getGlobal(GLOBAL_ORGANIZATION_SSO_IDENTIFIER);
|
||||
|
||||
const result = await sut.getOrganizationSsoIdentifier();
|
||||
expect(result).toBe(orgIdentifier);
|
||||
});
|
||||
|
||||
it("gets and sets SSO email", async () => {
|
||||
const email = "test@example.com";
|
||||
await sut.setSsoEmail(email);
|
||||
mockSingleUserStateProvider.getGlobal(SSO_EMAIL);
|
||||
|
||||
const result = await sut.getSsoEmail();
|
||||
expect(result).toBe(email);
|
||||
});
|
||||
|
||||
it("gets and sets active user organization SSO identifier", async () => {
|
||||
const userId = Utils.newGuid() as UserId;
|
||||
const orgIdentifier = "test-active-org-identifier";
|
||||
await sut.setActiveUserOrganizationSsoIdentifier(orgIdentifier, userId);
|
||||
mockSingleUserStateProvider.getUser(userId, USER_ORGANIZATION_SSO_IDENTIFIER);
|
||||
|
||||
const result = await sut.getActiveUserOrganizationSsoIdentifier(userId);
|
||||
expect(result).toBe(orgIdentifier);
|
||||
});
|
||||
|
||||
it("logs error when setting active user organization SSO identifier with undefined userId", async () => {
|
||||
const orgIdentifier = "test-active-org-identifier";
|
||||
await sut.setActiveUserOrganizationSsoIdentifier(orgIdentifier, undefined);
|
||||
|
||||
expect(mockLogService.warning).toHaveBeenCalledWith(
|
||||
"Tried to set a user organization sso identifier with an undefined user id.",
|
||||
);
|
||||
});
|
||||
});
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user