feat(hub): asset management API with PVC storage and image seed

Add internal/assets package that manages app assets (logos, screenshots)
on Hub PVC with automatic seeding from baked-in image copy on first run.
Two new API endpoints: GET /assets/manifest (JSON with SHA-256 checksums)
and GET /assets/file/{name} for controllers to sync assets.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-21 15:22:45 +01:00
parent df4d0a2255
commit 3690c5028e
7 changed files with 337 additions and 1 deletions
+49
View File
@@ -11,6 +11,7 @@ import (
"strings"
"time"
"gitea.dooplex.hu/admin/felhom-hub/internal/assets"
"gitea.dooplex.hu/admin/felhom-hub/internal/configgen"
"gitea.dooplex.hu/admin/felhom-hub/internal/notify"
"gitea.dooplex.hu/admin/felhom-hub/internal/store"
@@ -31,6 +32,7 @@ type Handler struct {
httpClient *http.Client
templateProvider ConfigTemplateProvider
dispatcher *notify.Dispatcher
assetsMgr *assets.Manager
}
// New creates a new API handler.
@@ -51,6 +53,11 @@ func (h *Handler) SetDispatcher(d *notify.Dispatcher) {
h.dispatcher = d
}
// SetAssetManager sets the asset manager for serving app assets to controllers.
func (h *Handler) SetAssetManager(am *assets.Manager) {
h.assetsMgr = am
}
// checkAuth verifies the Bearer token against the global API key or a per-customer API key.
// Returns true if authorized.
func (h *Handler) checkAuth(r *http.Request) bool {
@@ -115,6 +122,11 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
case r.Method == http.MethodGet && strings.HasPrefix(path, "/config/"):
customerID := strings.TrimPrefix(path, "/config/")
h.handleConfigRetrieve(w, r, customerID)
case r.Method == http.MethodGet && path == "/assets/manifest":
h.handleAssetsManifest(w, r)
case r.Method == http.MethodGet && strings.HasPrefix(path, "/assets/file/"):
filename := strings.TrimPrefix(path, "/assets/file/")
h.handleAssetFile(w, r, filename)
default:
http.NotFound(w, r)
}
@@ -755,3 +767,40 @@ Felhom.eu monitoring`
return subject, emailText
}
// --- Asset endpoints ---
func (h *Handler) handleAssetsManifest(w http.ResponseWriter, r *http.Request) {
if !h.checkAuth(r) {
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}
if h.assetsMgr == nil {
http.Error(w, "Assets not configured", http.StatusServiceUnavailable)
return
}
data, err := h.assetsMgr.MarshalManifestJSON()
if err != nil {
http.Error(w, "Internal error", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
w.Write(data)
}
func (h *Handler) handleAssetFile(w http.ResponseWriter, r *http.Request, filename string) {
if !h.checkAuth(r) {
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}
if h.assetsMgr == nil {
http.Error(w, "Assets not configured", http.StatusServiceUnavailable)
return
}
h.assetsMgr.ServeFile(w, r, filename)
}