feat(clients): scaffold top-level Clients section with sub-nav around Accounts
This commit is contained in:
@@ -0,0 +1,62 @@
|
||||
/**
|
||||
* Customer portal feature-flag gate.
|
||||
*
|
||||
* Blocks every /api/customer/* and /api/admin/customers/* endpoint
|
||||
* when the `customerPortal` flag is off. Returns 410 Gone so the
|
||||
* frontend can distinguish "feature has been disabled" from "you
|
||||
* don't have access" (which would be 403) — useful for the customer
|
||||
* dashboard's auto-redirect on a soft-kill scenario.
|
||||
*
|
||||
* Reads the flag via customerAccountsService.isCustomerPortalEnabled
|
||||
* (which itself reads from the maintainer's feature_flags table),
|
||||
* so a single source of truth.
|
||||
*/
|
||||
|
||||
const customerAccountsService = require('../services/customerAccountsService');
|
||||
const logger = require('../utils/logger');
|
||||
|
||||
async function isEnabled() {
|
||||
try {
|
||||
return await customerAccountsService.isCustomerPortalEnabled();
|
||||
} catch (err) {
|
||||
// Defensive: if the lookup throws (DB unavailable, table missing
|
||||
// mid-migration), fail closed so an enabled-by-default fallback
|
||||
// can't accidentally expose customer surfaces during boot.
|
||||
logger.warn('requireCustomerPortal: feature flag lookup failed, treating as off', {
|
||||
error: err?.message,
|
||||
});
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Customer-facing endpoints. Returns 410 with a code the frontend
|
||||
* can interpret to clear stale session storage + redirect to
|
||||
* /admin/login.
|
||||
*/
|
||||
async function requireCustomerPortalEnabled(req, res, next) {
|
||||
if (await isEnabled()) return next();
|
||||
return res.status(410).json({
|
||||
error: 'Customer portal is disabled',
|
||||
code: 'CUSTOMER_PORTAL_DISABLED',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Admin-facing /api/admin/customers/* endpoints. Same gate, same
|
||||
* status code — keeps the contract consistent across both halves of
|
||||
* the customer-portal surface. The sidebar UI already hides the
|
||||
* entry, but a stale tab or direct API call must also be blocked.
|
||||
*/
|
||||
async function requireCustomerPortalEnabledAdmin(req, res, next) {
|
||||
if (await isEnabled()) return next();
|
||||
return res.status(410).json({
|
||||
error: 'Customer portal is disabled',
|
||||
code: 'CUSTOMER_PORTAL_DISABLED',
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
requireCustomerPortalEnabled,
|
||||
requireCustomerPortalEnabledAdmin,
|
||||
};
|
||||
@@ -32,8 +32,14 @@ const KNOWN_FLAGS = [
|
||||
'messaging',
|
||||
'analytics',
|
||||
'userManagement',
|
||||
// Foundation flag for the customer-side surface (#354). See migration
|
||||
// 094 for the seeding rule.
|
||||
// Top-level "Clients" section (#354 follow-up). Parent flag that
|
||||
// gates the /admin/clients/* sidebar entry. customerPortal,
|
||||
// calendar, quotes, bills and messaging are conceptually its
|
||||
// children — when `clients` is off none of them surface in the
|
||||
// admin UI even if their individual flags are on.
|
||||
'clients',
|
||||
// Customer-side portal surface (#354). Gates /customer/* routes
|
||||
// and the Accounts sub-page under Clients. See migration 095.
|
||||
'customerPortal',
|
||||
];
|
||||
|
||||
@@ -49,6 +55,7 @@ const DEFAULT_FLAGS = {
|
||||
messaging: false,
|
||||
analytics: true,
|
||||
userManagement: true,
|
||||
clients: false,
|
||||
};
|
||||
|
||||
async function readAllFlags() {
|
||||
@@ -69,13 +76,27 @@ function applyDependencyRules(flags) {
|
||||
// Sub-features can't outlive their parents.
|
||||
if (out.quotes === false) out.bills = false;
|
||||
if (out.calendar === false) out.calendarBooking = false;
|
||||
// Clients parent flag is DERIVED from its children. Admins don't
|
||||
// toggle it directly in the Features tab — they enable a specific
|
||||
// sub-feature (Accounts today; Calendar/Quotes/Bills/Messaging
|
||||
// later) and the Clients sidebar section lights up automatically.
|
||||
// Computing the value here (rather than only on writes) means GET
|
||||
// /admin/feature-flags also returns a consistent state if the DB
|
||||
// ever drifts (e.g. partial migration run).
|
||||
out.clients = Boolean(
|
||||
out.customerPortal
|
||||
// future siblings (out.calendar || out.quotes || out.bills || out.messaging) go here
|
||||
);
|
||||
return out;
|
||||
}
|
||||
|
||||
router.get('/', adminAuth, requirePermission('settings.view'), async (req, res) => {
|
||||
try {
|
||||
const flags = await readAllFlags();
|
||||
res.json(flags);
|
||||
// Always run the rules so derived flags (e.g. `clients`) and
|
||||
// hard invariants (galleries always on) are consistent even if
|
||||
// the DB row is stale or missing.
|
||||
res.json(applyDependencyRules(flags));
|
||||
} catch (error) {
|
||||
logger.error('Failed to read feature flags', { error: error.message });
|
||||
res.status(500).json({ error: 'Failed to read feature flags' });
|
||||
|
||||
Reference in New Issue
Block a user