ice/server.js
Deco Vander d9559f71fe Update HTML files to use Bunny.net CDN for static assets
- Updated all static asset URLs to use iceymi.b-cdn.net CDN
- Changed favicon, CSS, and JS file references in index.html, admin.html, and privacy.html
- API calls remain pointed to origin server for dynamic content
- Ready for CDN deployment with proper cache separation
2025-07-03 20:44:16 -04:00

340 lines
11 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,
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({
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 48 hours)
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 > ? 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`);
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);
});
});