user management bugs fixed

bugs resolved:
Setup User: No validation applied; the form saves with empty inputs. User ID length validation is not implemented. The role column is not displayed in the table.

Reset Password: No success message is shown after password update. Strong password validation is not applied.

Change Password: No success message is shown after password update. Strong password validation is not applied. Validation to prevent using the same value for old and new passwords is not implemented.
mazdak/UX-2147
Mazdak Gibran 2 weeks ago
parent 45b34eef3f
commit f3685ba942

@ -16,7 +16,19 @@
</div>
<div class="card-body">
<form [formGroup]="firstTimeLoginForm">
<div class="w-100">
<div class="password-wrapper">
<input type="text" id="oldPassword" class="form-control" formControlName="oldPassword" type="{{passwordType}}"
placeholder="{{ 'oldPassword' | translate }}" appNoWhitespaces />
<app-password-hide-show #psh class="password-eye align-items-stretch" [showPassword]="true"
(onEyeClick)="togglePasswordType()"></app-password-hide-show>
</div>
<div class="text-danger"
*ngIf="changePasswordForm.get('oldPassword')?.touched && changePasswordForm.get('oldPassword')?.invalid">
{{ 'fieldRequired' | translate }}
</div>
</div>
<!-- New Password -->
<div class="mb-3">
<label for="newPassword" class="form-label">
@ -107,9 +119,14 @@
<app-password-hide-show #psh class="password-eye align-items-stretch" [showPassword]="true" (onEyeClick)="togglePasswordType()"></app-password-hide-show>
</div>
<div class="text-danger" *ngIf="changePasswordForm.get('oldPassword')?.touched &&
changePasswordForm.get('oldPassword')?.invalid">
{{ 'fieldRequired' | translate }}
<div class="text-danger" *ngIf="changePasswordForm.get('oldPassword')?.touched ">
<div *ngIf="changePasswordForm.get('oldPassword')?.hasError('required')">
{{ 'fieldRequired' | translate }}
</div>
<div *ngIf="changePasswordForm.get('oldPassword')?.hasError('pattern')">
{{ 'passwordPattern' | translate }}
</div>
</div>
</div>
</div>
@ -129,10 +146,21 @@
(onEyeClick)="togglePasswordType1()"></app-password-hide-show>
</div>
<div class="text-danger" *ngIf="changePasswordForm.get('newPassword')?.touched ">
<div *ngIf="changePasswordForm.get('newPassword')?.hasError('required')">
{{ 'fieldRequired' | translate }}
</div>
<div *ngIf="changePasswordForm.get('newPassword')?.hasError('pattern')">
{{ 'passwordPattern' | translate }}
</div>
<div *ngIf="changePasswordForm.hasError('oldAndNewPasswordSame')">
Old password and new password cannot be the same
</div>
</div>
<div class="text-danger" *ngIf="newPasswordError$">
{{ newPasswordError$ | translate }}
</div>
</div>
</div>
</div>
@ -153,8 +181,18 @@
(onEyeClick)="togglePasswordType2()"></app-password-hide-show>
</div>
<div class="text-danger" *ngIf="confirmPasswordError$">
{{ confirmPasswordError$ | translate }}
<div class="text-danger" *ngIf="changePasswordForm.get('confirmPassword')?.touched ">
<div *ngIf="changePasswordForm.get('confirmPassword')?.hasError('required')">
{{ 'fieldRequired' | translate }}
</div>
<div *ngIf="changePasswordForm.get('confirmPassword')?.hasError('pattern')">
{{ 'passwordPattern' | translate }}
</div>
<div *ngIf="changePasswordForm.hasError('passwordMismatch')">
{{ 'passwordsDoNotMatch' | translate }}
</div>
</div>
</div>

@ -43,30 +43,74 @@ constructor(private fb: FormBuilder, private httpURIService: HttpURIService, pri
this.passwordType2 = this.passwordHideShow2?.showPassword ? 'password' : 'text';
}
ngOnInit(): void {
this.checkIfFirstTimeChangePasswordOrNot();
if (this.isFirstLogin) {
this.firstTimeLoginForm = this.fb.group({
oldPassword: ['', Validators.required],
newPassword: ['', [
Validators.required,
Validators.minLength(8),
Validators.maxLength(20),
Validators.pattern(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]+$/)
]],
confirmPassword: ['', [
Validators.required,
Validators.minLength(8),
Validators.maxLength(20),
Validators.pattern(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]+$/)
]]
}, { validators: this.passwordMatchValidator });
}
else {
this.changePasswordForm = this.fb.group({
oldPassword: ['', Validators.required],
newPassword: ['', [
Validators.required,
Validators.minLength(8),
Validators.maxLength(20),
Validators.pattern(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]+$/)
]
],
confirmPassword: ['', [
Validators.required,
Validators.minLength(8),
Validators.maxLength(20),
Validators.pattern(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]+$/)
]
]
}, { validators: [
this.passwordMatchValidator,
this.oldAndNewPasswordNotSame,
]
},
);
}
}
passwordMatchValidator(group: AbstractControl): ValidationErrors | null {
const newPassword = group.get('newPassword')?.value;
const confirmPassword = group.get('confirmPassword')?.value;
return newPassword === confirmPassword ? null : { passwordMismatch: true };
}
ngOnInit(): void {
this.checkIfFirstTimeChangePasswordOrNot();
if (this.isFirstLogin) {
this.firstTimeLoginForm = this.fb.group({
newPassword: ['', [Validators.required, Validators.minLength(6)]],
confirmPassword: ['', [Validators.required, Validators.minLength(6)]]
}, { validators: this.passwordMatchValidator });
} else {
this.changePasswordForm = this.fb.group({
oldPassword: ['', Validators.required],
newPassword: ['', [Validators.required, Validators.minLength(6)]],
confirmPassword: ['', [Validators.required, Validators.minLength(6)]]
}, { validators: this.passwordMatchValidator });
oldAndNewPasswordNotSame(group: AbstractControl): ValidationErrors | null {
const oldPassword = group.get('oldPassword')?.value;
const newPassword = group.get('newPassword')?.value;
if (!oldPassword || !newPassword) {
return null;
}
return oldPassword === newPassword
? { oldAndNewPasswordSame: true }
: null;
}
checkIfFirstTimeChangePasswordOrNot(): void {
try {
const currentUser: any = JSON.parse(
@ -80,37 +124,6 @@ constructor(private fb: FormBuilder, private httpURIService: HttpURIService, pri
}
}
initChangePasswordForm(): void {
this.changePasswordForm = this.fb.group(
{
oldPassword: ['', Validators.required],
enterNewPassword: ['', [Validators.required, Validators.minLength(6)]],
confirmPassword: ['', [Validators.required, Validators.minLength(6)]],
},
{ validators: this.passwordMatchValidator }
);
}
get newPasswordError$() {
const control = this.changePasswordForm.get('newPassword');
if (!control || !control.touched) return null;
if (control.hasError('required')) return 'fieldRequired';
if (control.hasError('minlength')) return 'passwordTooShort';
return null;
}
get confirmPasswordError$() {
const control = this.changePasswordForm.get('confirmPassword');
if (!control || !control.touched) return null;
if (control.hasError('required')) return 'fieldRequired';
if (control.hasError('minlength')) return 'passwordTooShort';
if (this.changePasswordForm.hasError('passwordMismatch')) return 'passwordsDoNotMatch';
return null;
}
getFormPayload() {
const form = this.isFirstLogin ? this.firstTimeLoginForm : this.changePasswordForm;

@ -40,12 +40,8 @@
placeholder="{{ 'userID' | translate }}"
appNoWhitespaces
/>
</div>
<div class="text-danger" *ngIf="resetPasswordForm.get('userId')?.touched &&
resetPasswordForm.get('userId')?.invalid">
{{ 'fieldRequired' | translate }}
</div>
</div>
</div>
</div>
@ -57,23 +53,26 @@
class="mandatory">*</span>
</label>
<div class="w-100">
<div class="password-wrapper">
<input id="enterNewPassword"
formControlName="newPassword"
class="form-control"
autocomplete="new-password"
type="{{passwordType1}}"
maxlength="500"
placeholder="{{ 'enterNewPassword' | translate }}" appNoWhitespaces
rows="3" />
<app-password-hide-show #psh1 class="password-eye align-items-stretch" [showPassword]="true" (onEyeClick)="togglePasswordType1()"></app-password-hide-show>
<div class="password-wrapper">
<input id="enterNewPassword" formControlName="newPassword" class="form-control" autocomplete="new-password"
type="{{passwordType1}}" maxlength="500" placeholder="{{ 'enterNewPassword' | translate }}" appNoWhitespaces
rows="3" />
<app-password-hide-show #psh1 class="password-eye align-items-stretch" [showPassword]="true"
(onEyeClick)="togglePasswordType1()"></app-password-hide-show>
</div>
<div class="text-danger"
*ngIf="resetPasswordForm.get('newPassword')?.touched && resetPasswordForm.get('newPassword')?.invalid">
<div *ngIf="resetPasswordForm.get('newPassword')?.hasError('required')">
{{ 'fieldRequired' | translate }}
</div>
<div
*ngIf="!resetPasswordForm.get('newPassword')?.hasError('required') && resetPasswordForm.get('newPassword')?.hasError('pattern') ">
{{ 'passwordPattern' | translate }}
</div>
</div>
</div>
<div class="text-danger mt-1" *ngIf="newPasswordError">
{{ newPasswordError | translate }}
</div>
</div>
</div>
</div>
</div>
@ -94,8 +93,20 @@
<app-password-hide-show class="password-eye align-items-stretch" #psh2 [showPassword]="true" (onEyeClick)="togglePasswordType2()"></app-password-hide-show>
</div>
<div class="text-danger" *ngIf="confirmPasswordError">
{{ confirmPasswordError | translate }}
<div class="text-danger"
*ngIf="resetPasswordForm.get('confirmPassword')?.touched ">
<div *ngIf="resetPasswordForm.get('confirmPassword')?.hasError('required')">
{{ 'fieldRequired' | translate }}
</div>
<div
*ngIf="resetPasswordForm.get('confirmPassword')?.hasError('pattern')">
{{ 'passwordPattern' | translate }}
</div>
<div *ngIf="resetPasswordForm.hasError('passwordMismatch')">
{{ 'passwordsDoNotMatch' | translate }}
</div>
</div>
</div>
</div>

@ -29,15 +29,27 @@ export class ResetPasswordComponent implements OnInit{
ngOnInit(): void {
const userIdValue = this.storageService.getItem('USER_ID')
this.resetPasswordForm = this.fb.group({
userId: [userIdValue || '', Validators.required],
newPassword: ['', [Validators.required, Validators.minLength(6)]],
confirmPassword: ['', [Validators.required, Validators.minLength(6)]]
userId: [{value: userIdValue || '', disabled: true}],
newPassword: ['', [
Validators.required,
Validators.minLength(8),
Validators.maxLength(20),
Validators.pattern(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]+$/)
]
],
confirmPassword: ['', [
Validators.required,
Validators.minLength(8),
Validators.maxLength(20),
Validators.pattern(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]+$/)
]
]
},
{
validators: this.passwordMatchValidator
}
);
this.resetPasswordForm.get('newPassword')?.valueChanges.subscribe(()=>{
this.resetPasswordForm.get('confirmPassword')?.updateValueAndValidity();
});
@ -50,39 +62,30 @@ export class ResetPasswordComponent implements OnInit{
togglePasswordType2() {
this.passwordType2 = this.passwordHideShow2?.showPassword ? 'password' : 'text';
}
passwordMatchValidator(group: AbstractControl): ValidationErrors | null {
const newPassword = group.get('newPassword')?.value;
const confirmPassword = group.get('confirmPassword')?.value;
return newPassword === confirmPassword ? null : { passwordMismatch: true };
}
get newPasswordError() {
const control = this.resetPasswordForm.get('newPassword');
if (!control || !control.touched) return null;
if (control.hasError('required')) return 'fieldRequired';
if (control.hasError('minlength')) return 'passwordTooShort';
return null;
passwordMatchValidator(group: AbstractControl): ValidationErrors | null {
const newPassword = group.get('newPassword');
const confirmPassword = group.get('confirmPassword');
if (!newPassword || !confirmPassword) return null;
if (confirmPassword.errors && !confirmPassword.errors['passwordMismatch']) {
return null;
}
if (newPassword.value !== confirmPassword.value) {
confirmPassword.setErrors({ passwordMismatch: true });
return { passwordMismatch: true };
} else {
confirmPassword.setErrors(null);
return null;
}
}
get confirmPasswordError() {
const control = this.resetPasswordForm.get('confirmPassword');
if (!control || !control.touched) return null;
if (control.hasError('required')) return 'fieldRequired';
if (control.hasError('minlength')) return 'passwordTooShort';
if (this.resetPasswordForm.hasError('passwordMismatch')) return 'passwordsDoNotMatch';
return null;
}
onSubmit() {
if (this.resetPasswordForm.invalid) return;
const payload = {
userId: this.resetPasswordForm.value.userId,
newPassword: this.resetPasswordForm.value.newPassword,
userId: this.resetPasswordForm.get('userId')?.value,
newPassword: this.resetPasswordForm.get('newPassword')?.value,
porOrgaCode: this.storageService.getItem('POR_ORGACODE')
};
this.httpURIService.requestPOST(URIKey.RESET_PASSWORD_URI, payload)
@ -93,7 +96,7 @@ export class ResetPasswordComponent implements OnInit{
}
}
});
}
}
}

@ -41,8 +41,28 @@
</div>
<div class="text-danger" *ngIf="userForm.get('userId')?.touched && userForm.get('userId')?.invalid">
{{ 'fieldRequired' | translate }}
<div *ngIf="
userForm.get('userId')?.errors?.['required'] &&
!userForm.get('userId')?.value
">
{{ 'fieldRequired' | translate }}
</div>
</div>
<div class="text-danger" *ngIf="
userForm.get('userId')?.errors?.['minlength'] &&
userForm.get('userId')?.value
">
{{'userIdMinLength' | translate }}
</div>
<div class="text-danger" *ngIf="
userForm.get('userId')?.errors?.['pattern'] &&
userForm.get('userId')?.value
">
{{'emptySpaceRestriction' | translate}}
</div >
</div>
</div>
</div>
@ -63,10 +83,27 @@
placeholder="{{ 'userName' | translate }}" appNoWhitespaces
rows="3" />
<div class="text-danger" *ngIf="userForm.get('userFullname')?.touched && userForm.get('userFullname')?.invalid">
{{ 'fieldRequired' | translate }}
<div *ngIf="
userForm.get('userFullname')?.errors?.['required'] &&
!userForm.get('userFullname')?.value
">
{{ 'fieldRequired' | translate }}
</div>
</div>
<div class="text-danger" *ngIf="
userForm.get('userFullname')?.errors?.['minlength'] &&
userForm.get('userFullname')?.value
">
{{'nameMinLength' | translate }}
</div>
<div class="text-danger" *ngIf="
userForm.get('userFullname')?.errors?.['pattern'] &&
userForm.get('userFullname')?.value
">
{{'emptySpaceRestriction' | translate}}
</div >
</div>
</div>
@ -113,7 +150,15 @@
placeholder="{{ 'passwordPlaceHolder' | translate }}" appNoWhitespaces/>
<div class="text-danger" *ngIf="userForm.get('defaultPassword')?.touched && userForm.get('defaultPassword')?.invalid">
{{ 'fieldRequired' | translate }}
<div *ngIf="userForm.get('defaultPassword')?.hasError('required')">
{{ 'fieldRequired' | translate }}
</div>
<div *ngIf="
!userForm.get('defaultPassword')?.hasError('required') &&
userForm.get('defaultPassword')?.hasError('pattern')
">
{{ 'passwordPattern' | translate }}
</div>
</div>
</div>
@ -193,9 +238,10 @@
<table class="table mb-0 border">
<thead class="table-light">
<tr>
<th style="width: 40%">{{'userID' | translate}}</th>
<th style="width: 40%">{{'Name' | translate}}</th>
<th style="width: 20%">{{'action' | translate}}</th>
<th style="width: 30%">{{'userID' | translate}}</th>
<th style="width: 30%">{{'Name' | translate}}</th>
<th style="width: 30%">{{'Role' | translate}}</th>
<th style="width: 10%">{{'action' | translate}}</th>
</tr>
</thead>
<tbody>
@ -204,6 +250,7 @@
<td>{{ item.userId }}</td>
<td>{{ item.userFullname }}</td>
<td>{{item.role}}</td>
<td>

@ -10,8 +10,10 @@ import { ButtonManagementService } from '../../services/button-management.servic
import { StorageService } from '../../shared/services/storage.service';
import { TableFilterPipe } from '../../shared/pipes/table-filter.pipe';
import { URIKey } from '../../utils/uri-enums';
import { HttpParams } from '@angular/common/http';
import { HttpErrorResponse, HttpParams } from '@angular/common/http';
import { HttpURIService } from '../../app.http.uri.service';
import { I18NService } from '../../services/i18n.service';
import { SuccessMessages } from '../../utils/enums';
@ -52,7 +54,8 @@ export class SetupUserComponent implements OnInit {
private fb: FormBuilder,
private buttonManagementService: ButtonManagementService,
private storageService: StorageService,
private httpService: HttpURIService
private httpService: HttpURIService,
private i18nService: I18NService
){}
onSearch(value: string): void {
@ -95,13 +98,14 @@ export class SetupUserComponent implements OnInit {
}
this.httpService.requestPOST<SetupUser[]>(URIKey.CREATE_USER, newUser).subscribe({
next: () => {
this.userForm.reset();
this.mode = 'edit';
this.loadUsersDirect()
},
error: (err: any) => console.error(err)
next: (response) => {
if (!(response instanceof HttpErrorResponse)) {
this.i18nService.success(SuccessMessages.USER_CREATED_SUCCESS, []);
this.userForm.reset();
this.mode = 'edit';
this.loadUsersDirect()
}
}
});
@ -126,9 +130,25 @@ ngOnInit(): void {
this.getButtonPermissions();
this.userForm = this.fb.group({
userId: ['', [Validators.required]],
userFullname: ['', [Validators.required, Validators.maxLength(500)]],
defaultPassword: ['', Validators.required],
userId: ['', [
Validators.required,
Validators.minLength(5),
Validators.pattern(/^\S+$/)
]
],
userFullname: ['', [
Validators.required,
Validators.minLength(5),
Validators.pattern(/^\S+$/)
]
],
defaultPassword: ['', [
Validators.required,
Validators.pattern(
/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,20}$/
)
]
],
email: ['', [Validators.required, Validators.email]],
userRole: [null, Validators.required]
});
@ -182,14 +202,13 @@ ngOnInit(): void {
let params = new HttpParams().set('userId', userId);
this.httpService.requestDELETE<any>(URIKey.DELETE_USER, params).subscribe({
next: (response) =>{
if (!(response instanceof HttpErrorResponse)) {
this.i18nService.success(SuccessMessages.USER_DELETE_SUCCESS, []);
this.loadUsersDirect();
this.userForm.reset()
this.userForm.reset();
this.selectedUserId = null;
},
error: (err) =>{
console.error('Error fetching users:', err);
this.allItems = [];
this.isLoading = false;
}
}
})
}

@ -59,7 +59,10 @@ TRANSACTION_SUCCESSFUL = "TRANSACTION_SUCCESSFUL",
SAVED_SUCCESSFULLY = "SAVED_SUCCESSFULLY",
RECORD_DELETED_SUCCESSFULY = "RECORD_DELETED_SUCCESSFULY",
ACCOUNT_CLOSED_SUCCESSFULLY = "ACCOUNT_CLOSED_SUCCESSFULLY",
SUCCESS_MESSAGE = "SUCCESS_MESSAGE"
SUCCESS_MESSAGE = "SUCCESS_MESSAGE",
USER_CREATED_SUCCESS = "USER_CREATED_SUCCESS",
USER_DELETE_SUCCESS = "USER_DELETE_SUCCESS"
}
export enum MESSAGEKEY {

@ -260,5 +260,11 @@
"ERR_SEC_0005": "المستخدم غير موجود",
"ERR_SEC_0006": "كلمة المرور التي تم إدخالها غير صحيحة",
"toDateGreaterThanToday": "يجب أن يكون التاريخ الحالي أقل من التاريخ الحالي",
"fromDateGreaterThanToday": "يجب أن يكون تاريخ البدء أقل من التاريخ الحالي"
"fromDateGreaterThanToday": "يجب أن يكون تاريخ البدء أقل من التاريخ الحالي",
"userIdMinLength": "يجب أن يكون معرف المستخدم مكوّنًا من 5 أحرف على الأقل",
"nameMinLength": "يجب أن يكون الاسم مكوّنًا من 5 أحرف على الأقل",
"emptySpaceRestriction": "المسافات الفارغة غير مسموح بها",
"USER_CREATED_SUCCESS": "تم إنشاء المستخدم",
"USER_DELETE_SUCCESS": "تم حذف المستخدم"
}

@ -112,7 +112,7 @@
"oldPassword":"Old Password",
"newPasswordStatic":"New Password",
"savePassword":"Save",
"passwordPattern":"Password must be over 8 characters and include an uppercase letter, a lower case letter, a number and a special character",
"passwordPattern":"Password must be 820 characters and include uppercase, lowercase, number, and special character",
"SMSGatewaySelect":"Select Gateway",
"selectIdentValueType": "Select Type",
"IdTypeSelect":"Select Select Type",
@ -260,5 +260,10 @@
"ERR_SEC_0005": "User not found",
"ERR_SEC_0006": "Incorrect password",
"toDateGreaterThanToday": "To Date must be less than Current Date",
"fromDateGreaterThanToday": "From Date must be less than Current Date"
"fromDateGreaterThanToday": "From Date must be less than Current Date",
"userIdMinLength" : "User ID must be at least 5 characters",
"nameMinLength" : "Name must be at least 5 characters",
"emptySpaceRestriction" : "Empty spaces are not allowed",
"USER_CREATED_SUCCESS": "User Created",
"USER_DELETE_SUCCESS": "User Deleted"
}
Loading…
Cancel
Save