1
0
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:
CarleyDiaz-Bitwarden
2022-07-21 17:10:45 -04:00
431 changed files with 13919 additions and 5118 deletions

View File

@@ -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);

View File

@@ -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);

View File

@@ -20,6 +20,7 @@ const cardIcons: Record<string, string> = {
Maestro: "card-maestro",
UnionPay: "card-union-pay",
RuPay: "card-ru-pay",
Mir: "card-mir",
};
@Component({

View File

@@ -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;
};
}
}

View File

@@ -128,6 +128,7 @@ export class SetPasswordComponent extends BaseChangePasswordComponent {
);
const resetRequest = new OrganizationUserResetPasswordEnrollmentRequest();
resetRequest.masterPasswordHash = masterPasswordHash;
resetRequest.resetPasswordKey = encryptedKey.encryptedString;
return this.apiService.putOrganizationUserResetPasswordEnrollment(

Binary file not shown.

After

Width:  |  Height:  |  Size: 505 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 644 B

View File

@@ -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);
}

View File

@@ -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;
}

View File

@@ -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;
};
}
}

View File

@@ -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;

View File

@@ -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>;
}
}

View File

@@ -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] }],
};

View File

@@ -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 {

View File

@@ -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 {}

View File

@@ -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);

View 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);
}
};
}
}