Merge branch 'feat/footer-share-ci' into dev
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 2m39s
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 2m39s
CI keeps the 'test once per code state' plan but records commit and git tree as image labels instead of extra :sha-/:tree- tags, so the registry only holds dev, latest, vX.Y.Z and stable; the plan test now asserts the expected decisions. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
commit
141f87f3a0
22 files changed
+735
-61
No files matched your search
@@ -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
|
||||
|
||||
@@ -316,6 +316,11 @@ func (s *Server) Router() http.Handler {
|
||||
})
|
||||
})
|
||||
|
||||
// Web Share Target fallback when service worker is not active yet: redirect to /
|
||||
r.Post("/share-target", func(w http.ResponseWriter, r *http.Request) {
|
||||
http.Redirect(w, r, "/", http.StatusSeeOther)
|
||||
})
|
||||
|
||||
// Embedded Frontend SPA Handler
|
||||
if s.staticFS != nil {
|
||||
fileServer := http.FileServer(http.FS(s.staticFS))
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -1526,6 +1533,24 @@ func TestPasswordResetFlow(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestShareTargetFallbackRedirects(t *testing.T) {
|
||||
store, err := storage.NewStore(":memory:")
|
||||
if err != nil {
|
||||
t.Fatalf("NewStore: %v", err)
|
||||
}
|
||||
defer store.Close()
|
||||
router := NewServer(&config.Config{Port: "8080", DatabasePath: ":memory:", JWTSecret: "test-jwt-secret-key"}, store, ws.NewHub(), nil).Router()
|
||||
rec := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(http.MethodPost, "/share-target", nil)
|
||||
router.ServeHTTP(rec, req)
|
||||
if rec.Code != http.StatusSeeOther {
|
||||
t.Fatalf("expected 303 See Other, got %d", rec.Code)
|
||||
}
|
||||
if loc := rec.Header().Get("Location"); loc != "/" {
|
||||
t.Fatalf("expected Location '/', got %q", loc)
|
||||
}
|
||||
}
|
||||
|
||||
// testSession starts a server-side session, as a login would, for tokens made directly in tests.
|
||||
func testSession(t *testing.T, store *storage.Store, userID string) string {
|
||||
t.Helper()
|
||||
|
||||
@@ -99,8 +99,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
|
||||
@@ -176,6 +177,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")
|
||||
@@ -255,12 +270,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,
|
||||
}
|
||||
}(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user