feat(admin): hide the batch-create entry unless snapshot sharing is on

- a batch clones every user from a shared snapshot code, and handleUserBatch
  already refuses when snapshots.share is off; the entry point now follows, so
  the admin is not offered a form that can only be rejected
- carry snapshot-sharing state into the admin page data (ShareEnabled)
- correct the snapshots.share help text, which still claimed "default true"
  although sharing is an explicit opt-in and defaults to false
This commit is contained in:
bluebluesoda committed 2026-09-27 12:19:36 +00:00
1 parent b7ebdb5374
commit 1be1a94139
4 files changed
+22 -11

No files matched your search

+8 -2
View File
@@ -1278,12 +1278,13 @@ func TestUserBatchStatus(t *testing.T) {
}
// TestOverviewBatchUI checks the batch entry point and form render, including
// the admin-key checkboxes and the empty-store hint.
// the admin-key checkboxes and the empty-store hint, and that the entry point
// is hidden while snapshot sharing is off (a batch clones from a share code).
func TestOverviewBatchUI(t *testing.T) {
srv, _ := newTestServer(t)
withKeys := srv.renderToString(t, "admin_overview.html", pageData{
Prefix: "/" + testAdminSecret, Lang: langEn,
Prefix: "/" + testAdminSecret, Lang: langEn, ShareEnabled: true,
AdminKeys: []sshKeyRow{{ID: 5, Name: "ops-host", Key: "ssh-ed25519 AAAABODY ops-host", Active: true}},
})
for _, want := range []string{
@@ -1294,6 +1295,11 @@ func TestOverviewBatchUI(t *testing.T) {
t.Errorf("batch UI missing %q", want)
}
}
// Snapshot sharing off (the default): no invitation to start a batch.
hidden := srv.renderToString(t, "admin_overview.html", pageData{Prefix: "/" + testAdminSecret, Lang: langEn})
if strings.Contains(hidden, `id="batchBtn"`) {
t.Error("batch entry point must be hidden while snapshot sharing is off")
}
// "No keys yet" also appears (hidden) in the pre-existing SSH-keys modal,
// so assert on this form's own wording.
const emptyHint = "No admin keys"
+12 -7
View File
@@ -64,6 +64,10 @@ type pageData struct {
// BatchID, when set, is a running/recent batch create the page should show
// the progress modal for (carried through the redirect after submitting).
BatchID string
// ShareEnabled gates the batch-create entry point: a batch clones every user
// from a shared snapshot code, so with snapshot sharing off there is nothing
// to create from (the handler refuses the same way).
ShareEnabled bool
// Global dynamic CPU limit rule, edited by the card below (the DB is the
// single source of truth — there is no config.yaml / CLI equivalent). The
// Active list is the containers currently capped by it.
@@ -150,13 +154,14 @@ type userView struct {
func (s *Server) buildPageData(msg, errMsg string) pageData {
v4Caps := s.mgr.LiveV4Capabilities()
d := pageData{
Title: "VPS Manager Admin",
Prefix: s.prefix(),
Msg: msg,
Err: errMsg,
V4Policy: s.mgr.LiveV4Policy(),
DirectV4: v4Caps.DirectForwarding,
Colors: append([]string{}, userColorPalette...),
Title: "VPS Manager Admin",
Prefix: s.prefix(),
Msg: msg,
Err: errMsg,
V4Policy: s.mgr.LiveV4Policy(),
DirectV4: v4Caps.DirectForwarding,
ShareEnabled: s.mgr.SnapshotShareEnabled(),
Colors: append([]string{}, userColorPalette...),
}
hs := s.mgr.HostStats()
d.Reboot = hs.RebootNeeded
@@ -223,7 +223,7 @@ a.copy.copied { border-color: #86efac; background: #dcfce7; color: #15803d; }
<div class="card">
<h2>{{if eq .Lang "zh"}}创建用户{{else}}Create user{{end}}</h2>
<div style="margin:-4px 0 10px"><button type="button" class="btn" id="batchBtn">{{if eq .Lang "zh"}}批量创建用户{{else}}Batch create{{end}}</button></div>
{{if .ShareEnabled}}<div style="margin:-4px 0 10px"><button type="button" class="btn" id="batchBtn">{{if eq .Lang "zh"}}批量创建用户{{else}}Batch create{{end}}</button></div>{{end}}
<div class="plans" id="plans"></div>
<form action="{{.Prefix}}/user-add" method="post" class="form-grid" id="createForm">
<div class="field create-field name-field"><label>{{if eq .Lang "zh"}}用户名{{else}}Username{{end}}</label><input name="name" id="cfName" pattern="[a-z]([a-z0-9-]*[a-z0-9])?" maxlength="31" autocomplete="off" required title="{{if eq .Lang "zh"}}小写字母开头和结尾,仅含小写字母、数字和连字符{{else}}start/end with a lowercase letter; lowercase letters, digits, hyphens only{{end}}"><small id="cfNameHint" style="color:#6b7280;display:block;margin-top:4px;min-height:1em">{{if eq .Lang "zh"}}自动转为小写{{else}}Folded to lowercase{{end}}</small></div>
+1 -1
View File
@@ -449,7 +449,7 @@ var Fields = []Field{
return nil
}},
{"snapshots.share", KindOperator, ApplyImmediate,
"allow users to share a checkpoint behind a code and install a container from it (default true); stored codes are kept when turned off",
"allow users to share a checkpoint behind a code and install a container from it (off by default; explicit opt-in); stored codes are kept when turned off",
"true or false",
getStr(func(c *Config) string { return strconv.FormatBool(c.Snapshots.Share) }),
func(c *Config, v string) error {