Modal scroll fix; per-account editor/viewer roles
- Fix account settings modal overflow: add max-height to .modal, make
.modal-body flex/scrollable, widen #acct-settings-modal to 620px
- Add role column to user_accounts (editor|viewer) with migration;
existing assignments promoted to editor
- New isEditorForAccount() in auth middleware for per-account write checks
- Replace global requireEditor with per-account checks in checks.js,
deposits.js, pdf.js, deposit-pdf.js, qbo-import.js
- GET /api/accounts now returns user_role per account
- users.js returns {account_id, role} per assignment; POST/PUT accept
accounts as [{id, role}]
- Frontend: state.accountRole tracks effective role for active account;
applyRoleUI and renderRow use it; user management shows role dropdown
per account assignment
This commit is contained in:
+19
-5
@@ -3,7 +3,7 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const db = require('../db/database');
|
||||
const { requireEditor, canAccessAccount } = require('../middleware/auth');
|
||||
const { canAccessAccount, isEditorForAccount } = require('../middleware/auth');
|
||||
|
||||
// Helper: resolve account_id from a check id (for edit/delete access checks)
|
||||
function checkAccountId(checkId) {
|
||||
@@ -47,13 +47,16 @@ router.get('/:id', (req, res) => {
|
||||
// TODO: Add payee address book -- store and recall payee name + address lines, autocomplete on new check form
|
||||
|
||||
// POST /api/checks - create a new check (editor+)
|
||||
router.post('/', requireEditor, (req, res) => {
|
||||
router.post('/', (req, res) => {
|
||||
const { account_id, payee, amount, check_date, memo, note1, note2,
|
||||
payee_address1, payee_address2, payee_address3, payee_address4 } = req.body;
|
||||
|
||||
if (!account_id || !payee || !amount || !check_date) {
|
||||
return res.status(400).json({ error: 'account_id, payee, amount, and check_date are required' });
|
||||
}
|
||||
if (!isEditorForAccount(req.session, parseInt(account_id, 10))) {
|
||||
return res.status(403).json({ error: 'Write access required.' });
|
||||
}
|
||||
|
||||
const account = db.prepare('SELECT current_check_no FROM account WHERE id = ?').get(account_id);
|
||||
if (!account) return res.status(400).json({ error: 'Account not found.' });
|
||||
@@ -86,9 +89,12 @@ router.post('/', requireEditor, (req, res) => {
|
||||
});
|
||||
|
||||
// PUT /api/checks/:id - update a check (editor+)
|
||||
router.put('/:id', requireEditor, (req, res) => {
|
||||
router.put('/: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 (!isEditorForAccount(req.session, check.account_id)) {
|
||||
return res.status(403).json({ error: 'Write access required.' });
|
||||
}
|
||||
|
||||
const { payee, amount, check_date, memo, note1, note2,
|
||||
payee_address1, payee_address2, payee_address3, payee_address4 } = req.body;
|
||||
@@ -116,19 +122,27 @@ router.put('/:id', requireEditor, (req, res) => {
|
||||
});
|
||||
|
||||
// DELETE /api/checks/:id (editor+)
|
||||
router.delete('/:id', requireEditor, (req, res) => {
|
||||
router.delete('/: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 (!isEditorForAccount(req.session, check.account_id)) {
|
||||
return res.status(403).json({ error: 'Write access required.' });
|
||||
}
|
||||
db.prepare('DELETE FROM checks WHERE id = ?').run(req.params.id);
|
||||
res.status(204).send();
|
||||
});
|
||||
|
||||
// POST /api/checks/mark-printed (editor+)
|
||||
router.post('/mark-printed', requireEditor, (req, res) => {
|
||||
router.post('/mark-printed', (req, res) => {
|
||||
const { ids } = req.body;
|
||||
if (!Array.isArray(ids) || ids.length === 0) {
|
||||
return res.status(400).json({ error: 'ids array required' });
|
||||
}
|
||||
// Verify editor access via the first check's account
|
||||
const first = db.prepare('SELECT account_id FROM checks WHERE id = ?').get(ids[0]);
|
||||
if (!first || !isEditorForAccount(req.session, first.account_id)) {
|
||||
return res.status(403).json({ error: 'Write access required.' });
|
||||
}
|
||||
const placeholders = ids.map(() => '?').join(',');
|
||||
db.prepare(`UPDATE checks SET printed = 1 WHERE id IN (${placeholders})`).run(...ids);
|
||||
res.json({ updated: ids.length });
|
||||
|
||||
@@ -4,6 +4,7 @@ const express = require('express');
|
||||
const router = express.Router();
|
||||
const db = require('../db/database');
|
||||
const { generateDepositPdf } = require('../services/depositPdfService');
|
||||
const { isEditorForAccount } = require('../middleware/auth');
|
||||
|
||||
// POST /api/deposit-pdf
|
||||
// Body: { depositId, type: 'slip' | 'report', mark_printed: true }
|
||||
@@ -17,6 +18,9 @@ router.post('/', async (req, res) => {
|
||||
|
||||
const deposit = db.prepare('SELECT * FROM deposits WHERE id = ?').get(depositId);
|
||||
if (!deposit) return res.status(404).json({ error: 'Deposit not found.' });
|
||||
if (!isEditorForAccount(req.session, deposit.account_id)) {
|
||||
return res.status(403).json({ error: 'Write access required.' });
|
||||
}
|
||||
|
||||
const account = db.prepare('SELECT * FROM account WHERE id = ?').get(deposit.account_id);
|
||||
if (!account) return res.status(404).json({ error: 'Account not found.' });
|
||||
|
||||
+20
-7
@@ -3,7 +3,7 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const db = require('../db/database');
|
||||
const { requireEditor, canAccessAccount } = require('../middleware/auth');
|
||||
const { canAccessAccount, isEditorForAccount } = require('../middleware/auth');
|
||||
|
||||
// Helper: fetch deposit with items
|
||||
function getDepositWithItems(id) {
|
||||
@@ -42,10 +42,13 @@ router.get('/:id', (req, res) => {
|
||||
});
|
||||
|
||||
// POST /api/deposits
|
||||
router.post('/', requireEditor, (req, res) => {
|
||||
router.post('/', (req, res) => {
|
||||
const { account_id, deposit_date, currency, coin, cash_back, items } = req.body;
|
||||
if (!account_id) return res.status(400).json({ error: 'account_id is required.' });
|
||||
if (!deposit_date) return res.status(400).json({ error: 'deposit_date is required.' });
|
||||
if (!isEditorForAccount(req.session, parseInt(account_id, 10))) {
|
||||
return res.status(403).json({ error: 'Write access required.' });
|
||||
}
|
||||
|
||||
const insert = db.transaction(() => {
|
||||
const result = db.prepare(`
|
||||
@@ -86,9 +89,12 @@ router.post('/', requireEditor, (req, res) => {
|
||||
});
|
||||
|
||||
// PUT /api/deposits/:id
|
||||
router.put('/:id', requireEditor, (req, res) => {
|
||||
const existing = db.prepare('SELECT id FROM deposits WHERE id = ?').get(req.params.id);
|
||||
router.put('/:id', (req, res) => {
|
||||
const existing = db.prepare('SELECT id, account_id FROM deposits WHERE id = ?').get(req.params.id);
|
||||
if (!existing) return res.status(404).json({ error: 'Deposit not found.' });
|
||||
if (!isEditorForAccount(req.session, existing.account_id)) {
|
||||
return res.status(403).json({ error: 'Write access required.' });
|
||||
}
|
||||
|
||||
const { deposit_date, currency, coin, cash_back, items } = req.body;
|
||||
if (!deposit_date) return res.status(400).json({ error: 'deposit_date is required.' });
|
||||
@@ -129,16 +135,23 @@ router.put('/:id', requireEditor, (req, res) => {
|
||||
});
|
||||
|
||||
// DELETE /api/deposits/:id
|
||||
router.delete('/:id', requireEditor, (req, res) => {
|
||||
const existing = db.prepare('SELECT id FROM deposits WHERE id = ?').get(req.params.id);
|
||||
router.delete('/:id', (req, res) => {
|
||||
const existing = db.prepare('SELECT id, account_id FROM deposits WHERE id = ?').get(req.params.id);
|
||||
if (!existing) return res.status(404).json({ error: 'Deposit not found.' });
|
||||
if (!isEditorForAccount(req.session, existing.account_id)) {
|
||||
return res.status(403).json({ error: 'Write access required.' });
|
||||
}
|
||||
// deposit_items deleted via ON DELETE CASCADE
|
||||
db.prepare('DELETE FROM deposits WHERE id = ?').run(req.params.id);
|
||||
res.status(204).end();
|
||||
});
|
||||
|
||||
// PATCH /api/deposits/:id/mark-printed
|
||||
router.patch('/:id/mark-printed', requireEditor, (req, res) => {
|
||||
router.patch('/:id/mark-printed', (req, res) => {
|
||||
const existing = db.prepare('SELECT account_id FROM deposits WHERE id = ?').get(req.params.id);
|
||||
if (!existing || !isEditorForAccount(req.session, existing.account_id)) {
|
||||
return res.status(403).json({ error: 'Write access required.' });
|
||||
}
|
||||
db.prepare('UPDATE deposits SET printed = 1 WHERE id = ?').run(req.params.id);
|
||||
res.json({ ok: true });
|
||||
});
|
||||
|
||||
@@ -4,6 +4,7 @@ const express = require('express');
|
||||
const router = express.Router();
|
||||
const db = require('../db/database');
|
||||
const { generateCheckPdf } = require('../services/pdfService');
|
||||
const { isEditorForAccount } = require('../middleware/auth');
|
||||
|
||||
/**
|
||||
* POST /api/pdf
|
||||
@@ -19,6 +20,9 @@ router.post('/', async (req, res) => {
|
||||
if (!Array.isArray(checkIds) || checkIds.length === 0) {
|
||||
return res.status(400).json({ error: 'checkIds must be a non-empty array' });
|
||||
}
|
||||
if (!isEditorForAccount(req.session, parseInt(account_id, 10))) {
|
||||
return res.status(403).json({ error: 'Write access required.' });
|
||||
}
|
||||
|
||||
// Fetch checks in the order provided
|
||||
let checks;
|
||||
|
||||
@@ -7,6 +7,7 @@ const os = require('os');
|
||||
const fs = require('fs');
|
||||
|
||||
const upload = multer({ dest: os.tmpdir() });
|
||||
const { isEditorForAccount } = require('../middleware/auth');
|
||||
|
||||
// ── CSV helpers ───────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -291,6 +292,9 @@ router.post('/confirm', express.json(), (req, res) => {
|
||||
if (!type || !records || !account_id) {
|
||||
return res.status(400).json({ error: 'Missing required fields: type, records, account_id.' });
|
||||
}
|
||||
if (!isEditorForAccount(req.session, parseInt(account_id, 10))) {
|
||||
return res.status(403).json({ error: 'Write access required.' });
|
||||
}
|
||||
if (type !== 'checks' && type !== 'deposits') {
|
||||
return res.status(400).json({ error: 'Invalid type.' });
|
||||
}
|
||||
|
||||
+6
-8
@@ -12,8 +12,7 @@ router.use(requireAuth, requireAdmin);
|
||||
function userWithAccounts(id) {
|
||||
const user = db.prepare('SELECT id, username, role, created_at FROM users WHERE id = ?').get(id);
|
||||
if (!user) return null;
|
||||
user.accounts = db.prepare('SELECT account_id FROM user_accounts WHERE user_id = ?')
|
||||
.all(id).map(r => r.account_id);
|
||||
user.accounts = db.prepare('SELECT account_id, role FROM user_accounts WHERE user_id = ?').all(id);
|
||||
return user;
|
||||
}
|
||||
|
||||
@@ -21,8 +20,7 @@ function userWithAccounts(id) {
|
||||
router.get('/', (req, res) => {
|
||||
const users = db.prepare('SELECT id, username, role, created_at FROM users ORDER BY id ASC').all();
|
||||
users.forEach(u => {
|
||||
u.accounts = db.prepare('SELECT account_id FROM user_accounts WHERE user_id = ?')
|
||||
.all(u.id).map(r => r.account_id);
|
||||
u.accounts = db.prepare('SELECT account_id, role FROM user_accounts WHERE user_id = ?').all(u.id);
|
||||
});
|
||||
res.json(users);
|
||||
});
|
||||
@@ -48,8 +46,8 @@ router.post('/', async (req, res) => {
|
||||
}
|
||||
|
||||
if (role !== 'admin' && Array.isArray(accounts) && accounts.length > 0) {
|
||||
const stmt = db.prepare('INSERT OR IGNORE INTO user_accounts (user_id, account_id) VALUES (?, ?)');
|
||||
accounts.forEach(aid => stmt.run(userId, aid));
|
||||
const stmt = db.prepare('INSERT OR IGNORE INTO user_accounts (user_id, account_id, role) VALUES (?, ?, ?)');
|
||||
accounts.forEach(a => stmt.run(userId, a.id, a.role === 'editor' ? 'editor' : 'viewer'));
|
||||
}
|
||||
|
||||
res.status(201).json(userWithAccounts(userId));
|
||||
@@ -92,8 +90,8 @@ router.put('/:id', async (req, res) => {
|
||||
db.prepare('DELETE FROM user_accounts WHERE user_id = ?').run(req.params.id);
|
||||
const effectiveRole = role || user.role;
|
||||
if (effectiveRole !== 'admin' && accounts.length > 0) {
|
||||
const stmt = db.prepare('INSERT OR IGNORE INTO user_accounts (user_id, account_id) VALUES (?, ?)');
|
||||
accounts.forEach(aid => stmt.run(req.params.id, aid));
|
||||
const stmt = db.prepare('INSERT OR IGNORE INTO user_accounts (user_id, account_id, role) VALUES (?, ?, ?)');
|
||||
accounts.forEach(a => stmt.run(req.params.id, a.id, a.role === 'editor' ? 'editor' : 'viewer'));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user