328 lines
8.8 KiB
Go
328 lines
8.8 KiB
Go
package rest
|
|
|
|
import (
|
|
"fmt"
|
|
"log/slog"
|
|
"net/http"
|
|
"time"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/gomodule/redigo/redis"
|
|
"github.com/sasha-s/go-deadlock"
|
|
uuid "github.com/satori/go.uuid"
|
|
|
|
"quantex.com/qfixdpl/src/app"
|
|
"quantex.com/qfixdpl/src/app/version"
|
|
"quantex.com/qfixdpl/src/client/store"
|
|
"quantex.com/qfixdpl/src/common/tracerr"
|
|
"quantex.com/qfixdpl/src/domain"
|
|
)
|
|
|
|
const (
|
|
ProdEnv = "prod"
|
|
TokenExpireTime = 1200 * time.Second
|
|
)
|
|
|
|
const (
|
|
responseKey = "responseKey"
|
|
sessionTokenKey = "sessionTokenKey"
|
|
)
|
|
|
|
type Controller struct {
|
|
pool *redis.Pool
|
|
userData app.UserDataProvider
|
|
store *store.Store
|
|
tradeProvider TradeProvider
|
|
config Config
|
|
notify domain.Notifier
|
|
authMutex deadlock.Mutex
|
|
}
|
|
|
|
func newController(pool *redis.Pool, userData app.UserDataProvider,
|
|
s *store.Store, tradeProvider TradeProvider, config Config, n domain.Notifier,
|
|
) *Controller {
|
|
return &Controller{
|
|
pool: pool,
|
|
userData: userData,
|
|
store: s,
|
|
tradeProvider: tradeProvider,
|
|
config: config,
|
|
notify: n,
|
|
}
|
|
}
|
|
|
|
func (cont *Controller) GetUser(ctx *gin.Context) app.User {
|
|
// This is set on the AuthRequired middleware
|
|
response, ok := ctx.Get(responseKey)
|
|
if !ok {
|
|
// TODO log this issue
|
|
ctx.JSON(http.StatusInternalServerError, HTTPError{Error: "internal server error"})
|
|
}
|
|
|
|
val, ok := response.([]uint8)
|
|
if !ok {
|
|
// TODO log this issue
|
|
ctx.JSON(http.StatusInternalServerError, HTTPError{Error: "internal server error"})
|
|
}
|
|
|
|
return cont.userData.GetUserByEmail(string(val))
|
|
}
|
|
|
|
// Login godoc
|
|
// @Summary Login
|
|
// @Description Authenticate a User using credentials
|
|
// @Tags auth
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Param credentials body Credentials true "Authentication"
|
|
// @Success 200 {object} Session
|
|
// @Failure 400 {object} HTTPError
|
|
// @Failure 401 {object} HTTPError
|
|
// @Failure 404 {object} HTTPError
|
|
// @Failure 500 {object} HTTPError
|
|
// @Router /auth/login [post]
|
|
func (cont *Controller) Login(ctx *gin.Context) {
|
|
defer cont.authMutex.Unlock()
|
|
cont.authMutex.Lock()
|
|
|
|
setHeaders(ctx, cont.config)
|
|
|
|
// Get the JSON body and decode into credentials
|
|
var creds Credentials
|
|
if err := ctx.ShouldBindJSON(&creds); err != nil {
|
|
ctx.JSON(http.StatusBadRequest, HTTPError{Error: err.Error()})
|
|
|
|
return
|
|
}
|
|
|
|
// Get the expected Password from our in memory map
|
|
|
|
expectedUser := cont.userData.GetUserByEmail(creds.Email)
|
|
|
|
// If a Password exists for the given User
|
|
// AND, if it is the same as the Password we received, the we can move ahead
|
|
// if NOT, then we return an "Unauthorized" status
|
|
if expectedUser.Email == "" || expectedUser.Password != creds.Password {
|
|
ctx.JSON(http.StatusUnauthorized, HTTPError{Error: "Invalid credentials"})
|
|
|
|
return
|
|
}
|
|
|
|
// Create a new random session token
|
|
sessionToken := uuid.NewV4().String()
|
|
|
|
// Set the token in the cache, along with the User whom it represents
|
|
// The token has an expiry time of 120 seconds
|
|
conn := cont.pool.Get()
|
|
defer func() {
|
|
err := conn.Close()
|
|
if err != nil {
|
|
e := tracerr.Errorf("error closing connection: %w", err)
|
|
slog.Error(e.Error())
|
|
}
|
|
}()
|
|
|
|
_, err := conn.Do("SETEX", sessionToken, TokenExpireTime, creds.Email)
|
|
if err != nil {
|
|
slog.Error(tracerr.Errorf("Error setting token in redis cache: %w", err).Error())
|
|
// If there is an error in setting the cache, return an internal server error
|
|
ctx.JSON(http.StatusInternalServerError, HTTPError{Error: err.Error()})
|
|
|
|
return
|
|
}
|
|
|
|
// Finally, we set the client cookie for "session_token" as the session token we just generated
|
|
// we also set an expiry time of 120 seconds, the same as the cache
|
|
|
|
// Finally, we set the client cookie for "session_token" as the session token we just generated
|
|
// we also set an expiry time of TokenExpireTime, the same as the cache
|
|
cookie := &http.Cookie{
|
|
Name: "session_token",
|
|
Value: sessionToken,
|
|
HttpOnly: true,
|
|
Path: "/",
|
|
Secure: true,
|
|
Expires: time.Now().Add(TokenExpireTime),
|
|
}
|
|
|
|
if version.Environment() == version.EnvironmentTypeDev {
|
|
cookie.SameSite = http.SameSiteNoneMode
|
|
}
|
|
|
|
http.SetCookie(ctx.Writer, cookie)
|
|
|
|
ctx.JSON(http.StatusOK, Session{Email: creds.Email})
|
|
}
|
|
|
|
// Refresh godoc
|
|
// @Summary Refresh the authorization token
|
|
// @Description This endpoint must be called periodically to get a new token before the current one expires
|
|
// @Tags auth
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Success 200 {object} Msg
|
|
// @Failure 400 {object} HTTPError
|
|
// @Failure 401 {object} HTTPError
|
|
// @Failure 404 {object} HTTPError
|
|
// @Failure 500 {object} HTTPError
|
|
// @Router /auth/refresh [get]
|
|
func (cont *Controller) Refresh(ctx *gin.Context) {
|
|
defer cont.authMutex.Unlock()
|
|
cont.authMutex.Lock()
|
|
|
|
setHeaders(ctx, cont.config)
|
|
|
|
// (BEGIN) The code until this point is the same as the first part of the `Welcome` route
|
|
response, ok1 := ctx.Get(responseKey)
|
|
if !ok1 {
|
|
ctx.JSON(http.StatusInternalServerError, gin.H{"error": "internal server error"})
|
|
}
|
|
// (END) The code uptil this point is the same as the first part of the `Welcome` route
|
|
|
|
// Now, create a new session token for the current User
|
|
newSessionToken := uuid.NewV4().String()
|
|
|
|
conn := cont.pool.Get()
|
|
defer func() {
|
|
err := conn.Close()
|
|
if err != nil {
|
|
e := tracerr.Errorf("error closing connection: %w", err)
|
|
slog.Error(e.Error())
|
|
}
|
|
}()
|
|
|
|
_, err := conn.Do("SETEX", newSessionToken, "120", fmt.Sprintf("%s", response))
|
|
if err != nil {
|
|
ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
|
|
return
|
|
}
|
|
|
|
// Set the new token as the Users `session_token` cookie
|
|
cookie := &http.Cookie{
|
|
Name: "session_token",
|
|
Value: newSessionToken,
|
|
HttpOnly: true,
|
|
Path: "/",
|
|
Secure: true,
|
|
Expires: time.Now().Add(TokenExpireTime),
|
|
}
|
|
|
|
if version.Environment() == version.EnvironmentTypeDev {
|
|
cookie.SameSite = http.SameSiteNoneMode
|
|
}
|
|
|
|
http.SetCookie(ctx.Writer, cookie)
|
|
|
|
ctx.JSON(http.StatusOK, Msg{Text: "Token updated"})
|
|
}
|
|
|
|
// HealthCheck godoc
|
|
// @Summary Health check
|
|
// @Description Return service health
|
|
// @Tags health
|
|
// @Produce json
|
|
// @Success 200 {object} map[string]string
|
|
// @Router /health [get]
|
|
func (cont *Controller) HealthCheck(ctx *gin.Context) {
|
|
// ensure CORS and other headers are set consistently
|
|
setHeaders(ctx, cont.config)
|
|
|
|
status := struct {
|
|
Status string `json:"status"`
|
|
Build string `json:"build"`
|
|
Sha string `json:"sha"`
|
|
JwtAuthentications string `json:"jwtAuthentications,omitempty"`
|
|
}{
|
|
Status: "ok",
|
|
Build: version.BuildBranch(),
|
|
Sha: version.BuildHash(),
|
|
}
|
|
|
|
// Only check JWT authentication if enabled
|
|
if cont.config.EnableJWTAuth {
|
|
status.JwtAuthentications = "ok"
|
|
|
|
user, err := cont.store.UserByEmail("fede")
|
|
if err != nil || user == nil {
|
|
|
|
status.JwtAuthentications = "error"
|
|
status.Status = "degraded"
|
|
|
|
err = tracerr.Errorf("error fetching user: %w", err)
|
|
|
|
slog.Error(err.Error())
|
|
|
|
ctx.JSON(http.StatusInternalServerError, status)
|
|
|
|
return
|
|
}
|
|
}
|
|
|
|
// return a minimal JSON health response
|
|
ctx.JSON(http.StatusOK, status)
|
|
}
|
|
|
|
// revive:disable:cyclomatic // We need this complexity
|
|
func setHeaders(ctx *gin.Context, config Config) {
|
|
origin := ctx.Request.Header.Get("Origin")
|
|
|
|
if allowed(origin, config) {
|
|
ctx.Writer.Header().Set("Access-Control-Allow-Origin", origin)
|
|
ctx.Writer.Header().Set("Access-Control-Allow-Credentials", "true")
|
|
ctx.Writer.Header().Set("Access-Control-Allow-Headers",
|
|
"Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization, accept, origin,"+
|
|
"UserCache-Control, X-Requested-With")
|
|
ctx.Writer.Header().Set("Access-Control-Allow-Methods", "POST, OPTIONS, GET, PUT")
|
|
}
|
|
}
|
|
|
|
func allowed(origin string, config Config) bool {
|
|
if version.Environment() == version.EnvironmentTypeProd {
|
|
return origin == "https://monitor.quantex.com.ar"
|
|
}
|
|
|
|
for _, o := range config.AllowedOrigins {
|
|
if o == origin {
|
|
return true
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
// GetTrades godoc
|
|
// @Summary List active trades
|
|
// @Description Returns all active List Trading trades
|
|
// @Tags fix
|
|
// @Produce json
|
|
// @Success 200 {array} domain.ListTrade
|
|
// @Router /qfixdpl/v1/trades [get]
|
|
func (cont *Controller) GetTrades(ctx *gin.Context) {
|
|
trades := cont.tradeProvider.GetTrades()
|
|
ctx.JSON(http.StatusOK, trades)
|
|
}
|
|
|
|
// GetLogs godoc
|
|
// @Summary Get raw FIX logs for a trade
|
|
// @Description Returns raw FIX message logs for a given QuoteReqID
|
|
// @Tags fix
|
|
// @Produce json
|
|
// @Param quoteReqID path string true "QuoteReqID"
|
|
// @Success 200 {object} domain.Logs
|
|
// @Router /qfixdpl/v1/trades/{quoteReqID}/logs [get]
|
|
func (cont *Controller) GetLogs(ctx *gin.Context) {
|
|
quoteReqID := ctx.Param("quoteReqID")
|
|
|
|
logs, err := cont.store.GetLogsByQuoteReqID(quoteReqID)
|
|
if err != nil {
|
|
slog.Error("GetLogs: error fetching logs", "quoteReqID", quoteReqID, "error", err)
|
|
ctx.JSON(http.StatusInternalServerError, HTTPError{Error: "error fetching logs"})
|
|
|
|
return
|
|
}
|
|
|
|
ctx.JSON(http.StatusOK, logs)
|
|
}
|
|
|