feat(payment): standalone ZarinPal broker on pay.flatrender.ir
A generic multi-client payment gateway so FlatRender, meezi.ir and bargevasat.ir can all pay through ZarinPal's single verified callback domain (pay.flatrender.ir). New Go service services/payment (clones the notification skeleton + vendored deps): - migration 31_payment_broker.sql — `payment` schema: client_apps, transactions, webhook_deliveries. - ZarinPal v4 client ported from the proven identity PaymentService (request.json -> StartPay -> verify.json; codes 100/101). - client API: POST /v1/pay/request + /v1/pay/inquiry, authed by X-Api-Key + HMAC body signature; GET /callback/zarinpal (the single verified endpoint) verifies, then 302s the user back to the site's return_url (signed) and fires a signed, retried webhook. - per-client ZarinPal merchant override (default = shared merchant); amount stored canonically in Rial, unit to ZarinPal env-configurable. - admin API /v1/admin/* (FlatRender admin JWT): client-app CRUD + key issue/rotate + transactions list. Deploy wiring: payment-svc in docker-compose.v2.yml (host port 1607), pay.flatrender.ir server block in mirror-nginx conf, ENV_FILE + README updates (cert SAN + manual migration note). Admin UI: src/components/admin/PaymentsAdmin.tsx (client apps with one-time key reveal + rotate, transactions table) + /admin/payments page + nav link + fa/en strings; pay-admin proxy route to payment-svc. Docs/SDK: deploy/PAYMENTS.md (integration contract) + deploy/sdk/flatpay.js (zero-dep Node client + webhook verifier) for meezi/any site. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+164
@@ -0,0 +1,164 @@
|
||||
// Copyright 2014 Manu Martinez-Almeida. All rights reserved.
|
||||
// Use of this source code is governed by a MIT style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package gin
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
"reflect"
|
||||
"runtime"
|
||||
"strings"
|
||||
"unicode"
|
||||
)
|
||||
|
||||
// BindKey indicates a default bind key.
|
||||
const BindKey = "_gin-gonic/gin/bindkey"
|
||||
|
||||
// Bind is a helper function for given interface object and returns a Gin middleware.
|
||||
func Bind(val any) HandlerFunc {
|
||||
value := reflect.ValueOf(val)
|
||||
if value.Kind() == reflect.Ptr {
|
||||
panic(`Bind struct can not be a pointer. Example:
|
||||
Use: gin.Bind(Struct{}) instead of gin.Bind(&Struct{})
|
||||
`)
|
||||
}
|
||||
typ := value.Type()
|
||||
|
||||
return func(c *Context) {
|
||||
obj := reflect.New(typ).Interface()
|
||||
if c.Bind(obj) == nil {
|
||||
c.Set(BindKey, obj)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// WrapF is a helper function for wrapping http.HandlerFunc and returns a Gin middleware.
|
||||
func WrapF(f http.HandlerFunc) HandlerFunc {
|
||||
return func(c *Context) {
|
||||
f(c.Writer, c.Request)
|
||||
}
|
||||
}
|
||||
|
||||
// WrapH is a helper function for wrapping http.Handler and returns a Gin middleware.
|
||||
func WrapH(h http.Handler) HandlerFunc {
|
||||
return func(c *Context) {
|
||||
h.ServeHTTP(c.Writer, c.Request)
|
||||
}
|
||||
}
|
||||
|
||||
// H is a shortcut for map[string]any
|
||||
type H map[string]any
|
||||
|
||||
// MarshalXML allows type H to be used with xml.Marshal.
|
||||
func (h H) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
|
||||
start.Name = xml.Name{
|
||||
Space: "",
|
||||
Local: "map",
|
||||
}
|
||||
if err := e.EncodeToken(start); err != nil {
|
||||
return err
|
||||
}
|
||||
for key, value := range h {
|
||||
elem := xml.StartElement{
|
||||
Name: xml.Name{Space: "", Local: key},
|
||||
Attr: []xml.Attr{},
|
||||
}
|
||||
if err := e.EncodeElement(value, elem); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return e.EncodeToken(xml.EndElement{Name: start.Name})
|
||||
}
|
||||
|
||||
func assert1(guard bool, text string) {
|
||||
if !guard {
|
||||
panic(text)
|
||||
}
|
||||
}
|
||||
|
||||
func filterFlags(content string) string {
|
||||
for i, char := range content {
|
||||
if char == ' ' || char == ';' {
|
||||
return content[:i]
|
||||
}
|
||||
}
|
||||
return content
|
||||
}
|
||||
|
||||
func chooseData(custom, wildcard any) any {
|
||||
if custom != nil {
|
||||
return custom
|
||||
}
|
||||
if wildcard != nil {
|
||||
return wildcard
|
||||
}
|
||||
panic("negotiation config is invalid")
|
||||
}
|
||||
|
||||
func parseAccept(acceptHeader string) []string {
|
||||
parts := strings.Split(acceptHeader, ",")
|
||||
out := make([]string, 0, len(parts))
|
||||
for _, part := range parts {
|
||||
if i := strings.IndexByte(part, ';'); i > 0 {
|
||||
part = part[:i]
|
||||
}
|
||||
if part = strings.TrimSpace(part); part != "" {
|
||||
out = append(out, part)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func lastChar(str string) uint8 {
|
||||
if str == "" {
|
||||
panic("The length of the string can't be 0")
|
||||
}
|
||||
return str[len(str)-1]
|
||||
}
|
||||
|
||||
func nameOfFunction(f any) string {
|
||||
return runtime.FuncForPC(reflect.ValueOf(f).Pointer()).Name()
|
||||
}
|
||||
|
||||
func joinPaths(absolutePath, relativePath string) string {
|
||||
if relativePath == "" {
|
||||
return absolutePath
|
||||
}
|
||||
|
||||
finalPath := path.Join(absolutePath, relativePath)
|
||||
if lastChar(relativePath) == '/' && lastChar(finalPath) != '/' {
|
||||
return finalPath + "/"
|
||||
}
|
||||
return finalPath
|
||||
}
|
||||
|
||||
func resolveAddress(addr []string) string {
|
||||
switch len(addr) {
|
||||
case 0:
|
||||
if port := os.Getenv("PORT"); port != "" {
|
||||
debugPrint("Environment variable PORT=\"%s\"", port)
|
||||
return ":" + port
|
||||
}
|
||||
debugPrint("Environment variable PORT is undefined. Using port :8080 by default")
|
||||
return ":8080"
|
||||
case 1:
|
||||
return addr[0]
|
||||
default:
|
||||
panic("too many parameters")
|
||||
}
|
||||
}
|
||||
|
||||
// https://stackoverflow.com/questions/53069040/checking-a-string-contains-only-ascii-characters
|
||||
func isASCII(s string) bool {
|
||||
for i := 0; i < len(s); i++ {
|
||||
if s[i] > unicode.MaxASCII {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
Reference in New Issue
Block a user