- Replace OpenWeatherMap with Open-Meteo (no API key needed) - 16-day forecast (up from 5 days) - Historical climate averages as fallback beyond 16 days - Auto-upgrade from climate to real forecast when available - Fix Vacay WebSocket sync across devices (socket-ID exclusion instead of user-ID) - Add GitHub release history tab in admin panel - Show cluster count "1" for single map markers when zoomed out - Add weather info panel in admin settings (replaces OpenWeatherMap key input) - Update i18n translations (DE + EN)
160 lines
5.5 KiB
JavaScript
160 lines
5.5 KiB
JavaScript
require('dotenv').config();
|
|
const express = require('express');
|
|
const cors = require('cors');
|
|
const helmet = require('helmet');
|
|
const path = require('path');
|
|
const fs = require('fs');
|
|
|
|
const app = express();
|
|
|
|
// Create upload directories on startup
|
|
const uploadsDir = path.join(__dirname, '../uploads');
|
|
const photosDir = path.join(uploadsDir, 'photos');
|
|
const filesDir = path.join(uploadsDir, 'files');
|
|
const coversDir = path.join(uploadsDir, 'covers');
|
|
const backupsDir = path.join(__dirname, '../data/backups');
|
|
const tmpDir = path.join(__dirname, '../data/tmp');
|
|
|
|
[uploadsDir, photosDir, filesDir, coversDir, backupsDir, tmpDir].forEach(dir => {
|
|
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
});
|
|
|
|
// Middleware
|
|
const allowedOrigins = process.env.ALLOWED_ORIGINS
|
|
? process.env.ALLOWED_ORIGINS.split(',')
|
|
: null;
|
|
|
|
let corsOrigin;
|
|
if (allowedOrigins) {
|
|
// Explicit whitelist from env var
|
|
corsOrigin = (origin, callback) => {
|
|
if (!origin || allowedOrigins.includes(origin)) callback(null, true);
|
|
else callback(new Error('Not allowed by CORS'));
|
|
};
|
|
} else if (process.env.NODE_ENV === 'production') {
|
|
// Production: same-origin only (Express serves the static client)
|
|
corsOrigin = false;
|
|
} else {
|
|
// Development: allow all origins (needed for Vite dev server)
|
|
corsOrigin = true;
|
|
}
|
|
|
|
app.use(cors({
|
|
origin: corsOrigin,
|
|
credentials: true
|
|
}));
|
|
app.use(helmet({
|
|
contentSecurityPolicy: false, // managed by frontend meta tag or reverse proxy
|
|
crossOriginEmbedderPolicy: false, // allows loading external images (maps, etc.)
|
|
}));
|
|
app.use(express.json({ limit: '100kb' }));
|
|
app.use(express.urlencoded({ extended: true }));
|
|
|
|
// Serve uploaded files
|
|
app.use('/uploads', express.static(path.join(__dirname, '../uploads')));
|
|
|
|
// Routes
|
|
const authRoutes = require('./routes/auth');
|
|
const tripsRoutes = require('./routes/trips');
|
|
const daysRoutes = require('./routes/days');
|
|
const placesRoutes = require('./routes/places');
|
|
const assignmentsRoutes = require('./routes/assignments');
|
|
const packingRoutes = require('./routes/packing');
|
|
const tagsRoutes = require('./routes/tags');
|
|
const categoriesRoutes = require('./routes/categories');
|
|
const adminRoutes = require('./routes/admin');
|
|
const mapsRoutes = require('./routes/maps');
|
|
const filesRoutes = require('./routes/files');
|
|
const reservationsRoutes = require('./routes/reservations');
|
|
const dayNotesRoutes = require('./routes/dayNotes');
|
|
const weatherRoutes = require('./routes/weather');
|
|
const settingsRoutes = require('./routes/settings');
|
|
const budgetRoutes = require('./routes/budget');
|
|
const backupRoutes = require('./routes/backup');
|
|
|
|
const oidcRoutes = require('./routes/oidc');
|
|
app.use('/api/auth', authRoutes);
|
|
app.use('/api/auth/oidc', oidcRoutes);
|
|
app.use('/api/trips', tripsRoutes);
|
|
app.use('/api/trips/:tripId/days', daysRoutes);
|
|
app.use('/api/trips/:tripId/places', placesRoutes);
|
|
app.use('/api/trips/:tripId/packing', packingRoutes);
|
|
app.use('/api/trips/:tripId/files', filesRoutes);
|
|
app.use('/api/trips/:tripId/budget', budgetRoutes);
|
|
app.use('/api/trips/:tripId/reservations', reservationsRoutes);
|
|
app.use('/api/trips/:tripId/days/:dayId/notes', dayNotesRoutes);
|
|
app.use('/api', assignmentsRoutes);
|
|
app.use('/api/tags', tagsRoutes);
|
|
app.use('/api/categories', categoriesRoutes);
|
|
app.use('/api/admin', adminRoutes);
|
|
|
|
// Public addons endpoint (authenticated but not admin-only)
|
|
const { authenticate: addonAuth } = require('./middleware/auth');
|
|
const { db: addonDb } = require('./db/database');
|
|
app.get('/api/addons', addonAuth, (req, res) => {
|
|
const addons = addonDb.prepare('SELECT id, name, type, icon, enabled FROM addons WHERE enabled = 1 ORDER BY sort_order').all();
|
|
res.json({ addons: addons.map(a => ({ ...a, enabled: !!a.enabled })) });
|
|
});
|
|
|
|
// Addon routes
|
|
const vacayRoutes = require('./routes/vacay');
|
|
app.use('/api/addons/vacay', vacayRoutes);
|
|
const atlasRoutes = require('./routes/atlas');
|
|
app.use('/api/addons/atlas', atlasRoutes);
|
|
|
|
app.use('/api/maps', mapsRoutes);
|
|
app.use('/api/weather', weatherRoutes);
|
|
app.use('/api/settings', settingsRoutes);
|
|
app.use('/api/backup', backupRoutes);
|
|
|
|
// Serve static files in production
|
|
if (process.env.NODE_ENV === 'production') {
|
|
const publicPath = path.join(__dirname, '../public');
|
|
app.use(express.static(publicPath));
|
|
app.get('*', (req, res) => {
|
|
res.sendFile(path.join(publicPath, 'index.html'));
|
|
});
|
|
}
|
|
|
|
// Global error handler
|
|
app.use((err, req, res, next) => {
|
|
console.error('Unhandled error:', err);
|
|
res.status(500).json({ error: 'Internal server error' });
|
|
});
|
|
|
|
const scheduler = require('./scheduler');
|
|
|
|
const PORT = process.env.PORT || 3001;
|
|
const server = app.listen(PORT, () => {
|
|
console.log(`NOMAD API running on port ${PORT}`);
|
|
console.log(`Environment: ${process.env.NODE_ENV || 'development'}`);
|
|
if (process.env.DEMO_MODE === 'true') console.log('Demo mode: ENABLED');
|
|
scheduler.start();
|
|
scheduler.startDemoReset();
|
|
const { setupWebSocket } = require('./websocket');
|
|
setupWebSocket(server);
|
|
});
|
|
|
|
// Graceful shutdown
|
|
function shutdown(signal) {
|
|
console.log(`\n${signal} received — shutting down gracefully...`);
|
|
scheduler.stop();
|
|
server.close(() => {
|
|
console.log('HTTP server closed');
|
|
const { closeDb } = require('./db/database');
|
|
closeDb();
|
|
console.log('Shutdown complete');
|
|
process.exit(0);
|
|
});
|
|
// Force exit after 10s if connections don't close
|
|
setTimeout(() => {
|
|
console.error('Forced shutdown after timeout');
|
|
process.exit(1);
|
|
}, 10000);
|
|
}
|
|
|
|
process.on('SIGTERM', () => shutdown('SIGTERM'));
|
|
process.on('SIGINT', () => shutdown('SIGINT'));
|
|
|
|
module.exports = app;
|