- Remove sass and concurrently dependencies (31 packages) - Remove SCSS files and src/styles directory - Remove Sass-related npm scripts (build-css, watch-css, dev-with-css) - Remove CSS source map file - Keep hand-crafted style.css which is actually being used
340 lines
11 KiB
JavaScript
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({
|
|
// 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);
|
|
});
|
|
});
|