diff --git a/backend/migrations/core/095_add_customer_portal_flag.js b/backend/migrations/core/095_add_customer_portal_flag.js index c1c36e5d..d36c97ad 100644 --- a/backend/migrations/core/095_add_customer_portal_flag.js +++ b/backend/migrations/core/095_add_customer_portal_flag.js @@ -1,5 +1,5 @@ /** - * Migration 094: Add `customerPortal` to feature_flags. + * Migration 095: Add `customerPortal` to feature_flags. * * The customer portal (#354) is the foundation feature for the * customer-side UI surface — login, dashboard, profile, password reset, diff --git a/backend/migrations/core/090_backfill_photo_dimensions_v2.js b/backend/migrations/core/096_backfill_photo_dimensions_v2.js similarity index 100% rename from backend/migrations/core/090_backfill_photo_dimensions_v2.js rename to backend/migrations/core/096_backfill_photo_dimensions_v2.js diff --git a/backend/src/routes/customerAuth.js b/backend/src/routes/customerAuth.js index 5321f427..2f459177 100644 --- a/backend/src/routes/customerAuth.js +++ b/backend/src/routes/customerAuth.js @@ -48,6 +48,12 @@ const TOKEN_TTL_SECONDS = 24 * 60 * 60; // mirrors admin tokens // ---- login ------------------------------------------------------------- +// The customerPortal feature flag deliberately does NOT gate this route. +// Flipping the master toggle off in Settings → Features hides the admin +// UI surface (sidebar entry, /admin/customers page) but does not revoke +// access for customers who already accepted an invitation. To lock out +// existing customers, deactivate their accounts individually +// (customer_accounts.is_active = false) — which IS enforced below. router.post('/login', [ body('email').isEmail().normalizeEmail().withMessage('Valid email is required'), body('password').isString().notEmpty(), diff --git a/backend/src/services/customerAccountsService.js b/backend/src/services/customerAccountsService.js index 3f48593b..1271f58f 100644 --- a/backend/src/services/customerAccountsService.js +++ b/backend/src/services/customerAccountsService.js @@ -220,6 +220,12 @@ async function acceptInvitation({ token, name, password, profile }) { country_code: merged.country_code || null, password_hash: passwordHash, is_active: formatBoolean(true), + // must_change_password is decorative today — accept-invite always + // sets a customer-chosen password, so this flag is never true and + // customerAuth doesn't read it. TODO when we ship an "admin + // pre-loads a temporary password" flow: surface a code in the + // login response (mirroring adminAuth's MUST_CHANGE_PASSWORD) and + // add a /change-password gate to customerAuth. must_change_password: formatBoolean(false), // Leave password_changed_at NULL on initial accept. Setting it here // creates a millisecond/second-rounding race with the JWT issued