feat(cabana): add markdown preview admin route

- POST {prefix}/api/v1/markdown/preview renders {markdown} through
  cabana.RenderMarkdown in the backend-guarded group behind requireAjax
- refused output is a 422 validation_failed on markdown with a fixed message
- swag annotation, regenerated admin.json and schema.d.ts
- route inventories, CSRF walk (26) and OpenAPI conformance learn the route
- README and docs/backend/forms.md document the route
This commit is contained in:
Jakub Zych
2026-10-06 20:53:07 +02:00
parent 0ff928d6cf
commit b492e79f2b
13 changed files with 517 additions and 6 deletions

View File

@@ -3,8 +3,10 @@ package cabana
import (
"bytes"
"fmt"
"net/http"
"regexp"
"git.golem15.com/golem15/summercms/modules/bouncer"
"github.com/yuin/goldmark"
)
@@ -44,3 +46,44 @@ func rejectUnsafeMarkdownHTML(html string) error {
}
return nil
}
// msgMarkdownPreviewRefused is the fixed 422 detail of a preview whose
// rendered HTML the RenderMarkdown gate refuses. The renderer's error text is
// never written.
const msgMarkdownPreviewRefused = "The rendered HTML contains a script or iframe tag, an event handler, or a javascript, vbscript or data URL and cannot be previewed."
// AdminMarkdownPreviewRequest is the body of POST /markdown/preview: the
// markdown source of a form field.
type AdminMarkdownPreviewRequest struct {
Markdown string `json:"markdown"`
}
// AdminMarkdownPreviewResult is the data of a POST /markdown/preview answer:
// the HTML RenderMarkdown produced for the source.
type AdminMarkdownPreviewResult struct {
HTML string `json:"html"`
}
// markdownPreview serves POST /markdown/preview: it renders the source
// through RenderMarkdown for the admin form preview. Any signed-in backend
// administrator may call it; it reads and writes no records. Output the
// RenderMarkdown gate refuses is a 422 on markdown with a fixed message.
func (s *service) markdownPreview(w http.ResponseWriter, r *http.Request) {
principal, ok := bouncer.User(r.Context())
if !ok || principal == nil || !principal.Backend {
WriteError(w, http.StatusUnauthorized, "unauthenticated", msgUnauthenticated)
return
}
var body AdminMarkdownPreviewRequest
if err := s.decodeStrictBody(w, r, &body); err != nil {
writeCRUDError(w, err)
return
}
html, err := RenderMarkdown(body.Markdown)
if err != nil {
WriteErrorDetails(w, http.StatusUnprocessableEntity, "validation_failed", "Validation failed",
map[string]any{"markdown": []string{msgMarkdownPreviewRefused}})
return
}
WriteData(w, http.StatusOK, AdminMarkdownPreviewResult{HTML: html}, nil)
}