mirror of
https://github.com/bitwarden/browser
synced 2026-02-28 18:43:26 +00:00
Merge branch 'main' into km/auto-kdf
This commit is contained in:
@@ -9,6 +9,8 @@ import { ButtonModule } from "@bitwarden/components";
|
||||
* This component is used to display a message to the user that their authentication session has expired.
|
||||
* It provides a button to navigate to the login page.
|
||||
*/
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
|
||||
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection
|
||||
@Component({
|
||||
selector: "app-authentication-timeout",
|
||||
imports: [CommonModule, JslibModule, ButtonModule, RouterModule],
|
||||
|
||||
@@ -9,13 +9,19 @@ import {
|
||||
TwoFactorAuthWebAuthnIcon,
|
||||
} from "@bitwarden/assets/svg";
|
||||
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
|
||||
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection
|
||||
@Component({
|
||||
selector: "auth-two-factor-icon",
|
||||
templateUrl: "./two-factor-icon.component.html",
|
||||
standalone: false,
|
||||
})
|
||||
export class TwoFactorIconComponent {
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
||||
// eslint-disable-next-line @angular-eslint/prefer-signals
|
||||
@Input() provider: any;
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
||||
// eslint-disable-next-line @angular-eslint/prefer-signals
|
||||
@Input() name: string;
|
||||
|
||||
protected readonly IconProviderMap: { [key: number | string]: Icon } = {
|
||||
|
||||
@@ -26,6 +26,8 @@ import { KeyService } from "@bitwarden/key-management";
|
||||
})
|
||||
export class UserVerificationComponent implements ControlValueAccessor, OnInit, OnDestroy {
|
||||
private _invalidSecret = false;
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
||||
// eslint-disable-next-line @angular-eslint/prefer-signals
|
||||
@Input()
|
||||
get invalidSecret() {
|
||||
return this._invalidSecret;
|
||||
@@ -43,6 +45,8 @@ export class UserVerificationComponent implements ControlValueAccessor, OnInit,
|
||||
}
|
||||
this.secret.updateValueAndValidity({ emitEvent: false });
|
||||
}
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
||||
// eslint-disable-next-line @angular-eslint/prefer-output-emitter-ref
|
||||
@Output() invalidSecretChange = new EventEmitter<boolean>();
|
||||
|
||||
hasMasterPassword = true;
|
||||
|
||||
@@ -8,6 +8,8 @@ import { I18nPipe } from "@bitwarden/ui-common";
|
||||
import { DeviceDisplayData } from "./device-management.component";
|
||||
|
||||
/** Displays user devices in an item list view */
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
|
||||
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection
|
||||
@Component({
|
||||
standalone: true,
|
||||
selector: "auth-device-management-item-group",
|
||||
@@ -15,7 +17,11 @@ import { DeviceDisplayData } from "./device-management.component";
|
||||
imports: [BadgeModule, CommonModule, ItemModule, I18nPipe],
|
||||
})
|
||||
export class DeviceManagementItemGroupComponent {
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
||||
// eslint-disable-next-line @angular-eslint/prefer-signals
|
||||
@Input() devices: DeviceDisplayData[] = [];
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
||||
// eslint-disable-next-line @angular-eslint/prefer-output-emitter-ref
|
||||
@Output() onAuthRequestAnswered = new EventEmitter<DevicePendingAuthRequest>();
|
||||
|
||||
protected answerAuthRequest(pendingAuthRequest: DevicePendingAuthRequest | null) {
|
||||
|
||||
@@ -15,6 +15,8 @@ import {
|
||||
import { DeviceDisplayData } from "./device-management.component";
|
||||
|
||||
/** Displays user devices in a sortable table view */
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
|
||||
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection
|
||||
@Component({
|
||||
standalone: true,
|
||||
selector: "auth-device-management-table",
|
||||
@@ -22,7 +24,11 @@ import { DeviceDisplayData } from "./device-management.component";
|
||||
imports: [BadgeModule, ButtonModule, CommonModule, JslibModule, LinkModule, TableModule],
|
||||
})
|
||||
export class DeviceManagementTableComponent implements OnChanges {
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
||||
// eslint-disable-next-line @angular-eslint/prefer-signals
|
||||
@Input() devices: DeviceDisplayData[] = [];
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
||||
// eslint-disable-next-line @angular-eslint/prefer-output-emitter-ref
|
||||
@Output() onAuthRequestAnswered = new EventEmitter<DevicePendingAuthRequest>();
|
||||
|
||||
protected tableDataSource = new TableDataSource<DeviceDisplayData>();
|
||||
|
||||
@@ -50,6 +50,8 @@ export interface DeviceDisplayData {
|
||||
* - Medium to Large screens = `bit-table` view
|
||||
* - Small screens = `bit-item-group` view
|
||||
*/
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
|
||||
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection
|
||||
@Component({
|
||||
standalone: true,
|
||||
selector: "auth-device-management",
|
||||
|
||||
@@ -20,6 +20,8 @@ import {
|
||||
} from "@bitwarden/components";
|
||||
import { I18nPipe } from "@bitwarden/ui-common";
|
||||
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
|
||||
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection
|
||||
@Component({
|
||||
selector: "environment-selector",
|
||||
templateUrl: "environment-selector.component.html",
|
||||
|
||||
@@ -13,6 +13,8 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service"
|
||||
|
||||
import { activeAuthGuard } from "./active-auth.guard";
|
||||
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
|
||||
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection
|
||||
@Component({ template: "", standalone: false })
|
||||
class EmptyComponent {}
|
||||
|
||||
|
||||
@@ -33,6 +33,8 @@ export interface LoginApprovalDialogParams {
|
||||
notificationId: string;
|
||||
}
|
||||
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
|
||||
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection
|
||||
@Component({
|
||||
templateUrl: "login-approval-dialog.component.html",
|
||||
imports: [AsyncActionsModule, ButtonModule, CommonModule, DialogModule, JslibModule],
|
||||
|
||||
@@ -31,6 +31,8 @@ import {
|
||||
import { KeyService } from "@bitwarden/key-management";
|
||||
|
||||
export type State = "assert" | "assertFailed";
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
|
||||
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection
|
||||
@Component({
|
||||
selector: "app-login-via-webauthn",
|
||||
templateUrl: "login-via-webauthn.component.html",
|
||||
|
||||
@@ -39,12 +39,16 @@ import { ChangePasswordService } from "./change-password.service.abstraction";
|
||||
* and by design to maintain a strong security posture as some flows could have the user
|
||||
* end up at a change password without having one before.
|
||||
*/
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
|
||||
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection
|
||||
@Component({
|
||||
selector: "auth-change-password",
|
||||
templateUrl: "change-password.component.html",
|
||||
imports: [InputPasswordComponent, I18nPipe, CalloutComponent, CommonModule],
|
||||
})
|
||||
export class ChangePasswordComponent implements OnInit {
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
||||
// eslint-disable-next-line @angular-eslint/prefer-signals
|
||||
@Input() inputPasswordFlow: InputPasswordFlow = InputPasswordFlow.ChangePassword;
|
||||
|
||||
activeAccount: Account | null = null;
|
||||
|
||||
@@ -45,6 +45,8 @@ import {
|
||||
SetInitialPasswordUserType,
|
||||
} from "./set-initial-password.service.abstraction";
|
||||
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
|
||||
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection
|
||||
@Component({
|
||||
standalone: true,
|
||||
templateUrl: "set-initial-password.component.html",
|
||||
|
||||
@@ -869,6 +869,7 @@ const safeProviders: SafeProvider[] = [
|
||||
AuthServiceAbstraction,
|
||||
StateProvider,
|
||||
SecurityStateService,
|
||||
KdfConfigService,
|
||||
],
|
||||
}),
|
||||
safeProvider({
|
||||
|
||||
5
libs/assets/src/svg/svgs/auto-confirmation.ts
Normal file
5
libs/assets/src/svg/svgs/auto-confirmation.ts
Normal file
File diff suppressed because one or more lines are too long
@@ -1,6 +1,7 @@
|
||||
export * from "./account-warning.icon";
|
||||
export * from "./active-send.icon";
|
||||
export { default as AdminConsoleLogo } from "./admin-console";
|
||||
export * from "./auto-confirmation";
|
||||
export * from "./background-left-illustration";
|
||||
export * from "./background-right-illustration";
|
||||
export * from "./bitwarden-icon";
|
||||
|
||||
@@ -9,6 +9,8 @@ export type FingerprintDialogData = {
|
||||
fingerprint: string[];
|
||||
};
|
||||
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
|
||||
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection
|
||||
@Component({
|
||||
templateUrl: "fingerprint-dialog.component.html",
|
||||
imports: [JslibModule, ButtonModule, DialogModule],
|
||||
|
||||
@@ -99,6 +99,8 @@ interface InputPasswordForm {
|
||||
rotateUserKey?: FormControl<boolean>;
|
||||
}
|
||||
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
|
||||
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection
|
||||
@Component({
|
||||
selector: "auth-input-password",
|
||||
templateUrl: "./input-password.component.html",
|
||||
@@ -118,24 +120,48 @@ interface InputPasswordForm {
|
||||
],
|
||||
})
|
||||
export class InputPasswordComponent implements OnInit {
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
||||
// eslint-disable-next-line @angular-eslint/prefer-signals
|
||||
@ViewChild(PasswordStrengthV2Component) passwordStrengthComponent:
|
||||
| PasswordStrengthV2Component
|
||||
| undefined = undefined;
|
||||
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
||||
// eslint-disable-next-line @angular-eslint/prefer-output-emitter-ref
|
||||
@Output() onPasswordFormSubmit = new EventEmitter<PasswordInputResult>();
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
||||
// eslint-disable-next-line @angular-eslint/prefer-output-emitter-ref
|
||||
@Output() onSecondaryButtonClick = new EventEmitter<void>();
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
||||
// eslint-disable-next-line @angular-eslint/prefer-output-emitter-ref
|
||||
@Output() isSubmitting = new EventEmitter<boolean>();
|
||||
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
||||
// eslint-disable-next-line @angular-eslint/prefer-signals
|
||||
@Input({ required: true }) flow!: InputPasswordFlow;
|
||||
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
||||
// eslint-disable-next-line @angular-eslint/prefer-signals
|
||||
@Input({ transform: (val: string) => val?.trim().toLowerCase() }) email?: string;
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
||||
// eslint-disable-next-line @angular-eslint/prefer-signals
|
||||
@Input() userId?: UserId;
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
||||
// eslint-disable-next-line @angular-eslint/prefer-signals
|
||||
@Input() loading = false;
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
||||
// eslint-disable-next-line @angular-eslint/prefer-signals
|
||||
@Input() masterPasswordPolicyOptions?: MasterPasswordPolicyOptions;
|
||||
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
||||
// eslint-disable-next-line @angular-eslint/prefer-signals
|
||||
@Input() inlineButtons = false;
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
||||
// eslint-disable-next-line @angular-eslint/prefer-signals
|
||||
@Input() primaryButtonText?: Translation;
|
||||
protected primaryButtonTextStr: string = "";
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
||||
// eslint-disable-next-line @angular-eslint/prefer-signals
|
||||
@Input() secondaryButtonText?: Translation;
|
||||
protected secondaryButtonTextStr: string = "";
|
||||
|
||||
|
||||
@@ -50,6 +50,8 @@ enum State {
|
||||
ExistingUserUntrustedDevice,
|
||||
}
|
||||
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
|
||||
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection
|
||||
@Component({
|
||||
templateUrl: "./login-decryption-options.component.html",
|
||||
imports: [
|
||||
|
||||
@@ -56,6 +56,8 @@ const matchOptions: IsActiveMatchOptions = {
|
||||
matrixParams: "ignored",
|
||||
};
|
||||
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
|
||||
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection
|
||||
@Component({
|
||||
templateUrl: "./login-via-auth-request.component.html",
|
||||
imports: [ButtonModule, CommonModule, JslibModule, LinkModule, RouterModule],
|
||||
|
||||
@@ -8,6 +8,8 @@ import { DefaultServerSettingsService } from "@bitwarden/common/platform/service
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { LinkModule } from "@bitwarden/components";
|
||||
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
|
||||
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection
|
||||
@Component({
|
||||
imports: [CommonModule, JslibModule, LinkModule, RouterModule],
|
||||
template: `
|
||||
|
||||
@@ -67,6 +67,8 @@ export enum LoginUiState {
|
||||
MASTER_PASSWORD_ENTRY = "MasterPasswordEntry",
|
||||
}
|
||||
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
|
||||
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection
|
||||
@Component({
|
||||
templateUrl: "./login.component.html",
|
||||
imports: [
|
||||
@@ -83,6 +85,8 @@ export enum LoginUiState {
|
||||
],
|
||||
})
|
||||
export class LoginComponent implements OnInit, OnDestroy {
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
||||
// eslint-disable-next-line @angular-eslint/prefer-signals
|
||||
@ViewChild("masterPasswordInputRef") masterPasswordInputRef: ElementRef | undefined;
|
||||
|
||||
private destroy$ = new Subject<void>();
|
||||
|
||||
@@ -30,6 +30,8 @@ import { NewDeviceVerificationComponentService } from "./new-device-verification
|
||||
/**
|
||||
* Component for verifying a new device via a one-time password (OTP).
|
||||
*/
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
|
||||
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection
|
||||
@Component({
|
||||
selector: "app-new-device-verification",
|
||||
templateUrl: "./new-device-verification.component.html",
|
||||
|
||||
@@ -10,13 +10,19 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { CalloutModule } from "@bitwarden/components";
|
||||
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
|
||||
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection
|
||||
@Component({
|
||||
selector: "auth-password-callout",
|
||||
templateUrl: "password-callout.component.html",
|
||||
imports: [CommonModule, JslibModule, CalloutModule],
|
||||
})
|
||||
export class PasswordCalloutComponent {
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
||||
// eslint-disable-next-line @angular-eslint/prefer-signals
|
||||
@Input() message = "masterPasswordPolicyInEffect";
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
||||
// eslint-disable-next-line @angular-eslint/prefer-signals
|
||||
@Input() policy: MasterPasswordPolicyOptions;
|
||||
|
||||
constructor(private i18nService: I18nService) {}
|
||||
|
||||
@@ -22,6 +22,8 @@ import {
|
||||
ToastService,
|
||||
} from "@bitwarden/components";
|
||||
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
|
||||
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection
|
||||
@Component({
|
||||
templateUrl: "./password-hint.component.html",
|
||||
imports: [
|
||||
|
||||
@@ -25,12 +25,16 @@ import { SelfHostedEnvConfigDialogComponent } from "../../self-hosted-env-config
|
||||
* Component for selecting the environment to register with in the email verification registration flow.
|
||||
* Outputs the selected region to the parent component so it can respond as necessary.
|
||||
*/
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
|
||||
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection
|
||||
@Component({
|
||||
selector: "auth-registration-env-selector",
|
||||
templateUrl: "registration-env-selector.component.html",
|
||||
imports: [CommonModule, JslibModule, ReactiveFormsModule, FormFieldModule, SelectModule],
|
||||
})
|
||||
export class RegistrationEnvSelectorComponent implements OnInit, OnDestroy {
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
||||
// eslint-disable-next-line @angular-eslint/prefer-output-emitter-ref
|
||||
@Output() selectedRegionChange = new EventEmitter<RegionConfig | Region.SelfHosted | null>();
|
||||
|
||||
ServerEnvironmentType = Region;
|
||||
|
||||
@@ -31,6 +31,8 @@ import { PasswordInputResult } from "../../input-password/password-input-result"
|
||||
|
||||
import { RegistrationFinishService } from "./registration-finish.service";
|
||||
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
|
||||
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection
|
||||
@Component({
|
||||
selector: "auth-registration-finish",
|
||||
templateUrl: "./registration-finish.component.html",
|
||||
|
||||
@@ -19,6 +19,8 @@ export interface RegistrationLinkExpiredComponentData {
|
||||
loginRoute: string;
|
||||
}
|
||||
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
|
||||
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection
|
||||
@Component({
|
||||
selector: "auth-registration-link-expired",
|
||||
templateUrl: "./registration-link-expired.component.html",
|
||||
|
||||
@@ -18,6 +18,8 @@ export interface RegistrationStartSecondaryComponentData {
|
||||
loginRoute: string;
|
||||
}
|
||||
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
|
||||
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection
|
||||
@Component({
|
||||
selector: "auth-registration-start-secondary",
|
||||
templateUrl: "./registration-start-secondary.component.html",
|
||||
|
||||
@@ -40,6 +40,8 @@ const DEFAULT_MARKETING_EMAILS_PREF_BY_REGION: Record<Region, boolean> = {
|
||||
[Region.SelfHosted]: false,
|
||||
};
|
||||
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
|
||||
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection
|
||||
@Component({
|
||||
selector: "auth-registration-start",
|
||||
templateUrl: "./registration-start.component.html",
|
||||
@@ -57,6 +59,8 @@ const DEFAULT_MARKETING_EMAILS_PREF_BY_REGION: Record<Region, boolean> = {
|
||||
],
|
||||
})
|
||||
export class RegistrationStartComponent implements OnInit, OnDestroy {
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
||||
// eslint-disable-next-line @angular-eslint/prefer-output-emitter-ref
|
||||
@Output() registrationStartStateChange = new EventEmitter<RegistrationStartState>();
|
||||
|
||||
state: RegistrationStartState = RegistrationStartState.USER_DATA_ENTRY;
|
||||
|
||||
@@ -54,6 +54,8 @@ function selfHostedEnvSettingsFormValidator(): ValidatorFn {
|
||||
/**
|
||||
* Dialog for configuring self-hosted environment settings.
|
||||
*/
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
|
||||
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection
|
||||
@Component({
|
||||
selector: "self-hosted-env-config-dialog",
|
||||
templateUrl: "self-hosted-env-config-dialog.component.html",
|
||||
|
||||
@@ -62,6 +62,8 @@ interface QueryParams {
|
||||
/**
|
||||
* This component handles the SSO flow.
|
||||
*/
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
|
||||
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection
|
||||
@Component({
|
||||
templateUrl: "sso.component.html",
|
||||
imports: [
|
||||
|
||||
@@ -14,6 +14,8 @@ import {
|
||||
AsyncActionsModule,
|
||||
} from "@bitwarden/components";
|
||||
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
|
||||
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection
|
||||
@Component({
|
||||
selector: "app-two-factor-auth-authenticator",
|
||||
templateUrl: "two-factor-auth-authenticator.component.html",
|
||||
@@ -32,7 +34,11 @@ import {
|
||||
providers: [],
|
||||
})
|
||||
export class TwoFactorAuthAuthenticatorComponent {
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
||||
// eslint-disable-next-line @angular-eslint/prefer-signals
|
||||
@Input({ required: true }) tokenFormControl: FormControl | undefined = undefined;
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
||||
// eslint-disable-next-line @angular-eslint/prefer-output-emitter-ref
|
||||
@Output() tokenChange = new EventEmitter<{ token: string }>();
|
||||
|
||||
onTokenChange(event: Event) {
|
||||
|
||||
@@ -25,6 +25,8 @@ import {
|
||||
TwoFactorAuthDuoComponentService,
|
||||
} from "./two-factor-auth-duo-component.service";
|
||||
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
|
||||
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection
|
||||
@Component({
|
||||
selector: "app-two-factor-auth-duo",
|
||||
template: "",
|
||||
@@ -43,7 +45,11 @@ import {
|
||||
providers: [],
|
||||
})
|
||||
export class TwoFactorAuthDuoComponent implements OnInit {
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
||||
// eslint-disable-next-line @angular-eslint/prefer-output-emitter-ref
|
||||
@Output() tokenEmitter = new EventEmitter<string>();
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
||||
// eslint-disable-next-line @angular-eslint/prefer-signals
|
||||
@Input() providerData: any;
|
||||
|
||||
duoFramelessUrl: string | undefined = undefined;
|
||||
|
||||
@@ -36,7 +36,7 @@ export class TwoFactorAuthEmailComponentCacheService {
|
||||
/**
|
||||
* Signal for the cached email state.
|
||||
*/
|
||||
private emailCache: WritableSignal<TwoFactorAuthEmailComponentCache | null> =
|
||||
private readonly emailCache: WritableSignal<TwoFactorAuthEmailComponentCache | null> =
|
||||
this.viewCacheService.signal<TwoFactorAuthEmailComponentCache | null>({
|
||||
key: TWO_FACTOR_AUTH_EMAIL_COMPONENT_CACHE_KEY,
|
||||
initialValue: null,
|
||||
|
||||
@@ -26,6 +26,8 @@ import {
|
||||
|
||||
import { TwoFactorAuthEmailComponentCacheService } from "./two-factor-auth-email-component-cache.service";
|
||||
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
|
||||
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection
|
||||
@Component({
|
||||
selector: "app-two-factor-auth-email",
|
||||
templateUrl: "two-factor-auth-email.component.html",
|
||||
@@ -49,7 +51,11 @@ import { TwoFactorAuthEmailComponentCacheService } from "./two-factor-auth-email
|
||||
],
|
||||
})
|
||||
export class TwoFactorAuthEmailComponent implements OnInit {
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
||||
// eslint-disable-next-line @angular-eslint/prefer-signals
|
||||
@Input({ required: true }) tokenFormControl: FormControl | undefined = undefined;
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
||||
// eslint-disable-next-line @angular-eslint/prefer-output-emitter-ref
|
||||
@Output() tokenChange = new EventEmitter<{ token: string }>();
|
||||
|
||||
twoFactorEmail: string | undefined = undefined;
|
||||
|
||||
@@ -32,6 +32,8 @@ export interface WebAuthnResult {
|
||||
remember?: boolean;
|
||||
}
|
||||
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
|
||||
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection
|
||||
@Component({
|
||||
selector: "app-two-factor-auth-webauthn",
|
||||
templateUrl: "two-factor-auth-webauthn.component.html",
|
||||
@@ -50,7 +52,11 @@ export interface WebAuthnResult {
|
||||
providers: [],
|
||||
})
|
||||
export class TwoFactorAuthWebAuthnComponent implements OnInit, OnDestroy {
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
||||
// eslint-disable-next-line @angular-eslint/prefer-output-emitter-ref
|
||||
@Output() webAuthnResultEmitter = new EventEmitter<WebAuthnResult>();
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
||||
// eslint-disable-next-line @angular-eslint/prefer-output-emitter-ref
|
||||
@Output() webAuthnInNewTabEmitter = new EventEmitter<boolean>();
|
||||
|
||||
webAuthnReady = false;
|
||||
|
||||
@@ -14,6 +14,8 @@ import {
|
||||
AsyncActionsModule,
|
||||
} from "@bitwarden/components";
|
||||
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
|
||||
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection
|
||||
@Component({
|
||||
selector: "app-two-factor-auth-yubikey",
|
||||
templateUrl: "two-factor-auth-yubikey.component.html",
|
||||
@@ -32,5 +34,7 @@ import {
|
||||
providers: [],
|
||||
})
|
||||
export class TwoFactorAuthYubikeyComponent {
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
||||
// eslint-disable-next-line @angular-eslint/prefer-signals
|
||||
@Input({ required: true }) tokenFormControl: FormControl | undefined = undefined;
|
||||
}
|
||||
|
||||
@@ -42,7 +42,7 @@ export class TwoFactorAuthComponentCacheService {
|
||||
/**
|
||||
* Signal for the cached TwoFactorAuthData.
|
||||
*/
|
||||
private twoFactorAuthComponentCache: WritableSignal<TwoFactorAuthComponentCache | null> =
|
||||
private readonly twoFactorAuthComponentCache: WritableSignal<TwoFactorAuthComponentCache | null> =
|
||||
this.viewCacheService.signal<TwoFactorAuthComponentCache | null>({
|
||||
key: TWO_FACTOR_AUTH_COMPONENT_CACHE_KEY,
|
||||
initialValue: null,
|
||||
|
||||
@@ -46,6 +46,8 @@ import { TwoFactorAuthComponentCacheService } from "./two-factor-auth-component-
|
||||
import { TwoFactorAuthComponentService } from "./two-factor-auth-component.service";
|
||||
import { TwoFactorAuthComponent } from "./two-factor-auth.component";
|
||||
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
|
||||
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection
|
||||
@Component({ standalone: false })
|
||||
class TestTwoFactorComponent extends TwoFactorAuthComponent {}
|
||||
|
||||
|
||||
@@ -75,6 +75,8 @@ import {
|
||||
TwoFactorOptionsDialogResult,
|
||||
} from "./two-factor-options.component";
|
||||
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
|
||||
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection
|
||||
@Component({
|
||||
selector: "app-two-factor-auth",
|
||||
templateUrl: "two-factor-auth.component.html",
|
||||
@@ -99,6 +101,8 @@ import {
|
||||
],
|
||||
})
|
||||
export class TwoFactorAuthComponent implements OnInit, OnDestroy {
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
||||
// eslint-disable-next-line @angular-eslint/prefer-signals
|
||||
@ViewChild("continueButton", { read: ElementRef, static: false }) continueButton:
|
||||
| ElementRef
|
||||
| undefined = undefined;
|
||||
@@ -114,6 +118,8 @@ export class TwoFactorAuthComponent implements OnInit, OnDestroy {
|
||||
twoFactorProviders: Map<TwoFactorProviderType, { [key: string]: string }> | null = null;
|
||||
selectedProviderData: { [key: string]: string } | undefined;
|
||||
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
||||
// eslint-disable-next-line @angular-eslint/prefer-signals
|
||||
@ViewChild("duoComponent") duoComponent!: TwoFactorAuthDuoComponent;
|
||||
|
||||
form = this.formBuilder.group({
|
||||
|
||||
@@ -11,6 +11,8 @@ import { LoginStrategyServiceAbstraction } from "../../common";
|
||||
|
||||
import { TwoFactorAuthGuard } from "./two-factor-auth.guard";
|
||||
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
|
||||
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection
|
||||
@Component({ template: "", standalone: true })
|
||||
export class EmptyComponent {}
|
||||
|
||||
|
||||
@@ -30,6 +30,8 @@ export type TwoFactorOptionsDialogResult = {
|
||||
type: TwoFactorProviderType;
|
||||
};
|
||||
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
|
||||
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection
|
||||
@Component({
|
||||
selector: "app-two-factor-options",
|
||||
templateUrl: "two-factor-options.component.html",
|
||||
|
||||
@@ -30,6 +30,8 @@ import {
|
||||
} from "./user-verification-dialog.types";
|
||||
import { UserVerificationFormInputComponent } from "./user-verification-form-input.component";
|
||||
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
|
||||
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection
|
||||
@Component({
|
||||
templateUrl: "user-verification-dialog.component.html",
|
||||
imports: [
|
||||
|
||||
@@ -40,6 +40,8 @@ import { ActiveClientVerificationOption } from "./active-client-verification-opt
|
||||
* This is exposed to the parent component via the ControlValueAccessor interface (e.g. bind it to a FormControl).
|
||||
* Use UserVerificationService to verify the user's input.
|
||||
*/
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
|
||||
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection
|
||||
@Component({
|
||||
selector: "app-user-verification-form-input",
|
||||
templateUrl: "user-verification-form-input.component.html",
|
||||
@@ -69,8 +71,12 @@ import { ActiveClientVerificationOption } from "./active-client-verification-opt
|
||||
],
|
||||
})
|
||||
export class UserVerificationFormInputComponent implements ControlValueAccessor, OnInit, OnDestroy {
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
||||
// eslint-disable-next-line @angular-eslint/prefer-signals
|
||||
@Input() verificationType: "server" | "client" = "server"; // server represents original behavior
|
||||
private _invalidSecret = false;
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
||||
// eslint-disable-next-line @angular-eslint/prefer-signals
|
||||
@Input()
|
||||
get invalidSecret() {
|
||||
return this._invalidSecret;
|
||||
@@ -88,11 +94,17 @@ export class UserVerificationFormInputComponent implements ControlValueAccessor,
|
||||
}
|
||||
this.secret.updateValueAndValidity({ emitEvent: false });
|
||||
}
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
||||
// eslint-disable-next-line @angular-eslint/prefer-output-emitter-ref
|
||||
@Output() invalidSecretChange = new EventEmitter<boolean>();
|
||||
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
||||
// eslint-disable-next-line @angular-eslint/prefer-output-emitter-ref
|
||||
@Output() activeClientVerificationOptionChange =
|
||||
new EventEmitter<ActiveClientVerificationOption>();
|
||||
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
||||
// eslint-disable-next-line @angular-eslint/prefer-output-emitter-ref
|
||||
@Output() biometricsVerificationResultChange = new EventEmitter<boolean>();
|
||||
|
||||
readonly Icons = { UserVerificationBiometricsIcon };
|
||||
|
||||
@@ -44,6 +44,8 @@ type VaultTimeoutForm = FormGroup<{
|
||||
|
||||
type VaultTimeoutFormValue = VaultTimeoutForm["value"];
|
||||
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
|
||||
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection
|
||||
@Component({
|
||||
selector: "auth-vault-timeout-input",
|
||||
templateUrl: "vault-timeout-input.component.html",
|
||||
@@ -110,6 +112,8 @@ export class VaultTimeoutInputComponent
|
||||
}),
|
||||
});
|
||||
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
||||
// eslint-disable-next-line @angular-eslint/prefer-signals
|
||||
@Input() vaultTimeoutOptions: VaultTimeoutOption[];
|
||||
|
||||
vaultTimeoutPolicy: Policy;
|
||||
|
||||
@@ -16,7 +16,7 @@ const LOGIN_VIA_AUTH_CACHE_KEY = "login-via-auth-request-form-cache";
|
||||
export class LoginViaAuthRequestCacheService {
|
||||
private viewCacheService: ViewCacheService = inject(ViewCacheService);
|
||||
|
||||
private defaultLoginViaAuthRequestCache: WritableSignal<LoginViaAuthRequestView | null> =
|
||||
private readonly defaultLoginViaAuthRequestCache: WritableSignal<LoginViaAuthRequestView | null> =
|
||||
this.viewCacheService.signal<LoginViaAuthRequestView | null>({
|
||||
key: LOGIN_VIA_AUTH_CACHE_KEY,
|
||||
initialValue: null,
|
||||
|
||||
@@ -19,4 +19,5 @@ export enum PolicyType {
|
||||
RestrictedItemTypes = 15, // Restricts item types that can be created within an organization
|
||||
UriMatchDefaults = 16, // Sets the default URI matching strategy for all users within an organization
|
||||
AutotypeDefaultSetting = 17, // Sets the default autotype setting for desktop app
|
||||
AutoConfirm = 18, // Enables the auto confirmation feature for admins to enable in their client
|
||||
}
|
||||
|
||||
@@ -30,6 +30,7 @@ describe("ORGANIZATIONS state", () => {
|
||||
useSecretsManager: false,
|
||||
usePasswordManager: false,
|
||||
useActivateAutofillPolicy: false,
|
||||
useAutomaticUserConfirmation: false,
|
||||
selfHost: false,
|
||||
usersGetPremium: false,
|
||||
seats: 0,
|
||||
|
||||
@@ -30,6 +30,7 @@ export class OrganizationData {
|
||||
useSecretsManager: boolean;
|
||||
usePasswordManager: boolean;
|
||||
useActivateAutofillPolicy: boolean;
|
||||
useAutomaticUserConfirmation: boolean;
|
||||
selfHost: boolean;
|
||||
usersGetPremium: boolean;
|
||||
seats: number;
|
||||
@@ -99,6 +100,7 @@ export class OrganizationData {
|
||||
this.useSecretsManager = response.useSecretsManager;
|
||||
this.usePasswordManager = response.usePasswordManager;
|
||||
this.useActivateAutofillPolicy = response.useActivateAutofillPolicy;
|
||||
this.useAutomaticUserConfirmation = response.useAutomaticUserConfirmation;
|
||||
this.selfHost = response.selfHost;
|
||||
this.usersGetPremium = response.usersGetPremium;
|
||||
this.seats = response.seats;
|
||||
|
||||
@@ -38,6 +38,7 @@ export class Organization {
|
||||
useSecretsManager: boolean;
|
||||
usePasswordManager: boolean;
|
||||
useActivateAutofillPolicy: boolean;
|
||||
useAutomaticUserConfirmation: boolean;
|
||||
selfHost: boolean;
|
||||
usersGetPremium: boolean;
|
||||
seats: number;
|
||||
@@ -124,6 +125,7 @@ export class Organization {
|
||||
this.useSecretsManager = obj.useSecretsManager;
|
||||
this.usePasswordManager = obj.usePasswordManager;
|
||||
this.useActivateAutofillPolicy = obj.useActivateAutofillPolicy;
|
||||
this.useAutomaticUserConfirmation = obj.useAutomaticUserConfirmation;
|
||||
this.selfHost = obj.selfHost;
|
||||
this.usersGetPremium = obj.usersGetPremium;
|
||||
this.seats = obj.seats;
|
||||
|
||||
@@ -23,6 +23,7 @@ export class ProfileOrganizationResponse extends BaseResponse {
|
||||
useSecretsManager: boolean;
|
||||
usePasswordManager: boolean;
|
||||
useActivateAutofillPolicy: boolean;
|
||||
useAutomaticUserConfirmation: boolean;
|
||||
selfHost: boolean;
|
||||
usersGetPremium: boolean;
|
||||
seats: number;
|
||||
@@ -82,6 +83,7 @@ export class ProfileOrganizationResponse extends BaseResponse {
|
||||
this.useSecretsManager = this.getResponseProperty("UseSecretsManager");
|
||||
this.usePasswordManager = this.getResponseProperty("UsePasswordManager");
|
||||
this.useActivateAutofillPolicy = this.getResponseProperty("UseActivateAutofillPolicy");
|
||||
this.useAutomaticUserConfirmation = this.getResponseProperty("UseAutomaticUserConfirmation");
|
||||
this.selfHost = this.getResponseProperty("SelfHost");
|
||||
this.usersGetPremium = this.getResponseProperty("UsersGetPremium");
|
||||
this.seats = this.getResponseProperty("Seats");
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
import { AUTO_CONFIRM, UserKeyDefinition } from "../../../platform/state";
|
||||
|
||||
export class AutoConfirmState {
|
||||
enabled: boolean;
|
||||
showSetupDialog: boolean;
|
||||
showBrowserNotification: boolean | undefined;
|
||||
|
||||
constructor() {
|
||||
this.enabled = false;
|
||||
this.showSetupDialog = true;
|
||||
}
|
||||
}
|
||||
|
||||
export const AUTO_CONFIRM_STATE = UserKeyDefinition.record<AutoConfirmState>(
|
||||
AUTO_CONFIRM,
|
||||
"autoConfirm",
|
||||
{
|
||||
deserializer: (autoConfirmState) => autoConfirmState,
|
||||
clearOn: ["logout"],
|
||||
},
|
||||
);
|
||||
@@ -1,3 +1,5 @@
|
||||
import { PremiumPlanResponse } from "@bitwarden/common/billing/models/response/premium-plan.response";
|
||||
|
||||
import { OrganizationCreateRequest } from "../../admin-console/models/request/organization-create.request";
|
||||
import { SubscriptionCancellationRequest } from "../../billing/models/request/subscription-cancellation.request";
|
||||
import { OrganizationBillingMetadataResponse } from "../../billing/models/response/organization-billing-metadata.response";
|
||||
@@ -25,6 +27,8 @@ export abstract class BillingApiServiceAbstraction {
|
||||
|
||||
abstract getPlans(): Promise<ListResponse<PlanResponse>>;
|
||||
|
||||
abstract getPremiumPlan(): Promise<PremiumPlanResponse>;
|
||||
|
||||
abstract getProviderClientInvoiceReport(providerId: string, invoiceId: string): Promise<string>;
|
||||
|
||||
abstract getProviderInvoices(providerId: string): Promise<InvoicesResponse>;
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
import { BaseResponse } from "@bitwarden/common/models/response/base.response";
|
||||
|
||||
export class PremiumPlanResponse extends BaseResponse {
|
||||
seat: {
|
||||
stripePriceId: string;
|
||||
price: number;
|
||||
};
|
||||
storage: {
|
||||
stripePriceId: string;
|
||||
price: number;
|
||||
};
|
||||
|
||||
constructor(response: any) {
|
||||
super(response);
|
||||
|
||||
const seat = this.getResponseProperty("Seat");
|
||||
if (!seat || typeof seat !== "object") {
|
||||
throw new Error("PremiumPlanResponse: Missing or invalid 'Seat' property");
|
||||
}
|
||||
this.seat = new PurchasableResponse(seat);
|
||||
|
||||
const storage = this.getResponseProperty("Storage");
|
||||
if (!storage || typeof storage !== "object") {
|
||||
throw new Error("PremiumPlanResponse: Missing or invalid 'Storage' property");
|
||||
}
|
||||
this.storage = new PurchasableResponse(storage);
|
||||
}
|
||||
}
|
||||
|
||||
class PurchasableResponse extends BaseResponse {
|
||||
stripePriceId: string;
|
||||
price: number;
|
||||
|
||||
constructor(response: any) {
|
||||
super(response);
|
||||
|
||||
this.stripePriceId = this.getResponseProperty("StripePriceId");
|
||||
if (!this.stripePriceId || typeof this.stripePriceId !== "string") {
|
||||
throw new Error("PurchasableResponse: Missing or invalid 'StripePriceId' property");
|
||||
}
|
||||
|
||||
this.price = this.getResponseProperty("Price");
|
||||
if (typeof this.price !== "number" || isNaN(this.price)) {
|
||||
throw new Error("PurchasableResponse: Missing or invalid 'Price' property");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
|
||||
import { PremiumPlanResponse } from "@bitwarden/common/billing/models/response/premium-plan.response";
|
||||
|
||||
import { ApiService } from "../../abstractions/api.service";
|
||||
import { OrganizationCreateRequest } from "../../admin-console/models/request/organization-create.request";
|
||||
import { ListResponse } from "../../models/response/list.response";
|
||||
@@ -61,10 +63,15 @@ export class BillingApiService implements BillingApiServiceAbstraction {
|
||||
}
|
||||
|
||||
async getPlans(): Promise<ListResponse<PlanResponse>> {
|
||||
const r = await this.apiService.send("GET", "/plans", null, false, true);
|
||||
const r = await this.apiService.send("GET", "/plans", null, true, true);
|
||||
return new ListResponse(r, PlanResponse);
|
||||
}
|
||||
|
||||
async getPremiumPlan(): Promise<PremiumPlanResponse> {
|
||||
const response = await this.apiService.send("GET", "/plans/premium", null, true, true);
|
||||
return new PremiumPlanResponse(response);
|
||||
}
|
||||
|
||||
async getProviderClientInvoiceReport(providerId: string, invoiceId: string): Promise<string> {
|
||||
const response = await this.apiService.send(
|
||||
"GET",
|
||||
|
||||
@@ -12,6 +12,7 @@ import { ServerConfig } from "../platform/abstractions/config/server-config";
|
||||
export enum FeatureFlag {
|
||||
/* Admin Console Team */
|
||||
CreateDefaultLocation = "pm-19467-create-default-location",
|
||||
AutoConfirm = "pm-19934-auto-confirm-organization-users",
|
||||
|
||||
/* Auth */
|
||||
PM22110_DisableAlternateLoginMethods = "pm-22110-disable-alternate-login-methods",
|
||||
@@ -29,6 +30,7 @@ export enum FeatureFlag {
|
||||
PM25379_UseNewOrganizationMetadataStructure = "pm-25379-use-new-organization-metadata-structure",
|
||||
PM24996_ImplementUpgradeFromFreeDialog = "pm-24996-implement-upgrade-from-free-dialog",
|
||||
PM24033PremiumUpgradeNewDesign = "pm-24033-updat-premium-subscription-page",
|
||||
PM26793_FetchPremiumPriceFromPricingService = "pm-26793-fetch-premium-price-from-pricing-service",
|
||||
|
||||
/* Key Management */
|
||||
PrivateKeyRegeneration = "pm-12241-private-key-regeneration",
|
||||
@@ -45,7 +47,7 @@ export enum FeatureFlag {
|
||||
ChromiumImporterWithABE = "pm-25855-chromium-importer-abe",
|
||||
|
||||
/* DIRT */
|
||||
EventBasedOrganizationIntegrations = "event-based-organization-integrations",
|
||||
EventManagementForDataDogAndCrowdStrike = "event-management-for-datadog-and-crowdstrike",
|
||||
PhishingDetection = "phishing-detection",
|
||||
PM22887_RiskInsightsActivityTab = "pm-22887-risk-insights-activity-tab",
|
||||
|
||||
@@ -80,6 +82,7 @@ const FALSE = false as boolean;
|
||||
export const DefaultFeatureFlagValue = {
|
||||
/* Admin Console Team */
|
||||
[FeatureFlag.CreateDefaultLocation]: FALSE,
|
||||
[FeatureFlag.AutoConfirm]: FALSE,
|
||||
|
||||
/* Autofill */
|
||||
[FeatureFlag.MacOsNativeCredentialSync]: FALSE,
|
||||
@@ -91,7 +94,7 @@ export const DefaultFeatureFlagValue = {
|
||||
[FeatureFlag.ChromiumImporterWithABE]: FALSE,
|
||||
|
||||
/* DIRT */
|
||||
[FeatureFlag.EventBasedOrganizationIntegrations]: FALSE,
|
||||
[FeatureFlag.EventManagementForDataDogAndCrowdStrike]: FALSE,
|
||||
[FeatureFlag.PhishingDetection]: FALSE,
|
||||
[FeatureFlag.PM22887_RiskInsightsActivityTab]: FALSE,
|
||||
|
||||
@@ -113,6 +116,7 @@ export const DefaultFeatureFlagValue = {
|
||||
[FeatureFlag.PM25379_UseNewOrganizationMetadataStructure]: FALSE,
|
||||
[FeatureFlag.PM24996_ImplementUpgradeFromFreeDialog]: FALSE,
|
||||
[FeatureFlag.PM24033PremiumUpgradeNewDesign]: FALSE,
|
||||
[FeatureFlag.PM26793_FetchPremiumPriceFromPricingService]: FALSE,
|
||||
|
||||
/* Key Management */
|
||||
[FeatureFlag.PrivateKeyRegeneration]: FALSE,
|
||||
|
||||
@@ -15,7 +15,7 @@ import { EncString } from "@bitwarden/common/key-management/crypto/models/enc-st
|
||||
import { SecurityStateService } from "@bitwarden/common/key-management/security-state/abstractions/security-state.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 { KeyService, PBKDF2KdfConfig } from "@bitwarden/key-management";
|
||||
import { KdfConfigService, KeyService, PBKDF2KdfConfig } from "@bitwarden/key-management";
|
||||
|
||||
import { Matrix } from "../../../spec/matrix";
|
||||
import { ApiService } from "../../abstractions/api.service";
|
||||
@@ -75,6 +75,7 @@ describe("DefaultSyncService", () => {
|
||||
let authService: MockProxy<AuthService>;
|
||||
let stateProvider: MockProxy<StateProvider>;
|
||||
let securityStateService: MockProxy<SecurityStateService>;
|
||||
let kdfConfigService: MockProxy<KdfConfigService>;
|
||||
|
||||
let sut: DefaultSyncService;
|
||||
|
||||
@@ -105,6 +106,7 @@ describe("DefaultSyncService", () => {
|
||||
authService = mock();
|
||||
stateProvider = mock();
|
||||
securityStateService = mock();
|
||||
kdfConfigService = mock();
|
||||
|
||||
sut = new DefaultSyncService(
|
||||
masterPasswordAbstraction,
|
||||
@@ -132,6 +134,7 @@ describe("DefaultSyncService", () => {
|
||||
authService,
|
||||
stateProvider,
|
||||
securityStateService,
|
||||
kdfConfigService,
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop.
|
||||
import { SecurityStateService } from "@bitwarden/common/key-management/security-state/abstractions/security-state.service";
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { KeyService } from "@bitwarden/key-management";
|
||||
import { KdfConfigService, KeyService } from "@bitwarden/key-management";
|
||||
|
||||
// FIXME: remove `src` and fix import
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
@@ -100,6 +100,7 @@ export class DefaultSyncService extends CoreSyncService {
|
||||
authService: AuthService,
|
||||
stateProvider: StateProvider,
|
||||
private securityStateService: SecurityStateService,
|
||||
private kdfConfigService: KdfConfigService,
|
||||
) {
|
||||
super(
|
||||
tokenService,
|
||||
@@ -434,6 +435,7 @@ export class DefaultSyncService extends CoreSyncService {
|
||||
masterPasswordUnlockData,
|
||||
userId,
|
||||
);
|
||||
await this.kdfConfigService.setKdfConfig(userId, masterPasswordUnlockData.kdf);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,4 +5,5 @@ export class AttachmentRequest {
|
||||
key: string;
|
||||
fileSize: number;
|
||||
adminRequest: boolean;
|
||||
lastKnownRevisionDate: Date;
|
||||
}
|
||||
|
||||
@@ -201,6 +201,7 @@ export class CipherRequest {
|
||||
this.attachments[attachment.id] = fileName;
|
||||
const attachmentRequest = new AttachmentRequest();
|
||||
attachmentRequest.fileName = fileName;
|
||||
attachmentRequest.lastKnownRevisionDate = cipher.revisionDate;
|
||||
if (attachment.key != null) {
|
||||
attachmentRequest.key = attachment.key.encryptedString;
|
||||
}
|
||||
|
||||
@@ -174,6 +174,37 @@ describe("Cipher Service", () => {
|
||||
|
||||
expect(spy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should include lastKnownRevisionDate in the upload request", async () => {
|
||||
const fileName = "filename";
|
||||
const fileData = new Uint8Array(10);
|
||||
const testCipher = new Cipher(cipherData);
|
||||
const expectedRevisionDate = "2022-01-31T12:00:00.000Z";
|
||||
|
||||
keyService.getOrgKey.mockReturnValue(
|
||||
Promise.resolve<any>(new SymmetricCryptoKey(new Uint8Array(32)) as OrgKey),
|
||||
);
|
||||
keyService.makeDataEncKey.mockReturnValue(
|
||||
Promise.resolve([
|
||||
new SymmetricCryptoKey(new Uint8Array(32)),
|
||||
new EncString("encrypted-key"),
|
||||
] as any),
|
||||
);
|
||||
|
||||
configService.checkServerMeetsVersionRequirement$.mockReturnValue(of(false));
|
||||
configService.getFeatureFlag
|
||||
.calledWith(FeatureFlag.CipherKeyEncryption)
|
||||
.mockResolvedValue(false);
|
||||
|
||||
const uploadSpy = jest.spyOn(cipherFileUploadService, "upload").mockResolvedValue({} as any);
|
||||
|
||||
await cipherService.saveAttachmentRawWithServer(testCipher, fileName, fileData, userId);
|
||||
|
||||
// Verify upload was called with cipher that has revisionDate
|
||||
expect(uploadSpy).toHaveBeenCalled();
|
||||
const cipherArg = uploadSpy.mock.calls[0][0];
|
||||
expect(cipherArg.revisionDate).toEqual(new Date(expectedRevisionDate));
|
||||
});
|
||||
});
|
||||
|
||||
describe("createWithServer()", () => {
|
||||
|
||||
@@ -937,7 +937,12 @@ export class CipherService implements CipherServiceAbstraction {
|
||||
cipher.attachments.forEach((attachment) => {
|
||||
if (attachment.key == null) {
|
||||
attachmentPromises.push(
|
||||
this.shareAttachmentWithServer(attachment, cipher.id, organizationId),
|
||||
this.shareAttachmentWithServer(
|
||||
attachment,
|
||||
cipher.id,
|
||||
organizationId,
|
||||
cipher.revisionDate,
|
||||
),
|
||||
);
|
||||
}
|
||||
});
|
||||
@@ -1722,7 +1727,10 @@ export class CipherService implements CipherServiceAbstraction {
|
||||
attachmentView: AttachmentView,
|
||||
cipherId: string,
|
||||
organizationId: string,
|
||||
lastKnownRevisionDate: Date,
|
||||
): Promise<any> {
|
||||
const activeUserId = await firstValueFrom(this.accountService.activeAccount$);
|
||||
|
||||
const attachmentResponse = await this.apiService.nativeFetch(
|
||||
new Request(attachmentView.url, { cache: "no-store" }),
|
||||
);
|
||||
@@ -1731,7 +1739,6 @@ export class CipherService implements CipherServiceAbstraction {
|
||||
}
|
||||
|
||||
const encBuf = await EncArrayBuffer.fromResponse(attachmentResponse);
|
||||
const activeUserId = await firstValueFrom(this.accountService.activeAccount$);
|
||||
const userKey = await this.keyService.getUserKey(activeUserId.id);
|
||||
const decBuf = await this.encryptService.decryptFileData(encBuf, userKey);
|
||||
|
||||
@@ -1752,9 +1759,11 @@ export class CipherService implements CipherServiceAbstraction {
|
||||
const blob = new Blob([encData.buffer], { type: "application/octet-stream" });
|
||||
fd.append("key", dataEncKey[1].encryptedString);
|
||||
fd.append("data", blob, encFileName.encryptedString);
|
||||
fd.append("lastKnownRevisionDate", lastKnownRevisionDate.toISOString());
|
||||
} catch (e) {
|
||||
if (Utils.isNode && !Utils.isBrowser) {
|
||||
fd.append("key", dataEncKey[1].encryptedString);
|
||||
fd.append("lastKnownRevisionDate", lastKnownRevisionDate.toISOString());
|
||||
fd.append(
|
||||
"data",
|
||||
Buffer.from(encData.buffer) as any,
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
import { mock } from "jest-mock-extended";
|
||||
|
||||
import { ApiService } from "../../../abstractions/api.service";
|
||||
import { EncString } from "../../../key-management/crypto/models/enc-string";
|
||||
import { FileUploadService } from "../../../platform/abstractions/file-upload/file-upload.service";
|
||||
import { Utils } from "../../../platform/misc/utils";
|
||||
import { EncArrayBuffer } from "../../../platform/models/domain/enc-array-buffer";
|
||||
import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key";
|
||||
import { CipherType } from "../../enums/cipher-type";
|
||||
import { Cipher } from "../../models/domain/cipher";
|
||||
import { AttachmentUploadDataResponse } from "../../models/response/attachment-upload-data.response";
|
||||
import { CipherResponse } from "../../models/response/cipher.response";
|
||||
|
||||
import { CipherFileUploadService } from "./cipher-file-upload.service";
|
||||
|
||||
describe("CipherFileUploadService", () => {
|
||||
const apiService = mock<ApiService>();
|
||||
const fileUploadService = mock<FileUploadService>();
|
||||
|
||||
let service: CipherFileUploadService;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
|
||||
service = new CipherFileUploadService(apiService, fileUploadService);
|
||||
});
|
||||
|
||||
describe("upload", () => {
|
||||
it("should include lastKnownRevisionDate in the attachment request", async () => {
|
||||
const cipherId = Utils.newGuid();
|
||||
const mockCipher = new Cipher({
|
||||
id: cipherId,
|
||||
type: CipherType.Login,
|
||||
name: "Test Cipher",
|
||||
revisionDate: "2024-01-15T10:30:00.000Z",
|
||||
} as any);
|
||||
|
||||
const mockEncFileName = new EncString("encrypted-filename");
|
||||
const mockEncData = {
|
||||
buffer: new ArrayBuffer(100),
|
||||
} as unknown as EncArrayBuffer;
|
||||
|
||||
const mockDataEncKey: [SymmetricCryptoKey, EncString] = [
|
||||
new SymmetricCryptoKey(new Uint8Array(32)),
|
||||
new EncString("encrypted-key"),
|
||||
];
|
||||
|
||||
const mockUploadDataResponse = {
|
||||
attachmentId: "attachment-id",
|
||||
url: "https://upload.example.com",
|
||||
fileUploadType: 0,
|
||||
cipherResponse: {
|
||||
id: cipherId,
|
||||
type: CipherType.Login,
|
||||
revisionDate: "2024-01-15T10:30:00.000Z",
|
||||
} as CipherResponse,
|
||||
cipherMiniResponse: null,
|
||||
} as AttachmentUploadDataResponse;
|
||||
|
||||
apiService.postCipherAttachment.mockResolvedValue(mockUploadDataResponse);
|
||||
fileUploadService.upload.mockResolvedValue(undefined);
|
||||
|
||||
await service.upload(mockCipher, mockEncFileName, mockEncData, false, mockDataEncKey);
|
||||
|
||||
const callArgs = apiService.postCipherAttachment.mock.calls[0][1];
|
||||
|
||||
expect(apiService.postCipherAttachment).toHaveBeenCalledWith(
|
||||
cipherId,
|
||||
expect.objectContaining({
|
||||
key: "encrypted-key",
|
||||
fileName: "encrypted-filename",
|
||||
fileSize: 100,
|
||||
adminRequest: false,
|
||||
}),
|
||||
);
|
||||
|
||||
// Verify lastKnownRevisionDate is set (it's converted to a Date object)
|
||||
expect(callArgs.lastKnownRevisionDate).toBeDefined();
|
||||
expect(callArgs.lastKnownRevisionDate).toEqual(new Date("2024-01-15T10:30:00.000Z"));
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -33,6 +33,7 @@ export class CipherFileUploadService implements CipherFileUploadServiceAbstracti
|
||||
fileName: encFileName.encryptedString,
|
||||
fileSize: encData.buffer.byteLength,
|
||||
adminRequest: admin,
|
||||
lastKnownRevisionDate: cipher.revisionDate,
|
||||
};
|
||||
|
||||
let response: CipherResponse;
|
||||
|
||||
@@ -25,6 +25,12 @@ export abstract class TaskService {
|
||||
*/
|
||||
abstract pendingTasks$(userId: UserId): Observable<SecurityTask[]>;
|
||||
|
||||
/**
|
||||
* Observable of completed tasks for a given user.
|
||||
* @param userId
|
||||
*/
|
||||
abstract completedTasks$(userId: UserId): Observable<SecurityTask[]>;
|
||||
|
||||
/**
|
||||
* Retrieves tasks from the API for a given user and updates the local state.
|
||||
* @param userId
|
||||
|
||||
@@ -80,6 +80,12 @@ export class DefaultTaskService implements TaskService {
|
||||
);
|
||||
});
|
||||
|
||||
completedTasks$ = perUserCache$((userId) => {
|
||||
return this.tasks$(userId).pipe(
|
||||
map((tasks) => tasks.filter((t) => t.status === SecurityTaskStatus.Completed)),
|
||||
);
|
||||
});
|
||||
|
||||
async refreshTasks(userId: UserId): Promise<void> {
|
||||
await this.fetchTasksFromApi(userId);
|
||||
}
|
||||
|
||||
@@ -38,7 +38,8 @@ const defaultIcon: Record<BannerType, string> = {
|
||||
export class BannerComponent implements OnInit {
|
||||
readonly bannerType = input<BannerType>("info");
|
||||
|
||||
readonly icon = model<string>();
|
||||
// passing `null` will remove the icon from element from the banner
|
||||
readonly icon = model<string | null>();
|
||||
readonly useAlertRole = input(true);
|
||||
readonly showClose = input(true);
|
||||
|
||||
@@ -47,7 +48,7 @@ export class BannerComponent implements OnInit {
|
||||
@Output() onClose = new EventEmitter<void>();
|
||||
|
||||
ngOnInit(): void {
|
||||
if (!this.icon()) {
|
||||
if (!this.icon() && this.icon() !== null) {
|
||||
this.icon.set(defaultIcon[this.bannerType()]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,6 +36,7 @@ export const DELETE_MANAGED_USER_WARNING = new StateDefinition(
|
||||
web: "disk-local",
|
||||
},
|
||||
);
|
||||
export const AUTO_CONFIRM = new StateDefinition("autoConfirm", "disk");
|
||||
|
||||
// Billing
|
||||
export const BILLING_DISK = new StateDefinition("billing", "disk");
|
||||
@@ -210,6 +211,7 @@ export const VAULT_BROWSER_INTRO_CAROUSEL = new StateDefinition(
|
||||
"vaultBrowserIntroCarousel",
|
||||
"disk",
|
||||
);
|
||||
export const VAULT_AT_RISK_PASSWORDS_MEMORY = new StateDefinition("vaultAtRiskPasswords", "memory");
|
||||
|
||||
// KM
|
||||
|
||||
|
||||
@@ -240,6 +240,49 @@ describe("CipherAttachmentsComponent", () => {
|
||||
message: "maxFileSize",
|
||||
});
|
||||
});
|
||||
|
||||
it("shows error toast with server message when saveAttachmentWithServer fails", async () => {
|
||||
const file = { size: 100 } as File;
|
||||
component.attachmentForm.controls.file.setValue(file);
|
||||
|
||||
const serverError = new Error("Cipher has been modified by another client");
|
||||
saveAttachmentWithServer.mockRejectedValue(serverError);
|
||||
|
||||
await component.submit();
|
||||
|
||||
expect(showToast).toHaveBeenCalledWith({
|
||||
variant: "error",
|
||||
message: "Cipher has been modified by another client",
|
||||
});
|
||||
});
|
||||
|
||||
it("shows error toast with fallback message when error has no message property", async () => {
|
||||
const file = { size: 100 } as File;
|
||||
component.attachmentForm.controls.file.setValue(file);
|
||||
|
||||
saveAttachmentWithServer.mockRejectedValue({ code: "UNKNOWN_ERROR" });
|
||||
|
||||
await component.submit();
|
||||
|
||||
expect(showToast).toHaveBeenCalledWith({
|
||||
variant: "error",
|
||||
message: "unexpectedError",
|
||||
});
|
||||
});
|
||||
|
||||
it("shows error toast with string error message", async () => {
|
||||
const file = { size: 100 } as File;
|
||||
component.attachmentForm.controls.file.setValue(file);
|
||||
|
||||
saveAttachmentWithServer.mockRejectedValue("Network connection failed");
|
||||
|
||||
await component.submit();
|
||||
|
||||
expect(showToast).toHaveBeenCalledWith({
|
||||
variant: "error",
|
||||
message: "Network connection failed",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("success", () => {
|
||||
|
||||
@@ -222,6 +222,19 @@ export class CipherAttachmentsComponent implements OnInit, AfterViewInit {
|
||||
this.onUploadSuccess.emit();
|
||||
} catch (e) {
|
||||
this.logService.error(e);
|
||||
|
||||
// Extract error message from server response, fallback to generic message
|
||||
let errorMessage = this.i18nService.t("unexpectedError");
|
||||
if (typeof e === "string") {
|
||||
errorMessage = e;
|
||||
} else if (e?.message) {
|
||||
errorMessage = e.message;
|
||||
}
|
||||
|
||||
this.toastService.showToast({
|
||||
variant: "error",
|
||||
message: errorMessage,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -640,6 +640,46 @@ describe("ItemDetailsSectionComponent", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("initFromExistingCipher", () => {
|
||||
it("should set organizationId to null when prefillCipher.organizationId is undefined", async () => {
|
||||
component.config.organizationDataOwnershipDisabled = true;
|
||||
component.config.organizations = [{ id: "org1" } as Organization];
|
||||
|
||||
const prefillCipher = {
|
||||
name: "Test Cipher",
|
||||
organizationId: undefined,
|
||||
folderId: null,
|
||||
collectionIds: [],
|
||||
favorite: false,
|
||||
} as unknown as CipherView;
|
||||
|
||||
getInitialCipherView.mockReturnValueOnce(prefillCipher);
|
||||
|
||||
await component.ngOnInit();
|
||||
|
||||
expect(component.itemDetailsForm.controls.organizationId.value).toBeNull();
|
||||
});
|
||||
|
||||
it("should preserve organizationId when prefillCipher.organizationId has a value", async () => {
|
||||
component.config.organizationDataOwnershipDisabled = true;
|
||||
component.config.organizations = [{ id: "org1", name: "Organization 1" } as Organization];
|
||||
|
||||
const prefillCipher = {
|
||||
name: "Test Cipher",
|
||||
organizationId: "org1",
|
||||
folderId: null,
|
||||
collectionIds: [],
|
||||
favorite: false,
|
||||
} as unknown as CipherView;
|
||||
|
||||
getInitialCipherView.mockReturnValueOnce(prefillCipher);
|
||||
|
||||
await component.ngOnInit();
|
||||
|
||||
expect(component.itemDetailsForm.controls.organizationId.value).toBe("org1");
|
||||
});
|
||||
});
|
||||
|
||||
describe("form status when editing a cipher", () => {
|
||||
beforeEach(() => {
|
||||
component.config.mode = "edit";
|
||||
|
||||
@@ -4,7 +4,7 @@ import { CommonModule } from "@angular/common";
|
||||
import { Component, DestroyRef, Input, OnInit } from "@angular/core";
|
||||
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
|
||||
import { FormBuilder, FormControl, ReactiveFormsModule, Validators } from "@angular/forms";
|
||||
import { concatMap, firstValueFrom, map } from "rxjs";
|
||||
import { concatMap, distinctUntilChanged, firstValueFrom, map } 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
|
||||
@@ -236,6 +236,7 @@ export class ItemDetailsSectionComponent implements OnInit {
|
||||
this.itemDetailsForm.controls.organizationId.valueChanges
|
||||
.pipe(
|
||||
takeUntilDestroyed(this.destroyRef),
|
||||
distinctUntilChanged(),
|
||||
concatMap(async () => {
|
||||
await this.updateCollectionOptions();
|
||||
this.setFormState();
|
||||
@@ -314,7 +315,10 @@ export class ItemDetailsSectionComponent implements OnInit {
|
||||
|
||||
this.itemDetailsForm.patchValue({
|
||||
name: name ? name : (this.initialValues?.name ?? ""),
|
||||
organizationId: prefillCipher.organizationId, // We do not allow changing ownership of an existing cipher.
|
||||
// We do not allow changing ownership of an existing cipher.
|
||||
// Angular forms do not support `undefined` as a value for a form control,
|
||||
// force `null` if `organizationId` is undefined.
|
||||
organizationId: prefillCipher.organizationId ?? null,
|
||||
folderId: folderId ? folderId : (this.initialValues?.folderId ?? null),
|
||||
collectionIds: [],
|
||||
favorite: prefillCipher.favorite,
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
export {
|
||||
AtRiskPasswordCalloutService,
|
||||
AtRiskPasswordCalloutData,
|
||||
} from "./services/at-risk-password-callout.service";
|
||||
export { PasswordRepromptService } from "./services/password-reprompt.service";
|
||||
export { CopyCipherFieldService, CopyAction } from "./services/copy-cipher-field.service";
|
||||
export { CopyCipherFieldDirective } from "./components/copy-cipher-field.directive";
|
||||
|
||||
162
libs/vault/src/services/at-risk-password-callout.service.spec.ts
Normal file
162
libs/vault/src/services/at-risk-password-callout.service.spec.ts
Normal file
@@ -0,0 +1,162 @@
|
||||
import { TestBed } from "@angular/core/testing";
|
||||
import { firstValueFrom, of } from "rxjs";
|
||||
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import {
|
||||
SecurityTask,
|
||||
SecurityTaskStatus,
|
||||
SecurityTaskType,
|
||||
TaskService,
|
||||
} from "@bitwarden/common/vault/tasks";
|
||||
import { StateProvider } from "@bitwarden/state";
|
||||
import { UserId } from "@bitwarden/user-core";
|
||||
|
||||
import { FakeSingleUserState } from "../../../common/spec/fake-state";
|
||||
|
||||
import {
|
||||
AtRiskPasswordCalloutData,
|
||||
AtRiskPasswordCalloutService,
|
||||
} from "./at-risk-password-callout.service";
|
||||
|
||||
const fakeUserState = () =>
|
||||
({
|
||||
update: jest.fn().mockResolvedValue(undefined),
|
||||
state$: of(null),
|
||||
}) as unknown as FakeSingleUserState<AtRiskPasswordCalloutData>;
|
||||
|
||||
class MockCipherView {
|
||||
constructor(
|
||||
public id: string,
|
||||
private deleted: boolean,
|
||||
) {}
|
||||
get isDeleted() {
|
||||
return this.deleted;
|
||||
}
|
||||
}
|
||||
|
||||
describe("AtRiskPasswordCalloutService", () => {
|
||||
let service: AtRiskPasswordCalloutService;
|
||||
const mockTaskService = {
|
||||
pendingTasks$: jest.fn(),
|
||||
completedTasks$: jest.fn(),
|
||||
};
|
||||
const mockCipherService = { cipherViews$: jest.fn() };
|
||||
const mockStateProvider = { getUser: jest.fn().mockReturnValue(fakeUserState()) };
|
||||
const userId: UserId = "user1" as UserId;
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
AtRiskPasswordCalloutService,
|
||||
{
|
||||
provide: TaskService,
|
||||
useValue: mockTaskService,
|
||||
},
|
||||
{
|
||||
provide: CipherService,
|
||||
useValue: mockCipherService,
|
||||
},
|
||||
{
|
||||
provide: StateProvider,
|
||||
useValue: mockStateProvider,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
service = TestBed.inject(AtRiskPasswordCalloutService);
|
||||
});
|
||||
|
||||
describe("completedTasks$", () => {
|
||||
it(" should return true if completed tasks exist", async () => {
|
||||
const tasks: SecurityTask[] = [
|
||||
{
|
||||
id: "t1",
|
||||
cipherId: "c1",
|
||||
type: SecurityTaskType.UpdateAtRiskCredential,
|
||||
status: SecurityTaskStatus.Completed,
|
||||
} as any,
|
||||
{
|
||||
id: "t2",
|
||||
cipherId: "c2",
|
||||
type: SecurityTaskType.UpdateAtRiskCredential,
|
||||
status: SecurityTaskStatus.Pending,
|
||||
} as any,
|
||||
{
|
||||
id: "t3",
|
||||
cipherId: "nope",
|
||||
type: SecurityTaskType.UpdateAtRiskCredential,
|
||||
status: SecurityTaskStatus.Completed,
|
||||
} as any,
|
||||
{
|
||||
id: "t4",
|
||||
cipherId: "c3",
|
||||
type: SecurityTaskType.UpdateAtRiskCredential,
|
||||
status: SecurityTaskStatus.Completed,
|
||||
} as any,
|
||||
];
|
||||
|
||||
jest.spyOn(mockTaskService, "completedTasks$").mockReturnValue(of(tasks));
|
||||
|
||||
const result = await firstValueFrom(service.completedTasks$(userId));
|
||||
|
||||
expect(result).toEqual(tasks[0]);
|
||||
expect(result?.id).toBe("t1");
|
||||
});
|
||||
});
|
||||
|
||||
describe("showCompletedTasksBanner$", () => {
|
||||
beforeEach(() => {
|
||||
jest.spyOn(mockTaskService, "pendingTasks$").mockReturnValue(of([]));
|
||||
jest.spyOn(mockTaskService, "completedTasks$").mockReturnValue(of([]));
|
||||
jest.spyOn(mockCipherService, "cipherViews$").mockReturnValue(of([]));
|
||||
});
|
||||
|
||||
it("should return false if banner has been dismissed", async () => {
|
||||
const state: AtRiskPasswordCalloutData = {
|
||||
hasInteractedWithTasks: true,
|
||||
tasksBannerDismissed: true,
|
||||
};
|
||||
const mockState = { ...fakeUserState(), state$: of(state) };
|
||||
mockStateProvider.getUser.mockReturnValue(mockState);
|
||||
|
||||
const result = await firstValueFrom(service.showCompletedTasksBanner$(userId));
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it("should return true when has completed tasks, no pending tasks, and banner not dismissed", async () => {
|
||||
const completedTasks = [
|
||||
{
|
||||
id: "t1",
|
||||
cipherId: "c1",
|
||||
type: SecurityTaskType.UpdateAtRiskCredential,
|
||||
status: SecurityTaskStatus.Completed,
|
||||
},
|
||||
];
|
||||
const ciphers = [new MockCipherView("c1", false)];
|
||||
const state: AtRiskPasswordCalloutData = {
|
||||
hasInteractedWithTasks: true,
|
||||
tasksBannerDismissed: false,
|
||||
};
|
||||
|
||||
jest.spyOn(mockTaskService, "completedTasks$").mockReturnValue(of(completedTasks));
|
||||
jest.spyOn(mockCipherService, "cipherViews$").mockReturnValue(of(ciphers));
|
||||
mockStateProvider.getUser.mockReturnValue({ state$: of(state) });
|
||||
|
||||
const result = await firstValueFrom(service.showCompletedTasksBanner$(userId));
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it("returns false when no completed tasks", async () => {
|
||||
const state: AtRiskPasswordCalloutData = {
|
||||
hasInteractedWithTasks: true,
|
||||
tasksBannerDismissed: false,
|
||||
};
|
||||
mockStateProvider.getUser.mockReturnValue({ state$: of(state) });
|
||||
|
||||
const result = await firstValueFrom(service.showCompletedTasksBanner$(userId));
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
93
libs/vault/src/services/at-risk-password-callout.service.ts
Normal file
93
libs/vault/src/services/at-risk-password-callout.service.ts
Normal file
@@ -0,0 +1,93 @@
|
||||
import { Injectable } from "@angular/core";
|
||||
import { combineLatest, map, Observable } from "rxjs";
|
||||
|
||||
import {
|
||||
SingleUserState,
|
||||
StateProvider,
|
||||
UserKeyDefinition,
|
||||
VAULT_AT_RISK_PASSWORDS_MEMORY,
|
||||
} from "@bitwarden/common/platform/state";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { SecurityTask, SecurityTaskType, TaskService } from "@bitwarden/common/vault/tasks";
|
||||
import { UserId } from "@bitwarden/user-core";
|
||||
|
||||
export type AtRiskPasswordCalloutData = {
|
||||
hasInteractedWithTasks: boolean;
|
||||
tasksBannerDismissed: boolean;
|
||||
};
|
||||
|
||||
export const AT_RISK_PASSWORD_CALLOUT_KEY = new UserKeyDefinition<AtRiskPasswordCalloutData>(
|
||||
VAULT_AT_RISK_PASSWORDS_MEMORY,
|
||||
"atRiskPasswords",
|
||||
{
|
||||
deserializer: (jsonData) => jsonData,
|
||||
clearOn: ["lock", "logout"],
|
||||
},
|
||||
);
|
||||
|
||||
@Injectable()
|
||||
export class AtRiskPasswordCalloutService {
|
||||
constructor(
|
||||
private taskService: TaskService,
|
||||
private cipherService: CipherService,
|
||||
private stateProvider: StateProvider,
|
||||
) {}
|
||||
|
||||
pendingTasks$(userId: UserId): Observable<SecurityTask[]> {
|
||||
return combineLatest([
|
||||
this.taskService.pendingTasks$(userId),
|
||||
this.cipherService.cipherViews$(userId),
|
||||
]).pipe(
|
||||
map(([tasks, ciphers]) => {
|
||||
return tasks.filter((t: SecurityTask) => {
|
||||
const associatedCipher = ciphers.find((c) => c.id === t.cipherId);
|
||||
|
||||
return (
|
||||
t.type === SecurityTaskType.UpdateAtRiskCredential &&
|
||||
associatedCipher &&
|
||||
!associatedCipher.isDeleted
|
||||
);
|
||||
});
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
completedTasks$(userId: UserId): Observable<SecurityTask | undefined> {
|
||||
return this.taskService.completedTasks$(userId).pipe(
|
||||
map((tasks) => {
|
||||
return tasks.find((t: SecurityTask) => t.type === SecurityTaskType.UpdateAtRiskCredential);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
showCompletedTasksBanner$(userId: UserId): Observable<boolean> {
|
||||
return combineLatest([
|
||||
this.pendingTasks$(userId),
|
||||
this.completedTasks$(userId),
|
||||
this.atRiskPasswordState(userId).state$,
|
||||
]).pipe(
|
||||
map(([pendingTasks, completedTasks, state]) => {
|
||||
const hasPendingTasks = pendingTasks.length > 0;
|
||||
const bannerDismissed = state?.tasksBannerDismissed ?? false;
|
||||
const hasInteracted = state?.hasInteractedWithTasks ?? false;
|
||||
|
||||
// This will ensure the banner remains visible only in the client the user resolved their tasks in
|
||||
// e.g. if the user did not see tasks in the browser, and resolves them in the web, the browser will not show the banner
|
||||
if (!hasPendingTasks && (!hasInteracted || bannerDismissed)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Show banner if there are completed tasks and no pending tasks, and banner hasn't been dismissed
|
||||
return !!completedTasks && !hasPendingTasks && !(state?.tasksBannerDismissed ?? false);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
atRiskPasswordState(userId: UserId): SingleUserState<AtRiskPasswordCalloutData> {
|
||||
return this.stateProvider.getUser(userId, AT_RISK_PASSWORD_CALLOUT_KEY);
|
||||
}
|
||||
|
||||
updateAtRiskPasswordState(userId: UserId, updatedState: AtRiskPasswordCalloutData): void {
|
||||
void this.atRiskPasswordState(userId).update(() => updatedState);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user