internal/server/settings_sections.go
Rama main · réplica de lectura del repositorio original.
// SPDX-License-Identifier: Apache-2.0
package server
import (
"encoding/json"
"html/template"
"strings"
)
// v2.44.0: Settings is split into pages, one per area, instead of one form
// with sixteen cards. Each page posts only its own fields, so the save
// handler must only write the keys that page owns — otherwise saving
// "General" would blank the SMTP host, the analytics target and every
// other value the page did not render. settingsKeySection is that
// ownership table; postSettings drops every key the posted page does not
// own. Dynamic keys (DNS provider credentials, fleet integration settings)
// are gated at collection time instead.
type settingsSection struct {
Slug string
Label string
Blurb string
}
// settingsSections is the navigation, in order. Slugs are URL segments.
var settingsSections = []settingsSection{
{"general", "General", "Site title, maintenance, auto-sync, timezone, post-apply checks"},
{"notifications", "Notifications", "Email (SMTP), webhook, ntfy, certificate alerts"},
{"dns", "DNS", "Public IPs, provider credentials, zones"},
{"security", "Security", "2FA policy, sessions, allowlists, CAPTCHA"},
{"analytics", "Analytics", "Visitor analytics, retention, storage"},
{"integrations", "Integrations", "Caddy access logs, Prometheus metrics, CrowdSec"},
{"ai", "AI assistant", "Provider, model, system prompt"},
{"backup", "Backup", "Database backup"},
}
const defaultSettingsSection = "general"
// settingsSectionSlug validates a slug from the URL or the form: "" for
// unknown, so callers can 404 or fall back.
func settingsSectionSlug(raw string) string {
raw = strings.ToLower(strings.TrimSpace(raw))
for _, sec := range settingsSections {
if sec.Slug == raw {
return raw
}
}
return ""
}
func settingsSectionLabel(slug string) string {
for _, sec := range settingsSections {
if sec.Slug == slug {
return sec.Label
}
}
return "Settings"
}
// localizedSettingsSections is the navigation in lang (v2.63.0, issue #128).
// The English text above is the source: en.json carries the same strings under
// settings.section.<slug>.label / .blurb, and a test keeps the two equal.
func localizedSettingsSections(lang string) []settingsSection {
b := translations()
out := make([]settingsSection, len(settingsSections))
for i, sec := range settingsSections {
sec.Label = b.T(lang, "settings.section."+sec.Slug+".label")
sec.Blurb = b.T(lang, "settings.section."+sec.Slug+".blurb")
out[i] = sec
}
return out
}
// localizedSettingsSectionLabel is settingsSectionLabel in lang.
func localizedSettingsSectionLabel(lang, slug string) string {
if settingsSectionSlug(slug) == "" {
return translations().T(lang, "settings.title")
}
return translations().T(lang, "settings.section."+slug+".label")
}
// settingsAnchorSection maps the pre-v2.44.0 in-page anchors to the page
// that now holds the card, for old links and bookmarks.
var settingsAnchorSection = map[string]string{
"settings-general": "general",
"settings-timezone": "general",
"expectations": "general",
"settings-smtp": "notifications",
"settings-notifications": "notifications",
"settings-notifier": "notifications",
"settings-dns": "dns",
"settings-dns-ips": "dns",
"settings-dns-providers": "dns",
"settings-captcha": "security",
"settings-security": "security",
"settings-ai": "ai",
"settings-access-logs": "integrations",
"settings-metrics": "integrations",
"settings-crowdsec": "integrations",
"analytics": "analytics",
"settings-backup": "backup",
}
// settingsKeySection is which page owns each setting key the save handler
// can write. A key missing here is written on every page — keep it
// complete (TestSettingsSectionsOwnEveryStaticKey checks the handler).
var settingsKeySection = map[string]string{
settingSiteTitle: "general",
settingFaviconURL: "general",
settingCatchAll404HTML: "general",
settingFallbackStatus: "general", // v2.60.0 (issue #123)
settingDefaultLocale: "general", // v2.62.0 (issue #128)
settingGlobalMaintenance: "general",
settingAutoSyncHours: "general",
settingActivityLogDays: "general",
settingGlobalStripResponseHeaders: "general",
settingTimezone: "general",
settingExpectationsAutoRollback: "general",
settingNotifyWebhookURL: "notifications",
settingNotifyWebhookSecret: "notifications",
settingNotifyNtfyURL: "notifications",
settingNotifyNtfyToken: "notifications",
settingNotifyDaysBefore: "notifications",
settingSMTPHost: "notifications",
settingSMTPPort: "notifications",
settingSMTPUsername: "notifications",
settingSMTPPassword: "notifications",
settingSMTPFrom: "notifications",
settingSMTPTo: "notifications",
settingSMTPSecurity: "notifications",
settingSMTPSkipVerify: "notifications",
settingServerIP: "dns",
settingCFProxied: "dns",
settingDNSVerifyResolver: "dns",
settingACMEDNSResolvers: "dns",
settingRequire2FA: "security",
settingRequireTOTP: "security",
settingTrustedProxies: "security",
settingClientIPHeaders: "security",
settingDisableHTTP3: "security",
settingAdminAllowlist: "security",
settingGlobalIPBlocklist: "security",
settingOIDCEnabled: "security",
settingOIDCIssuer: "security",
settingOIDCClientID: "security",
settingOIDCClientSecret: "security",
settingOIDCRedirectURL: "security",
settingOIDCAutoCreate: "security",
settingOIDCButtonLabel: "security",
settingOIDCDefaultRole: "security", // v2.65.0 (issue #139)
settingOIDCGroupsClaim: "security",
settingOIDCRoleMappings: "security",
settingOIDCAllowedDomains: "security", // v2.67.0
settingSessionDays: "security",
settingMaxLoginAttempts: "security",
settingCaptchaProvider: "security",
settingTurnstileSiteKey: "security",
settingTurnstileSecretKey: "security",
settingRecaptchaSiteKey: "security",
settingRecaptchaSecretKey: "security",
settingRecaptchaMinScore: "security",
settingAnalyticsEnabled: "analytics",
settingAnalyticsIngestTarget: "analytics",
settingAnalyticsExcludeIPs: "analytics",
settingAnalyticsSoftStart: "analytics",
settingAnalyticsDialTimeoutSec: "analytics",
settingAnalyticsRetentionDays: "analytics",
settingCrowdSecAPIKey: "integrations",
settingAIEnabled: "ai",
settingAIProvider: "ai",
settingAIOllamaURL: "ai",
settingAIOllamaModel: "ai",
settingAIOllamaCloudModel: "ai",
settingAIOllamaCloudAPIKey: "ai",
settingAIAnthropicModel: "ai",
settingAIAnthropicAPIKey: "ai",
settingAIOpenAIBaseURL: "ai",
settingAIOpenAIModel: "ai",
settingAIOpenAIAPIKey: "ai",
settingAISystemPrompt: "ai",
settingAIRequestTimeoutSec: "ai",
settingBackupScheduleEnabled: "backup",
settingBackupScheduleDir: "backup",
settingBackupScheduleInterval: "backup",
settingBackupScheduleKeep: "backup",
}
// settingsSectionSyncsCaddy reports whether saving a page can change the
// live Caddy config and therefore warrants the auto-sync that follows a
// save. Notification, AI and backup settings never reach Caddy.
func settingsSectionSyncsCaddy(slug string) bool {
switch slug {
case "notifications", "ai", "backup":
return false
}
return true
}
// settingsAnchorsJSON is settingsAnchorSection for the page's redirect script.
func settingsAnchorsJSON() template.JS {
raw, err := json.Marshal(settingsAnchorSection)
if err != nil {
return template.JS("{}")
}
return template.JS(raw) // nolint:gosec // fixed table of slugs, no user input
}