feat(ui): add desktop footer, legal custom link and about dialog

This commit is contained in:
sandrodach committed 2026-10-30 20:19:02 +01:00
1 parent bc7bdc481e
commit c181ffea54
12 files changed
+179 -13

No files matched your search

+2
View File
@@ -52,6 +52,8 @@ func (s *Server) handleAuthStatus(w http.ResponseWriter, r *http.Request) {
"hasImprintContent": s.cfg.Legal.ImprintContent != "",
"privacyUrl": s.cfg.Legal.PrivacyURL,
"hasPrivacyContent": s.cfg.Legal.PrivacyContent != "",
"customLinkLabel": s.cfg.Legal.CustomLinkLabel,
"customLinkUrl": s.cfg.Legal.CustomLinkURL,
},
}
// Countdown to the next demo reset; seconds as well, so client clocks do not matter
+9 -2
View File
@@ -1388,7 +1388,12 @@ func TestLegalTextsArePublic(t *testing.T) {
}
defer store.Close()
cfg := &config.Config{Port: "8080", DatabasePath: dbPath, JWTSecret: "test-jwt-secret-key",
Legal: config.LegalTexts{ImprintContent: "# Impressum\nMax Muster", PrivacyURL: "https://example.org/datenschutz"}}
Legal: config.LegalTexts{
ImprintContent: "# Impressum\nMax Muster",
PrivacyURL: "https://example.org/datenschutz",
CustomLinkLabel: "Support",
CustomLinkURL: "https://support.example.org",
}}
router := NewServer(cfg, store, ws.NewHub(), nil).Router()
rec := httptest.NewRecorder()
@@ -1399,10 +1404,12 @@ func TestLegalTextsArePublic(t *testing.T) {
HasImprintContent bool `json:"hasImprintContent"`
PrivacyURL string `json:"privacyUrl"`
HasPrivacyContent bool `json:"hasPrivacyContent"`
CustomLinkLabel string `json:"customLinkLabel"`
CustomLinkURL string `json:"customLinkUrl"`
} `json:"legal"`
}
_ = json.Unmarshal(rec.Body.Bytes(), &st)
if !st.Legal.HasImprintContent || st.Legal.HasPrivacyContent || st.Legal.PrivacyURL != "https://example.org/datenschutz" {
if !st.Legal.HasImprintContent || st.Legal.HasPrivacyContent || st.Legal.PrivacyURL != "https://example.org/datenschutz" || st.Legal.CustomLinkLabel != "Support" || st.Legal.CustomLinkURL != "https://support.example.org" {
t.Fatalf("unexpected legal status: %+v", st.Legal)
}
+28 -8
View File
@@ -94,8 +94,9 @@ func (s SpeechConfig) Mode(demo bool) string {
}
type LegalTexts struct {
ImprintURL, ImprintContent string
PrivacyURL, PrivacyContent string
ImprintURL, ImprintContent string
PrivacyURL, PrivacyContent string
CustomLinkLabel, CustomLinkURL string
}
// Publicly known JWT secrets (old code default and the placeholders from .env.example, compose and
@@ -171,6 +172,20 @@ func legalURL(name string) string {
return v
}
// legalCustomLink accepts only https links for the custom footer link.
func legalCustomLink(labelVar, urlVar string) (string, string) {
u := strings.TrimSpace(os.Getenv(urlVar))
if u == "" {
return "", ""
}
if !strings.HasPrefix(u, "https://") {
slog.Warn("Custom-Link-URL ignoriert: nur https erlaubt", "variable", urlVar, "url", u)
return "", ""
}
label := strings.TrimSpace(os.Getenv(labelVar))
return label, u
}
func Load() *Config {
port := getEnv("PORT", "8080")
dataDir := getEnv("DATA_DIR", "./data")
@@ -247,12 +262,17 @@ func Load() *Config {
QuotaMB: envInt("MEDIA_QUOTA_MB", 0, 0, 10000000),
DemoMaxMB: envInt("MEDIA_DEMO_MAX_MB", 3, 1, 100),
},
Legal: LegalTexts{
ImprintURL: legalURL("LEGAL_IMPRINT_URL"),
ImprintContent: legalContent("LEGAL_IMPRINT_CONTENT"),
PrivacyURL: legalURL("LEGAL_PRIVACY_URL"),
PrivacyContent: legalContent("LEGAL_PRIVACY_CONTENT"),
},
Legal: func() LegalTexts {
customLabel, customURL := legalCustomLink("LEGAL_CUSTOM_LINK_LABEL", "LEGAL_CUSTOM_LINK_URL")
return LegalTexts{
ImprintURL: legalURL("LEGAL_IMPRINT_URL"),
ImprintContent: legalContent("LEGAL_IMPRINT_CONTENT"),
PrivacyURL: legalURL("LEGAL_PRIVACY_URL"),
PrivacyContent: legalContent("LEGAL_PRIVACY_CONTENT"),
CustomLinkLabel: customLabel,
CustomLinkURL: customURL,
}
}(),
}
}
+39
View File
@@ -81,3 +81,42 @@ func TestJWTSecretNeverUsesAKnownDefault(t *testing.T) {
t.Fatalf("a real JWT_SECRET must win, got %q", got)
}
}
func TestCustomLinkValidation(t *testing.T) {
t.Setenv("DATA_DIR", t.TempDir())
// 1. Valid https URL and label
t.Setenv("LEGAL_CUSTOM_LINK_LABEL", "Unternehmensseite")
t.Setenv("LEGAL_CUSTOM_LINK_URL", "https://example.com/company")
cfg := Load()
if cfg.Legal.CustomLinkLabel != "Unternehmensseite" {
t.Errorf("expected label 'Unternehmensseite', got %q", cfg.Legal.CustomLinkLabel)
}
if cfg.Legal.CustomLinkURL != "https://example.com/company" {
t.Errorf("expected url 'https://example.com/company', got %q", cfg.Legal.CustomLinkURL)
}
// 2. Insecure http URL -> ignored
t.Setenv("LEGAL_CUSTOM_LINK_LABEL", "Insecure Site")
t.Setenv("LEGAL_CUSTOM_LINK_URL", "http://insecure.example.com")
cfg = Load()
if cfg.Legal.CustomLinkURL != "" || cfg.Legal.CustomLinkLabel != "" {
t.Errorf("expected empty custom link for http, got url=%q label=%q", cfg.Legal.CustomLinkURL, cfg.Legal.CustomLinkLabel)
}
// 3. Invalid scheme (javascript:) -> ignored
t.Setenv("LEGAL_CUSTOM_LINK_LABEL", "Exploit")
t.Setenv("LEGAL_CUSTOM_LINK_URL", "javascript:alert(1)")
cfg = Load()
if cfg.Legal.CustomLinkURL != "" || cfg.Legal.CustomLinkLabel != "" {
t.Errorf("expected empty custom link for javascript:, got url=%q label=%q", cfg.Legal.CustomLinkURL, cfg.Legal.CustomLinkLabel)
}
// 4. Empty URL -> empty
t.Setenv("LEGAL_CUSTOM_LINK_LABEL", "Only Label")
t.Setenv("LEGAL_CUSTOM_LINK_URL", "")
cfg = Load()
if cfg.Legal.CustomLinkURL != "" || cfg.Legal.CustomLinkLabel != "" {
t.Errorf("expected empty custom link when url is empty, got url=%q label=%q", cfg.Legal.CustomLinkURL, cfg.Legal.CustomLinkLabel)
}
}