Files
base/plugins/platform/auth_proxy.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

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)
}