Release v1.14.0 — monetization launch (E5-E8) #237
@@ -121,5 +121,6 @@ func (s *Server) handleRobokassaResult(c *gin.Context) {
|
|||||||
s.log.Error("record payment event failed", zap.String("order", orderID.String()), zap.Error(err))
|
s.log.Error("record payment event failed", zap.String("order", orderID.String()), zap.Error(err))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
c.String(http.StatusOK, "OK"+v.Get("InvId"))
|
// The gateway echoes response verbatim to Robokassa, which requires the body "OK<InvId>".
|
||||||
|
c.JSON(http.StatusOK, gin.H{"response": "OK" + v.Get("InvId")})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -86,7 +86,7 @@
|
|||||||
# The game SPA and the Connect edge are served by the gateway. Strip any
|
# The game SPA and the Connect edge are served by the gateway. Strip any
|
||||||
# client-supplied X-Scrabble-Honeypot here so the gateway only ever honours the
|
# client-supplied X-Scrabble-Honeypot here so the gateway only ever honours the
|
||||||
# tag the honeypot block sets below (a client cannot self-tag a real request).
|
# tag the honeypot block sets below (a client cannot self-tag a real request).
|
||||||
@gateway path /app /app/* /telegram /telegram/* /vk /vk/* /dict/* /dl/* /metrics/* /telemetry/* /scrabble.edge.v1.Gateway/*
|
@gateway path /app /app/* /telegram /telegram/* /vk /vk/* /dict/* /dl/* /pay/* /metrics/* /telemetry/* /scrabble.edge.v1.Gateway/*
|
||||||
handle @gateway {
|
handle @gateway {
|
||||||
reverse_proxy gateway:8081 {
|
reverse_proxy gateway:8081 {
|
||||||
header_up -X-Scrabble-Honeypot
|
header_up -X-Scrabble-Honeypot
|
||||||
|
|||||||
@@ -414,6 +414,20 @@ func (c *Client) WalletOrder(ctx context.Context, userID, productID string) (Wal
|
|||||||
return out, err
|
return out, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// robokassaResultResp is the backend intake's reply: the body to echo back to Robokassa.
|
||||||
|
type robokassaResultResp struct {
|
||||||
|
Response string `json:"response"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// RobokassaResult forwards a Robokassa Result callback's parameters to the backend intake (the
|
||||||
|
// single writer, which verifies the signature and credits) and returns the body to echo to
|
||||||
|
// Robokassa ("OK<InvId>"). params carries the provider's raw form fields.
|
||||||
|
func (c *Client) RobokassaResult(ctx context.Context, params map[string]string) (string, error) {
|
||||||
|
var out robokassaResultResp
|
||||||
|
err := c.do(ctx, http.MethodPost, "/api/v1/internal/payments/robokassa/result", "", "", params, &out)
|
||||||
|
return out.Response, err
|
||||||
|
}
|
||||||
|
|
||||||
// CatalogAtomResp is one atom line of a storefront product: the value type it grants and quantity.
|
// CatalogAtomResp is one atom line of a storefront product: the value type it grants and quantity.
|
||||||
type CatalogAtomResp struct {
|
type CatalogAtomResp struct {
|
||||||
AtomType string `json:"atom_type"`
|
AtomType string `json:"atom_type"`
|
||||||
|
|||||||
@@ -196,6 +196,11 @@ func (s *Server) HTTPHandler() http.Handler {
|
|||||||
// through this session-gated route (not public); see dictBytesHandler.
|
// through this session-gated route (not public); see dictBytesHandler.
|
||||||
mux.Handle("/dict/", s.dictBytesHandler())
|
mux.Handle("/dict/", s.dictBytesHandler())
|
||||||
mux.Handle("/dl/", s.exportDownloadHandler())
|
mux.Handle("/dl/", s.exportDownloadHandler())
|
||||||
|
// Direct-rail (Robokassa) return + callback routes: the server Result callback (the single
|
||||||
|
// crediting signal, proxied to the backend intake) and the browser Success/Fail redirects.
|
||||||
|
mux.Handle("/pay/robokassa/result", s.robokassaResultHandler())
|
||||||
|
mux.Handle("/pay/robokassa/success", s.robokassaRedirectHandler())
|
||||||
|
mux.Handle("/pay/robokassa/fail", s.robokassaRedirectHandler())
|
||||||
// The client posts its local-move-preview adoption telemetry here (session-gated).
|
// The client posts its local-move-preview adoption telemetry here (session-gated).
|
||||||
mux.Handle("/metrics/local-eval", s.localEvalMetricsHandler())
|
mux.Handle("/metrics/local-eval", s.localEvalMetricsHandler())
|
||||||
// The index.html boot guard beacons here when it turns a client away on the unsupported-engine
|
// The index.html boot guard beacons here when it turns a client away on the unsupported-engine
|
||||||
@@ -482,6 +487,50 @@ func (s *Server) exportDownloadHandler() http.Handler {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// robokassaResultHandler proxies the Robokassa Result callback to the backend intake (the single
|
||||||
|
// writer). It rate-limits per IP, forwards the provider's form parameters, and echoes the backend's
|
||||||
|
// "OK<InvId>" to Robokassa on success; any error tells Robokassa the notification was not accepted,
|
||||||
|
// so it retries. The gateway does not verify the signature — the backend holds the secret.
|
||||||
|
func (s *Server) robokassaResultHandler() http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if s.backend == nil {
|
||||||
|
http.NotFound(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ip := peerIP(r.RemoteAddr, r.Header)
|
||||||
|
if !s.limiter.Allow("public:"+ip, s.publicPolicy) {
|
||||||
|
s.noteRateLimited(r.Context(), classPublic, ip, "robokassa-result")
|
||||||
|
http.Error(w, "rate limited", http.StatusTooManyRequests)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := r.ParseForm(); err != nil {
|
||||||
|
http.Error(w, "bad request", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
params := make(map[string]string, len(r.Form))
|
||||||
|
for k := range r.Form {
|
||||||
|
params[k] = r.Form.Get(k)
|
||||||
|
}
|
||||||
|
resp, err := s.backend.RobokassaResult(r.Context(), params)
|
||||||
|
if err != nil {
|
||||||
|
s.log.Warn("robokassa result proxy failed", zap.Error(err))
|
||||||
|
http.Error(w, "not accepted", http.StatusBadGateway)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
||||||
|
_, _ = w.Write([]byte(resp))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// robokassaRedirectHandler redirects the customer's browser back into the app after Robokassa's
|
||||||
|
// Success/Fail return. The credit rides the server Result callback, never these redirects, so this
|
||||||
|
// only navigates home; the wallet reflects the credit once the callback lands.
|
||||||
|
func (s *Server) robokassaRedirectHandler() http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
http.Redirect(w, r, "/app/", http.StatusSeeOther)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func (s *Server) dictBytesHandler() http.Handler {
|
func (s *Server) dictBytesHandler() http.Handler {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
if s.backend == nil {
|
if s.backend == nil {
|
||||||
|
|||||||
Reference in New Issue
Block a user