mirror of
https://github.com/bitwarden/browser
synced 2026-02-10 21:50:15 +00:00
Merge remote-tracking branch 'origin/main' into beeep/developer-tooling-feature-flags + merge conflict resolutions
This commit is contained in:
@@ -15,6 +15,7 @@
|
||||
"scripts": {
|
||||
"clean": "rimraf dist",
|
||||
"build": "npm run clean && tsc",
|
||||
"build:watch": "npm run clean && tsc -watch"
|
||||
"build:watch": "npm run clean && tsc -watch",
|
||||
"test": "jest"
|
||||
}
|
||||
}
|
||||
|
||||
41
libs/angular/project.json
Normal file
41
libs/angular/project.json
Normal file
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"name": "@bitwarden/angular",
|
||||
"$schema": "../../node_modules/nx/schemas/project-schema.json",
|
||||
"sourceRoot": "libs/angular/src",
|
||||
"projectType": "library",
|
||||
"tags": ["scope:angular", "type:lib"],
|
||||
"targets": {
|
||||
"build": {
|
||||
"executor": "nx:run-script",
|
||||
"dependsOn": [],
|
||||
"options": {
|
||||
"script": "build"
|
||||
}
|
||||
},
|
||||
"build:watch": {
|
||||
"executor": "nx:run-script",
|
||||
"options": {
|
||||
"script": "build:watch"
|
||||
}
|
||||
},
|
||||
"clean": {
|
||||
"executor": "nx:run-script",
|
||||
"options": {
|
||||
"script": "clean"
|
||||
}
|
||||
},
|
||||
"lint": {
|
||||
"executor": "@nx/eslint:lint",
|
||||
"outputs": ["{options.outputFile}"],
|
||||
"options": {
|
||||
"lintFilePatterns": ["libs/angular/**/*.ts"]
|
||||
}
|
||||
},
|
||||
"test": {
|
||||
"executor": "nx:run-script",
|
||||
"options": {
|
||||
"script": "test"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,232 +0,0 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Directive, OnDestroy, OnInit } from "@angular/core";
|
||||
import { Subject, firstValueFrom, map, switchMap, takeUntil } from "rxjs";
|
||||
|
||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { EncString } from "@bitwarden/common/platform/models/domain/enc-string";
|
||||
import { UserKey, MasterKey } from "@bitwarden/common/types/key";
|
||||
import { DialogService, ToastService } from "@bitwarden/components";
|
||||
import { KdfConfig, KdfConfigService, KeyService } from "@bitwarden/key-management";
|
||||
|
||||
import { PasswordColorText } from "../../tools/password-strength/password-strength.component";
|
||||
|
||||
@Directive()
|
||||
export class ChangePasswordComponent implements OnInit, OnDestroy {
|
||||
masterPassword: string;
|
||||
masterPasswordRetype: string;
|
||||
formPromise: Promise<any>;
|
||||
enforcedPolicyOptions: MasterPasswordPolicyOptions;
|
||||
passwordStrengthResult: any;
|
||||
color: string;
|
||||
text: string;
|
||||
leakedPassword: boolean;
|
||||
minimumLength = Utils.minimumPasswordLength;
|
||||
|
||||
protected email: string;
|
||||
protected kdfConfig: KdfConfig;
|
||||
|
||||
protected destroy$ = new Subject<void>();
|
||||
|
||||
constructor(
|
||||
protected accountService: AccountService,
|
||||
protected dialogService: DialogService,
|
||||
protected i18nService: I18nService,
|
||||
protected kdfConfigService: KdfConfigService,
|
||||
protected keyService: KeyService,
|
||||
protected masterPasswordService: InternalMasterPasswordServiceAbstraction,
|
||||
protected messagingService: MessagingService,
|
||||
protected platformUtilsService: PlatformUtilsService,
|
||||
protected policyService: PolicyService,
|
||||
protected toastService: ToastService,
|
||||
) {}
|
||||
|
||||
async ngOnInit() {
|
||||
this.email = await firstValueFrom(
|
||||
this.accountService.activeAccount$.pipe(map((a) => a?.email)),
|
||||
);
|
||||
this.accountService.activeAccount$
|
||||
.pipe(
|
||||
getUserId,
|
||||
switchMap((userId) => this.policyService.masterPasswordPolicyOptions$(userId)),
|
||||
takeUntil(this.destroy$),
|
||||
)
|
||||
.subscribe(
|
||||
(enforcedPasswordPolicyOptions) =>
|
||||
(this.enforcedPolicyOptions ??= enforcedPasswordPolicyOptions),
|
||||
);
|
||||
|
||||
if (this.enforcedPolicyOptions?.minLength) {
|
||||
this.minimumLength = this.enforcedPolicyOptions.minLength;
|
||||
}
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.destroy$.next();
|
||||
this.destroy$.complete();
|
||||
}
|
||||
|
||||
async submit() {
|
||||
if (!(await this.strongPassword())) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!(await this.setupSubmitActions())) {
|
||||
return;
|
||||
}
|
||||
|
||||
const [userId, email] = await firstValueFrom(
|
||||
this.accountService.activeAccount$.pipe(map((a) => [a?.id, a?.email])),
|
||||
);
|
||||
|
||||
if (this.kdfConfig == null) {
|
||||
this.kdfConfig = await this.kdfConfigService.getKdfConfig(userId);
|
||||
}
|
||||
|
||||
// Create new master key
|
||||
const newMasterKey = await this.keyService.makeMasterKey(
|
||||
this.masterPassword,
|
||||
email.trim().toLowerCase(),
|
||||
this.kdfConfig,
|
||||
);
|
||||
const newMasterKeyHash = await this.keyService.hashMasterKey(this.masterPassword, newMasterKey);
|
||||
|
||||
let newProtectedUserKey: [UserKey, EncString] = null;
|
||||
const userKey = await this.keyService.getUserKey();
|
||||
if (userKey == null) {
|
||||
newProtectedUserKey = await this.keyService.makeUserKey(newMasterKey);
|
||||
} else {
|
||||
newProtectedUserKey = await this.keyService.encryptUserKeyWithMasterKey(newMasterKey);
|
||||
}
|
||||
|
||||
await this.performSubmitActions(newMasterKeyHash, newMasterKey, newProtectedUserKey);
|
||||
}
|
||||
|
||||
async setupSubmitActions(): Promise<boolean> {
|
||||
// Override in sub-class
|
||||
// Can be used for additional validation and/or other processes the should occur before changing passwords
|
||||
return true;
|
||||
}
|
||||
|
||||
async performSubmitActions(
|
||||
newMasterKeyHash: string,
|
||||
newMasterKey: MasterKey,
|
||||
newUserKey: [UserKey, EncString],
|
||||
) {
|
||||
// Override in sub-class
|
||||
}
|
||||
|
||||
async strongPassword(): Promise<boolean> {
|
||||
if (this.masterPassword == null || this.masterPassword === "") {
|
||||
this.toastService.showToast({
|
||||
variant: "error",
|
||||
title: this.i18nService.t("errorOccurred"),
|
||||
message: this.i18nService.t("masterPasswordRequired"),
|
||||
});
|
||||
return false;
|
||||
}
|
||||
if (this.masterPassword.length < this.minimumLength) {
|
||||
this.toastService.showToast({
|
||||
variant: "error",
|
||||
title: this.i18nService.t("errorOccurred"),
|
||||
message: this.i18nService.t("masterPasswordMinimumlength", this.minimumLength),
|
||||
});
|
||||
return false;
|
||||
}
|
||||
if (this.masterPassword !== this.masterPasswordRetype) {
|
||||
this.toastService.showToast({
|
||||
variant: "error",
|
||||
title: this.i18nService.t("errorOccurred"),
|
||||
message: this.i18nService.t("masterPassDoesntMatch"),
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
const strengthResult = this.passwordStrengthResult;
|
||||
|
||||
if (
|
||||
this.enforcedPolicyOptions != null &&
|
||||
!this.policyService.evaluateMasterPassword(
|
||||
strengthResult.score,
|
||||
this.masterPassword,
|
||||
this.enforcedPolicyOptions,
|
||||
)
|
||||
) {
|
||||
this.toastService.showToast({
|
||||
variant: "error",
|
||||
title: this.i18nService.t("errorOccurred"),
|
||||
message: this.i18nService.t("masterPasswordPolicyRequirementsNotMet"),
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
const weakPassword = strengthResult != null && strengthResult.score < 3;
|
||||
|
||||
if (weakPassword && this.leakedPassword) {
|
||||
const result = await this.dialogService.openSimpleDialog({
|
||||
title: { key: "weakAndExposedMasterPassword" },
|
||||
content: { key: "weakAndBreachedMasterPasswordDesc" },
|
||||
type: "warning",
|
||||
});
|
||||
|
||||
if (!result) {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
if (weakPassword) {
|
||||
const result = await this.dialogService.openSimpleDialog({
|
||||
title: { key: "weakMasterPassword" },
|
||||
content: { key: "weakMasterPasswordDesc" },
|
||||
type: "warning",
|
||||
});
|
||||
|
||||
if (!result) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (this.leakedPassword) {
|
||||
const result = await this.dialogService.openSimpleDialog({
|
||||
title: { key: "exposedMasterPassword" },
|
||||
content: { key: "exposedMasterPasswordDesc" },
|
||||
type: "warning",
|
||||
});
|
||||
|
||||
if (!result) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
async logOut() {
|
||||
const confirmed = await this.dialogService.openSimpleDialog({
|
||||
title: { key: "logOut" },
|
||||
content: { key: "logOutConfirmation" },
|
||||
acceptButtonText: { key: "logOut" },
|
||||
type: "warning",
|
||||
});
|
||||
|
||||
if (confirmed) {
|
||||
this.messagingService.send("logout");
|
||||
}
|
||||
}
|
||||
|
||||
getStrengthResult(result: any) {
|
||||
this.passwordStrengthResult = result;
|
||||
}
|
||||
|
||||
getPasswordScoreText(event: PasswordColorText) {
|
||||
this.color = event.color;
|
||||
this.text = event.text;
|
||||
}
|
||||
}
|
||||
@@ -1,84 +0,0 @@
|
||||
<ng-container
|
||||
*ngIf="{
|
||||
selectedRegion: selectedRegion$ | async,
|
||||
} as data"
|
||||
>
|
||||
<div class="tw-text-sm tw-text-muted tw-leading-7 tw-font-normal tw-pl-4">
|
||||
{{ "accessing" | i18n }}:
|
||||
<button
|
||||
type="button"
|
||||
(click)="toggle(null)"
|
||||
cdkOverlayOrigin
|
||||
#trigger="cdkOverlayOrigin"
|
||||
aria-haspopup="dialog"
|
||||
aria-controls="cdk-overlay-container"
|
||||
>
|
||||
<span class="tw-text-primary-600 tw-text-sm tw-font-semibold">
|
||||
<ng-container *ngIf="data.selectedRegion; else fallback">
|
||||
{{ data.selectedRegion.domain }}
|
||||
</ng-container>
|
||||
<ng-template #fallback>
|
||||
{{ "selfHostedServer" | i18n }}
|
||||
</ng-template>
|
||||
</span>
|
||||
<i class="bwi bwi-fw bwi-sm bwi-angle-down" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<ng-template
|
||||
cdkConnectedOverlay
|
||||
[cdkConnectedOverlayOrigin]="trigger"
|
||||
[cdkConnectedOverlayOpen]="isOpen"
|
||||
[cdkConnectedOverlayPositions]="overlayPosition"
|
||||
[cdkConnectedOverlayHasBackdrop]="true"
|
||||
[cdkConnectedOverlayBackdropClass]="'cdk-overlay-transparent-backdrop'"
|
||||
(backdropClick)="isOpen = false"
|
||||
(detach)="close()"
|
||||
>
|
||||
<div class="tw-box-content">
|
||||
<div
|
||||
class="tw-bg-background tw-w-full tw-shadow-md tw-p-2 tw-rounded-md"
|
||||
data-testid="environment-selector-dialog"
|
||||
[@transformPanel]="'open'"
|
||||
cdkTrapFocus
|
||||
cdkTrapFocusAutoCapture
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
>
|
||||
<ng-container *ngFor="let region of availableRegions; let i = index">
|
||||
<button
|
||||
type="button"
|
||||
class="tw-text-main tw-w-full tw-text-left tw-py-0 tw-border tw-border-transparent tw-transition-all tw-duration-200 tw-ease-in-out tw-pr-2 tw-rounded-md"
|
||||
(click)="toggle(region.key)"
|
||||
[attr.aria-pressed]="data.selectedRegion === region ? 'true' : 'false'"
|
||||
[attr.data-testid]="'environment-selector-dialog-item-' + i"
|
||||
>
|
||||
<i
|
||||
class="bwi bwi-fw bwi-sm bwi-check"
|
||||
style="padding-bottom: 1px"
|
||||
aria-hidden="true"
|
||||
[style.visibility]="data.selectedRegion === region ? 'visible' : 'hidden'"
|
||||
></i>
|
||||
<span>{{ region.domain }}</span>
|
||||
</button>
|
||||
<br />
|
||||
</ng-container>
|
||||
<button
|
||||
type="button"
|
||||
class="tw-text-main tw-w-full tw-text-left tw-py-0 tw-pr-2 tw-border tw-border-transparent tw-transition-all tw-duration-200 tw-ease-in-out tw-rounded-md"
|
||||
(click)="toggle(ServerEnvironmentType.SelfHosted)"
|
||||
[attr.aria-pressed]="data.selectedRegion ? 'false' : 'true'"
|
||||
data-testid="environment-selector-dialog-item-self-hosted"
|
||||
>
|
||||
<i
|
||||
class="bwi bwi-fw bwi-sm bwi-check"
|
||||
style="padding-bottom: 1px"
|
||||
aria-hidden="true"
|
||||
[style.visibility]="data.selectedRegion ? 'hidden' : 'visible'"
|
||||
></i>
|
||||
<span>{{ "selfHostedServer" | i18n }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</ng-template>
|
||||
</ng-container>
|
||||
@@ -1,135 +0,0 @@
|
||||
import { animate, state, style, transition, trigger } from "@angular/animations";
|
||||
import { ConnectedPosition } from "@angular/cdk/overlay";
|
||||
import { Component, EventEmitter, Output, Input, OnInit, OnDestroy } from "@angular/core";
|
||||
import { ActivatedRoute } from "@angular/router";
|
||||
import { Observable, map, Subject, takeUntil } from "rxjs";
|
||||
|
||||
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop.
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { SelfHostedEnvConfigDialogComponent } from "@bitwarden/auth/angular";
|
||||
import {
|
||||
EnvironmentService,
|
||||
Region,
|
||||
RegionConfig,
|
||||
} from "@bitwarden/common/platform/abstractions/environment.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { DialogService, ToastService } from "@bitwarden/components";
|
||||
|
||||
export const ExtensionDefaultOverlayPosition: ConnectedPosition[] = [
|
||||
{
|
||||
originX: "start",
|
||||
originY: "top",
|
||||
overlayX: "start",
|
||||
overlayY: "bottom",
|
||||
},
|
||||
];
|
||||
export const DesktopDefaultOverlayPosition: ConnectedPosition[] = [
|
||||
{
|
||||
originX: "start",
|
||||
originY: "top",
|
||||
overlayX: "start",
|
||||
overlayY: "bottom",
|
||||
},
|
||||
];
|
||||
|
||||
export interface EnvironmentSelectorRouteData {
|
||||
overlayPosition?: ConnectedPosition[];
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: "environment-selector",
|
||||
templateUrl: "environment-selector.component.html",
|
||||
animations: [
|
||||
trigger("transformPanel", [
|
||||
state(
|
||||
"void",
|
||||
style({
|
||||
opacity: 0,
|
||||
}),
|
||||
),
|
||||
transition(
|
||||
"void => open",
|
||||
animate(
|
||||
"100ms linear",
|
||||
style({
|
||||
opacity: 1,
|
||||
}),
|
||||
),
|
||||
),
|
||||
transition("* => void", animate("100ms linear", style({ opacity: 0 }))),
|
||||
]),
|
||||
],
|
||||
standalone: false,
|
||||
})
|
||||
export class EnvironmentSelectorComponent implements OnInit, OnDestroy {
|
||||
@Output() onOpenSelfHostedSettings = new EventEmitter<void>();
|
||||
@Input() overlayPosition: ConnectedPosition[] = [
|
||||
{
|
||||
originX: "start",
|
||||
originY: "bottom",
|
||||
overlayX: "start",
|
||||
overlayY: "top",
|
||||
},
|
||||
];
|
||||
|
||||
protected isOpen = false;
|
||||
protected ServerEnvironmentType = Region;
|
||||
protected availableRegions = this.environmentService.availableRegions();
|
||||
protected selectedRegion$: Observable<RegionConfig | undefined> =
|
||||
this.environmentService.environment$.pipe(
|
||||
map((e) => e.getRegion()),
|
||||
map((r) => this.availableRegions.find((ar) => ar.key === r)),
|
||||
);
|
||||
|
||||
private destroy$ = new Subject<void>();
|
||||
|
||||
constructor(
|
||||
protected environmentService: EnvironmentService,
|
||||
private route: ActivatedRoute,
|
||||
private dialogService: DialogService,
|
||||
private toastService: ToastService,
|
||||
private i18nService: I18nService,
|
||||
) {}
|
||||
|
||||
ngOnInit() {
|
||||
this.route.data.pipe(takeUntil(this.destroy$)).subscribe((data) => {
|
||||
if (data && data["overlayPosition"]) {
|
||||
this.overlayPosition = data["overlayPosition"];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.destroy$.next();
|
||||
this.destroy$.complete();
|
||||
}
|
||||
|
||||
async toggle(option: Region) {
|
||||
this.isOpen = !this.isOpen;
|
||||
if (option === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the self-hosted settings dialog when the self-hosted option is selected.
|
||||
*/
|
||||
if (option === Region.SelfHosted) {
|
||||
const dialogResult = await SelfHostedEnvConfigDialogComponent.open(this.dialogService);
|
||||
if (dialogResult) {
|
||||
this.toastService.showToast({
|
||||
variant: "success",
|
||||
title: "",
|
||||
message: this.i18nService.t("environmentSaved"),
|
||||
});
|
||||
}
|
||||
// Don't proceed to setEnvironment when the self-hosted dialog is cancelled
|
||||
return;
|
||||
}
|
||||
|
||||
await this.environmentService.setEnvironment(option);
|
||||
}
|
||||
|
||||
close() {
|
||||
this.isOpen = false;
|
||||
}
|
||||
}
|
||||
@@ -1,300 +0,0 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Directive, OnInit } from "@angular/core";
|
||||
import { ActivatedRoute, Router } from "@angular/router";
|
||||
import { firstValueFrom, of } from "rxjs";
|
||||
import { filter, first, switchMap, tap } from "rxjs/operators";
|
||||
|
||||
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop.
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import {
|
||||
OrganizationUserApiService,
|
||||
OrganizationUserResetPasswordEnrollmentRequest,
|
||||
} from "@bitwarden/admin-console/common";
|
||||
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop.
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { InternalUserDecryptionOptionsServiceAbstraction } from "@bitwarden/auth/common";
|
||||
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction";
|
||||
import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction";
|
||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options";
|
||||
import { OrganizationAutoEnrollStatusResponse } from "@bitwarden/common/admin-console/models/response/organization-auto-enroll-status.response";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { MasterPasswordApiService } from "@bitwarden/common/auth/abstractions/master-password-api.service.abstraction";
|
||||
import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction";
|
||||
import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason";
|
||||
import { SetPasswordRequest } from "@bitwarden/common/auth/models/request/set-password.request";
|
||||
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction";
|
||||
import { KeysRequest } from "@bitwarden/common/models/request/keys.request";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { HashPurpose } from "@bitwarden/common/platform/enums";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { EncString } from "@bitwarden/common/platform/models/domain/enc-string";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { MasterKey, UserKey } from "@bitwarden/common/types/key";
|
||||
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
|
||||
import { DialogService, ToastService } from "@bitwarden/components";
|
||||
import { DEFAULT_KDF_CONFIG, KdfConfigService, KeyService } from "@bitwarden/key-management";
|
||||
|
||||
import { ChangePasswordComponent as BaseChangePasswordComponent } from "./change-password.component";
|
||||
|
||||
@Directive()
|
||||
export class SetPasswordComponent extends BaseChangePasswordComponent implements OnInit {
|
||||
syncLoading = true;
|
||||
showPassword = false;
|
||||
hint = "";
|
||||
orgSsoIdentifier: string = null;
|
||||
orgId: string;
|
||||
resetPasswordAutoEnroll = false;
|
||||
onSuccessfulChangePassword: () => Promise<void>;
|
||||
successRoute = "vault";
|
||||
activeUserId: UserId;
|
||||
|
||||
forceSetPasswordReason: ForceSetPasswordReason = ForceSetPasswordReason.None;
|
||||
ForceSetPasswordReason = ForceSetPasswordReason;
|
||||
|
||||
constructor(
|
||||
protected accountService: AccountService,
|
||||
protected dialogService: DialogService,
|
||||
protected encryptService: EncryptService,
|
||||
protected i18nService: I18nService,
|
||||
protected kdfConfigService: KdfConfigService,
|
||||
protected keyService: KeyService,
|
||||
protected masterPasswordApiService: MasterPasswordApiService,
|
||||
protected masterPasswordService: InternalMasterPasswordServiceAbstraction,
|
||||
protected messagingService: MessagingService,
|
||||
protected organizationApiService: OrganizationApiServiceAbstraction,
|
||||
protected organizationUserApiService: OrganizationUserApiService,
|
||||
protected platformUtilsService: PlatformUtilsService,
|
||||
protected policyApiService: PolicyApiServiceAbstraction,
|
||||
protected policyService: PolicyService,
|
||||
protected route: ActivatedRoute,
|
||||
protected router: Router,
|
||||
protected ssoLoginService: SsoLoginServiceAbstraction,
|
||||
protected syncService: SyncService,
|
||||
protected toastService: ToastService,
|
||||
protected userDecryptionOptionsService: InternalUserDecryptionOptionsServiceAbstraction,
|
||||
) {
|
||||
super(
|
||||
accountService,
|
||||
dialogService,
|
||||
i18nService,
|
||||
kdfConfigService,
|
||||
keyService,
|
||||
masterPasswordService,
|
||||
messagingService,
|
||||
platformUtilsService,
|
||||
policyService,
|
||||
toastService,
|
||||
);
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
super.ngOnInit();
|
||||
|
||||
await this.syncService.fullSync(true);
|
||||
this.syncLoading = false;
|
||||
|
||||
this.activeUserId = (await firstValueFrom(this.accountService.activeAccount$))?.id;
|
||||
|
||||
this.forceSetPasswordReason = await firstValueFrom(
|
||||
this.masterPasswordService.forceSetPasswordReason$(this.activeUserId),
|
||||
);
|
||||
|
||||
this.route.queryParams
|
||||
.pipe(
|
||||
first(),
|
||||
switchMap((qParams) => {
|
||||
if (qParams.identifier != null) {
|
||||
return of(qParams.identifier);
|
||||
} else {
|
||||
// Try to get orgSsoId from state as fallback
|
||||
// Note: this is primarily for the TDE user w/out MP obtains admin MP reset permission scenario.
|
||||
return this.ssoLoginService.getActiveUserOrganizationSsoIdentifier(this.activeUserId);
|
||||
}
|
||||
}),
|
||||
filter((orgSsoId) => orgSsoId != null),
|
||||
tap((orgSsoId: string) => {
|
||||
this.orgSsoIdentifier = orgSsoId;
|
||||
}),
|
||||
switchMap((orgSsoId: string) => this.organizationApiService.getAutoEnrollStatus(orgSsoId)),
|
||||
tap((orgAutoEnrollStatusResponse: OrganizationAutoEnrollStatusResponse) => {
|
||||
this.orgId = orgAutoEnrollStatusResponse.id;
|
||||
this.resetPasswordAutoEnroll = orgAutoEnrollStatusResponse.resetPasswordEnabled;
|
||||
}),
|
||||
switchMap((orgAutoEnrollStatusResponse: OrganizationAutoEnrollStatusResponse) =>
|
||||
// Must get org id from response to get master password policy options
|
||||
this.policyApiService.getMasterPasswordPolicyOptsForOrgUser(
|
||||
orgAutoEnrollStatusResponse.id,
|
||||
),
|
||||
),
|
||||
tap((masterPasswordPolicyOptions: MasterPasswordPolicyOptions) => {
|
||||
this.enforcedPolicyOptions = masterPasswordPolicyOptions;
|
||||
}),
|
||||
)
|
||||
.subscribe({
|
||||
error: () => {
|
||||
this.toastService.showToast({
|
||||
variant: "error",
|
||||
title: null,
|
||||
message: this.i18nService.t("errorOccurred"),
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async setupSubmitActions() {
|
||||
this.kdfConfig = DEFAULT_KDF_CONFIG;
|
||||
return true;
|
||||
}
|
||||
|
||||
async performSubmitActions(
|
||||
masterPasswordHash: string,
|
||||
masterKey: MasterKey,
|
||||
userKey: [UserKey, EncString],
|
||||
) {
|
||||
let keysRequest: KeysRequest | null = null;
|
||||
let newKeyPair: [string, EncString] | null = null;
|
||||
|
||||
if (
|
||||
this.forceSetPasswordReason !=
|
||||
ForceSetPasswordReason.TdeUserWithoutPasswordHasPasswordResetPermission
|
||||
) {
|
||||
// Existing JIT provisioned user in a MP encryption org setting first password
|
||||
// Users in this state will not already have a user asymmetric key pair so must create it for them
|
||||
// We don't want to re-create the user key pair if the user already has one (TDE user case)
|
||||
|
||||
// in case we have a local private key, and are not sure whether it has been posted to the server, we post the local private key instead of generating a new one
|
||||
const existingUserPrivateKey = (await firstValueFrom(
|
||||
this.keyService.userPrivateKey$(this.activeUserId),
|
||||
)) as Uint8Array;
|
||||
const existingUserPublicKey = await firstValueFrom(
|
||||
this.keyService.userPublicKey$(this.activeUserId),
|
||||
);
|
||||
if (existingUserPrivateKey != null && existingUserPublicKey != null) {
|
||||
const existingUserPublicKeyB64 = Utils.fromBufferToB64(existingUserPublicKey);
|
||||
newKeyPair = [
|
||||
existingUserPublicKeyB64,
|
||||
await this.encryptService.wrapDecapsulationKey(existingUserPrivateKey, userKey[0]),
|
||||
];
|
||||
} else {
|
||||
newKeyPair = await this.keyService.makeKeyPair(userKey[0]);
|
||||
}
|
||||
keysRequest = new KeysRequest(newKeyPair[0], newKeyPair[1].encryptedString);
|
||||
}
|
||||
|
||||
const request = new SetPasswordRequest(
|
||||
masterPasswordHash,
|
||||
userKey[1].encryptedString,
|
||||
this.hint,
|
||||
this.orgSsoIdentifier,
|
||||
keysRequest,
|
||||
this.kdfConfig.kdfType, //always PBKDF2 --> see this.setupSubmitActions
|
||||
this.kdfConfig.iterations,
|
||||
);
|
||||
try {
|
||||
if (this.resetPasswordAutoEnroll) {
|
||||
this.formPromise = this.masterPasswordApiService
|
||||
.setPassword(request)
|
||||
.then(async () => {
|
||||
await this.onSetPasswordSuccess(masterKey, userKey, newKeyPair);
|
||||
return this.organizationApiService.getKeys(this.orgId);
|
||||
})
|
||||
.then(async (response) => {
|
||||
if (response == null) {
|
||||
throw new Error(this.i18nService.t("resetPasswordOrgKeysError"));
|
||||
}
|
||||
const publicKey = Utils.fromB64ToArray(response.publicKey);
|
||||
|
||||
// RSA Encrypt user key with organization public key
|
||||
const userKey = await this.keyService.getUserKey();
|
||||
const encryptedUserKey = await this.encryptService.encapsulateKeyUnsigned(
|
||||
userKey,
|
||||
publicKey,
|
||||
);
|
||||
|
||||
const resetRequest = new OrganizationUserResetPasswordEnrollmentRequest();
|
||||
resetRequest.masterPasswordHash = masterPasswordHash;
|
||||
resetRequest.resetPasswordKey = encryptedUserKey.encryptedString;
|
||||
|
||||
return this.organizationUserApiService.putOrganizationUserResetPasswordEnrollment(
|
||||
this.orgId,
|
||||
this.activeUserId,
|
||||
resetRequest,
|
||||
);
|
||||
});
|
||||
} else {
|
||||
this.formPromise = this.masterPasswordApiService.setPassword(request).then(async () => {
|
||||
await this.onSetPasswordSuccess(masterKey, userKey, newKeyPair);
|
||||
});
|
||||
}
|
||||
|
||||
await this.formPromise;
|
||||
|
||||
if (this.onSuccessfulChangePassword != null) {
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.onSuccessfulChangePassword();
|
||||
} else {
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.router.navigate([this.successRoute]);
|
||||
}
|
||||
} catch {
|
||||
this.toastService.showToast({
|
||||
variant: "error",
|
||||
title: null,
|
||||
message: this.i18nService.t("errorOccurred"),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
togglePassword(confirmField: boolean) {
|
||||
this.showPassword = !this.showPassword;
|
||||
document.getElementById(confirmField ? "masterPasswordRetype" : "masterPassword").focus();
|
||||
}
|
||||
|
||||
protected async onSetPasswordSuccess(
|
||||
masterKey: MasterKey,
|
||||
userKey: [UserKey, EncString],
|
||||
keyPair: [string, EncString] | null,
|
||||
) {
|
||||
// Clear force set password reason to allow navigation back to vault.
|
||||
await this.masterPasswordService.setForceSetPasswordReason(
|
||||
ForceSetPasswordReason.None,
|
||||
this.activeUserId,
|
||||
);
|
||||
|
||||
// User now has a password so update account decryption options in state
|
||||
const userDecryptionOpts = await firstValueFrom(
|
||||
this.userDecryptionOptionsService.userDecryptionOptions$,
|
||||
);
|
||||
userDecryptionOpts.hasMasterPassword = true;
|
||||
await this.userDecryptionOptionsService.setUserDecryptionOptions(userDecryptionOpts);
|
||||
await this.kdfConfigService.setKdfConfig(this.activeUserId, this.kdfConfig);
|
||||
await this.masterPasswordService.setMasterKey(masterKey, this.activeUserId);
|
||||
await this.keyService.setUserKey(userKey[0], this.activeUserId);
|
||||
|
||||
// Set private key only for new JIT provisioned users in MP encryption orgs
|
||||
// Existing TDE users will have private key set on sync or on login
|
||||
if (
|
||||
keyPair !== null &&
|
||||
this.forceSetPasswordReason !=
|
||||
ForceSetPasswordReason.TdeUserWithoutPasswordHasPasswordResetPermission
|
||||
) {
|
||||
await this.keyService.setPrivateKey(keyPair[1].encryptedString, this.activeUserId);
|
||||
}
|
||||
|
||||
const localMasterKeyHash = await this.keyService.hashMasterKey(
|
||||
this.masterPassword,
|
||||
masterKey,
|
||||
HashPurpose.LocalAuthorization,
|
||||
);
|
||||
await this.masterPasswordService.setMasterKeyHash(localMasterKeyHash, this.activeUserId);
|
||||
}
|
||||
}
|
||||
@@ -4,11 +4,9 @@ import { Directive, OnInit } from "@angular/core";
|
||||
import { FormBuilder, Validators } from "@angular/forms";
|
||||
import { firstValueFrom } from "rxjs";
|
||||
|
||||
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop.
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { PinServiceAbstraction } from "@bitwarden/auth/common";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
|
||||
import { PinServiceAbstraction } from "@bitwarden/common/key-management/pin/pin.service.abstraction";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { DialogRef } from "@bitwarden/components";
|
||||
import { KeyService } from "@bitwarden/key-management";
|
||||
|
||||
@@ -1,12 +1,7 @@
|
||||
<ng-container>
|
||||
<!-- TOTP Authenticator -->
|
||||
<bit-icon [icon]="Icons.TOTPIcon" *ngIf="provider == 0"></bit-icon>
|
||||
<!-- Email -->
|
||||
<bit-icon [icon]="Icons.EmailIcon" *ngIf="provider == 1"></bit-icon>
|
||||
<!-- Webauthn -->
|
||||
<bit-icon [icon]="Icons.WebAuthnIcon" *ngIf="provider == 7"></bit-icon>
|
||||
<!-- Recovery Code -->
|
||||
<bit-icon [icon]="Icons.RecoveryCodeIcon" *ngIf="provider == 'rc'"></bit-icon>
|
||||
<div class="tw-size-[70px] tw-content-center" *ngIf="!!IconProviderMap[provider]">
|
||||
<bit-icon [icon]="IconProviderMap[provider]"></bit-icon>
|
||||
</div>
|
||||
<!-- Other 2FA Types (Duo, Yubico, U2F as PNG) -->
|
||||
<img
|
||||
[class]="'mfaType' + provider"
|
||||
|
||||
@@ -2,10 +2,12 @@
|
||||
// @ts-strict-ignore
|
||||
import { Component, Input } from "@angular/core";
|
||||
|
||||
import { EmailIcon } from "../icons/email.icon";
|
||||
import { RecoveryCodeIcon } from "../icons/recovery.icon";
|
||||
import { TOTPIcon } from "../icons/totp.icon";
|
||||
import { WebAuthnIcon } from "../icons/webauthn.icon";
|
||||
import {
|
||||
Icon,
|
||||
TwoFactorAuthAuthenticatorIcon,
|
||||
TwoFactorAuthEmailIcon,
|
||||
TwoFactorAuthWebAuthnIcon,
|
||||
} from "@bitwarden/assets/svg";
|
||||
|
||||
@Component({
|
||||
selector: "auth-two-factor-icon",
|
||||
@@ -16,11 +18,10 @@ export class TwoFactorIconComponent {
|
||||
@Input() provider: any;
|
||||
@Input() name: string;
|
||||
|
||||
protected readonly Icons = {
|
||||
TOTPIcon,
|
||||
EmailIcon,
|
||||
WebAuthnIcon,
|
||||
RecoveryCodeIcon,
|
||||
protected readonly IconProviderMap: { [key: number | string]: Icon } = {
|
||||
0: TwoFactorAuthAuthenticatorIcon,
|
||||
1: TwoFactorAuthEmailIcon,
|
||||
7: TwoFactorAuthWebAuthnIcon,
|
||||
};
|
||||
|
||||
constructor() {}
|
||||
|
||||
@@ -1,141 +0,0 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Directive } from "@angular/core";
|
||||
import { Router } from "@angular/router";
|
||||
import { firstValueFrom } from "rxjs";
|
||||
|
||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { MasterPasswordApiService } from "@bitwarden/common/auth/abstractions/master-password-api.service.abstraction";
|
||||
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
|
||||
import { VerificationType } from "@bitwarden/common/auth/enums/verification-type";
|
||||
import { PasswordRequest } from "@bitwarden/common/auth/models/request/password.request";
|
||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import { Verification } from "@bitwarden/common/auth/types/verification";
|
||||
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { EncString } from "@bitwarden/common/platform/models/domain/enc-string";
|
||||
import { MasterKey, UserKey } from "@bitwarden/common/types/key";
|
||||
import { DialogService, ToastService } from "@bitwarden/components";
|
||||
import { KdfConfigService, KeyService } from "@bitwarden/key-management";
|
||||
|
||||
import { ChangePasswordComponent as BaseChangePasswordComponent } from "./change-password.component";
|
||||
|
||||
@Directive()
|
||||
export class UpdatePasswordComponent extends BaseChangePasswordComponent {
|
||||
hint: string;
|
||||
key: string;
|
||||
enforcedPolicyOptions: MasterPasswordPolicyOptions;
|
||||
showPassword = false;
|
||||
currentMasterPassword: string;
|
||||
|
||||
onSuccessfulChangePassword: () => Promise<void>;
|
||||
|
||||
constructor(
|
||||
protected router: Router,
|
||||
i18nService: I18nService,
|
||||
platformUtilsService: PlatformUtilsService,
|
||||
policyService: PolicyService,
|
||||
keyService: KeyService,
|
||||
messagingService: MessagingService,
|
||||
private masterPasswordApiService: MasterPasswordApiService,
|
||||
private userVerificationService: UserVerificationService,
|
||||
private logService: LogService,
|
||||
dialogService: DialogService,
|
||||
kdfConfigService: KdfConfigService,
|
||||
masterPasswordService: InternalMasterPasswordServiceAbstraction,
|
||||
accountService: AccountService,
|
||||
toastService: ToastService,
|
||||
) {
|
||||
super(
|
||||
accountService,
|
||||
dialogService,
|
||||
i18nService,
|
||||
kdfConfigService,
|
||||
keyService,
|
||||
masterPasswordService,
|
||||
messagingService,
|
||||
platformUtilsService,
|
||||
policyService,
|
||||
toastService,
|
||||
);
|
||||
}
|
||||
|
||||
togglePassword(confirmField: boolean) {
|
||||
this.showPassword = !this.showPassword;
|
||||
document.getElementById(confirmField ? "masterPasswordRetype" : "masterPassword").focus();
|
||||
}
|
||||
|
||||
async cancel() {
|
||||
await this.router.navigate(["/vault"]);
|
||||
}
|
||||
|
||||
async setupSubmitActions(): Promise<boolean> {
|
||||
if (this.currentMasterPassword == null || this.currentMasterPassword === "") {
|
||||
this.toastService.showToast({
|
||||
variant: "error",
|
||||
title: this.i18nService.t("errorOccurred"),
|
||||
message: this.i18nService.t("masterPasswordRequired"),
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
const secret: Verification = {
|
||||
type: VerificationType.MasterPassword,
|
||||
secret: this.currentMasterPassword,
|
||||
};
|
||||
try {
|
||||
await this.userVerificationService.verifyUser(secret);
|
||||
} catch (e) {
|
||||
this.toastService.showToast({
|
||||
variant: "error",
|
||||
title: this.i18nService.t("errorOccurred"),
|
||||
message: e.message,
|
||||
});
|
||||
return false;
|
||||
}
|
||||
const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$));
|
||||
this.kdfConfig = await this.kdfConfigService.getKdfConfig(userId);
|
||||
return true;
|
||||
}
|
||||
|
||||
async performSubmitActions(
|
||||
newMasterKeyHash: string,
|
||||
newMasterKey: MasterKey,
|
||||
newUserKey: [UserKey, EncString],
|
||||
) {
|
||||
try {
|
||||
// Create Request
|
||||
const request = new PasswordRequest();
|
||||
request.masterPasswordHash = await this.keyService.hashMasterKey(
|
||||
this.currentMasterPassword,
|
||||
await this.keyService.getOrDeriveMasterKey(this.currentMasterPassword),
|
||||
);
|
||||
request.newMasterPasswordHash = newMasterKeyHash;
|
||||
request.key = newUserKey[1].encryptedString;
|
||||
|
||||
// Update user's password
|
||||
await this.masterPasswordApiService.postPassword(request);
|
||||
|
||||
this.toastService.showToast({
|
||||
variant: "success",
|
||||
title: this.i18nService.t("masterPasswordChanged"),
|
||||
message: this.i18nService.t("logBackIn"),
|
||||
});
|
||||
|
||||
if (this.onSuccessfulChangePassword != null) {
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.onSuccessfulChangePassword();
|
||||
} else {
|
||||
this.messagingService.send("logout");
|
||||
}
|
||||
} catch (e) {
|
||||
this.logService.error(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,232 +0,0 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Directive, OnInit } from "@angular/core";
|
||||
import { Router } from "@angular/router";
|
||||
import { firstValueFrom, map } from "rxjs";
|
||||
|
||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { MasterPasswordApiService } from "@bitwarden/common/auth/abstractions/master-password-api.service.abstraction";
|
||||
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
|
||||
import { VerificationType } from "@bitwarden/common/auth/enums/verification-type";
|
||||
import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason";
|
||||
import { PasswordRequest } from "@bitwarden/common/auth/models/request/password.request";
|
||||
import { UpdateTdeOffboardingPasswordRequest } from "@bitwarden/common/auth/models/request/update-tde-offboarding-password.request";
|
||||
import { UpdateTempPasswordRequest } from "@bitwarden/common/auth/models/request/update-temp-password.request";
|
||||
import { MasterPasswordVerification } from "@bitwarden/common/auth/types/verification";
|
||||
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { EncString } from "@bitwarden/common/platform/models/domain/enc-string";
|
||||
import { MasterKey, UserKey } from "@bitwarden/common/types/key";
|
||||
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
|
||||
import { DialogService, ToastService } from "@bitwarden/components";
|
||||
import { KdfConfigService, KeyService } from "@bitwarden/key-management";
|
||||
|
||||
import { ChangePasswordComponent as BaseChangePasswordComponent } from "./change-password.component";
|
||||
|
||||
@Directive()
|
||||
export class UpdateTempPasswordComponent extends BaseChangePasswordComponent implements OnInit {
|
||||
hint: string;
|
||||
key: string;
|
||||
enforcedPolicyOptions: MasterPasswordPolicyOptions;
|
||||
showPassword = false;
|
||||
reason: ForceSetPasswordReason = ForceSetPasswordReason.None;
|
||||
verification: MasterPasswordVerification = {
|
||||
type: VerificationType.MasterPassword,
|
||||
secret: "",
|
||||
};
|
||||
|
||||
onSuccessfulChangePassword: () => Promise<any>;
|
||||
|
||||
get requireCurrentPassword(): boolean {
|
||||
return this.reason === ForceSetPasswordReason.WeakMasterPassword;
|
||||
}
|
||||
|
||||
constructor(
|
||||
i18nService: I18nService,
|
||||
platformUtilsService: PlatformUtilsService,
|
||||
policyService: PolicyService,
|
||||
keyService: KeyService,
|
||||
messagingService: MessagingService,
|
||||
private masterPasswordApiService: MasterPasswordApiService,
|
||||
private syncService: SyncService,
|
||||
private logService: LogService,
|
||||
private userVerificationService: UserVerificationService,
|
||||
protected router: Router,
|
||||
dialogService: DialogService,
|
||||
kdfConfigService: KdfConfigService,
|
||||
accountService: AccountService,
|
||||
masterPasswordService: InternalMasterPasswordServiceAbstraction,
|
||||
toastService: ToastService,
|
||||
) {
|
||||
super(
|
||||
accountService,
|
||||
dialogService,
|
||||
i18nService,
|
||||
kdfConfigService,
|
||||
keyService,
|
||||
masterPasswordService,
|
||||
messagingService,
|
||||
platformUtilsService,
|
||||
policyService,
|
||||
toastService,
|
||||
);
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
await this.syncService.fullSync(true);
|
||||
|
||||
const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id;
|
||||
this.reason = await firstValueFrom(this.masterPasswordService.forceSetPasswordReason$(userId));
|
||||
|
||||
// If we somehow end up here without a reason, go back to the home page
|
||||
if (this.reason == ForceSetPasswordReason.None) {
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.router.navigate(["/"]);
|
||||
return;
|
||||
}
|
||||
|
||||
await super.ngOnInit();
|
||||
}
|
||||
|
||||
get masterPasswordWarningText(): string {
|
||||
if (this.reason == ForceSetPasswordReason.WeakMasterPassword) {
|
||||
return this.i18nService.t("updateWeakMasterPasswordWarning");
|
||||
} else if (this.reason == ForceSetPasswordReason.TdeOffboarding) {
|
||||
return this.i18nService.t("tdeDisabledMasterPasswordRequired");
|
||||
} else {
|
||||
return this.i18nService.t("updateMasterPasswordWarning");
|
||||
}
|
||||
}
|
||||
|
||||
togglePassword(confirmField: boolean) {
|
||||
this.showPassword = !this.showPassword;
|
||||
document.getElementById(confirmField ? "masterPasswordRetype" : "masterPassword").focus();
|
||||
}
|
||||
|
||||
async setupSubmitActions(): Promise<boolean> {
|
||||
const [userId, email] = await firstValueFrom(
|
||||
this.accountService.activeAccount$.pipe(map((a) => [a?.id, a?.email])),
|
||||
);
|
||||
this.email = email;
|
||||
this.kdfConfig = await this.kdfConfigService.getKdfConfig(userId);
|
||||
return true;
|
||||
}
|
||||
|
||||
async submit() {
|
||||
// Validation
|
||||
if (!(await this.strongPassword())) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!(await this.setupSubmitActions())) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Create new key and hash new password
|
||||
const newMasterKey = await this.keyService.makeMasterKey(
|
||||
this.masterPassword,
|
||||
this.email.trim().toLowerCase(),
|
||||
this.kdfConfig,
|
||||
);
|
||||
const newPasswordHash = await this.keyService.hashMasterKey(
|
||||
this.masterPassword,
|
||||
newMasterKey,
|
||||
);
|
||||
|
||||
// Grab user key
|
||||
const userKey = await this.keyService.getUserKey();
|
||||
|
||||
// Encrypt user key with new master key
|
||||
const newProtectedUserKey = await this.keyService.encryptUserKeyWithMasterKey(
|
||||
newMasterKey,
|
||||
userKey,
|
||||
);
|
||||
|
||||
await this.performSubmitActions(newPasswordHash, newMasterKey, newProtectedUserKey);
|
||||
} catch (e) {
|
||||
this.logService.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
async performSubmitActions(
|
||||
masterPasswordHash: string,
|
||||
masterKey: MasterKey,
|
||||
userKey: [UserKey, EncString],
|
||||
) {
|
||||
try {
|
||||
switch (this.reason) {
|
||||
case ForceSetPasswordReason.AdminForcePasswordReset:
|
||||
this.formPromise = this.updateTempPassword(masterPasswordHash, userKey);
|
||||
break;
|
||||
case ForceSetPasswordReason.WeakMasterPassword:
|
||||
this.formPromise = this.updatePassword(masterPasswordHash, userKey);
|
||||
break;
|
||||
case ForceSetPasswordReason.TdeOffboarding:
|
||||
this.formPromise = this.updateTdeOffboardingPassword(masterPasswordHash, userKey);
|
||||
break;
|
||||
}
|
||||
|
||||
await this.formPromise;
|
||||
this.toastService.showToast({
|
||||
variant: "success",
|
||||
title: null,
|
||||
message: this.i18nService.t("updatedMasterPassword"),
|
||||
});
|
||||
|
||||
const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id;
|
||||
await this.masterPasswordService.setForceSetPasswordReason(
|
||||
ForceSetPasswordReason.None,
|
||||
userId,
|
||||
);
|
||||
|
||||
if (this.onSuccessfulChangePassword != null) {
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.onSuccessfulChangePassword();
|
||||
} else {
|
||||
this.messagingService.send("logout");
|
||||
}
|
||||
} catch (e) {
|
||||
this.logService.error(e);
|
||||
}
|
||||
}
|
||||
private async updateTempPassword(masterPasswordHash: string, userKey: [UserKey, EncString]) {
|
||||
const request = new UpdateTempPasswordRequest();
|
||||
request.key = userKey[1].encryptedString;
|
||||
request.newMasterPasswordHash = masterPasswordHash;
|
||||
request.masterPasswordHint = this.hint;
|
||||
|
||||
return this.masterPasswordApiService.putUpdateTempPassword(request);
|
||||
}
|
||||
|
||||
private async updatePassword(newMasterPasswordHash: string, userKey: [UserKey, EncString]) {
|
||||
const request = await this.userVerificationService.buildRequest(
|
||||
this.verification,
|
||||
PasswordRequest,
|
||||
);
|
||||
request.masterPasswordHint = this.hint;
|
||||
request.newMasterPasswordHash = newMasterPasswordHash;
|
||||
request.key = userKey[1].encryptedString;
|
||||
|
||||
return this.masterPasswordApiService.postPassword(request);
|
||||
}
|
||||
|
||||
private async updateTdeOffboardingPassword(
|
||||
masterPasswordHash: string,
|
||||
userKey: [UserKey, EncString],
|
||||
) {
|
||||
const request = new UpdateTdeOffboardingPasswordRequest();
|
||||
request.key = userKey[1].encryptedString;
|
||||
request.newMasterPasswordHash = masterPasswordHash;
|
||||
request.masterPasswordHint = this.hint;
|
||||
|
||||
return this.masterPasswordApiService.putUpdateTdeOffboardingPassword(request);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import { DeviceManagementComponentServiceAbstraction } from "./device-management-component.service.abstraction";
|
||||
|
||||
/**
|
||||
* Default implementation of the device management component service
|
||||
*/
|
||||
export class DefaultDeviceManagementComponentService
|
||||
implements DeviceManagementComponentServiceAbstraction
|
||||
{
|
||||
/**
|
||||
* Show header information in web client
|
||||
*/
|
||||
showHeaderInformation(): boolean {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
/**
|
||||
* Service abstraction for device management component
|
||||
* Used to determine client-specific behavior
|
||||
*/
|
||||
export abstract class DeviceManagementComponentServiceAbstraction {
|
||||
/**
|
||||
* Whether to show header information (title, description, etc.) in the device management component
|
||||
* @returns true if header information should be shown, false otherwise
|
||||
*/
|
||||
abstract showHeaderInformation(): boolean;
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
<bit-item-group>
|
||||
<bit-item *ngFor="let device of devices">
|
||||
@if (device.pendingAuthRequest) {
|
||||
<button
|
||||
class="tw-relative"
|
||||
bit-item-content
|
||||
type="button"
|
||||
[attr.tabindex]="device.pendingAuthRequest != null ? 0 : null"
|
||||
(click)="answerAuthRequest(device.pendingAuthRequest)"
|
||||
>
|
||||
<!-- Default Content -->
|
||||
<span class="tw-text-base">{{ device.displayName }}</span>
|
||||
|
||||
<!-- Default Trailing Content -->
|
||||
<span class="tw-absolute tw-top-[6px] tw-right-3" slot="default-trailing">
|
||||
<span bitBadge variant="warning">
|
||||
{{ "requestPending" | i18n }}
|
||||
</span>
|
||||
</span>
|
||||
|
||||
<!-- Secondary Content -->
|
||||
<span slot="secondary" class="tw-text-sm">
|
||||
<br />
|
||||
<div>
|
||||
<span class="tw-font-semibold"> {{ "firstLogin" | i18n }}: </span>
|
||||
<span>{{ device.firstLogin | date: "medium" }}</span>
|
||||
</div>
|
||||
</span>
|
||||
</button>
|
||||
} @else {
|
||||
<bit-item-content ngClass="tw-relative">
|
||||
<!-- Default Content -->
|
||||
<span class="tw-text-base">{{ device.displayName }}</span>
|
||||
|
||||
<!-- Default Trailing Content -->
|
||||
<div
|
||||
*ngIf="device.isCurrentDevice"
|
||||
class="tw-absolute tw-top-[6px] tw-right-3"
|
||||
slot="default-trailing"
|
||||
>
|
||||
<span bitBadge variant="primary">
|
||||
{{ "currentSession" | i18n }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Secondary Content -->
|
||||
<div slot="secondary" class="tw-text-sm">
|
||||
@if (device.isTrusted) {
|
||||
<span>{{ "trusted" | i18n }}</span>
|
||||
} @else {
|
||||
<br />
|
||||
}
|
||||
|
||||
<div>
|
||||
<span class="tw-font-semibold">{{ "firstLogin" | i18n }}: </span>
|
||||
<span>{{ device.firstLogin | date: "medium" }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</bit-item-content>
|
||||
}
|
||||
</bit-item>
|
||||
</bit-item-group>
|
||||
@@ -0,0 +1,27 @@
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { Component, EventEmitter, Input, Output } from "@angular/core";
|
||||
|
||||
import { DevicePendingAuthRequest } from "@bitwarden/common/auth/abstractions/devices/responses/device.response";
|
||||
import { BadgeModule, ItemModule } from "@bitwarden/components";
|
||||
import { I18nPipe } from "@bitwarden/ui-common";
|
||||
|
||||
import { DeviceDisplayData } from "./device-management.component";
|
||||
|
||||
/** Displays user devices in an item list view */
|
||||
@Component({
|
||||
standalone: true,
|
||||
selector: "auth-device-management-item-group",
|
||||
templateUrl: "./device-management-item-group.component.html",
|
||||
imports: [BadgeModule, CommonModule, ItemModule, I18nPipe],
|
||||
})
|
||||
export class DeviceManagementItemGroupComponent {
|
||||
@Input() devices: DeviceDisplayData[] = [];
|
||||
@Output() onAuthRequestAnswered = new EventEmitter<DevicePendingAuthRequest>();
|
||||
|
||||
protected answerAuthRequest(pendingAuthRequest: DevicePendingAuthRequest | null) {
|
||||
if (pendingAuthRequest == null) {
|
||||
return;
|
||||
}
|
||||
this.onAuthRequestAnswered.emit(pendingAuthRequest);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
<bit-table-scroll [dataSource]="tableDataSource" [rowSize]="64">
|
||||
<!-- Table Header -->
|
||||
<ng-container header>
|
||||
<th
|
||||
*ngFor="let column of columnConfig"
|
||||
[class]="column.headerClass"
|
||||
bitCell
|
||||
[bitSortable]="column.sortable ? column.name : ''"
|
||||
scope="col"
|
||||
role="columnheader"
|
||||
>
|
||||
{{ column.title }}
|
||||
</th>
|
||||
</ng-container>
|
||||
|
||||
<!-- Table Rows -->
|
||||
<ng-template bitRowDef let-device>
|
||||
<!-- Column: Device Name -->
|
||||
<td bitCell class="tw-flex tw-gap-2 tw-items-center tw-h-16">
|
||||
<div class="tw-flex tw-items-center tw-justify-center tw-w-10">
|
||||
<i [class]="device.icon" class="bwi-lg" aria-hidden="true"></i>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
@if (device.pendingAuthRequest) {
|
||||
<a bitLink href="#" appStopClick (click)="answerAuthRequest(device.pendingAuthRequest)">
|
||||
{{ device.displayName }}
|
||||
</a>
|
||||
<br />
|
||||
} @else {
|
||||
<span>{{ device.displayName }}</span>
|
||||
<div *ngIf="device.isTrusted" class="tw-text-sm tw-text-muted">
|
||||
{{ "trusted" | i18n }}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</td>
|
||||
|
||||
<!-- Column: Login Status -->
|
||||
<td bitCell>
|
||||
<div class="tw-flex tw-gap-1">
|
||||
<span *ngIf="device.isCurrentDevice" bitBadge variant="primary">
|
||||
{{ "currentSession" | i18n }}
|
||||
</span>
|
||||
<span *ngIf="device.pendingAuthRequest" bitBadge variant="warning">
|
||||
{{ "requestPending" | i18n }}
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
<!-- Column: First Login -->
|
||||
<td bitCell>{{ device.firstLogin | date: "medium" }}</td>
|
||||
</ng-template>
|
||||
</bit-table-scroll>
|
||||
@@ -0,0 +1,65 @@
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { Component, EventEmitter, Input, OnChanges, Output, SimpleChanges } from "@angular/core";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { DevicePendingAuthRequest } from "@bitwarden/common/auth/abstractions/devices/responses/device.response";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import {
|
||||
BadgeModule,
|
||||
ButtonModule,
|
||||
LinkModule,
|
||||
TableDataSource,
|
||||
TableModule,
|
||||
} from "@bitwarden/components";
|
||||
|
||||
import { DeviceDisplayData } from "./device-management.component";
|
||||
|
||||
/** Displays user devices in a sortable table view */
|
||||
@Component({
|
||||
standalone: true,
|
||||
selector: "auth-device-management-table",
|
||||
templateUrl: "./device-management-table.component.html",
|
||||
imports: [BadgeModule, ButtonModule, CommonModule, JslibModule, LinkModule, TableModule],
|
||||
})
|
||||
export class DeviceManagementTableComponent implements OnChanges {
|
||||
@Input() devices: DeviceDisplayData[] = [];
|
||||
@Output() onAuthRequestAnswered = new EventEmitter<DevicePendingAuthRequest>();
|
||||
|
||||
protected tableDataSource = new TableDataSource<DeviceDisplayData>();
|
||||
|
||||
protected readonly columnConfig = [
|
||||
{
|
||||
name: "displayName",
|
||||
title: this.i18nService.t("device"),
|
||||
headerClass: "tw-w-1/3",
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
name: "loginStatus",
|
||||
title: this.i18nService.t("loginStatus"),
|
||||
headerClass: "tw-w-1/3",
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
name: "firstLogin",
|
||||
title: this.i18nService.t("firstLogin"),
|
||||
headerClass: "tw-w-1/3",
|
||||
sortable: true,
|
||||
},
|
||||
];
|
||||
|
||||
constructor(private i18nService: I18nService) {}
|
||||
|
||||
ngOnChanges(changes: SimpleChanges): void {
|
||||
if (changes.devices) {
|
||||
this.tableDataSource.data = this.devices;
|
||||
}
|
||||
}
|
||||
|
||||
protected answerAuthRequest(pendingAuthRequest: DevicePendingAuthRequest | null) {
|
||||
if (pendingAuthRequest == null) {
|
||||
return;
|
||||
}
|
||||
this.onAuthRequestAnswered.emit(pendingAuthRequest);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
<div *ngIf="showHeaderInfo" class="tw-mt-6 tw-mb-2 tw-pb-2.5">
|
||||
<div class="tw-flex tw-items-center tw-gap-2 tw-mb-5">
|
||||
<h1 class="tw-m-0">{{ "devices" | i18n }}</h1>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="tw-border-none tw-bg-transparent tw-text-primary-600 tw-flex tw-items-center tw-size-4"
|
||||
[bitPopoverTriggerFor]="infoPopover"
|
||||
position="right-start"
|
||||
>
|
||||
<i class="bwi bwi-question-circle" aria-hidden="true"></i>
|
||||
</button>
|
||||
|
||||
<bit-popover [title]="'whatIsADevice' | i18n" #infoPopover>
|
||||
<p class="tw-mb-0">{{ "aDeviceIs" | i18n }}</p>
|
||||
</bit-popover>
|
||||
</div>
|
||||
|
||||
<p>
|
||||
{{ "deviceListDescriptionTemp" | i18n }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@if (initializing) {
|
||||
<div class="tw-flex tw-justify-center tw-items-center tw-p-4">
|
||||
<i class="bwi bwi-spinner bwi-spin tw-text-2xl" aria-hidden="true"></i>
|
||||
</div>
|
||||
} @else {
|
||||
<!-- Table View: displays on medium to large screens -->
|
||||
<auth-device-management-table
|
||||
ngClass="tw-hidden md:tw-block"
|
||||
[devices]="devices"
|
||||
(onAuthRequestAnswered)="handleAuthRequestAnswered($event)"
|
||||
></auth-device-management-table>
|
||||
|
||||
<!-- List View: displays on small screens -->
|
||||
<auth-device-management-item-group
|
||||
ngClass="md:tw-hidden"
|
||||
[devices]="devices"
|
||||
(onAuthRequestAnswered)="handleAuthRequestAnswered($event)"
|
||||
></auth-device-management-item-group>
|
||||
}
|
||||
@@ -0,0 +1,264 @@
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { Component, DestroyRef, OnInit } from "@angular/core";
|
||||
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
|
||||
import { firstValueFrom } from "rxjs";
|
||||
|
||||
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop.
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { AuthRequestApiServiceAbstraction } from "@bitwarden/auth/common";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { DevicesServiceAbstraction } from "@bitwarden/common/auth/abstractions/devices/devices.service.abstraction";
|
||||
import {
|
||||
DevicePendingAuthRequest,
|
||||
DeviceResponse,
|
||||
} from "@bitwarden/common/auth/abstractions/devices/responses/device.response";
|
||||
import { DeviceView } from "@bitwarden/common/auth/abstractions/devices/views/device.view";
|
||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import { PendingAuthRequestsStateService } from "@bitwarden/common/auth/services/auth-request-answering/pending-auth-requests.state";
|
||||
import { DeviceType, DeviceTypeMetadata } from "@bitwarden/common/enums";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service";
|
||||
import { MessageListener } from "@bitwarden/common/platform/messaging";
|
||||
import { ButtonModule, DialogService, PopoverModule } from "@bitwarden/components";
|
||||
import { I18nPipe } from "@bitwarden/ui-common";
|
||||
|
||||
import { LoginApprovalDialogComponent } from "../login-approval";
|
||||
|
||||
import { DeviceManagementComponentServiceAbstraction } from "./device-management-component.service.abstraction";
|
||||
import { DeviceManagementItemGroupComponent } from "./device-management-item-group.component";
|
||||
import { DeviceManagementTableComponent } from "./device-management-table.component";
|
||||
import { clearAuthRequestAndResortDevices, resortDevices } from "./resort-devices.helper";
|
||||
|
||||
export interface DeviceDisplayData {
|
||||
creationDate: string;
|
||||
displayName: string;
|
||||
firstLogin: Date;
|
||||
icon: string;
|
||||
id: string;
|
||||
identifier: string;
|
||||
isCurrentDevice: boolean;
|
||||
isTrusted: boolean;
|
||||
loginStatus: string;
|
||||
pendingAuthRequest: DevicePendingAuthRequest | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* The `DeviceManagementComponent` fetches user devices and passes them down
|
||||
* to a child component for display.
|
||||
*
|
||||
* The specific child component that gets displayed depends on the viewport width:
|
||||
* - Medium to Large screens = `bit-table` view
|
||||
* - Small screens = `bit-item-group` view
|
||||
*/
|
||||
@Component({
|
||||
standalone: true,
|
||||
selector: "auth-device-management",
|
||||
templateUrl: "./device-management.component.html",
|
||||
imports: [
|
||||
ButtonModule,
|
||||
CommonModule,
|
||||
DeviceManagementItemGroupComponent,
|
||||
DeviceManagementTableComponent,
|
||||
I18nPipe,
|
||||
PopoverModule,
|
||||
],
|
||||
})
|
||||
export class DeviceManagementComponent implements OnInit {
|
||||
protected devices: DeviceDisplayData[] = [];
|
||||
protected initializing = true;
|
||||
protected showHeaderInfo = false;
|
||||
|
||||
constructor(
|
||||
private readonly accountService: AccountService,
|
||||
private readonly authRequestApiService: AuthRequestApiServiceAbstraction,
|
||||
private readonly destroyRef: DestroyRef,
|
||||
private readonly deviceManagementComponentService: DeviceManagementComponentServiceAbstraction,
|
||||
private readonly devicesService: DevicesServiceAbstraction,
|
||||
private readonly dialogService: DialogService,
|
||||
private readonly i18nService: I18nService,
|
||||
private readonly messageListener: MessageListener,
|
||||
private readonly pendingAuthRequestStateService: PendingAuthRequestsStateService,
|
||||
private readonly validationService: ValidationService,
|
||||
) {
|
||||
this.showHeaderInfo = this.deviceManagementComponentService.showHeaderInformation();
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
await this.loadDevices();
|
||||
|
||||
this.messageListener.allMessages$
|
||||
.pipe(takeUntilDestroyed(this.destroyRef))
|
||||
.subscribe((message) => {
|
||||
if (
|
||||
message.command === "openLoginApproval" &&
|
||||
message.notificationId &&
|
||||
typeof message.notificationId === "string"
|
||||
) {
|
||||
void this.upsertDeviceWithPendingAuthRequest(message.notificationId);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async loadDevices() {
|
||||
try {
|
||||
const devices = await firstValueFrom(this.devicesService.getDevices$());
|
||||
const currentDevice = await firstValueFrom(this.devicesService.getCurrentDevice$());
|
||||
|
||||
if (!devices || !currentDevice) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.devices = this.mapDevicesToDisplayData(devices, currentDevice);
|
||||
} catch (e) {
|
||||
this.validationService.showError(e);
|
||||
} finally {
|
||||
this.initializing = false;
|
||||
}
|
||||
}
|
||||
|
||||
private mapDevicesToDisplayData(
|
||||
devices: DeviceView[],
|
||||
currentDevice: DeviceResponse,
|
||||
): DeviceDisplayData[] {
|
||||
return devices
|
||||
.map((device): DeviceDisplayData | null => {
|
||||
if (!device.id) {
|
||||
this.validationService.showError(new Error(this.i18nService.t("deviceIdMissing")));
|
||||
return null;
|
||||
}
|
||||
|
||||
if (device.type == undefined) {
|
||||
this.validationService.showError(new Error(this.i18nService.t("deviceTypeMissing")));
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!device.creationDate) {
|
||||
this.validationService.showError(
|
||||
new Error(this.i18nService.t("deviceCreationDateMissing")),
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
creationDate: device.creationDate,
|
||||
displayName: this.devicesService.getReadableDeviceTypeName(device.type),
|
||||
firstLogin: device.creationDate ? new Date(device.creationDate) : new Date(),
|
||||
icon: this.getDeviceIcon(device.type),
|
||||
id: device.id || "",
|
||||
identifier: device.identifier ?? "",
|
||||
isCurrentDevice: this.isCurrentDevice(device, currentDevice),
|
||||
isTrusted: device.response?.isTrusted ?? false,
|
||||
loginStatus: this.getLoginStatus(device, currentDevice),
|
||||
pendingAuthRequest: device.response?.devicePendingAuthRequest ?? null,
|
||||
};
|
||||
})
|
||||
.filter((device) => device !== null)
|
||||
.sort(resortDevices);
|
||||
}
|
||||
|
||||
private async upsertDeviceWithPendingAuthRequest(authRequestId: string) {
|
||||
const authRequestResponse = await this.authRequestApiService.getAuthRequest(authRequestId);
|
||||
if (!authRequestResponse) {
|
||||
return;
|
||||
}
|
||||
|
||||
const upsertDevice: DeviceDisplayData = {
|
||||
creationDate: "",
|
||||
displayName: this.devicesService.getReadableDeviceTypeName(
|
||||
authRequestResponse.requestDeviceTypeValue,
|
||||
),
|
||||
firstLogin: new Date(authRequestResponse.creationDate),
|
||||
icon: this.getDeviceIcon(authRequestResponse.requestDeviceTypeValue),
|
||||
id: "",
|
||||
identifier: authRequestResponse.requestDeviceIdentifier,
|
||||
isCurrentDevice: false,
|
||||
isTrusted: false,
|
||||
loginStatus: this.i18nService.t("requestPending"),
|
||||
pendingAuthRequest: {
|
||||
id: authRequestResponse.id,
|
||||
creationDate: authRequestResponse.creationDate,
|
||||
},
|
||||
};
|
||||
|
||||
// If the device already exists in the DB, update the device id and first login date
|
||||
if (authRequestResponse.requestDeviceIdentifier) {
|
||||
const existingDevice = await firstValueFrom(
|
||||
this.devicesService.getDeviceByIdentifier$(authRequestResponse.requestDeviceIdentifier),
|
||||
);
|
||||
|
||||
if (existingDevice?.id && existingDevice.creationDate) {
|
||||
upsertDevice.creationDate = existingDevice.creationDate;
|
||||
upsertDevice.firstLogin = new Date(existingDevice.creationDate);
|
||||
upsertDevice.id = existingDevice.id;
|
||||
}
|
||||
}
|
||||
|
||||
const existingDeviceIndex = this.devices.findIndex(
|
||||
(device) => device.identifier === upsertDevice.identifier,
|
||||
);
|
||||
|
||||
if (existingDeviceIndex >= 0) {
|
||||
// Update existing device in device list
|
||||
this.devices[existingDeviceIndex] = upsertDevice;
|
||||
this.devices = [...this.devices].sort(resortDevices);
|
||||
} else {
|
||||
// Add new device to device list
|
||||
this.devices = [upsertDevice, ...this.devices].sort(resortDevices);
|
||||
}
|
||||
}
|
||||
|
||||
private getLoginStatus(device: DeviceView, currentDevice: DeviceResponse): string {
|
||||
if (this.isCurrentDevice(device, currentDevice)) {
|
||||
return this.i18nService.t("currentSession");
|
||||
}
|
||||
|
||||
if (this.hasPendingAuthRequest(device)) {
|
||||
return this.i18nService.t("requestPending");
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
private isCurrentDevice(device: DeviceView, currentDevice: DeviceResponse): boolean {
|
||||
return device.id === currentDevice.id;
|
||||
}
|
||||
|
||||
private hasPendingAuthRequest(device: DeviceView): boolean {
|
||||
return device.response?.devicePendingAuthRequest != null;
|
||||
}
|
||||
|
||||
private getDeviceIcon(type: DeviceType): string {
|
||||
const defaultIcon = "bwi bwi-desktop";
|
||||
const categoryIconMap: Record<string, string> = {
|
||||
webApp: "bwi bwi-browser",
|
||||
desktop: "bwi bwi-desktop",
|
||||
mobile: "bwi bwi-mobile",
|
||||
cli: "bwi bwi-cli",
|
||||
extension: "bwi bwi-puzzle",
|
||||
sdk: "bwi bwi-desktop",
|
||||
};
|
||||
|
||||
const metadata = DeviceTypeMetadata[type];
|
||||
return metadata ? (categoryIconMap[metadata.category] ?? defaultIcon) : defaultIcon;
|
||||
}
|
||||
|
||||
protected async handleAuthRequestAnswered(pendingAuthRequest: DevicePendingAuthRequest) {
|
||||
const loginApprovalDialog = LoginApprovalDialogComponent.open(this.dialogService, {
|
||||
notificationId: pendingAuthRequest.id,
|
||||
});
|
||||
|
||||
const result = await firstValueFrom(loginApprovalDialog.closed);
|
||||
|
||||
if (result !== undefined && typeof result === "boolean") {
|
||||
// Auth request was approved or denied, so clear the
|
||||
// pending auth request and re-sort the device array
|
||||
this.devices = clearAuthRequestAndResortDevices(this.devices, pendingAuthRequest);
|
||||
|
||||
// If a user ignores or doesn't see the auth request dialog, but comes to account settings
|
||||
// to approve a device login attempt, clear out the state for that user.
|
||||
await this.pendingAuthRequestStateService.clear(
|
||||
await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
import { DevicePendingAuthRequest } from "@bitwarden/common/auth/abstractions/devices/responses/device.response";
|
||||
|
||||
import { DeviceDisplayData } from "./device-management.component";
|
||||
|
||||
export function clearAuthRequestAndResortDevices(
|
||||
devices: DeviceDisplayData[],
|
||||
pendingAuthRequest: DevicePendingAuthRequest,
|
||||
): DeviceDisplayData[] {
|
||||
return devices
|
||||
.map((device) => {
|
||||
if (device.pendingAuthRequest?.id === pendingAuthRequest.id) {
|
||||
device.pendingAuthRequest = null;
|
||||
device.loginStatus = "";
|
||||
}
|
||||
return device;
|
||||
})
|
||||
.sort(resortDevices);
|
||||
}
|
||||
|
||||
/**
|
||||
* After a device is approved/denied, it will still be at the beginning of the array,
|
||||
* so we must resort the array to ensure it is in the correct order.
|
||||
*
|
||||
* This is a helper function that gets passed to the `Array.sort()` method
|
||||
*/
|
||||
export function resortDevices(deviceA: DeviceDisplayData, deviceB: DeviceDisplayData) {
|
||||
// Devices with a pending auth request should be first
|
||||
if (deviceA.pendingAuthRequest) {
|
||||
return -1;
|
||||
}
|
||||
if (deviceB.pendingAuthRequest) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Next is the current device
|
||||
if (deviceA.isCurrentDevice) {
|
||||
return -1;
|
||||
}
|
||||
if (deviceB.isCurrentDevice) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Then sort the rest by creation date (newest to oldest)
|
||||
if (deviceA.creationDate > deviceB.creationDate) {
|
||||
return -1;
|
||||
}
|
||||
if (deviceA.creationDate < deviceB.creationDate) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Default
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
<ng-container
|
||||
*ngIf="{
|
||||
selectedRegion: selectedRegion$ | async,
|
||||
} as data"
|
||||
>
|
||||
<div class="tw-mb-1">
|
||||
<bit-menu #environmentOptions>
|
||||
<button
|
||||
*ngFor="let region of availableRegions; let i = index"
|
||||
bitMenuItem
|
||||
type="button"
|
||||
[attr.aria-pressed]="data.selectedRegion === region ? 'true' : 'false'"
|
||||
(click)="toggle(region.key)"
|
||||
>
|
||||
<i
|
||||
class="bwi bwi-fw bwi-sm bwi-check"
|
||||
style="padding-bottom: 1px"
|
||||
aria-hidden="true"
|
||||
[style.visibility]="data.selectedRegion === region ? 'visible' : 'hidden'"
|
||||
></i>
|
||||
<span>{{ region.domain }}</span>
|
||||
</button>
|
||||
<button
|
||||
bitMenuItem
|
||||
type="button"
|
||||
[attr.aria-pressed]="data.selectedRegion ? 'false' : 'true'"
|
||||
(click)="toggle(ServerEnvironmentType.SelfHosted)"
|
||||
>
|
||||
<i
|
||||
class="bwi bwi-fw bwi-sm bwi-check"
|
||||
style="padding-bottom: 1px"
|
||||
aria-hidden="true"
|
||||
[style.visibility]="data.selectedRegion ? 'hidden' : 'visible'"
|
||||
></i>
|
||||
<span>{{ "selfHostedServer" | i18n }}</span>
|
||||
</button>
|
||||
</bit-menu>
|
||||
<div bitTypography="body2">
|
||||
{{ "accessing" | i18n }}:
|
||||
<button [bitMenuTriggerFor]="environmentOptions" bitLink type="button">
|
||||
<b class="tw-text-primary-600 tw-font-semibold">{{
|
||||
data.selectedRegion?.domain || ("selfHostedServer" | i18n)
|
||||
}}</b>
|
||||
<i class="bwi bwi-fw bwi-sm bwi-angle-down" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
@@ -0,0 +1,75 @@
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { Component, OnDestroy } from "@angular/core";
|
||||
import { Observable, map, Subject } from "rxjs";
|
||||
|
||||
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop.
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { SelfHostedEnvConfigDialogComponent } from "@bitwarden/auth/angular";
|
||||
import {
|
||||
EnvironmentService,
|
||||
Region,
|
||||
RegionConfig,
|
||||
} from "@bitwarden/common/platform/abstractions/environment.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import {
|
||||
DialogService,
|
||||
LinkModule,
|
||||
MenuModule,
|
||||
ToastService,
|
||||
TypographyModule,
|
||||
} from "@bitwarden/components";
|
||||
import { I18nPipe } from "@bitwarden/ui-common";
|
||||
|
||||
@Component({
|
||||
selector: "environment-selector",
|
||||
templateUrl: "environment-selector.component.html",
|
||||
standalone: true,
|
||||
imports: [CommonModule, I18nPipe, MenuModule, LinkModule, TypographyModule],
|
||||
})
|
||||
export class EnvironmentSelectorComponent implements OnDestroy {
|
||||
protected ServerEnvironmentType = Region;
|
||||
protected availableRegions = this.environmentService.availableRegions();
|
||||
protected selectedRegion$: Observable<RegionConfig | undefined> =
|
||||
this.environmentService.environment$.pipe(
|
||||
map((e) => e.getRegion()),
|
||||
map((r) => this.availableRegions.find((ar) => ar.key === r)),
|
||||
);
|
||||
|
||||
private destroy$ = new Subject<void>();
|
||||
|
||||
constructor(
|
||||
public environmentService: EnvironmentService,
|
||||
private dialogService: DialogService,
|
||||
private toastService: ToastService,
|
||||
private i18nService: I18nService,
|
||||
) {}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.destroy$.next();
|
||||
this.destroy$.complete();
|
||||
}
|
||||
|
||||
async toggle(option: Region) {
|
||||
if (option === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the self-hosted settings dialog when the self-hosted option is selected.
|
||||
*/
|
||||
if (option === Region.SelfHosted) {
|
||||
const dialogResult = await SelfHostedEnvConfigDialogComponent.open(this.dialogService);
|
||||
if (dialogResult) {
|
||||
this.toastService.showToast({
|
||||
variant: "success",
|
||||
title: "",
|
||||
message: this.i18nService.t("environmentSaved"),
|
||||
});
|
||||
}
|
||||
// Don't proceed to setEnvironment when the self-hosted dialog is cancelled
|
||||
return;
|
||||
}
|
||||
|
||||
await this.environmentService.setEnvironment(option);
|
||||
}
|
||||
}
|
||||
@@ -68,10 +68,7 @@ describe("AuthGuard", () => {
|
||||
{ path: "", component: EmptyComponent },
|
||||
{ path: "guarded-route", component: EmptyComponent, canActivate: [authGuard] },
|
||||
{ path: "lock", component: EmptyComponent },
|
||||
{ path: "set-password", component: EmptyComponent },
|
||||
{ path: "set-password-jit", component: EmptyComponent },
|
||||
{ path: "set-initial-password", component: EmptyComponent, canActivate: [authGuard] },
|
||||
{ path: "update-temp-password", component: EmptyComponent, canActivate: [authGuard] },
|
||||
{ path: "change-password", component: EmptyComponent },
|
||||
{ path: "remove-password", component: EmptyComponent, canActivate: [authGuard] },
|
||||
]),
|
||||
@@ -125,109 +122,58 @@ describe("AuthGuard", () => {
|
||||
});
|
||||
|
||||
describe("given user is Locked", () => {
|
||||
describe("given the PM16117_SetInitialPasswordRefactor feature flag is ON", () => {
|
||||
it("should redirect to /set-initial-password when the user has ForceSetPasswordReaason.TdeOffboardingUntrustedDevice", async () => {
|
||||
const { router } = setup(
|
||||
AuthenticationStatus.Locked,
|
||||
ForceSetPasswordReason.TdeOffboardingUntrustedDevice,
|
||||
false,
|
||||
FeatureFlag.PM16117_SetInitialPasswordRefactor,
|
||||
);
|
||||
it("should redirect to /set-initial-password when the user has ForceSetPasswordReaason.TdeOffboardingUntrustedDevice", async () => {
|
||||
const { router } = setup(
|
||||
AuthenticationStatus.Locked,
|
||||
ForceSetPasswordReason.TdeOffboardingUntrustedDevice,
|
||||
false,
|
||||
);
|
||||
|
||||
await router.navigate(["guarded-route"]);
|
||||
expect(router.url).toBe("/set-initial-password");
|
||||
});
|
||||
await router.navigate(["guarded-route"]);
|
||||
expect(router.url).toBe("/set-initial-password");
|
||||
});
|
||||
|
||||
it("should allow navigation to continue to /set-initial-password when the user has ForceSetPasswordReason.TdeOffboardingUntrustedDevice", async () => {
|
||||
const { router } = setup(
|
||||
AuthenticationStatus.Unlocked,
|
||||
ForceSetPasswordReason.TdeOffboardingUntrustedDevice,
|
||||
false,
|
||||
FeatureFlag.PM16117_SetInitialPasswordRefactor,
|
||||
);
|
||||
it("should allow navigation to continue to /set-initial-password when the user has ForceSetPasswordReason.TdeOffboardingUntrustedDevice", async () => {
|
||||
const { router } = setup(
|
||||
AuthenticationStatus.Unlocked,
|
||||
ForceSetPasswordReason.TdeOffboardingUntrustedDevice,
|
||||
false,
|
||||
);
|
||||
|
||||
await router.navigate(["/set-initial-password"]);
|
||||
expect(router.url).toContain("/set-initial-password");
|
||||
});
|
||||
await router.navigate(["/set-initial-password"]);
|
||||
expect(router.url).toContain("/set-initial-password");
|
||||
});
|
||||
});
|
||||
|
||||
describe("given user is Unlocked", () => {
|
||||
describe("given the PM16117_SetInitialPasswordRefactor feature flag is ON", () => {
|
||||
const tests = [
|
||||
ForceSetPasswordReason.TdeUserWithoutPasswordHasPasswordResetPermission,
|
||||
ForceSetPasswordReason.TdeOffboarding,
|
||||
];
|
||||
describe("given user is Unlocked and ForceSetPasswordReason requires setting an initial password", () => {
|
||||
const tests = [
|
||||
ForceSetPasswordReason.TdeUserWithoutPasswordHasPasswordResetPermission,
|
||||
ForceSetPasswordReason.TdeOffboarding,
|
||||
];
|
||||
|
||||
describe("given user attempts to navigate to an auth guarded route", () => {
|
||||
tests.forEach((reason) => {
|
||||
it(`should redirect to /set-initial-password when the user has ForceSetPasswordReason.${ForceSetPasswordReason[reason]}`, async () => {
|
||||
const { router } = setup(
|
||||
AuthenticationStatus.Unlocked,
|
||||
reason,
|
||||
false,
|
||||
FeatureFlag.PM16117_SetInitialPasswordRefactor,
|
||||
);
|
||||
describe("given user attempts to navigate to an auth guarded route", () => {
|
||||
tests.forEach((reason) => {
|
||||
it(`should redirect to /set-initial-password when the user has ForceSetPasswordReason.${ForceSetPasswordReason[reason]}`, async () => {
|
||||
const { router } = setup(AuthenticationStatus.Unlocked, reason, false);
|
||||
|
||||
await router.navigate(["guarded-route"]);
|
||||
expect(router.url).toContain("/set-initial-password");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("given user attempts to navigate to /set-initial-password", () => {
|
||||
tests.forEach((reason) => {
|
||||
it(`should allow navigation to continue to /set-initial-password when the user has ForceSetPasswordReason.${ForceSetPasswordReason[reason]}`, async () => {
|
||||
const { router } = setup(
|
||||
AuthenticationStatus.Unlocked,
|
||||
reason,
|
||||
false,
|
||||
FeatureFlag.PM16117_SetInitialPasswordRefactor,
|
||||
);
|
||||
|
||||
await router.navigate(["/set-initial-password"]);
|
||||
expect(router.url).toContain("/set-initial-password");
|
||||
});
|
||||
await router.navigate(["guarded-route"]);
|
||||
expect(router.url).toContain("/set-initial-password");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("given the PM16117_SetInitialPasswordRefactor feature flag is OFF", () => {
|
||||
const tests = [
|
||||
{
|
||||
reason: ForceSetPasswordReason.TdeUserWithoutPasswordHasPasswordResetPermission,
|
||||
url: "/set-password",
|
||||
},
|
||||
{
|
||||
reason: ForceSetPasswordReason.TdeOffboarding,
|
||||
url: "/update-temp-password",
|
||||
},
|
||||
];
|
||||
describe("given user attempts to navigate to /set-initial-password", () => {
|
||||
tests.forEach((reason) => {
|
||||
it(`should allow navigation to continue to /set-initial-password when the user has ForceSetPasswordReason.${ForceSetPasswordReason[reason]}`, async () => {
|
||||
const { router } = setup(AuthenticationStatus.Unlocked, reason, false);
|
||||
|
||||
describe("given user attempts to navigate to an auth guarded route", () => {
|
||||
tests.forEach(({ reason, url }) => {
|
||||
it(`should redirect to ${url} when user has ForceSetPasswordReason.${ForceSetPasswordReason[reason]}`, async () => {
|
||||
const { router } = setup(AuthenticationStatus.Unlocked, reason);
|
||||
|
||||
await router.navigate(["/guarded-route"]);
|
||||
expect(router.url).toContain(url);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("given user attempts to navigate to the set- or update- password route itself", () => {
|
||||
tests.forEach(({ reason, url }) => {
|
||||
it(`should allow navigation to continue to ${url} when user has ForceSetPasswordReason.${ForceSetPasswordReason[reason]}`, async () => {
|
||||
const { router } = setup(AuthenticationStatus.Unlocked, reason);
|
||||
|
||||
await router.navigate([url]);
|
||||
expect(router.url).toContain(url);
|
||||
});
|
||||
await router.navigate(["/set-initial-password"]);
|
||||
expect(router.url).toContain("/set-initial-password");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("given the PM16117_ChangeExistingPasswordRefactor feature flag is ON", () => {
|
||||
describe("given user is Unlocked and ForceSetPasswordReason requires changing an existing password", () => {
|
||||
const tests = [
|
||||
ForceSetPasswordReason.AdminForcePasswordReset,
|
||||
ForceSetPasswordReason.WeakMasterPassword,
|
||||
@@ -236,12 +182,7 @@ describe("AuthGuard", () => {
|
||||
describe("given user attempts to navigate to an auth guarded route", () => {
|
||||
tests.forEach((reason) => {
|
||||
it(`should redirect to /change-password when user has ForceSetPasswordReason.${ForceSetPasswordReason[reason]}`, async () => {
|
||||
const { router } = setup(
|
||||
AuthenticationStatus.Unlocked,
|
||||
reason,
|
||||
false,
|
||||
FeatureFlag.PM16117_ChangeExistingPasswordRefactor,
|
||||
);
|
||||
const { router } = setup(AuthenticationStatus.Unlocked, reason, false);
|
||||
|
||||
await router.navigate(["guarded-route"]);
|
||||
expect(router.url).toContain("/change-password");
|
||||
@@ -256,7 +197,6 @@ describe("AuthGuard", () => {
|
||||
AuthenticationStatus.Unlocked,
|
||||
ForceSetPasswordReason.AdminForcePasswordReset,
|
||||
false,
|
||||
FeatureFlag.PM16117_ChangeExistingPasswordRefactor,
|
||||
);
|
||||
|
||||
await router.navigate(["/change-password"]);
|
||||
@@ -265,34 +205,5 @@ describe("AuthGuard", () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("given the PM16117_ChangeExistingPasswordRefactor feature flag is OFF", () => {
|
||||
const tests = [
|
||||
ForceSetPasswordReason.AdminForcePasswordReset,
|
||||
ForceSetPasswordReason.WeakMasterPassword,
|
||||
];
|
||||
|
||||
describe("given user attempts to navigate to an auth guarded route", () => {
|
||||
tests.forEach((reason) => {
|
||||
it(`should redirect to /update-temp-password when user has ForceSetPasswordReason.${ForceSetPasswordReason[reason]}`, async () => {
|
||||
const { router } = setup(AuthenticationStatus.Unlocked, reason);
|
||||
|
||||
await router.navigate(["guarded-route"]);
|
||||
expect(router.url).toContain("/update-temp-password");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("given user attempts to navigate to /update-temp-password", () => {
|
||||
tests.forEach((reason) => {
|
||||
it(`should allow navigation to continue to /update-temp-password when user has ForceSetPasswordReason.${ForceSetPasswordReason[reason]}`, async () => {
|
||||
const { router } = setup(AuthenticationStatus.Unlocked, reason);
|
||||
|
||||
await router.navigate(["/update-temp-password"]);
|
||||
expect(router.url).toContain("/update-temp-password");
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -14,10 +14,8 @@ import { AccountService } from "@bitwarden/common/auth/abstractions/account.serv
|
||||
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
|
||||
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
||||
import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { KeyConnectorService } from "@bitwarden/common/key-management/key-connector/abstractions/key-connector.service";
|
||||
import { MasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||
|
||||
export const authGuard: CanActivateFn = async (
|
||||
@@ -30,7 +28,6 @@ export const authGuard: CanActivateFn = async (
|
||||
const keyConnectorService = inject(KeyConnectorService);
|
||||
const accountService = inject(AccountService);
|
||||
const masterPasswordService = inject(MasterPasswordServiceAbstraction);
|
||||
const configService = inject(ConfigService);
|
||||
|
||||
const authStatus = await authService.getAuthStatus();
|
||||
|
||||
@@ -44,16 +41,11 @@ export const authGuard: CanActivateFn = async (
|
||||
masterPasswordService.forceSetPasswordReason$(userId),
|
||||
);
|
||||
|
||||
const isSetInitialPasswordFlagOn = await configService.getFeatureFlag(
|
||||
FeatureFlag.PM16117_SetInitialPasswordRefactor,
|
||||
);
|
||||
|
||||
// User JIT provisioned into a master-password-encryption org
|
||||
if (
|
||||
authStatus === AuthenticationStatus.Locked &&
|
||||
forceSetPasswordReason === ForceSetPasswordReason.SsoNewJitProvisionedUser &&
|
||||
!routerState.url.includes("set-initial-password") &&
|
||||
isSetInitialPasswordFlagOn
|
||||
!routerState.url.includes("set-initial-password")
|
||||
) {
|
||||
return router.createUrlTree(["/set-initial-password"]);
|
||||
}
|
||||
@@ -62,8 +54,7 @@ export const authGuard: CanActivateFn = async (
|
||||
if (
|
||||
authStatus === AuthenticationStatus.Locked &&
|
||||
forceSetPasswordReason === ForceSetPasswordReason.TdeOffboardingUntrustedDevice &&
|
||||
!routerState.url.includes("set-initial-password") &&
|
||||
isSetInitialPasswordFlagOn
|
||||
!routerState.url.includes("set-initial-password")
|
||||
) {
|
||||
return router.createUrlTree(["/set-initial-password"]);
|
||||
}
|
||||
@@ -83,48 +74,39 @@ export const authGuard: CanActivateFn = async (
|
||||
return router.createUrlTree(["lock"], { queryParams: { promptBiometric: true } });
|
||||
}
|
||||
|
||||
// Handle cases where a user needs to set a password when they don't already have one:
|
||||
// - TDE org user has been given "manage account recovery" permission
|
||||
// - TDE offboarding on a trusted device, where we have access to their encryption key wrap with their new password
|
||||
if (
|
||||
(forceSetPasswordReason ===
|
||||
ForceSetPasswordReason.TdeUserWithoutPasswordHasPasswordResetPermission ||
|
||||
forceSetPasswordReason === ForceSetPasswordReason.TdeOffboarding) &&
|
||||
!routerState.url.includes("set-initial-password")
|
||||
) {
|
||||
const route = "/set-initial-password";
|
||||
return router.createUrlTree([route]);
|
||||
}
|
||||
|
||||
// Handle cases where a user has a password but needs to set a new one:
|
||||
// - Account recovery
|
||||
// - Weak Password on login
|
||||
if (
|
||||
(forceSetPasswordReason === ForceSetPasswordReason.AdminForcePasswordReset ||
|
||||
forceSetPasswordReason === ForceSetPasswordReason.WeakMasterPassword) &&
|
||||
!routerState.url.includes("change-password")
|
||||
) {
|
||||
const route = "/change-password";
|
||||
return router.createUrlTree([route]);
|
||||
}
|
||||
|
||||
// Remove password when Key Connector is enabled
|
||||
if (
|
||||
forceSetPasswordReason == ForceSetPasswordReason.None &&
|
||||
!routerState.url.includes("remove-password") &&
|
||||
(await firstValueFrom(keyConnectorService.convertAccountRequired$))
|
||||
) {
|
||||
return router.createUrlTree(["/remove-password"]);
|
||||
}
|
||||
|
||||
// TDE org user has "manage account recovery" permission
|
||||
if (
|
||||
forceSetPasswordReason ===
|
||||
ForceSetPasswordReason.TdeUserWithoutPasswordHasPasswordResetPermission &&
|
||||
!routerState.url.includes("set-password") &&
|
||||
!routerState.url.includes("set-initial-password")
|
||||
) {
|
||||
const route = isSetInitialPasswordFlagOn ? "/set-initial-password" : "/set-password";
|
||||
return router.createUrlTree([route]);
|
||||
}
|
||||
|
||||
// TDE Offboarding on trusted device
|
||||
if (
|
||||
forceSetPasswordReason === ForceSetPasswordReason.TdeOffboarding &&
|
||||
!routerState.url.includes("update-temp-password") &&
|
||||
!routerState.url.includes("set-initial-password")
|
||||
) {
|
||||
const route = isSetInitialPasswordFlagOn ? "/set-initial-password" : "/update-temp-password";
|
||||
return router.createUrlTree([route]);
|
||||
}
|
||||
|
||||
const isChangePasswordFlagOn = await configService.getFeatureFlag(
|
||||
FeatureFlag.PM16117_ChangeExistingPasswordRefactor,
|
||||
);
|
||||
|
||||
// Post- Account Recovery or Weak Password on login
|
||||
if (
|
||||
(forceSetPasswordReason === ForceSetPasswordReason.AdminForcePasswordReset ||
|
||||
forceSetPasswordReason === ForceSetPasswordReason.WeakMasterPassword) &&
|
||||
!routerState.url.includes("update-temp-password") &&
|
||||
!routerState.url.includes("change-password")
|
||||
) {
|
||||
const route = isChangePasswordFlagOn ? "/change-password" : "/update-temp-password";
|
||||
return router.createUrlTree([route]);
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
@@ -4,3 +4,4 @@ export * from "./lock.guard";
|
||||
export * from "./redirect/redirect.guard";
|
||||
export * from "./tde-decryption-required.guard";
|
||||
export * from "./unauth.guard";
|
||||
export * from "./redirect-to-vault-if-unlocked/redirect-to-vault-if-unlocked.guard";
|
||||
|
||||
@@ -15,6 +15,8 @@ import { UserVerificationService } from "@bitwarden/common/auth/abstractions/use
|
||||
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
||||
import { ClientType } from "@bitwarden/common/enums";
|
||||
import { DeviceTrustServiceAbstraction } from "@bitwarden/common/key-management/device-trust/abstractions/device-trust.service.abstraction";
|
||||
import { KeyConnectorService } from "@bitwarden/common/key-management/key-connector/abstractions/key-connector.service";
|
||||
import { KeyConnectorDomainConfirmation } from "@bitwarden/common/key-management/key-connector/models/key-connector-domain-confirmation";
|
||||
import { VaultTimeoutSettingsService } from "@bitwarden/common/key-management/vault-timeout";
|
||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
@@ -26,7 +28,6 @@ import { lockGuard } from "./lock.guard";
|
||||
interface SetupParams {
|
||||
authStatus: AuthenticationStatus;
|
||||
canLock?: boolean;
|
||||
isLegacyUser?: boolean;
|
||||
clientType?: ClientType;
|
||||
everHadUserKey?: boolean;
|
||||
supportsDeviceTrust?: boolean;
|
||||
@@ -34,30 +35,35 @@ interface SetupParams {
|
||||
}
|
||||
|
||||
describe("lockGuard", () => {
|
||||
const keyConnectorService = mock<KeyConnectorService>();
|
||||
|
||||
const setup = (setupParams: SetupParams) => {
|
||||
const authService: MockProxy<AuthService> = mock<AuthService>();
|
||||
authService.authStatusFor$.mockReturnValue(of(setupParams.authStatus));
|
||||
|
||||
const vaultTimeoutSettingsService: MockProxy<VaultTimeoutSettingsService> =
|
||||
mock<VaultTimeoutSettingsService>();
|
||||
vaultTimeoutSettingsService.canLock.mockResolvedValue(setupParams.canLock);
|
||||
vaultTimeoutSettingsService.canLock.mockResolvedValue(setupParams.canLock ?? true);
|
||||
|
||||
const keyService: MockProxy<KeyService> = mock<KeyService>();
|
||||
keyService.isLegacyUser.mockResolvedValue(setupParams.isLegacyUser);
|
||||
keyService.everHadUserKey$.mockReturnValue(of(setupParams.everHadUserKey));
|
||||
keyService.everHadUserKey$.mockReturnValue(of(setupParams.everHadUserKey ?? true));
|
||||
|
||||
const platformUtilService: MockProxy<PlatformUtilsService> = mock<PlatformUtilsService>();
|
||||
platformUtilService.getClientType.mockReturnValue(setupParams.clientType);
|
||||
platformUtilService.getClientType.mockReturnValue(setupParams.clientType ?? ClientType.Web);
|
||||
|
||||
const messagingService: MockProxy<MessagingService> = mock<MessagingService>();
|
||||
|
||||
const deviceTrustService: MockProxy<DeviceTrustServiceAbstraction> =
|
||||
mock<DeviceTrustServiceAbstraction>();
|
||||
deviceTrustService.supportsDeviceTrust$ = of(setupParams.supportsDeviceTrust);
|
||||
deviceTrustService.supportsDeviceTrust$ = of(setupParams.supportsDeviceTrust ?? false);
|
||||
|
||||
const userVerificationService: MockProxy<UserVerificationService> =
|
||||
mock<UserVerificationService>();
|
||||
userVerificationService.hasMasterPassword.mockResolvedValue(setupParams.hasMasterPassword);
|
||||
userVerificationService.hasMasterPassword.mockResolvedValue(
|
||||
setupParams.hasMasterPassword ?? true,
|
||||
);
|
||||
|
||||
keyConnectorService.requiresDomainConfirmation$.mockReturnValue(of(null));
|
||||
|
||||
const accountService: MockProxy<AccountService> = mock<AccountService>();
|
||||
const activeAccountSubject = new BehaviorSubject<Account | null>(null);
|
||||
@@ -79,6 +85,7 @@ describe("lockGuard", () => {
|
||||
{ path: "", component: EmptyComponent },
|
||||
{ path: "lock", component: EmptyComponent, canActivate: [lockGuard()] },
|
||||
{ path: "non-lock-route", component: EmptyComponent },
|
||||
{ path: "confirm-key-connector-domain", component: EmptyComponent },
|
||||
]),
|
||||
],
|
||||
providers: [
|
||||
@@ -90,6 +97,7 @@ describe("lockGuard", () => {
|
||||
{ provide: PlatformUtilsService, useValue: platformUtilService },
|
||||
{ provide: DeviceTrustServiceAbstraction, useValue: deviceTrustService },
|
||||
{ provide: UserVerificationService, useValue: userVerificationService },
|
||||
{ provide: KeyConnectorService, useValue: keyConnectorService },
|
||||
],
|
||||
});
|
||||
|
||||
@@ -155,37 +163,10 @@ describe("lockGuard", () => {
|
||||
expect(router.url).toBe("/");
|
||||
});
|
||||
|
||||
it("should log user out if they are a legacy user on a desktop client", async () => {
|
||||
const { router, messagingService } = setup({
|
||||
authStatus: AuthenticationStatus.Locked,
|
||||
canLock: true,
|
||||
isLegacyUser: true,
|
||||
clientType: ClientType.Desktop,
|
||||
});
|
||||
|
||||
await router.navigate(["lock"]);
|
||||
expect(router.url).toBe("/");
|
||||
expect(messagingService.send).toHaveBeenCalledWith("logout");
|
||||
});
|
||||
|
||||
it("should log user out if they are a legacy user on a browser extension client", async () => {
|
||||
const { router, messagingService } = setup({
|
||||
authStatus: AuthenticationStatus.Locked,
|
||||
canLock: true,
|
||||
isLegacyUser: true,
|
||||
clientType: ClientType.Browser,
|
||||
});
|
||||
|
||||
await router.navigate(["lock"]);
|
||||
expect(router.url).toBe("/");
|
||||
expect(messagingService.send).toHaveBeenCalledWith("logout");
|
||||
});
|
||||
|
||||
it("should allow navigation to the lock route when device trust is supported, the user has a MP, and the user is coming from the login-initiated page", async () => {
|
||||
const { router } = setup({
|
||||
authStatus: AuthenticationStatus.Locked,
|
||||
canLock: true,
|
||||
isLegacyUser: false,
|
||||
clientType: ClientType.Web,
|
||||
everHadUserKey: false,
|
||||
supportsDeviceTrust: true,
|
||||
@@ -213,7 +194,6 @@ describe("lockGuard", () => {
|
||||
const { router } = setup({
|
||||
authStatus: AuthenticationStatus.Locked,
|
||||
canLock: true,
|
||||
isLegacyUser: false,
|
||||
clientType: ClientType.Web,
|
||||
everHadUserKey: false,
|
||||
supportsDeviceTrust: true,
|
||||
@@ -223,4 +203,19 @@ describe("lockGuard", () => {
|
||||
await router.navigate(["lock"]);
|
||||
expect(router.url).toBe("/");
|
||||
});
|
||||
|
||||
it("should redirect to the confirm-key-connector-domain route when the auth status is locked, can't lock and requires key connector domain confirmation", async () => {
|
||||
const { router } = setup({
|
||||
authStatus: AuthenticationStatus.Locked,
|
||||
canLock: false,
|
||||
});
|
||||
keyConnectorService.requiresDomainConfirmation$.mockReturnValue(
|
||||
of({
|
||||
keyConnectorUrl: "https://example.com",
|
||||
} as KeyConnectorDomainConfirmation),
|
||||
);
|
||||
|
||||
await router.navigate(["lock"]);
|
||||
expect(router.url).toBe("/confirm-key-connector-domain");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -12,8 +12,8 @@ import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
|
||||
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
|
||||
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
||||
import { DeviceTrustServiceAbstraction } from "@bitwarden/common/key-management/device-trust/abstractions/device-trust.service.abstraction";
|
||||
import { KeyConnectorService } from "@bitwarden/common/key-management/key-connector/abstractions/key-connector.service";
|
||||
import { VaultTimeoutSettingsService } from "@bitwarden/common/key-management/vault-timeout";
|
||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||
import { KeyService } from "@bitwarden/key-management";
|
||||
|
||||
/**
|
||||
@@ -31,11 +31,11 @@ export function lockGuard(): CanActivateFn {
|
||||
const authService = inject(AuthService);
|
||||
const keyService = inject(KeyService);
|
||||
const deviceTrustService = inject(DeviceTrustServiceAbstraction);
|
||||
const messagingService = inject(MessagingService);
|
||||
const router = inject(Router);
|
||||
const userVerificationService = inject(UserVerificationService);
|
||||
const vaultTimeoutSettingsService = inject(VaultTimeoutSettingsService);
|
||||
const accountService = inject(AccountService);
|
||||
const keyConnectorService = inject(KeyConnectorService);
|
||||
|
||||
const activeUser = await firstValueFrom(accountService.activeAccount$);
|
||||
|
||||
@@ -50,17 +50,18 @@ export function lockGuard(): CanActivateFn {
|
||||
return router.createUrlTree(["/"]);
|
||||
}
|
||||
|
||||
if (
|
||||
(await firstValueFrom(keyConnectorService.requiresDomainConfirmation$(activeUser.id))) != null
|
||||
) {
|
||||
return router.createUrlTree(["confirm-key-connector-domain"]);
|
||||
}
|
||||
|
||||
// if user can't lock, they can't access the lock screen
|
||||
const canLock = await vaultTimeoutSettingsService.canLock(activeUser.id);
|
||||
if (!canLock) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (await keyService.isLegacyUser()) {
|
||||
messagingService.send("logout");
|
||||
return false;
|
||||
}
|
||||
|
||||
// User is authN and in locked state.
|
||||
|
||||
const tdeEnabled = await firstValueFrom(deviceTrustService.supportsDeviceTrust$);
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
# RedirectToVaultIfUnlocked Guard
|
||||
|
||||
The `redirectToVaultIfUnlocked` redirects the user to `/vault` if they are `Unlocked`. Otherwise, it allows access to the route.
|
||||
|
||||
This is particularly useful for routes that can handle BOTH unauthenticated AND authenticated-but-locked users (which makes the `authGuard` unusable on those routes).
|
||||
|
||||
<br>
|
||||
|
||||
### Special Use Case - Authenticating in the Extension Popout
|
||||
|
||||
Imagine a user is going through the Login with Device flow in the Extension pop*out*:
|
||||
|
||||
- They open the pop*out* while on `/login-with-device`
|
||||
- The approve the login from another device
|
||||
- They are authenticated and routed to `/vault` while in the pop*out*
|
||||
|
||||
If the `redirectToVaultIfUnlocked` were NOT applied, if this user now opens the pop*up* they would be shown the `/login-with-device`, not their `/vault`.
|
||||
|
||||
But by adding the `redirectToVaultIfUnlocked` to `/login-with-device` we make sure to check if the user has already `Unlocked`, and if so, route them to `/vault` upon opening the pop*up*.
|
||||
@@ -0,0 +1,98 @@
|
||||
import { TestBed } from "@angular/core/testing";
|
||||
import { Router, provideRouter } from "@angular/router";
|
||||
import { mock } from "jest-mock-extended";
|
||||
import { BehaviorSubject, of } from "rxjs";
|
||||
|
||||
import { EmptyComponent } from "@bitwarden/angular/platform/guard/feature-flag.guard.spec";
|
||||
import { Account, AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
|
||||
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
|
||||
import { redirectToVaultIfUnlockedGuard } from "./redirect-to-vault-if-unlocked.guard";
|
||||
|
||||
describe("redirectToVaultIfUnlockedGuard", () => {
|
||||
const activeUser: Account = {
|
||||
id: "userId" as UserId,
|
||||
email: "test@email.com",
|
||||
emailVerified: true,
|
||||
name: "Test User",
|
||||
};
|
||||
|
||||
const setup = (activeUser: Account | null, authStatus: AuthenticationStatus | null) => {
|
||||
const accountService = mock<AccountService>();
|
||||
const authService = mock<AuthService>();
|
||||
|
||||
accountService.activeAccount$ = new BehaviorSubject<Account | null>(activeUser);
|
||||
authService.authStatusFor$.mockReturnValue(of(authStatus));
|
||||
|
||||
const testBed = TestBed.configureTestingModule({
|
||||
providers: [
|
||||
{ provide: AccountService, useValue: accountService },
|
||||
{ provide: AuthService, useValue: authService },
|
||||
provideRouter([
|
||||
{ path: "", component: EmptyComponent },
|
||||
{ path: "vault", component: EmptyComponent },
|
||||
{
|
||||
path: "guarded-route",
|
||||
component: EmptyComponent,
|
||||
canActivate: [redirectToVaultIfUnlockedGuard()],
|
||||
},
|
||||
]),
|
||||
],
|
||||
});
|
||||
|
||||
return {
|
||||
router: testBed.inject(Router),
|
||||
};
|
||||
};
|
||||
|
||||
it("should be created", () => {
|
||||
const { router } = setup(null, null);
|
||||
expect(router).toBeTruthy();
|
||||
});
|
||||
|
||||
it("should redirect to /vault if the user is AuthenticationStatus.Unlocked", async () => {
|
||||
// Arrange
|
||||
const { router } = setup(activeUser, AuthenticationStatus.Unlocked);
|
||||
|
||||
// Act
|
||||
await router.navigate(["guarded-route"]);
|
||||
|
||||
// Assert
|
||||
expect(router.url).toBe("/vault");
|
||||
});
|
||||
|
||||
it("should allow navigation to continue to the route if there is no active user", async () => {
|
||||
// Arrange
|
||||
const { router } = setup(null, null);
|
||||
|
||||
// Act
|
||||
await router.navigate(["guarded-route"]);
|
||||
|
||||
// Assert
|
||||
expect(router.url).toBe("/guarded-route");
|
||||
});
|
||||
|
||||
it("should allow navigation to continue to the route if the user is AuthenticationStatus.LoggedOut", async () => {
|
||||
// Arrange
|
||||
const { router } = setup(null, AuthenticationStatus.LoggedOut);
|
||||
|
||||
// Act
|
||||
await router.navigate(["guarded-route"]);
|
||||
|
||||
// Assert
|
||||
expect(router.url).toBe("/guarded-route");
|
||||
});
|
||||
|
||||
it("should allow navigation to continue to the route if the user is AuthenticationStatus.Locked", async () => {
|
||||
// Arrange
|
||||
const { router } = setup(null, AuthenticationStatus.Locked);
|
||||
|
||||
// Act
|
||||
await router.navigate(["guarded-route"]);
|
||||
|
||||
// Assert
|
||||
expect(router.url).toBe("/guarded-route");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,36 @@
|
||||
import { inject } from "@angular/core";
|
||||
import { CanActivateFn, Router } from "@angular/router";
|
||||
import { firstValueFrom } from "rxjs";
|
||||
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
|
||||
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
||||
|
||||
/**
|
||||
* Redirects the user to `/vault` if they are `Unlocked`. Otherwise, it allows access to the route.
|
||||
* See ./redirect-to-vault-if-unlocked/README.md for more details.
|
||||
*/
|
||||
export function redirectToVaultIfUnlockedGuard(): CanActivateFn {
|
||||
return async () => {
|
||||
const accountService = inject(AccountService);
|
||||
const authService = inject(AuthService);
|
||||
const router = inject(Router);
|
||||
|
||||
const activeUser = await firstValueFrom(accountService.activeAccount$);
|
||||
|
||||
// If there is no active user, allow access to the route
|
||||
if (!activeUser) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const authStatus = await firstValueFrom(authService.authStatusFor$(activeUser.id));
|
||||
|
||||
// If user is Unlocked, redirect to vault
|
||||
if (authStatus === AuthenticationStatus.Unlocked) {
|
||||
return router.createUrlTree(["/vault"]);
|
||||
}
|
||||
|
||||
// If user is LoggedOut or Locked, allow access to the route
|
||||
return true;
|
||||
};
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
import { svgIcon } from "@bitwarden/components";
|
||||
|
||||
export const CreatePasskeyFailedIcon = svgIcon`
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="163" height="115" fill="none">
|
||||
<path class="tw-fill-secondary-600" fill-rule="evenodd" d="M31 19.46H9v22h22v-22Zm-24-2v26h26v-26H7Z"
|
||||
clip-rule="evenodd" />
|
||||
<path class="tw-fill-secondary-600" fill-rule="evenodd"
|
||||
d="M0 43.46a4 4 0 0 1 4-4h32a4 4 0 0 1 4 4v7h-4v-7H4v16.747l1.705 2.149a4 4 0 0 1 .866 2.486v22.205a4 4 0 0 1-1 2.645L4 91.475v17.985h32V91.475l-1.572-1.783a4 4 0 0 1-1-2.645V64.842a4 4 0 0 1 .867-2.486L36 60.207V56.46h4v3.747a4 4 0 0 1-.867 2.487l-1.704 2.148v22.205L39 88.83a4 4 0 0 1 1 2.645v17.985a4 4 0 0 1-4 4H4a4 4 0 0 1-4-4V91.475a4 4 0 0 1 1-2.645l1.571-1.783V64.842L.867 62.694A4 4 0 0 1 0 60.207V43.46Z"
|
||||
clip-rule="evenodd" />
|
||||
<path class="tw-fill-secondary-600" fill-rule="evenodd"
|
||||
d="M19.74 63.96a.5.5 0 0 1 .355.147l2.852 2.866a.5.5 0 0 1 .146.353V77.56c2.585 1.188 4.407 3.814 4.407 6.865 0 4.183-3.357 7.534-7.5 7.534-4.144 0-7.5-3.376-7.5-7.534a7.546 7.546 0 0 1 4.478-6.894v-1.443a.5.5 0 0 1 .146-.353l1.275-1.281-1.322-1.33a.5.5 0 0 1 0-.705l.332-.334-.262-.263a.5.5 0 0 1-.005-.7l1.332-1.377-1.445-1.452a.5.5 0 0 1-.145-.352v-1.114a.5.5 0 0 1 .145-.352l2.357-2.369a.5.5 0 0 1 .355-.147Zm-1.856 3.075v.7l1.645 1.654a.5.5 0 0 1 .005.7l-1.332 1.377.267.268a.5.5 0 0 1 0 .705l-.333.334 1.323 1.329a.5.5 0 0 1 0 .705l-1.48 1.488v1.57a.5.5 0 0 1-.32.466 6.545 6.545 0 0 0-4.159 6.095c0 3.61 2.913 6.534 6.5 6.534 3.588 0 6.5-2.901 6.5-6.534 0-2.749-1.707-5.105-4.095-6.074a.5.5 0 0 1-.312-.463V67.532L19.74 65.17l-1.857 1.866ZM20 85.623a1.27 1.27 0 0 0-1.268 1.276c0 .702.56 1.276 1.268 1.276.712 0 1.268-.555 1.268-1.276A1.27 1.27 0 0 0 20 85.623Zm-2.268 1.276A2.27 2.27 0 0 1 20 84.623a2.27 2.27 0 0 1 2.268 2.276c0 1.269-1 2.276-2.268 2.276a2.27 2.27 0 0 1-2.268-2.276ZM57.623 114a1 1 0 0 1 1-1h63.048a1 1 0 0 1 0 2H58.623a1 1 0 0 1-1-1Z"
|
||||
clip-rule="evenodd" />
|
||||
<path class="tw-fill-secondary-600" fill-rule="evenodd"
|
||||
d="M78.022 114V95.654h2V114h-2ZM98.418 114V95.654h2V114h-2Z" clip-rule="evenodd" />
|
||||
<path class="tw-fill-secondary-600" fill-rule="evenodd"
|
||||
d="M16 14.46c0-7.732 6.268-14 14-14h119c7.732 0 14 6.268 14 14v68c0 7.732-6.268 14-14 14H39.5v-4H149c5.523 0 10-4.477 10-10v-68c0-5.523-4.477-10-10-10H30c-5.523 0-10 4.477-10 10v5h-4v-5Z"
|
||||
clip-rule="evenodd" />
|
||||
<path class="tw-fill-secondary-600" fill-rule="evenodd"
|
||||
d="M25 15.46a6 6 0 0 1 6-6h117a6 6 0 0 1 6 6v66a6 6 0 0 1-6 6H36.5v-2H148a4 4 0 0 0 4-4v-66a4 4 0 0 0-4-4H31a4 4 0 0 0-4 4v3h-2v-3Z"
|
||||
clip-rule="evenodd" />
|
||||
<path class="tw-fill-secondary-600"
|
||||
d="M104.269 32.86a1.42 1.42 0 0 0-1.007-.4h-25.83c-.39 0-.722.132-1.007.4a1.26 1.26 0 0 0-.425.947v16.199c0 1.207.25 2.407.75 3.597a13.22 13.22 0 0 0 1.861 3.165c.74.919 1.62 1.817 2.646 2.69a30.93 30.93 0 0 0 2.834 2.172c.868.577 1.77 1.121 2.712 1.636.942.516 1.612.862 2.007 1.043.394.181.714.326.95.42.18.083.373.128.583.128.21 0 .403-.041.582-.128.241-.099.557-.239.956-.42.394-.181 1.064-.532 2.006-1.043a36.595 36.595 0 0 0 2.712-1.636c.867-.576 1.813-1.302 2.838-2.171a19.943 19.943 0 0 0 2.646-2.69 13.24 13.24 0 0 0 1.862-3.166 9.19 9.19 0 0 0 .749-3.597V33.812c.005-.367-.14-.684-.425-.952Zm-3.329 17.298c0 5.864-10.593 10.916-10.593 10.916V35.93h10.593v14.228Z" />
|
||||
<path class="tw-fill-secondary-600" fill-rule="evenodd" d="M18 24.46h-5v-2h5v2ZM27 24.46h-5v-2h5v2Z"
|
||||
clip-rule="evenodd" />
|
||||
<path class="tw-fill-danger-600"
|
||||
d="M51.066 66.894a2.303 2.303 0 0 1-2.455-.5l-10.108-9.797L28.375 66.4l-.002.002a2.294 2.294 0 0 1-3.185.005 2.24 2.24 0 0 1-.506-2.496c.117-.27.286-.518.503-.728l10.062-9.737-9.945-9.623a2.258 2.258 0 0 1-.698-1.6c-.004-.314.06-.619.176-.894a2.254 2.254 0 0 1 1.257-1.222 2.305 2.305 0 0 1 1.723.014c.267.11.518.274.732.486l10.01 9.682 9.995-9.688.009-.008a2.292 2.292 0 0 1 3.159.026c.425.411.68.98.684 1.59a2.242 2.242 0 0 1-.655 1.6l-.01.01-9.926 9.627 10.008 9.7.029.027a2.237 2.237 0 0 1 .53 2.496l-.002.004a2.258 2.258 0 0 1-1.257 1.222Z" />
|
||||
</svg>
|
||||
`;
|
||||
@@ -1,26 +0,0 @@
|
||||
import { svgIcon } from "@bitwarden/components";
|
||||
|
||||
export const CreatePasskeyIcon = svgIcon`
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="163" height="116" fill="none">
|
||||
<path class="tw-fill-secondary-600" fill-rule="evenodd" d="M31 19.58H9v22h22v-22Zm-24-2v26h26v-26H7Z"
|
||||
clip-rule="evenodd" />
|
||||
<path class="tw-fill-secondary-600" fill-rule="evenodd"
|
||||
d="M0 43.58a4 4 0 0 1 4-4h32a4 4 0 0 1 4 4v7h-4v-7H4v16.747l1.705 2.149a4 4 0 0 1 .866 2.486v22.204a4 4 0 0 1-1 2.646L4 91.595v17.985h32V91.595l-1.572-1.783a4 4 0 0 1-1-2.646V64.962a4 4 0 0 1 .867-2.486L36 60.327V56.58h4v3.747a4 4 0 0 1-.867 2.486l-1.704 2.149v22.204L39 88.95a4 4 0 0 1 1 2.646v17.985a4 4 0 0 1-4 4H4a4 4 0 0 1-4-4V91.595a4 4 0 0 1 1-2.646l1.571-1.783V64.962L.867 62.813A4 4 0 0 1 0 60.327V43.58Z"
|
||||
clip-rule="evenodd" />
|
||||
<path class="tw-fill-secondary-600" fill-rule="evenodd"
|
||||
d="M19.74 64.08a.5.5 0 0 1 .355.147l2.852 2.866a.5.5 0 0 1 .146.352V77.68c2.585 1.189 4.407 3.814 4.407 6.865 0 4.183-3.357 7.535-7.5 7.535-4.144 0-7.5-3.377-7.5-7.535a7.546 7.546 0 0 1 4.478-6.894V76.21a.5.5 0 0 1 .146-.353l1.275-1.282-1.322-1.329a.5.5 0 0 1 0-.705l.332-.334-.262-.263a.5.5 0 0 1-.005-.7l1.332-1.377-1.445-1.452a.5.5 0 0 1-.145-.353v-1.113a.5.5 0 0 1 .145-.353l2.357-2.368a.5.5 0 0 1 .355-.147Zm-1.856 3.074v.7l1.645 1.654a.5.5 0 0 1 .005.7l-1.332 1.377.267.268a.5.5 0 0 1 0 .706l-.333.334 1.323 1.329a.5.5 0 0 1 0 .705l-1.48 1.488v1.57a.5.5 0 0 1-.32.466 6.545 6.545 0 0 0-4.159 6.094c0 3.61 2.913 6.535 6.5 6.535 3.588 0 6.5-2.902 6.5-6.535 0-2.748-1.707-5.104-4.095-6.073a.5.5 0 0 1-.312-.463V67.651l-2.352-2.364-1.857 1.866ZM20 85.742a1.27 1.27 0 0 0-1.268 1.277c0 .701.56 1.276 1.268 1.276.712 0 1.268-.555 1.268-1.276A1.27 1.27 0 0 0 20 85.742Zm-2.268 1.277A2.27 2.27 0 0 1 20 84.742a2.27 2.27 0 0 1 2.268 2.277c0 1.268-1 2.276-2.268 2.276a2.27 2.27 0 0 1-2.268-2.276ZM41.796 42.844a1 1 0 0 1 1.413.058l5.526 6A1 1 0 0 1 48 50.58H27a1 1 0 1 1 0-2h18.72l-3.982-4.323a1 1 0 0 1 .058-1.413ZM33.315 62.315a1 1 0 0 1-1.413-.058l-5.526-6a1 1 0 0 1 .735-1.677h21a1 1 0 1 1 0 2h-18.72l3.982 4.322a1 1 0 0 1-.058 1.413ZM57.623 114.12a1 1 0 0 1 1-1h63.048a1 1 0 1 1 0 2H58.623a1 1 0 0 1-1-1Z"
|
||||
clip-rule="evenodd" />
|
||||
<path class="tw-fill-secondary-600" fill-rule="evenodd"
|
||||
d="M78.022 114.12V95.774h2v18.346h-2ZM98.418 114.12V95.774h2v18.346h-2Z" clip-rule="evenodd" />
|
||||
<path class="tw-fill-secondary-600" fill-rule="evenodd"
|
||||
d="M16 14.58c0-7.732 6.268-14 14-14h119c7.732 0 14 6.268 14 14v68c0 7.732-6.268 14-14 14H39.5v-4H149c5.523 0 10-4.478 10-10v-68c0-5.523-4.477-10-10-10H30c-5.523 0-10 4.477-10 10v5h-4v-5Z"
|
||||
clip-rule="evenodd" />
|
||||
<path class="tw-fill-secondary-600" fill-rule="evenodd"
|
||||
d="M25 15.58a6 6 0 0 1 6-6h117a6 6 0 0 1 6 6v66a6 6 0 0 1-6 6H36.5v-2H148a4 4 0 0 0 4-4v-66a4 4 0 0 0-4-4H31a4 4 0 0 0-4 4v3h-2v-3Z"
|
||||
clip-rule="evenodd" />
|
||||
<path class="tw-fill-secondary-600"
|
||||
d="M104.269 32.98a1.42 1.42 0 0 0-1.007-.4h-25.83c-.39 0-.722.132-1.007.4a1.26 1.26 0 0 0-.425.947v16.199c0 1.207.25 2.406.75 3.597a13.222 13.222 0 0 0 1.861 3.165c.74.919 1.62 1.817 2.646 2.69a30.93 30.93 0 0 0 2.834 2.172c.868.577 1.77 1.121 2.712 1.636.942.515 1.612.861 2.007 1.043.394.18.714.325.95.42.18.082.373.128.583.128.21 0 .403-.042.582-.128.241-.099.557-.24.956-.42.394-.182 1.064-.532 2.006-1.043a36.56 36.56 0 0 0 2.712-1.636c.867-.577 1.813-1.302 2.838-2.172a19.943 19.943 0 0 0 2.646-2.69 13.24 13.24 0 0 0 1.862-3.165c.5-1.187.749-2.386.749-3.597V33.93c.005-.367-.14-.684-.425-.952Zm-3.329 17.298c0 5.864-10.593 10.916-10.593 10.916V36.049h10.593v14.23Z" />
|
||||
<path class="tw-fill-secondary-600" fill-rule="evenodd" d="M18 24.58h-5v-2h5v2ZM27 24.58h-5v-2h5v2Z"
|
||||
clip-rule="evenodd" />
|
||||
</svg>
|
||||
`;
|
||||
@@ -1,44 +0,0 @@
|
||||
import { svgIcon } from "@bitwarden/components";
|
||||
|
||||
export const EmailIcon = svgIcon`
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="67"
|
||||
height="64"
|
||||
fill="none"
|
||||
class="tw-text-primary-600"
|
||||
role="img"
|
||||
aria-label="[title]"
|
||||
>
|
||||
<title>Email</title>
|
||||
<path
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
d="m9.28 19.155-5.848 4.363a4 4 0 0 0-1.608 3.206V59a4 4 0 0 0 4 4h56.044a4 4 0 0 0 4-4V26.742a4 4 0 0 0-1.63-3.223L58.3 19.155M42.438 7.49l-6.442-4.736a4 4 0 0 0-4.762.017l-6.324 4.72"
|
||||
/>
|
||||
<path
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-width="2"
|
||||
d="M58.373 30.978V9.473a2 2 0 0 0-2-2H11.318a2 2 0 0 0-2 2v21.505"
|
||||
/>
|
||||
<path
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
d="M64.504 61.637 43.35 42.107a6 6 0 0 0-4.07-1.59H27.421a6 6 0 0 0-4.175 1.69l-20.06 19.43"
|
||||
/>
|
||||
<path
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-width="2"
|
||||
d="m65.181 27.239-22.81 13.623M2.51 27.24l22.81 13.622"
|
||||
/>
|
||||
<rect width="35" height="12" x="16.324" y="17.5" stroke="currentColor" rx="6" />
|
||||
<circle cx="21.324" cy="23.5" r="1.5" fill="currentColor" />
|
||||
<circle cx="31.324" cy="23.5" r="1.5" fill="currentColor" />
|
||||
<circle cx="41.324" cy="23.5" r="1.5" fill="currentColor" />
|
||||
<circle cx="26.324" cy="23.5" r="1.5" fill="currentColor" />
|
||||
<circle cx="36.324" cy="23.5" r="1.5" fill="currentColor" />
|
||||
<circle cx="46.324" cy="23.5" r="1.5" fill="currentColor" />
|
||||
</svg>
|
||||
`;
|
||||
@@ -1,51 +0,0 @@
|
||||
import { svgIcon } from "@bitwarden/components";
|
||||
|
||||
export const RecoveryCodeIcon = svgIcon`
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
alt="rc logo"
|
||||
width="76"
|
||||
height="63"
|
||||
fill="none"
|
||||
class="tw-text-primary-600"
|
||||
role="img"
|
||||
aria-label="[title]"
|
||||
>
|
||||
<title>Recovery Code</title>
|
||||
<rect
|
||||
width="49.459"
|
||||
height="17.255"
|
||||
x="1"
|
||||
y="-1"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
rx="8.627"
|
||||
transform="matrix(-1 0 0 1 52.15 38.12)"
|
||||
/>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M7.742 19.337a1 1 0 0 1 1.635-1.152l-1.635 1.152Zm7.499 8.895.576.817a1 1 0 0 1-1.393-.24l.817-.577Zm8.895-7.498a1 1 0 1 1 1.152 1.634l-1.152-1.634Zm44.129 29.953-.776-.631.776.631ZM63.942 8.483l.631-.775-.631.775Zm-42.205 4.323.776.631-.776-.631Zm22.266 49.925a1 1 0 1 1 .063-1.999l-.063 2ZM16.358 27.183a1 1 0 1 1-1.975-.313l1.975.313Zm-6.981-8.998 6.681 9.47-1.634 1.153-6.682-9.47 1.635-1.154Zm5.287 9.23 9.472-6.681 1.152 1.634-9.47 6.681-1.154-1.634ZM67.49 50.056c10.112-12.42 8.241-30.685-4.179-40.797l1.263-1.551C77.85 18.517 79.85 38.042 69.04 51.318l-1.55-1.262ZM63.31 9.259C50.89-.853 32.625 1.018 22.513 13.437l-1.551-1.262C31.772-1.102 51.297-3.102 64.573 7.708l-1.263 1.55ZM44.066 60.732c8.732.275 17.484-3.381 23.423-10.676l1.551 1.263c-6.35 7.799-15.708 11.706-25.037 11.412l.063-1.999ZM22.513 13.437a28.861 28.861 0 0 0-6.155 13.746l-1.975-.313a30.861 30.861 0 0 1 6.579-14.695l1.551 1.262Z"
|
||||
/>
|
||||
<path
|
||||
stroke="currentColor"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M36.59 25.167v-5.445c0-4.021 3.806-7.267 8.475-7.267s8.475 3.264 8.475 7.267v5.445"
|
||||
/>
|
||||
<path
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
d="M32.353 37.345v-6.098a6 6 0 0 1 6-6h13.424a6 6 0 0 1 6 6v9.187a6 6 0 0 1-6 6h-.601"
|
||||
/>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M45.886 35.63c.491-.285.822-.82.822-1.432v-.5c0-.913-.736-1.653-1.643-1.653-.908 0-1.643.74-1.643 1.654v.499c0 .612.33 1.146.82 1.432v1.283c0 .457.368.827.822.827.454 0 .822-.37.822-.827v-1.282Z"
|
||||
/>
|
||||
<circle cx="2" cy="2" r="2" fill="currentColor" transform="matrix(-1 0 0 1 46.001 44.148)" />
|
||||
<circle cx="2" cy="2" r="2" fill="currentColor" transform="matrix(-1 0 0 1 32.001 44.148)" />
|
||||
<circle cx="2" cy="2" r="2" fill="currentColor" transform="matrix(-1 0 0 1 18.001 44.148)" />
|
||||
<circle cx="2" cy="2" r="2" fill="currentColor" transform="matrix(-1 0 0 1 39.001 44.148)" />
|
||||
<circle cx="2" cy="2" r="2" fill="currentColor" transform="matrix(-1 0 0 1 25.001 44.148)" />
|
||||
<circle cx="2" cy="2" r="2" fill="currentColor" transform="matrix(-1 0 0 1 11.001 44.148)" />
|
||||
</svg>
|
||||
`;
|
||||
@@ -1,61 +0,0 @@
|
||||
import { svgIcon } from "@bitwarden/components";
|
||||
|
||||
export const TOTPIcon = svgIcon`
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="120"
|
||||
height="62"
|
||||
fill="none"
|
||||
class="tw-text-primary-600"
|
||||
role="img"
|
||||
aria-label="[title]"
|
||||
>
|
||||
<title>TOTP Authenticator</title>
|
||||
<rect width="35" height="12" x="50.324" y="21.5" stroke="currentColor" rx="6" />
|
||||
<circle cx="55.324" cy="27.5" r="1.5" fill="currentColor" />
|
||||
<circle cx="65.324" cy="27.5" r="1.5" fill="currentColor" />
|
||||
<circle cx="75.324" cy="27.5" r="1.5" fill="currentColor" />
|
||||
<circle cx="60.324" cy="27.5" r="1.5" fill="currentColor" />
|
||||
<circle cx="70.324" cy="27.5" r="1.5" fill="currentColor" />
|
||||
<circle cx="80.324" cy="27.5" r="1.5" fill="currentColor" />
|
||||
<path
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M52.703 61h34.706"
|
||||
/>
|
||||
<path
|
||||
stroke="currentColor"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M63.932 51.166V61M75.16 51.166V61"
|
||||
/>
|
||||
<path
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
d="M38.952 51.166h64.894a6 6 0 0 0 6-6V7a6 6 0 0 0-6-6h-68a6 6 0 0 0-6 6v7.655"
|
||||
/>
|
||||
<path
|
||||
stroke="currentColor"
|
||||
d="M38.692 46.517h62.999a4 4 0 0 0 4-4V9.552a4 4 0 0 0-4-4H38.537a4 4 0 0 0-4 4v5.103"
|
||||
/>
|
||||
<path
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M32.284 27.382h11.064l-2.912-2.895M43.348 30.276H32.284l2.912 2.895"
|
||||
/>
|
||||
<path
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
d="M37.846 27.75V19a4 4 0 0 0-4-4h-18a4 4 0 0 0-4 4v38a4 4 0 0 0 4 4h18a4 4 0 0 0 4-4V30.5"
|
||||
/>
|
||||
<path stroke="currentColor" stroke-linecap="round" stroke-width="2" d="M24.806 18.5h.08" />
|
||||
<path stroke="currentColor" d="M24.846 36a8 8 0 1 0 7.858 6.5" />
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M21.547 45.36h1.02v-3.64l-1.221.584v-.626l1.215-.575h.657v4.258h1.007v.545h-2.678v-.545ZM25.793 45.358h2.23v.548h-2.948v-.548c.405-.424.76-.799 1.063-1.124a27 27 0 0 0 .627-.687c.216-.263.363-.475.438-.636.076-.164.114-.33.114-.5 0-.27-.08-.48-.24-.633-.158-.153-.376-.23-.653-.23a1.99 1.99 0 0 0-.621.107 3.493 3.493 0 0 0-.69.323v-.658c.224-.106.443-.185.657-.24.217-.053.43-.08.64-.08.475 0 .856.126 1.145.378.29.25.435.578.435.984 0 .207-.049.413-.146.62a3.005 3.005 0 0 1-.468.684c-.122.14-.298.334-.53.581-.23.248-.58.618-1.053 1.11Z"
|
||||
/>
|
||||
</svg>
|
||||
`;
|
||||
@@ -1,42 +0,0 @@
|
||||
import { svgIcon } from "@bitwarden/components";
|
||||
|
||||
export const WebAuthnIcon = svgIcon`
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="62.176998"
|
||||
height="60.152"
|
||||
fill="none"
|
||||
class="tw-text-primary-600"
|
||||
role="img"
|
||||
aria-label="[title]"
|
||||
>
|
||||
<title>Webauthn</title>
|
||||
<g stroke="currentColor" clip-path="url(#a)" transform="translate(-6.081,-1.143)">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-miterlimit="10"
|
||||
stroke-width="2"
|
||||
d="M20.418 33.19c5.614 0 10.42 3.56 12.396 8.533h29.012l5.434 5.522-4.491 4.563h-6.602l-3.413-3.468-3.279 3.24-1.167-1.187-1.303 1.323-3.188-3.24-3.1 3.149H32.86c-1.932 5.065-6.782 8.67-12.44 8.67-7.366 0-13.339-6.069-13.339-13.553 0-7.483 5.928-13.552 13.338-13.552Zm-6.152 16.975c1.841 0 3.368-1.506 3.368-3.422 0-1.871-1.482-3.423-3.368-3.423-1.887 0-3.369 1.506-3.369 3.422 0 1.917 1.527 3.423 3.369 3.423Z"
|
||||
/>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
d="M59.78 44.722H36.32M19.99 36.722c4.036 0 7.569 2.405 9.51 6"
|
||||
/>
|
||||
<circle
|
||||
cx="12.24"
|
||||
cy="12.24"
|
||||
r="12.24"
|
||||
stroke-width="2"
|
||||
transform="matrix(-1 0 0 1 50.6 2.143)"
|
||||
/>
|
||||
<path
|
||||
stroke-width="2"
|
||||
d="M22.753 33.423c3.836-4.174 9.41-6.8 15.615-6.8 9.698 0 17.857 6.417 20.243 15.13"
|
||||
/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="a"><path fill="#fff" d="M68.5.62H.5v61h68z" /></clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
`;
|
||||
@@ -0,0 +1,30 @@
|
||||
import { TestBed } from "@angular/core/testing";
|
||||
|
||||
import { DefaultLoginApprovalDialogComponentService } from "./default-login-approval-dialog-component.service";
|
||||
import { LoginApprovalDialogComponent } from "./login-approval-dialog.component";
|
||||
|
||||
describe("DefaultLoginApprovalDialogComponentService", () => {
|
||||
let service: DefaultLoginApprovalDialogComponentService;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
providers: [DefaultLoginApprovalDialogComponentService],
|
||||
});
|
||||
|
||||
service = TestBed.inject(DefaultLoginApprovalDialogComponentService);
|
||||
});
|
||||
|
||||
it("is created successfully", () => {
|
||||
expect(service).toBeTruthy();
|
||||
});
|
||||
|
||||
it("has showLoginRequestedAlertIfWindowNotVisible method that is a no-op", async () => {
|
||||
const loginApprovalDialogComponent = {} as LoginApprovalDialogComponent;
|
||||
|
||||
const result = await service.showLoginRequestedAlertIfWindowNotVisible(
|
||||
loginApprovalDialogComponent.email,
|
||||
);
|
||||
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,16 @@
|
||||
import { LoginApprovalDialogComponentServiceAbstraction } from "./login-approval-dialog-component.service.abstraction";
|
||||
|
||||
/**
|
||||
* Default implementation of the LoginApprovalDialogComponentServiceAbstraction.
|
||||
*/
|
||||
export class DefaultLoginApprovalDialogComponentService
|
||||
implements LoginApprovalDialogComponentServiceAbstraction
|
||||
{
|
||||
/**
|
||||
* No-op implementation of the showLoginRequestedAlertIfWindowNotVisible method.
|
||||
* @returns
|
||||
*/
|
||||
async showLoginRequestedAlertIfWindowNotVisible(email?: string): Promise<void> {
|
||||
return;
|
||||
}
|
||||
}
|
||||
3
libs/angular/src/auth/login-approval/index.ts
Normal file
3
libs/angular/src/auth/login-approval/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export * from "./login-approval-dialog.component";
|
||||
export * from "./login-approval-dialog-component.service.abstraction";
|
||||
export * from "./default-login-approval-dialog-component.service";
|
||||
@@ -0,0 +1,9 @@
|
||||
/**
|
||||
* Abstraction for the LoginApprovalDialogComponent service.
|
||||
*/
|
||||
export abstract class LoginApprovalDialogComponentServiceAbstraction {
|
||||
/**
|
||||
* Shows a login requested alert if the window is not visible.
|
||||
*/
|
||||
abstract showLoginRequestedAlertIfWindowNotVisible: (email?: string) => Promise<void>;
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
<bit-dialog>
|
||||
<span bitDialogTitle>{{ "loginRequest" | i18n }}</span>
|
||||
|
||||
<ng-container bitDialogContent>
|
||||
<ng-container *ngIf="loading">
|
||||
<div class="tw-flex tw-items-center tw-justify-center" *ngIf="loading">
|
||||
<i class="bwi bwi-spinner bwi-spin bwi-3x" aria-hidden="true"></i>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="!loading">
|
||||
<p>{{ "accessAttemptBy" | i18n: email }}</p>
|
||||
<div>
|
||||
<span class="tw-font-medium">{{ "fingerprintPhraseHeader" | i18n }}</span>
|
||||
<p class="tw-text-code">{{ fingerprintPhrase }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<span class="tw-font-medium">{{ "deviceType" | i18n }}</span>
|
||||
<p>{{ readableDeviceTypeName }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<span class="tw-font-medium">{{ "location" | i18n }}</span>
|
||||
<p>
|
||||
<span class="tw-capitalize">{{ authRequestResponse?.requestCountryName }} </span>
|
||||
({{ authRequestResponse?.requestIpAddress }})
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<span class="tw-font-medium">{{ "time" | i18n }}</span>
|
||||
<p>{{ requestTimeText }}</p>
|
||||
</div>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
|
||||
<ng-container bitDialogFooter>
|
||||
<button
|
||||
bitButton
|
||||
type="button"
|
||||
buttonType="primary"
|
||||
[bitAction]="approveLogin"
|
||||
[disabled]="loading"
|
||||
>
|
||||
{{ "confirmAccess" | i18n }}
|
||||
</button>
|
||||
<button
|
||||
bitButton
|
||||
type="button"
|
||||
buttonType="secondary"
|
||||
[bitAction]="denyLogin"
|
||||
[disabled]="loading"
|
||||
>
|
||||
{{ "denyAccess" | i18n }}
|
||||
</button>
|
||||
</ng-container>
|
||||
</bit-dialog>
|
||||
@@ -0,0 +1,126 @@
|
||||
import { ComponentFixture, TestBed } from "@angular/core/testing";
|
||||
import { mock, MockProxy } from "jest-mock-extended";
|
||||
import { of } from "rxjs";
|
||||
|
||||
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop.
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { AuthRequestServiceAbstraction } from "@bitwarden/auth/common";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { DevicesServiceAbstraction } from "@bitwarden/common/auth/abstractions/devices/devices.service.abstraction";
|
||||
import { AuthRequestResponse } from "@bitwarden/common/auth/models/response/auth-request.response";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { DialogRef, DIALOG_DATA, ToastService } from "@bitwarden/components";
|
||||
import { LogService } from "@bitwarden/logging";
|
||||
|
||||
import { LoginApprovalDialogComponentServiceAbstraction } from "./login-approval-dialog-component.service.abstraction";
|
||||
import { LoginApprovalDialogComponent } from "./login-approval-dialog.component";
|
||||
|
||||
describe("LoginApprovalDialogComponent", () => {
|
||||
let component: LoginApprovalDialogComponent;
|
||||
let fixture: ComponentFixture<LoginApprovalDialogComponent>;
|
||||
|
||||
let accountService: MockProxy<AccountService>;
|
||||
let apiService: MockProxy<ApiService>;
|
||||
let authRequestService: MockProxy<AuthRequestServiceAbstraction>;
|
||||
let devicesService: MockProxy<DevicesServiceAbstraction>;
|
||||
let dialogRef: MockProxy<DialogRef>;
|
||||
let i18nService: MockProxy<I18nService>;
|
||||
let logService: MockProxy<LogService>;
|
||||
let toastService: MockProxy<ToastService>;
|
||||
let validationService: MockProxy<ValidationService>;
|
||||
|
||||
const testNotificationId = "test-notification-id";
|
||||
const testEmail = "test@bitwarden.com";
|
||||
const testPublicKey = "test-public-key";
|
||||
|
||||
beforeEach(async () => {
|
||||
accountService = mock<AccountService>();
|
||||
apiService = mock<ApiService>();
|
||||
authRequestService = mock<AuthRequestServiceAbstraction>();
|
||||
devicesService = mock<DevicesServiceAbstraction>();
|
||||
dialogRef = mock<DialogRef>();
|
||||
i18nService = mock<I18nService>();
|
||||
logService = mock<LogService>();
|
||||
toastService = mock<ToastService>();
|
||||
validationService = mock<ValidationService>();
|
||||
|
||||
accountService.activeAccount$ = of({
|
||||
email: testEmail,
|
||||
id: "test-user-id" as UserId,
|
||||
emailVerified: true,
|
||||
name: null,
|
||||
});
|
||||
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [LoginApprovalDialogComponent],
|
||||
providers: [
|
||||
{ provide: DIALOG_DATA, useValue: { notificationId: testNotificationId } },
|
||||
{ provide: AccountService, useValue: accountService },
|
||||
{ provide: ApiService, useValue: apiService },
|
||||
{ provide: AuthRequestServiceAbstraction, useValue: authRequestService },
|
||||
{ provide: DevicesServiceAbstraction, useValue: devicesService },
|
||||
{ provide: DialogRef, useValue: dialogRef },
|
||||
{ provide: I18nService, useValue: i18nService },
|
||||
{ provide: LogService, useValue: logService },
|
||||
{ provide: ToastService, useValue: toastService },
|
||||
{ provide: ValidationService, useValue: validationService },
|
||||
{
|
||||
provide: LoginApprovalDialogComponentServiceAbstraction,
|
||||
useValue: mock<LoginApprovalDialogComponentServiceAbstraction>(),
|
||||
},
|
||||
],
|
||||
}).compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(LoginApprovalDialogComponent);
|
||||
component = fixture.componentInstance;
|
||||
});
|
||||
|
||||
it("creates successfully", () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
|
||||
describe("ngOnInit", () => {
|
||||
beforeEach(() => {
|
||||
apiService.getAuthRequest.mockResolvedValue({
|
||||
publicKey: testPublicKey,
|
||||
creationDate: new Date().toISOString(),
|
||||
} as AuthRequestResponse);
|
||||
authRequestService.getFingerprintPhrase.mockResolvedValue("test-phrase");
|
||||
});
|
||||
|
||||
it("retrieves and sets auth request data", async () => {
|
||||
await component.ngOnInit();
|
||||
|
||||
expect(apiService.getAuthRequest).toHaveBeenCalledWith(testNotificationId);
|
||||
expect(component.email).toBe(testEmail);
|
||||
expect(component.fingerprintPhrase).toBeDefined();
|
||||
});
|
||||
|
||||
it("updates time text initially", async () => {
|
||||
i18nService.t.mockReturnValue("justNow");
|
||||
|
||||
await component.ngOnInit();
|
||||
expect(component.requestTimeText).toBe("justNow");
|
||||
});
|
||||
});
|
||||
|
||||
describe("denyLogin", () => {
|
||||
it("denies auth request and shows info toast", async () => {
|
||||
const response = { requestApproved: false } as AuthRequestResponse;
|
||||
apiService.getAuthRequest.mockResolvedValue(response);
|
||||
authRequestService.approveOrDenyAuthRequest.mockResolvedValue(response);
|
||||
i18nService.t.mockReturnValue("denied message");
|
||||
|
||||
await component.denyLogin();
|
||||
|
||||
expect(authRequestService.approveOrDenyAuthRequest).toHaveBeenCalledWith(false, response);
|
||||
expect(toastService.showToast).toHaveBeenCalledWith({
|
||||
variant: "info",
|
||||
message: "denied message",
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,220 @@
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { Component, OnInit, OnDestroy, Inject } from "@angular/core";
|
||||
import { firstValueFrom, map } from "rxjs";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop.
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { AuthRequestServiceAbstraction } from "@bitwarden/auth/common";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { DevicesServiceAbstraction } from "@bitwarden/common/auth/abstractions/devices/devices.service.abstraction";
|
||||
import { AuthRequestResponse } from "@bitwarden/common/auth/models/response/auth-request.response";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import {
|
||||
DIALOG_DATA,
|
||||
DialogRef,
|
||||
AsyncActionsModule,
|
||||
ButtonModule,
|
||||
DialogModule,
|
||||
DialogService,
|
||||
ToastService,
|
||||
} from "@bitwarden/components";
|
||||
import { LogService } from "@bitwarden/logging";
|
||||
|
||||
import { LoginApprovalDialogComponentServiceAbstraction } from "./login-approval-dialog-component.service.abstraction";
|
||||
|
||||
const RequestTimeOut = 60000 * 15; // 15 Minutes
|
||||
const RequestTimeUpdate = 60000 * 5; // 5 Minutes
|
||||
|
||||
export interface LoginApprovalDialogParams {
|
||||
notificationId: string;
|
||||
}
|
||||
|
||||
@Component({
|
||||
templateUrl: "login-approval-dialog.component.html",
|
||||
imports: [AsyncActionsModule, ButtonModule, CommonModule, DialogModule, JslibModule],
|
||||
})
|
||||
export class LoginApprovalDialogComponent implements OnInit, OnDestroy {
|
||||
authRequestId: string;
|
||||
authRequestResponse?: AuthRequestResponse;
|
||||
email?: string;
|
||||
fingerprintPhrase?: string;
|
||||
interval?: NodeJS.Timeout;
|
||||
loading = true;
|
||||
readableDeviceTypeName?: string;
|
||||
requestTimeText?: string;
|
||||
|
||||
constructor(
|
||||
@Inject(DIALOG_DATA) private params: LoginApprovalDialogParams,
|
||||
private accountService: AccountService,
|
||||
private apiService: ApiService,
|
||||
private authRequestService: AuthRequestServiceAbstraction,
|
||||
private devicesService: DevicesServiceAbstraction,
|
||||
private dialogRef: DialogRef,
|
||||
private i18nService: I18nService,
|
||||
private loginApprovalDialogComponentService: LoginApprovalDialogComponentServiceAbstraction,
|
||||
private logService: LogService,
|
||||
private toastService: ToastService,
|
||||
private validationService: ValidationService,
|
||||
) {
|
||||
this.authRequestId = params.notificationId;
|
||||
}
|
||||
|
||||
async ngOnDestroy(): Promise<void> {
|
||||
clearInterval(this.interval);
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
if (this.authRequestId == null) {
|
||||
this.logService.error("LoginApprovalDialogComponent: authRequestId is null");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
this.authRequestResponse = await this.apiService.getAuthRequest(this.authRequestId);
|
||||
} catch (error) {
|
||||
this.validationService.showError(error);
|
||||
this.logService.error("LoginApprovalDialogComponent: getAuthRequest error", error);
|
||||
}
|
||||
|
||||
if (this.authRequestResponse == null) {
|
||||
this.logService.error("LoginApprovalDialogComponent: authRequestResponse not found");
|
||||
return;
|
||||
}
|
||||
|
||||
const publicKey = Utils.fromB64ToArray(this.authRequestResponse.publicKey);
|
||||
|
||||
this.email = await firstValueFrom(
|
||||
this.accountService.activeAccount$.pipe(map((a) => a?.email)),
|
||||
);
|
||||
|
||||
if (!this.email) {
|
||||
this.logService.error("LoginApprovalDialogComponent: email not found");
|
||||
return;
|
||||
}
|
||||
|
||||
this.fingerprintPhrase = await this.authRequestService.getFingerprintPhrase(
|
||||
this.email,
|
||||
publicKey,
|
||||
);
|
||||
|
||||
this.readableDeviceTypeName = this.devicesService.getReadableDeviceTypeName(
|
||||
this.authRequestResponse.requestDeviceTypeValue,
|
||||
);
|
||||
|
||||
this.updateTimeText();
|
||||
|
||||
this.interval = setInterval(() => {
|
||||
this.updateTimeText();
|
||||
}, RequestTimeUpdate);
|
||||
|
||||
await this.loginApprovalDialogComponentService.showLoginRequestedAlertIfWindowNotVisible(
|
||||
this.email,
|
||||
);
|
||||
|
||||
this.loading = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Strongly-typed helper to open a LoginApprovalDialog
|
||||
* @param dialogService Instance of the dialog service that will be used to open the dialog
|
||||
* @param data Configuration for the dialog
|
||||
*/
|
||||
static open(dialogService: DialogService, data: LoginApprovalDialogParams) {
|
||||
return dialogService.open(LoginApprovalDialogComponent, { data });
|
||||
}
|
||||
|
||||
denyLogin = async () => {
|
||||
await this.retrieveAuthRequestAndRespond(false);
|
||||
};
|
||||
|
||||
approveLogin = async () => {
|
||||
await this.retrieveAuthRequestAndRespond(true);
|
||||
};
|
||||
|
||||
private async retrieveAuthRequestAndRespond(approve: boolean) {
|
||||
this.authRequestResponse = await this.apiService.getAuthRequest(this.authRequestId);
|
||||
if (this.authRequestResponse.requestApproved || this.authRequestResponse.responseDate != null) {
|
||||
this.toastService.showToast({
|
||||
variant: "info",
|
||||
message: this.i18nService.t("thisRequestIsNoLongerValid"),
|
||||
});
|
||||
} else {
|
||||
const loginResponse = await this.authRequestService.approveOrDenyAuthRequest(
|
||||
approve,
|
||||
this.authRequestResponse,
|
||||
);
|
||||
this.showResultToast(loginResponse);
|
||||
}
|
||||
|
||||
this.dialogRef.close(approve);
|
||||
}
|
||||
|
||||
showResultToast(loginResponse: AuthRequestResponse) {
|
||||
if (loginResponse.requestApproved) {
|
||||
this.toastService.showToast({
|
||||
variant: "success",
|
||||
message: this.i18nService.t(
|
||||
"loginRequestApprovedForEmailOnDevice",
|
||||
this.email,
|
||||
this.devicesService.getReadableDeviceTypeName(loginResponse.requestDeviceTypeValue),
|
||||
),
|
||||
});
|
||||
} else {
|
||||
this.toastService.showToast({
|
||||
variant: "info",
|
||||
message: this.i18nService.t("youDeniedLoginAttemptFromAnotherDevice"),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
updateTimeText() {
|
||||
if (this.authRequestResponse == null) {
|
||||
this.logService.error("LoginApprovalDialogComponent: authRequestResponse not found");
|
||||
return;
|
||||
}
|
||||
|
||||
const requestDate = new Date(this.authRequestResponse.creationDate);
|
||||
const requestDateUTC = Date.UTC(
|
||||
requestDate.getUTCFullYear(),
|
||||
requestDate.getUTCMonth(),
|
||||
requestDate.getDate(),
|
||||
requestDate.getUTCHours(),
|
||||
requestDate.getUTCMinutes(),
|
||||
requestDate.getUTCSeconds(),
|
||||
requestDate.getUTCMilliseconds(),
|
||||
);
|
||||
|
||||
const dateNow = new Date(Date.now());
|
||||
const dateNowUTC = Date.UTC(
|
||||
dateNow.getUTCFullYear(),
|
||||
dateNow.getUTCMonth(),
|
||||
dateNow.getDate(),
|
||||
dateNow.getUTCHours(),
|
||||
dateNow.getUTCMinutes(),
|
||||
dateNow.getUTCSeconds(),
|
||||
dateNow.getUTCMilliseconds(),
|
||||
);
|
||||
|
||||
const diffInMinutes = dateNowUTC - requestDateUTC;
|
||||
|
||||
if (diffInMinutes <= RequestTimeUpdate) {
|
||||
this.requestTimeText = this.i18nService.t("justNow");
|
||||
} else if (diffInMinutes < RequestTimeOut) {
|
||||
this.requestTimeText = this.i18nService.t(
|
||||
"requestedXMinutesAgo",
|
||||
(diffInMinutes / 60000).toFixed(),
|
||||
);
|
||||
} else {
|
||||
clearInterval(this.interval);
|
||||
this.dialogRef.close();
|
||||
this.toastService.showToast({
|
||||
variant: "info",
|
||||
message: this.i18nService.t("loginRequestHasAlreadyExpired"),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<div class="tw-flex tw-flex-col tw-items-center">
|
||||
<ng-container *ngIf="currentState === 'assert'">
|
||||
<p bitTypography="body1" class="tw-text-center">{{ "readingPasskeyLoading" | i18n }}</p>
|
||||
<button type="button" bitButton block [loading]="true" buttonType="primary" class="tw-mb-4">
|
||||
{{ "loading" | i18n }}
|
||||
</button>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="currentState === 'assertFailed'">
|
||||
<p bitTypography="body1" class="tw-text-center">{{ "passkeyAuthenticationFailed" | i18n }}</p>
|
||||
<button type="button" bitButton block buttonType="primary" class="tw-mb-4" (click)="retry()">
|
||||
{{ "tryAgain" | i18n }}
|
||||
</button>
|
||||
</ng-container>
|
||||
|
||||
<p bitTypography="body1" class="tw-mb-0 tw-text-center">
|
||||
{{ "troubleLoggingIn" | i18n }}<br />
|
||||
<a bitLink routerLink="/login">{{ "useADifferentLogInMethod" | i18n }}</a>
|
||||
</p>
|
||||
</div>
|
||||
@@ -1,27 +1,69 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Directive, OnInit } from "@angular/core";
|
||||
import { Router } from "@angular/router";
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { Component, OnInit } from "@angular/core";
|
||||
import { Router, RouterModule } from "@angular/router";
|
||||
import { firstValueFrom } from "rxjs";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import {
|
||||
TwoFactorAuthSecurityKeyIcon,
|
||||
TwoFactorAuthSecurityKeyFailedIcon,
|
||||
} from "@bitwarden/assets/svg";
|
||||
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop.
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { LoginSuccessHandlerService } from "@bitwarden/auth/common";
|
||||
import { WebAuthnLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/webauthn/webauthn-login.service.abstraction";
|
||||
import { WebAuthnLoginCredentialAssertionView } from "@bitwarden/common/auth/models/view/webauthn-login/webauthn-login-credential-assertion.view";
|
||||
import { ClientType } from "@bitwarden/common/enums";
|
||||
import { ErrorResponse } from "@bitwarden/common/models/response/error.response";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service";
|
||||
import {
|
||||
AnonLayoutWrapperDataService,
|
||||
ButtonModule,
|
||||
IconModule,
|
||||
LinkModule,
|
||||
TypographyModule,
|
||||
} from "@bitwarden/components";
|
||||
import { KeyService } from "@bitwarden/key-management";
|
||||
|
||||
export type State = "assert" | "assertFailed";
|
||||
|
||||
@Directive()
|
||||
export class BaseLoginViaWebAuthnComponent implements OnInit {
|
||||
@Component({
|
||||
selector: "app-login-via-webauthn",
|
||||
templateUrl: "login-via-webauthn.component.html",
|
||||
standalone: true,
|
||||
imports: [
|
||||
CommonModule,
|
||||
RouterModule,
|
||||
JslibModule,
|
||||
ButtonModule,
|
||||
IconModule,
|
||||
LinkModule,
|
||||
TypographyModule,
|
||||
],
|
||||
})
|
||||
export class LoginViaWebAuthnComponent implements OnInit {
|
||||
protected currentState: State = "assert";
|
||||
|
||||
protected successRoute = "/vault";
|
||||
protected readonly Icons = {
|
||||
TwoFactorAuthSecurityKeyIcon,
|
||||
TwoFactorAuthSecurityKeyFailedIcon,
|
||||
};
|
||||
|
||||
private readonly successRoutes: Record<ClientType, string> = {
|
||||
[ClientType.Web]: "/vault",
|
||||
[ClientType.Browser]: "/tabs/vault",
|
||||
[ClientType.Desktop]: "/vault",
|
||||
[ClientType.Cli]: "/vault",
|
||||
};
|
||||
|
||||
protected get successRoute(): string {
|
||||
const clientType = this.platformUtilsService.getClientType();
|
||||
return this.successRoutes[clientType] || "/vault";
|
||||
}
|
||||
|
||||
constructor(
|
||||
private webAuthnLoginService: WebAuthnLoginServiceAbstraction,
|
||||
@@ -31,6 +73,8 @@ export class BaseLoginViaWebAuthnComponent implements OnInit {
|
||||
private i18nService: I18nService,
|
||||
private loginSuccessHandlerService: LoginSuccessHandlerService,
|
||||
private keyService: KeyService,
|
||||
private platformUtilsService: PlatformUtilsService,
|
||||
private anonLayoutWrapperDataService: AnonLayoutWrapperDataService,
|
||||
) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
@@ -41,6 +85,8 @@ export class BaseLoginViaWebAuthnComponent implements OnInit {
|
||||
|
||||
protected retry() {
|
||||
this.currentState = "assert";
|
||||
// Reset to default icon on retry
|
||||
this.setDefaultIcon();
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.authenticate();
|
||||
@@ -54,6 +100,7 @@ export class BaseLoginViaWebAuthnComponent implements OnInit {
|
||||
} catch (error) {
|
||||
this.validationService.showError(error);
|
||||
this.currentState = "assertFailed";
|
||||
this.setFailureIcon();
|
||||
return;
|
||||
}
|
||||
try {
|
||||
@@ -64,6 +111,7 @@ export class BaseLoginViaWebAuthnComponent implements OnInit {
|
||||
this.i18nService.t("twoFactorForPasskeysNotSupportedOnClientUpdateToLogIn"),
|
||||
);
|
||||
this.currentState = "assertFailed";
|
||||
this.setFailureIcon();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -80,6 +128,19 @@ export class BaseLoginViaWebAuthnComponent implements OnInit {
|
||||
}
|
||||
this.logService.error(error);
|
||||
this.currentState = "assertFailed";
|
||||
this.setFailureIcon();
|
||||
}
|
||||
}
|
||||
|
||||
private setDefaultIcon(): void {
|
||||
this.anonLayoutWrapperDataService.setAnonLayoutWrapperData({
|
||||
pageIcon: this.Icons.TwoFactorAuthSecurityKeyIcon,
|
||||
});
|
||||
}
|
||||
|
||||
private setFailureIcon(): void {
|
||||
this.anonLayoutWrapperDataService.setAnonLayoutWrapperData({
|
||||
pageIcon: this.Icons.TwoFactorAuthSecurityKeyFailedIcon,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { Component, Input, OnInit } from "@angular/core";
|
||||
import { firstValueFrom } from "rxjs";
|
||||
|
||||
import { LockIcon } from "@bitwarden/assets/svg";
|
||||
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop.
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import {
|
||||
@@ -23,7 +25,6 @@ import {
|
||||
AnonLayoutWrapperDataService,
|
||||
DialogService,
|
||||
ToastService,
|
||||
Icons,
|
||||
CalloutComponent,
|
||||
} from "@bitwarden/components";
|
||||
import { I18nPipe } from "@bitwarden/ui-common";
|
||||
@@ -41,7 +42,7 @@ import { ChangePasswordService } from "./change-password.service.abstraction";
|
||||
@Component({
|
||||
selector: "auth-change-password",
|
||||
templateUrl: "change-password.component.html",
|
||||
imports: [InputPasswordComponent, I18nPipe, CalloutComponent],
|
||||
imports: [InputPasswordComponent, I18nPipe, CalloutComponent, CommonModule],
|
||||
})
|
||||
export class ChangePasswordComponent implements OnInit {
|
||||
@Input() inputPasswordFlow: InputPasswordFlow = InputPasswordFlow.ChangePassword;
|
||||
@@ -96,13 +97,13 @@ export class ChangePasswordComponent implements OnInit {
|
||||
|
||||
if (this.forceSetPasswordReason === ForceSetPasswordReason.AdminForcePasswordReset) {
|
||||
this.anonLayoutWrapperDataService.setAnonLayoutWrapperData({
|
||||
pageIcon: Icons.LockIcon,
|
||||
pageIcon: LockIcon,
|
||||
pageTitle: { key: "updateMasterPassword" },
|
||||
pageSubtitle: { key: "accountRecoveryUpdateMasterPasswordSubtitle" },
|
||||
});
|
||||
} else if (this.forceSetPasswordReason === ForceSetPasswordReason.WeakMasterPassword) {
|
||||
this.anonLayoutWrapperDataService.setAnonLayoutWrapperData({
|
||||
pageIcon: Icons.LockIcon,
|
||||
pageIcon: LockIcon,
|
||||
pageTitle: { key: "updateMasterPassword" },
|
||||
pageSubtitle: { key: "updateMasterPasswordSubtitle" },
|
||||
maxWidth: "lg",
|
||||
@@ -177,6 +178,9 @@ export class ChangePasswordComponent implements OnInit {
|
||||
|
||||
// TODO: PM-23515 eventually use the logout service instead of messaging service once it is available without circular dependencies
|
||||
this.messagingService.send("logout");
|
||||
|
||||
// Close the popout if we are in a browser extension popout.
|
||||
this.changePasswordService.closeBrowserExtensionPopout?.();
|
||||
}
|
||||
} catch (error) {
|
||||
this.logService.error(error);
|
||||
|
||||
@@ -59,4 +59,10 @@ export abstract class ChangePasswordService {
|
||||
* - Currently only used on the web change password service.
|
||||
*/
|
||||
clearDeeplinkState?: () => Promise<void>;
|
||||
|
||||
/**
|
||||
* Optional method that closes the browser extension popout if in a popout
|
||||
* If not in a popout, does nothing.
|
||||
*/
|
||||
abstract closeBrowserExtensionPopout?(): void;
|
||||
}
|
||||
|
||||
@@ -5,8 +5,8 @@ import { mock, MockProxy } from "jest-mock-extended";
|
||||
import { PasswordInputResult } from "@bitwarden/auth/angular";
|
||||
import { Account } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { MasterPasswordApiService } from "@bitwarden/common/auth/abstractions/master-password-api.service.abstraction";
|
||||
import { EncString } from "@bitwarden/common/key-management/crypto/models/enc-string";
|
||||
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction";
|
||||
import { EncString } from "@bitwarden/common/platform/models/domain/enc-string";
|
||||
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { MasterKey, UserKey } from "@bitwarden/common/types/key";
|
||||
|
||||
@@ -5,8 +5,8 @@ import { Account } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { MasterPasswordApiService } from "@bitwarden/common/auth/abstractions/master-password-api.service.abstraction";
|
||||
import { PasswordRequest } from "@bitwarden/common/auth/models/request/password.request";
|
||||
import { UpdateTempPasswordRequest } from "@bitwarden/common/auth/models/request/update-temp-password.request";
|
||||
import { EncString } from "@bitwarden/common/key-management/crypto/models/enc-string";
|
||||
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction";
|
||||
import { EncString } from "@bitwarden/common/platform/models/domain/enc-string";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { UserKey } from "@bitwarden/common/types/key";
|
||||
import { KeyService } from "@bitwarden/key-management";
|
||||
|
||||
@@ -16,11 +16,11 @@ import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/for
|
||||
import { SetPasswordRequest } from "@bitwarden/common/auth/models/request/set-password.request";
|
||||
import { UpdateTdeOffboardingPasswordRequest } from "@bitwarden/common/auth/models/request/update-tde-offboarding-password.request";
|
||||
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||
import { EncString } from "@bitwarden/common/key-management/crypto/models/enc-string";
|
||||
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction";
|
||||
import { KeysRequest } from "@bitwarden/common/models/request/keys.request";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { EncString } from "@bitwarden/common/platform/models/domain/enc-string";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { MasterKey, UserKey } from "@bitwarden/common/types/key";
|
||||
import { KdfConfigService, KeyService, KdfConfig } from "@bitwarden/key-management";
|
||||
@@ -137,8 +137,7 @@ export class DefaultSetInitialPasswordService implements SetInitialPasswordServi
|
||||
newPasswordHint,
|
||||
orgSsoIdentifier,
|
||||
keysRequest,
|
||||
kdfConfig.kdfType,
|
||||
kdfConfig.iterations,
|
||||
kdfConfig,
|
||||
);
|
||||
|
||||
await this.masterPasswordApiService.setPassword(request);
|
||||
|
||||
@@ -21,11 +21,14 @@ import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/for
|
||||
import { SetPasswordRequest } from "@bitwarden/common/auth/models/request/set-password.request";
|
||||
import { UpdateTdeOffboardingPasswordRequest } from "@bitwarden/common/auth/models/request/update-tde-offboarding-password.request";
|
||||
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||
import {
|
||||
EncryptedString,
|
||||
EncString,
|
||||
} from "@bitwarden/common/key-management/crypto/models/enc-string";
|
||||
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction";
|
||||
import { KeysRequest } from "@bitwarden/common/models/request/keys.request";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { EncryptedString, EncString } from "@bitwarden/common/platform/models/domain/enc-string";
|
||||
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
||||
import { CsprngArray } from "@bitwarden/common/types/csprng";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
@@ -154,8 +157,7 @@ describe("DefaultSetInitialPasswordService", () => {
|
||||
credentials.newPasswordHint,
|
||||
credentials.orgSsoIdentifier,
|
||||
keysRequest,
|
||||
credentials.kdfConfig.kdfType,
|
||||
credentials.kdfConfig.iterations,
|
||||
credentials.kdfConfig,
|
||||
);
|
||||
|
||||
enrollmentRequest = new OrganizationUserResetPasswordEnrollmentRequest();
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { Component, OnInit } from "@angular/core";
|
||||
import { ActivatedRoute, Router } from "@angular/router";
|
||||
// import { NoAccess } from "libs/components/src/icon/icons";
|
||||
import { firstValueFrom } from "rxjs";
|
||||
|
||||
import { DeactivatedOrg } from "@bitwarden/assets/svg";
|
||||
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop.
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import {
|
||||
@@ -35,7 +35,6 @@ import {
|
||||
CalloutComponent,
|
||||
DialogService,
|
||||
ToastService,
|
||||
Icons,
|
||||
} from "@bitwarden/components";
|
||||
import { I18nPipe } from "@bitwarden/ui-common";
|
||||
|
||||
@@ -113,7 +112,7 @@ export class SetInitialPasswordComponent implements OnInit {
|
||||
this.userType = SetInitialPasswordUserType.OFFBOARDED_TDE_ORG_USER_UNTRUSTED_DEVICE;
|
||||
this.anonLayoutWrapperDataService.setAnonLayoutWrapperData({
|
||||
pageTitle: { key: "unableToCompleteLogin" },
|
||||
pageIcon: Icons.NoAccess,
|
||||
pageIcon: DeactivatedOrg,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
<form [formGroup]="formGroup" [bitSubmit]="submit">
|
||||
<bit-dialog [title]="'addCredit' | i18n">
|
||||
<ng-container bitDialogContent>
|
||||
<p bitTypography="body1">{{ "creditDelayed" | i18n }}</p>
|
||||
<div class="tw-grid tw-grid-cols-2">
|
||||
<bit-radio-group formControlName="paymentMethod">
|
||||
<bit-radio-button [value]="paymentMethodType.PayPal">
|
||||
<bit-label> <i class="bwi bwi-paypal"></i>{{ "payPal" | i18n }}</bit-label>
|
||||
</bit-radio-button>
|
||||
<bit-radio-button [value]="paymentMethodType.BitPay">
|
||||
<bit-label> <i class="bwi bwi-bitcoin"></i>{{ "bitcoin" | i18n }}</bit-label>
|
||||
</bit-radio-button>
|
||||
</bit-radio-group>
|
||||
</div>
|
||||
<div class="tw-grid tw-grid-cols-2">
|
||||
<bit-form-field>
|
||||
<bit-label>{{ "amount" | i18n }}</bit-label>
|
||||
<input bitInput type="number" formControlName="creditAmount" step="0.01" required />
|
||||
<span bitPrefix>$USD</span>
|
||||
</bit-form-field>
|
||||
</div>
|
||||
</ng-container>
|
||||
<ng-container bitDialogFooter>
|
||||
<button type="submit" bitButton bitFormButton buttonType="primary">
|
||||
{{ "submit" | i18n }}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
bitButton
|
||||
bitFormButton
|
||||
buttonType="secondary"
|
||||
[bitDialogClose]="ResultType.Closed"
|
||||
>
|
||||
{{ "cancel" | i18n }}
|
||||
</button>
|
||||
</ng-container>
|
||||
</bit-dialog>
|
||||
</form>
|
||||
<form #payPalForm action="{{ payPalConfig.buttonAction }}" method="post" target="_top">
|
||||
<input type="hidden" name="cmd" value="_xclick" />
|
||||
<input type="hidden" name="business" value="{{ payPalConfig.businessId }}" />
|
||||
<input type="hidden" name="button_subtype" value="services" />
|
||||
<input type="hidden" name="no_note" value="1" />
|
||||
<input type="hidden" name="no_shipping" value="1" />
|
||||
<input type="hidden" name="rm" value="1" />
|
||||
<input type="hidden" name="return" value="{{ payPalConfig.returnUrl }}" />
|
||||
<input type="hidden" name="cancel_return" value="{{ payPalConfig.returnUrl }}" />
|
||||
<input type="hidden" name="currency_code" value="USD" />
|
||||
<input type="hidden" name="image_url" value="https://bitwarden.com/images/paypal-banner.png" />
|
||||
<input type="hidden" name="bn" value="PP-BuyNowBF:btn_buynow_LG.gif:NonHosted" />
|
||||
<input type="hidden" name="amount" value="{{ formGroup.get('creditAmount').value }}" />
|
||||
<input type="hidden" name="custom" value="{{ payPalConfig.customField }}" />
|
||||
<input type="hidden" name="item_name" value="Bitwarden Account Credit" />
|
||||
<input type="hidden" name="item_number" value="{{ payPalConfig.subject }}" />
|
||||
</form>
|
||||
@@ -1,165 +0,0 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Component, ElementRef, Inject, OnInit, ViewChild } from "@angular/core";
|
||||
import { FormControl, FormGroup, Validators } from "@angular/forms";
|
||||
import { firstValueFrom, map } from "rxjs";
|
||||
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { ProviderService } from "@bitwarden/common/admin-console/abstractions/provider.service";
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { Provider } from "@bitwarden/common/admin-console/models/domain/provider";
|
||||
import { AccountService, AccountInfo } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { PaymentMethodType } from "@bitwarden/common/billing/enums";
|
||||
import { BitPayInvoiceRequest } from "@bitwarden/common/billing/models/request/bit-pay-invoice.request";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { DIALOG_DATA, DialogConfig, DialogRef, DialogService } from "@bitwarden/components";
|
||||
|
||||
export type AddAccountCreditDialogParams = {
|
||||
organizationId?: string;
|
||||
providerId?: string;
|
||||
};
|
||||
|
||||
// FIXME: update to use a const object instead of a typescript enum
|
||||
// eslint-disable-next-line @bitwarden/platform/no-enums
|
||||
export enum AddAccountCreditDialogResultType {
|
||||
Closed = "closed",
|
||||
Submitted = "submitted",
|
||||
}
|
||||
|
||||
export const openAddAccountCreditDialog = (
|
||||
dialogService: DialogService,
|
||||
dialogConfig: DialogConfig<AddAccountCreditDialogParams>,
|
||||
) =>
|
||||
dialogService.open<AddAccountCreditDialogResultType, AddAccountCreditDialogParams>(
|
||||
AddAccountCreditDialogComponent,
|
||||
dialogConfig,
|
||||
);
|
||||
|
||||
type PayPalConfig = {
|
||||
businessId?: string;
|
||||
buttonAction?: string;
|
||||
returnUrl?: string;
|
||||
customField?: string;
|
||||
subject?: string;
|
||||
};
|
||||
|
||||
@Component({
|
||||
templateUrl: "./add-account-credit-dialog.component.html",
|
||||
standalone: false,
|
||||
})
|
||||
export class AddAccountCreditDialogComponent implements OnInit {
|
||||
@ViewChild("payPalForm", { read: ElementRef, static: true }) payPalForm: ElementRef;
|
||||
protected formGroup = new FormGroup({
|
||||
paymentMethod: new FormControl<PaymentMethodType>(PaymentMethodType.PayPal),
|
||||
creditAmount: new FormControl<number>(null, [Validators.required, Validators.min(0.01)]),
|
||||
});
|
||||
protected payPalConfig: PayPalConfig;
|
||||
protected ResultType = AddAccountCreditDialogResultType;
|
||||
|
||||
private organization?: Organization;
|
||||
private provider?: Provider;
|
||||
private user?: { id: UserId } & AccountInfo;
|
||||
|
||||
constructor(
|
||||
private accountService: AccountService,
|
||||
private apiService: ApiService,
|
||||
private configService: ConfigService,
|
||||
@Inject(DIALOG_DATA) private dialogParams: AddAccountCreditDialogParams,
|
||||
private dialogRef: DialogRef<AddAccountCreditDialogResultType>,
|
||||
private organizationService: OrganizationService,
|
||||
private platformUtilsService: PlatformUtilsService,
|
||||
private providerService: ProviderService,
|
||||
) {
|
||||
this.payPalConfig = process.env.PAYPAL_CONFIG as PayPalConfig;
|
||||
}
|
||||
|
||||
protected readonly paymentMethodType = PaymentMethodType;
|
||||
|
||||
submit = async () => {
|
||||
this.formGroup.markAllAsTouched();
|
||||
|
||||
if (this.formGroup.invalid) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.formGroup.value.paymentMethod === PaymentMethodType.PayPal) {
|
||||
this.payPalForm.nativeElement.submit();
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.formGroup.value.paymentMethod === PaymentMethodType.BitPay) {
|
||||
const request = this.getBitPayInvoiceRequest();
|
||||
const bitPayUrl = await this.apiService.postBitPayInvoice(request);
|
||||
this.platformUtilsService.launchUri(bitPayUrl);
|
||||
return;
|
||||
}
|
||||
|
||||
this.dialogRef.close(AddAccountCreditDialogResultType.Submitted);
|
||||
};
|
||||
|
||||
async ngOnInit(): Promise<void> {
|
||||
let payPalCustomField: string;
|
||||
|
||||
if (this.dialogParams.organizationId) {
|
||||
this.formGroup.patchValue({
|
||||
creditAmount: 20.0,
|
||||
});
|
||||
this.user = await firstValueFrom(this.accountService.activeAccount$);
|
||||
this.organization = await firstValueFrom(
|
||||
this.organizationService
|
||||
.organizations$(this.user.id)
|
||||
.pipe(
|
||||
map((organizations) =>
|
||||
organizations.find((org) => org.id === this.dialogParams.organizationId),
|
||||
),
|
||||
),
|
||||
);
|
||||
payPalCustomField = "organization_id:" + this.organization.id;
|
||||
this.payPalConfig.subject = this.organization.name;
|
||||
} else if (this.dialogParams.providerId) {
|
||||
this.formGroup.patchValue({
|
||||
creditAmount: 20.0,
|
||||
});
|
||||
this.provider = await this.providerService.get(this.dialogParams.providerId);
|
||||
payPalCustomField = "provider_id:" + this.provider.id;
|
||||
this.payPalConfig.subject = this.provider.name;
|
||||
} else {
|
||||
this.formGroup.patchValue({
|
||||
creditAmount: 10.0,
|
||||
});
|
||||
payPalCustomField = "user_id:" + this.user.id;
|
||||
this.payPalConfig.subject = this.user.email;
|
||||
}
|
||||
|
||||
const region = await firstValueFrom(this.configService.cloudRegion$);
|
||||
|
||||
payPalCustomField += ",account_credit:1";
|
||||
payPalCustomField += `,region:${region}`;
|
||||
|
||||
this.payPalConfig.customField = payPalCustomField;
|
||||
this.payPalConfig.returnUrl = window.location.href;
|
||||
}
|
||||
|
||||
getBitPayInvoiceRequest(): BitPayInvoiceRequest {
|
||||
const request = new BitPayInvoiceRequest();
|
||||
if (this.organization) {
|
||||
request.name = this.organization.name;
|
||||
request.organizationId = this.organization.id;
|
||||
} else if (this.provider) {
|
||||
request.name = this.provider.name;
|
||||
request.providerId = this.provider.id;
|
||||
} else {
|
||||
request.email = this.user.email;
|
||||
request.userId = this.user.id;
|
||||
}
|
||||
|
||||
request.credit = true;
|
||||
request.amount = this.formGroup.value.creditAmount;
|
||||
request.returnUrl = window.location.href;
|
||||
|
||||
return request;
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1 @@
|
||||
export * from "./add-account-credit-dialog/add-account-credit-dialog.component";
|
||||
export * from "./invoices/invoices.component";
|
||||
export * from "./invoices/no-invoices.component";
|
||||
export * from "./manage-tax-information/manage-tax-information.component";
|
||||
export * from "./premium.component";
|
||||
|
||||
@@ -1,60 +0,0 @@
|
||||
<ng-container *ngIf="loading">
|
||||
<i
|
||||
class="bwi bwi-spinner bwi-spin tw-text-muted"
|
||||
title="{{ 'loading' | i18n }}"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
<span class="tw-sr-only">{{ "loading" | i18n }}</span>
|
||||
</ng-container>
|
||||
<bit-table *ngIf="!loading">
|
||||
<ng-container header>
|
||||
<tr>
|
||||
<th bitCell>{{ "date" | i18n }}</th>
|
||||
<th bitCell>{{ "invoice" | i18n }}</th>
|
||||
<th bitCell>{{ "total" | i18n }}</th>
|
||||
<th bitCell>{{ "status" | i18n }}</th>
|
||||
<th bitCell>{{ "clientDetails" | i18n }}</th>
|
||||
</tr>
|
||||
</ng-container>
|
||||
<ng-template body>
|
||||
<tr bitRow *ngFor="let invoice of invoices">
|
||||
<td bitCell>{{ invoice.date | date: "mediumDate" }}</td>
|
||||
<td bitCell>
|
||||
<a
|
||||
href="{{ invoice.url }}"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
title="{{ 'viewInvoice' | i18n }}"
|
||||
>
|
||||
{{ invoice.number }}
|
||||
</a>
|
||||
</td>
|
||||
<td bitCell>{{ invoice.total | currency: "$" }}</td>
|
||||
<td bitCell *ngIf="expandInvoiceStatus(invoice) as expandedInvoiceStatus">
|
||||
<span *ngIf="expandedInvoiceStatus === 'open'">
|
||||
{{ "open" | i18n | titlecase }}
|
||||
</span>
|
||||
<span *ngIf="expandedInvoiceStatus === 'unpaid'">
|
||||
<i class="bwi bwi-error tw-text-muted" aria-hidden="true"></i>
|
||||
{{ "unpaid" | i18n | titlecase }}
|
||||
</span>
|
||||
<span *ngIf="expandedInvoiceStatus === 'paid'">
|
||||
<i class="bwi bwi-check tw-text-success" aria-hidden="true"></i>
|
||||
{{ "paid" | i18n | titlecase }}
|
||||
</span>
|
||||
<span *ngIf="expandedInvoiceStatus === 'uncollectible'">
|
||||
<i class="bwi bwi-error tw-text-muted" aria-hidden="true"></i>
|
||||
{{ "uncollectible" | i18n | titlecase }}
|
||||
</span>
|
||||
</td>
|
||||
<td bitCell>
|
||||
<button type="button" bitLink (click)="runExport(invoice.id)">
|
||||
<span class="tw-font-normal">{{ "downloadCSV" | i18n }}</span>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
</bit-table>
|
||||
<div *ngIf="!invoices || invoices.length === 0" class="tw-mt-10">
|
||||
<app-no-invoices></app-no-invoices>
|
||||
</div>
|
||||
@@ -1,67 +0,0 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Component, Input, OnInit } from "@angular/core";
|
||||
|
||||
import {
|
||||
InvoiceResponse,
|
||||
InvoicesResponse,
|
||||
} from "@bitwarden/common/billing/models/response/invoices.response";
|
||||
import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service";
|
||||
|
||||
@Component({
|
||||
selector: "app-invoices",
|
||||
templateUrl: "./invoices.component.html",
|
||||
standalone: false,
|
||||
})
|
||||
export class InvoicesComponent implements OnInit {
|
||||
@Input() startWith?: InvoicesResponse;
|
||||
@Input() getInvoices?: () => Promise<InvoicesResponse>;
|
||||
@Input() getClientInvoiceReport?: (invoiceId: string) => Promise<string>;
|
||||
@Input() getClientInvoiceReportName?: (invoiceResponse: InvoiceResponse) => string;
|
||||
|
||||
protected invoices: InvoiceResponse[] = [];
|
||||
protected loading = true;
|
||||
|
||||
constructor(private fileDownloadService: FileDownloadService) {}
|
||||
|
||||
runExport = async (invoiceId: string): Promise<void> => {
|
||||
const blobData = await this.getClientInvoiceReport(invoiceId);
|
||||
let fileName = "report.csv";
|
||||
if (this.getClientInvoiceReportName) {
|
||||
const invoice = this.invoices.find((invoice) => invoice.id === invoiceId);
|
||||
fileName = this.getClientInvoiceReportName(invoice);
|
||||
}
|
||||
this.fileDownloadService.download({
|
||||
fileName,
|
||||
blobData,
|
||||
blobOptions: {
|
||||
type: "text/csv",
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
async ngOnInit(): Promise<void> {
|
||||
if (this.startWith) {
|
||||
this.invoices = this.startWith.invoices;
|
||||
} else if (this.getInvoices) {
|
||||
const response = await this.getInvoices();
|
||||
this.invoices = response.invoices;
|
||||
}
|
||||
this.loading = false;
|
||||
}
|
||||
|
||||
expandInvoiceStatus = (
|
||||
invoice: InvoiceResponse,
|
||||
): "open" | "unpaid" | "paid" | "uncollectible" => {
|
||||
switch (invoice.status) {
|
||||
case "open": {
|
||||
const dueDate = new Date(invoice.dueDate);
|
||||
return dueDate < new Date() ? "unpaid" : invoice.status;
|
||||
}
|
||||
case "paid":
|
||||
case "uncollectible": {
|
||||
return invoice.status;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
import { Component } from "@angular/core";
|
||||
|
||||
import { svgIcon } from "@bitwarden/components";
|
||||
|
||||
const partnerTrustIcon = svgIcon`
|
||||
<svg width="120" height="120" viewBox="0 0 120 120" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M69 43.6511C70.1025 44.1328 71.32 44.4 72.6 44.4C77.5706 44.4 81.6 40.3706 81.6 35.4C81.6 30.4294 77.5706 26.4 72.6 26.4C67.6295 26.4 63.6 30.4294 63.6 35.4C63.6 36.0164 63.662 36.6184 63.7801 37.2" stroke="#CED4DC" stroke-width="2"/>
|
||||
<path d="M69 44.8416C70.1522 44.553 71.3559 44.4 72.5943 44.4C80.1533 44.4 86.4194 50.0998 87.5598 57.5529C87.7996 59.1204 86.9541 60 85.3451 60C79.7231 60 75.6474 60 71.4 60" stroke="#CED4DC" stroke-width="2"/>
|
||||
<path d="M51 43.6511C49.8976 44.1328 48.68 44.4 47.4 44.4C42.4295 44.4 38.4 40.3706 38.4 35.4C38.4 30.4294 42.4295 26.4 47.4 26.4C52.3706 26.4 56.4 30.4294 56.4 35.4C56.4 36.0164 56.338 36.6184 56.22 37.2" stroke="#CED4DC" stroke-width="2"/>
|
||||
<path d="M51 44.8416C49.8478 44.553 48.6441 44.4 47.4057 44.4C39.8467 44.4 33.5806 50.0998 32.4402 57.5529C32.2004 59.1204 33.0459 60 34.6549 60C40.2769 60 44.3526 60 48.6 60" stroke="#CED4DC" stroke-width="2"/>
|
||||
<circle cx="60" cy="45.6" r="9" stroke="#CED4DC" stroke-width="2"/>
|
||||
<path d="M72.7451 70.2C62.3773 70.2 57.2682 70.2 46.6437 70.2C45.3864 70.2 44.8665 68.8141 45.0289 67.7529C46.1693 60.2998 52.4354 54.6 59.9943 54.6C67.5533 54.6 73.8194 60.2998 74.9598 67.7529C75.1996 69.3204 74.3541 70.2 72.7451 70.2Z" stroke="#CED4DC" stroke-width="2"/>
|
||||
<path d="M73.557 103.195C74.3197 104.319 74.3197 105.443 73.557 106.272C73.1462 106.745 72.3835 107.041 71.738 107.041C71.6242 107.041 71.5013 107.032 71.3801 107.011C71.3345 108.505 70.5896 109.217 70.0948 109.467C69.7427 109.703 69.3318 109.822 68.9212 109.822C68.8703 109.822 68.8204 109.82 68.7714 109.816C68.7035 109.811 68.6373 109.803 68.5729 109.792C68.5559 110.332 68.3591 110.827 67.9823 111.242C67.6302 111.833 66.926 112.129 66.2808 112.129C66.1045 112.129 65.9874 112.129 65.8112 112.07C65.7753 112.058 65.7381 112.044 65.6997 112.028C65.5215 112.624 65.0169 113.105 64.227 113.431C63.9924 113.49 63.8162 113.49 63.5815 113.49C62.4417 113.49 60.9477 112.633 60.0352 112.028M55.5 88.7567C53.6867 88.9991 51.6246 89.9258 50.4378 90.4744C50.2985 90.5095 50.1797 90.5655 50.0693 90.6176L50.0687 90.6179C49.9937 90.6534 49.9223 90.6871 49.851 90.711C49.5281 90.5373 49.0708 90.3955 48.4995 90.2184C47.5139 89.9127 46.1886 89.5016 44.6287 88.6402C44.3941 88.5219 44.0419 88.581 43.8657 88.8769L37.6461 99.5268C37.4699 99.8226 37.5287 100.178 37.822 100.355L46.8 105.019" stroke="#CED4DC" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M73.7624 103.371C73.6448 103.371 73.4683 103.313 73.3507 103.195L67.7625 97.9108C63.1743 96.7365 61.8803 93.9181 61.4097 92.8024C61.0567 92.8612 60.5273 92.9199 59.645 92.9199C59.2332 93.2135 58.1744 94.0355 56.7039 94.7988C55.3509 95.5034 53.998 95.2098 53.4098 94.2117C53.0568 93.5658 52.998 92.8024 53.351 92.1566C55.7039 87.5766 58.4685 86.6372 60.3509 86.6372C61.4685 86.6372 62.1744 86.9895 62.2332 86.9895L69.2919 90.1015L75.7624 87.1656C75.8801 87.1069 76.0565 87.1069 76.233 87.1656C76.4095 87.2243 76.5271 87.3418 76.5859 87.4592L82.6447 97.8521C82.7623 98.1457 82.6447 98.4393 82.4094 98.6154L74.0565 103.313C73.9389 103.371 73.8801 103.371 73.7624 103.371Z" stroke="#CED4DC" stroke-width="2"/>
|
||||
<path d="M46.6097 107.206C47.0221 108.443 48.394 109.005 49.044 109.195C49.217 109.923 49.5794 110.391 49.9178 110.692C50.5086 111.164 51.1584 111.342 51.7491 111.342C51.9248 111.342 52.0796 111.321 52.2135 111.291C52.2516 111.368 52.2936 111.444 52.3399 111.519C52.8716 112.346 53.8168 112.818 55.2346 112.937C55.3143 112.937 55.3897 112.919 55.4573 112.887C56.0276 113.588 56.9845 114 57.893 114C58.7791 114 59.488 113.527 59.7243 112.759C60.0787 111.223 59.9606 109.628 59.7243 108.742C59.5602 108.031 59.1935 106.763 57.6392 106.673C57.4017 106.121 56.803 105.205 55.4709 105.08C55.1879 105.053 54.9235 105.086 54.6774 105.164C54.4496 104.62 54.0132 103.917 53.2851 103.662C52.7534 103.485 51.9264 103.662 51.1584 104.134C51.1181 104.161 51.0782 104.187 51.0387 104.215C50.6899 103.795 50.1003 103.263 49.3271 103.189C48.6182 103.13 48.0275 103.485 47.4367 104.193C46.5506 105.316 46.3143 106.32 46.6097 107.206Z" stroke="#CED4DC" stroke-width="2"/>
|
||||
<path d="M51.9 104.4C51.5709 104.603 49.9229 105.531 49.5 106.8C49.2 107.7 49.5 108.9 49.8 109.5" stroke="#CED4DC" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M54.6703 105.317C54.1767 105.571 53.0709 106.485 52.597 108.109C52.1231 109.734 52.3995 110.816 52.597 111.155" stroke="#CED4DC" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M57.2 107.263C56.7063 107.517 56.051 108.431 55.7104 109.598C55.3697 110.766 55.5129 112.179 55.7104 112.517" stroke="#CED4DC" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M65.5675 111.739C64.0108 110.96 61.2866 108.431 61.2866 108.431" stroke="#CED4DC" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M68.2914 109.598C66.1686 108.591 62.4538 105.317 62.4538 105.317" stroke="#CED4DC" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M71.4049 106.874C68.9991 105.775 64.789 102.204 64.789 102.204" stroke="#CED4DC" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M86.9971 91.8C95.5145 86.0616 101.962 77.753 105.392 68.094C108.822 58.435 109.055 47.9345 106.056 38.134C103.057 28.3336 96.9838 19.7494 88.7289 13.6419C80.474 7.53438 70.4717 4.22518 60.1907 4.20014C49.9096 4.1751 39.8913 7.43554 31.6065 13.5028C23.3217 19.5701 17.2069 28.1245 14.1599 37.9102C11.1128 47.696 11.294 58.1975 14.6768 67.8731C18.0596 77.5486 24.4658 85.8885 32.955 91.6684" stroke="#CED4DC" stroke-width="2" stroke-linecap="round"/>
|
||||
<path d="M84.6771 88.2C92.4374 82.9725 98.3114 75.4037 101.437 66.6048C104.562 57.8059 104.774 48.2403 102.042 39.3125C99.3091 30.3847 93.7761 22.5649 86.255 17.0012C78.7338 11.4375 69.6207 8.42293 60.2535 8.40012C50.8863 8.37731 41.7585 11.3474 34.2101 16.8745C26.6618 22.4015 21.0905 30.1942 18.3143 39.1086C15.5381 48.023 15.7032 57.5895 18.7853 66.4035C21.8674 75.2176 27.7042 82.8149 35.4388 88.0801" stroke="#CED4DC" stroke-linecap="round"/>
|
||||
</svg>
|
||||
`;
|
||||
|
||||
@Component({
|
||||
selector: "app-no-invoices",
|
||||
template: `<div class="tw-flex tw-flex-col tw-items-center tw-text-info">
|
||||
<bit-icon [icon]="icon"></bit-icon>
|
||||
<p class="tw-mt-4">{{ "noInvoicesToList" | i18n }}</p>
|
||||
</div>`,
|
||||
standalone: false,
|
||||
})
|
||||
export class NoInvoicesComponent {
|
||||
icon = partnerTrustIcon;
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
<form [formGroup]="formGroup" [bitSubmit]="submit">
|
||||
<div class="tw-grid tw-grid-cols-12 tw-gap-4">
|
||||
<div class="tw-col-span-6">
|
||||
<bit-form-field disableMargin>
|
||||
<bit-label>{{ "country" | i18n }}</bit-label>
|
||||
<bit-select formControlName="country">
|
||||
<bit-option
|
||||
*ngFor="let country of countries"
|
||||
[value]="country.value"
|
||||
[disabled]="country.disabled"
|
||||
[label]="country.name"
|
||||
></bit-option>
|
||||
</bit-select>
|
||||
</bit-form-field>
|
||||
</div>
|
||||
<div class="tw-col-span-6">
|
||||
<bit-form-field disableMargin>
|
||||
<bit-label>{{ "zipPostalCode" | i18n }}</bit-label>
|
||||
<input bitInput type="text" formControlName="postalCode" autocomplete="postal-code" />
|
||||
</bit-form-field>
|
||||
</div>
|
||||
<ng-container *ngIf="isTaxSupported">
|
||||
<div class="tw-col-span-6">
|
||||
<bit-form-field disableMargin>
|
||||
<bit-label>{{ "address1" | i18n }}</bit-label>
|
||||
<input bitInput type="text" formControlName="line1" autocomplete="address-line1" />
|
||||
</bit-form-field>
|
||||
</div>
|
||||
<div class="tw-col-span-6">
|
||||
<bit-form-field disableMargin>
|
||||
<bit-label>{{ "address2" | i18n }}</bit-label>
|
||||
<input bitInput type="text" formControlName="line2" autocomplete="address-line2" />
|
||||
</bit-form-field>
|
||||
</div>
|
||||
<div class="tw-col-span-6">
|
||||
<bit-form-field disableMargin>
|
||||
<bit-label>{{ "cityTown" | i18n }}</bit-label>
|
||||
<input bitInput type="text" formControlName="city" autocomplete="address-level2" />
|
||||
</bit-form-field>
|
||||
</div>
|
||||
<div class="tw-col-span-6">
|
||||
<bit-form-field disableMargin>
|
||||
<bit-label>{{ "stateProvince" | i18n }}</bit-label>
|
||||
<input bitInput type="text" formControlName="state" autocomplete="address-level1" />
|
||||
</bit-form-field>
|
||||
</div>
|
||||
<div class="tw-col-span-6" *ngIf="showTaxIdField">
|
||||
<bit-form-field disableMargin>
|
||||
<bit-label>{{ "taxIdNumber" | i18n }}</bit-label>
|
||||
<input bitInput type="text" formControlName="taxId" />
|
||||
</bit-form-field>
|
||||
</div>
|
||||
</ng-container>
|
||||
<div class="tw-col-span-12" *ngIf="!!onSubmit">
|
||||
<button bitButton bitFormButton buttonType="primary" type="submit">
|
||||
{{ "submit" | i18n }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
@@ -1,149 +0,0 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from "@angular/core";
|
||||
import { FormBuilder, Validators } from "@angular/forms";
|
||||
import { Subject, takeUntil } from "rxjs";
|
||||
import { debounceTime } from "rxjs/operators";
|
||||
|
||||
import { TaxServiceAbstraction } from "@bitwarden/common/billing/abstractions/tax.service.abstraction";
|
||||
import { CountryListItem, TaxInformation } from "@bitwarden/common/billing/models/domain";
|
||||
|
||||
@Component({
|
||||
selector: "app-manage-tax-information",
|
||||
templateUrl: "./manage-tax-information.component.html",
|
||||
standalone: false,
|
||||
})
|
||||
export class ManageTaxInformationComponent implements OnInit, OnDestroy {
|
||||
@Input() startWith: TaxInformation;
|
||||
@Input() onSubmit?: (taxInformation: TaxInformation) => Promise<void>;
|
||||
@Input() showTaxIdField: boolean = true;
|
||||
|
||||
/**
|
||||
* Emits when the tax information has changed.
|
||||
*/
|
||||
@Output() taxInformationChanged = new EventEmitter<TaxInformation>();
|
||||
|
||||
/**
|
||||
* Emits when the tax information has been updated.
|
||||
*/
|
||||
@Output() taxInformationUpdated = new EventEmitter();
|
||||
|
||||
private taxInformation: TaxInformation;
|
||||
|
||||
protected formGroup = this.formBuilder.group({
|
||||
country: ["", Validators.required],
|
||||
postalCode: ["", Validators.required],
|
||||
taxId: "",
|
||||
line1: "",
|
||||
line2: "",
|
||||
city: "",
|
||||
state: "",
|
||||
});
|
||||
|
||||
protected isTaxSupported: boolean;
|
||||
|
||||
private destroy$ = new Subject<void>();
|
||||
|
||||
protected readonly countries: CountryListItem[] = this.taxService.getCountries();
|
||||
|
||||
constructor(
|
||||
private formBuilder: FormBuilder,
|
||||
private taxService: TaxServiceAbstraction,
|
||||
) {}
|
||||
|
||||
getTaxInformation(): TaxInformation {
|
||||
return this.taxInformation;
|
||||
}
|
||||
|
||||
submit = async () => {
|
||||
this.formGroup.markAllAsTouched();
|
||||
if (this.formGroup.invalid) {
|
||||
return;
|
||||
}
|
||||
await this.onSubmit?.(this.taxInformation);
|
||||
this.taxInformationUpdated.emit();
|
||||
};
|
||||
|
||||
validate(): boolean {
|
||||
this.formGroup.markAllAsTouched();
|
||||
return this.formGroup.valid;
|
||||
}
|
||||
|
||||
markAllAsTouched() {
|
||||
this.formGroup.markAllAsTouched();
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
this.formGroup.valueChanges.pipe(takeUntil(this.destroy$)).subscribe((values) => {
|
||||
this.taxInformation = {
|
||||
country: values.country,
|
||||
postalCode: values.postalCode,
|
||||
taxId: values.taxId,
|
||||
line1: values.line1,
|
||||
line2: values.line2,
|
||||
city: values.city,
|
||||
state: values.state,
|
||||
};
|
||||
});
|
||||
|
||||
if (this.startWith) {
|
||||
this.formGroup.controls.country.setValue(this.startWith.country);
|
||||
this.formGroup.controls.postalCode.setValue(this.startWith.postalCode);
|
||||
|
||||
this.isTaxSupported =
|
||||
this.startWith && this.startWith.country
|
||||
? await this.taxService.isCountrySupported(this.startWith.country)
|
||||
: false;
|
||||
|
||||
if (this.isTaxSupported) {
|
||||
this.formGroup.controls.taxId.setValue(this.startWith.taxId);
|
||||
this.formGroup.controls.line1.setValue(this.startWith.line1);
|
||||
this.formGroup.controls.line2.setValue(this.startWith.line2);
|
||||
this.formGroup.controls.city.setValue(this.startWith.city);
|
||||
this.formGroup.controls.state.setValue(this.startWith.state);
|
||||
}
|
||||
}
|
||||
|
||||
this.formGroup.controls.country.valueChanges
|
||||
.pipe(debounceTime(1000), takeUntil(this.destroy$))
|
||||
.subscribe((country: string) => {
|
||||
this.taxService
|
||||
.isCountrySupported(country)
|
||||
.then((isSupported) => (this.isTaxSupported = isSupported))
|
||||
.catch(() => (this.isTaxSupported = false))
|
||||
.finally(() => {
|
||||
if (!this.isTaxSupported) {
|
||||
this.formGroup.controls.taxId.setValue(null);
|
||||
this.formGroup.controls.line1.setValue(null);
|
||||
this.formGroup.controls.line2.setValue(null);
|
||||
this.formGroup.controls.city.setValue(null);
|
||||
this.formGroup.controls.state.setValue(null);
|
||||
}
|
||||
if (this.taxInformationChanged) {
|
||||
this.taxInformationChanged.emit(this.taxInformation);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
this.formGroup.controls.postalCode.valueChanges
|
||||
.pipe(debounceTime(1000), takeUntil(this.destroy$))
|
||||
.subscribe(() => {
|
||||
if (this.taxInformationChanged) {
|
||||
this.taxInformationChanged.emit(this.taxInformation);
|
||||
}
|
||||
});
|
||||
|
||||
this.formGroup.controls.taxId.valueChanges
|
||||
.pipe(debounceTime(1000), takeUntil(this.destroy$))
|
||||
.subscribe(() => {
|
||||
if (this.taxInformationChanged) {
|
||||
this.taxInformationChanged.emit(this.taxInformation);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.destroy$.next();
|
||||
this.destroy$.complete();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./premium-badge.component";
|
||||
@@ -0,0 +1,25 @@
|
||||
import { Component, input } from "@angular/core";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { PremiumUpgradePromptService } from "@bitwarden/common/vault/abstractions/premium-upgrade-prompt.service";
|
||||
import { BadgeModule } from "@bitwarden/components";
|
||||
|
||||
@Component({
|
||||
selector: "app-premium-badge",
|
||||
standalone: true,
|
||||
template: `
|
||||
<button type="button" *appNotPremium bitBadge variant="success" (click)="promptForPremium()">
|
||||
{{ "premium" | i18n }}
|
||||
</button>
|
||||
`,
|
||||
imports: [BadgeModule, JslibModule],
|
||||
})
|
||||
export class PremiumBadgeComponent {
|
||||
organizationId = input<string>();
|
||||
|
||||
constructor(private premiumUpgradePromptService: PremiumUpgradePromptService) {}
|
||||
|
||||
async promptForPremium() {
|
||||
await this.premiumUpgradePromptService.promptForPremium(this.organizationId());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
import { Meta, moduleMetadata, StoryObj } from "@storybook/angular";
|
||||
import { of } from "rxjs";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { MessageSender } from "@bitwarden/common/platform/messaging";
|
||||
import { PremiumUpgradePromptService } from "@bitwarden/common/vault/abstractions/premium-upgrade-prompt.service";
|
||||
import { BadgeModule, I18nMockService } from "@bitwarden/components";
|
||||
|
||||
import { PremiumBadgeComponent } from "./premium-badge.component";
|
||||
|
||||
class MockMessagingService implements MessageSender {
|
||||
send = () => {
|
||||
alert("Clicked on badge");
|
||||
};
|
||||
}
|
||||
|
||||
export default {
|
||||
title: "Billing/Premium Badge",
|
||||
component: PremiumBadgeComponent,
|
||||
decorators: [
|
||||
moduleMetadata({
|
||||
imports: [JslibModule, BadgeModule],
|
||||
providers: [
|
||||
{
|
||||
provide: AccountService,
|
||||
useValue: {
|
||||
activeAccount$: of({
|
||||
id: "123",
|
||||
}),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: I18nService,
|
||||
useFactory: () => {
|
||||
return new I18nMockService({
|
||||
premium: "Premium",
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: MessageSender,
|
||||
useFactory: () => {
|
||||
return new MockMessagingService();
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: BillingAccountProfileStateService,
|
||||
useValue: {
|
||||
hasPremiumFromAnySource$: () => of(false),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: PremiumUpgradePromptService,
|
||||
useValue: {
|
||||
promptForPremium: (orgId?: string) => {},
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
],
|
||||
} as Meta;
|
||||
|
||||
type Story = StoryObj<PremiumBadgeComponent>;
|
||||
|
||||
export const Primary: Story = {};
|
||||
@@ -2,12 +2,6 @@ import { CommonModule, DatePipe } from "@angular/common";
|
||||
import { NgModule } from "@angular/core";
|
||||
import { FormsModule, ReactiveFormsModule } from "@angular/forms";
|
||||
|
||||
import {
|
||||
AddAccountCreditDialogComponent,
|
||||
InvoicesComponent,
|
||||
NoInvoicesComponent,
|
||||
ManageTaxInformationComponent,
|
||||
} from "@bitwarden/angular/billing/components";
|
||||
import {
|
||||
AsyncActionsModule,
|
||||
AutofocusDirective,
|
||||
@@ -27,6 +21,7 @@ import {
|
||||
TypographyModule,
|
||||
CopyClickDirective,
|
||||
A11yTitleDirective,
|
||||
NoItemsModule,
|
||||
} from "@bitwarden/components";
|
||||
|
||||
import { TwoFactorIconComponent } from "./auth/components/two-factor-icon.component";
|
||||
@@ -54,7 +49,6 @@ 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 "./tools/password-strength/password-strength.component";
|
||||
import { IconComponent } from "./vault/components/icon.component";
|
||||
|
||||
@NgModule({
|
||||
@@ -78,6 +72,7 @@ import { IconComponent } from "./vault/components/icon.component";
|
||||
TypographyModule,
|
||||
TableModule,
|
||||
MenuModule,
|
||||
NoItemsModule,
|
||||
IconButtonModule,
|
||||
IconModule,
|
||||
LinkModule,
|
||||
@@ -108,14 +103,9 @@ import { IconComponent } from "./vault/components/icon.component";
|
||||
TrueFalseValueDirective,
|
||||
LaunchClickDirective,
|
||||
UserNamePipe,
|
||||
PasswordStrengthComponent,
|
||||
UserTypePipe,
|
||||
IfFeatureDirective,
|
||||
FingerprintPipe,
|
||||
AddAccountCreditDialogComponent,
|
||||
InvoicesComponent,
|
||||
NoInvoicesComponent,
|
||||
ManageTaxInformationComponent,
|
||||
TwoFactorIconComponent,
|
||||
],
|
||||
exports: [
|
||||
@@ -143,14 +133,9 @@ import { IconComponent } from "./vault/components/icon.component";
|
||||
CopyClickDirective,
|
||||
LaunchClickDirective,
|
||||
UserNamePipe,
|
||||
PasswordStrengthComponent,
|
||||
UserTypePipe,
|
||||
IfFeatureDirective,
|
||||
FingerprintPipe,
|
||||
AddAccountCreditDialogComponent,
|
||||
InvoicesComponent,
|
||||
NoInvoicesComponent,
|
||||
ManageTaxInformationComponent,
|
||||
TwoFactorIconComponent,
|
||||
TextDragDirective,
|
||||
],
|
||||
|
||||
@@ -23,9 +23,9 @@
|
||||
</metadata>
|
||||
<defs>
|
||||
<font id="bwi-font" horiz-adv-x="1024">
|
||||
<font-face units-per-em="1024" ascent="880.64" descent="-143.36" />
|
||||
<font-face units-per-em="1024" ascent="880.8602150537635" descent="-143.13978494623655" />
|
||||
<missing-glyph horiz-adv-x="1024" />
|
||||
<glyph unicode=" " horiz-adv-x="512" d="" />
|
||||
<glyph unicode=" " horiz-adv-x="512.5004887585533" d="" />
|
||||
<glyph unicode="" glyph-name="bwi-angle-down" data-tags="bwi-angle-down" d="M929.939 545.941c-18.746 18.745-49.133 18.745-67.878 0l-350.061-350.056-350.059 350.056c-18.745 18.745-49.137 18.745-67.882 0s-18.745-49.137 0-67.882l384.001-383.998c9.002-9.005 21.21-14.061 33.941-14.061s24.939 5.056 33.94 14.061l383.998 383.998c18.746 18.745 18.746 49.137 0 67.882z" />
|
||||
<glyph unicode="" glyph-name="bwi-angle-left" data-tags="bwi-angle-left" d="M673.939-33.939c18.746 18.746 18.746 49.133 0 67.878l-350.057 350.061 350.057 350.059c18.746 18.745 18.746 49.137 0 67.882s-49.135 18.745-67.88 0l-384-384c-18.745-18.746-18.745-49.137 0-67.882l384-383.998c18.745-18.746 49.135-18.746 67.88 0z" />
|
||||
<glyph unicode="" glyph-name="bwi-angle-right" data-tags="bwi-angle-right" d="M350.059 801.941c-18.745-18.745-18.745-49.137 0-67.882l350.056-350.059-350.056-350.061c-18.745-18.746-18.745-49.133 0-67.878s49.137-18.746 67.882 0l383.998 383.998c18.746 18.746 18.746 49.137 0 67.882l-383.998 384c-18.745 18.745-49.137 18.745-67.882 0z" />
|
||||
@@ -39,6 +39,7 @@
|
||||
<glyph unicode="" glyph-name="bwi-bug" data-tags="bwi-bug" d="M350.059 865.941c-18.745-18.745-18.745-49.137 0-67.882l24.25-24.25c-33.611-34.579-54.308-81.777-54.308-133.809v-13.715c0-2.588 0.12-5.148 0.353-7.674-24.548-11.608-46.648-27.547-65.313-46.829l-65.45 37.788c-22.958 13.255-52.314 5.389-65.569-17.569s-5.389-52.314 17.569-65.569l62.952-36.345c-8.124-23.19-12.541-48.122-12.541-74.086v-48h-80c-26.509 0-48-21.491-48-48s21.491-48 48-48h80v-16c0-41.21 7.791-80.602 21.979-116.781-1.047-0.512-2.083-1.056-3.107-1.651l-69.282-40c-22.958-13.254-30.824-42.611-17.569-65.568s42.611-30.822 65.569-17.568l69.282 40c1.046 0.602 2.060 1.242 3.042 1.907 58.638-73.35 148.875-120.339 250.086-120.339s191.45 46.989 250.086 120.339c0.979-0.666 1.997-1.306 3.040-1.907l69.286-40c22.957-13.254 52.314-5.389 65.568 17.568s5.389 52.314-17.568 65.568l-69.286 40c-1.024 0.595-2.061 1.139-3.104 1.651 14.189 36.179 21.978 75.571 21.978 116.781v16h80c26.509 0 48 21.491 48 48s-21.491 48-48 48h-80v48c0 25.964-4.416 50.896-12.544 74.086l62.957 36.345c22.957 13.255 30.822 42.611 17.568 65.569s-42.611 30.824-65.568 17.569l-65.453-37.788c-18.669 19.283-40.768 35.221-65.312 46.829 0.23 2.527 0.352 5.087 0.352 7.674v13.715c0 52.032-20.698 99.23-54.31 133.81l24.25 24.249c18.746 18.745 18.746 49.137 0 67.882s-49.135 18.745-67.88 0l-41.285-41.285c-16.767 4.783-34.472 7.344-52.774 7.344s-36.006-2.561-52.774-7.344l-41.285 41.285c-18.745 18.745-49.137 18.745-67.882 0zM547.943 729.044c4.242-3.062 8.871-5.347 13.691-6.854 27.791-16.819 46.366-47.335 46.366-82.19h-192c0 34.855 18.575 65.371 46.366 82.19 4.82 1.507 9.449 3.792 13.691 6.854 11.101 4.486 23.234 6.956 35.943 6.956s24.842-2.47 35.943-6.956zM288 416c0 70.692 57.308 128 128 128h192c70.694 0 128-57.308 128-128v-160c0-107.238-75.354-196.87-176-218.848v298.848c0 26.509-21.491 48-48 48s-48-21.491-48-48v-298.848c-100.646 21.978-176 111.61-176 218.848v160z" />
|
||||
<glyph unicode="" glyph-name="bwi-business" data-tags="bwi-business" d="M336 704c-26.509 0-48-21.491-48-48s21.491-48 48-48h96c26.509 0 48 21.491 48 48s-21.491 48-48 48h-96zM592 704c-26.509 0-48-21.491-48-48s21.491-48 48-48h96c26.509 0 48 21.491 48 48s-21.491 48-48 48h-96zM592 512c-26.509 0-48-21.491-48-48s21.491-48 48-48h96c26.509 0 48 21.491 48 48s-21.491 48-48 48h-96zM544 272c0 26.509 21.491 48 48 48h96c26.509 0 48-21.491 48-48s-21.491-48-48-48h-96c-26.509 0-48 21.491-48 48zM288 464c0 26.509 21.491 48 48 48h96c26.509 0 48-21.491 48-48s-21.491-48-48-48h-96c-26.509 0-48 21.491-48 48zM336 320c-26.509 0-48-21.491-48-48s21.491-48 48-48h96c26.509 0 48 21.491 48 48s-21.491 48-48 48h-96zM192 896h640c35.347 0 64-28.654 64-64v-896c0-35.347-28.653-64-64-64h-640c-35.346 0-64 28.653-64 64v896c0 35.346 28.654 64 64 64zM224 800v-832h160v96c0 35.347 28.654 64 64 64h128c35.346 0 64-28.653 64-64v-96h160v832h-576z" />
|
||||
<glyph unicode="" glyph-name="bwi-camera" data-tags="bwi-camera" d="M704 368c0 106.038-85.962 192-192 192s-192-85.962-192-192c0-106.038 85.962-192 192-192s192 85.962 192 192zM608 368c0-53.020-42.98-96-96-96s-96 42.98-96 96c0 53.020 42.98 96 96 96s96-42.98 96-96zM404.403 864c-51.203 0-97.48-30.515-117.651-77.578l-35.324-82.422h-123.428c-70.692 0-128-57.308-128-128v-448c0-70.694 57.308-128 128-128h768c70.694 0 128 57.306 128 128v448c0 70.692-57.306 128-128 128h-123.43l-35.322 82.422c-20.173 47.063-66.445 77.578-117.651 77.578h-215.195zM314.731 608l60.259 140.605c5.043 11.766 16.612 19.395 29.412 19.395h215.195c12.801 0 24.371-7.629 29.414-19.395l60.256-140.605h186.733c17.67 0 32-14.327 32-32v-448c0-17.67-14.33-32-32-32h-768c-17.673 0-32 14.33-32 32v448c0 17.673 14.327 32 32 32h186.731z" />
|
||||
<glyph unicode="" glyph-name="bwi-premium" data-tags="bwi-premium" d="M805.869 800.188c41.254-0.010 79.181-22.7 98.682-59.062l98.63-183.875c21.843-40.75 16.41-90.778-13.69-125.875l-391.75-456.687c-44.71-52.096-125.319-52.058-170 0.064l-391.875 457.248c-29.641 34.592-35.368 83.695-14.5 124.188l94.5 183.313c19.211 37.263 57.638 60.687 99.562 60.687h590.441zM464.928 78.874l0.625 367.876-175.688 1.187h-141.25l316.312-369.064zM561.553 446.75l-0.625-367.563 316.314 368.75h-141.376l-174.312-1.187zM364.178 543.437l149.437-1 142.813 1-84.188 160.75h-135.75l-72.313-160.75zM764.55 543.937h136.813l-81.434 151.812c-2.784 5.185-8.179 8.428-14.061 8.438h-125.254l83.936-160.25zM215.428 704.188c-5.99 0-11.506-3.363-14.25-8.687l-78.125-151.563h136.125l72.125 160.25h-115.875z" />
|
||||
<glyph unicode="" glyph-name="bwi-check-circle" data-tags="bwi-check-circle" d="M769.939 494.059c18.746 18.745 18.746 49.137 0 67.882s-49.133 18.745-67.878 0l-270.061-270.059-110.059 110.059c-18.745 18.745-49.137 18.745-67.882 0s-18.745-49.137 0-67.882l144-143.998c18.745-18.746 49.137-18.746 67.882 0l303.998 303.998zM1024 384c0 282.77-229.229 512-512 512s-512-229.23-512-512c0-282.771 229.23-512 512-512s512 229.229 512 512zM928 384c0-229.754-186.246-416-416-416-229.75 0-416 186.246-416 416 0 229.75 186.25 416 416 416 229.754 0 416-186.25 416-416z" />
|
||||
<glyph unicode="" glyph-name="bwi-check" data-tags="bwi-check" d="M1009.939 753.941c18.746-18.745 18.746-49.137 0-67.882l-623.998-623.998c-18.745-18.746-49.137-18.746-67.882 0l-304 303.998c-18.745 18.745-18.745 49.137 0 67.882s49.137 18.745 67.882 0l270.059-270.056 590.061 590.056c18.746 18.745 49.133 18.745 67.878 0z" />
|
||||
<glyph unicode="" glyph-name="bwi-cli" data-tags="bwi-cli" d="M270.059 545.941c-18.745-18.745-18.745-49.137 0-67.882l94.059-94.059-94.059-94.059c-18.745-18.745-18.745-49.135 0-67.88s49.137-18.746 67.882 0l128 127.998c18.745 18.745 18.745 49.137 0 67.882l-128 128c-18.745 18.745-49.137 18.745-67.882 0zM560 304h160c26.509 0 48-21.491 48-48s-21.491-48-48-48h-160c-26.509 0-48 21.491-48 48s21.491 48 48 48zM832 832h-640c-70.692 0-128-57.308-128-128v-640c0-70.694 57.308-128 128-128h640c70.694 0 128 57.306 128 128v640c0 70.692-57.306 128-128 128zM192 736h640c17.67 0 32-14.327 32-32v-640c0-17.67-14.33-32-32-32h-640c-17.673 0-32 14.33-32 32v640c0 17.673 14.327 32 32 32z" />
|
||||
@@ -49,6 +50,7 @@
|
||||
<glyph unicode="" glyph-name="collection" data-tags="collection" d="M288 848c0 26.51 21.491 48 48 48h352c26.509 0 48-21.49 48-48s-21.491-48-48-48h-352c-26.509 0-48 21.491-48 48zM208 768c-26.509 0-48-21.491-48-48s21.491-48 48-48h608c26.509 0 48 21.491 48 48s-21.491 48-48 48h-608zM832 640h-640c-70.692 0-128-57.308-128-128v-384c0-70.694 57.308-128 128-128h640c70.694 0 128 57.306 128 128v384c0 70.692-57.306 128-128 128zM832 544c17.67 0 32-14.327 32-32v-384c0-17.67-14.33-32-32-32h-640c-17.673 0-32 14.33-32 32v384c0 17.673 14.327 32 32 32h640z" />
|
||||
<glyph unicode="" glyph-name="bwi-collection-shared" data-tags="bwi-collection-shared" d="M288 848c0 26.51 21.491 48 48 48h352c26.509 0 48-21.49 48-48s-21.491-48-48-48h-352c-26.509 0-48 21.491-48 48zM208 768c-26.509 0-48-21.491-48-48s21.491-48 48-48h608c26.509 0 48 21.491 48 48s-21.491 48-48 48h-608zM448 384c0-35.346-28.654-64-64-64s-64 28.654-64 64c0 35.346 28.654 64 64 64s64-28.654 64-64zM704 384c0-35.346-28.653-64-64-64s-64 28.654-64 64 28.654 64 64 64c35.347 0 64-28.654 64-64zM512 162.509c0 16.48-3.311 32.8-9.743 48.026s-15.861 29.056-27.747 40.71c-11.885 11.652-25.996 20.897-41.526 27.203s-32.175 9.553-48.984 9.553c-16.809 0-33.454-3.246-48.984-9.553s-29.64-15.55-41.526-27.203c-11.886-11.654-21.315-25.485-27.747-40.71s-9.743-31.546-9.743-48.026c0-19.059 15.451-34.509 34.51-34.509h186.98c19.059 0 34.51 15.45 34.51 34.509zM576.004 162.509c0 25.062-5.037 49.85-14.79 72.934-2.665 6.304-5.667 12.448-8.987 18.406 11.293 10.431 24.435 18.769 38.788 24.598 15.53 6.307 32.175 9.553 48.984 9.553 16.806 0 33.453-3.246 48.986-9.553 15.526-6.306 29.638-15.55 41.523-27.203s21.312-25.485 27.75-40.71c6.432-15.226 9.741-31.546 9.741-48.026 0-19.059-15.45-34.509-34.509-34.509h-163.701c4.018 10.739 6.214 22.368 6.214 34.509zM832 640h-640c-70.692 0-128-57.308-128-128v-384c0-70.694 57.308-128 128-128h640c70.694 0 128 57.306 128 128v384c0 70.692-57.306 128-128 128zM832 544c17.67 0 32-14.327 32-32v-384c0-17.67-14.33-32-32-32h-640c-17.673 0-32 14.33-32 32v384c0 17.673 14.327 32 32 32h640z" />
|
||||
<glyph unicode="" glyph-name="bwi-copy" data-tags="bwi-clone" d="M320 704v-128h-128c-70.692 0-128-57.308-128-128v-384c0-70.694 57.308-128 128-128h384c70.694 0 128 57.306 128 128v128h128c70.694 0 128 57.306 128 128v384c0 70.692-57.306 128-128 128h-384c-70.692 0-128-57.308-128-128zM448 736h384c17.67 0 32-14.327 32-32v-384c0-17.673-14.33-32-32-32h-384c-17.673 0-32 14.327-32 32v384c0 17.673 14.327 32 32 32zM320 480v-160c0-70.694 57.308-128 128-128h160v-128c0-17.67-14.327-32-32-32h-384c-17.673 0-32 14.33-32 32v384c0 17.673 14.327 32 32 32h128z" />
|
||||
<glyph unicode="" glyph-name="bwi-unarchive" data-tags="unarchive (1)" d="M483.556 110.393c12.108 0 22.243 4.081 30.407 12.245 8.173 8.172 12.26 18.313 12.26 30.421v210.488l75.264-75.278c7.879-7.87 17.783-11.9 29.71-12.089 11.919-0.18 22.002 3.849 30.251 12.089 8.24 8.248 12.36 18.242 12.36 29.98s-4.12 21.732-12.36 29.98l-141.895 141.013c-10.288 10.288-22.287 15.432-35.996 15.432s-25.709-5.144-35.997-15.432l-141.895-141.013c-7.869-7.879-11.899-17.783-12.089-29.71-0.18-11.919 3.849-22.002 12.089-30.251 8.249-8.24 18.243-12.36 29.98-12.36s21.732 4.12 29.981 12.36l75.264 75.278v-210.488c0-12.108 4.087-22.249 12.259-30.421 8.164-8.164 18.3-12.245 30.408-12.245zM85.333 645.476v-613.749c0-5.111 1.64-9.301 4.921-12.587 3.28-3.277 7.476-4.915 12.587-4.915h761.432c5.111 0 9.301 1.638 12.587 4.915 3.277 3.285 4.915 7.475 4.915 12.587v613.749h-796.442zM107.221-71.108c-28.378 0-53.343 10.769-74.894 32.324s-32.327 46.515-32.327 74.897v660.448c0 12.175 1.953 23.789 5.86 34.845 3.897 11.046 9.747 21.239 17.55 30.577l79.872 96.597c9.33 12.184 20.997 21.466 35.001 27.847s28.809 9.571 44.416 9.571h600.619c15.607 0 30.502-3.19 44.687-9.571s25.944-15.663 35.288-27.847l80.41-97.692c7.799-9.34 13.653-19.622 17.553-30.848 3.9-11.236 5.854-22.945 5.854-35.129v-658.799c0-28.382-10.769-53.342-32.324-74.897s-46.515-32.324-74.897-32.324h-752.666zM107.548 730.795h751.477l-62.040 73.856c-1.821 1.82-3.916 3.28-6.287 4.38-2.371 1.091-4.831 1.636-7.381 1.636h-600.064c-2.551 0-5.016-0.545-7.396-1.636-2.37-1.1-4.465-2.56-6.286-4.38l-62.024-73.856z" />
|
||||
<glyph unicode="" glyph-name="bwi-dollar" data-tags="bwi-dollar" d="M512 896c26.509 0 48-21.49 48-48v-76.787c28.948-4.156 55.337-12.075 79.167-23.757 37.716-18.133 67.092-43.52 88.129-76.16 10.598-16.078 18.618-33.537 24.058-52.377 9.222-31.927-19.008-59.687-52.243-59.687-32.461 0-59.908 27.451-76.263 55.488-10.155 18.134-25.024 33.003-44.608 44.608s-43.52 17.408-71.808 17.408c-36.267 0.726-66.368-8.341-90.304-27.2-23.211-18.858-34.816-44.97-34.816-78.336 0-29.738 8.704-52.586 26.112-68.544s41.344-29.376 71.808-40.256c30.464-10.154 65.28-22.122 104.448-35.904 37.717-12.33 71.444-27.2 101.185-44.608s53.312-40.256 70.72-68.544c18.131-28.288 27.2-64.192 27.2-107.712 0-38.445-9.792-74.349-29.376-107.712-19.584-32.64-48.96-59.475-88.128-80.512-29.834-15.469-64.927-25.050-105.28-28.736v-68.672c0-26.509-21.491-48-48-48s-48 21.491-48 48v70.605c-32.986 4.474-63.087 13.043-90.305 25.715-42.070 19.584-75.072 47.507-99.008 83.776-13.869 21.67-23.725 45.805-29.565 72.397-6.787 30.893 19.551 57.075 51.185 57.075 30.918 0 55.636-25.862 69.656-53.414 9.344-18.368 22.56-35.014 37.108-49.946 27.562-28.288 66.368-42.432 116.416-42.432 44.245 0 78.698 10.515 103.36 31.552 25.389 21.76 38.081 49.325 38.081 82.688 0 26.835-6.163 48.595-18.496 65.28-11.606 16.683-27.926 30.464-48.961 41.344-20.31 10.88-43.883 20.672-70.72 29.376s-55.126 18.134-84.864 28.288c-58.752 19.584-102.998 44.971-132.736 76.16-29.014 31.19-43.52 72.171-43.52 122.944-0.726 42.795 9.066 80.15 29.376 112.064 21.034 31.915 50.048 56.576 87.040 73.984 25.772 12.391 54.423 20.548 85.953 24.472v76.072c0 26.51 21.491 48 48 48z" />
|
||||
<glyph unicode="" glyph-name="bwi-down-solid" data-tags="bwi-down-solid" d="M841.376 457.373l-284.12-284.118c-24.993-24.992-65.516-24.992-90.509 0l-284.118 284.118c-20.159 20.159-5.881 54.627 22.628 54.627h613.49c28.512 0 42.784-34.468 22.63-54.627z" />
|
||||
<glyph unicode="" glyph-name="bwi-download" data-tags="bwi-download" d="M560.001 784c0 26.509-21.49 48-48 48s-48-21.491-48-48v-389.112l-64.111 56.988c-19.813 17.612-50.153 15.827-67.765-3.987s-15.827-50.153 3.987-67.765l144.001-128.003c18.187-16.16 45.592-16.16 63.779 0l144.001 128.003c19.814 17.612 21.594 47.952 3.987 67.765-17.613 19.814-47.955 21.598-67.768 3.987l-64.11-56.987v389.111zM160 400c0 26.509-21.491 48-48 48s-48-21.491-48-48v-288c0-61.856 50.144-112 112-112h672c61.856 0 112 50.144 112 112v288c0 26.509-21.491 48-48 48s-48-21.491-48-48v-288c0-8.838-7.162-16-16-16h-672c-8.836 0-16 7.162-16 16v288z" />
|
||||
@@ -121,5 +123,6 @@
|
||||
<glyph unicode="" glyph-name="bwi-paypal" data-tags="bwi-paypal" d="M938.667 479.531c0-22.87-2.771-46.251-7.206-69.419-36.141-188.544-159.834-253.696-317.826-253.696h-24.406c-19.328 0-35.754-14.253-38.741-33.664l-34.261-217.171c-3.030-19.328-19.456-33.581-38.742-33.581h-116.736c-16.64 0-29.354 14.848-26.794 31.232l59.136 378.88 2.645 2.389h91.947c223.362 0 363.048 111.702 404.181 322.902 34.643-34.731 46.803-80.043 46.803-127.872v0zM332.12 298.112c4.949 35.243 19.584 50.219 59.093 50.304l96.427 0.085c195.155 0 307.117 89.174 342.317 272.64 34.131 176.384-89.69 274.859-257.325 274.859h-313.344c-22.059 0-40.875-16.299-44.288-38.443-98.304-632.962-126.72-793.897-129.621-843.010-0.896-15.443 11.477-28.672 27.093-28.672h170.197l49.451 312.237z" />
|
||||
<glyph unicode="" glyph-name="bwi-credit-card" data-tags="bwi-credit-card" d="M192 240c0 26.509 21.491 48 48 48h96c26.509 0 48-21.491 48-48s-21.491-48-48-48h-96c-26.509 0-48 21.491-48 48zM496 288c-26.509 0-48-21.491-48-48s21.491-48 48-48h160c26.509 0 48 21.491 48 48s-21.491 48-48 48h-160zM0 640v-512c0-70.694 57.308-128 128-128h768c70.694 0 128 57.306 128 128v512c0 70.692-57.306 128-128 128h-768c-70.692 0-128-57.308-128-128zM128 672h768c17.67 0 32-14.327 32-32v-64h-832v64c0 17.673 14.327 32 32 32zM928 128c0-17.67-14.33-32-32-32h-768c-17.673 0-32 14.33-32 32v320h832v-320z" />
|
||||
<glyph unicode="" glyph-name="bwi-desktop" data-tags="bwi-desktop" d="M128 768h768c70.694 0 128-57.308 128-128v-384c0-70.694-57.306-128-128-128h-224v-96h48c26.509 0 48-21.491 48-48s-21.491-48-48-48h-416c-26.509 0-48 21.491-48 48s21.491 48 48 48h48v96h-224c-70.692 0-128 57.306-128 128v384c0 70.692 57.308 128 128 128zM896 672h-768c-17.673 0-32-14.327-32-32v-384c0-17.67 14.327-32 32-32h768c17.67 0 32 14.33 32 32v384c0 17.673-14.33 32-32 32zM576 32v96h-128v-96h128z" />
|
||||
<glyph unicode="" glyph-name="dashboard" data-tags="bwi-dashboard" d="M640.026 896c-36.791 0-64.026-28.654-64.026-64v-256h-159.131c-36.79 0-66.614-28.653-66.614-64v-256h-155.642c-36.79 0-66.614-28.653-66.614-64v-255.757c0-35.347 29.824-64 66.614-64h155.642v-0.23l479.133-0.013c36.787 0 66.611 28.653 66.611 64v896c0 35.346-29.824 64-66.611 64h-189.363zM673.331 799.999h122.746v-831.999h-122.746v831.999zM350.255 160v-191.757h-122.335v191.757h122.335zM448-31.987v178.97c0 2.426 0.273 7.411 0 9.766v323.251h128v-511.987h-128z" />
|
||||
<glyph unicode="" glyph-name="bwi-archive" data-tags="bwi-archive" d="M432.792 416.78c-26.509 0-48-21.528-48-48.083s21.491-48.083 48-48.083h160c26.509 0 48.001 21.528 48.001 48.083s-21.492 48.083-48.001 48.083h-160zM64.003 511.788h32.282l-0.001-447.564c0-70.816 57.308-128.224 128-128.224h576.586c70.694 0 128 57.408 128 128.224v447.564h31.136c35.341 0 64 28.703 64 64.111l-0.006 127.99c0 35.408-28.653 64.111-64 64.111h-896c-35.346 0-64-28.703-64-64.111l0.003-127.99c0-35.407 28.654-64.111 64-64.111zM96.003 607.955l-0.003 63.879h832l0.006-62.493h-831.722v-1.386h-0.282zM192.284 64.224l0.001 447.564h640.586v-447.564c0-17.702-14.33-32.058-32-32.058h-576.586c-17.673 0-32 14.355-32 32.058z" />
|
||||
</font></defs></svg>
|
||||
|
Before Width: | Height: | Size: 81 KiB After Width: | Height: | Size: 84 KiB |
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -100,6 +100,7 @@ $icomoon-font-path: "~@bitwarden/angular/src/scss/bwicons/fonts/" !default;
|
||||
}
|
||||
|
||||
// For new icons - add their glyph name and value to the map below
|
||||
// Also add to `libs/components/src/shared/icon.ts`
|
||||
$icons: (
|
||||
"angle-down": "\e900",
|
||||
"angle-left": "\e901",
|
||||
@@ -124,6 +125,7 @@ $icons: (
|
||||
"collection": "\e915",
|
||||
"collection-shared": "\e916",
|
||||
"clone": "\e917",
|
||||
"dashboard": "\e9bf",
|
||||
"dollar": "\e919",
|
||||
"down-solid": "\e91a",
|
||||
"download": "\e91b",
|
||||
@@ -167,6 +169,7 @@ $icons: (
|
||||
"plus-circle": "\e942",
|
||||
"plus": "\e943",
|
||||
"popout": "\e944",
|
||||
"premium": "\e90d",
|
||||
"provider": "\e945",
|
||||
"puzzle": "\e946",
|
||||
"question-circle": "\e947",
|
||||
@@ -197,6 +200,7 @@ $icons: (
|
||||
"credit-card": "\e9a2",
|
||||
"desktop": "\e9a3",
|
||||
"archive": "\e9c1",
|
||||
"unarchive": "\e918",
|
||||
);
|
||||
|
||||
@each $name, $glyph in $icons {
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
@font-face {
|
||||
font-family: Roboto;
|
||||
src:
|
||||
url("webfonts/roboto.woff2") format("woff2 supports variations"),
|
||||
url("webfonts/roboto.woff2") format("woff2-variations");
|
||||
font-display: swap;
|
||||
font-weight: 100 900;
|
||||
}
|
||||
Binary file not shown.
@@ -13,7 +13,6 @@ import {
|
||||
ObservableStorageService,
|
||||
} from "@bitwarden/common/platform/abstractions/storage.service";
|
||||
import { Theme } from "@bitwarden/common/platform/enums";
|
||||
import { StateFactory } from "@bitwarden/common/platform/factories/state-factory";
|
||||
import { Message } from "@bitwarden/common/platform/messaging";
|
||||
import { HttpOperations } from "@bitwarden/common/services/api.service";
|
||||
import { SafeInjectionToken } from "@bitwarden/ui-common";
|
||||
@@ -33,7 +32,6 @@ export const OBSERVABLE_DISK_LOCAL_STORAGE = new SafeInjectionToken<
|
||||
>("OBSERVABLE_DISK_LOCAL_STORAGE");
|
||||
export const MEMORY_STORAGE = new SafeInjectionToken<AbstractStorageService>("MEMORY_STORAGE");
|
||||
export const SECURE_STORAGE = new SafeInjectionToken<AbstractStorageService>("SECURE_STORAGE");
|
||||
export const STATE_FACTORY = new SafeInjectionToken<StateFactory>("STATE_FACTORY");
|
||||
export const LOGOUT_CALLBACK = new SafeInjectionToken<
|
||||
(logoutReason: LogoutReason, userId?: string) => Promise<void>
|
||||
>("LOGOUT_CALLBACK");
|
||||
|
||||
@@ -18,17 +18,16 @@ import {
|
||||
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop.
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import {
|
||||
DefaultLoginApprovalComponentService,
|
||||
DefaultLoginComponentService,
|
||||
DefaultLoginDecryptionOptionsService,
|
||||
DefaultNewDeviceVerificationComponentService,
|
||||
DefaultRegistrationFinishService,
|
||||
DefaultSetPasswordJitService,
|
||||
DefaultTwoFactorAuthComponentService,
|
||||
DefaultTwoFactorAuthWebAuthnComponentService,
|
||||
LoginComponentService,
|
||||
LoginDecryptionOptionsService,
|
||||
NewDeviceVerificationComponentService,
|
||||
RegistrationFinishService as RegistrationFinishServiceAbstraction,
|
||||
SetPasswordJitService,
|
||||
TwoFactorAuthComponentService,
|
||||
TwoFactorAuthWebAuthnComponentService,
|
||||
} from "@bitwarden/auth/angular";
|
||||
@@ -42,7 +41,6 @@ import {
|
||||
DefaultLoginSuccessHandlerService,
|
||||
DefaultLogoutService,
|
||||
InternalUserDecryptionOptionsServiceAbstraction,
|
||||
LoginApprovalComponentServiceAbstraction,
|
||||
LoginEmailService,
|
||||
LoginEmailServiceAbstraction,
|
||||
LoginStrategyService,
|
||||
@@ -50,8 +48,6 @@ import {
|
||||
LoginSuccessHandlerService,
|
||||
LogoutReason,
|
||||
LogoutService,
|
||||
PinService,
|
||||
PinServiceAbstraction,
|
||||
UserDecryptionOptionsService,
|
||||
UserDecryptionOptionsServiceAbstraction,
|
||||
} from "@bitwarden/auth/common";
|
||||
@@ -93,6 +89,7 @@ import {
|
||||
InternalAccountService,
|
||||
} from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { AnonymousHubService as AnonymousHubServiceAbstraction } from "@bitwarden/common/auth/abstractions/anonymous-hub.service";
|
||||
import { AuthRequestAnsweringServiceAbstraction } from "@bitwarden/common/auth/abstractions/auth-request-answering/auth-request-answering.service.abstraction";
|
||||
import { AuthService as AuthServiceAbstraction } from "@bitwarden/common/auth/abstractions/auth.service";
|
||||
import { AvatarService as AvatarServiceAbstraction } from "@bitwarden/common/auth/abstractions/avatar.service";
|
||||
import { DevicesServiceAbstraction } from "@bitwarden/common/auth/abstractions/devices/devices.service.abstraction";
|
||||
@@ -107,11 +104,15 @@ import { UserVerificationService as UserVerificationServiceAbstraction } from "@
|
||||
import { WebAuthnLoginApiServiceAbstraction } from "@bitwarden/common/auth/abstractions/webauthn/webauthn-login-api.service.abstraction";
|
||||
import { WebAuthnLoginPrfKeyServiceAbstraction } from "@bitwarden/common/auth/abstractions/webauthn/webauthn-login-prf-key.service.abstraction";
|
||||
import { WebAuthnLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/webauthn/webauthn-login.service.abstraction";
|
||||
import { SendTokenService, DefaultSendTokenService } from "@bitwarden/common/auth/send-access";
|
||||
import { AccountApiServiceImplementation } from "@bitwarden/common/auth/services/account-api.service";
|
||||
import { AccountServiceImplementation } from "@bitwarden/common/auth/services/account.service";
|
||||
import { AnonymousHubService } from "@bitwarden/common/auth/services/anonymous-hub.service";
|
||||
import { NoopAuthRequestAnsweringService } from "@bitwarden/common/auth/services/auth-request-answering/noop-auth-request-answering.service";
|
||||
import { PendingAuthRequestsStateService } from "@bitwarden/common/auth/services/auth-request-answering/pending-auth-requests.state";
|
||||
import { AuthService } from "@bitwarden/common/auth/services/auth.service";
|
||||
import { AvatarService } from "@bitwarden/common/auth/services/avatar.service";
|
||||
import { DefaultActiveUserAccessor } from "@bitwarden/common/auth/services/default-active-user.accessor";
|
||||
import { DevicesServiceImplementation } from "@bitwarden/common/auth/services/devices/devices.service.implementation";
|
||||
import { DevicesApiServiceImplementation } from "@bitwarden/common/auth/services/devices-api.service.implementation";
|
||||
import { MasterPasswordApiService } from "@bitwarden/common/auth/services/master-password/master-password-api.service.implementation";
|
||||
@@ -126,6 +127,7 @@ import { UserVerificationService } from "@bitwarden/common/auth/services/user-ve
|
||||
import { WebAuthnLoginApiService } from "@bitwarden/common/auth/services/webauthn-login/webauthn-login-api.service";
|
||||
import { WebAuthnLoginPrfKeyService } from "@bitwarden/common/auth/services/webauthn-login/webauthn-login-prf-key.service";
|
||||
import { WebAuthnLoginService } from "@bitwarden/common/auth/services/webauthn-login/webauthn-login.service";
|
||||
import { TwoFactorApiService, DefaultTwoFactorApiService } from "@bitwarden/common/auth/two-factor";
|
||||
import {
|
||||
AutofillSettingsService,
|
||||
AutofillSettingsServiceAbstraction,
|
||||
@@ -144,31 +146,46 @@ import {
|
||||
} from "@bitwarden/common/billing/abstractions";
|
||||
import { AccountBillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions/account/account-billing-api.service.abstraction";
|
||||
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
|
||||
import { OrganizationMetadataServiceAbstraction } from "@bitwarden/common/billing/abstractions/organization-metadata.service.abstraction";
|
||||
import { OrganizationBillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions/organizations/organization-billing-api.service.abstraction";
|
||||
import { OrganizationSponsorshipApiServiceAbstraction } from "@bitwarden/common/billing/abstractions/organizations/organization-sponsorship-api.service.abstraction";
|
||||
import { TaxServiceAbstraction } from "@bitwarden/common/billing/abstractions/tax.service.abstraction";
|
||||
import { AccountBillingApiService } from "@bitwarden/common/billing/services/account/account-billing-api.service";
|
||||
import { DefaultBillingAccountProfileStateService } from "@bitwarden/common/billing/services/account/billing-account-profile-state.service";
|
||||
import { BillingApiService } from "@bitwarden/common/billing/services/billing-api.service";
|
||||
import { OrganizationBillingApiService } from "@bitwarden/common/billing/services/organization/organization-billing-api.service";
|
||||
import { DefaultOrganizationMetadataService } from "@bitwarden/common/billing/services/organization/organization-metadata.service";
|
||||
import { OrganizationSponsorshipApiService } from "@bitwarden/common/billing/services/organization/organization-sponsorship-api.service";
|
||||
import { OrganizationBillingService } from "@bitwarden/common/billing/services/organization-billing.service";
|
||||
import { TaxService } from "@bitwarden/common/billing/services/tax.service";
|
||||
import { BulkEncryptService } from "@bitwarden/common/key-management/crypto/abstractions/bulk-encrypt.service";
|
||||
import { HibpApiService } from "@bitwarden/common/dirt/services/hibp-api.service";
|
||||
import {
|
||||
DefaultKeyGenerationService,
|
||||
KeyGenerationService,
|
||||
} from "@bitwarden/common/key-management/crypto";
|
||||
import { CryptoFunctionService as CryptoFunctionServiceAbstraction } from "@bitwarden/common/key-management/crypto/abstractions/crypto-function.service";
|
||||
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||
import { BulkEncryptServiceImplementation } from "@bitwarden/common/key-management/crypto/services/bulk-encrypt.service.implementation";
|
||||
import { MultithreadEncryptServiceImplementation } from "@bitwarden/common/key-management/crypto/services/multithread-encrypt.service.implementation";
|
||||
import { EncryptServiceImplementation } from "@bitwarden/common/key-management/crypto/services/encrypt.service.implementation";
|
||||
import { WebCryptoFunctionService } from "@bitwarden/common/key-management/crypto/services/web-crypto-function.service";
|
||||
import { DeviceTrustServiceAbstraction } from "@bitwarden/common/key-management/device-trust/abstractions/device-trust.service.abstraction";
|
||||
import { DeviceTrustService } from "@bitwarden/common/key-management/device-trust/services/device-trust.service.implementation";
|
||||
import { DefaultChangeKdfApiService } from "@bitwarden/common/key-management/kdf/change-kdf-api.service";
|
||||
import { ChangeKdfApiService } from "@bitwarden/common/key-management/kdf/change-kdf-api.service.abstraction";
|
||||
import { DefaultChangeKdfService } from "@bitwarden/common/key-management/kdf/change-kdf-service";
|
||||
import { ChangeKdfService } from "@bitwarden/common/key-management/kdf/change-kdf-service.abstraction";
|
||||
import { KeyConnectorService as KeyConnectorServiceAbstraction } from "@bitwarden/common/key-management/key-connector/abstractions/key-connector.service";
|
||||
import { KeyConnectorService } from "@bitwarden/common/key-management/key-connector/services/key-connector.service";
|
||||
import { KeyApiService } from "@bitwarden/common/key-management/keys/services/abstractions/key-api-service.abstraction";
|
||||
import { DefaultKeyApiService } from "@bitwarden/common/key-management/keys/services/default-key-api-service.service";
|
||||
import { MasterPasswordUnlockService } from "@bitwarden/common/key-management/master-password/abstractions/master-password-unlock.service";
|
||||
import {
|
||||
InternalMasterPasswordServiceAbstraction,
|
||||
MasterPasswordServiceAbstraction,
|
||||
} from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction";
|
||||
import { DefaultMasterPasswordUnlockService } from "@bitwarden/common/key-management/master-password/services/default-master-password-unlock.service";
|
||||
import { MasterPasswordService } from "@bitwarden/common/key-management/master-password/services/master-password.service";
|
||||
import { PinServiceAbstraction } from "@bitwarden/common/key-management/pin/pin.service.abstraction";
|
||||
import { PinService } from "@bitwarden/common/key-management/pin/pin.service.implementation";
|
||||
import { SecurityStateService } from "@bitwarden/common/key-management/security-state/abstractions/security-state.service";
|
||||
import { DefaultSecurityStateService } from "@bitwarden/common/key-management/security-state/services/security-state.service";
|
||||
import {
|
||||
SendPasswordService,
|
||||
DefaultSendPasswordService,
|
||||
@@ -189,7 +206,6 @@ import {
|
||||
} from "@bitwarden/common/platform/abstractions/environment.service";
|
||||
import { FileUploadService as FileUploadServiceAbstraction } from "@bitwarden/common/platform/abstractions/file-upload/file-upload.service";
|
||||
import { I18nService as I18nServiceAbstraction } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { KeyGenerationService as KeyGenerationServiceAbstraction } from "@bitwarden/common/platform/abstractions/key-generation.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { MessagingService as MessagingServiceAbstraction } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||
import { PlatformUtilsService as PlatformUtilsServiceAbstraction } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
@@ -198,27 +214,26 @@ import { SdkService } from "@bitwarden/common/platform/abstractions/sdk/sdk.serv
|
||||
import { StateService as StateServiceAbstraction } from "@bitwarden/common/platform/abstractions/state.service";
|
||||
import { AbstractStorageService } from "@bitwarden/common/platform/abstractions/storage.service";
|
||||
import { ValidationService as ValidationServiceAbstraction } from "@bitwarden/common/platform/abstractions/validation.service";
|
||||
import { StateFactory } from "@bitwarden/common/platform/factories/state-factory";
|
||||
import { ActionsService } from "@bitwarden/common/platform/actions";
|
||||
import { UnsupportedActionsService } from "@bitwarden/common/platform/actions/unsupported-actions.service";
|
||||
import { Message, MessageListener, MessageSender } from "@bitwarden/common/platform/messaging";
|
||||
// eslint-disable-next-line no-restricted-imports -- Used for dependency injection
|
||||
import { SubjectMessageSender } from "@bitwarden/common/platform/messaging/internal";
|
||||
import { devFlagEnabled } from "@bitwarden/common/platform/misc/flags";
|
||||
import { Account } from "@bitwarden/common/platform/models/domain/account";
|
||||
import { GlobalState } from "@bitwarden/common/platform/models/domain/global-state";
|
||||
import { NotificationsService } from "@bitwarden/common/platform/notifications";
|
||||
// eslint-disable-next-line no-restricted-imports -- Needed for service creation
|
||||
import {
|
||||
DefaultNotificationsService,
|
||||
NoopNotificationsService,
|
||||
SignalRConnectionService,
|
||||
UnsupportedWebPushConnectionService,
|
||||
WebPushConnectionService,
|
||||
WebPushNotificationsApiService,
|
||||
} from "@bitwarden/common/platform/notifications/internal";
|
||||
import {
|
||||
DefaultTaskSchedulerService,
|
||||
TaskSchedulerService,
|
||||
} from "@bitwarden/common/platform/scheduling";
|
||||
import { ServerNotificationsService } from "@bitwarden/common/platform/server-notifications";
|
||||
// eslint-disable-next-line no-restricted-imports -- Needed for service creation
|
||||
import {
|
||||
DefaultServerNotificationsService,
|
||||
NoopServerNotificationsService,
|
||||
SignalRConnectionService,
|
||||
UnsupportedWebPushConnectionService,
|
||||
WebPushConnectionService,
|
||||
WebPushNotificationsApiService,
|
||||
} from "@bitwarden/common/platform/server-notifications/internal";
|
||||
import { AppIdService } from "@bitwarden/common/platform/services/app-id.service";
|
||||
import { ConfigApiService } from "@bitwarden/common/platform/services/config/config-api.service";
|
||||
import { DefaultConfigService } from "@bitwarden/common/platform/services/config/default-config.service";
|
||||
@@ -227,33 +242,17 @@ import { DefaultBroadcasterService } from "@bitwarden/common/platform/services/d
|
||||
import { DefaultEnvironmentService } from "@bitwarden/common/platform/services/default-environment.service";
|
||||
import { DefaultServerSettingsService } from "@bitwarden/common/platform/services/default-server-settings.service";
|
||||
import { FileUploadService } from "@bitwarden/common/platform/services/file-upload/file-upload.service";
|
||||
import { KeyGenerationService } from "@bitwarden/common/platform/services/key-generation.service";
|
||||
import { MigrationBuilderService } from "@bitwarden/common/platform/services/migration-builder.service";
|
||||
import { MigrationRunner } from "@bitwarden/common/platform/services/migration-runner";
|
||||
import { DefaultSdkService } from "@bitwarden/common/platform/services/sdk/default-sdk.service";
|
||||
import { StateService } from "@bitwarden/common/platform/services/state.service";
|
||||
import { StorageServiceProvider } from "@bitwarden/common/platform/services/storage-service.provider";
|
||||
import { UserAutoUnlockKeyService } from "@bitwarden/common/platform/services/user-auto-unlock-key.service";
|
||||
import { ValidationService } from "@bitwarden/common/platform/services/validation.service";
|
||||
import {
|
||||
ActiveUserStateProvider,
|
||||
DerivedStateProvider,
|
||||
GlobalStateProvider,
|
||||
SingleUserStateProvider,
|
||||
StateProvider,
|
||||
} from "@bitwarden/common/platform/state";
|
||||
/* eslint-disable import/no-restricted-paths -- We need the implementations to inject, but generally these should not be accessed */
|
||||
import { DefaultActiveUserStateProvider } from "@bitwarden/common/platform/state/implementations/default-active-user-state.provider";
|
||||
import { DefaultDerivedStateProvider } from "@bitwarden/common/platform/state/implementations/default-derived-state.provider";
|
||||
import { DefaultGlobalStateProvider } from "@bitwarden/common/platform/state/implementations/default-global-state.provider";
|
||||
import { DefaultSingleUserStateProvider } from "@bitwarden/common/platform/state/implementations/default-single-user-state.provider";
|
||||
import { DefaultStateProvider } from "@bitwarden/common/platform/state/implementations/default-state.provider";
|
||||
import { StateEventRegistrarService } from "@bitwarden/common/platform/state/state-event-registrar.service";
|
||||
import { StateEventRunnerService } from "@bitwarden/common/platform/state/state-event-runner.service";
|
||||
/* eslint-enable import/no-restricted-paths */
|
||||
import { SyncService } from "@bitwarden/common/platform/sync";
|
||||
// eslint-disable-next-line no-restricted-imports -- Needed for DI
|
||||
import { DefaultSyncService } from "@bitwarden/common/platform/sync/internal";
|
||||
import { SystemNotificationsService } from "@bitwarden/common/platform/system-notifications";
|
||||
import { UnsupportedSystemNotificationsService } from "@bitwarden/common/platform/system-notifications/unsupported-system-notifications.service";
|
||||
import {
|
||||
DefaultThemeStateService,
|
||||
ThemeStateService,
|
||||
@@ -275,6 +274,7 @@ import {
|
||||
InternalSendService,
|
||||
SendService as SendServiceAbstraction,
|
||||
} from "@bitwarden/common/tools/send/services/send.service.abstraction";
|
||||
import { CipherArchiveService } from "@bitwarden/common/vault/abstractions/cipher-archive.service";
|
||||
import { CipherEncryptionService } from "@bitwarden/common/vault/abstractions/cipher-encryption.service";
|
||||
import { CipherService as CipherServiceAbstraction } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { CipherFileUploadService as CipherFileUploadServiceAbstraction } from "@bitwarden/common/vault/abstractions/file-upload/cipher-file-upload.service";
|
||||
@@ -295,6 +295,7 @@ import {
|
||||
DefaultCipherAuthorizationService,
|
||||
} from "@bitwarden/common/vault/services/cipher-authorization.service";
|
||||
import { CipherService } from "@bitwarden/common/vault/services/cipher.service";
|
||||
import { DefaultCipherArchiveService } from "@bitwarden/common/vault/services/default-cipher-archive.service";
|
||||
import { DefaultCipherEncryptionService } from "@bitwarden/common/vault/services/default-cipher-encryption.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";
|
||||
@@ -332,6 +333,26 @@ import {
|
||||
UserAsymmetricKeysRegenerationApiService,
|
||||
UserAsymmetricKeysRegenerationService,
|
||||
} from "@bitwarden/key-management";
|
||||
import {
|
||||
ActiveUserStateProvider,
|
||||
DerivedStateProvider,
|
||||
GlobalStateProvider,
|
||||
SingleUserStateProvider,
|
||||
StateEventRegistrarService,
|
||||
StateEventRunnerService,
|
||||
StateProvider,
|
||||
} from "@bitwarden/state";
|
||||
import {
|
||||
ActiveUserAccessor,
|
||||
DefaultActiveUserStateProvider,
|
||||
DefaultDerivedStateProvider,
|
||||
DefaultGlobalStateProvider,
|
||||
DefaultSingleUserStateProvider,
|
||||
DefaultStateEventRegistrarService,
|
||||
DefaultStateEventRunnerService,
|
||||
DefaultStateProvider,
|
||||
DefaultStateService,
|
||||
} from "@bitwarden/state-internal";
|
||||
import { SafeInjectionToken } from "@bitwarden/ui-common";
|
||||
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop.
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
@@ -339,12 +360,16 @@ import { PasswordRepromptService } from "@bitwarden/vault";
|
||||
import {
|
||||
IndividualVaultExportService,
|
||||
IndividualVaultExportServiceAbstraction,
|
||||
DefaultVaultExportApiService,
|
||||
VaultExportApiService,
|
||||
OrganizationVaultExportService,
|
||||
OrganizationVaultExportServiceAbstraction,
|
||||
VaultExportService,
|
||||
VaultExportServiceAbstraction,
|
||||
} from "@bitwarden/vault-export-core";
|
||||
|
||||
import { DefaultLoginApprovalDialogComponentService } from "../auth/login-approval/default-login-approval-dialog-component.service";
|
||||
import { LoginApprovalDialogComponentServiceAbstraction } from "../auth/login-approval/login-approval-dialog-component.service.abstraction";
|
||||
import { DefaultSetInitialPasswordService } from "../auth/password-management/set-initial-password/default-set-initial-password.service.implementation";
|
||||
import { SetInitialPasswordService } from "../auth/password-management/set-initial-password/set-initial-password.service.abstraction";
|
||||
import { DeviceTrustToastService as DeviceTrustToastServiceAbstraction } from "../auth/services/device-trust-toast.service.abstraction";
|
||||
@@ -371,12 +396,10 @@ import {
|
||||
LOCKED_CALLBACK,
|
||||
LOG_MAC_FAILURES,
|
||||
LOGOUT_CALLBACK,
|
||||
MEMORY_STORAGE,
|
||||
OBSERVABLE_DISK_STORAGE,
|
||||
OBSERVABLE_MEMORY_STORAGE,
|
||||
REFRESH_ACCESS_TOKEN_ERROR_CALLBACK,
|
||||
SECURE_STORAGE,
|
||||
STATE_FACTORY,
|
||||
SUPPORTS_SECURE_STORAGE,
|
||||
SYSTEM_LANGUAGE,
|
||||
SYSTEM_THEME_OBSERVABLE,
|
||||
@@ -414,10 +437,6 @@ const safeProviders: SafeProvider[] = [
|
||||
useFactory: (window: Window) => window.navigator.language,
|
||||
deps: [WINDOW],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: STATE_FACTORY,
|
||||
useValue: new StateFactory(GlobalState, Account),
|
||||
}),
|
||||
// TODO: PM-21212 - Deprecate LogoutCallback in favor of LogoutService
|
||||
safeProvider({
|
||||
provide: LOGOUT_CALLBACK,
|
||||
@@ -461,7 +480,12 @@ const safeProviders: SafeProvider[] = [
|
||||
safeProvider({
|
||||
provide: AuditServiceAbstraction,
|
||||
useClass: AuditService,
|
||||
deps: [CryptoFunctionServiceAbstraction, ApiServiceAbstraction],
|
||||
deps: [CryptoFunctionServiceAbstraction, ApiServiceAbstraction, HibpApiService],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: HibpApiService,
|
||||
useClass: HibpApiService,
|
||||
deps: [ApiServiceAbstraction],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: AuthServiceAbstraction,
|
||||
@@ -520,7 +544,7 @@ const safeProviders: SafeProvider[] = [
|
||||
safeProvider({
|
||||
provide: DomainSettingsService,
|
||||
useClass: DefaultDomainSettingsService,
|
||||
deps: [StateProvider, ConfigService],
|
||||
deps: [StateProvider],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: CipherServiceAbstraction,
|
||||
@@ -530,16 +554,15 @@ const safeProviders: SafeProvider[] = [
|
||||
apiService: ApiServiceAbstraction,
|
||||
i18nService: I18nServiceAbstraction,
|
||||
searchService: SearchServiceAbstraction,
|
||||
stateService: StateServiceAbstraction,
|
||||
autofillSettingsService: AutofillSettingsServiceAbstraction,
|
||||
encryptService: EncryptService,
|
||||
bulkEncryptService: BulkEncryptService,
|
||||
fileUploadService: CipherFileUploadServiceAbstraction,
|
||||
configService: ConfigService,
|
||||
stateProvider: StateProvider,
|
||||
accountService: AccountServiceAbstraction,
|
||||
logService: LogService,
|
||||
cipherEncryptionService: CipherEncryptionService,
|
||||
messagingService: MessagingServiceAbstraction,
|
||||
) =>
|
||||
new CipherService(
|
||||
keyService,
|
||||
@@ -547,16 +570,15 @@ const safeProviders: SafeProvider[] = [
|
||||
apiService,
|
||||
i18nService,
|
||||
searchService,
|
||||
stateService,
|
||||
autofillSettingsService,
|
||||
encryptService,
|
||||
bulkEncryptService,
|
||||
fileUploadService,
|
||||
configService,
|
||||
stateProvider,
|
||||
accountService,
|
||||
logService,
|
||||
cipherEncryptionService,
|
||||
messagingService,
|
||||
),
|
||||
deps: [
|
||||
KeyService,
|
||||
@@ -564,16 +586,15 @@ const safeProviders: SafeProvider[] = [
|
||||
ApiServiceAbstraction,
|
||||
I18nServiceAbstraction,
|
||||
SearchServiceAbstraction,
|
||||
StateServiceAbstraction,
|
||||
AutofillSettingsServiceAbstraction,
|
||||
EncryptService,
|
||||
BulkEncryptService,
|
||||
CipherFileUploadServiceAbstraction,
|
||||
ConfigService,
|
||||
StateProvider,
|
||||
AccountServiceAbstraction,
|
||||
LogService,
|
||||
CipherEncryptionService,
|
||||
MessagingServiceAbstraction,
|
||||
],
|
||||
}),
|
||||
safeProvider({
|
||||
@@ -662,15 +683,15 @@ const safeProviders: SafeProvider[] = [
|
||||
GlobalStateProvider,
|
||||
SUPPORTS_SECURE_STORAGE,
|
||||
SECURE_STORAGE,
|
||||
KeyGenerationServiceAbstraction,
|
||||
KeyGenerationService,
|
||||
EncryptService,
|
||||
LogService,
|
||||
LOGOUT_CALLBACK,
|
||||
],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: KeyGenerationServiceAbstraction,
|
||||
useClass: KeyGenerationService,
|
||||
provide: KeyGenerationService,
|
||||
useClass: DefaultKeyGenerationService,
|
||||
deps: [CryptoFunctionServiceAbstraction],
|
||||
}),
|
||||
safeProvider({
|
||||
@@ -679,7 +700,7 @@ const safeProviders: SafeProvider[] = [
|
||||
deps: [
|
||||
PinServiceAbstraction,
|
||||
InternalMasterPasswordServiceAbstraction,
|
||||
KeyGenerationServiceAbstraction,
|
||||
KeyGenerationService,
|
||||
CryptoFunctionServiceAbstraction,
|
||||
EncryptService,
|
||||
PlatformUtilsServiceAbstraction,
|
||||
@@ -690,10 +711,15 @@ const safeProviders: SafeProvider[] = [
|
||||
KdfConfigService,
|
||||
],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: SecurityStateService,
|
||||
useClass: DefaultSecurityStateService,
|
||||
deps: [StateProvider],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: RestrictedItemTypesService,
|
||||
useClass: RestrictedItemTypesService,
|
||||
deps: [ConfigService, AccountService, OrganizationServiceAbstraction, PolicyServiceAbstraction],
|
||||
deps: [AccountService, OrganizationServiceAbstraction, PolicyServiceAbstraction],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: PasswordStrengthServiceAbstraction,
|
||||
@@ -756,6 +782,7 @@ const safeProviders: SafeProvider[] = [
|
||||
LogService,
|
||||
LOGOUT_CALLBACK,
|
||||
VaultTimeoutSettingsService,
|
||||
AccountService,
|
||||
HTTP_OPERATIONS,
|
||||
],
|
||||
}),
|
||||
@@ -767,9 +794,10 @@ const safeProviders: SafeProvider[] = [
|
||||
provide: InternalSendService,
|
||||
useClass: SendService,
|
||||
deps: [
|
||||
AccountServiceAbstraction,
|
||||
KeyService,
|
||||
I18nServiceAbstraction,
|
||||
KeyGenerationServiceAbstraction,
|
||||
KeyGenerationService,
|
||||
SendStateProviderAbstraction,
|
||||
EncryptService,
|
||||
],
|
||||
@@ -784,6 +812,11 @@ const safeProviders: SafeProvider[] = [
|
||||
useClass: SendApiService,
|
||||
deps: [ApiServiceAbstraction, FileUploadServiceAbstraction, InternalSendService],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: KeyApiService,
|
||||
useClass: DefaultKeyApiService,
|
||||
deps: [ApiServiceAbstraction],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: SyncService,
|
||||
useClass: DefaultSyncService,
|
||||
@@ -801,7 +834,6 @@ const safeProviders: SafeProvider[] = [
|
||||
InternalSendService,
|
||||
LogService,
|
||||
KeyConnectorServiceAbstraction,
|
||||
StateServiceAbstraction,
|
||||
ProviderServiceAbstraction,
|
||||
FolderApiServiceAbstraction,
|
||||
InternalOrganizationServiceAbstraction,
|
||||
@@ -813,6 +845,7 @@ const safeProviders: SafeProvider[] = [
|
||||
TokenServiceAbstraction,
|
||||
AuthServiceAbstraction,
|
||||
StateProvider,
|
||||
SecurityStateService,
|
||||
],
|
||||
}),
|
||||
safeProvider({
|
||||
@@ -849,6 +882,7 @@ const safeProviders: SafeProvider[] = [
|
||||
MessagingServiceAbstraction,
|
||||
SearchServiceAbstraction,
|
||||
StateServiceAbstraction,
|
||||
TokenServiceAbstraction,
|
||||
AuthServiceAbstraction,
|
||||
VaultTimeoutSettingsService,
|
||||
StateEventRunnerService,
|
||||
@@ -866,26 +900,12 @@ const safeProviders: SafeProvider[] = [
|
||||
safeProvider({
|
||||
provide: SsoLoginServiceAbstraction,
|
||||
useClass: SsoLoginService,
|
||||
deps: [StateProvider, LogService],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: STATE_FACTORY,
|
||||
useValue: new StateFactory(GlobalState, Account),
|
||||
deps: [StateProvider, LogService, PolicyServiceAbstraction],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: StateServiceAbstraction,
|
||||
useClass: StateService,
|
||||
deps: [
|
||||
AbstractStorageService,
|
||||
SECURE_STORAGE,
|
||||
MEMORY_STORAGE,
|
||||
LogService,
|
||||
STATE_FACTORY,
|
||||
AccountServiceAbstraction,
|
||||
EnvironmentService,
|
||||
TokenServiceAbstraction,
|
||||
MigrationRunner,
|
||||
],
|
||||
useClass: DefaultStateService,
|
||||
deps: [AbstractStorageService, SECURE_STORAGE, ActiveUserAccessor],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: IndividualVaultExportServiceAbstraction,
|
||||
@@ -898,31 +918,38 @@ const safeProviders: SafeProvider[] = [
|
||||
EncryptService,
|
||||
CryptoFunctionServiceAbstraction,
|
||||
KdfConfigService,
|
||||
AccountServiceAbstraction,
|
||||
ApiServiceAbstraction,
|
||||
RestrictedItemTypesService,
|
||||
],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: VaultExportApiService,
|
||||
useClass: DefaultVaultExportApiService,
|
||||
deps: [ApiServiceAbstraction],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: OrganizationVaultExportServiceAbstraction,
|
||||
useClass: OrganizationVaultExportService,
|
||||
deps: [
|
||||
CipherServiceAbstraction,
|
||||
ApiServiceAbstraction,
|
||||
VaultExportApiService,
|
||||
PinServiceAbstraction,
|
||||
KeyService,
|
||||
EncryptService,
|
||||
CryptoFunctionServiceAbstraction,
|
||||
CollectionService,
|
||||
KdfConfigService,
|
||||
AccountServiceAbstraction,
|
||||
RestrictedItemTypesService,
|
||||
],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: VaultExportServiceAbstraction,
|
||||
useClass: VaultExportService,
|
||||
deps: [IndividualVaultExportServiceAbstraction, OrganizationVaultExportServiceAbstraction],
|
||||
deps: [
|
||||
IndividualVaultExportServiceAbstraction,
|
||||
OrganizationVaultExportServiceAbstraction,
|
||||
AccountServiceAbstraction,
|
||||
],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: SearchServiceAbstraction,
|
||||
@@ -945,10 +972,35 @@ const safeProviders: SafeProvider[] = [
|
||||
deps: [],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: NotificationsService,
|
||||
provide: ActionsService,
|
||||
useClass: UnsupportedActionsService,
|
||||
deps: [],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: ActionsService,
|
||||
useClass: UnsupportedActionsService,
|
||||
deps: [],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: SystemNotificationsService,
|
||||
useClass: UnsupportedSystemNotificationsService,
|
||||
deps: [],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: PendingAuthRequestsStateService,
|
||||
useClass: PendingAuthRequestsStateService,
|
||||
deps: [StateProvider],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: AuthRequestAnsweringServiceAbstraction,
|
||||
useClass: NoopAuthRequestAnsweringService,
|
||||
deps: [],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: ServerNotificationsService,
|
||||
useClass: devFlagEnabled("noopNotifications")
|
||||
? NoopNotificationsService
|
||||
: DefaultNotificationsService,
|
||||
? NoopServerNotificationsService
|
||||
: DefaultServerNotificationsService,
|
||||
deps: [
|
||||
LogService,
|
||||
SyncService,
|
||||
@@ -960,6 +1012,8 @@ const safeProviders: SafeProvider[] = [
|
||||
SignalRConnectionService,
|
||||
AuthServiceAbstraction,
|
||||
WebPushConnectionService,
|
||||
AuthRequestAnsweringServiceAbstraction,
|
||||
ConfigService,
|
||||
],
|
||||
}),
|
||||
safeProvider({
|
||||
@@ -969,14 +1023,9 @@ const safeProviders: SafeProvider[] = [
|
||||
}),
|
||||
safeProvider({
|
||||
provide: EncryptService,
|
||||
useClass: MultithreadEncryptServiceImplementation,
|
||||
useClass: EncryptServiceImplementation,
|
||||
deps: [CryptoFunctionServiceAbstraction, LogService, LOG_MAC_FAILURES],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: BulkEncryptService,
|
||||
useClass: BulkEncryptServiceImplementation,
|
||||
deps: [CryptoFunctionServiceAbstraction, LogService],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: EventUploadServiceAbstraction,
|
||||
useClass: EventUploadService,
|
||||
@@ -1020,15 +1069,22 @@ const safeProviders: SafeProvider[] = [
|
||||
deps: [
|
||||
StateProvider,
|
||||
StateServiceAbstraction,
|
||||
KeyGenerationServiceAbstraction,
|
||||
KeyGenerationService,
|
||||
EncryptService,
|
||||
LogService,
|
||||
CryptoFunctionServiceAbstraction,
|
||||
AccountServiceAbstraction,
|
||||
],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: MasterPasswordServiceAbstraction,
|
||||
useExisting: InternalMasterPasswordServiceAbstraction,
|
||||
}),
|
||||
safeProvider({
|
||||
provide: MasterPasswordUnlockService,
|
||||
useClass: DefaultMasterPasswordUnlockService,
|
||||
deps: [InternalMasterPasswordServiceAbstraction, KeyService],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: KeyConnectorServiceAbstraction,
|
||||
useClass: KeyConnectorService,
|
||||
@@ -1040,7 +1096,7 @@ const safeProviders: SafeProvider[] = [
|
||||
TokenServiceAbstraction,
|
||||
LogService,
|
||||
OrganizationServiceAbstraction,
|
||||
KeyGenerationServiceAbstraction,
|
||||
KeyGenerationService,
|
||||
LOGOUT_CALLBACK,
|
||||
StateProvider,
|
||||
],
|
||||
@@ -1149,7 +1205,7 @@ const safeProviders: SafeProvider[] = [
|
||||
safeProvider({
|
||||
provide: ConfigApiServiceAbstraction,
|
||||
useClass: ConfigApiService,
|
||||
deps: [ApiServiceAbstraction, TokenServiceAbstraction],
|
||||
deps: [ApiServiceAbstraction],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: AnonymousHubServiceAbstraction,
|
||||
@@ -1188,7 +1244,7 @@ const safeProviders: SafeProvider[] = [
|
||||
safeProvider({
|
||||
provide: DevicesServiceAbstraction,
|
||||
useClass: DevicesServiceImplementation,
|
||||
deps: [DevicesApiServiceAbstraction, AppIdServiceAbstraction],
|
||||
deps: [AppIdServiceAbstraction, DevicesApiServiceAbstraction, I18nServiceAbstraction],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: AuthRequestApiServiceAbstraction,
|
||||
@@ -1199,7 +1255,7 @@ const safeProviders: SafeProvider[] = [
|
||||
provide: DeviceTrustServiceAbstraction,
|
||||
useClass: DeviceTrustService,
|
||||
deps: [
|
||||
KeyGenerationServiceAbstraction,
|
||||
KeyGenerationService,
|
||||
CryptoFunctionServiceAbstraction,
|
||||
KeyService,
|
||||
EncryptService,
|
||||
@@ -1214,6 +1270,16 @@ const safeProviders: SafeProvider[] = [
|
||||
ConfigService,
|
||||
],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: ChangeKdfApiService,
|
||||
useClass: DefaultChangeKdfApiService,
|
||||
deps: [ApiServiceAbstraction],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: ChangeKdfService,
|
||||
useClass: DefaultChangeKdfService,
|
||||
deps: [MasterPasswordServiceAbstraction, KeyService, KdfConfigService, ChangeKdfApiService],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: AuthRequestServiceAbstraction,
|
||||
useClass: AuthRequestService,
|
||||
@@ -1235,7 +1301,7 @@ const safeProviders: SafeProvider[] = [
|
||||
CryptoFunctionServiceAbstraction,
|
||||
EncryptService,
|
||||
KdfConfigService,
|
||||
KeyGenerationServiceAbstraction,
|
||||
KeyGenerationService,
|
||||
LogService,
|
||||
StateProvider,
|
||||
],
|
||||
@@ -1268,12 +1334,12 @@ const safeProviders: SafeProvider[] = [
|
||||
}),
|
||||
safeProvider({
|
||||
provide: StateEventRegistrarService,
|
||||
useClass: StateEventRegistrarService,
|
||||
useClass: DefaultStateEventRegistrarService,
|
||||
deps: [GlobalStateProvider, StorageServiceProvider],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: StateEventRunnerService,
|
||||
useClass: StateEventRunnerService,
|
||||
useClass: DefaultStateEventRunnerService,
|
||||
deps: [GlobalStateProvider, StorageServiceProvider],
|
||||
}),
|
||||
safeProvider({
|
||||
@@ -1281,10 +1347,15 @@ const safeProviders: SafeProvider[] = [
|
||||
useClass: DefaultGlobalStateProvider,
|
||||
deps: [StorageServiceProvider, LogService],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: ActiveUserAccessor,
|
||||
useClass: DefaultActiveUserAccessor,
|
||||
deps: [AccountServiceAbstraction],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: ActiveUserStateProvider,
|
||||
useClass: DefaultActiveUserStateProvider,
|
||||
deps: [AccountServiceAbstraction, SingleUserStateProvider],
|
||||
deps: [ActiveUserAccessor, SingleUserStateProvider],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: SingleUserStateProvider,
|
||||
@@ -1317,7 +1388,6 @@ const safeProviders: SafeProvider[] = [
|
||||
I18nServiceAbstraction,
|
||||
OrganizationApiServiceAbstraction,
|
||||
SyncService,
|
||||
ConfigService,
|
||||
],
|
||||
}),
|
||||
safeProvider({
|
||||
@@ -1356,9 +1426,9 @@ const safeProviders: SafeProvider[] = [
|
||||
deps: [ApiServiceAbstraction],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: TaxServiceAbstraction,
|
||||
useClass: TaxService,
|
||||
deps: [ApiServiceAbstraction],
|
||||
provide: OrganizationMetadataServiceAbstraction,
|
||||
useClass: DefaultOrganizationMetadataService,
|
||||
deps: [BillingApiServiceAbstraction, ConfigService],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: BillingAccountProfileStateService,
|
||||
@@ -1417,21 +1487,6 @@ const safeProviders: SafeProvider[] = [
|
||||
useClass: DefaultOrganizationInviteService,
|
||||
deps: [],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: SetPasswordJitService,
|
||||
useClass: DefaultSetPasswordJitService,
|
||||
deps: [
|
||||
EncryptService,
|
||||
I18nServiceAbstraction,
|
||||
KdfConfigService,
|
||||
KeyService,
|
||||
MasterPasswordApiServiceAbstraction,
|
||||
InternalMasterPasswordServiceAbstraction,
|
||||
OrganizationApiServiceAbstraction,
|
||||
OrganizationUserApiService,
|
||||
InternalUserDecryptionOptionsServiceAbstraction,
|
||||
],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: SetInitialPasswordService,
|
||||
useClass: DefaultSetInitialPasswordService,
|
||||
@@ -1473,6 +1528,11 @@ const safeProviders: SafeProvider[] = [
|
||||
useClass: DefaultTwoFactorAuthWebAuthnComponentService,
|
||||
deps: [],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: TwoFactorApiService,
|
||||
useClass: DefaultTwoFactorApiService,
|
||||
deps: [ApiServiceAbstraction],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: ViewCacheService,
|
||||
useExisting: NoopViewCacheService,
|
||||
@@ -1499,6 +1559,10 @@ const safeProviders: SafeProvider[] = [
|
||||
AccountServiceAbstraction,
|
||||
KdfConfigService,
|
||||
KeyService,
|
||||
SecurityStateService,
|
||||
ApiServiceAbstraction,
|
||||
StateProvider,
|
||||
ConfigService,
|
||||
],
|
||||
}),
|
||||
safeProvider({
|
||||
@@ -1512,8 +1576,8 @@ const safeProviders: SafeProvider[] = [
|
||||
deps: [CryptoFunctionServiceAbstraction],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: LoginApprovalComponentServiceAbstraction,
|
||||
useClass: DefaultLoginApprovalComponentService,
|
||||
provide: LoginApprovalDialogComponentServiceAbstraction,
|
||||
useClass: DefaultLoginApprovalDialogComponentService,
|
||||
deps: [],
|
||||
}),
|
||||
safeProvider({
|
||||
@@ -1542,7 +1606,14 @@ const safeProviders: SafeProvider[] = [
|
||||
safeProvider({
|
||||
provide: LoginSuccessHandlerService,
|
||||
useClass: DefaultLoginSuccessHandlerService,
|
||||
deps: [SyncService, UserAsymmetricKeysRegenerationService, LoginEmailService],
|
||||
deps: [
|
||||
ConfigService,
|
||||
LoginEmailService,
|
||||
SsoLoginServiceAbstraction,
|
||||
SyncService,
|
||||
UserAsymmetricKeysRegenerationService,
|
||||
LogService,
|
||||
],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: TaskService,
|
||||
@@ -1552,17 +1623,22 @@ const safeProviders: SafeProvider[] = [
|
||||
ApiServiceAbstraction,
|
||||
OrganizationServiceAbstraction,
|
||||
AuthServiceAbstraction,
|
||||
NotificationsService,
|
||||
ServerNotificationsService,
|
||||
MessageListener,
|
||||
],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: SendTokenService,
|
||||
useClass: DefaultSendTokenService,
|
||||
deps: [GlobalStateProvider, SdkService, SendPasswordService],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: EndUserNotificationService,
|
||||
useClass: DefaultEndUserNotificationService,
|
||||
deps: [
|
||||
StateProvider,
|
||||
ApiServiceAbstraction,
|
||||
NotificationsService,
|
||||
ServerNotificationsService,
|
||||
AuthServiceAbstraction,
|
||||
LogService,
|
||||
],
|
||||
@@ -1606,6 +1682,21 @@ const safeProviders: SafeProvider[] = [
|
||||
InternalMasterPasswordServiceAbstraction,
|
||||
],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: CipherArchiveService,
|
||||
useClass: DefaultCipherArchiveService,
|
||||
deps: [
|
||||
CipherServiceAbstraction,
|
||||
ApiServiceAbstraction,
|
||||
BillingAccountProfileStateService,
|
||||
ConfigService,
|
||||
],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: NewDeviceVerificationComponentService,
|
||||
useClass: DefaultNewDeviceVerificationComponentService,
|
||||
deps: [],
|
||||
}),
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
<div class="progress">
|
||||
<div
|
||||
class="progress-bar {{ color }}"
|
||||
role="progressbar"
|
||||
[ngStyle]="{ width: scoreWidth + '%' }"
|
||||
attr.aria-valuenow="{{ scoreWidth }}"
|
||||
aria-valuemin="0"
|
||||
aria-valuemax="100"
|
||||
>
|
||||
<ng-container *ngIf="showText && text">
|
||||
{{ text }}
|
||||
</ng-container>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,118 +0,0 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Component, EventEmitter, Input, OnChanges, Output } from "@angular/core";
|
||||
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength";
|
||||
|
||||
export interface PasswordColorText {
|
||||
color: string;
|
||||
text: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated July 2024: Use new PasswordStrengthV2Component instead
|
||||
*/
|
||||
@Component({
|
||||
selector: "app-password-strength",
|
||||
templateUrl: "password-strength.component.html",
|
||||
standalone: false,
|
||||
})
|
||||
export class PasswordStrengthComponent implements OnChanges {
|
||||
@Input() showText = false;
|
||||
@Input() email: string;
|
||||
@Input() name: string;
|
||||
@Input() set password(value: string) {
|
||||
this.updatePasswordStrength(value);
|
||||
}
|
||||
@Output() passwordStrengthResult = new EventEmitter<any>();
|
||||
@Output() passwordScoreColor = new EventEmitter<PasswordColorText>();
|
||||
|
||||
masterPasswordScore: number;
|
||||
scoreWidth = 0;
|
||||
color = "bg-danger";
|
||||
text: string;
|
||||
|
||||
private masterPasswordStrengthTimeout: any;
|
||||
|
||||
//used by desktop and browser to display strength text color
|
||||
get masterPasswordScoreColor() {
|
||||
switch (this.masterPasswordScore) {
|
||||
case 4:
|
||||
return "success";
|
||||
case 3:
|
||||
return "primary";
|
||||
case 2:
|
||||
return "warning";
|
||||
default:
|
||||
return "danger";
|
||||
}
|
||||
}
|
||||
|
||||
//used by desktop and browser to display strength text
|
||||
get masterPasswordScoreText() {
|
||||
switch (this.masterPasswordScore) {
|
||||
case 4:
|
||||
return this.i18nService.t("strong");
|
||||
case 3:
|
||||
return this.i18nService.t("good");
|
||||
case 2:
|
||||
return this.i18nService.t("weak");
|
||||
default:
|
||||
return this.masterPasswordScore != null ? this.i18nService.t("weak") : null;
|
||||
}
|
||||
}
|
||||
|
||||
constructor(
|
||||
private i18nService: I18nService,
|
||||
private passwordStrengthService: PasswordStrengthServiceAbstraction,
|
||||
) {}
|
||||
|
||||
ngOnChanges(): void {
|
||||
this.masterPasswordStrengthTimeout = setTimeout(() => {
|
||||
this.scoreWidth = this.masterPasswordScore == null ? 0 : (this.masterPasswordScore + 1) * 20;
|
||||
|
||||
switch (this.masterPasswordScore) {
|
||||
case 4:
|
||||
this.color = "bg-success";
|
||||
this.text = this.i18nService.t("strong");
|
||||
break;
|
||||
case 3:
|
||||
this.color = "bg-primary";
|
||||
this.text = this.i18nService.t("good");
|
||||
break;
|
||||
case 2:
|
||||
this.color = "bg-warning";
|
||||
this.text = this.i18nService.t("weak");
|
||||
break;
|
||||
default:
|
||||
this.color = "bg-danger";
|
||||
this.text = this.masterPasswordScore != null ? this.i18nService.t("weak") : null;
|
||||
break;
|
||||
}
|
||||
|
||||
this.setPasswordScoreText(this.color, this.text);
|
||||
}, 300);
|
||||
}
|
||||
|
||||
updatePasswordStrength(password: string) {
|
||||
const masterPassword = password;
|
||||
|
||||
if (this.masterPasswordStrengthTimeout != null) {
|
||||
clearTimeout(this.masterPasswordStrengthTimeout);
|
||||
}
|
||||
|
||||
const strengthResult = this.passwordStrengthService.getPasswordStrength(
|
||||
masterPassword,
|
||||
this.email,
|
||||
this.name?.trim().toLowerCase().split(" "),
|
||||
);
|
||||
this.passwordStrengthResult.emit(strengthResult);
|
||||
this.masterPasswordScore = strengthResult == null ? null : strengthResult.score;
|
||||
}
|
||||
|
||||
setPasswordScoreText(color: string, text: string) {
|
||||
color = color.slice(3);
|
||||
this.passwordScoreColor.emit({ color: color, text: text });
|
||||
}
|
||||
}
|
||||
@@ -260,12 +260,19 @@ export class AddEditComponent implements OnInit, OnDestroy {
|
||||
});
|
||||
|
||||
if (this.editMode) {
|
||||
this.sendService
|
||||
.get$(this.sendId)
|
||||
this.accountService.activeAccount$
|
||||
.pipe(
|
||||
//Promise.reject will complete the BehaviourSubject, if desktop starts relying only on BehaviourSubject, this should be changed.
|
||||
concatMap((s) =>
|
||||
s instanceof Send ? s.decrypt() : Promise.reject(new Error("Failed to load send.")),
|
||||
getUserId,
|
||||
switchMap((userId) =>
|
||||
this.sendService
|
||||
.get$(this.sendId)
|
||||
.pipe(
|
||||
concatMap((s) =>
|
||||
s instanceof Send
|
||||
? s.decrypt(userId)
|
||||
: Promise.reject(new Error("Failed to load send.")),
|
||||
),
|
||||
),
|
||||
),
|
||||
takeUntil(this.destroy$),
|
||||
)
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Observable } from "rxjs";
|
||||
|
||||
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop.
|
||||
|
||||
@@ -63,7 +63,7 @@ export class FolderAddEditComponent implements OnInit {
|
||||
|
||||
try {
|
||||
const activeUserId = await firstValueFrom(this.activeUserId$);
|
||||
const userKey = await this.keyService.getUserKeyWithLegacySupport(activeUserId);
|
||||
const userKey = await this.keyService.getUserKey(activeUserId);
|
||||
const folder = await this.folderService.encrypt(this.folder, userKey);
|
||||
this.formPromise = this.folderApiService.save(folder, activeUserId);
|
||||
await this.formPromise;
|
||||
|
||||
@@ -1,19 +1,44 @@
|
||||
<div class="tw-flex tw-justify-center tw-items-center" aria-hidden="true">
|
||||
<!-- Applying width and height styles directly to synchronize icon sizing between web/browser/desktop -->
|
||||
<div
|
||||
class="tw-flex tw-justify-center tw-items-center"
|
||||
[ngStyle]="coloredIcon() ? { width: '36px', height: '36px' } : {}"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<ng-container *ngIf="data$ | async as data">
|
||||
<img
|
||||
[src]="data.image"
|
||||
*ngIf="data.imageEnabled && data.image"
|
||||
class="tw-size-6 tw-rounded-md"
|
||||
alt=""
|
||||
decoding="async"
|
||||
loading="lazy"
|
||||
[ngClass]="{ 'tw-invisible tw-absolute': !imageLoaded() }"
|
||||
(load)="imageLoaded.set(true)"
|
||||
(error)="imageLoaded.set(false)"
|
||||
/>
|
||||
<i
|
||||
class="tw-w-6 tw-text-muted bwi bwi-lg {{ data.icon }}"
|
||||
*ngIf="!data.imageEnabled || !data.image || !imageLoaded()"
|
||||
></i>
|
||||
@if (data.imageEnabled && data.image) {
|
||||
<img
|
||||
[src]="data.image"
|
||||
class="tw-rounded-md"
|
||||
alt=""
|
||||
decoding="async"
|
||||
loading="lazy"
|
||||
[ngClass]="{
|
||||
'tw-invisible tw-absolute': !imageLoaded(),
|
||||
'tw-size-6': !coloredIcon(),
|
||||
}"
|
||||
[ngStyle]="coloredIcon() ? { width: '36px', height: '36px' } : {}"
|
||||
(load)="imageLoaded.set(true)"
|
||||
(error)="imageLoaded.set(false)"
|
||||
/>
|
||||
}
|
||||
@if (!data.imageEnabled || !data.image || !imageLoaded()) {
|
||||
<div
|
||||
[ngClass]="{
|
||||
'tw-flex tw-items-center tw-justify-center': coloredIcon(),
|
||||
'tw-bg-illustration-bg-primary tw-rounded-full':
|
||||
data.icon?.startsWith('bwi-') && coloredIcon(),
|
||||
}"
|
||||
[ngStyle]="coloredIcon() ? { width: '36px', height: '36px' } : {}"
|
||||
>
|
||||
<i
|
||||
class="tw-text-muted bwi bwi-lg {{ data.icon }}"
|
||||
[ngStyle]="{
|
||||
color: coloredIcon() ? 'rgb(var(--color-illustration-outline))' : null,
|
||||
width: data.icon?.startsWith('credit-card') && coloredIcon() ? '36px' : null,
|
||||
height: data.icon?.startsWith('credit-card') && coloredIcon() ? '30px' : null,
|
||||
}"
|
||||
></i>
|
||||
</div>
|
||||
}
|
||||
</ng-container>
|
||||
</div>
|
||||
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service";
|
||||
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||
import { buildCipherIcon, CipherIconDetails } from "@bitwarden/common/vault/icon/build-cipher-icon";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
import { CipherViewLike } from "@bitwarden/common/vault/utils/cipher-view-like-utils";
|
||||
|
||||
@Component({
|
||||
selector: "app-vault-icon",
|
||||
@@ -25,7 +25,12 @@ export class IconComponent {
|
||||
/**
|
||||
* The cipher to display the icon for.
|
||||
*/
|
||||
cipher = input.required<CipherView>();
|
||||
cipher = input.required<CipherViewLike>();
|
||||
|
||||
/**
|
||||
* coloredIcon will adjust the size of favicons and the colors of the text icon when user is in the item details view.
|
||||
*/
|
||||
coloredIcon = input<boolean>(false);
|
||||
|
||||
imageLoaded = signal(false);
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<div
|
||||
class="tw-rounded-2xl tw-bg-primary-100 tw-border-primary-600 tw-border-solid tw-border tw-p-4 tw-pt-3 tw-flex tw-flex-col tw-gap-2"
|
||||
class="tw-rounded-2xl tw-bg-primary-100 tw-border-primary-600 tw-border-solid tw-border tw-p-4 tw-pt-2 tw-flex tw-flex-col tw-gap-2 tw-mb-4"
|
||||
>
|
||||
<div class="tw-flex tw-justify-between tw-items-start tw-flex-grow">
|
||||
<div>
|
||||
@@ -18,8 +18,8 @@
|
||||
size="small"
|
||||
*ngIf="!persistent"
|
||||
(click)="handleDismiss()"
|
||||
[attr.title]="'close' | i18n"
|
||||
[attr.aria-label]="'close' | i18n"
|
||||
class="-tw-me-2"
|
||||
[label]="'close' | i18n"
|
||||
></button>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ const meta: Meta<SpotlightComponent> = {
|
||||
useFactory: () => {
|
||||
return new I18nMockService({
|
||||
close: "Close",
|
||||
loading: "Loading",
|
||||
});
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1,18 +1,16 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Directive, EventEmitter, Input, OnDestroy, OnInit, Output } from "@angular/core";
|
||||
import { Directive, EventEmitter, Input, OnDestroy, Output } from "@angular/core";
|
||||
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
|
||||
import {
|
||||
BehaviorSubject,
|
||||
Subject,
|
||||
combineLatest,
|
||||
filter,
|
||||
from,
|
||||
map,
|
||||
of,
|
||||
shareReplay,
|
||||
switchMap,
|
||||
takeUntil,
|
||||
} from "rxjs";
|
||||
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
@@ -21,20 +19,23 @@ import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { SearchService } from "@bitwarden/common/vault/abstractions/search.service";
|
||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
import { RestrictedItemTypesService } from "@bitwarden/common/vault/services/restricted-item-types.service";
|
||||
import { CIPHER_MENU_ITEMS } from "@bitwarden/common/vault/types/cipher-menu-items";
|
||||
import {
|
||||
CipherViewLike,
|
||||
CipherViewLikeUtils,
|
||||
} from "@bitwarden/common/vault/utils/cipher-view-like-utils";
|
||||
|
||||
@Directive()
|
||||
export class VaultItemsComponent implements OnInit, OnDestroy {
|
||||
export class VaultItemsComponent<C extends CipherViewLike> implements OnDestroy {
|
||||
@Input() activeCipherId: string = null;
|
||||
@Output() onCipherClicked = new EventEmitter<CipherView>();
|
||||
@Output() onCipherRightClicked = new EventEmitter<CipherView>();
|
||||
@Output() onCipherClicked = new EventEmitter<C>();
|
||||
@Output() onCipherRightClicked = new EventEmitter<C>();
|
||||
@Output() onAddCipher = new EventEmitter<CipherType | undefined>();
|
||||
@Output() onAddCipherOptions = new EventEmitter();
|
||||
|
||||
loaded = false;
|
||||
ciphers: CipherView[] = [];
|
||||
ciphers: C[] = [];
|
||||
deleted = false;
|
||||
organization: Organization;
|
||||
CipherType = CipherType;
|
||||
@@ -52,12 +53,9 @@ export class VaultItemsComponent implements OnInit, OnDestroy {
|
||||
shareReplay({ bufferSize: 1, refCount: true }),
|
||||
);
|
||||
|
||||
protected searchPending = false;
|
||||
|
||||
/** Construct filters as an observable so it can be appended to the cipher stream. */
|
||||
private _filter$ = new BehaviorSubject<(cipher: CipherView) => boolean | null>(null);
|
||||
private _filter$ = new BehaviorSubject<(cipher: C) => boolean | null>(null);
|
||||
private destroy$ = new Subject<void>();
|
||||
private isSearchable: boolean = false;
|
||||
private _searchText$ = new BehaviorSubject<string>("");
|
||||
|
||||
get searchText() {
|
||||
@@ -71,7 +69,7 @@ export class VaultItemsComponent implements OnInit, OnDestroy {
|
||||
return this._filter$.value;
|
||||
}
|
||||
|
||||
set filter(value: (cipher: CipherView) => boolean | null) {
|
||||
set filter(value: (cipher: C) => boolean | null) {
|
||||
this._filter$.next(value);
|
||||
}
|
||||
|
||||
@@ -84,31 +82,18 @@ export class VaultItemsComponent implements OnInit, OnDestroy {
|
||||
this.subscribeToCiphers();
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
combineLatest([getUserId(this.accountService.activeAccount$), this._searchText$])
|
||||
.pipe(
|
||||
switchMap(([userId, searchText]) =>
|
||||
from(this.searchService.isSearchable(userId, searchText)),
|
||||
),
|
||||
takeUntil(this.destroy$),
|
||||
)
|
||||
.subscribe((isSearchable) => {
|
||||
this.isSearchable = isSearchable;
|
||||
});
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.destroy$.next();
|
||||
this.destroy$.complete();
|
||||
}
|
||||
|
||||
async load(filter: (cipher: CipherView) => boolean = null, deleted = false) {
|
||||
async load(filter: (cipher: C) => boolean = null, deleted = false) {
|
||||
this.deleted = deleted ?? false;
|
||||
await this.applyFilter(filter);
|
||||
this.loaded = true;
|
||||
}
|
||||
|
||||
async reload(filter: (cipher: CipherView) => boolean = null, deleted = false) {
|
||||
async reload(filter: (cipher: C) => boolean = null, deleted = false) {
|
||||
this.loaded = false;
|
||||
await this.load(filter, deleted);
|
||||
}
|
||||
@@ -117,15 +102,15 @@ export class VaultItemsComponent implements OnInit, OnDestroy {
|
||||
await this.reload(this.filter, this.deleted);
|
||||
}
|
||||
|
||||
async applyFilter(filter: (cipher: CipherView) => boolean = null) {
|
||||
async applyFilter(filter: (cipher: C) => boolean = null) {
|
||||
this.filter = filter;
|
||||
}
|
||||
|
||||
selectCipher(cipher: CipherView) {
|
||||
selectCipher(cipher: C) {
|
||||
this.onCipherClicked.emit(cipher);
|
||||
}
|
||||
|
||||
rightClickCipher(cipher: CipherView) {
|
||||
rightClickCipher(cipher: C) {
|
||||
this.onCipherRightClicked.emit(cipher);
|
||||
}
|
||||
|
||||
@@ -137,11 +122,8 @@ export class VaultItemsComponent implements OnInit, OnDestroy {
|
||||
this.onAddCipherOptions.emit();
|
||||
}
|
||||
|
||||
isSearching() {
|
||||
return !this.searchPending && this.isSearchable;
|
||||
}
|
||||
|
||||
protected deletedFilter: (cipher: CipherView) => boolean = (c) => c.isDeleted === this.deleted;
|
||||
protected deletedFilter: (cipher: C) => boolean = (c) =>
|
||||
CipherViewLikeUtils.isDeleted(c) === this.deleted;
|
||||
|
||||
/**
|
||||
* Creates stream of dependencies that results in the list of ciphers to display
|
||||
@@ -156,7 +138,7 @@ export class VaultItemsComponent implements OnInit, OnDestroy {
|
||||
.pipe(
|
||||
switchMap((userId) =>
|
||||
combineLatest([
|
||||
this.cipherService.cipherViews$(userId).pipe(filter((ciphers) => ciphers != null)),
|
||||
this.cipherService.cipherListViews$(userId).pipe(filter((ciphers) => ciphers != null)),
|
||||
this.cipherService.failedToDecryptCiphers$(userId),
|
||||
this._searchText$,
|
||||
this._filter$,
|
||||
@@ -165,12 +147,12 @@ export class VaultItemsComponent implements OnInit, OnDestroy {
|
||||
]),
|
||||
),
|
||||
switchMap(([indexedCiphers, failedCiphers, searchText, filter, userId, restricted]) => {
|
||||
let allCiphers = indexedCiphers ?? [];
|
||||
let allCiphers = (indexedCiphers ?? []) as C[];
|
||||
const _failedCiphers = failedCiphers ?? [];
|
||||
|
||||
allCiphers = [..._failedCiphers, ...allCiphers];
|
||||
allCiphers = [..._failedCiphers, ...allCiphers] as C[];
|
||||
|
||||
const restrictedTypeFilter = (cipher: CipherView) =>
|
||||
const restrictedTypeFilter = (cipher: CipherViewLike) =>
|
||||
!this.restrictedItemTypesService.isCipherRestricted(cipher, restricted);
|
||||
|
||||
return this.searchService.searchCiphers(
|
||||
|
||||
@@ -3,13 +3,10 @@ import { Observable, combineLatest, from, of } from "rxjs";
|
||||
import { catchError, switchMap } from "rxjs/operators";
|
||||
|
||||
import { VaultProfileService } from "@bitwarden/angular/vault/services/vault-profile.service";
|
||||
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop.
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { PinServiceAbstraction } from "@bitwarden/auth/common";
|
||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { PolicyType } from "@bitwarden/common/admin-console/enums";
|
||||
import { VaultTimeoutSettingsService } from "@bitwarden/common/key-management/vault-timeout";
|
||||
import { PinServiceAbstraction } from "@bitwarden/common/key-management/pin/pin.service.abstraction";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { BiometricStateService } from "@bitwarden/key-management";
|
||||
@@ -24,7 +21,6 @@ export class AccountSecurityNudgeService extends DefaultSingleNudgeService {
|
||||
private vaultProfileService = inject(VaultProfileService);
|
||||
private logService = inject(LogService);
|
||||
private pinService = inject(PinServiceAbstraction);
|
||||
private vaultTimeoutSettingsService = inject(VaultTimeoutSettingsService);
|
||||
private biometricStateService = inject(BiometricStateService);
|
||||
private policyService = inject(PolicyService);
|
||||
private organizationService = inject(OrganizationService);
|
||||
@@ -76,7 +72,10 @@ export class AccountSecurityNudgeService extends DefaultSingleNudgeService {
|
||||
hasSpotlightDismissed: status.hasSpotlightDismissed || hideNudge,
|
||||
};
|
||||
|
||||
if (isPinSet || biometricUnlockEnabled || hasOrgWithRemovePinPolicyOn) {
|
||||
if (
|
||||
(isPinSet || biometricUnlockEnabled || hasOrgWithRemovePinPolicyOn) &&
|
||||
!status.hasSpotlightDismissed
|
||||
) {
|
||||
await this.setNudgeStatus(nudgeType, acctSecurityNudgeStatus, userId);
|
||||
}
|
||||
return acctSecurityNudgeStatus;
|
||||
|
||||
@@ -5,7 +5,7 @@ import { combineLatest, Observable, of, switchMap } from "rxjs";
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { CollectionService } from "@bitwarden/admin-console/common";
|
||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { OrganizationId, UserId } from "@bitwarden/common/types/guid";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
|
||||
import { DefaultSingleNudgeService } from "../default-single-nudge.service";
|
||||
@@ -25,9 +25,9 @@ export class EmptyVaultNudgeService extends DefaultSingleNudgeService {
|
||||
nudgeStatus$(nudgeType: NudgeType, userId: UserId): Observable<NudgeStatus> {
|
||||
return combineLatest([
|
||||
this.getNudgeStatus$(nudgeType, userId),
|
||||
this.cipherService.cipherViews$(userId),
|
||||
this.cipherService.cipherListViews$(userId),
|
||||
this.organizationService.organizations$(userId),
|
||||
this.collectionService.decryptedCollections$,
|
||||
this.collectionService.decryptedCollections$(userId),
|
||||
]).pipe(
|
||||
switchMap(([nudgeStatus, ciphers, orgs, collections]) => {
|
||||
const vaultHasContents = !(ciphers == null || ciphers.length === 0);
|
||||
@@ -42,7 +42,7 @@ export class EmptyVaultNudgeService extends DefaultSingleNudgeService {
|
||||
const orgIds = new Set(orgs.map((org) => org.id));
|
||||
const canCreateCollections = orgs.some((org) => org.canCreateNewCollections);
|
||||
const hasManageCollections = collections.some(
|
||||
(c) => c.manage && orgIds.has(c.organizationId),
|
||||
(c) => c.manage && orgIds.has(c.organizationId! as OrganizationId),
|
||||
);
|
||||
|
||||
// When the user has dismissed the nudge or spotlight, return the nudge status directly
|
||||
|
||||
@@ -44,7 +44,11 @@ export class HasItemsNudgeService extends DefaultSingleNudgeService {
|
||||
return cipher.deletedDate == null;
|
||||
});
|
||||
|
||||
if (profileOlderThanCutoff && filteredCiphers.length > 0) {
|
||||
if (
|
||||
profileOlderThanCutoff &&
|
||||
filteredCiphers.length > 0 &&
|
||||
!nudgeStatus.hasSpotlightDismissed
|
||||
) {
|
||||
const dismissedStatus = {
|
||||
hasSpotlightDismissed: true,
|
||||
hasBadgeDismissed: true,
|
||||
|
||||
@@ -49,7 +49,7 @@ export class NewItemNudgeService extends DefaultSingleNudgeService {
|
||||
|
||||
const ciphersBoolean = ciphers.some((cipher) => cipher.type === currentType);
|
||||
|
||||
if (ciphersBoolean) {
|
||||
if (ciphersBoolean && !nudgeStatus.hasSpotlightDismissed) {
|
||||
const dismissedStatus = {
|
||||
hasSpotlightDismissed: true,
|
||||
hasBadgeDismissed: true,
|
||||
|
||||
@@ -5,7 +5,7 @@ import { combineLatest, Observable, of, switchMap } from "rxjs";
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { CollectionService } from "@bitwarden/admin-console/common";
|
||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { OrganizationId, UserId } from "@bitwarden/common/types/guid";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
|
||||
import { DefaultSingleNudgeService } from "../default-single-nudge.service";
|
||||
@@ -27,7 +27,7 @@ export class VaultSettingsImportNudgeService extends DefaultSingleNudgeService {
|
||||
this.getNudgeStatus$(nudgeType, userId),
|
||||
this.cipherService.cipherViews$(userId),
|
||||
this.organizationService.organizations$(userId),
|
||||
this.collectionService.decryptedCollections$,
|
||||
this.collectionService.decryptedCollections$(userId),
|
||||
]).pipe(
|
||||
switchMap(([nudgeStatus, ciphers, orgs, collections]) => {
|
||||
const vaultHasMoreThanOneItem = (ciphers?.length ?? 0) > 1;
|
||||
@@ -46,7 +46,7 @@ export class VaultSettingsImportNudgeService extends DefaultSingleNudgeService {
|
||||
const orgIds = new Set(orgs.map((org) => org.id));
|
||||
const canCreateCollections = orgs.some((org) => org.canCreateNewCollections);
|
||||
const hasManageCollections = collections.some(
|
||||
(c) => c.manage && orgIds.has(c.organizationId),
|
||||
(c) => c.manage && orgIds.has(c.organizationId! as OrganizationId),
|
||||
);
|
||||
|
||||
// When the user has dismissed the nudge or spotlight, return the nudge status directly
|
||||
|
||||
@@ -2,15 +2,12 @@ import { TestBed } from "@angular/core/testing";
|
||||
import { mock } from "jest-mock-extended";
|
||||
import { firstValueFrom, of } from "rxjs";
|
||||
|
||||
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop.
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { PinServiceAbstraction } from "@bitwarden/auth/common";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { PinServiceAbstraction } from "@bitwarden/common/key-management/pin/pin.service.abstraction";
|
||||
import { VaultTimeoutSettingsService } from "@bitwarden/common/key-management/vault-timeout";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { StateProvider } from "@bitwarden/common/platform/state";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
@@ -23,6 +20,7 @@ import {
|
||||
HasItemsNudgeService,
|
||||
EmptyVaultNudgeService,
|
||||
NewAccountNudgeService,
|
||||
AccountSecurityNudgeService,
|
||||
VaultSettingsImportNudgeService,
|
||||
} from "./custom-nudges-services";
|
||||
import { DefaultSingleNudgeService } from "./default-single-nudge.service";
|
||||
@@ -32,12 +30,12 @@ describe("Vault Nudges Service", () => {
|
||||
let fakeStateProvider: FakeStateProvider;
|
||||
|
||||
let testBed: TestBed;
|
||||
const mockConfigService = {
|
||||
getFeatureFlag$: jest.fn().mockReturnValue(of(true)),
|
||||
getFeatureFlag: jest.fn().mockReturnValue(true),
|
||||
};
|
||||
|
||||
const nudgeServices = [EmptyVaultNudgeService, NewAccountNudgeService];
|
||||
const nudgeServices = [
|
||||
EmptyVaultNudgeService,
|
||||
NewAccountNudgeService,
|
||||
AccountSecurityNudgeService,
|
||||
];
|
||||
|
||||
beforeEach(async () => {
|
||||
fakeStateProvider = new FakeStateProvider(mockAccountServiceWith("user-id" as UserId));
|
||||
@@ -55,7 +53,6 @@ describe("Vault Nudges Service", () => {
|
||||
provide: StateProvider,
|
||||
useValue: fakeStateProvider,
|
||||
},
|
||||
{ provide: ConfigService, useValue: mockConfigService },
|
||||
{
|
||||
provide: HasItemsNudgeService,
|
||||
useValue: mock<HasItemsNudgeService>(),
|
||||
@@ -68,6 +65,10 @@ describe("Vault Nudges Service", () => {
|
||||
provide: EmptyVaultNudgeService,
|
||||
useValue: mock<EmptyVaultNudgeService>(),
|
||||
},
|
||||
{
|
||||
provide: AccountSecurityNudgeService,
|
||||
useValue: mock<AccountSecurityNudgeService>(),
|
||||
},
|
||||
{
|
||||
provide: VaultSettingsImportNudgeService,
|
||||
useValue: mock<VaultSettingsImportNudgeService>(),
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import { inject, Injectable } from "@angular/core";
|
||||
import { combineLatest, map, Observable, of, shareReplay, switchMap } from "rxjs";
|
||||
import { combineLatest, map, Observable, shareReplay } from "rxjs";
|
||||
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { UserKeyDefinition, NUDGES_DISK } from "@bitwarden/common/platform/state";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { UnionOfValues } from "@bitwarden/common/vault/types/union-of-values";
|
||||
@@ -83,7 +81,6 @@ export class NudgesService {
|
||||
* @private
|
||||
*/
|
||||
private defaultNudgeService = inject(DefaultSingleNudgeService);
|
||||
private configService = inject(ConfigService);
|
||||
|
||||
private getNudgeService(nudge: NudgeType): SingleNudgeService {
|
||||
return this.customNudgeServices[nudge] ?? this.defaultNudgeService;
|
||||
@@ -95,16 +92,9 @@ export class NudgesService {
|
||||
* @param userId
|
||||
*/
|
||||
showNudgeSpotlight$(nudge: NudgeType, userId: UserId): Observable<boolean> {
|
||||
return this.configService.getFeatureFlag$(FeatureFlag.PM8851_BrowserOnboardingNudge).pipe(
|
||||
switchMap((hasVaultNudgeFlag) => {
|
||||
if (!hasVaultNudgeFlag) {
|
||||
return of(false);
|
||||
}
|
||||
return this.getNudgeService(nudge)
|
||||
.nudgeStatus$(nudge, userId)
|
||||
.pipe(map((nudgeStatus) => !nudgeStatus.hasSpotlightDismissed));
|
||||
}),
|
||||
);
|
||||
return this.getNudgeService(nudge)
|
||||
.nudgeStatus$(nudge, userId)
|
||||
.pipe(map((nudgeStatus) => !nudgeStatus.hasSpotlightDismissed));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -113,16 +103,9 @@ export class NudgesService {
|
||||
* @param userId
|
||||
*/
|
||||
showNudgeBadge$(nudge: NudgeType, userId: UserId): Observable<boolean> {
|
||||
return this.configService.getFeatureFlag$(FeatureFlag.PM8851_BrowserOnboardingNudge).pipe(
|
||||
switchMap((hasVaultNudgeFlag) => {
|
||||
if (!hasVaultNudgeFlag) {
|
||||
return of(false);
|
||||
}
|
||||
return this.getNudgeService(nudge)
|
||||
.nudgeStatus$(nudge, userId)
|
||||
.pipe(map((nudgeStatus) => !nudgeStatus.hasBadgeDismissed));
|
||||
}),
|
||||
);
|
||||
return this.getNudgeService(nudge)
|
||||
.nudgeStatus$(nudge, userId)
|
||||
.pipe(map((nudgeStatus) => !nudgeStatus.hasBadgeDismissed));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -131,14 +114,7 @@ export class NudgesService {
|
||||
* @param userId
|
||||
*/
|
||||
showNudgeStatus$(nudge: NudgeType, userId: UserId) {
|
||||
return this.configService.getFeatureFlag$(FeatureFlag.PM8851_BrowserOnboardingNudge).pipe(
|
||||
switchMap((hasVaultNudgeFlag) => {
|
||||
if (!hasVaultNudgeFlag) {
|
||||
return of({ hasBadgeDismissed: true, hasSpotlightDismissed: true } as NudgeStatus);
|
||||
}
|
||||
return this.getNudgeService(nudge).nudgeStatus$(nudge, userId);
|
||||
}),
|
||||
);
|
||||
return this.getNudgeService(nudge).nudgeStatus$(nudge, userId);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -4,7 +4,7 @@ import { Directive, EventEmitter, Input, Output } from "@angular/core";
|
||||
|
||||
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop.
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { CollectionView } from "@bitwarden/admin-console/common";
|
||||
import { CollectionTypes, CollectionView } from "@bitwarden/admin-console/common";
|
||||
import { ITreeNodeObject } from "@bitwarden/common/vault/models/domain/tree-node";
|
||||
|
||||
import { DynamicTreeNode } from "../models/dynamic-tree-node.model";
|
||||
@@ -21,6 +21,7 @@ export class CollectionFilterComponent {
|
||||
@Output() onNodeCollapseStateChange: EventEmitter<ITreeNodeObject> =
|
||||
new EventEmitter<ITreeNodeObject>();
|
||||
@Output() onFilterChange: EventEmitter<VaultFilter> = new EventEmitter<VaultFilter>();
|
||||
DefaultCollectionType = CollectionTypes.DefaultUserCollection;
|
||||
|
||||
readonly collectionsGrouping: TopLevelTreeNode = {
|
||||
id: "collections",
|
||||
|
||||
@@ -118,15 +118,18 @@ describe("VaultFilter", () => {
|
||||
});
|
||||
|
||||
describe("given an organizational cipher (with organization and collections)", () => {
|
||||
const collection1 = "e9652fc0-1fe4-48d5-a3d8-d821e32fbd98";
|
||||
const collection2 = "42a971a5-8c16-48a3-a725-4be27cd99bc9";
|
||||
|
||||
const cipher = createCipher({
|
||||
organizationId: "organizationId",
|
||||
collectionIds: ["collectionId", "anotherId"],
|
||||
collectionIds: [collection1, collection2],
|
||||
});
|
||||
|
||||
it("should return true when filter matches collection id", () => {
|
||||
const filterFunction = createFilterFunction({
|
||||
selectedCollection: true,
|
||||
selectedCollectionId: "collectionId",
|
||||
selectedCollectionId: collection1,
|
||||
});
|
||||
|
||||
const result = filterFunction(cipher);
|
||||
@@ -137,7 +140,7 @@ describe("VaultFilter", () => {
|
||||
it("should return false when filter does not match collection id", () => {
|
||||
const filterFunction = createFilterFunction({
|
||||
selectedCollection: true,
|
||||
selectedCollectionId: "nonMatchingId",
|
||||
selectedCollectionId: "1ea7ad96-3fc1-4567-8fe5-91aa9f697fd1",
|
||||
});
|
||||
|
||||
const result = filterFunction(cipher);
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { asUuid } from "@bitwarden/common/platform/abstractions/sdk/sdk.service";
|
||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
import {
|
||||
CipherViewLike,
|
||||
CipherViewLikeUtils,
|
||||
} from "@bitwarden/common/vault/utils/cipher-view-like-utils";
|
||||
|
||||
import { CipherStatus } from "./cipher-status.model";
|
||||
|
||||
export type VaultFilterFunction = (cipher: CipherView) => boolean;
|
||||
export type VaultFilterFunction = (cipher: CipherViewLike) => boolean;
|
||||
|
||||
export class VaultFilter {
|
||||
cipherType?: CipherType;
|
||||
@@ -44,10 +48,10 @@ export class VaultFilter {
|
||||
cipherPassesFilter = cipher.favorite;
|
||||
}
|
||||
if (this.status === "trash" && cipherPassesFilter) {
|
||||
cipherPassesFilter = cipher.isDeleted;
|
||||
cipherPassesFilter = CipherViewLikeUtils.isDeleted(cipher);
|
||||
}
|
||||
if (this.cipherType != null && cipherPassesFilter) {
|
||||
cipherPassesFilter = cipher.type === this.cipherType;
|
||||
cipherPassesFilter = CipherViewLikeUtils.getType(cipher) === this.cipherType;
|
||||
}
|
||||
if (this.selectedFolder && this.selectedFolderId == null && cipherPassesFilter) {
|
||||
cipherPassesFilter = cipher.folderId == null;
|
||||
@@ -62,13 +66,14 @@ export class VaultFilter {
|
||||
}
|
||||
if (this.selectedCollection && this.selectedCollectionId != null && cipherPassesFilter) {
|
||||
cipherPassesFilter =
|
||||
cipher.collectionIds != null && cipher.collectionIds.includes(this.selectedCollectionId);
|
||||
cipher.collectionIds != null &&
|
||||
cipher.collectionIds.includes(asUuid(this.selectedCollectionId));
|
||||
}
|
||||
if (this.selectedOrganizationId != null && cipherPassesFilter) {
|
||||
cipherPassesFilter = cipher.organizationId === this.selectedOrganizationId;
|
||||
}
|
||||
if (this.myVaultOnly && cipherPassesFilter) {
|
||||
cipherPassesFilter = cipher.organizationId === null;
|
||||
cipherPassesFilter = cipher.organizationId == null;
|
||||
}
|
||||
return cipherPassesFilter;
|
||||
};
|
||||
|
||||
@@ -109,7 +109,12 @@ export class VaultFilterService implements DeprecatedVaultFilterServiceAbstracti
|
||||
}
|
||||
|
||||
async buildCollections(organizationId?: string): Promise<DynamicTreeNode<CollectionView>> {
|
||||
const storedCollections = await this.collectionService.getAllDecrypted();
|
||||
const storedCollections = await firstValueFrom(
|
||||
this.accountService.activeAccount$.pipe(
|
||||
getUserId,
|
||||
switchMap((userId) => this.collectionService.decryptedCollections$(userId)),
|
||||
),
|
||||
);
|
||||
const orgs = await this.buildOrganizations();
|
||||
const defaulCollectionsFlagEnabled = await this.configService.getFeatureFlag(
|
||||
FeatureFlag.CreateDefaultLocation,
|
||||
@@ -191,6 +196,9 @@ export function sortDefaultCollections(
|
||||
.sort((a, b) => {
|
||||
const aName = orgs.find((o) => o.id === a.organizationId)?.name ?? a.organizationId;
|
||||
const bName = orgs.find((o) => o.id === b.organizationId)?.name ?? b.organizationId;
|
||||
if (!aName || !bName) {
|
||||
throw new Error("Collection does not have an organizationId.");
|
||||
}
|
||||
return collator.compare(aName, bName);
|
||||
});
|
||||
return [
|
||||
|
||||
Reference in New Issue
Block a user