import 'dotenv/config'; import path from 'path'; import { fileURLToPath } from 'url'; import cors from 'cors'; import cron from 'node-cron'; import express, { type NextFunction, type Request, type Response } from 'express'; import { buildApiPath, getConfig } from './config'; import { TursoClient } from './lib/db'; import { createHealthRouter } from './routes/health'; import { createTodosRouter } from './routes/todos'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const config = getConfig(); const app = express(); const db = new TursoClient(config); app.disable('x-powered-by'); app.use(cors({ origin: config.corsOrigin === '*' ? true : config.corsOrigin })); app.use(express.json({ limit: '1mb' })); const apiRoot = config.apiPrefix || '/'; app.use(apiRoot, createHealthRouter(config)); app.use(apiRoot, createTodosRouter(config, db)); cron.schedule('*/3 * * * *', () => { console.log('[Cron A] Heartbeat executing every 3 minutes: ' + new Date().toISOString()); }); cron.schedule('*/10 * * * *', () => { console.log('[Cron B] Heartbeat executing every 10 minutes: ' + new Date().toISOString()); }); app.post('/_cron/tick', (req, res) => { const token = req.headers.authorization?.replace(/^Bearer\s+/i, '') || ''; const expected = process.env.CRON_TOKEN || ''; if (!expected || token !== expected) { res.status(401).json({ success: false, error: 'Invalid cron token' }); return; } console.log('[Tick] Heartbeat received. Waking up event loop.'); res.status(200).json({ success: true }); }); const publicDir = path.join(__dirname, '../public'); app.use(express.static(publicDir)); app.get('/{*path}', (req, res, next) => { if (req.path.startsWith(apiRoot)) return next(); if (req.path === '/_cron/tick') return next(); // Do not serve HTML for static assets or requests that do not accept HTML const isAsset = /\.[a-z0-9]{2,4}$/i.test(req.path); const acceptsHtml = req.headers.accept?.includes('text/html'); if (isAsset || !acceptsHtml) { res.status(404).json({ success: false, error: 'Not found' }); return; } if (req.path.startsWith('/mobile')) { res.sendFile(path.join(publicDir, 'mobile/index.html'), (err) => { if (err) next(); }); return; } if (req.path.startsWith('/games')) { res.sendFile(path.join(publicDir, 'games/index.html'), (err) => { if (err) next(); }); return; } res.sendFile(path.join(publicDir, 'index.html'), (err) => { if (err) next(); }); }); app.use((_req, res) => { res.status(404).json({ success: false, error: 'Not found' }); }); app.use((error: Error, _req: Request, res: Response, _next: NextFunction) => { const status = /required|invalid|missing/i.test(error.message) ? 400 : 500; res.status(status).json({ success: false, error: error.message || 'Unhandled error', }); }); app.listen(config.port, '0.0.0.0', () => { const healthPath = buildApiPath(config, '/health'); console.log(`Backend todo template listening on port ${config.port}`); console.log(`Health check available at ${healthPath}`); });