Files
decent-webui/internal/admin/admin.go
2025-09-27 09:33:40 +02:00

147 lines
4.2 KiB
Go

package admin
import (
"context"
_ "embed"
"html/template"
"net/http"
"strconv"
"strings"
"time"
"git.send.nrw/sendnrw/decent-webui/internal/filesvc"
"git.send.nrw/sendnrw/decent-webui/internal/mesh"
)
/*** Templates einbetten ***/
//go:embed tpl/layout.html
var layoutHTML string
//go:embed tpl/partials_items.html
var itemsPartialHTML string
//go:embed tpl/partials_peers.html
var peersPartialHTML string
var (
tplLayout = template.Must(template.New("layout").Parse(layoutHTML))
tplItems = template.Must(template.New("items").Funcs(template.FuncMap{
"timeRFC3339": func(unixNano int64) string {
if unixNano == 0 {
return ""
}
return time.Unix(0, unixNano).UTC().Format(time.RFC3339)
},
}).Parse(itemsPartialHTML))
tplPeers = template.Must(template.New("peers").Parse(peersPartialHTML))
)
type Deps struct {
Store filesvc.MeshStore
Mesh *mesh.Node
}
// Register hängt alle /admin Routen ein.
// Auth liegt optional VOR Register (BasicAuth-Middleware), siehe main.go.
func Register(mux *http.ServeMux, d Deps) {
// Dashboard
mux.HandleFunc("/admin", func(w http.ResponseWriter, r *http.Request) {
renderLayout(w, r, "Files", "/admin/items")
})
// Partials
mux.HandleFunc("/admin/items", func(w http.ResponseWriter, r *http.Request) {
// Liste rendern (Pagination optional via ?next=)
nextQ := strings.TrimSpace(r.URL.Query().Get("next"))
var nextID filesvc.ID
if nextQ != "" {
if n, err := strconv.ParseInt(nextQ, 10, 64); err == nil {
nextID = filesvc.ID(n)
}
}
items, nextOut, _ := d.Store.List(r.Context(), nextID, 100)
_ = tplItems.Execute(w, map[string]any{
"Items": items,
"Next": nextOut,
})
})
mux.HandleFunc("/admin/peers", func(w http.ResponseWriter, r *http.Request) {
peers := d.Mesh.PeerList()
_ = tplPeers.Execute(w, map[string]any{
"Peers": peers,
"Now": time.Now(),
})
})
// Actions (HTMX POSTs)
mux.HandleFunc("/admin/items/create", func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
name := strings.TrimSpace(r.FormValue("name"))
if name != "" {
_, _ = d.Store.Create(r.Context(), name)
_ = d.Mesh.SyncNow(r.Context()) // prompt push (best effort)
}
// Nach Aktion Items partial zurückgeben (HTMX swap)
http.Redirect(w, r, "/admin/items", http.StatusSeeOther)
})
mux.HandleFunc("/admin/items/rename", func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
idStr := r.FormValue("id")
newName := strings.TrimSpace(r.FormValue("name"))
if id, err := strconv.ParseInt(idStr, 10, 64); err == nil && newName != "" {
_, _ = d.Store.Rename(r.Context(), filesvc.ID(id), newName)
_ = d.Mesh.SyncNow(r.Context())
}
http.Redirect(w, r, "/admin/items", http.StatusSeeOther)
})
mux.HandleFunc("/admin/items/delete", func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
if id, err := strconv.ParseInt(r.FormValue("id"), 10, 64); err == nil {
_, _ = d.Store.Delete(r.Context(), filesvc.ID(id))
_ = d.Mesh.SyncNow(r.Context())
}
http.Redirect(w, r, "/admin/items", http.StatusSeeOther)
})
mux.HandleFunc("/admin/mesh/syncnow", func(w http.ResponseWriter, r *http.Request) {
_ = d.Mesh.SyncNow(context.Background())
http.Redirect(w, r, "/admin/peers", http.StatusSeeOther)
})
}
func renderLayout(w http.ResponseWriter, _ *http.Request, active string, initial string) {
_ = tplLayout.Execute(w, map[string]any{
"Active": active,
"Init": initial, // initialer HTMX Swap-Endpunkt
})
}
/*** Optional: einfache BasicAuth (siehe main.go) ***/
func BasicAuth(user, pass string, next http.Handler) http.Handler {
if strings.TrimSpace(user) == "" {
return next // deaktiviert
}
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
u, p, ok := r.BasicAuth()
if !ok || u != user || p != pass {
w.Header().Set("WWW-Authenticate", `Basic realm="admin"`)
http.Error(w, "unauthorized", http.StatusUnauthorized)
return
}
next.ServeHTTP(w, r)
})
}