AI Implementation feature(857): Authenticated Shell: Header, Quick Tabs and Change Password (#14)
This commit was merged in pull request #14.
This commit is contained in:
@@ -0,0 +1,163 @@
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
computed,
|
||||
HostListener,
|
||||
inject,
|
||||
input,
|
||||
output,
|
||||
signal,
|
||||
} from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms';
|
||||
import {
|
||||
passwordMatchValidator,
|
||||
PasswordPolicy,
|
||||
} from './change-password-modal.validators';
|
||||
|
||||
export type ChangePasswordMode = 'self' | 'admin-reset';
|
||||
|
||||
export interface ChangePasswordSubmitPayload {
|
||||
oldPassword: string;
|
||||
newPassword: string;
|
||||
confirmNewPassword: string;
|
||||
}
|
||||
|
||||
const DEFAULT_POLICY: PasswordPolicy = {
|
||||
minLength: 12,
|
||||
requireMixed: true,
|
||||
description: 'At least 12 characters with upper, lower, digit and symbol',
|
||||
};
|
||||
|
||||
@Component({
|
||||
selector: 'app-change-password-modal',
|
||||
standalone: true,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [CommonModule, ReactiveFormsModule],
|
||||
template: `
|
||||
@if (open()) {
|
||||
<div class="modal-overlay" data-testid="change-password-overlay" (click)="onCancel()">
|
||||
<div
|
||||
class="modal-card"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby="cp-title"
|
||||
data-testid="change-password-modal"
|
||||
(click)="$event.stopPropagation()"
|
||||
>
|
||||
<h2 id="cp-title">Change password</h2>
|
||||
|
||||
<form [formGroup]="form" (ngSubmit)="onSubmit()">
|
||||
@if (mode() === 'self') {
|
||||
<label class="field">
|
||||
<span>Old password</span>
|
||||
<input
|
||||
type="password"
|
||||
autocomplete="current-password"
|
||||
formControlName="oldPassword"
|
||||
data-testid="cp-old"
|
||||
/>
|
||||
</label>
|
||||
}
|
||||
|
||||
<label class="field">
|
||||
<span>New password</span>
|
||||
<input
|
||||
type="password"
|
||||
autocomplete="new-password"
|
||||
formControlName="newPassword"
|
||||
data-testid="cp-new"
|
||||
/>
|
||||
</label>
|
||||
|
||||
<label class="field">
|
||||
<span>Confirm new password</span>
|
||||
<input
|
||||
type="password"
|
||||
autocomplete="new-password"
|
||||
formControlName="confirmNewPassword"
|
||||
data-testid="cp-confirm"
|
||||
/>
|
||||
</label>
|
||||
|
||||
@if (errorMessage()) {
|
||||
<p class="error" data-testid="cp-error">{{ errorMessage() }}</p>
|
||||
}
|
||||
@if (formError()) {
|
||||
<p class="error" data-testid="cp-form-error">{{ formError() }}</p>
|
||||
}
|
||||
<p class="hint" data-testid="cp-policy">{{ policy().description }}</p>
|
||||
|
||||
<div class="actions">
|
||||
<button
|
||||
type="button"
|
||||
data-testid="cp-cancel"
|
||||
(click)="onCancel()"
|
||||
[disabled]="submitting()"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
data-testid="cp-ok"
|
||||
[disabled]="submitting()"
|
||||
>
|
||||
OK
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
`,
|
||||
})
|
||||
export class ChangePasswordModalComponent {
|
||||
private readonly fb = inject(FormBuilder);
|
||||
|
||||
readonly open = input<boolean>(false);
|
||||
readonly mode = input<ChangePasswordMode>('self');
|
||||
readonly policy = input<PasswordPolicy>(DEFAULT_POLICY);
|
||||
readonly errorMessage = input<string | null>(null);
|
||||
readonly submitting = input<boolean>(false);
|
||||
|
||||
readonly cancel = output<void>();
|
||||
readonly submit = output<ChangePasswordSubmitPayload>();
|
||||
|
||||
readonly form = this.fb.nonNullable.group(
|
||||
{
|
||||
oldPassword: this.fb.nonNullable.control(''),
|
||||
newPassword: this.fb.nonNullable.control('', [Validators.required]),
|
||||
confirmNewPassword: this.fb.nonNullable.control('', [Validators.required]),
|
||||
},
|
||||
{ validators: passwordMatchValidator },
|
||||
);
|
||||
|
||||
readonly formError = computed<string | null>(() => {
|
||||
const errs = this.form.errors;
|
||||
if (errs?.['passwordMismatch']) return 'Passwords do not match';
|
||||
return null;
|
||||
});
|
||||
|
||||
@HostListener('document:keydown.escape')
|
||||
onEscape(): void {
|
||||
if (this.open()) this.onCancel();
|
||||
}
|
||||
|
||||
onCancel(): void {
|
||||
this.form.reset({ oldPassword: '', newPassword: '', confirmNewPassword: '' });
|
||||
this.cancel.emit();
|
||||
}
|
||||
|
||||
onSubmit(): void {
|
||||
if (this.submitting()) return;
|
||||
const v = this.form.getRawValue();
|
||||
if (this.mode() === 'self' && !v.oldPassword) return;
|
||||
if (!v.newPassword || !v.confirmNewPassword) return;
|
||||
if (this.form.invalid) return;
|
||||
this.submit.emit({
|
||||
oldPassword: v.oldPassword,
|
||||
newPassword: v.newPassword,
|
||||
confirmNewPassword: v.confirmNewPassword,
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user