require('dotenv').config({ path: require('path').join(__dirname, '.env') }); const path = require('path'); const express = require('express'); const cookieParser = require('cookie-parser'); const auth = require('./src/auth'); const realtime = require('./src/realtime'); const api = require('./src/api'); const automations = require('./src/automations'); const importer = require('./src/importMonday'); const { get, run, id, now } = require('./src/db'); const PORT = Number(process.env.PORT || 3100); const app = express(); app.set('trust proxy', true); // behind the NAS reverse proxy, so req.secure reflects https app.disable('x-powered-by'); app.use(express.json({ limit: '5mb' })); app.use(cookieParser()); app.use((req, res, next) => { res.set('X-Content-Type-Options', 'nosniff'); res.set('Referrer-Policy', 'strict-origin-when-cross-origin'); // Google's sign-in popup needs to postMessage back to this page. res.set('Cross-Origin-Opener-Policy', 'same-origin-allow-popups'); next(); }); app.get('/healthz', (req, res) => res.json({ ok: true })); auth.mount(app); realtime.mount(app, auth.requireUser); api.mountFiles(app); // Admin: monday.com import app.post('/api/admin/import/monday/boards', auth.requireUser, auth.requireAdmin, async (req, res) => { try { res.json(await importer.listBoards(String(req.body.token || '').trim())); } catch (e) { res.status(400).json({ error: e.message }); } }); app.post('/api/admin/import/monday', auth.requireUser, auth.requireAdmin, (req, res) => { const token = String(req.body.token || '').trim(); if (!token) return res.status(400).json({ error: 'Paste a monday.com API token' }); if (importer.status.running) return res.status(409).json({ error: 'An import is already running' }); importer.importAll(token, { boardIds: req.body.boardIds }); // runs in the background; poll status res.json({ started: true }); }); app.get('/api/admin/import/monday', auth.requireUser, auth.requireAdmin, (req, res) => res.json(importer.status)); app.use('/api', api.router); app.use('/api', (req, res) => res.status(404).json({ error: 'Not found' })); app.use(express.static(path.join(__dirname, 'public'), { setHeaders: (res, p) => { if (/\.(html|webmanifest)$|sw\.js$/.test(p)) res.set('Cache-Control', 'no-cache'); }, })); // SPA routes app.get(/^\/(?!api|files).*/, (req, res) => res.sendFile(path.join(__dirname, 'public', 'index.html'))); // First run: make sure there's a workspace to land in. if (!get('SELECT 1 FROM workspaces LIMIT 1')) { run('INSERT INTO workspaces(id, name, description, color, position, created_at) VALUES(?,?,?,?,?,?)', id(), process.env.ORG_NAME || 'Waters Church', '', '#579bfc', 1, now()); } automations.scheduleDaily(); // Nightly snapshot of the database into data/backups (keeps 14). VACUUM INTO writes a // consistent copy while the app keeps running; uploads are plain files beside it. const fs = require('fs'); const { db, DATA_DIR, getSetting, setSetting } = require('./src/db'); setInterval(() => { const d = new Date(); const key = d.toISOString().slice(0, 10); if (d.getHours() < 2 || getSetting('lastBackup') === key) return; try { const dir = path.join(DATA_DIR, 'backups'); fs.mkdirSync(dir, { recursive: true }); const file = path.join(dir, `boards-${key}.db`); if (!fs.existsSync(file)) db.prepare('VACUUM INTO ?').run(file); setSetting('lastBackup', key); const old = fs.readdirSync(dir).filter((f) => /^boards-.*\.db$/.test(f)).sort().slice(0, -14); for (const f of old) fs.unlinkSync(path.join(dir, f)); } catch (e) { console.error('backup failed', e.message); } }, 10 * 60_000).unref(); app.listen(PORT, () => console.log(`Waters Boards listening on :${PORT}`));