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:
parent
a2e78afda5
commit
3413e21e0e
@ -86,22 +86,86 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="admin-auth-form">
|
<div class="admin-auth-form">
|
||||||
|
<!-- Username Field -->
|
||||||
<mat-form-field subscriptSizing="dynamic" appearance="outline" class="username-field">
|
<mat-form-field subscriptSizing="dynamic" appearance="outline" class="username-field">
|
||||||
<mat-label>Username</mat-label>
|
<mat-label>Username</mat-label>
|
||||||
<input matInput type="text" formControlName="adminUsername" placeholder="admin" />
|
<input matInput type="text" formControlName="username" placeholder="admin" />
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
|
|
||||||
|
<!-- Current Password Field -->
|
||||||
<mat-form-field subscriptSizing="dynamic" appearance="outline" class="password-field">
|
<mat-form-field subscriptSizing="dynamic" appearance="outline" class="password-field">
|
||||||
<mat-label>Password</mat-label>
|
<mat-label>Current Password</mat-label>
|
||||||
<input
|
<input
|
||||||
matInput
|
matInput
|
||||||
type="password"
|
[type]="showCurrentPassword() ? 'text' : 'password'"
|
||||||
formControlName="adminPassword"
|
formControlName="currentPassword"
|
||||||
placeholder="••••••••"
|
placeholder="Enter your current password"
|
||||||
/>
|
/>
|
||||||
<mat-hint>Minimum of 4 characters</mat-hint>
|
<button
|
||||||
@if (getAdminPasswordError()) {
|
mat-icon-button
|
||||||
<mat-error>{{ getAdminPasswordError() }}</mat-error>
|
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>
|
</mat-form-field>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -30,6 +30,10 @@
|
|||||||
::ng-deep .mat-mdc-form-field {
|
::ng-deep .mat-mdc-form-field {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
::ng-deep .input-btn {
|
||||||
|
padding: var(--ov-meet-spacing-sm);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.username-field {
|
.username-field {
|
||||||
|
|||||||
@ -1,5 +1,12 @@
|
|||||||
import { Component, OnInit, signal } from '@angular/core';
|
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 { MatButtonModule } from '@angular/material/button';
|
||||||
import { MatCardModule } from '@angular/material/card';
|
import { MatCardModule } from '@angular/material/card';
|
||||||
import { MatDividerModule } from '@angular/material/divider';
|
import { MatDividerModule } from '@angular/material/divider';
|
||||||
@ -8,6 +15,7 @@ import { MatIconModule } from '@angular/material/icon';
|
|||||||
import { MatInputModule } from '@angular/material/input';
|
import { MatInputModule } from '@angular/material/input';
|
||||||
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
|
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
|
||||||
import { MatSelectModule } from '@angular/material/select';
|
import { MatSelectModule } from '@angular/material/select';
|
||||||
|
import { MatTooltipModule } from '@angular/material/tooltip';
|
||||||
import { ProFeatureBadgeComponent } from '@lib/components';
|
import { ProFeatureBadgeComponent } from '@lib/components';
|
||||||
import { AuthService, GlobalPreferencesService, NotificationService } from '@lib/services';
|
import { AuthService, GlobalPreferencesService, NotificationService } from '@lib/services';
|
||||||
import { AuthMode } from '@lib/typings/ce';
|
import { AuthMode } from '@lib/typings/ce';
|
||||||
@ -22,6 +30,7 @@ import { AuthMode } from '@lib/typings/ce';
|
|||||||
MatInputModule,
|
MatInputModule,
|
||||||
MatFormFieldModule,
|
MatFormFieldModule,
|
||||||
MatSelectModule,
|
MatSelectModule,
|
||||||
|
MatTooltipModule,
|
||||||
MatProgressSpinnerModule,
|
MatProgressSpinnerModule,
|
||||||
MatDividerModule,
|
MatDividerModule,
|
||||||
ReactiveFormsModule,
|
ReactiveFormsModule,
|
||||||
@ -32,11 +41,16 @@ import { AuthMode } from '@lib/typings/ce';
|
|||||||
})
|
})
|
||||||
export class UsersPermissionsComponent implements OnInit {
|
export class UsersPermissionsComponent implements OnInit {
|
||||||
isLoading = signal(true);
|
isLoading = signal(true);
|
||||||
hasAccessSettingsChanges = signal(false);
|
|
||||||
|
showCurrentPassword = signal(false);
|
||||||
|
showNewPassword = signal(false);
|
||||||
|
showConfirmPassword = signal(false);
|
||||||
|
|
||||||
adminCredentialsForm = new FormGroup({
|
adminCredentialsForm = new FormGroup({
|
||||||
adminUsername: new FormControl({ value: '', disabled: true }, [Validators.required]),
|
username: new FormControl({ value: '', disabled: true }, [Validators.required]),
|
||||||
adminPassword: new FormControl('', [Validators.required, Validators.minLength(4)])
|
currentPassword: new FormControl('', [Validators.required]),
|
||||||
|
newPassword: new FormControl('', [Validators.required, Validators.minLength(5)]),
|
||||||
|
confirmPassword: new FormControl('', [Validators.required])
|
||||||
});
|
});
|
||||||
accessSettingsForm = new FormGroup({
|
accessSettingsForm = new FormGroup({
|
||||||
authModeToAccessRoom: new FormControl(AuthMode.NONE, [Validators.required])
|
authModeToAccessRoom: new FormControl(AuthMode.NONE, [Validators.required])
|
||||||
@ -49,6 +63,7 @@ export class UsersPermissionsComponent implements OnInit {
|
|||||||
{ value: AuthMode.ALL_USERS, label: 'Everyone' }
|
{ value: AuthMode.ALL_USERS, label: 'Everyone' }
|
||||||
];
|
];
|
||||||
|
|
||||||
|
hasAccessSettingsChanges = signal(false);
|
||||||
private initialAccessSettingsFormValue: any = null;
|
private initialAccessSettingsFormValue: any = null;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@ -60,6 +75,22 @@ export class UsersPermissionsComponent implements OnInit {
|
|||||||
this.accessSettingsForm.valueChanges.subscribe(() => {
|
this.accessSettingsForm.valueChanges.subscribe(() => {
|
||||||
this.checkForAccessSettingsChanges();
|
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() {
|
async ngOnInit() {
|
||||||
@ -67,6 +98,12 @@ export class UsersPermissionsComponent implements OnInit {
|
|||||||
await this.loadAdminUsername();
|
await this.loadAdminUsername();
|
||||||
await this.loadAccessSettings();
|
await this.loadAccessSettings();
|
||||||
this.isLoading.set(false);
|
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() {
|
private async loadAdminUsername() {
|
||||||
@ -77,7 +114,7 @@ export class UsersPermissionsComponent implements OnInit {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.adminCredentialsForm.get('adminUsername')?.setValue(username);
|
this.adminCredentialsForm.get('username')?.setValue(username);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async loadAccessSettings() {
|
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() {
|
private checkForAccessSettingsChanges() {
|
||||||
if (!this.initialAccessSettingsFormValue) {
|
if (!this.initialAccessSettingsFormValue) {
|
||||||
return;
|
return;
|
||||||
@ -109,12 +168,24 @@ export class UsersPermissionsComponent implements OnInit {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const formData = this.adminCredentialsForm.value;
|
const { username, currentPassword, newPassword } = this.adminCredentialsForm.value;
|
||||||
const adminPassword = formData.adminPassword!;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await this.authService.changePassword(adminPassword);
|
await this.authService.changePassword(currentPassword!, newPassword!);
|
||||||
this.notificationService.showSnackbar('Admin credentials updated successfully');
|
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) {
|
} catch (error) {
|
||||||
console.error('Error saving admin credentials:', error);
|
console.error('Error saving admin credentials:', error);
|
||||||
this.notificationService.showSnackbar('Failed to save admin credentials');
|
this.notificationService.showSnackbar('Failed to save admin credentials');
|
||||||
@ -145,20 +216,50 @@ export class UsersPermissionsComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Utility methods for form validation
|
// Utility methods for form validation
|
||||||
getAdminPasswordError(): string {
|
|
||||||
const control = this.adminCredentialsForm.get('adminPassword')!;
|
getCurrentPasswordError(): string | null {
|
||||||
if (!control.touched || !control.errors) {
|
const control = this.adminCredentialsForm.get('currentPassword');
|
||||||
return '';
|
|
||||||
|
if (control?.errors && control.touched) {
|
||||||
|
if (control.errors['required']) {
|
||||||
|
return 'Current password is required';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const errors = control.errors;
|
return null;
|
||||||
if (errors['required']) {
|
}
|
||||||
return 'Admin password is required';
|
|
||||||
}
|
getNewPasswordError(): string | null {
|
||||||
if (errors['minlength']) {
|
const control = this.adminCredentialsForm.get('newPassword');
|
||||||
return `Admin password must be at least ${errors['minlength'].requiredLength} characters`;
|
|
||||||
|
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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`;
|
const path = `${this.USERS_API}/change-password`;
|
||||||
return this.httpService.postRequest(path, { newPassword });
|
return this.httpService.postRequest(path, { currentPassword, newPassword });
|
||||||
}
|
}
|
||||||
|
|
||||||
async generateApiKey(): Promise<MeetApiKey> {
|
async generateApiKey(): Promise<MeetApiKey> {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user