- DB-Migration 002_auth.sql (users + sessions Tabellen) - AuthStore mit Session-Management, bcrypt, EnsureAdminUser - Login/Logout Handler mit Cookie-Session (HttpOnly, SameSite=Lax) - Login-Template (Bulma-Card, deutsche Labels) - Config: AdminPassword, DefaultTenantSlug, DevMode - Fallback-Texte: "Netzwerk offline" → "Server nicht erreichbar" - TENANT-FEATURE-PLAN.md mit 46 Checkboxen als Steuerungsdatei Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
153 lines
7.7 KiB
Go
153 lines
7.7 KiB
Go
package httpapi
|
|
|
|
import (
|
|
"log"
|
|
"net/http"
|
|
|
|
"git.az-it.net/az/morz-infoboard/server/backend/internal/config"
|
|
"git.az-it.net/az/morz-infoboard/server/backend/internal/httpapi/manage"
|
|
"git.az-it.net/az/morz-infoboard/server/backend/internal/mqttnotifier"
|
|
"git.az-it.net/az/morz-infoboard/server/backend/internal/store"
|
|
)
|
|
|
|
// RouterDeps holds all dependencies needed to build the HTTP router.
|
|
type RouterDeps struct {
|
|
StatusStore playerStatusStore
|
|
TenantStore *store.TenantStore
|
|
ScreenStore *store.ScreenStore
|
|
MediaStore *store.MediaStore
|
|
PlaylistStore *store.PlaylistStore
|
|
AuthStore *store.AuthStore
|
|
Notifier *mqttnotifier.Notifier
|
|
Config config.Config
|
|
UploadDir string
|
|
Logger *log.Logger
|
|
}
|
|
|
|
func NewRouter(deps RouterDeps) http.Handler {
|
|
mux := http.NewServeMux()
|
|
|
|
// ── Health ───────────────────────────────────────────────────────────
|
|
mux.HandleFunc("GET /healthz", func(w http.ResponseWriter, _ *http.Request) {
|
|
writeJSON(w, http.StatusOK, map[string]string{
|
|
"status": "ok",
|
|
"service": "morz-infoboard-backend",
|
|
})
|
|
})
|
|
|
|
// ── Status / diagnostic UI ───────────────────────────────────────────
|
|
mux.HandleFunc("GET /status", handleStatusPage(deps.StatusStore))
|
|
mux.HandleFunc("GET /status/{screenId}", handleScreenDetailPage(deps.StatusStore))
|
|
|
|
// ── API meta ─────────────────────────────────────────────────────────
|
|
mux.HandleFunc("GET /api/v1", func(w http.ResponseWriter, _ *http.Request) {
|
|
writeJSON(w, http.StatusOK, map[string]any{
|
|
"name": "morz-infoboard-backend",
|
|
"version": "dev",
|
|
"tools": []string{
|
|
"message-wall-resolve",
|
|
"screen-status-list",
|
|
"screen-status-detail",
|
|
"player-status-ingest",
|
|
"screen-status-delete",
|
|
},
|
|
})
|
|
})
|
|
mux.HandleFunc("GET /api/v1/meta", handleMeta)
|
|
|
|
// ── Player status (existing) ──────────────────────────────────────────
|
|
mux.HandleFunc("POST /api/v1/player/status", handlePlayerStatus(deps.StatusStore))
|
|
mux.HandleFunc("GET /api/v1/screens/status", handleListLatestPlayerStatuses(deps.StatusStore))
|
|
mux.HandleFunc("GET /api/v1/screens/{screenId}/status", handleGetLatestPlayerStatus(deps.StatusStore))
|
|
mux.HandleFunc("DELETE /api/v1/screens/{screenId}/status", handleDeletePlayerStatus(deps.StatusStore))
|
|
|
|
// ── Message wall ──────────────────────────────────────────────────────
|
|
mux.HandleFunc("POST /api/v1/tools/message-wall/resolve", handleResolveMessageWall)
|
|
|
|
// ── Playlist management — only register if stores are wired up ────────
|
|
if deps.TenantStore != nil {
|
|
registerManageRoutes(mux, deps)
|
|
}
|
|
|
|
return mux
|
|
}
|
|
|
|
func registerManageRoutes(mux *http.ServeMux, d RouterDeps) {
|
|
uploadDir := d.UploadDir
|
|
if uploadDir == "" {
|
|
uploadDir = "/tmp/morz-uploads"
|
|
}
|
|
|
|
// Ensure notifier is never nil inside handlers (no-op when broker not configured).
|
|
notifier := d.Notifier
|
|
if notifier == nil {
|
|
notifier = mqttnotifier.New("", "", "")
|
|
}
|
|
|
|
// Serve uploaded files.
|
|
mux.Handle("GET /uploads/", http.StripPrefix("/uploads/", http.FileServer(http.Dir(uploadDir))))
|
|
|
|
// Serve embedded static assets (Bulma CSS, SortableJS) — no external CDN needed.
|
|
mux.HandleFunc("GET /static/bulma.min.css", manage.HandleStaticBulmaCSS())
|
|
mux.HandleFunc("GET /static/Sortable.min.js", manage.HandleStaticSortableJS())
|
|
|
|
// ── Auth (no auth middleware required) ────────────────────────────────
|
|
mux.HandleFunc("GET /login", manage.HandleLoginUI(d.AuthStore))
|
|
mux.HandleFunc("POST /login", manage.HandleLoginPost(d.AuthStore, d.Config))
|
|
mux.HandleFunc("POST /logout", manage.HandleLogoutPost(d.AuthStore))
|
|
|
|
// ── Admin UI ──────────────────────────────────────────────────────────
|
|
mux.HandleFunc("GET /admin", manage.HandleAdminUI(d.TenantStore, d.ScreenStore))
|
|
mux.HandleFunc("POST /admin/screens/provision", manage.HandleProvisionUI(d.TenantStore, d.ScreenStore))
|
|
mux.HandleFunc("POST /admin/screens", manage.HandleCreateScreenUI(d.TenantStore, d.ScreenStore))
|
|
mux.HandleFunc("POST /admin/screens/{screenId}/delete", manage.HandleDeleteScreenUI(d.ScreenStore))
|
|
|
|
// ── Playlist management UI ────────────────────────────────────────────
|
|
mux.HandleFunc("GET /manage/{screenSlug}",
|
|
manage.HandleManageUI(d.TenantStore, d.ScreenStore, d.MediaStore, d.PlaylistStore))
|
|
mux.HandleFunc("POST /manage/{screenSlug}/upload",
|
|
manage.HandleUploadMediaUI(d.MediaStore, d.ScreenStore, uploadDir))
|
|
mux.HandleFunc("POST /manage/{screenSlug}/items",
|
|
manage.HandleAddItemUI(d.PlaylistStore, d.MediaStore, d.ScreenStore, notifier))
|
|
mux.HandleFunc("POST /manage/{screenSlug}/items/{itemId}",
|
|
manage.HandleUpdateItemUI(d.PlaylistStore, notifier))
|
|
mux.HandleFunc("POST /manage/{screenSlug}/items/{itemId}/delete",
|
|
manage.HandleDeleteItemUI(d.PlaylistStore, notifier))
|
|
mux.HandleFunc("POST /manage/{screenSlug}/reorder",
|
|
manage.HandleReorderUI(d.PlaylistStore, d.ScreenStore, notifier))
|
|
mux.HandleFunc("POST /manage/{screenSlug}/media/{mediaId}/delete",
|
|
manage.HandleDeleteMediaUI(d.MediaStore, d.ScreenStore, uploadDir, notifier))
|
|
|
|
// ── JSON API — screens ────────────────────────────────────────────────
|
|
// Self-registration: called by agent on startup (must be before /{tenantSlug}/ routes)
|
|
mux.HandleFunc("POST /api/v1/screens/register",
|
|
manage.HandleRegisterScreen(d.TenantStore, d.ScreenStore))
|
|
mux.HandleFunc("GET /api/v1/tenants/{tenantSlug}/screens",
|
|
manage.HandleListScreens(d.TenantStore, d.ScreenStore))
|
|
mux.HandleFunc("POST /api/v1/tenants/{tenantSlug}/screens",
|
|
manage.HandleCreateScreen(d.TenantStore, d.ScreenStore))
|
|
|
|
// ── JSON API — media ──────────────────────────────────────────────────
|
|
mux.HandleFunc("GET /api/v1/tenants/{tenantSlug}/media",
|
|
manage.HandleListMedia(d.TenantStore, d.MediaStore))
|
|
mux.HandleFunc("POST /api/v1/tenants/{tenantSlug}/media",
|
|
manage.HandleUploadMedia(d.TenantStore, d.MediaStore, uploadDir))
|
|
mux.HandleFunc("DELETE /api/v1/media/{id}",
|
|
manage.HandleDeleteMedia(d.MediaStore, uploadDir))
|
|
|
|
// ── JSON API — playlists ──────────────────────────────────────────────
|
|
mux.HandleFunc("GET /api/v1/screens/{screenId}/playlist",
|
|
manage.HandlePlayerPlaylist(d.ScreenStore, d.PlaylistStore))
|
|
mux.HandleFunc("GET /api/v1/playlists/{screenId}",
|
|
manage.HandleGetPlaylist(d.ScreenStore, d.PlaylistStore))
|
|
mux.HandleFunc("POST /api/v1/playlists/{playlistId}/items",
|
|
manage.HandleAddItem(d.PlaylistStore, d.MediaStore, notifier))
|
|
mux.HandleFunc("PATCH /api/v1/items/{itemId}",
|
|
manage.HandleUpdateItem(d.PlaylistStore, notifier))
|
|
mux.HandleFunc("DELETE /api/v1/items/{itemId}",
|
|
manage.HandleDeleteItem(d.PlaylistStore, notifier))
|
|
mux.HandleFunc("PUT /api/v1/playlists/{playlistId}/order",
|
|
manage.HandleReorder(d.PlaylistStore, notifier))
|
|
mux.HandleFunc("PATCH /api/v1/playlists/{playlistId}/duration",
|
|
manage.HandleUpdatePlaylistDuration(d.PlaylistStore))
|
|
}
|