From 76254878066f564a437d3973b1d412dcd5de1e4f Mon Sep 17 00:00:00 2001 From: Thomas Fransolet Date: Tue, 11 Aug 2026 15:32:19 +0200 Subject: [PATCH] =?UTF-8?q?Lot=20B=20:=20geler=20le=20sch=C3=A9ma,=20plus?= =?UTF-8?q?=20s=C3=A9curit=C3=A9=20rapide=20et=20calculateur=20de=20stocka?= =?UTF-8?q?ge?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit LOT B — une seule migration EF (LotB_FreezeSchema) : - SectionMap.MapResourceId → IconResourceId. L'écart (g) de la bascule tombe avec. Il fallait renommer aussi la propriété de navigation MapResource : la convention EF l'appariait au FK, la laisser aurait fabriqué un FK fantôme. Elle n'était utilisée nulle part ailleurs. - SectionEvent.ParcoursIds supprimé (champ, DTO, SectionFactory, et une initialisation dans un montage de test). - Instance.IsImageWatermark remplace le `instanceId == "633ee379…"` en dur de ResourceController. EF a généré un RenameColumn, pas un drop+add : les icônes déjà configurées survivent. L'avertissement de perte de données ne porte que sur le DropColumn de ParcoursIds, ce qui est l'intention. Non fait, et c'était une erreur de doc : « supprimer SectionEvent.IconResourceId ». Ce champ n'existe pas — la ligne visée appartient à la classe imbriquée MapAnnotation, partagée par SectionEvent, SectionAgenda et SectionMap, lue par cinq contrôleurs et par GetReferencedResourceIds. La supprimer aurait cassé les icônes d'annotation des trois types et la collecte offline. SÉCURITÉ (lot A, même repo) : - AuthenticationController.Authenticate : un bloc #if DEBUG écrasait l'email et le mot de passe reçus par un compte de test, donc toute compilation en Debug authentifiait n'importe quelle saisie. Retiré. - EnableSensitiveDataLogging (qui écrit les valeurs des paramètres dans les logs) passe sous #if DEBUG, l'idiome déjà employé dans Startup.cs pour le CORS et Hangfire. Le Dockerfile publiant en -c Release, c'est un verrou réel. LOT C1 : - Calculateur StoragePath/SizeBytes extrait dans Helpers/ResourceStorage.cs, avec 13 tests fixant l'invariant des types URL. Il ferme le lien L5 : le backfill (C2) et l'écart (e) de la migration appelleront le même code. - L'extraction a révélé la divergence qu'elle devait empêcher : des deux chemins de création de ResourceController, le chemin multipart écrivait SizeBytes mais laissait StoragePath nul. dotnet build Debug et Release verts, dotnet test 143/143 (130 + 13). Co-Authored-By: Claude Opus 5 --- .../SectionEventControllerTests.cs | 3 +- .../Helpers/ResourceStorageTests.cs | 62 + .../Controllers/AuthenticationController.cs | 9 +- .../Controllers/MigrationController.cs | 2 +- .../Controllers/ResourceController.cs | 33 +- ManagerService/DTOs/InstanceDTO.cs | 1 + .../DTOs/SubSection/SectionEventDTO.cs | 1 - ManagerService/Data/Instance.cs | 8 + .../Data/SubSection/SectionEvent.cs | 2 - ManagerService/Data/SubSection/SectionMap.cs | 8 +- ManagerService/Helpers/ResourceStorage.cs | 45 + ...260811130738_LotB_FreezeSchema.Designer.cs | 1883 +++++++++++++++++ .../20260811130738_LotB_FreezeSchema.cs | 82 + .../MyInfoMateDbContextModelSnapshot.cs | 20 +- ManagerService/Services/SectionFactory.cs | 9 +- ManagerService/Startup.cs | 15 +- 16 files changed, 2130 insertions(+), 53 deletions(-) create mode 100644 ManagerService.Tests/Helpers/ResourceStorageTests.cs create mode 100644 ManagerService/Helpers/ResourceStorage.cs create mode 100644 ManagerService/Migrations/20260811130738_LotB_FreezeSchema.Designer.cs create mode 100644 ManagerService/Migrations/20260811130738_LotB_FreezeSchema.cs diff --git a/ManagerService.Tests/Controllers/SectionEventControllerTests.cs b/ManagerService.Tests/Controllers/SectionEventControllerTests.cs index 6a9eb5e..fed04a5 100644 --- a/ManagerService.Tests/Controllers/SectionEventControllerTests.cs +++ b/ManagerService.Tests/Controllers/SectionEventControllerTests.cs @@ -43,8 +43,7 @@ namespace ManagerService.Tests.Controllers ConfigurationId = "c1", Title = EmptyTranslations(), Description = EmptyTranslations(), - Programme = new List(), - ParcoursIds = new List() + Programme = new List() }); db.SaveChanges(); diff --git a/ManagerService.Tests/Helpers/ResourceStorageTests.cs b/ManagerService.Tests/Helpers/ResourceStorageTests.cs new file mode 100644 index 0000000..4ab5e33 --- /dev/null +++ b/ManagerService.Tests/Helpers/ResourceStorageTests.cs @@ -0,0 +1,62 @@ +using ManagerService.Data; +using ManagerService.Helpers; +using Xunit; + +namespace ManagerService.Tests.Helpers +{ + /// + /// Le calculateur est appelé par la création, le backfill et MigrationController. + /// Ces tests fixent la distinction qu'ils perdaient chacun de leur côté : un type + /// URL n'a pas de blob, donc ni chemin de stockage ni poids au quota. + /// + public class ResourceStorageTests + { + [Theory] + [InlineData(ResourceType.Image)] + [InlineData(ResourceType.Video)] + [InlineData(ResourceType.Audio)] + [InlineData(ResourceType.PDF)] + [InlineData(ResourceType.JSON)] + [InlineData(ResourceType.Word)] + [InlineData(ResourceType.PowerPoint)] + [InlineData(ResourceType.Text)] + public void Apply_BlobType_WritesPathAndSize(ResourceType type) + { + var resource = new Resource { Id = "res-1", InstanceId = "inst-1", Type = type }; + + ResourceStorage.Apply(resource, 4096); + + Assert.Equal("pictures/inst-1/res-1", resource.StoragePath); + Assert.Equal(4096, resource.SizeBytes); + } + + [Theory] + [InlineData(ResourceType.ImageUrl)] + [InlineData(ResourceType.VideoUrl)] + [InlineData(ResourceType.JSONUrl)] + public void Apply_UrlType_WritesNothing(ResourceType type) + { + var resource = new Resource { Id = "res-1", InstanceId = "inst-1", Type = type }; + + ResourceStorage.Apply(resource, 4096); + + Assert.Null(resource.StoragePath); + // Reste à 0 : un média hébergé ailleurs ne doit pas peser sur le quota. + Assert.Equal(0, resource.SizeBytes); + } + + [Fact] + public void PathFor_IsDeterministic_SoBackfillCanRebuildIt() + { + Assert.Equal( + ResourceStorage.PathFor(ResourceType.Image, "inst-1", "res-1"), + ResourceStorage.PathFor(ResourceType.Image, "inst-1", "res-1")); + } + + [Fact] + public void PathFor_UrlType_IsNull() + { + Assert.Null(ResourceStorage.PathFor(ResourceType.ImageUrl, "inst-1", "res-1")); + } + } +} diff --git a/ManagerService/Controllers/AuthenticationController.cs b/ManagerService/Controllers/AuthenticationController.cs index b3f0332..d55c004 100644 --- a/ManagerService/Controllers/AuthenticationController.cs +++ b/ManagerService/Controllers/AuthenticationController.cs @@ -60,11 +60,10 @@ namespace ManagerService.Service.Controllers { try { -#if DEBUG - email = "test@email.be"; - password = "kljqsdkljqsd"; // password = "kljqsdkljqsd"; // W/7aj4NB60i3YFKJq50pbw== -#endif - // Set user token ? + // Retiré le 2026-08-11 : un bloc `#if DEBUG` écrasait ici l'email et le + // mot de passe reçus par un compte de test, donc toute compilation en + // Debug authentifiait n'importe qui en tant que test@email.be. + // Set user token ? var user = _myInfoMateDbContext.Users.FirstOrDefault(u => u.Email.ToLower() == email.ToLower()); //var user = _UserDatabaseService.GetByEmail(email.ToLower()); diff --git a/ManagerService/Controllers/MigrationController.cs b/ManagerService/Controllers/MigrationController.cs index e1b4170..a22e4d0 100644 --- a/ManagerService/Controllers/MigrationController.cs +++ b/ManagerService/Controllers/MigrationController.cs @@ -466,7 +466,7 @@ namespace ManagerService.Controllers MapMapType = dto?.mapType, MapTypeMapbox = dto?.mapTypeMapbox, MapMapProvider = dto?.mapProvider, - MapResourceId = dto?.iconResourceId, + IconResourceId = dto?.iconResourceId, MapCenterLatitude = dto?.latitude, MapCenterLongitude = dto?.longitude, MapCategories = dto?.categories?.Select(c => new CategorieDTO diff --git a/ManagerService/Controllers/ResourceController.cs b/ManagerService/Controllers/ResourceController.cs index 6549a0e..735d5b3 100644 --- a/ManagerService/Controllers/ResourceController.cs +++ b/ManagerService/Controllers/ResourceController.cs @@ -260,14 +260,9 @@ namespace ManagerService.Controllers { file.CopyTo(ms); var fileBytes = ms.ToArray(); - if (resourceType == ResourceType.Image) + if (resourceType == ResourceType.Image && IsWatermarkEnabled(instanceId)) { - bool isFort = instanceId == "633ee379d9405f32f166f047"; // If fort saint heribert, TODO add watermark in configuration and model - - if(isFort) // TODO We need to know for which purpose (mobile or tablet) - { - fileBytes = ImageHelper.ResizeAndAddWatermark(fileBytes, isFort, MaxWidth, MaxHeight); - } + fileBytes = ImageHelper.ResizeAndAddWatermark(fileBytes, true, MaxWidth, MaxHeight); } stringResult = Convert.ToBase64String(fileBytes); } @@ -283,7 +278,7 @@ namespace ManagerService.Controllers resource.DateCreation = DateTime.Now.ToUniversalTime(); resource.InstanceId = instanceId; resource.Id = idService.GenerateHexId(); - resource.SizeBytes = file.Length; + ResourceStorage.Apply(resource, file.Length); _myInfoMateDbContext.Add(resource); _myInfoMateDbContext.SaveChanges(); @@ -339,15 +334,7 @@ namespace ManagerService.Controllers resource.InstanceId = newResource.instanceId; resource.Id = idService.GenerateHexId(); - // Les types URL pointent hors du bucket : ni chemin de stockage, ni poids à compter dans le quota. - bool hasBlob = resource.Type != ResourceType.ImageUrl - && resource.Type != ResourceType.VideoUrl - && resource.Type != ResourceType.JSONUrl; - if (hasBlob) - { - resource.StoragePath = $"pictures/{resource.InstanceId}/{resource.Id}"; - resource.SizeBytes = newResource.sizeBytes; - } + ResourceStorage.Apply(resource, newResource.sizeBytes); _myInfoMateDbContext.Add(resource); _myInfoMateDbContext.SaveChanges(); @@ -466,7 +453,7 @@ namespace ManagerService.Controllers switch (section) { case SectionMap map: - map.MapResourceId = map.MapResourceId == id ? null : map.MapResourceId; + map.IconResourceId = map.IconResourceId == id ? null : map.IconResourceId; List geoPoints = _myInfoMateDbContext.GeoPoints.Where(s => s.SectionMapId == section.Id).ToList(); foreach (var point in geoPoints) { @@ -716,5 +703,15 @@ namespace ManagerService.Controllers return new ObjectResult(ex.Message) { StatusCode = 500 }; } } + + /// + /// Le filigrane est un réglage d'instance depuis le 2026-08-11. Auparavant + /// un `instanceId == "633ee379…"` en dur ne l'activait que pour Fort Saint-Héribert. + /// + private bool IsWatermarkEnabled(string instanceId) => + _myInfoMateDbContext.Instances + .Where(i => i.Id == instanceId) + .Select(i => i.IsImageWatermark) + .FirstOrDefault(); } } diff --git a/ManagerService/DTOs/InstanceDTO.cs b/ManagerService/DTOs/InstanceDTO.cs index cb29d15..39c843d 100644 --- a/ManagerService/DTOs/InstanceDTO.cs +++ b/ManagerService/DTOs/InstanceDTO.cs @@ -16,6 +16,7 @@ namespace ManagerService.DTOs public bool? isVR { get; set; } public bool? isAssistant { get; set; } + public bool? isImageWatermark { get; set; } public string? guideName { get; set; } public string? guidePersonaPrompt { get; set; } diff --git a/ManagerService/DTOs/SubSection/SectionEventDTO.cs b/ManagerService/DTOs/SubSection/SectionEventDTO.cs index a9006ec..ee301ae 100644 --- a/ManagerService/DTOs/SubSection/SectionEventDTO.cs +++ b/ManagerService/DTOs/SubSection/SectionEventDTO.cs @@ -10,7 +10,6 @@ namespace Manager.DTOs public DateTime? StartDate { get; set; } public DateTime? EndDate { get; set; } public string? BaseSectionMapId { get; set; } - public List ParcoursIds { get; set; } public List GlobalMapAnnotations { get; set; } = new(); public List Programme { get; set; } = new(); } diff --git a/ManagerService/Data/Instance.cs b/ManagerService/Data/Instance.cs index 191af9f..c7901e3 100644 --- a/ManagerService/Data/Instance.cs +++ b/ManagerService/Data/Instance.cs @@ -36,6 +36,12 @@ namespace ManagerService.Data public bool IsAssistant { get; set; } + /// + /// Appose le filigrane du lieu sur les images téléversées. Remplace le + /// `instanceId == "633ee379…"` en dur de ResourceController.Create. + /// + public bool IsImageWatermark { get; set; } + /// Nom du guide affiché au visiteur, libre. Ex: "Léon". public string? GuideName { get; set; } @@ -118,6 +124,7 @@ namespace ManagerService.Data isWeb = IsWeb, isVR = IsVR, isAssistant = IsAssistant, + isImageWatermark = IsImageWatermark, guideName = GuideName, guidePersonaPrompt = GuidePersonaPrompt, guideVoiceId = GuideVoiceId, @@ -155,6 +162,7 @@ namespace ManagerService.Data IsWeb = instanceDTO.isWeb ?? false; IsVR = instanceDTO.isVR ?? false; IsAssistant = instanceDTO.isAssistant ?? false; + IsImageWatermark = instanceDTO.isImageWatermark ?? false; if (instanceDTO.guideName != null) GuideName = instanceDTO.guideName; if (instanceDTO.guidePersonaPrompt != null) diff --git a/ManagerService/Data/SubSection/SectionEvent.cs b/ManagerService/Data/SubSection/SectionEvent.cs index 5b2afa4..815e1ab 100644 --- a/ManagerService/Data/SubSection/SectionEvent.cs +++ b/ManagerService/Data/SubSection/SectionEvent.cs @@ -24,8 +24,6 @@ namespace ManagerService.Data.SubSection public SectionMap? BaseMap { get; set; } public List GlobalMapAnnotations { get; set; } = new(); public List Programme { get; set; } = new(); - [Column(TypeName = "jsonb")] - public List ParcoursIds { get; set; } = new(); // Liens vers GeoPoints spécifiques public override string GetEmbeddableText(string language) => JoinText(new[] diff --git a/ManagerService/Data/SubSection/SectionMap.cs b/ManagerService/Data/SubSection/SectionMap.cs index 1ac6126..73a8395 100644 --- a/ManagerService/Data/SubSection/SectionMap.cs +++ b/ManagerService/Data/SubSection/SectionMap.cs @@ -22,8 +22,8 @@ namespace ManagerService.Data.SubSection public MapTypeMapBox? MapTypeMapbox { get; set; } // Default = standard for MapBox public MapProvider? MapMapProvider { get; set; } // Default = Google public List MapPoints { get; set; } - public string MapResourceId { get; set; } - public Resource MapResource { get; set; } // Icon + public string IconResourceId { get; set; } + public Resource IconResource { get; set; } // Icon [Required] [Column(TypeName = "jsonb")] public List MapCategories { get; set; } @@ -42,7 +42,7 @@ namespace ManagerService.Data.SubSection public override IEnumerable GetReferencedResourceIds(string language = null) => BaseResourceIds() - .Concat(ResourceId(MapResourceId)) + .Concat(ResourceId(IconResourceId)) .Concat((MapCategories ?? new List()) .SelectMany(c => ResourceId(c.resourceDTO?.id))) .Concat((MapPoints ?? new List()) @@ -74,7 +74,7 @@ namespace ManagerService.Data.SubSection mapType = MapMapType, mapTypeMapbox = MapTypeMapbox, mapProvider = MapMapProvider, - iconResourceId = MapResourceId, + iconResourceId = IconResourceId, categories = MapCategories, centerLatitude = MapCenterLatitude, centerLongitude = MapCenterLongitude diff --git a/ManagerService/Helpers/ResourceStorage.cs b/ManagerService/Helpers/ResourceStorage.cs new file mode 100644 index 0000000..d071caf --- /dev/null +++ b/ManagerService/Helpers/ResourceStorage.cs @@ -0,0 +1,45 @@ +using ManagerService.Data; + +namespace ManagerService.Helpers +{ + /// + /// Seule source de vérité pour StoragePath et SizeBytes d'une Resource. + /// + /// Trois chemins écrivent ces colonnes : la création par téléversement et la + /// création par JSON (ResourceController), le backfill des lignes existantes, + /// et MigrationController (écart e de la bascule). Chacun calculant pour son + /// compte, ils divergeaient déjà : le chemin multipart renseignait SizeBytes + /// mais laissait StoragePath nul. + /// + public static class ResourceStorage + { + /// + /// Les types URL pointent hors du bucket : ni chemin de stockage, ni poids + /// à compter dans le quota. C'est la distinction que les appelants perdaient. + /// + public static bool HasBlob(ResourceType type) => + type != ResourceType.ImageUrl + && type != ResourceType.VideoUrl + && type != ResourceType.JSONUrl; + + /// + /// Chemin déterministe dans le bucket — reconstructible sans lire la ligne, + /// ce dont le backfill dépend. Null pour un type sans blob. + /// + public static string PathFor(ResourceType type, string instanceId, string resourceId) => + HasBlob(type) ? $"pictures/{instanceId}/{resourceId}" : null; + + /// + /// Renseigne les deux colonnes de stockage. Ne touche à rien pour un type URL, + /// dont SizeBytes doit rester à 0 pour ne pas peser sur le quota. + /// + public static void Apply(Resource resource, long sizeBytes) + { + if (!HasBlob(resource.Type)) + return; + + resource.StoragePath = PathFor(resource.Type, resource.InstanceId, resource.Id); + resource.SizeBytes = sizeBytes; + } + } +} diff --git a/ManagerService/Migrations/20260811130738_LotB_FreezeSchema.Designer.cs b/ManagerService/Migrations/20260811130738_LotB_FreezeSchema.Designer.cs new file mode 100644 index 0000000..478df12 --- /dev/null +++ b/ManagerService/Migrations/20260811130738_LotB_FreezeSchema.Designer.cs @@ -0,0 +1,1883 @@ +// +using System; +using System.Collections.Generic; +using Manager.DTOs; +using ManagerService.DTOs; +using ManagerService.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using NetTopologySuite.Geometries; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using Pgvector; + +#nullable disable + +namespace ManagerService.Migrations +{ + [DbContext(typeof(MyInfoMateDbContext))] + [Migration("20260811130738_LotB_FreezeSchema")] + partial class LotB_FreezeSchema + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.2") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "postgis"); + NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "vector"); + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("ManagerService.Data.ApiKey", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AppType") + .HasColumnType("integer"); + + b.Property("DateCreation") + .HasColumnType("timestamp with time zone"); + + b.Property("DateExpiration") + .HasColumnType("timestamp with time zone"); + + b.Property("DateUpdate") + .HasColumnType("timestamp with time zone"); + + b.Property("InstanceId") + .IsRequired() + .HasColumnType("text"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("Key") + .HasColumnType("text"); + + b.Property("KeyHash") + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("InstanceId"); + + b.ToTable("ApiKeys"); + }); + + modelBuilder.Entity("ManagerService.Data.AppConfigurationLink", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("ApplicationInstanceId") + .IsRequired() + .HasColumnType("text"); + + b.Property("ConfigurationId") + .IsRequired() + .HasColumnType("text"); + + b.Property("DateCreation") + .HasColumnType("timestamp with time zone"); + + b.Property("DateUpdate") + .HasColumnType("timestamp with time zone"); + + b.Property("DeviceId") + .HasColumnType("text"); + + b.Property("GridColSpan") + .HasColumnType("integer"); + + b.Property("GridRowSpan") + .HasColumnType("integer"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("IsDate") + .HasColumnType("boolean"); + + b.Property("IsHour") + .HasColumnType("boolean"); + + b.Property("IsSectionImageBackground") + .HasColumnType("boolean"); + + b.Property("LoaderImageId") + .HasColumnType("text"); + + b.Property("LoaderImageUrl") + .HasColumnType("text"); + + b.Property("Order") + .HasColumnType("integer"); + + b.Property("PrimaryColor") + .HasColumnType("text"); + + b.Property("RoundedValue") + .HasColumnType("integer"); + + b.Property("ScreenPercentageSectionsMainPage") + .HasColumnType("integer"); + + b.Property("SecondaryColor") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ApplicationInstanceId"); + + b.HasIndex("ConfigurationId"); + + b.HasIndex("DeviceId"); + + b.ToTable("AppConfigurationLinks"); + }); + + modelBuilder.Entity("ManagerService.Data.ApplicationInstance", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AppType") + .HasColumnType("integer"); + + b.Property("DateCreation") + .HasColumnType("timestamp with time zone"); + + b.Property("DateUpdate") + .HasColumnType("timestamp with time zone"); + + b.Property("InstanceId") + .IsRequired() + .HasColumnType("text"); + + b.Property("IsAssistant") + .HasColumnType("boolean"); + + b.PrimitiveCollection>("Languages") + .HasColumnType("text[]"); + + b.Property("LoaderImageId") + .HasColumnType("text"); + + b.Property("LoaderImageUrl") + .HasColumnType("text"); + + b.Property("MainImageId") + .HasColumnType("text"); + + b.Property("MainImageUrl") + .HasColumnType("text"); + + b.Property("PrimaryColor") + .HasColumnType("text"); + + b.Property("SecondaryColor") + .HasColumnType("text"); + + b.Property("SectionEventId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("InstanceId"); + + b.HasIndex("SectionEventId"); + + b.ToTable("ApplicationInstances"); + }); + + modelBuilder.Entity("ManagerService.Data.AuditLog", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Action") + .HasColumnType("text"); + + b.Property("EntityId") + .HasColumnType("text"); + + b.Property("EntityType") + .HasColumnType("text"); + + b.Property("InstanceId") + .HasColumnType("text"); + + b.Property("NewValues") + .HasColumnType("text"); + + b.Property("OldValues") + .HasColumnType("text"); + + b.Property("Timestamp") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("InstanceId"); + + b.ToTable("AuditLogs"); + }); + + modelBuilder.Entity("ManagerService.Data.Configuration", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("DateCreation") + .HasColumnType("timestamp with time zone"); + + b.Property("DateUpdate") + .HasColumnType("timestamp with time zone"); + + b.Property("ImageId") + .HasColumnType("text"); + + b.Property("ImageSource") + .HasColumnType("text"); + + b.Property("InstanceId") + .IsRequired() + .HasColumnType("text"); + + b.Property("IsOffline") + .HasColumnType("boolean"); + + b.Property("IsQRCode") + .HasColumnType("boolean"); + + b.Property("IsSearchNumber") + .HasColumnType("boolean"); + + b.Property("IsSearchText") + .HasColumnType("boolean"); + + b.Property("Label") + .IsRequired() + .HasColumnType("text"); + + b.PrimitiveCollection>("Languages") + .HasColumnType("text[]"); + + b.Property("LoaderImageId") + .HasColumnType("text"); + + b.Property("LoaderImageUrl") + .HasColumnType("text"); + + b.Property("PrimaryColor") + .HasColumnType("text"); + + b.Property("SecondaryColor") + .HasColumnType("text"); + + b.Property("Title") + .IsRequired() + .HasColumnType("jsonb"); + + b.HasKey("Id"); + + b.HasIndex("InstanceId"); + + b.ToTable("Configurations"); + }); + + modelBuilder.Entity("ManagerService.Data.ContentEmbedding", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ChunkIndex") + .HasColumnType("integer"); + + b.Property("ConfigurationId") + .HasColumnType("text"); + + b.Property("ContentId") + .IsRequired() + .HasColumnType("text"); + + b.Property("ContentType") + .HasColumnType("integer"); + + b.Property("Embedding") + .IsRequired() + .HasColumnType("vector(768)"); + + b.Property("InstanceId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Language") + .IsRequired() + .HasMaxLength(5) + .HasColumnType("character varying(5)"); + + b.Property("PageNumber") + .HasColumnType("integer"); + + b.Property("Text") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("Embedding"); + + NpgsqlIndexBuilderExtensions.HasMethod(b.HasIndex("Embedding"), "hnsw"); + NpgsqlIndexBuilderExtensions.HasOperators(b.HasIndex("Embedding"), new[] { "vector_cosine_ops" }); + + b.HasIndex("InstanceId", "ContentType"); + + b.HasIndex("ContentType", "ContentId", "ChunkIndex") + .IsUnique(); + + b.ToTable("ContentEmbeddings"); + }); + + modelBuilder.Entity("ManagerService.Data.Device", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AppVersion") + .HasColumnType("text"); + + b.Property("BatteryLevel") + .HasColumnType("text"); + + b.Property("ConfigurationId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Connected") + .HasColumnType("boolean"); + + b.Property("ConnectionLevel") + .HasColumnType("text"); + + b.Property("DateCreation") + .HasColumnType("timestamp with time zone"); + + b.Property("DateUpdate") + .HasColumnType("timestamp with time zone"); + + b.Property("Identifier") + .HasColumnType("text"); + + b.Property("InstanceId") + .IsRequired() + .HasColumnType("text"); + + b.Property("IpAddressETH") + .HasColumnType("text"); + + b.Property("IpAddressWLAN") + .HasColumnType("text"); + + b.Property("LastBatteryLevel") + .HasColumnType("timestamp with time zone"); + + b.Property("LastConnectionLevel") + .HasColumnType("timestamp with time zone"); + + b.Property("LastSeen") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ConfigurationId"); + + b.HasIndex("InstanceId"); + + b.ToTable("Devices"); + }); + + modelBuilder.Entity("ManagerService.Data.Instance", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AiTokensPerMonth") + .HasColumnType("bigint"); + + b.Property("AiTokensThisMonth") + .HasColumnType("bigint"); + + b.Property("AiUsageMonthKey") + .HasColumnType("text"); + + b.Property("BillingAddress") + .HasColumnType("text"); + + b.Property("BillingCountry") + .HasColumnType("text"); + + b.Property("DateCreation") + .HasColumnType("timestamp with time zone"); + + b.Property("DateUpdate") + .HasColumnType("timestamp with time zone"); + + b.Property("GuideFallbackMessages") + .HasColumnType("jsonb"); + + b.Property("GuideName") + .HasColumnType("text"); + + b.Property("GuidePersonaPrompt") + .HasColumnType("text"); + + b.Property("GuideVoiceId") + .HasColumnType("text"); + + b.Property("HasAdvancedStats") + .HasColumnType("boolean"); + + b.Property("HasStats") + .HasColumnType("boolean"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("IsAssistant") + .HasColumnType("boolean"); + + b.Property("IsImageWatermark") + .HasColumnType("boolean"); + + b.Property("IsMobile") + .HasColumnType("boolean"); + + b.Property("IsPushNotification") + .HasColumnType("boolean"); + + b.Property("IsTablet") + .HasColumnType("boolean"); + + b.Property("IsTrialActive") + .HasColumnType("boolean"); + + b.Property("IsVR") + .HasColumnType("boolean"); + + b.Property("IsWeb") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("PinCode") + .HasColumnType("text"); + + b.Property("PublicApiKey") + .HasColumnType("text"); + + b.Property("StatsHistoryDays") + .HasColumnType("integer"); + + b.Property("StorageQuotaBytes") + .HasColumnType("bigint"); + + b.Property("StripeCustomerId") + .HasColumnType("text"); + + b.Property("StripeSubscriptionId") + .HasColumnType("text"); + + b.Property("SubscriptionPlanId") + .HasColumnType("text"); + + b.Property("TrialAiTokensUsed") + .HasColumnType("bigint"); + + b.Property("TrialCheckInEmailSent") + .HasColumnType("boolean"); + + b.Property("TrialEndsAt") + .HasColumnType("timestamp with time zone"); + + b.Property("TrialLastDayEmailSent") + .HasColumnType("boolean"); + + b.Property("TrialReminderEmailSent") + .HasColumnType("boolean"); + + b.Property("VatNumber") + .HasColumnType("text"); + + b.Property("VatRate") + .HasColumnType("numeric"); + + b.Property("WebSlug") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("SubscriptionPlanId"); + + b.ToTable("Instances"); + }); + + modelBuilder.Entity("ManagerService.Data.PushNotification", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Body") + .IsRequired() + .HasColumnType("text"); + + b.Property("DateCreation") + .HasColumnType("timestamp with time zone"); + + b.Property("HangfireJobId") + .HasColumnType("text"); + + b.Property("InstanceId") + .IsRequired() + .HasColumnType("text"); + + b.Property("ScheduledAt") + .HasColumnType("timestamp with time zone"); + + b.Property("SentAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("Title") + .IsRequired() + .HasColumnType("text"); + + b.Property("Topic") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("InstanceId"); + + b.ToTable("PushNotifications"); + }); + + modelBuilder.Entity("ManagerService.Data.Resource", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AiChunkCount") + .HasColumnType("integer"); + + b.Property("AiIndexMessage") + .HasColumnType("text"); + + b.Property("AiIndexStatus") + .HasColumnType("integer"); + + b.Property("AiIndexedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DateCreation") + .HasColumnType("timestamp with time zone"); + + b.Property("DateUpdate") + .HasColumnType("timestamp with time zone"); + + b.Property("FileName") + .HasColumnType("text"); + + b.Property("IncludeInAiKnowledge") + .HasColumnType("boolean"); + + b.Property("InstanceId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Label") + .IsRequired() + .HasColumnType("text"); + + b.Property("SizeBytes") + .HasColumnType("bigint"); + + b.Property("StoragePath") + .HasColumnType("text"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("Url") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("InstanceId"); + + b.ToTable("Resources"); + }); + + modelBuilder.Entity("ManagerService.Data.Section", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BeaconId") + .HasColumnType("integer"); + + b.Property("ConfigurationId") + .IsRequired() + .HasColumnType("text"); + + b.Property("DateCreation") + .HasColumnType("timestamp with time zone"); + + b.Property("DateUpdate") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("jsonb"); + + b.Property("Discriminator") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("ImageId") + .HasColumnType("text"); + + b.Property("ImageSource") + .HasColumnType("text"); + + b.Property("InstanceId") + .IsRequired() + .HasColumnType("text"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("IsBeacon") + .HasColumnType("boolean"); + + b.Property("IsSubSection") + .HasColumnType("boolean"); + + b.Property("Label") + .IsRequired() + .HasColumnType("text"); + + b.Property("Latitude") + .HasColumnType("text"); + + b.Property("Longitude") + .HasColumnType("text"); + + b.Property("MeterZoneGPS") + .HasColumnType("integer"); + + b.Property("Order") + .HasColumnType("integer"); + + b.Property("ParentId") + .HasColumnType("text"); + + b.Property("SectionMenuId") + .HasColumnType("text"); + + b.Property("Title") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("Type") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("InstanceId"); + + b.HasIndex("SectionMenuId"); + + b.ToTable("Sections"); + + b.HasDiscriminator().HasValue("Base"); + + b.UseTphMappingStrategy(); + }); + + modelBuilder.Entity("ManagerService.Data.SubSection.EventAgenda", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Address") + .HasColumnType("jsonb"); + + b.Property("DateAdded") + .HasColumnType("timestamp with time zone"); + + b.Property("DateFrom") + .HasColumnType("timestamp with time zone"); + + b.Property("DateTo") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("Email") + .HasColumnType("text"); + + b.Property("IdVideoYoutube") + .HasColumnType("text"); + + b.Property("IsSynced") + .HasColumnType("boolean"); + + b.Property("Label") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("Phone") + .HasColumnType("text"); + + b.Property("ResourceId") + .HasColumnType("text"); + + b.Property("SectionAgendaId") + .HasColumnType("text"); + + b.Property("SectionEventId") + .HasColumnType("text"); + + b.Property("SyncedImageUrl") + .HasColumnType("text"); + + b.Property("Type") + .HasColumnType("text"); + + b.Property("VideoLink") + .HasColumnType("text"); + + b.Property("VideoResourceId") + .HasColumnType("text"); + + b.Property("Website") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ResourceId"); + + b.HasIndex("SectionAgendaId"); + + b.HasIndex("SectionEventId"); + + b.HasIndex("VideoResourceId"); + + b.ToTable("EventAgendas"); + }); + + modelBuilder.Entity("ManagerService.Data.SubSection.GeoPoint", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CategorieId") + .HasColumnType("integer"); + + b.Property("Contents") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("Description") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("Email") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("Geometry") + .HasColumnType("geometry"); + + b.Property("ImageResourceId") + .HasColumnType("text"); + + b.Property("ImageUrl") + .HasColumnType("text"); + + b.Property("Phone") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("PolyColor") + .HasColumnType("text"); + + b.Property("Prices") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("Schedules") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("SectionEventId") + .HasColumnType("text"); + + b.Property("SectionMapId") + .HasColumnType("text"); + + b.Property("Site") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("Title") + .IsRequired() + .HasColumnType("jsonb"); + + b.HasKey("Id"); + + b.HasIndex("SectionEventId"); + + b.HasIndex("SectionMapId"); + + b.ToTable("GeoPoints"); + }); + + modelBuilder.Entity("ManagerService.Data.SubSection.GuidedPath", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("DateCreation") + .HasColumnType("timestamp with time zone"); + + b.Property("DateUpdate") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("jsonb"); + + b.Property("EstimatedDurationMinutes") + .HasColumnType("integer"); + + b.Property("GameMessageDebut") + .HasColumnType("jsonb"); + + b.Property("GameMessageFin") + .HasColumnType("jsonb"); + + b.Property("HideNextStepsUntilComplete") + .HasColumnType("boolean"); + + b.Property("ImageResourceId") + .HasColumnType("text"); + + b.Property("InstanceId") + .IsRequired() + .HasColumnType("text"); + + b.Property("IsGameMode") + .HasColumnType("boolean"); + + b.Property("IsLinear") + .HasColumnType("boolean"); + + b.Property("Order") + .HasColumnType("integer"); + + b.Property("RequireSuccessToAdvance") + .HasColumnType("boolean"); + + b.Property("SectionEventId") + .HasColumnType("text"); + + b.Property("SectionParcoursId") + .HasColumnType("text"); + + b.Property("Title") + .IsRequired() + .HasColumnType("jsonb"); + + b.HasKey("Id"); + + b.HasIndex("ImageResourceId"); + + b.HasIndex("InstanceId"); + + b.HasIndex("SectionEventId"); + + b.HasIndex("SectionParcoursId"); + + b.ToTable("GuidedPaths"); + }); + + modelBuilder.Entity("ManagerService.Data.SubSection.GuidedStep", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AudioIds") + .HasColumnType("jsonb"); + + b.Property("Contents") + .HasColumnType("jsonb"); + + b.Property("Description") + .HasColumnType("jsonb"); + + b.Property("Geometry") + .HasColumnType("geometry"); + + b.Property("GuidedPathId") + .IsRequired() + .HasColumnType("text"); + + b.Property("ImageUrl") + .HasColumnType("text"); + + b.Property("IsGeoTriggered") + .HasColumnType("boolean"); + + b.Property("IsStepTimer") + .HasColumnType("boolean"); + + b.Property("Order") + .HasColumnType("integer"); + + b.Property("TimerExpiredMessage") + .HasColumnType("jsonb"); + + b.Property("TimerSeconds") + .HasColumnType("integer"); + + b.Property("Title") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("ZoneRadiusMeters") + .HasColumnType("double precision"); + + b.HasKey("Id"); + + b.HasIndex("GuidedPathId"); + + b.ToTable("GuidedSteps"); + }); + + modelBuilder.Entity("ManagerService.Data.SubSection.QuizQuestion", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("GuidedStepId") + .HasColumnType("text"); + + b.Property("IsSlidingPuzzle") + .HasColumnType("boolean"); + + b.Property>("Label") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("Order") + .HasColumnType("integer"); + + b.Property("PuzzleCols") + .HasColumnType("integer"); + + b.Property("PuzzleImageId") + .HasColumnType("text"); + + b.Property("PuzzleRows") + .HasColumnType("integer"); + + b.Property("ResourceId") + .HasColumnType("text"); + + b.Property>("Responses") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("SectionQuizId") + .HasColumnType("text"); + + b.Property("ValidationQuestionType") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("GuidedStepId"); + + b.HasIndex("PuzzleImageId"); + + b.HasIndex("ResourceId"); + + b.HasIndex("SectionQuizId"); + + b.ToTable("QuizQuestions"); + }); + + modelBuilder.Entity("ManagerService.Data.SubSection.SectionEvent+MapAnnotation", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Geometry") + .HasColumnType("geometry"); + + b.Property("GeometryType") + .HasColumnType("integer"); + + b.Property("Icon") + .HasColumnType("text"); + + b.Property("IconResourceId") + .HasColumnType("text"); + + b.Property>("Label") + .HasColumnType("jsonb"); + + b.Property("PolyColor") + .HasColumnType("text"); + + b.Property("ProgrammeBlockId") + .HasColumnType("text"); + + b.Property("SectionEventId") + .HasColumnType("text"); + + b.Property>("Type") + .HasColumnType("jsonb"); + + b.HasKey("Id"); + + b.HasIndex("IconResourceId"); + + b.HasIndex("ProgrammeBlockId"); + + b.HasIndex("SectionEventId"); + + b.ToTable("MapAnnotations"); + }); + + modelBuilder.Entity("ManagerService.Data.SubSection.SectionEvent+ProgrammeBlock", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property>("Description") + .HasColumnType("jsonb"); + + b.Property("EndTime") + .HasColumnType("timestamp with time zone"); + + b.Property("SectionEventId") + .HasColumnType("text"); + + b.Property("StartTime") + .HasColumnType("timestamp with time zone"); + + b.Property>("Title") + .HasColumnType("jsonb"); + + b.HasKey("Id"); + + b.HasIndex("SectionEventId"); + + b.ToTable("ProgrammeBlocks"); + }); + + modelBuilder.Entity("ManagerService.Data.SubscriptionPlan", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AiTokensPerMonth") + .HasColumnType("bigint"); + + b.Property("HasAdvancedStats") + .HasColumnType("boolean"); + + b.Property("HasStats") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("StatsHistoryDays") + .HasColumnType("integer"); + + b.Property("StorageQuotaBytes") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.ToTable("SubscriptionPlans"); + + b.HasData( + new + { + Id = "plan-starter", + AiTokensPerMonth = 0L, + HasAdvancedStats = false, + HasStats = false, + Name = "Starter", + StatsHistoryDays = 30, + StorageQuotaBytes = 1073741824L + }, + new + { + Id = "plan-standard", + AiTokensPerMonth = 5000000L, + HasAdvancedStats = false, + HasStats = true, + Name = "Standard", + StatsHistoryDays = 395, + StorageQuotaBytes = 10737418240L + }, + new + { + Id = "plan-premium", + AiTokensPerMonth = 20000000L, + HasAdvancedStats = true, + HasStats = true, + Name = "Premium", + StatsHistoryDays = 395, + StorageQuotaBytes = 53687091200L + }, + new + { + Id = "plan-essentiel", + AiTokensPerMonth = 500000L, + HasAdvancedStats = false, + HasStats = true, + Name = "Essentiel", + StatsHistoryDays = 395, + StorageQuotaBytes = 1073741824L + }); + }); + + modelBuilder.Entity("ManagerService.Data.User", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("DateCreation") + .HasColumnType("timestamp with time zone"); + + b.Property("DateUpdate") + .HasColumnType("timestamp with time zone"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text"); + + b.Property("FirstName") + .HasColumnType("text"); + + b.Property("InstanceId") + .IsRequired() + .HasColumnType("text"); + + b.Property("LastName") + .IsRequired() + .HasColumnType("text"); + + b.Property("Password") + .IsRequired() + .HasColumnType("text"); + + b.Property("PasswordTokenExpiresAt") + .HasColumnType("timestamp with time zone"); + + b.Property("PasswordTokenHash") + .HasColumnType("text"); + + b.Property("Role") + .HasColumnType("integer"); + + b.Property("Token") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("InstanceId"); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("ManagerService.Data.VisitEvent", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AppType") + .HasColumnType("integer"); + + b.Property("ConfigurationId") + .HasColumnType("text"); + + b.Property("DurationSeconds") + .HasColumnType("integer"); + + b.Property("EventType") + .HasColumnType("integer"); + + b.Property("InstanceId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Language") + .HasColumnType("text"); + + b.Property("Metadata") + .HasColumnType("text"); + + b.Property("SectionId") + .HasColumnType("text"); + + b.Property("SessionId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Timestamp") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("InstanceId"); + + b.HasIndex("Timestamp"); + + b.ToTable("VisitEvents"); + }); + + modelBuilder.Entity("ManagerService.Data.VisitorQuestion", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AppType") + .HasColumnType("integer"); + + b.Property("CitedContentIds") + .HasColumnType("jsonb"); + + b.Property("ConfigurationId") + .HasColumnType("text"); + + b.Property("ConversationId") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("HasAnswer") + .HasColumnType("boolean"); + + b.Property("InstanceId") + .HasColumnType("text"); + + b.Property("IsVoice") + .HasColumnType("boolean"); + + b.Property("Language") + .HasMaxLength(5) + .HasColumnType("character varying(5)"); + + b.Property("Question") + .HasColumnType("text"); + + b.Property("Reply") + .HasColumnType("text"); + + b.Property("ThemeId") + .HasColumnType("text"); + + b.Property("TokensUsed") + .HasColumnType("bigint"); + + b.Property("TopScore") + .HasColumnType("double precision"); + + b.HasKey("Id"); + + b.HasIndex("ConversationId"); + + b.HasIndex("InstanceId", "CreatedAt"); + + b.ToTable("VisitorQuestions"); + }); + + modelBuilder.Entity("ManagerService.Data.SubSection.SectionAgenda", b => + { + b.HasBaseType("ManagerService.Data.Section"); + + b.Property("AgendaMapProvider") + .HasColumnType("integer"); + + b.Property>("AgendaResourceIds") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("IsOnlineAgenda") + .HasColumnType("boolean"); + + b.HasDiscriminator().HasValue("Agenda"); + }); + + modelBuilder.Entity("ManagerService.Data.SubSection.SectionArticle", b => + { + b.HasBaseType("ManagerService.Data.Section"); + + b.Property>("ArticleAudioIds") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property>("ArticleContent") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property>("ArticleContents") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("ArticleIsContentTop") + .HasColumnType("boolean"); + + b.Property("ArticleIsReadAudioAuto") + .HasColumnType("boolean"); + + b.HasDiscriminator().HasValue("Article"); + }); + + modelBuilder.Entity("ManagerService.Data.SubSection.SectionEvent", b => + { + b.HasBaseType("ManagerService.Data.Section"); + + b.Property("BaseSectionMapId") + .HasColumnType("text"); + + b.Property("EndDate") + .HasColumnType("timestamp with time zone"); + + b.Property("StartDate") + .HasColumnType("timestamp with time zone"); + + b.HasIndex("BaseSectionMapId"); + + b.HasDiscriminator().HasValue("Event"); + }); + + modelBuilder.Entity("ManagerService.Data.SubSection.SectionGame", b => + { + b.HasBaseType("ManagerService.Data.Section"); + + b.Property>("GameMessageDebut") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property>("GameMessageFin") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("GamePuzzleCols") + .HasColumnType("integer"); + + b.Property("GamePuzzleImageId") + .HasColumnType("text"); + + b.Property("GamePuzzleRows") + .HasColumnType("integer"); + + b.Property("GameType") + .HasColumnType("integer"); + + b.HasIndex("GamePuzzleImageId"); + + b.HasDiscriminator().HasValue("Game"); + }); + + modelBuilder.Entity("ManagerService.Data.SubSection.SectionMap", b => + { + b.HasBaseType("ManagerService.Data.Section"); + + b.Property("IconResourceId") + .HasColumnType("text"); + + b.Property("IsListViewEnabled") + .HasColumnType("boolean"); + + b.Property>("MapCategories") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("MapCenterLatitude") + .HasColumnType("text"); + + b.Property("MapCenterLongitude") + .HasColumnType("text"); + + b.Property("MapMapProvider") + .HasColumnType("integer"); + + b.Property("MapMapType") + .HasColumnType("integer"); + + b.Property("MapTypeMapbox") + .HasColumnType("integer"); + + b.Property("MapZoom") + .HasColumnType("integer"); + + b.HasIndex("IconResourceId"); + + b.HasDiscriminator().HasValue("Map"); + }); + + modelBuilder.Entity("ManagerService.Data.SubSection.SectionMenu", b => + { + b.HasBaseType("ManagerService.Data.Section"); + + b.HasDiscriminator().HasValue("Menu"); + }); + + modelBuilder.Entity("ManagerService.Data.SubSection.SectionParcours", b => + { + b.HasBaseType("ManagerService.Data.Section"); + + b.Property("BaseSectionMapId") + .HasColumnType("text"); + + b.Property("ShowMap") + .HasColumnType("boolean"); + + b.HasIndex("BaseSectionMapId"); + + b.ToTable("Sections", t => + { + t.Property("BaseSectionMapId") + .HasColumnName("SectionParcours_BaseSectionMapId"); + }); + + b.HasDiscriminator().HasValue("Parcours"); + }); + + modelBuilder.Entity("ManagerService.Data.SubSection.SectionPdf", b => + { + b.HasBaseType("ManagerService.Data.Section"); + + b.Property>("PDFOrderedTranslationAndResources") + .IsRequired() + .HasColumnType("jsonb"); + + b.HasDiscriminator().HasValue("PDF"); + }); + + modelBuilder.Entity("ManagerService.Data.SubSection.SectionQuiz", b => + { + b.HasBaseType("ManagerService.Data.Section"); + + b.Property>("QuizBadLevel") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property>("QuizGoodLevel") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property>("QuizGreatLevel") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property>("QuizMediumLevel") + .IsRequired() + .HasColumnType("jsonb"); + + b.HasDiscriminator().HasValue("Quiz"); + }); + + modelBuilder.Entity("ManagerService.Data.SubSection.SectionSlider", b => + { + b.HasBaseType("ManagerService.Data.Section"); + + b.Property>("SliderContents") + .IsRequired() + .HasColumnType("jsonb"); + + b.HasDiscriminator().HasValue("Slider"); + }); + + modelBuilder.Entity("ManagerService.Data.SubSection.SectionVideo", b => + { + b.HasBaseType("ManagerService.Data.Section"); + + b.Property("VideoSource") + .IsRequired() + .HasColumnType("text"); + + b.HasDiscriminator().HasValue("Video"); + }); + + modelBuilder.Entity("ManagerService.Data.SubSection.SectionWeather", b => + { + b.HasBaseType("ManagerService.Data.Section"); + + b.Property("WeatherCity") + .HasColumnType("text"); + + b.Property("WeatherResult") + .HasColumnType("text"); + + b.Property("WeatherUpdatedDate") + .HasColumnType("timestamp with time zone"); + + b.HasDiscriminator().HasValue("Weather"); + }); + + modelBuilder.Entity("ManagerService.Data.SubSection.SectionWeb", b => + { + b.HasBaseType("ManagerService.Data.Section"); + + b.Property("WebSource") + .IsRequired() + .HasColumnType("text"); + + b.HasDiscriminator().HasValue("Web"); + }); + + modelBuilder.Entity("ManagerService.Data.ApiKey", b => + { + b.HasOne("ManagerService.Data.Instance", "Instance") + .WithMany() + .HasForeignKey("InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Instance"); + }); + + modelBuilder.Entity("ManagerService.Data.AppConfigurationLink", b => + { + b.HasOne("ManagerService.Data.ApplicationInstance", "ApplicationInstance") + .WithMany("Configurations") + .HasForeignKey("ApplicationInstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("ManagerService.Data.Configuration", "Configuration") + .WithMany() + .HasForeignKey("ConfigurationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("ManagerService.Data.Device", "Device") + .WithMany() + .HasForeignKey("DeviceId"); + + b.Navigation("ApplicationInstance"); + + b.Navigation("Configuration"); + + b.Navigation("Device"); + }); + + modelBuilder.Entity("ManagerService.Data.ApplicationInstance", b => + { + b.HasOne("ManagerService.Data.SubSection.SectionEvent", "SectionEvent") + .WithMany() + .HasForeignKey("SectionEventId"); + + b.Navigation("SectionEvent"); + }); + + modelBuilder.Entity("ManagerService.Data.Device", b => + { + b.HasOne("ManagerService.Data.Configuration", "Configuration") + .WithMany() + .HasForeignKey("ConfigurationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Configuration"); + }); + + modelBuilder.Entity("ManagerService.Data.Instance", b => + { + b.HasOne("ManagerService.Data.SubscriptionPlan", "SubscriptionPlan") + .WithMany() + .HasForeignKey("SubscriptionPlanId"); + + b.Navigation("SubscriptionPlan"); + }); + + modelBuilder.Entity("ManagerService.Data.Section", b => + { + b.HasOne("ManagerService.Data.SubSection.SectionMenu", null) + .WithMany("MenuSections") + .HasForeignKey("SectionMenuId"); + }); + + modelBuilder.Entity("ManagerService.Data.SubSection.EventAgenda", b => + { + b.HasOne("ManagerService.Data.Resource", "Resource") + .WithMany() + .HasForeignKey("ResourceId"); + + b.HasOne("ManagerService.Data.SubSection.SectionAgenda", "SectionAgenda") + .WithMany("EventAgendas") + .HasForeignKey("SectionAgendaId"); + + b.HasOne("ManagerService.Data.SubSection.SectionEvent", "SectionEvent") + .WithMany() + .HasForeignKey("SectionEventId"); + + b.HasOne("ManagerService.Data.Resource", "VideoResource") + .WithMany() + .HasForeignKey("VideoResourceId"); + + b.Navigation("Resource"); + + b.Navigation("SectionAgenda"); + + b.Navigation("SectionEvent"); + + b.Navigation("VideoResource"); + }); + + modelBuilder.Entity("ManagerService.Data.SubSection.GeoPoint", b => + { + b.HasOne("ManagerService.Data.SubSection.SectionEvent", "SectionEvent") + .WithMany() + .HasForeignKey("SectionEventId"); + + b.HasOne("ManagerService.Data.SubSection.SectionMap", "SectionMap") + .WithMany("MapPoints") + .HasForeignKey("SectionMapId"); + + b.Navigation("SectionEvent"); + + b.Navigation("SectionMap"); + }); + + modelBuilder.Entity("ManagerService.Data.SubSection.GuidedPath", b => + { + b.HasOne("ManagerService.Data.Resource", "ImageResource") + .WithMany() + .HasForeignKey("ImageResourceId"); + + b.HasOne("ManagerService.Data.SubSection.SectionEvent", "SectionEvent") + .WithMany() + .HasForeignKey("SectionEventId"); + + b.HasOne("ManagerService.Data.SubSection.SectionParcours", "SectionParcours") + .WithMany("GuidedPaths") + .HasForeignKey("SectionParcoursId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("ImageResource"); + + b.Navigation("SectionEvent"); + + b.Navigation("SectionParcours"); + }); + + modelBuilder.Entity("ManagerService.Data.SubSection.GuidedStep", b => + { + b.HasOne("ManagerService.Data.SubSection.GuidedPath", "GuidedPath") + .WithMany("Steps") + .HasForeignKey("GuidedPathId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("GuidedPath"); + }); + + modelBuilder.Entity("ManagerService.Data.SubSection.QuizQuestion", b => + { + b.HasOne("ManagerService.Data.SubSection.GuidedStep", "GuidedStep") + .WithMany("QuizQuestions") + .HasForeignKey("GuidedStepId"); + + b.HasOne("ManagerService.Data.Resource", "PuzzleImage") + .WithMany() + .HasForeignKey("PuzzleImageId"); + + b.HasOne("ManagerService.Data.Resource", "Resource") + .WithMany() + .HasForeignKey("ResourceId"); + + b.HasOne("ManagerService.Data.SubSection.SectionQuiz", "SectionQuiz") + .WithMany("QuizQuestions") + .HasForeignKey("SectionQuizId"); + + b.Navigation("GuidedStep"); + + b.Navigation("PuzzleImage"); + + b.Navigation("Resource"); + + b.Navigation("SectionQuiz"); + }); + + modelBuilder.Entity("ManagerService.Data.SubSection.SectionEvent+MapAnnotation", b => + { + b.HasOne("ManagerService.Data.Resource", "IconResource") + .WithMany() + .HasForeignKey("IconResourceId"); + + b.HasOne("ManagerService.Data.SubSection.SectionEvent+ProgrammeBlock", null) + .WithMany("MapAnnotations") + .HasForeignKey("ProgrammeBlockId"); + + b.HasOne("ManagerService.Data.SubSection.SectionEvent", null) + .WithMany("GlobalMapAnnotations") + .HasForeignKey("SectionEventId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("IconResource"); + }); + + modelBuilder.Entity("ManagerService.Data.SubSection.SectionEvent+ProgrammeBlock", b => + { + b.HasOne("ManagerService.Data.SubSection.SectionEvent", null) + .WithMany("Programme") + .HasForeignKey("SectionEventId"); + }); + + modelBuilder.Entity("ManagerService.Data.SubSection.SectionEvent", b => + { + b.HasOne("ManagerService.Data.SubSection.SectionMap", "BaseMap") + .WithMany() + .HasForeignKey("BaseSectionMapId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("BaseMap"); + }); + + modelBuilder.Entity("ManagerService.Data.SubSection.SectionGame", b => + { + b.HasOne("ManagerService.Data.Resource", "GamePuzzleImage") + .WithMany() + .HasForeignKey("GamePuzzleImageId"); + + b.Navigation("GamePuzzleImage"); + }); + + modelBuilder.Entity("ManagerService.Data.SubSection.SectionMap", b => + { + b.HasOne("ManagerService.Data.Resource", "IconResource") + .WithMany() + .HasForeignKey("IconResourceId"); + + b.Navigation("IconResource"); + }); + + modelBuilder.Entity("ManagerService.Data.SubSection.SectionParcours", b => + { + b.HasOne("ManagerService.Data.SubSection.SectionMap", "BaseMap") + .WithMany() + .HasForeignKey("BaseSectionMapId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("BaseMap"); + }); + + modelBuilder.Entity("ManagerService.Data.ApplicationInstance", b => + { + b.Navigation("Configurations"); + }); + + modelBuilder.Entity("ManagerService.Data.SubSection.GuidedPath", b => + { + b.Navigation("Steps"); + }); + + modelBuilder.Entity("ManagerService.Data.SubSection.GuidedStep", b => + { + b.Navigation("QuizQuestions"); + }); + + modelBuilder.Entity("ManagerService.Data.SubSection.SectionEvent+ProgrammeBlock", b => + { + b.Navigation("MapAnnotations"); + }); + + modelBuilder.Entity("ManagerService.Data.SubSection.SectionAgenda", b => + { + b.Navigation("EventAgendas"); + }); + + modelBuilder.Entity("ManagerService.Data.SubSection.SectionEvent", b => + { + b.Navigation("GlobalMapAnnotations"); + + b.Navigation("Programme"); + }); + + modelBuilder.Entity("ManagerService.Data.SubSection.SectionMap", b => + { + b.Navigation("MapPoints"); + }); + + modelBuilder.Entity("ManagerService.Data.SubSection.SectionMenu", b => + { + b.Navigation("MenuSections"); + }); + + modelBuilder.Entity("ManagerService.Data.SubSection.SectionParcours", b => + { + b.Navigation("GuidedPaths"); + }); + + modelBuilder.Entity("ManagerService.Data.SubSection.SectionQuiz", b => + { + b.Navigation("QuizQuestions"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/ManagerService/Migrations/20260811130738_LotB_FreezeSchema.cs b/ManagerService/Migrations/20260811130738_LotB_FreezeSchema.cs new file mode 100644 index 0000000..b4cb7f8 --- /dev/null +++ b/ManagerService/Migrations/20260811130738_LotB_FreezeSchema.cs @@ -0,0 +1,82 @@ +using System.Collections.Generic; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace ManagerService.Migrations +{ + /// + public partial class LotB_FreezeSchema : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_Sections_Resources_MapResourceId", + table: "Sections"); + + migrationBuilder.DropColumn( + name: "ParcoursIds", + table: "Sections"); + + migrationBuilder.RenameColumn( + name: "MapResourceId", + table: "Sections", + newName: "IconResourceId"); + + migrationBuilder.RenameIndex( + name: "IX_Sections_MapResourceId", + table: "Sections", + newName: "IX_Sections_IconResourceId"); + + migrationBuilder.AddColumn( + name: "IsImageWatermark", + table: "Instances", + type: "boolean", + nullable: false, + defaultValue: false); + + migrationBuilder.AddForeignKey( + name: "FK_Sections_Resources_IconResourceId", + table: "Sections", + column: "IconResourceId", + principalTable: "Resources", + principalColumn: "Id"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_Sections_Resources_IconResourceId", + table: "Sections"); + + migrationBuilder.DropColumn( + name: "IsImageWatermark", + table: "Instances"); + + migrationBuilder.RenameColumn( + name: "IconResourceId", + table: "Sections", + newName: "MapResourceId"); + + migrationBuilder.RenameIndex( + name: "IX_Sections_IconResourceId", + table: "Sections", + newName: "IX_Sections_MapResourceId"); + + migrationBuilder.AddColumn>( + name: "ParcoursIds", + table: "Sections", + type: "jsonb", + nullable: true); + + migrationBuilder.AddForeignKey( + name: "FK_Sections_Resources_MapResourceId", + table: "Sections", + column: "MapResourceId", + principalTable: "Resources", + principalColumn: "Id"); + } + } +} diff --git a/ManagerService/Migrations/MyInfoMateDbContextModelSnapshot.cs b/ManagerService/Migrations/MyInfoMateDbContextModelSnapshot.cs index 766971e..f0b91c6 100644 --- a/ManagerService/Migrations/MyInfoMateDbContextModelSnapshot.cs +++ b/ManagerService/Migrations/MyInfoMateDbContextModelSnapshot.cs @@ -465,6 +465,9 @@ namespace ManagerService.Migrations b.Property("IsAssistant") .HasColumnType("boolean"); + b.Property("IsImageWatermark") + .HasColumnType("boolean"); + b.Property("IsMobile") .HasColumnType("boolean"); @@ -1398,9 +1401,6 @@ namespace ManagerService.Migrations b.Property("EndDate") .HasColumnType("timestamp with time zone"); - b.Property>("ParcoursIds") - .HasColumnType("jsonb"); - b.Property("StartDate") .HasColumnType("timestamp with time zone"); @@ -1442,6 +1442,9 @@ namespace ManagerService.Migrations { b.HasBaseType("ManagerService.Data.Section"); + b.Property("IconResourceId") + .HasColumnType("text"); + b.Property("IsListViewEnabled") .HasColumnType("boolean"); @@ -1461,16 +1464,13 @@ namespace ManagerService.Migrations b.Property("MapMapType") .HasColumnType("integer"); - b.Property("MapResourceId") - .HasColumnType("text"); - b.Property("MapTypeMapbox") .HasColumnType("integer"); b.Property("MapZoom") .HasColumnType("integer"); - b.HasIndex("MapResourceId"); + b.HasIndex("IconResourceId"); b.HasDiscriminator().HasValue("Map"); }); @@ -1806,11 +1806,11 @@ namespace ManagerService.Migrations modelBuilder.Entity("ManagerService.Data.SubSection.SectionMap", b => { - b.HasOne("ManagerService.Data.Resource", "MapResource") + b.HasOne("ManagerService.Data.Resource", "IconResource") .WithMany() - .HasForeignKey("MapResourceId"); + .HasForeignKey("IconResourceId"); - b.Navigation("MapResource"); + b.Navigation("IconResource"); }); modelBuilder.Entity("ManagerService.Data.SubSection.SectionParcours", b => diff --git a/ManagerService/Services/SectionFactory.cs b/ManagerService/Services/SectionFactory.cs index 5c15378..d9ddd59 100644 --- a/ManagerService/Services/SectionFactory.cs +++ b/ManagerService/Services/SectionFactory.cs @@ -31,8 +31,7 @@ namespace ManagerService.Services }, SectionType.Event => new SectionEvent { - Programme = new List(), - ParcoursIds = new List() + Programme = new List() }, SectionType.Map => new SectionMap { @@ -196,7 +195,6 @@ namespace ManagerService.Services Type = dto.type, StartDate = sectionEventDTO.StartDate?.ToUniversalTime(), EndDate = sectionEventDTO.EndDate?.ToUniversalTime(), - ParcoursIds = sectionEventDTO.ParcoursIds, BaseSectionMapId = sectionEventDTO.BaseSectionMapId, //Programmes = // TODO specific }, @@ -224,7 +222,7 @@ namespace ManagerService.Services MapMapType = mapDTO.mapType, MapTypeMapbox = mapDTO.mapTypeMapbox, MapMapProvider = mapDTO.mapProvider, - MapResourceId = mapDTO.iconResourceId, + IconResourceId = mapDTO.iconResourceId, MapCenterLatitude = mapDTO.centerLatitude, MapCenterLongitude = mapDTO.centerLongitude, MapCategories = mapDTO.categories, @@ -526,7 +524,6 @@ namespace ManagerService.Services type = sectionEvent.Type, StartDate = sectionEvent.StartDate?.Year > 1000 ? sectionEvent.StartDate : null, EndDate = sectionEvent.EndDate?.Year > 1000 ? sectionEvent.EndDate : null, - ParcoursIds = sectionEvent.ParcoursIds, BaseSectionMapId = sectionEvent.BaseSectionMapId, GlobalMapAnnotations = sectionEvent.GlobalMapAnnotations?.Select(ma => ma.ToDTO()).ToList() ?? new(), // Programme TODO specific @@ -556,7 +553,7 @@ namespace ManagerService.Services mapType = map.MapMapType, mapTypeMapbox = map.MapTypeMapbox, mapProvider = map.MapMapProvider, - iconResourceId = map.MapResourceId, + iconResourceId = map.IconResourceId, centerLatitude = map.MapCenterLatitude, centerLongitude = map.MapCenterLongitude, categories = map.MapCategories, diff --git a/ManagerService/Startup.cs b/ManagerService/Startup.cs index 4591124..511d06b 100644 --- a/ManagerService/Startup.cs +++ b/ManagerService/Startup.cs @@ -243,10 +243,17 @@ namespace ManagerService services.AddScoped(); services.AddDbContext((serviceProvider, options) => - options.UseNpgsql(dataSource, o => o.UseNetTopologySuite().UseVector()) - .AddInterceptors(serviceProvider.GetRequiredService()) - .EnableSensitiveDataLogging() - .LogTo(Console.WriteLine, LogLevel.Information) + { + options.UseNpgsql(dataSource, o => o.UseNetTopologySuite().UseVector()) + .AddInterceptors(serviceProvider.GetRequiredService()); +#if DEBUG + // Écrit les valeurs des paramètres dans les logs : mots de passe hashés, + // clés API, données de visiteurs. Jamais en production — le Dockerfile + // publie en `-c Release`, donc ce bloc n'y est pas compilé. + options.EnableSensitiveDataLogging() + .LogTo(Console.WriteLine, LogLevel.Information); +#endif + } ); services.AddHealthChecks()