Security hardening and edge case fixes across frontend

Security Improvements:
- Add input sanitization utilities (XSS, SQL injection prevention)
- Add token validation with JWT structure verification
- Add secure form validators with pattern enforcement
- Implement proper token storage with encryption support

Service Hardening:
- Add timeout (30s) and retry logic (3 attempts) to all API calls
- Add UUID validation for all ID parameters
- Add null/undefined checks with defensive defaults
- Proper error propagation with typed error handling

Component Fixes:
- Fix memory leaks with takeUntilDestroyed pattern
- Remove mock data fallbacks in error handlers
- Add proper loading/error state management
- Add form field length limits and validation

Files affected: 51 (6000+ lines added for security)
This commit is contained in:
Mahi
2026-02-08 02:10:09 -04:00
parent 80566bf0a2
commit 2c10cd5662
51 changed files with 6094 additions and 656 deletions

View File

@@ -15,6 +15,13 @@ import { Clipboard } from '@angular/cdk/clipboard';
import { DocumentService } from '../services/document.service';
import { NotificationService } from '../../../core/services/notification.service';
import { DocumentType, DocumentResponseDto } from '../../../api/models';
import {
INPUT_LIMITS,
noScriptValidator,
noNullBytesValidator,
normalizeWhitespace,
} from '../../../shared/utils/form-validators';
import { createSubmitDebounce } from '../../../shared/utils/form-utils';
export interface DocumentUploadDialogData {
requestId: string;
@@ -145,7 +152,15 @@ type UploadState = 'idle' | 'uploading' | 'processing' | 'complete' | 'error';
formControlName="description"
rows="2"
placeholder="Add any additional notes about this document"
[maxlength]="limits.DESCRIPTION_MAX"
></textarea>
@if (form.controls.description.hasError('maxlength')) {
<mat-error>Maximum {{ limits.DESCRIPTION_MAX }} characters allowed</mat-error>
}
@if (form.controls.description.hasError('dangerousContent')) {
<mat-error>Invalid characters detected</mat-error>
}
<mat-hint align="end">{{ form.controls.description.value?.length || 0 }}/{{ limits.DESCRIPTION_MAX }}</mat-hint>
</mat-form-field>
<!-- Drop Zone -->
@@ -819,6 +834,12 @@ export class DocumentUploadComponent {
private readonly data: DocumentUploadDialogData = inject(MAT_DIALOG_DATA);
private readonly clipboard = inject(Clipboard);
/** Debounce handler to prevent double-click submissions */
private readonly submitDebounce = createSubmitDebounce(500);
/** Input limits exposed for template binding */
readonly limits = INPUT_LIMITS;
// State signals
readonly uploadState = signal<UploadState>('idle');
readonly selectedFile = signal<File | null>(null);
@@ -846,7 +867,11 @@ export class DocumentUploadComponent {
readonly form = this.fb.nonNullable.group({
docType: ['' as DocumentType, [Validators.required]],
description: [''],
description: ['', [
Validators.maxLength(INPUT_LIMITS.DESCRIPTION_MAX),
noScriptValidator(),
noNullBytesValidator(),
]],
});
canUpload(): boolean {
@@ -981,12 +1006,21 @@ export class DocumentUploadComponent {
}
onUpload(): void {
// Debounce to prevent double-click submissions
this.submitDebounce(() => this.performUpload());
}
private performUpload(): void {
const file = this.selectedFile();
if (!file || this.form.invalid) return;
if (!file || this.form.invalid || this.uploadState() === 'uploading') return;
this.uploadState.set('uploading');
this.uploadProgress.set(0);
const { docType, description } = this.form.getRawValue();
const rawValues = this.form.getRawValue();
// Normalize description
const docType = rawValues.docType;
const description = normalizeWhitespace(rawValues.description);
this.documentService.uploadDocumentWithProgress(this.data.requestId, file, docType, description).subscribe({
next: (progress) => {