--consumo de api auth

This commit is contained in:
MSI\migue 2025-10-29 06:14:57 -04:00
parent f81a1d5a81
commit bbc517d256
9 changed files with 326 additions and 88 deletions

14
.env
View File

@ -8,7 +8,7 @@ DB_MAX_OPEN_CONNS=25
DB_MAX_IDLE_CONNS=10
DB_CONN_MAX_LIFETIME=30m
#Redis
REDIS_HOST=10.0.0.112
REDIS_HOST=localhost
REDIS_PORT=6379
REDIS_PASSWORD=TuPasswordSegura123
REDIS_DB=0
@ -35,6 +35,16 @@ ENVIRONMENT=development
# Elastic
#ELASTIC_URL=http://host.docker.internal:9200
ELASTIC_URL=http://10.0.0.124:9200
ELASTIC_ENABLED=true
ELASTIC_ENABLED=false
ENCRYPTION_KEY=12345678901234567890123456789012
AUTH_ENDPOINT= http://localhost:8085/auth/headers
AUTH_AUTORIZATION= 'Basic d29ya2VyLXJlbmRpY2lvbjpwcnVlYmE='
AUTH_METHOD=POST
TM_HEADER_ORIGIN=https://azure-function.timemanagerweb.com
TM_HEADER_TENANT_NAME=pruebas-dos
TM_HEADER_USER_AGENT='Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36'

8
.idea/.gitignore generated vendored Normal file
View File

@ -0,0 +1,8 @@
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

View File

@ -44,6 +44,17 @@ type Config struct {
TLSSkipVerify bool `mapstructure:"TLS_SKIP_VERIFY"`
LogRequests bool `mapstructure:"LOG_REQUESTS"`
EnableDebug bool `mapstructure:"ENABLE_DEBUG"`
//AUTH
AUTH_ENDPOINT string `mapstructure:"AUTH_ENDPOINT"`
AUTH_AUTORIZATION string `mapstructure:"AUTH_AUTORIZATION"`
AUTH_METHOD string `mapstructure:"AUTH_METHOD"`
//TM
TM_HEADER_ORIGIN string `mapstructure:"TM_HEADER_ORIGIN"`
TM_HEADER_TENANT_NAME string `mapstructure:"TM_HEADER_TENANT_NAME"`
TM_HEADER_USER_AGENT string `mapstructure:"TM_HEADER_USER_AGENT"`
}
var GlobalConfig *Config

View File

@ -0,0 +1,22 @@
package dto
import (
"time"
)
type Data struct {
SourceID int `json:"source_id"`
SourceName string `json:"source_name"`
ExpiresAt time.Time `json:"expires_at"`
Headers map[string]string `json:"headers"`
URL string `json:"url"`
}
type Response struct {
Status int `json:"status"`
Success bool `json:"success"`
Message string `json:"message"`
Data Data `json:"data"`
TraceID string `json:"trace_id"`
Timestamp time.Time `json:"timestamp"`
}

View File

@ -3,7 +3,8 @@ package dto
import "time"
type SessionData struct {
SessionId string `json:"session_id"`
ExpiresAt time.Time `json:"expires_at"`
EndPoint string `json:"end_point"`
//SessionId string `json:"session_id"`
Headers map[string]string `json:"headers"`
ExpiresAt time.Time `json:"expires_at"`
EndPoint string `json:"end_point"`
}

View File

@ -0,0 +1,16 @@
package dto
import "time"
type RequestTrace struct {
Method string
URL string
Headers map[string]string
RequestBody string
Response string
StatusCode int
Duration time.Duration
Retries int
StartedAt time.Time
EndedAt time.Time
}

View File

@ -0,0 +1,100 @@
package external_api
import (
"bytes"
"crypto/tls"
"encoding/json"
"errors"
"github.com/tuusuario/go-sync-service/internal/domain/dto"
"io"
"net/http"
"time"
)
// Estructura para guardar trazabilidad
// Client genérico con reintentos
type GenericClient struct {
Client *http.Client
MaxRetries int
RetryDelay time.Duration
EnableTrace bool // true = guarda trazabilidad
}
// Constructor
func NewGenericClient(maxRetries int, retryDelay time.Duration) *GenericClient {
return &GenericClient{
Client: &http.Client{Timeout: 30 * time.Second, Transport: &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
}},
MaxRetries: maxRetries,
RetryDelay: retryDelay,
}
}
// Método genérico
func (gc *GenericClient) DoRequest(method, url string, headers map[string]string, body []byte) (map[string]interface{}, *dto.RequestTrace, error) {
trace := &dto.RequestTrace{
Method: method,
URL: url,
Headers: headers,
RequestBody: string(body),
StartedAt: time.Now(),
}
var lastErr error
var resp *http.Response
for attempt := 0; attempt <= gc.MaxRetries; attempt++ {
trace.Retries = attempt
req, err := http.NewRequest(method, url, bytes.NewReader(body))
if err != nil {
lastErr = err
break
}
// Headers
for k, v := range headers {
req.Header.Set(k, v)
}
start := time.Now()
resp, err = gc.Client.Do(req)
trace.Duration = time.Since(start)
if err == nil && resp.StatusCode < 500 {
break // éxito o error de cliente, no reintentar
}
lastErr = err
time.Sleep(gc.RetryDelay * (1 << attempt)) // backoff exponencial
}
if lastErr != nil {
trace.EndedAt = time.Now()
return nil, trace, lastErr
}
defer resp.Body.Close()
trace.StatusCode = resp.StatusCode
bodyResp, _ := io.ReadAll(resp.Body)
trace.Response = string(bodyResp)
trace.EndedAt = time.Now()
if resp.StatusCode >= 400 {
return nil, trace, errors.New("HTTP error: " + resp.Status)
}
// Si el código de estado es 204 (No Content), no hay cuerpo de respuesta
if resp.StatusCode == http.StatusNoContent {
return nil, trace, nil
}
var result map[string]interface{}
if err := json.Unmarshal(bodyResp, &result); err != nil {
return nil, trace, err
}
return result, trace, nil
}

View File

@ -3,6 +3,8 @@ package http
import (
"encoding/json"
"fmt" // NEW
"github.com/tuusuario/go-sync-service/internal/http/external_api"
"log"
"sync"
"time"
@ -10,7 +12,6 @@ import (
"github.com/tuusuario/go-sync-service/internal/domain/dto"
"github.com/tuusuario/go-sync-service/internal/domain/model"
"github.com/tuusuario/go-sync-service/internal/domain/ports"
"github.com/tuusuario/go-sync-service/internal/security"
)
var (
@ -21,90 +22,108 @@ var (
type SAPSessionProvider struct{}
func GetSession(cfg ports.RedisConfigProvider, job dto.CronJob, auth dto.ServiceConfig, dbcore ports.Database, logPrefix string) (*dto.SessionData, error) {
mutex.Lock()
defer mutex.Unlock()
redisKey = "session:" + job.UnidadNegocio.CompanyName + ":" + job.UnidadNegocio.CompanyDB
if sess, err := loadSessionFromRedis(cfg, logPrefix); err == nil && time.Now().Before(sess.ExpiresAt) {
config.Log.Printf("%v 🔑 Sesión obtenida de Redis %v", logPrefix, redisKey)
return sess, nil
}
parametros := map[string]interface{}{
"company_name": job.UnidadNegocio.CompanyName,
"company_db": job.UnidadNegocio.CompanyDB,
}
credencial, err := dbcore.GetCredencialesFromTemplate(config.GlobalConfig.WhereUnitsBusiness, parametros)
//CLIENT HTTP
authenticacion, _, err := Authenticacion(job.UnidadNegocio.CompanyDB)
if err != nil {
config.Log.Printf("%v ❌ Error al obtener credenciales: %v", logPrefix, err)
log.Printf("Error en autenticacion: %v", err)
return nil, err
}
// ==============================
// DESCIFRAR PASSWORD (AES-GCM)
// ==============================
key, err := security.LoadEncryptionKey()
if err != nil {
return nil, fmt.Errorf("%s %v", logPrefix, err)
}
// Suponemos que credencial.Password viene como base64(nonce||cipher) generado por EncryptAESGCM
plainPass, err := security.DecryptAESGCM(credencial.Password, key)
if err != nil {
config.Log.Printf("%v ❌ Error al descifrar password: %v", logPrefix, err)
return nil, err
}
credencial.Password = plainPass
return &dto.SessionData{
Headers: authenticacion.Data.Headers,
ExpiresAt: authenticacion.Data.ExpiresAt,
EndPoint: authenticacion.Data.URL,
}, nil
// No loguees secretos. Si necesitas log, hazlo sin password:
config.Log.Debugf("%v Obteniendo credenciales para CompanyDB=%s UserName=%s (password oculto)",
logPrefix, credencial.CompanyDB, credencial.UserName)
config.Log.Printf("%v 🔑 Realizando login...", logPrefix)
mySession := &dto.SessionData{EndPoint: credencial.EndPoint}
if auth.Rest == nil {
auth.Rest = &dto.RestOptions{}
}
// 3. Preparar el body del login (usa cred.Password en claro ya descifrado)
auth = prepareAuthBody(auth, credencial, logPrefix)
config.Log.Debugf(" %v Url: %v + %v", logPrefix, credencial.EndPoint, auth.Path)
resp, err := SendRequest(credencial.EndPoint, auth)
if err != nil || resp.IsError() {
config.Log.Printf("%v ❌ Error al autenticar: %v", logPrefix, err)
return nil, err
}
if auth.GQL {
var dataGraphql struct {
Token string `json:"token"`
RefreshToken string `json:"refresh_token"`
/*
mutex.Lock()
defer mutex.Unlock()
redisKey = "session:" + job.UnidadNegocio.CompanyName + ":" + job.UnidadNegocio.CompanyDB
//REDIS
if sess, err := loadSessionFromRedis(cfg, logPrefix); err == nil && time.Now().Before(sess.ExpiresAt) {
config.Log.Printf("%v 🔑 Sesión obtenida de Redis %v", logPrefix, redisKey)
return sess, nil
}
if err := json.Unmarshal(resp.Body(), &dataGraphql); err != nil {
config.Log.Printf("%v ❌ Error al parsear sesión graphql: %v", logPrefix, err)
return nil, err
}
mySession.SessionId = dataGraphql.Token
mySession.ExpiresAt = time.Now().Add(10 * time.Minute)
} else {
var dataRest struct {
SessionId string `json:"SessionId"`
SessionTimeout int `json:"SessionTimeout"`
}
if err := json.Unmarshal(resp.Body(), &dataRest); err != nil {
config.Log.Printf("%v ❌ Error al parsear sesión rest: %v", logPrefix, err)
return nil, err
}
mySession.SessionId = dataRest.SessionId
mySession.ExpiresAt = time.Now().Add(time.Duration(dataRest.SessionTimeout) * time.Minute)
}
config.Log.Printf("%v ✅ Sesión obtenida", logPrefix)
saveSessionToRedis(mySession, cfg, logPrefix)
return mySession, nil
parametros := map[string]interface{}{
"company_name": job.UnidadNegocio.CompanyName,
"company_db": job.UnidadNegocio.CompanyDB,
}
credencial, err := dbcore.GetCredencialesFromTemplate(config.GlobalConfig.WhereUnitsBusiness, parametros)
if err != nil {
config.Log.Printf("%v ❌ Error al obtener credenciales: %v", logPrefix, err)
return nil, err
}
// ==============================
// DESCIFRAR PASSWORD (AES-GCM)
// ==============================
key, err := security.LoadEncryptionKey()
if err != nil {
return nil, fmt.Errorf("%s %v", logPrefix, err)
}
// Suponemos que credencial.Password viene como base64(nonce||cipher) generado por EncryptAESGCM
plainPass, err := security.DecryptAESGCM(credencial.Password, key)
if err != nil {
config.Log.Printf("%v ❌ Error al descifrar password: %v", logPrefix, err)
return nil, err
}
credencial.Password = plainPass
// No loguees secretos. Si necesitas log, hazlo sin password:
config.Log.Debugf("%v Obteniendo credenciales para CompanyDB=%s UserName=%s (password oculto)",
logPrefix, credencial.CompanyDB, credencial.UserName)
config.Log.Printf("%v 🔑 Realizando login...", logPrefix)
mySession := &dto.SessionData{EndPoint: credencial.EndPoint}
if auth.Rest == nil {
auth.Rest = &dto.RestOptions{}
}
// 3. Preparar el body del login (usa cred.Password en claro ya descifrado)
auth = prepareAuthBody(auth, credencial, logPrefix)
config.Log.Debugf(" %v Url: %v + %v", logPrefix, credencial.EndPoint, auth.Path)
resp, err := SendRequest(credencial.EndPoint, auth)
if err != nil || resp.IsError() {
config.Log.Printf("%v ❌ Error al autenticar: %v", logPrefix, err)
return nil, err
}
if auth.GQL {
var dataGraphql struct {
Token string `json:"token"`
RefreshToken string `json:"refresh_token"`
}
if err := json.Unmarshal(resp.Body(), &dataGraphql); err != nil {
config.Log.Printf("%v ❌ Error al parsear sesión graphql: %v", logPrefix, err)
return nil, err
}
mySession.SessionId = dataGraphql.Token
mySession.ExpiresAt = time.Now().Add(10 * time.Minute)
} else {
var dataRest struct {
SessionId string `json:"SessionId"`
SessionTimeout int `json:"SessionTimeout"`
}
if err := json.Unmarshal(resp.Body(), &dataRest); err != nil {
config.Log.Printf("%v ❌ Error al parsear sesión rest: %v", logPrefix, err)
return nil, err
}
mySession.SessionId = dataRest.SessionId
mySession.ExpiresAt = time.Now().Add(time.Duration(dataRest.SessionTimeout) * time.Minute)
}
config.Log.Printf("%v ✅ Sesión obtenida", logPrefix)
saveSessionToRedis(mySession, cfg, logPrefix)
return mySession, nil
*/
}
func loadSessionFromRedis(cfg ports.RedisConfigProvider, logPrefix string) (*dto.SessionData, error) {
@ -151,3 +170,53 @@ func prepareAuthBody(auth dto.ServiceConfig, cred *model.CredencialesSAP, logPre
}
return auth
}
func Authenticacion(company string) (*dto.Response, *dto.RequestTrace, error) {
url := config.GlobalConfig.AUTH_ENDPOINT
headers := map[string]string{
"Content-Type": "application/json",
"Authorization": config.GlobalConfig.AUTH_AUTORIZATION,
}
// Crea el cuerpo de la solicitud con el id recibido
body := []byte(fmt.Sprintf(`{"name": "%s"}`, company))
client := external_api.NewGenericClient(3, 3*time.Second)
// Hacer la solicitud usando el cliente genérico
response, trace, err := client.DoRequest(config.GlobalConfig.AUTH_METHOD, url, headers, body)
// Manejo de errores
if err != nil {
log.Println("Error en la solicitud: ", err)
return nil, nil, err
}
// Deserializar la respuesta en el modelo de dto.Response
var responseModel dto.Response
err = mapToModel(response, &responseModel)
if err != nil {
log.Println("Error al deserializar la respuesta: ", err)
return nil, nil, err
}
log.Printf("TRACE: %+v", trace)
log.Printf("RESPUESTA MAP: %+v", response)
return &responseModel, trace, nil
}
// mapToModel convierte un mapa genérico en el modelo ResponseModel
func mapToModel(data map[string]interface{}, model *dto.Response) error {
// Utiliza json.Marshal y json.Unmarshal para convertir el mapa en el modelo
jsonData, err := json.Marshal(data)
if err != nil {
return err
}
err = json.Unmarshal(jsonData, model)
if err != nil {
return err
}
return nil
}

View File

@ -47,16 +47,17 @@ func SyncData(redis ports.RedisConfigProvider, database *gorm.DB, job dto.CronJo
hasError = true
continue
}
if session == nil || session.SessionId == "" {
if session == nil || session.Headers == nil {
config.Log.Println(logPrefix + " ❌ Sesión inválida o vacía")
hasError = true
continue
}
jobIndividual.Service.Headers = session.Headers
if jobIndividual.Service.GQL {
jobIndividual.Service.Headers["Authorization"] = "Bearer " + session.SessionId
} else {
jobIndividual.Service.Headers["Cookie"] = "B1SESSION=" + session.SessionId
jobIndividual.Service.Headers["origin"] = config.GlobalConfig.TM_HEADER_ORIGIN
jobIndividual.Service.Headers["tenant-name"] = config.GlobalConfig.TM_HEADER_TENANT_NAME
jobIndividual.Service.Headers["User-Agent"] = config.GlobalConfig.TM_HEADER_USER_AGENT
}
response, err := FetchAllPaginatedManual[map[string]interface{}](session.EndPoint, jobIndividual.Service, logPrefix)