a93b6dc232
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.
56 lines
2.0 KiB
JavaScript
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 };
|