mirror of
https://github.com/bitwarden/browser
synced 2026-03-01 11:01:17 +00:00
Merge branch 'main' of github.com:bitwarden/clients into arch/ng-localize
# Conflicts: # apps/web/src/app/auth/login/login-v1.component.html # apps/web/src/app/auth/login/login-v1.component.ts # apps/web/webpack.config.js
This commit is contained in:
@@ -1,425 +0,0 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Directive, ViewChild, ViewContainerRef } from "@angular/core";
|
||||
import { FormControl } from "@angular/forms";
|
||||
import { firstValueFrom, concatMap, map, lastValueFrom, startWith, debounceTime } from "rxjs";
|
||||
|
||||
import { SearchPipe } from "@bitwarden/angular/pipes/search.pipe";
|
||||
import { UserNamePipe } from "@bitwarden/angular/pipes/user-name.pipe";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { SearchService } from "@bitwarden/common/abstractions/search.service";
|
||||
import { OrganizationManagementPreferencesService } from "@bitwarden/common/admin-console/abstractions/organization-management-preferences/organization-management-preferences.service";
|
||||
import {
|
||||
OrganizationUserStatusType,
|
||||
OrganizationUserType,
|
||||
ProviderUserStatusType,
|
||||
ProviderUserType,
|
||||
} from "@bitwarden/common/admin-console/enums";
|
||||
import { ProviderUserUserDetailsResponse } from "@bitwarden/common/admin-console/models/response/provider/provider-user.response";
|
||||
import { ListResponse } from "@bitwarden/common/models/response/list.response";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { DialogService, ToastService } from "@bitwarden/components";
|
||||
import { KeyService } from "@bitwarden/key-management";
|
||||
|
||||
import { OrganizationUserView } from "../organizations/core/views/organization-user.view";
|
||||
import { UserConfirmComponent } from "../organizations/manage/user-confirm.component";
|
||||
|
||||
type StatusType = OrganizationUserStatusType | ProviderUserStatusType;
|
||||
|
||||
const MaxCheckedCount = 500;
|
||||
|
||||
@Directive()
|
||||
export abstract class BasePeopleComponent<
|
||||
UserType extends ProviderUserUserDetailsResponse | OrganizationUserView,
|
||||
> {
|
||||
@ViewChild("confirmTemplate", { read: ViewContainerRef, static: true })
|
||||
confirmModalRef: ViewContainerRef;
|
||||
|
||||
get allCount() {
|
||||
return this.activeUsers != null ? this.activeUsers.length : 0;
|
||||
}
|
||||
|
||||
get invitedCount() {
|
||||
return this.statusMap.has(this.userStatusType.Invited)
|
||||
? this.statusMap.get(this.userStatusType.Invited).length
|
||||
: 0;
|
||||
}
|
||||
|
||||
get acceptedCount() {
|
||||
return this.statusMap.has(this.userStatusType.Accepted)
|
||||
? this.statusMap.get(this.userStatusType.Accepted).length
|
||||
: 0;
|
||||
}
|
||||
|
||||
get confirmedCount() {
|
||||
return this.statusMap.has(this.userStatusType.Confirmed)
|
||||
? this.statusMap.get(this.userStatusType.Confirmed).length
|
||||
: 0;
|
||||
}
|
||||
|
||||
get revokedCount() {
|
||||
return this.statusMap.has(this.userStatusType.Revoked)
|
||||
? this.statusMap.get(this.userStatusType.Revoked).length
|
||||
: 0;
|
||||
}
|
||||
|
||||
get showConfirmUsers(): boolean {
|
||||
return (
|
||||
this.activeUsers != null &&
|
||||
this.statusMap != null &&
|
||||
this.activeUsers.length > 1 &&
|
||||
this.confirmedCount > 0 &&
|
||||
this.confirmedCount < 3 &&
|
||||
this.acceptedCount > 0
|
||||
);
|
||||
}
|
||||
|
||||
get showBulkConfirmUsers(): boolean {
|
||||
return this.acceptedCount > 0;
|
||||
}
|
||||
|
||||
abstract userType: typeof OrganizationUserType | typeof ProviderUserType;
|
||||
abstract userStatusType: typeof OrganizationUserStatusType | typeof ProviderUserStatusType;
|
||||
|
||||
loading = true;
|
||||
statusMap = new Map<StatusType, UserType[]>();
|
||||
status: StatusType;
|
||||
users: UserType[] = [];
|
||||
pagedUsers: UserType[] = [];
|
||||
actionPromise: Promise<void>;
|
||||
|
||||
protected allUsers: UserType[] = [];
|
||||
protected activeUsers: UserType[] = [];
|
||||
|
||||
protected didScroll = false;
|
||||
protected pageSize = 100;
|
||||
|
||||
protected searchControl = new FormControl("", { nonNullable: true });
|
||||
protected isSearching$ = this.searchControl.valueChanges.pipe(
|
||||
debounceTime(500),
|
||||
concatMap((searchText) => this.searchService.isSearchable(searchText)),
|
||||
startWith(false),
|
||||
);
|
||||
protected isPaging$ = this.isSearching$.pipe(
|
||||
map((isSearching) => {
|
||||
if (isSearching && this.didScroll) {
|
||||
this.resetPaging();
|
||||
}
|
||||
return !isSearching && this.users && this.users.length > this.pageSize;
|
||||
}),
|
||||
);
|
||||
|
||||
private pagedUsersCount = 0;
|
||||
|
||||
constructor(
|
||||
protected apiService: ApiService,
|
||||
private searchService: SearchService,
|
||||
protected i18nService: I18nService,
|
||||
protected platformUtilsService: PlatformUtilsService,
|
||||
protected keyService: KeyService,
|
||||
protected validationService: ValidationService,
|
||||
private logService: LogService,
|
||||
private searchPipe: SearchPipe,
|
||||
protected userNamePipe: UserNamePipe,
|
||||
protected dialogService: DialogService,
|
||||
protected organizationManagementPreferencesService: OrganizationManagementPreferencesService,
|
||||
protected toastService: ToastService,
|
||||
) {}
|
||||
|
||||
abstract edit(user: UserType): void;
|
||||
abstract getUsers(): Promise<ListResponse<UserType> | UserType[]>;
|
||||
abstract deleteUser(id: string): Promise<void>;
|
||||
abstract revokeUser(id: string): Promise<void>;
|
||||
abstract restoreUser(id: string): Promise<void>;
|
||||
abstract reinviteUser(id: string): Promise<void>;
|
||||
abstract confirmUser(user: UserType, publicKey: Uint8Array): Promise<void>;
|
||||
|
||||
async load() {
|
||||
const response = await this.getUsers();
|
||||
this.statusMap.clear();
|
||||
this.activeUsers = [];
|
||||
for (const status of Utils.iterateEnum(this.userStatusType)) {
|
||||
this.statusMap.set(status, []);
|
||||
}
|
||||
|
||||
if (response instanceof ListResponse) {
|
||||
this.allUsers = response.data != null && response.data.length > 0 ? response.data : [];
|
||||
} else if (Array.isArray(response)) {
|
||||
this.allUsers = response;
|
||||
}
|
||||
|
||||
this.allUsers.sort(
|
||||
Utils.getSortFunction<ProviderUserUserDetailsResponse | OrganizationUserView>(
|
||||
this.i18nService,
|
||||
"email",
|
||||
),
|
||||
);
|
||||
this.allUsers.forEach((u) => {
|
||||
if (!this.statusMap.has(u.status)) {
|
||||
this.statusMap.set(u.status, [u]);
|
||||
} else {
|
||||
this.statusMap.get(u.status).push(u);
|
||||
}
|
||||
if (u.status !== this.userStatusType.Revoked) {
|
||||
this.activeUsers.push(u);
|
||||
}
|
||||
});
|
||||
this.filter(this.status);
|
||||
this.loading = false;
|
||||
}
|
||||
|
||||
filter(status: StatusType) {
|
||||
this.status = status;
|
||||
if (this.status != null) {
|
||||
this.users = this.statusMap.get(this.status);
|
||||
} else {
|
||||
this.users = this.activeUsers;
|
||||
}
|
||||
// Reset checkbox selecton
|
||||
this.selectAll(false);
|
||||
this.resetPaging();
|
||||
}
|
||||
|
||||
loadMore() {
|
||||
if (!this.users || this.users.length <= this.pageSize) {
|
||||
return;
|
||||
}
|
||||
const pagedLength = this.pagedUsers.length;
|
||||
let pagedSize = this.pageSize;
|
||||
if (pagedLength === 0 && this.pagedUsersCount > this.pageSize) {
|
||||
pagedSize = this.pagedUsersCount;
|
||||
}
|
||||
if (this.users.length > pagedLength) {
|
||||
this.pagedUsers = this.pagedUsers.concat(
|
||||
this.users.slice(pagedLength, pagedLength + pagedSize),
|
||||
);
|
||||
}
|
||||
this.pagedUsersCount = this.pagedUsers.length;
|
||||
this.didScroll = this.pagedUsers.length > this.pageSize;
|
||||
}
|
||||
|
||||
checkUser(user: UserType, select?: boolean) {
|
||||
(user as any).checked = select == null ? !(user as any).checked : select;
|
||||
}
|
||||
|
||||
selectAll(select: boolean) {
|
||||
if (select) {
|
||||
this.selectAll(false);
|
||||
}
|
||||
|
||||
const filteredUsers = this.searchPipe.transform(
|
||||
this.users,
|
||||
this.searchControl.value,
|
||||
"name",
|
||||
"email",
|
||||
"id",
|
||||
);
|
||||
|
||||
const selectCount =
|
||||
select && filteredUsers.length > MaxCheckedCount ? MaxCheckedCount : filteredUsers.length;
|
||||
for (let i = 0; i < selectCount; i++) {
|
||||
this.checkUser(filteredUsers[i], select);
|
||||
}
|
||||
}
|
||||
|
||||
resetPaging() {
|
||||
this.pagedUsers = [];
|
||||
this.loadMore();
|
||||
}
|
||||
|
||||
invite() {
|
||||
this.edit(null);
|
||||
}
|
||||
|
||||
protected async removeUserConfirmationDialog(user: UserType) {
|
||||
return this.dialogService.openSimpleDialog({
|
||||
title: this.userNamePipe.transform(user),
|
||||
content: { key: "removeUserConfirmation" },
|
||||
type: "warning",
|
||||
});
|
||||
}
|
||||
|
||||
async remove(user: UserType) {
|
||||
const confirmed = await this.removeUserConfirmationDialog(user);
|
||||
if (!confirmed) {
|
||||
return false;
|
||||
}
|
||||
|
||||
this.actionPromise = this.deleteUser(user.id);
|
||||
try {
|
||||
await this.actionPromise;
|
||||
this.toastService.showToast({
|
||||
variant: "success",
|
||||
title: null,
|
||||
message: this.i18nService.t("removedUserId", this.userNamePipe.transform(user)),
|
||||
});
|
||||
this.removeUser(user);
|
||||
} catch (e) {
|
||||
this.validationService.showError(e);
|
||||
}
|
||||
this.actionPromise = null;
|
||||
}
|
||||
|
||||
protected async revokeUserConfirmationDialog(user: UserType) {
|
||||
return this.dialogService.openSimpleDialog({
|
||||
title: { key: "revokeAccess", placeholders: [this.userNamePipe.transform(user)] },
|
||||
content: this.revokeWarningMessage(),
|
||||
acceptButtonText: { key: "revokeAccess" },
|
||||
type: "warning",
|
||||
});
|
||||
}
|
||||
|
||||
async revoke(user: UserType) {
|
||||
const confirmed = await this.revokeUserConfirmationDialog(user);
|
||||
|
||||
if (!confirmed) {
|
||||
return false;
|
||||
}
|
||||
|
||||
this.actionPromise = this.revokeUser(user.id);
|
||||
try {
|
||||
await this.actionPromise;
|
||||
this.toastService.showToast({
|
||||
variant: "success",
|
||||
title: null,
|
||||
message: this.i18nService.t("revokedUserId", this.userNamePipe.transform(user)),
|
||||
});
|
||||
await this.load();
|
||||
} catch (e) {
|
||||
this.validationService.showError(e);
|
||||
}
|
||||
this.actionPromise = null;
|
||||
}
|
||||
|
||||
async restore(user: UserType) {
|
||||
this.actionPromise = this.restoreUser(user.id);
|
||||
try {
|
||||
await this.actionPromise;
|
||||
this.toastService.showToast({
|
||||
variant: "success",
|
||||
title: null,
|
||||
message: this.i18nService.t("restoredUserId", this.userNamePipe.transform(user)),
|
||||
});
|
||||
await this.load();
|
||||
} catch (e) {
|
||||
this.validationService.showError(e);
|
||||
}
|
||||
this.actionPromise = null;
|
||||
}
|
||||
|
||||
async reinvite(user: UserType) {
|
||||
if (this.actionPromise != null) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.actionPromise = this.reinviteUser(user.id);
|
||||
try {
|
||||
await this.actionPromise;
|
||||
this.toastService.showToast({
|
||||
variant: "success",
|
||||
title: null,
|
||||
message: this.i18nService.t("hasBeenReinvited", this.userNamePipe.transform(user)),
|
||||
});
|
||||
} catch (e) {
|
||||
this.validationService.showError(e);
|
||||
}
|
||||
this.actionPromise = null;
|
||||
}
|
||||
|
||||
async confirm(user: UserType) {
|
||||
function updateUser(self: BasePeopleComponent<UserType>) {
|
||||
user.status = self.userStatusType.Confirmed;
|
||||
const mapIndex = self.statusMap.get(self.userStatusType.Accepted).indexOf(user);
|
||||
if (mapIndex > -1) {
|
||||
self.statusMap.get(self.userStatusType.Accepted).splice(mapIndex, 1);
|
||||
self.statusMap.get(self.userStatusType.Confirmed).push(user);
|
||||
}
|
||||
}
|
||||
|
||||
const confirmUser = async (publicKey: Uint8Array) => {
|
||||
try {
|
||||
this.actionPromise = this.confirmUser(user, publicKey);
|
||||
await this.actionPromise;
|
||||
updateUser(this);
|
||||
this.toastService.showToast({
|
||||
variant: "success",
|
||||
title: null,
|
||||
message: this.i18nService.t("hasBeenConfirmed", this.userNamePipe.transform(user)),
|
||||
});
|
||||
} catch (e) {
|
||||
this.validationService.showError(e);
|
||||
throw e;
|
||||
} finally {
|
||||
this.actionPromise = null;
|
||||
}
|
||||
};
|
||||
|
||||
if (this.actionPromise != null) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const publicKeyResponse = await this.apiService.getUserPublicKey(user.userId);
|
||||
const publicKey = Utils.fromB64ToArray(publicKeyResponse.publicKey);
|
||||
|
||||
const autoConfirm = await firstValueFrom(
|
||||
this.organizationManagementPreferencesService.autoConfirmFingerPrints.state$,
|
||||
);
|
||||
if (autoConfirm == null || !autoConfirm) {
|
||||
const dialogRef = UserConfirmComponent.open(this.dialogService, {
|
||||
data: {
|
||||
name: this.userNamePipe.transform(user),
|
||||
userId: user != null ? user.userId : null,
|
||||
publicKey: publicKey,
|
||||
confirmUser: () => confirmUser(publicKey),
|
||||
},
|
||||
});
|
||||
await lastValueFrom(dialogRef.closed);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const fingerprint = await this.keyService.getFingerprint(user.userId, publicKey);
|
||||
this.logService.info(`User's fingerprint: ${fingerprint.join("-")}`);
|
||||
} catch (e) {
|
||||
this.logService.error(e);
|
||||
}
|
||||
await confirmUser(publicKey);
|
||||
} catch (e) {
|
||||
this.logService.error(`Handled exception: ${e}`);
|
||||
}
|
||||
}
|
||||
|
||||
protected revokeWarningMessage(): string {
|
||||
return this.i18nService.t("revokeUserConfirmation");
|
||||
}
|
||||
|
||||
protected getCheckedUsers() {
|
||||
return this.users.filter((u) => (u as any).checked);
|
||||
}
|
||||
|
||||
protected removeUser(user: UserType) {
|
||||
let index = this.users.indexOf(user);
|
||||
if (index > -1) {
|
||||
this.users.splice(index, 1);
|
||||
this.resetPaging();
|
||||
}
|
||||
|
||||
index = this.allUsers.indexOf(user);
|
||||
if (index > -1) {
|
||||
this.allUsers.splice(index, 1);
|
||||
}
|
||||
|
||||
if (this.statusMap.has(user.status)) {
|
||||
index = this.statusMap.get(user.status).indexOf(user);
|
||||
if (index > -1) {
|
||||
this.statusMap.get(user.status).splice(index, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,7 @@ import { Component, EventEmitter, Input, Output } from "@angular/core";
|
||||
import { ButtonModule, NoItemsModule, svgIcon } from "@bitwarden/components";
|
||||
|
||||
import { SharedModule } from "../../../shared";
|
||||
import { CollectionDialogTabType } from "../../../vault/components/collection-dialog";
|
||||
import { CollectionDialogTabType } from "../shared/components/collection-dialog";
|
||||
|
||||
const icon = svgIcon`<svg xmlns="http://www.w3.org/2000/svg" width="120" height="120" viewBox="10 -10 120 140" fill="none">
|
||||
<rect class="tw-stroke-secondary-600" width="134" height="86" x="3" y="31.485" stroke-width="6" rx="11"/>
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
import { NgModule } from "@angular/core";
|
||||
|
||||
import { SharedModule } from "../../../../shared/shared.module";
|
||||
import { PipesModule } from "../../../../vault/individual-vault/pipes/pipes.module";
|
||||
|
||||
import { CollectionNameBadgeComponent } from "./collection-name.badge.component";
|
||||
|
||||
@NgModule({
|
||||
imports: [SharedModule, PipesModule],
|
||||
declarations: [CollectionNameBadgeComponent],
|
||||
exports: [CollectionNameBadgeComponent],
|
||||
})
|
||||
export class CollectionBadgeModule {}
|
||||
@@ -4,9 +4,14 @@ import { Component, Input } from "@angular/core";
|
||||
|
||||
import { CollectionView } from "@bitwarden/admin-console/common";
|
||||
|
||||
import { SharedModule } from "../../../../shared/shared.module";
|
||||
import { GetCollectionNameFromIdPipe } from "../pipes";
|
||||
|
||||
@Component({
|
||||
selector: "app-collection-badge",
|
||||
templateUrl: "collection-name-badge.component.html",
|
||||
standalone: true,
|
||||
imports: [SharedModule, GetCollectionNameFromIdPipe],
|
||||
})
|
||||
export class CollectionNameBadgeComponent {
|
||||
@Input() collectionIds: string[];
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./collection-name.badge.component";
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from "./utils";
|
||||
export * from "./collection-badge";
|
||||
@@ -5,6 +5,7 @@ import { CollectionView } from "@bitwarden/admin-console/common";
|
||||
@Pipe({
|
||||
name: "collectionNameFromId",
|
||||
pure: true,
|
||||
standalone: true,
|
||||
})
|
||||
export class GetCollectionNameFromIdPipe implements PipeTransform {
|
||||
transform(value: string, collections: CollectionView[]) {
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./get-collection-name.pipe";
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./collection-utils";
|
||||
@@ -84,7 +84,7 @@
|
||||
</ng-container>
|
||||
<small *ngIf="loading">
|
||||
<i
|
||||
class="bwi bwi-spinner bwi-spin text-muted"
|
||||
class="bwi bwi-spinner bwi-spin tw-text-muted"
|
||||
title="{{ 'loading' | i18n }}"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
|
||||
@@ -28,11 +28,11 @@ import {
|
||||
|
||||
import { HeaderModule } from "../../../../layouts/header/header.module";
|
||||
import { SharedModule } from "../../../../shared";
|
||||
import { CollectionDialogTabType } from "../../../../vault/components/collection-dialog";
|
||||
import {
|
||||
All,
|
||||
RoutedVaultFilterModel,
|
||||
} from "../../../../vault/individual-vault/vault-filter/shared/models/routed-vault-filter.model";
|
||||
import { CollectionDialogTabType } from "../../shared/components/collection-dialog";
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
|
||||
@@ -45,22 +45,16 @@
|
||||
(searchTextChanged)="filterSearchText($event)"
|
||||
></app-org-vault-header>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-3" *ngIf="!hideVaultFilters">
|
||||
<div class="groupings">
|
||||
<div class="content">
|
||||
<div class="inner-content">
|
||||
<app-organization-vault-filter
|
||||
[organization]="organization"
|
||||
[activeFilter]="activeFilter"
|
||||
[searchText]="currentSearchText$ | async"
|
||||
(searchTextChanged)="filterSearchText($event)"
|
||||
></app-organization-vault-filter>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tw-flex tw-flex-row">
|
||||
<div class="tw-w-1/4 tw-mr-5" *ngIf="!hideVaultFilters">
|
||||
<app-organization-vault-filter
|
||||
[organization]="organization"
|
||||
[activeFilter]="activeFilter"
|
||||
[searchText]="currentSearchText$ | async"
|
||||
(searchTextChanged)="filterSearchText($event)"
|
||||
></app-organization-vault-filter>
|
||||
</div>
|
||||
<div [class]="hideVaultFilters ? 'col-12' : 'col-9'">
|
||||
<div [class]="hideVaultFilters ? 'tw-w-4/5' : 'tw-w-3/4'">
|
||||
<bit-toggle-group
|
||||
*ngIf="showAddAccessToggle && activeFilter.selectedCollectionNode"
|
||||
[selected]="addAccessStatus$ | async"
|
||||
@@ -140,7 +134,7 @@
|
||||
*ngIf="performingInitialLoad"
|
||||
>
|
||||
<i
|
||||
class="bwi bwi-spinner bwi-spin text-muted"
|
||||
class="bwi bwi-spinner bwi-spin tw-text-muted"
|
||||
title="{{ 'loading' | i18n }}"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
switchMap,
|
||||
takeUntil,
|
||||
tap,
|
||||
catchError,
|
||||
} from "rxjs/operators";
|
||||
|
||||
import {
|
||||
@@ -76,6 +77,7 @@ import {
|
||||
PasswordRepromptService,
|
||||
} from "@bitwarden/vault";
|
||||
|
||||
import { BillingNotificationService } from "../../../billing/services/billing-notification.service";
|
||||
import {
|
||||
ResellerWarning,
|
||||
ResellerWarningService,
|
||||
@@ -84,11 +86,6 @@ import { TrialFlowService } from "../../../billing/services/trial-flow.service";
|
||||
import { FreeTrial } from "../../../billing/types/free-trial";
|
||||
import { SharedModule } from "../../../shared";
|
||||
import { AssignCollectionsWebComponent } from "../../../vault/components/assign-collections";
|
||||
import {
|
||||
CollectionDialogAction,
|
||||
CollectionDialogTabType,
|
||||
openCollectionDialog,
|
||||
} from "../../../vault/components/collection-dialog";
|
||||
import {
|
||||
VaultItemDialogComponent,
|
||||
VaultItemDialogMode,
|
||||
@@ -114,15 +111,20 @@ import {
|
||||
} from "../../../vault/individual-vault/vault-filter/shared/models/routed-vault-filter.model";
|
||||
import { VaultFilter } from "../../../vault/individual-vault/vault-filter/shared/models/vault-filter.model";
|
||||
import { AdminConsoleCipherFormConfigService } from "../../../vault/org-vault/services/admin-console-cipher-form-config.service";
|
||||
import { getNestedCollectionTree } from "../../../vault/utils/collection-utils";
|
||||
import { GroupApiService, GroupView } from "../core";
|
||||
import { openEntityEventsDialog } from "../manage/entity-events.component";
|
||||
import {
|
||||
CollectionDialogAction,
|
||||
CollectionDialogTabType,
|
||||
openCollectionDialog,
|
||||
} from "../shared/components/collection-dialog";
|
||||
|
||||
import {
|
||||
BulkCollectionsDialogComponent,
|
||||
BulkCollectionsDialogResult,
|
||||
} from "./bulk-collections-dialog";
|
||||
import { CollectionAccessRestrictedComponent } from "./collection-access-restricted.component";
|
||||
import { getNestedCollectionTree } from "./utils";
|
||||
import { VaultFilterModule } from "./vault-filter/vault-filter.module";
|
||||
import { VaultHeaderComponent } from "./vault-header/vault-header.component";
|
||||
|
||||
@@ -178,6 +180,7 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||
protected freeTrial$: Observable<FreeTrial>;
|
||||
protected resellerWarning$: Observable<ResellerWarning | null>;
|
||||
protected prevCipherId: string | null = null;
|
||||
protected userId: UserId;
|
||||
/**
|
||||
* A list of collections that the user can assign items to and edit those items within.
|
||||
* @protected
|
||||
@@ -255,9 +258,12 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||
private organizationBillingService: OrganizationBillingServiceAbstraction,
|
||||
private resellerWarningService: ResellerWarningService,
|
||||
private accountService: AccountService,
|
||||
private billingNotificationService: BillingNotificationService,
|
||||
) {}
|
||||
|
||||
async ngOnInit() {
|
||||
this.userId = await firstValueFrom(getUserId(this.accountService.activeAccount$));
|
||||
|
||||
this.resellerManagedOrgAlert = await this.configService.getFeatureFlag(
|
||||
FeatureFlag.ResellerManagedOrgAlert,
|
||||
);
|
||||
@@ -401,7 +407,7 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||
ciphers = await this.cipherService.getManyFromApiForOrganization(organization.id);
|
||||
}
|
||||
|
||||
await this.searchService.indexCiphers(ciphers, organization.id);
|
||||
await this.searchService.indexCiphers(this.userId, ciphers, organization.id);
|
||||
return ciphers;
|
||||
}),
|
||||
shareReplay({ refCount: true, bufferSize: 1 }),
|
||||
@@ -445,7 +451,7 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||
collectionsToReturn = selectedCollection?.children.map((c) => c.node) ?? [];
|
||||
}
|
||||
|
||||
if (await this.searchService.isSearchable(searchText)) {
|
||||
if (await this.searchService.isSearchable(this.userId, searchText)) {
|
||||
collectionsToReturn = this.searchPipe.transform(
|
||||
collectionsToReturn,
|
||||
searchText,
|
||||
@@ -519,8 +525,13 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||
|
||||
const filterFunction = createFilterFunction(filter);
|
||||
|
||||
if (await this.searchService.isSearchable(searchText)) {
|
||||
return await this.searchService.searchCiphers(searchText, [filterFunction], ciphers);
|
||||
if (await this.searchService.isSearchable(this.userId, searchText)) {
|
||||
return await this.searchService.searchCiphers(
|
||||
this.userId,
|
||||
searchText,
|
||||
[filterFunction],
|
||||
ciphers,
|
||||
);
|
||||
}
|
||||
|
||||
return ciphers.filter(filterFunction);
|
||||
@@ -628,12 +639,18 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||
combineLatest([
|
||||
of(org),
|
||||
this.organizationApiService.getSubscription(org.id),
|
||||
this.organizationBillingService.getPaymentSource(org.id),
|
||||
from(this.organizationBillingService.getPaymentSource(org.id)).pipe(
|
||||
catchError((error: unknown) => {
|
||||
this.billingNotificationService.handleError(error);
|
||||
return of(null);
|
||||
}),
|
||||
),
|
||||
]),
|
||||
),
|
||||
map(([org, sub, paymentSource]) => {
|
||||
return this.trialFlowService.checkForOrgsWithUpcomingPaymentIssues(org, sub, paymentSource);
|
||||
}),
|
||||
map(([org, sub, paymentSource]) =>
|
||||
this.trialFlowService.checkForOrgsWithUpcomingPaymentIssues(org, sub, paymentSource),
|
||||
),
|
||||
filter((result) => result !== null),
|
||||
);
|
||||
|
||||
this.resellerWarning$ = organization$.pipe(
|
||||
@@ -1169,7 +1186,8 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||
typeI18nKey = "password";
|
||||
} else if (field === "totp") {
|
||||
aType = "TOTP";
|
||||
value = await this.totpService.getCode(cipher.login.totp);
|
||||
const totpResponse = await firstValueFrom(this.totpService.getCode$(cipher.login.totp));
|
||||
value = totpResponse?.code;
|
||||
typeI18nKey = "verificationCodeTotp";
|
||||
} else {
|
||||
this.toastService.showToast({
|
||||
|
||||
@@ -2,11 +2,11 @@ import { NgModule } from "@angular/core";
|
||||
|
||||
import { LooseComponentsModule } from "../../../shared/loose-components.module";
|
||||
import { SharedModule } from "../../../shared/shared.module";
|
||||
import { CollectionDialogModule } from "../../../vault/components/collection-dialog";
|
||||
import { OrganizationBadgeModule } from "../../../vault/individual-vault/organization-badge/organization-badge.module";
|
||||
import { ViewComponent } from "../../../vault/individual-vault/view.component";
|
||||
import { CollectionDialogComponent } from "../shared/components/collection-dialog";
|
||||
|
||||
import { CollectionBadgeModule } from "./collection-badge/collection-badge.module";
|
||||
import { CollectionNameBadgeComponent } from "./collection-badge";
|
||||
import { GroupBadgeModule } from "./group-badge/group-badge.module";
|
||||
import { VaultRoutingModule } from "./vault-routing.module";
|
||||
import { VaultComponent } from "./vault.component";
|
||||
@@ -17,9 +17,9 @@ import { VaultComponent } from "./vault.component";
|
||||
SharedModule,
|
||||
LooseComponentsModule,
|
||||
GroupBadgeModule,
|
||||
CollectionBadgeModule,
|
||||
CollectionNameBadgeComponent,
|
||||
OrganizationBadgeModule,
|
||||
CollectionDialogModule,
|
||||
CollectionDialogComponent,
|
||||
VaultComponent,
|
||||
ViewComponent,
|
||||
],
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
<div bitDialogContent>
|
||||
<div *ngIf="loading">
|
||||
<i
|
||||
class="bwi bwi-spinner bwi-spin text-muted"
|
||||
class="bwi bwi-spinner bwi-spin tw-text-muted"
|
||||
title="{{ 'loading' | i18n }}"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
|
||||
<ng-container *ngIf="loading">
|
||||
<i
|
||||
class="bwi bwi-spinner bwi-spin text-muted"
|
||||
class="bwi bwi-spinner bwi-spin tw-text-muted"
|
||||
title="{{ 'loading' | i18n }}"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
|
||||
@@ -18,7 +18,9 @@
|
||||
</td>
|
||||
<td bitCell>
|
||||
{{ item.user.email }}
|
||||
<small class="text-muted d-block" *ngIf="item.user.name">{{ item.user.name }}</small>
|
||||
<small class="tw-text-muted tw-block" *ngIf="item.user.name">{{
|
||||
item.user.name
|
||||
}}</small>
|
||||
</td>
|
||||
<td class="tw-text-danger" *ngIf="item.error" bitCell>
|
||||
{{ item.message }}
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
<div bitDialogContent>
|
||||
<ng-container *ngIf="loading">
|
||||
<i
|
||||
class="bwi bwi-spinner bwi-spin text-muted"
|
||||
class="bwi bwi-spinner bwi-spin tw-text-muted"
|
||||
title="{{ 'loading' | i18n }}"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
|
||||
@@ -48,7 +48,7 @@
|
||||
</div>
|
||||
<ng-container *ngIf="!firstLoaded">
|
||||
<i
|
||||
class="bwi bwi-spinner bwi-spin text-muted"
|
||||
class="bwi bwi-spinner bwi-spin tw-text-muted"
|
||||
title="{{ 'loading' | i18n }}"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
|
||||
@@ -663,9 +663,6 @@ export class MembersComponent extends BaseMembersComponent<OrganizationUserView>
|
||||
this.organization.id,
|
||||
filteredUsers.map((user) => user.id),
|
||||
);
|
||||
// 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
|
||||
|
||||
// Bulk Status component open
|
||||
const dialogRef = BulkStatusComponent.open(this.dialogService, {
|
||||
data: {
|
||||
|
||||
@@ -68,7 +68,11 @@
|
||||
</bit-form-control>
|
||||
<bit-form-control>
|
||||
<input type="checkbox" bitCheckbox formControlName="useSpecial" id="useSpecial" />
|
||||
<bit-label>{{ "specialCharactersLabel" | i18n }}</bit-label>
|
||||
<!-- hard-coded the special characters string because `$` indicates an i18n interpolation,
|
||||
and is handled inconsistently across browsers. Angular template syntax is used to
|
||||
ensure special characters are entity-encoded.
|
||||
-->
|
||||
<bit-label>{{ "!@#$%^&*" }}</bit-label>
|
||||
</bit-form-control>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -25,7 +25,6 @@ import { PolicyEditComponent, PolicyEditDialogResult } from "./policy-edit.compo
|
||||
selector: "app-org-policies",
|
||||
templateUrl: "policies.component.html",
|
||||
})
|
||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
|
||||
export class PoliciesComponent implements OnInit {
|
||||
@ViewChild("editTemplate", { read: ViewContainerRef, static: true })
|
||||
editModalRef: ViewContainerRef;
|
||||
|
||||
@@ -31,7 +31,6 @@ import { TwoFactorVerifyComponent } from "../../../auth/settings/two-factor/two-
|
||||
selector: "app-two-factor-setup",
|
||||
templateUrl: "../../../auth/settings/two-factor/two-factor-setup.component.html",
|
||||
})
|
||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
|
||||
export class TwoFactorSetupComponent extends BaseTwoFactorSetupComponent implements OnInit {
|
||||
tabbedHeader = false;
|
||||
constructor(
|
||||
|
||||
@@ -124,7 +124,7 @@
|
||||
buttonType="primary"
|
||||
[disabled]="loading || dialogReadonly"
|
||||
>
|
||||
{{ "save" | i18n }}
|
||||
{{ buttonDisplayName | i18n }}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
@@ -24,6 +24,8 @@ import {
|
||||
OrganizationUserUserMiniResponse,
|
||||
CollectionResponse,
|
||||
CollectionView,
|
||||
CollectionService,
|
||||
Collection,
|
||||
} from "@bitwarden/admin-console/common";
|
||||
import {
|
||||
getOrganizationById,
|
||||
@@ -32,13 +34,18 @@ import {
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { BitValidators, DialogService, ToastService } from "@bitwarden/components";
|
||||
import { SelectModule, BitValidators, DialogService, ToastService } from "@bitwarden/components";
|
||||
|
||||
import { GroupApiService, GroupView } from "../../../admin-console/organizations/core";
|
||||
import { PermissionMode } from "../../../admin-console/organizations/shared/components/access-selector/access-selector.component";
|
||||
import { openChangePlanDialog } from "../../../../../billing/organizations/change-plan-dialog.component";
|
||||
import { SharedModule } from "../../../../../shared";
|
||||
import { GroupApiService, GroupView } from "../../../core";
|
||||
import { freeOrgCollectionLimitValidator } from "../../validators/free-org-collection-limit.validator";
|
||||
import { PermissionMode } from "../access-selector/access-selector.component";
|
||||
import {
|
||||
AccessItemType,
|
||||
AccessItemValue,
|
||||
@@ -46,13 +53,27 @@ import {
|
||||
CollectionPermission,
|
||||
convertToPermission,
|
||||
convertToSelectionView,
|
||||
} from "../../../admin-console/organizations/shared/components/access-selector/access-selector.models";
|
||||
} from "../access-selector/access-selector.models";
|
||||
import { AccessSelectorModule } from "../access-selector/access-selector.module";
|
||||
|
||||
export enum CollectionDialogTabType {
|
||||
Info = 0,
|
||||
Access = 1,
|
||||
}
|
||||
|
||||
/**
|
||||
* Enum representing button labels for the "Add New Collection" dialog.
|
||||
*
|
||||
* @readonly
|
||||
* @enum {string}
|
||||
*/
|
||||
enum ButtonType {
|
||||
/** Displayed when the user has reached the maximum number of collections allowed for the organization. */
|
||||
Upgrade = "upgrade",
|
||||
/** Displayed when the user can still add more collections within the allowed limit. */
|
||||
Save = "save",
|
||||
}
|
||||
|
||||
export interface CollectionDialogParams {
|
||||
collectionId?: string;
|
||||
organizationId: string;
|
||||
@@ -76,10 +97,13 @@ export enum CollectionDialogAction {
|
||||
Saved = "saved",
|
||||
Canceled = "canceled",
|
||||
Deleted = "deleted",
|
||||
Upgrade = "upgrade",
|
||||
}
|
||||
|
||||
@Component({
|
||||
templateUrl: "collection-dialog.component.html",
|
||||
standalone: true,
|
||||
imports: [SharedModule, AccessSelectorModule, SelectModule],
|
||||
})
|
||||
export class CollectionDialogComponent implements OnInit, OnDestroy {
|
||||
private destroy$ = new Subject<void>();
|
||||
@@ -103,6 +127,9 @@ export class CollectionDialogComponent implements OnInit, OnDestroy {
|
||||
protected PermissionMode = PermissionMode;
|
||||
protected showDeleteButton = false;
|
||||
protected showAddAccessWarning = false;
|
||||
protected collections: Collection[];
|
||||
protected buttonDisplayName: ButtonType = ButtonType.Save;
|
||||
private orgExceedingCollectionLimit!: Organization;
|
||||
|
||||
constructor(
|
||||
@Inject(DIALOG_DATA) private params: CollectionDialogParams,
|
||||
@@ -118,6 +145,8 @@ export class CollectionDialogComponent implements OnInit, OnDestroy {
|
||||
private changeDetectorRef: ChangeDetectorRef,
|
||||
private accountService: AccountService,
|
||||
private toastService: ToastService,
|
||||
private collectionService: CollectionService,
|
||||
private configService: ConfigService,
|
||||
) {
|
||||
this.tabIndex = params.initialTab ?? CollectionDialogTabType.Info;
|
||||
}
|
||||
@@ -147,6 +176,23 @@ export class CollectionDialogComponent implements OnInit, OnDestroy {
|
||||
this.formGroup.patchValue({ selectedOrg: this.params.organizationId });
|
||||
await this.loadOrg(this.params.organizationId);
|
||||
}
|
||||
|
||||
const isBreadcrumbEventLogsEnabled = await firstValueFrom(
|
||||
this.configService.getFeatureFlag$(FeatureFlag.PM12276_BreadcrumbEventLogs),
|
||||
);
|
||||
|
||||
if (isBreadcrumbEventLogsEnabled) {
|
||||
this.collections = await this.collectionService.getAll();
|
||||
this.organizationSelected.setAsyncValidators(
|
||||
freeOrgCollectionLimitValidator(this.organizations$, this.collections, this.i18nService),
|
||||
);
|
||||
this.formGroup.updateValueAndValidity();
|
||||
}
|
||||
|
||||
this.organizationSelected.valueChanges.pipe(takeUntil(this.destroy$)).subscribe((_) => {
|
||||
this.organizationSelected.markAsTouched();
|
||||
this.formGroup.updateValueAndValidity();
|
||||
});
|
||||
}
|
||||
|
||||
async loadOrg(orgId: string) {
|
||||
@@ -259,6 +305,10 @@ export class CollectionDialogComponent implements OnInit, OnDestroy {
|
||||
});
|
||||
}
|
||||
|
||||
get organizationSelected() {
|
||||
return this.formGroup.controls.selectedOrg;
|
||||
}
|
||||
|
||||
protected get collectionId() {
|
||||
return this.params.collectionId;
|
||||
}
|
||||
@@ -283,13 +333,18 @@ export class CollectionDialogComponent implements OnInit, OnDestroy {
|
||||
|
||||
this.formGroup.markAllAsTouched();
|
||||
|
||||
if (this.buttonDisplayName == ButtonType.Upgrade) {
|
||||
this.close(CollectionDialogAction.Upgrade);
|
||||
this.changePlan(this.orgExceedingCollectionLimit);
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.formGroup.invalid) {
|
||||
const accessTabError = this.formGroup.controls.access.hasError("managePermissionRequired");
|
||||
|
||||
if (this.tabIndex === CollectionDialogTabType.Access && !accessTabError) {
|
||||
this.toastService.showToast({
|
||||
variant: "error",
|
||||
title: null,
|
||||
message: this.i18nService.t(
|
||||
"fieldOnTabRequiresAttention",
|
||||
this.i18nService.t("collectionInfo"),
|
||||
@@ -298,7 +353,6 @@ export class CollectionDialogComponent implements OnInit, OnDestroy {
|
||||
} else if (this.tabIndex === CollectionDialogTabType.Info && accessTabError) {
|
||||
this.toastService.showToast({
|
||||
variant: "error",
|
||||
title: null,
|
||||
message: this.i18nService.t("fieldOnTabRequiresAttention", this.i18nService.t("access")),
|
||||
});
|
||||
}
|
||||
@@ -327,7 +381,6 @@ export class CollectionDialogComponent implements OnInit, OnDestroy {
|
||||
|
||||
this.toastService.showToast({
|
||||
variant: "success",
|
||||
title: null,
|
||||
message: this.i18nService.t(
|
||||
this.editMode ? "editedCollectionId" : "createdCollectionId",
|
||||
collectionView.name,
|
||||
@@ -357,7 +410,6 @@ export class CollectionDialogComponent implements OnInit, OnDestroy {
|
||||
|
||||
this.toastService.showToast({
|
||||
variant: "success",
|
||||
title: null,
|
||||
message: this.i18nService.t("deletedCollectionId", this.collection?.name),
|
||||
});
|
||||
|
||||
@@ -369,6 +421,16 @@ export class CollectionDialogComponent implements OnInit, OnDestroy {
|
||||
this.destroy$.complete();
|
||||
}
|
||||
|
||||
private changePlan(org: Organization) {
|
||||
openChangePlanDialog(this.dialogService, {
|
||||
data: {
|
||||
organizationId: org.id,
|
||||
subscription: null,
|
||||
productTierType: org.productTierType,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private handleAddAccessWarning(): boolean {
|
||||
if (
|
||||
!this.organization?.allowAdminAccessToAllCollectionItems &&
|
||||
@@ -493,10 +555,7 @@ function mapUserToAccessItemView(
|
||||
*/
|
||||
export function openCollectionDialog(
|
||||
dialogService: DialogService,
|
||||
config: DialogConfig<CollectionDialogParams>,
|
||||
config: DialogConfig<CollectionDialogParams, DialogRef<CollectionDialogResult>>,
|
||||
) {
|
||||
return dialogService.open<CollectionDialogResult, CollectionDialogParams>(
|
||||
CollectionDialogComponent,
|
||||
config,
|
||||
);
|
||||
return dialogService.open(CollectionDialogComponent, config);
|
||||
}
|
||||
@@ -1,2 +1 @@
|
||||
export * from "./collection-dialog.component";
|
||||
export * from "./collection-dialog.module";
|
||||
@@ -61,11 +61,10 @@ export class IntegrationCardComponent implements AfterViewInit, OnDestroy {
|
||||
if (theme === ThemeType.System) {
|
||||
// When the user's preference is the system theme,
|
||||
// use the system theme to determine the image
|
||||
const prefersDarkMode =
|
||||
systemTheme === ThemeType.Dark || systemTheme === ThemeType.SolarizedDark;
|
||||
const prefersDarkMode = systemTheme === ThemeType.Dark;
|
||||
|
||||
this.imageEle.nativeElement.src = prefersDarkMode ? this.imageDarkMode : this.image;
|
||||
} else if (theme === ThemeType.Dark || theme === ThemeType.SolarizedDark) {
|
||||
} else if (theme === ThemeType.Dark) {
|
||||
// When the user's preference is dark mode, use the dark mode image
|
||||
this.imageEle.nativeElement.src = this.imageDarkMode;
|
||||
} else {
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
import { AbstractControl, FormControl, ValidationErrors } from "@angular/forms";
|
||||
import { lastValueFrom, Observable, of } from "rxjs";
|
||||
|
||||
import { Collection } from "@bitwarden/admin-console/common";
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
|
||||
import { freeOrgCollectionLimitValidator } from "./free-org-collection-limit.validator";
|
||||
|
||||
describe("freeOrgCollectionLimitValidator", () => {
|
||||
let i18nService: I18nService;
|
||||
|
||||
beforeEach(() => {
|
||||
i18nService = {
|
||||
t: (key: string) => key,
|
||||
} as any;
|
||||
});
|
||||
|
||||
it("returns null if organization is not found", async () => {
|
||||
const orgs: Organization[] = [];
|
||||
const validator = freeOrgCollectionLimitValidator(of(orgs), [], i18nService);
|
||||
const control = new FormControl("org-id");
|
||||
|
||||
const result: Observable<ValidationErrors> = validator(control) as Observable<ValidationErrors>;
|
||||
|
||||
const value = await lastValueFrom(result);
|
||||
expect(value).toBeNull();
|
||||
});
|
||||
|
||||
it("returns null if control is not an instance of FormControl", async () => {
|
||||
const validator = freeOrgCollectionLimitValidator(of([]), [], i18nService);
|
||||
const control = {} as AbstractControl;
|
||||
|
||||
const result: Observable<ValidationErrors | null> = validator(
|
||||
control,
|
||||
) as Observable<ValidationErrors>;
|
||||
|
||||
const value = await lastValueFrom(result);
|
||||
expect(value).toBeNull();
|
||||
});
|
||||
|
||||
it("returns null if control is not provided", async () => {
|
||||
const validator = freeOrgCollectionLimitValidator(of([]), [], i18nService);
|
||||
|
||||
const result: Observable<ValidationErrors | null> = validator(
|
||||
undefined as any,
|
||||
) as Observable<ValidationErrors>;
|
||||
|
||||
const value = await lastValueFrom(result);
|
||||
expect(value).toBeNull();
|
||||
});
|
||||
|
||||
it("returns null if organization has not reached collection limit (Observable)", async () => {
|
||||
const org = { id: "org-id", maxCollections: 2 } as Organization;
|
||||
const collections = [{ organizationId: "org-id" } as Collection];
|
||||
const validator = freeOrgCollectionLimitValidator(of([org]), collections, i18nService);
|
||||
const control = new FormControl("org-id");
|
||||
|
||||
const result$ = validator(control) as Observable<ValidationErrors | null>;
|
||||
|
||||
const value = await lastValueFrom(result$);
|
||||
expect(value).toBeNull();
|
||||
});
|
||||
|
||||
it("returns error if organization has reached collection limit (Observable)", async () => {
|
||||
const org = { id: "org-id", maxCollections: 1 } as Organization;
|
||||
const collections = [{ organizationId: "org-id" } as Collection];
|
||||
const validator = freeOrgCollectionLimitValidator(of([org]), collections, i18nService);
|
||||
const control = new FormControl("org-id");
|
||||
|
||||
const result$ = validator(control) as Observable<ValidationErrors | null>;
|
||||
|
||||
const value = await lastValueFrom(result$);
|
||||
expect(value).toEqual({
|
||||
cannotCreateCollections: { message: "cannotCreateCollection" },
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,44 @@
|
||||
import { AbstractControl, AsyncValidatorFn, FormControl, ValidationErrors } from "@angular/forms";
|
||||
import { map, Observable, of } from "rxjs";
|
||||
|
||||
import { Collection } from "@bitwarden/admin-console/common";
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
|
||||
export function freeOrgCollectionLimitValidator(
|
||||
orgs: Observable<Organization[]>,
|
||||
collections: Collection[],
|
||||
i18nService: I18nService,
|
||||
): AsyncValidatorFn {
|
||||
return (control: AbstractControl): Observable<ValidationErrors | null> => {
|
||||
if (!(control instanceof FormControl)) {
|
||||
return of(null);
|
||||
}
|
||||
|
||||
const orgId = control.value;
|
||||
|
||||
if (!orgId) {
|
||||
return of(null);
|
||||
}
|
||||
|
||||
return orgs.pipe(
|
||||
map((organizations) => organizations.find((org) => org.id === orgId)),
|
||||
map((org) => {
|
||||
if (!org) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const orgCollections = collections.filter((c) => c.organizationId === org.id);
|
||||
const hasReachedLimit = org.maxCollections === orgCollections.length;
|
||||
|
||||
if (hasReachedLimit) {
|
||||
return {
|
||||
cannotCreateCollections: { message: i18nService.t("cannotCreateCollection") },
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
}),
|
||||
);
|
||||
};
|
||||
}
|
||||
@@ -1,13 +1,13 @@
|
||||
<div class="mt-5 d-flex justify-content-center" *ngIf="loading">
|
||||
<div class="tw-mt-10 tw-flex tw-justify-center" *ngIf="loading">
|
||||
<div>
|
||||
<img class="mb-4 logo logo-themed" alt="Bitwarden" />
|
||||
<p class="text-center">
|
||||
<bit-icon class="tw-w-72 tw-block tw-mb-4" [icon]="logo"></bit-icon>
|
||||
<div class="tw-flex tw-justify-center">
|
||||
<i
|
||||
class="bwi bwi-spinner bwi-spin bwi-2x text-muted"
|
||||
class="bwi bwi-spinner bwi-spin bwi-2x tw-text-muted"
|
||||
title="{{ 'loading' | i18n }}"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
<span class="tw-sr-only">{{ "loading" | i18n }}</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { Component, inject } from "@angular/core";
|
||||
import { Params } from "@angular/router";
|
||||
|
||||
import { BitwardenLogo } from "@bitwarden/auth/angular";
|
||||
import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction";
|
||||
import { OrganizationSponsorshipResponse } from "@bitwarden/common/admin-console/models/response/organization-sponsorship.response";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
@@ -20,6 +21,7 @@ import { BaseAcceptComponent } from "../../../common/base.accept.component";
|
||||
templateUrl: "accept-family-sponsorship.component.html",
|
||||
})
|
||||
export class AcceptFamilySponsorshipComponent extends BaseAcceptComponent {
|
||||
protected logo = BitwardenLogo;
|
||||
failedShortMessage = "inviteAcceptFailedShort";
|
||||
failedMessage = "inviteAcceptFailed";
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
<div *ngIf="showNewOrganization">
|
||||
<app-organization-plans></app-organization-plans>
|
||||
</div>
|
||||
<div class="w-1/2" *ngIf="!showNewOrganization">
|
||||
<div class="tw-w-1/2" *ngIf="!showNewOrganization">
|
||||
<button bitButton buttonType="primary" bitFormButton type="submit">
|
||||
{{ "acceptOffer" | i18n }}
|
||||
</button>
|
||||
|
||||
@@ -2,11 +2,13 @@
|
||||
// @ts-strict-ignore
|
||||
import { DOCUMENT } from "@angular/common";
|
||||
import { Component, Inject, NgZone, OnDestroy, OnInit } from "@angular/core";
|
||||
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
|
||||
import { NavigationEnd, Router } from "@angular/router";
|
||||
import * as jq from "jquery";
|
||||
import { Subject, filter, firstValueFrom, map, takeUntil, timeout } from "rxjs";
|
||||
|
||||
import { CollectionService } from "@bitwarden/admin-console/common";
|
||||
import { DeviceTrustToastService } from "@bitwarden/angular/auth/services/device-trust-toast.service.abstraction";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { EventUploadService } from "@bitwarden/common/abstractions/event/event-upload.service";
|
||||
import { SearchService } from "@bitwarden/common/abstractions/search.service";
|
||||
@@ -14,10 +16,10 @@ import { InternalOrganizationServiceAbstraction } from "@bitwarden/common/admin-
|
||||
import { InternalPolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
|
||||
import { KeyConnectorService } from "@bitwarden/common/auth/abstractions/key-connector.service";
|
||||
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import { ProcessReloadServiceAbstraction } from "@bitwarden/common/key-management/abstractions/process-reload.service";
|
||||
import { KeyConnectorService } from "@bitwarden/common/key-management/key-connector/abstractions/key-connector.service";
|
||||
import { VaultTimeoutService } from "@bitwarden/common/key-management/vault-timeout";
|
||||
import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service";
|
||||
import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service";
|
||||
@@ -95,7 +97,10 @@ export class AppComponent implements OnDestroy, OnInit {
|
||||
private apiService: ApiService,
|
||||
private appIdService: AppIdService,
|
||||
private processReloadService: ProcessReloadServiceAbstraction,
|
||||
) {}
|
||||
private deviceTrustToastService: DeviceTrustToastService,
|
||||
) {
|
||||
this.deviceTrustToastService.setupListeners$.pipe(takeUntilDestroyed()).subscribe();
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.i18nService.locale$.pipe(takeUntil(this.destroy$)).subscribe((locale) => {
|
||||
@@ -173,8 +178,6 @@ export class AppComponent implements OnDestroy, OnInit {
|
||||
type: "success",
|
||||
});
|
||||
if (premiumConfirmed) {
|
||||
// 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
|
||||
await this.router.navigate(["settings/subscription/premium"]);
|
||||
}
|
||||
break;
|
||||
@@ -309,7 +312,7 @@ export class AppComponent implements OnDestroy, OnInit {
|
||||
|
||||
await this.stateEventRunnerService.handleEvent("logout", userId);
|
||||
|
||||
await this.searchService.clearIndex();
|
||||
await this.searchService.clearIndex(userId);
|
||||
this.authService.logOut(async () => {
|
||||
await this.stateService.clean({ userId: userId });
|
||||
await this.accountService.clean(userId);
|
||||
@@ -359,12 +362,8 @@ export class AppComponent implements OnDestroy, OnInit {
|
||||
|
||||
private idleStateChanged() {
|
||||
if (this.isIdle) {
|
||||
// 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.notificationsService.disconnectFromInactivity();
|
||||
} else {
|
||||
// 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.notificationsService.reconnectFromActivity();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
<form [bitSubmit]="submit" [formGroup]="formGroup">
|
||||
<bit-form-field>
|
||||
<bit-label>{{ "emailAddress" | i18n }}</bit-label>
|
||||
<input
|
||||
bitInput
|
||||
appAutofocus
|
||||
inputmode="email"
|
||||
appInputVerbatim="false"
|
||||
type="email"
|
||||
formControlName="email"
|
||||
/>
|
||||
<bit-hint>{{ "enterEmailToGetHint" | i18n }}</bit-hint>
|
||||
</bit-form-field>
|
||||
<hr />
|
||||
<div class="tw-flex tw-gap-2">
|
||||
<button type="submit" bitButton bitFormButton buttonType="primary" [block]="true">
|
||||
{{ "submit" | i18n }}
|
||||
</button>
|
||||
<a bitButton buttonType="secondary" routerLink="/login" [block]="true">
|
||||
{{ "cancel" | i18n }}
|
||||
</a>
|
||||
</div>
|
||||
</form>
|
||||
@@ -1,64 +0,0 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Component, OnInit } from "@angular/core";
|
||||
import { FormBuilder, Validators } from "@angular/forms";
|
||||
import { Router } from "@angular/router";
|
||||
|
||||
import { HintComponent as BaseHintComponent } from "@bitwarden/angular/auth/components/hint.component";
|
||||
import { LoginEmailServiceAbstraction } from "@bitwarden/auth/common";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { ToastService } from "@bitwarden/components";
|
||||
|
||||
@Component({
|
||||
selector: "app-hint",
|
||||
templateUrl: "hint.component.html",
|
||||
})
|
||||
export class HintComponent extends BaseHintComponent implements OnInit {
|
||||
formGroup = this.formBuilder.group({
|
||||
email: ["", [Validators.email, Validators.required]],
|
||||
});
|
||||
|
||||
get emailFormControl() {
|
||||
return this.formGroup.controls.email;
|
||||
}
|
||||
|
||||
constructor(
|
||||
router: Router,
|
||||
i18nService: I18nService,
|
||||
apiService: ApiService,
|
||||
platformUtilsService: PlatformUtilsService,
|
||||
logService: LogService,
|
||||
loginEmailService: LoginEmailServiceAbstraction,
|
||||
private formBuilder: FormBuilder,
|
||||
protected toastService: ToastService,
|
||||
) {
|
||||
super(
|
||||
router,
|
||||
i18nService,
|
||||
apiService,
|
||||
platformUtilsService,
|
||||
logService,
|
||||
loginEmailService,
|
||||
toastService,
|
||||
);
|
||||
}
|
||||
|
||||
async ngOnInit(): Promise<void> {
|
||||
await super.ngOnInit();
|
||||
this.emailFormControl.setValue(this.email);
|
||||
}
|
||||
|
||||
// Wrapper method to call super.submit() since properties (e.g., submit) cannot use super directly
|
||||
// This is because properties are assigned per type and generally don't have access to the prototype
|
||||
async superSubmit() {
|
||||
await super.submit();
|
||||
}
|
||||
|
||||
submit = async () => {
|
||||
this.email = this.emailFormControl.value;
|
||||
await this.superSubmit();
|
||||
};
|
||||
}
|
||||
@@ -1,105 +0,0 @@
|
||||
<div class="tw-container tw-mx-auto">
|
||||
<div
|
||||
class="tw-mx-auto tw-mt-5 tw-flex tw-max-w-lg tw-flex-col tw-items-center tw-justify-center tw-p-8"
|
||||
>
|
||||
<div class="tw-mb-6">
|
||||
<img class="logo logo-themed" alt="Bitwarden" />
|
||||
</div>
|
||||
|
||||
<ng-container *ngIf="loading">
|
||||
<p class="text-center">
|
||||
<i
|
||||
class="bwi bwi-spinner bwi-spin bwi-2x text-muted"
|
||||
title="{{ 'loading' | i18n }}"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
<span class="tw-sr-only">{{ "loading" | i18n }}</span>
|
||||
</p>
|
||||
</ng-container>
|
||||
|
||||
<div
|
||||
*ngIf="!loading"
|
||||
class="tw-w-full tw-rounded-md tw-border tw-border-solid tw-border-secondary-300 tw-bg-background tw-p-6"
|
||||
>
|
||||
<ng-container *ngIf="data.state == State.ExistingUserUntrustedDevice">
|
||||
<h2 bitTypography="h2" class="tw-mb-6">{{ "loginInitiated" | i18n }}</h2>
|
||||
|
||||
<p bitTypography="body1" class="tw-mb-6">
|
||||
{{ "deviceApprovalRequired" | i18n }}
|
||||
</p>
|
||||
|
||||
<form [formGroup]="rememberDeviceForm">
|
||||
<bit-form-control>
|
||||
<input type="checkbox" bitCheckbox formControlName="rememberDevice" />
|
||||
<bit-label>{{ "rememberThisDevice" | i18n }} </bit-label>
|
||||
<bit-hint bitTypography="body2">{{ "uncheckIfPublicDevice" | i18n }}</bit-hint>
|
||||
</bit-form-control>
|
||||
</form>
|
||||
|
||||
<div class="tw-mb-6 tw-flex tw-flex-col tw-space-y-3">
|
||||
<button
|
||||
*ngIf="data.showApproveFromOtherDeviceBtn"
|
||||
(click)="approveFromOtherDevice()"
|
||||
bitButton
|
||||
type="button"
|
||||
buttonType="primary"
|
||||
block
|
||||
>
|
||||
{{ "approveFromYourOtherDevice" | i18n }}
|
||||
</button>
|
||||
|
||||
<button
|
||||
*ngIf="data.showReqAdminApprovalBtn"
|
||||
(click)="requestAdminApproval()"
|
||||
bitButton
|
||||
type="button"
|
||||
buttonType="secondary"
|
||||
>
|
||||
{{ "requestAdminApproval" | i18n }}
|
||||
</button>
|
||||
|
||||
<button
|
||||
*ngIf="data.showApproveWithMasterPasswordBtn"
|
||||
(click)="approveWithMasterPassword()"
|
||||
bitButton
|
||||
type="button"
|
||||
buttonType="secondary"
|
||||
block
|
||||
>
|
||||
{{ "approveWithMasterPassword" | i18n }}
|
||||
</button>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="data.state == State.NewUser">
|
||||
<h2 bitTypography="h2" class="tw-mb-6">{{ "loggedInExclamation" | i18n }}</h2>
|
||||
|
||||
<form [formGroup]="rememberDeviceForm">
|
||||
<bit-form-control>
|
||||
<input type="checkbox" bitCheckbox formControlName="rememberDevice" />
|
||||
<bit-label>{{ "rememberThisDevice" | i18n }} </bit-label>
|
||||
<bit-hint bitTypography="body2">{{ "uncheckIfPublicDevice" | i18n }}</bit-hint>
|
||||
</bit-form-control>
|
||||
</form>
|
||||
|
||||
<button
|
||||
bitButton
|
||||
type="button"
|
||||
buttonType="primary"
|
||||
block
|
||||
class="tw-mb-6"
|
||||
[bitAction]="createUserAction"
|
||||
>
|
||||
{{ "continue" | i18n }}
|
||||
</button>
|
||||
</ng-container>
|
||||
|
||||
<hr class="tw-mb-6 tw-mt-0" />
|
||||
|
||||
<div class="tw-m-0 tw-text-sm">
|
||||
<p class="tw-mb-1">{{ "loggingInAs" | i18n }} {{ data.userEmail }}</p>
|
||||
<a [routerLink]="[]" (click)="logOut()">{{ "notYou" | i18n }}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,34 +0,0 @@
|
||||
import { Component, inject } from "@angular/core";
|
||||
|
||||
import { BaseLoginDecryptionOptionsComponentV1 } from "@bitwarden/angular/auth/components/base-login-decryption-options-v1.component";
|
||||
|
||||
import { RouterService } from "../../../core";
|
||||
import { AcceptOrganizationInviteService } from "../../organization-invite/accept-organization.service";
|
||||
@Component({
|
||||
selector: "web-login-decryption-options",
|
||||
templateUrl: "login-decryption-options-v1.component.html",
|
||||
})
|
||||
export class LoginDecryptionOptionsComponentV1 extends BaseLoginDecryptionOptionsComponentV1 {
|
||||
protected routerService = inject(RouterService);
|
||||
protected acceptOrganizationInviteService = inject(AcceptOrganizationInviteService);
|
||||
|
||||
override async createUser(): Promise<void> {
|
||||
try {
|
||||
await super.createUser();
|
||||
|
||||
// Invites from TDE orgs go through here, but the invite is
|
||||
// accepted while being enrolled in admin recovery. So we need to clear
|
||||
// the redirect and stored org invite.
|
||||
await this.routerService.getAndClearLoginRedirectUrl();
|
||||
await this.acceptOrganizationInviteService.clearOrganizationInvitation();
|
||||
|
||||
await this.router.navigate(["/vault"]);
|
||||
} catch (error) {
|
||||
this.validationService.showError(error);
|
||||
}
|
||||
}
|
||||
|
||||
createUserAction = async (): Promise<void> => {
|
||||
return this.createUser();
|
||||
};
|
||||
}
|
||||
@@ -1,135 +0,0 @@
|
||||
<p i18n>Updated {minutes, plural, =0 {just now} =1 {one minute ago} other {{{ 5 }} minutes ago}}</p>
|
||||
|
||||
<p i18n>This is a inline link to <a href="/settings">Settings</a> with text before and after.</p>
|
||||
|
||||
<p i18n>A phrase we really <strong>need</strong> to highlight!</p>
|
||||
|
||||
<form
|
||||
[bitSubmit]="submitForm.bind(null, false)"
|
||||
[appApiAction]="formPromise"
|
||||
[formGroup]="formGroup"
|
||||
>
|
||||
<ng-container *ngIf="!validatedEmail">
|
||||
<div class="tw-mb-3">
|
||||
<bit-form-field>
|
||||
<bit-label i18n="@@email">Email address</bit-label>
|
||||
<input bitInput type="email" formControlName="email" appAutofocus />
|
||||
</bit-form-field>
|
||||
</div>
|
||||
|
||||
<div class="tw-mb-3 tw-flex tw-items-start">
|
||||
<bit-form-control class="tw-mb-0">
|
||||
<input type="checkbox" bitCheckbox formControlName="rememberEmail" />
|
||||
<bit-label>{{ "rememberEmail" | i18n }}</bit-label>
|
||||
</bit-form-control>
|
||||
</div>
|
||||
|
||||
<div class="tw-mb-3">
|
||||
<button
|
||||
bitButton
|
||||
type="submit"
|
||||
buttonType="primary"
|
||||
class="tw-w-full"
|
||||
(click)="validateEmail()"
|
||||
>
|
||||
<span> {{ "continue" | i18n }} </span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="tw-mb-3 tw-flex tw-flex-col tw-items-center tw-justify-center">
|
||||
<p class="tw-mb-3">{{ "or" | i18n }}</p>
|
||||
|
||||
<a
|
||||
bitLink
|
||||
block
|
||||
linkType="primary"
|
||||
routerLink="/login-with-passkey"
|
||||
(mousedown)="$event.preventDefault()"
|
||||
>
|
||||
<span><i class="bwi bwi-passkey"></i> {{ "logInWithPasskey" | i18n }}</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
<p class="tw-m-0 tw-text-sm">
|
||||
{{ "newAroundHere" | i18n }}
|
||||
<!-- Two notes:
|
||||
(1) We check the value and validity of email so we don't send an invalid email to autofill
|
||||
on load of register for both enter and mouse based navigation
|
||||
(2) We use mousedown to trigger navigation so that the onBlur form validation does not fire
|
||||
and move the create account link down the page on click which causes the user to miss actually
|
||||
clicking on the link. Mousedown fires before onBlur.
|
||||
-->
|
||||
<a
|
||||
bitLink
|
||||
routerLink="/signup"
|
||||
[queryParams]="emailFormControl.valid ? { email: emailFormControl.value } : {}"
|
||||
(mousedown)="goToRegister()"
|
||||
>
|
||||
{{ "createAccount" | i18n }}
|
||||
</a>
|
||||
</p>
|
||||
</ng-container>
|
||||
|
||||
<div [ngClass]="{ 'tw-hidden': !validatedEmail }">
|
||||
<div class="tw-mb-6 tw-h-28">
|
||||
<bit-form-field class="!tw-mb-1">
|
||||
<bit-label>{{ "masterPass" | i18n }}</bit-label>
|
||||
<input type="password" bitInput #masterPasswordInput formControlName="masterPassword" />
|
||||
<button type="button" bitSuffix bitIconButton bitPasswordInputToggle></button>
|
||||
</bit-form-field>
|
||||
<a
|
||||
bitLink
|
||||
class="tw-mt-2"
|
||||
routerLink="/hint"
|
||||
(mousedown)="goToHint()"
|
||||
(click)="saveEmailSettings()"
|
||||
>{{ "getMasterPasswordHint" | i18n }}</a
|
||||
>
|
||||
</div>
|
||||
|
||||
<div [hidden]="!showCaptcha()">
|
||||
<iframe id="hcaptcha_iframe" height="80" sandbox="allow-scripts allow-same-origin"></iframe>
|
||||
</div>
|
||||
|
||||
<div class="tw-mb-3 tw-flex tw-space-x-4">
|
||||
<button bitButton buttonType="primary" bitFormButton type="submit" [block]="true">
|
||||
<span> {{ "loginWithMasterPassword" | i18n }} </span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="tw-mb-3" *ngIf="showLoginWithDevice">
|
||||
<button
|
||||
bitButton
|
||||
type="button"
|
||||
[block]="true"
|
||||
buttonType="secondary"
|
||||
(click)="startAuthRequestLogin()"
|
||||
>
|
||||
<span> <i class="bwi bwi-mobile"></i> {{ "loginWithDevice" | i18n }} </span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="tw-mb-3">
|
||||
<a
|
||||
routerLink="/sso"
|
||||
[queryParams]="{ email: formGroup.value.email }"
|
||||
(click)="saveEmailSettings()"
|
||||
bitButton
|
||||
buttonType="secondary"
|
||||
class="tw-w-full"
|
||||
>
|
||||
<i class="bwi bwi-provider tw-mr-2"></i>
|
||||
{{ "enterpriseSingleSignOn" | i18n }}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
<div class="tw-m-0 tw-text-sm">
|
||||
<p class="tw-mb-1">{{ "loggingInAs" | i18n }} {{ loggedEmail }}</p>
|
||||
<a bitLink [routerLink]="[]" (click)="toggleValidateEmail(false)">{{ "notYou" | i18n }}</a>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
@@ -1,227 +0,0 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Component, NgZone, OnInit } from "@angular/core";
|
||||
import { FormBuilder } from "@angular/forms";
|
||||
import { ActivatedRoute, Router } from "@angular/router";
|
||||
import { takeUntil } from "rxjs";
|
||||
import { first } from "rxjs/operators";
|
||||
|
||||
import { LoginComponentV1 as BaseLoginComponent } from "@bitwarden/angular/auth/components/login-v1.component";
|
||||
import { FormValidationErrorsService } from "@bitwarden/angular/platform/abstractions/form-validation-errors.service";
|
||||
import {
|
||||
LoginStrategyServiceAbstraction,
|
||||
LoginEmailServiceAbstraction,
|
||||
} from "@bitwarden/auth/common";
|
||||
import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction";
|
||||
import { InternalPolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { PolicyData } from "@bitwarden/common/admin-console/models/data/policy.data";
|
||||
import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options";
|
||||
import { Policy } from "@bitwarden/common/admin-console/models/domain/policy";
|
||||
import { DevicesApiServiceAbstraction } from "@bitwarden/common/auth/abstractions/devices-api.service.abstraction";
|
||||
import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction";
|
||||
import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result";
|
||||
import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service";
|
||||
import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service";
|
||||
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||
import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { ToastService } from "@bitwarden/components";
|
||||
import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy";
|
||||
|
||||
import { RouterService } from "../../core";
|
||||
import { AcceptOrganizationInviteService } from "../organization-invite/accept-organization.service";
|
||||
import { OrganizationInvite } from "../organization-invite/organization-invite";
|
||||
|
||||
@Component({
|
||||
selector: "app-login",
|
||||
templateUrl: "login-v1.component.html",
|
||||
})
|
||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
|
||||
export class LoginComponentV1 extends BaseLoginComponent implements OnInit {
|
||||
showResetPasswordAutoEnrollWarning = false;
|
||||
enforcedPasswordPolicyOptions: MasterPasswordPolicyOptions;
|
||||
policies: Policy[];
|
||||
|
||||
protected minutes = 5;
|
||||
|
||||
constructor(
|
||||
private acceptOrganizationInviteService: AcceptOrganizationInviteService,
|
||||
devicesApiService: DevicesApiServiceAbstraction,
|
||||
appIdService: AppIdService,
|
||||
loginStrategyService: LoginStrategyServiceAbstraction,
|
||||
router: Router,
|
||||
i18nService: I18nService,
|
||||
route: ActivatedRoute,
|
||||
platformUtilsService: PlatformUtilsService,
|
||||
environmentService: EnvironmentService,
|
||||
passwordGenerationService: PasswordGenerationServiceAbstraction,
|
||||
private passwordStrengthService: PasswordStrengthServiceAbstraction,
|
||||
cryptoFunctionService: CryptoFunctionService,
|
||||
private policyApiService: PolicyApiServiceAbstraction,
|
||||
private policyService: InternalPolicyService,
|
||||
logService: LogService,
|
||||
ngZone: NgZone,
|
||||
protected stateService: StateService,
|
||||
private routerService: RouterService,
|
||||
formBuilder: FormBuilder,
|
||||
formValidationErrorService: FormValidationErrorsService,
|
||||
loginEmailService: LoginEmailServiceAbstraction,
|
||||
ssoLoginService: SsoLoginServiceAbstraction,
|
||||
toastService: ToastService,
|
||||
) {
|
||||
super(
|
||||
devicesApiService,
|
||||
appIdService,
|
||||
loginStrategyService,
|
||||
router,
|
||||
platformUtilsService,
|
||||
i18nService,
|
||||
stateService,
|
||||
environmentService,
|
||||
passwordGenerationService,
|
||||
cryptoFunctionService,
|
||||
logService,
|
||||
ngZone,
|
||||
formBuilder,
|
||||
formValidationErrorService,
|
||||
route,
|
||||
loginEmailService,
|
||||
ssoLoginService,
|
||||
toastService,
|
||||
);
|
||||
this.onSuccessfulLoginNavigate = this.goAfterLogIn;
|
||||
}
|
||||
|
||||
submitForm = async (showToast = true) => {
|
||||
return await this.submitFormHelper(showToast);
|
||||
};
|
||||
|
||||
private async submitFormHelper(showToast: boolean) {
|
||||
await super.submit(showToast);
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe
|
||||
this.route.queryParams.pipe(first()).subscribe(async (qParams) => {
|
||||
// If there is a query parameter called 'org', set previousUrl to `/create-organization?org=paramValue`
|
||||
if (qParams.org != null) {
|
||||
const route = this.router.createUrlTree(["create-organization"], {
|
||||
queryParams: { plan: qParams.org },
|
||||
});
|
||||
this.routerService.setPreviousUrl(route.toString());
|
||||
}
|
||||
|
||||
/**
|
||||
* If there is a query parameter called 'sponsorshipToken', that means they are coming
|
||||
* from an email for sponsoring a families organization. If so, then set the prevousUrl
|
||||
* to `/setup/families-for-enterprise?token=paramValue`
|
||||
*/
|
||||
if (qParams.sponsorshipToken != null) {
|
||||
const route = this.router.createUrlTree(["setup/families-for-enterprise"], {
|
||||
queryParams: { token: qParams.sponsorshipToken },
|
||||
});
|
||||
this.routerService.setPreviousUrl(route.toString());
|
||||
}
|
||||
|
||||
await super.ngOnInit();
|
||||
});
|
||||
|
||||
// If there's an existing org invite, use it to get the password policies
|
||||
const orgInvite = await this.acceptOrganizationInviteService.getOrganizationInvite();
|
||||
if (orgInvite != null) {
|
||||
await this.initPasswordPolicies(orgInvite);
|
||||
}
|
||||
}
|
||||
|
||||
async goAfterLogIn(userId: UserId) {
|
||||
const masterPassword = this.formGroup.value.masterPassword;
|
||||
|
||||
// Check master password against policy
|
||||
if (this.enforcedPasswordPolicyOptions != null) {
|
||||
const strengthResult = this.passwordStrengthService.getPasswordStrength(
|
||||
masterPassword,
|
||||
this.formGroup.value.email,
|
||||
);
|
||||
const masterPasswordScore = strengthResult == null ? null : strengthResult.score;
|
||||
|
||||
// If invalid, save policies and require update
|
||||
if (
|
||||
!this.policyService.evaluateMasterPassword(
|
||||
masterPasswordScore,
|
||||
masterPassword,
|
||||
this.enforcedPasswordPolicyOptions,
|
||||
)
|
||||
) {
|
||||
const policiesData: { [id: string]: PolicyData } = {};
|
||||
this.policies.map((p) => (policiesData[p.id] = PolicyData.fromPolicy(p)));
|
||||
await this.policyService.replace(policiesData, userId);
|
||||
await this.router.navigate(["update-password"]);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
this.loginEmailService.clearValues();
|
||||
await this.router.navigate([this.successRoute]);
|
||||
}
|
||||
|
||||
async goToHint() {
|
||||
await this.saveEmailSettings();
|
||||
await this.router.navigateByUrl("/hint");
|
||||
}
|
||||
|
||||
async goToRegister() {
|
||||
if (this.emailFormControl.valid) {
|
||||
await this.router.navigate(["/signup"], {
|
||||
queryParams: { email: this.emailFormControl.value },
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
await this.router.navigate(["/signup"]);
|
||||
}
|
||||
|
||||
protected override async handleMigrateEncryptionKey(result: AuthResult): Promise<boolean> {
|
||||
if (!result.requiresEncryptionKeyMigration) {
|
||||
return false;
|
||||
}
|
||||
await this.router.navigate(["migrate-legacy-encryption"]);
|
||||
return true;
|
||||
}
|
||||
|
||||
private async initPasswordPolicies(invite: OrganizationInvite): Promise<void> {
|
||||
try {
|
||||
this.policies = await this.policyApiService.getPoliciesByToken(
|
||||
invite.organizationId,
|
||||
invite.token,
|
||||
invite.email,
|
||||
invite.organizationUserId,
|
||||
);
|
||||
} catch (e) {
|
||||
this.logService.error(e);
|
||||
}
|
||||
|
||||
if (this.policies == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const resetPasswordPolicy = this.policyService.getResetPasswordPolicyOptions(
|
||||
this.policies,
|
||||
invite.organizationId,
|
||||
);
|
||||
|
||||
// Set to true if policy enabled and auto-enroll enabled
|
||||
this.showResetPasswordAutoEnrollWarning =
|
||||
resetPasswordPolicy[1] && resetPasswordPolicy[0].autoEnrollEnabled;
|
||||
|
||||
this.policyService
|
||||
.masterPasswordPolicyOptions$(this.policies)
|
||||
.pipe(takeUntil(this.destroy$))
|
||||
.subscribe((enforcedPasswordPolicyOptions) => {
|
||||
this.enforcedPasswordPolicyOptions = enforcedPasswordPolicyOptions;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,70 +0,0 @@
|
||||
<div
|
||||
class="tw-mx-auto tw-mt-5 tw-flex tw-max-w-lg tw-flex-col tw-items-center tw-justify-center tw-p-8"
|
||||
>
|
||||
<div>
|
||||
<img class="logo logo-themed" alt="Bitwarden" />
|
||||
|
||||
<ng-container *ngIf="state == StateEnum.StandardAuthRequest">
|
||||
<p class="tw-mx-4 tw-mb-4 tw-mt-3 tw-text-center tw-text-xl">
|
||||
{{ "loginOrCreateNewAccount" | i18n }}
|
||||
</p>
|
||||
|
||||
<div
|
||||
class="tw-mt-3 tw-rounded-md tw-border tw-border-solid tw-border-secondary-300 tw-bg-background tw-p-6"
|
||||
>
|
||||
<h2 class="tw-mb-6 tw-text-xl tw-font-semibold">{{ "logInRequestSent" | i18n }}</h2>
|
||||
|
||||
<p class="tw-mb-6">
|
||||
{{ "notificationSentDeviceComplete" | i18n }}
|
||||
</p>
|
||||
|
||||
<div class="tw-mb-6">
|
||||
<h4 class="tw-font-semibold">{{ "fingerprintPhraseHeader" | i18n }}</h4>
|
||||
<p>
|
||||
<code>{{ fingerprintPhrase }}</code>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="tw-my-10" *ngIf="showResendNotification">
|
||||
<a [routerLink]="[]" disabled="true" (click)="startAuthRequestLogin()">{{
|
||||
"resendNotification" | i18n
|
||||
}}</a>
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
<div class="tw-mt-3">
|
||||
{{ "loginWithDeviceEnabledNote" | i18n }}
|
||||
<a routerLink="/login">{{ "viewAllLoginOptions" | i18n }}</a>
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="state == StateEnum.AdminAuthRequest">
|
||||
<div
|
||||
class="tw-mt-3 tw-rounded-md tw-border tw-border-solid tw-border-secondary-300 tw-bg-background tw-p-6"
|
||||
>
|
||||
<h2 class="tw-mb-6 tw-text-xl tw-font-semibold">{{ "adminApprovalRequested" | i18n }}</h2>
|
||||
|
||||
<div>
|
||||
<p class="tw-mb-6">{{ "adminApprovalRequestSentToAdmins" | i18n }}</p>
|
||||
<p class="tw-mb-6">{{ "youWillBeNotifiedOnceApproved" | i18n }}</p>
|
||||
</div>
|
||||
|
||||
<div class="tw-mb-6">
|
||||
<h4 class="tw-font-semibold">{{ "fingerprintPhraseHeader" | i18n }}</h4>
|
||||
<p>
|
||||
<code>{{ fingerprintPhrase }}</code>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
<div class="tw-mt-3">
|
||||
{{ "troubleLoggingIn" | i18n }}
|
||||
<a routerLink="/login-initiated">{{ "viewAllLoginOptions" | i18n }}</a>
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,9 +0,0 @@
|
||||
import { Component } from "@angular/core";
|
||||
|
||||
import { LoginViaAuthRequestComponentV1 as BaseLoginViaAuthRequestComponentV1 } from "@bitwarden/angular/auth/components/login-via-auth-request-v1.component";
|
||||
|
||||
@Component({
|
||||
selector: "app-login-via-auth-request",
|
||||
templateUrl: "login-via-auth-request-v1.component.html",
|
||||
})
|
||||
export class LoginViaAuthRequestComponentV1 extends BaseLoginViaAuthRequestComponentV1 {}
|
||||
@@ -4,24 +4,11 @@ import { CheckboxModule } from "@bitwarden/components";
|
||||
|
||||
import { SharedModule } from "../../../app/shared";
|
||||
|
||||
import { LoginDecryptionOptionsComponentV1 } from "./login-decryption-options/login-decryption-options-v1.component";
|
||||
import { LoginComponentV1 } from "./login-v1.component";
|
||||
import { LoginViaAuthRequestComponentV1 } from "./login-via-auth-request-v1.component";
|
||||
import { LoginViaWebAuthnComponent } from "./login-via-webauthn/login-via-webauthn.component";
|
||||
|
||||
@NgModule({
|
||||
imports: [SharedModule, CheckboxModule],
|
||||
declarations: [
|
||||
LoginComponentV1,
|
||||
LoginViaAuthRequestComponentV1,
|
||||
LoginDecryptionOptionsComponentV1,
|
||||
LoginViaWebAuthnComponent,
|
||||
],
|
||||
exports: [
|
||||
LoginComponentV1,
|
||||
LoginViaAuthRequestComponentV1,
|
||||
LoginDecryptionOptionsComponentV1,
|
||||
LoginViaWebAuthnComponent,
|
||||
],
|
||||
declarations: [LoginViaWebAuthnComponent],
|
||||
exports: [LoginViaWebAuthnComponent],
|
||||
})
|
||||
export class LoginModule {}
|
||||
|
||||
@@ -5,14 +5,14 @@ import { Router } from "@angular/router";
|
||||
import { firstValueFrom, map } from "rxjs";
|
||||
|
||||
import { ChangePasswordComponent as BaseChangePasswordComponent } from "@bitwarden/angular/auth/components/change-password.component";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { AuditService } from "@bitwarden/common/abstractions/audit.service";
|
||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction";
|
||||
import { MasterPasswordApiService } from "@bitwarden/common/auth/abstractions/master-password-api.service.abstraction";
|
||||
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
|
||||
import { PasswordRequest } from "@bitwarden/common/auth/models/request/password.request";
|
||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
@@ -50,7 +50,7 @@ export class ChangePasswordComponent
|
||||
private auditService: AuditService,
|
||||
private cipherService: CipherService,
|
||||
private syncService: SyncService,
|
||||
private apiService: ApiService,
|
||||
private masterPasswordApiService: MasterPasswordApiService,
|
||||
private router: Router,
|
||||
dialogService: DialogService,
|
||||
private userVerificationService: UserVerificationService,
|
||||
@@ -213,14 +213,14 @@ export class ChangePasswordComponent
|
||||
|
||||
try {
|
||||
if (this.rotateUserKey) {
|
||||
this.formPromise = this.apiService.postPassword(request).then(async () => {
|
||||
this.formPromise = this.masterPasswordApiService.postPassword(request).then(async () => {
|
||||
// we need to save this for local masterkey verification during rotation
|
||||
await this.masterPasswordService.setMasterKeyHash(newLocalKeyHash, userId as UserId);
|
||||
await this.masterPasswordService.setMasterKey(newMasterKey, userId as UserId);
|
||||
return this.updateKey();
|
||||
});
|
||||
} else {
|
||||
this.formPromise = this.apiService.postPassword(request);
|
||||
this.formPromise = this.masterPasswordApiService.postPassword(request);
|
||||
}
|
||||
|
||||
await this.formPromise;
|
||||
|
||||
@@ -43,7 +43,6 @@ import {
|
||||
selector: "emergency-access",
|
||||
templateUrl: "emergency-access.component.html",
|
||||
})
|
||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
|
||||
export class EmergencyAccessComponent implements OnInit {
|
||||
@ViewChild("addEdit", { read: ViewContainerRef, static: true }) addEditModalRef: ViewContainerRef;
|
||||
@ViewChild("takeoverTemplate", { read: ViewContainerRef, static: true })
|
||||
|
||||
@@ -8,7 +8,7 @@ import { takeUntil } from "rxjs";
|
||||
import { ChangePasswordComponent } from "@bitwarden/angular/auth/components/change-password.component";
|
||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction";
|
||||
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||
@@ -33,7 +33,6 @@ type EmergencyAccessTakeoverDialogData = {
|
||||
selector: "emergency-access-takeover",
|
||||
templateUrl: "emergency-access-takeover.component.html",
|
||||
})
|
||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
|
||||
export class EmergencyAccessTakeoverComponent
|
||||
extends ChangePasswordComponent
|
||||
implements OnInit, OnDestroy
|
||||
@@ -86,7 +85,6 @@ export class EmergencyAccessTakeoverComponent
|
||||
.subscribe((enforcedPolicyOptions) => (this.enforcedPolicyOptions = enforcedPolicyOptions));
|
||||
}
|
||||
|
||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
|
||||
ngOnDestroy(): void {
|
||||
super.ngOnDestroy();
|
||||
}
|
||||
|
||||
@@ -18,7 +18,6 @@ import { EmergencyViewDialogComponent } from "./emergency-view-dialog.component"
|
||||
templateUrl: "emergency-access-view.component.html",
|
||||
providers: [{ provide: CipherFormConfigService, useClass: DefaultCipherFormConfigService }],
|
||||
})
|
||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
|
||||
export class EmergencyAccessViewComponent implements OnInit {
|
||||
@ViewChild("attachments", { read: ViewContainerRef, static: true })
|
||||
attachmentsModalRef: ViewContainerRef;
|
||||
|
||||
@@ -9,7 +9,13 @@ import { ViewPasswordHistoryService } from "@bitwarden/common/vault/abstractions
|
||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
import { ButtonModule, DialogModule, DialogService } from "@bitwarden/components";
|
||||
import { CipherViewComponent, DefaultTaskService, TaskService } from "@bitwarden/vault";
|
||||
import {
|
||||
ChangeLoginPasswordService,
|
||||
CipherViewComponent,
|
||||
DefaultChangeLoginPasswordService,
|
||||
DefaultTaskService,
|
||||
TaskService,
|
||||
} from "@bitwarden/vault";
|
||||
|
||||
import { WebViewPasswordHistoryService } from "../../../../vault/services/web-view-password-history.service";
|
||||
|
||||
@@ -34,6 +40,7 @@ class PremiumUpgradePromptNoop implements PremiumUpgradePromptService {
|
||||
{ provide: ViewPasswordHistoryService, useClass: WebViewPasswordHistoryService },
|
||||
{ provide: PremiumUpgradePromptService, useClass: PremiumUpgradePromptNoop },
|
||||
{ provide: TaskService, useClass: DefaultTaskService },
|
||||
{ provide: ChangeLoginPasswordService, useClass: DefaultChangeLoginPasswordService },
|
||||
],
|
||||
})
|
||||
export class EmergencyViewDialogComponent {
|
||||
|
||||
@@ -180,8 +180,20 @@ export class DeviceManagementComponent {
|
||||
private updateDeviceTable(devices: Array<DeviceView>): void {
|
||||
this.dataSource.data = devices
|
||||
.map((device: DeviceView): DeviceTableData | null => {
|
||||
if (!device.id || !device.type || !device.creationDate) {
|
||||
this.validationService.showError(new Error("Invalid device data"));
|
||||
if (device.id == undefined) {
|
||||
this.validationService.showError(new Error(this.i18nService.t("deviceIdMissing")));
|
||||
return null;
|
||||
}
|
||||
|
||||
if (device.type == undefined) {
|
||||
this.validationService.showError(new Error(this.i18nService.t("deviceTypeMissing")));
|
||||
return null;
|
||||
}
|
||||
|
||||
if (device.creationDate == undefined) {
|
||||
this.validationService.showError(
|
||||
new Error(this.i18nService.t("deviceCreationDateMissing")),
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -16,10 +16,10 @@ import { OrgDomainApiServiceAbstraction } from "@bitwarden/common/admin-console/
|
||||
import { OrganizationDomainSsoDetailsResponse } from "@bitwarden/common/admin-console/abstractions/organization-domain/responses/organization-domain-sso-details.response";
|
||||
import { VerifiedOrganizationDomainSsoDetailsResponse } from "@bitwarden/common/admin-console/abstractions/organization-domain/responses/verified-organization-domain-sso-details.response";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction";
|
||||
import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction";
|
||||
import { HttpStatusCode } from "@bitwarden/common/enums";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction";
|
||||
import { ErrorResponse } from "@bitwarden/common/models/response/error.response";
|
||||
import { ListResponse } from "@bitwarden/common/models/response/list.response";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
@@ -37,7 +37,6 @@ import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legac
|
||||
selector: "app-sso",
|
||||
templateUrl: "sso-v1.component.html",
|
||||
})
|
||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
|
||||
export class SsoComponentV1 extends BaseSsoComponent implements OnInit {
|
||||
protected formGroup = new FormGroup({
|
||||
identifier: new FormControl(null, [Validators.required]),
|
||||
|
||||
@@ -14,10 +14,10 @@ import {
|
||||
} from "@bitwarden/auth/common";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction";
|
||||
import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction";
|
||||
import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service";
|
||||
import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result";
|
||||
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction";
|
||||
import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||
@@ -37,7 +37,6 @@ import {
|
||||
selector: "app-two-factor",
|
||||
templateUrl: "two-factor-v1.component.html",
|
||||
})
|
||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
|
||||
export class TwoFactorComponentV1 extends BaseTwoFactorComponent implements OnInit, OnDestroy {
|
||||
@ViewChild("twoFactorOptions", { read: ViewContainerRef, static: true })
|
||||
twoFactorOptionsModal: ViewContainerRef;
|
||||
|
||||
@@ -16,7 +16,6 @@ import { ToastService } from "@bitwarden/components";
|
||||
selector: "app-verify-email-token",
|
||||
templateUrl: "verify-email-token.component.html",
|
||||
})
|
||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
|
||||
export class VerifyEmailTokenComponent implements OnInit {
|
||||
constructor(
|
||||
private router: Router,
|
||||
|
||||
@@ -15,7 +15,6 @@ import { ToastService } from "@bitwarden/components";
|
||||
selector: "app-verify-recover-delete",
|
||||
templateUrl: "verify-recover-delete.component.html",
|
||||
})
|
||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
|
||||
export class VerifyRecoverDeleteComponent implements OnInit {
|
||||
email: string;
|
||||
|
||||
|
||||
@@ -58,6 +58,7 @@ import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.serv
|
||||
import { DialogService, ToastService } from "@bitwarden/components";
|
||||
import { KeyService } from "@bitwarden/key-management";
|
||||
|
||||
import { BillingNotificationService } from "../services/billing-notification.service";
|
||||
import { BillingSharedModule } from "../shared/billing-shared.module";
|
||||
import { PaymentComponent } from "../shared/payment/payment.component";
|
||||
|
||||
@@ -208,6 +209,7 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy {
|
||||
private taxService: TaxServiceAbstraction,
|
||||
private accountService: AccountService,
|
||||
private organizationBillingService: OrganizationBillingService,
|
||||
private billingNotificationService: BillingNotificationService,
|
||||
) {}
|
||||
|
||||
async ngOnInit(): Promise<void> {
|
||||
@@ -228,10 +230,14 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy {
|
||||
.organizations$(userId)
|
||||
.pipe(getOrganizationById(this.organizationId)),
|
||||
);
|
||||
const { accountCredit, paymentSource } =
|
||||
await this.billingApiService.getOrganizationPaymentMethod(this.organizationId);
|
||||
this.accountCredit = accountCredit;
|
||||
this.paymentSource = paymentSource;
|
||||
try {
|
||||
const { accountCredit, paymentSource } =
|
||||
await this.billingApiService.getOrganizationPaymentMethod(this.organizationId);
|
||||
this.accountCredit = accountCredit;
|
||||
this.paymentSource = paymentSource;
|
||||
} catch (error) {
|
||||
this.billingNotificationService.handleError(error);
|
||||
}
|
||||
}
|
||||
|
||||
if (!this.selfHosted) {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
<bit-container>
|
||||
<ng-container *ngIf="!firstLoaded && loading">
|
||||
<i class="bwi bwi-spinner bwi-spin text-muted" title="{{ 'loading' | i18n }}"></i>
|
||||
<i class="bwi bwi-spinner bwi-spin tw-text-muted" title="{{ 'loading' | i18n }}"></i>
|
||||
<span class="tw-sr-only">{{ "loading" | i18n }}</span>
|
||||
</ng-container>
|
||||
|
||||
@@ -66,7 +66,7 @@
|
||||
</a>
|
||||
<form [formGroup]="form">
|
||||
<bit-radio-group formControlName="updateMethod" [block]="true">
|
||||
<h2 class="mt-5">
|
||||
<h2 class="tw-mt-5">
|
||||
{{ "licenseAndBillingManagement" | i18n }}
|
||||
</h2>
|
||||
<bit-radio-button
|
||||
|
||||
@@ -23,6 +23,7 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl
|
||||
import { SyncService } from "@bitwarden/common/platform/sync";
|
||||
import { DialogService, ToastService } from "@bitwarden/components";
|
||||
|
||||
import { BillingNotificationService } from "../../services/billing-notification.service";
|
||||
import { TrialFlowService } from "../../services/trial-flow.service";
|
||||
import {
|
||||
AddCreditDialogResult,
|
||||
@@ -66,6 +67,7 @@ export class OrganizationPaymentMethodComponent implements OnDestroy {
|
||||
private organizationService: OrganizationService,
|
||||
private accountService: AccountService,
|
||||
protected syncService: SyncService,
|
||||
private billingNotificationService: BillingNotificationService,
|
||||
) {
|
||||
this.activatedRoute.params
|
||||
.pipe(
|
||||
@@ -115,47 +117,52 @@ export class OrganizationPaymentMethodComponent implements OnDestroy {
|
||||
|
||||
protected load = async (): Promise<void> => {
|
||||
this.loading = true;
|
||||
const { accountCredit, paymentSource, subscriptionStatus } =
|
||||
await this.billingApiService.getOrganizationPaymentMethod(this.organizationId);
|
||||
this.accountCredit = accountCredit;
|
||||
this.paymentSource = paymentSource;
|
||||
this.subscriptionStatus = subscriptionStatus;
|
||||
try {
|
||||
const { accountCredit, paymentSource, subscriptionStatus } =
|
||||
await this.billingApiService.getOrganizationPaymentMethod(this.organizationId);
|
||||
this.accountCredit = accountCredit;
|
||||
this.paymentSource = paymentSource;
|
||||
this.subscriptionStatus = subscriptionStatus;
|
||||
|
||||
if (this.organizationId) {
|
||||
const organizationSubscriptionPromise = this.organizationApiService.getSubscription(
|
||||
this.organizationId,
|
||||
);
|
||||
const userId = await firstValueFrom(
|
||||
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
|
||||
);
|
||||
const organizationPromise = await firstValueFrom(
|
||||
this.organizationService
|
||||
.organizations$(userId)
|
||||
.pipe(getOrganizationById(this.organizationId)),
|
||||
);
|
||||
if (this.organizationId) {
|
||||
const organizationSubscriptionPromise = this.organizationApiService.getSubscription(
|
||||
this.organizationId,
|
||||
);
|
||||
const userId = await firstValueFrom(
|
||||
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
|
||||
);
|
||||
const organizationPromise = await firstValueFrom(
|
||||
this.organizationService
|
||||
.organizations$(userId)
|
||||
.pipe(getOrganizationById(this.organizationId)),
|
||||
);
|
||||
|
||||
[this.organizationSubscriptionResponse, this.organization] = await Promise.all([
|
||||
organizationSubscriptionPromise,
|
||||
organizationPromise,
|
||||
]);
|
||||
this.freeTrialData = this.trialFlowService.checkForOrgsWithUpcomingPaymentIssues(
|
||||
this.organization,
|
||||
this.organizationSubscriptionResponse,
|
||||
paymentSource,
|
||||
);
|
||||
[this.organizationSubscriptionResponse, this.organization] = await Promise.all([
|
||||
organizationSubscriptionPromise,
|
||||
organizationPromise,
|
||||
]);
|
||||
this.freeTrialData = this.trialFlowService.checkForOrgsWithUpcomingPaymentIssues(
|
||||
this.organization,
|
||||
this.organizationSubscriptionResponse,
|
||||
paymentSource,
|
||||
);
|
||||
}
|
||||
this.isUnpaid = this.subscriptionStatus === "unpaid" ?? false;
|
||||
// If the flag `launchPaymentModalAutomatically` is set to true,
|
||||
// we schedule a timeout (delay of 800ms) to automatically launch the payment modal.
|
||||
// This delay ensures that any prior UI/rendering operations complete before triggering the modal.
|
||||
if (this.launchPaymentModalAutomatically) {
|
||||
window.setTimeout(async () => {
|
||||
await this.changePayment();
|
||||
this.launchPaymentModalAutomatically = false;
|
||||
this.location.replaceState(this.location.path(), "", {});
|
||||
}, 800);
|
||||
}
|
||||
} catch (error) {
|
||||
this.billingNotificationService.handleError(error);
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
this.isUnpaid = this.subscriptionStatus === "unpaid" ?? false;
|
||||
// If the flag `launchPaymentModalAutomatically` is set to true,
|
||||
// we schedule a timeout (delay of 800ms) to automatically launch the payment modal.
|
||||
// This delay ensures that any prior UI/rendering operations complete before triggering the modal.
|
||||
if (this.launchPaymentModalAutomatically) {
|
||||
window.setTimeout(async () => {
|
||||
await this.changePayment();
|
||||
this.launchPaymentModalAutomatically = false;
|
||||
this.location.replaceState(this.location.path(), "", {});
|
||||
}, 800);
|
||||
}
|
||||
this.loading = false;
|
||||
};
|
||||
|
||||
protected updatePaymentMethod = async (): Promise<void> => {
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
import { mock, MockProxy } from "jest-mock-extended";
|
||||
|
||||
import { ErrorResponse } from "@bitwarden/common/models/response/error.response";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { ToastService } from "@bitwarden/components";
|
||||
|
||||
import { BillingNotificationService } from "./billing-notification.service";
|
||||
|
||||
describe("BillingNotificationService", () => {
|
||||
let service: BillingNotificationService;
|
||||
let logService: MockProxy<LogService>;
|
||||
let toastService: MockProxy<ToastService>;
|
||||
|
||||
beforeEach(() => {
|
||||
logService = mock<LogService>();
|
||||
toastService = mock<ToastService>();
|
||||
service = new BillingNotificationService(logService, toastService);
|
||||
});
|
||||
|
||||
describe("handleError", () => {
|
||||
it("should log error and show toast for ErrorResponse", () => {
|
||||
const error = new ErrorResponse(["test error"], 400);
|
||||
|
||||
expect(() => service.handleError(error)).toThrow();
|
||||
expect(logService.error).toHaveBeenCalledWith(error);
|
||||
expect(toastService.showToast).toHaveBeenCalledWith({
|
||||
variant: "error",
|
||||
title: "",
|
||||
message: error.getSingleMessage(),
|
||||
});
|
||||
});
|
||||
|
||||
it("shows error toast with the provided error", () => {
|
||||
const error = new ErrorResponse(["test error"], 400);
|
||||
|
||||
expect(() => service.handleError(error, "Test Title")).toThrow();
|
||||
expect(toastService.showToast).toHaveBeenCalledWith({
|
||||
variant: "error",
|
||||
title: "Test Title",
|
||||
message: error.getSingleMessage(),
|
||||
});
|
||||
});
|
||||
|
||||
it("should only log error for non-ErrorResponse", () => {
|
||||
const error = new Error("test error");
|
||||
|
||||
expect(() => service.handleError(error)).toThrow();
|
||||
expect(logService.error).toHaveBeenCalledWith(error);
|
||||
expect(toastService.showToast).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("showSuccess", () => {
|
||||
it("shows success toast with default title when provided title is empty", () => {
|
||||
const message = "test message";
|
||||
service.showSuccess(message);
|
||||
|
||||
expect(toastService.showToast).toHaveBeenCalledWith({
|
||||
variant: "success",
|
||||
title: "",
|
||||
message,
|
||||
});
|
||||
});
|
||||
|
||||
it("should show success toast with custom title", () => {
|
||||
const message = "test message";
|
||||
service.showSuccess(message, "Success Title");
|
||||
|
||||
expect(toastService.showToast).toHaveBeenCalledWith({
|
||||
variant: "success",
|
||||
title: "Success Title",
|
||||
message,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,35 @@
|
||||
import { Injectable } from "@angular/core";
|
||||
|
||||
import { ErrorResponse } from "@bitwarden/common/models/response/error.response";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { ToastService } from "@bitwarden/components";
|
||||
|
||||
@Injectable({
|
||||
providedIn: "root",
|
||||
})
|
||||
export class BillingNotificationService {
|
||||
constructor(
|
||||
private logService: LogService,
|
||||
private toastService: ToastService,
|
||||
) {}
|
||||
|
||||
handleError(error: unknown, title: string = "") {
|
||||
this.logService.error(error);
|
||||
if (error instanceof ErrorResponse) {
|
||||
this.toastService.showToast({
|
||||
variant: "error",
|
||||
title: title,
|
||||
message: error.getSingleMessage(),
|
||||
});
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
||||
showSuccess(message: string, title: string = "") {
|
||||
this.toastService.showToast({
|
||||
variant: "success",
|
||||
title: title,
|
||||
message: message,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,8 @@
|
||||
import { NgModule } from "@angular/core";
|
||||
|
||||
@NgModule({})
|
||||
import { BillingNotificationService } from "./billing-notification.service";
|
||||
|
||||
@NgModule({
|
||||
providers: [BillingNotificationService],
|
||||
})
|
||||
export class BillingServicesModule {}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<ng-container bitDialogContent>
|
||||
<app-payment
|
||||
[showAccountCredit]="false"
|
||||
[showBankAccount]="!!organizationId"
|
||||
[showBankAccount]="!!organizationId || !!providerId"
|
||||
[initialPaymentMethod]="initialPaymentMethod"
|
||||
></app-payment>
|
||||
<app-manage-tax-information
|
||||
|
||||
@@ -22,6 +22,7 @@ export interface AdjustPaymentDialogParams {
|
||||
initialPaymentMethod?: PaymentMethodType;
|
||||
organizationId?: string;
|
||||
productTier?: ProductTierType;
|
||||
providerId?: string;
|
||||
}
|
||||
|
||||
export enum AdjustPaymentDialogResultType {
|
||||
@@ -44,6 +45,7 @@ export class AdjustPaymentDialogComponent implements OnInit {
|
||||
protected initialPaymentMethod: PaymentMethodType;
|
||||
protected organizationId?: string;
|
||||
protected productTier?: ProductTierType;
|
||||
protected providerId?: string;
|
||||
|
||||
protected taxInformation: TaxInformation;
|
||||
|
||||
@@ -61,6 +63,7 @@ export class AdjustPaymentDialogComponent implements OnInit {
|
||||
this.initialPaymentMethod = this.dialogParams.initialPaymentMethod ?? PaymentMethodType.Card;
|
||||
this.organizationId = this.dialogParams.organizationId;
|
||||
this.productTier = this.dialogParams.productTier;
|
||||
this.providerId = this.dialogParams.providerId;
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
@@ -73,6 +76,13 @@ export class AdjustPaymentDialogComponent implements OnInit {
|
||||
.catch(() => {
|
||||
this.taxInformation = new TaxInformation();
|
||||
});
|
||||
} else if (this.providerId) {
|
||||
this.billingApiService
|
||||
.getProviderTaxInformation(this.providerId)
|
||||
.then((response) => (this.taxInformation = TaxInformation.from(response)))
|
||||
.catch(() => {
|
||||
this.taxInformation = new TaxInformation();
|
||||
});
|
||||
} else {
|
||||
this.apiService
|
||||
.getTaxInfo()
|
||||
@@ -104,10 +114,12 @@ export class AdjustPaymentDialogComponent implements OnInit {
|
||||
}
|
||||
|
||||
try {
|
||||
if (!this.organizationId) {
|
||||
await this.updatePremiumUserPaymentMethod();
|
||||
} else {
|
||||
if (this.organizationId) {
|
||||
await this.updateOrganizationPaymentMethod();
|
||||
} else if (this.providerId) {
|
||||
await this.updateProviderPaymentMethod();
|
||||
} else {
|
||||
await this.updatePremiumUserPaymentMethod();
|
||||
}
|
||||
|
||||
this.toastService.showToast({
|
||||
@@ -137,20 +149,6 @@ export class AdjustPaymentDialogComponent implements OnInit {
|
||||
await this.billingApiService.updateOrganizationPaymentMethod(this.organizationId, request);
|
||||
};
|
||||
|
||||
protected get showTaxIdField(): boolean {
|
||||
if (!this.organizationId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
switch (this.productTier) {
|
||||
case ProductTierType.Free:
|
||||
case ProductTierType.Families:
|
||||
return false;
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
private updatePremiumUserPaymentMethod = async () => {
|
||||
const { type, token } = await this.paymentComponent.tokenize();
|
||||
|
||||
@@ -168,6 +166,30 @@ export class AdjustPaymentDialogComponent implements OnInit {
|
||||
await this.apiService.postAccountPayment(request);
|
||||
};
|
||||
|
||||
private updateProviderPaymentMethod = async () => {
|
||||
const paymentSource = await this.paymentComponent.tokenize();
|
||||
|
||||
const request = new UpdatePaymentMethodRequest();
|
||||
request.paymentSource = paymentSource;
|
||||
request.taxInformation = ExpandedTaxInfoUpdateRequest.From(this.taxInformation);
|
||||
|
||||
await this.billingApiService.updateProviderPaymentMethod(this.providerId, request);
|
||||
};
|
||||
|
||||
protected get showTaxIdField(): boolean {
|
||||
if (this.organizationId) {
|
||||
switch (this.productTier) {
|
||||
case ProductTierType.Free:
|
||||
case ProductTierType.Families:
|
||||
return false;
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
return !!this.providerId;
|
||||
}
|
||||
}
|
||||
|
||||
static open = (
|
||||
dialogService: DialogService,
|
||||
dialogConfig: DialogConfig<AdjustPaymentDialogParams>,
|
||||
|
||||
@@ -36,7 +36,6 @@ import {
|
||||
@Component({
|
||||
templateUrl: "payment-method.component.html",
|
||||
})
|
||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
|
||||
export class PaymentMethodComponent implements OnInit, OnDestroy {
|
||||
loading = false;
|
||||
firstLoaded = false;
|
||||
|
||||
@@ -24,7 +24,6 @@ import { SharedModule } from "../../shared";
|
||||
standalone: true,
|
||||
imports: [SharedModule],
|
||||
})
|
||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
|
||||
export class TaxInfoComponent implements OnInit, OnDestroy {
|
||||
private destroy$ = new Subject<void>();
|
||||
|
||||
@@ -91,7 +90,7 @@ export class TaxInfoComponent implements OnInit, OnDestroy {
|
||||
|
||||
async ngOnInit() {
|
||||
// Provider setup
|
||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe
|
||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
|
||||
this.route.queryParams.subscribe((params) => {
|
||||
this.providerId = params.providerId;
|
||||
});
|
||||
|
||||
@@ -50,17 +50,17 @@ import {
|
||||
import { AccountApiService as AccountApiServiceAbstraction } from "@bitwarden/common/auth/abstractions/account-api.service";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
|
||||
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction";
|
||||
import { MasterPasswordApiService } from "@bitwarden/common/auth/abstractions/master-password-api.service.abstraction";
|
||||
import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction";
|
||||
import { ClientType } from "@bitwarden/common/enums";
|
||||
import { ProcessReloadServiceAbstraction } from "@bitwarden/common/key-management/abstractions/process-reload.service";
|
||||
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction";
|
||||
import {
|
||||
VaultTimeout,
|
||||
VaultTimeoutStringType,
|
||||
} from "@bitwarden/common/key-management/vault-timeout";
|
||||
import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service";
|
||||
import {
|
||||
EnvironmentService,
|
||||
@@ -74,7 +74,7 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl
|
||||
import { SdkClientFactory } from "@bitwarden/common/platform/abstractions/sdk/sdk-client-factory";
|
||||
import { SdkLoadService } from "@bitwarden/common/platform/abstractions/sdk/sdk-load.service";
|
||||
import { AbstractStorageService } from "@bitwarden/common/platform/abstractions/storage.service";
|
||||
import { ThemeType } from "@bitwarden/common/platform/enums";
|
||||
import { ThemeTypes } from "@bitwarden/common/platform/enums";
|
||||
// eslint-disable-next-line no-restricted-imports -- Needed for DI
|
||||
import {
|
||||
UnsupportedWebPushConnectionService,
|
||||
@@ -82,7 +82,6 @@ import {
|
||||
} from "@bitwarden/common/platform/notifications/internal";
|
||||
import { AppIdService as DefaultAppIdService } from "@bitwarden/common/platform/services/app-id.service";
|
||||
import { MemoryStorageService } from "@bitwarden/common/platform/services/memory-storage.service";
|
||||
// eslint-disable-next-line import/no-restricted-paths -- Implementation for memory storage
|
||||
import { MigrationBuilderService } from "@bitwarden/common/platform/services/migration-builder.service";
|
||||
import { MigrationRunner } from "@bitwarden/common/platform/services/migration-runner";
|
||||
import { DefaultSdkClientFactory } from "@bitwarden/common/platform/services/sdk/default-sdk-client-factory";
|
||||
@@ -98,6 +97,7 @@ import {
|
||||
DefaultThemeStateService,
|
||||
ThemeStateService,
|
||||
} from "@bitwarden/common/platform/theming/theme-state.service";
|
||||
import { DialogService, ToastService } from "@bitwarden/components";
|
||||
import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy";
|
||||
import {
|
||||
KdfConfigService,
|
||||
@@ -105,6 +105,7 @@ import {
|
||||
BiometricsService,
|
||||
} from "@bitwarden/key-management";
|
||||
import { LockComponentService } from "@bitwarden/key-management-ui";
|
||||
import { DefaultSshImportPromptService, SshImportPromptService } from "@bitwarden/vault";
|
||||
|
||||
import { flagEnabled } from "../../utils/flags";
|
||||
import { PolicyListService } from "../admin-console/core/policy-list.service";
|
||||
@@ -236,10 +237,10 @@ const safeProviders: SafeProvider[] = [
|
||||
}),
|
||||
safeProvider({
|
||||
provide: ThemeStateService,
|
||||
useFactory: (globalStateProvider: GlobalStateProvider, configService: ConfigService) =>
|
||||
useFactory: (globalStateProvider: GlobalStateProvider) =>
|
||||
// Web chooses to have Light as the default theme
|
||||
new DefaultThemeStateService(globalStateProvider, configService, ThemeType.Light),
|
||||
deps: [GlobalStateProvider, ConfigService],
|
||||
new DefaultThemeStateService(globalStateProvider, ThemeTypes.Light),
|
||||
deps: [GlobalStateProvider],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: CLIENT_TYPE,
|
||||
@@ -279,6 +280,7 @@ const safeProviders: SafeProvider[] = [
|
||||
useClass: WebSetPasswordJitService,
|
||||
deps: [
|
||||
ApiService,
|
||||
MasterPasswordApiService,
|
||||
KeyServiceAbstraction,
|
||||
EncryptService,
|
||||
I18nServiceAbstraction,
|
||||
@@ -351,6 +353,11 @@ const safeProviders: SafeProvider[] = [
|
||||
useClass: WebLoginDecryptionOptionsService,
|
||||
deps: [MessagingService, RouterService, AcceptOrganizationInviteService],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: SshImportPromptService,
|
||||
useClass: DefaultSshImportPromptService,
|
||||
deps: [DialogService, ToastService, PlatformUtilsService, I18nServiceAbstraction],
|
||||
}),
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
|
||||
@@ -3,6 +3,7 @@ import { FormControl, FormGroup, Validators } from "@angular/forms";
|
||||
import { firstValueFrom } from "rxjs";
|
||||
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { ErrorResponse } from "@bitwarden/common/models/response/error.response";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||
@@ -75,7 +76,7 @@ export class MigrateFromLegacyEncryptionComponent {
|
||||
} catch (e) {
|
||||
// If the error is due to missing folders, we can delete all folders and try again
|
||||
if (
|
||||
e instanceof Error &&
|
||||
e instanceof ErrorResponse &&
|
||||
e.message === "All existing folders must be included in the rotation."
|
||||
) {
|
||||
const deleteFolders = await this.dialogService.openSimpleDialog({
|
||||
|
||||
@@ -55,10 +55,6 @@ import { AcceptFamilySponsorshipComponent } from "./admin-console/organizations/
|
||||
import { FamiliesForEnterpriseSetupComponent } from "./admin-console/organizations/sponsorships/families-for-enterprise-setup.component";
|
||||
import { CreateOrganizationComponent } from "./admin-console/settings/create-organization.component";
|
||||
import { deepLinkGuard } from "./auth/guards/deep-link.guard";
|
||||
import { HintComponent } from "./auth/hint.component";
|
||||
import { LoginDecryptionOptionsComponentV1 } from "./auth/login/login-decryption-options/login-decryption-options-v1.component";
|
||||
import { LoginComponentV1 } from "./auth/login/login-v1.component";
|
||||
import { LoginViaAuthRequestComponentV1 } from "./auth/login/login-via-auth-request-v1.component";
|
||||
import { LoginViaWebAuthnComponent } from "./auth/login/login-via-webauthn/login-via-webauthn.component";
|
||||
import { AcceptOrganizationComponent } from "./auth/organization-invite/accept-organization.component";
|
||||
import { RecoverDeleteComponent } from "./auth/recover-delete.component";
|
||||
@@ -69,7 +65,6 @@ import { AccountComponent } from "./auth/settings/account/account.component";
|
||||
import { EmergencyAccessComponent } from "./auth/settings/emergency-access/emergency-access.component";
|
||||
import { EmergencyAccessViewComponent } from "./auth/settings/emergency-access/view/emergency-access-view.component";
|
||||
import { SecurityRoutingModule } from "./auth/settings/security/security-routing.module";
|
||||
import { SsoComponentV1 } from "./auth/sso-v1.component";
|
||||
import { TwoFactorComponentV1 } from "./auth/two-factor-v1.component";
|
||||
import { UpdatePasswordComponent } from "./auth/update-password.component";
|
||||
import { UpdateTempPasswordComponent } from "./auth/update-temp-password.component";
|
||||
@@ -172,172 +167,6 @@ const routes: Routes = [
|
||||
},
|
||||
],
|
||||
},
|
||||
...unauthUiRefreshSwap(
|
||||
LoginViaAuthRequestComponentV1,
|
||||
AnonLayoutWrapperComponent,
|
||||
{
|
||||
path: "login-with-device",
|
||||
data: { titleId: "loginWithDevice" } satisfies RouteDataProperties,
|
||||
},
|
||||
{
|
||||
path: "login-with-device",
|
||||
data: {
|
||||
pageIcon: DevicesIcon,
|
||||
pageTitle: {
|
||||
key: "logInRequestSent",
|
||||
},
|
||||
pageSubtitle: {
|
||||
key: "aNotificationWasSentToYourDevice",
|
||||
},
|
||||
titleId: "loginInitiated",
|
||||
} satisfies RouteDataProperties & AnonLayoutWrapperData,
|
||||
children: [
|
||||
{ path: "", component: LoginViaAuthRequestComponent },
|
||||
{
|
||||
path: "",
|
||||
component: EnvironmentSelectorComponent,
|
||||
outlet: "environment-selector",
|
||||
},
|
||||
],
|
||||
},
|
||||
),
|
||||
...unauthUiRefreshSwap(
|
||||
LoginViaAuthRequestComponentV1,
|
||||
AnonLayoutWrapperComponent,
|
||||
{
|
||||
path: "admin-approval-requested",
|
||||
data: { titleId: "adminApprovalRequested" } satisfies RouteDataProperties,
|
||||
},
|
||||
{
|
||||
path: "admin-approval-requested",
|
||||
data: {
|
||||
pageIcon: DevicesIcon,
|
||||
pageTitle: {
|
||||
key: "adminApprovalRequested",
|
||||
},
|
||||
pageSubtitle: {
|
||||
key: "adminApprovalRequestSentToAdmins",
|
||||
},
|
||||
titleId: "adminApprovalRequested",
|
||||
} satisfies RouteDataProperties & AnonLayoutWrapperData,
|
||||
children: [{ path: "", component: LoginViaAuthRequestComponent }],
|
||||
},
|
||||
),
|
||||
...unauthUiRefreshSwap(
|
||||
AnonLayoutWrapperComponent,
|
||||
AnonLayoutWrapperComponent,
|
||||
{
|
||||
path: "login",
|
||||
canActivate: [unauthGuardFn()],
|
||||
children: [
|
||||
{
|
||||
path: "",
|
||||
component: LoginComponentV1,
|
||||
},
|
||||
{
|
||||
path: "",
|
||||
component: EnvironmentSelectorComponent,
|
||||
outlet: "environment-selector",
|
||||
},
|
||||
],
|
||||
data: {
|
||||
pageTitle: {
|
||||
key: "logIn",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
path: "login",
|
||||
canActivate: [unauthGuardFn()],
|
||||
data: {
|
||||
pageTitle: {
|
||||
key: "logInToBitwarden",
|
||||
},
|
||||
pageIcon: VaultIcon,
|
||||
} satisfies RouteDataProperties & AnonLayoutWrapperData,
|
||||
children: [
|
||||
{
|
||||
path: "",
|
||||
component: LoginComponent,
|
||||
},
|
||||
{
|
||||
path: "",
|
||||
component: LoginSecondaryContentComponent,
|
||||
outlet: "secondary",
|
||||
},
|
||||
{
|
||||
path: "",
|
||||
component: EnvironmentSelectorComponent,
|
||||
outlet: "environment-selector",
|
||||
},
|
||||
],
|
||||
},
|
||||
),
|
||||
...unauthUiRefreshSwap(
|
||||
LoginDecryptionOptionsComponentV1,
|
||||
AnonLayoutWrapperComponent,
|
||||
{
|
||||
path: "login-initiated",
|
||||
canActivate: [tdeDecryptionRequiredGuard()],
|
||||
},
|
||||
{
|
||||
path: "login-initiated",
|
||||
canActivate: [tdeDecryptionRequiredGuard()],
|
||||
data: {
|
||||
pageIcon: DevicesIcon,
|
||||
},
|
||||
children: [{ path: "", component: LoginDecryptionOptionsComponent }],
|
||||
},
|
||||
),
|
||||
...unauthUiRefreshSwap(
|
||||
AnonLayoutWrapperComponent,
|
||||
AnonLayoutWrapperComponent,
|
||||
{
|
||||
path: "hint",
|
||||
canActivate: [unauthGuardFn()],
|
||||
data: {
|
||||
pageTitle: {
|
||||
key: "passwordHint",
|
||||
},
|
||||
titleId: "passwordHint",
|
||||
},
|
||||
children: [
|
||||
{ path: "", component: HintComponent },
|
||||
{
|
||||
path: "",
|
||||
component: EnvironmentSelectorComponent,
|
||||
outlet: "environment-selector",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: "",
|
||||
children: [
|
||||
{
|
||||
path: "hint",
|
||||
canActivate: [unauthGuardFn()],
|
||||
data: {
|
||||
pageTitle: {
|
||||
key: "requestPasswordHint",
|
||||
},
|
||||
pageSubtitle: {
|
||||
key: "enterYourAccountEmailAddressAndYourPasswordHintWillBeSentToYou",
|
||||
},
|
||||
pageIcon: UserLockIcon,
|
||||
state: "hint",
|
||||
},
|
||||
children: [
|
||||
{ path: "", component: PasswordHintComponent },
|
||||
{
|
||||
path: "",
|
||||
component: EnvironmentSelectorComponent,
|
||||
outlet: "environment-selector",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
),
|
||||
{
|
||||
path: "",
|
||||
component: AnonLayoutWrapperComponent,
|
||||
@@ -381,6 +210,97 @@ const routes: Routes = [
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: "login",
|
||||
canActivate: [unauthGuardFn()],
|
||||
data: {
|
||||
pageTitle: {
|
||||
key: "logInToBitwarden",
|
||||
},
|
||||
pageIcon: VaultIcon,
|
||||
} satisfies RouteDataProperties & AnonLayoutWrapperData,
|
||||
children: [
|
||||
{
|
||||
path: "",
|
||||
component: LoginComponent,
|
||||
},
|
||||
{
|
||||
path: "",
|
||||
component: LoginSecondaryContentComponent,
|
||||
outlet: "secondary",
|
||||
},
|
||||
{
|
||||
path: "",
|
||||
component: EnvironmentSelectorComponent,
|
||||
outlet: "environment-selector",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: "login-with-device",
|
||||
data: {
|
||||
pageIcon: DevicesIcon,
|
||||
pageTitle: {
|
||||
key: "logInRequestSent",
|
||||
},
|
||||
pageSubtitle: {
|
||||
key: "aNotificationWasSentToYourDevice",
|
||||
},
|
||||
titleId: "loginInitiated",
|
||||
} satisfies RouteDataProperties & AnonLayoutWrapperData,
|
||||
children: [
|
||||
{ path: "", component: LoginViaAuthRequestComponent },
|
||||
{
|
||||
path: "",
|
||||
component: EnvironmentSelectorComponent,
|
||||
outlet: "environment-selector",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: "admin-approval-requested",
|
||||
data: {
|
||||
pageIcon: DevicesIcon,
|
||||
pageTitle: {
|
||||
key: "adminApprovalRequested",
|
||||
},
|
||||
pageSubtitle: {
|
||||
key: "adminApprovalRequestSentToAdmins",
|
||||
},
|
||||
titleId: "adminApprovalRequested",
|
||||
} satisfies RouteDataProperties & AnonLayoutWrapperData,
|
||||
children: [{ path: "", component: LoginViaAuthRequestComponent }],
|
||||
},
|
||||
{
|
||||
path: "hint",
|
||||
canActivate: [unauthGuardFn()],
|
||||
data: {
|
||||
pageTitle: {
|
||||
key: "requestPasswordHint",
|
||||
},
|
||||
pageSubtitle: {
|
||||
key: "enterYourAccountEmailAddressAndYourPasswordHintWillBeSentToYou",
|
||||
},
|
||||
pageIcon: UserLockIcon,
|
||||
state: "hint",
|
||||
},
|
||||
children: [
|
||||
{ path: "", component: PasswordHintComponent },
|
||||
{
|
||||
path: "",
|
||||
component: EnvironmentSelectorComponent,
|
||||
outlet: "environment-selector",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: "login-initiated",
|
||||
canActivate: [tdeDecryptionRequiredGuard()],
|
||||
data: {
|
||||
pageIcon: DevicesIcon,
|
||||
},
|
||||
children: [{ path: "", component: LoginDecryptionOptionsComponent }],
|
||||
},
|
||||
{
|
||||
path: "send/:sendId/:key",
|
||||
data: {
|
||||
@@ -432,64 +352,24 @@ const routes: Routes = [
|
||||
},
|
||||
],
|
||||
},
|
||||
...unauthUiRefreshSwap(
|
||||
SsoComponentV1,
|
||||
SsoComponent,
|
||||
{
|
||||
path: "sso",
|
||||
canActivate: [unauthGuardFn()],
|
||||
data: {
|
||||
pageTitle: {
|
||||
key: "enterpriseSingleSignOn",
|
||||
},
|
||||
titleId: "enterpriseSingleSignOn",
|
||||
} satisfies RouteDataProperties & AnonLayoutWrapperData,
|
||||
children: [
|
||||
{
|
||||
path: "",
|
||||
component: SsoComponentV1,
|
||||
},
|
||||
{
|
||||
path: "",
|
||||
component: EnvironmentSelectorComponent,
|
||||
outlet: "environment-selector",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: "sso",
|
||||
canActivate: [unauthGuardFn()],
|
||||
data: {
|
||||
pageTitle: {
|
||||
key: "singleSignOn",
|
||||
},
|
||||
titleId: "enterpriseSingleSignOn",
|
||||
pageSubtitle: {
|
||||
key: "singleSignOnEnterOrgIdentifierText",
|
||||
},
|
||||
titleAreaMaxWidth: "md",
|
||||
pageIcon: SsoKeyIcon,
|
||||
} satisfies RouteDataProperties & AnonLayoutWrapperData,
|
||||
children: [
|
||||
{
|
||||
path: "",
|
||||
component: SsoComponent,
|
||||
},
|
||||
{
|
||||
path: "",
|
||||
component: EnvironmentSelectorComponent,
|
||||
outlet: "environment-selector",
|
||||
},
|
||||
],
|
||||
},
|
||||
),
|
||||
{
|
||||
path: "login",
|
||||
path: "sso",
|
||||
canActivate: [unauthGuardFn()],
|
||||
data: {
|
||||
pageTitle: {
|
||||
key: "singleSignOn",
|
||||
},
|
||||
titleId: "enterpriseSingleSignOn",
|
||||
pageSubtitle: {
|
||||
key: "singleSignOnEnterOrgIdentifierText",
|
||||
},
|
||||
titleAreaMaxWidth: "md",
|
||||
pageIcon: SsoKeyIcon,
|
||||
} satisfies RouteDataProperties & AnonLayoutWrapperData,
|
||||
children: [
|
||||
{
|
||||
path: "",
|
||||
component: LoginComponent,
|
||||
component: SsoComponent,
|
||||
},
|
||||
{
|
||||
path: "",
|
||||
@@ -497,11 +377,6 @@ const routes: Routes = [
|
||||
outlet: "environment-selector",
|
||||
},
|
||||
],
|
||||
data: {
|
||||
pageTitle: {
|
||||
key: "logIn",
|
||||
},
|
||||
},
|
||||
},
|
||||
...unauthUiRefreshSwap(
|
||||
TwoFactorComponentV1,
|
||||
@@ -544,6 +419,7 @@ const routes: Routes = [
|
||||
pageTitle: {
|
||||
key: "verifyYourIdentity",
|
||||
},
|
||||
titleAreaMaxWidth: "md",
|
||||
} satisfies RouteDataProperties & AnonLayoutWrapperData,
|
||||
},
|
||||
),
|
||||
|
||||
@@ -17,7 +17,7 @@ import {
|
||||
} from "@bitwarden/common/key-management/vault-timeout";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { ThemeType } from "@bitwarden/common/platform/enums";
|
||||
import { Theme, ThemeTypes } from "@bitwarden/common/platform/enums";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service";
|
||||
import { DialogService } from "@bitwarden/components";
|
||||
@@ -47,7 +47,7 @@ export class PreferencesComponent implements OnInit, OnDestroy {
|
||||
vaultTimeout: [null as VaultTimeout | null],
|
||||
vaultTimeoutAction: [VaultTimeoutAction.Lock],
|
||||
enableFavicons: true,
|
||||
theme: [ThemeType.Light],
|
||||
theme: [ThemeTypes.Light as Theme],
|
||||
locale: [null as string | null],
|
||||
});
|
||||
|
||||
@@ -90,9 +90,9 @@ export class PreferencesComponent implements OnInit, OnDestroy {
|
||||
localeOptions.splice(0, 0, { name: i18nService.t("default"), value: null });
|
||||
this.localeOptions = localeOptions;
|
||||
this.themeOptions = [
|
||||
{ name: i18nService.t("themeLight"), value: ThemeType.Light },
|
||||
{ name: i18nService.t("themeDark"), value: ThemeType.Dark },
|
||||
{ name: i18nService.t("themeSystem"), value: ThemeType.System },
|
||||
{ name: i18nService.t("themeLight"), value: ThemeTypes.Light },
|
||||
{ name: i18nService.t("themeDark"), value: ThemeTypes.Dark },
|
||||
{ name: i18nService.t("themeSystem"), value: ThemeTypes.System },
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -12,7 +12,6 @@ import { EventsComponent as OrgEventsComponent } from "../admin-console/organiza
|
||||
import { UserConfirmComponent as OrgUserConfirmComponent } from "../admin-console/organizations/manage/user-confirm.component";
|
||||
import { VerifyRecoverDeleteOrgComponent } from "../admin-console/organizations/manage/verify-recover-delete-org.component";
|
||||
import { AcceptFamilySponsorshipComponent } from "../admin-console/organizations/sponsorships/accept-family-sponsorship.component";
|
||||
import { HintComponent } from "../auth/hint.component";
|
||||
import { RecoverDeleteComponent } from "../auth/recover-delete.component";
|
||||
import { RecoverTwoFactorComponent } from "../auth/recover-two-factor.component";
|
||||
import { RemovePasswordComponent } from "../auth/remove-password.component";
|
||||
@@ -126,7 +125,6 @@ import { SharedModule } from "./shared.module";
|
||||
EmergencyAccessViewComponent,
|
||||
FolderAddEditComponent,
|
||||
FrontendLayoutComponent,
|
||||
HintComponent,
|
||||
OrgAddEditComponent,
|
||||
OrgAttachmentsComponent,
|
||||
OrgEventsComponent,
|
||||
@@ -188,7 +186,6 @@ import { SharedModule } from "./shared.module";
|
||||
EmergencyAccessViewComponent,
|
||||
FolderAddEditComponent,
|
||||
FrontendLayoutComponent,
|
||||
HintComponent,
|
||||
OrgAddEditComponent,
|
||||
OrganizationLayoutComponent,
|
||||
OrgAttachmentsComponent,
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { ComponentFixture, TestBed } from "@angular/core/testing";
|
||||
import { ReactiveFormsModule } from "@angular/forms";
|
||||
import { mock, MockProxy } from "jest-mock-extended";
|
||||
|
||||
@@ -26,77 +26,73 @@
|
||||
</bit-toggle>
|
||||
</ng-container>
|
||||
</bit-toggle-group>
|
||||
<bit-table [dataSource]="dataSource">
|
||||
<bit-table-scroll [dataSource]="dataSource" [rowSize]="53">
|
||||
<ng-container header>
|
||||
<tr bitRow>
|
||||
<th bitCell></th>
|
||||
<th bitCell bitSortable="name">{{ "name" | i18n }}</th>
|
||||
<th bitCell bitSortable="organizationId" *ngIf="!isAdminConsoleActive">
|
||||
{{ "owner" | i18n }}
|
||||
</th>
|
||||
<th bitCell class="tw-text-right" bitSortable="exposedXTimes">
|
||||
{{ "timesExposed" | i18n }}
|
||||
</th>
|
||||
</tr>
|
||||
<th bitCell></th>
|
||||
<th bitCell bitSortable="name">{{ "name" | i18n }}</th>
|
||||
<th bitCell bitSortable="organizationId" *ngIf="!isAdminConsoleActive">
|
||||
{{ "owner" | i18n }}
|
||||
</th>
|
||||
<th bitCell class="tw-text-right" bitSortable="exposedXTimes">
|
||||
{{ "timesExposed" | i18n }}
|
||||
</th>
|
||||
</ng-container>
|
||||
<ng-template body let-rows$>
|
||||
<tr bitRow *ngFor="let r of rows$ | async">
|
||||
<td bitCell>
|
||||
<app-vault-icon [cipher]="r"></app-vault-icon>
|
||||
</td>
|
||||
<td bitCell>
|
||||
<ng-container *ngIf="!organization || canManageCipher(r); else cantManage">
|
||||
<a
|
||||
bitLink
|
||||
href="#"
|
||||
appStopClick
|
||||
(click)="selectCipher(r)"
|
||||
title="{{ 'editItemWithName' | i18n: r.name }}"
|
||||
>{{ r.name }}</a
|
||||
>
|
||||
</ng-container>
|
||||
<ng-template #cantManage>
|
||||
<span>{{ r.name }}</span>
|
||||
</ng-template>
|
||||
<ng-container *ngIf="!organization && r.organizationId">
|
||||
<i
|
||||
class="bwi bwi-collection"
|
||||
appStopProp
|
||||
title="{{ 'shared' | i18n }}"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
<span class="tw-sr-only">{{ "shared" | i18n }}</span>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="r.hasAttachments">
|
||||
<i
|
||||
class="bwi bwi-paperclip"
|
||||
appStopProp
|
||||
title="{{ 'attachments' | i18n }}"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
<span class="tw-sr-only">{{ "attachments" | i18n }}</span>
|
||||
</ng-container>
|
||||
<br />
|
||||
<small>{{ r.subTitle }}</small>
|
||||
</td>
|
||||
<td bitCell *ngIf="!isAdminConsoleActive">
|
||||
<app-org-badge
|
||||
*ngIf="!organization"
|
||||
[disabled]="disabled"
|
||||
[organizationId]="r.organizationId"
|
||||
[organizationName]="r.organizationId | orgNameFromId: (organizations$ | async)"
|
||||
appStopProp
|
||||
<ng-template bitRowDef let-row>
|
||||
<td bitCell>
|
||||
<app-vault-icon [cipher]="row"></app-vault-icon>
|
||||
</td>
|
||||
<td bitCell>
|
||||
<ng-container *ngIf="!organization || canManageCipher(row); else cantManage">
|
||||
<a
|
||||
bitLink
|
||||
href="#"
|
||||
appStopClick
|
||||
(click)="selectCipher(row)"
|
||||
title="{{ 'editItemWithName' | i18n: row.name }}"
|
||||
>{{ row.name }}</a
|
||||
>
|
||||
</app-org-badge>
|
||||
</td>
|
||||
<td bitCell class="tw-text-right">
|
||||
<span bitBadge variant="warning">
|
||||
{{ "exposedXTimes" | i18n: (r.exposedXTimes | number) }}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
</ng-container>
|
||||
<ng-template #cantManage>
|
||||
<span>{{ row.name }}</span>
|
||||
</ng-template>
|
||||
<ng-container *ngIf="!organization && row.organizationId">
|
||||
<i
|
||||
class="bwi bwi-collection"
|
||||
appStopProp
|
||||
title="{{ 'shared' | i18n }}"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
<span class="tw-sr-only">{{ "shared" | i18n }}</span>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="row.hasAttachments">
|
||||
<i
|
||||
class="bwi bwi-paperclip"
|
||||
appStopProp
|
||||
title="{{ 'attachments' | i18n }}"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
<span class="tw-sr-only">{{ "attachments" | i18n }}</span>
|
||||
</ng-container>
|
||||
<br />
|
||||
<small>{{ row.subTitle }}</small>
|
||||
</td>
|
||||
<td bitCell *ngIf="!isAdminConsoleActive">
|
||||
<app-org-badge
|
||||
*ngIf="!organization"
|
||||
[disabled]="disabled"
|
||||
[organizationId]="row.organizationId"
|
||||
[organizationName]="row.organizationId | orgNameFromId: (organizations$ | async)"
|
||||
appStopProp
|
||||
>
|
||||
</app-org-badge>
|
||||
</td>
|
||||
<td bitCell class="tw-text-right">
|
||||
<span bitBadge variant="warning">
|
||||
{{ "exposedXTimes" | i18n: (row.exposedXTimes | number) }}
|
||||
</span>
|
||||
</td>
|
||||
</ng-template>
|
||||
</bit-table>
|
||||
</bit-table-scroll>
|
||||
</ng-container>
|
||||
</div>
|
||||
<ng-template #cipherAddEdit></ng-template>
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { ComponentFixture, TestBed } from "@angular/core/testing";
|
||||
import { mock, MockProxy } from "jest-mock-extended";
|
||||
import { of } from "rxjs";
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { ComponentFixture, TestBed } from "@angular/core/testing";
|
||||
import { MockProxy, mock } from "jest-mock-extended";
|
||||
import { of } from "rxjs";
|
||||
|
||||
@@ -19,7 +19,6 @@ import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
import { DialogService } from "@bitwarden/components";
|
||||
import { PasswordRepromptService, CipherFormConfigService } from "@bitwarden/vault";
|
||||
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { RoutedVaultFilterBridgeService } from "../../../../vault/individual-vault/vault-filter/services/routed-vault-filter-bridge.service";
|
||||
import { RoutedVaultFilterService } from "../../../../vault/individual-vault/vault-filter/services/routed-vault-filter.service";
|
||||
import { AdminConsoleCipherFormConfigService } from "../../../../vault/org-vault/services/admin-console-cipher-form-config.service";
|
||||
@@ -38,7 +37,6 @@ import { ExposedPasswordsReportComponent as BaseExposedPasswordsReportComponent
|
||||
RoutedVaultFilterBridgeService,
|
||||
],
|
||||
})
|
||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
|
||||
export class ExposedPasswordsReportComponent
|
||||
extends BaseExposedPasswordsReportComponent
|
||||
implements OnInit
|
||||
|
||||
@@ -18,7 +18,6 @@ import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
import { DialogService } from "@bitwarden/components";
|
||||
import { CipherFormConfigService, PasswordRepromptService } from "@bitwarden/vault";
|
||||
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { RoutedVaultFilterBridgeService } from "../../../../vault/individual-vault/vault-filter/services/routed-vault-filter-bridge.service";
|
||||
import { RoutedVaultFilterService } from "../../../../vault/individual-vault/vault-filter/services/routed-vault-filter.service";
|
||||
import { AdminConsoleCipherFormConfigService } from "../../../../vault/org-vault/services/admin-console-cipher-form-config.service";
|
||||
@@ -37,7 +36,6 @@ import { InactiveTwoFactorReportComponent as BaseInactiveTwoFactorReportComponen
|
||||
RoutedVaultFilterBridgeService,
|
||||
],
|
||||
})
|
||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
|
||||
export class InactiveTwoFactorReportComponent
|
||||
extends BaseInactiveTwoFactorReportComponent
|
||||
implements OnInit
|
||||
|
||||
@@ -18,7 +18,6 @@ import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
import { DialogService } from "@bitwarden/components";
|
||||
import { CipherFormConfigService, PasswordRepromptService } from "@bitwarden/vault";
|
||||
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { RoutedVaultFilterBridgeService } from "../../../../vault/individual-vault/vault-filter/services/routed-vault-filter-bridge.service";
|
||||
import { RoutedVaultFilterService } from "../../../../vault/individual-vault/vault-filter/services/routed-vault-filter.service";
|
||||
import { AdminConsoleCipherFormConfigService } from "../../../../vault/org-vault/services/admin-console-cipher-form-config.service";
|
||||
@@ -37,7 +36,6 @@ import { ReusedPasswordsReportComponent as BaseReusedPasswordsReportComponent }
|
||||
RoutedVaultFilterBridgeService,
|
||||
],
|
||||
})
|
||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
|
||||
export class ReusedPasswordsReportComponent
|
||||
extends BaseReusedPasswordsReportComponent
|
||||
implements OnInit
|
||||
|
||||
@@ -18,7 +18,6 @@ import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
import { DialogService } from "@bitwarden/components";
|
||||
import { CipherFormConfigService, PasswordRepromptService } from "@bitwarden/vault";
|
||||
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { RoutedVaultFilterBridgeService } from "../../../../vault/individual-vault/vault-filter/services/routed-vault-filter-bridge.service";
|
||||
import { RoutedVaultFilterService } from "../../../../vault/individual-vault/vault-filter/services/routed-vault-filter.service";
|
||||
import { AdminConsoleCipherFormConfigService } from "../../../../vault/org-vault/services/admin-console-cipher-form-config.service";
|
||||
@@ -37,7 +36,6 @@ import { UnsecuredWebsitesReportComponent as BaseUnsecuredWebsitesReportComponen
|
||||
RoutedVaultFilterBridgeService,
|
||||
],
|
||||
})
|
||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
|
||||
export class UnsecuredWebsitesReportComponent
|
||||
extends BaseUnsecuredWebsitesReportComponent
|
||||
implements OnInit
|
||||
|
||||
@@ -19,7 +19,6 @@ import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
import { DialogService } from "@bitwarden/components";
|
||||
import { CipherFormConfigService, PasswordRepromptService } from "@bitwarden/vault";
|
||||
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { RoutedVaultFilterBridgeService } from "../../../../vault/individual-vault/vault-filter/services/routed-vault-filter-bridge.service";
|
||||
import { RoutedVaultFilterService } from "../../../../vault/individual-vault/vault-filter/services/routed-vault-filter.service";
|
||||
import { AdminConsoleCipherFormConfigService } from "../../../../vault/org-vault/services/admin-console-cipher-form-config.service";
|
||||
@@ -38,7 +37,6 @@ import { WeakPasswordsReportComponent as BaseWeakPasswordsReportComponent } from
|
||||
RoutedVaultFilterBridgeService,
|
||||
],
|
||||
})
|
||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
|
||||
export class WeakPasswordsReportComponent
|
||||
extends BaseWeakPasswordsReportComponent
|
||||
implements OnInit
|
||||
|
||||
@@ -33,73 +33,69 @@
|
||||
</ng-container>
|
||||
</bit-toggle-group>
|
||||
|
||||
<bit-table [dataSource]="dataSource">
|
||||
<bit-table-scroll [dataSource]="dataSource" [rowSize]="75">
|
||||
<ng-container header *ngIf="!isAdminConsoleActive">
|
||||
<tr bitRow>
|
||||
<th bitCell></th>
|
||||
<th bitCell>{{ "name" | i18n }}</th>
|
||||
<th bitCell>{{ "owner" | i18n }}</th>
|
||||
<th bitCell class="tw-text-right">{{ "timesReused" | i18n }}</th>
|
||||
</tr>
|
||||
<th bitCell></th>
|
||||
<th bitCell>{{ "name" | i18n }}</th>
|
||||
<th bitCell>{{ "owner" | i18n }}</th>
|
||||
<th bitCell class="tw-text-right">{{ "timesReused" | i18n }}</th>
|
||||
</ng-container>
|
||||
<ng-template body let-rows$>
|
||||
<tr bitRow *ngFor="let r of rows$ | async">
|
||||
<td bitCell>
|
||||
<app-vault-icon [cipher]="r"></app-vault-icon>
|
||||
</td>
|
||||
<td bitCell>
|
||||
<ng-container *ngIf="!organization || canManageCipher(r); else cantManage">
|
||||
<a
|
||||
bitLink
|
||||
href="#"
|
||||
appStopClick
|
||||
(click)="selectCipher(r)"
|
||||
title="{{ 'editItemWithName' | i18n: r.name }}"
|
||||
>{{ r.name }}</a
|
||||
>
|
||||
</ng-container>
|
||||
<ng-template #cantManage>
|
||||
<span>{{ r.name }}</span>
|
||||
</ng-template>
|
||||
<ng-container *ngIf="!organization && r.organizationId">
|
||||
<i
|
||||
class="bwi bwi-collection"
|
||||
appStopProp
|
||||
title="{{ 'shared' | i18n }}"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
<span class="tw-sr-only">{{ "shared" | i18n }}</span>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="r.hasAttachments">
|
||||
<i
|
||||
class="bwi bwi-paperclip"
|
||||
appStopProp
|
||||
title="{{ 'attachments' | i18n }}"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
<span class="tw-sr-only">{{ "attachments" | i18n }}</span>
|
||||
</ng-container>
|
||||
<br />
|
||||
<small>{{ r.subTitle }}</small>
|
||||
</td>
|
||||
<td bitCell>
|
||||
<app-org-badge
|
||||
*ngIf="!organization"
|
||||
[disabled]="disabled"
|
||||
[organizationId]="r.organizationId"
|
||||
[organizationName]="r.organizationId | orgNameFromId: (organizations$ | async)"
|
||||
appStopProp
|
||||
<ng-template bitRowDef let-row>
|
||||
<td bitCell>
|
||||
<app-vault-icon [cipher]="row"></app-vault-icon>
|
||||
</td>
|
||||
<td bitCell>
|
||||
<ng-container *ngIf="!organization || canManageCipher(row); else cantManage">
|
||||
<a
|
||||
bitLink
|
||||
href="#"
|
||||
appStopClick
|
||||
(click)="selectCipher(row)"
|
||||
title="{{ 'editItemWithName' | i18n: row.name }}"
|
||||
>{{ row.name }}</a
|
||||
>
|
||||
</app-org-badge>
|
||||
</td>
|
||||
<td bitCell class="tw-text-right">
|
||||
<span bitBadge variant="warning">
|
||||
{{ "reusedXTimes" | i18n: passwordUseMap.get(r.login.password) }}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
</ng-container>
|
||||
<ng-template #cantManage>
|
||||
<span>{{ row.name }}</span>
|
||||
</ng-template>
|
||||
<ng-container *ngIf="!organization && row.organizationId">
|
||||
<i
|
||||
class="bwi bwi-collection"
|
||||
appStopProp
|
||||
title="{{ 'shared' | i18n }}"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
<span class="tw-sr-only">{{ "shared" | i18n }}</span>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="row.hasAttachments">
|
||||
<i
|
||||
class="bwi bwi-paperclip"
|
||||
appStopProp
|
||||
title="{{ 'attachments' | i18n }}"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
<span class="tw-sr-only">{{ "attachments" | i18n }}</span>
|
||||
</ng-container>
|
||||
<br />
|
||||
<small>{{ row.subTitle }}</small>
|
||||
</td>
|
||||
<td bitCell>
|
||||
<app-org-badge
|
||||
*ngIf="!organization"
|
||||
[disabled]="disabled"
|
||||
[organizationId]="row.organizationId"
|
||||
[organizationName]="row.organizationId | orgNameFromId: (organizations$ | async)"
|
||||
appStopProp
|
||||
>
|
||||
</app-org-badge>
|
||||
</td>
|
||||
<td bitCell class="tw-text-right">
|
||||
<span bitBadge variant="warning">
|
||||
{{ "reusedXTimes" | i18n: passwordUseMap.get(row.login.password) }}
|
||||
</span>
|
||||
</td>
|
||||
</ng-template>
|
||||
</bit-table>
|
||||
</bit-table-scroll>
|
||||
</ng-container>
|
||||
</div>
|
||||
<ng-template #cipherAddEdit></ng-template>
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { ComponentFixture, TestBed } from "@angular/core/testing";
|
||||
import { MockProxy, mock } from "jest-mock-extended";
|
||||
import { of } from "rxjs";
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { ComponentFixture, TestBed } from "@angular/core/testing";
|
||||
import { MockProxy, mock } from "jest-mock-extended";
|
||||
import { of } from "rxjs";
|
||||
|
||||
@@ -31,77 +31,73 @@
|
||||
</bit-toggle>
|
||||
</ng-container>
|
||||
</bit-toggle-group>
|
||||
<bit-table [dataSource]="dataSource">
|
||||
<bit-table-scroll [dataSource]="dataSource" [rowSize]="53">
|
||||
<ng-container header>
|
||||
<tr bitRow>
|
||||
<th bitCell></th>
|
||||
<th bitCell bitSortable="name">{{ "name" | i18n }}</th>
|
||||
<th bitCell bitSortable="organizationId" *ngIf="!isAdminConsoleActive">
|
||||
{{ "owner" | i18n }}
|
||||
</th>
|
||||
<th bitCell class="tw-text-right" bitSortable="score" default>
|
||||
{{ "weakness" | i18n }}
|
||||
</th>
|
||||
</tr>
|
||||
<th bitCell></th>
|
||||
<th bitCell bitSortable="name">{{ "name" | i18n }}</th>
|
||||
<th bitCell bitSortable="organizationId" *ngIf="!isAdminConsoleActive">
|
||||
{{ "owner" | i18n }}
|
||||
</th>
|
||||
<th bitCell class="tw-text-right" bitSortable="score" default>
|
||||
{{ "weakness" | i18n }}
|
||||
</th>
|
||||
</ng-container>
|
||||
<ng-template body let-rows$>
|
||||
<tr bitRow *ngFor="let r of rows$ | async">
|
||||
<td bitCell>
|
||||
<app-vault-icon [cipher]="r"></app-vault-icon>
|
||||
</td>
|
||||
<td bitCell>
|
||||
<ng-container *ngIf="!organization || canManageCipher(r); else cantManage">
|
||||
<a
|
||||
bitLink
|
||||
href="#"
|
||||
appStopClick
|
||||
(click)="selectCipher(r)"
|
||||
title="{{ 'editItemWithName' | i18n: r.name }}"
|
||||
>{{ r.name }}</a
|
||||
>
|
||||
</ng-container>
|
||||
<ng-template #cantManage>
|
||||
<span>{{ r.name }}</span>
|
||||
</ng-template>
|
||||
<ng-container *ngIf="!organization && r.organizationId">
|
||||
<i
|
||||
class="bwi bwi-collection"
|
||||
appStopProp
|
||||
title="{{ 'shared' | i18n }}"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
<span class="tw-sr-only">{{ "shared" | i18n }}</span>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="r.hasAttachments">
|
||||
<i
|
||||
class="bwi bwi-paperclip"
|
||||
appStopProp
|
||||
title="{{ 'attachments' | i18n }}"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
<span class="tw-sr-only">{{ "attachments" | i18n }}</span>
|
||||
</ng-container>
|
||||
<br />
|
||||
<small>{{ r.subTitle }}</small>
|
||||
</td>
|
||||
<td bitCell *ngIf="!isAdminConsoleActive">
|
||||
<app-org-badge
|
||||
*ngIf="!organization"
|
||||
[disabled]="disabled"
|
||||
[organizationId]="r.organizationId"
|
||||
[organizationName]="r.organizationId | orgNameFromId: (organizations$ | async)"
|
||||
appStopProp
|
||||
<ng-template bitRowDef let-row>
|
||||
<td bitCell>
|
||||
<app-vault-icon [cipher]="row"></app-vault-icon>
|
||||
</td>
|
||||
<td bitCell>
|
||||
<ng-container *ngIf="!organization || canManageCipher(row); else cantManage">
|
||||
<a
|
||||
bitLink
|
||||
href="#"
|
||||
appStopClick
|
||||
(click)="selectCipher(row)"
|
||||
title="{{ 'editItemWithName' | i18n: row.name }}"
|
||||
>{{ row.name }}</a
|
||||
>
|
||||
</app-org-badge>
|
||||
</td>
|
||||
<td bitCell class="tw-text-right">
|
||||
<span bitBadge [variant]="r.reportValue.badgeVariant">
|
||||
{{ r.reportValue.label | i18n }}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
</ng-container>
|
||||
<ng-template #cantManage>
|
||||
<span>{{ row.name }}</span>
|
||||
</ng-template>
|
||||
<ng-container *ngIf="!organization && row.organizationId">
|
||||
<i
|
||||
class="bwi bwi-collection"
|
||||
appStopProp
|
||||
title="{{ 'shared' | i18n }}"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
<span class="tw-sr-only">{{ "shared" | i18n }}</span>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="row.hasAttachments">
|
||||
<i
|
||||
class="bwi bwi-paperclip"
|
||||
appStopProp
|
||||
title="{{ 'attachments' | i18n }}"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
<span class="tw-sr-only">{{ "attachments" | i18n }}</span>
|
||||
</ng-container>
|
||||
<br />
|
||||
<small>{{ row.subTitle }}</small>
|
||||
</td>
|
||||
<td bitCell *ngIf="!isAdminConsoleActive">
|
||||
<app-org-badge
|
||||
*ngIf="!organization"
|
||||
[disabled]="disabled"
|
||||
[organizationId]="row.organizationId"
|
||||
[organizationName]="row.organizationId | orgNameFromId: (organizations$ | async)"
|
||||
appStopProp
|
||||
>
|
||||
</app-org-badge>
|
||||
</td>
|
||||
<td bitCell class="tw-text-right">
|
||||
<span bitBadge [variant]="row.reportValue.badgeVariant">
|
||||
{{ row.reportValue.label | i18n }}
|
||||
</span>
|
||||
</td>
|
||||
</ng-template>
|
||||
</bit-table>
|
||||
</bit-table-scroll>
|
||||
</ng-container>
|
||||
</div>
|
||||
<ng-template #cipherAddEdit></ng-template>
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { ComponentFixture, TestBed } from "@angular/core/testing";
|
||||
import { mock, MockProxy } from "jest-mock-extended";
|
||||
import { of } from "rxjs";
|
||||
|
||||
@@ -39,7 +39,6 @@ import { SendAccessTextComponent } from "./send-access-text.component";
|
||||
NoItemsModule,
|
||||
],
|
||||
})
|
||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
|
||||
export class AccessComponent implements OnInit {
|
||||
protected send: SendAccessView;
|
||||
protected sendType = SendType;
|
||||
|
||||
@@ -7,6 +7,7 @@ import { lastValueFrom } from "rxjs";
|
||||
import { SendComponent as BaseSendComponent } from "@bitwarden/angular/tools/send/send.component";
|
||||
import { SearchService } from "@bitwarden/common/abstractions/search.service";
|
||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service";
|
||||
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
@@ -74,6 +75,7 @@ export class SendComponent extends BaseSendComponent implements OnInit, OnDestro
|
||||
dialogService: DialogService,
|
||||
toastService: ToastService,
|
||||
private addEditFormConfigService: DefaultSendFormConfigService,
|
||||
accountService: AccountService,
|
||||
) {
|
||||
super(
|
||||
sendService,
|
||||
@@ -87,6 +89,7 @@ export class SendComponent extends BaseSendComponent implements OnInit, OnDestro
|
||||
sendApiService,
|
||||
dialogService,
|
||||
toastService,
|
||||
accountService,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -13,12 +13,27 @@ import { BrowserExtensionPromptComponent } from "./browser-extension-prompt.comp
|
||||
|
||||
describe("BrowserExtensionPromptComponent", () => {
|
||||
let fixture: ComponentFixture<BrowserExtensionPromptComponent>;
|
||||
|
||||
let component: BrowserExtensionPromptComponent;
|
||||
const start = jest.fn();
|
||||
const pageState$ = new BehaviorSubject(BrowserPromptState.Loading);
|
||||
const setAttribute = jest.fn();
|
||||
const getAttribute = jest.fn().mockReturnValue("width=1010");
|
||||
|
||||
beforeEach(async () => {
|
||||
start.mockClear();
|
||||
setAttribute.mockClear();
|
||||
getAttribute.mockClear();
|
||||
|
||||
// Store original querySelector
|
||||
const originalQuerySelector = document.querySelector.bind(document);
|
||||
|
||||
// Mock querySelector while preserving the document context
|
||||
jest.spyOn(document, "querySelector").mockImplementation(function (selector) {
|
||||
if (selector === 'meta[name="viewport"]') {
|
||||
return { setAttribute, getAttribute } as unknown as HTMLMetaElement;
|
||||
}
|
||||
return originalQuerySelector.call(document, selector);
|
||||
});
|
||||
|
||||
await TestBed.configureTestingModule({
|
||||
providers: [
|
||||
@@ -34,9 +49,14 @@ describe("BrowserExtensionPromptComponent", () => {
|
||||
}).compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(BrowserExtensionPromptComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
it("calls start on initialization", () => {
|
||||
expect(start).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
@@ -87,6 +107,33 @@ describe("BrowserExtensionPromptComponent", () => {
|
||||
const mobileText = fixture.debugElement.query(By.css("p")).nativeElement;
|
||||
expect(mobileText.textContent.trim()).toBe("reopenLinkOnDesktop");
|
||||
});
|
||||
|
||||
it("sets min-width on the body", () => {
|
||||
expect(document.body.style.minWidth).toBe("auto");
|
||||
});
|
||||
|
||||
it("stores viewport content", () => {
|
||||
expect(getAttribute).toHaveBeenCalledWith("content");
|
||||
expect(component["viewportContent"]).toBe("width=1010");
|
||||
});
|
||||
|
||||
it("sets viewport meta tag to be mobile friendly", () => {
|
||||
expect(setAttribute).toHaveBeenCalledWith("content", "width=device-width, initial-scale=1.0");
|
||||
});
|
||||
|
||||
describe("on destroy", () => {
|
||||
beforeEach(() => {
|
||||
fixture.destroy();
|
||||
});
|
||||
|
||||
it("resets body min-width", () => {
|
||||
expect(document.body.style.minWidth).toBe("");
|
||||
});
|
||||
|
||||
it("resets viewport meta tag", () => {
|
||||
expect(setAttribute).toHaveBeenCalledWith("content", "width=1010");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("manual error state", () => {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { Component, OnInit } from "@angular/core";
|
||||
import { CommonModule, DOCUMENT } from "@angular/common";
|
||||
import { Component, Inject, OnDestroy, OnInit } from "@angular/core";
|
||||
|
||||
import { ButtonComponent, IconModule } from "@bitwarden/components";
|
||||
import { I18nPipe } from "@bitwarden/ui-common";
|
||||
@@ -16,7 +16,7 @@ import {
|
||||
standalone: true,
|
||||
imports: [CommonModule, I18nPipe, ButtonComponent, IconModule],
|
||||
})
|
||||
export class BrowserExtensionPromptComponent implements OnInit {
|
||||
export class BrowserExtensionPromptComponent implements OnInit, OnDestroy {
|
||||
/** Current state of the prompt page */
|
||||
protected pageState$ = this.browserExtensionPromptService.pageState$;
|
||||
|
||||
@@ -25,10 +25,39 @@ export class BrowserExtensionPromptComponent implements OnInit {
|
||||
|
||||
protected BitwardenIcon = VaultIcons.BitwardenIcon;
|
||||
|
||||
constructor(private browserExtensionPromptService: BrowserExtensionPromptService) {}
|
||||
/** Content of the meta[name="viewport"] element */
|
||||
private viewportContent: string | null = null;
|
||||
|
||||
constructor(
|
||||
private browserExtensionPromptService: BrowserExtensionPromptService,
|
||||
@Inject(DOCUMENT) private document: Document,
|
||||
) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.browserExtensionPromptService.start();
|
||||
|
||||
// It is not be uncommon for users to hit this page from a mobile device.
|
||||
// There are global styles and the viewport meta tag that set a min-width
|
||||
// for the page which cause it to render poorly. Remove them here.
|
||||
// https://github.com/bitwarden/clients/blob/main/apps/web/src/scss/base.scss#L6
|
||||
this.document.body.style.minWidth = "auto";
|
||||
|
||||
const viewportMeta = this.document.querySelector('meta[name="viewport"]');
|
||||
|
||||
// Save the current viewport content to reset it when the component is destroyed
|
||||
this.viewportContent = viewportMeta?.getAttribute("content") ?? null;
|
||||
viewportMeta?.setAttribute("content", "width=device-width, initial-scale=1.0");
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
// Reset the body min-width when the component is destroyed
|
||||
this.document.body.style.minWidth = "";
|
||||
|
||||
if (this.viewportContent !== null) {
|
||||
this.document
|
||||
.querySelector('meta[name="viewport"]')
|
||||
?.setAttribute("content", this.viewportContent);
|
||||
}
|
||||
}
|
||||
|
||||
openExtension(): void {
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
import { NgModule } from "@angular/core";
|
||||
|
||||
import { SelectModule } from "@bitwarden/components";
|
||||
|
||||
import { AccessSelectorModule } from "../../../admin-console/organizations/shared/components/access-selector/access-selector.module";
|
||||
import { SharedModule } from "../../../shared";
|
||||
|
||||
import { CollectionDialogComponent } from "./collection-dialog.component";
|
||||
@NgModule({
|
||||
imports: [SharedModule, AccessSelectorModule, SelectModule],
|
||||
declarations: [CollectionDialogComponent],
|
||||
exports: [CollectionDialogComponent],
|
||||
})
|
||||
export class CollectionDialogModule {}
|
||||
@@ -15,6 +15,8 @@ import { AccountService } from "@bitwarden/common/auth/abstractions/account.serv
|
||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions";
|
||||
import { EventType } from "@bitwarden/common/enums";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||
@@ -231,7 +233,10 @@ export class VaultItemDialogComponent implements OnInit, OnDestroy {
|
||||
* A user may restore items if they have delete permissions and the item is in the trash.
|
||||
*/
|
||||
protected async canUserRestore() {
|
||||
return this.isTrashFilter && this.cipher?.isDeleted && this.canDelete;
|
||||
const featureFlagEnabled = await firstValueFrom(this.limitItemDeletion$);
|
||||
return this.isTrashFilter && this.cipher?.isDeleted && featureFlagEnabled
|
||||
? this.cipher?.permissions.restore
|
||||
: this.canDelete;
|
||||
}
|
||||
|
||||
protected showRestore: boolean;
|
||||
@@ -277,6 +282,8 @@ export class VaultItemDialogComponent implements OnInit, OnDestroy {
|
||||
|
||||
protected canDelete = false;
|
||||
|
||||
protected limitItemDeletion$ = this.configService.getFeatureFlag$(FeatureFlag.LimitItemDeletion);
|
||||
|
||||
constructor(
|
||||
@Inject(DIALOG_DATA) protected params: VaultItemDialogParams,
|
||||
private dialogRef: DialogRef<VaultItemDialogResult>,
|
||||
@@ -294,6 +301,7 @@ export class VaultItemDialogComponent implements OnInit, OnDestroy {
|
||||
private apiService: ApiService,
|
||||
private eventCollectionService: EventCollectionService,
|
||||
private routedVaultFilterService: RoutedVaultFilterService,
|
||||
private configService: ConfigService,
|
||||
) {
|
||||
this.updateTitle();
|
||||
}
|
||||
|
||||
@@ -86,7 +86,12 @@
|
||||
appStopProp
|
||||
></button>
|
||||
<bit-menu #corruptedCipherOptions>
|
||||
<button bitMenuItem *ngIf="canManageCollection" (click)="deleteCipher()" type="button">
|
||||
<button
|
||||
bitMenuItem
|
||||
*ngIf="(limitItemDeletion$ | async) ? canDeleteCipher : canManageCollection"
|
||||
(click)="deleteCipher()"
|
||||
type="button"
|
||||
>
|
||||
<span class="tw-text-danger">
|
||||
<i class="bwi bwi-fw bwi-trash" aria-hidden="true"></i>
|
||||
{{ (cipher.isDeleted ? "permanentlyDelete" : "delete") | i18n }}
|
||||
@@ -151,11 +156,21 @@
|
||||
<i class="bwi bwi-fw bwi-file-text" aria-hidden="true"></i>
|
||||
{{ "eventLogs" | i18n }}
|
||||
</button>
|
||||
<button bitMenuItem (click)="restore()" type="button" *ngIf="cipher.isDeleted">
|
||||
<button
|
||||
bitMenuItem
|
||||
(click)="restore()"
|
||||
type="button"
|
||||
*ngIf="(limitItemDeletion$ | async) ? cipher.isDeleted && canRestoreCipher : cipher.isDeleted"
|
||||
>
|
||||
<i class="bwi bwi-fw bwi-undo" aria-hidden="true"></i>
|
||||
{{ "restore" | i18n }}
|
||||
</button>
|
||||
<button bitMenuItem *ngIf="canManageCollection" (click)="deleteCipher()" type="button">
|
||||
<button
|
||||
bitMenuItem
|
||||
*ngIf="(limitItemDeletion$ | async) ? canDeleteCipher : canManageCollection"
|
||||
(click)="deleteCipher()"
|
||||
type="button"
|
||||
>
|
||||
<span class="tw-text-danger">
|
||||
<i class="bwi bwi-fw bwi-trash" aria-hidden="true"></i>
|
||||
{{ (cipher.isDeleted ? "permanentlyDelete" : "delete") | i18n }}
|
||||
|
||||
@@ -4,6 +4,8 @@ import { Component, EventEmitter, Input, OnInit, Output } from "@angular/core";
|
||||
|
||||
import { CollectionView } from "@bitwarden/admin-console/common";
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
@@ -36,12 +38,21 @@ export class VaultCipherRowComponent implements OnInit {
|
||||
@Input() canEditCipher: boolean;
|
||||
@Input() canAssignCollections: boolean;
|
||||
@Input() canManageCollection: boolean;
|
||||
/**
|
||||
* uses new permission delete logic from PM-15493
|
||||
*/
|
||||
@Input() canDeleteCipher: boolean;
|
||||
/**
|
||||
* uses new permission restore logic from PM-15493
|
||||
*/
|
||||
@Input() canRestoreCipher: boolean;
|
||||
|
||||
@Output() onEvent = new EventEmitter<VaultItemEvent>();
|
||||
|
||||
@Input() checked: boolean;
|
||||
@Output() checkedToggled = new EventEmitter<void>();
|
||||
|
||||
protected limitItemDeletion$ = this.configService.getFeatureFlag$(FeatureFlag.LimitItemDeletion);
|
||||
protected CipherType = CipherType;
|
||||
private permissionList = getPermissionList();
|
||||
private permissionPriority = [
|
||||
@@ -53,7 +64,10 @@ export class VaultCipherRowComponent implements OnInit {
|
||||
];
|
||||
protected organization?: Organization;
|
||||
|
||||
constructor(private i18nService: I18nService) {}
|
||||
constructor(
|
||||
private i18nService: I18nService,
|
||||
private configService: ConfigService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Lifecycle hook for component initialization.
|
||||
|
||||
@@ -86,11 +86,23 @@
|
||||
<i class="bwi bwi-fw bwi-collection" aria-hidden="true"></i>
|
||||
{{ "assignToCollections" | i18n }}
|
||||
</button>
|
||||
<button *ngIf="showBulkTrashOptions" type="button" bitMenuItem (click)="bulkRestore()">
|
||||
<button
|
||||
*ngIf="
|
||||
(limitItemDeletion$ | async) ? (canRestoreSelected$ | async) : showBulkTrashOptions
|
||||
"
|
||||
type="button"
|
||||
bitMenuItem
|
||||
(click)="bulkRestore()"
|
||||
>
|
||||
<i class="bwi bwi-fw bwi-undo" aria-hidden="true"></i>
|
||||
{{ "restoreSelected" | i18n }}
|
||||
</button>
|
||||
<button *ngIf="showDelete" type="button" bitMenuItem (click)="bulkDelete()">
|
||||
<button
|
||||
*ngIf="(limitItemDeletion$ | async) ? (canDeleteSelected$ | async) : showDelete"
|
||||
type="button"
|
||||
bitMenuItem
|
||||
(click)="bulkDelete()"
|
||||
>
|
||||
<span class="tw-text-danger">
|
||||
<i class="bwi bwi-fw bwi-trash" aria-hidden="true"></i>
|
||||
{{ (showBulkTrashOptions ? "permanentlyDeleteSelected" : "delete") | i18n }}
|
||||
@@ -146,6 +158,16 @@
|
||||
[canEditCipher]="canEditCipher(item.cipher)"
|
||||
[canAssignCollections]="canAssignCollections(item.cipher)"
|
||||
[canManageCollection]="canManageCollection(item.cipher)"
|
||||
[canDeleteCipher]="
|
||||
cipherAuthorizationService.canDeleteCipher$(
|
||||
item.cipher,
|
||||
[item.cipher.collectionId],
|
||||
showAdminActions
|
||||
) | async
|
||||
"
|
||||
[canRestoreCipher]="
|
||||
cipherAuthorizationService.canRestoreCipher$(item.cipher, showAdminActions) | async
|
||||
"
|
||||
(checkedToggled)="selection.toggle(item)"
|
||||
(onEvent)="event($event)"
|
||||
></tr>
|
||||
|
||||
@@ -2,10 +2,14 @@
|
||||
// @ts-strict-ignore
|
||||
import { SelectionModel } from "@angular/cdk/collections";
|
||||
import { Component, EventEmitter, Input, Output } from "@angular/core";
|
||||
import { Observable, combineLatest, map, of, startWith, switchMap } from "rxjs";
|
||||
|
||||
import { CollectionView, Unassigned, CollectionAdminView } from "@bitwarden/admin-console/common";
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
import { CipherAuthorizationService } from "@bitwarden/common/vault/services/cipher-authorization.service";
|
||||
import { SortDirection, TableDataSource } from "@bitwarden/components";
|
||||
|
||||
import { GroupView } from "../../../admin-console/organizations/core";
|
||||
@@ -75,9 +79,67 @@ export class VaultItemsComponent {
|
||||
|
||||
@Output() onEvent = new EventEmitter<VaultItemEvent>();
|
||||
|
||||
protected limitItemDeletion$ = this.configService.getFeatureFlag$(FeatureFlag.LimitItemDeletion);
|
||||
protected editableItems: VaultItem[] = [];
|
||||
protected dataSource = new TableDataSource<VaultItem>();
|
||||
protected selection = new SelectionModel<VaultItem>(true, [], true);
|
||||
protected canDeleteSelected$: Observable<boolean>;
|
||||
protected canRestoreSelected$: Observable<boolean>;
|
||||
|
||||
constructor(
|
||||
protected cipherAuthorizationService: CipherAuthorizationService,
|
||||
private configService: ConfigService,
|
||||
) {
|
||||
this.canDeleteSelected$ = this.selection.changed.pipe(
|
||||
startWith(null),
|
||||
switchMap(() => {
|
||||
const ciphers = this.selection.selected
|
||||
.filter((item) => item.cipher)
|
||||
.map((item) => item.cipher);
|
||||
|
||||
if (this.selection.selected.length === 0) {
|
||||
return of(true);
|
||||
}
|
||||
|
||||
const canDeleteCiphers$ = ciphers.map((c) =>
|
||||
cipherAuthorizationService.canDeleteCipher$(c, [], this.showAdminActions),
|
||||
);
|
||||
|
||||
const canDeleteCollections = this.selection.selected
|
||||
.filter((item) => item.collection)
|
||||
.every((item) => item.collection && this.canDeleteCollection(item.collection));
|
||||
|
||||
const canDelete$ = combineLatest(canDeleteCiphers$).pipe(
|
||||
map((results) => results.every((item) => item) && canDeleteCollections),
|
||||
);
|
||||
|
||||
return canDelete$;
|
||||
}),
|
||||
);
|
||||
|
||||
this.canRestoreSelected$ = this.selection.changed.pipe(
|
||||
startWith(null),
|
||||
switchMap(() => {
|
||||
const ciphers = this.selection.selected
|
||||
.filter((item) => item.cipher)
|
||||
.map((item) => item.cipher);
|
||||
|
||||
if (this.selection.selected.length === 0) {
|
||||
return of(true);
|
||||
}
|
||||
|
||||
const canRestoreCiphers$ = ciphers.map((c) =>
|
||||
cipherAuthorizationService.canRestoreCipher$(c, this.showAdminActions),
|
||||
);
|
||||
|
||||
const canRestore$ = combineLatest(canRestoreCiphers$).pipe(
|
||||
map((results) => results.every((item) => item)),
|
||||
);
|
||||
|
||||
return canRestore$;
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
get showExtraColumn() {
|
||||
return this.showCollections || this.showGroups || this.showOwner;
|
||||
@@ -99,6 +161,7 @@ export class VaultItemsComponent {
|
||||
);
|
||||
}
|
||||
|
||||
//@TODO: remove this function when removing the limitItemDeletion$ feature flag.
|
||||
get showDelete(): boolean {
|
||||
if (this.selection.selected.length === 0) {
|
||||
return true;
|
||||
|
||||
@@ -5,7 +5,7 @@ import { RouterModule } from "@angular/router";
|
||||
|
||||
import { TableModule } from "@bitwarden/components";
|
||||
|
||||
import { CollectionBadgeModule } from "../../../admin-console/organizations/collections/collection-badge/collection-badge.module";
|
||||
import { CollectionNameBadgeComponent } from "../../../admin-console/organizations/collections";
|
||||
import { GroupBadgeModule } from "../../../admin-console/organizations/collections/group-badge/group-badge.module";
|
||||
import { SharedModule } from "../../../shared/shared.module";
|
||||
import { OrganizationBadgeModule } from "../../individual-vault/organization-badge/organization-badge.module";
|
||||
@@ -23,7 +23,7 @@ import { VaultItemsComponent } from "./vault-items.component";
|
||||
SharedModule,
|
||||
TableModule,
|
||||
OrganizationBadgeModule,
|
||||
CollectionBadgeModule,
|
||||
CollectionNameBadgeComponent,
|
||||
GroupBadgeModule,
|
||||
PipesModule,
|
||||
],
|
||||
|
||||
@@ -17,7 +17,10 @@ import { AvatarService } from "@bitwarden/common/auth/abstractions/avatar.servic
|
||||
import { TokenService } from "@bitwarden/common/auth/abstractions/token.service";
|
||||
import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||
import {
|
||||
Environment,
|
||||
EnvironmentService,
|
||||
} from "@bitwarden/common/platform/abstractions/environment.service";
|
||||
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||
@@ -25,6 +28,7 @@ import { AttachmentView } from "@bitwarden/common/vault/models/view/attachment.v
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
import { LoginUriView } from "@bitwarden/common/vault/models/view/login-uri.view";
|
||||
import { LoginView } from "@bitwarden/common/vault/models/view/login.view";
|
||||
import { CipherAuthorizationService } from "@bitwarden/common/vault/services/cipher-authorization.service";
|
||||
|
||||
import { GroupView } from "../../../admin-console/organizations/core";
|
||||
import { PreloadedEnglishI18nModule } from "../../../core/tests";
|
||||
@@ -53,6 +57,11 @@ export default {
|
||||
getIconsUrl() {
|
||||
return "";
|
||||
},
|
||||
environment$: new BehaviorSubject({
|
||||
getIconsUrl() {
|
||||
return "";
|
||||
},
|
||||
} as Environment).asObservable(),
|
||||
} as Partial<EnvironmentService>,
|
||||
},
|
||||
{
|
||||
@@ -96,12 +105,20 @@ export default {
|
||||
{
|
||||
provide: ConfigService,
|
||||
useValue: {
|
||||
getFeatureFlag() {
|
||||
getFeatureFlag$() {
|
||||
// does not currently affect any display logic, default all to OFF
|
||||
return false;
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: CipherAuthorizationService,
|
||||
useValue: {
|
||||
canDeleteCipher$() {
|
||||
return of(true);
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
applicationConfig({
|
||||
|
||||
@@ -15,7 +15,6 @@ import { isCardExpired } from "@bitwarden/common/autofill/utils";
|
||||
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
|
||||
import { ProductTierType } from "@bitwarden/common/billing/enums";
|
||||
import { EventType } from "@bitwarden/common/enums";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
@@ -30,7 +29,7 @@ import { Launchable } from "@bitwarden/common/vault/interfaces/launchable";
|
||||
import { CipherAuthorizationService } from "@bitwarden/common/vault/services/cipher-authorization.service";
|
||||
import { DialogService, ToastService } from "@bitwarden/components";
|
||||
import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy";
|
||||
import { PasswordRepromptService } from "@bitwarden/vault";
|
||||
import { PasswordRepromptService, SshImportPromptService } from "@bitwarden/vault";
|
||||
|
||||
@Component({
|
||||
selector: "app-vault-add-edit",
|
||||
@@ -76,6 +75,7 @@ export class AddEditComponent extends BaseAddEditComponent implements OnInit, On
|
||||
cipherAuthorizationService: CipherAuthorizationService,
|
||||
toastService: ToastService,
|
||||
sdkService: SdkService,
|
||||
sshImportPromptService: SshImportPromptService,
|
||||
) {
|
||||
super(
|
||||
cipherService,
|
||||
@@ -98,6 +98,7 @@ export class AddEditComponent extends BaseAddEditComponent implements OnInit, On
|
||||
cipherAuthorizationService,
|
||||
toastService,
|
||||
sdkService,
|
||||
sshImportPromptService,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -105,17 +106,6 @@ export class AddEditComponent extends BaseAddEditComponent implements OnInit, On
|
||||
await super.ngOnInit();
|
||||
await this.load();
|
||||
|
||||
// https://bitwarden.atlassian.net/browse/PM-10413
|
||||
// cannot generate ssh keys so block creation
|
||||
if (
|
||||
this.type === CipherType.SshKey &&
|
||||
this.cipherId == null &&
|
||||
!(await this.configService.getFeatureFlag(FeatureFlag.SSHKeyVaultItem))
|
||||
) {
|
||||
this.type = CipherType.Login;
|
||||
this.cipher.type = CipherType.Login;
|
||||
}
|
||||
|
||||
this.viewOnly = !this.cipher.edit && this.editMode;
|
||||
// remove when all the title for all clients are updated to New Item
|
||||
if (this.cloneMode || !this.editMode) {
|
||||
@@ -135,12 +125,15 @@ export class AddEditComponent extends BaseAddEditComponent implements OnInit, On
|
||||
|
||||
if (this.showTotp()) {
|
||||
await this.totpUpdateCode();
|
||||
const interval = this.totpService.getTimeInterval(this.cipher.login.totp);
|
||||
await this.totpTick(interval);
|
||||
|
||||
this.totpInterval = window.setInterval(async () => {
|
||||
const totpResponse = await firstValueFrom(this.totpService.getCode$(this.cipher.login.totp));
|
||||
if (totpResponse) {
|
||||
const interval = totpResponse.period;
|
||||
await this.totpTick(interval);
|
||||
}, 1000);
|
||||
|
||||
this.totpInterval = window.setInterval(async () => {
|
||||
await this.totpTick(interval);
|
||||
}, 1000);
|
||||
}
|
||||
}
|
||||
|
||||
this.cardIsExpired = isCardExpired(this.cipher.card);
|
||||
@@ -273,7 +266,8 @@ export class AddEditComponent extends BaseAddEditComponent implements OnInit, On
|
||||
return;
|
||||
}
|
||||
|
||||
this.totpCode = await this.totpService.getCode(this.cipher.login.totp);
|
||||
const totpResponse = await firstValueFrom(this.totpService.getCode$(this.cipher.login.totp));
|
||||
this.totpCode = totpResponse?.code;
|
||||
if (this.totpCode != null) {
|
||||
if (this.totpCode.length > 4) {
|
||||
const half = Math.floor(this.totpCode.length / 2);
|
||||
|
||||
@@ -4,11 +4,10 @@ import { SharedModule } from "../../../shared";
|
||||
|
||||
import { BulkDeleteDialogComponent } from "./bulk-delete-dialog/bulk-delete-dialog.component";
|
||||
import { BulkMoveDialogComponent } from "./bulk-move-dialog/bulk-move-dialog.component";
|
||||
import { BulkShareDialogComponent } from "./bulk-share-dialog/bulk-share-dialog.component";
|
||||
|
||||
@NgModule({
|
||||
imports: [SharedModule],
|
||||
declarations: [BulkDeleteDialogComponent, BulkMoveDialogComponent, BulkShareDialogComponent],
|
||||
exports: [BulkDeleteDialogComponent, BulkMoveDialogComponent, BulkShareDialogComponent],
|
||||
declarations: [BulkDeleteDialogComponent, BulkMoveDialogComponent],
|
||||
exports: [BulkDeleteDialogComponent, BulkMoveDialogComponent],
|
||||
})
|
||||
export class BulkDialogsModule {}
|
||||
|
||||
@@ -1,73 +0,0 @@
|
||||
<bit-dialog>
|
||||
<span bitDialogTitle>
|
||||
{{ "moveSelectedToOrg" | i18n }}
|
||||
</span>
|
||||
<span bitDialogContent>
|
||||
<p>{{ "moveManyToOrgDesc" | i18n }}</p>
|
||||
<p>
|
||||
{{
|
||||
"moveSelectedItemsCountDesc"
|
||||
| i18n: this.ciphers.length : shareableCiphers.length : nonShareableCount
|
||||
}}
|
||||
</p>
|
||||
<bit-form-field>
|
||||
<bit-label for="organization">{{ "organization" | i18n }}</bit-label>
|
||||
<select
|
||||
bitInput
|
||||
[(ngModel)]="organizationId"
|
||||
id="organization"
|
||||
(change)="filterCollections()"
|
||||
>
|
||||
<option *ngFor="let o of organizations" [ngValue]="o.id">{{ o.name }}</option>
|
||||
</select>
|
||||
</bit-form-field>
|
||||
|
||||
<div class="d-flex">
|
||||
<label class="tw-mb-1 tw-block tw-font-semibold tw-text-main">{{
|
||||
"collections" | i18n
|
||||
}}</label>
|
||||
<div class="tw-ml-auto tw-flex tw-gap-2" *ngIf="collections && collections.length">
|
||||
<button bitLink type="button" (click)="selectAll(true)" class="tw-px-2">
|
||||
{{ "selectAll" | i18n }}
|
||||
</button>
|
||||
<button bitLink type="button" (click)="selectAll(false)" class="tw-px-2">
|
||||
{{ "unselectAll" | i18n }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div *ngIf="!collections || !collections.length">
|
||||
{{ "noCollectionsInList" | i18n }}
|
||||
</div>
|
||||
<table
|
||||
class="table table-hover table-list mb-0"
|
||||
*ngIf="collections && collections.length"
|
||||
id="collections"
|
||||
>
|
||||
<tbody>
|
||||
<tr *ngFor="let c of collections; let i = index" (click)="check(c)">
|
||||
<td class="table-list-checkbox">
|
||||
<input
|
||||
bitInput
|
||||
type="checkbox"
|
||||
[(ngModel)]="c.checked"
|
||||
name="Collection[{{ i }}].Checked"
|
||||
attr.aria-label="Check {{ c.name }}"
|
||||
appStopProp
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
{{ c.name }}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</span>
|
||||
<ng-container bitDialogFooter>
|
||||
<button bitButton type="submit" buttonType="primary" [bitAction]="submit">
|
||||
{{ "save" | i18n }}
|
||||
</button>
|
||||
<button bitButton type="button" buttonType="secondary" (click)="cancel()">
|
||||
{{ "cancel" | i18n }}
|
||||
</button>
|
||||
</ng-container>
|
||||
</bit-dialog>
|
||||
@@ -1,160 +0,0 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { DialogConfig, DialogRef, DIALOG_DATA } from "@angular/cdk/dialog";
|
||||
import { Component, Inject, OnDestroy, OnInit } from "@angular/core";
|
||||
import { firstValueFrom, map } from "rxjs";
|
||||
|
||||
import { CollectionService, CollectionView } from "@bitwarden/admin-console/common";
|
||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { Checkable, isChecked } from "@bitwarden/common/types/checkable";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
import { DialogService, ToastService } from "@bitwarden/components";
|
||||
|
||||
export interface BulkShareDialogParams {
|
||||
ciphers: CipherView[];
|
||||
organizationId?: string;
|
||||
}
|
||||
|
||||
export enum BulkShareDialogResult {
|
||||
Shared = "shared",
|
||||
Canceled = "canceled",
|
||||
}
|
||||
|
||||
/**
|
||||
* Strongly typed helper to open a BulkShareDialog
|
||||
* @param dialogService Instance of the dialog service that will be used to open the dialog
|
||||
* @param config Configuration for the dialog
|
||||
*/
|
||||
export const openBulkShareDialog = (
|
||||
dialogService: DialogService,
|
||||
config: DialogConfig<BulkShareDialogParams>,
|
||||
) => {
|
||||
return dialogService.open<BulkShareDialogResult, BulkShareDialogParams>(
|
||||
BulkShareDialogComponent,
|
||||
config,
|
||||
);
|
||||
};
|
||||
|
||||
@Component({
|
||||
templateUrl: "bulk-share-dialog.component.html",
|
||||
})
|
||||
export class BulkShareDialogComponent implements OnInit, OnDestroy {
|
||||
ciphers: CipherView[] = [];
|
||||
organizationId: string;
|
||||
|
||||
nonShareableCount = 0;
|
||||
collections: Checkable<CollectionView>[] = [];
|
||||
organizations: Organization[] = [];
|
||||
shareableCiphers: CipherView[] = [];
|
||||
|
||||
private writeableCollections: CollectionView[] = [];
|
||||
|
||||
constructor(
|
||||
@Inject(DIALOG_DATA) params: BulkShareDialogParams,
|
||||
private dialogRef: DialogRef<BulkShareDialogResult>,
|
||||
private cipherService: CipherService,
|
||||
private i18nService: I18nService,
|
||||
private collectionService: CollectionService,
|
||||
private organizationService: OrganizationService,
|
||||
private logService: LogService,
|
||||
private accountService: AccountService,
|
||||
private toastService: ToastService,
|
||||
) {
|
||||
this.ciphers = params.ciphers ?? [];
|
||||
this.organizationId = params.organizationId;
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
this.shareableCiphers = this.ciphers.filter(
|
||||
(c) => !c.hasOldAttachments && c.organizationId == null,
|
||||
);
|
||||
this.nonShareableCount = this.ciphers.length - this.shareableCiphers.length;
|
||||
const allCollections = await this.collectionService.getAllDecrypted();
|
||||
this.writeableCollections = allCollections.filter((c) => !c.readOnly);
|
||||
const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$));
|
||||
this.organizations = await firstValueFrom(this.organizationService.organizations$(userId));
|
||||
if (this.organizationId == null && this.organizations.length > 0) {
|
||||
this.organizationId = this.organizations[0].id;
|
||||
}
|
||||
this.filterCollections();
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.selectAll(false);
|
||||
}
|
||||
|
||||
filterCollections() {
|
||||
this.selectAll(false);
|
||||
if (this.organizationId == null || this.writeableCollections.length === 0) {
|
||||
this.collections = [];
|
||||
} else {
|
||||
this.collections = this.writeableCollections.filter(
|
||||
(c) => c.organizationId === this.organizationId,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
submit = async () => {
|
||||
const checkedCollectionIds = this.collections.filter(isChecked).map((c) => c.id);
|
||||
try {
|
||||
const activeUserId = await firstValueFrom(
|
||||
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
|
||||
);
|
||||
await this.cipherService.shareManyWithServer(
|
||||
this.shareableCiphers,
|
||||
this.organizationId,
|
||||
checkedCollectionIds,
|
||||
activeUserId,
|
||||
);
|
||||
const orgName =
|
||||
this.organizations.find((o) => o.id === this.organizationId)?.name ??
|
||||
this.i18nService.t("organization");
|
||||
this.toastService.showToast({
|
||||
variant: "success",
|
||||
title: null,
|
||||
message: this.i18nService.t("movedItemsToOrg", orgName),
|
||||
});
|
||||
this.close(BulkShareDialogResult.Shared);
|
||||
} catch (e) {
|
||||
this.logService.error(e);
|
||||
}
|
||||
};
|
||||
|
||||
check(c: Checkable<CollectionView>, select?: boolean) {
|
||||
c.checked = select == null ? !c.checked : select;
|
||||
}
|
||||
|
||||
selectAll(select: boolean) {
|
||||
const collections = select ? this.collections : this.writeableCollections;
|
||||
collections.forEach((c) => this.check(c, select));
|
||||
}
|
||||
|
||||
get canSave() {
|
||||
if (
|
||||
this.shareableCiphers != null &&
|
||||
this.shareableCiphers.length > 0 &&
|
||||
this.collections != null
|
||||
) {
|
||||
for (let i = 0; i < this.collections.length; i++) {
|
||||
if (this.collections[i].checked) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
protected cancel() {
|
||||
this.close(BulkShareDialogResult.Canceled);
|
||||
}
|
||||
|
||||
private close(result: BulkShareDialogResult) {
|
||||
this.dialogRef.close(result);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
<bit-dialog dialogSize="small" background="alt">
|
||||
<bit-dialog background="alt">
|
||||
<span bitDialogTitle>
|
||||
{{ "passwordHistory" | i18n }}
|
||||
</span>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user