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, persistent INTEGER DEFAULT 0, created_at DATETIME DEFAULT CURRENT_TIMESTAMP )`, (err) => { if (err) { console.error('Error initializing database:', err); } else { console.log('Database initialized successfully'); // Add persistent column to existing tables if it doesn't exist db.run(`ALTER TABLE locations ADD COLUMN persistent INTEGER DEFAULT 0`, (alterErr) => { if (alterErr && !alterErr.message.includes('duplicate column name')) { console.error('Error adding persistent column:', alterErr); } else if (!alterErr) { console.log('Added persistent column to existing table'); } }); } }); }); // Clean up expired locations (older than 48 hours, but not persistent ones) const cleanupExpiredLocations = () => { console.log('Running cleanup of expired locations'); const fortyEightHoursAgo = new Date(Date.now() - 48 * 60 * 60 * 1000).toISOString(); db.run('DELETE FROM locations WHERE created_at < ? AND persistent = 0', [fortyEightHoursAgo], function(err) { if (err) { console.error('Error cleaning up expired locations:', err); } else { console.log(`Cleaned up ${this.changes} expired locations (persistent reports preserved)`); } }); }; // 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({ // MapBox tokens are designed to be public (they have domain restrictions) mapboxAccessToken: MAPBOX_ACCESS_TOKEN, hasMapbox: !!MAPBOX_ACCESS_TOKEN // SECURITY: Google Maps API key is kept server-side only }); }); // 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 48 hours OR persistent) app.get('/api/locations', (req, res) => { console.log('Fetching active locations'); const fortyEightHoursAgo = new Date(Date.now() - 48 * 60 * 60 * 1000).toISOString(); db.all( 'SELECT * FROM locations WHERE created_at > ? OR persistent = 1 ORDER BY created_at DESC', [fortyEightHoursAgo], (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 (including persistent)`); 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, persistent, 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, persistent: !!row.persistent, created_at: row.created_at, isActive: new Date(row.created_at) > new Date(Date.now() - 48 * 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' }); } ); }); // Toggle persistent status of a location (admin only) app.patch('/api/admin/locations/:id/persistent', authenticateAdmin, (req, res) => { const { id } = req.params; const { persistent } = req.body; if (typeof persistent !== 'boolean') { res.status(400).json({ error: 'Persistent value must be a boolean' }); return; } db.run( 'UPDATE locations SET persistent = ? WHERE id = ?', [persistent ? 1 : 0, id], function(err) { if (err) { console.error('Error updating persistent status:', err); res.status(500).json({ error: 'Internal server error' }); return; } if (this.changes === 0) { res.status(404).json({ error: 'Location not found' }); return; } console.log(`Location ${id} persistent status set to ${persistent}`); res.json({ message: 'Persistent status updated successfully', persistent }); } ); }); // 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')); }); // Serve the privacy policy page app.get('/privacy', (req, res) => { console.log('Serving the privacy policy page'); res.sendFile(path.join(__dirname, 'public', 'privacy.html')); }); // Start server app.listen(PORT, () => { console.log('==========================================='); console.log('Great Lakes Ice Report server 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); }); });