mirror of
https://github.com/bitwarden/browser
synced 2026-02-12 14:34:02 +00:00
Merge branch 'master' into PS55-6-22
This commit is contained in:
@@ -1,10 +1,11 @@
|
||||
import { Directive, EventEmitter, Input, OnInit, Output } from "@angular/core";
|
||||
import { Observable } from "rxjs";
|
||||
|
||||
import { AuditService } from "@bitwarden/common/abstractions/audit.service";
|
||||
import { CipherService } from "@bitwarden/common/abstractions/cipher.service";
|
||||
import { CollectionService } from "@bitwarden/common/abstractions/collection.service";
|
||||
import { EventService } from "@bitwarden/common/abstractions/event.service";
|
||||
import { FolderService } from "@bitwarden/common/abstractions/folder.service";
|
||||
import { FolderService } from "@bitwarden/common/abstractions/folder/folder.service.abstraction";
|
||||
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/abstractions/log.service";
|
||||
import { MessagingService } from "@bitwarden/common/abstractions/messaging.service";
|
||||
@@ -51,7 +52,7 @@ export class AddEditComponent implements OnInit {
|
||||
|
||||
editMode = false;
|
||||
cipher: CipherView;
|
||||
folders: FolderView[];
|
||||
folders$: Observable<FolderView[]>;
|
||||
collections: CollectionView[] = [];
|
||||
title: string;
|
||||
formPromise: Promise<any>;
|
||||
@@ -109,6 +110,7 @@ export class AddEditComponent implements OnInit {
|
||||
{ name: "Maestro", value: "Maestro" },
|
||||
{ name: "UnionPay", value: "UnionPay" },
|
||||
{ name: "RuPay", value: "RuPay" },
|
||||
{ name: i18nService.t("cardBrandMir"), value: "Mir" },
|
||||
{ name: i18nService.t("other"), value: "Other" },
|
||||
];
|
||||
this.cardExpMonthOptions = [
|
||||
@@ -243,7 +245,7 @@ export class AddEditComponent implements OnInit {
|
||||
}
|
||||
}
|
||||
|
||||
this.folders = await this.folderService.getAllDecrypted();
|
||||
this.folders$ = this.folderService.folderViews$;
|
||||
|
||||
if (this.editMode && this.previousCipherId !== this.cipherId) {
|
||||
this.eventService.collect(EventType.Cipher_ClientViewed, this.cipherId);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Directive, EventEmitter, Input, OnInit, Output } from "@angular/core";
|
||||
|
||||
import { FolderService } from "@bitwarden/common/abstractions/folder.service";
|
||||
import { FolderApiServiceAbstraction } from "@bitwarden/common/abstractions/folder/folder-api.service.abstraction";
|
||||
import { FolderService } from "@bitwarden/common/abstractions/folder/folder.service.abstraction";
|
||||
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/abstractions/log.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
|
||||
@@ -20,6 +21,7 @@ export class FolderAddEditComponent implements OnInit {
|
||||
|
||||
constructor(
|
||||
protected folderService: FolderService,
|
||||
protected folderApiService: FolderApiServiceAbstraction,
|
||||
protected i18nService: I18nService,
|
||||
protected platformUtilsService: PlatformUtilsService,
|
||||
private logService: LogService
|
||||
@@ -41,7 +43,7 @@ export class FolderAddEditComponent implements OnInit {
|
||||
|
||||
try {
|
||||
const folder = await this.folderService.encrypt(this.folder);
|
||||
this.formPromise = this.folderService.saveWithServer(folder);
|
||||
this.formPromise = this.folderApiService.save(folder);
|
||||
await this.formPromise;
|
||||
this.platformUtilsService.showToast(
|
||||
"success",
|
||||
@@ -70,7 +72,7 @@ export class FolderAddEditComponent implements OnInit {
|
||||
}
|
||||
|
||||
try {
|
||||
this.deletePromise = this.folderService.deleteWithServer(this.folder.id);
|
||||
this.deletePromise = this.folderApiService.delete(this.folder.id);
|
||||
await this.deletePromise;
|
||||
this.platformUtilsService.showToast("success", null, this.i18nService.t("deletedFolder"));
|
||||
this.onDeletedFolder.emit(this.folder);
|
||||
|
||||
@@ -20,6 +20,7 @@ const cardIcons: Record<string, string> = {
|
||||
Maestro: "card-maestro",
|
||||
UnionPay: "card-union-pay",
|
||||
RuPay: "card-ru-pay",
|
||||
Mir: "card-mir",
|
||||
};
|
||||
|
||||
@Component({
|
||||
|
||||
@@ -1,16 +1,23 @@
|
||||
import { Directive, OnInit } from "@angular/core";
|
||||
import { Directive, EventEmitter, Input, OnInit, Output } from "@angular/core";
|
||||
import { AbstractControl, FormBuilder, ValidatorFn, Validators } from "@angular/forms";
|
||||
import { Router } from "@angular/router";
|
||||
|
||||
import { InputsFieldMatch } from "@bitwarden/angular/validators/inputsFieldMatch.validator";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { AuthService } from "@bitwarden/common/abstractions/auth.service";
|
||||
import { CryptoService } from "@bitwarden/common/abstractions/crypto.service";
|
||||
import { EnvironmentService } from "@bitwarden/common/abstractions/environment.service";
|
||||
import {
|
||||
AllValidationErrors,
|
||||
FormValidationErrorsService,
|
||||
} from "@bitwarden/common/abstractions/formValidationErrors.service";
|
||||
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/abstractions/log.service";
|
||||
import { PasswordGenerationService } from "@bitwarden/common/abstractions/passwordGeneration.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
|
||||
import { StateService } from "@bitwarden/common/abstractions/state.service";
|
||||
import { DEFAULT_KDF_ITERATIONS, DEFAULT_KDF_TYPE } from "@bitwarden/common/enums/kdfType";
|
||||
import { PasswordLogInCredentials } from "@bitwarden/common/models/domain/logInCredentials";
|
||||
import { KeysRequest } from "@bitwarden/common/models/request/keysRequest";
|
||||
import { ReferenceEventRequest } from "@bitwarden/common/models/request/referenceEventRequest";
|
||||
import { RegisterRequest } from "@bitwarden/common/models/request/registerRequest";
|
||||
@@ -19,22 +26,48 @@ import { CaptchaProtectedComponent } from "./captchaProtected.component";
|
||||
|
||||
@Directive()
|
||||
export class RegisterComponent extends CaptchaProtectedComponent implements OnInit {
|
||||
name = "";
|
||||
email = "";
|
||||
masterPassword = "";
|
||||
confirmMasterPassword = "";
|
||||
hint = "";
|
||||
@Input() isInTrialFlow = false;
|
||||
@Output() createdAccount = new EventEmitter<string>();
|
||||
|
||||
showPassword = false;
|
||||
formPromise: Promise<any>;
|
||||
masterPasswordScore: number;
|
||||
referenceData: ReferenceEventRequest;
|
||||
showTerms = true;
|
||||
acceptPolicies = false;
|
||||
showErrorSummary = false;
|
||||
|
||||
formGroup = this.formBuilder.group(
|
||||
{
|
||||
email: ["", [Validators.required, Validators.email]],
|
||||
name: [""],
|
||||
masterPassword: ["", [Validators.required, Validators.minLength(8)]],
|
||||
confirmMasterPassword: ["", [Validators.required, Validators.minLength(8)]],
|
||||
hint: [
|
||||
null,
|
||||
[
|
||||
InputsFieldMatch.validateInputsDoesntMatch(
|
||||
"masterPassword",
|
||||
this.i18nService.t("hintEqualsPassword")
|
||||
),
|
||||
],
|
||||
],
|
||||
acceptPolicies: [false, [this.acceptPoliciesValidation()]],
|
||||
},
|
||||
{
|
||||
validator: InputsFieldMatch.validateFormInputsMatch(
|
||||
"masterPassword",
|
||||
"confirmMasterPassword",
|
||||
this.i18nService.t("masterPassDoesntMatch")
|
||||
),
|
||||
}
|
||||
);
|
||||
|
||||
protected successRoute = "login";
|
||||
private masterPasswordStrengthTimeout: any;
|
||||
|
||||
constructor(
|
||||
protected formValidationErrorService: FormValidationErrorsService,
|
||||
protected formBuilder: FormBuilder,
|
||||
protected authService: AuthService,
|
||||
protected router: Router,
|
||||
i18nService: I18nService,
|
||||
@@ -84,59 +117,38 @@ export class RegisterComponent extends CaptchaProtectedComponent implements OnIn
|
||||
}
|
||||
}
|
||||
|
||||
async submit() {
|
||||
if (!this.acceptPolicies && this.showTerms) {
|
||||
async submit(showToast = true) {
|
||||
let email = this.formGroup.get("email")?.value;
|
||||
let name = this.formGroup.get("name")?.value;
|
||||
const masterPassword = this.formGroup.get("masterPassword")?.value;
|
||||
const hint = this.formGroup.get("hint")?.value;
|
||||
|
||||
this.formGroup.markAllAsTouched();
|
||||
this.showErrorSummary = true;
|
||||
|
||||
if (this.formGroup.get("acceptPolicies").hasError("required")) {
|
||||
this.platformUtilsService.showToast(
|
||||
"error",
|
||||
this.i18nService.t("errorOccurred"),
|
||||
this.i18nService.t("acceptPoliciesError")
|
||||
this.i18nService.t("acceptPoliciesRequired")
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.email == null || this.email === "") {
|
||||
this.platformUtilsService.showToast(
|
||||
"error",
|
||||
this.i18nService.t("errorOccurred"),
|
||||
this.i18nService.t("emailRequired")
|
||||
);
|
||||
//web
|
||||
if (this.formGroup.invalid && !showToast) {
|
||||
return;
|
||||
}
|
||||
if (this.email.indexOf("@") === -1) {
|
||||
this.platformUtilsService.showToast(
|
||||
"error",
|
||||
this.i18nService.t("errorOccurred"),
|
||||
this.i18nService.t("invalidEmail")
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (this.masterPassword == null || this.masterPassword === "") {
|
||||
this.platformUtilsService.showToast(
|
||||
"error",
|
||||
this.i18nService.t("errorOccurred"),
|
||||
this.i18nService.t("masterPassRequired")
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (this.masterPassword.length < 8) {
|
||||
this.platformUtilsService.showToast(
|
||||
"error",
|
||||
this.i18nService.t("errorOccurred"),
|
||||
this.i18nService.t("masterPassLength")
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (this.masterPassword !== this.confirmMasterPassword) {
|
||||
this.platformUtilsService.showToast(
|
||||
"error",
|
||||
this.i18nService.t("errorOccurred"),
|
||||
this.i18nService.t("masterPassDoesntMatch")
|
||||
);
|
||||
|
||||
//desktop, browser
|
||||
if (this.formGroup.invalid && showToast) {
|
||||
const errorText = this.getErrorToastMessage();
|
||||
this.platformUtilsService.showToast("error", this.i18nService.t("errorOccurred"), errorText);
|
||||
return;
|
||||
}
|
||||
|
||||
const strengthResult = this.passwordGenerationService.passwordStrength(
|
||||
this.masterPassword,
|
||||
masterPassword,
|
||||
this.getPasswordStrengthUserInput()
|
||||
);
|
||||
if (strengthResult != null && strengthResult.score < 3) {
|
||||
@@ -152,33 +164,19 @@ export class RegisterComponent extends CaptchaProtectedComponent implements OnIn
|
||||
}
|
||||
}
|
||||
|
||||
if (this.hint === this.masterPassword) {
|
||||
this.platformUtilsService.showToast(
|
||||
"error",
|
||||
this.i18nService.t("errorOccurred"),
|
||||
this.i18nService.t("hintEqualsPassword")
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
this.name = this.name === "" ? null : this.name;
|
||||
this.email = this.email.trim().toLowerCase();
|
||||
name = name === "" ? null : name;
|
||||
email = email.trim().toLowerCase();
|
||||
const kdf = DEFAULT_KDF_TYPE;
|
||||
const kdfIterations = DEFAULT_KDF_ITERATIONS;
|
||||
const key = await this.cryptoService.makeKey(
|
||||
this.masterPassword,
|
||||
this.email,
|
||||
kdf,
|
||||
kdfIterations
|
||||
);
|
||||
const key = await this.cryptoService.makeKey(masterPassword, email, kdf, kdfIterations);
|
||||
const encKey = await this.cryptoService.makeEncKey(key);
|
||||
const hashedPassword = await this.cryptoService.hashPassword(this.masterPassword, key);
|
||||
const hashedPassword = await this.cryptoService.hashPassword(masterPassword, key);
|
||||
const keys = await this.cryptoService.makeKeyPair(encKey[0]);
|
||||
const request = new RegisterRequest(
|
||||
this.email,
|
||||
this.name,
|
||||
email,
|
||||
name,
|
||||
hashedPassword,
|
||||
this.hint,
|
||||
hint,
|
||||
encKey[1].encryptedString,
|
||||
kdf,
|
||||
kdfIterations,
|
||||
@@ -203,25 +201,49 @@ export class RegisterComponent extends CaptchaProtectedComponent implements OnIn
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
this.platformUtilsService.showToast("success", null, this.i18nService.t("newAccountCreated"));
|
||||
this.router.navigate([this.successRoute], { queryParams: { email: this.email } });
|
||||
|
||||
if (this.isInTrialFlow) {
|
||||
this.platformUtilsService.showToast(
|
||||
"success",
|
||||
null,
|
||||
this.i18nService.t("trialAccountCreated")
|
||||
);
|
||||
//login user here
|
||||
const credentials = new PasswordLogInCredentials(
|
||||
email,
|
||||
masterPassword,
|
||||
this.captchaToken,
|
||||
null
|
||||
);
|
||||
await this.authService.logIn(credentials);
|
||||
|
||||
this.createdAccount.emit(this.formGroup.get("email")?.value);
|
||||
} else {
|
||||
this.platformUtilsService.showToast(
|
||||
"success",
|
||||
null,
|
||||
this.i18nService.t("newAccountCreated")
|
||||
);
|
||||
this.router.navigate([this.successRoute], { queryParams: { email: email } });
|
||||
}
|
||||
} catch (e) {
|
||||
this.logService.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
togglePassword(confirmField: boolean) {
|
||||
togglePassword() {
|
||||
this.showPassword = !this.showPassword;
|
||||
document.getElementById(confirmField ? "masterPasswordRetype" : "masterPassword").focus();
|
||||
}
|
||||
|
||||
updatePasswordStrength() {
|
||||
const masterPassword = this.formGroup.get("masterPassword")?.value;
|
||||
|
||||
if (this.masterPasswordStrengthTimeout != null) {
|
||||
clearTimeout(this.masterPasswordStrengthTimeout);
|
||||
}
|
||||
this.masterPasswordStrengthTimeout = setTimeout(() => {
|
||||
const strengthResult = this.passwordGenerationService.passwordStrength(
|
||||
this.masterPassword,
|
||||
masterPassword,
|
||||
this.getPasswordStrengthUserInput()
|
||||
);
|
||||
this.masterPasswordScore = strengthResult == null ? null : strengthResult.score;
|
||||
@@ -230,19 +252,56 @@ export class RegisterComponent extends CaptchaProtectedComponent implements OnIn
|
||||
|
||||
private getPasswordStrengthUserInput() {
|
||||
let userInput: string[] = [];
|
||||
const atPosition = this.email.indexOf("@");
|
||||
const email = this.formGroup.get("email")?.value;
|
||||
const name = this.formGroup.get("name").value;
|
||||
const atPosition = email.indexOf("@");
|
||||
if (atPosition > -1) {
|
||||
userInput = userInput.concat(
|
||||
this.email
|
||||
email
|
||||
.substr(0, atPosition)
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
.split(/[^A-Za-z0-9]/)
|
||||
);
|
||||
}
|
||||
if (this.name != null && this.name !== "") {
|
||||
userInput = userInput.concat(this.name.trim().toLowerCase().split(" "));
|
||||
if (name != null && name !== "") {
|
||||
userInput = userInput.concat(name.trim().toLowerCase().split(" "));
|
||||
}
|
||||
return userInput;
|
||||
}
|
||||
|
||||
private getErrorToastMessage() {
|
||||
const error: AllValidationErrors = this.formValidationErrorService
|
||||
.getFormValidationErrors(this.formGroup.controls)
|
||||
.shift();
|
||||
|
||||
if (error) {
|
||||
switch (error.errorName) {
|
||||
case "email":
|
||||
return this.i18nService.t("invalidEmail");
|
||||
case "inputsDoesntMatchError":
|
||||
return this.i18nService.t("masterPassDoesntMatch");
|
||||
case "inputsMatchError":
|
||||
return this.i18nService.t("hintEqualsPassword");
|
||||
default:
|
||||
return this.i18nService.t(this.errorTag(error));
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
private errorTag(error: AllValidationErrors): string {
|
||||
const name = error.errorName.charAt(0).toUpperCase() + error.errorName.slice(1);
|
||||
return `${error.controlName}${name}`;
|
||||
}
|
||||
|
||||
//validation would be ignored on selfhosted
|
||||
private acceptPoliciesValidation(): ValidatorFn {
|
||||
return (control: AbstractControl) => {
|
||||
const ctrlValue = control.value;
|
||||
|
||||
return !ctrlValue && this.showTerms ? { required: true } : null;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -128,6 +128,7 @@ export class SetPasswordComponent extends BaseChangePasswordComponent {
|
||||
);
|
||||
|
||||
const resetRequest = new OrganizationUserResetPasswordEnrollmentRequest();
|
||||
resetRequest.masterPasswordHash = masterPasswordHash;
|
||||
resetRequest.resetPasswordKey = encryptedKey.encryptedString;
|
||||
|
||||
return this.apiService.putOrganizationUserResetPasswordEnrollment(
|
||||
|
||||
BIN
libs/angular/src/images/cards/mir-dark.png
Normal file
BIN
libs/angular/src/images/cards/mir-dark.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 505 B |
BIN
libs/angular/src/images/cards/mir-light.png
Normal file
BIN
libs/angular/src/images/cards/mir-light.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 644 B |
@@ -41,6 +41,7 @@ export class CollectionFilterComponent {
|
||||
|
||||
applyFilter(collection: CollectionView) {
|
||||
this.activeFilter.resetFilter();
|
||||
this.activeFilter.selectedCollection = true;
|
||||
this.activeFilter.selectedCollectionId = collection.id;
|
||||
this.onFilterChange.emit(this.activeFilter);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,237 @@
|
||||
import { CipherType } from "@bitwarden/common/enums/cipherType";
|
||||
import { CipherView } from "@bitwarden/common/models/view/cipherView";
|
||||
|
||||
import { VaultFilter } from "./vault-filter.model";
|
||||
|
||||
describe("VaultFilter", () => {
|
||||
describe("filterFunction", () => {
|
||||
describe("generic cipher", () => {
|
||||
it("should return true when not filtering for anything specific", () => {
|
||||
const cipher = createCipher();
|
||||
const filterFunction = createFilterFunction({ status: "all" });
|
||||
|
||||
const result = filterFunction(cipher);
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("given a favorite cipher", () => {
|
||||
const cipher = createCipher({ favorite: true });
|
||||
|
||||
it("should return true when filtering for favorites", () => {
|
||||
const filterFunction = createFilterFunction({ status: "favorites" });
|
||||
|
||||
const result = filterFunction(cipher);
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it("should return false when filtering for trash", () => {
|
||||
const filterFunction = createFilterFunction({ status: "trash" });
|
||||
|
||||
const result = filterFunction(cipher);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("given a deleted cipher", () => {
|
||||
const cipher = createCipher({ deletedDate: new Date() });
|
||||
|
||||
it("should return true when filtering for trash", () => {
|
||||
const filterFunction = createFilterFunction({ status: "trash" });
|
||||
|
||||
const result = filterFunction(cipher);
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it("should return false when filtering for favorites", () => {
|
||||
const filterFunction = createFilterFunction({ status: "favorites" });
|
||||
|
||||
const result = filterFunction(cipher);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("given a cipher with type", () => {
|
||||
it("should return true when filter matches cipher type", () => {
|
||||
const cipher = createCipher({ type: CipherType.Identity });
|
||||
const filterFunction = createFilterFunction({ cipherType: CipherType.Identity });
|
||||
|
||||
const result = filterFunction(cipher);
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it("should return false when filter does not match cipher type", () => {
|
||||
const cipher = createCipher({ type: CipherType.Card });
|
||||
const filterFunction = createFilterFunction({ cipherType: CipherType.Identity });
|
||||
|
||||
const result = filterFunction(cipher);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("given a cipher with folder id", () => {
|
||||
it("should return true when filter matches folder id", () => {
|
||||
const cipher = createCipher({ folderId: "folderId" });
|
||||
const filterFunction = createFilterFunction({
|
||||
selectedFolder: true,
|
||||
selectedFolderId: "folderId",
|
||||
});
|
||||
|
||||
const result = filterFunction(cipher);
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it("should return false when filter does not match folder id", () => {
|
||||
const cipher = createCipher({ folderId: "folderId" });
|
||||
const filterFunction = createFilterFunction({
|
||||
selectedFolder: true,
|
||||
selectedFolderId: "anotherFolderId",
|
||||
});
|
||||
|
||||
const result = filterFunction(cipher);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("given a cipher without folder", () => {
|
||||
const cipher = createCipher({ folderId: null });
|
||||
|
||||
it("should return true when filtering on unassigned folder", () => {
|
||||
const filterFunction = createFilterFunction({
|
||||
selectedFolder: true,
|
||||
selectedFolderId: null,
|
||||
});
|
||||
|
||||
const result = filterFunction(cipher);
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("given an organizational cipher (with organization and collections)", () => {
|
||||
const cipher = createCipher({
|
||||
organizationId: "organizationId",
|
||||
collectionIds: ["collectionId", "anotherId"],
|
||||
});
|
||||
|
||||
it("should return true when filter matches collection id", () => {
|
||||
const filterFunction = createFilterFunction({
|
||||
selectedCollection: true,
|
||||
selectedCollectionId: "collectionId",
|
||||
});
|
||||
|
||||
const result = filterFunction(cipher);
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it("should return false when filter does not match collection id", () => {
|
||||
const filterFunction = createFilterFunction({
|
||||
selectedCollection: true,
|
||||
selectedCollectionId: "nonMatchingId",
|
||||
});
|
||||
|
||||
const result = filterFunction(cipher);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it("should return false when filter does not match organization id", () => {
|
||||
const filterFunction = createFilterFunction({
|
||||
selectedOrganizationId: "anotherOrganizationId",
|
||||
});
|
||||
|
||||
const result = filterFunction(cipher);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it("should return false when filtering for my vault only", () => {
|
||||
const filterFunction = createFilterFunction({
|
||||
myVaultOnly: true,
|
||||
});
|
||||
|
||||
const result = filterFunction(cipher);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("given an unassigned organizational cipher (with organization, without collection)", () => {
|
||||
const cipher = createCipher({ organizationId: "organizationId", collectionIds: [] });
|
||||
|
||||
it("should return true when filtering for unassigned collection", () => {
|
||||
const filterFunction = createFilterFunction({
|
||||
selectedCollection: true,
|
||||
selectedCollectionId: null,
|
||||
});
|
||||
|
||||
const result = filterFunction(cipher);
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it("should return true when filter matches organization id", () => {
|
||||
const filterFunction = createFilterFunction({
|
||||
selectedOrganizationId: "organizationId",
|
||||
});
|
||||
|
||||
const result = filterFunction(cipher);
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("given an individual cipher (without organization or collection)", () => {
|
||||
const cipher = createCipher({ organizationId: null, collectionIds: [] });
|
||||
|
||||
it("should return false when filtering for unassigned collection", () => {
|
||||
const filterFunction = createFilterFunction({
|
||||
selectedCollection: true,
|
||||
selectedCollectionId: null,
|
||||
});
|
||||
|
||||
const result = filterFunction(cipher);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it("should return true when filtering for my vault only", () => {
|
||||
const cipher = createCipher({ organizationId: null });
|
||||
const filterFunction = createFilterFunction({
|
||||
myVaultOnly: true,
|
||||
});
|
||||
|
||||
const result = filterFunction(cipher);
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function createFilterFunction(options: Partial<VaultFilter> = {}) {
|
||||
return new VaultFilter(options).buildFilter();
|
||||
}
|
||||
|
||||
function createCipher(options: Partial<CipherView> = {}) {
|
||||
const cipher = new CipherView();
|
||||
|
||||
cipher.favorite = options.favorite ?? false;
|
||||
cipher.deletedDate = options.deletedDate;
|
||||
cipher.type = options.type;
|
||||
cipher.folderId = options.folderId;
|
||||
cipher.collectionIds = options.collectionIds;
|
||||
cipher.organizationId = options.organizationId;
|
||||
|
||||
return cipher;
|
||||
}
|
||||
@@ -1,9 +1,13 @@
|
||||
import { CipherType } from "@bitwarden/common/enums/cipherType";
|
||||
import { CipherView } from "@bitwarden/common/models/view/cipherView";
|
||||
|
||||
import { CipherStatus } from "./cipher-status.model";
|
||||
|
||||
export type VaultFilterFunction = (cipher: CipherView) => boolean;
|
||||
|
||||
export class VaultFilter {
|
||||
cipherType?: CipherType;
|
||||
selectedCollection = false; // This is needed because of how the "Unassigned" collection works. It has a null id.
|
||||
selectedCollectionId?: string;
|
||||
status?: CipherStatus;
|
||||
selectedFolder = false; // This is needed because of how the "No Folder" folder works. It has a null id.
|
||||
@@ -19,6 +23,7 @@ export class VaultFilter {
|
||||
resetFilter() {
|
||||
this.cipherType = null;
|
||||
this.status = null;
|
||||
this.selectedCollection = false;
|
||||
this.selectedCollectionId = null;
|
||||
this.selectedFolder = false;
|
||||
this.selectedFolderId = null;
|
||||
@@ -29,4 +34,41 @@ export class VaultFilter {
|
||||
this.selectedOrganizationId = null;
|
||||
this.resetFilter();
|
||||
}
|
||||
|
||||
buildFilter(): VaultFilterFunction {
|
||||
return (cipher) => {
|
||||
let cipherPassesFilter = true;
|
||||
if (this.status === "favorites" && cipherPassesFilter) {
|
||||
cipherPassesFilter = cipher.favorite;
|
||||
}
|
||||
if (this.status === "trash" && cipherPassesFilter) {
|
||||
cipherPassesFilter = cipher.isDeleted;
|
||||
}
|
||||
if (this.cipherType != null && cipherPassesFilter) {
|
||||
cipherPassesFilter = cipher.type === this.cipherType;
|
||||
}
|
||||
if (this.selectedFolder && this.selectedFolderId == null && cipherPassesFilter) {
|
||||
cipherPassesFilter = cipher.folderId == null;
|
||||
}
|
||||
if (this.selectedFolder && this.selectedFolderId != null && cipherPassesFilter) {
|
||||
cipherPassesFilter = cipher.folderId === this.selectedFolderId;
|
||||
}
|
||||
if (this.selectedCollection && this.selectedCollectionId == null && cipherPassesFilter) {
|
||||
cipherPassesFilter =
|
||||
cipher.organizationId != null &&
|
||||
(cipher.collectionIds == null || cipher.collectionIds.length === 0);
|
||||
}
|
||||
if (this.selectedCollection && this.selectedCollectionId != null && cipherPassesFilter) {
|
||||
cipherPassesFilter =
|
||||
cipher.collectionIds != null && cipher.collectionIds.includes(this.selectedCollectionId);
|
||||
}
|
||||
if (this.selectedOrganizationId != null && cipherPassesFilter) {
|
||||
cipherPassesFilter = cipher.organizationId === this.selectedOrganizationId;
|
||||
}
|
||||
if (this.myVaultOnly && cipherPassesFilter) {
|
||||
cipherPassesFilter = cipher.organizationId === null;
|
||||
}
|
||||
return cipherPassesFilter;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Directive, EventEmitter, Input, OnInit, Output } from "@angular/core";
|
||||
import { firstValueFrom, Observable } from "rxjs";
|
||||
|
||||
import { Organization } from "@bitwarden/common/models/domain/organization";
|
||||
import { ITreeNodeObject } from "@bitwarden/common/models/domain/treeNode";
|
||||
@@ -28,7 +29,7 @@ export class VaultFilterComponent implements OnInit {
|
||||
activePersonalOwnershipPolicy: boolean;
|
||||
activeSingleOrganizationPolicy: boolean;
|
||||
collections: DynamicTreeNode<CollectionView>;
|
||||
folders: DynamicTreeNode<FolderView>;
|
||||
folders$: Observable<DynamicTreeNode<FolderView>>;
|
||||
|
||||
constructor(protected vaultFilterService: VaultFilterService) {}
|
||||
|
||||
@@ -45,7 +46,7 @@ export class VaultFilterComponent implements OnInit {
|
||||
this.activeSingleOrganizationPolicy =
|
||||
await this.vaultFilterService.checkForSingleOrganizationPolicy();
|
||||
}
|
||||
this.folders = await this.vaultFilterService.buildFolders();
|
||||
this.folders$ = await this.vaultFilterService.buildNestedFolders();
|
||||
this.collections = await this.initCollections();
|
||||
this.isLoaded = true;
|
||||
}
|
||||
@@ -67,13 +68,13 @@ export class VaultFilterComponent implements OnInit {
|
||||
async applyFilter(filter: VaultFilter) {
|
||||
if (filter.refreshCollectionsAndFolders) {
|
||||
await this.reloadCollectionsAndFolders(filter);
|
||||
filter = this.pruneInvalidatedFilterSelections(filter);
|
||||
filter = await this.pruneInvalidatedFilterSelections(filter);
|
||||
}
|
||||
this.onFilterChange.emit(filter);
|
||||
}
|
||||
|
||||
async reloadCollectionsAndFolders(filter: VaultFilter) {
|
||||
this.folders = await this.vaultFilterService.buildFolders(filter.selectedOrganizationId);
|
||||
this.folders$ = await this.vaultFilterService.buildNestedFolders(filter.selectedOrganizationId);
|
||||
this.collections = filter.myVaultOnly
|
||||
? null
|
||||
: await this.vaultFilterService.buildCollections(filter.selectedOrganizationId);
|
||||
@@ -95,14 +96,17 @@ export class VaultFilterComponent implements OnInit {
|
||||
this.onEditFolder.emit(folder);
|
||||
}
|
||||
|
||||
protected pruneInvalidatedFilterSelections(filter: VaultFilter): VaultFilter {
|
||||
filter = this.pruneInvalidFolderSelection(filter);
|
||||
protected async pruneInvalidatedFilterSelections(filter: VaultFilter): Promise<VaultFilter> {
|
||||
filter = await this.pruneInvalidFolderSelection(filter);
|
||||
filter = this.pruneInvalidCollectionSelection(filter);
|
||||
return filter;
|
||||
}
|
||||
|
||||
protected pruneInvalidFolderSelection(filter: VaultFilter): VaultFilter {
|
||||
if (filter.selectedFolder && !this.folders?.hasId(filter.selectedFolderId)) {
|
||||
protected async pruneInvalidFolderSelection(filter: VaultFilter): Promise<VaultFilter> {
|
||||
if (
|
||||
filter.selectedFolder &&
|
||||
!(await firstValueFrom(this.folders$))?.hasId(filter.selectedFolderId)
|
||||
) {
|
||||
filter.selectedFolder = false;
|
||||
filter.selectedFolderId = null;
|
||||
}
|
||||
@@ -111,9 +115,12 @@ export class VaultFilterComponent implements OnInit {
|
||||
|
||||
protected pruneInvalidCollectionSelection(filter: VaultFilter): VaultFilter {
|
||||
if (
|
||||
filter.selectedCollectionId != null &&
|
||||
!this.collections?.hasId(filter.selectedCollectionId)
|
||||
filter.myVaultOnly ||
|
||||
(filter.selectedCollection &&
|
||||
filter.selectedCollectionId != null &&
|
||||
!this.collections?.hasId(filter.selectedCollectionId))
|
||||
) {
|
||||
filter.selectedCollection = false;
|
||||
filter.selectedCollectionId = null;
|
||||
}
|
||||
return filter;
|
||||
|
||||
@@ -1,18 +1,23 @@
|
||||
import { Injectable } from "@angular/core";
|
||||
import { firstValueFrom, from, mergeMap, Observable } from "rxjs";
|
||||
|
||||
import { CipherService } from "@bitwarden/common/abstractions/cipher.service";
|
||||
import { CollectionService } from "@bitwarden/common/abstractions/collection.service";
|
||||
import { FolderService } from "@bitwarden/common/abstractions/folder.service";
|
||||
import { FolderService } from "@bitwarden/common/abstractions/folder/folder.service.abstraction";
|
||||
import { OrganizationService } from "@bitwarden/common/abstractions/organization.service";
|
||||
import { PolicyService } from "@bitwarden/common/abstractions/policy.service";
|
||||
import { StateService } from "@bitwarden/common/abstractions/state.service";
|
||||
import { PolicyType } from "@bitwarden/common/enums/policyType";
|
||||
import { ServiceUtils } from "@bitwarden/common/misc/serviceUtils";
|
||||
import { Organization } from "@bitwarden/common/models/domain/organization";
|
||||
import { TreeNode } from "@bitwarden/common/models/domain/treeNode";
|
||||
import { CollectionView } from "@bitwarden/common/models/view/collectionView";
|
||||
import { FolderView } from "@bitwarden/common/models/view/folderView";
|
||||
|
||||
import { DynamicTreeNode } from "./models/dynamic-tree-node.model";
|
||||
|
||||
const NestingDelimiter = "/";
|
||||
|
||||
@Injectable()
|
||||
export class VaultFilterService {
|
||||
constructor(
|
||||
@@ -36,25 +41,30 @@ export class VaultFilterService {
|
||||
return await this.organizationService.getAll();
|
||||
}
|
||||
|
||||
async buildFolders(organizationId?: string): Promise<DynamicTreeNode<FolderView>> {
|
||||
const storedFolders = await this.folderService.getAllDecrypted();
|
||||
let folders: FolderView[];
|
||||
if (organizationId != null) {
|
||||
const ciphers = await this.cipherService.getAllDecrypted();
|
||||
const orgCiphers = ciphers.filter((c) => c.organizationId == organizationId);
|
||||
folders = storedFolders.filter(
|
||||
(f) =>
|
||||
orgCiphers.filter((oc) => oc.folderId == f.id).length > 0 ||
|
||||
ciphers.filter((c) => c.folderId == f.id).length < 1
|
||||
);
|
||||
} else {
|
||||
folders = storedFolders;
|
||||
}
|
||||
const nestedFolders = await this.folderService.getAllNested(folders);
|
||||
return new DynamicTreeNode<FolderView>({
|
||||
fullList: folders,
|
||||
nestedList: nestedFolders,
|
||||
});
|
||||
buildNestedFolders(organizationId?: string): Observable<DynamicTreeNode<FolderView>> {
|
||||
const transformation = async (storedFolders: FolderView[]) => {
|
||||
let folders: FolderView[];
|
||||
if (organizationId != null) {
|
||||
const ciphers = await this.cipherService.getAllDecrypted();
|
||||
const orgCiphers = ciphers.filter((c) => c.organizationId == organizationId);
|
||||
folders = storedFolders.filter(
|
||||
(f) =>
|
||||
orgCiphers.filter((oc) => oc.folderId == f.id).length > 0 ||
|
||||
ciphers.filter((c) => c.folderId == f.id).length < 1
|
||||
);
|
||||
} else {
|
||||
folders = storedFolders;
|
||||
}
|
||||
const nestedFolders = await this.getAllFoldersNested(folders);
|
||||
return new DynamicTreeNode<FolderView>({
|
||||
fullList: folders,
|
||||
nestedList: nestedFolders,
|
||||
});
|
||||
};
|
||||
|
||||
return this.folderService.folderViews$.pipe(
|
||||
mergeMap((folders) => from(transformation(folders)))
|
||||
);
|
||||
}
|
||||
|
||||
async buildCollections(organizationId?: string): Promise<DynamicTreeNode<CollectionView>> {
|
||||
@@ -79,4 +89,23 @@ export class VaultFilterService {
|
||||
async checkForPersonalOwnershipPolicy(): Promise<boolean> {
|
||||
return await this.policyService.policyAppliesToUser(PolicyType.PersonalOwnership);
|
||||
}
|
||||
|
||||
protected async getAllFoldersNested(folders: FolderView[]): Promise<TreeNode<FolderView>[]> {
|
||||
const nodes: TreeNode<FolderView>[] = [];
|
||||
folders.forEach((f) => {
|
||||
const folderCopy = new FolderView();
|
||||
folderCopy.id = f.id;
|
||||
folderCopy.revisionDate = f.revisionDate;
|
||||
const parts = f.name != null ? f.name.replace(/^\/+|\/+$/g, "").split(NestingDelimiter) : [];
|
||||
ServiceUtils.nestedTraverse(nodes, 0, parts, folderCopy, null, NestingDelimiter);
|
||||
});
|
||||
return nodes;
|
||||
}
|
||||
|
||||
async getFolderNested(id: string): Promise<TreeNode<FolderView>> {
|
||||
const folders = await this.getAllFoldersNested(
|
||||
await firstValueFrom(this.folderService.folderViews$)
|
||||
);
|
||||
return ServiceUtils.getTreeNodeObject(folders, id) as TreeNode<FolderView>;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,6 +25,12 @@ const numberFormats: Record<string, CardRuleEntry[]> = {
|
||||
{ cardLength: 19, blocks: [6, 13] },
|
||||
],
|
||||
Amex: [{ cardLength: 15, blocks: [4, 6, 5] }],
|
||||
Mir: [
|
||||
{ cardLength: 16, blocks: [4, 4, 4, 4] },
|
||||
{ cardLength: 17, blocks: [5, 4, 4, 4] },
|
||||
{ cardLength: 18, blocks: [6, 4, 4, 4] },
|
||||
{ cardLength: 19, blocks: [7, 4, 4, 4] },
|
||||
],
|
||||
Other: [{ cardLength: 16, blocks: [4, 4, 4, 4] }],
|
||||
};
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ $card-icons: (
|
||||
"mastercard": $card-icons-base + "mastercard-light.png",
|
||||
"union-pay": $card-icons-base + "union_pay-light.png",
|
||||
"ru-pay": $card-icons-base + "ru_pay-light.png",
|
||||
"mir": $card-icons-base + "mir-light.png",
|
||||
);
|
||||
|
||||
$card-icons-dark: (
|
||||
@@ -21,6 +22,7 @@ $card-icons-dark: (
|
||||
"mastercard": $card-icons-base + "mastercard-dark.png",
|
||||
"union-pay": $card-icons-base + "union_pay-dark.png",
|
||||
"ru-pay": $card-icons-base + "ru_pay-dark.png",
|
||||
"mir": $card-icons-base + "mir-dark.png",
|
||||
);
|
||||
|
||||
.credit-card-icon {
|
||||
|
||||
@@ -16,7 +16,12 @@ import { EnvironmentService as EnvironmentServiceAbstraction } from "@bitwarden/
|
||||
import { EventService as EventServiceAbstraction } from "@bitwarden/common/abstractions/event.service";
|
||||
import { ExportService as ExportServiceAbstraction } from "@bitwarden/common/abstractions/export.service";
|
||||
import { FileUploadService as FileUploadServiceAbstraction } from "@bitwarden/common/abstractions/fileUpload.service";
|
||||
import { FolderService as FolderServiceAbstraction } from "@bitwarden/common/abstractions/folder.service";
|
||||
import { FolderApiServiceAbstraction } from "@bitwarden/common/abstractions/folder/folder-api.service.abstraction";
|
||||
import {
|
||||
FolderService as FolderServiceAbstraction,
|
||||
InternalFolderService,
|
||||
} from "@bitwarden/common/abstractions/folder/folder.service.abstraction";
|
||||
import { FormValidationErrorsService as FormValidationErrorsServiceAbstraction } from "@bitwarden/common/abstractions/formValidationErrors.service";
|
||||
import { I18nService as I18nServiceAbstraction } from "@bitwarden/common/abstractions/i18n.service";
|
||||
import { KeyConnectorService as KeyConnectorServiceAbstraction } from "@bitwarden/common/abstractions/keyConnector.service";
|
||||
import { LogService } from "@bitwarden/common/abstractions/log.service";
|
||||
@@ -57,7 +62,9 @@ import { EnvironmentService } from "@bitwarden/common/services/environment.servi
|
||||
import { EventService } from "@bitwarden/common/services/event.service";
|
||||
import { ExportService } from "@bitwarden/common/services/export.service";
|
||||
import { FileUploadService } from "@bitwarden/common/services/fileUpload.service";
|
||||
import { FolderService } from "@bitwarden/common/services/folder.service";
|
||||
import { FolderApiService } from "@bitwarden/common/services/folder/folder-api.service";
|
||||
import { FolderService } from "@bitwarden/common/services/folder/folder.service";
|
||||
import { FormValidationErrorsService } from "@bitwarden/common/services/formValidationErrors.service";
|
||||
import { KeyConnectorService } from "@bitwarden/common/services/keyConnector.service";
|
||||
import { NotificationsService } from "@bitwarden/common/services/notifications.service";
|
||||
import { OrganizationService } from "@bitwarden/common/services/organization.service";
|
||||
@@ -213,12 +220,20 @@ export const LOG_MAC_FAILURES = new InjectionToken<string>("LOG_MAC_FAILURES");
|
||||
useClass: FolderService,
|
||||
deps: [
|
||||
CryptoServiceAbstraction,
|
||||
ApiServiceAbstraction,
|
||||
I18nServiceAbstraction,
|
||||
CipherServiceAbstraction,
|
||||
StateServiceAbstraction,
|
||||
],
|
||||
},
|
||||
{
|
||||
provide: InternalFolderService,
|
||||
useExisting: FolderServiceAbstraction,
|
||||
},
|
||||
{
|
||||
provide: FolderApiServiceAbstraction,
|
||||
useClass: FolderApiService,
|
||||
deps: [FolderServiceAbstraction, ApiServiceAbstraction],
|
||||
},
|
||||
{ provide: LogService, useFactory: () => new ConsoleLogService(false) },
|
||||
{
|
||||
provide: CollectionServiceAbstraction,
|
||||
@@ -291,6 +306,7 @@ export const LOG_MAC_FAILURES = new InjectionToken<string>("LOG_MAC_FAILURES");
|
||||
StateServiceAbstraction,
|
||||
OrganizationServiceAbstraction,
|
||||
ProviderServiceAbstraction,
|
||||
FolderApiServiceAbstraction,
|
||||
LOGOUT_CALLBACK,
|
||||
],
|
||||
},
|
||||
@@ -444,6 +460,10 @@ export const LOG_MAC_FAILURES = new InjectionToken<string>("LOG_MAC_FAILURES");
|
||||
provide: AbstractThemingService,
|
||||
useClass: ThemingService,
|
||||
},
|
||||
{
|
||||
provide: FormValidationErrorsServiceAbstraction,
|
||||
useClass: FormValidationErrorsService,
|
||||
},
|
||||
],
|
||||
})
|
||||
export class JslibServicesModule {}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { MediaMatcher } from "@angular/cdk/layout";
|
||||
import { DOCUMENT } from "@angular/common";
|
||||
import { Inject, Injectable } from "@angular/core";
|
||||
import { BehaviorSubject, filter, fromEvent, Observable } from "rxjs";
|
||||
@@ -6,6 +5,8 @@ import { BehaviorSubject, filter, fromEvent, Observable } from "rxjs";
|
||||
import { StateService } from "@bitwarden/common/abstractions/state.service";
|
||||
import { ThemeType } from "@bitwarden/common/enums/themeType";
|
||||
|
||||
import { WINDOW } from "../jslib-services.module";
|
||||
|
||||
import { Theme } from "./theme";
|
||||
import { ThemeBuilder } from "./themeBuilder";
|
||||
import { AbstractThemingService } from "./theming.service.abstraction";
|
||||
@@ -17,7 +18,7 @@ export class ThemingService implements AbstractThemingService {
|
||||
|
||||
constructor(
|
||||
private stateService: StateService,
|
||||
private mediaMatcher: MediaMatcher,
|
||||
@Inject(WINDOW) private window: Window,
|
||||
@Inject(DOCUMENT) private document: Document
|
||||
) {
|
||||
this.monitorThemeChanges();
|
||||
@@ -55,14 +56,14 @@ export class ThemingService implements AbstractThemingService {
|
||||
// We use a media match query for monitoring the system theme on web and browser, but this doesn't work for electron apps on Linux.
|
||||
// In desktop we override these methods to track systemTheme with the electron renderer instead, which works for all OSs.
|
||||
protected async getSystemTheme(): Promise<ThemeType> {
|
||||
return this.mediaMatcher.matchMedia("(prefers-color-scheme: dark)").matches
|
||||
return this.window.matchMedia("(prefers-color-scheme: dark)").matches
|
||||
? ThemeType.Dark
|
||||
: ThemeType.Light;
|
||||
}
|
||||
|
||||
protected monitorSystemThemeChanges(): void {
|
||||
fromEvent<MediaQueryListEvent>(
|
||||
this.mediaMatcher.matchMedia("(prefers-color-scheme: dark)"),
|
||||
this.window.matchMedia("(prefers-color-scheme: dark)"),
|
||||
"change"
|
||||
).subscribe((event) => {
|
||||
this.updateSystemTheme(event.matches ? ThemeType.Dark : ThemeType.Light);
|
||||
|
||||
57
libs/angular/src/validators/inputsFieldMatch.validator.ts
Normal file
57
libs/angular/src/validators/inputsFieldMatch.validator.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
import { AbstractControl, FormGroup, ValidatorFn } from "@angular/forms";
|
||||
|
||||
import { FormGroupControls } from "@bitwarden/common/abstractions/formValidationErrors.service";
|
||||
|
||||
export class InputsFieldMatch {
|
||||
//check to ensure two fields do not have the same value
|
||||
static validateInputsDoesntMatch(matchTo: string, errorMessage: string): ValidatorFn {
|
||||
return (control: AbstractControl) => {
|
||||
if (control.parent && control.parent.controls) {
|
||||
return control?.value === (control?.parent?.controls as FormGroupControls)[matchTo].value
|
||||
? {
|
||||
inputsMatchError: {
|
||||
message: errorMessage,
|
||||
},
|
||||
}
|
||||
: null;
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
}
|
||||
|
||||
//check to ensure two fields have the same value
|
||||
static validateInputsMatch(matchTo: string, errorMessage: string): ValidatorFn {
|
||||
return (control: AbstractControl) => {
|
||||
if (control.parent && control.parent.controls) {
|
||||
return control?.value === (control?.parent?.controls as FormGroupControls)[matchTo].value
|
||||
? null
|
||||
: {
|
||||
inputsDoesntMatchError: {
|
||||
message: errorMessage,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
}
|
||||
|
||||
//checks the formGroup if two fields have the same value and validation is controlled from either field
|
||||
static validateFormInputsMatch(field: string, fieldMatchTo: string, errorMessage: string) {
|
||||
return (formGroup: FormGroup) => {
|
||||
const fieldCtrl = formGroup.controls[field];
|
||||
const fieldMatchToCtrl = formGroup.controls[fieldMatchTo];
|
||||
|
||||
if (fieldCtrl.value !== fieldMatchToCtrl.value) {
|
||||
fieldMatchToCtrl.setErrors({
|
||||
inputsDoesntMatchError: {
|
||||
message: errorMessage,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
fieldMatchToCtrl.setErrors(null);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user