/** * Two-Factor Authentication Component * Professional Angular Resume Builder - Device Registration 2FA * * @author David Valera Melendez * @created 2025-08-08 * @location Made in Germany 🇩🇪 */ import { Component, OnInit, OnDestroy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { ReactiveFormsModule, FormBuilder, FormGroup, Validators } from '@angular/forms'; import { Router, ActivatedRoute } from '@angular/router'; import { Subject, timer } from 'rxjs'; import { takeUntil, finalize, tap } from 'rxjs/operators'; // Angular Material Modules 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 { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; import { MatSnackBarModule, MatSnackBar } from '@angular/material/snack-bar'; import { MatDividerModule } from '@angular/material/divider'; import { MatChipsModule } from '@angular/material/chips'; // Services and Models import { AuthService } from '../../../services/auth.service'; import { TwoFactorInitiationResponse } from '../../../models/auth.model'; interface TwoFactorPageData { userId: number; userEmail: string; userName: string; reason?: string; riskScore?: number; flowType?: 'device_registration' | 'code_required'; deviceInfo?: { isTrusted: boolean; deviceName?: string; lastUsed?: Date; }; tempToken?: string | null; timestamp?: number; // For data expiration checking } /** * Two-Factor Authentication Page Component */ @Component({ selector: 'app-two-factor-auth', standalone: true, imports: [ CommonModule, ReactiveFormsModule, MatCardModule, MatFormFieldModule, MatInputModule, MatButtonModule, MatIconModule, MatProgressSpinnerModule, MatSnackBarModule, MatDividerModule, MatChipsModule ], templateUrl: './two-factor-auth.component.html', styleUrls: ['./two-factor-auth.component.css'] }) export class TwoFactorAuthComponent implements OnInit, OnDestroy { // Form Management twoFactorForm!: FormGroup; // State Management isInitiating = false; isVerifying = false; step: 'request' | 'verify' = 'request'; twoFactorResponse: TwoFactorInitiationResponse | null = null; countdown = 0; // Computed Properties get isVerificationCodeValid(): boolean { const codeControl = this.twoFactorForm?.get('verificationCode'); return !!(codeControl && codeControl.valid && codeControl.value && codeControl.value.length === 6); } get canSubmitVerification(): boolean { return this.isVerificationCodeValid && !this.isVerifying && !!this.twoFactorResponse; } // Page Data data: TwoFactorPageData = { userId: 0, userEmail: '', userName: '' }; // Cleanup private destroy$ = new Subject(); private countdownTimer$ = new Subject(); constructor( private formBuilder: FormBuilder, private authService: AuthService, private snackBar: MatSnackBar, private router: Router, private route: ActivatedRoute ) { this.initializeForms(); } ngOnInit(): void { // Get data from sessionStorage (more secure than URL parameters) const storedData = sessionStorage.getItem('twoFactorData'); if (storedData) { try { const parsedData = JSON.parse(storedData); // Check if data is not too old (10 minutes max) const maxAge = 10 * 60 * 1000; // 10 minutes in milliseconds if (Date.now() - parsedData.timestamp > maxAge) { sessionStorage.removeItem('twoFactorData'); this.router.navigate(['/login']); return; } this.data = parsedData; } catch (error) { this.router.navigate(['/login']); return; } } else { // Fallback: try to get from query params (backward compatibility) this.route.queryParams.subscribe(params => { if (params['userId'] || params['email']) { this.data = { userId: +params['userId'] || 0, userEmail: params['email'] || '', userName: params['name'] || '', reason: params['reason'] || 'New device detected', riskScore: +params['riskScore'] || 0, flowType: params['flowType'] || 'device_registration', tempToken: params['tempToken'] || null, timestamp: Date.now() }; } else { // No data found, redirect to login this.snackBar.open('Invalid 2FA session. Please login again.', 'Close', { duration: 5000, panelClass: ['error-snackbar'] }); this.router.navigate(['/login']); return; } }); } // If no valid data, redirect back to login if (!this.data.userId || !this.data.userEmail) { this.snackBar.open('Invalid 2FA session. Please login again.', 'Close', { duration: 5000, panelClass: ['error-snackbar'] }); this.router.navigate(['/login']); } } ngOnDestroy(): void { this.destroy$.next(); this.destroy$.complete(); this.countdownTimer$.next(); this.countdownTimer$.complete(); // Clean up sensitive data when component is destroyed sessionStorage.removeItem('twoFactorData'); } /** * Initialize reactive forms */ private initializeForms(): void { this.twoFactorForm = this.formBuilder.group({ verificationCode: ['', [ Validators.required, Validators.pattern(/^\d{6}$/) ]] }); } /** * Request 2FA code from backend */ requestTwoFactorCode(): void { if (this.isInitiating) return; this.isInitiating = true; this.authService.initiateTwoFactor(this.data.userId) .pipe( takeUntil(this.destroy$), finalize(() => { this.isInitiating = false; }) ) .subscribe({ next: (response) => { this.twoFactorResponse = response; this.step = 'verify'; this.startCountdown(response.durationMinutes * 60); this.snackBar.open(response.message, 'Close', { duration: 5000, panelClass: ['info-snackbar'] }); }, error: (error) => { this.snackBar.open('Failed to request verification code. Please try again.', 'Close', { duration: 5000, panelClass: ['error-snackbar'] }); } }); } /** * Submit verification code */ submitVerificationCode(): void { // Enhanced validation if (!this.canSubmitVerification) { this.twoFactorForm.markAllAsTouched(); // Show specific error message const codeControl = this.twoFactorForm.get('verificationCode'); if (!codeControl?.value) { this.snackBar.open('Please enter the verification code', 'Close', { duration: 3000, panelClass: ['error-snackbar'] }); } else if (codeControl.value.length !== 6) { this.snackBar.open('Verification code must be 6 digits', 'Close', { duration: 3000, panelClass: ['error-snackbar'] }); } return; } this.isVerifying = true; const code = this.twoFactorForm.get('verificationCode')?.value; this.authService.verifyTwoFactor(code, this.twoFactorResponse!.verificationId, this.data.userId, this.data.tempToken || undefined) .pipe( takeUntil(this.destroy$), finalize(() => { this.isVerifying = false; }) ) .subscribe({ next: (response) => { if (response.success) { // Clear sensitive data from sessionStorage on success sessionStorage.removeItem('twoFactorData'); if (this.data.flowType === 'device_registration') { this.handleDeviceRegistrationSuccess(response); } else if (this.data.flowType === 'code_required' && this.data.tempToken) { this.handleLoginCompletionSuccess(response); } } else { this.handleVerificationError(response.message || 'Verification failed'); } }, error: (error) => { this.handleVerificationError(error.error?.message || 'Verification failed. Please try again.'); } }); } /** * Handle successful device registration */ private handleDeviceRegistrationSuccess(response: any): void { this.snackBar.open('Device registered successfully! You can now login.', 'Close', { duration: 5000, panelClass: ['success-snackbar'] }); // Navigate back to login after successful device registration this.router.navigate(['/login'], { queryParams: { twoFactorComplete: 'true', email: this.data.userEmail } }); } /** * Handle successful login completion */ private handleLoginCompletionSuccess(response: any): void { // Check if authentication was completed by backend if (response.authCompleted && response.accessToken) { // Store the access token using the correct key that AuthService expects localStorage.setItem('david_auth_access_token', response.accessToken); // Store user data if available using the correct key if (response.user) { const userData = { id: response.user.id, email: response.user.email, firstName: response.user.firstName || '', lastName: response.user.lastName || '' }; localStorage.setItem('david_auth_user_data', JSON.stringify(userData)); } this.snackBar.open('Authentication successful! Redirecting to dashboard...', 'Close', { duration: 3000, panelClass: ['success-snackbar'] }); this.authService.refreshAuthState(); // Navigate to dashboard setTimeout(() => { this.router.navigate(['/dashboard']); }, 1000); } else { // Authentication not completed - redirect to login this.snackBar.open('2FA verification successful. Please complete login.', 'Close', { duration: 3000, panelClass: ['success-snackbar'] }); this.router.navigate(['/login'], { queryParams: { twoFactorVerified: 'true', email: this.data.userEmail } }); } } /** * Handle verification error */ private handleVerificationError(message: string): void { this.snackBar.open(message, 'Close', { duration: 4000, panelClass: ['error-snackbar'] }); // Clear the form to let user try again this.twoFactorForm.get('verificationCode')?.setValue(''); this.twoFactorForm.get('verificationCode')?.markAsUntouched(); } /** * Auto-fill demo code if available */ fillDemoCode(): void { if (this.twoFactorResponse?.verificationCode) { this.twoFactorForm.patchValue({ verificationCode: this.twoFactorResponse.verificationCode }); this.snackBar.open('Demo verification code filled', 'Close', { duration: 2000, panelClass: ['info-snackbar'] }); } } /** * Start countdown timer for code expiration */ private startCountdown(seconds: number): void { this.countdown = seconds; timer(0, 1000) .pipe( takeUntil(this.countdownTimer$), takeUntil(this.destroy$), tap(() => { this.countdown--; if (this.countdown <= 0) { this.countdownTimer$.next(); } }) ) .subscribe(); } /** * Format countdown time as MM:SS */ getFormattedCountdown(): string { const minutes = Math.floor(this.countdown / 60); const seconds = this.countdown % 60; return `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`; } /** * Navigate back to login */ navigateToLogin(): void { this.router.navigate(['/login']); } /** * Get field error message */ getFieldError(fieldName: string): string { const field = this.twoFactorForm.get(fieldName); if (!field || !field.errors || !field.touched) return ''; const errors = field.errors; if (errors['required']) return 'Verification code is required'; if (errors['pattern']) return 'Please enter a 6-digit code'; return 'Invalid input'; } /** * Check if field has error */ hasFieldError(fieldName: string): boolean { const field = this.twoFactorForm.get(fieldName); return !!(field && field.invalid && field.touched); } /** * Check if demo mode is enabled */ isDemoMode(): boolean { return this.twoFactorResponse?.environmentInfo.demoModeEnabled || false; } }