Ctrl K
ComunidadGuías…

internal/server/settings_sections.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
// 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
}