feat(legal): imprint & privacy notices, "Über MindSift"
- LEGAL_IMPRINT_/LEGAL_PRIVACY_ URL and Markdown (inline or *_FILE);
only http(s) URLs accepted
- /auth/status exposes legal info; public GET /legal/{imprint|privacy}
- Links on login/setup screen and in the settings footer: URL opens a new
tab, Markdown a reading modal; hidden when nothing is configured
- "Über MindSift" modal (version, license, repository) in settings and help
- Docs and compose/env examples
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
a62b204e14
commit
d6c70d7a8e
17 files changed
+318
-2
No files matched your search
@@ -83,6 +83,7 @@ func (s *Server) Router() http.Handler {
|
||||
// Public Health, Setup & Auth Routes
|
||||
r.Get("/api/v1/health", s.handleHealth)
|
||||
r.Get("/api/v1/auth/status", s.handleAuthStatus)
|
||||
r.Get("/api/v1/legal/{type}", s.handleLegalText)
|
||||
r.Post("/api/v1/auth/setup", s.handleFirstBootSetup)
|
||||
r.Post("/api/v1/auth/register", s.handleRegister)
|
||||
r.Post("/api/v1/auth/login", s.handleLogin)
|
||||
@@ -458,9 +459,32 @@ func (s *Server) handleAuthStatus(w http.ResponseWriter, r *http.Request) {
|
||||
"instanceName": instanceName,
|
||||
"demoMode": s.cfg.DemoMode,
|
||||
"sharingMode": s.store.SharingMode(),
|
||||
// Public so the login screen can link Impressum and Datenschutz
|
||||
"legal": map[string]interface{}{
|
||||
"imprintUrl": s.cfg.Legal.ImprintURL,
|
||||
"hasImprintContent": s.cfg.Legal.ImprintContent != "",
|
||||
"privacyUrl": s.cfg.Legal.PrivacyURL,
|
||||
"hasPrivacyContent": s.cfg.Legal.PrivacyContent != "",
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// handleLegalText serves the configured Markdown of the imprint or privacy notice (public, no login).
|
||||
func (s *Server) handleLegalText(w http.ResponseWriter, r *http.Request) {
|
||||
content := ""
|
||||
switch chi.URLParam(r, "type") {
|
||||
case "imprint":
|
||||
content = s.cfg.Legal.ImprintContent
|
||||
case "privacy":
|
||||
content = s.cfg.Legal.PrivacyContent
|
||||
}
|
||||
if content == "" {
|
||||
writeError(w, http.StatusNotFound, "Kein Text hinterlegt")
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, map[string]string{"content": content})
|
||||
}
|
||||
|
||||
func (s *Server) handleFirstBootSetup(w http.ResponseWriter, r *http.Request) {
|
||||
count, err := s.store.CountUsers()
|
||||
if err != nil || count > 0 {
|
||||
|
||||
@@ -1366,3 +1366,42 @@ func TestFirstBootSetupWithSampleData(t *testing.T) {
|
||||
t.Fatalf("expected sample notes and task lists, got %d notes, %d task lists", len(docs), len(lists))
|
||||
}
|
||||
}
|
||||
|
||||
// Impressum and Datenschutz must be reachable without login; unset texts stay hidden.
|
||||
func TestLegalTextsArePublic(t *testing.T) {
|
||||
dbPath := filepath.Join(t.TempDir(), "legal.db")
|
||||
store, err := storage.NewStore(dbPath)
|
||||
if err != nil {
|
||||
t.Fatalf("NewStore: %v", err)
|
||||
}
|
||||
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"}}
|
||||
router := NewServer(cfg, store, ws.NewHub(), nil).Router()
|
||||
|
||||
rec := httptest.NewRecorder()
|
||||
router.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/api/v1/auth/status", nil))
|
||||
var st struct {
|
||||
Legal struct {
|
||||
ImprintURL string `json:"imprintUrl"`
|
||||
HasImprintContent bool `json:"hasImprintContent"`
|
||||
PrivacyURL string `json:"privacyUrl"`
|
||||
HasPrivacyContent bool `json:"hasPrivacyContent"`
|
||||
} `json:"legal"`
|
||||
}
|
||||
_ = json.Unmarshal(rec.Body.Bytes(), &st)
|
||||
if !st.Legal.HasImprintContent || st.Legal.HasPrivacyContent || st.Legal.PrivacyURL != "https://example.org/datenschutz" {
|
||||
t.Fatalf("unexpected legal status: %+v", st.Legal)
|
||||
}
|
||||
|
||||
rec = httptest.NewRecorder()
|
||||
router.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/api/v1/legal/imprint", nil))
|
||||
if rec.Code != http.StatusOK || !strings.Contains(rec.Body.String(), "Max Muster") {
|
||||
t.Fatalf("imprint without login: %d %s", rec.Code, rec.Body.String())
|
||||
}
|
||||
rec = httptest.NewRecorder()
|
||||
router.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/api/v1/legal/privacy", nil))
|
||||
if rec.Code != http.StatusNotFound {
|
||||
t.Fatalf("privacy without text should be 404, got %d", rec.Code)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user