Major optimizations: multi-worker OCR, caching, rate limiting, healthchecks, logging

This commit is contained in:
2026-01-13 13:21:00 -03:00
parent 9b15c7a480
commit d7be8d7036
4 changed files with 202 additions and 102 deletions

View File

@@ -44,4 +44,8 @@ model AccessLog {
plateNumber String
accessStatus String // GRANTED, DENIED, UNKNOWN
timestamp DateTime @default(now())
@@index([plateNumber])
@@index([timestamp])
@@index([plateNumber, timestamp])
}

View File

@@ -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 () => {