diff --git a/ManagerService/Controllers/InstanceController.cs b/ManagerService/Controllers/InstanceController.cs
index 1938a0e..edf2654 100644
--- a/ManagerService/Controllers/InstanceController.cs
+++ b/ManagerService/Controllers/InstanceController.cs
@@ -8,6 +8,7 @@ using ManagerService.Data;
using ManagerService.DTOs;
using ManagerService.Helpers;
using ManagerService.Services;
+using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
@@ -70,14 +71,48 @@ namespace ManagerService.Controllers
/// Get a specific instance
///
/// id instance
+ ///
+ /// Ouverte aux apps visiteur par X-Api-Key : mymuseum-visitapp appelle cette
+ /// route au démarrage pour lire la voix du guide, et recevait un 403 — tout le
+ /// contrôleur exige SuperAdmin. Une clé API ne donne accès qu'à SON instance, et à
+ /// une vue réduite : plan, quotas, TVA, facturation et pinCode ne sortent que pour
+ /// un utilisateur du manager.
+ ///
+ [AllowAnonymous]
[ProducesResponseType(typeof(InstanceDTO), 200)]
+ [ProducesResponseType(typeof(string), 401)]
+ [ProducesResponseType(typeof(string), 403)]
[ProducesResponseType(typeof(string), 404)]
[ProducesResponseType(typeof(string), 500)]
[HttpGet("{id}")]
- public ObjectResult GetDetail(string id)
+ public async Task GetDetail(string id)
{
try
{
+ // Le schéma ApiKey n'est pas le schéma par défaut : sur une action
+ // [AllowAnonymous] il faut le déclencher explicitement.
+ var apiKeyAuth = await HttpContext.AuthenticateAsync("ApiKey");
+ var keyInstanceId = apiKeyAuth.Succeeded
+ ? apiKeyAuth.Principal?.FindFirst(ManagerService.Service.Security.ClaimTypes.InstanceId)?.Value
+ : null;
+
+ // ⚠️ Ne PAS déduire « utilisateur du manager » d'un claim de permission.
+ // AuthorizationMiddleware authentifie avec les schémas de la policy du
+ // contrôleur — JwtBearer ET ApiKey (Startup.cs:133) — et peuple
+ // HttpContext.User AVANT de court-circuiter sur [AllowAnonymous]. Une clé
+ // API donne donc un User authentifié, et le handler lui pose le claim
+ // Viewer : le test « a Viewer » était vrai pour tout le monde.
+ var isManager = !apiKeyAuth.Succeeded
+ && User?.Identity?.IsAuthenticated == true
+ && User.HasClaim(ManagerService.Service.Security.ClaimTypes.Permission,
+ ManagerService.Service.Security.Permissions.Viewer);
+
+ if (!isManager && keyInstanceId == null)
+ return new ObjectResult("Authentication required") { StatusCode = 401 };
+
+ if (!isManager && keyInstanceId != id)
+ return new ObjectResult("This API key does not grant access to this instance") { StatusCode = 403 };
+
Instance instance = _myInfoMateDbContext.Instances.FirstOrDefault(i => i.Id == id);
//OldInstance instance = _instanceService.GetById(id);
@@ -87,7 +122,12 @@ namespace ManagerService.Controllers
var applicationInstances = _myInfoMateDbContext.ApplicationInstances.Where(ai => ai.InstanceId == instance.Id).ToList();
- return new OkObjectResult(instance.ToDTO(applicationInstances.Select(ai => ai.ToDTO(_myInfoMateDbContext)).ToList()));
+ var dto = instance.ToDTO(applicationInstances.Select(ai => ai.ToDTO(_myInfoMateDbContext)).ToList());
+
+ if (!isManager)
+ StripCommercialFields(dto);
+
+ return new OkObjectResult(dto);
}
catch (KeyNotFoundException ex)
{
@@ -339,6 +379,34 @@ namespace ManagerService.Controllers
///
/// Instance PIN code
/// App type (VisitApp, TabletApp, Other)
+
+ ///
+ /// Retire d'un tout ce qui ne regarde pas une app visiteur.
+ ///
+ /// Ce que l'app garde : identité, drapeaux de canal, réglages du guide, webSlug,
+ /// publicApiKey (elle la détient déjà pour appeler) et les ApplicationInstances.
+ /// Ce qui part : le commercial et le pinCode, qui ouvre l'appairage des tablettes.
+ ///
+ private static void StripCommercialFields(InstanceDTO dto)
+ {
+ dto.pinCode = null;
+ dto.subscriptionPlanId = null;
+ dto.subscriptionPlan = null;
+ dto.aiTokensThisMonth = null;
+ dto.aiUsageMonthKey = null;
+ dto.storageQuotaBytes = null;
+ dto.aiTokensPerMonth = null;
+ dto.hasStats = null;
+ dto.statsHistoryDays = null;
+ dto.hasAdvancedStats = null;
+ dto.isTrialActive = null;
+ dto.trialEndsAt = null;
+ dto.trialAiTokensUsed = null;
+ dto.billingAddress = null;
+ dto.billingCountry = null;
+ dto.vatNumber = null;
+ dto.vatRate = null;
+ }
[AllowAnonymous]
[ProducesResponseType(typeof(object), 200)]
[ProducesResponseType(typeof(string), 404)]