Implémentation d’un système de commentaires dans une application blog .NET Core

Ce guide technique décrit la conception et l’implémentation d’un système de commentaires robuste pour une application blog basée sur .NET Core 6+, avec une architecture front-end Vue.js et un backend API RESTful. L’accent est mis sur la sécurité, la modération automatisée et l’expérience utilisateur fluide — sans exigence de compte préalable.

Architecture clé

Le système repose sur trois couches fonctionnelles interconnectées :

  • Modèle métier : deux entités fortement typées — Comment (avec hiérarchie récursive via ParentId) et AnonymousVisitor (pour les contributeurs non authentifiés) ;
  • Service métier : CommentModerationService, encapsulant la génération/validatinos de jetons OTP, la création d’utilisateurs anonymes, et l’application des règles de modération ;
  • Interface utilisateur : composant Vue réactif (CommentSection.vue) piloté par Axios, intégrant la validation côté client avec Element Plus et le cache local des jetons.

Modèles de données simplifiés

Voici les définitions C# réécrites pour plus de clarté et de cohérence avec les bonnes pratiques modernes :

public record Comment(
    string Id,
    string? ParentId,
    string PostId,
    string? AnonymousVisitorId,
    string Content,
    bool IsVisible = false,
    bool RequiresManualReview = false,
    string? RejectionReason = null,
    string? UserAgent = null
) : IEntity;

public record AnonymousVisitor(
    string Id,
    string Name,
    string Email,
    string? WebsiteUrl = null,
    string IpAddress = ""
) : IEntity;

Gestion des jetons d’authentification temporaire

Plutôt que de stocker des codes en mémoire brute, le service utilise IMemoryCache avec une stratégie d’expiration stricte :

public async Task<Result<string>> RequestOtpForEmail(string email)
{
    if (!IsValidEmail(email))
        return Result.Failure<string>("Adresse e-mail invalide.");

    var cacheKey = $"otp:{email}";
    if (_cache.TryGetValue(cacheKey, out string existing))
        return Result.Success(existing);

    var otp = Random.Shared.Next(1000, 9999).ToString();
    _cache.Set(cacheKey, otp, TimeSpan.FromMinutes(5));

    await _mailer.SendAsync(
        subject: "[StarBlog] Code de vérification",
        htmlBody: $@"<p>Votre code est : <strong>{otp}</strong></p>",
        recipient: email
    );

    return Result.Success(otp);
}

Flux de soumission sécurisé

Le contrôleur CommentsController applique une séquence rigoureuse :

  1. Vérification du jeton OTP via VerifyOtp(email, otp) ;
  2. Récupération ou création d’un AnonymousVisitor ;
  3. Analyse du contenu via un moteur DFA personnalisé (ContentSanitizer) ;
  4. Persistance conditionnelle : visible immédiatement si propre, sinon marqué comme « en attente de validation ».
[HttpPost]
public async Task<IResult> Create([FromBody] CommentSubmissionDto dto)
{
    var verification = await _moderationService.VerifyOtp(dto.Email, dto.Otp);
    if (!verification.IsSuccess) 
        return Results.BadRequest(new { error = "Code expiré ou incorrect." });

    var visitor = await _moderationService.EnsureVisitorExists(dto);
    var isSuspicious = _sanitizer.ContainsProhibitedTerms(dto.Content);

    var comment = new Comment(
        Id: Guid.NewGuid().ToString(),
        ParentId: dto.ReplyToId,
        PostId: dto.PostId,
        AnonymousVisitorId: visitor.Id,
        Content: dto.Content,
        IsVisible: !isSuspicious,
        RequiresManualReview: isSuspicious,
        UserAgent: Request.Headers["User-Agent"]
    );

    await _commentRepository.InsertAsync(comment);

    return isSuspicious
        ? Results.Ok(new { message = "Soumis à modération manuelle.", id = comment.Id })
        : Results.Ok(new { message = "Publié immédiatement.", id = comment.Id });
}

Filtrage intelligent par automate fini déterministe

L’implémentation utilise une bibliothèque interne TextFilterEngine reposant sur un graphe d’états précompilé. Contrairement aux approches naïves (recherche linéaire ou regex), ce moteur détecte les termes prohibés en O(n) quel que soit le nombre de mots-clés — même avec des milliers d’entrées.

Exemple de configuraton minimaliste :

// Chargement depuis un fichier JSON structuré
var bannedWords = JsonSerializer.Deserialize<IEnumerable<BannedTerm>>(
    await File.ReadAllTextAsync("banned-terms.json")
);

_sanitizer = new TextFilterEngine(bannedWords.Select(t => t.Pattern));

Workflow d’approbation manuelle

Dans l’interface d’administration (Vue 3 + Pinia), chaque commentaire signalé come sensible affiche deux actions contextuelles :

  • Approve(id) : met à jour IsVisible = true et RequiresManualReview = false ;
  • Reject(id, reason) : archive le commentaire et stocke une justification explicite.

L’appel API correspondant utilise une autorisation stricte [Authorize(Policy = "AdminOnly")] pour garantir l’intégrité du processus.

Validation côté client avancée

Le formulaire Vue exploite le système de règles natif d’Element Plus avec une logique dynamique :

const rules = {
  name: [
    { required: true, message: 'Nom requis', trigger: 'blur' },
    { min: 2, max: 24, message: '2–24 caractères', trigger: 'blur' }
  ],
  email: [
    { type: 'email', message: 'Format invalide', trigger: 'blur' }
  ],
  content: [
    { required: true, message: 'Contenu obligatoire', trigger: 'blur' },
    { min: 3, max: 300, message: '3–300 caractères', trigger: 'blur' }
  ]
};

La soumission déclenche une transaction atomique : validation → appel API → réinitialisation sélective (conservation de l’e-mail) → rafraîchissement asynchrone de la liste.

Étiquettes: .net-core aspnetcore comments-system vuejs deterministic-finite-automaton

Publié le 21 septembre à 19h41