Responde las 10 preguntas que mandaron sobre admin.u-site.app leyendo el
código del servidor en vez de inferirlas desde el cliente, con cita de
archivo y línea en cada una.
Lo que sale de ahí:
- expires_in sí es timestamp absoluto (su lectura era correcta), pero el
token se emite con 90 años de vigencia porque Login no pasa vencimiento.
- La API v1 NO acepta Bearer: autentica solo por la cookie
Verify-Rest-Token, así que el placeholder "..." sin completar deja los 12
endpoints en 401. Es la causa más probable de que la integración nunca
haya autenticado por sí misma.
- Hay DOS textos de error 401 ("Token not found" y "Invalid Attempt"), y su
reintento solo cubre el segundo.
- El envoltorio doble-codificado de dLocal es real, nadie lo adivinó mal.
- generate-qr devuelve imagen binaria, no JSON.
Incluye los pendientes de nuestro lado (vigencia del token, Secure=false en
la cookie, auth por cookie en una API máquina-a-máquina) y la recomendación
de usar /api/v1/pagos-externos, que ya usa Bearer + IP, para integraciones
nuevas.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
8.2 KiB
Contrato real de la API v1 de admin.u-site.app
Respuestas verificadas leyendo el código del servidor, no inferidas desde el cliente. Cada punto cita el archivo y la línea para que se pueda auditar.
Base de todos los endpoints: https://admin.u-site.app/api/v1/…
(rest/routes/routes.go:10 monta /api, rest/routes/api.go:38 agrega v1).
1. Formato de expires_in → timestamp Unix absoluto
Su interpretación es la correcta. En config/token.go:22-49:
t.Expire = ninetyYears // acá es duración en segundos
expiresIn := time.Now().Add(time.Duration(t.Expire)*time.Second).Unix()
claims["exp"] = expiresIn
t.Expire = expiresIn // ← se SOBREESCRIBE con el absoluto
El campo se reusa: entra como duración y sale como timestamp. Lo que viaja en
la respuesta es expiresIn, o sea segundos desde epoch.
Pero hay algo más importante: cuando
Loginllama aCreateTokenno le pasa vencimiento (pkg/auth/user.go:98), así que aplica el default deninetyYears— 90 años. En la práctica el token de esa integración nunca expira, y toda la lógica de caché y renovación no se ejerce jamás. Esto hay que cambiarlo del lado nuestro (ver Pendientes).
2. Respuesta real de POST /api/v1/oauth/token
rest/controllers/api/auth_controller.go:42-45:
{
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"expires_in": 4626547200
}
- El campo es
token, noaccess_token. expires_inestá en el nivel raíz.- No hay
token_type, nirefresh_token, ni envoltorio.
Errores (todos con HTTP 401), auth_controller.go:15-40:
{ "error": true, "message": "Invalid Credentials" }
3. QR y VCF → JSON plano
| Endpoint | Método | Respuesta |
|---|---|---|
/api/v1/generate-qr-tmp |
POST | {"url": "https://…"} |
/api/v1/generate-url-qr |
POST | {"url": "https://…"} |
/api/v1/generate-vcf |
POST | {"success": true, "url": "https://…"} |
/api/v1/generate-qr |
POST | no es JSON: devuelve la imagen binaria Content-Type: image/webp |
vcard_qr.go:133-135, vcard_vcf.go:103-106, vcard_qr.go:169-170.
Ojo con el último: generate-qr responde bytes de imagen, no JSON. Si el
cliente hace json_decode de eso, falla siempre.
Errores: {"error": "Datos inválidos"} con 400, o {"error": "<detalle>"} con 500.
4. dLocal → sí, el envoltorio raro es real
Nadie lo adivinó mal. rest/controllers/api/dlocal_controller.go:42-45:
return c.Status(200).JSON(fiber.Map{
"message": "Plan creado exitosamente.",
"response": string(response), // ← el JSON de dLocal, como STRING
})
O sea, doble codificación real:
{
"message": "Plan creado exitosamente.",
"response": "{\"id\":\"PLAN-123\",\"status\":\"active\"}"
}
Hay que hacer json_decode dos veces: una del cuerpo y otra del campo
response. Aplica igual a los 7 endpoints dLocal (líneas 44, 77, 110, 137,
162, 188, 221). El campo message cambia de texto según el endpoint, así que
no conviene usarlo para decidir nada.
5. Cuerpo de error de sesión inválida → hay DOS textos distintos
Acá está el problema que sospechaban. rest/middlewares/auth.go:271-286:
token := c.Cookies("Verify-Rest-Token")
if token == "" {
return c.Status(401).JSON("Token not found") // ← falta la cookie
}
… ErrorHandler: return ctx.Status(401).JSON("Invalid Attempt") // ← cookie inválida/vencida
- Sin cookie → cuerpo
"Token not found" - Cookie presente pero inválida o vencida → cuerpo
"Invalid Attempt"
Los dos son JSON string, con comillas incluidas — el cuerpo literal es
"Invalid Attempt", 16 bytes, no el texto pelado.
Su reintento solo cubre el segundo caso. Recomendación: reintentar ante cualquier 401, sin mirar el texto. Es más robusto y no depende de una redacción que puede cambiar.
6. URLs base → una sola raíz, sin derivar por str_replace
Todos cuelgan de https://admin.u-site.app/api/v1/. No hay hosts separados
por grupo, así que lo correcto es guardar la raíz y concatenar la ruta,
no guardar la URL del token y derivar las demás:
POST /api/v1/oauth/token
POST /api/v1/generate-qr-tmp
POST /api/v1/generate-qr (devuelve imagen)
POST /api/v1/generate-vcf
POST /api/v1/generate-url-qr
POST /api/v1/dlocal/subscription/crear-plan
GET /api/v1/dlocal/subscription/ver-plan/:planID
PATCH /api/v1/dlocal/subscription/actualizar-plan/:planID
GET /api/v1/dlocal/subscription/plan/all
PATCH /api/v1/dlocal/subscription/plan/:planId/subscription/:subscriptionId/deactivate
GET /api/v1/dlocal/subscription/:subscriptionId/execution/:invoiceId
POST /api/v1/dlocal/payment/crear-pago
POST /api/v1/rapyd/wallet/create
7. El header Cookie → es OBLIGATORIO, y hoy está roto
Esta es la más crítica de las 10.
La API no acepta Bearer. AuthApi() lee exclusivamente la cookie
(rest/middlewares/auth.go:272-281):
token := c.Cookies("Verify-Rest-Token")
if token == "" { return c.Status(401).JSON("Token not found") }
TokenLookup: "cookie:Verify-Rest-Token"
Consecuencias:
- Mandar el token en el body o como
Authorization: Bearerno autentica nada. - Con el placeholder literal
...sin completar, los 12 endpoints autenticados devuelven 401 "Token not found". Si algo de eso "funciona" hoy, es porque el cliente HTTP tiene cookie jar y está reusando la cookie quePOST /oauth/tokendeja seteada en la respuesta (config/token.go:40-47). session_idno hace falta: el middleware no lo mira. Se puede quitar.
Lo correcto es tomar el token de la respuesta de oauth/token y mandarlo
como Cookie: Verify-Rest-Token=<token>, o dejar que el cliente maneje
cookies solo.
8. Control de acceso por IP → no, para /api/v1 no hay
/api/v1 solo valida la cookie JWT. No hay allowlist de IP; no hace falta
registrar la IP del servidor de producción.
Sí existe validación por IP, pero en otra API: /api/v2 usa ApiKey con
IP obligatoria y scopes, y /api/v1/pagos-externos usa AuthServicioPago
(ambas excluidas de AuthApi en auth.go:266-268). Si prefieren un esquema
con IP fija y token que no vive en una cookie, /api/v2 es el camino y
vale la pena migrar ahí en vez de arreglar el actual.
9. Usuario y contraseña
No van en este documento. Son las credenciales de un usuario real de la tabla
users (login.CheckLogin(), auth_controller.go:28). Que las entreguen por
un canal seguro y, dado que llevan 15 meses sin rotar, conviene crear un
usuario dedicado a la integración en vez de reusar uno de persona.
10. Webhooks → sí existen, pero no para esto
Hay webhooks entrantes de pago ya montados (rest/routes/publicas.go:22-33):
/webhooks/bold, /webhooks/dlocal, /webhooks/paypal, más Coolify y
Telegram.
Pero son para que la pasarela nos avise a nosotros, no para avisarle a un tercero. Hoy no existe un callback saliente hacia el sistema de ustedes ni para pagos ni para QR. Si lo necesitan, es desarrollo nuevo de nuestro lado.
Mientras tanto, la alternativa ya construida es /api/v1/pagos-externos
(api.go:32-34), pensada justo para apps de terceros:
GET /api/v1/pagos-externos/pasarelas
POST /api/v1/pagos-externos/solicitar
GET /api/v1/pagos-externos/:referencia/estado
Usa token Bearer + IP (no cookie), así que evita todo el problema del punto 7. Para una integración nueva, recomendamos esta y no los endpoints dLocal directos.
Pendientes de nuestro lado (no son preguntas, son cosas a corregir)
- Tokens de 90 años (
config/token.go:26). Un token filtrado es acceso permanente. Hay que ponerle un vencimiento razonable — y recién ahí la caché y el reintento del cliente van a tener sentido. - La cookie se emite con
Secure: false(config/token.go:44), así que viaja también por HTTP plano. - Autenticación por cookie en una API máquina-a-máquina es el problema de
fondo: obliga a manejar cookie jar y no permite IP allowlist.
/api/v2ya resuelve esto bien. - La doble codificación de dLocal (punto 4) no aporta nada; devolver el JSON anidado directo sería un cambio compatible si se versiona.