mirror of
https://github.com/bitwarden/browser
synced 2026-03-02 03:21:19 +00:00
Merge branch 'main' into ps/extension-refresh
This commit is contained in:
@@ -1434,6 +1434,24 @@
|
||||
"typeIdentity": {
|
||||
"message": "Identity"
|
||||
},
|
||||
"newItemHeader":{
|
||||
"message": "New $TYPE$",
|
||||
"placeholders": {
|
||||
"type": {
|
||||
"content": "$1",
|
||||
"example": "Login"
|
||||
}
|
||||
}
|
||||
},
|
||||
"editItemHeader":{
|
||||
"message": "Edit $TYPE$",
|
||||
"placeholders": {
|
||||
"type": {
|
||||
"content": "$1",
|
||||
"example": "Login"
|
||||
}
|
||||
}
|
||||
},
|
||||
"passwordHistory": {
|
||||
"message": "Password history"
|
||||
},
|
||||
|
||||
@@ -1,71 +0,0 @@
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
|
||||
import AutofillPageDetails from "../models/autofill-page-details";
|
||||
import { AutofillService } from "../services/abstractions/autofill.service";
|
||||
|
||||
export class AutofillTabCommand {
|
||||
constructor(private autofillService: AutofillService) {}
|
||||
|
||||
async doAutofillTabCommand(tab: chrome.tabs.Tab) {
|
||||
if (!tab.id) {
|
||||
throw new Error("Tab does not have an id, cannot complete autofill.");
|
||||
}
|
||||
|
||||
const details = await this.collectPageDetails(tab.id);
|
||||
await this.autofillService.doAutoFillOnTab(
|
||||
[
|
||||
{
|
||||
frameId: 0,
|
||||
tab: tab,
|
||||
details: details,
|
||||
},
|
||||
],
|
||||
tab,
|
||||
true,
|
||||
);
|
||||
}
|
||||
|
||||
async doAutofillTabWithCipherCommand(tab: chrome.tabs.Tab, cipher: CipherView) {
|
||||
if (!tab.id) {
|
||||
throw new Error("Tab does not have an id, cannot complete autofill.");
|
||||
}
|
||||
|
||||
const details = await this.collectPageDetails(tab.id);
|
||||
await this.autofillService.doAutoFill({
|
||||
tab: tab,
|
||||
cipher: cipher,
|
||||
pageDetails: [
|
||||
{
|
||||
frameId: 0,
|
||||
tab: tab,
|
||||
details: details,
|
||||
},
|
||||
],
|
||||
skipLastUsed: false,
|
||||
skipUsernameOnlyFill: false,
|
||||
onlyEmptyFields: false,
|
||||
onlyVisibleFields: false,
|
||||
fillNewPassword: true,
|
||||
allowTotpAutofill: true,
|
||||
});
|
||||
}
|
||||
|
||||
private async collectPageDetails(tabId: number): Promise<AutofillPageDetails> {
|
||||
return new Promise((resolve, reject) => {
|
||||
chrome.tabs.sendMessage(
|
||||
tabId,
|
||||
{
|
||||
command: "collectPageDetailsImmediately",
|
||||
},
|
||||
(response: AutofillPageDetails) => {
|
||||
if (chrome.runtime.lastError) {
|
||||
reject(chrome.runtime.lastError);
|
||||
return;
|
||||
}
|
||||
|
||||
resolve(response);
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
14
apps/browser/src/autofill/enums/autofill-message.enums.ts
Normal file
14
apps/browser/src/autofill/enums/autofill-message.enums.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
export const AutofillMessageCommand = {
|
||||
collectPageDetails: "collectPageDetails",
|
||||
collectPageDetailsResponse: "collectPageDetailsResponse",
|
||||
} as const;
|
||||
|
||||
export type AutofillMessageCommandType =
|
||||
(typeof AutofillMessageCommand)[keyof typeof AutofillMessageCommand];
|
||||
|
||||
export const AutofillMessageSender = {
|
||||
collectPageDetailsFromTabObservable: "collectPageDetailsFromTabObservable",
|
||||
} as const;
|
||||
|
||||
export type AutofillMessageSenderType =
|
||||
(typeof AutofillMessageSender)[keyof typeof AutofillMessageSender];
|
||||
@@ -1,7 +1,11 @@
|
||||
import { Observable } from "rxjs";
|
||||
|
||||
import { UriMatchStrategySetting } from "@bitwarden/common/models/domain/domain-service";
|
||||
import { CommandDefinition } from "@bitwarden/common/platform/messaging";
|
||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
|
||||
import { AutofillMessageCommand } from "../../enums/autofill-message.enums";
|
||||
import AutofillField from "../../models/autofill-field";
|
||||
import AutofillForm from "../../models/autofill-form";
|
||||
import AutofillPageDetails from "../../models/autofill-page-details";
|
||||
@@ -44,7 +48,20 @@ export interface GenerateFillScriptOptions {
|
||||
defaultUriMatch: UriMatchStrategySetting;
|
||||
}
|
||||
|
||||
export type CollectPageDetailsResponseMessage = {
|
||||
tab: chrome.tabs.Tab;
|
||||
details: AutofillPageDetails;
|
||||
sender?: string;
|
||||
webExtSender: chrome.runtime.MessageSender;
|
||||
};
|
||||
|
||||
export const COLLECT_PAGE_DETAILS_RESPONSE_COMMAND =
|
||||
new CommandDefinition<CollectPageDetailsResponseMessage>(
|
||||
AutofillMessageCommand.collectPageDetailsResponse,
|
||||
);
|
||||
|
||||
export abstract class AutofillService {
|
||||
collectPageDetailsFromTab$: (tab: chrome.tabs.Tab) => Observable<PageDetail[]>;
|
||||
loadAutofillScriptsOnInstall: () => Promise<void>;
|
||||
reloadAutofillScripts: () => Promise<void>;
|
||||
injectAutofillScripts: (
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { mock, mockReset, MockProxy } from "jest-mock-extended";
|
||||
import { BehaviorSubject, of } from "rxjs";
|
||||
import { BehaviorSubject, of, Subject } from "rxjs";
|
||||
|
||||
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
|
||||
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
||||
@@ -16,12 +16,14 @@ import { EventType } from "@bitwarden/common/enums";
|
||||
import { UriMatchStrategy } from "@bitwarden/common/models/domain/domain-service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { MessageListener } from "@bitwarden/common/platform/messaging";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { EventCollectionService } from "@bitwarden/common/services/event/event-collection.service";
|
||||
import {
|
||||
FakeStateProvider,
|
||||
FakeAccountService,
|
||||
mockAccountServiceWith,
|
||||
subscribeTo,
|
||||
} from "@bitwarden/common/spec";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { FieldType, LinkedIdType, LoginLinkedId, CipherType } from "@bitwarden/common/vault/enums";
|
||||
@@ -37,6 +39,7 @@ import { TotpService } from "@bitwarden/common/vault/services/totp.service";
|
||||
|
||||
import { BrowserApi } from "../../platform/browser/browser-api";
|
||||
import { BrowserScriptInjectorService } from "../../platform/services/browser-script-injector.service";
|
||||
import { AutofillMessageCommand, AutofillMessageSender } from "../enums/autofill-message.enums";
|
||||
import { AutofillPort } from "../enums/autofill-port.enums";
|
||||
import AutofillField from "../models/autofill-field";
|
||||
import AutofillPageDetails from "../models/autofill-page-details";
|
||||
@@ -52,6 +55,7 @@ import { flushPromises, triggerTestFailure } from "../spec/testing-utils";
|
||||
|
||||
import {
|
||||
AutoFillOptions,
|
||||
CollectPageDetailsResponseMessage,
|
||||
GenerateFillScriptOptions,
|
||||
PageDetail,
|
||||
} from "./abstractions/autofill.service";
|
||||
@@ -82,6 +86,7 @@ describe("AutofillService", () => {
|
||||
const platformUtilsService = mock<PlatformUtilsService>();
|
||||
let activeAccountStatusMock$: BehaviorSubject<AuthenticationStatus>;
|
||||
let authService: MockProxy<AuthService>;
|
||||
let messageListener: MockProxy<MessageListener>;
|
||||
|
||||
beforeEach(() => {
|
||||
scriptInjectorService = new BrowserScriptInjectorService(platformUtilsService, logService);
|
||||
@@ -91,6 +96,7 @@ describe("AutofillService", () => {
|
||||
activeAccountStatusMock$ = new BehaviorSubject(AuthenticationStatus.Unlocked);
|
||||
authService = mock<AuthService>();
|
||||
authService.activeAccountStatus$ = activeAccountStatusMock$;
|
||||
messageListener = mock<MessageListener>();
|
||||
autofillService = new AutofillService(
|
||||
cipherService,
|
||||
autofillSettingsService,
|
||||
@@ -103,10 +109,11 @@ describe("AutofillService", () => {
|
||||
scriptInjectorService,
|
||||
accountService,
|
||||
authService,
|
||||
messageListener,
|
||||
);
|
||||
|
||||
domainSettingsService = new DefaultDomainSettingsService(fakeStateProvider);
|
||||
domainSettingsService.equivalentDomains$ = of(mockEquivalentDomains);
|
||||
jest.spyOn(BrowserApi, "tabSendMessage");
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@@ -114,6 +121,84 @@ describe("AutofillService", () => {
|
||||
mockReset(cipherService);
|
||||
});
|
||||
|
||||
describe("collectPageDetailsFromTab$", () => {
|
||||
const tab = mock<chrome.tabs.Tab>({ id: 1 });
|
||||
const messages = new Subject<CollectPageDetailsResponseMessage>();
|
||||
|
||||
function mockCollectPageDetailsResponseMessage(
|
||||
tab: chrome.tabs.Tab,
|
||||
webExtSender: chrome.runtime.MessageSender = mock<chrome.runtime.MessageSender>(),
|
||||
sender: string = AutofillMessageSender.collectPageDetailsFromTabObservable,
|
||||
): CollectPageDetailsResponseMessage {
|
||||
return mock<CollectPageDetailsResponseMessage>({
|
||||
tab,
|
||||
webExtSender,
|
||||
sender,
|
||||
});
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
messageListener.messages$.mockReturnValue(messages.asObservable());
|
||||
});
|
||||
|
||||
it("sends a `collectPageDetails` message to the passed tab", () => {
|
||||
autofillService.collectPageDetailsFromTab$(tab);
|
||||
|
||||
expect(BrowserApi.tabSendMessage).toHaveBeenCalledWith(tab, {
|
||||
command: AutofillMessageCommand.collectPageDetails,
|
||||
sender: AutofillMessageSender.collectPageDetailsFromTabObservable,
|
||||
tab,
|
||||
});
|
||||
});
|
||||
|
||||
it("builds an array of page details from received `collectPageDetailsResponse` messages", async () => {
|
||||
const topLevelSender = mock<chrome.runtime.MessageSender>({ tab, frameId: 0 });
|
||||
const subFrameSender = mock<chrome.runtime.MessageSender>({ tab, frameId: 1 });
|
||||
|
||||
const tracker = subscribeTo(autofillService.collectPageDetailsFromTab$(tab));
|
||||
const pausePromise = tracker.pauseUntilReceived(2);
|
||||
|
||||
messages.next(mockCollectPageDetailsResponseMessage(tab, topLevelSender));
|
||||
messages.next(mockCollectPageDetailsResponseMessage(tab, subFrameSender));
|
||||
|
||||
await pausePromise;
|
||||
|
||||
expect(tracker.emissions[1].length).toBe(2);
|
||||
});
|
||||
|
||||
it("ignores messages from a different tab", async () => {
|
||||
const otherTab = mock<chrome.tabs.Tab>({ id: 2 });
|
||||
|
||||
const tracker = subscribeTo(autofillService.collectPageDetailsFromTab$(tab));
|
||||
const pausePromise = tracker.pauseUntilReceived(1);
|
||||
|
||||
messages.next(mockCollectPageDetailsResponseMessage(tab));
|
||||
messages.next(mockCollectPageDetailsResponseMessage(otherTab));
|
||||
|
||||
await pausePromise;
|
||||
|
||||
expect(tracker.emissions[1]).toBeUndefined();
|
||||
});
|
||||
|
||||
it("ignores messages from a different sender", async () => {
|
||||
const tracker = subscribeTo(autofillService.collectPageDetailsFromTab$(tab));
|
||||
const pausePromise = tracker.pauseUntilReceived(1);
|
||||
|
||||
messages.next(mockCollectPageDetailsResponseMessage(tab));
|
||||
messages.next(
|
||||
mockCollectPageDetailsResponseMessage(
|
||||
tab,
|
||||
mock<chrome.runtime.MessageSender>(),
|
||||
"some-other-sender",
|
||||
),
|
||||
);
|
||||
|
||||
await pausePromise;
|
||||
|
||||
expect(tracker.emissions[1]).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("loadAutofillScriptsOnInstall", () => {
|
||||
let tab1: chrome.tabs.Tab;
|
||||
let tab2: chrome.tabs.Tab;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { firstValueFrom, startWith } from "rxjs";
|
||||
import { filter, firstValueFrom, Observable, scan, startWith } from "rxjs";
|
||||
import { pairwise } from "rxjs/operators";
|
||||
|
||||
import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
UriMatchStrategy,
|
||||
} from "@bitwarden/common/models/domain/domain-service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { MessageListener } from "@bitwarden/common/platform/messaging";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { TotpService } from "@bitwarden/common/vault/abstractions/totp.service";
|
||||
import { FieldType, CipherType } from "@bitwarden/common/vault/enums";
|
||||
@@ -27,6 +28,7 @@ import { FieldView } from "@bitwarden/common/vault/models/view/field.view";
|
||||
import { BrowserApi } from "../../platform/browser/browser-api";
|
||||
import { ScriptInjectorService } from "../../platform/services/abstractions/script-injector.service";
|
||||
import { openVaultItemPasswordRepromptPopout } from "../../vault/popup/utils/vault-popout-window";
|
||||
import { AutofillMessageCommand, AutofillMessageSender } from "../enums/autofill-message.enums";
|
||||
import { AutofillPort } from "../enums/autofill-port.enums";
|
||||
import AutofillField from "../models/autofill-field";
|
||||
import AutofillPageDetails from "../models/autofill-page-details";
|
||||
@@ -35,6 +37,7 @@ import AutofillScript from "../models/autofill-script";
|
||||
import {
|
||||
AutoFillOptions,
|
||||
AutofillService as AutofillServiceInterface,
|
||||
COLLECT_PAGE_DETAILS_RESPONSE_COMMAND,
|
||||
FormData,
|
||||
GenerateFillScriptOptions,
|
||||
PageDetail,
|
||||
@@ -64,8 +67,47 @@ export default class AutofillService implements AutofillServiceInterface {
|
||||
private scriptInjectorService: ScriptInjectorService,
|
||||
private accountService: AccountService,
|
||||
private authService: AuthService,
|
||||
private messageListener: MessageListener,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Collects page details from the specific tab. This method returns an observable that can
|
||||
* be subscribed to in order to build the results from all collectPageDetailsResponse
|
||||
* messages from the given tab.
|
||||
*
|
||||
* @param tab The tab to collect page details from
|
||||
*/
|
||||
collectPageDetailsFromTab$(tab: chrome.tabs.Tab): Observable<PageDetail[]> {
|
||||
const pageDetailsFromTab$ = this.messageListener
|
||||
.messages$(COLLECT_PAGE_DETAILS_RESPONSE_COMMAND)
|
||||
.pipe(
|
||||
filter(
|
||||
(message) =>
|
||||
message.tab.id === tab.id &&
|
||||
message.sender === AutofillMessageSender.collectPageDetailsFromTabObservable,
|
||||
),
|
||||
scan(
|
||||
(acc, message) => [
|
||||
...acc,
|
||||
{
|
||||
frameId: message.webExtSender.frameId,
|
||||
tab: message.tab,
|
||||
details: message.details,
|
||||
},
|
||||
],
|
||||
[] as PageDetail[],
|
||||
),
|
||||
);
|
||||
|
||||
void BrowserApi.tabSendMessage(tab, {
|
||||
tab: tab,
|
||||
command: AutofillMessageCommand.collectPageDetails,
|
||||
sender: AutofillMessageSender.collectPageDetailsFromTabObservable,
|
||||
});
|
||||
|
||||
return pageDetailsFromTab$;
|
||||
}
|
||||
|
||||
/**
|
||||
* Triggers on installation of the extension Handles injecting
|
||||
* content scripts into all tabs that are currently open, and
|
||||
|
||||
@@ -889,6 +889,7 @@ export default class MainBackground {
|
||||
this.scriptInjectorService,
|
||||
this.accountService,
|
||||
this.authService,
|
||||
messageListener,
|
||||
);
|
||||
this.auditService = new AuditService(this.cryptoFunctionService, this.apiService);
|
||||
|
||||
|
||||
@@ -57,6 +57,7 @@ import { VaultFilterComponent } from "../vault/popup/components/vault/vault-filt
|
||||
import { VaultItemsComponent } from "../vault/popup/components/vault/vault-items.component";
|
||||
import { VaultV2Component } from "../vault/popup/components/vault/vault-v2.component";
|
||||
import { ViewComponent } from "../vault/popup/components/vault/view.component";
|
||||
import { AddEditV2Component } from "../vault/popup/components/vault-v2/add-edit/add-edit-v2.component";
|
||||
import { AppearanceComponent } from "../vault/popup/settings/appearance.component";
|
||||
import { FolderAddEditComponent } from "../vault/popup/settings/folder-add-edit.component";
|
||||
import { FoldersComponent } from "../vault/popup/settings/folders.component";
|
||||
@@ -195,20 +196,18 @@ const routes: Routes = [
|
||||
canActivate: [AuthGuard],
|
||||
data: { state: "cipher-password-history" },
|
||||
},
|
||||
{
|
||||
...extensionRefreshSwap(AddEditComponent, AddEditV2Component, {
|
||||
path: "add-cipher",
|
||||
component: AddEditComponent,
|
||||
canActivate: [AuthGuard, debounceNavigationGuard()],
|
||||
data: { state: "add-cipher" },
|
||||
runGuardsAndResolvers: "always",
|
||||
},
|
||||
{
|
||||
}),
|
||||
...extensionRefreshSwap(AddEditComponent, AddEditV2Component, {
|
||||
path: "edit-cipher",
|
||||
component: AddEditComponent,
|
||||
canActivate: [AuthGuard, debounceNavigationGuard()],
|
||||
data: { state: "edit-cipher" },
|
||||
runGuardsAndResolvers: "always",
|
||||
},
|
||||
}),
|
||||
{
|
||||
path: "share-cipher",
|
||||
component: ShareComponent,
|
||||
|
||||
@@ -342,6 +342,7 @@ const safeProviders: SafeProvider[] = [
|
||||
ScriptInjectorService,
|
||||
AccountServiceAbstraction,
|
||||
AuthService,
|
||||
MessageListener,
|
||||
],
|
||||
}),
|
||||
safeProvider({
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
<popup-page>
|
||||
<popup-header slot="header" [pageTitle]="headerText" showBackButton> </popup-header>
|
||||
|
||||
<popup-footer slot="footer">
|
||||
<button bitButton type="button" buttonType="primary">
|
||||
{{ "save" | i18n }}
|
||||
</button>
|
||||
</popup-footer>
|
||||
</popup-page>
|
||||
@@ -0,0 +1,64 @@
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { Component } from "@angular/core";
|
||||
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
|
||||
import { FormsModule } from "@angular/forms";
|
||||
import { ActivatedRoute } from "@angular/router";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||
import { SearchModule, ButtonModule } from "@bitwarden/components";
|
||||
|
||||
import { PopupFooterComponent } from "../../../../../platform/popup/layout/popup-footer.component";
|
||||
import { PopupHeaderComponent } from "../../../../../platform/popup/layout/popup-header.component";
|
||||
import { PopupPageComponent } from "../../../../../platform/popup/layout/popup-page.component";
|
||||
|
||||
@Component({
|
||||
selector: "app-add-edit-v2",
|
||||
templateUrl: "add-edit-v2.component.html",
|
||||
standalone: true,
|
||||
imports: [
|
||||
CommonModule,
|
||||
SearchModule,
|
||||
JslibModule,
|
||||
FormsModule,
|
||||
ButtonModule,
|
||||
PopupPageComponent,
|
||||
PopupHeaderComponent,
|
||||
PopupFooterComponent,
|
||||
],
|
||||
})
|
||||
export class AddEditV2Component {
|
||||
headerText: string;
|
||||
|
||||
constructor(
|
||||
private route: ActivatedRoute,
|
||||
private i18nService: I18nService,
|
||||
) {
|
||||
this.subscribeToParams();
|
||||
}
|
||||
|
||||
subscribeToParams(): void {
|
||||
this.route.queryParams.pipe(takeUntilDestroyed()).subscribe((params) => {
|
||||
const isNew = params.isNew.toLowerCase() === "true";
|
||||
const cipherType = parseInt(params.type);
|
||||
|
||||
this.headerText = this.setHeader(isNew, cipherType);
|
||||
});
|
||||
}
|
||||
|
||||
setHeader(isNew: boolean, type: CipherType) {
|
||||
const partOne = isNew ? "newItemHeader" : "editItemHeader";
|
||||
|
||||
switch (type) {
|
||||
case CipherType.Login:
|
||||
return this.i18nService.t(partOne, this.i18nService.t("typeLogin"));
|
||||
case CipherType.Card:
|
||||
return this.i18nService.t(partOne, this.i18nService.t("typeCard"));
|
||||
case CipherType.Identity:
|
||||
return this.i18nService.t(partOne, this.i18nService.t("typeIdentity"));
|
||||
case CipherType.SecureNote:
|
||||
return this.i18nService.t(partOne, this.i18nService.t("note"));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
<button bitButton [bitMenuTriggerFor]="itemOptions" buttonType="primary" type="button">
|
||||
<i class="bwi bwi-plus-f" aria-hidden="true"></i>
|
||||
{{ "new" | i18n }}
|
||||
</button>
|
||||
<bit-menu #itemOptions>
|
||||
<a type="button" bitMenuItem (click)="newItemNavigate(cipherType.Login)">
|
||||
<i class="bwi bwi-globe" slot="start" aria-hidden="true"></i>
|
||||
{{ "typeLogin" | i18n }}
|
||||
</a>
|
||||
<a type="button" bitMenuItem (click)="newItemNavigate(cipherType.Card)">
|
||||
<i class="bwi bwi-credit-card" slot="start" aria-hidden="true"></i>
|
||||
{{ "typeCard" | i18n }}
|
||||
</a>
|
||||
<a type="button" bitMenuItem (click)="newItemNavigate(cipherType.Identity)">
|
||||
<i class="bwi bwi-id-card" slot="start" aria-hidden="true"></i>
|
||||
{{ "typeIdentity" | i18n }}
|
||||
</a>
|
||||
<a type="button" bitMenuItem (click)="newItemNavigate(cipherType.SecureNote)">
|
||||
<i class="bwi bwi-sticky-note" slot="start" aria-hidden="true"></i>
|
||||
{{ "note" | i18n }}
|
||||
</a>
|
||||
</bit-menu>
|
||||
@@ -0,0 +1,28 @@
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { Component, OnDestroy, OnInit } from "@angular/core";
|
||||
import { Router, RouterLink } from "@angular/router";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||
import { ButtonModule, NoItemsModule, MenuModule } from "@bitwarden/components";
|
||||
|
||||
@Component({
|
||||
selector: "app-new-item-dropdown",
|
||||
templateUrl: "new-item-dropdown-v2.component.html",
|
||||
standalone: true,
|
||||
imports: [NoItemsModule, JslibModule, CommonModule, ButtonModule, RouterLink, MenuModule],
|
||||
})
|
||||
export class NewItemDropdownV2Component implements OnInit, OnDestroy {
|
||||
cipherType = CipherType;
|
||||
|
||||
constructor(private router: Router) {}
|
||||
|
||||
ngOnInit(): void {}
|
||||
|
||||
ngOnDestroy(): void {}
|
||||
|
||||
// TODO PM-6826: add selectedVault query param
|
||||
newItemNavigate(type: CipherType) {
|
||||
void this.router.navigate(["/add-cipher"], { queryParams: { type: type, isNew: true } });
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { ChangeDetectorRef, Component, NgZone, OnDestroy, OnInit } from "@angular/core";
|
||||
import { Router } from "@angular/router";
|
||||
import { Subject, firstValueFrom, from } from "rxjs";
|
||||
import { Subject, firstValueFrom, from, Subscription } from "rxjs";
|
||||
import { debounceTime, switchMap, takeUntil } from "rxjs/operators";
|
||||
|
||||
import { UnassignedItemsBannerService } from "@bitwarden/angular/services/unassigned-items-banner.service";
|
||||
@@ -51,12 +51,12 @@ export class CurrentTabComponent implements OnInit, OnDestroy {
|
||||
autofillCalloutText: string;
|
||||
protected search$ = new Subject<void>();
|
||||
private destroy$ = new Subject<void>();
|
||||
private collectPageDetailsSubscription: Subscription;
|
||||
|
||||
private totpCode: string;
|
||||
private totpTimeout: number;
|
||||
private loadedTimeout: number;
|
||||
private searchTimeout: number;
|
||||
private initPageDetailsTimeout: number;
|
||||
|
||||
protected unassignedItemsBannerEnabled$ = this.configService.getFeatureFlag$(
|
||||
FeatureFlag.UnassignedItemsBanner,
|
||||
@@ -100,15 +100,6 @@ export class CurrentTabComponent implements OnInit, OnDestroy {
|
||||
}, 500);
|
||||
}
|
||||
break;
|
||||
case "collectPageDetailsResponse":
|
||||
if (message.sender === BroadcasterSubscriptionId) {
|
||||
this.pageDetails.push({
|
||||
frameId: message.webExtSender.frameId,
|
||||
tab: message.tab,
|
||||
details: message.details,
|
||||
});
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
@@ -266,6 +257,7 @@ export class CurrentTabComponent implements OnInit, OnDestroy {
|
||||
protected async load() {
|
||||
this.isLoading = false;
|
||||
this.tab = await BrowserApi.getTabFromCurrentWindow();
|
||||
|
||||
if (this.tab != null) {
|
||||
this.url = this.tab.url;
|
||||
} else {
|
||||
@@ -274,8 +266,14 @@ export class CurrentTabComponent implements OnInit, OnDestroy {
|
||||
return;
|
||||
}
|
||||
|
||||
this.hostname = Utils.getHostname(this.url);
|
||||
this.pageDetails = [];
|
||||
this.collectPageDetailsSubscription?.unsubscribe();
|
||||
this.collectPageDetailsSubscription = this.autofillService
|
||||
.collectPageDetailsFromTab$(this.tab)
|
||||
.pipe(takeUntil(this.destroy$))
|
||||
.subscribe((pageDetails) => (this.pageDetails = pageDetails));
|
||||
|
||||
this.hostname = Utils.getHostname(this.url);
|
||||
const otherTypes: CipherType[] = [];
|
||||
const dontShowCards = !(await firstValueFrom(this.vaultSettingsService.showCardsCurrentTab$));
|
||||
const dontShowIdentities = !(await firstValueFrom(
|
||||
@@ -323,7 +321,6 @@ export class CurrentTabComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
this.isLoading = this.loaded = true;
|
||||
this.collectTabPageDetails();
|
||||
}
|
||||
|
||||
async goToSettings() {
|
||||
@@ -361,19 +358,4 @@ export class CurrentTabComponent implements OnInit, OnDestroy {
|
||||
this.autofillCalloutText = this.i18nService.t("autofillSelectInfoWithoutCommand");
|
||||
}
|
||||
}
|
||||
|
||||
private collectTabPageDetails() {
|
||||
void BrowserApi.tabSendMessage(this.tab, {
|
||||
command: "collectPageDetails",
|
||||
tab: this.tab,
|
||||
sender: BroadcasterSubscriptionId,
|
||||
});
|
||||
|
||||
window.clearTimeout(this.initPageDetailsTimeout);
|
||||
this.initPageDetailsTimeout = window.setTimeout(() => {
|
||||
if (this.pageDetails.length === 0) {
|
||||
this.collectTabPageDetails();
|
||||
}
|
||||
}, 250);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
<popup-page>
|
||||
<popup-header slot="header" [pageTitle]="'vault' | i18n">
|
||||
<ng-container slot="end">
|
||||
<!-- TODO PM-6826: add selectedVault query param -->
|
||||
<a bitButton buttonType="primary" type="button" routerLink="/add-cipher">
|
||||
<i class="bwi bwi-plus-f" aria-hidden="true"></i>
|
||||
{{ "new" | i18n }}
|
||||
</a>
|
||||
<app-new-item-dropdown></app-new-item-dropdown>
|
||||
|
||||
<app-pop-out></app-pop-out>
|
||||
<app-current-account></app-current-account>
|
||||
</ng-container>
|
||||
@@ -18,9 +15,7 @@
|
||||
<bit-no-items [icon]="vaultIcon">
|
||||
<ng-container slot="title">{{ "yourVaultIsEmpty" | i18n }}</ng-container>
|
||||
<ng-container slot="description">{{ "autofillSuggestionsTip" | i18n }}</ng-container>
|
||||
<button slot="button" type="button" bitButton buttonType="primary" (click)="addCipher()">
|
||||
{{ "new" | i18n }}
|
||||
</button>
|
||||
<app-new-item-dropdown slot="button"></app-new-item-dropdown>
|
||||
</bit-no-items>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import { Router, RouterLink } from "@angular/router";
|
||||
import { combineLatest } from "rxjs";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||
import { ButtonModule, Icons, NoItemsModule } from "@bitwarden/components";
|
||||
|
||||
import { CurrentAccountComponent } from "../../../../auth/popup/account-switching/current-account.component";
|
||||
@@ -13,6 +14,7 @@ import { PopupHeaderComponent } from "../../../../platform/popup/layout/popup-he
|
||||
import { PopupPageComponent } from "../../../../platform/popup/layout/popup-page.component";
|
||||
import { VaultPopupItemsService } from "../../services/vault-popup-items.service";
|
||||
import { AutofillVaultListItemsComponent, VaultListItemsContainerComponent } from "../vault-v2";
|
||||
import { NewItemDropdownV2Component } from "../vault-v2/new-item-dropdown/new-item-dropdown-v2.component";
|
||||
import { VaultListFiltersComponent } from "../vault-v2/vault-list-filters/vault-list-filters.component";
|
||||
import { VaultV2SearchComponent } from "../vault-v2/vault-search/vault-v2-search.component";
|
||||
|
||||
@@ -40,9 +42,11 @@ enum VaultState {
|
||||
ButtonModule,
|
||||
RouterLink,
|
||||
VaultV2SearchComponent,
|
||||
NewItemDropdownV2Component,
|
||||
],
|
||||
})
|
||||
export class VaultV2Component implements OnInit, OnDestroy {
|
||||
cipherType = CipherType;
|
||||
protected favoriteCiphers$ = this.vaultPopupItemsService.favoriteCiphers$;
|
||||
protected remainingCiphers$ = this.vaultPopupItemsService.remainingCiphers$;
|
||||
|
||||
@@ -86,9 +90,4 @@ export class VaultV2Component implements OnInit, OnDestroy {
|
||||
ngOnInit(): void {}
|
||||
|
||||
ngOnDestroy(): void {}
|
||||
|
||||
addCipher() {
|
||||
// TODO: Add currently filtered organization to query params if available
|
||||
void this.router.navigate(["/add-cipher"], {});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { DatePipe, Location } from "@angular/common";
|
||||
import { ChangeDetectorRef, Component, NgZone } from "@angular/core";
|
||||
import { ActivatedRoute, Router } from "@angular/router";
|
||||
import { Subject, firstValueFrom, takeUntil } from "rxjs";
|
||||
import { Subject, firstValueFrom, takeUntil, Subscription } from "rxjs";
|
||||
import { first } from "rxjs/operators";
|
||||
|
||||
import { ViewComponent as BaseViewComponent } from "@bitwarden/angular/vault/components/view.component";
|
||||
@@ -68,6 +68,7 @@ export class ViewComponent extends BaseViewComponent {
|
||||
inPopout = false;
|
||||
cipherType = CipherType;
|
||||
private fido2PopoutSessionData$ = fido2PopoutSessionData$();
|
||||
private collectPageDetailsSubscription: Subscription;
|
||||
|
||||
private destroy$ = new Subject<void>();
|
||||
|
||||
@@ -152,15 +153,6 @@ export class ViewComponent extends BaseViewComponent {
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.ngZone.run(async () => {
|
||||
switch (message.command) {
|
||||
case "collectPageDetailsResponse":
|
||||
if (message.sender === BroadcasterSubscriptionId) {
|
||||
this.pageDetails.push({
|
||||
frameId: message.webExtSender.frameId,
|
||||
tab: message.tab,
|
||||
details: message.details,
|
||||
});
|
||||
}
|
||||
break;
|
||||
case "tabChanged":
|
||||
case "windowChanged":
|
||||
if (this.loadPageDetailsTimeout != null) {
|
||||
@@ -198,7 +190,9 @@ export class ViewComponent extends BaseViewComponent {
|
||||
|
||||
// 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(["/edit-cipher"], { queryParams: { cipherId: this.cipher.id } });
|
||||
this.router.navigate(["/edit-cipher"], {
|
||||
queryParams: { cipherId: this.cipher.id, type: this.cipher.type, isNew: false },
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -335,6 +329,7 @@ export class ViewComponent extends BaseViewComponent {
|
||||
}
|
||||
|
||||
private async loadPageDetails() {
|
||||
this.collectPageDetailsSubscription?.unsubscribe();
|
||||
this.pageDetails = [];
|
||||
this.tab = this.senderTabId
|
||||
? await BrowserApi.getTab(this.senderTabId)
|
||||
@@ -344,13 +339,10 @@ export class ViewComponent extends BaseViewComponent {
|
||||
return;
|
||||
}
|
||||
|
||||
// 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
|
||||
BrowserApi.tabSendMessage(this.tab, {
|
||||
command: "collectPageDetails",
|
||||
tab: this.tab,
|
||||
sender: BroadcasterSubscriptionId,
|
||||
});
|
||||
this.collectPageDetailsSubscription = this.autofillService
|
||||
.collectPageDetailsFromTab$(this.tab)
|
||||
.pipe(takeUntil(this.destroy$))
|
||||
.subscribe((pageDetails) => (this.pageDetails = pageDetails));
|
||||
}
|
||||
|
||||
private async doAutofill() {
|
||||
|
||||
@@ -379,6 +379,54 @@ describe("VaultPopupItemsService", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("loading$", () => {
|
||||
let tracked: ObservableTracker<boolean>;
|
||||
let trackedCiphers: ObservableTracker<any>;
|
||||
beforeEach(() => {
|
||||
// Start tracking loading$ emissions
|
||||
tracked = new ObservableTracker(service.loading$);
|
||||
|
||||
// Track remainingCiphers$ to make cipher observables active
|
||||
trackedCiphers = new ObservableTracker(service.remainingCiphers$);
|
||||
});
|
||||
|
||||
it("should initialize with true first", async () => {
|
||||
expect(tracked.emissions[0]).toBe(true);
|
||||
});
|
||||
|
||||
it("should emit false once ciphers are available", async () => {
|
||||
expect(tracked.emissions.length).toBe(2);
|
||||
expect(tracked.emissions[0]).toBe(true);
|
||||
expect(tracked.emissions[1]).toBe(false);
|
||||
});
|
||||
|
||||
it("should cycle when cipherService.ciphers$ emits", async () => {
|
||||
// Restart tracking
|
||||
tracked = new ObservableTracker(service.loading$);
|
||||
(cipherServiceMock.ciphers$ as BehaviorSubject<any>).next(null);
|
||||
|
||||
await trackedCiphers.pauseUntilReceived(2);
|
||||
|
||||
expect(tracked.emissions.length).toBe(3);
|
||||
expect(tracked.emissions[0]).toBe(false);
|
||||
expect(tracked.emissions[1]).toBe(true);
|
||||
expect(tracked.emissions[2]).toBe(false);
|
||||
});
|
||||
|
||||
it("should cycle when filters are applied", async () => {
|
||||
// Restart tracking
|
||||
tracked = new ObservableTracker(service.loading$);
|
||||
service.applyFilter("test");
|
||||
|
||||
await trackedCiphers.pauseUntilReceived(2);
|
||||
|
||||
expect(tracked.emissions.length).toBe(3);
|
||||
expect(tracked.emissions[0]).toBe(false);
|
||||
expect(tracked.emissions[1]).toBe(true);
|
||||
expect(tracked.emissions[2]).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("applyFilter", () => {
|
||||
it("should call search Service with the new search term", (done) => {
|
||||
const searchText = "Hello";
|
||||
|
||||
@@ -2,6 +2,7 @@ import { inject, Injectable, NgZone } from "@angular/core";
|
||||
import {
|
||||
BehaviorSubject,
|
||||
combineLatest,
|
||||
distinctUntilChanged,
|
||||
distinctUntilKeyChanged,
|
||||
from,
|
||||
map,
|
||||
@@ -12,6 +13,8 @@ import {
|
||||
startWith,
|
||||
Subject,
|
||||
switchMap,
|
||||
tap,
|
||||
withLatestFrom,
|
||||
} from "rxjs";
|
||||
|
||||
import { SearchService } from "@bitwarden/common/abstractions/search.service";
|
||||
@@ -40,6 +43,13 @@ import { MY_VAULT_ID, VaultPopupListFiltersService } from "./vault-popup-list-fi
|
||||
export class VaultPopupItemsService {
|
||||
private _refreshCurrentTab$ = new Subject<void>();
|
||||
private _searchText$ = new BehaviorSubject<string>("");
|
||||
|
||||
/**
|
||||
* Subject that emits whenever new ciphers are being processed/filtered.
|
||||
* @private
|
||||
*/
|
||||
private _ciphersLoading$ = new Subject<void>();
|
||||
|
||||
latestSearchText$: Observable<string> = this._searchText$.asObservable();
|
||||
|
||||
/**
|
||||
@@ -84,6 +94,7 @@ export class VaultPopupItemsService {
|
||||
this.cipherService.localData$,
|
||||
).pipe(
|
||||
runInsideAngular(inject(NgZone)), // Workaround to ensure cipher$ state provider emissions are run inside Angular
|
||||
tap(() => this._ciphersLoading$.next()),
|
||||
switchMap(() => Utils.asyncToObservable(() => this.cipherService.getAllDecrypted())),
|
||||
switchMap((ciphers) =>
|
||||
combineLatest([
|
||||
@@ -112,6 +123,7 @@ export class VaultPopupItemsService {
|
||||
this._searchText$,
|
||||
this.vaultPopupListFiltersService.filterFunction$,
|
||||
]).pipe(
|
||||
tap(() => this._ciphersLoading$.next()),
|
||||
map(([ciphers, searchText, filterFunction]): [CipherView[], string] => [
|
||||
filterFunction(ciphers),
|
||||
searchText,
|
||||
@@ -148,10 +160,8 @@ export class VaultPopupItemsService {
|
||||
* List of favorite ciphers that are not currently suggested for autofill.
|
||||
* Ciphers are sorted by last used date, then by name.
|
||||
*/
|
||||
favoriteCiphers$: Observable<PopupCipherView[]> = combineLatest([
|
||||
this.autoFillCiphers$,
|
||||
this._filteredCipherList$,
|
||||
]).pipe(
|
||||
favoriteCiphers$: Observable<PopupCipherView[]> = this.autoFillCiphers$.pipe(
|
||||
withLatestFrom(this._filteredCipherList$),
|
||||
map(([autoFillCiphers, ciphers]) =>
|
||||
ciphers.filter((cipher) => cipher.favorite && !autoFillCiphers.includes(cipher)),
|
||||
),
|
||||
@@ -165,12 +175,9 @@ export class VaultPopupItemsService {
|
||||
* List of all remaining ciphers that are not currently suggested for autofill or marked as favorite.
|
||||
* Ciphers are sorted by name.
|
||||
*/
|
||||
remainingCiphers$: Observable<PopupCipherView[]> = combineLatest([
|
||||
this.autoFillCiphers$,
|
||||
this.favoriteCiphers$,
|
||||
this._filteredCipherList$,
|
||||
]).pipe(
|
||||
map(([autoFillCiphers, favoriteCiphers, ciphers]) =>
|
||||
remainingCiphers$: Observable<PopupCipherView[]> = this.favoriteCiphers$.pipe(
|
||||
withLatestFrom(this._filteredCipherList$, this.autoFillCiphers$),
|
||||
map(([favoriteCiphers, ciphers, autoFillCiphers]) =>
|
||||
ciphers.filter(
|
||||
(cipher) => !autoFillCiphers.includes(cipher) && !favoriteCiphers.includes(cipher),
|
||||
),
|
||||
@@ -179,6 +186,14 @@ export class VaultPopupItemsService {
|
||||
shareReplay({ refCount: false, bufferSize: 1 }),
|
||||
);
|
||||
|
||||
/**
|
||||
* Observable that indicates whether the service is currently loading ciphers.
|
||||
*/
|
||||
loading$: Observable<boolean> = merge(
|
||||
this._ciphersLoading$.pipe(map(() => true)),
|
||||
this.remainingCiphers$.pipe(map(() => false)),
|
||||
).pipe(startWith(true), distinctUntilChanged(), shareReplay({ refCount: false, bufferSize: 1 }));
|
||||
|
||||
/**
|
||||
* Observable that indicates whether a filter is currently applied to the ciphers.
|
||||
*/
|
||||
|
||||
@@ -3,6 +3,8 @@ import { FormBuilder } from "@angular/forms";
|
||||
import { BehaviorSubject, skipWhile } from "rxjs";
|
||||
|
||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { PolicyType } from "@bitwarden/common/admin-console/enums";
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { ProductType } from "@bitwarden/common/enums";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
@@ -23,6 +25,7 @@ describe("VaultPopupListFiltersService", () => {
|
||||
const folderViews$ = new BehaviorSubject([]);
|
||||
const cipherViews$ = new BehaviorSubject({});
|
||||
const decryptedCollections$ = new BehaviorSubject<CollectionView[]>([]);
|
||||
const policyAppliesToActiveUser$ = new BehaviorSubject<boolean>(false);
|
||||
|
||||
const collectionService = {
|
||||
decryptedCollections$,
|
||||
@@ -45,9 +48,15 @@ describe("VaultPopupListFiltersService", () => {
|
||||
t: (key: string) => key,
|
||||
} as I18nService;
|
||||
|
||||
const policyService = {
|
||||
policyAppliesToActiveUser$: jest.fn(() => policyAppliesToActiveUser$),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
memberOrganizations$.next([]);
|
||||
decryptedCollections$.next([]);
|
||||
policyAppliesToActiveUser$.next(false);
|
||||
policyService.policyAppliesToActiveUser$.mockClear();
|
||||
|
||||
collectionService.getAllNested = () => Promise.resolve([]);
|
||||
TestBed.configureTestingModule({
|
||||
@@ -72,6 +81,10 @@ describe("VaultPopupListFiltersService", () => {
|
||||
provide: CollectionService,
|
||||
useValue: collectionService,
|
||||
},
|
||||
{
|
||||
provide: PolicyService,
|
||||
useValue: policyService,
|
||||
},
|
||||
{ provide: FormBuilder, useClass: FormBuilder },
|
||||
],
|
||||
});
|
||||
@@ -127,6 +140,65 @@ describe("VaultPopupListFiltersService", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("PersonalOwnership policy", () => {
|
||||
it('calls policyAppliesToActiveUser$ with "PersonalOwnership"', () => {
|
||||
expect(policyService.policyAppliesToActiveUser$).toHaveBeenCalledWith(
|
||||
PolicyType.PersonalOwnership,
|
||||
);
|
||||
});
|
||||
|
||||
it("returns an empty array when the policy applies and there is a single organization", (done) => {
|
||||
policyAppliesToActiveUser$.next(true);
|
||||
memberOrganizations$.next([
|
||||
{ name: "bobby's org", id: "1234-3323-23223" },
|
||||
] as Organization[]);
|
||||
|
||||
service.organizations$.subscribe((organizations) => {
|
||||
expect(organizations).toEqual([]);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('adds "myVault" when the policy does not apply and there are multiple organizations', (done) => {
|
||||
policyAppliesToActiveUser$.next(false);
|
||||
const orgs = [
|
||||
{ name: "bobby's org", id: "1234-3323-23223" },
|
||||
{ name: "alice's org", id: "2223-4343-99888" },
|
||||
] as Organization[];
|
||||
|
||||
memberOrganizations$.next(orgs);
|
||||
|
||||
service.organizations$.subscribe((organizations) => {
|
||||
expect(organizations.map((o) => o.label)).toEqual([
|
||||
"myVault",
|
||||
"alice's org",
|
||||
"bobby's org",
|
||||
]);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('does not add "myVault" the policy applies and there are multiple organizations', (done) => {
|
||||
policyAppliesToActiveUser$.next(true);
|
||||
const orgs = [
|
||||
{ name: "bobby's org", id: "1234-3323-23223" },
|
||||
{ name: "alice's org", id: "2223-3242-99888" },
|
||||
{ name: "catherine's org", id: "77733-4343-99888" },
|
||||
] as Organization[];
|
||||
|
||||
memberOrganizations$.next(orgs);
|
||||
|
||||
service.organizations$.subscribe((organizations) => {
|
||||
expect(organizations.map((o) => o.label)).toEqual([
|
||||
"alice's org",
|
||||
"bobby's org",
|
||||
"catherine's org",
|
||||
]);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("icons", () => {
|
||||
it("sets family icon for family organizations", (done) => {
|
||||
const orgs = [
|
||||
|
||||
@@ -13,6 +13,8 @@ import {
|
||||
|
||||
import { DynamicTreeNode } from "@bitwarden/angular/vault/vault-filter/models/dynamic-tree-node.model";
|
||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { PolicyType } from "@bitwarden/common/admin-console/enums";
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { ProductType } from "@bitwarden/common/enums";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
@@ -88,6 +90,7 @@ export class VaultPopupListFiltersService {
|
||||
private i18nService: I18nService,
|
||||
private collectionService: CollectionService,
|
||||
private formBuilder: FormBuilder,
|
||||
private policyService: PolicyService,
|
||||
) {
|
||||
this.filterForm.controls.organization.valueChanges
|
||||
.pipe(takeUntilDestroyed())
|
||||
@@ -167,44 +170,63 @@ export class VaultPopupListFiltersService {
|
||||
/**
|
||||
* Organization array structured to be directly passed to `ChipSelectComponent`
|
||||
*/
|
||||
organizations$: Observable<ChipSelectOption<Organization>[]> =
|
||||
this.organizationService.memberOrganizations$.pipe(
|
||||
map((orgs) => orgs.sort(Utils.getSortFunction(this.i18nService, "name"))),
|
||||
map((orgs) => {
|
||||
if (!orgs.length) {
|
||||
return [];
|
||||
}
|
||||
organizations$: Observable<ChipSelectOption<Organization>[]> = combineLatest([
|
||||
this.organizationService.memberOrganizations$,
|
||||
this.policyService.policyAppliesToActiveUser$(PolicyType.PersonalOwnership),
|
||||
]).pipe(
|
||||
map(([orgs, personalOwnershipApplies]): [Organization[], boolean] => [
|
||||
orgs.sort(Utils.getSortFunction(this.i18nService, "name")),
|
||||
personalOwnershipApplies,
|
||||
]),
|
||||
map(([orgs, personalOwnershipApplies]) => {
|
||||
// When there are no organizations return an empty array,
|
||||
// resulting in the org filter being hidden
|
||||
if (!orgs.length) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [
|
||||
// When the user is a member of an organization, make the "My Vault" option available
|
||||
{
|
||||
value: { id: MY_VAULT_ID } as Organization,
|
||||
label: this.i18nService.t("myVault"),
|
||||
icon: "bwi-user",
|
||||
},
|
||||
...orgs.map((org) => {
|
||||
let icon = "bwi-business";
|
||||
// When there is only one organization and personal ownership policy applies,
|
||||
// return an empty array, resulting in the org filter being hidden
|
||||
if (orgs.length === 1 && personalOwnershipApplies) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (!org.enabled) {
|
||||
// Show a warning icon if the organization is deactivated
|
||||
icon = "bwi-exclamation-triangle tw-text-danger";
|
||||
} else if (
|
||||
org.planProductType === ProductType.Families ||
|
||||
org.planProductType === ProductType.Free
|
||||
) {
|
||||
// Show a family icon if the organization is a family or free org
|
||||
icon = "bwi-family";
|
||||
}
|
||||
const myVaultOrg: ChipSelectOption<Organization>[] = [];
|
||||
|
||||
return {
|
||||
value: org,
|
||||
label: org.name,
|
||||
icon,
|
||||
};
|
||||
}),
|
||||
];
|
||||
}),
|
||||
);
|
||||
// Only add "My vault" if personal ownership policy does not apply
|
||||
if (!personalOwnershipApplies) {
|
||||
myVaultOrg.push({
|
||||
value: { id: MY_VAULT_ID } as Organization,
|
||||
label: this.i18nService.t("myVault"),
|
||||
icon: "bwi-user",
|
||||
});
|
||||
}
|
||||
|
||||
return [
|
||||
...myVaultOrg,
|
||||
...orgs.map((org) => {
|
||||
let icon = "bwi-business";
|
||||
|
||||
if (!org.enabled) {
|
||||
// Show a warning icon if the organization is deactivated
|
||||
icon = "bwi-exclamation-triangle tw-text-danger";
|
||||
} else if (
|
||||
org.planProductType === ProductType.Families ||
|
||||
org.planProductType === ProductType.Free
|
||||
) {
|
||||
// Show a family icon if the organization is a family or free org
|
||||
icon = "bwi-family";
|
||||
}
|
||||
|
||||
return {
|
||||
value: org,
|
||||
label: org.name,
|
||||
icon,
|
||||
};
|
||||
}),
|
||||
];
|
||||
}),
|
||||
);
|
||||
|
||||
/**
|
||||
* Folder array structured to be directly passed to `ChipSelectComponent`
|
||||
|
||||
@@ -80,7 +80,7 @@
|
||||
"papaparse": "5.4.1",
|
||||
"proper-lockfile": "4.1.2",
|
||||
"rxjs": "7.8.1",
|
||||
"tldts": "6.1.22",
|
||||
"tldts": "6.1.25",
|
||||
"zxcvbn": "4.4.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -753,6 +753,7 @@ export class ServiceContainer {
|
||||
|
||||
await this.stateService.clean();
|
||||
await this.accountService.clean(userId);
|
||||
await this.accountService.switchAccount(null);
|
||||
process.env.BW_SESSION = null;
|
||||
}
|
||||
|
||||
|
||||
@@ -80,7 +80,6 @@ export class InternalGroupService extends GroupService {
|
||||
async save(group: GroupView): Promise<GroupView> {
|
||||
const request = new GroupRequest();
|
||||
request.name = group.name;
|
||||
request.accessAll = group.accessAll;
|
||||
request.users = group.members;
|
||||
request.collections = group.collections.map(
|
||||
(c) => new SelectionReadOnlyRequest(c.id, c.readOnly, c.hidePasswords, c.manage),
|
||||
|
||||
@@ -2,7 +2,6 @@ import { SelectionReadOnlyRequest } from "@bitwarden/common/admin-console/models
|
||||
|
||||
export class GroupRequest {
|
||||
name: string;
|
||||
accessAll: boolean;
|
||||
collections: SelectionReadOnlyRequest[] = [];
|
||||
users: string[] = [];
|
||||
}
|
||||
|
||||
@@ -5,11 +5,6 @@ export class GroupResponse extends BaseResponse {
|
||||
id: string;
|
||||
organizationId: string;
|
||||
name: string;
|
||||
/**
|
||||
* @deprecated
|
||||
* To be removed after Flexible Collections.
|
||||
**/
|
||||
accessAll: boolean;
|
||||
externalId: string;
|
||||
|
||||
constructor(response: any) {
|
||||
@@ -17,7 +12,6 @@ export class GroupResponse extends BaseResponse {
|
||||
this.id = this.getResponseProperty("Id");
|
||||
this.organizationId = this.getResponseProperty("OrganizationId");
|
||||
this.name = this.getResponseProperty("Name");
|
||||
this.accessAll = this.getResponseProperty("AccessAll");
|
||||
this.externalId = this.getResponseProperty("ExternalId");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,7 +41,6 @@ export class UserAdminService {
|
||||
|
||||
async save(user: OrganizationUserAdminView): Promise<void> {
|
||||
const request = new OrganizationUserUpdateRequest();
|
||||
request.accessAll = user.accessAll;
|
||||
request.permissions = user.permissions;
|
||||
request.type = user.type;
|
||||
request.collections = user.collections;
|
||||
@@ -54,7 +53,6 @@ export class UserAdminService {
|
||||
async invite(emails: string[], user: OrganizationUserAdminView): Promise<void> {
|
||||
const request = new OrganizationUserInviteRequest();
|
||||
request.emails = emails;
|
||||
request.accessAll = user.accessAll;
|
||||
request.permissions = user.permissions;
|
||||
request.type = user.type;
|
||||
request.collections = user.collections;
|
||||
@@ -77,7 +75,6 @@ export class UserAdminService {
|
||||
view.type = u.type;
|
||||
view.status = u.status;
|
||||
view.externalId = u.externalId;
|
||||
view.accessAll = u.accessAll;
|
||||
view.permissions = u.permissions;
|
||||
view.resetPasswordEnrolled = u.resetPasswordEnrolled;
|
||||
view.collections = u.collections.map((c) => ({
|
||||
|
||||
@@ -8,12 +8,6 @@ export class GroupView implements View {
|
||||
id: string;
|
||||
organizationId: string;
|
||||
name: string;
|
||||
/**
|
||||
* @deprecated
|
||||
* To be removed after Flexible Collections.
|
||||
* This will always return `false` if Flexible Collections is enabled.
|
||||
**/
|
||||
accessAll: boolean;
|
||||
externalId: string;
|
||||
collections: CollectionAccessSelectionView[] = [];
|
||||
members: string[] = [];
|
||||
|
||||
@@ -13,12 +13,6 @@ export class OrganizationUserAdminView {
|
||||
type: OrganizationUserType;
|
||||
status: OrganizationUserStatusType;
|
||||
externalId: string;
|
||||
/**
|
||||
* @deprecated
|
||||
* To be removed after Flexible Collections.
|
||||
* This will always return `false` if Flexible Collections is enabled.
|
||||
**/
|
||||
accessAll: boolean;
|
||||
permissions: PermissionsApi;
|
||||
resetPasswordEnrolled: boolean;
|
||||
hasMasterPassword: boolean;
|
||||
|
||||
@@ -12,12 +12,6 @@ export class OrganizationUserView {
|
||||
userId: string;
|
||||
type: OrganizationUserType;
|
||||
status: OrganizationUserStatusType;
|
||||
/**
|
||||
* @deprecated
|
||||
* To be removed after Flexible Collections.
|
||||
* This will always return `false` if Flexible Collections is enabled.
|
||||
**/
|
||||
accessAll: boolean;
|
||||
permissions: PermissionsApi;
|
||||
resetPasswordEnrolled: boolean;
|
||||
name: string;
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
|
||||
<bit-nav-item
|
||||
icon="bwi-collection"
|
||||
[text]="(organization.flexibleCollections ? 'collections' : 'vault') | i18n"
|
||||
[text]="'collections' | i18n"
|
||||
route="vault"
|
||||
*ngIf="canShowVaultTab(organization)"
|
||||
>
|
||||
|
||||
@@ -45,7 +45,6 @@
|
||||
[columnHeader]="'member' | i18n"
|
||||
[selectorLabelText]="'selectMembers' | i18n"
|
||||
[emptySelectionText]="'noMembersAdded' | i18n"
|
||||
[flexibleCollectionsEnabled]="flexibleCollectionsEnabled$ | async"
|
||||
></bit-access-selector>
|
||||
</bit-tab>
|
||||
|
||||
@@ -56,24 +55,14 @@
|
||||
{{ "restrictedCollectionAssignmentDesc" | i18n }}
|
||||
</span>
|
||||
</p>
|
||||
<div *ngIf="!(flexibleCollectionsEnabled$ | async)" class="tw-my-3">
|
||||
<input type="checkbox" formControlName="accessAll" id="accessAll" />
|
||||
<label class="tw-mb-0 tw-text-lg" for="accessAll">{{
|
||||
"accessAllCollectionsDesc" | i18n
|
||||
}}</label>
|
||||
<p class="tw-my-0 tw-text-muted">{{ "accessAllCollectionsHelp" | i18n }}</p>
|
||||
</div>
|
||||
<ng-container *ngIf="!groupForm.value.accessAll">
|
||||
<bit-access-selector
|
||||
formControlName="collections"
|
||||
[items]="collections"
|
||||
[permissionMode]="PermissionMode.Edit"
|
||||
[columnHeader]="'collection' | i18n"
|
||||
[selectorLabelText]="'selectCollections' | i18n"
|
||||
[emptySelectionText]="'noCollectionsAdded' | i18n"
|
||||
[flexibleCollectionsEnabled]="flexibleCollectionsEnabled$ | async"
|
||||
></bit-access-selector>
|
||||
</ng-container>
|
||||
<bit-access-selector
|
||||
formControlName="collections"
|
||||
[items]="collections"
|
||||
[permissionMode]="PermissionMode.Edit"
|
||||
[columnHeader]="'collection' | i18n"
|
||||
[selectorLabelText]="'selectCollections' | i18n"
|
||||
[emptySelectionText]="'noCollectionsAdded' | i18n"
|
||||
></bit-access-selector>
|
||||
</bit-tab>
|
||||
</bit-tab-group>
|
||||
</div>
|
||||
|
||||
@@ -96,9 +96,6 @@ export class GroupAddEditComponent implements OnInit, OnDestroy {
|
||||
private organization$ = this.organizationService
|
||||
.get$(this.organizationId)
|
||||
.pipe(shareReplay({ refCount: true }));
|
||||
protected flexibleCollectionsEnabled$ = this.organization$.pipe(
|
||||
map((o) => o?.flexibleCollections),
|
||||
);
|
||||
private flexibleCollectionsV1Enabled$ = this.configService.getFeatureFlag$(
|
||||
FeatureFlag.FlexibleCollectionsV1,
|
||||
);
|
||||
@@ -114,7 +111,6 @@ export class GroupAddEditComponent implements OnInit, OnDestroy {
|
||||
group: GroupView;
|
||||
|
||||
groupForm = this.formBuilder.group({
|
||||
accessAll: [false],
|
||||
name: ["", [Validators.required, Validators.maxLength(100)]],
|
||||
externalId: this.formBuilder.control({ value: "", disabled: true }),
|
||||
members: [[] as AccessItemValue[]],
|
||||
@@ -188,7 +184,7 @@ export class GroupAddEditComponent implements OnInit, OnDestroy {
|
||||
this.flexibleCollectionsV1Enabled$,
|
||||
]).pipe(
|
||||
map(([organization, flexibleCollectionsV1Enabled]) => {
|
||||
if (!flexibleCollectionsV1Enabled || !organization.flexibleCollections) {
|
||||
if (!flexibleCollectionsV1Enabled) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -276,7 +272,6 @@ export class GroupAddEditComponent implements OnInit, OnDestroy {
|
||||
this.groupForm.patchValue({
|
||||
name: this.group.name,
|
||||
externalId: this.group.externalId,
|
||||
accessAll: this.group.accessAll,
|
||||
members: this.group.members.map((m) => ({
|
||||
id: m,
|
||||
type: AccessItemType.Member,
|
||||
@@ -328,12 +323,8 @@ export class GroupAddEditComponent implements OnInit, OnDestroy {
|
||||
|
||||
const formValue = this.groupForm.value;
|
||||
groupView.name = formValue.name;
|
||||
groupView.accessAll = formValue.accessAll;
|
||||
groupView.members = formValue.members?.map((m) => m.id) ?? [];
|
||||
|
||||
if (!groupView.accessAll) {
|
||||
groupView.collections = formValue.collections.map((c) => convertToSelectionView(c));
|
||||
}
|
||||
groupView.collections = formValue.collections.map((c) => convertToSelectionView(c));
|
||||
|
||||
await this.groupService.save(groupView);
|
||||
|
||||
|
||||
@@ -74,12 +74,10 @@
|
||||
</td>
|
||||
<td bitCell (click)="edit(g, ModalTabType.Collections)" class="tw-cursor-pointer">
|
||||
<bit-badge-list
|
||||
*ngIf="!g.details.accessAll"
|
||||
[items]="g.collectionNames"
|
||||
[maxItems]="3"
|
||||
variant="secondary"
|
||||
></bit-badge-list>
|
||||
<span *ngIf="g.details.accessAll">{{ "all" | i18n }}</span>
|
||||
</td>
|
||||
<td bitCell>
|
||||
<button
|
||||
|
||||
@@ -49,14 +49,6 @@
|
||||
<bit-label>{{ "user" | i18n }}</bit-label>
|
||||
<bit-hint>{{ "userDesc" | i18n }}</bit-hint>
|
||||
</bit-radio-button>
|
||||
<bit-radio-button
|
||||
*ngIf="!organization.flexibleCollections"
|
||||
id="userTypeManager"
|
||||
[value]="organizationUserType.Manager"
|
||||
>
|
||||
<bit-label>{{ "manager" | i18n }}</bit-label>
|
||||
<bit-hint>{{ "managerDesc" | i18n }}</bit-hint>
|
||||
</bit-radio-button>
|
||||
<bit-radio-button id="userTypeAdmin" [value]="organizationUserType.Admin">
|
||||
<bit-label>{{ "admin" | i18n }}</bit-label>
|
||||
<bit-hint>{{ "adminDesc" | i18n }}</bit-hint>
|
||||
@@ -91,140 +83,64 @@
|
||||
</bit-radio-button>
|
||||
</bit-radio-group>
|
||||
<ng-container *ngIf="customUserTypeSelected">
|
||||
<ng-container *ngIf="!organization.flexibleCollections; else customPermissionsFC">
|
||||
<h3 bitTypography="h3">
|
||||
{{ "permissions" | i18n }}
|
||||
</h3>
|
||||
<div class="tw-grid tw-grid-cols-12 tw-gap-4" [formGroup]="permissionsGroup">
|
||||
<div class="tw-col-span-6">
|
||||
<div class="tw-mb-3">
|
||||
<bit-label class="tw-font-semibold">{{
|
||||
"managerPermissions" | i18n
|
||||
}}</bit-label>
|
||||
<hr class="tw-mb-2 tw-mr-2 tw-mt-0" />
|
||||
<app-nested-checkbox
|
||||
parentId="manageAssignedCollections"
|
||||
[checkboxes]="permissionsGroup.controls.manageAssignedCollectionsGroup"
|
||||
>
|
||||
</app-nested-checkbox>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tw-col-span-6">
|
||||
<div class="tw-mb-3">
|
||||
<bit-label class="tw-font-semibold">{{ "adminPermissions" | i18n }}</bit-label>
|
||||
<hr class="tw-mb-2 tw-mr-2 tw-mt-0" />
|
||||
<bit-form-control>
|
||||
<input type="checkbox" bitCheckbox formControlName="accessEventLogs" />
|
||||
<bit-label>{{ "accessEventLogs" | i18n }}</bit-label>
|
||||
</bit-form-control>
|
||||
<bit-form-control>
|
||||
<input type="checkbox" bitCheckbox formControlName="accessImportExport" />
|
||||
<bit-label>{{ "accessImportExport" | i18n }}</bit-label>
|
||||
</bit-form-control>
|
||||
<bit-form-control>
|
||||
<input type="checkbox" bitCheckbox formControlName="accessReports" />
|
||||
<bit-label>{{ "accessReports" | i18n }}</bit-label>
|
||||
</bit-form-control>
|
||||
<app-nested-checkbox
|
||||
parentId="manageAllCollections"
|
||||
[checkboxes]="permissionsGroup.controls.manageAllCollectionsGroup"
|
||||
>
|
||||
</app-nested-checkbox>
|
||||
<bit-form-control>
|
||||
<input type="checkbox" bitCheckbox formControlName="manageGroups" />
|
||||
<bit-label>{{ "manageGroups" | i18n }}</bit-label>
|
||||
</bit-form-control>
|
||||
<bit-form-control>
|
||||
<input type="checkbox" bitCheckbox formControlName="manageSso" />
|
||||
<bit-label>{{ "manageSso" | i18n }}</bit-label>
|
||||
</bit-form-control>
|
||||
<bit-form-control>
|
||||
<input type="checkbox" bitCheckbox formControlName="managePolicies" />
|
||||
<bit-label>{{ "managePolicies" | i18n }}</bit-label>
|
||||
</bit-form-control>
|
||||
<bit-form-control>
|
||||
<input
|
||||
id="manageUsers"
|
||||
type="checkbox"
|
||||
bitCheckbox
|
||||
formControlName="manageUsers"
|
||||
(change)="handleDependentPermissions()"
|
||||
/>
|
||||
<bit-label>{{ "manageUsers" | i18n }}</bit-label>
|
||||
</bit-form-control>
|
||||
<bit-form-control>
|
||||
<input
|
||||
type="checkbox"
|
||||
bitCheckbox
|
||||
formControlName="manageResetPassword"
|
||||
(change)="handleDependentPermissions()"
|
||||
/>
|
||||
<bit-label>{{ "manageAccountRecovery" | i18n }}</bit-label>
|
||||
</bit-form-control>
|
||||
</div>
|
||||
<div class="tw-grid tw-grid-cols-12 tw-gap-4" [formGroup]="permissionsGroup">
|
||||
<div class="tw-col-span-4">
|
||||
<bit-form-control>
|
||||
<input type="checkbox" bitCheckbox formControlName="accessEventLogs" />
|
||||
<bit-label>{{ "accessEventLogs" | i18n }}</bit-label>
|
||||
</bit-form-control>
|
||||
<bit-form-control>
|
||||
<input type="checkbox" bitCheckbox formControlName="accessImportExport" />
|
||||
<bit-label>{{ "accessImportExport" | i18n }}</bit-label>
|
||||
</bit-form-control>
|
||||
<bit-form-control>
|
||||
<input type="checkbox" bitCheckbox formControlName="accessReports" />
|
||||
<bit-label>{{ "accessReports" | i18n }}</bit-label>
|
||||
</bit-form-control>
|
||||
</div>
|
||||
<div class="tw-col-span-4">
|
||||
<app-nested-checkbox
|
||||
parentId="manageAllCollections"
|
||||
[checkboxes]="permissionsGroup.controls.manageAllCollectionsGroup"
|
||||
>
|
||||
</app-nested-checkbox>
|
||||
</div>
|
||||
<div class="tw-col-span-4">
|
||||
<div class="tw-mb-3">
|
||||
<bit-form-control>
|
||||
<input type="checkbox" bitCheckbox formControlName="manageGroups" />
|
||||
<bit-label>{{ "manageGroups" | i18n }}</bit-label>
|
||||
</bit-form-control>
|
||||
<bit-form-control>
|
||||
<input type="checkbox" bitCheckbox formControlName="manageSso" />
|
||||
<bit-label>{{ "manageSso" | i18n }}</bit-label>
|
||||
</bit-form-control>
|
||||
<bit-form-control>
|
||||
<input type="checkbox" bitCheckbox formControlName="managePolicies" />
|
||||
<bit-label>{{ "managePolicies" | i18n }}</bit-label>
|
||||
</bit-form-control>
|
||||
<bit-form-control>
|
||||
<input
|
||||
id="manageUsers"
|
||||
type="checkbox"
|
||||
bitCheckbox
|
||||
formControlName="manageUsers"
|
||||
(change)="handleDependentPermissions()"
|
||||
/>
|
||||
<bit-label>{{ "manageUsers" | i18n }}</bit-label>
|
||||
</bit-form-control>
|
||||
<bit-form-control>
|
||||
<input
|
||||
type="checkbox"
|
||||
bitCheckbox
|
||||
formControlName="manageResetPassword"
|
||||
(change)="handleDependentPermissions()"
|
||||
/>
|
||||
<bit-label>{{ "manageAccountRecovery" | i18n }}</bit-label>
|
||||
</bit-form-control>
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
<ng-template #customPermissionsFC>
|
||||
<div class="tw-grid tw-grid-cols-12 tw-gap-4" [formGroup]="permissionsGroup">
|
||||
<div class="tw-col-span-4">
|
||||
<bit-form-control>
|
||||
<input type="checkbox" bitCheckbox formControlName="accessEventLogs" />
|
||||
<bit-label>{{ "accessEventLogs" | i18n }}</bit-label>
|
||||
</bit-form-control>
|
||||
<bit-form-control>
|
||||
<input type="checkbox" bitCheckbox formControlName="accessImportExport" />
|
||||
<bit-label>{{ "accessImportExport" | i18n }}</bit-label>
|
||||
</bit-form-control>
|
||||
<bit-form-control>
|
||||
<input type="checkbox" bitCheckbox formControlName="accessReports" />
|
||||
<bit-label>{{ "accessReports" | i18n }}</bit-label>
|
||||
</bit-form-control>
|
||||
</div>
|
||||
<div class="tw-col-span-4">
|
||||
<app-nested-checkbox
|
||||
parentId="manageAllCollections"
|
||||
[checkboxes]="permissionsGroup.controls.manageAllCollectionsGroup"
|
||||
>
|
||||
</app-nested-checkbox>
|
||||
</div>
|
||||
<div class="tw-col-span-4">
|
||||
<div class="tw-mb-3">
|
||||
<bit-form-control>
|
||||
<input type="checkbox" bitCheckbox formControlName="manageGroups" />
|
||||
<bit-label>{{ "manageGroups" | i18n }}</bit-label>
|
||||
</bit-form-control>
|
||||
<bit-form-control>
|
||||
<input type="checkbox" bitCheckbox formControlName="manageSso" />
|
||||
<bit-label>{{ "manageSso" | i18n }}</bit-label>
|
||||
</bit-form-control>
|
||||
<bit-form-control>
|
||||
<input type="checkbox" bitCheckbox formControlName="managePolicies" />
|
||||
<bit-label>{{ "managePolicies" | i18n }}</bit-label>
|
||||
</bit-form-control>
|
||||
<bit-form-control>
|
||||
<input
|
||||
id="manageUsers"
|
||||
type="checkbox"
|
||||
bitCheckbox
|
||||
formControlName="manageUsers"
|
||||
(change)="handleDependentPermissions()"
|
||||
/>
|
||||
<bit-label>{{ "manageUsers" | i18n }}</bit-label>
|
||||
</bit-form-control>
|
||||
<bit-form-control>
|
||||
<input
|
||||
type="checkbox"
|
||||
bitCheckbox
|
||||
formControlName="manageResetPassword"
|
||||
(change)="handleDependentPermissions()"
|
||||
/>
|
||||
<bit-label>{{ "manageAccountRecovery" | i18n }}</bit-label>
|
||||
</bit-form-control>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ng-template>
|
||||
</div>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="organization.useSecretsManager">
|
||||
<h3 class="tw-mt-4">
|
||||
@@ -272,7 +188,6 @@
|
||||
[columnHeader]="'groups' | i18n"
|
||||
[selectorLabelText]="'selectGroups' | i18n"
|
||||
[emptySelectionText]="'noGroupsAdded' | i18n"
|
||||
[flexibleCollectionsEnabled]="organization.flexibleCollections"
|
||||
[hideMultiSelect]="restrictEditingSelf$ | async"
|
||||
></bit-access-selector>
|
||||
</bit-tab>
|
||||
@@ -294,26 +209,7 @@
|
||||
{{ "restrictedCollectionAssignmentDesc" | i18n }}
|
||||
</span>
|
||||
</div>
|
||||
<div *ngIf="!organization.flexibleCollections" class="tw-mb-6">
|
||||
<bit-form-control>
|
||||
<input type="checkbox" bitCheckbox formControlName="accessAllCollections" />
|
||||
<bit-label>
|
||||
{{ "accessAllCollectionsDesc" | i18n }}
|
||||
<a
|
||||
bitLink
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
appA11yTitle="{{ 'learnMore' | i18n }}"
|
||||
href="https://bitwarden.com/help/user-types-access-control/#access-control"
|
||||
>
|
||||
<i class="bwi bwi-question-circle" aria-hidden="true"></i>
|
||||
</a>
|
||||
</bit-label>
|
||||
<bit-hint>{{ "accessAllCollectionsHelp" | i18n }}</bit-hint>
|
||||
</bit-form-control>
|
||||
</div>
|
||||
<bit-access-selector
|
||||
*ngIf="!accessAllCollections"
|
||||
[permissionMode]="PermissionMode.Edit"
|
||||
formControlName="access"
|
||||
[showGroupColumn]="organization.useGroups"
|
||||
@@ -321,7 +217,6 @@
|
||||
[columnHeader]="'collection' | i18n"
|
||||
[selectorLabelText]="'selectCollections' | i18n"
|
||||
[emptySelectionText]="'noCollectionsAdded' | i18n"
|
||||
[flexibleCollectionsEnabled]="organization.flexibleCollections"
|
||||
[hideMultiSelect]="restrictEditingSelf$ | async"
|
||||
></bit-access-selector
|
||||
></bit-tab>
|
||||
|
||||
@@ -99,7 +99,6 @@ export class MemberDialogComponent implements OnDestroy {
|
||||
emails: [""],
|
||||
type: OrganizationUserType.User,
|
||||
externalId: this.formBuilder.control({ value: "", disabled: true }),
|
||||
accessAllCollections: false,
|
||||
accessSecretsManager: false,
|
||||
access: [[] as AccessItemValue[]],
|
||||
groups: [[] as AccessItemValue[]],
|
||||
@@ -110,11 +109,6 @@ export class MemberDialogComponent implements OnDestroy {
|
||||
protected canAssignAccessToAnyCollection$: Observable<boolean>;
|
||||
|
||||
protected permissionsGroup = this.formBuilder.group({
|
||||
manageAssignedCollectionsGroup: this.formBuilder.group<Record<string, boolean>>({
|
||||
manageAssignedCollections: false,
|
||||
editAssignedCollections: false,
|
||||
deleteAssignedCollections: false,
|
||||
}),
|
||||
manageAllCollectionsGroup: this.formBuilder.group<Record<string, boolean>>({
|
||||
manageAllCollections: false,
|
||||
createNewCollections: false,
|
||||
@@ -137,10 +131,6 @@ export class MemberDialogComponent implements OnDestroy {
|
||||
return this.formGroup.value.type === OrganizationUserType.Custom;
|
||||
}
|
||||
|
||||
get accessAllCollections(): boolean {
|
||||
return this.formGroup.value.accessAllCollections;
|
||||
}
|
||||
|
||||
constructor(
|
||||
@Inject(DIALOG_DATA) protected params: MemberDialogParams,
|
||||
private dialogRef: DialogRef<MemberDialogResult>,
|
||||
@@ -189,7 +179,7 @@ export class MemberDialogComponent implements OnDestroy {
|
||||
this.configService.getFeatureFlag$(FeatureFlag.FlexibleCollectionsV1),
|
||||
]).pipe(
|
||||
map(([organization, flexibleCollectionsV1Enabled]) => {
|
||||
if (!flexibleCollectionsV1Enabled || !organization.flexibleCollections) {
|
||||
if (!flexibleCollectionsV1Enabled) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -316,13 +306,6 @@ export class MemberDialogComponent implements OnDestroy {
|
||||
this.showNoMasterPasswordWarning =
|
||||
userDetails.status > OrganizationUserStatusType.Invited &&
|
||||
userDetails.hasMasterPassword === false;
|
||||
const assignedCollectionsPermissions = {
|
||||
editAssignedCollections: userDetails.permissions.editAssignedCollections,
|
||||
deleteAssignedCollections: userDetails.permissions.deleteAssignedCollections,
|
||||
manageAssignedCollections:
|
||||
userDetails.permissions.editAssignedCollections &&
|
||||
userDetails.permissions.deleteAssignedCollections,
|
||||
};
|
||||
const allCollectionsPermissions = {
|
||||
createNewCollections: userDetails.permissions.createNewCollections,
|
||||
editAnyCollection: userDetails.permissions.editAnyCollection,
|
||||
@@ -342,7 +325,6 @@ export class MemberDialogComponent implements OnDestroy {
|
||||
managePolicies: userDetails.permissions.managePolicies,
|
||||
manageUsers: userDetails.permissions.manageUsers,
|
||||
manageResetPassword: userDetails.permissions.manageResetPassword,
|
||||
manageAssignedCollectionsGroup: assignedCollectionsPermissions,
|
||||
manageAllCollectionsGroup: allCollectionsPermissions,
|
||||
});
|
||||
}
|
||||
@@ -378,7 +360,6 @@ export class MemberDialogComponent implements OnDestroy {
|
||||
this.formGroup.patchValue({
|
||||
type: userDetails.type,
|
||||
externalId: userDetails.externalId,
|
||||
accessAllCollections: userDetails.accessAll,
|
||||
access: accessSelections,
|
||||
accessSecretsManager: userDetails.accessSecretsManager,
|
||||
groups: groupAccessSelections,
|
||||
@@ -414,10 +395,6 @@ export class MemberDialogComponent implements OnDestroy {
|
||||
editAnyCollection: this.permissionsGroup.value.manageAllCollectionsGroup.editAnyCollection,
|
||||
deleteAnyCollection:
|
||||
this.permissionsGroup.value.manageAllCollectionsGroup.deleteAnyCollection,
|
||||
editAssignedCollections:
|
||||
this.permissionsGroup.value.manageAssignedCollectionsGroup.editAssignedCollections,
|
||||
deleteAssignedCollections:
|
||||
this.permissionsGroup.value.manageAssignedCollectionsGroup.deleteAssignedCollections,
|
||||
};
|
||||
|
||||
return Object.assign(p, partialPermissions);
|
||||
@@ -467,7 +444,6 @@ export class MemberDialogComponent implements OnDestroy {
|
||||
const userView = new OrganizationUserAdminView();
|
||||
userView.id = this.params.organizationUserId;
|
||||
userView.organizationId = this.params.organizationId;
|
||||
userView.accessAll = this.accessAllCollections;
|
||||
userView.type = this.formGroup.value.type;
|
||||
userView.permissions = this.setRequestPermissions(
|
||||
userView.permissions ?? new PermissionsApi(),
|
||||
|
||||
@@ -190,12 +190,10 @@
|
||||
class="tw-cursor-pointer"
|
||||
>
|
||||
<bit-badge-list
|
||||
*ngIf="organization.useGroups || !u.accessAll"
|
||||
[items]="organization.useGroups ? u.groupNames : u.collectionNames"
|
||||
[maxItems]="3"
|
||||
variant="secondary"
|
||||
></bit-badge-list>
|
||||
<span *ngIf="!organization.useGroups && u.accessAll">{{ "all" | i18n }}</span>
|
||||
</td>
|
||||
|
||||
<td
|
||||
|
||||
@@ -51,7 +51,7 @@
|
||||
</button>
|
||||
</ng-container>
|
||||
<form
|
||||
*ngIf="org && !loading && org.flexibleCollections"
|
||||
*ngIf="org && !loading"
|
||||
[bitSubmit]="submitCollectionManagement"
|
||||
[formGroup]="collectionManagementFormGroup"
|
||||
>
|
||||
|
||||
@@ -110,15 +110,6 @@
|
||||
</ng-container>
|
||||
|
||||
<ng-template #readOnlyPerm>
|
||||
<div
|
||||
*ngIf="item.accessAllItems"
|
||||
class="tw-max-w-40 tw-overflow-hidden tw-overflow-ellipsis tw-whitespace-nowrap tw-border tw-border-solid tw-border-transparent tw-font-bold tw-text-muted"
|
||||
[appA11yTitle]="accessAllLabelId(item) | i18n"
|
||||
>
|
||||
{{ "canEdit" | i18n }}
|
||||
<i class="bwi bwi-filter tw-ml-1" aria-hidden="true"></i>
|
||||
</div>
|
||||
|
||||
<div
|
||||
*ngIf="item.readonly || disabled"
|
||||
class="tw-max-w-40 tw-overflow-hidden tw-overflow-ellipsis tw-whitespace-nowrap tw-font-bold tw-text-muted"
|
||||
|
||||
@@ -75,7 +75,7 @@ export class AccessSelectorComponent implements ControlValueAccessor, OnInit, On
|
||||
|
||||
// The enable() above also enables the permission control, so we need to disable it again
|
||||
// Disable permission control if accessAllItems is enabled or not in Edit mode
|
||||
if (item.accessAllItems || this.permissionMode != PermissionMode.Edit) {
|
||||
if (this.permissionMode != PermissionMode.Edit) {
|
||||
controlRow.controls.permission.disable();
|
||||
}
|
||||
}
|
||||
@@ -196,21 +196,11 @@ export class AccessSelectorComponent implements ControlValueAccessor, OnInit, On
|
||||
*/
|
||||
@Input() showGroupColumn: boolean;
|
||||
|
||||
/**
|
||||
* Enable Flexible Collections changes (feature flag)
|
||||
*/
|
||||
@Input() set flexibleCollectionsEnabled(value: boolean) {
|
||||
this._flexibleCollectionsEnabled = value;
|
||||
this.permissionList = getPermissionList(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Hide the multi-select so that new items cannot be added
|
||||
*/
|
||||
@Input() hideMultiSelect = false;
|
||||
|
||||
private _flexibleCollectionsEnabled: boolean;
|
||||
|
||||
constructor(
|
||||
private readonly formBuilder: FormBuilder,
|
||||
private readonly i18nService: I18nService,
|
||||
@@ -275,7 +265,7 @@ export class AccessSelectorComponent implements ControlValueAccessor, OnInit, On
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
this.permissionList = getPermissionList(this._flexibleCollectionsEnabled);
|
||||
this.permissionList = getPermissionList();
|
||||
// Watch the internal formArray for changes and propagate them
|
||||
this.selectionList.formArray.valueChanges.pipe(takeUntil(this.destroy$)).subscribe((v) => {
|
||||
if (!this.notifyOnChange || this.pauseChangeNotification) {
|
||||
@@ -328,12 +318,8 @@ export class AccessSelectorComponent implements ControlValueAccessor, OnInit, On
|
||||
return this.permissionList.find((p) => p.perm == perm)?.labelId;
|
||||
}
|
||||
|
||||
protected accessAllLabelId(item: AccessItemView) {
|
||||
return item.type == AccessItemType.Group ? "groupAccessAll" : "memberAccessAll";
|
||||
}
|
||||
|
||||
protected canEditItemPermission(item: AccessItemView) {
|
||||
return this.permissionMode == PermissionMode.Edit && !item.readonly && !item.accessAllItems;
|
||||
return this.permissionMode == PermissionMode.Edit && !item.readonly;
|
||||
}
|
||||
|
||||
private _itemComparator(a: AccessItemView, b: AccessItemView) {
|
||||
|
||||
@@ -34,12 +34,6 @@ export enum AccessItemType {
|
||||
*
|
||||
*/
|
||||
export type AccessItemView = SelectItemView & {
|
||||
/**
|
||||
* Flag that this group/member can access all items.
|
||||
* This will disable the permission editor for this item.
|
||||
*/
|
||||
accessAllItems?: boolean;
|
||||
|
||||
/**
|
||||
* Flag that this item cannot be modified.
|
||||
* This will disable the permission editor and will keep
|
||||
@@ -82,16 +76,14 @@ export type Permission = {
|
||||
labelId: string;
|
||||
};
|
||||
|
||||
export const getPermissionList = (flexibleCollectionsEnabled: boolean): Permission[] => {
|
||||
export const getPermissionList = (): Permission[] => {
|
||||
const permissions = [
|
||||
{ perm: CollectionPermission.View, labelId: "canView" },
|
||||
{ perm: CollectionPermission.ViewExceptPass, labelId: "canViewExceptPass" },
|
||||
{ perm: CollectionPermission.Edit, labelId: "canEdit" },
|
||||
{ perm: CollectionPermission.EditExceptPass, labelId: "canEditExceptPass" },
|
||||
{ perm: CollectionPermission.Manage, labelId: "canManage" },
|
||||
];
|
||||
if (flexibleCollectionsEnabled) {
|
||||
permissions.push({ perm: CollectionPermission.Manage, labelId: "canManage" });
|
||||
}
|
||||
|
||||
return permissions;
|
||||
};
|
||||
@@ -142,8 +134,6 @@ export function mapGroupToAccessItemView(group: GroupView): AccessItemView {
|
||||
type: AccessItemType.Group,
|
||||
listName: group.name,
|
||||
labelName: group.name,
|
||||
accessAllItems: group.accessAll,
|
||||
readonly: group.accessAll,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -157,7 +147,5 @@ export function mapUserToAccessItemView(user: OrganizationUserUserDetailsRespons
|
||||
listName: user.name?.length > 0 ? `${user.name} (${user.email})` : user.email,
|
||||
labelName: user.name ?? user.email,
|
||||
status: user.status,
|
||||
accessAllItems: user.accessAll,
|
||||
readonly: user.accessAll,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -253,7 +253,6 @@ MemberGroupAccess.args = {
|
||||
type: AccessItemType.Group,
|
||||
listName: "Admin Group",
|
||||
labelName: "Admin Group",
|
||||
accessAllItems: true,
|
||||
},
|
||||
]),
|
||||
};
|
||||
@@ -309,7 +308,6 @@ CollectionAccess.args = {
|
||||
type: AccessItemType.Group,
|
||||
listName: "Admin Group",
|
||||
labelName: "Admin Group",
|
||||
accessAllItems: true,
|
||||
readonly: true,
|
||||
},
|
||||
{
|
||||
@@ -320,7 +318,6 @@ CollectionAccess.args = {
|
||||
status: OrganizationUserStatusType.Confirmed,
|
||||
role: OrganizationUserType.Admin,
|
||||
email: "admin@email.com",
|
||||
accessAllItems: true,
|
||||
readonly: true,
|
||||
},
|
||||
]),
|
||||
|
||||
@@ -20,8 +20,6 @@ export class UserTypePipe implements PipeTransform {
|
||||
return this.i18nService.t("admin");
|
||||
case OrganizationUserType.User:
|
||||
return this.i18nService.t("user");
|
||||
case OrganizationUserType.Manager:
|
||||
return this.i18nService.t("manager");
|
||||
case OrganizationUserType.Custom:
|
||||
return this.i18nService.t("custom");
|
||||
}
|
||||
|
||||
@@ -26,12 +26,12 @@
|
||||
appInputVerbatim="false"
|
||||
/>
|
||||
</bit-form-field>
|
||||
<div class="tw-mb-3 tw-flex">
|
||||
<div class="tw-mb-3 tw-flex tw-items-center">
|
||||
<button bitButton type="button" buttonType="primary" [bitAction]="sendEmail">
|
||||
{{ "sendEmail" | i18n }}
|
||||
</button>
|
||||
<span class="tw-text-success tw-ml-3" *ngIf="sentEmail">
|
||||
{{ "verificationCodeEmailSent" | i18n: sentEmail }}
|
||||
{{ "emailSent" | i18n }}
|
||||
</span>
|
||||
</div>
|
||||
<bit-form-field>
|
||||
|
||||
@@ -31,7 +31,7 @@ export class TwoFactorEmailComponent extends TwoFactorBaseComponent {
|
||||
emailPromise: Promise<unknown>;
|
||||
override componentName = "app-two-factor-email";
|
||||
formGroup = this.formBuilder.group({
|
||||
token: [null],
|
||||
token: ["", [Validators.required]],
|
||||
email: ["", [Validators.email, Validators.required]],
|
||||
});
|
||||
|
||||
@@ -79,6 +79,10 @@ export class TwoFactorEmailComponent extends TwoFactorBaseComponent {
|
||||
}
|
||||
|
||||
submit = async () => {
|
||||
this.formGroup.markAllAsTouched();
|
||||
if (this.formGroup.invalid) {
|
||||
return;
|
||||
}
|
||||
if (this.enabled) {
|
||||
await this.disableEmail();
|
||||
this.onChangeStatus.emit(false);
|
||||
|
||||
@@ -1,173 +1,124 @@
|
||||
<form
|
||||
#form
|
||||
(ngSubmit)="submit()"
|
||||
[appApiAction]="formPromise"
|
||||
class="container"
|
||||
ngNativeValidate
|
||||
autocomplete="off"
|
||||
>
|
||||
<div class="row justify-content-md-center mt-5">
|
||||
<div
|
||||
class="col-5"
|
||||
[ngClass]="{
|
||||
'col-9': !duoFrameless && isDuoProvider
|
||||
}"
|
||||
<form [bitSubmit]="submitForm" [formGroup]="formGroup" autocomplete="off">
|
||||
<div class="tw-min-w-96">
|
||||
<ng-container
|
||||
*ngIf="
|
||||
selectedProviderType === providerType.Email ||
|
||||
selectedProviderType === providerType.Authenticator
|
||||
"
|
||||
>
|
||||
<p class="lead text-center mb-4">{{ title }}</p>
|
||||
<div class="card d-block">
|
||||
<div class="card-body">
|
||||
<ng-container
|
||||
*ngIf="
|
||||
selectedProviderType === providerType.Email ||
|
||||
selectedProviderType === providerType.Authenticator
|
||||
"
|
||||
<p bitTypography="body1" *ngIf="selectedProviderType === providerType.Authenticator">
|
||||
{{ "enterVerificationCodeApp" | i18n }}
|
||||
</p>
|
||||
<p bitTypography="body1" *ngIf="selectedProviderType === providerType.Email">
|
||||
{{ "enterVerificationCodeEmail" | i18n: twoFactorEmail }}
|
||||
</p>
|
||||
<bit-form-field>
|
||||
<bit-label>{{ "verificationCode" | i18n }}</bit-label>
|
||||
<input bitInput type="text" formControlName="token" appAutofocus appInputVerbatim />
|
||||
<bit-hint *ngIf="selectedProviderType === providerType.Email">
|
||||
<a
|
||||
bitLink
|
||||
href="#"
|
||||
appStopClick
|
||||
(click)="sendEmail(true)"
|
||||
*ngIf="selectedProviderType === providerType.Email"
|
||||
>
|
||||
<p *ngIf="selectedProviderType === providerType.Authenticator">
|
||||
{{ "enterVerificationCodeApp" | i18n }}
|
||||
</p>
|
||||
<p *ngIf="selectedProviderType === providerType.Email">
|
||||
{{ "enterVerificationCodeEmail" | i18n: twoFactorEmail }}
|
||||
</p>
|
||||
<div class="form-group">
|
||||
<label for="code" class="sr-only">{{ "verificationCode" | i18n }}</label>
|
||||
<input
|
||||
id="code"
|
||||
type="text"
|
||||
name="Code"
|
||||
class="form-control"
|
||||
[(ngModel)]="token"
|
||||
required
|
||||
appAutofocus
|
||||
inputmode="tel"
|
||||
appInputVerbatim
|
||||
/>
|
||||
<small class="form-text" *ngIf="selectedProviderType === providerType.Email">
|
||||
<a
|
||||
href="#"
|
||||
appStopClick
|
||||
(click)="sendEmail(true)"
|
||||
[appApiAction]="emailPromise"
|
||||
*ngIf="selectedProviderType === providerType.Email"
|
||||
>
|
||||
{{ "sendVerificationCodeEmailAgain" | i18n }}
|
||||
</a>
|
||||
</small>
|
||||
</div>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="selectedProviderType === providerType.Yubikey">
|
||||
<p class="text-center">{{ "insertYubiKey" | i18n }}</p>
|
||||
<picture>
|
||||
<source srcset="../../images/yubikey.avif" type="image/avif" />
|
||||
<source srcset="../../images/yubikey.webp" type="image/webp" />
|
||||
<img src="../../images/yubikey.jpg" class="rounded img-fluid mb-3" alt="" />
|
||||
</picture>
|
||||
<div class="form-group">
|
||||
<label for="code" class="sr-only">{{ "verificationCode" | i18n }}</label>
|
||||
<input
|
||||
id="code"
|
||||
type="password"
|
||||
name="Code"
|
||||
class="form-control"
|
||||
[(ngModel)]="token"
|
||||
required
|
||||
appAutofocus
|
||||
appInputVerbatim
|
||||
autocomplete="new-password"
|
||||
/>
|
||||
</div>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="selectedProviderType === providerType.WebAuthn">
|
||||
<div id="web-authn-frame" class="mb-3">
|
||||
<iframe id="webauthn_iframe" sandbox="allow-scripts allow-same-origin"></iframe>
|
||||
</div>
|
||||
</ng-container>
|
||||
<!-- Duo -->
|
||||
<ng-container *ngIf="isDuoProvider">
|
||||
<ng-container *ngIf="duoFrameless">
|
||||
<p *ngIf="selectedProviderType === providerType.OrganizationDuo" class="tw-mb-0">
|
||||
{{ "duoRequiredByOrgForAccount" | i18n }}
|
||||
</p>
|
||||
<p>{{ "launchDuoAndFollowStepsToFinishLoggingIn" | i18n }}</p>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="!duoFrameless">
|
||||
<div id="duo-frame" class="mb-3">
|
||||
<iframe
|
||||
id="duo_iframe"
|
||||
sandbox="allow-scripts allow-forms allow-same-origin allow-popups allow-popups-to-escape-sandbox"
|
||||
></iframe>
|
||||
</div>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
<i
|
||||
class="bwi bwi-spinner text-muted bwi-spin pull-right"
|
||||
title="{{ 'loading' | i18n }}"
|
||||
*ngIf="form.loading && selectedProviderType === providerType.WebAuthn"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
<div class="form-check" *ngIf="selectedProviderType != null">
|
||||
<input
|
||||
id="remember"
|
||||
type="checkbox"
|
||||
name="Remember"
|
||||
class="form-check-input"
|
||||
[(ngModel)]="remember"
|
||||
/>
|
||||
<label for="remember" class="form-check-label">{{ "rememberMe" | i18n }}</label>
|
||||
</div>
|
||||
<ng-container *ngIf="selectedProviderType == null">
|
||||
<p>{{ "noTwoStepProviders" | i18n }}</p>
|
||||
<p>{{ "noTwoStepProviders2" | i18n }}</p>
|
||||
</ng-container>
|
||||
<hr />
|
||||
<div [hidden]="!showCaptcha()">
|
||||
<iframe
|
||||
id="hcaptcha_iframe"
|
||||
height="80"
|
||||
sandbox="allow-scripts allow-same-origin"
|
||||
></iframe>
|
||||
</div>
|
||||
<!-- Buttons -->
|
||||
<div class="tw-flex tw-flex-col tw-mb-3">
|
||||
<button
|
||||
type="submit"
|
||||
class="btn btn-primary btn-block btn-submit"
|
||||
[disabled]="form.loading"
|
||||
*ngIf="
|
||||
selectedProviderType != null &&
|
||||
!isDuoProvider &&
|
||||
selectedProviderType !== providerType.WebAuthn
|
||||
"
|
||||
>
|
||||
<span>
|
||||
<i class="bwi bwi-sign-in" aria-hidden="true"></i> {{ "continue" | i18n }}
|
||||
</span>
|
||||
<i
|
||||
class="bwi bwi-spinner bwi-spin"
|
||||
title="{{ 'loading' | i18n }}"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
</button>
|
||||
<button
|
||||
(click)="launchDuoFrameless()"
|
||||
type="button"
|
||||
class="btn btn-primary btn-block"
|
||||
[disabled]="form.loading"
|
||||
*ngIf="duoFrameless && isDuoProvider"
|
||||
>
|
||||
<span> {{ "launchDuo" | i18n }} </span>
|
||||
</button>
|
||||
<a routerLink="/login" class="btn btn-outline-secondary btn-block">
|
||||
{{ "cancel" | i18n }}
|
||||
</a>
|
||||
</div>
|
||||
<div class="text-center">
|
||||
<a href="#" appStopClick (click)="anotherMethod()">{{
|
||||
"useAnotherTwoStepMethod" | i18n
|
||||
}}</a>
|
||||
</div>
|
||||
</div>
|
||||
{{ "sendVerificationCodeEmailAgain" | i18n }}
|
||||
</a></bit-hint
|
||||
>
|
||||
</bit-form-field>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="selectedProviderType === providerType.Yubikey">
|
||||
<p bitTypography="body1" class="tw-text-center">{{ "insertYubiKey" | i18n }}</p>
|
||||
<picture>
|
||||
<source srcset="../../images/yubikey.avif" type="image/avif" />
|
||||
<source srcset="../../images/yubikey.webp" type="image/webp" />
|
||||
<img src="../../images/yubikey.jpg" class="tw-rounded img-fluid tw-mb-3" alt="" />
|
||||
</picture>
|
||||
<bit-form-field>
|
||||
<bit-label class="tw-sr-only">{{ "verificationCode" | i18n }}</bit-label>
|
||||
<input
|
||||
type="text"
|
||||
bitInput
|
||||
formControlName="token"
|
||||
appAutofocus
|
||||
appInputVerbatim
|
||||
autocomplete="new-password"
|
||||
/>
|
||||
</bit-form-field>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="selectedProviderType === providerType.WebAuthn">
|
||||
<div id="web-authn-frame" class="tw-mb-3">
|
||||
<iframe id="webauthn_iframe" sandbox="allow-scripts allow-same-origin"></iframe>
|
||||
</div>
|
||||
</ng-container>
|
||||
<!-- Duo -->
|
||||
<ng-container *ngIf="isDuoProvider">
|
||||
<ng-container *ngIf="duoFrameless">
|
||||
<p
|
||||
bitTypography="body1"
|
||||
*ngIf="selectedProviderType === providerType.OrganizationDuo"
|
||||
class="tw-mb-0"
|
||||
>
|
||||
{{ "duoRequiredByOrgForAccount" | i18n }}
|
||||
</p>
|
||||
<p bitTypography="body1">{{ "launchDuoAndFollowStepsToFinishLoggingIn" | i18n }}</p>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="!duoFrameless">
|
||||
<div id="duo-frame" class="tw-mb-3">
|
||||
<iframe
|
||||
id="duo_iframe"
|
||||
sandbox="allow-scripts allow-forms allow-same-origin allow-popups allow-popups-to-escape-sandbox"
|
||||
></iframe>
|
||||
</div>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
<bit-form-control *ngIf="selectedProviderType != null">
|
||||
<bit-label>{{ "rememberMe" | i18n }}</bit-label>
|
||||
<input type="checkbox" bitCheckbox formControlName="remember" />
|
||||
</bit-form-control>
|
||||
<ng-container *ngIf="selectedProviderType == null">
|
||||
<p bitTypography="body1">{{ "noTwoStepProviders" | i18n }}</p>
|
||||
<p bitTypography="body1">{{ "noTwoStepProviders2" | i18n }}</p>
|
||||
</ng-container>
|
||||
<hr />
|
||||
<div [hidden]="!showCaptcha()">
|
||||
<iframe id="hcaptcha_iframe" height="80" sandbox="allow-scripts allow-same-origin"></iframe>
|
||||
</div>
|
||||
<!-- Buttons -->
|
||||
<div class="tw-flex tw-flex-col tw-space-y-2.5 tw-mb-3">
|
||||
<button
|
||||
type="submit"
|
||||
buttonType="primary"
|
||||
bitButton
|
||||
bitFormButton
|
||||
*ngIf="
|
||||
selectedProviderType != null &&
|
||||
!isDuoProvider &&
|
||||
selectedProviderType !== providerType.WebAuthn
|
||||
"
|
||||
>
|
||||
<span> <i class="bwi bwi-sign-in" aria-hidden="true"></i> {{ "continue" | i18n }} </span>
|
||||
</button>
|
||||
<button
|
||||
(click)="launchDuoFrameless()"
|
||||
type="button"
|
||||
buttonType="primary"
|
||||
bitButton
|
||||
bitFormButton
|
||||
*ngIf="duoFrameless && isDuoProvider"
|
||||
>
|
||||
<span> {{ "launchDuo" | i18n }} </span>
|
||||
</button>
|
||||
<a routerLink="/login" bitButton buttonType="secondary">
|
||||
{{ "cancel" | i18n }}
|
||||
</a>
|
||||
</div>
|
||||
<div class="text-center">
|
||||
<a bitLink href="#" appStopClick (click)="anotherMethod()">{{
|
||||
"useAnotherTwoStepMethod" | i18n
|
||||
}}</a>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Component, Inject, OnDestroy, ViewChild, ViewContainerRef } from "@angular/core";
|
||||
import { FormBuilder, Validators } from "@angular/forms";
|
||||
import { ActivatedRoute, Router } from "@angular/router";
|
||||
import { lastValueFrom } from "rxjs";
|
||||
import { Subject, takeUntil, lastValueFrom } from "rxjs";
|
||||
|
||||
import { TwoFactorComponent as BaseTwoFactorComponent } from "@bitwarden/angular/auth/components/two-factor.component";
|
||||
import { WINDOW } from "@bitwarden/angular/services/injection-tokens";
|
||||
@@ -38,7 +39,17 @@ import {
|
||||
export class TwoFactorComponent extends BaseTwoFactorComponent implements OnDestroy {
|
||||
@ViewChild("twoFactorOptions", { read: ViewContainerRef, static: true })
|
||||
twoFactorOptionsModal: ViewContainerRef;
|
||||
|
||||
formGroup = this.formBuilder.group({
|
||||
token: [
|
||||
"",
|
||||
{
|
||||
validators: [Validators.required],
|
||||
updateOn: "submit",
|
||||
},
|
||||
],
|
||||
remember: [false],
|
||||
});
|
||||
private destroy$ = new Subject<void>();
|
||||
constructor(
|
||||
loginStrategyService: LoginStrategyServiceAbstraction,
|
||||
router: Router,
|
||||
@@ -58,6 +69,7 @@ export class TwoFactorComponent extends BaseTwoFactorComponent implements OnDest
|
||||
configService: ConfigService,
|
||||
masterPasswordService: InternalMasterPasswordServiceAbstraction,
|
||||
accountService: AccountService,
|
||||
private formBuilder: FormBuilder,
|
||||
@Inject(WINDOW) protected win: Window,
|
||||
) {
|
||||
super(
|
||||
@@ -82,6 +94,16 @@ export class TwoFactorComponent extends BaseTwoFactorComponent implements OnDest
|
||||
);
|
||||
this.onSuccessfulLoginNavigate = this.goAfterLogIn;
|
||||
}
|
||||
async ngOnInit() {
|
||||
await super.ngOnInit();
|
||||
this.formGroup.valueChanges.pipe(takeUntil(this.destroy$)).subscribe((value) => {
|
||||
this.token = value.token;
|
||||
this.remember = value.remember;
|
||||
});
|
||||
}
|
||||
submitForm = async () => {
|
||||
await this.submit();
|
||||
};
|
||||
|
||||
async anotherMethod() {
|
||||
const dialogRef = TwoFactorOptionsComponent.open(this.dialogService);
|
||||
|
||||
@@ -82,7 +82,6 @@ const routes: Routes = [
|
||||
component: LoginViaAuthRequestComponent,
|
||||
data: { titleId: "adminApprovalRequested" } satisfies DataProperties,
|
||||
},
|
||||
{ path: "2fa", component: TwoFactorComponent, canActivate: [UnauthGuard] },
|
||||
{
|
||||
path: "login-initiated",
|
||||
component: LoginDecryptionOptionsComponent,
|
||||
@@ -189,6 +188,33 @@ const routes: Routes = [
|
||||
path: "",
|
||||
component: AnonLayoutWrapperComponent,
|
||||
children: [
|
||||
{
|
||||
path: "2fa",
|
||||
component: TwoFactorComponent,
|
||||
canActivate: [unauthGuardFn()],
|
||||
data: {
|
||||
pageTitle: "verifyIdentity",
|
||||
},
|
||||
},
|
||||
{
|
||||
path: "recover-2fa",
|
||||
canActivate: [unauthGuardFn()],
|
||||
children: [
|
||||
{
|
||||
path: "",
|
||||
component: RecoverTwoFactorComponent,
|
||||
},
|
||||
{
|
||||
path: "",
|
||||
component: EnvironmentSelectorComponent,
|
||||
outlet: "environment-selector",
|
||||
},
|
||||
],
|
||||
data: {
|
||||
pageTitle: "recoverAccountTwoStep",
|
||||
titleId: "recoverAccountTwoStep",
|
||||
} satisfies DataProperties & AnonLayoutWrapperData,
|
||||
},
|
||||
{
|
||||
path: "accept-emergency",
|
||||
canActivate: [deepLinkGuard()],
|
||||
@@ -212,25 +238,6 @@ const routes: Routes = [
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: "recover-2fa",
|
||||
canActivate: [unauthGuardFn()],
|
||||
children: [
|
||||
{
|
||||
path: "",
|
||||
component: RecoverTwoFactorComponent,
|
||||
},
|
||||
{
|
||||
path: "",
|
||||
component: EnvironmentSelectorComponent,
|
||||
outlet: "environment-selector",
|
||||
},
|
||||
],
|
||||
data: {
|
||||
pageTitle: "recoverAccountTwoStep",
|
||||
titleId: "recoverAccountTwoStep",
|
||||
} satisfies DataProperties & AnonLayoutWrapperData,
|
||||
},
|
||||
{
|
||||
path: "remove-password",
|
||||
component: RemovePasswordComponent,
|
||||
|
||||
@@ -64,7 +64,7 @@
|
||||
</bit-form-field>
|
||||
</bit-tab>
|
||||
<bit-tab label="{{ 'access' | i18n }}">
|
||||
<div class="tw-mb-3" *ngIf="organization.flexibleCollections">
|
||||
<div class="tw-mb-3">
|
||||
<ng-container *ngIf="dialogReadonly">
|
||||
<span>{{ "readOnlyCollectionAccess" | i18n }}</span>
|
||||
</ng-container>
|
||||
@@ -107,7 +107,6 @@
|
||||
[selectorLabelText]="'selectGroupsAndMembers' | i18n"
|
||||
[selectorHelpText]="'userPermissionOverrideHelperDesc' | i18n"
|
||||
[emptySelectionText]="'noMembersOrGroupsAdded' | i18n"
|
||||
[flexibleCollectionsEnabled]="organization.flexibleCollections"
|
||||
></bit-access-selector>
|
||||
<bit-access-selector
|
||||
*ngIf="!organization.useGroups"
|
||||
@@ -117,7 +116,6 @@
|
||||
[columnHeader]="'memberColumnHeader' | i18n"
|
||||
[selectorLabelText]="'selectMembers' | i18n"
|
||||
[emptySelectionText]="'noMembersAdded' | i18n"
|
||||
[flexibleCollectionsEnabled]="organization.flexibleCollections"
|
||||
></bit-access-selector>
|
||||
</bit-tab>
|
||||
</bit-tab-group>
|
||||
|
||||
@@ -223,7 +223,7 @@ export class CollectionDialogComponent implements OnInit, OnDestroy {
|
||||
(u) => u.userId === this.organization?.userId,
|
||||
)?.id;
|
||||
const initialSelection: AccessItemValue[] =
|
||||
currentOrgUserId !== undefined && organization.flexibleCollections
|
||||
currentOrgUserId !== undefined
|
||||
? [
|
||||
{
|
||||
id: currentOrgUserId,
|
||||
@@ -239,11 +239,7 @@ export class CollectionDialogComponent implements OnInit, OnDestroy {
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
organization.flexibleCollections &&
|
||||
flexibleCollectionsV1 &&
|
||||
!organization.allowAdminAccessToAllCollectionItems
|
||||
) {
|
||||
if (flexibleCollectionsV1 && !organization.allowAdminAccessToAllCollectionItems) {
|
||||
this.formGroup.controls.access.addValidators(validateCanManagePermission);
|
||||
} else {
|
||||
this.formGroup.controls.access.removeValidators(validateCanManagePermission);
|
||||
@@ -444,8 +440,7 @@ function mapGroupToAccessItemView(group: GroupView, collectionId: string): Acces
|
||||
type: AccessItemType.Group,
|
||||
listName: group.name,
|
||||
labelName: group.name,
|
||||
accessAllItems: group.accessAll,
|
||||
readonly: group.accessAll,
|
||||
readonly: false,
|
||||
readonlyPermission:
|
||||
collectionId != null
|
||||
? convertToPermission(group.collections.find((gc) => gc.id == collectionId))
|
||||
@@ -471,8 +466,7 @@ function mapUserToAccessItemView(
|
||||
listName: user.name?.length > 0 ? `${user.name} (${user.email})` : user.email,
|
||||
labelName: user.name ?? user.email,
|
||||
status: user.status,
|
||||
accessAllItems: user.accessAll,
|
||||
readonly: user.accessAll,
|
||||
readonly: false,
|
||||
readonlyPermission:
|
||||
collectionId != null
|
||||
? convertToPermission(
|
||||
|
||||
@@ -86,7 +86,7 @@ export class VaultCollectionRowComponent {
|
||||
return this.i18nService.t("canEdit");
|
||||
}
|
||||
if ((this.collection as CollectionAdminView).assigned) {
|
||||
const permissionList = getPermissionList(this.organization?.flexibleCollections);
|
||||
const permissionList = getPermissionList();
|
||||
return this.i18nService.t(
|
||||
permissionList.find((p) => p.perm === convertToPermission(this.collection))?.labelId,
|
||||
);
|
||||
|
||||
@@ -15,7 +15,6 @@ import { VaultFilter } from "../models/vault-filter.model";
|
||||
})
|
||||
export class VaultFilterSectionComponent implements OnInit, OnDestroy {
|
||||
private destroy$ = new Subject<void>();
|
||||
protected flexibleCollectionsEnabled: boolean;
|
||||
|
||||
@Input() activeFilter: VaultFilter;
|
||||
@Input() section: VaultFilterSection;
|
||||
@@ -40,12 +39,6 @@ export class VaultFilterSectionComponent implements OnInit, OnDestroy {
|
||||
this.section?.data$?.pipe(takeUntil(this.destroy$)).subscribe((data) => {
|
||||
this.data = data;
|
||||
});
|
||||
this.vaultFilterService
|
||||
.getOrganizationFilter()
|
||||
.pipe(takeUntil(this.destroy$))
|
||||
.subscribe((org) => {
|
||||
this.flexibleCollectionsEnabled = org != null ? org.flexibleCollections : false;
|
||||
});
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
@@ -77,10 +70,9 @@ export class VaultFilterSectionComponent implements OnInit, OnDestroy {
|
||||
const { organizationId, cipherTypeId, folderId, collectionId, isCollectionSelected } =
|
||||
this.activeFilter;
|
||||
|
||||
const collectionStatus = this.flexibleCollectionsEnabled
|
||||
? filterNode?.node.id === "AllCollections" &&
|
||||
(isCollectionSelected || collectionId === "AllCollections")
|
||||
: collectionId === filterNode?.node.id;
|
||||
const collectionStatus =
|
||||
filterNode?.node.id === "AllCollections" &&
|
||||
(isCollectionSelected || collectionId === "AllCollections");
|
||||
|
||||
return (
|
||||
organizationId === filterNode?.node.id ||
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
[selectorLabelText]="'selectGroupsAndMembers' | i18n"
|
||||
[selectorHelpText]="'userPermissionOverrideHelperDesc' | i18n"
|
||||
[emptySelectionText]="'noMembersOrGroupsAdded' | i18n"
|
||||
[flexibleCollectionsEnabled]="flexibleCollectionsEnabled$ | async"
|
||||
></bit-access-selector>
|
||||
<bit-access-selector
|
||||
*ngIf="!organization?.useGroups"
|
||||
@@ -27,7 +26,6 @@
|
||||
[columnHeader]="'memberColumnHeader' | i18n"
|
||||
[selectorLabelText]="'selectMembers' | i18n"
|
||||
[emptySelectionText]="'noMembersAdded' | i18n"
|
||||
[flexibleCollectionsEnabled]="flexibleCollectionsEnabled$ | async"
|
||||
></bit-access-selector>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { DIALOG_DATA, DialogConfig, DialogRef } from "@angular/cdk/dialog";
|
||||
import { Component, Inject, OnDestroy } from "@angular/core";
|
||||
import { FormBuilder } from "@angular/forms";
|
||||
import { combineLatest, map, of, Subject, switchMap, takeUntil } from "rxjs";
|
||||
import { combineLatest, of, Subject, switchMap, takeUntil } from "rxjs";
|
||||
|
||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { OrganizationUserService } from "@bitwarden/common/admin-console/abstractions/organization-user/organization-user.service";
|
||||
@@ -42,10 +42,6 @@ export enum BulkCollectionsDialogResult {
|
||||
standalone: true,
|
||||
})
|
||||
export class BulkCollectionsDialogComponent implements OnDestroy {
|
||||
protected flexibleCollectionsEnabled$ = this.organizationService
|
||||
.get$(this.params.organizationId)
|
||||
.pipe(map((o) => o?.flexibleCollections));
|
||||
|
||||
protected readonly PermissionMode = PermissionMode;
|
||||
|
||||
protected formGroup = this.formBuilder.group({
|
||||
|
||||
@@ -103,11 +103,7 @@ export class VaultFilterComponent extends BaseVaultFilterComponent implements On
|
||||
async buildAllFilters(): Promise<VaultFilterList> {
|
||||
const builderFilter = {} as VaultFilterList;
|
||||
builderFilter.typeFilter = await this.addTypeFilter(["favorites"]);
|
||||
if (this._organization?.flexibleCollections) {
|
||||
builderFilter.collectionFilter = await this.addCollectionFilter();
|
||||
} else {
|
||||
builderFilter.collectionFilter = await super.addCollectionFilter();
|
||||
}
|
||||
builderFilter.collectionFilter = await this.addCollectionFilter();
|
||||
builderFilter.trashFilter = await this.addTrashFilter();
|
||||
return builderFilter;
|
||||
}
|
||||
|
||||
@@ -6,10 +6,7 @@
|
||||
queryParamsHandling="merge"
|
||||
>
|
||||
{{ organization.name }}
|
||||
<span *ngIf="!organization.flexibleCollections">
|
||||
{{ "vault" | i18n | lowercase }}
|
||||
</span>
|
||||
<span *ngIf="organization.flexibleCollections">
|
||||
<span>
|
||||
{{ "collections" | i18n | lowercase }}
|
||||
</span>
|
||||
</bit-breadcrumb>
|
||||
|
||||
@@ -89,9 +89,7 @@ export class VaultHeaderComponent implements OnInit {
|
||||
}
|
||||
|
||||
get title() {
|
||||
const headerType = this.organization?.flexibleCollections
|
||||
? this.i18nService.t("collections").toLowerCase()
|
||||
: this.i18nService.t("vault").toLowerCase();
|
||||
const headerType = this.i18nService.t("collections").toLowerCase();
|
||||
|
||||
if (this.collection != null) {
|
||||
return this.collection.node.name;
|
||||
|
||||
@@ -65,8 +65,8 @@
|
||||
[useEvents]="organization?.canAccessEventLogs"
|
||||
[showAdminActions]="true"
|
||||
(onEvent)="onVaultItemsEvent($event)"
|
||||
[showBulkEditCollectionAccess]="organization?.flexibleCollections"
|
||||
[showBulkAddToCollections]="organization?.flexibleCollections"
|
||||
[showBulkEditCollectionAccess]="true"
|
||||
[showBulkAddToCollections]="true"
|
||||
[viewingOrgVault]="true"
|
||||
[flexibleCollectionsV1Enabled]="flexibleCollectionsV1Enabled"
|
||||
[addAccessStatus]="addAccessStatus$ | async"
|
||||
|
||||
@@ -156,7 +156,7 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||
private _flexibleCollectionsV1FlagEnabled: boolean;
|
||||
|
||||
protected get flexibleCollectionsV1Enabled(): boolean {
|
||||
return this._flexibleCollectionsV1FlagEnabled && this.organization?.flexibleCollections;
|
||||
return this._flexibleCollectionsV1FlagEnabled;
|
||||
}
|
||||
protected orgRevokedUsers: OrganizationUserUserDetailsResponse[];
|
||||
|
||||
|
||||
@@ -722,6 +722,9 @@
|
||||
"logIn": {
|
||||
"message": "Log in"
|
||||
},
|
||||
"verifyIdentity": {
|
||||
"message": "Verify your Identity"
|
||||
},
|
||||
"logInInitiated": {
|
||||
"message": "Log in initiated"
|
||||
},
|
||||
@@ -2794,12 +2797,6 @@
|
||||
"userDesc": {
|
||||
"message": "Access and add items to assigned collections"
|
||||
},
|
||||
"manager": {
|
||||
"message": "Manager"
|
||||
},
|
||||
"managerDesc": {
|
||||
"message": "Create, delete, and manage access in assigned collections"
|
||||
},
|
||||
"all": {
|
||||
"message": "All"
|
||||
},
|
||||
@@ -4576,12 +4573,6 @@
|
||||
"permission": {
|
||||
"message": "Permission"
|
||||
},
|
||||
"managerPermissions": {
|
||||
"message": "Manager Permissions"
|
||||
},
|
||||
"adminPermissions": {
|
||||
"message": "Admin Permissions"
|
||||
},
|
||||
"accessEventLogs": {
|
||||
"message": "Access event logs"
|
||||
},
|
||||
@@ -4606,9 +4597,6 @@
|
||||
"deleteAnyCollection": {
|
||||
"message": "Delete any collection"
|
||||
},
|
||||
"manageAssignedCollections": {
|
||||
"message": "Manage assigned collections"
|
||||
},
|
||||
"editAssignedCollections": {
|
||||
"message": "Edit assigned collections"
|
||||
},
|
||||
@@ -6669,12 +6657,6 @@
|
||||
"restrictedCollectionAssignmentDesc": {
|
||||
"message": "You can only assign collections you manage."
|
||||
},
|
||||
"accessAllCollectionsDesc": {
|
||||
"message": "Grant access to all current and future collections."
|
||||
},
|
||||
"accessAllCollectionsHelp": {
|
||||
"message": "If checked, this will replace all other collection permissions."
|
||||
},
|
||||
"selectMembers": {
|
||||
"message": "Select members"
|
||||
},
|
||||
@@ -6717,12 +6699,6 @@
|
||||
"group": {
|
||||
"message": "Group"
|
||||
},
|
||||
"groupAccessAll": {
|
||||
"message": "This group can access and modify all items."
|
||||
},
|
||||
"memberAccessAll": {
|
||||
"message": "This member can access and modify all items."
|
||||
},
|
||||
"domainVerification": {
|
||||
"message": "Domain verification"
|
||||
},
|
||||
@@ -8357,5 +8333,12 @@
|
||||
},
|
||||
"viewSecret": {
|
||||
"message": "View secret"
|
||||
},
|
||||
"noClients": {
|
||||
"message": "There are no clients to list"
|
||||
},
|
||||
"providerBillingEmailHint": {
|
||||
"message": "This email address will receive all invoices pertaining to this provider",
|
||||
"description": "A hint that shows up on the Provider setup page to inform the admin the billing email will receive the provider's invoices."
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user