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>
220 lines
8.7 KiB
C#
220 lines
8.7 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using Manager.DTOs;
|
|
using ManagerService.Data;
|
|
using ManagerService.Data.SubSection;
|
|
using ManagerService.Security;
|
|
using Microsoft.AspNetCore.Authorization;
|
|
using Microsoft.AspNetCore.Mvc;
|
|
using Microsoft.EntityFrameworkCore;
|
|
using Microsoft.Extensions.Logging;
|
|
using NSwag.Annotations;
|
|
|
|
namespace ManagerService.Controllers
|
|
{
|
|
/// <summary>
|
|
/// Les points d'intérêt d'une <b>maquette 3D</b> — item E7 du lot XR-4.
|
|
///
|
|
/// <b>Pourquoi un contrôleur à part.</b> Le CRUD des points existe déjà, mais il est
|
|
/// écrit sur <c>SectionMap</c> : <c>OfType<SectionMap>()</c>, <c>MapPoints</c>.
|
|
/// Une maquette a les mêmes points — c'est le même <c>GeoPoint</c> — sur une autre
|
|
/// collection. Élargir <c>SectionMapController</c> aurait mélangé deux types de
|
|
/// section dans chaque méthode ; ce contrôleur-ci ne parle que de maquettes.
|
|
///
|
|
/// La différence de fond tient en un champ : ici un point porte une
|
|
/// <c>LocalTransform</c> (x, y, z dans le repère du modèle) là où une carte porte
|
|
/// une géométrie PostGIS. Les deux coexistent sur l'entité, et rien n'oblige un point
|
|
/// à n'en avoir qu'une.
|
|
/// </summary>
|
|
[Authorize(Policy = ManagerService.Service.Security.Policies.ContentEditor)]
|
|
[ApiController, Route("api/[controller]")]
|
|
[OpenApiTag("SectionScene3D", Description = "3D model section points of interest")]
|
|
public class SectionScene3DController : ControllerBase
|
|
{
|
|
private readonly ILogger<SectionScene3DController> _logger;
|
|
private readonly MyInfoMateDbContext _myInfoMateDbContext;
|
|
|
|
public SectionScene3DController(ILogger<SectionScene3DController> logger,
|
|
MyInfoMateDbContext myInfoMateDbContext)
|
|
{
|
|
_logger = logger;
|
|
_myInfoMateDbContext = myInfoMateDbContext;
|
|
}
|
|
|
|
/// <summary>Get all points of interest of a 3D model section</summary>
|
|
/// <param name="sectionId">Section id</param>
|
|
[AllowAnonymous]
|
|
[RequireAppKey]
|
|
[ProducesResponseType(typeof(List<GeoPointDTO>), 200)]
|
|
[ProducesResponseType(typeof(string), 404)]
|
|
[ProducesResponseType(typeof(string), 500)]
|
|
[HttpGet("{sectionId}/points")]
|
|
public ObjectResult GetAllPointsFromSection(string sectionId)
|
|
{
|
|
try
|
|
{
|
|
var section = _myInfoMateDbContext.Sections
|
|
.OfType<SectionScene3D>()
|
|
.Include(s => s.Points)
|
|
.FirstOrDefault(s => s.Id == sectionId);
|
|
|
|
if (section == null)
|
|
throw new KeyNotFoundException("3D model section does not exist");
|
|
|
|
return new OkObjectResult(
|
|
(section.Points ?? new List<GeoPoint>()).Select(p => p.ToDTO()).ToList());
|
|
}
|
|
catch (KeyNotFoundException ex)
|
|
{
|
|
return new NotFoundObjectResult(ex.Message) { };
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
return new ObjectResult(ex.Message) { StatusCode = 500 };
|
|
}
|
|
}
|
|
|
|
/// <summary>Add a point of interest to a 3D model section</summary>
|
|
/// <param name="sectionId">Section id</param>
|
|
/// <param name="geoPointDTO">Point to create</param>
|
|
[ProducesResponseType(typeof(GeoPointDTO), 200)]
|
|
[ProducesResponseType(typeof(string), 400)]
|
|
[ProducesResponseType(typeof(string), 404)]
|
|
[ProducesResponseType(typeof(string), 500)]
|
|
[HttpPost("{sectionId}/points")]
|
|
public ObjectResult CreatePoint(string sectionId, [FromBody] GeoPointDTO geoPointDTO)
|
|
{
|
|
try
|
|
{
|
|
if (geoPointDTO == null)
|
|
throw new ArgumentNullException("GeoPoint is null");
|
|
|
|
var section = _myInfoMateDbContext.Sections
|
|
.OfType<SectionScene3D>()
|
|
.Include(s => s.Points)
|
|
.FirstOrDefault(s => s.Id == sectionId);
|
|
|
|
if (section == null)
|
|
throw new KeyNotFoundException("3D model section does not exist");
|
|
|
|
var point = new GeoPoint
|
|
{
|
|
Title = geoPointDTO.title,
|
|
Description = geoPointDTO.description,
|
|
Contents = geoPointDTO.contents,
|
|
ImageResourceId = geoPointDTO.imageResourceId,
|
|
ImageUrl = geoPointDTO.imageUrl,
|
|
Schedules = geoPointDTO.schedules,
|
|
Prices = geoPointDTO.prices,
|
|
Phone = geoPointDTO.phone,
|
|
Email = geoPointDTO.email,
|
|
Site = geoPointDTO.site,
|
|
|
|
// Un point neuf n'a pas encore été posé sur la maquette : il arrive à
|
|
// l'origine du modèle, là où l'éditeur le montrera pour qu'on le place.
|
|
LocalTransform = geoPointDTO.localTransform ?? new Position3D()
|
|
};
|
|
|
|
section.Points ??= new List<GeoPoint>();
|
|
section.Points.Add(point);
|
|
|
|
_myInfoMateDbContext.SaveChanges();
|
|
|
|
return new OkObjectResult(point.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 point of interest, position included</summary>
|
|
/// <param name="geoPointDTO">Point to update</param>
|
|
[ProducesResponseType(typeof(GeoPointDTO), 200)]
|
|
[ProducesResponseType(typeof(string), 400)]
|
|
[ProducesResponseType(typeof(string), 404)]
|
|
[ProducesResponseType(typeof(string), 500)]
|
|
[HttpPut("points")]
|
|
public ObjectResult UpdatePoint([FromBody] GeoPointDTO geoPointDTO)
|
|
{
|
|
try
|
|
{
|
|
if (geoPointDTO == null)
|
|
throw new ArgumentNullException("GeoPoint param is null");
|
|
|
|
var point = _myInfoMateDbContext.GeoPoints
|
|
.FirstOrDefault(p => p.Id == geoPointDTO.id);
|
|
|
|
if (point == null)
|
|
throw new KeyNotFoundException("GeoPoint does not exist");
|
|
|
|
point.Title = geoPointDTO.title ?? point.Title;
|
|
point.Description = geoPointDTO.description ?? point.Description;
|
|
point.Contents = geoPointDTO.contents ?? point.Contents;
|
|
point.ImageResourceId = geoPointDTO.imageResourceId ?? point.ImageResourceId;
|
|
point.ImageUrl = geoPointDTO.imageUrl ?? point.ImageUrl;
|
|
|
|
// C'est le champ que l'éditeur 3D renvoie à chaque déplacement, et la
|
|
// seule raison pour laquelle cette route est appelée souvent.
|
|
if (geoPointDTO.localTransform != null)
|
|
point.LocalTransform = geoPointDTO.localTransform;
|
|
|
|
_myInfoMateDbContext.SaveChanges();
|
|
|
|
return new OkObjectResult(point.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 point of interest</summary>
|
|
/// <param name="id">Point id</param>
|
|
[ProducesResponseType(typeof(string), 200)]
|
|
[ProducesResponseType(typeof(string), 404)]
|
|
[ProducesResponseType(typeof(string), 500)]
|
|
[HttpDelete("points/{id}")]
|
|
public ObjectResult DeletePoint(int id)
|
|
{
|
|
try
|
|
{
|
|
var point = _myInfoMateDbContext.GeoPoints.FirstOrDefault(p => p.Id == id);
|
|
|
|
if (point == null)
|
|
throw new KeyNotFoundException("GeoPoint does not exist");
|
|
|
|
_myInfoMateDbContext.GeoPoints.Remove(point);
|
|
_myInfoMateDbContext.SaveChanges();
|
|
|
|
return new OkObjectResult("Point deleted");
|
|
}
|
|
catch (KeyNotFoundException ex)
|
|
{
|
|
return new NotFoundObjectResult(ex.Message) { };
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
return new ObjectResult(ex.Message) { StatusCode = 500 };
|
|
}
|
|
}
|
|
}
|
|
}
|