Gestión de cuenta — Referencia técnica
Dónde vive esto
Backend
apps/backend/graphql/resolvers/user.resolver.jsya no existe — el resolver monolítico se dividió en varios archivosuser-*.resolver.js.accountStatus,breakStatus,getAccountAnalytics,deleteAccount/deleteAccountImmediately/requestAccountDeletion,deactivateAccount/reactivateAccount, ytakeBreak/endBreakestán todos conectados enapps/backend/graphql/resolvers/user-account-management.resolver.js.apps/backend/graphql/types/account-management.type.js— solo declaraAccountDeletionRequest/EmailVerificationStatus; las consultas/mutaciones reales (deactivateAccount,reactivateAccount,deleteAccount,deleteAccountImmediately,requestAccountDeletion,cancelAccountDeletion,sendEmailVerification,resendEmailVerification,verifyEmail) viven engraphql/types/user.type.jsapps/backend/managers/user-managers/account-management.manager.js— lógica de negocio para desactivación, reactivación, eliminación programada/inmediata, y modo de descansoapps/backend/managers/user-managers/email-verification.manager.js— generación de tokens de verificación de email y flujo de validaciónapps/backend/managers/user-managers/activity-tracking.manager.js— estadísticas de actividad, mapa de calor de actividad, analítica de cuenta, estadísticas de dispositivosapps/backend/managers/user-managers/privacy-settings.manager.js— restablecer configuración de privacidad/notificaciones y recomendaciones de privacidadapps/backend/managers/user-managers/memorialized-accounts.manager.js— solicitudes de memorialización, aprobación/rechazo por parte de un administrador, asignación de contacto heredado, gestión de prueba de fallecimiento; persiste directamente en la fila del usuario fallecido (sin una tabla de solicitudes separada)apps/backend/graphql/types/memorialization.type.js+apps/backend/graphql/resolvers/memorialization.resolver.js— mutaciónrequestMemorializationorientada al usuario y consultamemorializationRequestStatusapps/backend/graphql/types/admin/memorialization-admin.type.js+apps/backend/graphql/resolvers/admin/memorialization-admin.resolver.js— cola de revisión para administradores (adminGetMemorializationRequests,adminReviewMemorialization), restringida al permisoMODERATE_CONTENTapps/backend/services/email.service.js— reexportación delgada retrocompatible; la implementación real ahora vive enapps/backend/services/email/(una fachada de proveedor, con AWS SES como proveedor predeterminado, seleccionable medianteEMAIL_PROVIDER)
Frontend
apps/frontend-nextjs/src/page-components/settings/AccountManagementPage.tsx(enrutado en/settings/account) —accountStatus,deactivateAccount,reactivateAccount,requestAccountDeletion/cancelAccountDeletion,deleteAccountImmediately, y eltakeBreakdel modo de descanso (con unautoReplyMessageopcional)/endBreakapps/frontend-nextjs/src/page-components/settings/EmailVerificationPage.tsx(enrutado en/settings/email-verification) — muestraisEmailVerified, llama aresendEmailVerification, y procesa un enlace?token=recibido por correo al montarse medianteverifyEmailapps/frontend-nextjs/src/page-components/settings/ActivityInsightsPage.tsx(enrutado en/settings/activity-insights) — tarjetas de estadísticas degetUserActivityStatsmás un mapa de calor de contribuciones degetActivityHeatmapapps/frontend-nextjs/src/page-components/settings/PrivacyCheckupPage.tsx(enrutado en/settings/privacy-checkup) — lista degetPrivacyRecommendationscon un botónrefreshRecommendationsapps/frontend-nextjs/src/page-components/settings/MemorializationPage.tsx(enrutado en/settings/memorialization) — envíarequestMemorializationapps/frontend-admin/src/app/moderation/memorialization/page.tsx(enrutado en/moderation/memorialization) — cola de revisión para administradores, llama aadminGetMemorializationRequestsyadminReviewMemorialization
Checklist de implementación técnica
-
accountStatus— resolver conectado enuser-account-management.resolver.js; consultado porAccountManagementPage.tsx -
resendEmailVerification/verifyEmail— resolvers conectados enuser-profile.resolver.js;EmailVerificationPage.tsxllama aresendEmailVerificationy procesa un enlace?token=recibido por correo medianteverifyEmail.sendEmailVerificationen sí todavía no tiene ningún llamador en el frontend. -
deactivateAccount— resolver conectado;handleDeactivatedeAccountManagementPage.tsxlo llama -
reactivateAccount— resolver conectado enuser-account-management.resolver.js; el flujo de "Reactivar cuenta" deAccountManagementPage.tsxlo llama (confirmado con contraseña) -
requestAccountDeletion/cancelAccountDeletion— resolvers conectados; ambos se llaman desdeAccountManagementPage.tsx -
deleteAccount— resolver conectado enuser-account-management.resolver.js; a pesar de su nombre, esto solo finaliza una eliminación ya programada porrequestAccountDeletion— lanza un error a menos que la cuenta esté enpending_deletionydeletionScheduledAtya haya pasado. No tiene ningún llamador en el frontend en ninguna parte de la app. -
deleteAccountImmediately— resolver conectado; el botón "Eliminar permanentemente ahora" deAccountManagementPage.tsxlo llama directamente, omitiendo por completo el período de gracia y el requisito depending_deletion -
takeBreak/endBreak(modo de descanso) — resolvers conectados;AccountManagementPage.tsxllama atakeBreakcondurationDays -
autoReplyMessageen el modo de descanso — el flujo de "Tomar un descanso" deAccountManagementPage.tsxtiene un campo de texto opcional para la respuesta automática, enviado comoautoReplyMessageen elinputdetakeBreak -
resetPrivacySettings(PrivacySettings!, sin argumentos) — reexpuesto en el esquema. Antes se había eliminado por completo (la declaración anterior vivía engraphql/types/privacy-settings.type.js, duplicaba unPrivacySettingsInputcon el mismo nombre proveniente deuser.type.js, y en el proceso corrompía el input real deupdatePrivacySettings— ver el comentario que sigue en ese archivo). Ahora está declarado una sola vez, correctamente, engraphql/types/user.type.jsy conectado enuser-privacy.resolver.js, que llama al método del manager que siempre existió (userManager.resetPrivacySettings→privacySettingsManager.resetPrivacySettings) y remapea su resultado en snake_case de la misma forma queprivacySettings/updatePrivacySettings.AccountPrivacyPage.tsxahora tiene un botón "Restablecer valores predeterminados" (con confirmación) conectado a esta mutation. -
resetNotificationSettings— resolver conectado enuser-notifications.resolver.js; el botón "Restablecer configuración de notificaciones a los valores predeterminados" de la página de Notificaciones (NotificationsSettingsPage.tsx) lo llama -
getUserActivityStats/getActivityHeatmap— resolvers conectados enuser-activity-tracking.resolver.js;ActivityInsightsPage.tsxllama a ambos (tarjetas de estadísticas más un mapa de calor de contribuciones estilo GitHub de la actividad de publicación). Este es un conjunto de datos distinto delusageStatsde Configuración → Gestión del tiempo (TimeManagementPage.tsx), que está implementado por separado mediantetime-management.resolver.js -
getAccountAnalytics/getDeviceStats— resolvers conectados (user-account-management.resolver.jsyuser-sessions.resolver.jsrespectivamente); sin llamador en el frontend -
getPrivacyRecommendations/refreshRecommendations— resolvers conectados enuser-privacy.resolver.jsyuser-search-discovery.resolver.jsrespectivamente;PrivacyCheckupPage.tsxllama a ambos - Cuentas memorializadas — ahora existe la superficie completa de GraphQL:
requestMemorialization+memorializationRequestStatus(graphql/resolvers/memorialization.resolver.js), consumidas porMemorializationPage.tsx;adminGetMemorializationRequests+adminReviewMemorialization(graphql/resolvers/admin/memorialization-admin.resolver.js, restringidas al permisoMODERATE_CONTENT), consumidas por la página/moderation/memorializationdel panel de administración. Ambas delegan enmemorialized-accounts.manager.js, que persiste la solicitud directamente en la fila del usuario fallecido (memorializationStatus/memorializationRequestedBy/memorializationProof) en lugar de una tabla separada. Los resolvers de campo de solo lectura enUser(memorializedBy,memorializationRequestedAt,memorializationProof,legacyContactId,legacyContactAddedAtenuser-fields.resolver.js, todosselfOnly()) se mantienen además de las mutaciones.
Estado de la cuenta
accountStatus devuelve el estado actual de la cuenta del usuario autenticado de un vistazo. Úsalo en la pantalla principal de configuración para avisar al usuario si su cuenta está suspendida (isSuspended: true) y mostrar el motivo y la fecha de vencimiento.
query AccountStatus {
accountStatus {
isActive isDeactivated isSuspended isDeleted
suspensionReason suspensionExpires
lastActivity accountType
}
}
Valores de accountType: personal, creator, business.
Verificación de email
sendEmailVerification envía un correo de verificación mediante AWS SES a la dirección registrada del usuario. resendEmailVerification hace lo mismo pero aplica un período de espera para evitar spam de correos. verifyEmail valida el token de un solo uso del enlace del correo y marca la cuenta como verificada.
mutation SendEmailVerification { sendEmailVerification { success message } }
mutation ResendEmailVerification { resendEmailVerification { success message } }
mutation VerifyEmail($token: String!) { verifyEmail(token: $token) { success message } }
Desactivar y reactivar
La desactivación oculta la cuenta a otros usuarios y la elimina de los resultados de búsqueda sin borrar ningún dato. El contenido del usuario se conserva. Se requiere password como medida de confirmación.
reactivateAccount restaura la visibilidad de inmediato — no se necesita aprobación de un administrador.
# Ocultar la cuenta sin eliminar datos (reversible)
mutation Deactivate($password: String!) {
deactivateAccount(password: $password) { success message }
}
# Restaurar una cuenta desactivada
mutation Reactivate($password: String!) {
reactivateAccount(password: $password) { success message }
}
Eliminación de cuenta
La eliminación es un proceso de dos pasos para dar a los usuarios la oportunidad de reconsiderarlo. requestAccountDeletion programa la eliminación para una fecha futura (normalmente 30 días después) y devuelve un timestamp scheduledDeletion. Durante esta ventana, cancelAccountDeletion puede abortar el proceso. Una vez que el período de gracia ha transcurrido, deleteAccount la finaliza — el resolver rechaza la llamada a menos que la cuenta ya esté en pending_deletion y su deletionScheduledAt ya haya pasado, así que a pesar del nombre no es un mecanismo manual para omitir el proceso. deleteAccountImmediately es el verdadero mecanismo para omitirlo: se salta por completo el período de gracia y el requisito de pending_deletion, verificando solo la contraseña antes de anonimizar la cuenta de inmediato — esto es lo que impulsa la opción "Eliminar permanentemente ahora" de Configuración → Gestión de cuenta.
# Paso 1 — programar la eliminación (el período de gracia permite cancelar)
mutation RequestDeletion($password: String!) {
requestAccountDeletion(password: $password) { success message }
}
# Cancelar antes de la fecha de eliminación programada
mutation CancelDeletion {
cancelAccountDeletion { success message }
}
# Finalizar una eliminación después de que el período de gracia haya transcurrido (falla en caso contrario)
mutation DeleteAccount($password: String!) {
deleteAccount(password: $password) { success message }
}
# Omitir el período de gracia por completo — eliminación inmediata y permanente
mutation DeleteAccountImmediately($password: String!) {
deleteAccountImmediately(password: $password) { success message }
}
AccountDeletionRequest (devuelto por las consultas de estado) incluye scheduledDeletion, reason y canCancel.
Modo de descanso
Los usuarios pueden poner su cuenta en un descanso temporal sin desactivarla. Consulta Perfil de usuario → Modo de descanso para la API completa. breakStatus devuelve si hay un descanso activo, su ventana de inicio/fin, y el autoReplyMessage que se envía a cualquiera que le escriba al usuario durante el descanso.
query BreakStatus {
breakStatus {
isOnBreak breakStart breakEnd breakType autoReplyMessage
}
}
Configuración de privacidad
resetNotificationSettings revierte todas las preferencias de notificaciones a los valores predeterminados de la plataforma en una sola llamada — respalda el botón "Restablecer configuración de notificaciones a los valores predeterminados" de la página de Notificaciones. resetPrivacySettings (PrivacySettings!, sin argumentos) hace lo equivalente para la configuración de privacidad; está reexpuesto en el esquema (user.type.js) y conectado en user-privacy.resolver.js, llamando a la lógica de privacySettingsManager.resetPrivacySettings que había existido inalcanzable desde que se eliminó la mutation — ver el checklist anterior. El botón "Restablecer valores predeterminados" de AccountPrivacyPage.tsx ya lo llama. setAllNotifications sigue eliminado del schema/resolvers (solo queda un método huérfano del manager, inalcanzable vía GraphQL); markAllNotificationsAsRead y deleteAllNotifications (graphql/types/notification.type.js) son las operaciones masivas de notificaciones más cercanas que sobreviven, pero actúan sobre notificaciones existentes en lugar de alternar configuraciones.
# Reset all notification preferences to defaults
mutation ResetNotifications { resetNotificationSettings }
# Reset all privacy settings to defaults
mutation ResetPrivacy { resetPrivacySettings { isPrivate showActivityStatus allowMessagesFrom } }
Actividad y presencia
getUserActivityStats devuelve el resumen de uso de la plataforma del usuario para un período de tiempo dado (day, week, month). totalTime está en segundos; mostActiveHour va de 0 a 23. activityStreak es el número de días consecutivos con actividad. userId es obligatorio y debe ser el id del propio llamador, a menos que el llamador sea un administrador.
getActivityHeatmap devuelve una cuadrícula de series de tiempo ({ timeframe, days, total, maxCount, cells: [{date, count}] }, calculada a partir de las publicaciones del usuario por día) apta para renderizar un mapa de calor de contribuciones estilo GitHub — ActivityInsightsPage.tsx lo renderiza exactamente así. getAccountAnalytics ofrece una visión más amplia del engagement y getDeviceStats desglosa el uso por tipo de dispositivo; ninguno de los dos tiene todavía un llamador en el frontend.
query ActivityStats($userId: ID!, $timeframe: String) {
getUserActivityStats(userId: $userId, timeframe: $timeframe) {
totalTime # seconds
activeDays
mostActiveHour # 0-23
activityStreak # consecutive days
lastActivity
}
}
query ActivityHeatmap($timeframe: String) { getActivityHeatmap(timeframe: $timeframe) }
query AccountAnalytics($timeframe: String) { getAccountAnalytics(timeframe: $timeframe) }
query DeviceStats($timeframe: String) { getDeviceStats(timeframe: $timeframe) }
Recomendaciones de privacidad
getPrivacyRecommendations devuelve una lista clasificada de mejoras de privacidad sugeridas según la configuración actual del usuario. Cada recomendación tiene una priority (high, medium, low) y un indicador isApplied que muestra si el usuario ya la adoptó. refreshRecommendations (resolver en user-search-discovery.resolver.js, devuelve Boolean!) las vuelve a calcular. PrivacyCheckupPage.tsx llama a ambos.
query PrivacyRecommendations {
getPrivacyRecommendations {
id recommendationType title description priority isApplied
}
}
mutation RefreshRecommendations { refreshRecommendations }
Cuentas memorializadas
Cualquier usuario autenticado puede enviar una solicitud para memorializar la cuenta de un usuario fallecido; la aprobación es una acción exclusiva de administradores en un schema de administración separado. La solicitud se almacena directamente en la fila del usuario fallecido en lugar de en su propia tabla — memorializationStatus (none/pending/approved/rejected), memorializationRequestedBy, memorializationRequestedAt, y un blob JSON memorializationProof (prueba de fallecimiento, identificación del solicitante, relación, etc.). Las cuentas aprobadas obtienen isMemorialized: true y, a menos que el administrador lo desactive, showMemorialBanner: true, que PublicProfilePage.tsx usa para renderizar un banner "En memoria".
# User-facing: submit a request
mutation RequestMemorialization($input: MemorializationRequestInput!) {
requestMemorialization(input: $input) {
success message requestId estimatedReviewTime submittedAt
}
}
query MemorializationRequestStatus($deceasedUserId: ID!) {
memorializationRequestStatus(deceasedUserId: $deceasedUserId) {
hasRequest canSubmitNew
request { status submittedAt reviewedAt rejectionReason }
}
}
# Admin-only: review queue (requires the MODERATE_CONTENT permission)
query AdminMemorializationQueue($limit: Int, $offset: Int) {
adminGetMemorializationRequests(limit: $limit, offset: $offset) {
totalCount
requests { deceasedUserId deceasedUser { username } status requestedBy requestedAt relationship proof }
}
}
mutation AdminReviewMemorialization($deceasedUserId: ID!, $decision: String!) {
adminReviewMemorialization(deceasedUserId: $deceasedUserId, decision: $decision) {
success message status reviewedAt
}
}