services/api/src/models/User.js

Overview

Este archivo define el modelo Mongoose User, que concentra identidad, perfil, roles, suscripción, verificación y ciclo de vida de cuenta en la colección users. También sostiene los campos que otros flujos del backend leen para Google Auth, Twilio Verify, el gate ALTCHA de contacto y la purga diferida.

El modelo funciona como el registro central de cuenta para la plataforma. Otras capas no duplican esa información; la consultan y la actualizan de forma determinista según el estado de la cuenta.

[!NOTE] Las cuentas creadas con Google pueden omitir phone y passwordHash. El registro local sigue exigiendo esos datos en auth.validation.js, pero el schema se relaja para ese caso.

How it works

flowchart TD
  UserModel[User model]
  AuthService[auth.service.js]
  PhoneVerificationService[phone-verification.service.js]
  ContactGateModule[contact-gate module]
  PurgeJob[purgeExpiredAccounts.js]
  SendWarningsJob[sendPurgeWarnings.js]
  ListingCommentsRoutes[listing-comments.routes.js]
  GoogleAuth[Google Auth]
  TwilioVerify[Twilio Verify]

  AuthService --> UserModel
  PhoneVerificationService --> UserModel
  ContactGateModule --> UserModel
  PurgeJob --> UserModel
  SendWarningsJob --> UserModel
  ListingCommentsRoutes --> UserModel
  GoogleAuth --> UserModel
  TwilioVerify --> PhoneVerificationService
  PhoneVerificationService -->|phoneVerifiedAt| UserModel
  PurgeJob -->|clears account state| UserModel
  SendWarningsJob -->|reads accountStatus and purgeScheduledAt| UserModel

El schema define la forma del documento y la validación de sus campos. La lógica de negocio vive fuera del modelo, en servicios y jobs que leen o actualizan campos concretos como phoneVerifiedAt, accountStatus, google.sub o deletionGracePreferences.

Key components

El archivo expone un único componente principal: el modelo Mongoose User. Ese modelo se compila sobre userSchema, usa la colección users y activa timestamps.

{
    "title": "User",
    "content": "Modelo Mongoose que representa cuentas de usuario en la colecci\u00f3n users. Centraliza identidad, perfil, suscripci\u00f3n, verificaci\u00f3n, preferencias y ciclo de cierre.",
    "type": "class",
    "filePath": "services/api/src/models/User.js",
    "badges": [
        "public"
    ]
}

Data & fields

El schema contiene campos obligatorios, opcionales y subdocumentos. Los tipos y validaciones del schema se aplican antes de persistir cada documento.

[
    {
        "name": "name",
        "type": "string",
        "required": true,
        "description": "Nombre visible. Se recorta y acepta entre 2 y 120 caracteres."
    },
    {
        "name": "firstName",
        "type": "string",
        "required": true,
        "description": "Nombre de pila. Se recorta y acepta entre 2 y 80 caracteres."
    },
    {
        "name": "lastName",
        "type": "string",
        "required": true,
        "description": "Apellido. Se recorta y acepta entre 2 y 80 caracteres."
    },
    {
        "name": "email",
        "type": "string",
        "required": true,
        "description": "Correo principal de la cuenta. Se recorta y se guarda en minúsculas."
    },
    {
        "name": "phone",
        "type": "string",
        "description": "Teléfono opcional. Puede faltar en cuentas creadas con Google Auth."
    },
    {
        "name": "phoneVerifiedAt",
        "type": "Date",
        "description": "Fecha de verificación telefónica. Null significa que no está verificado."
    },
    {
        "name": "username",
        "type": "string",
        "description": "Nombre público opcional. Se recorta, se guarda en minúsculas y solo acepta letras, números, punto y guion bajo."
    },
    {
        "name": "passwordHash",
        "type": "string",
        "description": "Hash de contraseña local. Puede faltar en cuentas Google-only."
    },
    {
        "name": "systemRole",
        "type": "string",
        "required": true,
        "description": "Rol del sistema. Valores permitidos: user, admin."
    },
    {
        "name": "accountRole",
        "type": "string",
        "required": true,
        "description": "Rol de cuenta. Valores permitidos: creator, seeker."
    },
    {
        "name": "plan",
        "type": "string",
        "description": "Plan comercial. Valores permitidos: free, professional, creator, enterprise."
    },
    {
        "name": "stripeCustomerId",
        "type": "string",
        "description": "Identificador de cliente en Stripe. Se recorta y por defecto queda vacío."
    },
    {
        "name": "stripeSubscriptionId",
        "type": "string",
        "description": "Identificador de suscripción en Stripe. Se recorta y por defecto queda vacío."
    },
    {
        "name": "subscriptionStatus",
        "type": "string",
        "description": "Estado de suscripción. Valores permitidos: none, incomplete, incomplete_expired, trialing, active, past_due, canceled, unpaid, paused."
    },
    {
        "name": "currentPeriodEnd",
        "type": "Date",
        "description": "Fecha de finalización del periodo actual de suscripción. Null por defecto."
    },
    {
        "name": "cancelAtPeriodEnd",
        "type": "boolean",
        "description": "Marca si la suscripción termina al final del periodo actual."
    },
    {
        "name": "bio",
        "type": "string",
        "description": "Biografía pública. Se recorta y admite hasta 280 caracteres."
    },
    {
        "name": "photo",
        "type": "string",
        "description": "URL o referencia de foto de perfil. Se recorta."
    },
    {
        "name": "coverPhoto",
        "type": "string",
        "description": "URL o referencia de portada. Se recorta."
    },
    {
        "name": "website",
        "type": "string",
        "description": "Sitio web público. Se recorta."
    },
    {
        "name": "location",
        "type": "string",
        "description": "Ubicación pública. Se recorta y admite hasta 120 caracteres."
    },
    {
        "name": "seo",
        "type": "object",
        "description": "Metadatos SEO del perfil.",
        "children": [
            {
                "name": "descriptors",
                "type": "string",
                "description": "Texto descriptivo para SEO. Se recorta y admite hasta 500 caracteres."
            },
            {
                "name": "aiKeywords",
                "type": "string",
                "description": "Palabras clave asociadas al perfil. Se recorta y admite hasta 500 caracteres."
            },
            {
                "name": "schemaType",
                "type": "string",
                "description": "Tipo de schema estructurado asociado al perfil."
            }
        ]
    },
    {
        "name": "accountStatus",
        "type": "string",
        "required": true,
        "description": "Estado de la cuenta. Valores permitidos: active, pending_deletion, anonymized_pending_purge, purged, disabled."
    },
    {
        "name": "deletionRequestedAt",
        "type": "Date",
        "description": "Fecha en la que se solicitó el cierre de cuenta."
    },
    {
        "name": "anonymizationScheduledAt",
        "type": "Date",
        "description": "Momento programado para la anonimización dentro del ciclo de cierre."
    },
    {
        "name": "purgeScheduledAt",
        "type": "Date",
        "description": "Momento programado para la purga definitiva."
    },
    {
        "name": "anonymizedAt",
        "type": "Date",
        "description": "Fecha real de anonimización. Null por defecto."
    },
    {
        "name": "deletionCompletedAt",
        "type": "Date",
        "description": "Fecha real de finalización de la eliminación."
    },
    {
        "name": "restorationToken",
        "type": "string",
        "description": "Token de un solo uso para restaurar una cuenta durante la ventana de restauración."
    },
    {
        "name": "restorationTokenExpiresAt",
        "type": "Date",
        "description": "Fecha de expiración del token de restauración."
    },
    {
        "name": "googleReauthTokenJti",
        "type": "string",
        "description": "JTI de la reautenticación con Google usada para flujos de cierre de cuenta."
    },
    {
        "name": "retentionContact",
        "type": "string",
        "description": "Canal de contacto cifrado conservado temporalmente entre anonimización y purga."
    },
    {
        "name": "deletionGracePreferences",
        "type": "object",
        "description": "Preferencias del titular durante la ventana de protección del cierre de cuenta.",
        "children": [
            {
                "name": "keepPublishedListingsVisible",
                "type": "boolean",
                "description": "Controla si los listings publicados siguen visibles."
            },
            {
                "name": "allowLeadCapture",
                "type": "boolean",
                "description": "Controla si se siguen permitiendo leads."
            },
            {
                "name": "allowComments",
                "type": "boolean",
                "description": "Controla si se siguen permitiendo comentarios."
            }
        ]
    },
    {
        "name": "tokenVersion",
        "type": "number",
        "required": true,
        "description": "Versión de tokens de la cuenta. No puede ser negativa."
    },
    {
        "name": "passwordResetToken",
        "type": "string",
        "description": "Token temporal para restablecer contraseña."
    },
    {
        "name": "passwordResetExpires",
        "type": "Date",
        "description": "Expiración del token de restablecimiento."
    },
    {
        "name": "emailVerifiedAt",
        "type": "Date",
        "description": "Fecha de verificación de correo. Null significa no verificado."
    },
    {
        "name": "emailVerificationTokenHash",
        "type": "string",
        "description": "Hash del token de verificación de correo."
    },
    {
        "name": "emailVerificationExpiresAt",
        "type": "Date",
        "description": "Fecha de expiración de la verificación de correo."
    },
    {
        "name": "emailVerificationRequestedAt",
        "type": "Date",
        "description": "Marca temporal de la última solicitud de verificación de correo."
    },
    {
        "name": "notificationPreferences",
        "type": "object",
        "description": "Preferencias mínimas de notificación.",
        "children": [
            {
                "name": "listingPerformanceDigest",
                "type": "boolean",
                "description": "Preferencia para el digest de rendimiento de listings."
            }
        ]
    },
    {
        "name": "contactGate",
        "type": "object",
        "description": "Estado del gate ALTCHA de contacto.",
        "children": [
            {
                "name": "validatedAt",
                "type": "Date",
                "description": "Fecha de la última validación del challenge."
            },
            {
                "name": "count",
                "type": "number",
                "description": "Conteo de intenciones autorizadas desde la última validación."
            }
        ]
    },
    {
        "name": "google",
        "type": "object",
        "description": "Identidad federada de Google.",
        "children": [
            {
                "name": "sub",
                "type": "string",
                "description": "Identificador estable del proveedor Google."
            },
            {
                "name": "linkedAt",
                "type": "Date",
                "description": "Fecha en que se vinculó la cuenta de Google."
            }
        ]
    }
]

Índices y unicidad

El modelo declara un índice único para email y tres índices únicos parciales para phone, username y google.sub. También añade índices compuestos sobre accountStatus con anonymizationScheduledAt y purgeScheduledAt.

  • email tiene unicidad global.
  • phone solo participa en la unicidad cuando existe como cadena.
  • username solo participa en la unicidad cuando existe como cadena.
  • google.sub solo participa en la unicidad cuando existe como cadena.
  • accountStatus + anonymizationScheduledAt acelera consultas del ciclo de anonimización.
  • accountStatus + purgeScheduledAt acelera consultas del ciclo de purga.

[!IMPORTANT] phone, username y google.sub usan índices parciales. Eso permite cuentas locales sin esos campos y evita colisiones por documentos incompletos.

Behaviors

  • User compila userSchema como modelo Mongoose y lo exporta con el nombre User.
  • El modelo persiste documentos en la colección users.
  • El schema activa timestamps, por lo que Mongoose maneja createdAt y updatedAt.
  • No define métodos de instancia, statics ni middleware propios en este archivo.

Gotchas & edge cases

[!WARNING] phoneVerifiedAt es el único dato persistido para representar verificación telefónica. No existe un booleano paralelo en el schema, y las lecturas públicas deben derivar el estado desde ese campo.

[!WARNING] Las cuentas Google-only pueden omitir passwordHash. auth.service.js comprueba su presencia antes de comparar contraseñas, para no revelar el método de alta.

[!IMPORTANT] accountStatus gobierna el ciclo de cierre de cuenta. Los jobs de restauración, anonimización y purga leen y mutan este estado junto con deletionRequestedAt, anonymizationScheduledAt, purgeScheduledAt y retentionContact.

[!NOTE] retentionContact es temporal. El flujo de purga lo borra junto con phoneVerifiedAt, contactGate y google.

[!CAUTION] email, phone, username y google.sub dependen de índices de unicidad. Los flujos de alta y verificación deben respetar esas restricciones para evitar duplicados.

services/api/src/modules/auth/auth.service.js consume User para sanitizar perfiles y exponer campos derivados como hasPassword, hasGoogleAccount, publicProfileActive y phoneVerifiedAt.

services/api/src/modules/listing-comments/listing-comments.routes.js carga solo deletionGracePreferences y accountStatus cuando necesita aplicar la política de comentarios del propietario.

services/api/src/scripts/sendPurgeWarnings.js consulta accountStatus y purgeScheduledAt para identificar cuentas próximas a purga.

services/api/src/modules/auth/account-export.service.js usa User para habilitar exportaciones de actividad de cuenta durante el ciclo de cierre.

services/api/src/scripts/purgeExpiredAccounts.js limpia datos sensibles del usuario, incluyendo emailVerifiedAt, phoneVerifiedAt, contactGate y google.

services/api/src/modules/auth/phone-verification.client.js y el servicio de verificación telefónica trabajan con phoneVerifiedAt como señal persistida de aprobación.

services/api/src/modules/auth/google-identity.client.js y los flujos de Google Auth usan google.sub como identidad estable del proveedor.