feat(platform): add cmsctl and document newsletter and shell updates

Generated with Codebuff 🤖
Co-Authored-By: Codebuff <noreply@codebuff.com>
This commit is contained in:
mats committed 2026-08-20 18:13:49 +02:00
1 parent 0f8ed97878
commit fea6eb51b1
7 files changed
+2408

No files matched your search

+178
View File
@@ -0,0 +1,178 @@
package main
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"path"
"strings"
"time"
)
const maxResponseBytes = 64 << 20
type Client struct {
cfg Config
http *http.Client
token string
apiKey string
timeout time.Duration
}
func newClient(cfg Config, token, apiKey string, timeout time.Duration) *Client {
if timeout <= 0 {
timeout = 30 * time.Second
}
return &Client{
cfg: cfg,
http: &http.Client{Timeout: timeout},
token: token,
apiKey: apiKey,
timeout: timeout,
}
}
func (c *Client) cmsURL(endpoint string) string {
return joinURL(effectiveBaseURL(c.cfg), "/api", endpoint)
}
func (c *Client) finderURL(endpoint string) string {
return joinURL(effectiveFinderURL(c.cfg), "/api", endpoint)
}
// finderAdminURL is deliberately based on BaseURL, never FinderURL. The
// backoffice owns the writeradmin surface and is the only supported write path.
func (c *Client) finderAdminURL(endpoint string) string {
return joinURL(effectiveBaseURL(c.cfg), "/catalog/admin", endpoint)
}
func joinURL(base string, parts ...string) string {
u, err := url.Parse(strings.TrimRight(base, "/"))
if err != nil || u.Scheme == "" || u.Host == "" {
return strings.TrimRight(base, "/") + "/" + strings.TrimLeft(path.Join(parts...), "/")
}
joined := strings.TrimRight(u.Path, "/")
for _, part := range parts {
joined = path.Join(joined, "/"+strings.Trim(part, "/"))
}
u.Path = joined
return u.String()
}
func escapePathSegment(value string) string {
return url.PathEscape(value)
}
func (c *Client) request(ctx context.Context, method, endpoint string, body []byte, contentType string) ([]byte, int, error) {
var reader io.Reader
if body != nil {
reader = bytes.NewReader(body)
}
req, err := http.NewRequestWithContext(ctx, method, endpoint, reader)
if err != nil {
return nil, 0, err
}
req.Header.Set("Accept", "application/json, application/jsonl+json, text/plain")
if body != nil {
if contentType == "" {
contentType = "application/json"
}
req.Header.Set("Content-Type", contentType)
}
if c.apiKey != "" {
req.Header.Set("X-API-Key", c.apiKey)
} else if c.token != "" {
tokenType := c.cfg.TokenType
if tokenType == "" {
tokenType = "Bearer"
}
req.Header.Set("Authorization", tokenType+" "+c.token)
}
resp, err := c.http.Do(req)
if err != nil {
return nil, 0, err
}
defer resp.Body.Close()
responseBody, err := io.ReadAll(io.LimitReader(resp.Body, maxResponseBytes+1))
if err != nil {
return nil, resp.StatusCode, err
}
if len(responseBody) > maxResponseBytes {
return nil, resp.StatusCode, fmt.Errorf("response exceeds %d MiB", maxResponseBytes/(1<<20))
}
if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusMultipleChoices {
return responseBody, resp.StatusCode, &HTTPError{StatusCode: resp.StatusCode, Body: responseBody}
}
return responseBody, resp.StatusCode, nil
}
type HTTPError struct {
StatusCode int
Body []byte
}
func (e *HTTPError) Error() string {
body := strings.TrimSpace(string(e.Body))
if body == "" {
return fmt.Sprintf("HTTP %d", e.StatusCode)
}
var payload struct {
Error string `json:"error"`
ErrorDescription string `json:"error_description"`
Message string `json:"message"`
}
if json.Unmarshal(e.Body, &payload) == nil {
for _, msg := range []string{payload.ErrorDescription, payload.Message, payload.Error} {
if msg != "" {
return fmt.Sprintf("HTTP %d: %s", e.StatusCode, msg)
}
}
}
if len(body) > 500 {
body = body[:500] + "…"
}
return fmt.Sprintf("HTTP %d: %s", e.StatusCode, body)
}
func (c *Client) get(ctx context.Context, endpoint string) ([]byte, error) {
body, _, err := c.request(ctx, http.MethodGet, endpoint, nil, "")
return body, err
}
func (c *Client) postJSON(ctx context.Context, endpoint string, value any) ([]byte, error) {
body, err := json.Marshal(value)
if err != nil {
return nil, err
}
return c.postBody(ctx, endpoint, body, "application/json")
}
func (c *Client) postBody(ctx context.Context, endpoint string, body []byte, contentType string) ([]byte, error) {
response, _, err := c.request(ctx, http.MethodPost, endpoint, body, contentType)
return response, err
}
func (c *Client) putBody(ctx context.Context, endpoint string, body []byte, contentType string) ([]byte, error) {
response, _, err := c.request(ctx, http.MethodPut, endpoint, body, contentType)
return response, err
}
func (c *Client) patchBody(ctx context.Context, endpoint string, body []byte, contentType string) ([]byte, error) {
response, _, err := c.request(ctx, http.MethodPatch, endpoint, body, contentType)
return response, err
}
func (c *Client) delete(ctx context.Context, endpoint string) ([]byte, error) {
response, _, err := c.request(ctx, http.MethodDelete, endpoint, nil, "")
return response, err
}
func (c *Client) deleteBody(ctx context.Context, endpoint string, body []byte, contentType string) ([]byte, error) {
response, _, err := c.request(ctx, http.MethodDelete, endpoint, body, contentType)
return response, err
}