-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathserver.js
More file actions
executable file
·135 lines (121 loc) · 5.74 KB
/
Copy pathserver.js
File metadata and controls
executable file
·135 lines (121 loc) · 5.74 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
// NoteKeep - self-hosted notes synced to your own Nextcloud.
// Copyright (C) 2026 MtoseD
// SPDX-License-Identifier: AGPL-3.0-or-later
// Free software under the GNU AGPL v3 or later; see LICENSE. Comes with
// ABSOLUTELY NO WARRANTY. If you run a modified version for others over a
// network, AGPL section 13 requires you to offer them its source.
// NoteKeep server
// Serves the PWA frontend and proxies note data to a Nextcloud WebDAV folder.
// All Nextcloud credentials stay on the server — the browser never talks to
// Nextcloud directly, which sidesteps CORS entirely and keeps the app
// password off every device.
require('dotenv').config();
const express = require('express');
const { createClient } = require('webdav');
const path = require('path');
const PORT = process.env.PORT || 3077;
const NC_URL = process.env.NEXTCLOUD_URL;
const NC_USER = process.env.NEXTCLOUD_USERNAME;
const NC_PASS = process.env.NEXTCLOUD_APP_PASSWORD;
const NOTES_DIR = (process.env.NOTES_FOLDER || 'NoteKeep').replace(/^\/+|\/+$/g, '');
const DATA_FILE = `${NOTES_DIR}/data.json`;
// Optional shared secret so randoms on your network can't read/write your notes.
const APP_TOKEN = process.env.APP_TOKEN || '';
if (!NC_URL || !NC_USER || !NC_PASS) {
console.error('Missing Nextcloud config. Copy .env.example to .env and fill it in.');
process.exit(1);
}
const webdavBase = NC_URL.replace(/\/+$/, '') + '/remote.php/dav/files/' + encodeURIComponent(NC_USER);
const client = createClient(webdavBase, { username: NC_USER, password: NC_PASS });
const app = express();
app.use(express.json({ limit: '10mb' }));
// --- simple auth gate (optional, only active if APP_TOKEN is set) ---
// Scoped to /api/* only: the app shell (HTML/JS/CSS/icons) must stay
// reachable without the token, since the browser's own navigation and the
// service worker's precache requests can't attach custom headers. The
// token protects your notes data at the API layer, which is what matters.
app.use('/api', (req, res, next) => {
if (!APP_TOKEN) return next();
if (req.path === '/ping') return next();
const header = req.get('x-app-token');
if (header === APP_TOKEN) return next();
res.status(401).json({ error: 'unauthorized' });
});
app.get('/api/ping', (req, res) => res.json({ ok: true, tokenRequired: !!APP_TOKEN }));
async function ensureRemoteDir() {
const exists = await client.exists('/' + NOTES_DIR);
if (!exists) await client.createDirectory('/' + NOTES_DIR, { recursive: true });
}
const EMPTY_DATA = { notes: [], labels: [], updatedAt: 0 };
// GET current data + a version stamp (etag/last-modified) the client can
// compare against before pushing changes, to avoid clobbering another
// device's fresher save.
app.get('/api/data', async (req, res) => {
try {
await ensureRemoteDir();
const exists = await client.exists('/' + DATA_FILE);
if (!exists) {
return res.json({ data: EMPTY_DATA, version: null });
}
const stat = await client.stat('/' + DATA_FILE);
const content = await client.getFileContents('/' + DATA_FILE, { format: 'text' });
let data;
try {
data = JSON.parse(content);
} catch (e) {
data = EMPTY_DATA;
}
const version = (stat.etag || stat.lastmod || String(stat.size)) + '';
res.json({ data, version });
} catch (err) {
console.error('GET /api/data failed:', err.message);
res.status(502).json({ error: 'nextcloud_unreachable', detail: err.message });
}
});
// PUT full data blob. Client sends the version it started editing from;
// if the remote has moved on, we tell it instead of silently overwriting.
app.put('/api/data', async (req, res) => {
const { data, baseVersion, force } = req.body || {};
if (!data || typeof data !== 'object') {
return res.status(400).json({ error: 'invalid_payload' });
}
try {
await ensureRemoteDir();
const exists = await client.exists('/' + DATA_FILE);
if (exists && !force) {
const stat = await client.stat('/' + DATA_FILE);
const currentVersion = (stat.etag || stat.lastmod || String(stat.size)) + '';
if (baseVersion && currentVersion !== baseVersion) {
const remoteContent = await client.getFileContents('/' + DATA_FILE, { format: 'text' });
let remoteData;
try { remoteData = JSON.parse(remoteContent); } catch (e) { remoteData = EMPTY_DATA; }
return res.status(409).json({ error: 'conflict', remote: remoteData, version: currentVersion });
}
}
const payload = JSON.stringify({ ...data, updatedAt: Date.now() }, null, 2);
await client.putFileContents('/' + DATA_FILE, payload, { overwrite: true });
const stat = await client.stat('/' + DATA_FILE);
const version = (stat.etag || stat.lastmod || String(stat.size)) + '';
res.json({ ok: true, version });
} catch (err) {
console.error('PUT /api/data failed:', err.message);
res.status(502).json({ error: 'nextcloud_unreachable', detail: err.message });
}
});
// The shell is a handful of small files that change on every deploy, and a
// stale one is worse than a slow one: an old app.js running against a new
// index.html breaks in ways that look nothing like a caching problem. The
// default here was "public, max-age=0", which lets a shared cache — a reverse
// proxy — store the response and, if it is configured to, serve it on. Say
// no-cache instead: still cached, but never reused without revalidating with
// us first. ETag/Last-Modified still make that a cheap 304.
app.use(express.static(path.join(__dirname, 'public'), {
setHeaders: (res) => {
res.setHeader('Cache-Control', 'no-cache, must-revalidate');
},
}));
app.listen(PORT, () => {
console.log(`NoteKeep running on http://localhost:${PORT}`);
console.log(`Nextcloud folder: ${NOTES_DIR} (as user ${NC_USER})`);
if (APP_TOKEN) console.log('App token protection: ON');
});