mirror of
https://github.com/bitwarden/browser.git
synced 2024-12-22 16:29:09 +01:00
[AC-1586] individual reports filter (#8598)
* add filtering to individual reports
This commit is contained in:
parent
7e9ab6a15b
commit
8ae71fabaf
@ -4,6 +4,7 @@ import { ActivatedRoute } from "@angular/router";
|
|||||||
import { ModalService } from "@bitwarden/angular/services/modal.service";
|
import { ModalService } from "@bitwarden/angular/services/modal.service";
|
||||||
import { AuditService } from "@bitwarden/common/abstractions/audit.service";
|
import { AuditService } from "@bitwarden/common/abstractions/audit.service";
|
||||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||||
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||||
import { Cipher } from "@bitwarden/common/vault/models/domain/cipher";
|
import { Cipher } from "@bitwarden/common/vault/models/domain/cipher";
|
||||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||||
@ -27,11 +28,20 @@ export class ExposedPasswordsReportComponent extends BaseExposedPasswordsReportC
|
|||||||
organizationService: OrganizationService,
|
organizationService: OrganizationService,
|
||||||
private route: ActivatedRoute,
|
private route: ActivatedRoute,
|
||||||
passwordRepromptService: PasswordRepromptService,
|
passwordRepromptService: PasswordRepromptService,
|
||||||
|
i18nService: I18nService,
|
||||||
) {
|
) {
|
||||||
super(cipherService, auditService, organizationService, modalService, passwordRepromptService);
|
super(
|
||||||
|
cipherService,
|
||||||
|
auditService,
|
||||||
|
organizationService,
|
||||||
|
modalService,
|
||||||
|
passwordRepromptService,
|
||||||
|
i18nService,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async ngOnInit() {
|
async ngOnInit() {
|
||||||
|
this.isAdminConsoleActive = true;
|
||||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe
|
// eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe
|
||||||
this.route.parent.parent.params.subscribe(async (params) => {
|
this.route.parent.parent.params.subscribe(async (params) => {
|
||||||
this.organization = await this.organizationService.get(params.organizationId);
|
this.organization = await this.organizationService.get(params.organizationId);
|
||||||
|
@ -3,6 +3,7 @@ import { ActivatedRoute } from "@angular/router";
|
|||||||
|
|
||||||
import { ModalService } from "@bitwarden/angular/services/modal.service";
|
import { ModalService } from "@bitwarden/angular/services/modal.service";
|
||||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||||
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||||
@ -24,11 +25,20 @@ export class InactiveTwoFactorReportComponent extends BaseInactiveTwoFactorRepor
|
|||||||
logService: LogService,
|
logService: LogService,
|
||||||
passwordRepromptService: PasswordRepromptService,
|
passwordRepromptService: PasswordRepromptService,
|
||||||
organizationService: OrganizationService,
|
organizationService: OrganizationService,
|
||||||
|
i18nService: I18nService,
|
||||||
) {
|
) {
|
||||||
super(cipherService, organizationService, modalService, logService, passwordRepromptService);
|
super(
|
||||||
|
cipherService,
|
||||||
|
organizationService,
|
||||||
|
modalService,
|
||||||
|
logService,
|
||||||
|
passwordRepromptService,
|
||||||
|
i18nService,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async ngOnInit() {
|
async ngOnInit() {
|
||||||
|
this.isAdminConsoleActive = true;
|
||||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe
|
// eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe
|
||||||
this.route.parent.parent.params.subscribe(async (params) => {
|
this.route.parent.parent.params.subscribe(async (params) => {
|
||||||
this.organization = await this.organizationService.get(params.organizationId);
|
this.organization = await this.organizationService.get(params.organizationId);
|
||||||
|
@ -3,6 +3,7 @@ import { ActivatedRoute } from "@angular/router";
|
|||||||
|
|
||||||
import { ModalService } from "@bitwarden/angular/services/modal.service";
|
import { ModalService } from "@bitwarden/angular/services/modal.service";
|
||||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||||
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||||
import { Cipher } from "@bitwarden/common/vault/models/domain/cipher";
|
import { Cipher } from "@bitwarden/common/vault/models/domain/cipher";
|
||||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||||
@ -25,11 +26,13 @@ export class ReusedPasswordsReportComponent extends BaseReusedPasswordsReportCom
|
|||||||
private route: ActivatedRoute,
|
private route: ActivatedRoute,
|
||||||
organizationService: OrganizationService,
|
organizationService: OrganizationService,
|
||||||
passwordRepromptService: PasswordRepromptService,
|
passwordRepromptService: PasswordRepromptService,
|
||||||
|
i18nService: I18nService,
|
||||||
) {
|
) {
|
||||||
super(cipherService, organizationService, modalService, passwordRepromptService);
|
super(cipherService, organizationService, modalService, passwordRepromptService, i18nService);
|
||||||
}
|
}
|
||||||
|
|
||||||
async ngOnInit() {
|
async ngOnInit() {
|
||||||
|
this.isAdminConsoleActive = true;
|
||||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe
|
// eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe
|
||||||
this.route.parent.parent.params.subscribe(async (params) => {
|
this.route.parent.parent.params.subscribe(async (params) => {
|
||||||
this.organization = await this.organizationService.get(params.organizationId);
|
this.organization = await this.organizationService.get(params.organizationId);
|
||||||
|
@ -3,6 +3,7 @@ import { ActivatedRoute } from "@angular/router";
|
|||||||
|
|
||||||
import { ModalService } from "@bitwarden/angular/services/modal.service";
|
import { ModalService } from "@bitwarden/angular/services/modal.service";
|
||||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||||
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||||
import { PasswordRepromptService } from "@bitwarden/vault";
|
import { PasswordRepromptService } from "@bitwarden/vault";
|
||||||
@ -22,11 +23,13 @@ export class UnsecuredWebsitesReportComponent extends BaseUnsecuredWebsitesRepor
|
|||||||
private route: ActivatedRoute,
|
private route: ActivatedRoute,
|
||||||
organizationService: OrganizationService,
|
organizationService: OrganizationService,
|
||||||
passwordRepromptService: PasswordRepromptService,
|
passwordRepromptService: PasswordRepromptService,
|
||||||
|
i18nService: I18nService,
|
||||||
) {
|
) {
|
||||||
super(cipherService, organizationService, modalService, passwordRepromptService);
|
super(cipherService, organizationService, modalService, passwordRepromptService, i18nService);
|
||||||
}
|
}
|
||||||
|
|
||||||
async ngOnInit() {
|
async ngOnInit() {
|
||||||
|
this.isAdminConsoleActive = true;
|
||||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe
|
// eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe
|
||||||
this.route.parent.parent.params.subscribe(async (params) => {
|
this.route.parent.parent.params.subscribe(async (params) => {
|
||||||
this.organization = await this.organizationService.get(params.organizationId);
|
this.organization = await this.organizationService.get(params.organizationId);
|
||||||
|
@ -3,6 +3,7 @@ import { ActivatedRoute } from "@angular/router";
|
|||||||
|
|
||||||
import { ModalService } from "@bitwarden/angular/services/modal.service";
|
import { ModalService } from "@bitwarden/angular/services/modal.service";
|
||||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||||
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength";
|
import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength";
|
||||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||||
import { Cipher } from "@bitwarden/common/vault/models/domain/cipher";
|
import { Cipher } from "@bitwarden/common/vault/models/domain/cipher";
|
||||||
@ -27,6 +28,7 @@ export class WeakPasswordsReportComponent extends BaseWeakPasswordsReportCompone
|
|||||||
private route: ActivatedRoute,
|
private route: ActivatedRoute,
|
||||||
organizationService: OrganizationService,
|
organizationService: OrganizationService,
|
||||||
passwordRepromptService: PasswordRepromptService,
|
passwordRepromptService: PasswordRepromptService,
|
||||||
|
i18nService: I18nService,
|
||||||
) {
|
) {
|
||||||
super(
|
super(
|
||||||
cipherService,
|
cipherService,
|
||||||
@ -34,10 +36,12 @@ export class WeakPasswordsReportComponent extends BaseWeakPasswordsReportCompone
|
|||||||
organizationService,
|
organizationService,
|
||||||
modalService,
|
modalService,
|
||||||
passwordRepromptService,
|
passwordRepromptService,
|
||||||
|
i18nService,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async ngOnInit() {
|
async ngOnInit() {
|
||||||
|
this.isAdminConsoleActive = true;
|
||||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe
|
// eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe
|
||||||
this.route.parent.parent.params.subscribe(async (params) => {
|
this.route.parent.parent.params.subscribe(async (params) => {
|
||||||
this.organization = await this.organizationService.get(params.organizationId);
|
this.organization = await this.organizationService.get(params.organizationId);
|
||||||
|
@ -1,9 +1,11 @@
|
|||||||
import { Directive, ViewChild, ViewContainerRef } from "@angular/core";
|
import { Directive, ViewChild, ViewContainerRef, OnDestroy } from "@angular/core";
|
||||||
import { Observable } from "rxjs";
|
import { BehaviorSubject, Observable, Subject, takeUntil } from "rxjs";
|
||||||
|
|
||||||
import { ModalService } from "@bitwarden/angular/services/modal.service";
|
import { ModalService } from "@bitwarden/angular/services/modal.service";
|
||||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||||
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
|
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||||
import { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-reprompt-type";
|
import { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-reprompt-type";
|
||||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||||
import { PasswordRepromptService } from "@bitwarden/vault";
|
import { PasswordRepromptService } from "@bitwarden/vault";
|
||||||
@ -12,27 +14,111 @@ import { AddEditComponent } from "../../../vault/individual-vault/add-edit.compo
|
|||||||
import { AddEditComponent as OrgAddEditComponent } from "../../../vault/org-vault/add-edit.component";
|
import { AddEditComponent as OrgAddEditComponent } from "../../../vault/org-vault/add-edit.component";
|
||||||
|
|
||||||
@Directive()
|
@Directive()
|
||||||
export class CipherReportComponent {
|
export class CipherReportComponent implements OnDestroy {
|
||||||
@ViewChild("cipherAddEdit", { read: ViewContainerRef, static: true })
|
@ViewChild("cipherAddEdit", { read: ViewContainerRef, static: true })
|
||||||
cipherAddEditModalRef: ViewContainerRef;
|
cipherAddEditModalRef: ViewContainerRef;
|
||||||
|
isAdminConsoleActive = false;
|
||||||
|
|
||||||
loading = false;
|
loading = false;
|
||||||
hasLoaded = false;
|
hasLoaded = false;
|
||||||
ciphers: CipherView[] = [];
|
ciphers: CipherView[] = [];
|
||||||
|
allCiphers: CipherView[] = [];
|
||||||
organization: Organization;
|
organization: Organization;
|
||||||
|
organizations: Organization[];
|
||||||
organizations$: Observable<Organization[]>;
|
organizations$: Observable<Organization[]>;
|
||||||
|
|
||||||
|
filterStatus: any = [0];
|
||||||
|
showFilterToggle: boolean = false;
|
||||||
|
vaultMsg: string = "vault";
|
||||||
|
currentFilterStatus: number | string;
|
||||||
|
protected filterOrgStatus$ = new BehaviorSubject<number | string>(0);
|
||||||
|
private destroyed$: Subject<void> = new Subject();
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
|
protected cipherService: CipherService,
|
||||||
private modalService: ModalService,
|
private modalService: ModalService,
|
||||||
protected passwordRepromptService: PasswordRepromptService,
|
protected passwordRepromptService: PasswordRepromptService,
|
||||||
protected organizationService: OrganizationService,
|
protected organizationService: OrganizationService,
|
||||||
|
protected i18nService: I18nService,
|
||||||
) {
|
) {
|
||||||
this.organizations$ = this.organizationService.organizations$;
|
this.organizations$ = this.organizationService.organizations$;
|
||||||
|
this.organizations$.pipe(takeUntil(this.destroyed$)).subscribe((orgs) => {
|
||||||
|
this.organizations = orgs;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnDestroy(): void {
|
||||||
|
this.destroyed$.next();
|
||||||
|
this.destroyed$.complete();
|
||||||
|
}
|
||||||
|
|
||||||
|
getName(filterId: string | number) {
|
||||||
|
let orgName: any;
|
||||||
|
|
||||||
|
if (filterId === 0) {
|
||||||
|
orgName = this.i18nService.t("all");
|
||||||
|
} else if (filterId === 1) {
|
||||||
|
orgName = this.i18nService.t("me");
|
||||||
|
} else {
|
||||||
|
this.organizations.filter((org: Organization) => {
|
||||||
|
if (org.id === filterId) {
|
||||||
|
orgName = org.name;
|
||||||
|
return org;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return orgName;
|
||||||
|
}
|
||||||
|
|
||||||
|
getCount(filterId: string | number) {
|
||||||
|
let orgFilterStatus: any;
|
||||||
|
let cipherCount;
|
||||||
|
|
||||||
|
if (filterId === 0) {
|
||||||
|
cipherCount = this.allCiphers.length;
|
||||||
|
} else if (filterId === 1) {
|
||||||
|
cipherCount = this.allCiphers.filter((c: any) => c.orgFilterStatus === null).length;
|
||||||
|
} else {
|
||||||
|
this.organizations.filter((org: Organization) => {
|
||||||
|
if (org.id === filterId) {
|
||||||
|
orgFilterStatus = org.id;
|
||||||
|
return org;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
cipherCount = this.allCiphers.filter(
|
||||||
|
(c: any) => c.orgFilterStatus === orgFilterStatus,
|
||||||
|
).length;
|
||||||
|
}
|
||||||
|
return cipherCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
async filterOrgToggle(status: any) {
|
||||||
|
this.currentFilterStatus = status;
|
||||||
|
await this.setCiphers();
|
||||||
|
if (status === 0) {
|
||||||
|
return;
|
||||||
|
} else if (status === 1) {
|
||||||
|
this.ciphers = this.ciphers.filter((c: any) => c.orgFilterStatus == null);
|
||||||
|
} else {
|
||||||
|
this.ciphers = this.ciphers.filter((c: any) => c.orgFilterStatus === status);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async load() {
|
async load() {
|
||||||
this.loading = true;
|
this.loading = true;
|
||||||
|
// when a user fixes an item in a report we want to persist the filter they had
|
||||||
|
// if they fix the last item of that filter we will go back to the "All" filter
|
||||||
|
if (this.currentFilterStatus) {
|
||||||
|
if (this.ciphers.length > 2) {
|
||||||
|
this.filterOrgStatus$.next(this.currentFilterStatus);
|
||||||
|
await this.filterOrgToggle(this.currentFilterStatus);
|
||||||
|
} else {
|
||||||
|
this.filterOrgStatus$.next(0);
|
||||||
|
await this.filterOrgToggle(0);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
await this.setCiphers();
|
await this.setCiphers();
|
||||||
|
}
|
||||||
this.loading = false;
|
this.loading = false;
|
||||||
this.hasLoaded = true;
|
this.hasLoaded = true;
|
||||||
}
|
}
|
||||||
@ -76,7 +162,7 @@ export class CipherReportComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected async setCiphers() {
|
protected async setCiphers() {
|
||||||
this.ciphers = [];
|
this.allCiphers = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async repromptCipher(c: CipherView) {
|
protected async repromptCipher(c: CipherView) {
|
||||||
@ -85,4 +171,32 @@ export class CipherReportComponent {
|
|||||||
(await this.passwordRepromptService.showPasswordPrompt())
|
(await this.passwordRepromptService.showPasswordPrompt())
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected async getAllCiphers(): Promise<CipherView[]> {
|
||||||
|
return await this.cipherService.getAllDecrypted();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected filterCiphersByOrg(ciphersList: CipherView[]) {
|
||||||
|
this.allCiphers = [...ciphersList];
|
||||||
|
|
||||||
|
this.ciphers = ciphersList.map((ciph: any) => {
|
||||||
|
ciph.orgFilterStatus = ciph.organizationId;
|
||||||
|
|
||||||
|
if (this.filterStatus.indexOf(ciph.organizationId) === -1 && ciph.organizationId != null) {
|
||||||
|
this.filterStatus.push(ciph.organizationId);
|
||||||
|
} else if (this.filterStatus.indexOf(1) === -1 && ciph.organizationId == null) {
|
||||||
|
this.filterStatus.splice(1, 0, 1);
|
||||||
|
}
|
||||||
|
return ciph;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (this.filterStatus.length > 2) {
|
||||||
|
this.showFilterToggle = true;
|
||||||
|
this.vaultMsg = "vaults";
|
||||||
|
} else {
|
||||||
|
// If a user fixes an item and there is only one item left remove the filter toggle and change the vault message to singular
|
||||||
|
this.showFilterToggle = false;
|
||||||
|
this.vaultMsg = "vault";
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -11,9 +11,32 @@
|
|||||||
</app-callout>
|
</app-callout>
|
||||||
<ng-container *ngIf="ciphers.length">
|
<ng-container *ngIf="ciphers.length">
|
||||||
<app-callout type="danger" title="{{ 'exposedPasswordsFound' | i18n }}" [useAlertRole]="true">
|
<app-callout type="danger" title="{{ 'exposedPasswordsFound' | i18n }}" [useAlertRole]="true">
|
||||||
{{ "exposedPasswordsFoundDesc" | i18n: (ciphers.length | number) }}
|
{{ "exposedPasswordsFoundReportDesc" | i18n: (ciphers.length | number) : vaultMsg }}
|
||||||
</app-callout>
|
</app-callout>
|
||||||
|
<bit-toggle-group
|
||||||
|
*ngIf="showFilterToggle && !isAdminConsoleActive"
|
||||||
|
[selected]="filterOrgStatus$ | async"
|
||||||
|
(selectedChange)="filterOrgToggle($event)"
|
||||||
|
[attr.aria-label]="'addAccessFilter' | i18n"
|
||||||
|
>
|
||||||
|
<ng-container *ngFor="let status of filterStatus">
|
||||||
|
<bit-toggle [value]="status">
|
||||||
|
{{ getName(status) }}
|
||||||
|
<span bitBadge variant="info"> {{ getCount(status) }} </span>
|
||||||
|
</bit-toggle>
|
||||||
|
</ng-container>
|
||||||
|
</bit-toggle-group>
|
||||||
<table class="table table-hover table-list table-ciphers">
|
<table class="table table-hover table-list table-ciphers">
|
||||||
|
<thead
|
||||||
|
class="tw-border-0 tw-border-b-2 tw-border-solid tw-border-secondary-300 tw-font-bold tw-text-muted"
|
||||||
|
*ngIf="!isAdminConsoleActive"
|
||||||
|
>
|
||||||
|
<tr>
|
||||||
|
<th></th>
|
||||||
|
<th>{{ "name" | i18n }}</th>
|
||||||
|
<th>{{ "owner" | i18n }}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr *ngFor="let c of ciphers">
|
<tr *ngFor="let c of ciphers">
|
||||||
<td class="table-list-icon">
|
<td class="table-list-icon">
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
// eslint-disable-next-line no-restricted-imports
|
// eslint-disable-next-line no-restricted-imports
|
||||||
import { ComponentFixture, TestBed } from "@angular/core/testing";
|
import { ComponentFixture, TestBed } from "@angular/core/testing";
|
||||||
import { mock, MockProxy } from "jest-mock-extended";
|
import { mock, MockProxy } from "jest-mock-extended";
|
||||||
|
import { of } from "rxjs";
|
||||||
|
|
||||||
import { I18nPipe } from "@bitwarden/angular/platform/pipes/i18n.pipe";
|
import { I18nPipe } from "@bitwarden/angular/platform/pipes/i18n.pipe";
|
||||||
import { ModalService } from "@bitwarden/angular/services/modal.service";
|
import { ModalService } from "@bitwarden/angular/services/modal.service";
|
||||||
@ -17,9 +18,12 @@ describe("ExposedPasswordsReportComponent", () => {
|
|||||||
let component: ExposedPasswordsReportComponent;
|
let component: ExposedPasswordsReportComponent;
|
||||||
let fixture: ComponentFixture<ExposedPasswordsReportComponent>;
|
let fixture: ComponentFixture<ExposedPasswordsReportComponent>;
|
||||||
let auditService: MockProxy<AuditService>;
|
let auditService: MockProxy<AuditService>;
|
||||||
|
let organizationService: MockProxy<OrganizationService>;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
auditService = mock<AuditService>();
|
auditService = mock<AuditService>();
|
||||||
|
organizationService = mock<OrganizationService>();
|
||||||
|
organizationService.organizations$ = of([]);
|
||||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
// 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
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
@ -35,7 +39,7 @@ describe("ExposedPasswordsReportComponent", () => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
provide: OrganizationService,
|
provide: OrganizationService,
|
||||||
useValue: mock<OrganizationService>(),
|
useValue: organizationService,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
provide: ModalService,
|
provide: ModalService,
|
||||||
|
@ -3,6 +3,7 @@ import { Component, OnInit } from "@angular/core";
|
|||||||
import { ModalService } from "@bitwarden/angular/services/modal.service";
|
import { ModalService } from "@bitwarden/angular/services/modal.service";
|
||||||
import { AuditService } from "@bitwarden/common/abstractions/audit.service";
|
import { AuditService } from "@bitwarden/common/abstractions/audit.service";
|
||||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||||
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||||
@ -24,8 +25,9 @@ export class ExposedPasswordsReportComponent extends CipherReportComponent imple
|
|||||||
protected organizationService: OrganizationService,
|
protected organizationService: OrganizationService,
|
||||||
modalService: ModalService,
|
modalService: ModalService,
|
||||||
passwordRepromptService: PasswordRepromptService,
|
passwordRepromptService: PasswordRepromptService,
|
||||||
|
i18nService: I18nService,
|
||||||
) {
|
) {
|
||||||
super(modalService, passwordRepromptService, organizationService);
|
super(cipherService, modalService, passwordRepromptService, organizationService, i18nService);
|
||||||
}
|
}
|
||||||
|
|
||||||
async ngOnInit() {
|
async ngOnInit() {
|
||||||
@ -36,7 +38,9 @@ export class ExposedPasswordsReportComponent extends CipherReportComponent imple
|
|||||||
const allCiphers = await this.getAllCiphers();
|
const allCiphers = await this.getAllCiphers();
|
||||||
const exposedPasswordCiphers: CipherView[] = [];
|
const exposedPasswordCiphers: CipherView[] = [];
|
||||||
const promises: Promise<void>[] = [];
|
const promises: Promise<void>[] = [];
|
||||||
allCiphers.forEach((ciph) => {
|
this.filterStatus = [0];
|
||||||
|
|
||||||
|
allCiphers.forEach((ciph: any) => {
|
||||||
const { type, login, isDeleted, edit, viewPassword, id } = ciph;
|
const { type, login, isDeleted, edit, viewPassword, id } = ciph;
|
||||||
if (
|
if (
|
||||||
type !== CipherType.Login ||
|
type !== CipherType.Login ||
|
||||||
@ -48,6 +52,7 @@ export class ExposedPasswordsReportComponent extends CipherReportComponent imple
|
|||||||
) {
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const promise = this.auditService.passwordLeaked(login.password).then((exposedCount) => {
|
const promise = this.auditService.passwordLeaked(login.password).then((exposedCount) => {
|
||||||
if (exposedCount > 0) {
|
if (exposedCount > 0) {
|
||||||
exposedPasswordCiphers.push(ciph);
|
exposedPasswordCiphers.push(ciph);
|
||||||
@ -57,11 +62,8 @@ export class ExposedPasswordsReportComponent extends CipherReportComponent imple
|
|||||||
promises.push(promise);
|
promises.push(promise);
|
||||||
});
|
});
|
||||||
await Promise.all(promises);
|
await Promise.all(promises);
|
||||||
this.ciphers = [...exposedPasswordCiphers];
|
|
||||||
}
|
|
||||||
|
|
||||||
protected getAllCiphers(): Promise<CipherView[]> {
|
this.filterCiphersByOrg(exposedPasswordCiphers);
|
||||||
return this.cipherService.getAllDecrypted();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected canManageCipher(c: CipherView): boolean {
|
protected canManageCipher(c: CipherView): boolean {
|
||||||
|
@ -16,9 +16,32 @@
|
|||||||
</app-callout>
|
</app-callout>
|
||||||
<ng-container *ngIf="ciphers.length">
|
<ng-container *ngIf="ciphers.length">
|
||||||
<app-callout type="danger" title="{{ 'inactive2faFound' | i18n }}">
|
<app-callout type="danger" title="{{ 'inactive2faFound' | i18n }}">
|
||||||
{{ "inactive2faFoundDesc" | i18n: (ciphers.length | number) }}
|
{{ "inactive2faFoundReportDesc" | i18n: (ciphers.length | number) : vaultMsg }}
|
||||||
</app-callout>
|
</app-callout>
|
||||||
|
<bit-toggle-group
|
||||||
|
*ngIf="showFilterToggle && !isAdminConsoleActive"
|
||||||
|
[selected]="filterOrgStatus$ | async"
|
||||||
|
(selectedChange)="filterOrgToggle($event)"
|
||||||
|
[attr.aria-label]="'addAccessFilter' | i18n"
|
||||||
|
>
|
||||||
|
<ng-container *ngFor="let status of filterStatus">
|
||||||
|
<bit-toggle [value]="status">
|
||||||
|
{{ getName(status) }}
|
||||||
|
<span bitBadge variant="info"> {{ getCount(status) }} </span>
|
||||||
|
</bit-toggle>
|
||||||
|
</ng-container>
|
||||||
|
</bit-toggle-group>
|
||||||
<table class="table table-hover table-list table-ciphers">
|
<table class="table table-hover table-list table-ciphers">
|
||||||
|
<thead
|
||||||
|
class="tw-border-0 tw-border-b-2 tw-border-solid tw-border-secondary-300 tw-font-bold tw-text-muted"
|
||||||
|
*ngIf="!isAdminConsoleActive"
|
||||||
|
>
|
||||||
|
<tr>
|
||||||
|
<th></th>
|
||||||
|
<th>{{ "name" | i18n }}</th>
|
||||||
|
<th>{{ "owner" | i18n }}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr *ngFor="let c of ciphers">
|
<tr *ngFor="let c of ciphers">
|
||||||
<td class="table-list-icon">
|
<td class="table-list-icon">
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
// eslint-disable-next-line no-restricted-imports
|
// eslint-disable-next-line no-restricted-imports
|
||||||
import { ComponentFixture, TestBed } from "@angular/core/testing";
|
import { ComponentFixture, TestBed } from "@angular/core/testing";
|
||||||
import { mock } from "jest-mock-extended";
|
import { MockProxy, mock } from "jest-mock-extended";
|
||||||
|
import { of } from "rxjs";
|
||||||
|
|
||||||
import { I18nPipe } from "@bitwarden/angular/platform/pipes/i18n.pipe";
|
import { I18nPipe } from "@bitwarden/angular/platform/pipes/i18n.pipe";
|
||||||
import { ModalService } from "@bitwarden/angular/services/modal.service";
|
import { ModalService } from "@bitwarden/angular/services/modal.service";
|
||||||
@ -16,8 +17,11 @@ import { cipherData } from "./reports-ciphers.mock";
|
|||||||
describe("InactiveTwoFactorReportComponent", () => {
|
describe("InactiveTwoFactorReportComponent", () => {
|
||||||
let component: InactiveTwoFactorReportComponent;
|
let component: InactiveTwoFactorReportComponent;
|
||||||
let fixture: ComponentFixture<InactiveTwoFactorReportComponent>;
|
let fixture: ComponentFixture<InactiveTwoFactorReportComponent>;
|
||||||
|
let organizationService: MockProxy<OrganizationService>;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
organizationService = mock<OrganizationService>();
|
||||||
|
organizationService.organizations$ = of([]);
|
||||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
// 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
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
@ -29,7 +33,7 @@ describe("InactiveTwoFactorReportComponent", () => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
provide: OrganizationService,
|
provide: OrganizationService,
|
||||||
useValue: mock<OrganizationService>(),
|
useValue: organizationService,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
provide: ModalService,
|
provide: ModalService,
|
||||||
|
@ -2,6 +2,7 @@ import { Component, OnInit } from "@angular/core";
|
|||||||
|
|
||||||
import { ModalService } from "@bitwarden/angular/services/modal.service";
|
import { ModalService } from "@bitwarden/angular/services/modal.service";
|
||||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||||
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||||
@ -26,8 +27,9 @@ export class InactiveTwoFactorReportComponent extends CipherReportComponent impl
|
|||||||
modalService: ModalService,
|
modalService: ModalService,
|
||||||
private logService: LogService,
|
private logService: LogService,
|
||||||
passwordRepromptService: PasswordRepromptService,
|
passwordRepromptService: PasswordRepromptService,
|
||||||
|
i18nService: I18nService,
|
||||||
) {
|
) {
|
||||||
super(modalService, passwordRepromptService, organizationService);
|
super(cipherService, modalService, passwordRepromptService, organizationService, i18nService);
|
||||||
}
|
}
|
||||||
|
|
||||||
async ngOnInit() {
|
async ngOnInit() {
|
||||||
@ -45,6 +47,7 @@ export class InactiveTwoFactorReportComponent extends CipherReportComponent impl
|
|||||||
const allCiphers = await this.getAllCiphers();
|
const allCiphers = await this.getAllCiphers();
|
||||||
const inactive2faCiphers: CipherView[] = [];
|
const inactive2faCiphers: CipherView[] = [];
|
||||||
const docs = new Map<string, string>();
|
const docs = new Map<string, string>();
|
||||||
|
this.filterStatus = [0];
|
||||||
|
|
||||||
allCiphers.forEach((ciph) => {
|
allCiphers.forEach((ciph) => {
|
||||||
const { type, login, isDeleted, edit, id, viewPassword } = ciph;
|
const { type, login, isDeleted, edit, id, viewPassword } = ciph;
|
||||||
@ -58,6 +61,7 @@ export class InactiveTwoFactorReportComponent extends CipherReportComponent impl
|
|||||||
) {
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (let i = 0; i < login.uris.length; i++) {
|
for (let i = 0; i < login.uris.length; i++) {
|
||||||
const u = login.uris[i];
|
const u = login.uris[i];
|
||||||
if (u.uri != null && u.uri !== "") {
|
if (u.uri != null && u.uri !== "") {
|
||||||
@ -75,15 +79,12 @@ export class InactiveTwoFactorReportComponent extends CipherReportComponent impl
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
this.ciphers = [...inactive2faCiphers];
|
|
||||||
|
this.filterCiphersByOrg(inactive2faCiphers);
|
||||||
this.cipherDocs = docs;
|
this.cipherDocs = docs;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected getAllCiphers(): Promise<CipherView[]> {
|
|
||||||
return this.cipherService.getAllDecrypted();
|
|
||||||
}
|
|
||||||
|
|
||||||
private async load2fa() {
|
private async load2fa() {
|
||||||
if (this.services.size > 0) {
|
if (this.services.size > 0) {
|
||||||
return;
|
return;
|
||||||
|
@ -16,9 +16,34 @@
|
|||||||
</app-callout>
|
</app-callout>
|
||||||
<ng-container *ngIf="ciphers.length">
|
<ng-container *ngIf="ciphers.length">
|
||||||
<app-callout type="danger" title="{{ 'reusedPasswordsFound' | i18n }}">
|
<app-callout type="danger" title="{{ 'reusedPasswordsFound' | i18n }}">
|
||||||
{{ "reusedPasswordsFoundDesc" | i18n: (ciphers.length | number) }}
|
{{ "reusedPasswordsFoundReportDesc" | i18n: (ciphers.length | number) : vaultMsg }}
|
||||||
</app-callout>
|
</app-callout>
|
||||||
|
|
||||||
|
<bit-toggle-group
|
||||||
|
*ngIf="showFilterToggle && !isAdminConsoleActive"
|
||||||
|
[selected]="filterOrgStatus$ | async"
|
||||||
|
(selectedChange)="filterOrgToggle($event)"
|
||||||
|
[attr.aria-label]="'addAccessFilter' | i18n"
|
||||||
|
>
|
||||||
|
<ng-container *ngFor="let status of filterStatus">
|
||||||
|
<bit-toggle [value]="status">
|
||||||
|
{{ getName(status) }}
|
||||||
|
<span bitBadge variant="info"> {{ getCount(status) }} </span>
|
||||||
|
</bit-toggle>
|
||||||
|
</ng-container>
|
||||||
|
</bit-toggle-group>
|
||||||
|
|
||||||
<table class="table table-hover table-list table-ciphers">
|
<table class="table table-hover table-list table-ciphers">
|
||||||
|
<thead
|
||||||
|
class="tw-border-0 tw-border-b-2 tw-border-solid tw-border-secondary-300 tw-font-bold tw-text-muted"
|
||||||
|
*ngIf="!isAdminConsoleActive"
|
||||||
|
>
|
||||||
|
<tr>
|
||||||
|
<th></th>
|
||||||
|
<th>{{ "name" | i18n }}</th>
|
||||||
|
<th>{{ "owner" | i18n }}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr *ngFor="let c of ciphers">
|
<tr *ngFor="let c of ciphers">
|
||||||
<td class="table-list-icon">
|
<td class="table-list-icon">
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
// eslint-disable-next-line no-restricted-imports
|
// eslint-disable-next-line no-restricted-imports
|
||||||
import { ComponentFixture, TestBed } from "@angular/core/testing";
|
import { ComponentFixture, TestBed } from "@angular/core/testing";
|
||||||
import { mock } from "jest-mock-extended";
|
import { MockProxy, mock } from "jest-mock-extended";
|
||||||
|
import { of } from "rxjs";
|
||||||
|
|
||||||
import { I18nPipe } from "@bitwarden/angular/platform/pipes/i18n.pipe";
|
import { I18nPipe } from "@bitwarden/angular/platform/pipes/i18n.pipe";
|
||||||
import { ModalService } from "@bitwarden/angular/services/modal.service";
|
import { ModalService } from "@bitwarden/angular/services/modal.service";
|
||||||
@ -15,8 +16,11 @@ import { ReusedPasswordsReportComponent } from "./reused-passwords-report.compon
|
|||||||
describe("ReusedPasswordsReportComponent", () => {
|
describe("ReusedPasswordsReportComponent", () => {
|
||||||
let component: ReusedPasswordsReportComponent;
|
let component: ReusedPasswordsReportComponent;
|
||||||
let fixture: ComponentFixture<ReusedPasswordsReportComponent>;
|
let fixture: ComponentFixture<ReusedPasswordsReportComponent>;
|
||||||
|
let organizationService: MockProxy<OrganizationService>;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
organizationService = mock<OrganizationService>();
|
||||||
|
organizationService.organizations$ = of([]);
|
||||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
// 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
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
@ -28,7 +32,7 @@ describe("ReusedPasswordsReportComponent", () => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
provide: OrganizationService,
|
provide: OrganizationService,
|
||||||
useValue: mock<OrganizationService>(),
|
useValue: organizationService,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
provide: ModalService,
|
provide: ModalService,
|
||||||
|
@ -2,6 +2,7 @@ import { Component, OnInit } from "@angular/core";
|
|||||||
|
|
||||||
import { ModalService } from "@bitwarden/angular/services/modal.service";
|
import { ModalService } from "@bitwarden/angular/services/modal.service";
|
||||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||||
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||||
@ -22,8 +23,9 @@ export class ReusedPasswordsReportComponent extends CipherReportComponent implem
|
|||||||
protected organizationService: OrganizationService,
|
protected organizationService: OrganizationService,
|
||||||
modalService: ModalService,
|
modalService: ModalService,
|
||||||
passwordRepromptService: PasswordRepromptService,
|
passwordRepromptService: PasswordRepromptService,
|
||||||
|
i18nService: I18nService,
|
||||||
) {
|
) {
|
||||||
super(modalService, passwordRepromptService, organizationService);
|
super(cipherService, modalService, passwordRepromptService, organizationService, i18nService);
|
||||||
}
|
}
|
||||||
|
|
||||||
async ngOnInit() {
|
async ngOnInit() {
|
||||||
@ -34,6 +36,8 @@ export class ReusedPasswordsReportComponent extends CipherReportComponent implem
|
|||||||
const allCiphers = await this.getAllCiphers();
|
const allCiphers = await this.getAllCiphers();
|
||||||
const ciphersWithPasswords: CipherView[] = [];
|
const ciphersWithPasswords: CipherView[] = [];
|
||||||
this.passwordUseMap = new Map<string, number>();
|
this.passwordUseMap = new Map<string, number>();
|
||||||
|
this.filterStatus = [0];
|
||||||
|
|
||||||
allCiphers.forEach((ciph) => {
|
allCiphers.forEach((ciph) => {
|
||||||
const { type, login, isDeleted, edit, viewPassword } = ciph;
|
const { type, login, isDeleted, edit, viewPassword } = ciph;
|
||||||
if (
|
if (
|
||||||
@ -46,6 +50,7 @@ export class ReusedPasswordsReportComponent extends CipherReportComponent implem
|
|||||||
) {
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
ciphersWithPasswords.push(ciph);
|
ciphersWithPasswords.push(ciph);
|
||||||
if (this.passwordUseMap.has(login.password)) {
|
if (this.passwordUseMap.has(login.password)) {
|
||||||
this.passwordUseMap.set(login.password, this.passwordUseMap.get(login.password) + 1);
|
this.passwordUseMap.set(login.password, this.passwordUseMap.get(login.password) + 1);
|
||||||
@ -57,11 +62,8 @@ export class ReusedPasswordsReportComponent extends CipherReportComponent implem
|
|||||||
(c) =>
|
(c) =>
|
||||||
this.passwordUseMap.has(c.login.password) && this.passwordUseMap.get(c.login.password) > 1,
|
this.passwordUseMap.has(c.login.password) && this.passwordUseMap.get(c.login.password) > 1,
|
||||||
);
|
);
|
||||||
this.ciphers = reusedPasswordCiphers;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected getAllCiphers(): Promise<CipherView[]> {
|
this.filterCiphersByOrg(reusedPasswordCiphers);
|
||||||
return this.cipherService.getAllDecrypted();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected canManageCipher(c: CipherView): boolean {
|
protected canManageCipher(c: CipherView): boolean {
|
||||||
|
@ -16,9 +16,33 @@
|
|||||||
</app-callout>
|
</app-callout>
|
||||||
<ng-container *ngIf="ciphers.length">
|
<ng-container *ngIf="ciphers.length">
|
||||||
<app-callout type="danger" title="{{ 'unsecuredWebsitesFound' | i18n }}">
|
<app-callout type="danger" title="{{ 'unsecuredWebsitesFound' | i18n }}">
|
||||||
{{ "unsecuredWebsitesFoundDesc" | i18n: (ciphers.length | number) }}
|
{{ "unsecuredWebsitesFoundReportDesc" | i18n: (ciphers.length | number) : vaultMsg }}
|
||||||
</app-callout>
|
</app-callout>
|
||||||
|
|
||||||
|
<bit-toggle-group
|
||||||
|
*ngIf="showFilterToggle && !isAdminConsoleActive"
|
||||||
|
[selected]="filterOrgStatus$ | async"
|
||||||
|
(selectedChange)="filterOrgToggle($event)"
|
||||||
|
[attr.aria-label]="'addAccessFilter' | i18n"
|
||||||
|
>
|
||||||
|
<ng-container *ngFor="let status of filterStatus">
|
||||||
|
<bit-toggle [value]="status">
|
||||||
|
{{ getName(status) }}
|
||||||
|
<span bitBadge variant="info"> {{ getCount(status) }} </span>
|
||||||
|
</bit-toggle>
|
||||||
|
</ng-container>
|
||||||
|
</bit-toggle-group>
|
||||||
<table class="table table-hover table-list table-ciphers">
|
<table class="table table-hover table-list table-ciphers">
|
||||||
|
<thead
|
||||||
|
class="tw-border-0 tw-border-b-2 tw-border-solid tw-border-secondary-300 tw-font-bold tw-text-muted"
|
||||||
|
*ngIf="!isAdminConsoleActive"
|
||||||
|
>
|
||||||
|
<tr>
|
||||||
|
<th></th>
|
||||||
|
<th>{{ "name" | i18n }}</th>
|
||||||
|
<th>{{ "owner" | i18n }}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr *ngFor="let c of ciphers">
|
<tr *ngFor="let c of ciphers">
|
||||||
<td class="table-list-icon">
|
<td class="table-list-icon">
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
// eslint-disable-next-line no-restricted-imports
|
// eslint-disable-next-line no-restricted-imports
|
||||||
import { ComponentFixture, TestBed } from "@angular/core/testing";
|
import { ComponentFixture, TestBed } from "@angular/core/testing";
|
||||||
import { mock } from "jest-mock-extended";
|
import { MockProxy, mock } from "jest-mock-extended";
|
||||||
|
import { of } from "rxjs";
|
||||||
|
|
||||||
import { I18nPipe } from "@bitwarden/angular/platform/pipes/i18n.pipe";
|
import { I18nPipe } from "@bitwarden/angular/platform/pipes/i18n.pipe";
|
||||||
import { ModalService } from "@bitwarden/angular/services/modal.service";
|
import { ModalService } from "@bitwarden/angular/services/modal.service";
|
||||||
@ -15,8 +16,11 @@ import { UnsecuredWebsitesReportComponent } from "./unsecured-websites-report.co
|
|||||||
describe("UnsecuredWebsitesReportComponent", () => {
|
describe("UnsecuredWebsitesReportComponent", () => {
|
||||||
let component: UnsecuredWebsitesReportComponent;
|
let component: UnsecuredWebsitesReportComponent;
|
||||||
let fixture: ComponentFixture<UnsecuredWebsitesReportComponent>;
|
let fixture: ComponentFixture<UnsecuredWebsitesReportComponent>;
|
||||||
|
let organizationService: MockProxy<OrganizationService>;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
organizationService = mock<OrganizationService>();
|
||||||
|
organizationService.organizations$ = of([]);
|
||||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
// 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
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
@ -28,7 +32,7 @@ describe("UnsecuredWebsitesReportComponent", () => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
provide: OrganizationService,
|
provide: OrganizationService,
|
||||||
useValue: mock<OrganizationService>(),
|
useValue: organizationService,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
provide: ModalService,
|
provide: ModalService,
|
||||||
|
@ -2,9 +2,9 @@ import { Component, OnInit } from "@angular/core";
|
|||||||
|
|
||||||
import { ModalService } from "@bitwarden/angular/services/modal.service";
|
import { ModalService } from "@bitwarden/angular/services/modal.service";
|
||||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||||
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
|
||||||
import { PasswordRepromptService } from "@bitwarden/vault";
|
import { PasswordRepromptService } from "@bitwarden/vault";
|
||||||
|
|
||||||
import { CipherReportComponent } from "./cipher-report.component";
|
import { CipherReportComponent } from "./cipher-report.component";
|
||||||
@ -21,8 +21,9 @@ export class UnsecuredWebsitesReportComponent extends CipherReportComponent impl
|
|||||||
protected organizationService: OrganizationService,
|
protected organizationService: OrganizationService,
|
||||||
modalService: ModalService,
|
modalService: ModalService,
|
||||||
passwordRepromptService: PasswordRepromptService,
|
passwordRepromptService: PasswordRepromptService,
|
||||||
|
i18nService: I18nService,
|
||||||
) {
|
) {
|
||||||
super(modalService, passwordRepromptService, organizationService);
|
super(cipherService, modalService, passwordRepromptService, organizationService, i18nService);
|
||||||
}
|
}
|
||||||
|
|
||||||
async ngOnInit() {
|
async ngOnInit() {
|
||||||
@ -31,18 +32,15 @@ export class UnsecuredWebsitesReportComponent extends CipherReportComponent impl
|
|||||||
|
|
||||||
async setCiphers() {
|
async setCiphers() {
|
||||||
const allCiphers = await this.getAllCiphers();
|
const allCiphers = await this.getAllCiphers();
|
||||||
|
this.filterStatus = [0];
|
||||||
const unsecuredCiphers = allCiphers.filter((c) => {
|
const unsecuredCiphers = allCiphers.filter((c) => {
|
||||||
if (c.type !== CipherType.Login || !c.login.hasUris || c.isDeleted) {
|
if (c.type !== CipherType.Login || !c.login.hasUris || c.isDeleted) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return c.login.uris.some((u) => u.uri != null && u.uri.indexOf("http://") === 0);
|
|
||||||
});
|
|
||||||
this.ciphers = unsecuredCiphers.filter(
|
|
||||||
(c) => (!this.organization && c.edit) || (this.organization && !c.edit),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected getAllCiphers(): Promise<CipherView[]> {
|
return c.login.uris.some((u: any) => u.uri != null && u.uri.indexOf("http://") === 0);
|
||||||
return this.cipherService.getAllDecrypted();
|
});
|
||||||
|
|
||||||
|
this.filterCiphersByOrg(unsecuredCiphers);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -16,9 +16,32 @@
|
|||||||
</app-callout>
|
</app-callout>
|
||||||
<ng-container *ngIf="ciphers.length">
|
<ng-container *ngIf="ciphers.length">
|
||||||
<app-callout type="danger" title="{{ 'weakPasswordsFound' | i18n }}">
|
<app-callout type="danger" title="{{ 'weakPasswordsFound' | i18n }}">
|
||||||
{{ "weakPasswordsFoundDesc" | i18n: (ciphers.length | number) }}
|
{{ "weakPasswordsFoundReportDesc" | i18n: (ciphers.length | number) : vaultMsg }}
|
||||||
</app-callout>
|
</app-callout>
|
||||||
|
<bit-toggle-group
|
||||||
|
*ngIf="showFilterToggle && !isAdminConsoleActive"
|
||||||
|
[selected]="filterOrgStatus$ | async"
|
||||||
|
(selectedChange)="filterOrgToggle($event)"
|
||||||
|
[attr.aria-label]="'addAccessFilter' | i18n"
|
||||||
|
>
|
||||||
|
<ng-container *ngFor="let status of filterStatus">
|
||||||
|
<bit-toggle [value]="status">
|
||||||
|
{{ getName(status) }}
|
||||||
|
<span bitBadge variant="info"> {{ getCount(status) }} </span>
|
||||||
|
</bit-toggle>
|
||||||
|
</ng-container>
|
||||||
|
</bit-toggle-group>
|
||||||
<table class="table table-hover table-list table-ciphers">
|
<table class="table table-hover table-list table-ciphers">
|
||||||
|
<thead
|
||||||
|
class="tw-border-0 tw-border-b-2 tw-border-solid tw-border-secondary-300 tw-font-bold tw-text-muted"
|
||||||
|
*ngIf="!isAdminConsoleActive"
|
||||||
|
>
|
||||||
|
<tr>
|
||||||
|
<th></th>
|
||||||
|
<th>{{ "name" | i18n }}</th>
|
||||||
|
<th>{{ "owner" | i18n }}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr *ngFor="let c of ciphers">
|
<tr *ngFor="let c of ciphers">
|
||||||
<td class="table-list-icon">
|
<td class="table-list-icon">
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
// eslint-disable-next-line no-restricted-imports
|
// eslint-disable-next-line no-restricted-imports
|
||||||
import { ComponentFixture, TestBed } from "@angular/core/testing";
|
import { ComponentFixture, TestBed } from "@angular/core/testing";
|
||||||
import { mock, MockProxy } from "jest-mock-extended";
|
import { mock, MockProxy } from "jest-mock-extended";
|
||||||
|
import { of } from "rxjs";
|
||||||
|
|
||||||
import { I18nPipe } from "@bitwarden/angular/platform/pipes/i18n.pipe";
|
import { I18nPipe } from "@bitwarden/angular/platform/pipes/i18n.pipe";
|
||||||
import { ModalService } from "@bitwarden/angular/services/modal.service";
|
import { ModalService } from "@bitwarden/angular/services/modal.service";
|
||||||
@ -17,9 +18,12 @@ describe("WeakPasswordsReportComponent", () => {
|
|||||||
let component: WeakPasswordsReportComponent;
|
let component: WeakPasswordsReportComponent;
|
||||||
let fixture: ComponentFixture<WeakPasswordsReportComponent>;
|
let fixture: ComponentFixture<WeakPasswordsReportComponent>;
|
||||||
let passwordStrengthService: MockProxy<PasswordStrengthServiceAbstraction>;
|
let passwordStrengthService: MockProxy<PasswordStrengthServiceAbstraction>;
|
||||||
|
let organizationService: MockProxy<OrganizationService>;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
passwordStrengthService = mock<PasswordStrengthServiceAbstraction>();
|
passwordStrengthService = mock<PasswordStrengthServiceAbstraction>();
|
||||||
|
organizationService = mock<OrganizationService>();
|
||||||
|
organizationService.organizations$ = of([]);
|
||||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
// 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
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
@ -35,7 +39,7 @@ describe("WeakPasswordsReportComponent", () => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
provide: OrganizationService,
|
provide: OrganizationService,
|
||||||
useValue: mock<OrganizationService>(),
|
useValue: organizationService,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
provide: ModalService,
|
provide: ModalService,
|
||||||
|
@ -2,6 +2,7 @@ import { Component, OnInit } from "@angular/core";
|
|||||||
|
|
||||||
import { ModalService } from "@bitwarden/angular/services/modal.service";
|
import { ModalService } from "@bitwarden/angular/services/modal.service";
|
||||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||||
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||||
import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength";
|
import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength";
|
||||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||||
@ -29,8 +30,9 @@ export class WeakPasswordsReportComponent extends CipherReportComponent implemen
|
|||||||
protected organizationService: OrganizationService,
|
protected organizationService: OrganizationService,
|
||||||
modalService: ModalService,
|
modalService: ModalService,
|
||||||
passwordRepromptService: PasswordRepromptService,
|
passwordRepromptService: PasswordRepromptService,
|
||||||
|
i18nService: I18nService,
|
||||||
) {
|
) {
|
||||||
super(modalService, passwordRepromptService, organizationService);
|
super(cipherService, modalService, passwordRepromptService, organizationService, i18nService);
|
||||||
}
|
}
|
||||||
|
|
||||||
async ngOnInit() {
|
async ngOnInit() {
|
||||||
@ -38,7 +40,10 @@ export class WeakPasswordsReportComponent extends CipherReportComponent implemen
|
|||||||
}
|
}
|
||||||
|
|
||||||
async setCiphers() {
|
async setCiphers() {
|
||||||
const allCiphers = await this.getAllCiphers();
|
const allCiphers: any = await this.getAllCiphers();
|
||||||
|
this.passwordStrengthCache = new Map<string, number>();
|
||||||
|
this.weakPasswordCiphers = [];
|
||||||
|
this.filterStatus = [0];
|
||||||
this.findWeakPasswords(allCiphers);
|
this.findWeakPasswords(allCiphers);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -55,6 +60,7 @@ export class WeakPasswordsReportComponent extends CipherReportComponent implemen
|
|||||||
) {
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const hasUserName = this.isUserNameNotEmpty(ciph);
|
const hasUserName = this.isUserNameNotEmpty(ciph);
|
||||||
const cacheKey = this.getCacheKey(ciph);
|
const cacheKey = this.getCacheKey(ciph);
|
||||||
if (!this.passwordStrengthCache.has(cacheKey)) {
|
if (!this.passwordStrengthCache.has(cacheKey)) {
|
||||||
@ -87,6 +93,7 @@ export class WeakPasswordsReportComponent extends CipherReportComponent implemen
|
|||||||
this.passwordStrengthCache.set(cacheKey, result.score);
|
this.passwordStrengthCache.set(cacheKey, result.score);
|
||||||
}
|
}
|
||||||
const score = this.passwordStrengthCache.get(cacheKey);
|
const score = this.passwordStrengthCache.get(cacheKey);
|
||||||
|
|
||||||
if (score != null && score <= 2) {
|
if (score != null && score <= 2) {
|
||||||
this.passwordStrengthMap.set(id, this.scoreKey(score));
|
this.passwordStrengthMap.set(id, this.scoreKey(score));
|
||||||
this.weakPasswordCiphers.push(ciph);
|
this.weakPasswordCiphers.push(ciph);
|
||||||
@ -98,11 +105,8 @@ export class WeakPasswordsReportComponent extends CipherReportComponent implemen
|
|||||||
this.passwordStrengthCache.get(this.getCacheKey(b))
|
this.passwordStrengthCache.get(this.getCacheKey(b))
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
this.ciphers = [...this.weakPasswordCiphers];
|
|
||||||
}
|
|
||||||
|
|
||||||
protected getAllCiphers(): Promise<CipherView[]> {
|
this.filterCiphersByOrg(this.weakPasswordCiphers);
|
||||||
return this.cipherService.getAllDecrypted();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected canManageCipher(c: CipherView): boolean {
|
protected canManageCipher(c: CipherView): boolean {
|
||||||
|
@ -1809,12 +1809,16 @@
|
|||||||
"unsecuredWebsitesFound": {
|
"unsecuredWebsitesFound": {
|
||||||
"message": "Unsecured websites found"
|
"message": "Unsecured websites found"
|
||||||
},
|
},
|
||||||
"unsecuredWebsitesFoundDesc": {
|
"unsecuredWebsitesFoundReportDesc": {
|
||||||
"message": "We found $COUNT$ items in your vault with unsecured URIs. You should change their URI scheme to https:// if the website allows it.",
|
"message": "We found $COUNT$ items in your $VAULT$ with unsecured URIs. You should change their URI scheme to https:// if the website allows it.",
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
"count": {
|
"count": {
|
||||||
"content": "$1",
|
"content": "$1",
|
||||||
"example": "8"
|
"example": "8"
|
||||||
|
},
|
||||||
|
"vault": {
|
||||||
|
"content": "$2",
|
||||||
|
"example": "this will be 'vault' or 'vaults'"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -1830,12 +1834,16 @@
|
|||||||
"inactive2faFound": {
|
"inactive2faFound": {
|
||||||
"message": "Logins without two-step login found"
|
"message": "Logins without two-step login found"
|
||||||
},
|
},
|
||||||
"inactive2faFoundDesc": {
|
"inactive2faFoundReportDesc": {
|
||||||
"message": "We found $COUNT$ website(s) in your vault that may not be configured with two-step login (according to 2fa.directory). To further protect these accounts, you should set up two-step login.",
|
"message": "We found $COUNT$ website(s) in your $VAULT$ that may not be configured with two-step login (according to 2fa.directory). To further protect these accounts, you should set up two-step login.",
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
"count": {
|
"count": {
|
||||||
"content": "$1",
|
"content": "$1",
|
||||||
"example": "8"
|
"example": "8"
|
||||||
|
},
|
||||||
|
"vault": {
|
||||||
|
"content": "$2",
|
||||||
|
"example": "this will be 'vault' or 'vaults'"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -1854,12 +1862,16 @@
|
|||||||
"exposedPasswordsFound": {
|
"exposedPasswordsFound": {
|
||||||
"message": "Exposed passwords found"
|
"message": "Exposed passwords found"
|
||||||
},
|
},
|
||||||
"exposedPasswordsFoundDesc": {
|
"exposedPasswordsFoundReportDesc": {
|
||||||
"message": "We found $COUNT$ items in your vault that have passwords that were exposed in known data breaches. You should change them to use a new password.",
|
"message": "We found $COUNT$ items in your $VAULT$ that have passwords that were exposed in known data breaches. You should change them to use a new password.",
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
"count": {
|
"count": {
|
||||||
"content": "$1",
|
"content": "$1",
|
||||||
"example": "8"
|
"example": "8"
|
||||||
|
},
|
||||||
|
"vault": {
|
||||||
|
"content": "$2",
|
||||||
|
"example": "this will be 'vault' or 'vaults'"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -1887,12 +1899,16 @@
|
|||||||
"weakPasswordsFound": {
|
"weakPasswordsFound": {
|
||||||
"message": "Weak passwords found"
|
"message": "Weak passwords found"
|
||||||
},
|
},
|
||||||
"weakPasswordsFoundDesc": {
|
"weakPasswordsFoundReportDesc": {
|
||||||
"message": "We found $COUNT$ items in your vault with passwords that are not strong. You should update them to use stronger passwords.",
|
"message": "We found $COUNT$ items in your $VAULT$ with passwords that are not strong. You should update them to use stronger passwords.",
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
"count": {
|
"count": {
|
||||||
"content": "$1",
|
"content": "$1",
|
||||||
"example": "8"
|
"example": "8"
|
||||||
|
},
|
||||||
|
"vault": {
|
||||||
|
"content": "$2",
|
||||||
|
"example": "this will be 'vault' or 'vaults'"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -1908,12 +1924,16 @@
|
|||||||
"reusedPasswordsFound": {
|
"reusedPasswordsFound": {
|
||||||
"message": "Reused passwords found"
|
"message": "Reused passwords found"
|
||||||
},
|
},
|
||||||
"reusedPasswordsFoundDesc": {
|
"reusedPasswordsFoundReportDesc": {
|
||||||
"message": "We found $COUNT$ passwords that are being reused in your vault. You should change them to a unique value.",
|
"message": "We found $COUNT$ passwords that are being reused in your $VAULT$. You should change them to a unique value.",
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
"count": {
|
"count": {
|
||||||
"content": "$1",
|
"content": "$1",
|
||||||
"example": "8"
|
"example": "8"
|
||||||
|
},
|
||||||
|
"vault": {
|
||||||
|
"content": "$2",
|
||||||
|
"example": "this will be 'vault' or 'vaults'"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
Loading…
Reference in New Issue
Block a user