manager-service/ManagerService/Controllers/SectionEventController.cs
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

472 lines
19 KiB
C#

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 NSwag.Annotations;
using System;
using System.Collections.Generic;
using System.Linq;
using static ManagerService.Data.SubSection.SectionEvent;
namespace ManagerService.Controllers
{
[Authorize(Policy = ManagerService.Service.Security.Policies.ContentEditor)]
[ApiController, Route("api/[controller]")]
[OpenApiTag("Section event", Description = "Section event management")]
public class SectionEventController : ControllerBase
{
private readonly MyInfoMateDbContext _myInfoMateDbContext;
private readonly ILogger<SectionEventController> _logger;
private readonly IConfiguration _configuration;
IHexIdGeneratorService idService = new HexIdGeneratorService();
public SectionEventController(IConfiguration configuration, ILogger<SectionEventController> logger, MyInfoMateDbContext myInfoMateDbContext)
{
_logger = logger;
_configuration = configuration;
_myInfoMateDbContext = myInfoMateDbContext;
}
/// <summary>
/// Get all programme block from section
/// </summary>
/// <param name="sectionEventId">Section id</param>
[AllowAnonymous]
[RequireAppKey]
[ProducesResponseType(typeof(List<ProgrammeBlock>), 200)]
[ProducesResponseType(typeof(string), 404)]
[ProducesResponseType(typeof(string), 500)]
[HttpGet("{sectionEventId}/programmes")]
public ObjectResult GetAllProgrammeBlockFromSection(string sectionEventId)
{
try
{
SectionEvent sectionEvent = _myInfoMateDbContext.Sections.OfType<SectionEvent>().Include(se => se.Programme).ThenInclude(se => se.MapAnnotations).ThenInclude(se => se.IconResource).FirstOrDefault(se => se.Id == sectionEventId);
if (sectionEvent == null)
throw new KeyNotFoundException("Section event does not exist");
/*List<ProgrammeBlock> programmeBlocks = new List<ProgrammeBlock>();
foreach (var program in sectionEvent.Programme)
{
foreach (var mapAnnotation in program.MapAnnotations) {
var resource = _myInfoMateDbContext.Resources.FirstOrDefault(r => r.Id == mapAnnotation.IconResourceId);
if (resource != null)
{
mapAnnotation.IconResource = resource; // TO check.. use DTO instead ?
}
}
}*/
return new OkObjectResult(sectionEvent.Programme);
}
catch (KeyNotFoundException ex)
{
return new NotFoundObjectResult(ex.Message) { };
}
catch (Exception ex)
{
return new ObjectResult(ex.Message) { StatusCode = 500 };
}
}
/// <summary>
/// Create new programme block
/// </summary>
/// <param name="sectionEventId">Section event Id</param>
/// <param name="programmeBlockDTO">Programme block</param>
[ProducesResponseType(typeof(ProgrammeBlockDTO), 200)]
[ProducesResponseType(typeof(string), 400)]
[ProducesResponseType(typeof(string), 409)]
[ProducesResponseType(typeof(string), 500)]
[HttpPost("{sectionEventId}/programmes")]
public ObjectResult CreateProgrammeBlock(string sectionEventId, [FromBody] ProgrammeBlockDTO programmeBlockDTO)
{
try
{
if (sectionEventId == null)
throw new ArgumentNullException("Section param is null");
if (programmeBlockDTO == null)
throw new ArgumentNullException("ProgrammeBlock param is null");
var existingSection = _myInfoMateDbContext.Sections.OfType<SectionEvent>().Include(se => se.Programme).FirstOrDefault(se => se.Id == sectionEventId);
if (existingSection == null)
throw new KeyNotFoundException("Section event does not exist");
ProgrammeBlock programmeBlock = new ProgrammeBlock().FromDTO(programmeBlockDTO);
programmeBlock.Id = idService.GenerateHexId();
existingSection.Programme.Add(programmeBlock);
_myInfoMateDbContext.SaveChanges();
return new OkObjectResult(programmeBlock.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 program block
/// </summary>
/// <param name="programmeBlock">ProgramBlock to update</param>
[ProducesResponseType(typeof(ProgrammeBlockDTO), 200)]
[ProducesResponseType(typeof(string), 400)]
[ProducesResponseType(typeof(string), 404)]
[ProducesResponseType(typeof(string), 500)]
[HttpPut("programmes")]
public ObjectResult UpdateProgrammeBlock([FromBody] ProgrammeBlockDTO programmeBlockDTO)
{
try
{
if (programmeBlockDTO == null)
throw new ArgumentNullException("ProgrammeBlock param is null");
var existingProgramBlock = _myInfoMateDbContext.ProgrammeBlocks.FirstOrDefault(pb => pb.Id == programmeBlockDTO.id);
if (existingProgramBlock == null)
throw new KeyNotFoundException("ProgrammeBlock does not exist");
existingProgramBlock.Title = programmeBlockDTO.title;
existingProgramBlock.Description = programmeBlockDTO.description;
existingProgramBlock.StartTime = programmeBlockDTO.startTime;
existingProgramBlock.EndTime = programmeBlockDTO.endTime;
_myInfoMateDbContext.SaveChanges();
return new OkObjectResult(existingProgramBlock);
}
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 programBlock
/// </summary>
/// <param name="programBlockId">Id of program block to delete</param>
[ProducesResponseType(typeof(string), 202)]
[ProducesResponseType(typeof(string), 404)]
[ProducesResponseType(typeof(string), 500)]
[HttpDelete("programmes/{programBlockId}")]
public ObjectResult DeleteProgrammeBlock(string programBlockId)
{
try
{
var programBlock = _myInfoMateDbContext.ProgrammeBlocks.Include(pb => pb.MapAnnotations).FirstOrDefault(pb => pb.Id == programBlockId);
if (programBlock == null)
throw new KeyNotFoundException("ProgramBlock does not exist");
foreach (var mapAnnotation in programBlock.MapAnnotations) // Is it really needed?
{
_myInfoMateDbContext.Remove(mapAnnotation);
}
_myInfoMateDbContext.Remove(programBlock);
_myInfoMateDbContext.SaveChanges();
return new ObjectResult("The programBlock 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>
/// Get all map annotation from program block
/// </summary>
/// <param name="programBlockId">Program block id</param>
[AllowAnonymous]
[RequireAppKey]
[ProducesResponseType(typeof(List<MapAnnotationDTO>), 200)]
[ProducesResponseType(typeof(string), 404)]
[ProducesResponseType(typeof(string), 500)]
[HttpGet("{programBlockId}/map-annotations")]
public ObjectResult GetAllMapAnnotationsFromProgrammeBlock(string programBlockId)
{
try
{
ProgrammeBlock programmeBlock = _myInfoMateDbContext.ProgrammeBlocks.Include(pb => pb.MapAnnotations).ThenInclude(pb => pb.IconResource).FirstOrDefault(pb => pb.Id == programBlockId);
if (programmeBlock == null)
throw new KeyNotFoundException("ProgrammeBlock does not exist");
/*List<ProgrammeBlock> programmeBlocks = new List<ProgrammeBlock>();
foreach (var program in sectionEvent.Programme)
{
foreach (var mapAnnotation in program.MapAnnotations) {
var resource = _myInfoMateDbContext.Resources.FirstOrDefault(r => r.Id == mapAnnotation.IconResourceId);
if (resource != null)
{
mapAnnotation.IconResource = resource; // TO check.. use DTO instead ?
}
}
}*/
return new OkObjectResult(programmeBlock.MapAnnotations.Select(ma => ma.ToDTO()));
}
catch (KeyNotFoundException ex)
{
return new NotFoundObjectResult(ex.Message) { };
}
catch (Exception ex)
{
return new ObjectResult(ex.Message) { StatusCode = 500 };
}
}
/// <summary>
/// Create new map annotation
/// </summary>
/// <param name="programmeBlockId">Programme block id</param>
/// <param name="mapAnnotation">Map annotation</param>
[ProducesResponseType(typeof(MapAnnotationDTO), 200)]
[ProducesResponseType(typeof(string), 400)]
[ProducesResponseType(typeof(string), 409)]
[ProducesResponseType(typeof(string), 500)]
[HttpPost("{programmeBlockId}/map-annotations")]
public ObjectResult CreateMapAnnotation(string programmeBlockId, [FromBody] MapAnnotationDTO mapAnnotationDTO)
{
try
{
if (programmeBlockId == null)
throw new ArgumentNullException("ProgrammeBlockId param is null");
if (mapAnnotationDTO == null)
throw new ArgumentNullException("MapAnnotation param is null");
var existingProgrammeBloc = _myInfoMateDbContext.ProgrammeBlocks.Include(pb => pb.MapAnnotations).FirstOrDefault(pb => pb.Id == programmeBlockId);
if (existingProgrammeBloc == null)
throw new KeyNotFoundException("ProgrammeBlock does not exist");
// TODO verification ?
MapAnnotation mapAnnotation = new MapAnnotation().FromDTO(mapAnnotationDTO);
mapAnnotation.Id = idService.GenerateHexId();
existingProgrammeBloc.MapAnnotations.Add(mapAnnotation);
_myInfoMateDbContext.SaveChanges();
return new OkObjectResult(mapAnnotation);
}
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 map annotation
/// </summary>
/// <param name="mapAnnotation">mapAnnotation to update</param>
[ProducesResponseType(typeof(MapAnnotationDTO), 200)]
[ProducesResponseType(typeof(string), 400)]
[ProducesResponseType(typeof(string), 404)]
[ProducesResponseType(typeof(string), 500)]
[HttpPut("map-annotations")]
public ObjectResult UpdateMapAnnotation([FromBody] MapAnnotationDTO mapAnnotationDTO)
{
try
{
if (mapAnnotationDTO == null)
throw new ArgumentNullException("MapAnnotation param is null");
var existingMapAnnotation = _myInfoMateDbContext.MapAnnotations.Include(ma => ma.IconResource).FirstOrDefault(ma => ma.Id == mapAnnotationDTO.id);
if (existingMapAnnotation == null)
throw new KeyNotFoundException("MapAnnotation does not exist");
existingMapAnnotation.Type = mapAnnotationDTO.type;
existingMapAnnotation.Label = mapAnnotationDTO.label;
existingMapAnnotation.GeometryType = mapAnnotationDTO.geometryType;
if (mapAnnotationDTO.geometry != null)
{
existingMapAnnotation.Geometry = mapAnnotationDTO.geometry.FromDto();
}
else
{
existingMapAnnotation.Geometry = null;
}
existingMapAnnotation.PolyColor = mapAnnotationDTO.polyColor;
existingMapAnnotation.Icon = mapAnnotationDTO.icon;
existingMapAnnotation.IconResourceId = mapAnnotationDTO.iconResourceId;
_myInfoMateDbContext.SaveChanges();
return new OkObjectResult(existingMapAnnotation);
}
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 mapAnnotation
/// </summary>
/// <param name="mapAnnotationId">Id of map annotation to delete</param>
[ProducesResponseType(typeof(string), 202)]
[ProducesResponseType(typeof(string), 404)]
[ProducesResponseType(typeof(string), 500)]
[HttpDelete("map-annotations/{mapAnnotationId}")]
public ObjectResult DeleteMapAnnotation(string mapAnnotationId)
{
try
{
var mapAnnotation = _myInfoMateDbContext.MapAnnotations.FirstOrDefault(ma => ma.Id == mapAnnotationId);
if (mapAnnotation == null)
throw new KeyNotFoundException("MapAnnotation does not exist");
_myInfoMateDbContext.Remove(mapAnnotation);
_myInfoMateDbContext.SaveChanges();
return new ObjectResult("The mapAnnotation 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>
/// Get all global map annotations from a section event
/// </summary>
/// <param name="sectionEventId">Section event id</param>
[AllowAnonymous]
[RequireAppKey]
[ProducesResponseType(typeof(List<MapAnnotationDTO>), 200)]
[ProducesResponseType(typeof(string), 404)]
[ProducesResponseType(typeof(string), 500)]
[HttpGet("{sectionEventId}/global-map-annotations")]
public ObjectResult GetGlobalMapAnnotationsFromSectionEvent(string sectionEventId)
{
try
{
SectionEvent sectionEvent = _myInfoMateDbContext.Sections.OfType<SectionEvent>()
.Include(se => se.GlobalMapAnnotations).ThenInclude(ma => ma.IconResource)
.FirstOrDefault(se => se.Id == sectionEventId);
if (sectionEvent == null)
throw new KeyNotFoundException("Section event does not exist");
return new OkObjectResult(sectionEvent.GlobalMapAnnotations.Select(ma => ma.ToDTO()));
}
catch (KeyNotFoundException ex)
{
return new NotFoundObjectResult(ex.Message) { };
}
catch (Exception ex)
{
return new ObjectResult(ex.Message) { StatusCode = 500 };
}
}
/// <summary>
/// Create a new global map annotation on a section event
/// </summary>
/// <param name="sectionEventId">Section event id</param>
/// <param name="mapAnnotationDTO">Map annotation</param>
[ProducesResponseType(typeof(MapAnnotationDTO), 200)]
[ProducesResponseType(typeof(string), 400)]
[ProducesResponseType(typeof(string), 404)]
[ProducesResponseType(typeof(string), 500)]
[HttpPost("{sectionEventId}/global-map-annotations")]
public ObjectResult CreateGlobalMapAnnotation(string sectionEventId, [FromBody] MapAnnotationDTO mapAnnotationDTO)
{
try
{
if (sectionEventId == null)
throw new ArgumentNullException("SectionEventId param is null");
if (mapAnnotationDTO == null)
throw new ArgumentNullException("MapAnnotation param is null");
var existingSectionEvent = _myInfoMateDbContext.Sections.OfType<SectionEvent>()
.Include(se => se.GlobalMapAnnotations)
.FirstOrDefault(se => se.Id == sectionEventId);
if (existingSectionEvent == null)
throw new KeyNotFoundException("Section event does not exist");
MapAnnotation mapAnnotation = new MapAnnotation().FromDTO(mapAnnotationDTO);
mapAnnotation.Id = idService.GenerateHexId();
mapAnnotation.SectionEventId = sectionEventId;
existingSectionEvent.GlobalMapAnnotations.Add(mapAnnotation);
_myInfoMateDbContext.SaveChanges();
return new OkObjectResult(mapAnnotation.ToDTO());
}
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 };
}
}
}
}