← back to Rentv
PR intel (a): clean client login — server delegates PR surface to the PR auth layer
a3ed3e45ff2e9d35c45fac64adfb42901087a304 · 2026-08-06 09:39:22 -0700 · Steve
- server.js: /api/pr + /admin/pr-intelligence delegate to the PR layer (its own
per-tenant login + router capability gate), while still resolving Basic/service
creds so every loopback tool keeps working.
- index.js: shadow adminOnly on PR routes to accept any valid PR auth (pr_session
or server-authed); per-route ROLE enforcement stays with the router gate +
requireCap. login.html served PUBLIC; credentials.html behind auth.
- Verified full matrix: Basic 200, anon 401, login public, admin/pro/user each
gated to their role (pro✗users, user✗write). No under-gating; no regression.
Files touched
M server.jsM src/pr/index.js
Diff
commit a3ed3e45ff2e9d35c45fac64adfb42901087a304
Author: Steve <steve@designerwallcoverings.com>
Date: Thu Aug 6 09:39:22 2026 -0700
PR intel (a): clean client login — server delegates PR surface to the PR auth layer
- server.js: /api/pr + /admin/pr-intelligence delegate to the PR layer (its own
per-tenant login + router capability gate), while still resolving Basic/service
creds so every loopback tool keeps working.
- index.js: shadow adminOnly on PR routes to accept any valid PR auth (pr_session
or server-authed); per-route ROLE enforcement stays with the router gate +
requireCap. login.html served PUBLIC; credentials.html behind auth.
- Verified full matrix: Basic 200, anon 401, login public, admin/pro/user each
gated to their role (pro✗users, user✗write). No under-gating; no regression.
---
server.js | 11 +++++++++++
src/pr/index.js | 16 ++++++++++++++++
2 files changed, 27 insertions(+)
diff --git a/server.js b/server.js
index 2be1bbb2..c5f5e40e 100644
--- a/server.js
+++ b/server.js
@@ -104,6 +104,17 @@ app.get('/api/whoami', (req, res) => { const s = sessionOf(req); res.json({ auth
// Global authentication gate — session cookie → service token → Basic → clean-login redirect.
app.use((req, res, next) => {
if (process.env.OPEN === '1') { req.role = 'admin'; req.authVia = 'open'; return next(); } // local-preview bypass
+ // PR-Intelligence CRM self-authenticates (its own per-tenant login + router capability gate in
+ // src/pr/index.js — every /api/pr route 401/403s by role). Delegate its surface to that layer so
+ // CRM clients sign in with only their own credentials, but STILL resolve Basic/service creds here
+ // so every existing loopback tool (enrichment, openclaw, crawl) keeps working unchanged.
+ if (req.path.startsWith('/api/pr/') || req.path.startsWith('/admin/pr-intelligence/')) {
+ const az = req.headers.authorization || '';
+ const r = CRED_ROLE.get(az);
+ if (r) { req.role = r; req.authVia = 'basic'; }
+ else if (SERVICE_TOKEN && az === 'Bearer ' + SERVICE_TOKEN) { req.role = 'admin'; req.authVia = 'service'; }
+ return next(); // no server creds → the PR layer's own login/session (or its gate's 401) applies
+ }
const s = sessionOf(req);
if (s) { req.role = s.role; req.authVia = 'session'; req.session = s; return next(); }
const authz = req.headers.authorization || '';
diff --git a/src/pr/index.js b/src/pr/index.js
index 37a6a93e..583cf655 100644
--- a/src/pr/index.js
+++ b/src/pr/index.js
@@ -89,6 +89,18 @@ module.exports = function mountPR(app, { adminOnly, sendPage }) {
next();
} catch (e) { res.status(500).json({ ok: false, error: e.message }); }
};
+ // Shadow the server's admin-only `adminOnly` on PR routes: accept ANY valid PR auth (a pr_session
+ // OR a server-authed caller). Per-route ROLE enforcement is handled by the router capability gate
+ // + inline requireCap — so this only needs to confirm the request is authenticated. This is what
+ // lets a CRM client (pr_session, no server-admin role) reach the routes once server.js delegates.
+ adminOnly = async (req, res, next) => {
+ try {
+ const ctx = await prAuthCtx(req);
+ if (!ctx) return res.status(401).json({ ok: false, error: 'authentication required' });
+ req.prAuth = ctx;
+ next();
+ } catch (e) { res.status(500).json({ ok: false, error: e.message }); }
+ };
const sessionCookie = (req, tok, maxAge) => `pr_session=${tok}; HttpOnly; SameSite=Strict; Path=/; Max-Age=${maxAge}${(req.secure || req.headers['x-forwarded-proto'] === 'https') ? '; Secure' : ''}`;
// Register the router-level capability gate AFTER the public auth routes (login/logout) so they
// stay reachable pre-login; it self-protects every other /api/pr route (caps by method + path).
@@ -555,6 +567,10 @@ module.exports = function mountPR(app, { adminOnly, sendPage }) {
app.get('/api/pr/export/evidence', adminOnly, h(async (req, res) => sendCSV(res, 'pr-source-evidence.csv', await importexport.exportSourceEvidence(req.query.entity_type))));
// ── Admin shells ───────────────────────────────────────────────────────────
+ // login.html is PUBLIC (the CRM front door — must be reachable before auth); the .html
+ // suffix variants are served explicitly since they aren't in the PAGES list.
+ app.get('/admin/pr-intelligence/login.html', (_q, r) => sendPage(r, path.join(SHELL_DIR, 'login.html')));
+ app.get('/admin/pr-intelligence/credentials.html', adminOnly, (_q, r) => sendPage(r, path.join(SHELL_DIR, 'credentials.html')));
app.get('/admin/pr-intelligence', adminOnly, (_q, r) => sendPage(r, path.join(SHELL_DIR, 'index.html')));
for (const page of PAGES.filter((p) => p !== 'index')) {
app.get('/admin/pr-intelligence/' + page, adminOnly, (_q, r) => sendPage(r, path.join(SHELL_DIR, page + '.html')));
← 12d0f414 harden(boomer-voice): Cody gate — digit-anchor every forSpee
·
back to Rentv
·
Public polish: hide all admin nav links from public + SEO fo 88858d33 →