mirror of
https://github.com/GMWalletApp/epusdt.git
synced 2026-07-07 18:26:16 +00:00
feat: add admin panel with multi-chain support and notification system
- Add full admin REST API: auth, API keys, chains, chain tokens, wallets, orders, RPC nodes, settings, dashboard stats, notifications - Add JWT-based admin authentication and API key auth middleware - Add multi-chain listeners: BSC, ETH, Polygon, Plasma, EVM WebSocket - Add RPC node health check job with automatic failover - Add Telegram notification channel for order events - Add installer for first-run database initialization - Add new DB models: admin_user, api_key, chain, chain_token, rpc_node, settings, notification_channel - Add order statistics aggregation (daily/monthly/by-chain) - Serve built admin SPA from embedded www/ assets - Add comprehensive test coverage for all new features
This commit is contained in:
@@ -2,37 +2,142 @@ package middleware
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io/ioutil"
|
||||
"crypto/subtle"
|
||||
"io"
|
||||
"net"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/assimon/luuu/config"
|
||||
"github.com/assimon/luuu/model/data"
|
||||
"github.com/assimon/luuu/util/constant"
|
||||
"github.com/assimon/luuu/util/json"
|
||||
"github.com/assimon/luuu/util/sign"
|
||||
"github.com/labstack/echo/v4"
|
||||
)
|
||||
|
||||
// Context keys populated by CheckApiSign after successful verification.
|
||||
// Handlers (pay/order creation) pull ApiKeyIDKey to stamp order.api_key_id.
|
||||
const (
|
||||
ApiKeyIDKey = "api_key_id"
|
||||
ApiKeyRowKey = "api_key_row"
|
||||
)
|
||||
|
||||
// CheckApiSign validates the body signature against the secret_key of
|
||||
// the api_keys row matching the submitted "pid" field. A single row is
|
||||
// valid for all gateway flows — identification is always by pid.
|
||||
//
|
||||
// Flow:
|
||||
// 1. Extract the pid from the request body.
|
||||
// 2. Look up the enabled row by pid; if missing, return signature error.
|
||||
// 3. Verify signature == MD5(sorted_params + secret_key).
|
||||
// 4. Enforce IP whitelist (empty = allow any).
|
||||
// 5. Bump call_count / last_used_at (best-effort).
|
||||
// 6. Stash api_key_id + row in context and rewind the body.
|
||||
func CheckApiSign() echo.MiddlewareFunc {
|
||||
return func(next echo.HandlerFunc) echo.HandlerFunc {
|
||||
return func(ctx echo.Context) error {
|
||||
params, err := ioutil.ReadAll(ctx.Request().Body)
|
||||
params, err := io.ReadAll(ctx.Request().Body)
|
||||
if err != nil {
|
||||
return constant.SignatureErr
|
||||
}
|
||||
// Rewind the body for downstream bindings regardless of outcome.
|
||||
ctx.Request().Body = io.NopCloser(bytes.NewBuffer(params))
|
||||
|
||||
m := make(map[string]interface{})
|
||||
err = json.Cjson.Unmarshal(params, &m)
|
||||
contentType := ctx.Request().Header.Get("Content-Type")
|
||||
if strings.Contains(contentType, "application/x-www-form-urlencoded") {
|
||||
values, parseErr := url.ParseQuery(string(params))
|
||||
if parseErr != nil {
|
||||
return constant.SignatureErr
|
||||
}
|
||||
for k, vs := range values {
|
||||
if len(vs) > 0 {
|
||||
m[k] = vs[0]
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if err = json.Cjson.Unmarshal(params, &m); err != nil {
|
||||
return constant.SignatureErr
|
||||
}
|
||||
}
|
||||
signature, ok := m["signature"]
|
||||
if !ok {
|
||||
return constant.SignatureErr
|
||||
}
|
||||
checkSignature, err := sign.Get(m, config.GetApiAuthToken())
|
||||
identifier := extractPid(m)
|
||||
if identifier == "" {
|
||||
return constant.SignatureErr
|
||||
}
|
||||
|
||||
row, err := data.GetEnabledApiKey(identifier)
|
||||
if err != nil || row.ID == 0 {
|
||||
return constant.SignatureErr
|
||||
}
|
||||
|
||||
checkSignature, err := sign.Get(m, row.SecretKey)
|
||||
if err != nil {
|
||||
return constant.SignatureErr
|
||||
}
|
||||
if checkSignature != signature {
|
||||
signatureStr, _ := signature.(string)
|
||||
if subtle.ConstantTimeCompare([]byte(checkSignature), []byte(signatureStr)) != 1 {
|
||||
return constant.SignatureErr
|
||||
}
|
||||
ctx.Request().Body = ioutil.NopCloser(bytes.NewBuffer(params))
|
||||
|
||||
if !IsIPWhitelisted(row.IpWhitelist, ctx.RealIP()) {
|
||||
return constant.SignatureErr
|
||||
}
|
||||
|
||||
_ = data.TouchApiKeyUsage(row.ID)
|
||||
|
||||
ctx.Set(ApiKeyIDKey, row.ID)
|
||||
ctx.Set(ApiKeyRowKey, row)
|
||||
return next(ctx)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// extractPid reads the "pid" field from the request body. JSON numbers
|
||||
// unmarshal to float64 (EPAY merchants typically send numeric pid); we
|
||||
// format with -1 precision to drop trailing zeros so "1000" matches
|
||||
// regardless of whether the client sent a string or a number.
|
||||
func extractPid(m map[string]interface{}) string {
|
||||
v, ok := m["pid"]
|
||||
if !ok || v == nil {
|
||||
return ""
|
||||
}
|
||||
switch t := v.(type) {
|
||||
case string:
|
||||
return strings.TrimSpace(t)
|
||||
case float64:
|
||||
return strconv.FormatFloat(t, 'f', -1, 64)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// IsIPWhitelisted checks the CSV IP list. Empty list = open.
|
||||
// Entries may be single IPs or CIDR blocks.
|
||||
func IsIPWhitelisted(csv, remote string) bool {
|
||||
csv = strings.TrimSpace(csv)
|
||||
if csv == "" {
|
||||
return true
|
||||
}
|
||||
remoteIP := net.ParseIP(remote)
|
||||
for _, raw := range strings.Split(csv, ",") {
|
||||
entry := strings.TrimSpace(raw)
|
||||
if entry == "" {
|
||||
continue
|
||||
}
|
||||
if strings.Contains(entry, "/") {
|
||||
_, cidr, err := net.ParseCIDR(entry)
|
||||
if err == nil && remoteIP != nil && cidr.Contains(remoteIP) {
|
||||
return true
|
||||
}
|
||||
continue
|
||||
}
|
||||
if entry == remote {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user