Perfil de Usuario — Referencia Técnica
Dónde vive esto
Backend
apps/backend/graphql/resolvers/user.resolver.jsse dividió en varios archivos por área temática, todos cargados automáticamente y fusionados porgraphql/resolvers.js(loadFilesSync+mergeResolvers), por lo que ya no existe un único archivo de "resolver de usuario":user-profile.resolver.js—me,user,userByUsername,updateProfile,isEmailVerifieduser-social.resolver.js—userStats,relationshipStatus, mutaciones de seguir/bloquear/silenciar/amigos cercanos,updateInterestsuser-search-discovery.resolver.js—searchUsers,suggestedUsers,interestSuggestions,getTrendingUsers,getRecommendedUsersuser-activity-tracking.resolver.js—getOnlineUsersy el seguimiento de presencia relacionadouser-account-management.resolver.js— desactivar/reactivar/eliminar cuenta, modo pausauser-privacy.resolver.js—updatePrivacySettingsuser-mentions-tags.resolver.js—tagPermissions,updateTagPermissions,removeTag,taggedInPostsuser-favorites.resolver.jsyuser-bulk-operations.resolver.js— favoritos y operaciones masivas de seguir/bloquear/silenciar
apps/backend/graphql/types/user.type.js— esquema GraphQLUsery campos principales del perfilapps/backend/graphql/types/user-features.type.js— tiposTagPermissions,OnlineUser,TrendingUsery sus consultas/mutaciones relacionadasapps/backend/graphql/types/social-actions.type.js—BulkOperationResult/MuteActionResponsey las mutacionesbulkBlockUsers/bulkRemoveFollowers/bulkProcessFollowRequests(el resto del esquema de seguir/bloquear/silenciar/amigos cercanos/favoritos ahora vive enuser.type.jsyfavorites.type.js)apps/backend/graphql/types/appeal.type.js/appeal.resolver.js— esquema y resolvers de apelación de restricciones, que reemplazan los antiguos stubs rotos que solían estar encontacts-validation.type.jsapps/backend/graphql/types/social-link.type.js/social-link.resolver.js— el esquema GraphQLSocialLinky sus resolvers, ahora en funcionamientoapps/backend/data-access-services/user/user.access-service.js— consultas de base de datos deUserapps/backend/data-access-services/user/social-link.access-service.js— persistencia de enlaces sociales, usada pormanagers/user-managers/social-link.manager.jsy expuesta por elsocial-link.resolver.jsmencionado arribaapps/backend/managers/user-managers/profile-view.manager.js— registra las visitas al perfil (omitiendo las autovisitas y respetando el interruptor de privacidadhideProfileVisits) y lista los visitantes de un usuario, sin duplicadosapps/backend/data-access-services/user/user-profile-view.access-service.js— registro de solo escriturauser_profile_viewcon un debounce de 60s para visitas repetidas del mismo visitanteapps/backend/graphql/types/user-profile-view.type.js/user-profile-view.resolver.js— queriesmyProfileViewers/myProfileViewerCounty mutationsrecordProfileVisit/clearMyProfileViewers
Frontend
apps/frontend-nextjs/src/page-components/ProfilePage.tsx— la página de perfil propio del usuario autenticadoapps/frontend-nextjs/src/page-components/PublicProfilePage.tsx— visualización del perfil de otro usuario (acciones de seguir/mensaje/bloquear)apps/frontend-nextjs/src/page-components/settings/EditProfilePage.tsx— interfaz de edición de perfil, llama aupdateProfileapps/frontend-nextjs/src/app/profile/page.tsx— ruta/profileapps/frontend-nextjs/src/app/[username]/page.tsx— ruta pública/[username]apps/frontend-nextjs/src/page-components/settings/ProfileViewersPage.tsx— listamyProfileViewers, enlazado desde el menú del perfil propio (ProfilePage.tsx) en/settings/profile-viewersapps/frontend-nextjs/src/page-components/PublicProfilePage.tsx— dispararecordProfileVisital montar cuando se ve el perfil de otra persona
Checklist de implementación técnica
-
me— resolver conectado enuser-profile.resolver.js; mostrado porProfilePage.tsx -
updateProfile— mutation conectada;handleSavedeEditProfilePage.tsxla invoca en el botón "Guardar cambios" (este documento afirmaba antes que no existía un controladoronClick— corregido) -
userByUsername— resolver conectado; mostrado porPublicProfilePage.tsx - Insignia
isVerified— se muestra junto al nombre de usuario cuandoisVerifiedes verdadero, tanto enProfilePage.tsxcomo enPublicProfilePage.tsx - Enlaces sociales (
addSocialLink/updateSocialLink/deleteSocialLink/mySocialLinks/userSocialLinks) — ahora en funcionamiento de extremo a extremo:social-link.resolver.js+social-link.type.jsexponen el esquema sobresocial-link.manager.js/social-link.access-service.js, ySocialLinksPage.tsx/PublicProfilePage.tsxlo consumen (este documento afirmaba antes que no existía exposición de GraphQL — corregido) -
updatePrivacySettings— mutation conectada;AccountPrivacyPage.tsxla llama medianteuseMutationcon controladores reales para los interruptores (este documento afirmaba antes que eran elementos estáticos de relleno — corregido; la página se desarrolló sustancialmente durante la revisión de Configuración, ver Configuración) -
followUser/unfollowUser— mutations conectadas; el botón de seguir dePublicProfilePage.tsxllama ahandleFollowClick, que las invoca (este documento afirmaba antes que no existía un controladoronClick— corregido) - Visitantes del perfil —
PublicProfilePage.tsxdispararecordProfileVisital montar;ProfileViewersPage.tsxlistamyProfileViewers; el interruptorhideProfileVisitsenAccountPrivacyPage.tsxhace querecordVisitsea un no-op para las visitas de ese usuario a otros perfiles
Campos del modelo User
Identidad
| Campo | Descripción |
|---|---|
username | Nombre de usuario único |
firstName / lastName | Nombre completo |
bio | Biografía del perfil |
profilePicture | URL de la foto de perfil |
coverPhoto | URL de la foto de portada |
dateOfBirth / gender | Información demográfica |
accountType | Tipo de cuenta (personal, creator, etc.) |
Estado de la cuenta
| Campo | Descripción |
|---|---|
accountStatus | Estado actual de la cuenta |
isVerified / verificationStatus | Insignia de verificación |
verificationCategory | Categoría de la insignia (por ejemplo, creator, business) |
isPrivate | Cuenta privada (requiere aprobación de seguidor) |
isEmailVerified / isPhoneVerified | Verificación de contacto |
isOnboardingCompleted / onboardingStep | Progreso de incorporación |
Actividad
| Campo | Descripción |
|---|---|
isOnline | Actualmente en línea |
lastActiveAt | Marca de tiempo de la última vez visto |
lastPostAt | Fecha de la última publicación |
engagementScore | Puntaje de interacción calculado |
locale / timezone | Configuración regional |
Seguridad
| Campo | Descripción |
|---|---|
twoFactorEnabled | 2FA habilitado |
securityNotificationsEnabled | Alertas de seguridad activas |
isTermsConditionsAccepted | Términos y condiciones aceptados |
Restricciones y suspensión
| Campo | Descripción |
|---|---|
suspensionCount / suspendedAt | Historial de suspensiones |
isRestricted / restrictionTypes | Restricciones activas |
tagApprovalRequired | Debe aprobar etiquetas antes de que aparezcan |
autoRemoveTags | Elimina automáticamente las etiquetas del contenido |
Modo pausa
Permite pausar temporalmente la cuenta:
| Campo | Descripción |
|---|---|
isOnBreak | Actualmente en pausa |
breakStartedAt / breakEndsAt | Ventana de la pausa |
breakDurationDays | Duración planificada |
scheduledBreakStartsAt | Pausa programada futura |
autoReplyMessage | Respuesta automática enviada a cualquiera que envíe un mensaje durante la pausa |
Memorialización
Para cuentas de usuarios fallecidos:
| Campo | Descripción |
|---|---|
isMemorialized / memorializedAt | Estado de memorialización |
memorializationStatus | Estado del proceso |
legacyContactId | Contacto de legado designado |
allowTributes | Permitir publicaciones de tributo |
allowLegacyContactPosts | El contacto de legado puede publicar |
showMemorialBanner | Mostrar el banner de memorial en el perfil |
Consultas de perfil
me devuelve al usuario autenticado completo — se usa al iniciar la app para hidratar el contexto del usuario actual. user y userByUsername obtienen datos de perfil público de cualquier cuenta. searchUsers es el autocompletado de la barra de búsqueda. suggestedUsers devuelve recomendaciones de seguimiento personalizadas para la pantalla de inicio.
query Me { me { id username bio profilePicture coverPhoto isVerified isPrivate twoFactorEnabled } }
query User($id: ID!) { user(id: $id) { id username } }
query UserByUsername($username: String!) { userByUsername(username: $username) { id username bio isVerified } }
query SearchUsers($query: String!, $limit: Int) { searchUsers(query: $query, limit: $limit) { id username profilePicture } }
query SuggestedUsers($limit: Int) { suggestedUsers(limit: $limit) { id username profilePicture } }
Disponibilidad de nombre de usuario / correo electrónico
Estas dos consultas se llaman en tiempo real mientras el usuario escribe durante el registro o la edición de perfil. isUsernameAvailable devuelve un message explicando por qué el nombre de usuario está en uso (por ejemplo, "Ya está en uso").
query CheckUsername($username: String!) { isUsernameAvailable(username: $username) { available message } }
query CheckEmail($email: String!) { isEmailAvailable(email: $email) { available message } }
Estadísticas y relación
userStats devuelve contadores (seguidores, seguidos, amigos cercanos) y las banderas de relación entre quien llama y el usuario objetivo — se usa para renderizar el estado del botón seguir/mensaje en un perfil.
relationshipStatus es un objeto de relación más detallado que también incluye canMessage y canViewPosts — se usa al renderizar un perfil para saber si mostrar el botón "Mensaje" y si las publicaciones son accesibles.
query UserStats($userId: ID!) {
userStats(userId: $userId) {
followersCount followingCount closeFriendsCount
isFollowing isFollower isCloseFriend isBlocked isBlockedBy hasPendingRequest
}
}
query RelationshipStatus($targetUserId: ID!) {
relationshipStatus(targetUserId: $targetUserId) {
isSelf isFollowing isFollower isBlocked isMuted isCloseFriend
hasPendingRequest isMutualFollowing canMessage canViewPosts
}
}
Acciones sociales
followUser siempre crea la relación de seguimiento de inmediato (no verifica isPrivate) — está pensado para llamantes que ya saben que el objetivo es público, por ejemplo, el botón de seguimiento rápido de SuggestedUsers.tsx. sendFollowRequest es el punto de entrada que sí tiene en cuenta las cuentas privadas: internamente sigue de inmediato si el objetivo es público, o crea una solicitud pendiente si es privado, y su respuesta incluye requestRequired: true en este último caso. handleFollowClick de PublicProfilePage.tsx elige entre ambas usando el user.isPrivate ya cargado.
Nota: el manager de sendFollowRequest en realidad devuelve claves en snake_case (request_required/request_id), mientras que el esquema declara requestRequired en camelCase y no nulo — seleccionarlo provoca un error "Cannot return null for non-nullable field", por lo que PublicProfilePage.tsx deliberadamente solo selecciona success/message/requestId y depende de un refetch de relationshipStatus para conocer el estado real.
unfollowUser elimina la relación de seguimiento. blockUser también deja de seguir si existía un seguimiento. muteUser oculta el contenido del usuario de tu feed sin eliminar la relación de seguimiento. removeFollower elimina a alguien de tus seguidores sin bloquearlo.
mutation FollowUser($userId: ID!) { followUser(userId: $userId) { success } }
mutation UnfollowUser($userId: ID!) { unfollowUser(userId: $userId) { success } }
mutation BlockUser($userId: ID!) { blockUser(userId: $userId) { success } }
mutation MuteUser($userId: ID!) { muteUser(userId: $userId) { success } }
mutation RemoveFollower($id: ID!) { removeFollower(followerId: $id) { success } }
# Follow requests (private accounts) - do not select requestRequired, see note above
mutation SendRequest($userId: ID!) { sendFollowRequest(userId: $userId) { success requestId } }
mutation AcceptRequest($id: ID!) { acceptFollowRequest(requestId: $id) { success } }
mutation RejectRequest($id: ID!) { rejectFollowRequest(requestId: $id) { success } }
mutation CancelRequest($id: ID!) { cancelFollowRequest(requestId: $id) { success } }
# Close Friends
mutation AddCloseFriend($userId: ID!) { addToCloseFriends(userId: $userId) { success } }
mutation RemoveCloseFriend($userId: ID!) { removeFromCloseFriends(userId: $userId) { success } }
# Favorites (reorderable, separate from following)
mutation AddFavorite($userId: ID!) { addToFavorites(userId: $userId) { favorite { position } } }
mutation RemoveFavorite($userId: ID!) { removeFromFavorites(userId: $userId) { success } }
mutation ReorderFavorites($ids: [ID!]!) { reorderFavorites(userIds: $ids) { success } }
Operaciones masivas
Estas mutaciones procesan una lista de IDs de usuario en una sola llamada a la API. successCount y failedCount permiten al cliente mostrar un toast resumen sin verificar cada resultado individualmente.
mutation BulkFollow($ids: [ID!]!) { bulkFollowUsers(userIds: $ids) { successCount failedCount } }
mutation BulkUnfollow($ids: [ID!]!) { bulkUnfollowUsers(userIds: $ids) { successCount } }
mutation BulkBlock($ids: [ID!]!) { bulkBlockUsers(userIds: $ids) { processedCount } }
mutation BulkRemoveFollowers($ids: [ID!]!) { bulkRemoveFollowers(followerIds: $ids) { processedCount } }
mutation BulkProcessRequests($ids: [ID!]!, $action: String!) {
bulkProcessFollowRequests(requestIds: $ids, action: $action) { processedCount }
}
mutation BulkMute($ids: [ID!]!) { bulkMuteUsers(userIds: $ids) { success message results { success failed } } }
Permisos de etiquetas
Controla quién puede etiquetarte en publicaciones y si las etiquetas requieren tu aprobación antes de aparecer. requireApproval: true significa que las etiquetas quedan en cola para revisión. friendsOnly: true limita el etiquetado a seguidores mutuos. disabled: true impide todo etiquetado.
removeTag elimina una etiqueta existente de una de tus propias publicaciones y devuelve un Boolean. Aprobar o rechazar una etiqueta pendiente (una que quedó en cola porque requireApproval está activado) es un flujo aparte mediante pendingTagReviews/approvePostTag/rejectPostTag (más abajo); se planeó un par approveTag/rejectTag con el mismo nombre para este sistema genérico de permisos, pero nunca se conectó (updateTagPermissions y removeTag son las únicas mutaciones activas aquí).
type TagPermissions {
canBeTagged: Boolean!
requireApproval: Boolean!
friendsOnly: Boolean!
disabled: Boolean!
}
query TagPermissions { tagPermissions { canBeTagged requireApproval friendsOnly disabled } }
# input is a JSON blob (no dedicated TagPermissionsInput type), shaped like TagPermissions
mutation UpdateTagPermissions($input: JSON!) {
updateTagPermissions(input: $input) { canBeTagged requireApproval friendsOnly disabled }
}
mutation RemoveTag($tagId: ID!) { removeTag(tagId: $tagId) }
Revisar las etiquetas que otras personas te pusieron (a diferencia de editar tus propios permisos) es un par de mutaciones separado, expuesto por tag-review.resolver.js:
query PendingTagReviews { pendingTagReviews { id user { id username } media { id } positionX positionY createdAt } }
mutation ApprovePostTag($mentionId: ID!) { approvePostTag(mentionId: $mentionId) }
mutation RejectPostTag($mentionId: ID!) { rejectPostTag(mentionId: $mentionId) }
Intereses
Los usuarios seleccionan intereses durante la incorporación (completeOnboarding) o más tarde desde la configuración de su perfil. Los intereses potencian interestSuggestions y otros rankings de recomendación. A diferencia de un sistema respaldado por una taxonomía aparte, aquí los intereses son simplemente un arreglo de cadenas de texto libre almacenadas en User (columna interestTags, expuesta como User.interests) — no existe un tipo Interest con id/slug/icono en este flujo de GraphQL. updateInterests reemplaza toda la selección del usuario de una sola vez (máximo 50 entradas, recortadas y filtradas por el resolver).
query MyInterests { me { interests } }
mutation UpdateInterests($interests: [String!]!) {
updateInterests(interests: $interests) { id interests }
}
query InterestSuggestions($limit: Int, $interests: [String!]) {
interestSuggestions(limit: $limit, interests: $interests) { id username profilePicture }
}
Enlaces sociales
Los usuarios pueden vincular sus perfiles en otras plataformas. platform es una cadena como twitter, instagram o website. Estos enlaces se muestran en la tarjeta de perfil propio del usuario (SocialLinksPage.tsx en Configuración → Enlaces) y en los perfiles públicos (PublicProfilePage.tsx mediante userSocialLinks).
mutation AddSocialLink($input: CreateSocialLinkInput!) {
addSocialLink(input: $input) { id platform url title clickCount }
}
mutation UpdateSocialLink($id: ID!, $input: UpdateSocialLinkInput!) {
updateSocialLink(id: $id, input: $input) { id platform url isActive visibility }
}
mutation DeleteSocialLink($id: ID!) { deleteSocialLink(id: $id) }
mutation ReorderSocialLinks($ids: [ID!]!) { reorderSocialLinks(ids: $ids) }
mutation TrackSocialLinkClick($id: ID!) { trackSocialLinkClick(id: $id) { id clickCount } }
query MySocialLinks { mySocialLinks { id platform url title displayOrder clickCount } }
query UserSocialLinks($userId: ID!) { userSocialLinks(userId: $userId) { id platform url title } }
query SocialLinkStats($userId: ID) { socialLinkStats(userId: $userId) { totalLinks totalClicks visibleLinks hiddenLinks platforms } }
Apelaciones de restricción
Si una cuenta está suspendida o baneada (accountStatus), el usuario puede presentar una apelación contra ella mediante appealRestriction, siempre que no tenga ya una pendiente. myLatestAppeal devuelve la apelación más reciente del llamante, de cualquier estado (o null si nunca ha apelado); getAppealStatus busca una apelación específica por id, y el llamante debe ser su propietario.
mutation AppealRestriction($reason: String!) {
appealRestriction(reason: $reason) { id status submittedAt reason }
}
query GetAppealStatus($appealId: ID!) {
getAppealStatus(appealId: $appealId) { id status reason submittedAt reviewedAt decisionNotes }
}
query MyLatestAppeal { myLatestAppeal { id status reason submittedAt reviewedAt decisionNotes } }
Usuarios en tendencia
getTrendingUsers devuelve cuentas con un crecimiento rápido en su número de seguidores, ordenadas por trendingScore. trendingReason es una etiqueta legible para humanos como "Publicación viral" o "Cuenta verificada nueva" — se muestra como subetiqueta en la tarjeta de tendencia. El manager en sí devuelve un envoltorio { trendingUsers, timeframe, category, pagination }, que el resolver desenvuelve a la lista [TrendingUser!]! que declara el esquema.
query TrendingUsers($limit: Int, $offset: Int, $timeframe: String, $category: String) {
getTrendingUsers(limit: $limit, offset: $offset, timeframe: $timeframe, category: $category) {
user { id username profilePicture isVerified followersCount }
trendingScore growthRate trendingReason
}
}
Usuarios en línea
getOnlineUsers devuelve las cuentas que están actualmente en línea — útil para un widget de "Quién está en línea". lastSeen es la marca de tiempo del último latido (heartbeat) del usuario.
query OnlineUsers($limit: Int, $offset: Int) {
getOnlineUsers(limit: $limit, offset: $offset) {
user { id username profilePicture }
lastSeen isCurrentlyOnline activityStatus
}
}
Editar perfil
updateProfile actualiza parcialmente el perfil del usuario autenticado. Solo se actualizan los campos pasados en input.
mutation UpdateProfile($input: UserProfileUpdateInput!) {
updateProfile(input: $input) { id username bio profilePicture coverPhoto }
}
Gestión de cuenta
mutation DeactivateAccount($password: String!) { deactivateAccount(password: $password) { success } }
mutation ReactivateAccount($password: String!) { reactivateAccount(password: $password) { success } }
mutation RequestDeletion($password: String!) { requestAccountDeletion(password: $password) { success } }
mutation CancelDeletion { cancelAccountDeletion { success } }
mutation DeleteAccount($password: String!) { deleteAccount(password: $password) { success } }
# Skips the grace period requestAccountDeletion schedules and deletes right away
mutation DeleteAccountImmediately($password: String!) { deleteAccountImmediately(password: $password) { success } }
Visitantes del perfil
Cada vez que un usuario abre el perfil público de otra persona, el frontend dispara recordProfileVisit en segundo plano. El resolver nunca lanza un error al llamante — un fallo se silencia y devuelve false — de modo que un problema de registro nunca bloquea la carga del perfil. Las visitas se registran en un log de solo escritura (user_profile_view, modelado a partir de user_post_view) y se eliminan duplicados: una visita repetida del mismo visitante dentro de 60 segundos no crea una nueva fila, y getMyViewers además elimina duplicados por visitante, de modo que alguien que ha visitado muchas veces solo aparece una vez, con la hora de su visita más reciente.
Las visitas a tu propio perfil nunca se registran. Si el visitante tiene activado hideProfileVisits (ver Configuración de privacidad), recordVisit no hace nada silenciosamente — la lista del usuario visitado simplemente no incluirá a ese visitante, sin ningún error visible para ninguna de las dos partes.
mutation RecordProfileVisit($targetUserId: ID!) { recordProfileVisit(targetUserId: $targetUserId) }
query MyProfileViewers($limit: Int, $offset: Int) {
myProfileViewers(limit: $limit, offset: $offset) {
viewer { id username profilePicture isVerified }
viewedAt
}
}
query MyProfileViewerCount { myProfileViewerCount }
mutation ClearMyProfileViewers { clearMyProfileViewers }