diff --git a/package.json b/package.json
index 8da1b68..bbc8824 100644
--- a/package.json
+++ b/package.json
@@ -5,7 +5,7 @@
"main": "src/app.js",
"scripts": {
"start": "node src/app.js",
- "dev": "nodemon src/app.js",
+ "dev": "nodemon --exec \"node --env-file=.env\" src/app.js",
"migrate": "node migrations/import-mdb.js"
},
"dependencies": {
diff --git a/public/index.html b/public/index.html
index 8c53200..519007c 100644
--- a/public/index.html
+++ b/public/index.html
@@ -20,7 +20,7 @@
-
+
@@ -621,7 +621,7 @@
-
+
@@ -652,7 +652,7 @@
-
+
diff --git a/src/app.js b/src/app.js
index 16139ee..e0a5fd7 100644
--- a/src/app.js
+++ b/src/app.js
@@ -18,12 +18,11 @@ const upload = multer({ dest: os.tmpdir() });
// ── Session store (SQLite-backed, no extra packages) ──────────────────────────
const SessionStore = require('./lib/SessionStore');
-if (!process.env.SESSION_SECRET && process.env.NODE_ENV === 'production') {
- console.error('[fatal] SESSION_SECRET environment variable must be set in production. Exiting.');
+if (!process.env.SESSION_SECRET) {
+ console.error('[fatal] SESSION_SECRET environment variable is not set. See .env.example. Exiting.');
process.exit(1);
}
-const SESSION_SECRET = process.env.SESSION_SECRET ||
- (() => { console.warn('[warn] SESSION_SECRET not set — using random secret (sessions will reset on restart)'); return crypto.randomBytes(32).toString('hex'); })();
+const SESSION_SECRET = process.env.SESSION_SECRET;
const SESSION_MAX_AGE_MS = (parseInt(process.env.SESSION_MAX_AGE_HOURS, 10) || 168) * 60 * 60 * 1000;
diff --git a/src/routes/checks.js b/src/routes/checks.js
index 0bbcffa..4099930 100644
--- a/src/routes/checks.js
+++ b/src/routes/checks.js
@@ -41,6 +41,9 @@ router.get('/', (req, res) => {
router.get('/:id', (req, res) => {
const check = db.prepare('SELECT * FROM checks WHERE id = ?').get(req.params.id);
if (!check) return res.status(404).json({ error: 'Check not found' });
+ if (!canAccessAccount(req.session, check.account_id)) {
+ return res.status(403).json({ error: 'Access denied.' });
+ }
res.json(check);
});
@@ -99,6 +102,7 @@ router.put('/:id', (req, res) => {
if (!isEditorForAccount(req.session, check.account_id)) {
return res.status(403).json({ error: 'Write access required.' });
}
+ if (check.printed) return res.status(409).json({ error: 'Cannot modify a printed check.' });
const { payee, amount, check_date, memo, note1, note2,
payee_address1, payee_address2, payee_address3, payee_address4 } = req.body;
@@ -140,6 +144,7 @@ router.delete('/:id', (req, res) => {
if (!isEditorForAccount(req.session, check.account_id)) {
return res.status(403).json({ error: 'Write access required.' });
}
+ if (check.printed) return res.status(409).json({ error: 'Cannot delete a printed check.' });
db.prepare('DELETE FROM checks WHERE id = ?').run(req.params.id);
res.status(204).send();
});
diff --git a/src/routes/deposits.js b/src/routes/deposits.js
index 01fcc60..5236607 100644
--- a/src/routes/deposits.js
+++ b/src/routes/deposits.js
@@ -38,6 +38,9 @@ router.get('/', (req, res) => {
router.get('/:id', (req, res) => {
const deposit = getDepositWithItems(req.params.id);
if (!deposit) return res.status(404).json({ error: 'Deposit not found.' });
+ if (!canAccessAccount(req.session, deposit.account_id)) {
+ return res.status(403).json({ error: 'Access denied.' });
+ }
res.json(deposit);
});
diff --git a/src/routes/pdf.js b/src/routes/pdf.js
index 8f30346..36940b8 100644
--- a/src/routes/pdf.js
+++ b/src/routes/pdf.js
@@ -17,9 +17,13 @@ const { isEditorForAccount } = require('../middleware/auth');
router.post('/', async (req, res) => {
const { checkIds, account_id } = req.body;
+ const MAX_CHECKS_PER_JOB = 300; // 100 pages × 3 checks per page
if (!Array.isArray(checkIds) || checkIds.length === 0) {
return res.status(400).json({ error: 'checkIds must be a non-empty array' });
}
+ if (checkIds.length > MAX_CHECKS_PER_JOB) {
+ return res.status(400).json({ error: `Cannot generate more than ${MAX_CHECKS_PER_JOB} checks per PDF job.` });
+ }
const resolvedAccountId = parseInt(account_id, 10);
if (!isEditorForAccount(req.session, resolvedAccountId)) {
return res.status(403).json({ error: 'Write access required.' });
@@ -62,7 +66,7 @@ router.post('/', async (req, res) => {
res.send(pdfBuffer);
} catch (err) {
console.error('PDF generation error:', err);
- res.status(500).json({ error: 'PDF generation failed', detail: err.message });
+ res.status(500).json({ error: 'PDF generation failed.' });
}
});
diff --git a/src/routes/qbo-import.js b/src/routes/qbo-import.js
index 454eda3..5413db4 100644
--- a/src/routes/qbo-import.js
+++ b/src/routes/qbo-import.js
@@ -309,6 +309,9 @@ router.post('/confirm', express.json(), (req, res) => {
if (!Array.isArray(records) || records.length === 0) {
return res.status(400).json({ error: 'No records provided.' });
}
+ if (records.length > 1000) {
+ return res.status(400).json({ error: 'Cannot import more than 1000 records at a time.' });
+ }
const db = require('../db/database');
try {