Files
picpeak/backend/src/middleware/requireFeatureFlag.js
T
Luca a93b6dc232 fix(accounting): PR #622 concerns — flag-cache, customer master gate, VAT-unconfigured, helpers, page cap
1. requireFeatureFlag now caches each flag for 10s (the accounting area is 10+
   gated endpoints); PUT /admin/feature-flags invalidates the cache so toggles
   still take effect immediately.
2. Customer routes (/quotes, /invoices, /contracts + their PDFs) now gate via
   getEffectiveFeaturesForCustomer — the global MASTER flag AND the per-customer
   override — instead of the per-customer column alone, via a shared
   customerFeatureAllowed() helper. Admin disabling a feature globally is now
   honoured for customers too.
4. Tax-report VAT-payable: when accounting_vat_registered is UNSET, stop guessing
   from grandTotalVat>0 (a zero-output-VAT quarter silently flipped to "not
   registered" and hid the reclaim). Treat null as "not configured":
   vatPayableMinor=null + vatRegistrationConfigured=false; the UI renders "—" and
   a "configure VAT registration" warning. Tests updated.
5. Shared upsertAppSetting() in utils/appSettings — the two adminSettings upsert
   loops use it, so the app_settings created_at class can't be re-introduced.
6. PDF rasterise per-file bound: getRenderedPagePath refuses pages beyond
   MAX_RENDERABLE_PAGES (200); page_count is capped to match at ingest, so a
   hostile high-page PDF can't drive an unbounded pager.
7. (no code) original_filename is only rendered via auto-escaped JSX; the two
   dangerouslySetInnerHTML sites are admin-authored content — paranoia pass clean.

Concerns 3 (foreign-VAT reclaim-country) and 8 (imap_pass plaintext) are PR-reply
/ doc items, addressed in the PR response, not code.
2026-06-16 18:33:47 +02:00

56 lines
2.0 KiB
JavaScript

/**
* requireFeatureFlag(key, code?) — 403 when the named `feature_flags` row is off.
*
* Belt-and-braces gate for admin routes whose feature can be toggled in
* Settings → Features. The frontend hides disabled surfaces, but a direct API
* hit must still be refused so a disabled feature is never actable. Mirrors the
* truthy logic feature_flags uses everywhere (true | 1 | '1').
*
* Cached: the accounting area alone is 10+ gated endpoints and the dashboard
* polls several, so a per-request DB read is wasteful. Flags change rarely and
* only via `PUT /admin/feature-flags`, which calls invalidateFeatureFlagCache()
* — so a short TTL is belt-and-braces against any other mutation path.
*
* Several route files (adminLedger, adminExpenses) predate this and define an
* identical local `requireFlag`; new gates should import this instead.
*/
const { db } = require('../database/db');
const TTL_MS = 10_000;
const cache = new Map(); // key -> { enabled, expires }
function flagEnabledFromRow(row) {
return !!(row && (row.value === true || row.value === 1 || row.value === '1'));
}
async function isFeatureEnabled(key) {
const now = Date.now();
const hit = cache.get(key);
if (hit && hit.expires > now) return hit.enabled;
const row = await db('feature_flags').where({ key }).first();
const enabled = flagEnabledFromRow(row);
cache.set(key, { enabled, expires: now + TTL_MS });
return enabled;
}
/** Clear the flag cache — call after any write to feature_flags. */
function invalidateFeatureFlagCache() {
cache.clear();
}
function requireFeatureFlag(key, code) {
return async (req, res, next) => {
try {
if (await isFeatureEnabled(key)) return next();
return res.status(403).json({
error: `${key} feature is disabled`,
code: code || `${key.replace(/([a-z])([A-Z])/g, '$1_$2').toUpperCase()}_DISABLED`,
});
} catch (err) {
return next(err);
}
};
}
module.exports = { requireFeatureFlag, isFeatureEnabled, invalidateFeatureFlagCache };