mirror of
https://github.com/hanzoai/base.git
synced 2026-10-21 03:41:50 +00:00
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.
173 lines
4.9 KiB
Go
173 lines
4.9 KiB
Go
package platform
|
|
|
|
import (
|
|
"encoding/json"
|
|
"io"
|
|
"net/http"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/hanzoai/base/core"
|
|
"github.com/hanzoai/base/tools/router"
|
|
)
|
|
|
|
// AuthProxyConfig holds the subset of PlatformConfig needed by the auth proxy.
|
|
type AuthProxyConfig struct {
|
|
IAMEndpoint string
|
|
IAMOrg string
|
|
IAMApp string
|
|
}
|
|
|
|
// registerAuthRoutes registers IAM auth proxy routes under /v1/platform/auth.
|
|
func (p *plugin) registerAuthRoutes(r *router.Router[*core.RequestEvent]) {
|
|
auth := r.Group("/v1/platform/auth")
|
|
|
|
auth.POST("/verify-phone", p.handleVerifyPhone)
|
|
auth.POST("/verify-code", p.handleVerifyCode)
|
|
auth.POST("/login", p.handleLogin)
|
|
auth.POST("/signup", p.handleSignup)
|
|
auth.GET("/userinfo", p.handleUserinfo)
|
|
}
|
|
|
|
// proxyToIAM forwards a request to the IAM service and copies the response back.
|
|
func (p *plugin) proxyToIAM(e *core.RequestEvent, method, path string, body []byte) error {
|
|
endpoint := strings.TrimRight(p.config.IAMEndpoint, "/")
|
|
url := endpoint + path
|
|
|
|
var reqBody io.Reader
|
|
if body != nil {
|
|
reqBody = strings.NewReader(string(body))
|
|
}
|
|
|
|
client := &http.Client{Timeout: 15 * time.Second}
|
|
|
|
req, err := http.NewRequestWithContext(e.Request.Context(), method, url, reqBody)
|
|
if err != nil {
|
|
p.app.Logger().Error("auth proxy: failed to build request",
|
|
"path", path,
|
|
"error", err.Error(),
|
|
)
|
|
return e.JSON(http.StatusBadGateway, map[string]any{"message": "proxy error"})
|
|
}
|
|
req.Header.Set("Content-Type", "application/json")
|
|
|
|
// Forward auth headers from the original request.
|
|
if v := e.Request.Header.Get("Authorization"); v != "" {
|
|
req.Header.Set("Authorization", v)
|
|
}
|
|
|
|
resp, err := client.Do(req)
|
|
if err != nil {
|
|
p.app.Logger().Warn("auth proxy: IAM request failed",
|
|
"path", path,
|
|
"error", err.Error(),
|
|
)
|
|
return e.JSON(http.StatusBadGateway, map[string]any{"message": "IAM unavailable"})
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
// Copy all response headers back.
|
|
for k, vv := range resp.Header {
|
|
for _, v := range vv {
|
|
e.Response.Header().Add(k, v)
|
|
}
|
|
}
|
|
|
|
// Copy status and body.
|
|
e.Response.WriteHeader(resp.StatusCode)
|
|
io.Copy(e.Response, resp.Body)
|
|
return nil
|
|
}
|
|
|
|
// handleVerifyPhone proxies a phone verification request to IAM.
|
|
//
|
|
// Maps { countryCode, phone } to IAM /api/send-verification-code.
|
|
func (p *plugin) handleVerifyPhone(e *core.RequestEvent) error {
|
|
raw, err := io.ReadAll(e.Request.Body)
|
|
if err != nil {
|
|
return e.JSON(http.StatusBadRequest, map[string]any{"message": "failed to read request body"})
|
|
}
|
|
|
|
var body struct {
|
|
CountryCode string `json:"countryCode"`
|
|
Phone string `json:"phone"`
|
|
}
|
|
if err := json.Unmarshal(raw, &body); err != nil {
|
|
return e.JSON(http.StatusBadRequest, map[string]any{"message": "invalid request body"})
|
|
}
|
|
|
|
phone := strings.TrimSpace(body.Phone)
|
|
if phone == "" {
|
|
return e.JSON(http.StatusBadRequest, map[string]any{"message": "phone is required"})
|
|
}
|
|
|
|
iamBody, _ := json.Marshal(map[string]any{
|
|
"applicationId": p.config.IAMApp,
|
|
"dest": phone,
|
|
"type": "phone",
|
|
"countryCode": body.CountryCode,
|
|
"method": "login",
|
|
})
|
|
|
|
return p.proxyToIAM(e, http.MethodPost, "/api/send-verification-code", iamBody)
|
|
}
|
|
|
|
// handleVerifyCode proxies OTP verification to IAM login.
|
|
//
|
|
// Maps { phone, code } to IAM /api/login.
|
|
func (p *plugin) handleVerifyCode(e *core.RequestEvent) error {
|
|
raw, err := io.ReadAll(e.Request.Body)
|
|
if err != nil {
|
|
return e.JSON(http.StatusBadRequest, map[string]any{"message": "failed to read request body"})
|
|
}
|
|
|
|
var body struct {
|
|
Phone string `json:"phone"`
|
|
Code string `json:"code"`
|
|
}
|
|
if err := json.Unmarshal(raw, &body); err != nil {
|
|
return e.JSON(http.StatusBadRequest, map[string]any{"message": "invalid request body"})
|
|
}
|
|
|
|
phone := strings.TrimSpace(body.Phone)
|
|
code := strings.TrimSpace(body.Code)
|
|
if phone == "" || code == "" {
|
|
return e.JSON(http.StatusBadRequest, map[string]any{"message": "phone and code are required"})
|
|
}
|
|
|
|
iamBody, _ := json.Marshal(map[string]any{
|
|
"application": p.config.IAMApp,
|
|
"organization": p.config.IAMOrg,
|
|
"username": phone,
|
|
"code": code,
|
|
"type": "code",
|
|
})
|
|
|
|
return p.proxyToIAM(e, http.MethodPost, "/api/login", iamBody)
|
|
}
|
|
|
|
// handleLogin proxies login credentials to IAM.
|
|
func (p *plugin) handleLogin(e *core.RequestEvent) error {
|
|
raw, err := io.ReadAll(e.Request.Body)
|
|
if err != nil {
|
|
return e.JSON(http.StatusBadRequest, map[string]any{"message": "failed to read request body"})
|
|
}
|
|
|
|
return p.proxyToIAM(e, http.MethodPost, "/api/login", raw)
|
|
}
|
|
|
|
// handleSignup proxies signup to IAM.
|
|
func (p *plugin) handleSignup(e *core.RequestEvent) error {
|
|
raw, err := io.ReadAll(e.Request.Body)
|
|
if err != nil {
|
|
return e.JSON(http.StatusBadRequest, map[string]any{"message": "failed to read request body"})
|
|
}
|
|
|
|
return p.proxyToIAM(e, http.MethodPost, "/api/signup", raw)
|
|
}
|
|
|
|
// handleUserinfo proxies a userinfo request to IAM.
|
|
func (p *plugin) handleUserinfo(e *core.RequestEvent) error {
|
|
return p.proxyToIAM(e, http.MethodGet, "/api/userinfo", nil)
|
|
}
|