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).
Modèle de données
Section titled “Modèle de données”Table polymorphe hxa_bo.report :
| Colonne | Type | Notes |
|---|---|---|
id | BINARY(16) | UUID v4. |
reporter_user_id | BINARY(16) | Auteur du signalement. Jamais exposé côté API publique. |
target_type | ENUM('media','user','comment') | Discriminateur. |
target_id | BINARY(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_code | VARCHAR(64) | Slug whitelisté par config/report_reasons.php. |
details | TEXT NULL | Texte libre optionnel (capé à REPORT_DETAILS_MAX_LENGTH). |
status | ENUM('pending','reviewed','action_taken','dismissed') | Cycle de vie côté modération. |
resolved_by_user_id | BINARY(16) NULL | Opérateur qui a tranché (peut être NULL pour un job automatisé). |
resolved_at | DATETIME NULL | Horodatage du verdict. |
resolution_note | TEXT NULL | Commentaire interne du modérateur. |
created_at / updated_at | DATETIME |
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.
Codes de motif (reason_code)
Section titled “Codes de motif (reason_code)”Whitelist statique (config/report_reasons.php), différente par target_type :
| Motif | media | user | comment | Description |
|---|---|---|---|---|
spam | ✓ | ✓ | ✓ | Contenu/compte/commentaire spam ou répétitif. |
harassment | ✓ | ✓ | ✓ | Harcèlement ciblé. |
hate_speech | ✓ | ✓ | ✓ | Discours de haine. |
sexual_content | ✓ | — | ✓ | Contenu sexuel explicite non signalé. |
violence | ✓ | — | ✓ | Violence graphique. |
copyright | ✓ | — | — | Atteinte aux droits d’auteur. |
fake_account | — | ✓ | — | Compte bot ou fake. |
impersonation | — | ✓ | — | Usurpation d’identité. |
other | ✓ | ✓ | ✓ | Motif libre (à expliciter dans details). |
Un motif hors whitelist pour le target_type ⇒ 422 report.invalidReason.
Variables d’environnement
Section titled “Variables d’environnement”REPORT_DETAILS_MAX_LENGTH(défaut1000) — cap glyphes UTF-8 surdetails.REPORT_RATE_LIMIT_PER_DAY(défaut20) — 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.
Règles métier (validées)
Section titled “Règles métier (validées)”- 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
reportsrenvoyée n’expose jamaisreporterUserId/resolvedByUserId/resolutionNote. Ces champs ne sont accessibles que via l’API admin.
Codes d’erreur (report.*)
Section titled “Codes d’erreur (report.*)”meta.code | HTTP | Quand |
|---|---|---|
report.actorNotConfirmed | 403 | L’utilisateur n’a pas confirmé son email. |
report.actorBanned | 403 | Compte banni. |
report.selfReport | 403 | Le rapporteur est le propriétaire de la cible. |
report.targetNotFound | 404 | La cible n’existe pas. |
report.reasonMissing | 422 | Champ reason manquant ou vide. |
report.invalidReason | 422 | reason hors whitelist pour ce target_type. |
report.detailsTooLong | 422 | details dépasse REPORT_DETAILS_MAX_LENGTH. |
report.rateLimited | 429 | REPORT_RATE_LIMIT_PER_DAY atteint sur 24h. |
Ressource reports
Section titled “Ressource reports”{ "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.
POST /api/media/{mediaId}/reports
Section titled “POST /api/media/{mediaId}/reports”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 ressourcereportsest retournée).200 OK— re-signalement de la même cible par le même utilisateur ; la ligne existante est retournée telle quelle (statuspréservé, même si déjà résolue).403— voir codes d’erreur.404— média introuvable.422— payload invalide.429— quota journalier atteint.
POST /api/users/{userHex}/reports
Section titled “POST /api/users/{userHex}/reports”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.