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>
386 lines
16 KiB
C#
386 lines
16 KiB
C#
using Manager.DTOs;
|
|
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;
|
|
|
|
namespace ManagerService.Controllers
|
|
{
|
|
[Authorize(Policy = ManagerService.Service.Security.Policies.ContentEditor)]
|
|
[ApiController, Route("api/[controller]")]
|
|
[OpenApiTag("Section parcours", Description = "Section parcours management")]
|
|
public class SectionParcoursController : ControllerBase
|
|
{
|
|
private readonly MyInfoMateDbContext _myInfoMateDbContext;
|
|
private readonly ILogger<SectionParcoursController> _logger;
|
|
private readonly IConfiguration _configuration;
|
|
IHexIdGeneratorService idService = new HexIdGeneratorService();
|
|
|
|
public SectionParcoursController(IConfiguration configuration, ILogger<SectionParcoursController> logger, MyInfoMateDbContext myInfoMateDbContext)
|
|
{
|
|
_logger = logger;
|
|
_configuration = configuration;
|
|
_myInfoMateDbContext = myInfoMateDbContext;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get all guided paths from a parcours section
|
|
/// </summary>
|
|
[AllowAnonymous]
|
|
[RequireAppKey]
|
|
[ProducesResponseType(typeof(List<GuidedPathDTO>), 200)]
|
|
[ProducesResponseType(typeof(string), 404)]
|
|
[ProducesResponseType(typeof(string), 500)]
|
|
[HttpGet("{sectionParcoursId}/guided-path")]
|
|
public ObjectResult GetAllGuidedPathFromSection(string sectionParcoursId)
|
|
{
|
|
try
|
|
{
|
|
List<GuidedPath> guidedPaths = _myInfoMateDbContext.GuidedPaths
|
|
.Include(gp => gp.ImageResource)
|
|
.Include(gp => gp.Steps).ThenInclude(s => s.QuizQuestions).ThenInclude(q => q.PuzzleImage)
|
|
.Where(gp => gp.SectionParcoursId == sectionParcoursId)
|
|
.OrderBy(gp => gp.Order)
|
|
.ToList();
|
|
|
|
return new OkObjectResult(guidedPaths.Select(gp => gp.ToDTO()));
|
|
}
|
|
catch (KeyNotFoundException ex)
|
|
{
|
|
return new NotFoundObjectResult(ex.Message);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
return new ObjectResult(ex.Message) { StatusCode = 500 };
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Create a new guided path in a parcours section
|
|
/// </summary>
|
|
[ProducesResponseType(typeof(GuidedPathDTO), 200)]
|
|
[ProducesResponseType(typeof(string), 400)]
|
|
[ProducesResponseType(typeof(string), 404)]
|
|
[ProducesResponseType(typeof(string), 500)]
|
|
[HttpPost("{sectionParcoursId}/guided-path")]
|
|
public ObjectResult CreateGuidedPath(string sectionParcoursId, [FromBody] GuidedPathDTO guidedPathDTO)
|
|
{
|
|
try
|
|
{
|
|
if (sectionParcoursId == null)
|
|
throw new ArgumentNullException("SectionParcoursId param is null");
|
|
|
|
if (guidedPathDTO == null)
|
|
throw new ArgumentNullException("GuidedPath param is null");
|
|
|
|
var sectionParcours = _myInfoMateDbContext.Sections.OfType<SectionParcours>().FirstOrDefault(sp => sp.Id == sectionParcoursId);
|
|
if (sectionParcours == null)
|
|
throw new KeyNotFoundException("SectionParcours does not exist");
|
|
|
|
GuidedPath guidedPath = new GuidedPath().FromDTO(guidedPathDTO);
|
|
guidedPath.Id = idService.GenerateHexId();
|
|
guidedPath.SectionParcoursId = sectionParcoursId;
|
|
guidedPath.Order = _myInfoMateDbContext.GuidedPaths.Count(gp => gp.SectionParcoursId == sectionParcoursId);
|
|
guidedPath.Steps = new List<GuidedStep>();
|
|
|
|
foreach (var stepDTO in guidedPathDTO.steps ?? new List<GuidedStepDTO>())
|
|
{
|
|
GuidedStep step = new GuidedStep().FromDTO(stepDTO);
|
|
step.Id = idService.GenerateHexId();
|
|
step.GuidedPathId = guidedPath.Id;
|
|
guidedPath.Steps.Add(step);
|
|
}
|
|
|
|
_myInfoMateDbContext.GuidedPaths.Add(guidedPath);
|
|
_myInfoMateDbContext.SaveChanges();
|
|
|
|
return new OkObjectResult(guidedPath.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 };
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Update a guided path
|
|
/// </summary>
|
|
[ProducesResponseType(typeof(GuidedPathDTO), 200)]
|
|
[ProducesResponseType(typeof(string), 400)]
|
|
[ProducesResponseType(typeof(string), 404)]
|
|
[ProducesResponseType(typeof(string), 500)]
|
|
[HttpPut("guided-path")]
|
|
public ObjectResult UpdateGuidedPath([FromBody] GuidedPathDTO guidedPathDTO)
|
|
{
|
|
try
|
|
{
|
|
if (guidedPathDTO == null)
|
|
throw new ArgumentNullException("GuidedPath param is null");
|
|
|
|
var existingGuidedPath = _myInfoMateDbContext.GuidedPaths
|
|
.Include(gp => gp.Steps).ThenInclude(s => s.QuizQuestions)
|
|
.FirstOrDefault(gp => gp.Id == guidedPathDTO.id);
|
|
if (existingGuidedPath == null)
|
|
throw new KeyNotFoundException("GuidedPath does not exist");
|
|
|
|
existingGuidedPath.InstanceId = guidedPathDTO.instanceId;
|
|
existingGuidedPath.Title = guidedPathDTO.title ?? new List<TranslationDTO>();
|
|
existingGuidedPath.Description = guidedPathDTO.description ?? new List<TranslationDTO>();
|
|
existingGuidedPath.IsLinear = guidedPathDTO.isLinear;
|
|
existingGuidedPath.RequireSuccessToAdvance = guidedPathDTO.requireSuccessToAdvance;
|
|
existingGuidedPath.HideNextStepsUntilComplete = guidedPathDTO.hideNextStepsUntilComplete;
|
|
existingGuidedPath.EstimatedDurationMinutes = guidedPathDTO.estimatedDurationMinutes;
|
|
existingGuidedPath.ImageResourceId = guidedPathDTO.imageResourceId;
|
|
existingGuidedPath.Order = guidedPathDTO.order.GetValueOrDefault();
|
|
existingGuidedPath.IsGameMode = guidedPathDTO.isGameMode;
|
|
existingGuidedPath.GameMessageDebut = guidedPathDTO.gameMessageDebut ?? new List<TranslationAndResourceDTO>();
|
|
existingGuidedPath.GameMessageFin = guidedPathDTO.gameMessageFin ?? new List<TranslationAndResourceDTO>();
|
|
|
|
// Sync steps
|
|
var dtoStepIds = (guidedPathDTO.steps ?? new List<GuidedStepDTO>())
|
|
.Where(s => s.id != null)
|
|
.Select(s => s.id)
|
|
.ToHashSet();
|
|
|
|
var stepsToRemove = existingGuidedPath.Steps
|
|
.Where(s => !dtoStepIds.Contains(s.Id))
|
|
.ToList();
|
|
foreach (var s in stepsToRemove)
|
|
existingGuidedPath.Steps.Remove(s);
|
|
|
|
foreach (var stepDTO in guidedPathDTO.steps ?? new List<GuidedStepDTO>())
|
|
{
|
|
var existing = existingGuidedPath.Steps.FirstOrDefault(s => s.Id == stepDTO.id);
|
|
if (existing != null)
|
|
{
|
|
existing.FromDTO(stepDTO);
|
|
existing.GuidedPathId = existingGuidedPath.Id;
|
|
}
|
|
else
|
|
{
|
|
var newStep = new GuidedStep().FromDTO(stepDTO);
|
|
newStep.Id = idService.GenerateHexId();
|
|
newStep.GuidedPathId = existingGuidedPath.Id;
|
|
existingGuidedPath.Steps.Add(newStep);
|
|
}
|
|
}
|
|
|
|
_myInfoMateDbContext.SaveChanges();
|
|
|
|
return new OkObjectResult(existingGuidedPath.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 };
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Delete a guided path
|
|
/// </summary>
|
|
[ProducesResponseType(typeof(string), 202)]
|
|
[ProducesResponseType(typeof(string), 404)]
|
|
[ProducesResponseType(typeof(string), 500)]
|
|
[HttpDelete("guided-path/{guidedPathId}")]
|
|
public ObjectResult DeleteGuidedPath(string guidedPathId)
|
|
{
|
|
try
|
|
{
|
|
var guidedPath = _myInfoMateDbContext.GuidedPaths.Include(gp => gp.Steps).FirstOrDefault(gp => gp.Id == guidedPathId);
|
|
if (guidedPath == null)
|
|
throw new KeyNotFoundException("GuidedPath does not exist");
|
|
|
|
_myInfoMateDbContext.RemoveRange(guidedPath.Steps);
|
|
_myInfoMateDbContext.Remove(guidedPath);
|
|
_myInfoMateDbContext.SaveChanges();
|
|
|
|
return new ObjectResult("The guidedPath has been deleted") { StatusCode = 202 };
|
|
}
|
|
catch (KeyNotFoundException ex)
|
|
{
|
|
return new NotFoundObjectResult(ex.Message);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
return new ObjectResult(ex.Message) { StatusCode = 500 };
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get all steps from a guided path
|
|
/// </summary>
|
|
[AllowAnonymous]
|
|
[RequireAppKey]
|
|
[ProducesResponseType(typeof(List<GuidedStepDTO>), 200)]
|
|
[ProducesResponseType(typeof(string), 404)]
|
|
[ProducesResponseType(typeof(string), 500)]
|
|
[HttpGet("guided-path/{guidedPathId}/guided-step")]
|
|
public ObjectResult GetAllGuidedStepFromGuidedPath(string guidedPathId)
|
|
{
|
|
try
|
|
{
|
|
List<GuidedStep> guidedSteps = _myInfoMateDbContext.GuidedSteps
|
|
.Include(gs => gs.QuizQuestions)
|
|
.Where(gs => gs.GuidedPathId == guidedPathId)
|
|
.OrderBy(gs => gs.Order)
|
|
.ToList();
|
|
|
|
return new OkObjectResult(guidedSteps.Select(gs => gs.ToDTO()));
|
|
}
|
|
catch (KeyNotFoundException ex)
|
|
{
|
|
return new NotFoundObjectResult(ex.Message);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
return new ObjectResult(ex.Message) { StatusCode = 500 };
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Create a new step in a guided path
|
|
/// </summary>
|
|
[ProducesResponseType(typeof(GuidedStepDTO), 200)]
|
|
[ProducesResponseType(typeof(string), 400)]
|
|
[ProducesResponseType(typeof(string), 404)]
|
|
[ProducesResponseType(typeof(string), 500)]
|
|
[HttpPost("guided-path/{guidedPathId}/guided-step")]
|
|
public ObjectResult CreateGuidedStep(string guidedPathId, [FromBody] GuidedStepDTO guidedStepDTO)
|
|
{
|
|
try
|
|
{
|
|
if (guidedPathId == null)
|
|
throw new ArgumentNullException("GuidedPathId param is null");
|
|
|
|
if (guidedStepDTO == null)
|
|
throw new ArgumentNullException("GuidedStep param is null");
|
|
|
|
var guidedPath = _myInfoMateDbContext.GuidedPaths.FirstOrDefault(gp => gp.Id == guidedPathId);
|
|
if (guidedPath == null)
|
|
throw new KeyNotFoundException("GuidedPath does not exist");
|
|
|
|
GuidedStep guidedStep = new GuidedStep().FromDTO(guidedStepDTO);
|
|
guidedStep.Id = idService.GenerateHexId();
|
|
guidedStep.GuidedPathId = guidedPathId;
|
|
guidedStep.Order = _myInfoMateDbContext.GuidedSteps.Count(gs => gs.GuidedPathId == guidedPathId);
|
|
|
|
_myInfoMateDbContext.GuidedSteps.Add(guidedStep);
|
|
_myInfoMateDbContext.SaveChanges();
|
|
|
|
return new OkObjectResult(guidedStep.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 };
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Update a guided step
|
|
/// </summary>
|
|
[ProducesResponseType(typeof(GuidedStepDTO), 200)]
|
|
[ProducesResponseType(typeof(string), 400)]
|
|
[ProducesResponseType(typeof(string), 404)]
|
|
[ProducesResponseType(typeof(string), 500)]
|
|
[HttpPut("guided-step")]
|
|
public ObjectResult UpdateGuidedStep([FromBody] GuidedStepDTO guidedStepDTO)
|
|
{
|
|
try
|
|
{
|
|
if (guidedStepDTO == null)
|
|
throw new ArgumentNullException("GuidedStep param is null");
|
|
|
|
var existingGuidedStep = _myInfoMateDbContext.GuidedSteps
|
|
.Include(gs => gs.QuizQuestions)
|
|
.FirstOrDefault(gs => gs.Id == guidedStepDTO.id);
|
|
if (existingGuidedStep == null)
|
|
throw new KeyNotFoundException("GuidedStep does not exist");
|
|
|
|
existingGuidedStep.FromDTO(guidedStepDTO);
|
|
|
|
_myInfoMateDbContext.SaveChanges();
|
|
|
|
return new OkObjectResult(existingGuidedStep.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 };
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Delete a guided step
|
|
/// </summary>
|
|
[ProducesResponseType(typeof(string), 202)]
|
|
[ProducesResponseType(typeof(string), 404)]
|
|
[ProducesResponseType(typeof(string), 500)]
|
|
[HttpDelete("guided-step/{guidedStepId}")]
|
|
public ObjectResult DeleteGuidedStep(string guidedStepId)
|
|
{
|
|
try
|
|
{
|
|
var guidedStep = _myInfoMateDbContext.GuidedSteps.Include(gs => gs.QuizQuestions).FirstOrDefault(gs => gs.Id == guidedStepId);
|
|
if (guidedStep == null)
|
|
throw new KeyNotFoundException("GuidedStep does not exist");
|
|
|
|
guidedStep.QuizQuestions?.Clear();
|
|
_myInfoMateDbContext.Remove(guidedStep);
|
|
_myInfoMateDbContext.SaveChanges();
|
|
|
|
return new ObjectResult("The guidedStep has been deleted") { StatusCode = 202 };
|
|
}
|
|
catch (KeyNotFoundException ex)
|
|
{
|
|
return new NotFoundObjectResult(ex.Message);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
return new ObjectResult(ex.Message) { StatusCode = 500 };
|
|
}
|
|
}
|
|
}
|
|
}
|