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>
310 lines
12 KiB
C#
310 lines
12 KiB
C#
using Manager.DTOs;
|
||
using Manager.Helpers;
|
||
using ManagerService.Data;
|
||
using ManagerService.Data.SubSection;
|
||
using ManagerService.DTOs;
|
||
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.Reflection.Emit;
|
||
using System.Security.Cryptography;
|
||
using System.Text.Json;
|
||
|
||
namespace ManagerService.Controllers
|
||
{
|
||
[Authorize(Policy = ManagerService.Service.Security.Policies.ContentEditor)]
|
||
[ApiController, Route("api/[controller]")]
|
||
[OpenApiTag("Section quiz", Description = "Section quiz management")]
|
||
public class SectionQuizController : ControllerBase
|
||
{
|
||
private readonly MyInfoMateDbContext _myInfoMateDbContext;
|
||
|
||
private readonly ILogger<SectionQuizController> _logger;
|
||
private readonly IConfiguration _configuration;
|
||
IHexIdGeneratorService idService = new HexIdGeneratorService();
|
||
|
||
public SectionQuizController(IConfiguration configuration, ILogger<SectionQuizController> logger, MyInfoMateDbContext myInfoMateDbContext)
|
||
{
|
||
_logger = logger;
|
||
_configuration = configuration;
|
||
_myInfoMateDbContext = myInfoMateDbContext;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Get all quiz questions from section
|
||
/// </summary>
|
||
/// <param name="sectionId">Section id</param>
|
||
[AllowAnonymous]
|
||
[RequireAppKey]
|
||
[ProducesResponseType(typeof(List<QuestionDTO>), 200)]
|
||
[ProducesResponseType(typeof(string), 404)]
|
||
[ProducesResponseType(typeof(string), 500)]
|
||
[HttpGet("{sectionId}/questions")]
|
||
public ObjectResult GetAllQuizQuestionFromSection(string sectionId)
|
||
{
|
||
try
|
||
{
|
||
SectionQuiz sectionQuiz = _myInfoMateDbContext.Sections.OfType<SectionQuiz>().Include(sq => sq.QuizQuestions).ThenInclude(qq => qq.Resource).FirstOrDefault(sq => sq.Id == sectionId);
|
||
|
||
List<QuestionDTO> questionDTOs = new List<QuestionDTO>();
|
||
foreach (var question in sectionQuiz.QuizQuestions)
|
||
{
|
||
questionDTOs.Add(new QuestionDTO()
|
||
{
|
||
id = question.Id,
|
||
label = question.Label,
|
||
responses = question.Responses,
|
||
imageBackgroundResourceId = question.ResourceId,
|
||
imageBackgroundResourceType = question.Resource?.Type,
|
||
imageBackgroundResourceUrl = question.Resource?.Url,
|
||
order = question.Order
|
||
});
|
||
}
|
||
|
||
return new OkObjectResult(questionDTOs);
|
||
}
|
||
catch (KeyNotFoundException ex)
|
||
{
|
||
return new NotFoundObjectResult(ex.Message) { };
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
return new ObjectResult(ex.Message) { StatusCode = 500 };
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Create new question
|
||
/// </summary>
|
||
/// <param name="sectionId">Section Id</param>
|
||
/// <param name="questionDTO">question</param>
|
||
[ProducesResponseType(typeof(QuestionDTO), 200)]
|
||
[ProducesResponseType(typeof(string), 400)]
|
||
[ProducesResponseType(typeof(string), 409)]
|
||
[ProducesResponseType(typeof(string), 500)]
|
||
[HttpPost("{sectionId}/questions")]
|
||
public ObjectResult Create(string sectionId, [FromBody] QuestionDTO questionDTO)
|
||
{
|
||
try
|
||
{
|
||
if (sectionId == null)
|
||
throw new ArgumentNullException("Section param is null");
|
||
|
||
if (questionDTO == null)
|
||
throw new ArgumentNullException("Question is null");
|
||
|
||
var existingSection = _myInfoMateDbContext.Sections.OfType<SectionQuiz>().Include(sq => sq.QuizQuestions).ThenInclude(qq => qq.Resource).FirstOrDefault(sq => sq.Id == sectionId);
|
||
if (existingSection == null)
|
||
throw new KeyNotFoundException("Section quiz does not exist");
|
||
|
||
// TODO verification ?
|
||
QuizQuestion quizQuestion = new QuizQuestion();
|
||
quizQuestion.Label = questionDTO.label;
|
||
quizQuestion.Responses = questionDTO.responses;
|
||
quizQuestion.ResourceId = questionDTO.imageBackgroundResourceId;
|
||
quizQuestion.Order = existingSection.QuizQuestions.Count();
|
||
|
||
existingSection.QuizQuestions.Add(quizQuestion);
|
||
|
||
_myInfoMateDbContext.SaveChanges();
|
||
|
||
var questionDTOToSend = 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
|
||
};
|
||
|
||
return new OkObjectResult(questionDTOToSend);
|
||
}
|
||
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 quiz question
|
||
/// </summary>
|
||
/// <param name="updatedQuizQuestion">QuizQuestion to update</param>
|
||
[ProducesResponseType(typeof(QuestionDTO), 200)]
|
||
[ProducesResponseType(typeof(string), 400)]
|
||
[ProducesResponseType(typeof(string), 404)]
|
||
[ProducesResponseType(typeof(string), 500)]
|
||
[HttpPut]
|
||
public ObjectResult Update([FromBody] QuestionDTO questionDTO)
|
||
{
|
||
try
|
||
{
|
||
if (questionDTO == null)
|
||
throw new ArgumentNullException("Question param is null");
|
||
|
||
var existingQuestion = _myInfoMateDbContext.QuizQuestions.FirstOrDefault(qq => qq.Id == questionDTO.id);
|
||
if (existingQuestion == null)
|
||
throw new KeyNotFoundException("Question quiz does not exist");
|
||
|
||
if (questionDTO.order != existingQuestion.Order)
|
||
{
|
||
// Order update
|
||
var existingSection = _myInfoMateDbContext.Sections.OfType<SectionQuiz>().Include(sq => sq.QuizQuestions.OrderBy(q => q.Order)).FirstOrDefault(sq => sq.Id == existingQuestion.SectionQuizId);
|
||
|
||
if (existingSection == null)
|
||
throw new Exception("Section quiz not found");
|
||
|
||
var questions = existingSection.QuizQuestions.OrderBy(q => q.Order).ToList();
|
||
|
||
// Retirer la question d<>plac<61>e
|
||
questions.RemoveAll(q => q.Id == existingQuestion.Id);
|
||
|
||
// Ins<6E>rer <20> la nouvelle position (d<>j<EFBFBD> en 0-based)
|
||
int newIndex = questionDTO.order.Value;
|
||
newIndex = Math.Clamp(newIndex, 0, questions.Count);
|
||
questions.Insert(newIndex, existingQuestion);
|
||
|
||
// R<>assigner les ordres en 0-based
|
||
for (int i = 0; i < questions.Count; i++)
|
||
{
|
||
questions[i].Order = i;
|
||
}
|
||
|
||
}
|
||
else {
|
||
// Simple update
|
||
existingQuestion.Label = questionDTO.label;
|
||
existingQuestion.Responses = questionDTO.responses;
|
||
existingQuestion.Order = questionDTO.order.Value; // TO TEST
|
||
existingQuestion.ResourceId = questionDTO.imageBackgroundResourceId;
|
||
}
|
||
|
||
_myInfoMateDbContext.SaveChanges();
|
||
|
||
return new OkObjectResult(questionDTO);
|
||
}
|
||
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 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)
|
||
{
|
||
// TODO REWRITE LOGIC..
|
||
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");
|
||
}
|
||
|
||
foreach (var updatedSection in updatedSectionsOrder)
|
||
{
|
||
var section = _myInfoMateDbContext.Sections.FirstOrDefault(s => s.Id == updatedSection.id);
|
||
//OldSection section = _sectionService.GetById(updatedSection.id);
|
||
section.Order = updatedSection.order.GetValueOrDefault();
|
||
_myInfoMateDbContext.SaveChanges();
|
||
|
||
//_sectionService.Update(section.Id, section);
|
||
}
|
||
|
||
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 quiz question
|
||
/// </summary>
|
||
/// <param name="quizQuestionId">Id of quizQuestion to delete</param>
|
||
[ProducesResponseType(typeof(string), 202)]
|
||
[ProducesResponseType(typeof(string), 404)]
|
||
[ProducesResponseType(typeof(string), 500)]
|
||
[HttpDelete("questions/delete/{quizQuestionId}")]
|
||
public ObjectResult Delete(int quizQuestionId)
|
||
{
|
||
try
|
||
{
|
||
var quizQuestion = _myInfoMateDbContext.QuizQuestions.FirstOrDefault(qq => qq.Id == quizQuestionId);
|
||
if (quizQuestion == null)
|
||
throw new KeyNotFoundException("QuizQuestion does not exist");
|
||
|
||
_myInfoMateDbContext.Remove(quizQuestion);
|
||
_myInfoMateDbContext.SaveChanges();
|
||
|
||
return new ObjectResult("The quiz question 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 };
|
||
}
|
||
}
|
||
}
|
||
}
|