mirror of
https://github.com/MeshTender/MeshTender.git
synced 2026-09-01 17:38:15 +00:00
Hide command permissions when steward is active
This commit is contained in:
@@ -10,22 +10,31 @@
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form method="post" action="/repeaters/{{.Repeater.PublicID}}/share/{{.Target.ID}}/access" id="person-access-form">
|
||||
<div class="hr-text hr-text-left mt-0 mb-3">Steward</div>
|
||||
{{/* Steward supersedes the per-command grants (store.ListSendableCommandIDs
|
||||
gives a steward every command), so the two are not parallel choices and
|
||||
showing them side by side invited setting both. The switch collapses the
|
||||
grid via data-hide-target; the checkboxes keep submitting, so turning
|
||||
steward back off restores the selection rather than starting from none. */}}
|
||||
<div class="hr-text hr-text-left mt-0 mb-3">Access level</div>
|
||||
<label class="form-check form-switch">
|
||||
<input class="form-check-input" type="checkbox" name="steward" value="1" {{if .Steward}}checked{{end}}>
|
||||
<input class="form-check-input" type="checkbox" name="steward" value="1"
|
||||
data-hide-target="#person-commands" {{if .Steward}}checked{{end}}>
|
||||
<span class="form-check-label"><strong>Steward — full access</strong></span>
|
||||
</label>
|
||||
<p class="form-hint">
|
||||
<p class="form-hint mb-0">
|
||||
A steward can run <strong>every</strong> command (including risky ones), just like you, and is listed
|
||||
as a backup maintainer on this repeater's public page.
|
||||
as a backup maintainer on this repeater's public page. Leave it off to grant individual commands
|
||||
instead.
|
||||
</p>
|
||||
|
||||
<div class="hr-text hr-text-left mt-5 mb-3">Permissions</div>
|
||||
<p class="text-secondary">
|
||||
Choose which commands {{.Target.Name}} may send. These apply when steward is off — a steward already
|
||||
has every command. Risky commands can take over or brick the node; grant them only with care.
|
||||
</p>
|
||||
{{template "command-grid" .}}
|
||||
<div id="person-commands">
|
||||
<div class="hr-text hr-text-left mt-4 mb-3">Commands</div>
|
||||
<p class="text-secondary">
|
||||
Choose which commands {{.Target.Name}} may send. Risky commands can take over or brick the node;
|
||||
grant them only with care.
|
||||
</p>
|
||||
{{template "command-grid" .}}
|
||||
</div>
|
||||
</form>
|
||||
<form method="post" action="/repeaters/{{.Repeater.PublicID}}/unshare" id="person-revoke-form">
|
||||
<input type="hidden" name="user_id" value="{{.Target.ID}}">
|
||||
|
||||
@@ -58,3 +58,70 @@ func TestE2EPersonAccessModal(t *testing.T) {
|
||||
}
|
||||
watch.assertClean(t)
|
||||
}
|
||||
|
||||
// TestE2EStewardSwitchCollapsesCommands: a steward already runs every command, so
|
||||
// the per-command grid is moot while the switch is on and the modal collapses it.
|
||||
// Browser-only behavior worth pinning — it's a delegated ui.js handler acting on an
|
||||
// htmx-swapped fragment, which is exactly the combination that breaks silently
|
||||
// (a fragment can't run inline script, and the strict CSP forbids one anyway).
|
||||
func TestE2EStewardSwitchCollapsesCommands(t *testing.T) {
|
||||
srv := newE2EServer(t)
|
||||
owner, cookie := srv.login(t, "e2esteward")
|
||||
rep, err := srv.store.CreateRepeater(srv.ctx, &store.Repeater{
|
||||
OwnerID: owner.ID, Name: "Rep", PublicKeyHex: strings.Repeat("b", 64),
|
||||
RadioFreqHz: 1, RadioBwHz: 1, RadioSF: 11, RadioCR: 5,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("create repeater: %v", err)
|
||||
}
|
||||
sharee, err := srv.store.CreateUser(srv.ctx, "e2estewardee", "")
|
||||
if err != nil {
|
||||
t.Fatalf("create sharee: %v", err)
|
||||
}
|
||||
if _, err := srv.store.AddShare(srv.ctx, rep.ID, sharee.ID); err != nil {
|
||||
t.Fatalf("add share: %v", err)
|
||||
}
|
||||
// Already a steward, so the fragment arrives with the switch checked: the
|
||||
// collapse has to be applied on swap, not only on a change event.
|
||||
if err := srv.store.SetShareSteward(srv.ctx, rep.ID, sharee.ID, true); err != nil {
|
||||
t.Fatalf("set steward: %v", err)
|
||||
}
|
||||
|
||||
bctx, cancel, watch := startBrowser(t)
|
||||
defer cancel()
|
||||
|
||||
const visible = `(() => {
|
||||
const el = document.querySelector('#person-commands');
|
||||
return !!el && !el.hidden;
|
||||
})()`
|
||||
|
||||
shareURL := srv.appURL + "/repeaters/" + rep.PublicID + "/share"
|
||||
var shownAsSteward, shownAfterOff, shownAfterOn bool
|
||||
if err := chromedp.Run(bctx,
|
||||
network.Enable(),
|
||||
cdplog.Enable(),
|
||||
setSessionCookie(cookie),
|
||||
chromedp.Navigate(shareURL),
|
||||
chromedp.WaitVisible(`[data-testid="manage-person"]`, chromedp.ByQuery),
|
||||
chromedp.Click(`[data-testid="manage-person"]`, chromedp.ByQuery),
|
||||
chromedp.WaitVisible(`#person-modal-content input[name=steward]`, chromedp.ByQuery),
|
||||
chromedp.Evaluate(visible, &shownAsSteward),
|
||||
// Turning steward off reveals the grants that apply instead.
|
||||
chromedp.Click(`#person-modal-content input[name=steward]`, chromedp.ByQuery),
|
||||
chromedp.Evaluate(visible, &shownAfterOff),
|
||||
chromedp.Click(`#person-modal-content input[name=steward]`, chromedp.ByQuery),
|
||||
chromedp.Evaluate(visible, &shownAfterOn),
|
||||
); err != nil {
|
||||
t.Fatalf("browser run against %s: %v", shareURL, err)
|
||||
}
|
||||
if shownAsSteward {
|
||||
t.Error("command grid is visible for an existing steward; the swapped-in fragment was not synced")
|
||||
}
|
||||
if !shownAfterOff {
|
||||
t.Error("turning steward off did not reveal the per-command grants")
|
||||
}
|
||||
if shownAfterOn {
|
||||
t.Error("turning steward back on did not collapse the per-command grants")
|
||||
}
|
||||
watch.assertClean(t)
|
||||
}
|
||||
|
||||
@@ -12,6 +12,14 @@
|
||||
// it is (un)checked.
|
||||
// [data-gated] — a link/button whose activation is blocked while
|
||||
// aria-disabled="true".
|
||||
// [data-hide-target="#sel"] — a checkbox that hides the target while checked,
|
||||
// for a switch that makes a section moot (the
|
||||
// steward toggle vs. per-command grants). The
|
||||
// target's inputs still submit: hiding says the
|
||||
// section doesn't apply right now, not that its
|
||||
// values are gone. Synced on load and after an
|
||||
// htmx swap, so a fragment rendered with the box
|
||||
// already checked starts collapsed.
|
||||
// [data-check-all] / — a button that checks (all) or unchecks (none)
|
||||
// [data-check-none] every enabled checkbox within its scope. The
|
||||
// scope is the closest [data-check-scope]
|
||||
@@ -126,9 +134,23 @@
|
||||
}
|
||||
}
|
||||
|
||||
// A checkbox that hides a section its state makes irrelevant. Inputs inside stay
|
||||
// enabled so they keep submitting — see the data-hide-target note at the top.
|
||||
function syncHideTargets(scope) {
|
||||
var boxes = (scope || document).querySelectorAll("input[type=checkbox][data-hide-target]");
|
||||
for (var i = 0; i < boxes.length; i++) applyHideTarget(boxes[i]);
|
||||
}
|
||||
|
||||
function applyHideTarget(box) {
|
||||
var target = document.querySelector(box.getAttribute("data-hide-target"));
|
||||
if (!target) return;
|
||||
target.hidden = !!box.checked;
|
||||
}
|
||||
|
||||
function onReady() {
|
||||
formatTimes(document);
|
||||
localTodayInputs(document);
|
||||
syncHideTargets(document);
|
||||
}
|
||||
|
||||
if (document.readyState === "loading") {
|
||||
@@ -139,6 +161,7 @@
|
||||
// htmx swaps in server HTML that may contain <time> elements.
|
||||
document.body && document.addEventListener("htmx:afterSwap", function (e) {
|
||||
formatTimes(e.target || document);
|
||||
syncHideTargets(e.target || document);
|
||||
});
|
||||
|
||||
document.addEventListener("submit", function (e) {
|
||||
@@ -215,6 +238,11 @@
|
||||
return;
|
||||
}
|
||||
|
||||
if (el.matches("input[type=checkbox][data-hide-target]")) {
|
||||
applyHideTarget(el);
|
||||
return;
|
||||
}
|
||||
|
||||
if (el.matches("[data-consent-target]")) {
|
||||
var target = document.querySelector(el.getAttribute("data-consent-target"));
|
||||
if (!target) return;
|
||||
|
||||
Reference in New Issue
Block a user