feat(portal): route-level gate when client_portal_enabled is off
Adds isPortalDisabledGlobally() helper that returns true when every configured per-port client_portal_enabled row is false. The (portal) layout calls it and renders a "Portal not available" notice instead of the login/activate/reset pages when the kill switch is flipped. Closes the gap where flipping the admin System Settings toggle would leave /portal/login publicly reachable as a form that rejects every submit with a ConflictError. Now a clean notice page appears instead. Single-port deployments get a global toggle out of this — the existing per-port admin UI in System Settings effectively becomes the master switch. Multi-port future will need URL-level port discrimination (subdomain or path prefix) before the all-ports-off heuristic should be replaced with a per-port resolution. API routes (/api/portal/*) stay on the existing service-layer gate (every portal-auth function checks isPortalEnabledForPort). Direct curl gets a per-call ConflictError, which is acceptable for non-human clients; the UI gate is what matters for accidental discovery. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -39,6 +39,33 @@ export async function isPortalEnabledForPort(portId: string): Promise<boolean> {
|
||||
return row.value === true || row.value === 'true';
|
||||
}
|
||||
|
||||
/**
|
||||
* Route-level gate for the (portal) layout. Returns true when every
|
||||
* configured per-port `client_portal_enabled` row evaluates to false,
|
||||
* i.e. the portal is off everywhere. In single-port deployments this is
|
||||
* exactly "the portal is off" (the admin toggle in System Settings).
|
||||
*
|
||||
* Used to render a "Portal not available" notice instead of the login
|
||||
* form when the kill switch is flipped, so guessed `/portal/*` URLs
|
||||
* don't surface a working-looking form that just rejects every submit.
|
||||
*
|
||||
* Default-OFF (returns false) when there are no setting rows — preserves
|
||||
* the legacy default-on behaviour for fresh installs / ports that never
|
||||
* touched the setting.
|
||||
*
|
||||
* For future multi-port routing (subdomain-per-port or path-prefix),
|
||||
* callers should pass the resolved portId to `isPortalEnabledForPort`
|
||||
* instead and not rely on the all-ports-off heuristic here.
|
||||
*/
|
||||
export async function isPortalDisabledGlobally(): Promise<boolean> {
|
||||
const rows = await db
|
||||
.select({ value: systemSettings.value })
|
||||
.from(systemSettings)
|
||||
.where(eq(systemSettings.key, PORTAL_ENABLED_KEY));
|
||||
if (rows.length === 0) return false;
|
||||
return rows.every((r) => r.value === false || r.value === 'false');
|
||||
}
|
||||
|
||||
// ─── Admin-side: invite a client to the portal ───────────────────────────────
|
||||
|
||||
export async function createPortalUser(args: {
|
||||
|
||||
Reference in New Issue
Block a user