revert(customer-portal): make the global flag UI-only, drop the kill-switch middleware
This commit is contained in:
+27
-21
@@ -568,30 +568,36 @@ app.use('/api/admin/photo-export', require('./src/routes/adminPhotoExport'));
|
|||||||
app.use('/api/admin/css-templates', require('./src/routes/adminCssTemplates'));
|
app.use('/api/admin/css-templates', require('./src/routes/adminCssTemplates'));
|
||||||
app.use('/api/admin/events', require('./src/routes/adminEventRename'));
|
app.use('/api/admin/events', require('./src/routes/adminEventRename'));
|
||||||
app.use('/api/admin/users', require('./src/routes/adminUsers'));
|
app.use('/api/admin/users', require('./src/routes/adminUsers'));
|
||||||
// Customer portal (#354). The customerPortal feature flag is enforced
|
// Customer portal (#354). The customerPortal feature flag is a
|
||||||
// in TWO places:
|
// VISIBILITY toggle for the admin surface, not a kill switch for
|
||||||
|
// customer access. Enforcement:
|
||||||
|
//
|
||||||
// 1. Frontend: RequireFeature guards + AdminSidebar visibility
|
// 1. Frontend: RequireFeature guards + AdminSidebar visibility
|
||||||
// (handles navigation cleanly when an admin is using the app).
|
// hide the Clients section when the flag is off. Customer-side
|
||||||
// 2. Backend: the requireCustomerPortalEnabled middleware below.
|
// /customer/* surfaces stay reachable.
|
||||||
// Belt-and-braces — a stale tab, a saved bookmark, or any
|
// 2. Backend: NO route-level gate. The admin surface is gated by
|
||||||
// third-party API client trying to hit /api/customer/* or
|
// adminAuth + permission checks (admin still has rights to
|
||||||
// /api/admin/customers/* gets a 410 Gone the moment the toggle
|
// manage customer records even if the section is hidden in
|
||||||
// is flipped off. Includes /api/customer/auth/login: flag off
|
// their UI). The customer surface is gated by customerAuth +
|
||||||
// = nobody can log in until the admin re-enables, including
|
// is_active checks on customer_accounts.
|
||||||
// already-issued customers (their sessions still have valid
|
//
|
||||||
// JWTs but every API call returns 410 → frontend boots them
|
// For close-to-realtime access changes use the dedicated tools:
|
||||||
// out). PR #458 deliberate departure from the prior design
|
// - Revoke a customer's access to ONE gallery → "Manage galleries"
|
||||||
// that left login alive when the rest of the surface was off.
|
// dialog removes the event_customer_assignments row, which
|
||||||
const {
|
// verifyGalleryAccess re-checks on every customer-minted JWT.
|
||||||
requireCustomerPortalEnabled,
|
// - Lock out a customer entirely → "Deactivate" sets is_active=false
|
||||||
requireCustomerPortalEnabledAdmin,
|
// and bumps password_changed_at, killing every outstanding JWT.
|
||||||
} = require('./src/middleware/requireCustomerPortal');
|
// - Toggle per-customer feature surfaces (calendar/quotes/bills)
|
||||||
|
// → toggles on the customer detail page.
|
||||||
app.use('/api/admin/customers', requireCustomerPortalEnabledAdmin, require('./src/routes/adminCustomers'));
|
//
|
||||||
|
// Putting the global flag in the kill-switch role was a mistake — a
|
||||||
|
// stray click in Settings → Features would lock every paying
|
||||||
|
// customer out at once. PR-revert moved the gate back to per-record.
|
||||||
|
app.use('/api/admin/customers', require('./src/routes/adminCustomers'));
|
||||||
// Customer-side surface (#354). Strictly separate from /api/admin/* —
|
// Customer-side surface (#354). Strictly separate from /api/admin/* —
|
||||||
// distinct token type, distinct cookie, distinct middleware.
|
// distinct token type, distinct cookie, distinct middleware.
|
||||||
app.use('/api/customer/auth', requireCustomerPortalEnabled, require('./src/routes/customerAuth'));
|
app.use('/api/customer/auth', require('./src/routes/customerAuth'));
|
||||||
app.use('/api/customer', requireCustomerPortalEnabled, require('./src/routes/customer'));
|
app.use('/api/customer', require('./src/routes/customer'));
|
||||||
app.use('/api/admin/event-types', require('./src/routes/adminEventTypes'));
|
app.use('/api/admin/event-types', require('./src/routes/adminEventTypes'));
|
||||||
app.use('/api/admin/api-tokens', require('./src/routes/adminApiTokens'));
|
app.use('/api/admin/api-tokens', require('./src/routes/adminApiTokens'));
|
||||||
app.use('/api/admin/webhooks', require('./src/routes/adminWebhooks'));
|
app.use('/api/admin/webhooks', require('./src/routes/adminWebhooks'));
|
||||||
|
|||||||
@@ -1,62 +0,0 @@
|
|||||||
/**
|
|
||||||
* 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,
|
|
||||||
};
|
|
||||||
@@ -48,15 +48,20 @@ const TOKEN_TTL_SECONDS = 24 * 60 * 60; // mirrors admin tokens
|
|||||||
|
|
||||||
// ---- login -------------------------------------------------------------
|
// ---- login -------------------------------------------------------------
|
||||||
|
|
||||||
// Flag-gate note: this route IS now gated by the customerPortal feature
|
// The customerPortal feature flag deliberately does NOT gate this route.
|
||||||
// flag via the requireCustomerPortalEnabled middleware mounted in
|
// Flipping the master toggle off in Settings → Features hides the
|
||||||
// server.js (`app.use('/api/customer/auth', requireCustomerPortalEnabled, …)`).
|
// admin-side Clients section (sidebar entry, /admin/clients pages) but
|
||||||
// When the admin flips the toggle off in Settings → Features, every
|
// must not revoke access for customers who already accepted an
|
||||||
// customer-side endpoint — including login — returns 410. The previous
|
// invitation — that would mean a stray click in the Features tab
|
||||||
// design left login reachable while the rest of the surface was gated;
|
// locks every paying customer out at once.
|
||||||
// that was confusing and asymmetric. Single source of truth wins.
|
//
|
||||||
// To lock out a specific customer without disabling the feature for
|
// To revoke access at the customer level, use the per-record tools:
|
||||||
// everyone, deactivate the account (customer_accounts.is_active = false).
|
// - "Deactivate" on the customer detail page → sets
|
||||||
|
// customer_accounts.is_active = false AND bumps password_changed_at,
|
||||||
|
// which customerAuth rejects below + on every protected route.
|
||||||
|
// - "Manage galleries" dialog → removes event_customer_assignments
|
||||||
|
// rows, which verifyGalleryAccess re-checks on customer-minted
|
||||||
|
// gallery JWTs (instant per-gallery revocation).
|
||||||
router.post('/login', [
|
router.post('/login', [
|
||||||
body('email').isEmail().normalizeEmail().withMessage('Valid email is required'),
|
body('email').isEmail().normalizeEmail().withMessage('Valid email is required'),
|
||||||
body('password').isString().notEmpty(),
|
body('password').isString().notEmpty(),
|
||||||
|
|||||||
Reference in New Issue
Block a user