Files
ControlPatente/backend/src/index.js
2025-12-28 20:52:25 -03:00

316 lines
7.9 KiB
JavaScript

const express = require('express');
const cors = require('cors');
const { PrismaClient } = require('@prisma/client');
const http = require('http');
const { Server } = require('socket.io');
const app = express();
const prisma = new PrismaClient();
app.use(cors());
app.use(express.json());
const server = http.createServer(app);
const io = new Server(server, {
cors: {
origin: "*",
methods: ["GET", "POST"]
}
});
// Hello World
app.get('/', (req, res) => {
res.send('ALPR Backend Running');
});
const authRoutes = require('./routes/auth');
const { authenticateToken, isAdmin } = require('./middleware/auth');
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 } } }
});
res.json(plates);
} catch (err) {
res.status(500).json({ error: err.message });
}
});
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 {
const plate = await prisma.plate.create({
data: {
number,
owner,
status,
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 });
}
});
// 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
if (!['ALLOWED', 'DENIED'].includes(status)) {
return res.status(400).json({ error: 'Invalid status' });
}
try {
const plate = await prisma.plate.update({
where: { id: parseInt(id) },
data: { status }
});
res.json(plate);
} catch (err) {
res.status(500).json({ error: err.message });
}
});
// Admin: Delete Plate (Optional but good to have)
app.delete('/api/plates/:id', authenticateToken, isAdmin, async (req, res) => {
const { id } = req.params;
try {
await prisma.plate.delete({ where: { id: parseInt(id) } });
res.json({ message: 'Plate deleted' });
} catch (err) {
res.status(500).json({ error: err.message });
}
});
// History Endpoint
app.get('/api/history', async (req, res) => {
const { date } = req.query; // Format: YYYY-MM-DD
if (!date) {
return res.status(400).json({ error: 'Date is required' });
}
const startDate = new Date(date);
startDate.setHours(0, 0, 0, 0);
const endDate = new Date(date);
endDate.setHours(23, 59, 59, 999);
try {
const logs = await prisma.accessLog.findMany({
where: {
timestamp: {
gte: startDate,
lte: endDate
}
},
orderBy: {
timestamp: 'desc'
}
});
res.json(logs);
} catch (err) {
res.status(500).json({ error: err.message });
}
});
// Recent Scans Endpoint (Last 5 Hours)
app.get('/api/recent', async (req, res) => {
try {
const fiveHoursAgo = new Date(Date.now() - 5 * 60 * 60 * 1000);
const logs = await prisma.accessLog.findMany({
where: {
timestamp: {
gte: fiveHoursAgo
}
},
orderBy: {
timestamp: 'desc'
}
});
res.json(logs);
} catch (err) {
res.status(500).json({ error: err.message });
}
});
// Helper: RUT Validation
function validateRut(rut) {
if (!rut || !/^[0-9]+-[0-9kK]{1}$/.test(rut)) return false;
let [num, dv] = rut.split('-');
let total = 0;
let multiple = 2;
for (let i = num.length - 1; i >= 0; i--) {
total += parseInt(num.charAt(i)) * multiple;
multiple = (multiple + 1) % 8 || 2;
}
let res = 11 - (total % 11);
let finalDv = res === 11 ? '0' : res === 10 ? 'K' : res.toString();
return finalDv.toUpperCase() === dv.toUpperCase();
}
// People CRUD
app.get('/api/people', authenticateToken, async (req, res) => {
try {
const where = req.user.role === 'ADMIN' ? {} : { addedById: req.user.id };
const people = await prisma.person.findMany({
where,
include: { addedBy: { select: { username: true } } }
});
res.json(people);
} catch (err) {
res.status(500).json({ error: err.message });
}
});
app.post('/api/people', authenticateToken, async (req, res) => {
const { rut, name, durationDays } = req.body;
if (!validateRut(rut)) {
return res.status(400).json({ error: 'Invalid RUT format (12345678-K)' });
}
const startDate = new Date();
const endDate = new Date();
endDate.setDate(endDate.getDate() + parseInt(durationDays || 1));
try {
const person = await prisma.person.create({
data: {
rut,
name,
startDate,
endDate,
status: 'PENDING',
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 });
}
});
// Admin: Bulk Approve People by User
app.post('/api/people/bulk-approve', authenticateToken, isAdmin, async (req, res) => {
const { userId } = req.body;
try {
await prisma.person.updateMany({
where: { addedById: parseInt(userId), status: 'PENDING' },
data: { status: 'APPROVED' }
});
res.json({ message: 'Bulk approval successful' });
} catch (err) {
res.status(500).json({ error: err.message });
}
});
// Detection Endpoint (from Python)
app.post('/api/detect', async (req, res) => {
const { plate_number } = req.body;
console.log(`Detected: ${plate_number}`);
const DUPLICATE_COOLDOWN_MS = 30000; // 30 seconds
try {
// Check for recent duplicate
const lastLog = await prisma.accessLog.findFirst({
where: { plateNumber: plate_number },
orderBy: { timestamp: 'desc' }
});
if (lastLog) {
const timeDiff = new Date() - new Date(lastLog.timestamp);
if (timeDiff < DUPLICATE_COOLDOWN_MS) {
console.log(`Duplicate detection ignored for ${plate_number} (${timeDiff}ms since last)`);
return res.json({ message: 'Duplicate detection ignored', ignored: true, accessStatus: lastLog.accessStatus });
}
}
// Check if plate exists
let plate = await prisma.plate.findUnique({
where: { number: plate_number }
});
let accessStatus = 'DENIED';
if (plate && plate.status === 'ALLOWED') {
accessStatus = 'GRANTED';
}
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,
accessStatus,
timestamp: new Date()
}
});
// Notify Frontend via WebSocket
io.emit('new_detection', {
plate: plate_number,
status: accessStatus,
timestamp: log.timestamp
});
res.json({ message: 'Processed', accessStatus });
} catch (err) {
console.error(err);
res.status(500).json({ error: err.message });
}
});
const bcrypt = require('bcryptjs');
const PORT = process.env.PORT || 3000;
server.listen(PORT, async () => {
console.log(`Server running on port ${PORT}`);
// Seed Admin User if none exists
try {
const userCount = await prisma.user.count();
if (userCount === 0) {
console.log('No users found. Creating default admin user...');
const hashedPassword = await bcrypt.hash('admin123', 10);
await prisma.user.create({
data: {
username: 'admin',
password: hashedPassword,
role: 'ADMIN'
}
});
console.log('Default admin created: admin / admin123');
}
} catch (err) {
console.error('Error seeding admin user:', err);
}
});