Files
base/cmd/cli/client.go
zeekay 70a06029a4 base: route convention /api/* -> /v1/*
One path: /v1/*. We do not use /api/ — that's Casdoor's path. The
BASE_API_PREFIX env knob is gone; the prefix is hard-coded in
apis/base.go and apis/serve.go banner.

Server-side mounts:
- apis: bindHealthApi, bindCollectionApi, bindRecordCrudApi, etc. all
  attach under r.Group("/v1") (was r.Group(BASE_API_PREFIX, default /api))
- platform plugin: /api/platform/* -> /v1/platform/* (including auth and
  org sub-groups)
- kms plugin: /api/kms/* -> /v1/kms/*
- functions plugin: /api/functions/* -> /v1/functions/*
- cloudsql plugin: /api/cloud-sql/*, /api/meta/* -> /v1/*
- superuser OIDC: /_/api/oidc/config -> /_/v1/oidc/config
- core settings: default RateLimitRule labels /api/batch, /api/ ->
  /v1/batch, /v1/
- apis/batch.go: ValidBatchActions regexes match /v1/collections/*;
  internal rewrite targets /v1/collections/* (was /v1/base/collections/*,
  a stale artifact)

Clients:
- sdk/base-js (BaseClient.collections, files, realtime, CRDT WS,
  signIn/refresh/health) targets /v1/*
- sdk/base-react (use-crdt WS path) targets /v1/crdt
- ui-react admin (lib/api.ts, settings.application copy) targets /v1/*
- cmd/cli (NewClient.APIPrefix) defaults to /v1; client_test.go and
  cmd/cli_test.go stubServer use /v1/*
- cmd/typegen calls /v1/collections

Upstream paths untouched: Casdoor IAM (/api/login, /api/userinfo,
/api/signup, /api/send-verification-code, /api/get-user), KMS Infisical
(/api/v1/auth/universal-auth/login, /api/v2/workspace/environments),
Kubernetes tasks (/api/v1/namespaces/...), IPFS (/api/v0/...), Gitea/
GitLab/Discord/Strava/Patreon/Box/Wakatime upstream APIs. Those are
their contracts, not ours.

Tests: all 437 prior /api/ test URLs across apis/*_test.go and
plugins/platform/auth_proxy_test.go updated to /v1/. Build + go vet
clean. ./apis ./core ./plugins/platform ./plugins/jsvm ./forms all
green. Pre-existing red probes (network/TestRedProbe_*,
migratecmd/TestAutomigrateCollection*) unchanged.
2026-05-15 14:41:26 -07:00

125 lines
2.9 KiB
Go

package cli
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"strings"
"time"
)
// Client is a thin HTTP client for the Base API.
type Client struct {
BaseURL string
Token string
Tenant string
APIPrefix string
HTTP *http.Client
}
// NewClient returns a Client with sensible defaults.
func NewClient(baseURL, token, tenant string) *Client {
return &Client{
BaseURL: strings.TrimRight(baseURL, "/"),
Token: token,
Tenant: tenant,
APIPrefix: "/v1",
HTTP: &http.Client{Timeout: 30 * time.Second},
}
}
// Do executes a raw HTTP request against the Base API and returns
// the decoded JSON body. Non-2xx responses are returned as an error.
func (c *Client) Do(method, path string, body any) (json.RawMessage, int, error) {
var reqBody io.Reader
if body != nil {
b, err := json.Marshal(body)
if err != nil {
return nil, 0, fmt.Errorf("marshal request body: %w", err)
}
reqBody = bytes.NewReader(b)
}
fullURL := c.BaseURL + c.APIPrefix + path
req, err := http.NewRequest(method, fullURL, reqBody)
if err != nil {
return nil, 0, fmt.Errorf("build request: %w", err)
}
if body != nil {
req.Header.Set("Content-Type", "application/json")
}
if c.Token != "" {
req.Header.Set("Authorization", c.Token)
}
if c.Tenant != "" {
req.Header.Set("X-Org-Id", c.Tenant)
}
resp, err := c.HTTP.Do(req)
if err != nil {
return nil, 0, fmt.Errorf("http request: %w", err)
}
defer resp.Body.Close()
raw, err := io.ReadAll(resp.Body)
if err != nil {
return nil, resp.StatusCode, fmt.Errorf("read response: %w", err)
}
if resp.StatusCode >= 400 {
return nil, resp.StatusCode, fmt.Errorf("HTTP %d: %s", resp.StatusCode, string(raw))
}
if len(raw) == 0 {
return nil, resp.StatusCode, nil
}
return json.RawMessage(raw), resp.StatusCode, nil
}
// Get performs a GET request.
func (c *Client) Get(path string) (json.RawMessage, int, error) {
return c.Do(http.MethodGet, path, nil)
}
// Post performs a POST request with a JSON body.
func (c *Client) Post(path string, body any) (json.RawMessage, int, error) {
return c.Do(http.MethodPost, path, body)
}
// Patch performs a PATCH request with a JSON body.
func (c *Client) Patch(path string, body any) (json.RawMessage, int, error) {
return c.Do(http.MethodPatch, path, body)
}
// Delete performs a DELETE request.
func (c *Client) Delete(path string) (json.RawMessage, int, error) {
return c.Do(http.MethodDelete, path, nil)
}
// BuildQuery encodes query parameters for list endpoints.
func BuildQuery(filter string, limit int, sort string, extra map[string]string) string {
params := url.Values{}
if filter != "" {
params.Set("filter", filter)
}
if limit > 0 {
params.Set("perPage", fmt.Sprintf("%d", limit))
}
if sort != "" {
params.Set("sort", sort)
}
for k, v := range extra {
params.Set(k, v)
}
if len(params) == 0 {
return ""
}
return "?" + params.Encode()
}