const express = require('express'); const cors = require('cors'); const helmet = require('helmet'); const { PrismaClient } = require('@prisma/client'); const authRoutes = require('./routes/auth'); const entriesRoutes = require('./routes/entries'); const timesheetsRoutes = require('./routes/timesheets'); const adminRoutes = require('./routes/admin'); const homeownersRoutes = require('./routes/homeowners'); const app = express(); const prisma = new PrismaClient(); const PORT = process.env.PORT || 3001; // Trust proxy (behind nginx) app.set('trust proxy', 1); // Security headers app.use(helmet()); // CORS const allowedOrigins = process.env.CORS_ORIGINS ? process.env.CORS_ORIGINS.split(',').map((o) => o.trim()) : ['http://localhost:5173', 'http://localhost:3000']; app.use( cors({ origin(origin, callback) { // Allow requests with no origin (mobile apps, curl, etc.) if (!origin || allowedOrigins.includes(origin)) { callback(null, true); } else { callback(new Error('Not allowed by CORS')); } }, credentials: true, }) ); // Body parsing app.use(express.json({ limit: '1mb' })); // Handle malformed JSON errors app.use((err, req, res, next) => { if (err.type === 'entity.parse.failed') { return res.status(400).json({ error: 'Invalid JSON body' }); } next(err); }); // Attach prisma to request app.use((req, _res, next) => { req.prisma = prisma; next(); }); // Health check app.get('/api/health', async (_req, res) => { try { await prisma.$queryRaw`SELECT 1`; res.json({ status: 'ok', timestamp: new Date().toISOString() }); } catch (err) { res.status(503).json({ status: 'error', message: 'Database unavailable' }); } }); // Routes app.use('/api/auth', authRoutes); app.use('/api/entries', entriesRoutes); app.use('/api/timesheets', timesheetsRoutes); app.use('/api/homeowners', homeownersRoutes); app.use('/api/admin', adminRoutes); // 404 handler app.use((_req, res) => { res.status(404).json({ error: 'Not found' }); }); // Global error handler app.use((err, _req, res, _next) => { console.error('Unhandled error:', err); if (err.message === 'Not allowed by CORS') { return res.status(403).json({ error: 'CORS policy violation' }); } res.status(500).json({ error: 'Internal server error' }); }); // Graceful shutdown async function shutdown(signal) { console.log(`\n${signal} received. Shutting down gracefully...`); await prisma.$disconnect(); process.exit(0); } process.on('SIGTERM', () => shutdown('SIGTERM')); process.on('SIGINT', () => shutdown('SIGINT')); // Start server app.listen(PORT, '0.0.0.0', () => { console.log(`🚀 Coastal Timesheet API running on port ${PORT}`); console.log(`📋 Health check: http://localhost:${PORT}/api/health`); }); module.exports = app;