fiumba
This commit is contained in:
290
src/client/api/rest/controller.go
Normal file
290
src/client/api/rest/controller.go
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user