From 3413e21e0eb44af5d35e124ec2c212085b6333a5 Mon Sep 17 00:00:00 2001 From: juancarmore Date: Fri, 22 Aug 2025 22:50:31 +0200 Subject: [PATCH] frontend: enhance change admin password security to include current password and confirm password in form, and add password visibility toggle --- .../users-permissions.component.html | 80 +++++++++- .../users-permissions.component.scss | 4 + .../users-permissions.component.ts | 139 +++++++++++++++--- .../src/lib/services/auth.service.ts | 4 +- 4 files changed, 198 insertions(+), 29 deletions(-) diff --git a/frontend/projects/shared-meet-components/src/lib/pages/console/users-permissions/users-permissions.component.html b/frontend/projects/shared-meet-components/src/lib/pages/console/users-permissions/users-permissions.component.html index 1d9c810..20e6d51 100644 --- a/frontend/projects/shared-meet-components/src/lib/pages/console/users-permissions/users-permissions.component.html +++ b/frontend/projects/shared-meet-components/src/lib/pages/console/users-permissions/users-permissions.component.html @@ -86,22 +86,86 @@
+ Username - + + - Password + Current Password - Minimum of 4 characters - @if (getAdminPasswordError()) { - {{ getAdminPasswordError() }} + + @if (getCurrentPasswordError()) { + {{ getCurrentPasswordError() }} + } + + + + + New Password + + + Minimum of 5 characters + @if (getNewPasswordError()) { + {{ getNewPasswordError() }} + } + + + + + Confirm New Password + + + @if (getConfirmPasswordError()) { + {{ getConfirmPasswordError() }} }
diff --git a/frontend/projects/shared-meet-components/src/lib/pages/console/users-permissions/users-permissions.component.scss b/frontend/projects/shared-meet-components/src/lib/pages/console/users-permissions/users-permissions.component.scss index ec7f62d..2ab1aeb 100644 --- a/frontend/projects/shared-meet-components/src/lib/pages/console/users-permissions/users-permissions.component.scss +++ b/frontend/projects/shared-meet-components/src/lib/pages/console/users-permissions/users-permissions.component.scss @@ -30,6 +30,10 @@ ::ng-deep .mat-mdc-form-field { width: 100%; } + + ::ng-deep .input-btn { + padding: var(--ov-meet-spacing-sm); + } } .username-field { diff --git a/frontend/projects/shared-meet-components/src/lib/pages/console/users-permissions/users-permissions.component.ts b/frontend/projects/shared-meet-components/src/lib/pages/console/users-permissions/users-permissions.component.ts index ae10f5b..9b6a267 100644 --- a/frontend/projects/shared-meet-components/src/lib/pages/console/users-permissions/users-permissions.component.ts +++ b/frontend/projects/shared-meet-components/src/lib/pages/console/users-permissions/users-permissions.component.ts @@ -1,5 +1,12 @@ import { Component, OnInit, signal } from '@angular/core'; -import { FormControl, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms'; +import { + AbstractControl, + FormControl, + FormGroup, + ReactiveFormsModule, + ValidationErrors, + Validators +} from '@angular/forms'; import { MatButtonModule } from '@angular/material/button'; import { MatCardModule } from '@angular/material/card'; import { MatDividerModule } from '@angular/material/divider'; @@ -8,6 +15,7 @@ import { MatIconModule } from '@angular/material/icon'; import { MatInputModule } from '@angular/material/input'; import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; import { MatSelectModule } from '@angular/material/select'; +import { MatTooltipModule } from '@angular/material/tooltip'; import { ProFeatureBadgeComponent } from '@lib/components'; import { AuthService, GlobalPreferencesService, NotificationService } from '@lib/services'; import { AuthMode } from '@lib/typings/ce'; @@ -22,6 +30,7 @@ import { AuthMode } from '@lib/typings/ce'; MatInputModule, MatFormFieldModule, MatSelectModule, + MatTooltipModule, MatProgressSpinnerModule, MatDividerModule, ReactiveFormsModule, @@ -32,11 +41,16 @@ import { AuthMode } from '@lib/typings/ce'; }) export class UsersPermissionsComponent implements OnInit { isLoading = signal(true); - hasAccessSettingsChanges = signal(false); + + showCurrentPassword = signal(false); + showNewPassword = signal(false); + showConfirmPassword = signal(false); adminCredentialsForm = new FormGroup({ - adminUsername: new FormControl({ value: '', disabled: true }, [Validators.required]), - adminPassword: new FormControl('', [Validators.required, Validators.minLength(4)]) + username: new FormControl({ value: '', disabled: true }, [Validators.required]), + currentPassword: new FormControl('', [Validators.required]), + newPassword: new FormControl('', [Validators.required, Validators.minLength(5)]), + confirmPassword: new FormControl('', [Validators.required]) }); accessSettingsForm = new FormGroup({ authModeToAccessRoom: new FormControl(AuthMode.NONE, [Validators.required]) @@ -49,6 +63,7 @@ export class UsersPermissionsComponent implements OnInit { { value: AuthMode.ALL_USERS, label: 'Everyone' } ]; + hasAccessSettingsChanges = signal(false); private initialAccessSettingsFormValue: any = null; constructor( @@ -60,6 +75,22 @@ export class UsersPermissionsComponent implements OnInit { this.accessSettingsForm.valueChanges.subscribe(() => { this.checkForAccessSettingsChanges(); }); + + // Revalidate new password when current password changes + this.adminCredentialsForm.get('currentPassword')?.valueChanges.subscribe(() => { + const newPasswordControl = this.adminCredentialsForm.get('newPassword'); + if (newPasswordControl?.value) { + newPasswordControl.updateValueAndValidity(); + } + }); + + // Revalidate confirm password when new password changes + this.adminCredentialsForm.get('newPassword')?.valueChanges.subscribe(() => { + const confirmPasswordControl = this.adminCredentialsForm.get('confirmPassword'); + if (confirmPasswordControl?.value) { + confirmPasswordControl.updateValueAndValidity(); + } + }); } async ngOnInit() { @@ -67,6 +98,12 @@ export class UsersPermissionsComponent implements OnInit { await this.loadAdminUsername(); await this.loadAccessSettings(); this.isLoading.set(false); + + // Add custom validator for new password to prevent same password + this.adminCredentialsForm.get('newPassword')?.addValidators(this.newPasswordValidator.bind(this)); + + // Add custom validator for confirm password + this.adminCredentialsForm.get('confirmPassword')?.addValidators(this.confirmPasswordValidator.bind(this)); } private async loadAdminUsername() { @@ -77,7 +114,7 @@ export class UsersPermissionsComponent implements OnInit { return; } - this.adminCredentialsForm.get('adminUsername')?.setValue(username); + this.adminCredentialsForm.get('username')?.setValue(username); } private async loadAccessSettings() { @@ -94,6 +131,28 @@ export class UsersPermissionsComponent implements OnInit { } } + private newPasswordValidator(control: AbstractControl): ValidationErrors | null { + const currentPassword = this.adminCredentialsForm?.get('currentPassword')?.value; + const newPassword = control.value; + + if (!currentPassword || !newPassword) { + return null; + } + + return currentPassword === newPassword ? { samePassword: true } : null; + } + + private confirmPasswordValidator(control: AbstractControl): ValidationErrors | null { + const newPassword = this.adminCredentialsForm?.get('newPassword')?.value; + const confirmPassword = control.value; + + if (!newPassword || !confirmPassword) { + return null; + } + + return newPassword === confirmPassword ? null : { passwordMismatch: true }; + } + private checkForAccessSettingsChanges() { if (!this.initialAccessSettingsFormValue) { return; @@ -109,12 +168,24 @@ export class UsersPermissionsComponent implements OnInit { return; } - const formData = this.adminCredentialsForm.value; - const adminPassword = formData.adminPassword!; + const { username, currentPassword, newPassword } = this.adminCredentialsForm.value; try { - await this.authService.changePassword(adminPassword); + await this.authService.changePassword(currentPassword!, newPassword!); this.notificationService.showSnackbar('Admin credentials updated successfully'); + + // Reset the form + this.adminCredentialsForm.reset({ + username: username, + currentPassword: '', + newPassword: '', + confirmPassword: '' + }); + + // Hide all password fields + this.showCurrentPassword.set(false); + this.showNewPassword.set(false); + this.showConfirmPassword.set(false); } catch (error) { console.error('Error saving admin credentials:', error); this.notificationService.showSnackbar('Failed to save admin credentials'); @@ -145,20 +216,50 @@ export class UsersPermissionsComponent implements OnInit { } // Utility methods for form validation - getAdminPasswordError(): string { - const control = this.adminCredentialsForm.get('adminPassword')!; - if (!control.touched || !control.errors) { - return ''; + + getCurrentPasswordError(): string | null { + const control = this.adminCredentialsForm.get('currentPassword'); + + if (control?.errors && control.touched) { + if (control.errors['required']) { + return 'Current password is required'; + } } - const errors = control.errors; - if (errors['required']) { - return 'Admin password is required'; - } - if (errors['minlength']) { - return `Admin password must be at least ${errors['minlength'].requiredLength} characters`; + return null; + } + + getNewPasswordError(): string | null { + const control = this.adminCredentialsForm.get('newPassword'); + + if (control?.errors && control.touched) { + const errors = control.errors; + if (errors['required']) { + return 'New password is required'; + } + if (errors['minlength']) { + return `Password must be at least ${errors['minlength'].requiredLength} characters long`; + } + if (errors['samePassword']) { + return 'New password must be different from current password'; + } } - return ''; + return null; + } + + getConfirmPasswordError(): string | null { + const control = this.adminCredentialsForm.get('confirmPassword'); + + if (control?.touched && control?.errors) { + if (control.errors['required']) { + return 'Please confirm your password'; + } + if (control.errors['passwordMismatch']) { + return 'Passwords do not match'; + } + } + + return null; } } diff --git a/frontend/projects/shared-meet-components/src/lib/services/auth.service.ts b/frontend/projects/shared-meet-components/src/lib/services/auth.service.ts index 463dcd7..4cd5369 100644 --- a/frontend/projects/shared-meet-components/src/lib/services/auth.service.ts +++ b/frontend/projects/shared-meet-components/src/lib/services/auth.service.ts @@ -86,9 +86,9 @@ export class AuthService { } } - async changePassword(newPassword: string): Promise { + async changePassword(currentPassword: string, newPassword: string): Promise { const path = `${this.USERS_API}/change-password`; - return this.httpService.postRequest(path, { newPassword }); + return this.httpService.postRequest(path, { currentPassword, newPassword }); } async generateApiKey(): Promise {