194 lines
7.3 KiB
Go
194 lines
7.3 KiB
Go
// Das main-Package ist der Einstiegspunkt jedes Go-Programms.
|
|
package main
|
|
|
|
import (
|
|
"context"
|
|
"embed"
|
|
"io/fs"
|
|
"log"
|
|
"net/http"
|
|
"os"
|
|
"os/signal"
|
|
"path/filepath"
|
|
"syscall"
|
|
"time"
|
|
|
|
"github.com/joho/godotenv"
|
|
"wiki/handler"
|
|
"wiki/middleware"
|
|
)
|
|
|
|
// //go:embed ist eine spezielle Go-Direktive die Dateien beim Kompilieren direkt
|
|
// ins Binary einbettet. Nach dem Build sind templates/ und static/ nicht mehr als
|
|
// separate Ordner nötig — alles steckt im Binary selbst.
|
|
//
|
|
// Das Ergebnis ist ein einzelnes Binary das überall läuft ohne externe Dateien.
|
|
|
|
//go:embed templates/*
|
|
var templatesFS embed.FS // enthält templates/page.html, password.html, login.html, switch-team.html
|
|
|
|
//go:embed static/*
|
|
var staticFS embed.FS // enthält static/style.css
|
|
|
|
func main() {
|
|
// .env-Datei laden falls vorhanden.
|
|
// Echte Umgebungsvariablen haben immer Vorrang über .env.
|
|
if err := godotenv.Load(); err != nil && !os.IsNotExist(err) {
|
|
log.Printf("Hinweis: .env konnte nicht geladen werden: %v", err)
|
|
}
|
|
|
|
// Konfiguration lesen — Reihenfolge: echte ENV > .env > Standardwert
|
|
dataRoot := getEnvOrDefault("DATA_ROOT", "data")
|
|
teamsConfig := getEnvOrDefault("TEAMS_CONFIG", "teams.yaml")
|
|
adminToken := os.Getenv("ADMIN_TOKEN")
|
|
sessionSecret := os.Getenv("SESSION_SECRET")
|
|
webhookSecret := os.Getenv("WEBHOOK_SECRET")
|
|
port := getEnvOrDefault("PORT", "8080")
|
|
|
|
if sessionSecret == "" {
|
|
log.Fatal("SESSION_SECRET ist nicht gesetzt — wird gebraucht um Web-Sessions fälschungssicher zu signieren. Siehe .env.example.")
|
|
}
|
|
|
|
// Kein unsicherer Default (z.B. "dev-token") — der globale Admin-Zugang sieht/verwaltet
|
|
// ALLE Teams, ein erratbarer Standardwert wäre ein Backdoor. Lieber der Server startet
|
|
// gar nicht, als dass er mit einem schwachen Token läuft.
|
|
if adminToken == "" {
|
|
log.Fatal("ADMIN_TOKEN ist nicht gesetzt — wird für den globalen Admin-Zugang gebraucht. Siehe .env.example.")
|
|
}
|
|
|
|
// data-Wurzelordner automatisch anlegen falls er noch nicht existiert.
|
|
if err := os.MkdirAll(dataRoot, 0755); err != nil {
|
|
log.Fatal("Daten-Ordner konnte nicht erstellt werden:", err)
|
|
}
|
|
|
|
// Team-Konfiguration laden (siehe teams.yaml.example) und die Registry aufbauen —
|
|
// legt für jedes Team den Ordner data/<id>/content an und baut dessen PageCache auf.
|
|
teamsCfg, err := handler.LoadTeamsConfig(teamsConfig)
|
|
if err != nil {
|
|
log.Fatal("Team-Konfiguration konnte nicht geladen werden:", err)
|
|
}
|
|
|
|
registry, err := handler.NewTeamRegistry(dataRoot, teamsCfg, adminToken)
|
|
if err != nil {
|
|
log.Fatal("Fehler beim Aufbauen der Team-Registry:", err)
|
|
}
|
|
|
|
// teams.yaml auf Änderungen überwachen — neue/geänderte/entfernte Teams wirken
|
|
// dann ohne Serverneustart (siehe handler/teams_config.go).
|
|
handler.StartTeamsConfigWatcher(teamsConfig, registry, 5*time.Second)
|
|
|
|
// Audit-Log: protokolliert Upload/Delete/Login/Webhook-Sync als JSON Lines.
|
|
// Eine Datei für alle Teams, siehe handler/audit.go.
|
|
auditLogger := handler.NewAuditLogger(filepath.Join(dataRoot, "audit.log"))
|
|
|
|
// Die Handler erstellen.
|
|
// templatesFS wird an die Handler übergeben — sie lesen die Templates
|
|
// direkt aus dem eingebetteten Dateisystem im Binary.
|
|
wikiHandler, err := handler.NewWikiHandler(templatesFS)
|
|
if err != nil {
|
|
log.Fatal("Fehler beim Laden der Templates:", err)
|
|
}
|
|
|
|
authHandler, err := handler.NewAuthHandler(templatesFS, registry, []byte(sessionSecret), auditLogger)
|
|
if err != nil {
|
|
log.Fatal("Fehler beim Laden der Login-Templates:", err)
|
|
}
|
|
|
|
auditPageHandler, err := handler.NewAuditPageHandler(templatesFS, auditLogger)
|
|
if err != nil {
|
|
log.Fatal("Fehler beim Laden des Audit-Log-Templates:", err)
|
|
}
|
|
|
|
apiHandler := handler.NewAPIHandler(registry, auditLogger)
|
|
mcpHandler := handler.NewMCPHandler(registry, auditLogger)
|
|
|
|
// Den Router einrichten.
|
|
mux := http.NewServeMux()
|
|
|
|
// Statische Dateien aus dem eingebetteten Dateisystem ausliefern.
|
|
// fs.Sub() gibt eine "Unterperspektive" des FS zurück — schneidet den
|
|
// "static/"-Prefix weg damit der FileServer die Dateien direkt findet.
|
|
// Ohne Sub() würde /static/style.css nach "static/static/style.css" suchen.
|
|
staticSubFS, err := fs.Sub(staticFS, "static")
|
|
if err != nil {
|
|
log.Fatal("Fehler beim Einrichten des Static-FS:", err)
|
|
}
|
|
mux.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.FS(staticSubFS))))
|
|
|
|
// Login/Logout brauchen selbst keine bestehende Session.
|
|
mux.HandleFunc("/login", authHandler.HandleLogin)
|
|
mux.HandleFunc("/logout", authHandler.HandleLogout)
|
|
|
|
sessionSecretBytes := []byte(sessionSecret)
|
|
|
|
// Wiki-Seiten anzeigen, Suche, Seiten-Passwort-Formular und Team-Umschaltung —
|
|
// alle hinter der Team-Session (siehe middleware/session.go).
|
|
mux.Handle("/", middleware.RequireTeamSession(wikiHandler, registry, sessionSecretBytes))
|
|
mux.Handle("/search", middleware.RequireTeamSession(http.HandlerFunc(wikiHandler.HandleSearch), registry, sessionSecretBytes))
|
|
mux.Handle("/auth/", middleware.RequireTeamSession(http.HandlerFunc(wikiHandler.HandlePasswordSubmit), registry, sessionSecretBytes))
|
|
mux.Handle("/switch-team", middleware.RequireTeamSession(http.HandlerFunc(authHandler.HandleSwitchTeam), registry, sessionSecretBytes))
|
|
mux.Handle("/admin/audit", middleware.RequireTeamSession(http.HandlerFunc(auditPageHandler.HandleAuditPage), registry, sessionSecretBytes))
|
|
|
|
// REST-API — durch Team-Key oder Admin-Token geschützt (middleware.RequireTeamOrAdminKey
|
|
// löst den Bearer-Key gegen die TeamRegistry auf; der Admin-Token braucht zusätzlich
|
|
// den Parameter "team", siehe handler/api.go).
|
|
mux.Handle("/api/upload", middleware.RequireTeamOrAdminKey(
|
|
http.HandlerFunc(apiHandler.HandleUpload), registry,
|
|
))
|
|
mux.Handle("/api/delete", middleware.RequireTeamOrAdminKey(
|
|
http.HandlerFunc(apiHandler.HandleDelete), registry,
|
|
))
|
|
|
|
// MCP-Endpoint für KI-Tools — gleiche Team-Auflösung wie die REST-API;
|
|
// der Admin-Token braucht hier den Header "X-Wiki-Team" (siehe handler/mcp.go).
|
|
mux.Handle("/mcp", middleware.RequireTeamOrAdminKey(mcpHandler, registry))
|
|
|
|
// Webhook — nur aktiv wenn WEBHOOK_SECRET gesetzt ist.
|
|
if webhookSecret != "" {
|
|
webhookHandler := handler.NewWebhookHandler(dataRoot, webhookSecret, registry, auditLogger)
|
|
mux.Handle("/api/webhook", webhookHandler)
|
|
log.Println("Git-Webhook aktiv unter /api/webhook")
|
|
} else {
|
|
log.Println("Hinweis: WEBHOOK_SECRET nicht gesetzt — Git-Webhook deaktiviert")
|
|
}
|
|
|
|
// Server starten mit Graceful Shutdown.
|
|
server := &http.Server{
|
|
Addr: ":" + port,
|
|
Handler: mux,
|
|
}
|
|
|
|
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
|
|
defer stop()
|
|
|
|
go func() {
|
|
log.Printf("Wiki läuft auf http://localhost:%s", port)
|
|
log.Printf("Daten-Ordner: %s (%d Teams)", dataRoot, len(registry.All()))
|
|
|
|
err := server.ListenAndServe()
|
|
if err != nil && err != http.ErrServerClosed {
|
|
log.Fatal("Server-Fehler:", err)
|
|
}
|
|
}()
|
|
|
|
<-ctx.Done()
|
|
log.Println("Server wird gestoppt...")
|
|
|
|
shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
|
defer cancel()
|
|
|
|
if err := server.Shutdown(shutdownCtx); err != nil {
|
|
log.Fatal("Fehler beim Stoppen:", err)
|
|
}
|
|
|
|
log.Println("Server gestoppt.")
|
|
}
|
|
|
|
// getEnvOrDefault liest eine Umgebungsvariable und gibt einen Standardwert zurück
|
|
// falls die Variable nicht gesetzt ist.
|
|
func getEnvOrDefault(key, defaultValue string) string {
|
|
if value := os.Getenv(key); value != "" {
|
|
return value
|
|
}
|
|
return defaultValue
|
|
}
|