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)]