navidocs/server/services/file-safety.js
ggq-admin 155a8c0305 feat: NaviDocs MVP - Complete codebase extraction from lilian1
## 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>
2025-10-19 01:55:44 +02:00

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
};