Fixed admin display.
This commit is contained in:
parent
352bdc9fb6
commit
f8c1f164f6
1 changed files with 49 additions and 38 deletions
|
|
@ -85,37 +85,48 @@ const csrfField = (req) => {
|
|||
};
|
||||
|
||||
|
||||
const layout = (title, body) => {
|
||||
return `<!doctype html>
|
||||
<html lang="en"><head>
|
||||
<meta charset="utf-8">
|
||||
<title>${escapeHtml(title)}</title>
|
||||
<style>
|
||||
body { font-family: system-ui, -apple-system, sans-serif; margin: 1.5rem; max-width: 900px; }
|
||||
h1 { font-size: 1.4rem; }
|
||||
h2 { font-size: 1.1rem; margin-top: 1.5rem; }
|
||||
table { border-collapse: collapse; width: 100%; font-size: 0.9rem; }
|
||||
th, td { border: 1px solid #ddd; padding: 0.35rem 0.6rem; text-align: left; vertical-align: top; }
|
||||
th { background: #f5f5f5; }
|
||||
code { font-family: ui-monospace, Menlo, Consolas, monospace; }
|
||||
// Bootstrap nav-pills with the current page marked active. The active link is the
|
||||
// one whose href is the LONGEST boundary-safe prefix of the request path, so a
|
||||
// sub-page (e.g. /clients/<id>/secret) still highlights its parent tab.
|
||||
const navPills = (req, links) => {
|
||||
const cur = String((req && (req.originalUrl || req.path)) || "").split("?")[0].replace(/\/+$/, "");
|
||||
let activeHref = "";
|
||||
for (const [href] of links) {
|
||||
const h = href.replace(/\/+$/, "");
|
||||
if ((cur === h || cur.startsWith(h + "/")) && h.length > activeHref.length) {
|
||||
activeHref = h;
|
||||
}
|
||||
}
|
||||
const items = links.map(([href, label]) => {
|
||||
const on = href.replace(/\/+$/, "") === activeHref;
|
||||
return ` <li class="nav-item"><a class="nav-link${on ? " active" : ""}"${on ? ' aria-current="page"' : ""} href="${escapeHtml(href)}">${escapeHtml(label)}</a></li>`;
|
||||
}).join("\n");
|
||||
return `<ul class="nav nav-pills mb-3">\n${items}\n</ul>`;
|
||||
};
|
||||
|
||||
|
||||
// Returns the INNER page content only (Bootstrap sub-nav + a small residual
|
||||
// style for the non-table classes the body markup uses + body). The page is
|
||||
// wrapped in the active Saltcorn theme by res.sendWrap at the call sites, so
|
||||
// there is no <html>/<head>/<body> here.
|
||||
const layout = (req, body) => {
|
||||
const base = constants.ADMIN_BASE_PATH;
|
||||
const links = [
|
||||
[base, "Dashboard"],
|
||||
[`${base}/clients`, "Clients"],
|
||||
[`${base}/groups`, "Groups"],
|
||||
[`${base}/saml-sps`, "SAML SPs"],
|
||||
[`${base}/ldap`, "LDAP"],
|
||||
];
|
||||
return `<style>
|
||||
.muted { color: #888; }
|
||||
nav { margin-bottom: 1rem; }
|
||||
nav a { margin-right: 1rem; }
|
||||
form.inline { display: inline; }
|
||||
input[type=text] { padding: 0.2rem; }
|
||||
button { padding: 0.2rem 0.5rem; cursor: pointer; }
|
||||
button:not(.btn) { padding: 0.2rem 0.5rem; cursor: pointer; }
|
||||
</style>
|
||||
</head><body>
|
||||
<nav>
|
||||
<a href="${escapeHtml(constants.ADMIN_BASE_PATH)}">Dashboard</a>
|
||||
<a href="${escapeHtml(constants.ADMIN_BASE_PATH)}/clients">Clients</a>
|
||||
<a href="${escapeHtml(constants.ADMIN_BASE_PATH)}/groups">Groups</a>
|
||||
<a href="${escapeHtml(constants.ADMIN_BASE_PATH)}/saml-sps">SAML SPs</a>
|
||||
<a href="${escapeHtml(constants.ADMIN_BASE_PATH)}/ldap">LDAP</a>
|
||||
</nav>
|
||||
${navPills(req, links)}
|
||||
${body}
|
||||
<p class="muted">${escapeHtml(constants.PLUGIN_NAME)} v${escapeHtml(constants.PLUGIN_VERSION)}</p>
|
||||
</body></html>`;
|
||||
<p class="text-muted small mt-3">${escapeHtml(constants.PLUGIN_NAME)} v${escapeHtml(constants.PLUGIN_VERSION)}</p>`;
|
||||
};
|
||||
|
||||
|
||||
|
|
@ -133,7 +144,7 @@ const dashboard = async (req, res) => {
|
|||
const active = await keys.getActiveKeyMeta();
|
||||
const issuer = issuerForReq(req);
|
||||
const body = `
|
||||
<table>
|
||||
<table class="table table-sm table-bordered">
|
||||
<tr><th>Env ID</th><td><code>${escapeHtml(env ? env.env_id : "?")}</code></td></tr>
|
||||
<tr><th>Bootstrapped at</th><td>${escapeHtml(env ? env.bootstrapped_at : "")}</td></tr>
|
||||
<tr><th>Issuer</th><td><code>${escapeHtml(issuer)}</code></td></tr>
|
||||
|
|
@ -148,7 +159,7 @@ const dashboard = async (req, res) => {
|
|||
<h2>Signing key rotation</h2>
|
||||
<p class="muted">Generates a new active signing key (new kid). The previous key keeps verifying issued tokens (stays in JWKS as <code>retiring</code>) until its grace window elapses, then drops out.</p>
|
||||
<form method="post" action="${escapeHtml(constants.ADMIN_BASE_PATH)}/rotate-key">${csrfField(req)}<button>rotate signing key</button></form>`;
|
||||
res.type("text/html").send(layout("saltcorn-idp dashboard", body));
|
||||
res.sendWrap("saltcorn-idp dashboard", layout(req, body));
|
||||
} catch (e) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(`[${constants.PLUGIN_NAME}] dashboard failed:`, e);
|
||||
|
|
@ -198,12 +209,12 @@ const groupsPage = async (req, res) => {
|
|||
const body = `
|
||||
<h1>Groups</h1>
|
||||
<p class="muted">The OIDC <code>groups</code> claim = each user's Saltcorn role (as <code>role:<name></code>) plus these custom groups (as <code>group:<name></code>).</p>
|
||||
<table><tr><th>Group</th><th>Members</th><th></th></tr>${rows || '<tr><td colspan="3" class="muted">no groups yet</td></tr>'}</table>
|
||||
<table class="table table-sm table-bordered"><tr><th>Group</th><th>Members</th><th></th></tr>${rows || '<tr><td colspan="3" class="muted">no groups yet</td></tr>'}</table>
|
||||
<h2>Create group</h2>
|
||||
<form method="post" action="${escapeHtml(constants.ADMIN_BASE_PATH)}/groups/create">${csrfField(req)}
|
||||
<input type="text" name="name" placeholder="group name" required> <button>create</button>
|
||||
</form>`;
|
||||
res.type("text/html").send(layout("saltcorn-idp groups", body));
|
||||
res.sendWrap("saltcorn-idp groups", layout(req, body));
|
||||
} catch (e) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(`[${constants.PLUGIN_NAME}] groups page failed:`, e);
|
||||
|
|
@ -306,7 +317,7 @@ const clientsPage = async (req, res) => {
|
|||
}
|
||||
const body = `
|
||||
<h1>Clients (relying parties)</h1>
|
||||
<table><tr><th>client_id</th><th>label</th><th>auth</th><th>redirect URIs</th><th>scope</th><th></th></tr>${rows || '<tr><td colspan="6" class="muted">no clients yet</td></tr>'}</table>
|
||||
<table class="table table-sm table-bordered"><tr><th>client_id</th><th>label</th><th>auth</th><th>redirect URIs</th><th>scope</th><th></th></tr>${rows || '<tr><td colspan="6" class="muted">no clients yet</td></tr>'}</table>
|
||||
<h2>Register client</h2>
|
||||
<form method="post" action="${escapeHtml(constants.ADMIN_BASE_PATH)}/clients/create">${csrfField(req)}
|
||||
<p>client_id <input type="text" name="client_id" required></p>
|
||||
|
|
@ -322,7 +333,7 @@ const clientsPage = async (req, res) => {
|
|||
<p>scope <input type="text" name="scope" value="openid email profile groups"></p>
|
||||
<button>register</button>
|
||||
</form>`;
|
||||
res.type("text/html").send(layout("saltcorn-idp clients", body));
|
||||
res.sendWrap("saltcorn-idp clients", layout(req, body));
|
||||
} catch (e) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(`[${constants.PLUGIN_NAME}] clients page failed:`, e);
|
||||
|
|
@ -368,7 +379,7 @@ const createClientHandler = async (req, res) => {
|
|||
<p>Client secret (shown once - copy it now):</p>
|
||||
<p><code>${escapeHtml(created.secret)}</code></p>
|
||||
<p><a href="${escapeHtml(constants.ADMIN_BASE_PATH)}/clients">Back to clients</a></p>`;
|
||||
res.type("text/html").send(layout("client secret", body));
|
||||
res.sendWrap("client secret", layout(req, body));
|
||||
} else {
|
||||
res.redirect(constants.ADMIN_BASE_PATH + "/clients");
|
||||
}
|
||||
|
|
@ -409,7 +420,7 @@ const samlSpsPage = async (req, res) => {
|
|||
const body = `
|
||||
<h1>SAML service providers</h1>
|
||||
<p class="muted">Only registered SPs receive assertions, and only at an allow-listed ACS URL. A signing cert enables (and "require signed" enforces) AuthnRequest signature verification.</p>
|
||||
<table><tr><th>entityID</th><th>label</th><th>ACS URLs</th><th>req signed</th><th>cert</th><th></th></tr>${rows || '<tr><td colspan="6" class="muted">no SPs yet</td></tr>'}</table>
|
||||
<table class="table table-sm table-bordered"><tr><th>entityID</th><th>label</th><th>ACS URLs</th><th>req signed</th><th>cert</th><th></th></tr>${rows || '<tr><td colspan="6" class="muted">no SPs yet</td></tr>'}</table>
|
||||
<h2>Register SP</h2>
|
||||
<form method="post" action="${escapeHtml(constants.ADMIN_BASE_PATH)}/saml-sps/create">${csrfField(req)}
|
||||
<p>entityID <input type="text" name="entity_id" required></p>
|
||||
|
|
@ -419,7 +430,7 @@ const samlSpsPage = async (req, res) => {
|
|||
<p><label><input type="checkbox" name="want_signed" value="1"> require signed AuthnRequests</label></p>
|
||||
<button>register</button>
|
||||
</form>`;
|
||||
res.type("text/html").send(layout("saltcorn-idp saml sps", body));
|
||||
res.sendWrap("saltcorn-idp saml sps", layout(req, body));
|
||||
} catch (e) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(`[${constants.PLUGIN_NAME}] saml sps page failed:`, e);
|
||||
|
|
@ -512,7 +523,7 @@ const ldapListenerSection = async (req) => {
|
|||
const applied = await ldapSettings.getApplied();
|
||||
const effective = runtime.enabled ? `${escapeHtml(runtime.host)}:${escapeHtml(String(runtime.port))}` : "disabled";
|
||||
const running = (applied && applied.enabled) ? `${escapeHtml(applied.host)}:${escapeHtml(String(applied.port))}` : "disabled";
|
||||
const statusTable = `<table>
|
||||
const statusTable = `<table class="table table-sm table-bordered">
|
||||
<tr><th>Currently running</th><td>${running}</td></tr>
|
||||
<tr><th>Effective after restart</th><td>${effective}</td></tr>
|
||||
</table>`;
|
||||
|
|
@ -557,7 +568,7 @@ const ldapServicePage = async (req, res) => {
|
|||
${listener}
|
||||
<h1>LDAP service account</h1>
|
||||
<p class="muted">A service DN + password for the search-then-bind flow (an application binds as this DN, searches for a user, then re-binds as that user to validate the password). The password is sealed at rest and never displayed.</p>
|
||||
<table><tr><th>Configured service DN</th><td>${dn ? `<code>${escapeHtml(dn)}</code>` : '<span class="muted">(none)</span>'}</td></tr></table>
|
||||
<table class="table table-sm table-bordered"><tr><th>Configured service DN</th><td>${dn ? `<code>${escapeHtml(dn)}</code>` : '<span class="muted">(none)</span>'}</td></tr></table>
|
||||
<h2>Set service account</h2>
|
||||
<form method="post" action="${escapeHtml(constants.ADMIN_BASE_PATH)}/ldap/service">${csrfField(req)}
|
||||
<p>service DN <input type="text" name="dn" placeholder="cn=svc,ou=people,dc=saltcorn,dc=local" size="55"></p>
|
||||
|
|
@ -566,7 +577,7 @@ const ldapServicePage = async (req, res) => {
|
|||
</form>
|
||||
<h2>Clear</h2>
|
||||
<form method="post" action="${escapeHtml(constants.ADMIN_BASE_PATH)}/ldap/service/clear">${csrfField(req)}<button>clear service account</button></form>`;
|
||||
res.type("text/html").send(layout("saltcorn-idp ldap", body));
|
||||
res.sendWrap("saltcorn-idp ldap", layout(req, body));
|
||||
} catch (e) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(`[${constants.PLUGIN_NAME}] ldap service page failed:`, e);
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue