Major optimizations: multi-worker OCR, caching, rate limiting, healthchecks, logging
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
const express = require('express');
|
||||
const cors = require('cors');
|
||||
const bcrypt = require('bcryptjs'); // Movido al inicio (E)
|
||||
const { PrismaClient } = require('@prisma/client');
|
||||
const http = require('http');
|
||||
const { Server } = require('socket.io');
|
||||
@@ -10,6 +11,36 @@ const prisma = new PrismaClient();
|
||||
app.use(cors());
|
||||
app.use(express.json());
|
||||
|
||||
// Rate limiting simple para /api/detect (G)
|
||||
const detectRateLimit = new Map();
|
||||
const RATE_LIMIT_WINDOW = 1000; // 1 segundo
|
||||
const RATE_LIMIT_MAX = 10; // máximo 10 requests por segundo
|
||||
|
||||
function checkRateLimit(ip) {
|
||||
const now = Date.now();
|
||||
const record = detectRateLimit.get(ip) || { count: 0, resetTime: now + RATE_LIMIT_WINDOW };
|
||||
|
||||
if (now > record.resetTime) {
|
||||
record.count = 1;
|
||||
record.resetTime = now + RATE_LIMIT_WINDOW;
|
||||
} else {
|
||||
record.count++;
|
||||
}
|
||||
|
||||
detectRateLimit.set(ip, record);
|
||||
return record.count <= RATE_LIMIT_MAX;
|
||||
}
|
||||
|
||||
// Limpiar rate limit map cada minuto
|
||||
setInterval(() => {
|
||||
const now = Date.now();
|
||||
for (const [ip, record] of detectRateLimit.entries()) {
|
||||
if (now > record.resetTime + 60000) {
|
||||
detectRateLimit.delete(ip);
|
||||
}
|
||||
}
|
||||
}, 60000);
|
||||
|
||||
const server = http.createServer(app);
|
||||
const io = new Server(server, {
|
||||
cors: {
|
||||
@@ -31,9 +62,7 @@ app.use('/api/auth', authRoutes);
|
||||
// Plates CRUD
|
||||
app.get('/api/plates', authenticateToken, async (req, res) => {
|
||||
try {
|
||||
// Filter based on role
|
||||
const where = req.user.role === 'ADMIN' ? {} : { addedById: req.user.id };
|
||||
|
||||
const plates = await prisma.plate.findMany({
|
||||
where,
|
||||
include: { addedBy: { select: { username: true } } }
|
||||
@@ -47,7 +76,6 @@ app.get('/api/plates', authenticateToken, async (req, res) => {
|
||||
app.post('/api/plates', authenticateToken, async (req, res) => {
|
||||
const { number, owner } = req.body;
|
||||
const isAdm = req.user.role === 'ADMIN';
|
||||
// Admin -> ALLOWED, User -> PENDING
|
||||
const status = isAdm ? 'ALLOWED' : 'PENDING';
|
||||
|
||||
try {
|
||||
@@ -59,10 +87,7 @@ app.post('/api/plates', authenticateToken, async (req, res) => {
|
||||
addedById: req.user.id
|
||||
}
|
||||
});
|
||||
|
||||
// Notify Admin via WebSocket
|
||||
io.emit('new_plate_registered', plate);
|
||||
|
||||
res.json(plate);
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message });
|
||||
@@ -72,7 +97,7 @@ app.post('/api/plates', authenticateToken, async (req, res) => {
|
||||
// Admin: Approve/Reject Plate
|
||||
app.put('/api/plates/:id/approve', authenticateToken, isAdmin, async (req, res) => {
|
||||
const { id } = req.params;
|
||||
const { status } = req.body; // ALLOWED or DENIED
|
||||
const { status } = req.body;
|
||||
|
||||
if (!['ALLOWED', 'DENIED'].includes(status)) {
|
||||
return res.status(400).json({ error: 'Invalid status' });
|
||||
@@ -83,17 +108,13 @@ app.put('/api/plates/:id/approve', authenticateToken, isAdmin, async (req, res)
|
||||
where: { id: parseInt(id) },
|
||||
data: { status }
|
||||
});
|
||||
|
||||
// Notify Users via WebSocket
|
||||
io.emit('plate_status_updated', plate);
|
||||
|
||||
res.json(plate);
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Admin: Delete Plate (Optional but good to have)
|
||||
// Delete Plate (Admin or Owner)
|
||||
app.delete('/api/plates/:id', authenticateToken, async (req, res) => {
|
||||
const { id } = req.params;
|
||||
@@ -101,15 +122,12 @@ app.delete('/api/plates/:id', authenticateToken, async (req, res) => {
|
||||
const plate = await prisma.plate.findUnique({ where: { id: parseInt(id) } });
|
||||
if (!plate) return res.status(404).json({ error: 'Plate not found' });
|
||||
|
||||
// Check permissions
|
||||
if (req.user.role !== 'ADMIN' && plate.addedById !== req.user.id) {
|
||||
return res.status(403).json({ error: 'Unauthorized' });
|
||||
}
|
||||
|
||||
await prisma.plate.delete({ where: { id: parseInt(id) } });
|
||||
|
||||
io.emit('plate_deleted', { id: parseInt(id) });
|
||||
|
||||
res.json({ message: 'Plate deleted' });
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message });
|
||||
@@ -128,9 +146,7 @@ app.delete('/api/people/:id', authenticateToken, async (req, res) => {
|
||||
}
|
||||
|
||||
await prisma.person.delete({ where: { id: parseInt(id) } });
|
||||
|
||||
io.emit('person_deleted', { id: parseInt(id) });
|
||||
|
||||
res.json({ message: 'Person deleted' });
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message });
|
||||
@@ -139,7 +155,7 @@ app.delete('/api/people/:id', authenticateToken, async (req, res) => {
|
||||
|
||||
// History Endpoint
|
||||
app.get('/api/history', async (req, res) => {
|
||||
const { date } = req.query; // Format: YYYY-MM-DD
|
||||
const { date } = req.query;
|
||||
if (!date) {
|
||||
return res.status(400).json({ error: 'Date is required' });
|
||||
}
|
||||
@@ -158,9 +174,7 @@ app.get('/api/history', async (req, res) => {
|
||||
lte: endDate
|
||||
}
|
||||
},
|
||||
orderBy: {
|
||||
timestamp: 'desc'
|
||||
}
|
||||
orderBy: { timestamp: 'desc' }
|
||||
});
|
||||
res.json(logs);
|
||||
} catch (err) {
|
||||
@@ -174,13 +188,9 @@ app.get('/api/recent', async (req, res) => {
|
||||
const fiveHoursAgo = new Date(Date.now() - 5 * 60 * 60 * 1000);
|
||||
const logs = await prisma.accessLog.findMany({
|
||||
where: {
|
||||
timestamp: {
|
||||
gte: fiveHoursAgo
|
||||
}
|
||||
timestamp: { gte: fiveHoursAgo }
|
||||
},
|
||||
orderBy: {
|
||||
timestamp: 'desc'
|
||||
}
|
||||
orderBy: { timestamp: 'desc' }
|
||||
});
|
||||
res.json(logs);
|
||||
} catch (err) {
|
||||
@@ -238,10 +248,7 @@ app.post('/api/people', authenticateToken, async (req, res) => {
|
||||
addedById: req.user.id
|
||||
}
|
||||
});
|
||||
|
||||
// Notify Admin via WebSocket
|
||||
io.emit('new_person_registered', person);
|
||||
|
||||
res.json(person);
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message });
|
||||
@@ -256,25 +263,28 @@ app.post('/api/people/bulk-approve', authenticateToken, isAdmin, async (req, res
|
||||
where: { addedById: parseInt(userId), status: 'PENDING' },
|
||||
data: { status: 'APPROVED' }
|
||||
});
|
||||
|
||||
// Notify Users via WebSocket
|
||||
io.emit('people_updated', { userId });
|
||||
|
||||
res.json({ message: 'Bulk approval successful' });
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Detection Endpoint (from Python)
|
||||
// Detection Endpoint (from Python) with Rate Limiting (G)
|
||||
app.post('/api/detect', async (req, res) => {
|
||||
const clientIp = req.ip || req.connection.remoteAddress;
|
||||
|
||||
// Check rate limit
|
||||
if (!checkRateLimit(clientIp)) {
|
||||
return res.status(429).json({ error: 'Too many requests' });
|
||||
}
|
||||
|
||||
const { plate_number } = req.body;
|
||||
console.log(`Detected: ${plate_number}`);
|
||||
|
||||
const DUPLICATE_COOLDOWN_MS = 30000; // 30 seconds
|
||||
const DUPLICATE_COOLDOWN_MS = 30000;
|
||||
|
||||
try {
|
||||
// Check for recent duplicate
|
||||
const lastLog = await prisma.accessLog.findFirst({
|
||||
where: { plateNumber: plate_number },
|
||||
orderBy: { timestamp: 'desc' }
|
||||
@@ -288,7 +298,6 @@ app.post('/api/detect', async (req, res) => {
|
||||
}
|
||||
}
|
||||
|
||||
// Check if plate exists
|
||||
let plate = await prisma.plate.findUnique({
|
||||
where: { number: plate_number }
|
||||
});
|
||||
@@ -300,12 +309,9 @@ app.post('/api/detect', async (req, res) => {
|
||||
}
|
||||
|
||||
if (!plate) {
|
||||
// Optional: Auto-create unknown plates?
|
||||
// For now, treat as UNKNOWN (Denied)
|
||||
accessStatus = 'UNKNOWN';
|
||||
}
|
||||
|
||||
// Log the access attempt
|
||||
const log = await prisma.accessLog.create({
|
||||
data: {
|
||||
plateNumber: plate_number,
|
||||
@@ -314,7 +320,6 @@ app.post('/api/detect', async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
// Notify Frontend via WebSocket
|
||||
io.emit('new_detection', {
|
||||
plate: plate_number,
|
||||
status: accessStatus,
|
||||
@@ -334,7 +339,6 @@ app.post('/api/dataset/capture', (req, res) => {
|
||||
const { plate_number, filename, count } = req.body;
|
||||
console.log(`📸 Dataset capture: ${plate_number} (Total: ${count})`);
|
||||
|
||||
// Notify Frontend via WebSocket
|
||||
io.emit('dataset_updated', {
|
||||
plate: plate_number,
|
||||
filename,
|
||||
@@ -345,7 +349,15 @@ app.post('/api/dataset/capture', (req, res) => {
|
||||
res.json({ message: 'Notification sent' });
|
||||
});
|
||||
|
||||
const bcrypt = require('bcryptjs');
|
||||
// Health check endpoint
|
||||
app.get('/api/health', async (req, res) => {
|
||||
try {
|
||||
await prisma.$queryRaw`SELECT 1`;
|
||||
res.json({ status: 'ok', database: 'connected' });
|
||||
} catch (err) {
|
||||
res.status(500).json({ status: 'error', database: 'disconnected' });
|
||||
}
|
||||
});
|
||||
|
||||
const PORT = process.env.PORT || 3000;
|
||||
server.listen(PORT, async () => {
|
||||
|
||||
Reference in New Issue
Block a user