Api key configuracion and quotes endpooint

This commit is contained in:
2026-06-01 13:03:06 -03:00
parent 7cc4a96a03
commit 298e9c39e3
6 changed files with 30 additions and 5 deletions

View File

@ -36,6 +36,10 @@ type Service struct {
AuthorizedServices map[string]AuthorizedService `toml:"AuthorizedServices"` AuthorizedServices map[string]AuthorizedService `toml:"AuthorizedServices"`
APIBasePort string APIBasePort string
EnableJWTAuth bool // Enable JWT authentication for service-to-service communication EnableJWTAuth bool // Enable JWT authentication for service-to-service communication
// ServiceAPIKey is the shared secret that authenticates qbymarouter (and any
// other internal service) when posting to /qfixdpl/v1/quotes and
// /qfixdpl/v1/messages. Must match the DPLAPIKey configured on the caller.
ServiceAPIKey string
FIX FIXConfig FIX FIXConfig
} }

View File

@ -348,7 +348,7 @@ func (cont *Controller) AllMessages(ctx *gin.Context) {
return return
} }
if req.APIKey != "1234" { if !cont.checkServiceAPIKey(req.APIKey) {
ctx.JSON(http.StatusUnauthorized, HTTPError{Error: "Not allowed to perform this request"}) ctx.JSON(http.StatusUnauthorized, HTTPError{Error: "Not allowed to perform this request"})
return return
} }
@ -356,6 +356,13 @@ func (cont *Controller) AllMessages(ctx *gin.Context) {
ctx.JSON(http.StatusOK, cont.tradeProvider.GetAllMessages(req.In, req.Out)) ctx.JSON(http.StatusOK, cont.tradeProvider.GetAllMessages(req.In, req.Out))
} }
// checkServiceAPIKey returns true when the provided key matches the configured
// service-to-service shared secret. Empty configured key is always rejected
// to avoid open authentication when misconfigured.
func (cont *Controller) checkServiceAPIKey(key string) bool {
return cont.config.ServiceAPIKey != "" && key == cont.config.ServiceAPIKey
}
// GetPendingQuoteRequests godoc // GetPendingQuoteRequests godoc
// @Summary List pending QuoteRequests // @Summary List pending QuoteRequests
// @Description Returns all QuoteRequests received from TW that have not been quoted yet by the dealer // @Description Returns all QuoteRequests received from TW that have not been quoted yet by the dealer
@ -382,12 +389,19 @@ func (cont *Controller) GetPendingQuoteRequests(ctx *gin.Context) {
// @Failure 500 {object} HTTPError // @Failure 500 {object} HTTPError
// @Router /qfixdpl/v1/quotes [post] // @Router /qfixdpl/v1/quotes [post]
func (cont *Controller) SendQuote(ctx *gin.Context) { func (cont *Controller) SendQuote(ctx *gin.Context) {
setHeaders(ctx, cont.config)
var req SendQuoteRequest var req SendQuoteRequest
if err := ctx.ShouldBindJSON(&req); err != nil { if err := ctx.ShouldBindJSON(&req); err != nil {
ctx.JSON(http.StatusBadRequest, HTTPError{Error: err.Error()}) ctx.JSON(http.StatusBadRequest, HTTPError{Error: err.Error()})
return return
} }
if !cont.checkServiceAPIKey(req.APIKey) {
ctx.JSON(http.StatusUnauthorized, HTTPError{Error: "Not allowed to perform this request"})
return
}
price, err := decimal.NewFromString(req.Price) price, err := decimal.NewFromString(req.Price)
if err != nil { if err != nil {
ctx.JSON(http.StatusBadRequest, HTTPError{Error: "invalid price: " + err.Error()}) ctx.JSON(http.StatusBadRequest, HTTPError{Error: "invalid price: " + err.Error()})

View File

@ -18,6 +18,7 @@ type Session struct {
} }
type SendQuoteRequest struct { type SendQuoteRequest struct {
APIKey string `json:"APIKey" binding:"required"`
QuoteReqID string `json:"QuoteReqID" binding:"required"` QuoteReqID string `json:"QuoteReqID" binding:"required"`
Price string `json:"Price" binding:"required" example:"99.6"` Price string `json:"Price" binding:"required" example:"99.6"`
} }

View File

@ -24,10 +24,11 @@ func SetRoutes(api *API) {
qfixdpl.GET("/trades", cont.GetTrades) qfixdpl.GET("/trades", cont.GetTrades)
qfixdpl.GET("/trades/:quoteReqID/logs", cont.GetLogs) qfixdpl.GET("/trades/:quoteReqID/logs", cont.GetLogs)
qfixdpl.GET("/quote-requests", cont.GetPendingQuoteRequests) qfixdpl.GET("/quote-requests", cont.GetPendingQuoteRequests)
qfixdpl.POST("/quotes", cont.SendQuote)
msgs := v1.Group("/") // services group: API-key auth via body, no session cookie required.
msgs.POST("/messages", cont.AllMessages) services := v1.Group("/")
services.POST("/messages", cont.AllMessages)
services.POST("/quotes", cont.SendQuote)
backoffice := qfixdpl.Group("/backoffice") backoffice := qfixdpl.Group("/backoffice")
backoffice.Use(cont.BackOfficeUser) backoffice.Use(cont.BackOfficeUser)

View File

@ -39,6 +39,10 @@ type Config struct {
AuthorizedServices map[string]app.AuthorizedService `toml:"AuthorizedServices"` AuthorizedServices map[string]app.AuthorizedService `toml:"AuthorizedServices"`
Port string Port string
EnableJWTAuth bool EnableJWTAuth bool
// ServiceAPIKey authenticates internal services (qbymarouter, etc.) calling
// /qfixdpl/v1/quotes and /qfixdpl/v1/messages. Compared against the APIKey
// field in the request body.
ServiceAPIKey string
} }
func New(userData app.UserDataProvider, storeInstance *store.Store, tradeProvider TradeProvider, config Config, notify domain.Notifier) *API { func New(userData app.UserDataProvider, storeInstance *store.Store, tradeProvider TradeProvider, config Config, notify domain.Notifier) *API {

View File

@ -51,6 +51,7 @@ func Runner(cfg app.Config) error {
External: cfg.External, External: cfg.External,
AuthorizedServices: cfg.AuthorizedServices, AuthorizedServices: cfg.AuthorizedServices,
EnableJWTAuth: cfg.EnableJWTAuth, EnableJWTAuth: cfg.EnableJWTAuth,
ServiceAPIKey: cfg.ServiceAPIKey,
} }
api := rest.New(userData, appStore, fixManager, apiConfig, notify) api := rest.New(userData, appStore, fixManager, apiConfig, notify)