spendwise 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +119 -0
- package/package.json +62 -0
- package/src/main/api-core.js +231 -0
- package/src/main/auth.js +29 -0
- package/src/main/backup.js +48 -0
- package/src/main/db-open.js +77 -0
- package/src/main/ipc.js +187 -0
- package/src/main/main.js +125 -0
- package/src/main/migrate.js +181 -0
- package/src/main/storage/local.js +55 -0
- package/src/main/update-check.js +99 -0
- package/src/main/webserver.js +294 -0
- package/src/preload.cjs +27 -0
- package/src/renderer/assets/icon.ico +0 -0
- package/src/renderer/assets/icon.png +0 -0
- package/src/renderer/css/app.css +2 -0
- package/src/renderer/fonts/roca.otf +0 -0
- package/src/renderer/fonts/roca.woff +0 -0
- package/src/renderer/fonts/roca.woff2 +0 -0
- package/src/renderer/index.html +1136 -0
- package/src/renderer/js/app.js +185 -0
- package/src/renderer/js/boot.js +4 -0
- package/src/renderer/js/charts.js +116 -0
- package/src/renderer/js/components.js +39 -0
- package/src/renderer/js/helpers.js +76 -0
- package/src/renderer/js/views/insights.js +259 -0
- package/src/renderer/js/views/month.js +687 -0
- package/src/renderer/js/views/settings.js +353 -0
- package/src/renderer/js/web-api.js +50 -0
- package/src/renderer/manifest.json +18 -0
- package/src/renderer/vendor/alpine-collapse.min.js +1 -0
- package/src/renderer/vendor/alpine.min.js +5 -0
- package/src/renderer/vendor/apexcharts.min.js +38 -0
- package/src/server/cli.mjs +164 -0
- package/src/shared/engine.js +728 -0
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/*
|
|
3
|
+
* Headless server: runs the app without Electron, for self-hosting on a box
|
|
4
|
+
* that's always on. Shares the exact same api-core + web server the desktop
|
|
5
|
+
* app uses - the only thing missing is the desktop window (and with it the
|
|
6
|
+
* native-dialog features: export, move database, legacy import; do those in
|
|
7
|
+
* the desktop app against the same db.json, or pre-seed the file).
|
|
8
|
+
*
|
|
9
|
+
* spendwise-server # serve
|
|
10
|
+
* spendwise-server --set-password # set/change the login (then exit)
|
|
11
|
+
* spendwise-server --help
|
|
12
|
+
*
|
|
13
|
+
* Config comes from the environment (all optional):
|
|
14
|
+
* FINANCES_DB_PATH default ./data/db.json
|
|
15
|
+
* FINANCES_PORT / PORT default 4180
|
|
16
|
+
* FINANCES_HOST default 0.0.0.0
|
|
17
|
+
* FINANCES_PASSWORD bootstrap only - sets the login if none exists yet
|
|
18
|
+
* FINANCES_TRUST_PROXY number of reverse proxies in front (1 for the usual
|
|
19
|
+
* nginx/Caddy/Traefik setup). Needed for per-client
|
|
20
|
+
* rate limiting; leave unset when directly exposed.
|
|
21
|
+
* FINANCES_SECURE_COOKIE 1 to force the Secure cookie flag (auto-detected
|
|
22
|
+
* from X-Forwarded-Proto when a proxy is trusted)
|
|
23
|
+
*/
|
|
24
|
+
'use strict';
|
|
25
|
+
|
|
26
|
+
import path from 'node:path';
|
|
27
|
+
import fs from 'node:fs';
|
|
28
|
+
import readline from 'node:readline';
|
|
29
|
+
import { webcrypto } from 'node:crypto';
|
|
30
|
+
import { fileURLToPath } from 'node:url';
|
|
31
|
+
import { openDb } from '../main/db-open.js';
|
|
32
|
+
|
|
33
|
+
// node 18 has no global `crypto` (it became one in 19). The shared engine is
|
|
34
|
+
// UMD and reads uuid() off the global, so without this it would silently drop
|
|
35
|
+
// to its Math.random fallback here - fine for uniqueness, but needlessly weak
|
|
36
|
+
// when a CSPRNG is one import away. Electron and node 20+ already have it.
|
|
37
|
+
if (!globalThis.crypto) globalThis.crypto = webcrypto;
|
|
38
|
+
import { createCore } from '../main/api-core.js';
|
|
39
|
+
import { createWebServer } from '../main/webserver.js';
|
|
40
|
+
|
|
41
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
42
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(__dirname, '../../package.json'), 'utf8'));
|
|
43
|
+
|
|
44
|
+
const argv = process.argv.slice(2);
|
|
45
|
+
const flag = (name) => argv.includes(name);
|
|
46
|
+
const flagValue = (name) => {
|
|
47
|
+
const i = argv.indexOf(name);
|
|
48
|
+
return i !== -1 && argv[i + 1] && !argv[i + 1].startsWith('--') ? argv[i + 1] : null;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
if (flag('--help') || flag('-h')) {
|
|
52
|
+
process.stdout.write(`
|
|
53
|
+
Spend Wise - self-hosted server (v${pkg.version})
|
|
54
|
+
|
|
55
|
+
Usage
|
|
56
|
+
spendwise-server serve the app
|
|
57
|
+
spendwise-server --set-password set or change the login password, then exit
|
|
58
|
+
spendwise-server --version
|
|
59
|
+
|
|
60
|
+
Environment
|
|
61
|
+
FINANCES_DB_PATH path to db.json (default ./data/db.json)
|
|
62
|
+
FINANCES_PORT | PORT listen port (default 4180)
|
|
63
|
+
FINANCES_HOST bind address (default 0.0.0.0)
|
|
64
|
+
FINANCES_PASSWORD sets the login on first run only
|
|
65
|
+
FINANCES_TRUST_PROXY reverse proxies in front (e.g. 1 behind nginx)
|
|
66
|
+
FINANCES_SECURE_COOKIE 1 to force Secure cookies
|
|
67
|
+
|
|
68
|
+
Everyone signs in with the one app password; the desktop app uses the same
|
|
69
|
+
db.json, so you can point it at this file too (one writer at a time - or let
|
|
70
|
+
people co-edit through this server, which merges concurrent edits).
|
|
71
|
+
|
|
72
|
+
`);
|
|
73
|
+
process.exit(0);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (flag('--version') || flag('-v')) {
|
|
77
|
+
process.stdout.write(pkg.version + '\n');
|
|
78
|
+
process.exit(0);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const dbPath = path.resolve(process.env.FINANCES_DB_PATH || 'data/db.json');
|
|
82
|
+
const port = Number(process.env.FINANCES_PORT || process.env.PORT || 4180);
|
|
83
|
+
const host = process.env.FINANCES_HOST || '0.0.0.0';
|
|
84
|
+
|
|
85
|
+
const db = await openDb(dbPath);
|
|
86
|
+
const ctx = { db, dbPath, version: pkg.version };
|
|
87
|
+
const core = createCore(ctx);
|
|
88
|
+
|
|
89
|
+
/** Read a password from a TTY without echoing it, or from piped stdin. */
|
|
90
|
+
function askPassword (prompt) {
|
|
91
|
+
return new Promise((resolve) => {
|
|
92
|
+
if (!process.stdin.isTTY) { // echo "pw" | spendwise-server --set-password
|
|
93
|
+
let data = '';
|
|
94
|
+
process.stdin.on('data', (c) => { data += c; });
|
|
95
|
+
process.stdin.on('end', () => resolve(data.trim()));
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout, terminal: true });
|
|
99
|
+
rl._writeToOutput = function (s) { // mute everything after the prompt
|
|
100
|
+
if (s.includes(prompt)) rl.output.write(prompt);
|
|
101
|
+
};
|
|
102
|
+
rl.question(prompt, (answer) => { rl.output.write('\n'); rl.close(); resolve(answer.trim()); });
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (flag('--set-password')) {
|
|
107
|
+
const existing = core.authHas().hasPassword;
|
|
108
|
+
let next = flagValue('--set-password');
|
|
109
|
+
let current = '';
|
|
110
|
+
if (existing) {
|
|
111
|
+
current = process.env.FINANCES_CURRENT_PASSWORD || await askPassword('Current password: ');
|
|
112
|
+
}
|
|
113
|
+
if (!next) {
|
|
114
|
+
next = await askPassword(existing ? 'New password: ' : 'Password: ');
|
|
115
|
+
const confirm = await askPassword('Confirm: ');
|
|
116
|
+
if (next !== confirm) {
|
|
117
|
+
console.error('Passwords did not match - nothing changed.');
|
|
118
|
+
process.exit(1);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
if (!next) {
|
|
122
|
+
console.error('Empty password - nothing changed. (Removing the password disables web access.)');
|
|
123
|
+
process.exit(1);
|
|
124
|
+
}
|
|
125
|
+
try {
|
|
126
|
+
await core.authSet({ current, next });
|
|
127
|
+
console.log(`Password ${existing ? 'changed' : 'set'} for ${dbPath}`);
|
|
128
|
+
process.exit(0);
|
|
129
|
+
} catch (e) {
|
|
130
|
+
console.error(e.message);
|
|
131
|
+
process.exit(1);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Bootstrap (first run / container start): only ever sets a password when the
|
|
136
|
+
// database has none - it must not clobber one the user later changed.
|
|
137
|
+
if (!core.authHas().hasPassword && process.env.FINANCES_PASSWORD) {
|
|
138
|
+
await core.authSet({ next: process.env.FINANCES_PASSWORD });
|
|
139
|
+
console.log('[auth] password set from FINANCES_PASSWORD');
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const web = createWebServer(ctx, core, {
|
|
143
|
+
host,
|
|
144
|
+
trustProxy: process.env.FINANCES_TRUST_PROXY,
|
|
145
|
+
secureCookie: process.env.FINANCES_SECURE_COOKIE === '1' || process.env.FINANCES_SECURE_COOKIE === 'true',
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
await web.start(port, host);
|
|
149
|
+
|
|
150
|
+
console.log(`Spend Wise v${pkg.version}`);
|
|
151
|
+
console.log(` serving http://${host === '0.0.0.0' ? 'localhost' : host}:${port}`);
|
|
152
|
+
console.log(` database ${dbPath}`);
|
|
153
|
+
if (!core.authHas().hasPassword) {
|
|
154
|
+
console.log('\n ⚠ No app password set - the server will refuse to serve the app until you run:');
|
|
155
|
+
console.log(' spendwise-server --set-password\n');
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const shutdown = async (signal) => {
|
|
159
|
+
console.log(`\n[${signal}] shutting down…`);
|
|
160
|
+
await web.stop();
|
|
161
|
+
process.exit(0);
|
|
162
|
+
};
|
|
163
|
+
process.on('SIGINT', () => shutdown('SIGINT'));
|
|
164
|
+
process.on('SIGTERM', () => shutdown('SIGTERM'));
|