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>
238 lines
9.7 KiB
C#
238 lines
9.7 KiB
C#
using ManagerService.DTOs;
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.ComponentModel.DataAnnotations;
|
|
using System.ComponentModel.DataAnnotations.Schema;
|
|
|
|
namespace ManagerService.Data
|
|
{
|
|
/// <summary>
|
|
/// Instance Information
|
|
/// </summary>
|
|
public class Instance : IAuditableEntity
|
|
{
|
|
[Key]
|
|
[Required]
|
|
public string Id { get; set; }
|
|
|
|
[Required]
|
|
public string Name { get; set; } // UNIQUE !..
|
|
|
|
public DateTime DateCreation { get; set; }
|
|
|
|
public DateTime DateUpdate { get; set; }
|
|
|
|
public string PinCode { get; set; }
|
|
|
|
public bool IsPushNotification { get; set; }
|
|
|
|
public bool IsMobile { get; set; }
|
|
|
|
public bool IsTablet { get; set; }
|
|
|
|
public bool IsWeb { get; set; }
|
|
|
|
public bool IsVR { get; set; }
|
|
|
|
public bool IsAssistant { get; set; }
|
|
|
|
/// <summary>
|
|
/// Add-on <b>« Contenu immersif »</b> : image 360, vidéo 360 et modèles 3D.
|
|
///
|
|
/// Un add-on est un <c>bool</c> sur l'instance — c'est déjà le patron d'
|
|
/// <see cref="IsAssistant"/>, et il tient parce que l'<c>Instance</c> recopie
|
|
/// localement les valeurs de son plan au lieu de les lire à travers : le
|
|
/// <c>SubscriptionPlan</c> n'est qu'un gabarit.
|
|
///
|
|
/// ⚠️ <b>Le stockage est le point de vigilance</b>, pas la facturation : une
|
|
/// vidéo 360 de cinq minutes pèse des Go, et un client sur un petit palier ferait
|
|
/// sauter la marge. L'activation doit s'accompagner d'un relèvement explicite de
|
|
/// <see cref="StorageQuotaBytes"/> (§6 de DOCS/v2/vr-quest-unity-plan.md).
|
|
/// </summary>
|
|
public bool HasImmersiveContent { get; set; }
|
|
|
|
/// <summary>
|
|
/// Appose le filigrane du lieu sur les images téléversées. Remplace le
|
|
/// `instanceId == "633ee379…"` en dur de ResourceController.Create.
|
|
/// </summary>
|
|
public bool IsImageWatermark { get; set; }
|
|
|
|
/// <summary>Nom du guide affiché au visiteur, libre. Ex: "Léon".</summary>
|
|
public string? GuideName { get; set; }
|
|
|
|
/// <summary>
|
|
/// Personnalité du guide : instruction envoyée au modèle, jamais affichée.
|
|
/// Écrite dans une seule langue — le modèle répond dans celle du visiteur.
|
|
/// </summary>
|
|
public string? GuidePersonaPrompt { get; set; }
|
|
|
|
/// <summary>Voix Gemini TTS : "Sulafat" (Viva, féminine) ou "Umbriel" (Marco, masculine).</summary>
|
|
public string? GuideVoiceId { get; set; }
|
|
|
|
/// <summary>
|
|
/// Le client autorise-t-il l'enregistrement des questions posées par ses visiteurs ?
|
|
///
|
|
/// ⚠️ **C'est lui le responsable de traitement**, pas nous : il doit pouvoir refuser
|
|
/// la collecte. Elle était inconditionnelle dès que l'assistant était actif.
|
|
///
|
|
/// Défaut à <c>true</c> : couvert par les CGU §8, et le désactiver par défaut viderait
|
|
/// l'onglet « Ce que demandent vos visiteurs » et le rapport de trous de contenu chez
|
|
/// tout le monde — une fonction qu'il paie, perdue sans l'avoir demandé.
|
|
///
|
|
/// ⚠️ Couper la collecte n'efface rien : les questions déjà enregistrées vivent
|
|
/// jusqu'à leur purge à 90 jours, et les agrégats de thèmes leur survivent.
|
|
/// </summary>
|
|
public bool IsVisitorQuestionCollectionEnabled { get; set; } = true;
|
|
|
|
/// <summary>
|
|
/// Formulations utilisées quand le guide ne trouve pas de réponse.
|
|
/// Liste à plat : plusieurs entrées peuvent partager la même langue,
|
|
/// une est tirée au hasard parmi celles de la langue du visiteur.
|
|
/// </summary>
|
|
public List<TranslationDTO>? GuideFallbackMessages { get; set; }
|
|
|
|
public string? WebSlug { get; set; }
|
|
|
|
public string? PublicApiKey { get; set; }
|
|
|
|
public string? SubscriptionPlanId { get; set; }
|
|
|
|
[ForeignKey("SubscriptionPlanId")]
|
|
public SubscriptionPlan? SubscriptionPlan { get; set; }
|
|
|
|
public long AiTokensThisMonth { get; set; } = 0;
|
|
|
|
public string AiUsageMonthKey { get; set; } = "";
|
|
|
|
public long StorageQuotaBytes { get; set; } = 0;
|
|
|
|
public long AiTokensPerMonth { get; set; } = 0;
|
|
|
|
public bool HasStats { get; set; } = false;
|
|
|
|
public int StatsHistoryDays { get; set; } = 30;
|
|
|
|
public bool HasAdvancedStats { get; set; } = false;
|
|
|
|
public bool IsActive { get; set; } = true;
|
|
|
|
public bool IsTrialActive { get; set; } = false;
|
|
|
|
public DateTime? TrialEndsAt { get; set; }
|
|
|
|
public long TrialAiTokensUsed { get; set; } = 0;
|
|
|
|
public bool TrialCheckInEmailSent { get; set; } = false;
|
|
|
|
public bool TrialReminderEmailSent { get; set; } = false;
|
|
|
|
public bool TrialLastDayEmailSent { get; set; } = false;
|
|
|
|
public string? BillingAddress { get; set; }
|
|
|
|
public string? BillingCountry { get; set; }
|
|
|
|
public string? VatNumber { get; set; }
|
|
|
|
public decimal? VatRate { get; set; }
|
|
|
|
public string? StripeCustomerId { get; set; }
|
|
|
|
public string? StripeSubscriptionId { get; set; }
|
|
|
|
public InstanceDTO ToDTO(List<ApplicationInstanceDTO> applicationInstanceDTOs)
|
|
{
|
|
return new InstanceDTO()
|
|
{
|
|
id = Id,
|
|
name = Name,
|
|
dateCreation = DateCreation,
|
|
pinCode = PinCode,
|
|
isPushNotification = IsPushNotification,
|
|
isMobile = IsMobile,
|
|
isTablet = IsTablet,
|
|
isWeb = IsWeb,
|
|
isVR = IsVR,
|
|
isAssistant = IsAssistant,
|
|
hasImmersiveContent = HasImmersiveContent,
|
|
isImageWatermark = IsImageWatermark,
|
|
guideName = GuideName,
|
|
guidePersonaPrompt = GuidePersonaPrompt,
|
|
guideVoiceId = GuideVoiceId,
|
|
isVisitorQuestionCollectionEnabled = IsVisitorQuestionCollectionEnabled,
|
|
guideFallbackMessages = GuideFallbackMessages,
|
|
webSlug = WebSlug,
|
|
publicApiKey = PublicApiKey,
|
|
subscriptionPlanId = SubscriptionPlanId,
|
|
subscriptionPlan = SubscriptionPlan?.ToDTO(),
|
|
aiTokensThisMonth = AiTokensThisMonth,
|
|
aiUsageMonthKey = AiUsageMonthKey,
|
|
storageQuotaBytes = StorageQuotaBytes,
|
|
aiTokensPerMonth = AiTokensPerMonth,
|
|
hasStats = HasStats,
|
|
statsHistoryDays = StatsHistoryDays,
|
|
hasAdvancedStats = HasAdvancedStats,
|
|
isTrialActive = IsTrialActive,
|
|
trialEndsAt = TrialEndsAt,
|
|
trialAiTokensUsed = TrialAiTokensUsed,
|
|
billingAddress = BillingAddress,
|
|
billingCountry = BillingCountry,
|
|
vatNumber = VatNumber,
|
|
vatRate = VatRate,
|
|
applicationInstanceDTOs = applicationInstanceDTOs
|
|
};
|
|
}
|
|
|
|
public Instance FromDTO(InstanceDTO instanceDTO)
|
|
{
|
|
Name = instanceDTO.name;
|
|
DateCreation = instanceDTO.dateCreation != null ? instanceDTO.dateCreation.Value : DateTime.Now.ToUniversalTime();
|
|
PinCode = instanceDTO.pinCode;
|
|
IsPushNotification = instanceDTO.isPushNotification ?? false;
|
|
IsMobile = instanceDTO.isMobile ?? false;
|
|
IsTablet = instanceDTO.isTablet ?? false;
|
|
IsWeb = instanceDTO.isWeb ?? false;
|
|
IsVR = instanceDTO.isVR ?? false;
|
|
IsAssistant = instanceDTO.isAssistant ?? false;
|
|
HasImmersiveContent = instanceDTO.hasImmersiveContent ?? false;
|
|
IsImageWatermark = instanceDTO.isImageWatermark ?? false;
|
|
if (instanceDTO.guideName != null)
|
|
GuideName = instanceDTO.guideName;
|
|
if (instanceDTO.guidePersonaPrompt != null)
|
|
GuidePersonaPrompt = instanceDTO.guidePersonaPrompt;
|
|
if (instanceDTO.guideVoiceId != null)
|
|
GuideVoiceId = instanceDTO.guideVoiceId;
|
|
if (instanceDTO.isVisitorQuestionCollectionEnabled != null)
|
|
IsVisitorQuestionCollectionEnabled = instanceDTO.isVisitorQuestionCollectionEnabled.Value;
|
|
if (instanceDTO.guideFallbackMessages != null)
|
|
GuideFallbackMessages = instanceDTO.guideFallbackMessages;
|
|
if (instanceDTO.webSlug != null)
|
|
WebSlug = instanceDTO.webSlug;
|
|
if (instanceDTO.publicApiKey != null)
|
|
PublicApiKey = instanceDTO.publicApiKey;
|
|
if (instanceDTO.subscriptionPlanId != null)
|
|
SubscriptionPlanId = instanceDTO.subscriptionPlanId;
|
|
if (instanceDTO.storageQuotaBytes.HasValue)
|
|
StorageQuotaBytes = instanceDTO.storageQuotaBytes.Value;
|
|
if (instanceDTO.aiTokensPerMonth.HasValue)
|
|
AiTokensPerMonth = instanceDTO.aiTokensPerMonth.Value;
|
|
if (instanceDTO.hasStats.HasValue)
|
|
HasStats = instanceDTO.hasStats.Value;
|
|
if (instanceDTO.statsHistoryDays.HasValue)
|
|
StatsHistoryDays = instanceDTO.statsHistoryDays.Value;
|
|
if (instanceDTO.hasAdvancedStats.HasValue)
|
|
HasAdvancedStats = instanceDTO.hasAdvancedStats.Value;
|
|
if (instanceDTO.billingAddress != null)
|
|
BillingAddress = instanceDTO.billingAddress;
|
|
if (instanceDTO.billingCountry != null)
|
|
BillingCountry = instanceDTO.billingCountry;
|
|
if (instanceDTO.vatNumber != null)
|
|
VatNumber = instanceDTO.vatNumber;
|
|
if (instanceDTO.vatRate.HasValue)
|
|
VatRate = instanceDTO.vatRate.Value;
|
|
return this;
|
|
}
|
|
|
|
}
|
|
}
|