316 lines
7.9 KiB
JavaScript
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);
|
|
}
|
|
});
|