From de6f7a88da8d2166ca4023eccb6eb13a5220e46a Mon Sep 17 00:00:00 2001 From: Mikhail Yenuchenko Date: Tue, 11 Aug 2026 22:42:01 +0300 Subject: [PATCH] panel: disable demote and delete for the only global admin Co-authored-by: Cursor --- CHANGELOG.md | 3 +++ internal/web/handlers/handlers_users.go | 9 +++++++++ internal/web/view/static/panel.css | 8 ++++++++ internal/web/view/templates/user_form.html | 7 +++++-- 4 files changed, 25 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8f765e0..f25337f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,9 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); version ### Changed +- panel: the user edit form disables role change and delete for the only global + administrator, with a short note, instead of allowing the action and showing + an error on submit. - panel: the user create/edit form hides **Assigned domains** when the role is global administrator, since that role manages every domain anyway. - panel: **Settings** shows panel credentials and DMARC aggregate reports side diff --git a/internal/web/handlers/handlers_users.go b/internal/web/handlers/handlers_users.go index c1a2338..6edb923 100644 --- a/internal/web/handlers/handlers_users.go +++ b/internal/web/handlers/handlers_users.go @@ -132,9 +132,18 @@ func (h *Handlers) renderUserForm(w http.ResponseWriter, r *http.Request, status data["FormDomains"] = view.FormDomains data["FormPassword"] = view.FormPassword data["IsEdit"] = userID != 0 + data["LastGlobalLocked"] = lastGlobalLocked(h, userID, view.FormRole) h.view.Render(w, status, "user_form", data) } +func lastGlobalLocked(h *Handlers, userID int64, formRole string) bool { + if userID == 0 || formRole != string(store.RoleGlobal) { + return false + } + n, err := h.store.CountGlobalUsers() + return err == nil && n <= 1 +} + func (h *Handlers) submitUserCreate(w http.ResponseWriter, r *http.Request) { if err := r.ParseForm(); err != nil { h.renderUserForm(w, r, http.StatusBadRequest, 0, userFormView{FormErr: "Invalid form submission."}) diff --git a/internal/web/view/static/panel.css b/internal/web/view/static/panel.css index 1311efa..69139db 100644 --- a/internal/web/view/static/panel.css +++ b/internal/web/view/static/panel.css @@ -193,6 +193,14 @@ button, a.btn, a.danger { color: var(--on-accent); background: var(--accent-fill); border: 0; border-radius: 5px; cursor: pointer; } button:hover, a.btn:hover { background: var(--accent-fill-hover); } +button:disabled, button.danger:disabled { + opacity: 0.45; cursor: not-allowed; +} +button:disabled:hover { background: var(--accent-fill); } +button.danger:disabled:hover { background: var(--danger-fill); } +select:disabled { + opacity: 0.65; cursor: not-allowed; background: var(--code-bg); +} .error { color: var(--danger-fg); margin: 0.6rem 0 0; font-weight: 600; } .muted { color: var(--muted); } /* Links take the accent. The panel never set a colour here and ran on the diff --git a/internal/web/view/templates/user_form.html b/internal/web/view/templates/user_form.html index 1918ca9..a338578 100644 --- a/internal/web/view/templates/user_form.html +++ b/internal/web/view/templates/user_form.html @@ -14,10 +14,12 @@ - {{end}} + + {{if .LastGlobalLocked}}

The only global administrator cannot be demoted.

{{end}}
Assigned domains @@ -32,7 +34,8 @@ {{if .IsEdit}} - + + {{if .LastGlobalLocked}}

The only global administrator cannot be deleted.

{{end}} {{end}}