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>
243 lines
9.6 KiB
TypeScript
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 [];
|
|
}
|
|
}
|
|
} |