Skip to content

Signalements (modération)

API permettant aux utilisateurs authentifiés de signaler un média, un compte ou un commentaire pour examen par la modération. Les lignes sont persistées dans la base back-office (hxa_bo.report) — la base applicative ne voit jamais ces données. Les modérateurs lisent la file via /admin/reports (voir docs/admin.md).

Table polymorphe hxa_bo.report :

ColonneTypeNotes
idBINARY(16)UUID v4.
reporter_user_idBINARY(16)Auteur du signalement. Jamais exposé côté API publique.
target_typeENUM('media','user','comment')Discriminateur.
target_idBINARY(16)Référence vers media.id / user.id / media_comment.id. Pas de FK déclarée : la modération doit survivre à un hard-delete de la cible.
reason_codeVARCHAR(64)Slug whitelisté par config/report_reasons.php.
detailsTEXT NULLTexte libre optionnel (capé à REPORT_DETAILS_MAX_LENGTH).
statusENUM('pending','reviewed','action_taken','dismissed')Cycle de vie côté modération.
resolved_by_user_idBINARY(16) NULLOpérateur qui a tranché (peut être NULL pour un job automatisé).
resolved_atDATETIME NULLHorodatage du verdict.
resolution_noteTEXT NULLCommentaire interne du modérateur.
created_at / updated_atDATETIME

Index :

  • UNIQUE (reporter_user_id, target_type, target_id) — idempotence du POST + check « ai-je déjà signalé X ? ».
  • KEY (target_type, target_id, status) — agrégat « combien de signalements ouverts sur cette cible ? ».
  • KEY (status, created_at) — scan de file modérateur.

Whitelist statique (config/report_reasons.php), différente par target_type :

MotifmediausercommentDescription
spamContenu/compte/commentaire spam ou répétitif.
harassmentHarcèlement ciblé.
hate_speechDiscours de haine.
sexual_contentContenu sexuel explicite non signalé.
violenceViolence graphique.
copyrightAtteinte aux droits d’auteur.
fake_accountCompte bot ou fake.
impersonationUsurpation d’identité.
otherMotif libre (à expliciter dans details).

Un motif hors whitelist pour le target_type ⇒ 422 report.invalidReason.

  • REPORT_DETAILS_MAX_LENGTH (défaut 1000) — cap glyphes UTF-8 sur details.
  • REPORT_RATE_LIMIT_PER_DAY (défaut 20) — nombre maximal de signalements distincts qu’un même utilisateur peut soumettre sur une fenêtre glissante de 24h. Un re-signalement de la même cible (qui retourne la ligne existante) ne consomme PAS de quota.
  • Auth requise — un signalement anonyme serait du bruit.
  • NO self-report — un utilisateur ne peut PAS signaler son propre média / compte / commentaire (403 report.selfReport).
  • NO XP, NO notification au signalé, NO compteur public — l’acte reste strictement privé entre le rapporteur et la modération.
  • Idempotence : POST répété sur la même cible par le même rapporteur ⇒ retour 200 avec la ligne existante (le verdict d’une cible déjà résolue est préservé, pas réouvert).
  • Anonymat côté API : la ressource reports renvoyée n’expose jamais reporterUserId / resolvedByUserId / resolutionNote. Ces champs ne sont accessibles que via l’API admin.
meta.codeHTTPQuand
report.actorNotConfirmed403L’utilisateur n’a pas confirmé son email.
report.actorBanned403Compte banni.
report.selfReport403Le rapporteur est le propriétaire de la cible.
report.targetNotFound404La cible n’existe pas.
report.reasonMissing422Champ reason manquant ou vide.
report.invalidReason422reason hors whitelist pour ce target_type.
report.detailsTooLong422details dépasse REPORT_DETAILS_MAX_LENGTH.
report.rateLimited429REPORT_RATE_LIMIT_PER_DAY atteint sur 24h.
{
"type": "reports",
"id": "<hex>",
"attributes": {
"targetType": "media | user | comment",
"targetId": "<hex>",
"reason": "spam | harassment | …",
"status": "pending | reviewed | action_taken | dismissed",
"createdAt": "2026-06-13T12:34:56+00:00"
}
}

reporterUserId, details, resolvedByUserId, resolvedAt, resolutionNote sont volontairement absents côté public.


Signale un média. {mediaId} au format UUID dashed (cohérent avec les autres routes /media/{mediaId}/*).

Auth : requise.

Corps (formes acceptées) :

// Plate
{ "reason": "spam", "details": "optional context" }
// JSON:API
{
"data": {
"type": "reports",
"attributes": { "reason": "spam", "details": "optional context" }
}
}

details est optionnel.

Réponses :

  • 201 Created — première fois (la ressource reports est retournée).
  • 200 OK — re-signalement de la même cible par le même utilisateur ; la ligne existante est retournée telle quelle (status préservé, même si déjà résolue).
  • 403 — voir codes d’erreur.
  • 404 — média introuvable.
  • 422 — payload invalide.
  • 429 — quota journalier atteint.

Signale un compte utilisateur. {userHex} au format hex (32 caractères, sans tirets), aligné sur les autres endpoints user-centric.

Auth : requise.

Corps : identique à /api/media/{mediaId}/reports ; les motifs fake_account et impersonation deviennent valides ici.

Réponses : mêmes statuts.


POST /api/media/comments/{commentId}/reports

Section titled “POST /api/media/comments/{commentId}/reports”

Signale un commentaire. {commentId} au format UUID dashed (cohérent avec /media/comments/{commentId}/*).

Auth : requise.

Réponses : mêmes statuts. Le check self-report s’appuie sur media_comment.user_id — signaler son propre commentaire est un 403 report.selfReport, pas un 404.