Thomas Fransolet b87e49d808 Cle d'API sur les routes visiteur, canal VR et contenu immersif
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>
2026-09-13 16:54:10 +02:00

1134 lines
50 KiB
C#
Raw Blame History

using Hangfire;
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.Helpers;
using ManagerService.Services;
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;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Security.Cryptography;
using System.Text.Json;
using System.Threading.Tasks;
using static ManagerService.Data.SubSection.SectionEvent;
namespace ManagerService.Controllers
{
[Authorize(Policy = ManagerService.Service.Security.Policies.ContentEditor)]
[ApiController, Route("api/[controller]")]
[OpenApiTag("Section", Description = "Section management")]
public class SectionController : ControllerBase
{
private readonly MyInfoMateDbContext _myInfoMateDbContext;
private SectionDatabaseService _sectionService;
private ConfigurationDatabaseService _configurationService;
private readonly ILogger<SectionController> _logger;
private readonly IConfiguration _configuration;
IHexIdGeneratorService idService = new HexIdGeneratorService();
public SectionController(IConfiguration configuration, ILogger<SectionController> logger, SectionDatabaseService sectionService, ConfigurationDatabaseService configurationService, MyInfoMateDbContext myInfoMateDbContext)
{
_logger = logger;
_configuration = configuration;
_sectionService = sectionService;
_configurationService = configurationService;
_myInfoMateDbContext = myInfoMateDbContext;
}
/// <summary>
/// Get a list of all section (summary)
/// </summary>
/// <param name="id">id instance</param>
[ProducesResponseType(typeof(List<SectionDTO>), 200)]
[ProducesResponseType(typeof(string), 500)]
[ProducesResponseType(typeof(string), 400)]
[HttpGet]
public ObjectResult Get([FromQuery] string instanceId)
{
try
{
if (instanceId == null)
throw new ArgumentNullException("Param is null");
//List<OldSection> sections = _sectionService.GetAll(instanceId);
List<Section> sections = _myInfoMateDbContext.Sections.Where(s => s.InstanceId == instanceId).ToList();
/* CLEAN ARTICLE AUDIO - Init new field AudioIds */
/*foreach (var article in sections.Where(s => s.Type == SectionType.Article))
{
try
{
ArticleDTO articleDTO = JsonConvert.DeserializeObject<ArticleDTO>(article.Data);
List<string> languages = _configuration.GetSection("SupportedLanguages").Get<List<string>>();
articleDTO.audioIds = LanguageInit.Init("Audio", languages, true);
article.Data = JsonConvert.SerializeObject(articleDTO); // Include all info from specific section as JSON
Section sectionModified = _sectionService.Update(article.Id, article);
}
catch (Exception ex)
{
}
}*/
return new OkObjectResult(sections.Select(s => s.ToDTO()));
}
catch (Exception ex)
{
return new ObjectResult(ex.Message) { StatusCode = 500 };
}
}
/// <summary>
/// Get a list of all section (summary)
/// </summary>
/// <param name="id">id instance</param>
[ProducesResponseType(typeof(List<SectionDTO>), 200)]
[ProducesResponseType(typeof(string), 500)]
[ProducesResponseType(typeof(string), 400)]
[HttpGet("detail")]
public ObjectResult GetAllFromType([FromQuery] string instanceId, [FromQuery] SectionType sectionType)
{
try
{
if (instanceId == null)
throw new ArgumentNullException("Param is null");
List<Section> sections = _myInfoMateDbContext.Sections.Where(s => s.InstanceId == instanceId && s.Type == sectionType).OrderBy(s => s.Order).ToList();
//List<OldSection> sections = _sectionService.GetAllSubSection(id);
List<object> sectionsToReturn = new List<object>();
foreach (var section in sections)
{
var dto = SectionFactory.ToDTO(section);
sectionsToReturn.Add(dto);
}
return new OkObjectResult(sectionsToReturn);
}
catch (Exception ex)
{
return new ObjectResult(ex.Message) { StatusCode = 500 };
}
}
/// <summary>
/// Get a list of all section from a specific configuration
/// </summary>
/// <param name="id">configuration id</param>
[AllowAnonymous]
[RequireAppKey]
[ProducesResponseType(typeof(List<SectionDTO>), 200)]
[ProducesResponseType(typeof(string), 500)]
[ProducesResponseType(typeof(string), 400)]
[HttpGet("configuration/{id}")]
public ObjectResult GetFromConfiguration(string id)
{
try
{
if (id == null)
throw new ArgumentNullException("Param is null");
Configuration configuration = _myInfoMateDbContext.Configurations.FirstOrDefault(c => c.Id == id);
if (configuration != null)
{
List<Section> sections = _myInfoMateDbContext.Sections.Where(s => s.ConfigurationId == id && !s.IsSubSection).ToList();
//List<OldSection> sections = _sectionService.GetAllFromConfiguration(id);
return new OkObjectResult(sections.Select(r => r.ToDTO()));
}
else
return new NotFoundObjectResult("Configuration not found");
}
catch (ArgumentNullException ex)
{
return new BadRequestObjectResult(ex.Message) { };
}
catch (Exception ex)
{
return new ObjectResult(ex.Message) { StatusCode = 500 };
}
}
/// <summary>
/// Get a list of all section from a specific configuration (mobile format)
/// </summary>
/// <param name="id">configuration id</param>
[AllowAnonymous]
[RequireAppKey]
[ProducesResponseType(typeof(List<object>), 200)]
[ProducesResponseType(typeof(string), 500)]
[ProducesResponseType(typeof(string), 400)]
[HttpGet("configuration/{id}/detail")]
public async Task<ObjectResult> GetFromConfigurationDetail(string id)
{
try
{
if (id == null)
throw new ArgumentNullException("Param is null");
Configuration configuration = _myInfoMateDbContext.Configurations.FirstOrDefault(c => c.Id == id);
if (configuration != null)
{
List<Section> sections = _myInfoMateDbContext.Sections.Where(s => s.ConfigurationId == id && !s.IsSubSection).OrderBy(s => s.Order).ToList();
List<object> sectionsToReturn = new List<object>();
foreach (var section in sections)
{
var dto = SectionFactory.ToDTO(section);
switch (section.Type)
{
case SectionType.Agenda:
var eventAgendas = _myInfoMateDbContext.EventAgendas.Where(ea => ea.SectionAgendaId == section.Id).Include(ea => ea.Resource).ToList();
List<EventAgendaDTO> eventAgendaDTOs = new List<EventAgendaDTO>();
foreach (var eventAgenda in eventAgendas)
{
eventAgendaDTOs.Add(eventAgenda.ToDTO());
}
(dto as AgendaDTO).events = eventAgendaDTOs;
break;
case SectionType.Event:
var sectionEvent = _myInfoMateDbContext.Sections.OfType<SectionEvent>()
.Include(se => se.Programme).ThenInclude(se => se.MapAnnotations)
.Include(se => se.GlobalMapAnnotations)
.FirstOrDefault(s => s.Id == section.Id);
(dto as SectionEventDTO).Programme = sectionEvent.Programme; // TODO test ! Need dto ?
(dto as SectionEventDTO).GlobalMapAnnotations = sectionEvent.GlobalMapAnnotations?.Select(ma => ma.ToDTO()).ToList() ?? new();
break;
case SectionType.Slider:
var sliderContents = (dto as SliderDTO).contents;
if (sliderContents != null)
{
foreach (var content in sliderContents)
{
if (content.resourceId != null && content.resource == null)
{
var contentResource = _myInfoMateDbContext.Resources.FirstOrDefault(r => r.Id == content.resourceId);
content.resource = contentResource?.ToDTO();
}
}
}
break;
case SectionType.Game:
Resource resource = _myInfoMateDbContext.Resources.FirstOrDefault(r => r.Id == (dto as GameDTO).puzzleImageId);
(dto as GameDTO).puzzleImage = resource?.ToDTO();
break;
case SectionType.Map:
var geoPoints = _myInfoMateDbContext.GeoPoints.Where(gp => gp.SectionMapId == section.Id)/*.OrderBy(gp => gp.or)*/.ToList();
List<GeoPointDTO> geoPointDTOs = new List<GeoPointDTO>();
foreach (var geoPoint in geoPoints)
{
geoPointDTOs.Add(new GeoPointDTO() {
id = geoPoint.Id,
title = geoPoint.Title,
description = geoPoint.Description,
contents = geoPoint.Contents,
categorieId = geoPoint.CategorieId,
imageResourceId = geoPoint.ImageResourceId,
imageUrl = geoPoint.ImageUrl,
schedules = geoPoint.Schedules,
prices = geoPoint.Prices,
phone = geoPoint.Phone,
email = geoPoint.Email,
site = geoPoint.Site,
polyColor = geoPoint.PolyColor,
geometry = geoPoint.Geometry?.ToDto()
});
}
(dto as MapDTO).points = geoPointDTOs;
break;
case SectionType.Quiz:
var quizQuestions = _myInfoMateDbContext.QuizQuestions.Where(qq => qq.SectionQuizId == section.Id).OrderBy(q => q.Order).ToList();
List<QuestionDTO> questionDTOs = new List<QuestionDTO>();
foreach (var quizQuestion in quizQuestions)
{
questionDTOs.Add(new QuestionDTO()
{
id = quizQuestion.Id,
label = quizQuestion.Label,
responses = quizQuestion.Responses,
imageBackgroundResourceId = quizQuestion.ResourceId,
imageBackgroundResourceType = quizQuestion.Resource?.Type,
imageBackgroundResourceUrl = quizQuestion.Resource?.Url,
order = quizQuestion.Order,
});
}
(dto as QuizDTO).questions = questionDTOs;
break;
case SectionType.Menu:
var subSections = _myInfoMateDbContext.Sections.Where(s => s.IsSubSection && s.ParentId == section.Id).OrderBy(s => s.Order).ToList();
List<object> subSectionToReturn = new List<object>();
foreach (var subSection in subSections)
{
var subDTO = SectionFactory.ToDTO(subSection);
switch (subSection.Type)
{
case SectionType.Slider:
var sliderContentsSub = (subDTO as SliderDTO).contents;
if (sliderContentsSub != null)
{
foreach (var content in sliderContentsSub)
{
if (content.resourceId != null && content.resource == null)
{
var contentResource = _myInfoMateDbContext.Resources.FirstOrDefault(r => r.Id == content.resourceId);
content.resource = contentResource?.ToDTO();
}
}
}
break;
case SectionType.Game:
Resource resourceSub = _myInfoMateDbContext.Resources.FirstOrDefault(r => r.Id == (subDTO as GameDTO).puzzleImageId);
(subDTO as GameDTO).puzzleImage = resourceSub?.ToDTO();
break;
case SectionType.Map:
var geoPointsSub = _myInfoMateDbContext.GeoPoints.Where(gp => gp.SectionMapId == subSection.Id).ToList();
List<GeoPointDTO> geoPointDTOsSub = new List<GeoPointDTO>();
foreach (var geoPointSub in geoPointsSub)
{
geoPointDTOsSub.Add(new GeoPointDTO()
{
id = geoPointSub.Id,
title = geoPointSub.Title,
description = geoPointSub.Description,
contents = geoPointSub.Contents,
categorieId = geoPointSub.CategorieId,
imageResourceId = geoPointSub.ImageResourceId,
imageUrl = geoPointSub.ImageUrl,
schedules = geoPointSub.Schedules,
prices = geoPointSub.Prices,
phone = geoPointSub.Phone,
email = geoPointSub.Email,
site = geoPointSub.Site,
polyColor = geoPointSub.PolyColor,
geometry = geoPointSub.Geometry?.ToDto()
});
}
(subDTO as MapDTO).points = geoPointDTOsSub;
break;
case SectionType.Quiz:
var quizQuestionsSub = _myInfoMateDbContext.QuizQuestions.Where(qq => qq.SectionQuizId == subSection.Id).OrderBy(q => q.Order).ToList();
List<QuestionDTO> questionDTOsSub = new List<QuestionDTO>();
foreach (var quizQuestionSub in quizQuestionsSub)
{
questionDTOsSub.Add(new QuestionDTO()
{
id = quizQuestionSub.Id,
label = quizQuestionSub.Label,
responses = quizQuestionSub.Responses,
imageBackgroundResourceId = quizQuestionSub.ResourceId,
imageBackgroundResourceType = quizQuestionSub.Resource?.Type,
imageBackgroundResourceUrl = quizQuestionSub.Resource?.Url,
order = quizQuestionSub.Order,
});
}
(subDTO as QuizDTO).questions = questionDTOsSub;
break;
}
subSectionToReturn.Add(subDTO);
}
(dto as MenuDTO).sections = subSectionToReturn;
break;
case SectionType.Parcours:
var parcoursPaths = _myInfoMateDbContext.GuidedPaths
.Include(gp => gp.ImageResource)
.Include(gp => gp.Steps).ThenInclude(s => s.QuizQuestions)
.Where(gp => gp.SectionParcoursId == section.Id)
.OrderBy(gp => gp.Order)
.ToList();
(dto as ParcoursDTO).guidedPaths = parcoursPaths.Select(gp => gp.ToDTO()).ToList();
break;
}
sectionsToReturn.Add(dto);
}
return new OkObjectResult(sectionsToReturn);
}
else
return new NotFoundObjectResult("Configuration not found");
}
catch (ArgumentNullException ex)
{
return new BadRequestObjectResult(ex.Message) { };
}
catch (Exception ex)
{
return new ObjectResult(ex.Message) { StatusCode = 500 };
}
}
/// <summary>
/// Delete all section from a specific configuration
/// </summary>
/// <param name="id">configuration id</param>
[ProducesResponseType(typeof(string), 202)]
[ProducesResponseType(typeof(string), 500)]
[ProducesResponseType(typeof(string), 400)]
[HttpDelete("configuration/{id}")]
public ObjectResult DeleteAllForConfiguration(string id)
{
try
{
if (id == null)
throw new ArgumentNullException("Param is null");
//_sectionService.DeleteAllFromConfiguration(id);
List<Section> sections = _myInfoMateDbContext.Sections.Where(s => s.ConfigurationId == id).ToList();
// TODO test
_myInfoMateDbContext.RemoveRange(sections);
return new ObjectResult("All section from the specified configuration has been deleted") { StatusCode = 202 };
}
catch (ArgumentNullException ex)
{
return new BadRequestObjectResult(ex.Message) { };
}
catch (Exception ex)
{
return new ObjectResult(ex.Message) { StatusCode = 500 };
}
}
/// <summary>
/// Get a list of all subsection (summary) of a specific section
/// </summary>
/// <param name="id">section id</param>
[ProducesResponseType(typeof(List<object>), 200)]
[ProducesResponseType(typeof(string), 500)]
[ProducesResponseType(typeof(string), 400)]
[HttpGet("{id}/subsections")]
public ObjectResult GetAllSectionSubSections(string id)
{
try
{
if (id == null)
throw new ArgumentNullException("Param is null");
List<Section> sections = _myInfoMateDbContext.Sections.Where(s => s.ParentId == id && s.IsSubSection).OrderBy(s=> s.Order).ToList();
//List<OldSection> sections = _sectionService.GetAllSubSection(id);
List<object> sectionsToReturn = new List<object>();
foreach (var section in sections)
{
var dto = SectionFactory.ToDTO(section);
sectionsToReturn.Add(dto);
}
return new OkObjectResult(sectionsToReturn);
}
catch (ArgumentNullException ex)
{
return new BadRequestObjectResult(ex.Message) { };
}
catch (Exception ex)
{
return new ObjectResult(ex.Message) { StatusCode = 500 };
}
}
/// <summary>
/// Get a specific section
/// </summary>
/// <param name="id">section id</param>
[AllowAnonymous]
[RequireAppKey]
[ProducesResponseType(typeof(object), 200)]
[ProducesResponseType(typeof(string), 404)]
[ProducesResponseType(typeof(string), 500)]
[HttpGet("{id}")]
public ObjectResult GetDetail(string id)
{
try
{
Section section = _myInfoMateDbContext.Sections.FirstOrDefault(s => s.Id == id);
if (section == null)
throw new KeyNotFoundException("This section was not found");
var dto = SectionFactory.ToDTO(section);
return new OkObjectResult(dto);
}
catch (KeyNotFoundException ex)
{
return new NotFoundObjectResult(ex.Message) {};
}
catch (Exception ex)
{
return new ObjectResult(ex.Message) { StatusCode = 500 };
}
}
/// <summary>
/// Get all section with beacon
/// </summary>
/// <param name="instanceId">Instance id</param>
[AllowAnonymous]
[RequireAppKey]
[ProducesResponseType(typeof(List<SectionDTO>), 200)]
[ProducesResponseType(typeof(string), 404)]
[ProducesResponseType(typeof(string), 500)]
[HttpGet("beacons/{instanceId}")]
public ObjectResult GetAllBeaconsForInstance(string instanceId)
{
try
{
List<Section> sections = _myInfoMateDbContext.Sections.Where(s => s.InstanceId == instanceId && s.IsBeacon && s.BeaconId != null).ToList();
//List<OldSection> sections = _sectionService.GetAll(instanceId);
//sections = sections.Where(s => s.IsBeacon && s.BeaconId != null).ToList();
return new OkObjectResult(sections.Select(s => s.ToDTO()));
}
catch (KeyNotFoundException ex)
{
return new NotFoundObjectResult(ex.Message) { };
}
catch (Exception ex)
{
return new ObjectResult(ex.Message) { StatusCode = 500 };
}
}
/// <summary>
/// Create a new section
/// </summary>
/// <param name="newSection">New section info</param>
[ProducesResponseType(typeof(SectionDTO), 200)]
[ProducesResponseType(typeof(string), 400)]
[ProducesResponseType(typeof(string), 409)]
[ProducesResponseType(typeof(string), 500)]
[HttpPost()]
public ObjectResult Create([FromBody] SectionDTO newSection)
{
try
{
if (newSection == null)
throw new ArgumentNullException("Section param is null");
if (newSection.configurationId == null)
throw new ArgumentNullException("Configuration param is null");
var configuration = _myInfoMateDbContext.Configurations.FirstOrDefault(c => c.Id == newSection.configurationId);
if (configuration == null)
throw new KeyNotFoundException("Configuration does not exist");
// Preparation
List<string> languages = _configuration.GetSection("SupportedLanguages").Get<List<string>>();
Section section = SectionFactory.CreateEmpty(newSection.type);
if (section is SectionArticle article)
{
article.ArticleContent = LanguageInit.Init("Content", languages);
article.ArticleAudioIds = LanguageInit.Init("Audio", languages, true);
}
section.InstanceId = newSection.instanceId;
section.Label = newSection.label;
section.ImageId = newSection.imageId;
section.ImageSource = newSection.imageSource;
section.ConfigurationId = newSection.configurationId;
section.DateCreation = newSection.dateCreation == null ? DateTime.Now.ToUniversalTime() : newSection.dateCreation.Value;
section.IsSubSection = newSection.isSubSection;
section.ParentId = newSection.parentId;
section.Type = newSection.type;
// TODO test that in new format
section.Order = _myInfoMateDbContext.Sections.Count(s => s.ConfigurationId == newSection.configurationId && !s.IsSubSection) + 1;
/*if (configuration.IsMobile)
{
section.Order = _myInfoMateDbContext.Sections.Count(s => s.ConfigurationId == newSection.configurationId && !s.IsSubSection && (s.Type == SectionType.Article || s.Type == SectionType.Quiz)) + 1;
}
else
{
section.Order = 0; // _myInfoMateDbContext.Sections.Count(s => s.ConfigurationId == newSection.configurationId && !s.IsSubSection) + 1;
}*/
section.IsBeacon = newSection.isBeacon;
section.BeaconId = newSection.beaconId;
section.Latitude = newSection.latitude;
section.Longitude = newSection.longitude;
section.MeterZoneGPS = newSection.meterZoneGPS;
section.Title = LanguageInit.Init("Title", languages);
section.Description = LanguageInit.Init("Description", languages);
section.Id = idService.GenerateHexId();
//_sectionService.Create(section);
_myInfoMateDbContext.Add(section);
_myInfoMateDbContext.SaveChanges();
// UPDATE OTHER ORDER
var sections = _myInfoMateDbContext.Sections.Where(s => s.ConfigurationId == newSection.configurationId && !s.IsSubSection).OrderBy(s => s.Order).ToList();
// Retirer la question d<>plac<61>e
sections.RemoveAll(q => q.Id == section.Id);
// Ins<6E>rer <20> la premi<6D>re position (d<>j<EFBFBD> en 0-based)
sections.Insert(0, section);
// R<>assigner les ordres en 0-based
for (int i = 0; i < sections.Count; i++)
{
sections[i].Order = i;
}
_myInfoMateDbContext.SaveChanges();
return new OkObjectResult(section.ToDTO());
}
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>
/// Create a new section - slider
/// </summary>
/// <param name="newSection">New section info - slider</param>
[ProducesResponseType(typeof(SliderDTO), 200)]
[ProducesResponseType(typeof(string), 400)]
[ProducesResponseType(typeof(string), 409)]
[ProducesResponseType(typeof(string), 500)]
[HttpPost("Slider")]
public ObjectResult CreateSlider([FromBody] SliderDTO newSectionSlider)
{
try
{
if (newSectionSlider == null)
throw new ArgumentNullException("Section param is null");
// Todo add some verification ?
Slider sliderSection = new Slider();
sliderSection.Label = newSectionSlider.Label;
sliderSection.ImageId = newSectionSlider.ImageId;
sliderSection.DateCreation = DateTime.Now;
sliderSection.IsSubSection = false;
sliderSection.ParentId = null;
sliderSection.Type = SectionType.Slider;
sliderSection.Images = newSectionSlider.Images.Select(p =>
new Image()
{
Title = p.Title,
Description = p.Description,
Source = p.Source,
}).ToList();
Slider sectionCreated = _sectionService.CreateSlider(sliderSection);
return new OkObjectResult(sectionCreated.ToDTO());
}
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 section
/// </summary>
/// <param name="updatedSection">Section to update</param>
[ProducesResponseType(typeof(object), 200)]
[ProducesResponseType(typeof(string), 400)]
[ProducesResponseType(typeof(string), 404)]
[ProducesResponseType(typeof(string), 500)]
[HttpPut]
public ObjectResult Update([FromBody] dynamic updatedSection)
{
try
{
if (updatedSection.ValueKind == JsonValueKind.Null)
throw new ArgumentNullException("Section param is null");
SectionDTO sectionDTO;
if (updatedSection is JsonElement jsonElement)
{
if (jsonElement.ValueKind == JsonValueKind.Null)
throw new ArgumentNullException("Section param is null");
// D<>s<EFBFBD>rialisation de jsonElement en SectionDTO
sectionDTO = JsonConvert.DeserializeObject<SectionDTO>(jsonElement.ToString());
}
else
{
throw new InvalidOperationException("Expected a JsonElement");
}
Section existingSection = _myInfoMateDbContext.Sections.FirstOrDefault(s => s.Id == sectionDTO.id);
if (existingSection == null)
throw new KeyNotFoundException("Section does not exist");
if (existingSection.Type != sectionDTO.type)
return BadRequest("Type mismatch: cannot change section type");
if (existingSection.IsSubSection && sectionDTO.order != existingSection.Order)
{
// If subsection, check if order changed
var subSections = _myInfoMateDbContext.Sections.Where(s => s.ParentId == existingSection.ParentId).OrderBy(s => s.Order).ToList();
// Retirer la sous section d<>plac<61>e
subSections.RemoveAll(q => q.Id == existingSection.Id);
// Ins<6E>rer <20> la nouvelle position (d<>j<EFBFBD> en 0-based)
int newIndex = sectionDTO.order.Value;
newIndex = Math.Clamp(newIndex, 0, subSections.Count);
subSections.Insert(newIndex, existingSection);
// R<>assigner les ordres en 0-based
for (int i = 0; i < subSections.Count; i++)
{
subSections[i].Order = i;
}
_myInfoMateDbContext.SaveChanges();
}
else
{
// classic update
var updatedSectionDB = SectionFactory.Create(updatedSection, sectionDTO);
_myInfoMateDbContext.Entry(existingSection).CurrentValues.SetValues(updatedSectionDB);
_myInfoMateDbContext.SaveChanges();
MqttClientService.PublishMessage($"config/{existingSection.ConfigurationId}", JsonConvert.SerializeObject(new PlayerMessageDTO() { configChanged = true }));
// Un agenda en ligne doit d'abord rapatrier ses événements : la ré-indexation
// suivra le SaveChanges d'AgendaSyncService, donc sur les dates fraîches.
// Pour tous les autres cas, SectionIndexingInterceptor a déjà pris le relais
// au SaveChanges ci-dessus — rien à enqueue ici.
var syncedAgenda = existingSection.Type == SectionType.Agenda
&& _myInfoMateDbContext.Sections.OfType<SectionAgenda>()
.Any(s => s.Id == existingSection.Id && s.IsOnlineAgenda && s.AgendaResourceIds.Count > 0);
if (syncedAgenda)
BackgroundJob.Enqueue<AgendaSyncService>(s => s.SyncSectionAsync(existingSection.Id));
}
return new OkObjectResult(SectionFactory.ToDTO(existingSection));
}
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>
/// Show or hide a section in the visitor apps
/// </summary>
/// <param name="id">Section id</param>
/// <param name="isActive">true = visible, false = hidden</param>
/// <remarks>
/// Endpoint dédié plutôt que <see cref="Update"/> : celui-ci reconstruit le
/// sous-type via SectionFactory, donc un SectionDTO nu effacerait tout le
/// contenu spécifique de la section.
/// </remarks>
[ProducesResponseType(typeof(object), 200)]
[ProducesResponseType(typeof(string), 404)]
[ProducesResponseType(typeof(string), 500)]
[HttpPut("{id}/visibility")]
public ObjectResult SetVisibility(string id, [FromQuery] bool isActive)
{
try
{
Section section = _myInfoMateDbContext.Sections.FirstOrDefault(s => s.Id == id);
if (section == null)
throw new KeyNotFoundException("Section does not exist");
section.IsActive = isActive;
_myInfoMateDbContext.SaveChanges();
MqttClientService.PublishMessage($"config/{section.ConfigurationId}", JsonConvert.SerializeObject(new PlayerMessageDTO() { configChanged = true }));
return new OkObjectResult(SectionFactory.ToDTO(section));
}
catch (KeyNotFoundException ex)
{
return new NotFoundObjectResult(ex.Message) { };
}
catch (Exception ex)
{
return new ObjectResult(ex.Message) { StatusCode = 500 };
}
}
/// <summary>
/// Update sections order
/// </summary>
/// <param name="updatedSectionsOrder">New sections order</param>
[ProducesResponseType(typeof(string), 200)]
[ProducesResponseType(typeof(string), 400)]
[ProducesResponseType(typeof(string), 404)]
[ProducesResponseType(typeof(string), 500)]
[HttpPut("order")]
public ObjectResult UpdateOrder([FromBody] List<SectionDTO> updatedSectionsOrder)
{
try
{
if (updatedSectionsOrder == null)
throw new ArgumentNullException("Sections param is null");
foreach (var section in updatedSectionsOrder)
{
var sectionDB = _myInfoMateDbContext.Sections.FirstOrDefault(s => s.Id == section.id);
if (sectionDB == null)
throw new KeyNotFoundException($"Section {section.label} with id {section.id} does not exist");
}
// The client may only send a subset of the configuration's sections (e.g. a filtered
// reorder view) — renormalize against the full top-level set so `Order` always stays
// 0-based and contiguous, instead of blindly writing the payload's values.
foreach (var configurationId in updatedSectionsOrder.Select(s => s.configurationId).Distinct())
{
var baseline = _myInfoMateDbContext.Sections
.Where(s => s.ConfigurationId == configurationId && !s.IsSubSection)
.OrderBy(s => s.Order)
.ToList();
var includedIds = updatedSectionsOrder
.Where(s => s.configurationId == configurationId)
.Select(s => s.id)
.ToHashSet();
var newSubsequence = updatedSectionsOrder
.Where(s => s.configurationId == configurationId)
.OrderBy(s => s.order.GetValueOrDefault())
.Select(s => baseline.First(b => b.Id == s.id))
.ToList();
var slots = baseline
.Select((section, index) => (section, index))
.Where(t => includedIds.Contains(t.section.Id))
.Select(t => t.index)
.ToList();
for (int k = 0; k < slots.Count; k++)
baseline[slots[k]] = newSubsequence[k];
for (int i = 0; i < baseline.Count; i++)
baseline[i].Order = i;
}
_myInfoMateDbContext.SaveChanges();
if (updatedSectionsOrder.Count > 0) {
MqttClientService.PublishMessage($"config/{updatedSectionsOrder[0].configurationId}", JsonConvert.SerializeObject(new PlayerMessageDTO() { configChanged = true }));
}
return new ObjectResult("Sections order has been successfully modified") { StatusCode = 200 };
}
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 section
/// </summary>
/// <param name="id">Id of section 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("Section param is null");
var section = _myInfoMateDbContext.Sections.FirstOrDefault(s => s.Id == id);
if (section == null)
throw new KeyNotFoundException("Section does not exist");
if (section.Type == SectionType.Agenda)
{
var sectionAgenda = _myInfoMateDbContext.Sections.OfType<SectionAgenda>().Include(sa => sa.EventAgendas).FirstOrDefault(sa => sa.Id == id);
_myInfoMateDbContext.RemoveRange(sectionAgenda.EventAgendas);
}
if (section.Type == SectionType.Event)
{
var sectionEvent = _myInfoMateDbContext.Sections.OfType<SectionEvent>().Include(se => se.Programme).ThenInclude(se => se.MapAnnotations).FirstOrDefault(s => s.Id == id);
foreach (var programBlock in sectionEvent.Programme)
{
_myInfoMateDbContext.RemoveRange(programBlock.MapAnnotations);
_myInfoMateDbContext.Remove(programBlock);
}
var guidedPaths = _myInfoMateDbContext.GuidedPaths.Include(gp => gp.Steps).Where(gp => gp.SectionEventId == id);
foreach (var guidedPath in guidedPaths)
{
_myInfoMateDbContext.RemoveRange(guidedPath.Steps);
_myInfoMateDbContext.Remove(guidedPath);
}
var applicationInstances = _myInfoMateDbContext.ApplicationInstances.Where(ai => ai.SectionEventId == id);
foreach (var applicationInstance in applicationInstances)
{
applicationInstance.SectionEventId = null;
}
}
if (section.Type == SectionType.Map)
{
// REMOVE ALL POINTS
var geoPoints = _myInfoMateDbContext.GeoPoints.Where(gp => gp.SectionMapId == section.Id).ToList();
_myInfoMateDbContext.RemoveRange(geoPoints);
}
if (section.Type == SectionType.Quiz)
{
// REMOVE ALL Questions
var quizQuestions = _myInfoMateDbContext.QuizQuestions.Where(qq => qq.SectionQuizId == section.Id).ToList();
_myInfoMateDbContext.RemoveRange(quizQuestions);
}
if (section.Type == SectionType.Parcours)
{
var guidedPaths = _myInfoMateDbContext.GuidedPaths.Include(gp => gp.Steps).Where(gp => gp.SectionParcoursId == id);
foreach (var guidedPath in guidedPaths)
{
_myInfoMateDbContext.RemoveRange(guidedPath.Steps);
_myInfoMateDbContext.Remove(guidedPath);
}
}
_myInfoMateDbContext.Remove(section);
var configuration = _myInfoMateDbContext.Configurations.FirstOrDefault(c => c.Id == section.ConfigurationId);
List<Section> sections = _myInfoMateDbContext.Sections.Where(s => s.ConfigurationId == section.ConfigurationId && !s.IsSubSection).ToList();
int i = 0;
List<Section> orderedSection = sections.OrderBy(s => s.Order).ToList();
foreach (var sectionDb in orderedSection)
{
sectionDb.Order = i;
i++;
}
_myInfoMateDbContext.SaveChanges();
return new ObjectResult("The section 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>
/// Useless, just to generate dto code
/// </summary>
[ProducesResponseType(typeof(MapDTO), 200)]
[HttpGet("MapDTO")]
public ObjectResult GetMapDTO()
{
return new ObjectResult("MapDTO") { StatusCode = 200 };
}
/// <summary>
/// Useless, just to generate dto code
/// </summary>
[ProducesResponseType(typeof(SliderDTO), 200)]
[HttpGet("SliderDTO")]
public ObjectResult GetSliderDTO()
{
return new ObjectResult("SliderDTO") { StatusCode = 200 };
}
/// <summary>
/// Useless, just to generate dto code
/// </summary>
[ProducesResponseType(typeof(VideoDTO), 200)]
[HttpGet("VideoDTO")]
public ObjectResult GetVideoDTO()
{
return new ObjectResult("VideoDTO") { StatusCode = 200 };
}
/// <summary>
/// Useless, just to generate dto code
/// </summary>
[ProducesResponseType(typeof(WebDTO), 200)]
[HttpGet("WebDTO")]
public ObjectResult GetWebDTO()
{
return new ObjectResult("WebDTO") { StatusCode = 200 };
}
/// <summary>
/// Useless, just to generate dto code
/// </summary>
[ProducesResponseType(typeof(MenuDTO), 200)]
[HttpGet("MenuDTO")]
public ObjectResult GetMenuDTO()
{
return new ObjectResult("MenuDTO") { StatusCode = 200 };
}
/// <summary>
/// Useless, just to generate dto code
/// </summary>
[ProducesResponseType(typeof(PlayerMessageDTO), 200)]
[HttpGet("PlayerMessageDTO")]
public ObjectResult PlayerMessageDTO()
{
return new ObjectResult("PlayerMessageDTO") { StatusCode = 200 };
}
/// <summary>
/// Useless, just to generate dto code
/// </summary>
[ProducesResponseType(typeof(QuizDTO), 200)]
[HttpGet("QuizDTO")]
public ObjectResult GetQuizDTO()
{
return new ObjectResult("QuizDTO") { StatusCode = 200 };
}
/// <summary>
/// Useless, just to generate dto code
/// </summary>
[ProducesResponseType(typeof(ArticleDTO), 200)]
[HttpGet("ArticleDTO")]
public ObjectResult GetArticleDTO()
{
return new ObjectResult("ArticleDTO") { StatusCode = 200 };
}
/// <summary>
/// Useless, just to generate dto code
/// </summary>
[ProducesResponseType(typeof(PdfDTO), 200)]
[HttpGet("PdfDTO")]
public ObjectResult GetPdfDTO()
{
return new ObjectResult("PdfDTO") { StatusCode = 200 };
}
/// <summary>
/// Useless, just to generate dto code
/// </summary>
[ProducesResponseType(typeof(GameDTO), 200)]
[HttpGet("PuzzleDTO")]
public ObjectResult GetPuzzleDTO()
{
return new ObjectResult("PuzzleDTO") { StatusCode = 200 };
}
/// <summary>
/// Useless, just to generate dto code
/// </summary>
[ProducesResponseType(typeof(AgendaDTO), 200)]
[HttpGet("AgendaDTO")]
public ObjectResult GetAgendaDTO()
{
return new ObjectResult("AgendaDTO") { StatusCode = 200 };
}
/// <summary>
/// Useless, just to generate dto code
/// </summary>
[ProducesResponseType(typeof(WeatherDTO), 200)]
[HttpGet("WeatherDTO")]
public ObjectResult GetWeatherDTO()
{
return new ObjectResult("WeatherDTO") { StatusCode = 200 };
}
/// <summary>
/// Useless, just to generate dto code
/// </summary>
[ProducesResponseType(typeof(SectionEventDTO), 200)]
[HttpGet("SectionEventDTO")]
public ObjectResult GetSectionEventDTO()
{
return new ObjectResult("SectionEventDTO") { StatusCode = 200 };
}
/// <summary>
/// Useless, just to generate dto code
/// </summary>
[ProducesResponseType(typeof(GuidedPathDTO), 200)]
[HttpGet("GuidedPathDTO")]
public ObjectResult GetGuidedPathDTO()
{
return new ObjectResult("GuidedPathDTO") { StatusCode = 200 };
}
}
}