1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-07 12:13:45 +00:00

feat(change-password-component): Change Password Update [18720] - Very close to complete.

This commit is contained in:
Patrick Pimentel
2025-05-21 14:55:31 -04:00
parent 27884d9ffe
commit f45e2d7d8a
12 changed files with 121 additions and 25 deletions

View File

@@ -98,7 +98,7 @@ export class WebLoginComponentService
const enforcedPasswordPolicyOptions = await firstValueFrom(
this.accountService.activeAccount$.pipe(
getUserId,
switchMap((userId) => this.policyService.masterPasswordPolicyOptions$(userId)),
switchMap((userId) => this.policyService.masterPasswordPolicyOptions$(userId, policies)),
),
);

View File

@@ -3,7 +3,6 @@
</div>
<div class="tw-max-w-lg tw-mb-12">
<bit-callout type="warning">{{ "loggedOutWarning" | i18n }}</bit-callout>
<auth-change-password [inputPasswordFlow]="inputPasswordFlow"></auth-change-password>
</div>

View File

@@ -10,6 +10,7 @@ import {
unauthGuardFn,
activeAuthGuard,
} from "@bitwarden/angular/auth/guards";
import { canAccessFeature } from "@bitwarden/angular/platform/guard/feature-flag.guard";
import {
AnonLayoutWrapperComponent,
AnonLayoutWrapperData,
@@ -38,7 +39,9 @@ import {
TwoFactorAuthGuard,
NewDeviceVerificationComponent,
DeviceVerificationIcon,
ChangePasswordComponent,
} from "@bitwarden/auth/angular";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { LockComponent } from "@bitwarden/key-management-ui";
import { VaultIcons } from "@bitwarden/vault";
@@ -139,16 +142,54 @@ const routes: Routes = [
canActivate: [unauthGuardFn()],
data: { titleId: "deleteOrganization" },
},
{
path: "",
component: AnonLayoutWrapperComponent,
children: [
{
path: "change-password",
children: [
{
path: "",
component: ChangePasswordComponent,
},
],
data: {
pageIcon: LockIcon,
pageTitle: { key: "updateMasterPassword" },
hideFooter: true,
maxWidth: "lg",
} satisfies AnonLayoutWrapperData,
},
],
data: { titleId: "updatePassword" } satisfies RouteDataProperties,
},
{
path: "update-temp-password",
component: UpdateTempPasswordComponent,
canActivate: [authGuard],
canActivate: [
canAccessFeature(
FeatureFlag.PM16117_ChangeExistingPasswordRefactor,
true,
"/change-password",
false,
),
authGuard,
],
data: { titleId: "updateTempPassword" } satisfies RouteDataProperties,
},
{
path: "update-password",
component: UpdatePasswordComponent,
canActivate: [authGuard],
canActivate: [
canAccessFeature(
FeatureFlag.PM16117_ChangeExistingPasswordRefactor,
true,
"/change-password",
false,
),
authGuard,
],
data: { titleId: "updatePassword" } satisfies RouteDataProperties,
},
{

View File

@@ -1788,6 +1788,9 @@
"loggedOutWarning": {
"message": "Proceeding will log you out of your current session, requiring you to log back in. Active sessions on other devices may continue to remain active for up to one hour."
},
"changePasswordWarning": {
"message": "After changing your password, you will need to log in with your new password. Active sessions on other devices will be logged out within one hour."
},
"emailChanged": {
"message": "Email saved"
},

View File

@@ -5,6 +5,7 @@
[showReadonlyHostname]="showReadonlyHostname"
[maxWidth]="maxWidth"
[titleAreaMaxWidth]="titleAreaMaxWidth"
[hideFooter]="hideFooter"
>
<router-outlet></router-outlet>
<router-outlet slot="secondary" name="secondary"></router-outlet>

View File

@@ -34,11 +34,16 @@ export interface AnonLayoutWrapperData {
/**
* Optional flag to set the max-width of the page. Defaults to 'md' if not provided.
*/
maxWidth?: "md" | "3xl";
maxWidth?: "sm" | "md" | "lg" | "xl" | "2xl" | "3xl";
/**
* Optional flag to set the max-width of the title area. Defaults to null if not provided.
*/
titleAreaMaxWidth?: "md";
/**
* Optional flag to hide the whole footer.
*/
hideFooter?: boolean;
}
@Component({
@@ -55,6 +60,7 @@ export class AnonLayoutWrapperComponent implements OnInit, OnDestroy {
protected showReadonlyHostname: boolean;
protected maxWidth: "md" | "3xl";
protected titleAreaMaxWidth: "md";
protected hideFooter: boolean;
constructor(
private router: Router,
@@ -106,6 +112,7 @@ export class AnonLayoutWrapperComponent implements OnInit, OnDestroy {
this.showReadonlyHostname = Boolean(firstChildRouteData["showReadonlyHostname"]);
this.maxWidth = firstChildRouteData["maxWidth"];
this.titleAreaMaxWidth = firstChildRouteData["titleAreaMaxWidth"];
this.hideFooter = Boolean(firstChildRouteData["hideFooter"]);
}
private listenForServiceDataChanges() {

View File

@@ -37,7 +37,14 @@
<div
class="tw-grow tw-w-full tw-max-w-md tw-mx-auto tw-flex tw-flex-col tw-items-center sm:tw-min-w-[28rem]"
[ngClass]="{ 'tw-max-w-md': maxWidth === 'md', 'tw-max-w-3xl': maxWidth === '3xl' }"
[ngClass]="{
'tw-max-w-sm': maxWidth === 'sm',
'tw-max-w-md': maxWidth === 'md',
'tw-max-w-lg': maxWidth === 'lg',
'tw-max-w-xl': maxWidth === 'xl',
'tw-max-w-2xl': maxWidth === '2xl',
'tw-max-w-3xl': maxWidth === '3xl',
}"
>
<div
class="tw-rounded-2xl tw-mb-6 sm:tw-mb-10 tw-mx-auto tw-w-full sm:tw-bg-background sm:tw-border sm:tw-border-solid sm:tw-border-secondary-300 sm:tw-p-8"

View File

@@ -1,5 +1,3 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { CommonModule } from "@angular/common";
import { Component, HostBinding, Input, OnChanges, OnInit, SimpleChanges } from "@angular/core";
import { RouterModule } from "@angular/router";
@@ -33,10 +31,10 @@ export class AnonLayoutComponent implements OnInit, OnChanges {
return ["tw-h-full"];
}
@Input() title: string;
@Input() subtitle: string;
@Input() icon: Icon;
@Input() showReadonlyHostname: boolean;
@Input() title?: string;
@Input() subtitle?: string;
@Input() icon?: Icon;
@Input() showReadonlyHostname?: boolean;
@Input() hideLogo: boolean = false;
@Input() hideFooter: boolean = false;
@Input() hideIcon: boolean = false;
@@ -53,13 +51,13 @@ export class AnonLayoutComponent implements OnInit, OnChanges {
*
* @default 'md'
*/
@Input() maxWidth: "md" | "3xl" = "md";
@Input() maxWidth: "sm" | "md" | "lg" | "xl" | "2xl" | "3xl" = "md";
protected logo = BitwardenLogo;
protected year = "2024";
protected year: string;
protected clientType: ClientType;
protected hostname: string;
protected version: string;
protected hostname?: string;
protected version?: string;
protected hideYearAndVersion = false;
@@ -74,7 +72,7 @@ export class AnonLayoutComponent implements OnInit, OnChanges {
async ngOnInit() {
this.maxWidth = this.maxWidth ?? "md";
this.titleAreaMaxWidth = this.titleAreaMaxWidth ?? null;
this.titleAreaMaxWidth = this.titleAreaMaxWidth ?? undefined;
this.hostname = (await firstValueFrom(this.environmentService.environment$)).getHostname();
this.version = await this.platformUtilsService.getApplicationVersion();

View File

@@ -9,12 +9,14 @@
<auth-input-password
[flow]="inputPasswordFlow"
[email]="email"
[userId]="userId"
[userId]="activeUserId"
[loading]="submitting"
[masterPasswordPolicyOptions]="masterPasswordPolicyOptions"
[inlineButtons]="true"
[primaryButtonText]="{ key: 'changeMasterPassword' }"
(onPasswordFormSubmit)="handlePasswordFormSubmit($event)"
[secondaryButtonText]="{ key: 'cancel' }"
(onSecondaryButtonClick)="logOut()"
>
</auth-input-password>
}

View File

@@ -1,14 +1,18 @@
import { Component, Input, OnInit } 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 { Account, AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason";
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
import { SyncService } from "@bitwarden/common/platform/sync";
import { UserId } from "@bitwarden/common/types/guid";
import { ToastService } from "@bitwarden/components";
import { DialogService, ToastService, Translation } from "@bitwarden/components";
import { I18nPipe } from "@bitwarden/ui-common";
import {
@@ -30,35 +34,65 @@ export class ChangePasswordComponent implements OnInit {
activeAccount: Account | null = null;
email?: string;
userId?: UserId;
activeUserId?: UserId;
masterPasswordPolicyOptions?: MasterPasswordPolicyOptions;
initializing = true;
submitting = false;
formPromise?: Promise<any>;
forceSetPasswordReason: ForceSetPasswordReason = ForceSetPasswordReason.None;
warningText?: Translation;
constructor(
private accountService: AccountService,
private changePasswordService: ChangePasswordService,
private configService: ConfigService,
private i18nService: I18nService,
private masterPasswordService: InternalMasterPasswordServiceAbstraction,
private messagingService: MessagingService,
private policyService: PolicyService,
private toastService: ToastService,
private syncService: SyncService,
// private routerService: RouterService,
// private acceptOrganizationInviteService: AcceptOrganizationInviteService,
private dialogService: DialogService,
private router: Router,
) {}
async ngOnInit() {
this.activeAccount = await firstValueFrom(this.accountService.activeAccount$);
this.userId = this.activeAccount?.id;
this.activeUserId = this.activeAccount?.id;
this.email = this.activeAccount?.email;
if (!this.userId) {
if (!this.activeUserId) {
throw new Error("userId not found");
}
this.masterPasswordPolicyOptions = await firstValueFrom(
this.policyService.masterPasswordPolicyOptions$(this.userId),
this.policyService.masterPasswordPolicyOptions$(this.activeUserId),
);
this.forceSetPasswordReason = await firstValueFrom(
this.masterPasswordService.forceSetPasswordReason$(this.activeUserId),
);
this.initializing = false;
if (this.masterPasswordPolicyOptions?.enforceOnLogin) {
this.warningText = { key: "masterPasswordInvalidWarning" };
}
}
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");
}
}
async handlePasswordFormSubmit(passwordInputResult: PasswordInputResult) {
@@ -83,11 +117,11 @@ export class ChangePasswordComponent implements OnInit {
passwordInputResult.newPasswordHint,
);
} else {
if (!this.userId) {
if (!this.activeUserId) {
throw new Error("userId not found");
}
await this.changePasswordService.changePassword(passwordInputResult, this.userId);
await this.changePasswordService.changePassword(passwordInputResult, this.activeUserId);
this.toastService.showToast({
variant: "success",

View File

@@ -1,4 +1,6 @@
<form [formGroup]="formGroup" [bitSubmit]="submit">
<bit-callout type="warning">{{ "changePasswordWarning" | i18n }}</bit-callout>
<auth-password-callout
*ngIf="masterPasswordPolicyOptions"
[policy]="masterPasswordPolicyOptions"

View File

@@ -20,6 +20,7 @@ import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.servi
import {
AsyncActionsModule,
ButtonModule,
CalloutComponent,
CheckboxModule,
DialogService,
FormFieldModule,
@@ -86,6 +87,7 @@ interface InputPasswordForm {
imports: [
AsyncActionsModule,
ButtonModule,
CalloutComponent,
CheckboxModule,
FormFieldModule,
IconButtonModule,