## Backend (server/) - Express 5 API with security middleware (helmet, rate limiting) - SQLite database with WAL mode (schema from docs/architecture/) - Meilisearch integration with tenant tokens - BullMQ + Redis background job queue - OCR pipeline with Tesseract.js - File safety validation (extension, MIME, size) - 4 API route modules: upload, jobs, search, documents ## Frontend (client/) - Vue 3 with Composition API (<script setup>) - Vite 5 build system with HMR - Tailwind CSS (Meilisearch-inspired design) - UploadModal with drag-and-drop - FigureZoom component (ported from lilian1) - Meilisearch search integration with tenant tokens - Job polling composable - Clean SVG icons (no emojis) ## Code Extraction - ✅ manuals.js → UploadModal.vue, useJobPolling.js - ✅ figure-zoom.js → FigureZoom.vue - ✅ service-worker.js → client/public/service-worker.js (TODO) - ✅ glossary.json → Merged into Meilisearch synonyms - ❌ Discarded: quiz.js, persona.js, gamification.js (Frank-AI junk) ## Documentation - Complete extraction plan in docs/analysis/ - README with quick start guide - Architecture summary in docs/architecture/ ## Build Status - Server dependencies: ✅ Installed (234 packages) - Client dependencies: ✅ Installed (160 packages) - Client build: ✅ Successful (2.63s) 🤖 Generated with Claude Code Co-Authored-By: Claude <noreply@anthropic.com>
103 lines
2.6 KiB
JavaScript
103 lines
2.6 KiB
JavaScript
/**
|
|
* File Safety Validation Service
|
|
* Validates uploaded files for security and format compliance
|
|
*/
|
|
|
|
import { fileTypeFromBuffer } from 'file-type';
|
|
import path from 'path';
|
|
|
|
const MAX_FILE_SIZE = parseInt(process.env.MAX_FILE_SIZE || '52428800'); // 50MB default
|
|
const ALLOWED_EXTENSIONS = ['.pdf'];
|
|
const ALLOWED_MIME_TYPES = ['application/pdf'];
|
|
|
|
/**
|
|
* Validate file safety and format
|
|
* @param {Object} file - Multer file object
|
|
* @param {Buffer} file.buffer - File buffer for MIME type detection
|
|
* @param {string} file.originalname - Original filename
|
|
* @param {number} file.size - File size in bytes
|
|
* @returns {Promise<{valid: boolean, error?: string}>}
|
|
*/
|
|
export async function validateFile(file) {
|
|
// Check file exists
|
|
if (!file) {
|
|
return { valid: false, error: 'No file provided' };
|
|
}
|
|
|
|
// Check file size
|
|
if (file.size > MAX_FILE_SIZE) {
|
|
return {
|
|
valid: false,
|
|
error: `File size exceeds maximum allowed size of ${MAX_FILE_SIZE / 1024 / 1024}MB`
|
|
};
|
|
}
|
|
|
|
// Check file extension
|
|
const ext = path.extname(file.originalname).toLowerCase();
|
|
if (!ALLOWED_EXTENSIONS.includes(ext)) {
|
|
return {
|
|
valid: false,
|
|
error: `File extension ${ext} not allowed. Only PDF files are accepted.`
|
|
};
|
|
}
|
|
|
|
// Check MIME type via file-type (magic number detection)
|
|
try {
|
|
const detectedType = await fileTypeFromBuffer(file.buffer);
|
|
|
|
// PDF files should be detected
|
|
if (!detectedType || !ALLOWED_MIME_TYPES.includes(detectedType.mime)) {
|
|
return {
|
|
valid: false,
|
|
error: 'File is not a valid PDF document (MIME type mismatch)'
|
|
};
|
|
}
|
|
} catch (error) {
|
|
return {
|
|
valid: false,
|
|
error: 'Unable to verify file type'
|
|
};
|
|
}
|
|
|
|
// Check for null bytes (potential attack vector)
|
|
if (file.originalname.includes('\0')) {
|
|
return {
|
|
valid: false,
|
|
error: 'Invalid filename'
|
|
};
|
|
}
|
|
|
|
// All checks passed
|
|
return { valid: true };
|
|
}
|
|
|
|
/**
|
|
* Sanitize filename for safe storage
|
|
* @param {string} filename - Original filename
|
|
* @returns {string} Sanitized filename
|
|
*/
|
|
export function sanitizeFilename(filename) {
|
|
// Remove path separators and null bytes
|
|
let sanitized = filename
|
|
.replace(/[\/\\]/g, '_')
|
|
.replace(/\0/g, '');
|
|
|
|
// Remove potentially dangerous characters
|
|
sanitized = sanitized.replace(/[^a-zA-Z0-9._-]/g, '_');
|
|
|
|
// Limit length
|
|
const ext = path.extname(sanitized);
|
|
const name = path.basename(sanitized, ext);
|
|
const maxNameLength = 200;
|
|
|
|
if (name.length > maxNameLength) {
|
|
sanitized = name.substring(0, maxNameLength) + ext;
|
|
}
|
|
|
|
return sanitized;
|
|
}
|
|
|
|
export default {
|
|
validateFile,
|
|
sanitizeFilename
|
|
};
|