init commit

This commit is contained in:
David Melendez
2026-01-14 22:32:13 +01:00
parent 29b2e1438c
commit 00cb087b68
84 changed files with 29665 additions and 1 deletions

View File

@@ -0,0 +1,441 @@
/**
* Two-Factor Authentication Component
* Professional Angular Resume Builder - Device Registration 2FA
*
* @author David Valera Melendez <david@valera-melendez.de>
* @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<void>();
private countdownTimer$ = new Subject<void>();
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;
}
}