diff --git a/ManagerService/Controllers/AiController.cs b/ManagerService/Controllers/AiController.cs
index eaddf10..392f337 100644
--- a/ManagerService/Controllers/AiController.cs
+++ b/ManagerService/Controllers/AiController.cs
@@ -4,6 +4,7 @@ using ManagerService.DTOs;
using ManagerService.Services;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.RateLimiting;
using Microsoft.Extensions.Logging;
using NSwag.Annotations;
using Microsoft.EntityFrameworkCore;
@@ -19,6 +20,13 @@ namespace ManagerService.Controllers
[OpenApiTag("AI", Description = "Assistant IA")]
public class AiController : ControllerBase
{
+ ///
+ /// Politique de limitation appliquée aux endpoints qui consomment des jetons.
+ /// Déclarée ici parce que c'est le seul contrôleur concerné ; Startup la lit
+ /// pour enregistrer le limiteur.
+ ///
+ public const string RateLimitPolicy = "ai";
+
///
/// Plafond IA cumulé sur toute la durée de l'essai gratuit (14 jours), distinct du
/// compteur mensuel : ~30 requêtes * ~10k tokens/req estimés. Empêche qu'un essai à
@@ -301,8 +309,10 @@ namespace ManagerService.Controllers
/// Traduit un texte HTML vers plusieurs langues via IA
///
[HttpPost("translate")]
+ [EnableRateLimiting(RateLimitPolicy)]
[ProducesResponseType(typeof(AiTranslateResponse), 200)]
[ProducesResponseType(403)]
+ [ProducesResponseType(429)]
[ProducesResponseType(typeof(string), 500)]
public async Task Translate([FromBody] AiTranslateRequest request, [FromQuery] string instanceId)
{
@@ -336,8 +346,10 @@ namespace ManagerService.Controllers
/// Envoie un message à l'assistant IA, scopé à l'instance et optionnellement à une configuration
///
[HttpPost("chat")]
+ [EnableRateLimiting(RateLimitPolicy)]
[ProducesResponseType(typeof(AiChatResponse), 200)]
[ProducesResponseType(403)]
+ [ProducesResponseType(429)]
[ProducesResponseType(typeof(string), 500)]
public async Task Chat([FromBody] AiChatRequest request)
{
diff --git a/ManagerService/Startup.cs b/ManagerService/Startup.cs
index af0ce15..92ef10f 100644
--- a/ManagerService/Startup.cs
+++ b/ManagerService/Startup.cs
@@ -25,6 +25,9 @@ using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.HttpsPolicy;
using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.RateLimiting;
+using System.Threading.RateLimiting;
+using ManagerService.Controllers;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
@@ -262,6 +265,38 @@ namespace ManagerService
services.AddHealthChecks()
.AddNpgSql(connectionString);
+
+ // La clé API publique d'une instance est embarquée dans les apps visiteur et
+ // lisible en clair dans le navigateur sur visitapp-web : elle est publique par
+ // construction. Les endpoints IA sont les seuls qui coûtent de l'argent réel
+ // (jetons Gemini), et rien n'empêchait d'y boucler jusqu'à vider le quota mensuel
+ // d'un client qui n'a rien fait.
+ //
+ // Partition par instance, parce que c'est l'instance qui porte le quota qu'on
+ // protège : l'abus chez un client ne doit pas ralentir les autres. Le plafond est
+ // large — un musée à 100 visiteurs simultanés posant une question par minute
+ // reste très en dessous — il coupe la boucle automatique, pas l'usage réel.
+ services.AddRateLimiter(options =>
+ {
+ options.RejectionStatusCode = StatusCodes.Status429TooManyRequests;
+
+ options.OnRejected = (context, _) =>
+ {
+ context.HttpContext.Response.Headers.RetryAfter = "60";
+ return ValueTask.CompletedTask;
+ };
+
+ options.AddPolicy(AiController.RateLimitPolicy, httpContext =>
+ RateLimitPartition.GetFixedWindowLimiter(
+ httpContext.User.FindFirst(ManagerService.Service.Security.ClaimTypes.InstanceId)?.Value
+ ?? httpContext.Connection.RemoteIpAddress?.ToString()
+ ?? "unknown",
+ _ => new FixedWindowRateLimiterOptions
+ {
+ PermitLimit = 120,
+ Window = TimeSpan.FromMinutes(1)
+ }));
+ });
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
@@ -302,6 +337,12 @@ namespace ManagerService
#endif
);
+ // Après UseCors : un 429 renvoyé avant que les en-têtes CORS soient posés
+ // s'affiche comme une erreur CORS dans le navigateur, et visitapp-web ne verrait
+ // jamais le vrai code. Après UseAuthentication aussi, sinon la partition n'a pas
+ // encore le claim d'instance et tout le monde tomberait dans le même seau.
+ app.UseRateLimiter();
+
#if DEBUG
app.UseHangfireDashboard("/hangfire");
#else