87 lines
3.6 KiB
JavaScript
87 lines
3.6 KiB
JavaScript
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}`));
|