services/api/src/modules/auth/account-export.service.js
Overview
Este módulo construye y valida la exportación temporal de actividad de cuenta. Genera un enlace firmado, verifica su vigencia contra el estado actual del usuario y arma un ZIP en memoria con activity.csv y leads.csv. También se integra con auth.controller.js para servir la descarga y con auth.service.js para generar enlaces de correo. fileciteturn0file5turn0file8turn0file11
El archivo está orientado a la ventana de cierre de cuenta. Solo funciona mientras la cuenta sigue siendo exportable y antes de la purga definitiva. El ZIP se reconstruye en cada petición y no se persiste en disco ni en almacenamiento externo. fileciteturn0file5turn0file17
[!IMPORTANT] La disponibilidad del export no depende solo del JWT. Cada descarga vuelve a revisar
accountStatus,deletionCompletedAtypurgeScheduledAten tiempo real. fileciteturn0file5turn0file9turn0file17
How it works
flowchart TD
Client[Client]
AuthService[auth service]
Controller[auth controller]
VerifyReq[verifyAccountExportRequest]
VerifyToken[verifyAccessToken]
UserModel[User model]
CheckAvail[isAccountExportAvailable]
ZipBuild[buildAccountExportZip]
ActivityCsv[buildAccountExportCsv]
LeadsCsv[buildLeadsExportCsv]
ListingModel[Listing model]
LeadModel[Lead model]
ZipFileNode[ZipFile]
StreamBuffer[streamToBuffer]
Download[ZIP response]
UrlBuild[buildAccountExportUrl]
TokenBuild[buildAccountExportToken]
SignToken[signScopedToken]
AuthService --> UrlBuild
UrlBuild --> CheckAvail
UrlBuild --> TokenBuild
TokenBuild --> SignToken
Client --> Controller
Controller --> VerifyReq
VerifyReq --> VerifyToken
VerifyReq --> UserModel
VerifyReq --> CheckAvail
VerifyReq --> ZipBuild
ZipBuild --> ActivityCsv
ZipBuild --> LeadsCsv
ActivityCsv --> ListingModel
ActivityCsv --> LeadModel
LeadsCsv --> LeadModel
ZipBuild --> ZipFileNode
ZipFileNode --> StreamBuffer
StreamBuffer --> Download
La ruta de descarga primero valida el token con verifyAccessToken. Después vuelve a leer al usuario y comprueba que siga siendo elegible. Si todo pasa, el módulo genera ambos CSV, los empaqueta con yazl, y convierte el outputStream en Buffer con node:stream/consumers. fileciteturn0file5turn0file9turn0file18
Key components
{
"title": "isAccountExportAvailable",
"content": "Valida si una cuenta puede descargar el export. Revisa `accountStatus`, `deletionCompletedAt` y `purgeScheduledAt` contra la fecha actual.",
"type": "function",
"filePath": "services/api/src/modules/auth/account-export.service.js",
"badges": [
"public"
]
}
{
"title": "buildAccountExportToken",
"content": "Crea un JWT de un solo prop\u00f3sito con `sub` y `purpose=account-export`. Su expiraci\u00f3n se calcula hasta `purgeScheduledAt`.",
"type": "function",
"filePath": "services/api/src/modules/auth/account-export.service.js",
"badges": [
"public"
]
}
{
"title": "buildAccountExportUrl",
"content": "Devuelve la URL directa de descarga para el correo. Si la cuenta no califica, devuelve `null`.",
"type": "function",
"filePath": "services/api/src/modules/auth/account-export.service.js",
"badges": [
"public"
]
}
{
"title": "verifyAccountExportRequest",
"content": "Comprueba la firma y el prop\u00f3sito del token, luego revalida el estado en vivo del usuario. Devuelve el `user._id` o lanza `ApiError`.",
"type": "function",
"filePath": "services/api/src/modules/auth/account-export.service.js",
"badges": [
"public"
]
}
{
"title": "buildAccountExportCsv",
"content": "Construye `activity.csv` con consultas de solo lectura sobre `Listing` y un agregado de `Lead`. Usa columnas allowlist y produce un CSV en memoria.",
"type": "function",
"filePath": "services/api/src/modules/auth/account-export.service.js",
"badges": [
"public"
]
}
{
"title": "buildLeadsExportCsv",
"content": "Construye `leads.csv` con los `Lead` del propietario autenticado. Devuelve cabeceras aunque no existan filas.",
"type": "function",
"filePath": "services/api/src/modules/auth/account-export.service.js",
"badges": [
"public"
]
}
{
"title": "buildAccountExportZip",
"content": "Empaqueta `activity.csv` y `leads.csv` en un ZIP ef\u00edmero. Usa `ZipFile.addBuffer` y `streamToBuffer`, sin tocar disco.",
"type": "function",
"filePath": "services/api/src/modules/auth/account-export.service.js",
"badges": [
"public"
]
}
Data & fields
Los datos del módulo son pequeños, pero están muy definidos. El archivo usa constantes para fijar el propósito del token, las cuentas exportables, los nombres de archivos del ZIP y las columnas de ambos CSV. fileciteturn0file5turn0file8
EXPORT_TOKEN_PURPOSE[badge:read-only]string— valor fijoaccount-export. Se usa para firmar y validar el token. fileciteturn0file5turn0file9EXPORTABLE_ACCOUNT_STATUSES[badge:read-only]string[]— estados permitidos:pending_deletionyanonymized_pending_purge.CSV_COLUMNS[badge:read-only]string[]— cabeceras deactivity.csv. fileciteturn0file5turn0file18LEADS_CSV_COLUMNS[badge:read-only]string[]— cabeceras deleads.csv. fileciteturn0file5turn0file18ZIP_ACTIVITY_ENTRY[badge:read-only]string— nombre de la entrada ZIP para actividad.ZIP_LEADS_ENTRY[badge:read-only]string— nombre de la entrada ZIP para leads. fileciteturn0file5turn0file8ZIP_ENTRY_MTIME[badge:read-only]Date— marca de tiempo fija para hacer estable el ZIP entre descargas. fileciteturn0file5turn0file8FORMULA_INJECTION_PREFIXES[badge:read-only]string[]— prefijos neutralizados:=,+,-y@.ACCOUNT_EXPORT_CSV_COLUMNS[badge:read-only]string[]— alias exportado deCSV_COLUMNS. fileciteturn0file5turn0file18LEADS_EXPORT_CSV_COLUMNS[badge:read-only]string[]— alias exportado deLEADS_CSV_COLUMNS. fileciteturn0file5turn0file18ACCOUNT_EXPORT_ZIP_ENTRIES[badge:read-only]{ activity, leads }— mapa inmutable de entradas del ZIP.
listing_id— identificador del listing.title— título del listing.created_at— fecha de creación en ISO.publication_status— estado de publicación.lifecycle_status— estado de ciclo de vida.likes— conteo agregado de likes.comments— conteo agregado de comentarios.detail_opens— conteo agregado de aperturas de detalle.contact_clicks— conteo agregado de clics de contacto.leads— conteo agregado de leads por listing. fileciteturn0file5turn0file18
lead_id— identificador del lead.name— nombre del lead.email— correo del lead.phone— teléfono del lead.status— estado del lead.source— origen del lead.listing_id— listing asociado.listing_title— título del listing asociado.message— mensaje del lead.notes— notas internas visibles en el snapshot.created_at— fecha de creación en ISO.updated_at— fecha de actualización en ISO. fileciteturn0file5turn0file18
user.accountStatus— debe estar en uno de los estados exportables.user.deletionCompletedAt— debe sernullo no existir.user.purgeScheduledAt— debe existir y ser una fecha futura.user._id— se usa comosubdel token y como clave de consulta.token.purpose— debe ser exactamenteaccount-export. fileciteturn0file5turn0file9turn0file17
Behaviors
El módulo está dividido en cinco comportamientos visibles y tres helpers internos. Cada uno trabaja sobre datos de solo lectura o sobre contenido efímero en memoria. fileciteturn0file5turn0file9
isAccountExportAvailable(user, now)— devuelvetruesolo si la cuenta existe, está en un estado exportable, no tienedeletionCompletedAty supurgeScheduledAtsigue en el futuro.buildAccountExportToken(user, now)— firma un token scoped consubypurpose=account-export. La expiración queda limitada porpurgeScheduledAt.buildAccountExportUrl(user, now)— devuelve una URL completa contraenv.apiUrlcontokenen query string, onullsi la cuenta no califica.verifyAccountExportRequest(token, now)— verifica la firma del token, exige el propósito correcto, consulta al usuario enUser, y devuelveuser._id. Si falla, lanzaApiErrorcon401o410. fileciteturn0file9turn0file17buildAccountExportCsv(userId)— consultaListingdel usuario, agrega conteos deLeadpor listing, y generaactivity.csvcon BOM UTF-8 y saltos\r\n. fileciteturn0file5turn0file18buildLeadsExportCsv(userId)— consultaLeadporownerId, ordena porcreatedAt, y generaleads.csvcon BOM UTF-8. Si no hay leads, devuelve solo cabeceras. fileciteturn0file5turn0file18buildAccountExportZip(userId)— construye ambos CSV, los añade al ZIP como buffers y consume eloutputStreamhacia memoria.csvEscape(value)— aplica el quoting CSV estándar cuando el valor contiene comillas, saltos de línea, retornos de carro o comas.neutralizeFormulaInjection(value)— antepone un apóstrofo si el texto empieza con=,+,-o@.csvField(value)— combina la neutralización de fórmula y el escape CSV. Es la única ruta de escape usada por ambos exports. fileciteturn0file5turn0file9
[!WARNING]
verifyAccountExportRequestno distingue entre token inválido y token ajeno. En ambos casos devuelve el mismo401conACCOUNT_EXPORT_TOKEN_INVALID. fileciteturn0file9turn0file17
[!CAUTION] Un token válido puede dejar de servir antes de expirar. Si la cuenta cambia de estado, si
deletionCompletedAtse marca, o si pasapurgeScheduledAt, la descarga responde410. fileciteturn0file5turn0file9turn0file17
API
La ruta está expuesta desde auth.controller.js, que importa verifyAccountExportRequest y buildAccountExportZip desde este módulo. La documentación de backend confirma que el endpoint devuelve un ZIP y usa el token de propósito único como prueba de identidad. fileciteturn0file8turn0file17
GET /api/auth/account-export
{
"title": "Download account export",
"description": "Genera y devuelve el ZIP efimero con activity.csv y leads.csv para una cuenta elegible.",
"method": "GET",
"baseUrl": "env.apiUrl",
"endpoint": "/api/auth/account-export",
"headers": [
{
"key": "Accept",
"value": "application/zip",
"required": false
}
],
"queryParams": [
{
"key": "token",
"value": "JWT con purpose account-export",
"required": true
}
],
"pathParams": [],
"bodyType": "none",
"requestBody": "",
"formData": [],
"rawBody": "",
"responses": {
"200": {
"description": "ZIP con activity.csv y leads.csv",
"body": "<binary ZIP with activity.csv and leads.csv>"
},
"401": {
"description": "Token ausente, invalido, expirado o con purpose incorrecto",
"body": "{\n \"error\": \"El enlace de exportacion no es valido o expiro\",\n \"code\": \"ACCOUNT_EXPORT_TOKEN_INVALID\"\n}"
},
"410": {
"description": "La cuenta ya no califica para exportacion",
"body": "{\n \"error\": \"El resumen de actividad ya no esta disponible\",\n \"code\": \"ACCOUNT_EXPORT_UNAVAILABLE\"\n}"
}
}
}
La respuesta exitosa devuelve 200, Content-Type: application/zip y Content-Disposition: attachment; filename="somos-plano-export.zip". El ZIP contiene activity.csv y leads.csv, cada uno con su propio BOM UTF-8. fileciteturn0file17turn0file18
Workflows
Este flujo resume la secuencia real del módulo. No introduce pasos externos ni almacenamiento intermedio. fileciteturn0file5turn0file8
1. Validar elegibilidad | `isAccountExportAvailable` comprueba el estado actual del usuario.
2. Firmar token | `buildAccountExportToken` crea un JWT con `purpose=account-export`.
3. Construir CSVs | `buildAccountExportCsv` y `buildLeadsExportCsv` leen datos con consultas de solo lectura.
4. Empaquetar ZIP | `buildAccountExportZip` añade ambos buffers con `ZipFile.addBuffer` y consume el stream a memoria.
5. Responder descarga | `auth.controller.js` devuelve el ZIP al cliente con el token verificado.
Gotchas & edge cases
[!WARNING]
activity.csvno incluye datos sensibles de contacto de listings ni detalles completos de leads. Solo expone métricas agregadas y conteos. fileciteturn0file5turn0file17
[!WARNING]
leads.csvsí incluye PII real del propietario del lead, porque ese archivo representa el CRM privado previo a la purga.
[!CAUTION] El módulo neutraliza CSV injection. Si un valor empieza con
=,+,-o@, se prefija un apóstrofo antes del escape CSV. Esto incluye teléfonos internacionales legítimos.
[!NOTE] El ZIP usa
ZIP_ENTRY_MTIMEfijo para mantener bytes estables entre descargas con el mismo contenido. Eso evita que la hora real de descarga cambie el archivo generado. fileciteturn0file5turn0file8
[!IMPORTANT]
buildAccountExportUrldevuelvenullsi la cuenta no califica. Así evita emitir enlaces ya muertos para correos o notificaciones.
Related
Este módulo se conecta con services/api/src/modules/auth/auth.controller.js, donde el endpoint GET /api/auth/account-export usa verifyAccountExportRequest y buildAccountExportZip. También se conecta con services/api/src/modules/auth/auth.service.js, que importa buildAccountExportUrl para incluir el enlace de descarga en correos relacionados con el ciclo de cuenta. fileciteturn0file8turn0file11
El comportamiento general también aparece documentado en la referencia de backend y en los flujos críticos del proyecto. Esas referencias confirman la política de no persistencia, el formato ZIP y el uso exclusivo antes de la purga definitiva. fileciteturn0file17turn0file0