Securite
- RequireAppKey : filtre qui exige une cle d'API valide ou un utilisateur du
manager. Pose sur les routes de contenu consommees par les apps visiteur
(Configuration, Section, Resource, SectionMap, SectionEvent, SectionAgenda,
SectionParcours, SectionQuiz, ApplicationInstance). Un [Authorize] d'action
ne peut pas assouplir celui de la classe ; seul [AllowAnonymous] le
court-circuite, et le filtre redevient le controle d'acces. Il ferme
l'enumeration par identifiant, pas la confidentialite : la cle s'obtient
par le slug ou le pincode.
- Instance/slug/{slug} rendait le pinCode, l'adresse de facturation, la TVA et
les quotas a qui lit l'URL du site visiteur. StripCommercialFields est
desormais applique sur slug et byPin ; isTrialActive reste expose pour le
filigrane d'essai.
- Device.Create et Device/{id}/detail repondaient 403 a toute tablette depuis
a452f4a (13/03) : la classe exige InstanceAdmin, une cle ne porte que
AppRead. Ouverts a la cle, le cloisonnement par instance etait deja ecrit.
Canal VR
- Device.AppType (defaut Tablet, backfill a 1) ; Create resout
l'ApplicationInstance sur ce type au lieu de Tablet en dur.
- Get filtre optionnellement par appType ; DeviceDetailDTO expose appVersion
et lastSeen.
- PUT Device/{id}/heartbeat : batterie, version, connexion. Volontairement
etroit, une app ne peut ni se renommer ni changer d'instance.
- ApiKeyAppType.VrApp en fin d'enum.
Contenu immersif
- ResourceType : Image360 (11), Video360 (12), Model3D (13), en fin d'enum.
- Section Scene3D : un modele GLB et ses points d'interet, en objet manipule
ou en decor habite. Les points sont des GeoPoint, avec une LocalTransform
en jsonb (convention glTF) ; CRUD dans SectionScene3DController.
- Instance.HasImmersiveContent : l'add-on ajoute 100 Go au quota de stockage,
repose apres un changement de plan et retire a la desactivation.
- ImmersiveBackground (owned) sur Configuration et ApplicationInstance, avec
une image de repli pour les canaux qui ne rendent pas l'immersif.
Export de configuration
- exportVersion (1) et generatedAt : le JSON devient un contrat, lu tel quel
par l'app Unity.
- Section.ToDTO() n'etant pas virtuelle, l'export ne portait aucun champ
specifique de sous-type. Passe par SectionFactory.ToDTO, et charge les
points des Map et des Scene3D.
- Le fond immersif et son repli partent avec les ressources, URL resolues.
Migrations : AddAppTypeToDevice, AddScene3DSectionAndImmersiveAddon,
AddImmersiveBackground.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
700 lines
34 KiB
C#
700 lines
34 KiB
C#
using System;
|
||
using System.Collections.Generic;
|
||
using System.Linq;
|
||
using System.Net.Http;
|
||
using System.Text;
|
||
using System.Threading.Tasks;
|
||
using Manager.DTOs;
|
||
using Manager.Helpers;
|
||
using Manager.Interfaces.Models;
|
||
using Manager.Services;
|
||
using ManagerService.Data;
|
||
using ManagerService.Data.SubSection;
|
||
using ManagerService.DTOs;
|
||
using ManagerService.Services;
|
||
using Microsoft.AspNetCore.Authentication;
|
||
using Microsoft.AspNetCore.Authorization;
|
||
using ManagerService.Security;
|
||
using Microsoft.AspNetCore.Mvc;
|
||
using Microsoft.EntityFrameworkCore;
|
||
using Microsoft.Extensions.Configuration;
|
||
using Microsoft.Extensions.Logging;
|
||
using Mqtt.Client.AspNetCore.Services;
|
||
using Newtonsoft.Json;
|
||
using NSwag.Annotations;
|
||
|
||
namespace ManagerService.Controllers
|
||
{
|
||
[Authorize(Policy = ManagerService.Service.Security.Policies.ContentEditor)]
|
||
[ApiController, Route("api/[controller]")]
|
||
[OpenApiTag("Configuration", Description = "Configuration management")]
|
||
public class ConfigurationController : ControllerBase
|
||
{
|
||
private readonly MyInfoMateDbContext _myInfoMateDbContext;
|
||
|
||
/*private ConfigurationDatabaseService _configurationService;
|
||
private InstanceDatabaseService _instanceService;
|
||
private SectionDatabaseService _sectionService;
|
||
private ResourceDatabaseService _resourceService;
|
||
private DeviceDatabaseService _deviceService;*/
|
||
private readonly ILogger<ConfigurationController> _logger;
|
||
private readonly IConfiguration _configuration;
|
||
IHexIdGeneratorService idService = new HexIdGeneratorService();
|
||
|
||
public ConfigurationController(IConfiguration configuration, ILogger<ConfigurationController> logger, MyInfoMateDbContext myInfoMateDbContext)
|
||
{
|
||
_logger = logger;
|
||
_configuration = configuration;
|
||
_myInfoMateDbContext = myInfoMateDbContext;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Get a list of all configuration (summary)
|
||
/// </summary>
|
||
/// <param name="id">id instance</param>
|
||
[AllowAnonymous]
|
||
[RequireAppKey]
|
||
[ProducesResponseType(typeof(List<ConfigurationDTO>), 200)]
|
||
[ProducesResponseType(typeof(string), 500)]
|
||
[HttpGet]
|
||
public ObjectResult Get([FromQuery] string instanceId)
|
||
{
|
||
try
|
||
{
|
||
List<Configuration> configurations = _myInfoMateDbContext.Configurations.Where(c => c.InstanceId == instanceId).ToList();
|
||
|
||
List<ConfigurationDTO> configurationDTOs = new List<ConfigurationDTO>();
|
||
|
||
foreach(var configuration in configurations)
|
||
{
|
||
List<string> sectionIds = _myInfoMateDbContext.Sections.Where(s => s.ConfigurationId == configuration.Id && !s.IsSubSection).Select(s => s.Id).ToList();
|
||
ConfigurationDTO configurationDTO = configuration.ToDTO(sectionIds);
|
||
configurationDTOs.Add(configurationDTO);
|
||
}
|
||
|
||
return new OkObjectResult(configurationDTOs.OrderBy(c => c.dateCreation));
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
return new ObjectResult(ex.Message) { StatusCode = 500 };
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Get Confuguration list by instanceId' pincode
|
||
/// </summary>
|
||
/// <param name="pinCode">Code pin</param>
|
||
[Authorize(Policy = ManagerService.Service.Security.Policies.AppReadAccess)]
|
||
[ProducesResponseType(typeof(List<ConfigurationDTO>), 200)]
|
||
[ProducesResponseType(typeof(string), 404)]
|
||
[ProducesResponseType(typeof(string), 500)]
|
||
[HttpGet("byPin")]
|
||
public ObjectResult GetConfigurationsByPinCode([FromQuery] string pinCode)
|
||
{
|
||
try
|
||
{
|
||
Instance instance = _myInfoMateDbContext.Instances.FirstOrDefault(i => i.PinCode == pinCode);
|
||
|
||
if (instance == null)
|
||
throw new KeyNotFoundException("None instance is linked to this pin code");
|
||
|
||
List<Configuration> configurations = _myInfoMateDbContext.Configurations.Where(c => c.InstanceId == instance.Id).ToList();
|
||
List<ConfigurationDTO> configurationDTOs = new List<ConfigurationDTO>();
|
||
foreach (var configuration in configurations)
|
||
{
|
||
List<string> sectionIds = _myInfoMateDbContext.Sections.Where(s => s.ConfigurationId == configuration.Id && !s.IsSubSection).Select(s => s.Id).ToList();
|
||
configurationDTOs.Add(configuration.ToDTO(sectionIds));
|
||
}
|
||
|
||
return new OkObjectResult(configurationDTOs);
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
return new ObjectResult(ex.Message) { StatusCode = 500 };
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Get a specific display configuration
|
||
/// </summary>
|
||
/// <param name="id">id configuration</param>
|
||
[AllowAnonymous]
|
||
[RequireAppKey]
|
||
[ProducesResponseType(typeof(ConfigurationDTO), 200)]
|
||
[ProducesResponseType(typeof(string), 404)]
|
||
[ProducesResponseType(typeof(string), 500)]
|
||
[HttpGet("{id}")]
|
||
public async Task<ObjectResult> GetDetailAsync(string id)
|
||
{
|
||
try
|
||
{
|
||
Configuration configuration = _myInfoMateDbContext.Configurations.FirstOrDefault(c => c.Id == id);
|
||
|
||
if (configuration == null)
|
||
throw new KeyNotFoundException("This configuration was not found");
|
||
|
||
List<string> sectionIds = _myInfoMateDbContext.Sections.Where(s => s.ConfigurationId == id && !s.IsSubSection).Select(s => s.Id).ToList();
|
||
|
||
try
|
||
{
|
||
var weatherSections = _myInfoMateDbContext.Sections.OfType<SectionWeather>()
|
||
.Where(s => s.ConfigurationId == id && !s.IsSubSection)
|
||
.ToList();
|
||
|
||
foreach (var weatherSection in weatherSections)
|
||
{
|
||
if (weatherSection.WeatherCity != null && weatherSection.WeatherCity.Length >= 2 &&
|
||
(weatherSection.WeatherUpdatedDate == null || weatherSection.WeatherUpdatedDate.Value.AddHours(3) < DateTimeOffset.Now)) // Update all 4 hours
|
||
{
|
||
// Call Openweather api with token from appSettings and update result with json
|
||
var apiKey = _configuration.GetSection("OpenWeatherApiKey").Get<string>();
|
||
|
||
if (apiKey != null && apiKey.Length > 0)
|
||
{
|
||
string url = $"http://api.openweathermap.org/geo/1.0/direct?q={weatherSection.WeatherCity}&limit=1&appid={apiKey}";
|
||
|
||
using (HttpClient client = new HttpClient())
|
||
{
|
||
try
|
||
{
|
||
HttpResponseMessage response = await client.GetAsync(url);
|
||
response.EnsureSuccessStatusCode();
|
||
string responseBody = await response.Content.ReadAsStringAsync();
|
||
|
||
List<CityData> cities = JsonConvert.DeserializeObject<List<CityData>>(responseBody);
|
||
|
||
if (cities.Count > 0)
|
||
{
|
||
double lat = cities[0].Lat;
|
||
double lon = cities[0].Lon;
|
||
|
||
//string onecallUrl = $"https://api.openweathermap.org/data/3.0/onecall?lat={lat}&lon={lon}&appid={apiKey}";
|
||
string callUrl = $"https://api.openweathermap.org/data/2.5/forecast?lat={lat}&lon={lon}&units=metric&appid={apiKey}";
|
||
|
||
HttpResponseMessage callResponse = await client.GetAsync(callUrl);
|
||
callResponse.EnsureSuccessStatusCode();
|
||
string callResponseBody = await callResponse.Content.ReadAsStringAsync();
|
||
|
||
|
||
weatherSection.WeatherUpdatedDate = DateTimeOffset.Now.ToUniversalTime(); ;
|
||
weatherSection.WeatherResult = callResponseBody;
|
||
_myInfoMateDbContext.SaveChanges();
|
||
}
|
||
else
|
||
{
|
||
Console.WriteLine("Aucune ville trouv<75>e.");
|
||
}
|
||
}
|
||
catch (HttpRequestException e)
|
||
{
|
||
Console.WriteLine($"Une erreur s'est produite lors de la requ<71>te HTTP : {e.Message}");
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
}
|
||
} catch (Exception e)
|
||
{
|
||
Console.WriteLine($"Une erreur s'est produite lors de la mise <20> jour des sections de type m<>t<EFBFBD>o : {e.Message}");
|
||
}
|
||
|
||
return new OkObjectResult(configuration.ToDTO(sectionIds));
|
||
}
|
||
catch (KeyNotFoundException ex)
|
||
{
|
||
return new NotFoundObjectResult(ex.Message) {};
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
return new ObjectResult(ex.Message) { StatusCode = 500 };
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Create a new configuration
|
||
/// </summary>
|
||
/// <param name="newConfiguration">New configuration info</param>
|
||
[ProducesResponseType(typeof(ConfigurationDTO), 200)]
|
||
[ProducesResponseType(typeof(string), 400)]
|
||
[ProducesResponseType(typeof(string), 409)]
|
||
[ProducesResponseType(typeof(string), 500)]
|
||
[HttpPost]
|
||
public ObjectResult Create([FromBody] ConfigurationDTO newConfiguration)
|
||
{
|
||
try
|
||
{
|
||
if (newConfiguration == null)
|
||
throw new ArgumentNullException("Configuration param is null");
|
||
|
||
// Todo add some verification ?
|
||
Configuration configuration = new Configuration();
|
||
configuration.InstanceId = newConfiguration.instanceId;
|
||
configuration.Label = newConfiguration.label;
|
||
configuration.Title = new List<TranslationDTO>();
|
||
configuration.ImageId = newConfiguration.imageId;
|
||
configuration.ImageSource = newConfiguration.imageSource;
|
||
configuration.PrimaryColor = newConfiguration.primaryColor;
|
||
configuration.SecondaryColor = newConfiguration.secondaryColor;
|
||
configuration.LoaderImageId = newConfiguration.loaderImageId;
|
||
configuration.LoaderImageUrl = newConfiguration.loaderImageUrl;
|
||
configuration.Languages = newConfiguration.languages;
|
||
configuration.Title = LanguageInit.Init("Title", configuration.Languages);
|
||
configuration.DateCreation = DateTime.Now.ToUniversalTime();
|
||
configuration.IsOffline = newConfiguration.isOffline;
|
||
|
||
configuration.Id = idService.GenerateHexId();
|
||
|
||
_myInfoMateDbContext.Configurations.Add(configuration);
|
||
//Configuration configurationCreated = _configurationService.Create(configuration);
|
||
|
||
_myInfoMateDbContext.SaveChanges();
|
||
|
||
return new OkObjectResult(configuration.ToDTO(new List<string>())); // Empty list
|
||
}
|
||
catch (ArgumentNullException ex)
|
||
{
|
||
return new BadRequestObjectResult(ex.Message) {};
|
||
}
|
||
catch (InvalidOperationException ex)
|
||
{
|
||
return new ConflictObjectResult(ex.Message) {};
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
return new ObjectResult(ex.Message) { StatusCode = 500 };
|
||
}
|
||
}
|
||
|
||
|
||
/// <summary>
|
||
/// Update a configuration
|
||
/// </summary>
|
||
/// <param name="updatedConfiguration">Configuration to update</param>
|
||
[ProducesResponseType(typeof(ConfigurationDTO), 200)]
|
||
[ProducesResponseType(typeof(string), 400)]
|
||
[ProducesResponseType(typeof(string), 404)]
|
||
[ProducesResponseType(typeof(string), 500)]
|
||
[HttpPut]
|
||
public ObjectResult Update([FromBody] ConfigurationDTO updatedConfiguration)
|
||
{
|
||
try
|
||
{
|
||
if (updatedConfiguration == null)
|
||
throw new ArgumentNullException("configuration param is null");
|
||
|
||
Configuration configuration = _myInfoMateDbContext.Configurations.FirstOrDefault(c => c.Id == updatedConfiguration.id);
|
||
|
||
if (configuration == null)
|
||
throw new KeyNotFoundException("Configuration does not exist");
|
||
|
||
// Todo add some verification ?
|
||
configuration.InstanceId = updatedConfiguration.instanceId;
|
||
configuration.Label = updatedConfiguration.label;
|
||
configuration.Title = updatedConfiguration.title;
|
||
configuration.ImageId = updatedConfiguration.imageId;
|
||
configuration.ImageSource = updatedConfiguration.imageSource;
|
||
configuration.PrimaryColor = updatedConfiguration.primaryColor;
|
||
configuration.SecondaryColor = updatedConfiguration.secondaryColor;
|
||
configuration.Languages = updatedConfiguration.languages;
|
||
configuration.IsOffline = updatedConfiguration.isOffline;
|
||
configuration.LoaderImageId = updatedConfiguration.loaderImageId;
|
||
configuration.LoaderImageUrl = updatedConfiguration.loaderImageUrl;
|
||
configuration.ImmersiveBackground =
|
||
ImmersiveBackground.FromDTO(updatedConfiguration.immersiveBackground);
|
||
|
||
//Configuration configurationModified = _configurationService.Update(updatedConfiguration.id, configuration);
|
||
_myInfoMateDbContext.SaveChanges();
|
||
|
||
MqttClientService.PublishMessage($"config/{configuration.Id}", JsonConvert.SerializeObject(new PlayerMessageDTO() { configChanged = true }));
|
||
|
||
List<string> sectionIds = _myInfoMateDbContext.Sections.Where(s => s.ConfigurationId == configuration.Id && !s.IsSubSection).Select(s => s.Id).ToList(); // _sectionService.GetAllIdsFromConfiguration(configuration.Id);
|
||
|
||
return new OkObjectResult(configuration.ToDTO(sectionIds));
|
||
}
|
||
catch (ArgumentNullException ex)
|
||
{
|
||
return new BadRequestObjectResult(ex.Message) {};
|
||
}
|
||
catch (KeyNotFoundException ex)
|
||
{
|
||
return new NotFoundObjectResult(ex.Message) {};
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
return new ObjectResult(ex.Message) { StatusCode = 500 };
|
||
}
|
||
}
|
||
|
||
|
||
/// <summary>
|
||
/// Delete a configuration
|
||
/// </summary>
|
||
/// <param name="id">Id of configuration to delete</param>
|
||
[ProducesResponseType(typeof(string), 202)]
|
||
[ProducesResponseType(typeof(string), 400)]
|
||
[ProducesResponseType(typeof(string), 404)]
|
||
[ProducesResponseType(typeof(string), 500)]
|
||
[HttpDelete("{id}")]
|
||
public ObjectResult Delete(string id)
|
||
{
|
||
try
|
||
{
|
||
if (id == null)
|
||
throw new ArgumentNullException("Configuration param is null");
|
||
|
||
var configuration = _myInfoMateDbContext.Configurations.FirstOrDefault(c => c.Id == id);
|
||
|
||
if (configuration == null)
|
||
throw new KeyNotFoundException("Configuration does not exist");
|
||
|
||
_myInfoMateDbContext.Remove(configuration);
|
||
_myInfoMateDbContext.SaveChanges();
|
||
|
||
// Delete config for all devices
|
||
List<Device> devices = _myInfoMateDbContext.Devices.Where(d => d.ConfigurationId == id).ToList();
|
||
|
||
foreach (var device in devices)
|
||
{
|
||
device.Configuration = null;
|
||
device.ConfigurationId = null;
|
||
_myInfoMateDbContext.SaveChanges();
|
||
//_deviceService.Update(device.Id, device);
|
||
}
|
||
|
||
MqttClientService.PublishMessage($"config/{id}", JsonConvert.SerializeObject(new PlayerMessageDTO() { configChanged = true, isDeleted = true }));
|
||
|
||
return new ObjectResult("The configuration has been deleted") { StatusCode = 202 };
|
||
|
||
}
|
||
catch (ArgumentNullException ex)
|
||
{
|
||
return new BadRequestObjectResult(ex.Message) { };
|
||
}
|
||
catch (KeyNotFoundException ex)
|
||
{
|
||
return new NotFoundObjectResult(ex.Message) { };
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
return new ObjectResult(ex.Message) { StatusCode = 500 };
|
||
}
|
||
}
|
||
|
||
|
||
/// <summary>
|
||
/// Export a configuration
|
||
/// </summary>
|
||
/// <param name="id">Id of configuration to export</param>
|
||
/// <param name="language">Language to export</param>
|
||
/// <remarks>
|
||
/// Ouverte aux apps visiteur par <c>X-Api-Key</c> : c'est l'appel que fait
|
||
/// mymuseum-visitapp pour télécharger une visite hors ligne.
|
||
///
|
||
/// ⚠️ <c>[Authorize(AppReadAccess)]</c> ne suffisait pas : ASP.NET Core **combine**
|
||
/// les <c>[Authorize]</c> de la classe et de l'action. Le contrôleur exige
|
||
/// <c>ContentEditor</c>, qu'une clé API n'a pas — la clé authentifiait donc la
|
||
/// requête, puis l'autorisation la refusait : 403 sans corps, et côté app un
|
||
/// téléchargement qui échouait sans rien dire. Seul <c>[AllowAnonymous]</c>
|
||
/// court-circuite la policy du contrôleur ; le contrôle d'accès se fait ici.
|
||
/// Même correctif que <see cref="InstanceController.GetDetail"/>.
|
||
/// </remarks>
|
||
[AllowAnonymous]
|
||
[ProducesResponseType(typeof(FileContentResult), 200)]
|
||
[ProducesResponseType(typeof(string), 400)]
|
||
[ProducesResponseType(typeof(string), 401)]
|
||
[ProducesResponseType(typeof(string), 403)]
|
||
[ProducesResponseType(typeof(string), 404)]
|
||
[ProducesResponseType(typeof(string), 500)]
|
||
[HttpGet("{id}/export")]
|
||
public async Task<IActionResult> Export(string id, [FromQuery] string language)
|
||
{
|
||
try
|
||
{
|
||
if (id == null)
|
||
throw new ArgumentNullException("Configuration param is null");
|
||
|
||
Configuration configuration = _myInfoMateDbContext.Configurations.FirstOrDefault(c => c.Id == id);
|
||
|
||
if (configuration == null)
|
||
throw new KeyNotFoundException("Configuration does not exist");
|
||
|
||
// 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 : le
|
||
// handler de clé API pose lui aussi le claim Viewer.
|
||
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 != configuration.InstanceId)
|
||
return new ObjectResult("This API key does not grant access to this configuration") { StatusCode = 403 };
|
||
|
||
// Les entités, pas seulement leurs DTO : la collecte des ressources passe
|
||
// par GetReferencedResourceIds, qui vit sur le sous-type.
|
||
List<Section> sections = _myInfoMateDbContext.Sections.Where(s => s.ConfigurationId == configuration.Id).ToList();
|
||
|
||
// Les collections des sous-types sont dans d'autres tables : sans ces
|
||
// chargements, une Map exporte zéro point et une maquette zéro POI. EF
|
||
// rattache les entités à celles déjà suivies, il n'y a rien à réaffecter.
|
||
var configurationId = configuration.Id;
|
||
|
||
_myInfoMateDbContext.Sections.OfType<SectionMap>()
|
||
.Include(s => s.MapPoints)
|
||
.Where(s => s.ConfigurationId == configurationId).ToList();
|
||
|
||
_myInfoMateDbContext.Sections.OfType<SectionScene3D>()
|
||
.Include(s => s.Points)
|
||
.Where(s => s.ConfigurationId == configurationId).ToList();
|
||
|
||
// ⚠️ `Section.ToDTO()` n'est pas virtuelle : appelée sur une variable de
|
||
// type `Section`, elle ne rend que les champs communs. L'export ne portait
|
||
// donc **aucun champ spécifique** — ni les contenus d'un Slider, ni les
|
||
// points d'une Map, ni le programme d'un Event — alors qu'il est censé
|
||
// être « tout le contenu en un appel ». `SectionFactory.ToDTO` fait le
|
||
// bon sous-type, et la sérialisation suit le type réel.
|
||
List<SectionDTO> sectionDTOs = sections
|
||
.Select(s => (SectionDTO)SectionFactory.ToDTO(s))
|
||
.ToList();
|
||
List<ResourceDTO> resourceDTOs = new List<ResourceDTO>();
|
||
|
||
if (configuration.ImageId != null)
|
||
{
|
||
addResourceToList(resourceDTOs, configuration.ImageId);
|
||
}
|
||
|
||
if (configuration.LoaderImageId != null)
|
||
{
|
||
addResourceToList(resourceDTOs, configuration.LoaderImageId);
|
||
}
|
||
|
||
// Le fond immersif est une ressource comme une autre : oubliée ici, elle
|
||
// n'existerait pas dans la visite hors ligne, et le casque afficherait son
|
||
// menu dans le noir dès la première coupure de réseau.
|
||
if (configuration.ImmersiveBackground != null)
|
||
{
|
||
addResourceToList(resourceDTOs, configuration.ImmersiveBackground.ResourceId);
|
||
addResourceToList(resourceDTOs, configuration.ImmersiveBackground.FallbackResourceId);
|
||
}
|
||
|
||
foreach (var section in sections)
|
||
{
|
||
// Remplace 157 lignes de `switch` commenté qui n'ont jamais tourné :
|
||
// une visite téléchargée n'embarquait que l'image de la configuration,
|
||
// celle du loader et l'image de chaque section — ni contenus d'articles,
|
||
// ni audios, ni icônes de carte, ni images de quiz.
|
||
// `GetReferencedResourceIds` est déjà implémentée sur les 13 sous-types
|
||
// et connaît chacun ses propres médias ; c'est aussi elle qui garantit
|
||
// qu'un nouveau type de section n'ouvre pas un trou silencieux ici.
|
||
foreach (var resourceId in section.GetReferencedResourceIds(language))
|
||
addResourceToList(resourceDTOs, resourceId);
|
||
}
|
||
ExportConfigurationDTO toDownload = configuration.ToExportDTO(sectionDTOs, resourceDTOs);
|
||
|
||
// Les URL sont posées ici parce que c'est le seul endroit qui a les
|
||
// ressources sous la main. Le casque lit ce JSON sans client généré et
|
||
// souvent sans réseau : lui faire résoudre un id de plus serait un appel
|
||
// qu'il ne peut pas passer.
|
||
if (toDownload.immersiveBackground != null)
|
||
{
|
||
toDownload.immersiveBackground.resourceUrl = resourceDTOs
|
||
.FirstOrDefault(r => r.id == toDownload.immersiveBackground.resourceId)?.url;
|
||
toDownload.immersiveBackground.fallbackUrl = resourceDTOs
|
||
.FirstOrDefault(r => r.id == toDownload.immersiveBackground.fallbackResourceId)?.url;
|
||
}
|
||
|
||
string jsonString = JsonConvert.SerializeObject(toDownload);
|
||
var fileName = $"{configuration.Label}.json";
|
||
var mimeType = "application/json";
|
||
var fileBytes = Encoding.UTF8.GetBytes(jsonString);
|
||
return new FileContentResult(fileBytes, mimeType)
|
||
{
|
||
FileDownloadName = fileName
|
||
};
|
||
}
|
||
// Les trois `catch` renvoyaient `null` : l'app recevait un 200 vide et croyait
|
||
// la visite exportée. Les codes ci-dessous étaient déjà écrits, en commentaire.
|
||
catch (ArgumentNullException ex)
|
||
{
|
||
return new BadRequestObjectResult(ex.Message);
|
||
}
|
||
catch (KeyNotFoundException ex)
|
||
{
|
||
return new NotFoundObjectResult(ex.Message);
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
return new ObjectResult(ex.Message) { StatusCode = 500 };
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Import a configuration
|
||
/// </summary>
|
||
/// <param name="exportConfiguration">Configuration to import</param>
|
||
[ProducesResponseType(typeof(string), 202)]
|
||
[ProducesResponseType(typeof(string), 400)]
|
||
[ProducesResponseType(typeof(string), 404)]
|
||
[ProducesResponseType(typeof(string), 409)]
|
||
[ProducesResponseType(typeof(string), 500)]
|
||
[HttpPost("import")]
|
||
public ObjectResult Import([FromBody] ExportConfigurationDTO exportConfiguration)
|
||
{
|
||
try
|
||
{
|
||
if (exportConfiguration == null)
|
||
throw new ArgumentNullException("File to import is null");
|
||
|
||
Configuration configuration = _myInfoMateDbContext.Configurations.FirstOrDefault(c => c.Id == exportConfiguration.id);
|
||
if (configuration != null)
|
||
throw new InvalidOperationException("Configuration already exist in the system");
|
||
|
||
configuration = new Configuration();
|
||
configuration.Id = exportConfiguration.id;
|
||
configuration.InstanceId = exportConfiguration.instanceId;
|
||
configuration.Label = exportConfiguration.label;
|
||
configuration.Title = exportConfiguration.title;
|
||
configuration.ImageId = exportConfiguration.imageId;
|
||
configuration.ImageSource = exportConfiguration.imageSource;
|
||
|
||
if (configuration.ImageId != null)
|
||
{
|
||
createResource(exportConfiguration.resources.Where(r => r.id == configuration.ImageId).FirstOrDefault());
|
||
}
|
||
|
||
configuration.DateCreation = exportConfiguration.dateCreation;
|
||
configuration.PrimaryColor = exportConfiguration.primaryColor;
|
||
configuration.SecondaryColor = exportConfiguration.secondaryColor;
|
||
configuration.Languages = exportConfiguration.languages;
|
||
configuration.IsOffline = exportConfiguration.isOffline;
|
||
configuration.LoaderImageId = exportConfiguration.loaderImageId;
|
||
configuration.LoaderImageUrl = exportConfiguration.loaderImageUrl;
|
||
|
||
if (configuration.LoaderImageId != null)
|
||
{
|
||
createResource(exportConfiguration.resources.Where(r => r.id == configuration.LoaderImageId).FirstOrDefault());
|
||
}
|
||
|
||
configuration.ImmersiveBackground =
|
||
ImmersiveBackground.FromDTO(exportConfiguration.immersiveBackground);
|
||
|
||
if (configuration.ImmersiveBackground != null)
|
||
{
|
||
createResource(exportConfiguration.resources.FirstOrDefault(
|
||
r => r.id == configuration.ImmersiveBackground.ResourceId));
|
||
createResource(exportConfiguration.resources.FirstOrDefault(
|
||
r => r.id == configuration.ImmersiveBackground.FallbackResourceId));
|
||
}
|
||
|
||
_myInfoMateDbContext.Configurations.Add(configuration);
|
||
//_configurationService.Create(configuration);
|
||
|
||
var sectionsAlreadyInDB = _myInfoMateDbContext.Sections.Where(s => !exportConfiguration.sections.Select(s => s.id).Contains(s.Id)).Select(s => s.Id).ToList();
|
||
|
||
// Toutes les ressources de la charge, en une passe. `createResource` est
|
||
// idempotente, donc les quelques appels ciblés qui subsistent plus haut sont
|
||
// sans effet — et un nouveau type de section ne peut plus ouvrir de trou ici.
|
||
foreach (var resourceExport in exportConfiguration.resources ?? new List<ResourceDTO>())
|
||
createResource(resourceExport);
|
||
|
||
foreach (var section in exportConfiguration.sections.Where(s => !sectionsAlreadyInDB.Contains(s.id)))
|
||
{
|
||
Section newSection = SectionFactory.CreateEmpty(section.type);
|
||
newSection.Id = section.id;
|
||
newSection.InstanceId = section.instanceId;
|
||
newSection.Label = section.label;
|
||
newSection.Title = section.title;
|
||
newSection.Description = section.description;
|
||
newSection.Order = section.order.GetValueOrDefault(); // if one day we can use same section in multiple configuration, need to change that
|
||
newSection.Type = section.type;
|
||
newSection.ImageId = section.imageId;
|
||
newSection.ImageSource = section.imageSource;
|
||
newSection.ConfigurationId = section.configurationId;
|
||
newSection.IsSubSection = section.isSubSection;
|
||
newSection.ParentId = section.parentId;
|
||
//newSection.Data = section.data;
|
||
newSection.DateCreation = section.dateCreation.Value;
|
||
newSection.IsBeacon = section.isBeacon;
|
||
newSection.BeaconId = section.beaconId;
|
||
newSection.Latitude = section.latitude;
|
||
newSection.Longitude = section.longitude;
|
||
newSection.MeterZoneGPS = section.meterZoneGPS;
|
||
|
||
if (newSection.ImageId != null)
|
||
{
|
||
createResource(exportConfiguration.resources.Where(r => r.id == newSection.ImageId).FirstOrDefault());
|
||
}
|
||
|
||
_myInfoMateDbContext.Sections.Add(newSection);
|
||
//_sectionService.Create(newSection);
|
||
|
||
// Le `switch` de 126 lignes qui vivait ici cherchait, section par
|
||
// section, quelles ressources de la charge créer — et il était commenté,
|
||
// donc seule l'image de la section arrivait. Remplacé par la boucle
|
||
// unique plus haut : l'export embarque désormais toutes les ressources
|
||
// référencées, l'import n'a plus à les redécouvrir.
|
||
}
|
||
|
||
return new ObjectResult("The configuration has been successfully imported") { StatusCode = 202 };
|
||
|
||
}
|
||
catch (ArgumentNullException ex)
|
||
{
|
||
return new BadRequestObjectResult(ex.Message) { };
|
||
}
|
||
catch (KeyNotFoundException ex)
|
||
{
|
||
return new NotFoundObjectResult(ex.Message) { };
|
||
}
|
||
catch (InvalidOperationException ex)
|
||
{
|
||
return new ConflictObjectResult(ex.Message) { };
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
return new ObjectResult(ex.Message) { StatusCode = 500 };
|
||
}
|
||
}
|
||
|
||
private void createResource(ResourceDTO resourceExport)
|
||
{
|
||
if (resourceExport != null)
|
||
{
|
||
Resource resource = new Resource();
|
||
resource.Id = resourceExport.id;
|
||
resource.InstanceId = resourceExport.instanceId;
|
||
resource.Type = resourceExport.type;
|
||
resource.Label = resourceExport.label;
|
||
resource.DateCreation = resourceExport.dateCreation ?? DateTime.Now.ToUniversalTime();
|
||
//resource.Data = resourceExport.data;
|
||
|
||
var resourceInDb = _myInfoMateDbContext.Resources.FirstOrDefault(r => r.Id == resourceExport.id);
|
||
|
||
if (resourceInDb == null)
|
||
_myInfoMateDbContext.Resources.Add(resource);
|
||
//_resourceService.Create(resource);
|
||
}
|
||
}
|
||
|
||
private List<ResourceDTO> addResourceToList(List<ResourceDTO> resourceDTOs, string resourceId) {
|
||
if (!resourceDTOs.Select(r => r.id).Contains(resourceId)) {
|
||
Resource resource = _myInfoMateDbContext.Resources.FirstOrDefault(r => r.Id == resourceId);
|
||
|
||
if (resource != null && !resourceDTOs.Any(r => r.id == resource.Id)) {
|
||
resourceDTOs.Add(resource.ToDTO());
|
||
}
|
||
}
|
||
return resourceDTOs;
|
||
}
|
||
}
|
||
}
|