ice/server.js
Deco Vander edfdeb5117 Initial commit: ICE Watch Michigan community safety tool
- Node.js/Express backend with SQLite database
- Interactive map with real-time location tracking
- MapBox API integration for fast geocoding
- Admin panel for content moderation
- 24-hour auto-expiring reports
- Deployment scripts for Debian 12 ARM64
- Caddy reverse proxy with automatic HTTPS
2025-07-02 23:27:22 -04:00

293 lines
9.2 KiB
JavaScript

require('dotenv').config({ path: '.env.local' });
require('dotenv').config();
const express = require('express');
const cors = require('cors');
const sqlite3 = require('sqlite3').verbose();
const path = require('path');
const cron = require('node-cron');
const app = express();
const PORT = process.env.PORT || 3000;
// Middleware
app.use(cors());
app.use(express.json());
app.use(express.static('public'));
// Database setup
const db = new sqlite3.Database('icewatch.db');
console.log('Database connection established');
// Initialize database
db.serialize(() => {
db.run(`CREATE TABLE IF NOT EXISTS locations (
id INTEGER PRIMARY KEY AUTOINCREMENT,
address TEXT NOT NULL,
latitude REAL,
longitude REAL,
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP,
description TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
)`, (err) => {
if (err) {
console.error('Error initializing database:', err);
} else {
console.log('Database initialized successfully');
}
});
});
// Clean up expired locations (older than 24 hours)
const cleanupExpiredLocations = () => {
console.log('Running cleanup of expired locations');
const twentyFourHoursAgo = new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString();
db.run('DELETE FROM locations WHERE created_at < ?', [twentyFourHoursAgo], function(err) {
if (err) {
console.error('Error cleaning up expired locations:', err);
} else {
console.log(`Cleaned up ${this.changes} expired locations`);
}
});
};
// Run cleanup every hour
console.log('Scheduling hourly cleanup task');
cron.schedule('0 * * * *', cleanupExpiredLocations);
// Configuration
const ADMIN_PASSWORD = process.env.ADMIN_PASSWORD || 'admin123'; // Change this!
const MAPBOX_ACCESS_TOKEN = process.env.MAPBOX_ACCESS_TOKEN || null; // Set this for better performance
const GOOGLE_MAPS_API_KEY = process.env.GOOGLE_MAPS_API_KEY || null; // Fallback option
const authenticateAdmin = (req, res, next) => {
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return res.status(401).json({ error: 'Unauthorized' });
}
const token = authHeader.substring(7);
if (token !== ADMIN_PASSWORD) {
return res.status(401).json({ error: 'Invalid credentials' });
}
next();
};
// API Routes
// Get API configuration
app.get('/api/config', (req, res) => {
console.log('📡 API Config requested');
console.log('MapBox token present:', !!MAPBOX_ACCESS_TOKEN);
console.log('MapBox token starts with pk:', MAPBOX_ACCESS_TOKEN?.startsWith('pk.'));
res.json({
mapboxAccessToken: MAPBOX_ACCESS_TOKEN,
hasMapbox: !!MAPBOX_ACCESS_TOKEN,
googleMapsApiKey: GOOGLE_MAPS_API_KEY,
hasGoogleMaps: !!GOOGLE_MAPS_API_KEY
});
});
// Admin login
app.post('/api/admin/login', (req, res) => {
console.log('Admin login attempt');
const { password } = req.body;
if (password === ADMIN_PASSWORD) {
console.log('Admin login successful');
res.json({ token: ADMIN_PASSWORD, message: 'Login successful' });
} else {
console.warn('Admin login failed: invalid password');
res.status(401).json({ error: 'Invalid password' });
}
});
// Get all active locations (within 24 hours)
app.get('/api/locations', (req, res) => {
console.log('Fetching active locations');
const twentyFourHoursAgo = new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString();
db.all(
'SELECT * FROM locations WHERE created_at > ? ORDER BY created_at DESC',
[twentyFourHoursAgo],
(err, rows) => {
if (err) {
console.error('Error fetching locations:', err);
res.status(500).json({ error: 'Internal server error' });
return;
}
console.log(`Fetched ${rows.length} active locations`);
res.json(rows);
}
);
});
// Add a new location
app.post('/api/locations', (req, res) => {
const { address, latitude, longitude, description } = req.body;
console.log(`Attempt to add new location: ${address}`);
if (!address) {
console.warn('Failed to add location: Address is required');
res.status(400).json({ error: 'Address is required' });
return;
}
db.run(
'INSERT INTO locations (address, latitude, longitude, description) VALUES (?, ?, ?, ?)',
[address, latitude, longitude, description],
function(err) {
if (err) {
console.error('Error inserting location:', err);
res.status(500).json({ error: 'Internal server error' });
return;
}
console.log(`Location added successfully: ${address}`);
res.json({
id: this.lastID,
address,
latitude,
longitude,
description,
created_at: new Date().toISOString()
});
}
);
});
// Admin Routes
// Get all locations for admin (including expired ones)
app.get('/api/admin/locations', authenticateAdmin, (req, res) => {
db.all(
'SELECT id, address, description, latitude, longitude, created_at FROM locations ORDER BY created_at DESC',
[],
(err, rows) => {
if (err) {
console.error('Error fetching all locations:', err);
res.status(500).json({ error: 'Internal server error' });
return;
}
// Process and clean data before sending
const locations = rows.map(row => ({
id: row.id,
address: row.address,
description: row.description || '',
latitude: row.latitude,
longitude: row.longitude,
created_at: row.created_at,
isActive: new Date(row.created_at) > new Date(Date.now() - 24 * 60 * 60 * 1000)
}));
res.json(locations);
}
);
});
// Update a location (admin only)
app.put('/api/admin/locations/:id', authenticateAdmin, (req, res) => {
const { id } = req.params;
const { address, latitude, longitude, description } = req.body;
if (!address) {
res.status(400).json({ error: 'Address is required' });
return;
}
db.run(
'UPDATE locations SET address = ?, latitude = ?, longitude = ?, description = ? WHERE id = ?',
[address, latitude, longitude, description, id],
function(err) {
if (err) {
console.error('Error updating location:', err);
res.status(500).json({ error: 'Internal server error' });
return;
}
if (this.changes === 0) {
res.status(404).json({ error: 'Location not found' });
return;
}
res.json({ message: 'Location updated successfully' });
}
);
});
// Delete a location (admin authentication required)
app.delete('/api/admin/locations/:id', authenticateAdmin, (req, res) => {
const { id } = req.params;
db.run('DELETE FROM locations WHERE id = ?', [id], function(err) {
if (err) {
console.error('Error deleting location:', err);
res.status(500).json({ error: 'Internal server error' });
return;
}
if (this.changes === 0) {
res.status(404).json({ error: 'Location not found' });
return;
}
res.json({ message: 'Location deleted successfully' });
});
});
// Legacy delete route (keeping for backwards compatibility)
app.delete('/api/locations/:id', (req, res) => {
const { id } = req.params;
db.run('DELETE FROM locations WHERE id = ?', [id], function(err) {
if (err) {
console.error('Error deleting location:', err);
res.status(500).json({ error: 'Internal server error' });
return;
}
if (this.changes === 0) {
res.status(404).json({ error: 'Location not found' });
return;
}
res.json({ message: 'Location deleted successfully' });
});
});
// Serve the main page
app.get('/', (req, res) => {
console.log('Serving the main page');
res.sendFile(path.join(__dirname, 'public', 'index.html'));
});
// Serve the admin page
app.get('/admin', (req, res) => {
console.log('Serving the admin page');
res.sendFile(path.join(__dirname, 'public', 'admin.html'));
});
// Start server
app.listen(PORT, () => {
console.log('=======================================');
console.log('ICE Watch server successfully started');
console.log(`Listening on port ${PORT}`);
console.log(`Visit http://localhost:${PORT} to view the website`);
console.log('=======================================');
});
// Graceful shutdown
process.on('SIGINT', () => {
console.log('\nShutting down server...');
db.close((err) => {
if (err) {
console.error('Error closing database:', err);
} else {
console.log('Database connection closed.');
}
process.exit(0);
});
});