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