feat: route report endpoint
- POST /_seo/api/v1/registry/routes (public, bucket seo-notfound): the frontend server reports its static routes with the bearer token golem15.seo.route_report_token, else notfound_token (constant-time; both empty answer 404); at most 2000 paths - unknown paths become registry pages (origin reported, kind page, a name from the last segment), known ones get reported_at; paths another content type owns and invalid ones are skipped; nothing is deleted or overwritten, and the report's time marks missing pages
This commit is contained in:
@@ -3,16 +3,82 @@ package api
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/subtle"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"git.golem15.com/golem15/sm-seo-plugin/classes"
|
||||
"git.golem15.com/golem15/sm-seo-plugin/models"
|
||||
"git.golem15.com/golem15/summercms/modules/wire"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// maxRouteReportBody caps the route report body: 2000 paths of 2048 bytes
|
||||
// with their JSON quoting.
|
||||
const maxRouteReportBody = classes.MaxReportedRoutes*(classes.MaxRouteBytes+8) + 1024
|
||||
|
||||
// routeReport is the route report body.
|
||||
type routeReport struct {
|
||||
Routes []string `json:"routes"`
|
||||
}
|
||||
|
||||
// RouteReport serves POST /_seo/api/v1/registry/routes (D-33, P33-20): the
|
||||
// frontend server reports its static routes with "Authorization: Bearer
|
||||
// <token>", golem15.seo.route_report_token (else notfound_token), compared
|
||||
// in constant time. Unknown paths become registry pages (origin reported),
|
||||
// known ones get reported_at; nothing is deleted or overwritten. It answers
|
||||
// 200 with the counts, 401 for a missing or wrong token, 404 when no token
|
||||
// is configured (the report is off), and 422 for a malformed body or more
|
||||
// than 2000 paths.
|
||||
func RouteReport(service func() *classes.Service) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
svc := service()
|
||||
if svc == nil {
|
||||
wire.WriteJSON(w, http.StatusServiceUnavailable, map[string]string{"error": "SEO is not available"})
|
||||
return
|
||||
}
|
||||
want := svc.RouteReportToken()
|
||||
if want == "" {
|
||||
wire.WriteJSON(w, http.StatusNotFound, map[string]string{"error": errNotFound})
|
||||
return
|
||||
}
|
||||
got, ok := strings.CutPrefix(r.Header.Get("Authorization"), "Bearer ")
|
||||
if !ok || subtle.ConstantTimeCompare([]byte(strings.TrimSpace(got)), []byte(want)) != 1 {
|
||||
wire.WriteJSON(w, http.StatusUnauthorized, map[string]string{"error": "Unauthorized"})
|
||||
return
|
||||
}
|
||||
var in routeReport
|
||||
dec := json.NewDecoder(http.MaxBytesReader(w, r.Body, maxRouteReportBody))
|
||||
if err := dec.Decode(&in); err != nil {
|
||||
wire.WriteJSON(w, http.StatusUnprocessableEntity, map[string]string{"error": errInvalidBody})
|
||||
return
|
||||
}
|
||||
if len(in.Routes) > classes.MaxReportedRoutes {
|
||||
wire.WriteJSON(w, http.StatusUnprocessableEntity, map[string]any{"error": "too_many_routes", "max": classes.MaxReportedRoutes})
|
||||
return
|
||||
}
|
||||
var out classes.RouteReport
|
||||
err := write(r.Context(), svc, func(ctx context.Context, tx *gorm.DB) error {
|
||||
var err error
|
||||
out, err = svc.ReportRoutes(ctx, tx, in.Routes)
|
||||
return err
|
||||
})
|
||||
if err != nil {
|
||||
if errors.Is(err, classes.ErrTooManyRoutes) {
|
||||
wire.WriteJSON(w, http.StatusUnprocessableEntity, map[string]any{"error": "too_many_routes", "max": classes.MaxReportedRoutes})
|
||||
return
|
||||
}
|
||||
log.Error("golem15.seo: route report", "error", err)
|
||||
wire.WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": errInternal})
|
||||
return
|
||||
}
|
||||
wire.WriteJSON(w, http.StatusOK, out)
|
||||
}
|
||||
}
|
||||
|
||||
// ListPagesInput is the query of GET /_seo/api/v1/pages.
|
||||
type ListPagesInput struct {
|
||||
Page int `json:"page,omitempty" query:"page" jsonschema:"the page number, from 1"`
|
||||
|
||||
Reference in New Issue
Block a user