1 Commits

Author SHA1 Message Date
15e7eb5bd1 Remove all inline JS from admin templates
script-src 'self' blocks inline onclick handlers and <script> blocks.
Migrate all interactive behavior to data-* attributes wired by mcias.js:

- data-toggle-form: accounts, policies, pgcreds create-form buttons
- data-href: audit clickable table rows
- data-tab: policy form tab switching (moved showTab from inline script)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 08:26:29 -07:00
6 changed files with 68 additions and 39 deletions

View File

@@ -1,4 +1,8 @@
// mcias.js — HTMX event wiring for the MCIAS web UI. // mcias.js — HTMX event wiring and CSP-safe UI helpers for the MCIAS web UI.
//
// All interactive behavior that would otherwise require inline onclick/onchange
// handlers is wired here via data-* attributes so that script-src 'self'
// (without 'unsafe-inline') is sufficient.
// Show server error responses in the global #htmx-error-banner. // Show server error responses in the global #htmx-error-banner.
// //
@@ -15,9 +19,17 @@ document.body.addEventListener('htmx:responseError', function (evt) {
banner.scrollIntoView({ behavior: 'instant', block: 'nearest' }); banner.scrollIntoView({ behavior: 'instant', block: 'nearest' });
}); });
// Toggle visibility of a create-form panel. The button text alternates // Clear the error banner whenever a successful HTMX swap completes so
// between the given labels. Used by admin pages (accounts, policies, // stale errors do not persist after the user corrects their input.
// pgcreds, SSO clients) to show/hide the inline create form. document.body.addEventListener('htmx:afterSwap', function () {
var banner = document.getElementById('htmx-error-banner');
if (banner) {
banner.style.display = 'none';
banner.innerHTML = '';
}
});
// --- Toggle-form buttons ---
// //
// Usage: <button data-toggle-form="create-form" // Usage: <button data-toggle-form="create-form"
// data-label-show="Add Item" data-label-hide="Cancel"> // data-label-show="Add Item" data-label-hide="Cancel">
@@ -33,19 +45,43 @@ function toggleForm(btn) {
btn.textContent = show ? labelHide : labelShow; btn.textContent = show ? labelHide : labelShow;
} }
// Auto-wire all toggle-form buttons on page load. // --- Clickable rows ---
//
// Usage: <tr class="clickable-row" data-href="/audit/123">
function handleClickableRow(row) {
var href = row.getAttribute('data-href');
if (href) { window.location = href; }
}
// --- Policy form tab switching ---
//
// Usage: <button data-tab="form"> / <button data-tab="json">
function showTab(tab) {
var formMode = document.getElementById('pf-form-mode');
var jsonMode = document.getElementById('pf-json-mode');
var formBtn = document.getElementById('tab-form');
var jsonBtn = document.getElementById('tab-json');
if (!formMode || !jsonMode) { return; }
formMode.style.display = tab === 'form' ? '' : 'none';
jsonMode.style.display = tab === 'json' ? '' : 'none';
if (formBtn) { formBtn.className = tab === 'form' ? 'btn btn-sm btn-secondary' : 'btn btn-sm'; }
if (jsonBtn) { jsonBtn.className = tab === 'json' ? 'btn btn-sm btn-secondary' : 'btn btn-sm'; }
}
// --- Auto-wire on DOMContentLoaded ---
document.addEventListener('DOMContentLoaded', function () { document.addEventListener('DOMContentLoaded', function () {
// Toggle-form buttons.
document.querySelectorAll('[data-toggle-form]').forEach(function (btn) { document.querySelectorAll('[data-toggle-form]').forEach(function (btn) {
btn.addEventListener('click', function () { toggleForm(btn); }); btn.addEventListener('click', function () { toggleForm(btn); });
}); });
});
// Clear the error banner whenever a successful HTMX swap completes so // Clickable table rows.
// stale errors do not persist after the user corrects their input. document.querySelectorAll('[data-href]').forEach(function (row) {
document.body.addEventListener('htmx:afterSwap', function () { row.addEventListener('click', function () { handleClickableRow(row); });
var banner = document.getElementById('htmx-error-banner'); });
if (banner) {
banner.style.display = 'none'; // Tab buttons.
banner.innerHTML = ''; document.querySelectorAll('[data-tab]').forEach(function (btn) {
} btn.addEventListener('click', function () { showTab(btn.getAttribute('data-tab')); });
});
}); });

View File

@@ -7,11 +7,13 @@
<p class="text-muted text-small">Manage user and service accounts</p> <p class="text-muted text-small">Manage user and service accounts</p>
</div> </div>
<button class="btn btn-primary" <button class="btn btn-primary"
onclick="var f=document.getElementById('create-form');f.style.display=f.style.display==='none'?'block':'none'"> data-toggle-form="create-form"
data-label-show="+ New Account"
data-label-hide="Cancel">
+ New Account + New Account
</button> </button>
</div> </div>
<div id="create-form" class="card mt-2" style="display:none"> <div id="create-form" class="card mt-2" hidden>
<h2 style="font-size:1rem;font-weight:600;margin-bottom:1rem">Create Account</h2> <h2 style="font-size:1rem;font-weight:600;margin-bottom:1rem">Create Account</h2>
<form hx-post="/accounts" hx-target="#accounts-tbody" hx-swap="afterbegin"> <form hx-post="/accounts" hx-target="#accounts-tbody" hx-swap="afterbegin">
<input type="hidden" name="_csrf" value="{{.CSRFToken}}"> <input type="hidden" name="_csrf" value="{{.CSRFToken}}">
@@ -36,7 +38,9 @@
<div class="form-actions"> <div class="form-actions">
<button class="btn btn-primary" type="submit">Create</button> <button class="btn btn-primary" type="submit">Create</button>
<button class="btn btn-secondary" type="button" <button class="btn btn-secondary" type="button"
onclick="document.getElementById('create-form').style.display='none'">Cancel</button> data-toggle-form="create-form"
data-label-show="Cancel"
data-label-hide="Cancel">Cancel</button>
</div> </div>
</form> </form>
</div> </div>

View File

@@ -1,6 +1,6 @@
{{define "audit_rows"}} {{define "audit_rows"}}
{{range .Events}} {{range .Events}}
<tr class="clickable-row" onclick="window.location='/audit/{{.ID}}'"> <tr class="clickable-row" data-href="/audit/{{.ID}}">
<td class="text-small text-muted"><a href="/audit/{{.ID}}">{{formatTime .EventTime}}</a></td> <td class="text-small text-muted"><a href="/audit/{{.ID}}">{{formatTime .EventTime}}</a></td>
<td><code style="font-size:.8rem">{{.EventType}}</code></td> <td><code style="font-size:.8rem">{{.EventType}}</code></td>
<td class="text-small text-muted">{{if .ActorUsername}}{{.ActorUsername}}{{else}}&mdash;{{end}}</td> <td class="text-small text-muted">{{if .ActorUsername}}{{.ActorUsername}}{{else}}&mdash;{{end}}</td>

View File

@@ -1,9 +1,9 @@
{{define "policy_form"}} {{define "policy_form"}}
<div style="margin-bottom:.75rem;border-bottom:1px solid var(--border-color);padding-bottom:.5rem;display:flex;gap:.5rem"> <div style="margin-bottom:.75rem;border-bottom:1px solid var(--border-color);padding-bottom:.5rem;display:flex;gap:.5rem">
<button type="button" id="tab-form" class="btn btn-sm btn-secondary" <button type="button" id="tab-form" class="btn btn-sm btn-secondary"
onclick="showTab('form')" style="font-size:.8rem">Form</button> data-tab="form">Form</button>
<button type="button" id="tab-json" class="btn btn-sm" <button type="button" id="tab-json" class="btn btn-sm"
onclick="showTab('json')" style="font-size:.8rem;opacity:.6">JSON</button> data-tab="json">JSON</button>
</div> </div>
<form hx-post="/policies" hx-target="#policies-tbody" hx-swap="afterbegin"> <form hx-post="/policies" hx-target="#policies-tbody" hx-swap="afterbegin">
<div id="pf-form-mode"> <div id="pf-form-mode">
@@ -121,20 +121,4 @@
<button class="btn btn-sm btn-secondary" type="submit">Create Rule</button> <button class="btn btn-sm btn-secondary" type="submit">Create Rule</button>
</form> </form>
<script>
(function() {
var active = 'form';
window.showTab = function(tab) {
active = tab;
document.getElementById('pf-form-mode').style.display = tab === 'form' ? '' : 'none';
document.getElementById('pf-json-mode').style.display = tab === 'json' ? '' : 'none';
document.getElementById('tab-form').style.opacity = tab === 'form' ? '1' : '.6';
document.getElementById('tab-json').style.opacity = tab === 'json' ? '1' : '.6';
var formBtn = document.getElementById('tab-form');
var jsonBtn = document.getElementById('tab-json');
formBtn.className = tab === 'form' ? 'btn btn-sm btn-secondary' : 'btn btn-sm';
jsonBtn.className = tab === 'json' ? 'btn btn-sm btn-secondary' : 'btn btn-sm';
};
})();
</script>
{{end}} {{end}}

View File

@@ -82,7 +82,9 @@
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:.5rem"> <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:.5rem">
<h2 style="font-size:1rem;font-weight:600;margin:0">New Credentials</h2> <h2 style="font-size:1rem;font-weight:600;margin:0">New Credentials</h2>
<button class="btn btn-sm btn-secondary" <button class="btn btn-sm btn-secondary"
onclick="var f=document.getElementById('pgcreds-create-form');f.hidden=!f.hidden;this.textContent=f.hidden?'Add Credentials':'Cancel'">Add Credentials</button> data-toggle-form="pgcreds-create-form"
data-label-show="Add Credentials"
data-label-hide="Cancel">Add Credentials</button>
</div> </div>
<div id="pgcreds-create-form" hidden> <div id="pgcreds-create-form" hidden>
<p class="text-muted text-small" style="margin-bottom:1rem;margin-top:.5rem"> <p class="text-muted text-small" style="margin-bottom:1rem;margin-top:.5rem">

View File

@@ -6,12 +6,15 @@
<h1>Policy Rules</h1> <h1>Policy Rules</h1>
<p class="text-muted text-small">{{len .Rules}} operator rules (built-in defaults not shown)</p> <p class="text-muted text-small">{{len .Rules}} operator rules (built-in defaults not shown)</p>
</div> </div>
<button class="btn btn-primary" onclick="document.getElementById('create-form').style.display='block';this.style.display='none'"> <button class="btn btn-primary"
data-toggle-form="create-form"
data-label-show="Add Rule"
data-label-hide="Cancel">
Add Rule Add Rule
</button> </button>
</div> </div>
<div id="create-form" class="card mt-2" style="display:none"> <div id="create-form" class="card mt-2" hidden>
<h2 style="font-size:1rem;font-weight:600;margin-bottom:1rem">Create Policy Rule</h2> <h2 style="font-size:1rem;font-weight:600;margin-bottom:1rem">Create Policy Rule</h2>
{{template "policy_form" .}} {{template "policy_form" .}}
</div> </div>