Files
waters-boards/server/server.js
T

87 lines
3.6 KiB
JavaScript
Raw Normal View History

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}`));