Files
honeydany/app/dashboard/webtemplate_api.go
T
2025-09-28 21:28:39 +01:00

229 lines
7.8 KiB
Go

package dashboard
import (
"encoding/json"
"fmt"
"net/http"
"path/filepath"
"strings"
)
// WebTemplateAPI exposes endpoints to manage web templates
type WebTemplateAPI struct {
templateManager interface {
GetTemplate(name string) (string, error)
SaveTemplate(name, content string) error
ListTemplates() ([]string, error)
DeleteTemplate(name string) error
ValidateTemplate(content string) error
CreateDefaultTemplate(name string) error
}
}
// NewWebTemplateAPI constructs the API wrapper
func NewWebTemplateAPI(tm interface {
GetTemplate(name string) (string, error)
SaveTemplate(name, content string) error
ListTemplates() ([]string, error)
DeleteTemplate(name string) error
ValidateTemplate(content string) error
CreateDefaultTemplate(name string) error
}) *WebTemplateAPI {
return &WebTemplateAPI{templateManager: tm}
}
// RegisterRoutes registers all endpoints (guarded by security middleware from caller)
func (wta *WebTemplateAPI) RegisterRoutes(mux *http.ServeMux, sm *SecurityManager) {
mux.HandleFunc("/api/webtemplates", sm.APIAuthMiddleware(sm.RoleMiddleware("admin")(wta.handleTemplates)))
mux.HandleFunc("/api/webtemplates/", sm.APIAuthMiddleware(sm.RoleMiddleware("admin")(wta.handleTemplate)))
mux.HandleFunc("/api/webtemplates/validate", sm.APIAuthMiddleware(sm.RoleMiddleware("admin")(wta.handleValidateTemplate)))
}
// handleTemplates supports list and create
func (wta *WebTemplateAPI) handleTemplates(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodGet:
wta.handleListTemplates(w, r)
case http.MethodPost:
wta.handleCreateTemplate(w, r)
default:
wta.sendJSONError(w, "Method not allowed", http.StatusMethodNotAllowed)
}
}
// handleTemplate supports get/update/delete for a specific template name in the path
func (wta *WebTemplateAPI) handleTemplate(w http.ResponseWriter, r *http.Request) {
name := strings.TrimPrefix(r.URL.Path, "/api/webtemplates/")
if name == "" || name == "/" {
wta.sendJSONError(w, "Template name required", http.StatusBadRequest)
return
}
// Normalize (strip leading slash, append .html if missing)
name = strings.TrimPrefix(strings.TrimSpace(name), "/")
if name != "" && !strings.HasSuffix(strings.ToLower(name), ".html") {
name += ".html"
}
if !wta.isValidTemplateName(name) {
wta.sendJSONError(w, "Invalid template name", http.StatusBadRequest)
return
}
switch r.Method {
case http.MethodGet:
wta.handleGetTemplate(w, r, name)
case http.MethodPut:
wta.handleUpdateTemplate(w, r, name)
case http.MethodDelete:
wta.handleDeleteTemplate(w, r, name)
default:
wta.sendJSONError(w, "Method not allowed", http.StatusMethodNotAllowed)
}
}
// List
func (wta *WebTemplateAPI) handleListTemplates(w http.ResponseWriter, r *http.Request) {
names, err := wta.templateManager.ListTemplates()
if err != nil {
wta.sendJSONError(w, "Failed to list templates", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(map[string]any{
"templates": names,
"count": len(names),
})
}
// Get
func (wta *WebTemplateAPI) handleGetTemplate(w http.ResponseWriter, r *http.Request, name string) {
content, err := wta.templateManager.GetTemplate(name)
if err != nil {
wta.sendJSONError(w, "Template not found", http.StatusNotFound)
return
}
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(map[string]any{"name": name, "content": content})
}
// Create
func (wta *WebTemplateAPI) handleCreateTemplate(w http.ResponseWriter, r *http.Request) {
var req struct {
Name string `json:"name"`
Content string `json:"content"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
wta.sendJSONError(w, "Invalid JSON", http.StatusBadRequest)
return
}
req.Name = strings.TrimSpace(req.Name)
if req.Name != "" && !strings.HasSuffix(strings.ToLower(req.Name), ".html") {
req.Name += ".html"
}
if !wta.isValidTemplateName(req.Name) {
wta.sendJSONError(w, fmt.Sprintf("Invalid template name: %v", req.Name), http.StatusBadRequest)
return
}
if strings.TrimSpace(req.Content) == "" {
if err := wta.templateManager.CreateDefaultTemplate(req.Name); err != nil {
wta.sendJSONError(w, fmt.Sprintf("Failed to create default template: %v", err), http.StatusInternalServerError)
return
}
} else {
if err := wta.templateManager.ValidateTemplate(req.Content); err != nil {
wta.sendJSONError(w, fmt.Sprintf("Invalid template: %v", err), http.StatusBadRequest)
return
}
if err := wta.templateManager.SaveTemplate(req.Name, req.Content); err != nil {
wta.sendJSONError(w, fmt.Sprintf("Failed to save template: %v", err), http.StatusInternalServerError)
return
}
}
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(map[string]string{"success": "Template created successfully", "name": req.Name})
}
// Update
func (wta *WebTemplateAPI) handleUpdateTemplate(w http.ResponseWriter, r *http.Request, name string) {
var req struct{ Content string `json:"content"` }
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
wta.sendJSONError(w, "Invalid JSON", http.StatusBadRequest)
return
}
if err := wta.templateManager.ValidateTemplate(req.Content); err != nil {
wta.sendJSONError(w, fmt.Sprintf("Invalid template: %v", err), http.StatusBadRequest)
return
}
if err := wta.templateManager.SaveTemplate(name, req.Content); err != nil {
wta.sendJSONError(w, "Failed to update template", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(map[string]string{"success": "Template updated successfully", "name": name})
}
// Delete
func (wta *WebTemplateAPI) handleDeleteTemplate(w http.ResponseWriter, r *http.Request, name string) {
if err := wta.templateManager.DeleteTemplate(name); err != nil {
wta.sendJSONError(w, "Failed to delete template", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(map[string]string{"success": "Template deleted successfully", "name": name})
}
// Validate (content only)
func (wta *WebTemplateAPI) handleValidateTemplate(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
wta.sendJSONError(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
var req struct{ Content string `json:"content"` }
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
wta.sendJSONError(w, "Invalid JSON", http.StatusBadRequest)
return
}
if err := wta.templateManager.ValidateTemplate(req.Content); err != nil {
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(map[string]any{"valid": false, "error": err.Error()})
return
}
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(map[string]any{"valid": true})
}
// Validation helper for names
func (wta *WebTemplateAPI) isValidTemplateName(name string) bool {
name = strings.TrimSpace(name)
if name == "" {
return false
}
if !strings.HasSuffix(strings.ToLower(name), ".html") {
return false
}
// Disallow path separators and traversal
if strings.Contains(name, "/") || strings.Contains(name, "\\") || strings.Contains(name, "..") {
return false
}
// Must be a base filename
if name != filepath.Base(name) {
return false
}
if len(name) > 100 {
return false
}
// Only safe characters
for i := 0; i < len(name); i++ {
c := name[i]
if !(c == '-' || c == '_' || c == '.' || (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9')) {
return false
}
}
return true
}
func (wta *WebTemplateAPI) sendJSONError(w http.ResponseWriter, message string, statusCode int) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(statusCode)
_ = json.NewEncoder(w).Encode(map[string]string{"error": message})
}