This commit is contained in:
Ramiro Paz
2026-03-09 15:09:06 -03:00
parent 8299f8bc96
commit 0e8fe168ef
85 changed files with 14079 additions and 0 deletions

View File

@ -0,0 +1,290 @@
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
config Config
notify domain.Notifier
authMutex deadlock.Mutex
}
func newController(pool *redis.Pool, userData app.UserDataProvider,
s *store.Store, config Config, n domain.Notifier,
) *Controller {
return &Controller{
pool: pool,
userData: userData,
store: s,
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
}