Harden the server and add a Links page

Security headers, cross-site write refusal, atomic JSON writes, admin backup download, /healthz and Docker health check, OnlyOffice requires a JWT secret, npm test, CI audit, arm64 hub image. New Links tab for personal bookmarks, saved with per-person settings.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
nmemmertandClaude Sonnet 5.5 committed 2026-10-22 07:43:34 -04:00
1 parent b0d3aec811
commit 1f8c9493b7
16 files changed
+147 -25

No files matched your search

+3 -2
View File
@@ -1,6 +1,7 @@
// Calendar agenda from private ICS feeds (Google "secret address", Outlook/iCloud published calendars).
// Feeds are fetched server-side, cached briefly, parsed, and recurring events are expanded for the requested window.
const fs = require('fs');
const atomicWrite = require('./atomic');
const path = require('path');
const dns = require('dns').promises;
const net = require('net');
@@ -12,7 +13,7 @@ const users = require('./users');
const here = f => path.join(users.dir(), f); // each person's own files
const FILE_NAME = 'calendars.json';
const load = () => { try { return JSON.parse(fs.readFileSync(here(FILE_NAME), 'utf8')); } catch { return []; } };
const save = v => fs.writeFileSync(here(FILE_NAME), JSON.stringify(v, null, 2), { mode: 0o600 });
const save = v => atomicWrite(here(FILE_NAME), JSON.stringify(v, null, 2), { mode: 0o600 });
// ---- Fetching, with SSRF protection: https only, and never to loopback/private/link-local addresses.
const isPrivate = ip => {
@@ -224,7 +225,7 @@ function expand(events, win) { // win: { from, to (ms), fd, td (local YYYY-MM-DD
// ---- The NE Hub calendar: events created in the app, stored here, and published as a subscribe feed.
const EV_FILE = 'events.json', FEED_FILE = 'feed.json';
const readJ = (f, d) => { try { return JSON.parse(fs.readFileSync(f, 'utf8')); } catch { return d; } };
const writeJ = (f, v) => fs.writeFileSync(here(f), JSON.stringify(v, null, 2), { mode: 0o600 });
const writeJ = (f, v) => atomicWrite(here(f), JSON.stringify(v, null, 2), { mode: 0o600 });
const loadLocal = () => readJ(here(EV_FILE), []);
const LOCAL = { id: 'local', name: 'NE Hub', color: '#4f7cff' };
function feedToken(reset) {