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:
2026-03-18 23:31:23 -06:00
parent 764def4f7d
commit af88549ad8
12 changed files with 137 additions and 41 deletions
+20 -7
View File
@@ -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 });
});