Moderadores de Cuenta — Referencia Técnica
← Volver a Moderadores de Cuenta
Dónde vive esto
Backend
apps/backend/database/models/AccountModerator.js— modelo (dueño/moderador + 9 flags de permisos)apps/backend/database/models/AccountModeratorAction.js— modelo de rastro de auditoríaapps/backend/database/migrations/20260721130000-create-account-moderator.js— tablaaccount_moderator(5 flags originales)apps/backend/database/migrations/20260721150000-expand-account-moderator.js— añadecan_comment/can_manage_comments/can_story/can_react+ creaaccount_moderator_actionapps/backend/data-access-services/moderator/account-moderator.access-service.js— CRUD +getByOwner/getByModeratorapps/backend/data-access-services/moderator/account-moderator-action.access-service.js—create+getByOwner(feed de auditoría, paginado, máximo 200)apps/backend/managers/user-managers/account-moderator.manager.js— puerta de 2FA (assertOwner2FA), reautenticación step-up (assertRecentAuth/verifyPasswordStepUp, respaldada porservices/step-up-auth.service.js— una "ventana sudo" de 15 minutos respaldada por Redis),canActFor,resolveActingUser,resolveAndLog,logAction,getModeratorActivityapps/backend/graphql/types/account-moderator.type.js/resolvers/account-moderator.resolver.js- Aplicación (
actAsUserId):post.resolver.js(createPostpost/clip, ycreateStory),post-interaction.resolver.js(likePost),live-stream.resolver.js,conversation.resolver.js,message.resolver.js,post-comment.resolver.js(createComment,deleteComment).createStoryahora aceptaactAsUserIdy aplicacanStory,likePostaplicacanReact, ydeleteCommentaplicacanManageComments— esta documentación antes decía que los tres estaban definidos pero nunca se comprobaban; corregido. - Atribución de mensajes: el resolver
Message.viaModeratorenmessage.resolver.jsleemetadata.viaModeratorId, visible solo para el remitente efectivo (el dueño).
Frontend
apps/frontend-nextjs/src/page-components/settings/ModeratorsPage.tsx— Configuración → Moderadores (gestión con puerta de 2FA, 9 interruptores de permisos, feedmoderatorActivity, solicitud de contraseña de reautenticación step-up anteREQUIRES_REAUTH)apps/frontend-nextjs/src/components/CreatePostModal.tsx— selector "Publicar como" (accountsIModerate→actAsUserId)apps/frontend-nextjs/src/components/PostModal.tsx— selector "Comentar como" encreateComment(accountsIModeratefiltrado porcanComment→actAsUserId)apps/frontend-nextjs/src/components/live/GoLiveModal.tsx— selector "actuar como" encreateLiveStream(accountsIModeratefiltrado porcanLive→actAsUserId)apps/frontend-nextjs/src/components/chat/ConversationList.tsx— selector "actuar como" en el flujo de chat/grupo nuevocreateConversation(accountsIModeratefiltrado porcanCreateChats→actAsUserId)apps/frontend-nextjs/src/components/chat/ChatView.tsx— selector "actuar como" en el compositor de mensajes (accountsIModeratefiltrado porcanSendMessages→actAsUserIden cada llamada asendMessage)
Checklist de implementación técnica
- tabla
account_moderator+ modelo + asociaciones (owner,moderator) — 9 flags de permisos:canPost,canClip,canLive,canStory,canCreateChats,canSendMessages,canComment,canReact,canManageComments - tabla
account_moderator_action+ modelo — rastro de auditoría de cada acción realizada en nombre de un dueño -
addModerator/updateModeratorPermissions— requierenowner.twoFactorEnabled(lanzanmoderators.requires_2fade lo contrario) y una ventana de reautenticación step-up reciente (lanzanmoderators.requires_reauth/REQUIRES_REAUTHde lo contrario) -
verifyPasswordForModerators(password)— reverifica la contraseña del dueño y abre una nueva ventana step-up de 15 minutos -
removeModerator— no requiere 2FA ni step-up - consultas
myModerators/accountsIModerate/moderatorActivity - aplicación de
resolveActingUser(callerId, actAsUserId, action)encreatePost(post/clip),createStory,likePost,createLiveStream,createConversation,sendMessage,createComment,deleteComment -
logActionescribe en el rastro de auditoría cada vez que el usuario efectivo difiere del que llama (best-effort, nunca lanza error) -
Message.viaModerator— atribución visible solo para el dueño de quién realmente envió un mensaje enviado "como" él - "Publicar como" en el compositor de posts, "Comentar como" en el cuadro de comentarios
- Aplicación de
canStory—createStoryaceptaactAsUserIdy llama aresolveActingUser(..., 'story', ...); esta documentación antes decía quecanStoryestaba definido de punta a punta pero nunca se aplicaba. Corregido. - Aplicación de
canReact/canManageComments—likePost(post-interaction.resolver.js) llama aresolveActingUser(..., 'react', ...)ydeleteComment(post-comment.resolver.js) llama aresolveActingUser(..., 'manage_comment', ...); esta documentación antes decía que ningún resolver comprobaba ninguno de los dos permisos. Corregido. - Disparador de UI de "actuar como" en los compositores de en vivo / nuevo chat / mensajes —
GoLiveModal.tsx(filtrado porcanLive),ConversationList.tsx(filtrado porcanCreateChats) yChatView.tsx(filtrado porcanSendMessages) ahora exponen un selector de "actuar como" conectado aactAsUserId, el mismo patrón que ya usabanCreatePostModal/PostModal; esta documentación antes decía que en la UI solo estaban conectados posts/clips/comentarios. Corregido.
GraphQL
type ModeratorPermissions {
canPost: Boolean! canClip: Boolean! canLive: Boolean! canCreateChats: Boolean! canSendMessages: Boolean!
canComment: Boolean! canManageComments: Boolean! canStory: Boolean! canReact: Boolean!
}
type AccountModerator {
id: ID!
owner: User
moderator: User
canPost: Boolean! canClip: Boolean! canLive: Boolean! canCreateChats: Boolean! canSendMessages: Boolean!
canComment: Boolean! canManageComments: Boolean! canStory: Boolean! canReact: Boolean!
createdAt: DateTime! updatedAt: DateTime!
}
input ModeratorPermissionsInput {
canPost: Boolean canClip: Boolean canLive: Boolean canCreateChats: Boolean canSendMessages: Boolean
canComment: Boolean canManageComments: Boolean canStory: Boolean canReact: Boolean
}
"A single entry in the account-moderator audit trail (who did what, when)."
type ModeratorAction {
id: ID!
moderator: User
action: String! # post | clip | live | story | create_chat | send_message | comment | manage_comment | react
actedAs: String! # owner | self
targetType: String # post | live_stream | conversation | message | comment
targetId: ID
metadata: JSON
createdAt: DateTime!
}
type Query {
myModerators: [AccountModerator!]!
accountsIModerate: [AccountModerator!]!
moderatorActivity(limit: Int, offset: Int, action: String, moderatorUserId: ID): [ModeratorAction!]!
}
type Mutation {
addModerator(userId: ID!, permissions: ModeratorPermissionsInput!): AccountModerator! # 2FA + step-up required
updateModeratorPermissions(userId: ID!, permissions: ModeratorPermissionsInput!): AccountModerator! # 2FA + step-up required
removeModerator(userId: ID!): Boolean!
verifyPasswordForModerators(password: String!): Boolean! # step-up re-auth
}
# The create/send inputs gain: actAsUserId: ID
# PostCreateInput / StoryCreateInput / LiveStreamCreateInput / ConversationCreateInput / MessageCreateInput / PostCommentCreateInput
# likePost(postId, interactionType, actAsUserId) and deleteComment(commentId, actAsUserId) take actAsUserId
# as a plain argument (not via an input type)
Mapa de permisos → acción
| Permiso | Acción | Resolver |
|---|---|---|
canPost | post regular | createPost |
canClip | clip de un solo video | createPost (clip detectado) |
canLive | iniciar un en vivo | createLiveStream |
canCreateChats | crear una conversación | createConversation |
canSendMessages | enviar un mensaje | sendMessage |
canComment | comentar / responder en un post | createComment (post-comment.resolver.js) |
canStory | publicar una historia | createStory |
canReact | reaccionar/dar like | likePost (post-interaction.resolver.js) |
canManageComments | moderar/eliminar un comentario | deleteComment (post-comment.resolver.js) |
Configuración
Ejecuta 20260721130000-create-account-moderator y 20260721150000-expand-account-moderator, luego reinicia el backend. No hay variables de entorno nuevas. El dueño debe habilitar 2FA (Configuración → Seguridad) antes de otorgar acceso de moderador; añadir/actualizar moderadores también requiere una ventana step-up reciente (services/step-up-auth.service.js, respaldada por Redis, 15 minutos), que el dueño puede (re)abrir mediante verifyPasswordForModerators.