IR
irwinrodriguez.dev
Volver a documentación

Middleware

Los middlewares de FoxServer son piezas del host, escritas en X#, que procesan la petición antes y después de que llegue a tu controlador. Vienen con el servidor y se encienden y se configuran desde el manifiesto: JWT, registro de peticiones, SSE, webhooks, estado, OpenAPI, JSON-RPC y MCP, entre otros.

Dos matices que ahorran una tarde. No hay forma de escribir un middleware desde VFP: lo que tu proyecto puede hacer es BeforeEndpoint y AfterEndpoint por controlador, y el ApplicationLifecycle para lo que es de todo el proyecto. Y CORS no es un middleware: se resuelve antes de la cadena, así que no se enciende ni se apaga con los demás.

Autenticación JWT

Protege los endpoints que no llevan public en su HELP. El cliente manda Authorization: Bearer <token> y el middleware lo valida antes de que tu código corra.

En el manifiesto:

"middleware": {
  "auth": {
    "enabled": true,
    "type": "jwt",
    "secret": "<cifrado con middleware-set, nunca en claro>",
    "tokenexpirationseconds": 86400,
    "loginendpoint": "auth/login"
  }
}
El secreto va cifrado auth.secret se guarda cifrado, y se escribe con foxserver middleware-set. Un secreto en claro en el manifiesto hace que el motor responda 500 («not a valid Base-64 string»). Cifrarlo necesita la coclase FoxServer.SecurityHelper registrada: el instalador lo hace, y en una instalación desde ZIP se registra bin\SecurityHelper.comhost.dll con el regsvr32 de 32 bits.

El contrato del endpoint de login

Esto no se adivina, y es donde se atasca todo el mundo la primera vez: el token lo emite el middleware, no tu código. Tu controlador valida las credenciales y le dice al motor si valen; el motor firma el JWT.

La rutaTiene que coincidir con middleware.auth.loginendpoint, y llevar public en el HELP
El cuerpoNo puede estar vacío
X-Auth-StatusExactamente "true" o "false". Ningún otro valor vale
Con "true"El JSON tiene que traer un objeto en data, lo que obliga a "responseformat": "foxserver": con el formato libre no hay envoltura
Dentro de dataTres campos obligatorios: id, name y role. Si falta uno o viene vacío, no hay token
Lo demás de dataViaja como claim adicional del token, y se lee con req.GetClaim()
En ese endpointNo valen this.Ok() ni this.Fail(): construyen su propia respuesta y se pierde la cabecera, así que el motor contesta 406. Ahí se usa res.status(n).json(...)

El token que sale lleva sub (el id), name, role, iat, exp y los claims adicionales. iat y exp son NumericDate del RFC 7519, segundos enteros, y su diferencia es exactamente tokenexpirationseconds.

El login, entero:

pLogin: validar credenciales para que el middleware emita el JWT
PROCEDURE pLogin(req, res) HELP "POST: auth/login public"
    LOCAL loRes, loData, lcEmail, lcPassword, loR

    loRes = THIS.newObject("status,data,message")

    TRY
        * El cuerpo crudo y ParseJson: mas control que req.json cuando
        * el cuerpo puede no venir.
        LOCAL loBody
        loBody = THIS.ParseJson(req.GetRawBody())
        IF ISNULL(loBody)
            loRes.status  = "error"
            loRes.message = "Body JSON requerido"
            res.AddHeader("X-Auth-Status", "false")
            loR = res.status(400)
            loR.json(THIS.ToJson(loRes))
            EXIT
        ENDIF

        lcEmail    = LOWER(ALLTRIM(NVL(loBody.email, "")))
        lcPassword = ALLTRIM(NVL(loBody.password, ""))

        IF !THIS.CredencialesValidas(lcEmail, lcPassword)
            loRes.status  = "error"
            loRes.message = "Invalid credentials"
            res.AddHeader("X-Auth-Status", "false")
            loR = res.status(401)
            loR.json(THIS.ToJson(loRes))
            EXIT
        ENDIF

        * Los tres campos son obligatorios. Cualquier otro que anadas a
        * loData viaja como claim del token.
        loData      = THIS.newObject("id,name,role")
        loData.id   = ALLTRIM(usuarios.USR_ID)
        loData.name = ALLTRIM(usuarios.USR_NOMBRE)
        loData.role = ALLTRIM(usuarios.USR_ROL)

        loRes.status  = "ok"
        loRes.data    = loData
        loRes.message = "Successful login"

        * Esta es la linea que emite el token. El JWT lo firma el
        * middleware; aqui no se genera nada.
        res.AddHeader("X-Auth-Status", "true")
        loR = res.status(200)
        loR.json(THIS.ToJson(loRes))
    CATCH TO loEx
        loRes.status  = "error"
        loRes.message = loEx.Message
        THIS.LogFromException(loEx)
        res.AddHeader("X-Auth-Status", "false")
        loR = res.status(500)
        loR.json(THIS.ToJson(loRes))
    ENDTRY
ENDPROC

Un endpoint protegido: basta con no poner public

pMiPerfil: un endpoint protegido por JWT
PROCEDURE pMiPerfil(req, res) HELP "GET: profile"
    LOCAL loRes

    * El middleware ya validó el token antes de entrar aquí. El usuario
    * viene resuelto, y los claims extra se leen uno a uno.
    loRes = THIS.newObject("id,name,role,empresa")
    loRes.id      = req.AuthenticatedUser.id
    loRes.name    = req.AuthenticatedUser.name
    loRes.role    = req.AuthenticatedUser.role
    loRes.empresa = req.GetClaim("empresa", "")

    RETURN THIS.Ok(loRes)
ENDPROC

Y si el token falta, está caducado o mal firmado, el middleware contesta solo:

HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer realm="foxserver", error="invalid_token",
                  error_description="The access token expired"
Content-Type: application/problem+json; charset=utf-8

{"type":"about:blank","title":"Unauthorized","status":401,
 "detail":"Invalid token","instance":"/profile","code":"unauthorized"}

La cabecera WWW-Authenticate es la del RFC 6750, y distingue los tres casos: sin credencial no lleva error=, que es lo que manda el §3.1. El cuerpo es Problem Details (RFC 9457), como todos los errores del motor.

CORS

Responde a las peticiones preflight OPTIONS sin que lleguen al controlador, y pone las cabeceras en todas las respuestas. Se configura en el manifiesto:

{
  "allowedorigins": "https://miapp.com,https://admin.miapp.com",
  "allowedmethods": "GET,POST,PUT,PATCH,DELETE,OPTIONS",
  "allowedheaders": "Content-Type,Authorization,X-Requested-With"
}
CORS se resuelve antes de la cadena de middlewares, así que no aparece en la lista de los que se encienden y se apagan. Y allowedorigins no es autenticación: si expones MCP o JSON-RPC sin auth, el motor te lo avisa al arrancar.

Registro de peticiones

Escribe cada petición en ficheros diarios, en la carpeta que diga logging.path. El nombre lo componen el prefijo, el tipo y la fecha:

logging.path\
  api_Request_20260915.log     peticiones servidas
  api_Error_20260915.log       errores

El log del host es otro: logs\FoxServer_<fecha>.log, en la raíz de la instalación, uno por día, partido a 10 MB y purgado a los 30. El nivel se ajusta con FOXSERVER_LOG_LEVEL y la retención con FOXSERVER_LOG_RETENTION_DAYS.

El Visor de Eventos de Windows no recibe tus logs Ni siquiera en modo servicio. Sólo se escribe una entrada allí si falla la escritura del propio fichero de log, para que un fallo del registro no se quede sin registrar.

Lo que sí puedes escribir tú: los ganchos del controlador

No hay API para escribir un middleware desde VFP, pero cada controlador tiene dos ganchos que el motor llama alrededor de cada endpoint suyo. Son el sitio de abrir las tablas y de cerrarlas.

BeforeEndpoint y AfterEndpoint: abrir y cerrar la base de datos
DEFINE CLASS ProductosController AS ApiController OLEPUBLIC

    * Antes de CADA endpoint de este controlador.
    * Devolver .F. corta: el endpoint no llega a ejecutarse.
    FUNCTION BeforeEndpoint(req, res)
        SET DELETED ON
        SET EXCLUSIVE OFF
        OPEN DATABASE (THIS.cPath + "data\tienda.dbc") SHARED
        RETURN .T.
    ENDFUNC

    * Despues de CADA endpoint, haya ido bien o mal.
    PROCEDURE AfterEndpoint(req, res)
        CLOSE DATABASES ALL
    ENDPROC

ENDDEFINE
Son por controlador: no existe un «controlador raíz» con ganchos para todo el proyecto. Lo que sí es de todo el proyecto es el ApplicationLifecycle, que corre al arrancar y al parar el servidor.

Siguiente: Licencias →