mirror of
https://github.com/bitwarden/browser
synced 2025-12-17 16:53:34 +00:00
Merge branch 'master' into PM-2135-beeep-refactor-and-refresh-web-user-verification-components
This commit is contained in:
@@ -1,13 +1,13 @@
|
||||
import { Directive, EventEmitter, Input, OnInit, Output } from "@angular/core";
|
||||
|
||||
import { CollectionService } from "@bitwarden/common/admin-console/abstractions/collection.service";
|
||||
import { CollectionView } from "@bitwarden/common/admin-console/models/view/collection.view";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { CollectionService } from "@bitwarden/common/vault/abstractions/collection.service";
|
||||
import { Cipher } from "@bitwarden/common/vault/models/domain/cipher";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
import { CollectionView } from "@bitwarden/common/vault/models/view/collection.view";
|
||||
|
||||
@Directive()
|
||||
export class CollectionsComponent implements OnInit {
|
||||
|
||||
@@ -6,7 +6,10 @@ import { Subject, takeUntil } from "rxjs";
|
||||
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { ConfigServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config.service.abstraction";
|
||||
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||
import {
|
||||
EnvironmentService as EnvironmentServiceAbstraction,
|
||||
Region,
|
||||
} from "@bitwarden/common/platform/abstractions/environment.service";
|
||||
|
||||
@Component({
|
||||
selector: "environment-selector",
|
||||
@@ -37,8 +40,8 @@ export class EnvironmentSelectorComponent implements OnInit, OnDestroy {
|
||||
euServerFlagEnabled: boolean;
|
||||
isOpen = false;
|
||||
showingModal = false;
|
||||
selectedEnvironment: ServerEnvironment;
|
||||
ServerEnvironmentType = ServerEnvironment;
|
||||
selectedEnvironment: Region;
|
||||
ServerEnvironmentType = Region;
|
||||
overlayPostition: ConnectedPosition[] = [
|
||||
{
|
||||
originX: "start",
|
||||
@@ -50,7 +53,7 @@ export class EnvironmentSelectorComponent implements OnInit, OnDestroy {
|
||||
protected componentDestroyed$: Subject<void> = new Subject();
|
||||
|
||||
constructor(
|
||||
protected environmentService: EnvironmentService,
|
||||
protected environmentService: EnvironmentServiceAbstraction,
|
||||
protected configService: ConfigServiceAbstraction,
|
||||
protected router: Router
|
||||
) {}
|
||||
@@ -67,18 +70,20 @@ export class EnvironmentSelectorComponent implements OnInit, OnDestroy {
|
||||
this.componentDestroyed$.complete();
|
||||
}
|
||||
|
||||
async toggle(option: ServerEnvironment) {
|
||||
async toggle(option: Region) {
|
||||
this.isOpen = !this.isOpen;
|
||||
if (option === null) {
|
||||
return;
|
||||
}
|
||||
if (option === ServerEnvironment.EU) {
|
||||
await this.environmentService.setUrls({ base: "https://vault.bitwarden.eu" });
|
||||
} else if (option === ServerEnvironment.US) {
|
||||
await this.environmentService.setUrls({ base: "https://vault.bitwarden.com" });
|
||||
} else if (option === ServerEnvironment.SelfHosted) {
|
||||
|
||||
this.updateEnvironmentInfo();
|
||||
|
||||
if (option === Region.SelfHosted) {
|
||||
this.onOpenSelfHostedSettings.emit();
|
||||
return;
|
||||
}
|
||||
|
||||
await this.environmentService.setRegion(option);
|
||||
this.updateEnvironmentInfo();
|
||||
}
|
||||
|
||||
@@ -86,14 +91,8 @@ export class EnvironmentSelectorComponent implements OnInit, OnDestroy {
|
||||
this.euServerFlagEnabled = await this.configService.getFeatureFlagBool(
|
||||
FeatureFlag.DisplayEuEnvironmentFlag
|
||||
);
|
||||
const webvaultUrl = this.environmentService.getWebVaultUrl();
|
||||
if (this.environmentService.isSelfHosted()) {
|
||||
this.selectedEnvironment = ServerEnvironment.SelfHosted;
|
||||
} else if (webvaultUrl != null && webvaultUrl.includes("bitwarden.eu")) {
|
||||
this.selectedEnvironment = ServerEnvironment.EU;
|
||||
} else {
|
||||
this.selectedEnvironment = ServerEnvironment.US;
|
||||
}
|
||||
|
||||
this.selectedEnvironment = this.environmentService.selectedRegion;
|
||||
}
|
||||
|
||||
close() {
|
||||
@@ -101,9 +100,3 @@ export class EnvironmentSelectorComponent implements OnInit, OnDestroy {
|
||||
this.updateEnvironmentInfo();
|
||||
}
|
||||
}
|
||||
|
||||
enum ServerEnvironment {
|
||||
US = "US",
|
||||
EU = "EU",
|
||||
SelfHosted = "Self-hosted",
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ import { StateService } from "@bitwarden/common/platform/abstractions/state.serv
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { EncString } from "@bitwarden/common/platform/models/domain/enc-string";
|
||||
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
||||
import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password";
|
||||
import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength";
|
||||
|
||||
import { DialogServiceAbstraction, SimpleDialogType } from "../../services/dialog";
|
||||
|
||||
@@ -69,7 +69,7 @@ export class LockComponent implements OnInit, OnDestroy {
|
||||
protected ngZone: NgZone,
|
||||
protected policyApiService: PolicyApiServiceAbstraction,
|
||||
protected policyService: InternalPolicyService,
|
||||
protected passwordGenerationService: PasswordGenerationServiceAbstraction,
|
||||
protected passwordStrengthService: PasswordStrengthServiceAbstraction,
|
||||
protected dialogService: DialogServiceAbstraction
|
||||
) {}
|
||||
|
||||
@@ -333,7 +333,7 @@ export class LockComponent implements OnInit, OnDestroy {
|
||||
return false;
|
||||
}
|
||||
|
||||
const passwordStrength = this.passwordGenerationService.passwordStrength(
|
||||
const passwordStrength = this.passwordStrengthService.getPasswordStrength(
|
||||
this.masterPassword,
|
||||
this.email
|
||||
)?.score;
|
||||
|
||||
@@ -12,10 +12,6 @@ import { PasswordLogInCredentials } from "@bitwarden/common/auth/models/domain/l
|
||||
import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service";
|
||||
import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service";
|
||||
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||
import {
|
||||
AllValidationErrors,
|
||||
FormValidationErrorsService,
|
||||
} from "@bitwarden/common/platform/abstractions/form-validation-errors.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
@@ -23,6 +19,11 @@ import { StateService } from "@bitwarden/common/platform/abstractions/state.serv
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password";
|
||||
|
||||
import {
|
||||
AllValidationErrors,
|
||||
FormValidationErrorsService,
|
||||
} from "../../platform/abstractions/form-validation-errors.service";
|
||||
|
||||
import { CaptchaProtectedComponent } from "./captcha-protected.component";
|
||||
|
||||
@Directive()
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { Directive, EventEmitter, Output } from "@angular/core";
|
||||
|
||||
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||
import {
|
||||
EnvironmentService,
|
||||
Region,
|
||||
} from "@bitwarden/common/platform/abstractions/environment.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
|
||||
@@ -25,6 +28,9 @@ export class EnvironmentComponent {
|
||||
private modalService: ModalService
|
||||
) {
|
||||
const urls = this.environmentService.getUrls();
|
||||
if (this.environmentService.selectedRegion != Region.SelfHosted) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.baseUrl = urls.base || "";
|
||||
this.webVaultUrl = urls.webVault || "";
|
||||
|
||||
@@ -13,10 +13,6 @@ import { ReferenceEventRequest } from "@bitwarden/common/models/request/referenc
|
||||
import { RegisterRequest } from "@bitwarden/common/models/request/register.request";
|
||||
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
|
||||
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||
import {
|
||||
AllValidationErrors,
|
||||
FormValidationErrorsService,
|
||||
} from "@bitwarden/common/platform/abstractions/form-validation-errors.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
@@ -25,6 +21,10 @@ import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password";
|
||||
|
||||
import { CaptchaProtectedComponent } from "../auth/components/captcha-protected.component";
|
||||
import {
|
||||
AllValidationErrors,
|
||||
FormValidationErrorsService,
|
||||
} from "../platform/abstractions/form-validation-errors.service";
|
||||
import { DialogServiceAbstraction, SimpleDialogType } from "../services/dialog";
|
||||
import { PasswordColorText } from "../shared/components/password-strength/password-strength.component";
|
||||
import { InputsFieldMatch } from "../validators/inputsFieldMatch.validator";
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
import { Directive, EventEmitter, Input, OnDestroy, OnInit, Output } from "@angular/core";
|
||||
import { firstValueFrom, map, Observable, Subject, takeUntil } from "rxjs";
|
||||
|
||||
import { CollectionService } from "@bitwarden/common/admin-console/abstractions/collection.service";
|
||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { OrganizationUserStatusType } from "@bitwarden/common/admin-console/enums";
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { CollectionView } from "@bitwarden/common/admin-console/models/view/collection.view";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { Checkable, isChecked } from "@bitwarden/common/types/checkable";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { CollectionService } from "@bitwarden/common/vault/abstractions/collection.service";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
import { CollectionView } from "@bitwarden/common/vault/models/view/collection.view";
|
||||
|
||||
@Directive()
|
||||
export class ShareComponent implements OnInit, OnDestroy {
|
||||
|
||||
137
libs/angular/src/directives/if-feature.directive.spec.ts
Normal file
137
libs/angular/src/directives/if-feature.directive.spec.ts
Normal file
@@ -0,0 +1,137 @@
|
||||
import { Component } from "@angular/core";
|
||||
import { ComponentFixture, TestBed } from "@angular/core/testing";
|
||||
import { By } from "@angular/platform-browser";
|
||||
import { mock, MockProxy } from "jest-mock-extended";
|
||||
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { ConfigServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config.service.abstraction";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
|
||||
import { IfFeatureDirective } from "./if-feature.directive";
|
||||
|
||||
const testBooleanFeature: FeatureFlag = "boolean-feature" as FeatureFlag;
|
||||
const testStringFeature: FeatureFlag = "string-feature" as FeatureFlag;
|
||||
const testStringFeatureValue = "test-value";
|
||||
|
||||
@Component({
|
||||
template: `
|
||||
<div *appIfFeature="testBooleanFeature">
|
||||
<div data-testid="boolean-content">Hidden behind feature flag</div>
|
||||
</div>
|
||||
<div *appIfFeature="stringFeature; value: stringFeatureValue">
|
||||
<div data-testid="string-content">Hidden behind feature flag</div>
|
||||
</div>
|
||||
<div *appIfFeature="missingFlag">
|
||||
<div data-testid="missing-flag-content">
|
||||
Hidden behind missing flag. Should not be visible.
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
})
|
||||
class TestComponent {
|
||||
testBooleanFeature = testBooleanFeature;
|
||||
stringFeature = testStringFeature;
|
||||
stringFeatureValue = testStringFeatureValue;
|
||||
|
||||
missingFlag = "missing-flag" as FeatureFlag;
|
||||
}
|
||||
|
||||
describe("IfFeatureDirective", () => {
|
||||
let fixture: ComponentFixture<TestComponent>;
|
||||
let content: HTMLElement;
|
||||
let mockConfigService: MockProxy<ConfigServiceAbstraction>;
|
||||
|
||||
const mockConfigFlagValue = (flag: FeatureFlag, flagValue: any) => {
|
||||
if (typeof flagValue === "boolean") {
|
||||
mockConfigService.getFeatureFlagBool.mockImplementation((f, defaultValue = false) =>
|
||||
flag == f ? Promise.resolve(flagValue) : Promise.resolve(defaultValue)
|
||||
);
|
||||
} else if (typeof flagValue === "string") {
|
||||
mockConfigService.getFeatureFlagString.mockImplementation((f, defaultValue = "") =>
|
||||
flag == f ? Promise.resolve(flagValue) : Promise.resolve(defaultValue)
|
||||
);
|
||||
} else if (typeof flagValue === "number") {
|
||||
mockConfigService.getFeatureFlagNumber.mockImplementation((f, defaultValue = 0) =>
|
||||
flag == f ? Promise.resolve(flagValue) : Promise.resolve(defaultValue)
|
||||
);
|
||||
}
|
||||
};
|
||||
const queryContent = (testId: string) =>
|
||||
fixture.debugElement.query(By.css(`[data-testid="${testId}"]`))?.nativeElement;
|
||||
|
||||
beforeEach(async () => {
|
||||
mockConfigService = mock<ConfigServiceAbstraction>();
|
||||
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [IfFeatureDirective, TestComponent],
|
||||
providers: [
|
||||
{ provide: LogService, useValue: mock<LogService>() },
|
||||
{
|
||||
provide: ConfigServiceAbstraction,
|
||||
useValue: mockConfigService,
|
||||
},
|
||||
],
|
||||
}).compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(TestComponent);
|
||||
});
|
||||
|
||||
it("renders content when the feature flag is enabled", async () => {
|
||||
mockConfigFlagValue(testBooleanFeature, true);
|
||||
fixture.detectChanges();
|
||||
await fixture.whenStable();
|
||||
|
||||
content = queryContent("boolean-content");
|
||||
|
||||
expect(content).toBeDefined();
|
||||
});
|
||||
|
||||
it("renders content when the feature flag value matches the provided value", async () => {
|
||||
mockConfigFlagValue(testStringFeature, testStringFeatureValue);
|
||||
fixture.detectChanges();
|
||||
await fixture.whenStable();
|
||||
|
||||
content = queryContent("string-content");
|
||||
|
||||
expect(content).toBeDefined();
|
||||
});
|
||||
|
||||
it("hides content when the feature flag is disabled", async () => {
|
||||
mockConfigFlagValue(testBooleanFeature, false);
|
||||
fixture.detectChanges();
|
||||
await fixture.whenStable();
|
||||
|
||||
content = queryContent("boolean-content");
|
||||
|
||||
expect(content).toBeUndefined();
|
||||
});
|
||||
|
||||
it("hides content when the feature flag value does not match the provided value", async () => {
|
||||
mockConfigFlagValue(testStringFeature, "wrong-value");
|
||||
fixture.detectChanges();
|
||||
await fixture.whenStable();
|
||||
|
||||
content = queryContent("string-content");
|
||||
|
||||
expect(content).toBeUndefined();
|
||||
});
|
||||
|
||||
it("hides content when the feature flag is missing", async () => {
|
||||
fixture.detectChanges();
|
||||
await fixture.whenStable();
|
||||
|
||||
content = queryContent("missing-flag-content");
|
||||
|
||||
expect(content).toBeUndefined();
|
||||
});
|
||||
|
||||
it("hides content when the directive throws an unexpected exception", async () => {
|
||||
mockConfigService.getFeatureFlagBool.mockImplementation(() => Promise.reject("Some error"));
|
||||
fixture.detectChanges();
|
||||
await fixture.whenStable();
|
||||
|
||||
content = queryContent("boolean-content");
|
||||
|
||||
expect(content).toBeUndefined();
|
||||
});
|
||||
});
|
||||
67
libs/angular/src/directives/if-feature.directive.ts
Normal file
67
libs/angular/src/directives/if-feature.directive.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
import { Directive, Input, OnInit, TemplateRef, ViewContainerRef } from "@angular/core";
|
||||
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { ConfigServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config.service.abstraction";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
|
||||
// Replace this with a type safe lookup of the feature flag values in PM-2282
|
||||
type FlagValue = boolean | number | string;
|
||||
|
||||
/**
|
||||
* Directive that conditionally renders the element when the feature flag is enabled and/or
|
||||
* matches the value specified by {@link appIfFeatureValue}.
|
||||
*
|
||||
* When a feature flag is not found in the config service, the element is hidden.
|
||||
*/
|
||||
@Directive({
|
||||
selector: "[appIfFeature]",
|
||||
})
|
||||
export class IfFeatureDirective implements OnInit {
|
||||
/**
|
||||
* The feature flag to check.
|
||||
*/
|
||||
@Input() appIfFeature: FeatureFlag;
|
||||
|
||||
/**
|
||||
* Optional value to compare against the value of the feature flag in the config service.
|
||||
* @default true
|
||||
*/
|
||||
@Input() appIfFeatureValue: FlagValue = true;
|
||||
|
||||
private hasView = false;
|
||||
|
||||
constructor(
|
||||
private templateRef: TemplateRef<any>,
|
||||
private viewContainer: ViewContainerRef,
|
||||
private configService: ConfigServiceAbstraction,
|
||||
private logService: LogService
|
||||
) {}
|
||||
|
||||
async ngOnInit() {
|
||||
try {
|
||||
let flagValue: FlagValue;
|
||||
|
||||
if (typeof this.appIfFeatureValue === "boolean") {
|
||||
flagValue = await this.configService.getFeatureFlagBool(this.appIfFeature);
|
||||
} else if (typeof this.appIfFeatureValue === "number") {
|
||||
flagValue = await this.configService.getFeatureFlagNumber(this.appIfFeature);
|
||||
} else if (typeof this.appIfFeatureValue === "string") {
|
||||
flagValue = await this.configService.getFeatureFlagString(this.appIfFeature);
|
||||
}
|
||||
|
||||
if (this.appIfFeatureValue === flagValue) {
|
||||
if (!this.hasView) {
|
||||
this.viewContainer.createEmbeddedView(this.templateRef);
|
||||
this.hasView = true;
|
||||
}
|
||||
} else {
|
||||
this.viewContainer.clear();
|
||||
this.hasView = false;
|
||||
}
|
||||
} catch (e) {
|
||||
this.logService.error(e);
|
||||
this.viewContainer.clear();
|
||||
this.hasView = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
152
libs/angular/src/guard/feature-flag.guard.spec.ts
Normal file
152
libs/angular/src/guard/feature-flag.guard.spec.ts
Normal file
@@ -0,0 +1,152 @@
|
||||
import { Component } from "@angular/core";
|
||||
import { TestBed } from "@angular/core/testing";
|
||||
import { CanActivateFn, Router } from "@angular/router";
|
||||
import { RouterTestingModule } from "@angular/router/testing";
|
||||
import { mock, MockProxy } from "jest-mock-extended";
|
||||
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { ConfigServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config.service.abstraction";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { I18nMockService } from "@bitwarden/components/src";
|
||||
|
||||
import { canAccessFeature } from "./feature-flag.guard";
|
||||
|
||||
@Component({ template: "" })
|
||||
export class EmptyComponent {}
|
||||
|
||||
describe("canAccessFeature", () => {
|
||||
const testFlag: FeatureFlag = "test-flag" as FeatureFlag;
|
||||
const featureRoute = "enabled-feature";
|
||||
const redirectRoute = "redirect";
|
||||
|
||||
let mockConfigService: MockProxy<ConfigServiceAbstraction>;
|
||||
let mockPlatformUtilsService: MockProxy<PlatformUtilsService>;
|
||||
|
||||
const setup = (featureGuard: CanActivateFn, flagValue: any) => {
|
||||
mockConfigService = mock<ConfigServiceAbstraction>();
|
||||
mockPlatformUtilsService = mock<PlatformUtilsService>();
|
||||
|
||||
// Mock the correct getter based on the type of flagValue; also mock default values if one is not provided
|
||||
if (typeof flagValue === "boolean") {
|
||||
mockConfigService.getFeatureFlagBool.mockImplementation((flag, defaultValue = false) =>
|
||||
flag == testFlag ? Promise.resolve(flagValue) : Promise.resolve(defaultValue)
|
||||
);
|
||||
} else if (typeof flagValue === "string") {
|
||||
mockConfigService.getFeatureFlagString.mockImplementation((flag, defaultValue = "") =>
|
||||
flag == testFlag ? Promise.resolve(flagValue) : Promise.resolve(defaultValue)
|
||||
);
|
||||
} else if (typeof flagValue === "number") {
|
||||
mockConfigService.getFeatureFlagNumber.mockImplementation((flag, defaultValue = 0) =>
|
||||
flag == testFlag ? Promise.resolve(flagValue) : Promise.resolve(defaultValue)
|
||||
);
|
||||
}
|
||||
|
||||
const testBed = TestBed.configureTestingModule({
|
||||
imports: [
|
||||
RouterTestingModule.withRoutes([
|
||||
{ path: "", component: EmptyComponent },
|
||||
{
|
||||
path: featureRoute,
|
||||
component: EmptyComponent,
|
||||
canActivate: [featureGuard],
|
||||
},
|
||||
{ path: redirectRoute, component: EmptyComponent },
|
||||
]),
|
||||
],
|
||||
providers: [
|
||||
{ provide: ConfigServiceAbstraction, useValue: mockConfigService },
|
||||
{ provide: PlatformUtilsService, useValue: mockPlatformUtilsService },
|
||||
{ provide: LogService, useValue: mock<LogService>() },
|
||||
{
|
||||
provide: I18nService,
|
||||
useValue: new I18nMockService({
|
||||
accessDenied: "Access Denied!",
|
||||
}),
|
||||
},
|
||||
],
|
||||
});
|
||||
return {
|
||||
router: testBed.inject(Router),
|
||||
};
|
||||
};
|
||||
|
||||
it("successfully navigates when the feature flag is enabled", async () => {
|
||||
const { router } = setup(canAccessFeature(testFlag), true);
|
||||
|
||||
await router.navigate([featureRoute]);
|
||||
|
||||
expect(router.url).toBe(`/${featureRoute}`);
|
||||
});
|
||||
|
||||
it("successfully navigates when the feature flag value matches the required value", async () => {
|
||||
const { router } = setup(canAccessFeature(testFlag, "some-value"), "some-value");
|
||||
|
||||
await router.navigate([featureRoute]);
|
||||
|
||||
expect(router.url).toBe(`/${featureRoute}`);
|
||||
});
|
||||
|
||||
it("fails to navigate when the feature flag is disabled", async () => {
|
||||
const { router } = setup(canAccessFeature(testFlag), false);
|
||||
|
||||
await router.navigate([featureRoute]);
|
||||
|
||||
expect(router.url).toBe("/");
|
||||
});
|
||||
|
||||
it("fails to navigate when the feature flag value does not match the required value", async () => {
|
||||
const { router } = setup(canAccessFeature(testFlag, "some-value"), "some-wrong-value");
|
||||
|
||||
await router.navigate([featureRoute]);
|
||||
|
||||
expect(router.url).toBe("/");
|
||||
});
|
||||
|
||||
it("fails to navigate when the feature flag does not exist", async () => {
|
||||
const { router } = setup(canAccessFeature("missing-flag" as FeatureFlag), true);
|
||||
|
||||
await router.navigate([featureRoute]);
|
||||
|
||||
expect(router.url).toBe("/");
|
||||
});
|
||||
|
||||
it("shows an error toast when the feature flag is disabled", async () => {
|
||||
const { router } = setup(canAccessFeature(testFlag), false);
|
||||
|
||||
await router.navigate([featureRoute]);
|
||||
|
||||
expect(mockPlatformUtilsService.showToast).toHaveBeenCalledWith(
|
||||
"error",
|
||||
null,
|
||||
"Access Denied!"
|
||||
);
|
||||
});
|
||||
|
||||
it("does not show an error toast when the feature flag is enabled", async () => {
|
||||
const { router } = setup(canAccessFeature(testFlag), true);
|
||||
|
||||
await router.navigate([featureRoute]);
|
||||
|
||||
expect(mockPlatformUtilsService.showToast).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("redirects to the specified redirect url when the feature flag is disabled", async () => {
|
||||
const { router } = setup(canAccessFeature(testFlag, true, redirectRoute), false);
|
||||
|
||||
await router.navigate([featureRoute]);
|
||||
|
||||
expect(router.url).toBe(`/${redirectRoute}`);
|
||||
});
|
||||
|
||||
it("fails to navigate when the config service throws an unexpected exception", async () => {
|
||||
const { router } = setup(canAccessFeature(testFlag), true);
|
||||
|
||||
mockConfigService.getFeatureFlagBool.mockImplementation(() => Promise.reject("Some error"));
|
||||
|
||||
await router.navigate([featureRoute]);
|
||||
|
||||
expect(router.url).toBe("/");
|
||||
});
|
||||
});
|
||||
58
libs/angular/src/guard/feature-flag.guard.ts
Normal file
58
libs/angular/src/guard/feature-flag.guard.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import { inject } from "@angular/core";
|
||||
import { CanActivateFn, Router } from "@angular/router";
|
||||
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { ConfigServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config.service.abstraction";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
|
||||
// Replace this with a type safe lookup of the feature flag values in PM-2282
|
||||
type FlagValue = boolean | number | string;
|
||||
|
||||
/**
|
||||
* Returns a CanActivateFn that checks if the feature flag is enabled. If not, it shows an "Access Denied!"
|
||||
* toast and optionally redirects to the specified url.
|
||||
* @param featureFlag - The feature flag to check
|
||||
* @param requiredFlagValue - Optional value to the feature flag must be equal to, defaults to true
|
||||
* @param redirectUrlOnDisabled - Optional url to redirect to if the feature flag is disabled
|
||||
*/
|
||||
export const canAccessFeature = (
|
||||
featureFlag: FeatureFlag,
|
||||
requiredFlagValue: FlagValue = true,
|
||||
redirectUrlOnDisabled?: string
|
||||
): CanActivateFn => {
|
||||
return async () => {
|
||||
const configService = inject(ConfigServiceAbstraction);
|
||||
const platformUtilsService = inject(PlatformUtilsService);
|
||||
const router = inject(Router);
|
||||
const i18nService = inject(I18nService);
|
||||
const logService = inject(LogService);
|
||||
|
||||
let flagValue: FlagValue;
|
||||
|
||||
try {
|
||||
if (typeof requiredFlagValue === "boolean") {
|
||||
flagValue = await configService.getFeatureFlagBool(featureFlag);
|
||||
} else if (typeof requiredFlagValue === "number") {
|
||||
flagValue = await configService.getFeatureFlagNumber(featureFlag);
|
||||
} else if (typeof requiredFlagValue === "string") {
|
||||
flagValue = await configService.getFeatureFlagString(featureFlag);
|
||||
}
|
||||
|
||||
if (flagValue === requiredFlagValue) {
|
||||
return true;
|
||||
}
|
||||
|
||||
platformUtilsService.showToast("error", null, i18nService.t("accessDenied"));
|
||||
|
||||
if (redirectUrlOnDisabled != null) {
|
||||
return router.createUrlTree([redirectUrlOnDisabled]);
|
||||
}
|
||||
return false;
|
||||
} catch (e) {
|
||||
logService.error(e);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
};
|
||||
@@ -11,6 +11,7 @@ import { AutofocusDirective } from "./directives/autofocus.directive";
|
||||
import { BoxRowDirective } from "./directives/box-row.directive";
|
||||
import { CopyClickDirective } from "./directives/copy-click.directive";
|
||||
import { FallbackSrcDirective } from "./directives/fallback-src.directive";
|
||||
import { IfFeatureDirective } from "./directives/if-feature.directive";
|
||||
import { InputStripSpacesDirective } from "./directives/input-strip-spaces.directive";
|
||||
import { InputVerbatimDirective } from "./directives/input-verbatim.directive";
|
||||
import { LaunchClickDirective } from "./directives/launch-click.directive";
|
||||
@@ -25,6 +26,7 @@ import { SearchPipe } from "./pipes/search.pipe";
|
||||
import { UserNamePipe } from "./pipes/user-name.pipe";
|
||||
import { UserTypePipe } from "./pipes/user-type.pipe";
|
||||
import { EllipsisPipe } from "./platform/pipes/ellipsis.pipe";
|
||||
import { FingerprintPipe } from "./platform/pipes/fingerprint.pipe";
|
||||
import { I18nPipe } from "./platform/pipes/i18n.pipe";
|
||||
import { PasswordStrengthComponent } from "./shared/components/password-strength/password-strength.component";
|
||||
import { ExportScopeCalloutComponent } from "./tools/export/components/export-scope-callout.component";
|
||||
@@ -68,6 +70,8 @@ import { IconComponent } from "./vault/components/icon.component";
|
||||
UserNamePipe,
|
||||
PasswordStrengthComponent,
|
||||
UserTypePipe,
|
||||
IfFeatureDirective,
|
||||
FingerprintPipe,
|
||||
],
|
||||
exports: [
|
||||
A11yInvalidDirective,
|
||||
@@ -97,7 +101,17 @@ import { IconComponent } from "./vault/components/icon.component";
|
||||
UserNamePipe,
|
||||
PasswordStrengthComponent,
|
||||
UserTypePipe,
|
||||
IfFeatureDirective,
|
||||
FingerprintPipe,
|
||||
],
|
||||
providers: [
|
||||
CreditCardNumberPipe,
|
||||
DatePipe,
|
||||
I18nPipe,
|
||||
SearchPipe,
|
||||
UserNamePipe,
|
||||
UserTypePipe,
|
||||
FingerprintPipe,
|
||||
],
|
||||
providers: [CreditCardNumberPipe, DatePipe, I18nPipe, SearchPipe, UserNamePipe, UserTypePipe],
|
||||
})
|
||||
export class JslibModule {}
|
||||
|
||||
@@ -3,6 +3,9 @@ import { Pipe, PipeTransform } from "@angular/core";
|
||||
@Pipe({
|
||||
name: "ellipsis",
|
||||
})
|
||||
/**
|
||||
* @deprecated Use the tailwind class 'tw-truncate' instead
|
||||
*/
|
||||
export class EllipsisPipe implements PipeTransform {
|
||||
transform(value: string, limit = 25, completeWords = false, ellipsis = "...") {
|
||||
if (value.length <= limit) {
|
||||
|
||||
32
libs/angular/src/platform/pipes/fingerprint.pipe.ts
Normal file
32
libs/angular/src/platform/pipes/fingerprint.pipe.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { Pipe } from "@angular/core";
|
||||
|
||||
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
|
||||
@Pipe({
|
||||
name: "fingerprint",
|
||||
})
|
||||
export class FingerprintPipe {
|
||||
constructor(private cryptoService: CryptoService) {}
|
||||
|
||||
async transform(publicKey: string | Uint8Array, fingerprintMaterial: string): Promise<string> {
|
||||
try {
|
||||
if (typeof publicKey === "string") {
|
||||
publicKey = Utils.fromB64ToArray(publicKey);
|
||||
}
|
||||
|
||||
const fingerprint = await this.cryptoService.getFingerprint(
|
||||
fingerprintMaterial,
|
||||
publicKey.buffer
|
||||
);
|
||||
|
||||
if (fingerprint != null) {
|
||||
return fingerprint.join("-");
|
||||
}
|
||||
|
||||
return "";
|
||||
} catch {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -20,7 +20,6 @@ import { SettingsService as SettingsServiceAbstraction } from "@bitwarden/common
|
||||
import { TotpService as TotpServiceAbstraction } from "@bitwarden/common/abstractions/totp.service";
|
||||
import { VaultTimeoutService as VaultTimeoutServiceAbstraction } from "@bitwarden/common/abstractions/vaultTimeout/vaultTimeout.service";
|
||||
import { VaultTimeoutSettingsService as VaultTimeoutSettingsServiceAbstraction } from "@bitwarden/common/abstractions/vaultTimeout/vaultTimeoutSettings.service";
|
||||
import { CollectionService as CollectionServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/collection.service";
|
||||
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction";
|
||||
import {
|
||||
InternalOrganizationService,
|
||||
@@ -32,7 +31,6 @@ import {
|
||||
PolicyService as PolicyServiceAbstraction,
|
||||
} from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { ProviderService as ProviderServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/provider.service";
|
||||
import { CollectionService } from "@bitwarden/common/admin-console/services/collection.service";
|
||||
import { OrganizationApiService } from "@bitwarden/common/admin-console/services/organization/organization-api.service";
|
||||
import { OrganizationService } from "@bitwarden/common/admin-console/services/organization/organization.service";
|
||||
import { PolicyApiService } from "@bitwarden/common/admin-console/services/policy/policy-api.service";
|
||||
@@ -68,7 +66,6 @@ import { CryptoService as CryptoServiceAbstraction } from "@bitwarden/common/pla
|
||||
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
|
||||
import { EnvironmentService as EnvironmentServiceAbstraction } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||
import { FileUploadService as FileUploadServiceAbstraction } from "@bitwarden/common/platform/abstractions/file-upload/file-upload.service";
|
||||
import { FormValidationErrorsService as FormValidationErrorsServiceAbstraction } from "@bitwarden/common/platform/abstractions/form-validation-errors.service";
|
||||
import { I18nService as I18nServiceAbstraction } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { MessagingService as MessagingServiceAbstraction } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||
@@ -90,7 +87,6 @@ import { EncryptServiceImplementation } from "@bitwarden/common/platform/service
|
||||
import { MultithreadEncryptServiceImplementation } from "@bitwarden/common/platform/services/cryptography/multithread-encrypt.service.implementation";
|
||||
import { EnvironmentService } from "@bitwarden/common/platform/services/environment.service";
|
||||
import { FileUploadService } from "@bitwarden/common/platform/services/file-upload/file-upload.service";
|
||||
import { FormValidationErrorsService } from "@bitwarden/common/platform/services/form-validation-errors.service";
|
||||
import { StateMigrationService } from "@bitwarden/common/platform/services/state-migration.service";
|
||||
import { StateService } from "@bitwarden/common/platform/services/state.service";
|
||||
import { ValidationService } from "@bitwarden/common/platform/services/validation.service";
|
||||
@@ -120,11 +116,16 @@ import {
|
||||
UsernameGenerationService,
|
||||
UsernameGenerationServiceAbstraction,
|
||||
} from "@bitwarden/common/tools/generator/username";
|
||||
import {
|
||||
PasswordStrengthService,
|
||||
PasswordStrengthServiceAbstraction,
|
||||
} from "@bitwarden/common/tools/password-strength";
|
||||
import { SendApiService } from "@bitwarden/common/tools/send/services/send-api.service";
|
||||
import { SendApiService as SendApiServiceAbstraction } from "@bitwarden/common/tools/send/services/send-api.service.abstraction";
|
||||
import { SendService } from "@bitwarden/common/tools/send/services/send.service";
|
||||
import { SendService as SendServiceAbstraction } from "@bitwarden/common/tools/send/services/send.service.abstraction";
|
||||
import { CipherService as CipherServiceAbstraction } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { CollectionService as CollectionServiceAbstraction } from "@bitwarden/common/vault/abstractions/collection.service";
|
||||
import { CipherFileUploadService as CipherFileUploadServiceAbstraction } from "@bitwarden/common/vault/abstractions/file-upload/cipher-file-upload.service";
|
||||
import { FolderApiServiceAbstraction } from "@bitwarden/common/vault/abstractions/folder/folder-api.service.abstraction";
|
||||
import {
|
||||
@@ -135,6 +136,7 @@ import { PasswordRepromptService as PasswordRepromptServiceAbstraction } from "@
|
||||
import { SyncNotifierService as SyncNotifierServiceAbstraction } from "@bitwarden/common/vault/abstractions/sync/sync-notifier.service.abstraction";
|
||||
import { SyncService as SyncServiceAbstraction } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
|
||||
import { CipherService } from "@bitwarden/common/vault/services/cipher.service";
|
||||
import { CollectionService } from "@bitwarden/common/vault/services/collection.service";
|
||||
import { CipherFileUploadService } from "@bitwarden/common/vault/services/file-upload/cipher-file-upload.service";
|
||||
import { FolderApiService } from "@bitwarden/common/vault/services/folder/folder-api.service";
|
||||
import { FolderService } from "@bitwarden/common/vault/services/folder/folder.service";
|
||||
@@ -148,7 +150,9 @@ import {
|
||||
import { AuthGuard } from "../auth/guards/auth.guard";
|
||||
import { LockGuard } from "../auth/guards/lock.guard";
|
||||
import { UnauthGuard } from "../auth/guards/unauth.guard";
|
||||
import { FormValidationErrorsService as FormValidationErrorsServiceAbstraction } from "../platform/abstractions/form-validation-errors.service";
|
||||
import { BroadcasterService } from "../platform/services/broadcaster.service";
|
||||
import { FormValidationErrorsService } from "../platform/services/form-validation-errors.service";
|
||||
import { PasswordRepromptService } from "../vault/services/password-reprompt.service";
|
||||
|
||||
import {
|
||||
@@ -239,7 +243,7 @@ import { AbstractThemingService } from "./theming/theming.service.abstraction";
|
||||
TwoFactorServiceAbstraction,
|
||||
I18nServiceAbstraction,
|
||||
EncryptService,
|
||||
PasswordGenerationServiceAbstraction,
|
||||
PasswordStrengthServiceAbstraction,
|
||||
PolicyServiceAbstraction,
|
||||
],
|
||||
},
|
||||
@@ -359,6 +363,11 @@ import { AbstractThemingService } from "./theming/theming.service.abstraction";
|
||||
DevicesApiServiceAbstraction,
|
||||
],
|
||||
},
|
||||
{
|
||||
provide: PasswordStrengthServiceAbstraction,
|
||||
useClass: PasswordStrengthService,
|
||||
deps: [],
|
||||
},
|
||||
{
|
||||
provide: PasswordGenerationServiceAbstraction,
|
||||
useClass: PasswordGenerationService,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Component, EventEmitter, Input, OnChanges, Output } from "@angular/core";
|
||||
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password";
|
||||
import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength";
|
||||
|
||||
export interface PasswordColorText {
|
||||
color: string;
|
||||
@@ -59,7 +59,7 @@ export class PasswordStrengthComponent implements OnChanges {
|
||||
|
||||
constructor(
|
||||
private i18nService: I18nService,
|
||||
private passwordGenerationService: PasswordGenerationServiceAbstraction
|
||||
private passwordStrengthService: PasswordStrengthServiceAbstraction
|
||||
) {}
|
||||
|
||||
ngOnChanges(): void {
|
||||
@@ -96,7 +96,7 @@ export class PasswordStrengthComponent implements OnChanges {
|
||||
clearTimeout(this.masterPasswordStrengthTimeout);
|
||||
}
|
||||
|
||||
const strengthResult = this.passwordGenerationService.passwordStrength(
|
||||
const strengthResult = this.passwordStrengthService.getPasswordStrength(
|
||||
masterPassword,
|
||||
this.email,
|
||||
this.name?.trim().toLowerCase().split(" ")
|
||||
|
||||
@@ -242,6 +242,7 @@ export class GeneratorComponent implements OnInit {
|
||||
{ name: "Fastmail", value: "fastmail", validForSelfHosted: true },
|
||||
{ name: "Firefox Relay", value: "firefoxrelay", validForSelfHosted: false },
|
||||
{ name: "SimpleLogin", value: "simplelogin", validForSelfHosted: true },
|
||||
{ name: "Forward Email", value: "forwardemail", validForSelfHosted: true },
|
||||
];
|
||||
|
||||
this.usernameOptions = await this.usernameGenerationService.getOptions();
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { AbstractControl, UntypedFormGroup, ValidatorFn } from "@angular/forms";
|
||||
|
||||
import { FormGroupControls } from "@bitwarden/common/platform/abstractions/form-validation-errors.service";
|
||||
import { FormGroupControls } from "../platform/abstractions/form-validation-errors.service";
|
||||
|
||||
export class InputsFieldMatch {
|
||||
//check to ensure two fields do not have the same value
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Observable } from "rxjs";
|
||||
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { CollectionView } from "@bitwarden/common/admin-console/models/view/collection.view";
|
||||
import { CollectionView } from "@bitwarden/common/vault/models/view/collection.view";
|
||||
import { FolderView } from "@bitwarden/common/vault/models/view/folder.view";
|
||||
|
||||
import { DynamicTreeNode } from "../vault-filter/models/dynamic-tree-node.model";
|
||||
|
||||
@@ -3,7 +3,6 @@ import { Observable, Subject, takeUntil, concatMap } from "rxjs";
|
||||
|
||||
import { AuditService } from "@bitwarden/common/abstractions/audit.service";
|
||||
import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
|
||||
import { CollectionService } from "@bitwarden/common/admin-console/abstractions/collection.service";
|
||||
import {
|
||||
isMember,
|
||||
OrganizationService,
|
||||
@@ -11,7 +10,6 @@ import {
|
||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { OrganizationUserStatusType, PolicyType } from "@bitwarden/common/admin-console/enums";
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { CollectionView } from "@bitwarden/common/admin-console/models/view/collection.view";
|
||||
import { EventType, SecureNoteType, UriMatchType } from "@bitwarden/common/enums";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
@@ -21,6 +19,7 @@ import { StateService } from "@bitwarden/common/platform/abstractions/state.serv
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { SendApiService } from "@bitwarden/common/tools/send/services/send-api.service.abstraction";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { CollectionService } from "@bitwarden/common/vault/abstractions/collection.service";
|
||||
import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
|
||||
import { PasswordRepromptService } from "@bitwarden/common/vault/abstractions/password-reprompt.service";
|
||||
import { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-reprompt-type";
|
||||
@@ -28,6 +27,7 @@ import { CipherType } from "@bitwarden/common/vault/enums/cipher-type";
|
||||
import { Cipher } from "@bitwarden/common/vault/models/domain/cipher";
|
||||
import { CardView } from "@bitwarden/common/vault/models/view/card.view";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
import { CollectionView } from "@bitwarden/common/vault/models/view/collection.view";
|
||||
import { FolderView } from "@bitwarden/common/vault/models/view/folder.view";
|
||||
import { IdentityView } from "@bitwarden/common/vault/models/view/identity.view";
|
||||
import { LoginUriView } from "@bitwarden/common/vault/models/view/login-uri.view";
|
||||
@@ -368,6 +368,10 @@ export class AddEditComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
}
|
||||
|
||||
onCardNumberChange(): void {
|
||||
this.cipher.card.brand = CardView.getCardBrandByPatterns(this.cipher.card.number);
|
||||
}
|
||||
|
||||
getCardExpMonthDisplay() {
|
||||
return this.cardExpMonthOptions.find((x) => x.value == this.cipher.card.expMonth)?.name;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Directive, EventEmitter, Input, Output } from "@angular/core";
|
||||
|
||||
import { CollectionView } from "@bitwarden/common/admin-console/models/view/collection.view";
|
||||
import { ITreeNodeObject } from "@bitwarden/common/models/domain/tree-node";
|
||||
import { CollectionView } from "@bitwarden/common/vault/models/view/collection.view";
|
||||
|
||||
import { DynamicTreeNode } from "../models/dynamic-tree-node.model";
|
||||
import { TopLevelTreeNode } from "../models/top-level-tree-node.model";
|
||||
|
||||
@@ -2,8 +2,8 @@ import { Directive, EventEmitter, Input, OnInit, Output } from "@angular/core";
|
||||
import { firstValueFrom, Observable } from "rxjs";
|
||||
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { CollectionView } from "@bitwarden/common/admin-console/models/view/collection.view";
|
||||
import { ITreeNodeObject } from "@bitwarden/common/models/domain/tree-node";
|
||||
import { CollectionView } from "@bitwarden/common/vault/models/view/collection.view";
|
||||
import { FolderView } from "@bitwarden/common/vault/models/view/folder.view";
|
||||
|
||||
import { DeprecatedVaultFilterService } from "../../abstractions/deprecated-vault-filter.service";
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { Injectable } from "@angular/core";
|
||||
import { firstValueFrom, from, mergeMap, Observable } from "rxjs";
|
||||
|
||||
import { CollectionService } from "@bitwarden/common/admin-console/abstractions/collection.service";
|
||||
import {
|
||||
isMember,
|
||||
OrganizationService,
|
||||
@@ -9,12 +8,13 @@ import {
|
||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { PolicyType } from "@bitwarden/common/admin-console/enums";
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { CollectionView } from "@bitwarden/common/admin-console/models/view/collection.view";
|
||||
import { ServiceUtils } from "@bitwarden/common/misc/serviceUtils";
|
||||
import { TreeNode } from "@bitwarden/common/models/domain/tree-node";
|
||||
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { CollectionService } from "@bitwarden/common/vault/abstractions/collection.service";
|
||||
import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
|
||||
import { CollectionView } from "@bitwarden/common/vault/models/view/collection.view";
|
||||
import { FolderView } from "@bitwarden/common/vault/models/view/folder.view";
|
||||
|
||||
import { DeprecatedVaultFilterService as DeprecatedVaultFilterServiceAbstraction } from "../../abstractions/deprecated-vault-filter.service";
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { OrganizationConnectionType } from "../admin-console/enums";
|
||||
import { CollectionRequest } from "../admin-console/models/request/collection.request";
|
||||
import { OrganizationSponsorshipCreateRequest } from "../admin-console/models/request/organization/organization-sponsorship-create.request";
|
||||
import { OrganizationSponsorshipRedeemRequest } from "../admin-console/models/request/organization/organization-sponsorship-redeem.request";
|
||||
import { OrganizationConnectionRequest } from "../admin-console/models/request/organization-connection.request";
|
||||
@@ -14,10 +13,6 @@ import { ProviderUserConfirmRequest } from "../admin-console/models/request/prov
|
||||
import { ProviderUserInviteRequest } from "../admin-console/models/request/provider/provider-user-invite.request";
|
||||
import { ProviderUserUpdateRequest } from "../admin-console/models/request/provider/provider-user-update.request";
|
||||
import { SelectionReadOnlyRequest } from "../admin-console/models/request/selection-read-only.request";
|
||||
import {
|
||||
CollectionAccessDetailsResponse,
|
||||
CollectionResponse,
|
||||
} from "../admin-console/models/response/collection.response";
|
||||
import {
|
||||
OrganizationConnectionConfigApis,
|
||||
OrganizationConnectionResponse,
|
||||
@@ -135,9 +130,14 @@ import { CipherCreateRequest } from "../vault/models/request/cipher-create.reque
|
||||
import { CipherPartialRequest } from "../vault/models/request/cipher-partial.request";
|
||||
import { CipherShareRequest } from "../vault/models/request/cipher-share.request";
|
||||
import { CipherRequest } from "../vault/models/request/cipher.request";
|
||||
import { CollectionRequest } from "../vault/models/request/collection.request";
|
||||
import { AttachmentUploadDataResponse } from "../vault/models/response/attachment-upload-data.response";
|
||||
import { AttachmentResponse } from "../vault/models/response/attachment.response";
|
||||
import { CipherResponse } from "../vault/models/response/cipher.response";
|
||||
import {
|
||||
CollectionAccessDetailsResponse,
|
||||
CollectionResponse,
|
||||
} from "../vault/models/response/collection.response";
|
||||
import { SyncResponse } from "../vault/models/response/sync.response";
|
||||
|
||||
/**
|
||||
|
||||
@@ -14,6 +14,7 @@ export class OrganizationUserResponse extends BaseResponse {
|
||||
accessSecretsManager: boolean;
|
||||
permissions: PermissionsApi;
|
||||
resetPasswordEnrolled: boolean;
|
||||
hasMasterPassword: boolean;
|
||||
collections: SelectionReadOnlyResponse[] = [];
|
||||
groups: string[] = [];
|
||||
|
||||
@@ -28,6 +29,7 @@ export class OrganizationUserResponse extends BaseResponse {
|
||||
this.accessAll = this.getResponseProperty("AccessAll");
|
||||
this.accessSecretsManager = this.getResponseProperty("AccessSecretsManager");
|
||||
this.resetPasswordEnrolled = this.getResponseProperty("ResetPasswordEnrolled");
|
||||
this.hasMasterPassword = this.getResponseProperty("HasMasterPassword");
|
||||
|
||||
const collections = this.getResponseProperty("Collections");
|
||||
if (collections != null) {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { BaseResponse } from "../../../models/response/base.response";
|
||||
import { CipherResponse } from "../../../vault/models/response/cipher.response";
|
||||
|
||||
import { CollectionResponse } from "./collection.response";
|
||||
import { CollectionResponse } from "../../../vault/models/response/collection.response";
|
||||
|
||||
export class OrganizationExportResponse extends BaseResponse {
|
||||
collections: CollectionResponse[];
|
||||
|
||||
@@ -11,7 +11,10 @@ import { StateService } from "../../platform/abstractions/state.service";
|
||||
import { Utils } from "../../platform/misc/utils";
|
||||
import { Account, AccountProfile, AccountTokens } from "../../platform/models/domain/account";
|
||||
import { EncString } from "../../platform/models/domain/enc-string";
|
||||
import { PasswordGenerationService } from "../../tools/generator/password";
|
||||
import {
|
||||
PasswordStrengthService,
|
||||
PasswordStrengthServiceAbstraction,
|
||||
} from "../../tools/password-strength";
|
||||
import { AuthService } from "../abstractions/auth.service";
|
||||
import { TokenService } from "../abstractions/token.service";
|
||||
import { TwoFactorService } from "../abstractions/two-factor.service";
|
||||
@@ -85,7 +88,7 @@ describe("LogInStrategy", () => {
|
||||
let twoFactorService: MockProxy<TwoFactorService>;
|
||||
let authService: MockProxy<AuthService>;
|
||||
let policyService: MockProxy<PolicyService>;
|
||||
let passwordGenerationService: MockProxy<PasswordGenerationService>;
|
||||
let passwordStrengthService: MockProxy<PasswordStrengthServiceAbstraction>;
|
||||
|
||||
let passwordLogInStrategy: PasswordLogInStrategy;
|
||||
let credentials: PasswordLogInCredentials;
|
||||
@@ -102,7 +105,7 @@ describe("LogInStrategy", () => {
|
||||
twoFactorService = mock<TwoFactorService>();
|
||||
authService = mock<AuthService>();
|
||||
policyService = mock<PolicyService>();
|
||||
passwordGenerationService = mock<PasswordGenerationService>();
|
||||
passwordStrengthService = mock<PasswordStrengthService>();
|
||||
|
||||
appIdService.getAppId.mockResolvedValue(deviceId);
|
||||
tokenService.decodeToken.calledWith(accessToken).mockResolvedValue(decodedToken);
|
||||
@@ -118,7 +121,7 @@ describe("LogInStrategy", () => {
|
||||
logService,
|
||||
stateService,
|
||||
twoFactorService,
|
||||
passwordGenerationService,
|
||||
passwordStrengthService,
|
||||
policyService,
|
||||
authService
|
||||
);
|
||||
|
||||
@@ -11,7 +11,10 @@ import { PlatformUtilsService } from "../../platform/abstractions/platform-utils
|
||||
import { StateService } from "../../platform/abstractions/state.service";
|
||||
import { Utils } from "../../platform/misc/utils";
|
||||
import { SymmetricCryptoKey } from "../../platform/models/domain/symmetric-crypto-key";
|
||||
import { PasswordGenerationService } from "../../tools/generator/password";
|
||||
import {
|
||||
PasswordStrengthService,
|
||||
PasswordStrengthServiceAbstraction,
|
||||
} from "../../tools/password-strength";
|
||||
import { AuthService } from "../abstractions/auth.service";
|
||||
import { TokenService } from "../abstractions/token.service";
|
||||
import { TwoFactorService } from "../abstractions/two-factor.service";
|
||||
@@ -51,7 +54,7 @@ describe("PasswordLogInStrategy", () => {
|
||||
let twoFactorService: MockProxy<TwoFactorService>;
|
||||
let authService: MockProxy<AuthService>;
|
||||
let policyService: MockProxy<PolicyService>;
|
||||
let passwordGenerationService: MockProxy<PasswordGenerationService>;
|
||||
let passwordStrengthService: MockProxy<PasswordStrengthServiceAbstraction>;
|
||||
|
||||
let passwordLogInStrategy: PasswordLogInStrategy;
|
||||
let credentials: PasswordLogInCredentials;
|
||||
@@ -68,7 +71,7 @@ describe("PasswordLogInStrategy", () => {
|
||||
twoFactorService = mock<TwoFactorService>();
|
||||
authService = mock<AuthService>();
|
||||
policyService = mock<PolicyService>();
|
||||
passwordGenerationService = mock<PasswordGenerationService>();
|
||||
passwordStrengthService = mock<PasswordStrengthService>();
|
||||
|
||||
appIdService.getAppId.mockResolvedValue(deviceId);
|
||||
tokenService.decodeToken.mockResolvedValue({});
|
||||
@@ -94,7 +97,7 @@ describe("PasswordLogInStrategy", () => {
|
||||
logService,
|
||||
stateService,
|
||||
twoFactorService,
|
||||
passwordGenerationService,
|
||||
passwordStrengthService,
|
||||
policyService,
|
||||
authService
|
||||
);
|
||||
@@ -141,7 +144,7 @@ describe("PasswordLogInStrategy", () => {
|
||||
});
|
||||
|
||||
it("does not force the user to update their master password when it meets requirements", async () => {
|
||||
passwordGenerationService.passwordStrength.mockReturnValue({ score: 5 } as any);
|
||||
passwordStrengthService.getPasswordStrength.mockReturnValue({ score: 5 } as any);
|
||||
policyService.evaluateMasterPassword.mockReturnValue(true);
|
||||
|
||||
const result = await passwordLogInStrategy.logIn(credentials);
|
||||
@@ -151,7 +154,7 @@ describe("PasswordLogInStrategy", () => {
|
||||
});
|
||||
|
||||
it("forces the user to update their master password on successful login when it does not meet master password policy requirements", async () => {
|
||||
passwordGenerationService.passwordStrength.mockReturnValue({ score: 0 } as any);
|
||||
passwordStrengthService.getPasswordStrength.mockReturnValue({ score: 0 } as any);
|
||||
policyService.evaluateMasterPassword.mockReturnValue(false);
|
||||
|
||||
const result = await passwordLogInStrategy.logIn(credentials);
|
||||
@@ -164,7 +167,7 @@ describe("PasswordLogInStrategy", () => {
|
||||
});
|
||||
|
||||
it("forces the user to update their master password on successful 2FA login when it does not meet master password policy requirements", async () => {
|
||||
passwordGenerationService.passwordStrength.mockReturnValue({ score: 0 } as any);
|
||||
passwordStrengthService.getPasswordStrength.mockReturnValue({ score: 0 } as any);
|
||||
policyService.evaluateMasterPassword.mockReturnValue(false);
|
||||
|
||||
const token2FAResponse = new IdentityTwoFactorResponse({
|
||||
|
||||
@@ -9,7 +9,7 @@ import { MessagingService } from "../../platform/abstractions/messaging.service"
|
||||
import { PlatformUtilsService } from "../../platform/abstractions/platform-utils.service";
|
||||
import { StateService } from "../../platform/abstractions/state.service";
|
||||
import { SymmetricCryptoKey } from "../../platform/models/domain/symmetric-crypto-key";
|
||||
import { PasswordGenerationServiceAbstraction } from "../../tools/generator/password";
|
||||
import { PasswordStrengthServiceAbstraction } from "../../tools/password-strength";
|
||||
import { AuthService } from "../abstractions/auth.service";
|
||||
import { TokenService } from "../abstractions/token.service";
|
||||
import { TwoFactorService } from "../abstractions/two-factor.service";
|
||||
@@ -54,7 +54,7 @@ export class PasswordLogInStrategy extends LogInStrategy {
|
||||
logService: LogService,
|
||||
protected stateService: StateService,
|
||||
twoFactorService: TwoFactorService,
|
||||
private passwordGenerationService: PasswordGenerationServiceAbstraction,
|
||||
private passwordStrengthService: PasswordStrengthServiceAbstraction,
|
||||
private policyService: PolicyService,
|
||||
private authService: AuthService
|
||||
) {
|
||||
@@ -158,7 +158,7 @@ export class PasswordLogInStrategy extends LogInStrategy {
|
||||
{ masterPassword, email }: PasswordLogInCredentials,
|
||||
options: MasterPasswordPolicyOptions
|
||||
): boolean {
|
||||
const passwordStrength = this.passwordGenerationService.passwordStrength(
|
||||
const passwordStrength = this.passwordStrengthService.getPasswordStrength(
|
||||
masterPassword,
|
||||
email
|
||||
)?.score;
|
||||
|
||||
@@ -17,7 +17,7 @@ import { PlatformUtilsService } from "../../platform/abstractions/platform-utils
|
||||
import { StateService } from "../../platform/abstractions/state.service";
|
||||
import { Utils } from "../../platform/misc/utils";
|
||||
import { SymmetricCryptoKey } from "../../platform/models/domain/symmetric-crypto-key";
|
||||
import { PasswordGenerationServiceAbstraction } from "../../tools/generator/password";
|
||||
import { PasswordStrengthServiceAbstraction } from "../../tools/password-strength";
|
||||
import { AuthService as AuthServiceAbstraction } from "../abstractions/auth.service";
|
||||
import { KeyConnectorService } from "../abstractions/key-connector.service";
|
||||
import { TokenService } from "../abstractions/token.service";
|
||||
@@ -102,7 +102,7 @@ export class AuthService implements AuthServiceAbstraction {
|
||||
protected twoFactorService: TwoFactorService,
|
||||
protected i18nService: I18nService,
|
||||
protected encryptService: EncryptService,
|
||||
protected passwordGenerationService: PasswordGenerationServiceAbstraction,
|
||||
protected passwordStrengthService: PasswordStrengthServiceAbstraction,
|
||||
protected policyService: PolicyService
|
||||
) {}
|
||||
|
||||
@@ -133,7 +133,7 @@ export class AuthService implements AuthServiceAbstraction {
|
||||
this.logService,
|
||||
this.stateService,
|
||||
this.twoFactorService,
|
||||
this.passwordGenerationService,
|
||||
this.passwordStrengthService,
|
||||
this.policyService,
|
||||
this
|
||||
);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Collection as CollectionDomain } from "../../admin-console/models/domain/collection";
|
||||
import { CollectionView } from "../../admin-console/models/view/collection.view";
|
||||
import { Collection as CollectionDomain } from "../../vault/models/domain/collection";
|
||||
import { CollectionView } from "../../vault/models/view/collection.view";
|
||||
|
||||
import { CollectionExport } from "./collection.export";
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Collection as CollectionDomain } from "../../admin-console/models/domain/collection";
|
||||
import { CollectionView } from "../../admin-console/models/view/collection.view";
|
||||
import { EncString } from "../../platform/models/domain/enc-string";
|
||||
import { Collection as CollectionDomain } from "../../vault/models/domain/collection";
|
||||
import { CollectionView } from "../../vault/models/view/collection.view";
|
||||
|
||||
export class CollectionExport {
|
||||
static template(): CollectionExport {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { CollectionWithIdRequest } from "../../admin-console/models/request/collection-with-id.request";
|
||||
import { CipherRequest } from "../../vault/models/request/cipher.request";
|
||||
import { CollectionWithIdRequest } from "../../vault/models/request/collection-with-id.request";
|
||||
|
||||
import { KvpRequest } from "./kvp.request";
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ export abstract class CryptoService {
|
||||
getEncKey: (key?: SymmetricCryptoKey) => Promise<SymmetricCryptoKey>;
|
||||
getPublicKey: () => Promise<ArrayBuffer>;
|
||||
getPrivateKey: () => Promise<ArrayBuffer>;
|
||||
getFingerprint: (userId: string, publicKey?: ArrayBuffer) => Promise<string[]>;
|
||||
getFingerprint: (fingerprintMaterial: string, publicKey?: ArrayBuffer) => Promise<string[]>;
|
||||
getOrgKeys: () => Promise<Map<string, SymmetricCryptoKey>>;
|
||||
getOrgKey: (orgId: string) => Promise<SymmetricCryptoKey>;
|
||||
getProviderKey: (providerId: string) => Promise<SymmetricCryptoKey>;
|
||||
|
||||
@@ -17,8 +17,17 @@ export type PayPalConfig = {
|
||||
buttonAction?: string;
|
||||
};
|
||||
|
||||
export enum Region {
|
||||
US = "US",
|
||||
EU = "EU",
|
||||
SelfHosted = "Self-hosted",
|
||||
}
|
||||
|
||||
export abstract class EnvironmentService {
|
||||
urls: Observable<Urls>;
|
||||
urls: Observable<void>;
|
||||
usUrls: Urls;
|
||||
euUrls: Urls;
|
||||
selectedRegion?: Region;
|
||||
|
||||
hasBaseUrl: () => boolean;
|
||||
getNotificationsUrl: () => string;
|
||||
@@ -32,8 +41,10 @@ export abstract class EnvironmentService {
|
||||
getScimUrl: () => string;
|
||||
setUrlsFromStorage: () => Promise<void>;
|
||||
setUrls: (urls: Urls) => Promise<Urls>;
|
||||
setRegion: (region: Region) => Promise<void>;
|
||||
getUrls: () => Urls;
|
||||
isCloud: () => boolean;
|
||||
isEmpty: () => boolean;
|
||||
/**
|
||||
* @remarks For desktop and browser use only.
|
||||
* For web, use PlatformUtilsService.isSelfHost()
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
import { Observable } from "rxjs";
|
||||
|
||||
import { CollectionData } from "../../admin-console/models/data/collection.data";
|
||||
import { EncryptedOrganizationKeyData } from "../../admin-console/models/data/encrypted-organization-key.data";
|
||||
import { OrganizationData } from "../../admin-console/models/data/organization.data";
|
||||
import { PolicyData } from "../../admin-console/models/data/policy.data";
|
||||
import { ProviderData } from "../../admin-console/models/data/provider.data";
|
||||
import { Policy } from "../../admin-console/models/domain/policy";
|
||||
import { CollectionView } from "../../admin-console/models/view/collection.view";
|
||||
import { EnvironmentUrls } from "../../auth/models/domain/environment-urls";
|
||||
import { ForceResetPasswordReason } from "../../auth/models/domain/force-reset-password-reason";
|
||||
import { KdfConfig } from "../../auth/models/domain/kdf-config";
|
||||
@@ -18,9 +16,11 @@ import { GeneratedPasswordHistory } from "../../tools/generator/password";
|
||||
import { SendData } from "../../tools/send/models/data/send.data";
|
||||
import { SendView } from "../../tools/send/models/view/send.view";
|
||||
import { CipherData } from "../../vault/models/data/cipher.data";
|
||||
import { CollectionData } from "../../vault/models/data/collection.data";
|
||||
import { FolderData } from "../../vault/models/data/folder.data";
|
||||
import { LocalData } from "../../vault/models/data/local.data";
|
||||
import { CipherView } from "../../vault/models/view/cipher.view";
|
||||
import { CollectionView } from "../../vault/models/view/collection.view";
|
||||
import { AddEditCipherInfo } from "../../vault/types/add-edit-cipher-info";
|
||||
import { ServerConfigData } from "../models/data/server-config.data";
|
||||
import { Account, AccountSettingsSettings } from "../models/domain/account";
|
||||
@@ -263,6 +263,8 @@ export abstract class StateService<T extends Account = Account> {
|
||||
setEntityType: (value: string, options?: StorageOptions) => Promise<void>;
|
||||
getEnvironmentUrls: (options?: StorageOptions) => Promise<EnvironmentUrls>;
|
||||
setEnvironmentUrls: (value: EnvironmentUrls, options?: StorageOptions) => Promise<void>;
|
||||
getRegion: (options?: StorageOptions) => Promise<string>;
|
||||
setRegion: (value: string, options?: StorageOptions) => Promise<void>;
|
||||
getEquivalentDomains: (options?: StorageOptions) => Promise<string[][]>;
|
||||
setEquivalentDomains: (value: string, options?: StorageOptions) => Promise<void>;
|
||||
getEventCollection: (options?: StorageOptions) => Promise<EventData[]>;
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
import { Jsonify } from "type-fest";
|
||||
|
||||
import { CollectionData } from "../../../admin-console/models/data/collection.data";
|
||||
import { EncryptedOrganizationKeyData } from "../../../admin-console/models/data/encrypted-organization-key.data";
|
||||
import { OrganizationData } from "../../../admin-console/models/data/organization.data";
|
||||
import { PolicyData } from "../../../admin-console/models/data/policy.data";
|
||||
import { ProviderData } from "../../../admin-console/models/data/provider.data";
|
||||
import { Policy } from "../../../admin-console/models/domain/policy";
|
||||
import { CollectionView } from "../../../admin-console/models/view/collection.view";
|
||||
import { AuthenticationStatus } from "../../../auth/enums/authentication-status";
|
||||
import { EnvironmentUrls } from "../../../auth/models/domain/environment-urls";
|
||||
import { ForceResetPasswordReason } from "../../../auth/models/domain/force-reset-password-reason";
|
||||
@@ -17,8 +15,10 @@ import { SendData } from "../../../tools/send/models/data/send.data";
|
||||
import { SendView } from "../../../tools/send/models/view/send.view";
|
||||
import { DeepJsonify } from "../../../types/deep-jsonify";
|
||||
import { CipherData } from "../../../vault/models/data/cipher.data";
|
||||
import { CollectionData } from "../../../vault/models/data/collection.data";
|
||||
import { FolderData } from "../../../vault/models/data/folder.data";
|
||||
import { CipherView } from "../../../vault/models/view/cipher.view";
|
||||
import { CollectionView } from "../../../vault/models/view/collection.view";
|
||||
import { Utils } from "../../misc/utils";
|
||||
import { ServerConfigData } from "../../models/data/server-config.data";
|
||||
|
||||
@@ -233,6 +233,7 @@ export class AccountSettings {
|
||||
approveLoginRequests?: boolean;
|
||||
avatarColor?: string;
|
||||
activateAutoFillOnPageLoadFromPolicy?: boolean;
|
||||
region?: string;
|
||||
smOnboardingTasks?: Record<string, Record<string, boolean>>;
|
||||
|
||||
static fromJSON(obj: Jsonify<AccountSettings>): AccountSettings {
|
||||
|
||||
@@ -36,4 +36,5 @@ export class GlobalState {
|
||||
enableBrowserIntegration?: boolean;
|
||||
enableBrowserIntegrationFingerprint?: boolean;
|
||||
enableDuckDuckGoBrowserIntegration?: boolean;
|
||||
region?: string;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { Injectable, OnDestroy } from "@angular/core";
|
||||
import { BehaviorSubject, Subject, concatMap, from, takeUntil, timer } from "rxjs";
|
||||
import { BehaviorSubject, concatMap, from, timer } from "rxjs";
|
||||
|
||||
import { AuthService } from "../../../auth/abstractions/auth.service";
|
||||
import { AuthenticationStatus } from "../../../auth/enums/authentication-status";
|
||||
@@ -11,11 +10,9 @@ import { EnvironmentService } from "../../abstractions/environment.service";
|
||||
import { StateService } from "../../abstractions/state.service";
|
||||
import { ServerConfigData } from "../../models/data/server-config.data";
|
||||
|
||||
@Injectable()
|
||||
export class ConfigService implements ConfigServiceAbstraction, OnDestroy {
|
||||
export class ConfigService implements ConfigServiceAbstraction {
|
||||
protected _serverConfig = new BehaviorSubject<ServerConfig | null>(null);
|
||||
serverConfig$ = this._serverConfig.asObservable();
|
||||
private destroy$ = new Subject<void>();
|
||||
|
||||
constructor(
|
||||
private stateService: StateService,
|
||||
@@ -30,16 +27,11 @@ export class ConfigService implements ConfigServiceAbstraction, OnDestroy {
|
||||
this._serverConfig.next(serverConfig);
|
||||
});
|
||||
|
||||
this.environmentService.urls.pipe(takeUntil(this.destroy$)).subscribe(() => {
|
||||
this.environmentService.urls.subscribe(() => {
|
||||
this.fetchServerConfig();
|
||||
});
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.destroy$.next();
|
||||
this.destroy$.complete();
|
||||
}
|
||||
|
||||
async fetchServerConfig(): Promise<ServerConfig> {
|
||||
try {
|
||||
const response = await this.configApiService.get();
|
||||
|
||||
@@ -204,7 +204,7 @@ export class CryptoService implements CryptoServiceAbstraction {
|
||||
return privateKey;
|
||||
}
|
||||
|
||||
async getFingerprint(userId: string, publicKey?: ArrayBuffer): Promise<string[]> {
|
||||
async getFingerprint(fingerprintMaterial: string, publicKey?: ArrayBuffer): Promise<string[]> {
|
||||
if (publicKey == null) {
|
||||
publicKey = await this.getPublicKey();
|
||||
}
|
||||
@@ -214,7 +214,7 @@ export class CryptoService implements CryptoServiceAbstraction {
|
||||
const keyFingerprint = await this.cryptoFunctionService.hash(publicKey, "sha256");
|
||||
const userFingerprint = await this.cryptoFunctionService.hkdfExpand(
|
||||
keyFingerprint,
|
||||
userId,
|
||||
fingerprintMaterial,
|
||||
32,
|
||||
"sha256"
|
||||
);
|
||||
|
||||
@@ -3,13 +3,15 @@ import { concatMap, Observable, Subject } from "rxjs";
|
||||
import { EnvironmentUrls } from "../../auth/models/domain/environment-urls";
|
||||
import {
|
||||
EnvironmentService as EnvironmentServiceAbstraction,
|
||||
Region,
|
||||
Urls,
|
||||
} from "../abstractions/environment.service";
|
||||
import { StateService } from "../abstractions/state.service";
|
||||
|
||||
export class EnvironmentService implements EnvironmentServiceAbstraction {
|
||||
private readonly urlsSubject = new Subject<Urls>();
|
||||
urls: Observable<Urls> = this.urlsSubject;
|
||||
private readonly urlsSubject = new Subject<void>();
|
||||
urls: Observable<void> = this.urlsSubject.asObservable();
|
||||
selectedRegion?: Region;
|
||||
|
||||
protected baseUrl: string;
|
||||
protected webVaultUrl: string;
|
||||
@@ -21,6 +23,28 @@ export class EnvironmentService implements EnvironmentServiceAbstraction {
|
||||
private keyConnectorUrl: string;
|
||||
private scimUrl: string = null;
|
||||
|
||||
readonly usUrls: Urls = {
|
||||
base: null,
|
||||
api: "https://api.bitwarden.com",
|
||||
identity: "https://identity.bitwarden.com",
|
||||
icons: "https://icons.bitwarden.net",
|
||||
webVault: "https://vault.bitwarden.com",
|
||||
notifications: "https://notifications.bitwarden.com",
|
||||
events: "https://events.bitwarden.com",
|
||||
scim: "https://scim.bitwarden.com/v2",
|
||||
};
|
||||
|
||||
readonly euUrls: Urls = {
|
||||
base: null,
|
||||
api: "https://api.bitwarden.eu",
|
||||
identity: "https://identity.bitwarden.eu",
|
||||
icons: "https://icons.bitwarden.eu",
|
||||
webVault: "https://vault.bitwarden.eu",
|
||||
notifications: "https://notifications.bitwarden.eu",
|
||||
events: "https://events.bitwarden.eu",
|
||||
scim: "https://scim.bitwarden.eu/v2",
|
||||
};
|
||||
|
||||
constructor(private stateService: StateService) {
|
||||
this.stateService.activeAccount$
|
||||
.pipe(
|
||||
@@ -127,18 +151,42 @@ export class EnvironmentService implements EnvironmentServiceAbstraction {
|
||||
}
|
||||
|
||||
async setUrlsFromStorage(): Promise<void> {
|
||||
const urls: any = await this.stateService.getEnvironmentUrls();
|
||||
const region = await this.stateService.getRegion();
|
||||
const savedUrls = await this.stateService.getEnvironmentUrls();
|
||||
const envUrls = new EnvironmentUrls();
|
||||
|
||||
this.baseUrl = envUrls.base = urls.base;
|
||||
this.webVaultUrl = urls.webVault;
|
||||
this.apiUrl = envUrls.api = urls.api;
|
||||
this.identityUrl = envUrls.identity = urls.identity;
|
||||
this.iconsUrl = urls.icons;
|
||||
this.notificationsUrl = urls.notifications;
|
||||
this.eventsUrl = envUrls.events = urls.events;
|
||||
this.keyConnectorUrl = urls.keyConnector;
|
||||
// scimUrl is not saved to storage
|
||||
// fix environment urls for old users
|
||||
if (savedUrls.base === "https://vault.bitwarden.com") {
|
||||
this.setRegion(Region.US);
|
||||
return;
|
||||
}
|
||||
if (savedUrls.base === "https://vault.bitwarden.eu") {
|
||||
this.setRegion(Region.EU);
|
||||
return;
|
||||
}
|
||||
|
||||
switch (region) {
|
||||
case Region.EU:
|
||||
this.setRegion(Region.EU);
|
||||
return;
|
||||
case Region.US:
|
||||
this.setRegion(Region.US);
|
||||
return;
|
||||
case Region.SelfHosted:
|
||||
default:
|
||||
this.baseUrl = envUrls.base = savedUrls.base;
|
||||
this.webVaultUrl = savedUrls.webVault;
|
||||
this.apiUrl = envUrls.api = savedUrls.api;
|
||||
this.identityUrl = envUrls.identity = savedUrls.identity;
|
||||
this.iconsUrl = savedUrls.icons;
|
||||
this.notificationsUrl = savedUrls.notifications;
|
||||
this.eventsUrl = envUrls.events = savedUrls.events;
|
||||
this.keyConnectorUrl = savedUrls.keyConnector;
|
||||
// scimUrl is not saved to storage
|
||||
this.urlsSubject.next();
|
||||
this.setRegion(Region.SelfHosted);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
async setUrls(urls: Urls): Promise<Urls> {
|
||||
@@ -176,7 +224,9 @@ export class EnvironmentService implements EnvironmentServiceAbstraction {
|
||||
this.keyConnectorUrl = urls.keyConnector;
|
||||
this.scimUrl = urls.scim;
|
||||
|
||||
this.urlsSubject.next(urls);
|
||||
await this.setRegion(Region.SelfHosted);
|
||||
|
||||
this.urlsSubject.next();
|
||||
|
||||
return urls;
|
||||
}
|
||||
@@ -195,6 +245,52 @@ export class EnvironmentService implements EnvironmentServiceAbstraction {
|
||||
};
|
||||
}
|
||||
|
||||
isEmpty(): boolean {
|
||||
return (
|
||||
this.baseUrl == null &&
|
||||
this.webVaultUrl == null &&
|
||||
this.apiUrl == null &&
|
||||
this.identityUrl == null &&
|
||||
this.iconsUrl == null &&
|
||||
this.notificationsUrl == null &&
|
||||
this.eventsUrl == null
|
||||
);
|
||||
}
|
||||
|
||||
async setRegion(region: Region) {
|
||||
this.selectedRegion = region;
|
||||
await this.stateService.setRegion(region);
|
||||
switch (region) {
|
||||
case Region.EU:
|
||||
this.setUrlsInternal(this.euUrls);
|
||||
break;
|
||||
case Region.US:
|
||||
this.setUrlsInternal(this.usUrls);
|
||||
break;
|
||||
case Region.SelfHosted:
|
||||
// if user saves with empty fields, default to US
|
||||
if (this.isEmpty()) {
|
||||
this.setRegion(Region.US);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private setUrlsInternal(urls: Urls) {
|
||||
this.baseUrl = this.formatUrl(urls.base);
|
||||
this.webVaultUrl = this.formatUrl(urls.webVault);
|
||||
this.apiUrl = this.formatUrl(urls.api);
|
||||
this.identityUrl = this.formatUrl(urls.identity);
|
||||
this.iconsUrl = this.formatUrl(urls.icons);
|
||||
this.notificationsUrl = this.formatUrl(urls.notifications);
|
||||
this.eventsUrl = this.formatUrl(urls.events);
|
||||
this.keyConnectorUrl = this.formatUrl(urls.keyConnector);
|
||||
|
||||
// scimUrl cannot be cleared
|
||||
this.scimUrl = this.formatUrl(urls.scim) ?? this.scimUrl;
|
||||
this.urlsSubject.next();
|
||||
}
|
||||
|
||||
private formatUrl(url: string): string {
|
||||
if (url == null || url === "") {
|
||||
return null;
|
||||
@@ -209,9 +305,12 @@ export class EnvironmentService implements EnvironmentServiceAbstraction {
|
||||
}
|
||||
|
||||
isCloud(): boolean {
|
||||
return ["https://api.bitwarden.com", "https://vault.bitwarden.com/api"].includes(
|
||||
this.getApiUrl()
|
||||
);
|
||||
return [
|
||||
"https://api.bitwarden.com",
|
||||
"https://vault.bitwarden.com/api",
|
||||
"https://api.bitwarden.eu",
|
||||
"https://vault.bitwarden.eu/api",
|
||||
].includes(this.getApiUrl());
|
||||
}
|
||||
|
||||
isSelfHosted(): boolean {
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { CollectionData } from "../../admin-console/models/data/collection.data";
|
||||
import { OrganizationData } from "../../admin-console/models/data/organization.data";
|
||||
import { PolicyData } from "../../admin-console/models/data/policy.data";
|
||||
import { ProviderData } from "../../admin-console/models/data/provider.data";
|
||||
@@ -9,6 +8,7 @@ import { EventData } from "../../models/data/event.data";
|
||||
import { GeneratedPasswordHistory } from "../../tools/generator/password";
|
||||
import { SendData } from "../../tools/send/models/data/send.data";
|
||||
import { CipherData } from "../../vault/models/data/cipher.data";
|
||||
import { CollectionData } from "../../vault/models/data/collection.data";
|
||||
import { FolderData } from "../../vault/models/data/folder.data";
|
||||
import { AbstractStorageService } from "../abstractions/storage.service";
|
||||
import { StateFactory } from "../factories/state-factory";
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
import { BehaviorSubject, concatMap } from "rxjs";
|
||||
import { Jsonify, JsonValue } from "type-fest";
|
||||
|
||||
import { CollectionData } from "../../admin-console/models/data/collection.data";
|
||||
import { EncryptedOrganizationKeyData } from "../../admin-console/models/data/encrypted-organization-key.data";
|
||||
import { OrganizationData } from "../../admin-console/models/data/organization.data";
|
||||
import { PolicyData } from "../../admin-console/models/data/policy.data";
|
||||
import { ProviderData } from "../../admin-console/models/data/provider.data";
|
||||
import { Policy } from "../../admin-console/models/domain/policy";
|
||||
import { CollectionView } from "../../admin-console/models/view/collection.view";
|
||||
import { EnvironmentUrls } from "../../auth/models/domain/environment-urls";
|
||||
import { ForceResetPasswordReason } from "../../auth/models/domain/force-reset-password-reason";
|
||||
import { KdfConfig } from "../../auth/models/domain/kdf-config";
|
||||
@@ -26,9 +24,11 @@ import { GeneratedPasswordHistory } from "../../tools/generator/password";
|
||||
import { SendData } from "../../tools/send/models/data/send.data";
|
||||
import { SendView } from "../../tools/send/models/view/send.view";
|
||||
import { CipherData } from "../../vault/models/data/cipher.data";
|
||||
import { CollectionData } from "../../vault/models/data/collection.data";
|
||||
import { FolderData } from "../../vault/models/data/folder.data";
|
||||
import { LocalData } from "../../vault/models/data/local.data";
|
||||
import { CipherView } from "../../vault/models/view/cipher.view";
|
||||
import { CollectionView } from "../../vault/models/view/collection.view";
|
||||
import { AddEditCipherInfo } from "../../vault/types/add-edit-cipher-info";
|
||||
import { LogService } from "../abstractions/log.service";
|
||||
import { StateMigrationService } from "../abstractions/state-migration.service";
|
||||
@@ -1598,6 +1598,28 @@ export class StateService<
|
||||
);
|
||||
}
|
||||
|
||||
async getRegion(options?: StorageOptions): Promise<string> {
|
||||
if ((await this.state())?.activeUserId == null) {
|
||||
options = this.reconcileOptions(options, await this.defaultOnDiskOptions());
|
||||
return (await this.getGlobals(options)).region ?? null;
|
||||
}
|
||||
options = this.reconcileOptions(options, await this.defaultOnDiskOptions());
|
||||
return (await this.getAccount(options))?.settings?.region ?? null;
|
||||
}
|
||||
|
||||
async setRegion(value: string, options?: StorageOptions): Promise<void> {
|
||||
// Global values are set on each change and the current global settings are passed to any newly authed accounts.
|
||||
// This is to allow setting region values before an account is active, while still allowing individual accounts to have their own region.
|
||||
const globals = await this.getGlobals(
|
||||
this.reconcileOptions(options, await this.defaultOnDiskOptions())
|
||||
);
|
||||
globals.region = value;
|
||||
await this.saveGlobals(
|
||||
globals,
|
||||
this.reconcileOptions(options, await this.defaultOnDiskOptions())
|
||||
);
|
||||
}
|
||||
|
||||
async getEquivalentDomains(options?: StorageOptions): Promise<string[][]> {
|
||||
return (
|
||||
await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions()))
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { Injectable } from "@angular/core";
|
||||
import {
|
||||
HttpTransportType,
|
||||
HubConnection,
|
||||
@@ -17,7 +16,6 @@ import {
|
||||
NotificationResponse,
|
||||
} from "./../models/response/notification.response";
|
||||
|
||||
@Injectable()
|
||||
export class AnonymousHubService implements AnonymousHubServiceAbstraction {
|
||||
private anonHubConnection: HubConnection;
|
||||
private url: string;
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { ApiService as ApiServiceAbstraction } from "../abstractions/api.service";
|
||||
import { OrganizationConnectionType } from "../admin-console/enums";
|
||||
import { CollectionRequest } from "../admin-console/models/request/collection.request";
|
||||
import { OrganizationSponsorshipCreateRequest } from "../admin-console/models/request/organization/organization-sponsorship-create.request";
|
||||
import { OrganizationSponsorshipRedeemRequest } from "../admin-console/models/request/organization/organization-sponsorship-redeem.request";
|
||||
import { OrganizationConnectionRequest } from "../admin-console/models/request/organization-connection.request";
|
||||
@@ -15,10 +14,6 @@ import { ProviderUserConfirmRequest } from "../admin-console/models/request/prov
|
||||
import { ProviderUserInviteRequest } from "../admin-console/models/request/provider/provider-user-invite.request";
|
||||
import { ProviderUserUpdateRequest } from "../admin-console/models/request/provider/provider-user-update.request";
|
||||
import { SelectionReadOnlyRequest } from "../admin-console/models/request/selection-read-only.request";
|
||||
import {
|
||||
CollectionAccessDetailsResponse,
|
||||
CollectionResponse,
|
||||
} from "../admin-console/models/response/collection.response";
|
||||
import {
|
||||
OrganizationConnectionConfigApis,
|
||||
OrganizationConnectionResponse,
|
||||
@@ -144,9 +139,14 @@ import { CipherCreateRequest } from "../vault/models/request/cipher-create.reque
|
||||
import { CipherPartialRequest } from "../vault/models/request/cipher-partial.request";
|
||||
import { CipherShareRequest } from "../vault/models/request/cipher-share.request";
|
||||
import { CipherRequest } from "../vault/models/request/cipher.request";
|
||||
import { CollectionRequest } from "../vault/models/request/collection.request";
|
||||
import { AttachmentUploadDataResponse } from "../vault/models/response/attachment-upload-data.response";
|
||||
import { AttachmentResponse } from "../vault/models/response/attachment.response";
|
||||
import { CipherResponse } from "../vault/models/response/cipher.response";
|
||||
import {
|
||||
CollectionAccessDetailsResponse,
|
||||
CollectionResponse,
|
||||
} from "../vault/models/response/collection.response";
|
||||
import { SyncResponse } from "../vault/models/response/sync.response";
|
||||
|
||||
/**
|
||||
|
||||
@@ -3,7 +3,6 @@ import { firstValueFrom } from "rxjs";
|
||||
import { SearchService } from "../../abstractions/search.service";
|
||||
import { VaultTimeoutService as VaultTimeoutServiceAbstraction } from "../../abstractions/vaultTimeout/vaultTimeout.service";
|
||||
import { VaultTimeoutSettingsService } from "../../abstractions/vaultTimeout/vaultTimeoutSettings.service";
|
||||
import { CollectionService } from "../../admin-console/abstractions/collection.service";
|
||||
import { AuthService } from "../../auth/abstractions/auth.service";
|
||||
import { KeyConnectorService } from "../../auth/abstractions/key-connector.service";
|
||||
import { AuthenticationStatus } from "../../auth/enums/authentication-status";
|
||||
@@ -13,6 +12,7 @@ import { MessagingService } from "../../platform/abstractions/messaging.service"
|
||||
import { PlatformUtilsService } from "../../platform/abstractions/platform-utils.service";
|
||||
import { StateService } from "../../platform/abstractions/state.service";
|
||||
import { CipherService } from "../../vault/abstractions/cipher.service";
|
||||
import { CollectionService } from "../../vault/abstractions/collection.service";
|
||||
import { FolderService } from "../../vault/abstractions/folder/folder.service.abstraction";
|
||||
|
||||
export class VaultTimeoutService implements VaultTimeoutServiceAbstraction {
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import * as zxcvbn from "zxcvbn";
|
||||
|
||||
import { PasswordGeneratorPolicyOptions } from "../../../admin-console/models/domain/password-generator-policy-options";
|
||||
|
||||
import { GeneratedPasswordHistory } from "./generated-password-history";
|
||||
@@ -17,11 +15,6 @@ export abstract class PasswordGenerationServiceAbstraction {
|
||||
getHistory: () => Promise<GeneratedPasswordHistory[]>;
|
||||
addHistory: (password: string) => Promise<void>;
|
||||
clear: (userId?: string) => Promise<void>;
|
||||
passwordStrength: (
|
||||
password: string,
|
||||
email?: string,
|
||||
userInputs?: string[]
|
||||
) => zxcvbn.ZXCVBNResult;
|
||||
normalizeOptions: (
|
||||
options: PasswordGeneratorOptions,
|
||||
enforcedPolicyOptions: PasswordGeneratorPolicyOptions
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import * as zxcvbn from "zxcvbn";
|
||||
|
||||
import { PolicyService } from "../../../admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { PolicyType } from "../../../admin-console/enums";
|
||||
import { PasswordGeneratorPolicyOptions } from "../../../admin-console/models/domain/password-generator-policy-options";
|
||||
@@ -387,33 +385,6 @@ export class PasswordGenerationService implements PasswordGenerationServiceAbstr
|
||||
await this.stateService.setDecryptedPasswordGenerationHistory(null, { userId: userId });
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates a password strength score using zxcvbn.
|
||||
* @param password The password to calculate the strength of.
|
||||
* @param emailInput An unparsed email address to use as user input.
|
||||
* @param userInputs An array of additional user inputs to use when calculating the strength.
|
||||
*/
|
||||
passwordStrength(
|
||||
password: string,
|
||||
emailInput: string = null,
|
||||
userInputs: string[] = null
|
||||
): zxcvbn.ZXCVBNResult {
|
||||
if (password == null || password.length === 0) {
|
||||
return null;
|
||||
}
|
||||
const globalUserInputs = [
|
||||
"bitwarden",
|
||||
"bit",
|
||||
"warden",
|
||||
...(userInputs ?? []),
|
||||
...this.emailToUserInputs(emailInput),
|
||||
];
|
||||
// Use a hash set to get rid of any duplicate user inputs
|
||||
const finalUserInputs = Array.from(new Set(globalUserInputs));
|
||||
const result = zxcvbn(password, finalUserInputs);
|
||||
return result;
|
||||
}
|
||||
|
||||
normalizeOptions(
|
||||
options: PasswordGeneratorOptions,
|
||||
enforcedPolicyOptions: PasswordGeneratorPolicyOptions
|
||||
@@ -476,27 +447,6 @@ export class PasswordGenerationService implements PasswordGenerationServiceAbstr
|
||||
this.sanitizePasswordLength(options, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert an email address into a list of user inputs for zxcvbn by
|
||||
* taking the local part of the email address and splitting it into words.
|
||||
* @param email
|
||||
* @private
|
||||
*/
|
||||
private emailToUserInputs(email: string): string[] {
|
||||
if (email == null || email.length === 0) {
|
||||
return [];
|
||||
}
|
||||
const atPosition = email.indexOf("@");
|
||||
if (atPosition < 0) {
|
||||
return [];
|
||||
}
|
||||
return email
|
||||
.substring(0, atPosition)
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
.split(/[^A-Za-z0-9]/);
|
||||
}
|
||||
|
||||
private capitalize(str: string) {
|
||||
return str.charAt(0).toUpperCase() + str.slice(1);
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ export class AnonAddyForwarder implements Forwarder {
|
||||
headers: new Headers({
|
||||
Authorization: "Bearer " + options.apiKey,
|
||||
"Content-Type": "application/json",
|
||||
"X-Requested-With": "XMLHttpRequest",
|
||||
}),
|
||||
};
|
||||
const url = "https://app.anonaddy.com/api/v1/aliases";
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
import { ApiService } from "../../../../abstractions/api.service";
|
||||
import { Utils } from "../../../../platform/misc/utils";
|
||||
|
||||
import { Forwarder } from "./forwarder";
|
||||
import { ForwarderOptions } from "./forwarder-options";
|
||||
|
||||
export class ForwardEmailForwarder implements Forwarder {
|
||||
async generate(apiService: ApiService, options: ForwarderOptions): Promise<string> {
|
||||
if (options.apiKey == null || options.apiKey === "") {
|
||||
throw "Invalid Forward Email API key.";
|
||||
}
|
||||
if (options.forwardemail?.domain == null || options.forwardemail.domain === "") {
|
||||
throw "Invalid Forward Email domain.";
|
||||
}
|
||||
const requestInit: RequestInit = {
|
||||
redirect: "manual",
|
||||
cache: "no-store",
|
||||
method: "POST",
|
||||
headers: new Headers({
|
||||
Authorization: "Basic " + Utils.fromUtf8ToB64(options.apiKey + ":"),
|
||||
"Content-Type": "application/json",
|
||||
}),
|
||||
};
|
||||
const url = `https://api.forwardemail.net/v1/domains/${options.forwardemail.domain}/aliases`;
|
||||
requestInit.body = JSON.stringify({
|
||||
labels: options.website,
|
||||
description:
|
||||
(options.website != null ? "Website: " + options.website + ". " : "") +
|
||||
"Generated by Bitwarden.",
|
||||
});
|
||||
const request = new Request(url, requestInit);
|
||||
const response = await apiService.nativeFetch(request);
|
||||
if (response.status === 200 || response.status === 201) {
|
||||
const json = await response.json();
|
||||
return json?.name + "@" + (json?.domain?.name || options.forwardemail.domain);
|
||||
}
|
||||
if (response.status === 401) {
|
||||
throw "Invalid Forward Email API key.";
|
||||
}
|
||||
const json = await response.json();
|
||||
if (json?.message != null) {
|
||||
throw "Forward Email error:\n" + json.message;
|
||||
}
|
||||
if (json?.error != null) {
|
||||
throw "Forward Email error:\n" + json.error;
|
||||
}
|
||||
throw "Unknown Forward Email error occurred.";
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ export class ForwarderOptions {
|
||||
website: string;
|
||||
fastmail = new FastmailForwarderOptions();
|
||||
anonaddy = new AnonAddyForwarderOptions();
|
||||
forwardemail = new ForwardEmailForwarderOptions();
|
||||
}
|
||||
|
||||
export class FastmailForwarderOptions {
|
||||
@@ -12,3 +13,7 @@ export class FastmailForwarderOptions {
|
||||
export class AnonAddyForwarderOptions {
|
||||
domain: string;
|
||||
}
|
||||
|
||||
export class ForwardEmailForwarderOptions {
|
||||
domain: string;
|
||||
}
|
||||
|
||||
@@ -5,3 +5,4 @@ export { FirefoxRelayForwarder } from "./firefox-relay-forwarder";
|
||||
export { Forwarder } from "./forwarder";
|
||||
export { ForwarderOptions } from "./forwarder-options";
|
||||
export { SimpleLoginForwarder } from "./simple-login-forwarder";
|
||||
export { ForwardEmailForwarder } from "./forward-email-forwarder";
|
||||
|
||||
@@ -35,13 +35,9 @@ export class SimpleLoginForwarder implements Forwarder {
|
||||
if (response.status === 401) {
|
||||
throw "Invalid SimpleLogin API key.";
|
||||
}
|
||||
try {
|
||||
const json = await response.json();
|
||||
if (json?.error != null) {
|
||||
throw "SimpleLogin error:" + json.error;
|
||||
}
|
||||
} catch {
|
||||
// Do nothing...
|
||||
const json = await response.json();
|
||||
if (json?.error != null) {
|
||||
throw "SimpleLogin error:" + json.error;
|
||||
}
|
||||
throw "Unknown SimpleLogin error occurred.";
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
DuckDuckGoForwarder,
|
||||
FastmailForwarder,
|
||||
FirefoxRelayForwarder,
|
||||
ForwardEmailForwarder,
|
||||
Forwarder,
|
||||
ForwarderOptions,
|
||||
SimpleLoginForwarder,
|
||||
@@ -22,6 +23,7 @@ const DefaultOptions = {
|
||||
catchallType: "random",
|
||||
forwardedService: "",
|
||||
forwardedAnonAddyDomain: "anonaddy.me",
|
||||
forwardedForwardEmailDomain: "hideaddress.net",
|
||||
};
|
||||
|
||||
export class UsernameGenerationService implements UsernameGenerationServiceAbstraction {
|
||||
@@ -137,6 +139,10 @@ export class UsernameGenerationService implements UsernameGenerationServiceAbstr
|
||||
} else if (o.forwardedService === "duckduckgo") {
|
||||
forwarder = new DuckDuckGoForwarder();
|
||||
forwarderOptions.apiKey = o.forwardedDuckDuckGoToken;
|
||||
} else if (o.forwardedService === "forwardemail") {
|
||||
forwarder = new ForwardEmailForwarder();
|
||||
forwarderOptions.apiKey = o.forwardedForwardEmailApiToken;
|
||||
forwarderOptions.forwardemail.domain = o.forwardedForwardEmailDomain;
|
||||
}
|
||||
|
||||
if (forwarder == null) {
|
||||
|
||||
2
libs/common/src/tools/password-strength/index.ts
Normal file
2
libs/common/src/tools/password-strength/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export { PasswordStrengthServiceAbstraction } from "./password-strength.service.abstraction";
|
||||
export { PasswordStrengthService } from "./password-strength.service";
|
||||
@@ -0,0 +1,5 @@
|
||||
import { ZXCVBNResult } from "zxcvbn";
|
||||
|
||||
export abstract class PasswordStrengthServiceAbstraction {
|
||||
getPasswordStrength: (password: string, email?: string, userInputs?: string[]) => ZXCVBNResult;
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
import * as zxcvbn from "zxcvbn";
|
||||
|
||||
import { PasswordStrengthServiceAbstraction } from "./password-strength.service.abstraction";
|
||||
|
||||
export class PasswordStrengthService implements PasswordStrengthServiceAbstraction {
|
||||
/**
|
||||
* Calculates a password strength score using zxcvbn.
|
||||
* @param password The password to calculate the strength of.
|
||||
* @param emailInput An unparsed email address to use as user input.
|
||||
* @param userInputs An array of additional user inputs to use when calculating the strength.
|
||||
*/
|
||||
getPasswordStrength(
|
||||
password: string,
|
||||
emailInput: string = null,
|
||||
userInputs: string[] = null
|
||||
): zxcvbn.ZXCVBNResult {
|
||||
if (password == null || password.length === 0) {
|
||||
return null;
|
||||
}
|
||||
const globalUserInputs = [
|
||||
"bitwarden",
|
||||
"bit",
|
||||
"warden",
|
||||
...(userInputs ?? []),
|
||||
...this.emailToUserInputs(emailInput),
|
||||
];
|
||||
// Use a hash set to get rid of any duplicate user inputs
|
||||
const finalUserInputs = Array.from(new Set(globalUserInputs));
|
||||
const result = zxcvbn(password, finalUserInputs);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert an email address into a list of user inputs for zxcvbn by
|
||||
* taking the local part of the email address and splitting it into words.
|
||||
* @param email
|
||||
* @private
|
||||
*/
|
||||
private emailToUserInputs(email: string): string[] {
|
||||
if (email == null || email.length === 0) {
|
||||
return [];
|
||||
}
|
||||
const atPosition = email.indexOf("@");
|
||||
if (atPosition < 0) {
|
||||
return [];
|
||||
}
|
||||
return email
|
||||
.substring(0, atPosition)
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
.split(/[^A-Za-z0-9]/);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Collection } from "../domain/collection";
|
||||
import { CollectionRequest } from "../request/collection.request";
|
||||
|
||||
import { CollectionRequest } from "./collection.request";
|
||||
|
||||
export class CollectionWithIdRequest extends CollectionRequest {
|
||||
id: string;
|
||||
@@ -1,7 +1,6 @@
|
||||
import { SelectionReadOnlyRequest } from "../../../admin-console/models/request/selection-read-only.request";
|
||||
import { Collection } from "../domain/collection";
|
||||
|
||||
import { SelectionReadOnlyRequest } from "./selection-read-only.request";
|
||||
|
||||
export class CollectionRequest {
|
||||
name: string;
|
||||
externalId: string;
|
||||
@@ -1,7 +1,6 @@
|
||||
import { SelectionReadOnlyResponse } from "../../../admin-console/models/response/selection-read-only.response";
|
||||
import { BaseResponse } from "../../../models/response/base.response";
|
||||
|
||||
import { SelectionReadOnlyResponse } from "./selection-read-only.response";
|
||||
|
||||
export class CollectionResponse extends BaseResponse {
|
||||
id: string;
|
||||
organizationId: string;
|
||||
@@ -1,4 +1,3 @@
|
||||
import { CollectionDetailsResponse } from "../../../admin-console/models/response/collection.response";
|
||||
import { PolicyResponse } from "../../../admin-console/models/response/policy.response";
|
||||
import { BaseResponse } from "../../../models/response/base.response";
|
||||
import { DomainsResponse } from "../../../models/response/domains.response";
|
||||
@@ -6,6 +5,7 @@ import { ProfileResponse } from "../../../models/response/profile.response";
|
||||
import { SendResponse } from "../../../tools/send/models/response/send.response";
|
||||
|
||||
import { CipherResponse } from "./cipher.response";
|
||||
import { CollectionDetailsResponse } from "./collection.response";
|
||||
import { FolderResponse } from "./folder.response";
|
||||
|
||||
export class SyncResponse extends BaseResponse {
|
||||
|
||||
@@ -81,4 +81,67 @@ export class CardView extends ItemView {
|
||||
static fromJSON(obj: Partial<Jsonify<CardView>>): CardView {
|
||||
return Object.assign(new CardView(), obj);
|
||||
}
|
||||
|
||||
// ref https://stackoverflow.com/a/5911300
|
||||
static getCardBrandByPatterns(cardNum: string): string {
|
||||
if (cardNum == null || typeof cardNum !== "string" || cardNum.trim() === "") {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Visa
|
||||
let re = new RegExp("^4");
|
||||
if (cardNum.match(re) != null) {
|
||||
return "Visa";
|
||||
}
|
||||
|
||||
// Mastercard
|
||||
// Updated for Mastercard 2017 BINs expansion
|
||||
if (
|
||||
/^(5[1-5][0-9]{14}|2(22[1-9][0-9]{12}|2[3-9][0-9]{13}|[3-6][0-9]{14}|7[0-1][0-9]{13}|720[0-9]{12}))$/.test(
|
||||
cardNum
|
||||
)
|
||||
) {
|
||||
return "Mastercard";
|
||||
}
|
||||
|
||||
// AMEX
|
||||
re = new RegExp("^3[47]");
|
||||
if (cardNum.match(re) != null) {
|
||||
return "Amex";
|
||||
}
|
||||
|
||||
// Discover
|
||||
re = new RegExp(
|
||||
"^(6011|622(12[6-9]|1[3-9][0-9]|[2-8][0-9]{2}|9[0-1][0-9]|92[0-5]|64[4-9])|65)"
|
||||
);
|
||||
if (cardNum.match(re) != null) {
|
||||
return "Discover";
|
||||
}
|
||||
|
||||
// Diners
|
||||
re = new RegExp("^36");
|
||||
if (cardNum.match(re) != null) {
|
||||
return "Diners Club";
|
||||
}
|
||||
|
||||
// Diners - Carte Blanche
|
||||
re = new RegExp("^30[0-5]");
|
||||
if (cardNum.match(re) != null) {
|
||||
return "Diners Club";
|
||||
}
|
||||
|
||||
// JCB
|
||||
re = new RegExp("^35(2[89]|[3-8][0-9])");
|
||||
if (cardNum.match(re) != null) {
|
||||
return "JCB";
|
||||
}
|
||||
|
||||
// Visa Electron
|
||||
re = new RegExp("^(4026|417500|4508|4844|491(3|7))");
|
||||
if (cardNum.match(re) != null) {
|
||||
return "Visa";
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import { CryptoService } from "../../platform/abstractions/crypto.service";
|
||||
import { I18nService } from "../../platform/abstractions/i18n.service";
|
||||
import { StateService } from "../../platform/abstractions/state.service";
|
||||
import { Utils } from "../../platform/misc/utils";
|
||||
import { CollectionService as CollectionServiceAbstraction } from "../abstractions/collection.service";
|
||||
import { CollectionService as CollectionServiceAbstraction } from "../../vault/abstractions/collection.service";
|
||||
import { CollectionData } from "../models/data/collection.data";
|
||||
import { Collection } from "../models/domain/collection";
|
||||
import { CollectionView } from "../models/view/collection.view";
|
||||
@@ -1,14 +1,11 @@
|
||||
import { ApiService } from "../../../abstractions/api.service";
|
||||
import { SettingsService } from "../../../abstractions/settings.service";
|
||||
import { CollectionService } from "../../../admin-console/abstractions/collection.service";
|
||||
import { InternalOrganizationService } from "../../../admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { InternalPolicyService } from "../../../admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { ProviderService } from "../../../admin-console/abstractions/provider.service";
|
||||
import { CollectionData } from "../../../admin-console/models/data/collection.data";
|
||||
import { OrganizationData } from "../../../admin-console/models/data/organization.data";
|
||||
import { PolicyData } from "../../../admin-console/models/data/policy.data";
|
||||
import { ProviderData } from "../../../admin-console/models/data/provider.data";
|
||||
import { CollectionDetailsResponse } from "../../../admin-console/models/response/collection.response";
|
||||
import { PolicyResponse } from "../../../admin-console/models/response/policy.response";
|
||||
import { KeyConnectorService } from "../../../auth/abstractions/key-connector.service";
|
||||
import { ForceResetPasswordReason } from "../../../auth/models/domain/force-reset-password-reason";
|
||||
@@ -36,6 +33,9 @@ import { CipherData } from "../../../vault/models/data/cipher.data";
|
||||
import { FolderData } from "../../../vault/models/data/folder.data";
|
||||
import { CipherResponse } from "../../../vault/models/response/cipher.response";
|
||||
import { FolderResponse } from "../../../vault/models/response/folder.response";
|
||||
import { CollectionService } from "../../abstractions/collection.service";
|
||||
import { CollectionData } from "../../models/data/collection.data";
|
||||
import { CollectionDetailsResponse } from "../../models/response/collection.response";
|
||||
|
||||
export class SyncService implements SyncServiceAbstraction {
|
||||
syncInProgress = false;
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
<div class="tw-inline-flex tw-gap-2">
|
||||
<span *ngFor="let item of filteredItems; let last = last" bitBadge [badgeType]="badgeType">
|
||||
{{ item }}
|
||||
<ng-container *ngFor="let item of filteredItems; let last = last">
|
||||
<span bitBadge [badgeType]="badgeType" [truncate]="truncate">
|
||||
{{ item }}
|
||||
</span>
|
||||
<span class="tw-sr-only" *ngIf="!last || isFiltered">, </span>
|
||||
</span>
|
||||
</ng-container>
|
||||
<span *ngIf="isFiltered" bitBadge [badgeType]="badgeType">
|
||||
{{ "plusNMore" | i18n : (items.length - filteredItems.length).toString() }}
|
||||
</span>
|
||||
|
||||
@@ -14,6 +14,7 @@ export class BadgeListComponent implements OnChanges {
|
||||
|
||||
@Input() badgeType: BadgeTypes = "primary";
|
||||
@Input() items: string[] = [];
|
||||
@Input() truncate = true;
|
||||
|
||||
@Input()
|
||||
get maxItems(): number | undefined {
|
||||
|
||||
@@ -29,6 +29,7 @@ export default {
|
||||
],
|
||||
args: {
|
||||
badgeType: "primary",
|
||||
truncate: false,
|
||||
},
|
||||
parameters: {
|
||||
design: {
|
||||
@@ -44,7 +45,7 @@ export const Default: Story = {
|
||||
render: (args) => ({
|
||||
props: args,
|
||||
template: `
|
||||
<bit-badge-list [badgeType]="badgeType" [maxItems]="maxItems" [items]="items"></bit-badge-list>
|
||||
<bit-badge-list [badgeType]="badgeType" [maxItems]="maxItems" [items]="items" [truncate]="truncate"></bit-badge-list>
|
||||
`,
|
||||
}),
|
||||
|
||||
@@ -52,5 +53,16 @@ export const Default: Story = {
|
||||
badgeType: "info",
|
||||
maxItems: 3,
|
||||
items: ["Badge 1", "Badge 2", "Badge 3", "Badge 4", "Badge 5"],
|
||||
truncate: false,
|
||||
},
|
||||
};
|
||||
|
||||
export const Truncated: Story = {
|
||||
...Default,
|
||||
args: {
|
||||
badgeType: "info",
|
||||
maxItems: 3,
|
||||
items: ["Badge 1", "Badge 2 containing lengthy text", "Badge 3", "Badge 4", "Badge 5"],
|
||||
truncate: true,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -26,11 +26,12 @@ const hoverStyles: Record<BadgeTypes, string[]> = {
|
||||
export class BadgeDirective {
|
||||
@HostBinding("class") get classList() {
|
||||
return [
|
||||
"tw-inline",
|
||||
"tw-inline-block",
|
||||
"tw-py-0.5",
|
||||
"tw-px-1.5",
|
||||
"tw-font-bold",
|
||||
"tw-text-center",
|
||||
"tw-align-text-top",
|
||||
"!tw-text-contrast",
|
||||
"tw-rounded",
|
||||
"tw-border-none",
|
||||
@@ -44,14 +45,19 @@ export class BadgeDirective {
|
||||
"focus:tw-ring-primary-700",
|
||||
]
|
||||
.concat(styles[this.badgeType])
|
||||
.concat(this.hasHoverEffects ? hoverStyles[this.badgeType] : []);
|
||||
.concat(this.hasHoverEffects ? hoverStyles[this.badgeType] : [])
|
||||
.concat(this.truncate ? ["tw-truncate", "tw-max-w-40"] : []);
|
||||
}
|
||||
@HostBinding("attr.title") get title() {
|
||||
return this.truncate ? this.el.nativeElement.textContent.trim() : null;
|
||||
}
|
||||
|
||||
@Input() badgeType: BadgeTypes = "primary";
|
||||
@Input() truncate = true;
|
||||
|
||||
private hasHoverEffects = false;
|
||||
|
||||
constructor(el: ElementRef<Element>) {
|
||||
constructor(private el: ElementRef<HTMLElement>) {
|
||||
this.hasHoverEffects = el?.nativeElement?.nodeName != "SPAN";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ export default {
|
||||
],
|
||||
args: {
|
||||
badgeType: "primary",
|
||||
truncate: false,
|
||||
},
|
||||
parameters: {
|
||||
design: {
|
||||
@@ -29,11 +30,11 @@ export const Primary: Story = {
|
||||
render: (args) => ({
|
||||
props: args,
|
||||
template: `
|
||||
<span class="tw-text-main">Span </span><span bitBadge [badgeType]="badgeType">Badge</span>
|
||||
<span class="tw-text-main">Span </span><span bitBadge [badgeType]="badgeType" [truncate]="truncate">Badge containing lengthy text</span>
|
||||
<br><br>
|
||||
<span class="tw-text-main">Link </span><a href="#" bitBadge [badgeType]="badgeType">Badge</a>
|
||||
<span class="tw-text-main">Link </span><a href="#" bitBadge [badgeType]="badgeType" [truncate]="truncate">Badge</a>
|
||||
<br><br>
|
||||
<span class="tw-text-main">Button </span><button bitBadge [badgeType]="badgeType">Badge</button>
|
||||
<span class="tw-text-main">Button </span><button bitBadge [badgeType]="badgeType" [truncate]="truncate">Badge</button>
|
||||
`,
|
||||
}),
|
||||
};
|
||||
@@ -72,3 +73,10 @@ export const Info: Story = {
|
||||
badgeType: "info",
|
||||
},
|
||||
};
|
||||
|
||||
export const Truncated: Story = {
|
||||
...Primary,
|
||||
args: {
|
||||
truncate: true,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Component } from "@angular/core";
|
||||
import { Component, Input } from "@angular/core";
|
||||
|
||||
import { Icons } from "..";
|
||||
|
||||
@@ -10,5 +10,5 @@ import { Icons } from "..";
|
||||
templateUrl: "./no-items.component.html",
|
||||
})
|
||||
export class NoItemsComponent {
|
||||
protected icon = Icons.Search;
|
||||
@Input() icon = Icons.Search;
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { HostBinding, Directive } from "@angular/core";
|
||||
import { Directive, HostBinding } from "@angular/core";
|
||||
|
||||
@Directive({
|
||||
selector: "th[bitCell], td[bitCell]",
|
||||
})
|
||||
export class CellDirective {
|
||||
@HostBinding("class") get classList() {
|
||||
return ["tw-p-3", "tw-align-middle"];
|
||||
return ["tw-p-3"];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import { Directive, HostBinding, Input } from "@angular/core";
|
||||
selector: "tr[bitRow]",
|
||||
})
|
||||
export class RowDirective {
|
||||
@Input() alignContent: "top" | "middle" | "bottom" | "baseline" = "baseline";
|
||||
@Input() alignContent: "top" | "middle" | "bottom" | "baseline" = "middle";
|
||||
|
||||
get alignmentClass(): string {
|
||||
switch (this.alignContent) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ScrollingModule } from "@angular/cdk/scrolling";
|
||||
import { Meta, StoryObj, moduleMetadata } from "@storybook/angular";
|
||||
import { Meta, moduleMetadata, StoryObj } from "@storybook/angular";
|
||||
|
||||
import { countries } from "../form/countries";
|
||||
|
||||
@@ -62,7 +62,7 @@ export const Default: Story = {
|
||||
`,
|
||||
}),
|
||||
args: {
|
||||
alignRowContent: "baseline",
|
||||
alignRowContent: "middle",
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
import * as papa from "papaparse";
|
||||
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { CollectionData } from "@bitwarden/common/admin-console/models/data/collection.data";
|
||||
import { Collection } from "@bitwarden/common/admin-console/models/domain/collection";
|
||||
import { CollectionDetailsResponse } from "@bitwarden/common/admin-console/models/response/collection.response";
|
||||
import { CollectionView } from "@bitwarden/common/admin-console/models/view/collection.view";
|
||||
import { KdfConfig } from "@bitwarden/common/auth/models/domain/kdf-config";
|
||||
import { KdfType } from "@bitwarden/common/enums";
|
||||
import {
|
||||
@@ -20,9 +16,13 @@ import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.servi
|
||||
import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
|
||||
import { CipherType } from "@bitwarden/common/vault/enums/cipher-type";
|
||||
import { CipherData } from "@bitwarden/common/vault/models/data/cipher.data";
|
||||
import { CollectionData } from "@bitwarden/common/vault/models/data/collection.data";
|
||||
import { Cipher } from "@bitwarden/common/vault/models/domain/cipher";
|
||||
import { Collection } from "@bitwarden/common/vault/models/domain/collection";
|
||||
import { Folder } from "@bitwarden/common/vault/models/domain/folder";
|
||||
import { CollectionDetailsResponse } from "@bitwarden/common/vault/models/response/collection.response";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
import { CollectionView } from "@bitwarden/common/vault/models/view/collection.view";
|
||||
import { FolderView } from "@bitwarden/common/vault/models/view/folder.view";
|
||||
|
||||
import { ExportHelper } from "../../export-helper";
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { SecureNoteType } from "@bitwarden/common/enums";
|
||||
import { CipherType } from "@bitwarden/common/vault/enums/cipher-type";
|
||||
import { CardView } from "@bitwarden/common/vault/models/view/card.view";
|
||||
|
||||
import { ImportResult } from "../../models/import-result";
|
||||
import { BaseImporter } from "../base-importer";
|
||||
@@ -48,7 +49,7 @@ export class AvastJsonImporter extends BaseImporter implements Importer {
|
||||
cipher.card.cardholderName = this.getValueOrDefault(value.holderName);
|
||||
cipher.card.number = this.getValueOrDefault(value.cardNumber);
|
||||
cipher.card.code = this.getValueOrDefault(value.cvv);
|
||||
cipher.card.brand = this.getCardBrand(cipher.card.number);
|
||||
cipher.card.brand = CardView.getCardBrandByPatterns(cipher.card.number);
|
||||
if (value.expirationDate != null) {
|
||||
if (value.expirationDate.month != null) {
|
||||
cipher.card.expMonth = value.expirationDate.month + "";
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import * as papa from "papaparse";
|
||||
|
||||
import { CollectionView } from "@bitwarden/common/admin-console/models/view/collection.view";
|
||||
import { FieldType, SecureNoteType } from "@bitwarden/common/enums";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { ConsoleLogService } from "@bitwarden/common/platform/services/console-log.service";
|
||||
import { CipherType } from "@bitwarden/common/vault/enums/cipher-type";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
import { CollectionView } from "@bitwarden/common/vault/models/view/collection.view";
|
||||
import { FieldView } from "@bitwarden/common/vault/models/view/field.view";
|
||||
import { FolderView } from "@bitwarden/common/vault/models/view/folder.view";
|
||||
import { LoginUriView } from "@bitwarden/common/vault/models/view/login-uri.view";
|
||||
@@ -241,69 +241,6 @@ export abstract class BaseImporter {
|
||||
return str.split(this.newLineRegex);
|
||||
}
|
||||
|
||||
// ref https://stackoverflow.com/a/5911300
|
||||
protected getCardBrand(cardNum: string) {
|
||||
if (this.isNullOrWhitespace(cardNum)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Visa
|
||||
let re = new RegExp("^4");
|
||||
if (cardNum.match(re) != null) {
|
||||
return "Visa";
|
||||
}
|
||||
|
||||
// Mastercard
|
||||
// Updated for Mastercard 2017 BINs expansion
|
||||
if (
|
||||
/^(5[1-5][0-9]{14}|2(22[1-9][0-9]{12}|2[3-9][0-9]{13}|[3-6][0-9]{14}|7[0-1][0-9]{13}|720[0-9]{12}))$/.test(
|
||||
cardNum
|
||||
)
|
||||
) {
|
||||
return "Mastercard";
|
||||
}
|
||||
|
||||
// AMEX
|
||||
re = new RegExp("^3[47]");
|
||||
if (cardNum.match(re) != null) {
|
||||
return "Amex";
|
||||
}
|
||||
|
||||
// Discover
|
||||
re = new RegExp(
|
||||
"^(6011|622(12[6-9]|1[3-9][0-9]|[2-8][0-9]{2}|9[0-1][0-9]|92[0-5]|64[4-9])|65)"
|
||||
);
|
||||
if (cardNum.match(re) != null) {
|
||||
return "Discover";
|
||||
}
|
||||
|
||||
// Diners
|
||||
re = new RegExp("^36");
|
||||
if (cardNum.match(re) != null) {
|
||||
return "Diners Club";
|
||||
}
|
||||
|
||||
// Diners - Carte Blanche
|
||||
re = new RegExp("^30[0-5]");
|
||||
if (cardNum.match(re) != null) {
|
||||
return "Diners Club";
|
||||
}
|
||||
|
||||
// JCB
|
||||
re = new RegExp("^35(2[89]|[3-8][0-9])");
|
||||
if (cardNum.match(re) != null) {
|
||||
return "JCB";
|
||||
}
|
||||
|
||||
// Visa Electron
|
||||
re = new RegExp("^(4026|417500|4508|4844|491(3|7))");
|
||||
if (cardNum.match(re) != null) {
|
||||
return "Visa";
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
protected setCardExpiration(cipher: CipherView, expiration: string): boolean {
|
||||
if (this.isNullOrWhitespace(expiration)) {
|
||||
return false;
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { CollectionView } from "@bitwarden/common/admin-console/models/view/collection.view";
|
||||
import { FieldType, SecureNoteType } from "@bitwarden/common/enums";
|
||||
import { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-reprompt-type";
|
||||
import { CipherType } from "@bitwarden/common/vault/enums/cipher-type";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
import { CollectionView } from "@bitwarden/common/vault/models/view/collection.view";
|
||||
import { FieldView } from "@bitwarden/common/vault/models/view/field.view";
|
||||
import { LoginView } from "@bitwarden/common/vault/models/view/login.view";
|
||||
import { SecureNoteView } from "@bitwarden/common/vault/models/view/secure-note.view";
|
||||
|
||||
@@ -136,7 +136,7 @@ export class DashlaneCsvImporter extends BaseImporter implements Importer {
|
||||
case "credit_card":
|
||||
cipher.card.cardholderName = row.account_name;
|
||||
cipher.card.number = row.cc_number;
|
||||
cipher.card.brand = this.getCardBrand(cipher.card.number);
|
||||
cipher.card.brand = CardView.getCardBrandByPatterns(cipher.card.number);
|
||||
cipher.card.code = row.code;
|
||||
this.setCardExpiration(cipher, `${row.expiration_month}/${row.expiration_year}`);
|
||||
|
||||
|
||||
@@ -134,7 +134,7 @@ export class DashlaneJsonImporter extends BaseImporter implements Importer {
|
||||
cipher.type = CipherType.Card;
|
||||
cipher.name = this.getValueOrDefault(obj.bank);
|
||||
cipher.card.number = this.getValueOrDefault(obj.cardNumber);
|
||||
cipher.card.brand = this.getCardBrand(cipher.card.number);
|
||||
cipher.card.brand = CardView.getCardBrandByPatterns(cipher.card.number);
|
||||
cipher.card.cardholderName = this.getValueOrDefault(obj.owner);
|
||||
if (!this.isNullOrWhitespace(cipher.card.brand)) {
|
||||
if (this.isNullOrWhitespace(cipher.name)) {
|
||||
|
||||
@@ -38,7 +38,7 @@ export class EncryptrCsvImporter extends BaseImporter implements Importer {
|
||||
cipher.card = new CardView();
|
||||
cipher.card.cardholderName = this.getValueOrDefault(value["Name on card"]);
|
||||
cipher.card.number = this.getValueOrDefault(value["Card Number"]);
|
||||
cipher.card.brand = this.getCardBrand(cipher.card.number);
|
||||
cipher.card.brand = CardView.getCardBrandByPatterns(cipher.card.number);
|
||||
cipher.card.code = this.getValueOrDefault(value.CVV);
|
||||
const expiry = this.getValueOrDefault(value.Expiry);
|
||||
if (!this.isNullOrWhitespace(expiry)) {
|
||||
|
||||
@@ -90,7 +90,7 @@ export class EnpassCsvImporter extends BaseImporter implements Importer {
|
||||
continue;
|
||||
} else if (fieldNameLower === "number" && this.isNullOrWhitespace(cipher.card.number)) {
|
||||
cipher.card.number = fieldValue;
|
||||
cipher.card.brand = this.getCardBrand(fieldValue);
|
||||
cipher.card.brand = CardView.getCardBrandByPatterns(cipher.card.number);
|
||||
continue;
|
||||
} else if (fieldNameLower === "cvc" && this.isNullOrWhitespace(cipher.card.code)) {
|
||||
cipher.card.code = fieldValue;
|
||||
|
||||
@@ -126,7 +126,7 @@ export class EnpassJsonImporter extends BaseImporter implements Importer {
|
||||
cipher.card.cardholderName = field.value;
|
||||
} else if (field.type === "ccNumber" && this.isNullOrWhitespace(cipher.card.number)) {
|
||||
cipher.card.number = field.value;
|
||||
cipher.card.brand = this.getCardBrand(cipher.card.number);
|
||||
cipher.card.brand = CardView.getCardBrandByPatterns(cipher.card.number);
|
||||
} else if (field.type === "ccCvc" && this.isNullOrWhitespace(cipher.card.code)) {
|
||||
cipher.card.code = field.value;
|
||||
} else if (field.type === "ccExpiry" && this.isNullOrWhitespace(cipher.card.expYear)) {
|
||||
|
||||
@@ -66,7 +66,7 @@ export class FSecureFskImporter extends BaseImporter implements Importer {
|
||||
cipher.card = new CardView();
|
||||
cipher.card.cardholderName = this.getValueOrDefault(entry.username);
|
||||
cipher.card.number = this.getValueOrDefault(entry.creditNumber);
|
||||
cipher.card.brand = this.getCardBrand(cipher.card.number);
|
||||
cipher.card.brand = CardView.getCardBrandByPatterns(cipher.card.number);
|
||||
cipher.card.code = this.getValueOrDefault(entry.creditCvv);
|
||||
if (!this.isNullOrWhitespace(entry.creditExpiry)) {
|
||||
if (!this.setCardExpiration(cipher, entry.creditExpiry)) {
|
||||
|
||||
@@ -122,7 +122,7 @@ export class LastPassCsvImporter extends BaseImporter implements Importer {
|
||||
card.cardholderName = this.getValueOrDefault(value.ccname);
|
||||
card.number = this.getValueOrDefault(value.ccnum);
|
||||
card.code = this.getValueOrDefault(value.cccsc);
|
||||
card.brand = this.getCardBrand(value.ccnum);
|
||||
card.brand = CardView.getCardBrandByPatterns(card.number);
|
||||
|
||||
if (!this.isNullOrWhitespace(value.ccexp) && value.ccexp.indexOf("-") > -1) {
|
||||
const ccexpParts = (value.ccexp as string).split("-");
|
||||
|
||||
@@ -72,7 +72,7 @@ export class MykiCsvImporter extends BaseImporter implements Importer {
|
||||
cipher.type = CipherType.Card;
|
||||
cipher.card.cardholderName = this.getValueOrDefault(value.cardName);
|
||||
cipher.card.number = this.getValueOrDefault(value.cardNumber);
|
||||
cipher.card.brand = this.getCardBrand(cipher.card.number);
|
||||
cipher.card.brand = CardView.getCardBrandByPatterns(cipher.card.number);
|
||||
cipher.card.expMonth = this.getValueOrDefault(value.exp_month);
|
||||
cipher.card.expYear = this.getValueOrDefault(value.exp_year);
|
||||
cipher.card.code = this.getValueOrDefault(value.cvv);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { SecureNoteType } from "@bitwarden/common/enums";
|
||||
import { CipherType } from "@bitwarden/common/vault/enums/cipher-type";
|
||||
import { CardView } from "@bitwarden/common/vault/models/view/card.view";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
import { LoginView } from "@bitwarden/common/vault/models/view/login.view";
|
||||
|
||||
@@ -66,7 +67,7 @@ export class NordPassCsvImporter extends BaseImporter implements Importer {
|
||||
cipher.card.cardholderName = this.getValueOrDefault(record.cardholdername);
|
||||
cipher.card.number = this.getValueOrDefault(record.cardnumber);
|
||||
cipher.card.code = this.getValueOrDefault(record.cvc);
|
||||
cipher.card.brand = this.getCardBrand(cipher.card.number);
|
||||
cipher.card.brand = CardView.getCardBrandByPatterns(cipher.card.number);
|
||||
this.setCardExpiration(cipher, record.expirydate);
|
||||
break;
|
||||
|
||||
|
||||
@@ -198,7 +198,7 @@ export class OnePassword1PifImporter extends BaseImporter implements Importer {
|
||||
} else if (cipher.type === CipherType.Card) {
|
||||
if (this.isNullOrWhitespace(cipher.card.number) && fieldDesignation === "ccnum") {
|
||||
cipher.card.number = fieldValue;
|
||||
cipher.card.brand = this.getCardBrand(fieldValue);
|
||||
cipher.card.brand = CardView.getCardBrandByPatterns(cipher.card.number);
|
||||
return;
|
||||
} else if (this.isNullOrWhitespace(cipher.card.code) && fieldDesignation === "cvv") {
|
||||
cipher.card.code = fieldValue;
|
||||
|
||||
@@ -407,7 +407,7 @@ export class OnePassword1PuxImporter extends BaseImporter implements Importer {
|
||||
private fillCreditCard(field: FieldsEntity, fieldValue: string, cipher: CipherView): boolean {
|
||||
if (this.isNullOrWhitespace(cipher.card.number) && field.id === "ccnum") {
|
||||
cipher.card.number = fieldValue;
|
||||
cipher.card.brand = this.getCardBrand(fieldValue);
|
||||
cipher.card.brand = CardView.getCardBrandByPatterns(cipher.card.number);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { FieldType } from "@bitwarden/common/enums";
|
||||
import { CipherType } from "@bitwarden/common/vault/enums/cipher-type";
|
||||
import { CardView } from "@bitwarden/common/vault/models/view/card.view";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
|
||||
import { ImportResult } from "../../models/import-result";
|
||||
@@ -295,7 +296,7 @@ export abstract class OnePasswordCsvImporter extends BaseImporter implements Imp
|
||||
context.lowerProperty.includes("number")
|
||||
) {
|
||||
context.cipher.card.number = context.importRecord[context.property];
|
||||
context.cipher.card.brand = this.getCardBrand(context.cipher.card.number);
|
||||
context.cipher.card.brand = CardView.getCardBrandByPatterns(context.cipher.card.number);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { CollectionView } from "@bitwarden/common/admin-console/models/view/collection.view";
|
||||
import { CollectionView } from "@bitwarden/common/vault/models/view/collection.view";
|
||||
|
||||
import { ImportResult } from "../models/import-result";
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { CollectionView } from "@bitwarden/common/admin-console/models/view/collection.view";
|
||||
import { CollectionView } from "@bitwarden/common/vault/models/view/collection.view";
|
||||
|
||||
import { ImportResult } from "../models/import-result";
|
||||
|
||||
|
||||
@@ -75,7 +75,7 @@ export class PasswordBossJsonImporter extends BaseImporter implements Importer {
|
||||
if (cipher.type === CipherType.Card) {
|
||||
if (property === "cardNumber") {
|
||||
cipher.card.number = val;
|
||||
cipher.card.brand = this.getCardBrand(val);
|
||||
cipher.card.brand = CardView.getCardBrandByPatterns(cipher.card.number);
|
||||
continue;
|
||||
} else if (property === "nameOnCard") {
|
||||
cipher.card.cardholderName = val;
|
||||
|
||||
@@ -31,7 +31,7 @@ export class RememBearCsvImporter extends BaseImporter implements Importer {
|
||||
cipher.card = new CardView();
|
||||
cipher.card.cardholderName = this.getValueOrDefault(value.cardholder);
|
||||
cipher.card.number = this.getValueOrDefault(value.number);
|
||||
cipher.card.brand = this.getCardBrand(cipher.card.number);
|
||||
cipher.card.brand = CardView.getCardBrandByPatterns(cipher.card.number);
|
||||
cipher.card.code = this.getValueOrDefault(value.verification);
|
||||
|
||||
try {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user