talk2me/static/js/src/translationCache.ts
Adolfo Delorenzo aedface2a9 Add comprehensive input validation and sanitization
Frontend Validation:
- Created Validator class with comprehensive validation methods
- HTML sanitization to prevent XSS attacks
- Text sanitization removing dangerous characters
- Language code validation against allowed list
- Audio file validation (size, type, extension)
- URL validation preventing injection attacks
- API key format validation
- Request size validation
- Filename sanitization
- Settings validation with type checking
- Cache key sanitization
- Client-side rate limiting tracking

Backend Validation:
- Created validators.py module for server-side validation
- Audio file validation with size and type checks
- Text sanitization with length limits
- Language code validation
- URL and API key validation
- JSON request size validation
- Rate limiting per endpoint (30 req/min)
- Added validation to all API endpoints
- Error boundary decorators on all routes
- CSRF token support ready

Security Features:
- Prevents XSS through HTML escaping
- Prevents SQL injection through input sanitization
- Prevents directory traversal in filenames
- Prevents oversized requests (DoS protection)
- Rate limiting prevents abuse
- Type checking prevents type confusion attacks
- Length limits prevent memory exhaustion
- Character filtering prevents control character injection

All user inputs are now validated and sanitized before processing.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-06-02 22:58:17 -06:00

243 lines
9.6 KiB
TypeScript

// Translation cache management for offline support
import { TranslationCacheEntry, CacheStats } from './types';
import { Validator } from './validator';
export class TranslationCache {
private static DB_NAME = 'VoiceTranslatorDB';
private static DB_VERSION = 2; // Increment version for cache store
private static CACHE_STORE = 'translationCache';
// private static MAX_CACHE_SIZE = 50 * 1024 * 1024; // 50MB limit - Reserved for future use
private static MAX_ENTRIES = 1000; // Maximum number of cached translations
private static CACHE_EXPIRY_DAYS = 30; // Expire entries after 30 days
// Generate cache key from input parameters
static generateCacheKey(text: string, sourceLang: string, targetLang: string): string {
// Normalize and sanitize text to create a consistent key
const normalizedText = text.trim().toLowerCase();
const sanitized = Validator.sanitizeCacheKey(normalizedText);
return `${sourceLang}:${targetLang}:${sanitized}`;
}
// Open or create the cache database
static async openDB(): Promise<IDBDatabase> {
return new Promise((resolve, reject) => {
const request = indexedDB.open(this.DB_NAME, this.DB_VERSION);
request.onupgradeneeded = (event: IDBVersionChangeEvent) => {
const db = (event.target as IDBOpenDBRequest).result;
// Create cache store if it doesn't exist
if (!db.objectStoreNames.contains(this.CACHE_STORE)) {
const store = db.createObjectStore(this.CACHE_STORE, { keyPath: 'key' });
store.createIndex('timestamp', 'timestamp', { unique: false });
store.createIndex('lastAccessed', 'lastAccessed', { unique: false });
store.createIndex('sourceLanguage', 'sourceLanguage', { unique: false });
store.createIndex('targetLanguage', 'targetLanguage', { unique: false });
}
};
request.onsuccess = (event: Event) => {
resolve((event.target as IDBOpenDBRequest).result);
};
request.onerror = () => {
reject('Failed to open translation cache database');
};
});
}
// Get cached translation
static async getCachedTranslation(
text: string,
sourceLang: string,
targetLang: string
): Promise<string | null> {
try {
const db = await this.openDB();
const transaction = db.transaction([this.CACHE_STORE], 'readwrite');
const store = transaction.objectStore(this.CACHE_STORE);
const key = this.generateCacheKey(text, sourceLang, targetLang);
const request = store.get(key);
return new Promise((resolve) => {
request.onsuccess = (event: Event) => {
const entry = (event.target as IDBRequest).result as TranslationCacheEntry;
if (entry) {
// Check if entry is not expired
const expiryTime = entry.timestamp + (this.CACHE_EXPIRY_DAYS * 24 * 60 * 60 * 1000);
if (Date.now() < expiryTime) {
// Update access count and last accessed time
entry.accessCount++;
entry.lastAccessed = Date.now();
store.put(entry);
console.log(`Cache hit for translation: ${sourceLang} -> ${targetLang}`);
resolve(entry.targetText);
} else {
// Entry expired, delete it
store.delete(key);
resolve(null);
}
} else {
resolve(null);
}
};
request.onerror = () => {
console.error('Failed to get cached translation');
resolve(null);
};
});
} catch (error) {
console.error('Cache lookup error:', error);
return null;
}
}
// Save translation to cache
static async cacheTranslation(
sourceText: string,
sourceLang: string,
targetText: string,
targetLang: string
): Promise<void> {
try {
const db = await this.openDB();
const transaction = db.transaction([this.CACHE_STORE], 'readwrite');
const store = transaction.objectStore(this.CACHE_STORE);
const key = this.generateCacheKey(sourceText, sourceLang, targetLang);
const entry: TranslationCacheEntry = {
key,
sourceText,
sourceLanguage: sourceLang,
targetText,
targetLanguage: targetLang,
timestamp: Date.now(),
accessCount: 1,
lastAccessed: Date.now()
};
// Check cache size before adding
await this.ensureCacheSize(db);
store.put(entry);
console.log(`Cached translation: ${sourceLang} -> ${targetLang}`);
} catch (error) {
console.error('Failed to cache translation:', error);
}
}
// Ensure cache doesn't exceed size limits
static async ensureCacheSize(db: IDBDatabase): Promise<void> {
const transaction = db.transaction([this.CACHE_STORE], 'readwrite');
const store = transaction.objectStore(this.CACHE_STORE);
// Count entries
const countRequest = store.count();
countRequest.onsuccess = async () => {
const count = countRequest.result;
if (count >= this.MAX_ENTRIES) {
// Delete least recently accessed entries
const index = store.index('lastAccessed');
const cursor = index.openCursor();
let deleted = 0;
const toDelete = Math.floor(count * 0.2); // Delete 20% of entries
cursor.onsuccess = (event: Event) => {
const cursor = (event.target as IDBRequest).result;
if (cursor && deleted < toDelete) {
cursor.delete();
deleted++;
cursor.continue();
}
};
}
};
}
// Get cache statistics
static async getCacheStats(): Promise<CacheStats> {
try {
const db = await this.openDB();
const transaction = db.transaction([this.CACHE_STORE], 'readonly');
const store = transaction.objectStore(this.CACHE_STORE);
return new Promise((resolve) => {
const stats: CacheStats = {
totalEntries: 0,
totalSize: 0,
oldestEntry: Date.now(),
newestEntry: 0
};
const countRequest = store.count();
countRequest.onsuccess = () => {
stats.totalEntries = countRequest.result;
};
const cursor = store.openCursor();
cursor.onsuccess = (event: Event) => {
const cursor = (event.target as IDBRequest).result;
if (cursor) {
const entry = cursor.value as TranslationCacheEntry;
// Estimate size (rough calculation)
stats.totalSize += (entry.sourceText.length + entry.targetText.length) * 2;
stats.oldestEntry = Math.min(stats.oldestEntry, entry.timestamp);
stats.newestEntry = Math.max(stats.newestEntry, entry.timestamp);
cursor.continue();
} else {
resolve(stats);
}
};
});
} catch (error) {
console.error('Failed to get cache stats:', error);
return {
totalEntries: 0,
totalSize: 0,
oldestEntry: 0,
newestEntry: 0
};
}
}
// Clear all cache
static async clearCache(): Promise<void> {
try {
const db = await this.openDB();
const transaction = db.transaction([this.CACHE_STORE], 'readwrite');
const store = transaction.objectStore(this.CACHE_STORE);
store.clear();
console.log('Translation cache cleared');
} catch (error) {
console.error('Failed to clear cache:', error);
}
}
// Export cache for backup
static async exportCache(): Promise<TranslationCacheEntry[]> {
try {
const db = await this.openDB();
const transaction = db.transaction([this.CACHE_STORE], 'readonly');
const store = transaction.objectStore(this.CACHE_STORE);
const request = store.getAll();
return new Promise((resolve) => {
request.onsuccess = () => {
resolve(request.result);
};
request.onerror = () => {
resolve([]);
};
});
} catch (error) {
console.error('Failed to export cache:', error);
return [];
}
}
}