const express = require('express'); const multer = require('multer'); const cors = require('cors'); const path = require('path'); const fs = require('fs').promises; const { v4: uuidv4 } = require('uuid'); const { validationResult } = require('express-validator'); // Import custom modules const config = require('./config/config'); const logger = require('./config/logger'); const { apiLimiter, uploadLimiter, securityHeaders, fileValidation, fileIdValidation, searchValidation } = require('./config/security'); const STLDatabase = require('./database'); const app = express(); // Security middleware app.use(securityHeaders); app.use(cors({ origin: config.nodeEnv === 'production' ? false : true, credentials: true })); // Rate limiting app.use('/api/', apiLimiter); app.use('/api/upload', uploadLimiter); // Body parsing app.use(express.json({ limit: '10mb' })); app.use(express.urlencoded({ extended: true, limit: '10mb' })); // Static files app.use(express.static('public')); const db = new STLDatabase(config.database.path, config.database.uploadDir); // Enhanced file validation const validateSTLFile = async (buffer) => { // Check if it's a valid STL file by looking for STL signatures const text = buffer.toString('utf8', 0, 80); const isBinarySTL = buffer.length > 80 && buffer.readUInt32LE(80) > 0; const isAsciiSTL = text.toLowerCase().includes('solid'); return isBinarySTL || isAsciiSTL; }; const storage = multer.diskStorage({ destination: async (req, file, cb) => { const uploadPath = path.join(__dirname, config.upload.destination); try { await fs.access(uploadPath); } catch { await fs.mkdir(uploadPath, { recursive: true }); } cb(null, uploadPath); }, filename: (req, file, cb) => { // Sanitize filename const sanitizedName = file.originalname.replace(/[^a-zA-Z0-9.-]/g, '_'); const uniqueName = `${uuidv4()}-${sanitizedName}`; cb(null, uniqueName); } }); const fileFilter = async (req, file, cb) => { const fileExt = path.extname(file.originalname).toLowerCase(); if (!config.upload.allowedExtensions.includes(fileExt)) { return cb(new Error('Only STL files are allowed'), false); } // Additional MIME type check const allowedMimeTypes = ['application/octet-stream', 'application/sla', 'text/plain']; if (file.mimetype && !allowedMimeTypes.includes(file.mimetype)) { return cb(new Error('Invalid file type'), false); } cb(null, true); }; const upload = multer({ storage: storage, fileFilter: fileFilter, limits: { fileSize: config.upload.maxFileSize, files: config.upload.maxFiles } }); // Validation helper const handleValidationErrors = (req, res, next) => { const errors = validationResult(req); if (!errors.isEmpty()) { return res.status(400).json({ success: false, error: 'Validation failed', details: errors.array() }); } next(); }; app.post('/api/upload', fileValidation, handleValidationErrors, upload.single('stlFile'), async (req, res) => { try { if (!req.file) { return res.status(400).json({ success: false, error: 'No file uploaded' }); } // Additional file content validation const fileBuffer = await fs.readFile(req.file.path); if (!await validateSTLFile(fileBuffer)) { await fs.unlink(req.file.path); return res.status(400).json({ success: false, error: 'Invalid STL file format' }); } const fileData = { filename: req.file.filename, originalName: req.file.originalname, filePath: req.file.path, fileSize: req.file.size, description: req.body.description || null, tags: req.body.tags || null, printSettings: req.body.printSettings ? JSON.parse(req.body.printSettings) : null, dimensions: req.body.dimensions ? JSON.parse(req.body.dimensions) : null }; const fileId = await db.addSTLFile(fileData); const savedFile = await db.getSTLFile(fileId); logger.info('File uploaded successfully', { fileId, filename: req.file.originalname, size: req.file.size, ip: req.ip }); res.json({ success: true, message: 'File uploaded successfully', file: savedFile }); } catch (error) { logger.error('Upload error', { error: error.message, stack: error.stack }); if (req.file) { try { await fs.unlink(req.file.path); } catch (unlinkError) { logger.error('Error cleaning up file', { error: unlinkError.message }); } } res.status(500).json({ success: false, error: 'Upload failed', message: config.nodeEnv === 'development' ? error.message : 'Internal server error' }); } }); app.get('/api/files', searchValidation, handleValidationErrors, async (req, res) => { try { const limit = Math.min(parseInt(req.query.limit) || 50, 100); const offset = Math.max(parseInt(req.query.offset) || 0, 0); const search = req.query.search; let files; if (search) { files = await db.searchSTLFiles(search); } else { files = await db.getAllSTLFiles(limit, offset); } res.json({ success: true, files: files, pagination: { limit: limit, offset: offset, hasMore: files.length === limit } }); } catch (error) { logger.error('Get files error', { error: error.message, stack: error.stack }); res.status(500).json({ success: false, error: 'Failed to retrieve files', message: config.nodeEnv === 'development' ? error.message : 'Internal server error' }); } }); app.get('/api/files/:id', fileIdValidation, handleValidationErrors, async (req, res) => { try { const fileId = parseInt(req.params.id); const file = await db.getSTLFile(fileId); if (!file) { return res.status(404).json({ success: false, error: 'File not found' }); } res.json({ success: true, file: file }); } catch (error) { logger.error('Get file error', { error: error.message, fileId: req.params.id }); res.status(500).json({ success: false, error: 'Failed to retrieve file', message: config.nodeEnv === 'development' ? error.message : 'Internal server error' }); } }); app.put('/api/files/:id', async (req, res) => { try { const fileId = parseInt(req.params.id); const updates = {}; if (req.body.description !== undefined) updates.description = req.body.description; if (req.body.tags !== undefined) updates.tags = req.body.tags; if (req.body.printSettings !== undefined) updates.print_settings = req.body.printSettings; if (req.body.dimensions !== undefined) updates.dimensions = req.body.dimensions; const updated = await db.updateSTLFile(fileId, updates); if (!updated) { return res.status(404).json({ error: 'File not found' }); } const updatedFile = await db.getSTLFile(fileId); res.json({ success: true, message: 'File updated successfully', file: updatedFile }); } catch (error) { console.error('Update file error:', error); res.status(500).json({ error: 'Failed to update file', message: error.message }); } }); app.delete('/api/files/:id', async (req, res) => { try { const fileId = parseInt(req.params.id); const deleted = await db.deleteSTLFile(fileId); if (!deleted) { return res.status(404).json({ error: 'File not found' }); } res.json({ success: true, message: 'File deleted successfully' }); } catch (error) { console.error('Delete file error:', error); res.status(500).json({ error: 'Failed to delete file', message: error.message }); } }); app.get('/api/files/:id/download', async (req, res) => { try { const fileId = parseInt(req.params.id); const file = await db.getSTLFile(fileId); if (!file) { return res.status(404).json({ error: 'File not found' }); } try { await fs.access(file.file_path); } catch { return res.status(404).json({ error: 'Physical file not found' }); } res.download(file.file_path, file.original_name); } catch (error) { console.error('Download error:', error); res.status(500).json({ error: 'Failed to download file', message: error.message }); } }); app.use('/uploads', express.static(path.join(__dirname, 'uploads'), { setHeaders: (res, filePath) => { if (path.extname(filePath).toLowerCase() === '.stl') { res.setHeader('Content-Type', 'application/octet-stream'); res.setHeader('Access-Control-Allow-Origin', '*'); } } })); app.use((error, req, res, next) => { if (error instanceof multer.MulterError) { if (error.code === 'LIMIT_FILE_SIZE') { return res.status(400).json({ error: 'File too large (max 100MB)' }); } } console.error('Unhandled error:', error); res.status(500).json({ error: 'Internal server error', message: error.message }); }); async function startServer() { try { await db.initialize(); logger.info('Database initialized successfully'); app.listen(config.port, config.host, () => { logger.info('STL Storage server started', { host: config.host, port: config.port, nodeEnv: config.nodeEnv }); console.log(`šŸš€ STL Storage server running on ${config.host}:${config.port}`); console.log(`šŸ“ Upload endpoint: http://${config.host}:${config.port}/api/upload`); console.log(`šŸ” Browse files: http://${config.host}:${config.port}/api/files`); }); } catch (error) { logger.error('Failed to start server', { error: error.message, stack: error.stack }); console.error('āŒ Failed to start server:', error); process.exit(1); } } process.on('SIGINT', async () => { console.log('\nšŸ›‘ Shutting down server...'); await db.close(); process.exit(0); }); if (require.main === module) { startServer(); } module.exports = app;