Files
quiz-frontend/src/app/features/auth/register/register.ts
2025-12-27 22:00:37 +02:00

265 lines
7.3 KiB
TypeScript

import { Component, inject, signal, computed, OnDestroy } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormBuilder, FormGroup, Validators, ReactiveFormsModule, AbstractControl, ValidationErrors } from '@angular/forms';
import { Router, RouterModule } from '@angular/router';
import { MatCardModule } from '@angular/material/card';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatInputModule } from '@angular/material/input';
import { MatButtonModule } from '@angular/material/button';
import { MatIconModule } from '@angular/material/icon';
import { MatProgressBarModule } from '@angular/material/progress-bar';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { AuthService } from '../../../core/services/auth.service';
import { StorageService } from '../../../core/services/storage.service';
import { Subject, takeUntil } from 'rxjs';
@Component({
selector: 'app-register',
imports: [
CommonModule,
ReactiveFormsModule,
RouterModule,
MatCardModule,
MatFormFieldModule,
MatInputModule,
MatButtonModule,
MatIconModule,
MatProgressBarModule,
MatProgressSpinnerModule
],
templateUrl: './register.html',
styleUrl: './register.scss'
})
export class RegisterComponent implements OnDestroy {
private fb = inject(FormBuilder);
private authService = inject(AuthService);
private storageService = inject(StorageService);
private router = inject(Router);
private destroy$ = new Subject<void>();
// Signals
isSubmitting = signal<boolean>(false);
hidePassword = signal<boolean>(true);
hideConfirmPassword = signal<boolean>(true);
// Form
registerForm: FormGroup;
// Password strength computed signal
passwordStrength = computed(() => {
const password = this.registerForm?.get('password')?.value || '';
return this.calculatePasswordStrength(password);
});
constructor() {
// Check if converting from guest
const guestToken = this.storageService.getGuestToken();
// Initialize form
this.registerForm = this.fb.group({
username: ['', [
Validators.required,
Validators.minLength(3),
Validators.maxLength(30),
Validators.pattern(/^[a-zA-Z0-9_]+$/)
]],
email: ['', [
Validators.required,
Validators.email
]],
password: ['', [
Validators.required,
Validators.minLength(8),
this.passwordStrengthValidator
]],
confirmPassword: ['', [Validators.required]]
}, { validators: this.passwordMatchValidator });
// Redirect if already authenticated
if (this.authService.isAuthenticated()) {
this.router.navigate(['/dashboard']);
}
}
/**
* Password strength validator
*/
private passwordStrengthValidator(control: AbstractControl): ValidationErrors | null {
const password = control.value;
if (!password) {
return null;
}
const hasUpperCase = /[A-Z]/.test(password);
const hasLowerCase = /[a-z]/.test(password);
const hasNumber = /[0-9]/.test(password);
const hasSpecialChar = /[!@#$%^&*(),.?":{}|<>]/.test(password);
const isValid = hasUpperCase && hasLowerCase && hasNumber && hasSpecialChar;
return isValid ? null : { weakPassword: true };
}
/**
* Password match validator
*/
private passwordMatchValidator(group: AbstractControl): ValidationErrors | null {
const password = group.get('password')?.value;
const confirmPassword = group.get('confirmPassword')?.value;
return password === confirmPassword ? null : { passwordMismatch: true };
}
/**
* Calculate password strength
*/
private calculatePasswordStrength(password: string): {
score: number;
label: string;
color: string;
} {
if (!password) {
return { score: 0, label: '', color: '' };
}
let score = 0;
// Length
if (password.length >= 8) score += 25;
if (password.length >= 12) score += 25;
// Character types
if (/[a-z]/.test(password)) score += 15;
if (/[A-Z]/.test(password)) score += 15;
if (/[0-9]/.test(password)) score += 10;
if (/[!@#$%^&*(),.?":{}|<>]/.test(password)) score += 10;
let label = '';
let color = '';
if (score < 40) {
label = 'Weak';
color = 'warn';
} else if (score < 70) {
label = 'Fair';
color = 'accent';
} else if (score < 90) {
label = 'Good';
color = 'primary';
} else {
label = 'Strong';
color = 'primary';
}
return { score, label, color };
}
/**
* Toggle password visibility
*/
togglePasswordVisibility(): void {
this.hidePassword.update(val => !val);
}
/**
* Toggle confirm password visibility
*/
toggleConfirmPasswordVisibility(): void {
this.hideConfirmPassword.update(val => !val);
}
/**
* Submit registration form
*/
onSubmit(): void {
if (this.registerForm.invalid || this.isSubmitting()) {
this.registerForm.markAllAsTouched();
return;
}
this.isSubmitting.set(true);
const { username, email, password } = this.registerForm.value;
const guestSessionId = this.storageService.getGuestToken() || undefined;
this.authService.register(username, email, password, guestSessionId)
.pipe(takeUntil(this.destroy$))
.subscribe({
next: () => {
this.isSubmitting.set(false);
// Navigation handled by service
},
error: () => {
this.isSubmitting.set(false);
}
});
}
/**
* Get form control error message
*/
getErrorMessage(controlName: string): string {
const control = this.registerForm.get(controlName);
if (!control || !control.touched) {
return '';
}
if (control.hasError('required')) {
return `${this.getFieldLabel(controlName)} is required`;
}
if (control.hasError('email')) {
return 'Please enter a valid email address';
}
if (control.hasError('minlength')) {
const minLength = control.getError('minlength').requiredLength;
return `Must be at least ${minLength} characters`;
}
if (control.hasError('maxlength')) {
const maxLength = control.getError('maxlength').requiredLength;
return `Must not exceed ${maxLength} characters`;
}
if (control.hasError('pattern') && controlName === 'username') {
return 'Username can only contain letters, numbers, and underscores';
}
if (control.hasError('weakPassword')) {
return 'Password must include uppercase, lowercase, number, and special character';
}
return '';
}
/**
* Get field label
*/
private getFieldLabel(controlName: string): string {
const labels: { [key: string]: string } = {
username: 'Username',
email: 'Email',
password: 'Password',
confirmPassword: 'Confirm Password'
};
return labels[controlName] || controlName;
}
/**
* Check if form has password mismatch error
*/
hasPasswordMismatch(): boolean {
const confirmControl = this.registerForm.get('confirmPassword');
return !!confirmControl?.touched && this.registerForm.hasError('passwordMismatch');
}
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
}
}