frontend: enhance change admin password security to include current password and confirm password in form, and add password visibility toggle

This commit is contained in:
juancarmore 2025-08-22 22:50:31 +02:00
parent a2e78afda5
commit 3413e21e0e
4 changed files with 198 additions and 29 deletions

View File

@ -86,22 +86,86 @@
</div>
<div class="admin-auth-form">
<!-- Username Field -->
<mat-form-field subscriptSizing="dynamic" appearance="outline" class="username-field">
<mat-label>Username</mat-label>
<input matInput type="text" formControlName="adminUsername" placeholder="admin" />
<input matInput type="text" formControlName="username" placeholder="admin" />
</mat-form-field>
<!-- Current Password Field -->
<mat-form-field subscriptSizing="dynamic" appearance="outline" class="password-field">
<mat-label>Password</mat-label>
<mat-label>Current Password</mat-label>
<input
matInput
type="password"
formControlName="adminPassword"
placeholder="••••••••"
[type]="showCurrentPassword() ? 'text' : 'password'"
formControlName="currentPassword"
placeholder="Enter your current password"
/>
<mat-hint>Minimum of 4 characters</mat-hint>
@if (getAdminPasswordError()) {
<mat-error>{{ getAdminPasswordError() }}</mat-error>
<button
mat-icon-button
matSuffix
type="button"
class="input-btn"
(click)="showCurrentPassword.set(!showCurrentPassword())"
matTooltip="{{ showCurrentPassword() ? 'Hide password' : 'Show password' }}"
>
<mat-icon>{{
showCurrentPassword() ? 'visibility_off' : 'visibility'
}}</mat-icon>
</button>
@if (getCurrentPasswordError()) {
<mat-error>{{ getCurrentPasswordError() }}</mat-error>
}
</mat-form-field>
<!-- New Password Field -->
<mat-form-field subscriptSizing="dynamic" appearance="outline" class="password-field">
<mat-label>New Password</mat-label>
<input
matInput
[type]="showNewPassword() ? 'text' : 'password'"
formControlName="newPassword"
placeholder="Enter your new password"
/>
<button
mat-icon-button
matSuffix
type="button"
class="input-btn"
(click)="showNewPassword.set(!showNewPassword())"
matTooltip="{{ showNewPassword() ? 'Hide password' : 'Show password' }}"
>
<mat-icon>{{ showNewPassword() ? 'visibility_off' : 'visibility' }}</mat-icon>
</button>
<mat-hint>Minimum of 5 characters</mat-hint>
@if (getNewPasswordError()) {
<mat-error>{{ getNewPasswordError() }}</mat-error>
}
</mat-form-field>
<!-- Confirm New Password Field -->
<mat-form-field subscriptSizing="dynamic" appearance="outline" class="password-field">
<mat-label>Confirm New Password</mat-label>
<input
matInput
[type]="showConfirmPassword() ? 'text' : 'password'"
formControlName="confirmPassword"
placeholder="Confirm your new password"
/>
<button
mat-icon-button
matSuffix
type="button"
class="input-btn"
(click)="showConfirmPassword.set(!showConfirmPassword())"
matTooltip="{{ showConfirmPassword() ? 'Hide password' : 'Show password' }}"
>
<mat-icon>{{
showConfirmPassword() ? 'visibility_off' : 'visibility'
}}</mat-icon>
</button>
@if (getConfirmPasswordError()) {
<mat-error>{{ getConfirmPasswordError() }}</mat-error>
}
</mat-form-field>
</div>

View File

@ -30,6 +30,10 @@
::ng-deep .mat-mdc-form-field {
width: 100%;
}
::ng-deep .input-btn {
padding: var(--ov-meet-spacing-sm);
}
}
.username-field {

View File

@ -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;
}
}

View File

@ -86,9 +86,9 @@ export class AuthService {
}
}
async changePassword(newPassword: string): Promise<any> {
async changePassword(currentPassword: string, newPassword: string): Promise<any> {
const path = `${this.USERS_API}/change-password`;
return this.httpService.postRequest(path, { newPassword });
return this.httpService.postRequest(path, { currentPassword, newPassword });
}
async generateApiKey(): Promise<MeetApiKey> {