+
+
+
+
diff --git a/server/cmd/server/buy_page.go b/server/cmd/server/buy_page.go
new file mode 100644
index 0000000..41819ff
--- /dev/null
+++ b/server/cmd/server/buy_page.go
@@ -0,0 +1,22 @@
+package main
+
+import (
+ _ "embed"
+ "net/http"
+)
+
+// buyPageHTML is the self-contained, same-origin "/buy" purchase page: login
+// -> pick a plan + payment method (USDT/nezha) -> create order -> poll until
+// activated. It talks to /v1/auth/login, /v1/pay/catalog, /v1/pay/orders and
+// /v1/pay/orders/{orderNo} via relative fetch() calls, so it's always same
+// origin as the API (no CORS needed) as long as it's served from this binary.
+//
+//go:embed buy.html
+var buyPageHTML []byte
+
+// serveBuyPage handles GET /buy. Public (no JWT) โ the page itself drives the
+// login flow client-side.
+func serveBuyPage(w http.ResponseWriter, _ *http.Request) {
+ w.Header().Set("Content-Type", "text/html; charset=utf-8")
+ _, _ = w.Write(buyPageHTML)
+}
diff --git a/server/cmd/server/main.go b/server/cmd/server/main.go
index c1bfdd4..9b683d0 100644
--- a/server/cmd/server/main.go
+++ b/server/cmd/server/main.go
@@ -131,6 +131,10 @@ func main() {
_ = json.NewEncoder(w).Encode(map[string]string{"status": "ok"})
})
+ // Public (no JWT): self-contained purchase page, same-origin as /v1/pay/*
+ // so it can call the API with plain relative fetch()s (no CORS).
+ r.Get("/buy", serveBuyPage)
+
// Optional probe ingest route. sharedProbeStore is reused by the scheduler
// (below) when both are enabled, so they share one Redis-backed store.
var sharedProbeStore *probe.Store