diff --git a/.env.example b/.env.example index fc13357..ea53378 100644 --- a/.env.example +++ b/.env.example @@ -11,6 +11,12 @@ # instance never emits mail from a domain that isn't yours. SELF_HOSTED=true +# Hide the Subscription/billing UI (nav item + pricing cards) and bounce #/billing to +# the dashboard. Opt-in; default off (billing shown). For instances that bill customers +# externally and don't sell plans through the app. UI-only — does not change SELF_HOSTED +# or disable any /api/subscription endpoints. +HIDE_BILLING=true + # Close public self-service registration — for instances where all accounts are # provisioned by your team (admin "Add user" / invites). When true, the public # signup route is blocked (OAuth auto-signup with it) AND the login page hides diff --git a/README.md b/README.md index 2b11264..ae9a341 100644 --- a/README.md +++ b/README.md @@ -109,6 +109,7 @@ Schema migrations run automatically on first boot — no manual migration comman | `HTTPS_PORT` | HTTPS port (used when SSL certs are present) | `3443` | | `NODE_ENV` | Runtime env (`production` enables Express production optimizations + stricter error handling) | _(none)_ | | `SELF_HOSTED` | First user gets all features unlocked | `false` | +| `HIDE_BILLING` | Hide the Subscription nav item + billing view; `#/billing` redirects to the dashboard (UI-only, opt-in) | `false` | | `DISABLE_REGISTRATION` | Block new account creation (including OAuth auto-signup). First-user setup on an empty DB is still allowed. | `false` | | `DISABLE_HOMEPAGE` | Redirect `/` to `/app` instead of serving the marketing landing page. For internal-only self-hosted deployments. | `false` | | `APP_URL` | Your public URL (used for Stripe callbacks and invite-accept URLs in emailed invites) | _(none)_ | diff --git a/frontend/index.html b/frontend/index.html index cec2974..89c14f9 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -132,7 +132,7 @@ Settings -
  • +
  • diff --git a/frontend/js/app.js b/frontend/js/app.js index 12e2152..a62ac8c 100644 --- a/frontend/js/app.js +++ b/frontend/js/app.js @@ -458,8 +458,17 @@ function route() { currentView = settings; settings.render(app); } else if (hash === '#/billing') { - currentView = billing; - billing.render(app); + // #116: when HIDE_BILLING is set, a direct #/billing navigation is bounced to the + // dashboard. replaceState (not a hash assignment) so it doesn't add a history entry + // — the back button skips over it instead of looping back into the guard. + if (getCurrentUser()?.hide_billing) { + history.replaceState(null, '', window.location.pathname + '#/'); + currentView = dashboard; + dashboard.render(app); + } else { + currentView = billing; + billing.render(app); + } } else { currentView = dashboard; dashboard.render(app); @@ -474,6 +483,11 @@ function updateSidebarUser() { const adminNav = document.getElementById('adminNavItem'); if (adminNav) adminNav.style.display = isPlatformAdmin(user) ? '' : 'none'; + // #116: hide the Subscription nav item when HIDE_BILLING is set (surfaced on /me). + // Runs at boot from the cached user (no flash on warm loads) and again after /me. + const billingNav = document.getElementById('billingNavItem'); + if (billingNav) billingNav.style.display = user.hide_billing ? 'none' : ''; + let userEl = document.getElementById('sidebarUser'); if (!userEl) { const footer = document.querySelector('.sidebar-footer'); diff --git a/server/config.js b/server/config.js index f86c162..83b895f 100644 --- a/server/config.js +++ b/server/config.js @@ -74,6 +74,10 @@ module.exports = { graphDevRestrictTo: process.env.GRAPH_DEV_RESTRICT_TO || '', // Self-hosted mode: if true, first user gets enterprise plan and no billing selfHosted: process.env.SELF_HOSTED === 'true', + // #116: opt-in UI gate. When true, hides the Subscription nav item + billing view + // and bounces #/billing to the dashboard. Default off, so existing deployments are + // unchanged. UI-only — /api/subscription/* stays in place (internal usage reads). + hideBilling: process.env.HIDE_BILLING === 'true', // Disable public registration (OAuth auto-signup is also blocked when set). // First-user setup is still allowed so a fresh install can be initialized. disableRegistration: ['true', '1'].includes(String(process.env.DISABLE_REGISTRATION || '').toLowerCase()), diff --git a/server/routes/auth.js b/server/routes/auth.js index 7c35f3e..70cf480 100644 --- a/server/routes/auth.js +++ b/server/routes/auth.js @@ -522,6 +522,7 @@ router.get('/me', requireAuth, resolveTenancy, (req, res) => { res.json({ ...req.user, + hide_billing: config.hideBilling, // #116: client hides the Subscription nav + guards #/billing current_workspace_id: req.workspaceId, current_workspace: req.workspace ? { id: req.workspace.id, name: req.workspace.name, organization_id: req.workspace.organization_id } : null, current_organization: currentOrg,