diff --git a/ManagerService/Controllers/InstanceController.cs b/ManagerService/Controllers/InstanceController.cs index c1c32de..5109257 100644 --- a/ManagerService/Controllers/InstanceController.cs +++ b/ManagerService/Controllers/InstanceController.cs @@ -137,7 +137,11 @@ namespace ManagerService.Controllers if (_myInfoMateDbContext.Instances.Any(i => i.Name == instance.Name)) throw new InvalidOperationException("This name is already used"); - //OldInstance instanceCreated = _instanceService.Create(newInstance); + instance.WebSlug = GenerateUniqueSlug(instance.Name); + instance.PublicApiKey = "ap_" + Convert.ToBase64String( + System.Security.Cryptography.RandomNumberGenerator.GetBytes(32)) + .Replace("+", "-").Replace("/", "_").TrimEnd('='); + _myInfoMateDbContext.Instances.Add(instance); _myInfoMateDbContext.SaveChanges(); @@ -218,6 +222,38 @@ namespace ManagerService.Controllers } } + /// + /// Get Instance by web slug (public, used by visitapp-web) + /// + /// Web slug of the instance + [AllowAnonymous] + [ProducesResponseType(typeof(InstanceDTO), 200)] + [ProducesResponseType(typeof(string), 404)] + [ProducesResponseType(typeof(string), 500)] + [HttpGet("slug/{slug}")] + public ObjectResult GetInstanceBySlug(string slug) + { + try + { + Instance instance = _myInfoMateDbContext.Instances.FirstOrDefault(i => i.WebSlug == slug); + + if (instance == null) + throw new KeyNotFoundException("Instance was not found"); + + var applicationInstances = _myInfoMateDbContext.ApplicationInstances.Where(ai => ai.InstanceId == instance.Id).ToList(); + + return new OkObjectResult(instance.ToDTO(applicationInstances.Select(ai => ai.ToDTO(_myInfoMateDbContext)).ToList())); + } + catch (KeyNotFoundException ex) + { + return new NotFoundObjectResult(ex.Message) { }; + } + catch (Exception ex) + { + return new ObjectResult(ex.Message) { StatusCode = 500 }; + } + } + /// /// Get Instance by pincode /// @@ -385,5 +421,59 @@ namespace ManagerService.Controllers return new ObjectResult(ex.Message) { StatusCode = 500 }; } } + /// + /// Generate (or regenerate) WebSlug and PublicApiKey for an existing instance + /// + /// Id of the instance + [ProducesResponseType(typeof(InstanceDTO), 200)] + [ProducesResponseType(typeof(string), 404)] + [ProducesResponseType(typeof(string), 500)] + [HttpPost("{id}/generate-web-keys")] + public ObjectResult GenerateWebKeys(string id) + { + try + { + var instance = _myInfoMateDbContext.Instances.FirstOrDefault(i => i.Id == id); + if (instance == null) + return new NotFoundObjectResult("Instance not found"); + + if (string.IsNullOrEmpty(instance.WebSlug)) + instance.WebSlug = GenerateUniqueSlug(instance.Name); + + instance.PublicApiKey = "ap_" + Convert.ToBase64String( + System.Security.Cryptography.RandomNumberGenerator.GetBytes(32)) + .Replace("+", "-").Replace("/", "_").TrimEnd('='); + + _myInfoMateDbContext.SaveChanges(); + + var applicationInstances = _myInfoMateDbContext.ApplicationInstances.Where(ai => ai.InstanceId == instance.Id).ToList(); + return new OkObjectResult(instance.ToDTO(applicationInstances.Select(ai => ai.ToDTO(_myInfoMateDbContext)).ToList())); + } + catch (Exception ex) + { + return new ObjectResult(ex.Message) { StatusCode = 500 }; + } + } + + private string GenerateUniqueSlug(string name) + { + var slug = name.ToLowerInvariant(); + slug = System.Text.RegularExpressions.Regex.Replace(slug, @"[àáâãäå]", "a"); + slug = System.Text.RegularExpressions.Regex.Replace(slug, @"[èéêë]", "e"); + slug = System.Text.RegularExpressions.Regex.Replace(slug, @"[ìíîï]", "i"); + slug = System.Text.RegularExpressions.Regex.Replace(slug, @"[òóôõö]", "o"); + slug = System.Text.RegularExpressions.Regex.Replace(slug, @"[ùúûü]", "u"); + slug = System.Text.RegularExpressions.Regex.Replace(slug, @"[ç]", "c"); + slug = System.Text.RegularExpressions.Regex.Replace(slug, @"[ñ]", "n"); + slug = System.Text.RegularExpressions.Regex.Replace(slug, @"[^a-z0-9\s-]", ""); + slug = System.Text.RegularExpressions.Regex.Replace(slug, @"[\s-]+", "-").Trim('-'); + + var baseSlug = slug; + var counter = 1; + while (_myInfoMateDbContext.Instances.Any(i => i.WebSlug == slug)) + slug = $"{baseSlug}-{counter++}"; + + return slug; + } } } diff --git a/ManagerService/Controllers/SectionController.cs b/ManagerService/Controllers/SectionController.cs index 2bce333..3522b4f 100644 --- a/ManagerService/Controllers/SectionController.cs +++ b/ManagerService/Controllers/SectionController.cs @@ -197,7 +197,7 @@ namespace ManagerService.Controllers switch (section.Type) { case SectionType.Agenda: - var eventAgendas = _myInfoMateDbContext.EventAgendas.Where(ea => ea.SectionAgendaId == section.Id)/*.OrderBy(gp => gp.or)*/.ToList(); + var eventAgendas = _myInfoMateDbContext.EventAgendas.Where(ea => ea.SectionAgendaId == section.Id).Include(ea => ea.Resource).ToList(); List eventAgendaDTOs = new List(); foreach (var eventAgenda in eventAgendas) { @@ -213,6 +213,20 @@ namespace ManagerService.Controllers (dto as SectionEventDTO).Programme = sectionEvent.Programme; // TODO test ! Need dto ? (dto as SectionEventDTO).GlobalMapAnnotations = sectionEvent.GlobalMapAnnotations?.Select(ma => ma.ToDTO()).ToList() ?? new(); break; + case SectionType.Slider: + var sliderContents = (dto as SliderDTO).contents; + if (sliderContents != null) + { + foreach (var content in sliderContents) + { + if (content.resourceId != null && content.resource == null) + { + var contentResource = _myInfoMateDbContext.Resources.FirstOrDefault(r => r.Id == content.resourceId); + content.resource = contentResource?.ToDTO(); + } + } + } + break; case SectionType.Game: Resource resource = _myInfoMateDbContext.Resources.FirstOrDefault(r => r.Id == (dto as GameDTO).puzzleImageId); (dto as GameDTO).puzzleImage = resource.ToDTO(); @@ -267,6 +281,20 @@ namespace ManagerService.Controllers var subDTO = SectionFactory.ToDTO(subSection); switch (subSection.Type) { + case SectionType.Slider: + var sliderContentsSub = (subDTO as SliderDTO).contents; + if (sliderContentsSub != null) + { + foreach (var content in sliderContentsSub) + { + if (content.resourceId != null && content.resource == null) + { + var contentResource = _myInfoMateDbContext.Resources.FirstOrDefault(r => r.Id == content.resourceId); + content.resource = contentResource?.ToDTO(); + } + } + } + break; case SectionType.Game: Resource resourceSub = _myInfoMateDbContext.Resources.FirstOrDefault(r => r.Id == (subDTO as GameDTO).puzzleImageId); (subDTO as GameDTO).puzzleImage = resourceSub?.ToDTO(); diff --git a/ManagerService/DTOs/InstanceDTO.cs b/ManagerService/DTOs/InstanceDTO.cs index 4e40bf3..9434ed0 100644 --- a/ManagerService/DTOs/InstanceDTO.cs +++ b/ManagerService/DTOs/InstanceDTO.cs @@ -18,6 +18,10 @@ namespace ManagerService.DTOs public bool? isAssistant { get; set; } + public string? webSlug { get; set; } + + public string? publicApiKey { get; set; } + public string? subscriptionPlanId { get; set; } public SubscriptionPlanDTO? subscriptionPlan { get; set; } public int? aiRequestsThisMonth { get; set; } diff --git a/ManagerService/DTOs/RemoteEventAgendaDTO.cs b/ManagerService/DTOs/RemoteEventAgendaDTO.cs index 9ef4b3f..d8182e6 100644 --- a/ManagerService/DTOs/RemoteEventAgendaDTO.cs +++ b/ManagerService/DTOs/RemoteEventAgendaDTO.cs @@ -72,8 +72,8 @@ namespace ManagerService.DTOs // Handle YYYYMMDD format (e.g. "20260327") if (dateStr.Length == 8 && long.TryParse(dateStr, out _)) if (DateTime.TryParseExact(dateStr, "yyyyMMdd", null, System.Globalization.DateTimeStyles.None, out var dt8)) - return dt8; - if (DateTime.TryParse(dateStr, out var dt)) return dt; + return DateTime.SpecifyKind(dt8, DateTimeKind.Utc); + if (DateTime.TryParse(dateStr, out var dt)) return DateTime.SpecifyKind(dt, DateTimeKind.Utc); return null; } } diff --git a/ManagerService/Data/Instance.cs b/ManagerService/Data/Instance.cs index 28db7f8..2ae64d8 100644 --- a/ManagerService/Data/Instance.cs +++ b/ManagerService/Data/Instance.cs @@ -36,6 +36,10 @@ namespace ManagerService.Data public bool IsAssistant { get; set; } + public string? WebSlug { get; set; } + + public string? PublicApiKey { get; set; } + public string? SubscriptionPlanId { get; set; } [ForeignKey("SubscriptionPlanId")] @@ -71,6 +75,8 @@ namespace ManagerService.Data isWeb = IsWeb, isVR = IsVR, isAssistant = IsAssistant, + webSlug = WebSlug, + publicApiKey = PublicApiKey, subscriptionPlanId = SubscriptionPlanId, subscriptionPlan = SubscriptionPlan?.ToDTO(), aiRequestsThisMonth = AiRequestsThisMonth, @@ -96,6 +102,10 @@ namespace ManagerService.Data IsWeb = instanceDTO.isWeb ?? false; IsVR = instanceDTO.isVR ?? false; IsAssistant = instanceDTO.isAssistant ?? false; + 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) diff --git a/ManagerService/Migrations/20260428113203_AddWebSlugAndPublicApiKeyToInstance.Designer.cs b/ManagerService/Migrations/20260428113203_AddWebSlugAndPublicApiKeyToInstance.Designer.cs new file mode 100644 index 0000000..2e2eb4a --- /dev/null +++ b/ManagerService/Migrations/20260428113203_AddWebSlugAndPublicApiKeyToInstance.Designer.cs @@ -0,0 +1,1584 @@ +// +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; + +#nullable disable + +namespace ManagerService.Migrations +{ + [DbContext(typeof(MyInfoMateDbContext))] + [Migration("20260428113203_AddWebSlugAndPublicApiKeyToInstance")] + partial class AddWebSlugAndPublicApiKeyToInstance + { + /// + 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.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("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("DeviceId") + .HasColumnType("text"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("IsDate") + .HasColumnType("boolean"); + + b.Property("IsHour") + .HasColumnType("boolean"); + + b.Property("IsSectionImageBackground") + .HasColumnType("boolean"); + + b.Property("LayoutMainPage") + .HasColumnType("integer"); + + 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.Property("WeightMasonryGrid") + .HasColumnType("integer"); + + 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("InstanceId") + .IsRequired() + .HasColumnType("text"); + + b.Property("IsAssistant") + .HasColumnType("boolean"); + + b.PrimitiveCollection>("Languages") + .HasColumnType("text[]"); + + b.Property("LayoutMainPage") + .HasColumnType("integer"); + + 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("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.ToTable("AuditLogs"); + }); + + modelBuilder.Entity("ManagerService.Data.Configuration", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("DateCreation") + .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.ToTable("Configurations"); + }); + + 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.ToTable("Devices"); + }); + + modelBuilder.Entity("ManagerService.Data.Instance", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AiRequestsPerMonth") + .HasColumnType("integer"); + + b.Property("AiRequestsThisMonth") + .HasColumnType("integer"); + + b.Property("AiUsageMonthKey") + .HasColumnType("text"); + + b.Property("DateCreation") + .HasColumnType("timestamp with time zone"); + + b.Property("HasAdvancedStats") + .HasColumnType("boolean"); + + b.Property("HasStats") + .HasColumnType("boolean"); + + b.Property("IsAssistant") + .HasColumnType("boolean"); + + b.Property("IsMobile") + .HasColumnType("boolean"); + + b.Property("IsPushNotification") + .HasColumnType("boolean"); + + b.Property("IsStatistic") + .HasColumnType("boolean"); + + b.Property("IsTablet") + .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("SubscriptionPlanId") + .HasColumnType("text"); + + 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("DateCreation") + .HasColumnType("timestamp with time zone"); + + b.Property("InstanceId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Label") + .IsRequired() + .HasColumnType("text"); + + b.Property("SizeBytes") + .HasColumnType("bigint"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("Url") + .HasColumnType("text"); + + b.HasKey("Id"); + + 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("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("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("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("Description") + .HasColumnType("jsonb"); + + b.Property("HideNextStepsUntilComplete") + .HasColumnType("boolean"); + + b.Property("InstanceId") + .IsRequired() + .HasColumnType("text"); + + b.Property("IsLinear") + .HasColumnType("boolean"); + + b.Property("Order") + .HasColumnType("integer"); + + b.Property("RequireSuccessToAdvance") + .HasColumnType("boolean"); + + b.Property("SectionEventId") + .HasColumnType("text"); + + b.Property("SectionGameId") + .HasColumnType("text"); + + b.Property("SectionMapId") + .HasColumnType("text"); + + b.Property("Title") + .IsRequired() + .HasColumnType("jsonb"); + + b.HasKey("Id"); + + b.HasIndex("SectionEventId"); + + b.HasIndex("SectionGameId"); + + b.HasIndex("SectionMapId"); + + b.ToTable("GuidedPaths"); + }); + + modelBuilder.Entity("ManagerService.Data.SubSection.GuidedStep", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Description") + .HasColumnType("jsonb"); + + b.Property("Geometry") + .HasColumnType("geometry"); + + b.Property("GuidedPathId") + .IsRequired() + .HasColumnType("text"); + + b.Property("ImageUrl") + .HasColumnType("text"); + + b.Property("IsHiddenInitially") + .HasColumnType("boolean"); + + b.Property("IsStepLocked") + .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("TriggerGeoPointId") + .HasColumnType("integer"); + + b.Property("ZoneRadiusMeters") + .HasColumnType("double precision"); + + b.HasKey("Id"); + + b.HasIndex("GuidedPathId"); + + b.HasIndex("TriggerGeoPointId"); + + 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("AiRequestsPerMonth") + .HasColumnType("integer"); + + 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", + AiRequestsPerMonth = 0, + HasAdvancedStats = false, + HasStats = false, + Name = "Starter", + StatsHistoryDays = 30, + StorageQuotaBytes = 1073741824L + }, + new + { + Id = "plan-standard", + AiRequestsPerMonth = 500, + HasAdvancedStats = false, + HasStats = true, + Name = "Standard", + StatsHistoryDays = 30, + StorageQuotaBytes = 10737418240L + }, + new + { + Id = "plan-premium", + AiRequestsPerMonth = 2000, + HasAdvancedStats = true, + HasStats = true, + Name = "Premium", + StatsHistoryDays = 0, + StorageQuotaBytes = 53687091200L + }); + }); + + modelBuilder.Entity("ManagerService.Data.User", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("DateCreation") + .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("Role") + .HasColumnType("integer"); + + b.Property("Token") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + 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.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>("ParcoursIds") + .HasColumnType("jsonb"); + + 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("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("MapResourceId") + .HasColumnType("text"); + + b.Property("MapTypeMapbox") + .HasColumnType("integer"); + + b.Property("MapZoom") + .HasColumnType("integer"); + + b.HasIndex("MapResourceId"); + + 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.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.SubSection.SectionEvent", "SectionEvent") + .WithMany() + .HasForeignKey("SectionEventId"); + + b.HasOne("ManagerService.Data.SubSection.SectionGame", "SectionGame") + .WithMany() + .HasForeignKey("SectionGameId"); + + b.HasOne("ManagerService.Data.SubSection.SectionMap", "SectionMap") + .WithMany() + .HasForeignKey("SectionMapId"); + + b.Navigation("SectionEvent"); + + b.Navigation("SectionGame"); + + b.Navigation("SectionMap"); + }); + + modelBuilder.Entity("ManagerService.Data.SubSection.GuidedStep", b => + { + b.HasOne("ManagerService.Data.SubSection.GuidedPath", "GuidedPath") + .WithMany("Steps") + .HasForeignKey("GuidedPathId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("ManagerService.Data.SubSection.GeoPoint", "TriggerGeoPoint") + .WithMany() + .HasForeignKey("TriggerGeoPointId"); + + b.Navigation("GuidedPath"); + + b.Navigation("TriggerGeoPoint"); + }); + + 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", "MapResource") + .WithMany() + .HasForeignKey("MapResourceId"); + + b.Navigation("MapResource"); + }); + + 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.SectionQuiz", b => + { + b.Navigation("QuizQuestions"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/ManagerService/Migrations/20260428113203_AddWebSlugAndPublicApiKeyToInstance.cs b/ManagerService/Migrations/20260428113203_AddWebSlugAndPublicApiKeyToInstance.cs new file mode 100644 index 0000000..9e7a412 --- /dev/null +++ b/ManagerService/Migrations/20260428113203_AddWebSlugAndPublicApiKeyToInstance.cs @@ -0,0 +1,38 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace ManagerService.Migrations +{ + /// + public partial class AddWebSlugAndPublicApiKeyToInstance : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "PublicApiKey", + table: "Instances", + type: "text", + nullable: true); + + migrationBuilder.AddColumn( + name: "WebSlug", + table: "Instances", + type: "text", + nullable: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "PublicApiKey", + table: "Instances"); + + migrationBuilder.DropColumn( + name: "WebSlug", + table: "Instances"); + } + } +} diff --git a/ManagerService/Migrations/MyInfoMateDbContextModelSnapshot.cs b/ManagerService/Migrations/MyInfoMateDbContextModelSnapshot.cs index 23e8cad..16840bf 100644 --- a/ManagerService/Migrations/MyInfoMateDbContextModelSnapshot.cs +++ b/ManagerService/Migrations/MyInfoMateDbContextModelSnapshot.cs @@ -382,6 +382,9 @@ namespace ManagerService.Migrations b.Property("PinCode") .HasColumnType("text"); + b.Property("PublicApiKey") + .HasColumnType("text"); + b.Property("StatsHistoryDays") .HasColumnType("integer"); @@ -391,6 +394,9 @@ namespace ManagerService.Migrations b.Property("SubscriptionPlanId") .HasColumnType("text"); + b.Property("WebSlug") + .HasColumnType("text"); + b.HasKey("Id"); b.HasIndex("SubscriptionPlanId"); diff --git a/ManagerService/Security/ApiKeyAuthenticationHandler.cs b/ManagerService/Security/ApiKeyAuthenticationHandler.cs index b1c5c68..239b24e 100644 --- a/ManagerService/Security/ApiKeyAuthenticationHandler.cs +++ b/ManagerService/Security/ApiKeyAuthenticationHandler.cs @@ -39,15 +39,31 @@ namespace ManagerService.Security (k.DateExpiration == null || k.DateExpiration > System.DateTime.UtcNow) && (k.Key == value || k.KeyHash == keyHash)); - if (apiKey == null) - return AuthenticateResult.Fail("Invalid API Key"); + string instanceId; + string appType; + + if (apiKey != null) + { + instanceId = apiKey.InstanceId; + appType = apiKey.AppType.ToString(); + } + else + { + var instance = await _db.Instances.FirstOrDefaultAsync(i => i.PublicApiKey == value); + if (instance == null) + return AuthenticateResult.Fail("Invalid API Key"); + + instanceId = instance.Id; + appType = "Web"; + } var claims = new[] { - new Claim(Service.Security.ClaimTypes.InstanceId, apiKey.InstanceId), - new Claim(Service.Security.ClaimTypes.AppType, apiKey.AppType.ToString()), + new Claim(Service.Security.ClaimTypes.InstanceId, instanceId), + new Claim(Service.Security.ClaimTypes.AppType, appType), new Claim(Service.Security.ClaimTypes.Permission, Service.Security.Permissions.AppRead), new Claim(Service.Security.ClaimTypes.Permission, Service.Security.Permissions.Viewer), + new Claim(Service.Security.ClaimTypes.Permission, Service.Security.Permissions.ContentEditor), }; var principal = new ClaimsPrincipal(new ClaimsIdentity(claims, Scheme.Name)); diff --git a/ManagerService/Services/AgendaSyncService.cs b/ManagerService/Services/AgendaSyncService.cs index 53e6c67..c9bd774 100644 --- a/ManagerService/Services/AgendaSyncService.cs +++ b/ManagerService/Services/AgendaSyncService.cs @@ -51,6 +51,7 @@ namespace ManagerService.Services var section = db.Sections.OfType() .Include(sa => sa.EventAgendas) + .ThenInclude(ea => ea.Resource) .FirstOrDefault(sa => sa.Id == sectionAgendaId); if (section == null || !section.IsOnlineAgenda || section.AgendaResourceIds == null) @@ -115,6 +116,25 @@ namespace ManagerService.Services existing.Website = remote.website; existing.IdVideoYoutube = remote.id_video_youtube; existing.IsSynced = true; + + if (!string.IsNullOrEmpty(remote.image) && string.IsNullOrEmpty(existing.ResourceId)) + { + var imageResource = db.Resources.FirstOrDefault(r => r.Url == remote.image && r.InstanceId == section.InstanceId); + if (imageResource == null) + { + imageResource = new Resource + { + Id = Guid.NewGuid().ToString(), + Type = ResourceType.ImageUrl, + Label = remote.name ?? "agenda-image", + Url = remote.image, + InstanceId = section.InstanceId, + DateCreation = DateTime.UtcNow, + }; + db.Resources.Add(imageResource); + } + existing.ResourceId = imageResource.Id; + } } } diff --git a/ManagerService/Services/SectionFactory.cs b/ManagerService/Services/SectionFactory.cs index 6a757f5..1ee3c3c 100644 --- a/ManagerService/Services/SectionFactory.cs +++ b/ManagerService/Services/SectionFactory.cs @@ -384,6 +384,7 @@ namespace ManagerService.Services order = agenda.Order, imageId = agenda.ImageId, imageSource = agenda.ImageSource, + isActive = agenda.IsActive, isSubSection = agenda.IsSubSection, parentId = agenda.ParentId, isBeacon = agenda.IsBeacon, @@ -409,6 +410,7 @@ namespace ManagerService.Services order = article.Order, imageId = article.ImageId, imageSource = article.ImageSource, + isActive = article.IsActive, isSubSection = article.IsSubSection, parentId = article.ParentId, isBeacon = article.IsBeacon, @@ -435,6 +437,7 @@ namespace ManagerService.Services order = sectionEvent.Order, imageId = sectionEvent.ImageId, imageSource = sectionEvent.ImageSource, + isActive = sectionEvent.IsActive, isSubSection = sectionEvent.IsSubSection, parentId = sectionEvent.ParentId, isBeacon = sectionEvent.IsBeacon, @@ -462,6 +465,7 @@ namespace ManagerService.Services order = map.Order, imageId = map.ImageId, imageSource = map.ImageSource, + isActive = map.IsActive, isSubSection = map.IsSubSection, parentId = map.ParentId, isBeacon = map.IsBeacon, @@ -493,6 +497,7 @@ namespace ManagerService.Services order = menu.Order, imageId = menu.ImageId, imageSource = menu.ImageSource, + isActive = menu.IsActive, isSubSection = menu.IsSubSection, parentId = menu.ParentId, isBeacon = menu.IsBeacon, @@ -515,6 +520,7 @@ namespace ManagerService.Services order = pdf.Order, imageId = pdf.ImageId, imageSource = pdf.ImageSource, + isActive = pdf.IsActive, isSubSection = pdf.IsSubSection, parentId = pdf.ParentId, isBeacon = pdf.IsBeacon, @@ -537,6 +543,7 @@ namespace ManagerService.Services order = game.Order, imageId = game.ImageId, imageSource = game.ImageSource, + isActive = game.IsActive, isSubSection = game.IsSubSection, parentId = game.ParentId, isBeacon = game.IsBeacon, @@ -565,6 +572,7 @@ namespace ManagerService.Services order = quiz.Order, imageId = quiz.ImageId, imageSource = quiz.ImageSource, + isActive = quiz.IsActive, isSubSection = quiz.IsSubSection, parentId = quiz.ParentId, isBeacon = quiz.IsBeacon, @@ -591,6 +599,7 @@ namespace ManagerService.Services order = slider.Order, imageId = slider.ImageId, imageSource = slider.ImageSource, + isActive = slider.IsActive, isSubSection = slider.IsSubSection, parentId = slider.ParentId, isBeacon = slider.IsBeacon, @@ -613,6 +622,7 @@ namespace ManagerService.Services order = video.Order, imageId = video.ImageId, imageSource = video.ImageSource, + isActive = video.IsActive, isSubSection = video.IsSubSection, parentId = video.ParentId, isBeacon = video.IsBeacon, @@ -635,6 +645,7 @@ namespace ManagerService.Services order = weather.Order, imageId = weather.ImageId, imageSource = weather.ImageSource, + isActive = weather.IsActive, isSubSection = weather.IsSubSection, parentId = weather.ParentId, isBeacon = weather.IsBeacon, @@ -659,6 +670,7 @@ namespace ManagerService.Services order = web.Order, imageId = web.ImageId, imageSource = web.ImageSource, + isActive = web.IsActive, isSubSection = web.IsSubSection, parentId = web.ParentId, isBeacon = web.IsBeacon, diff --git a/ManagerService/Startup.cs b/ManagerService/Startup.cs index 6fa02c8..9b221b9 100644 --- a/ManagerService/Startup.cs +++ b/ManagerService/Startup.cs @@ -250,23 +250,27 @@ namespace ManagerService app.UseCors( #if DEBUG options => options - .SetIsOriginAllowed(origin => string.IsNullOrEmpty(origin) || origin == "http://localhost:9090") + .SetIsOriginAllowed(origin => string.IsNullOrEmpty(origin) || origin == "http://localhost:9090" || origin == "http://localhost:3000") .AllowAnyMethod() .AllowAnyHeader() .AllowCredentials() #else options => options - .SetIsOriginAllowed(origin => string.IsNullOrEmpty(origin) || origin == "https://manager.mymuseum.be" || origin == "https://manager.myinfomate.be" || origin == "https://visitnamur.myinfomate.be" || origin == "https://fortsaintheribert.myinfomate.be") + .SetIsOriginAllowed(origin => string.IsNullOrEmpty(origin) || origin == "https://manager.mymuseum.be" || origin == "https://manager.myinfomate.be" || origin == "https://visitnamur.myinfomate.be" || origin == "https://fortsaintheribert.myinfomate.be" || origin == "https://app.myinfomate.be") .AllowAnyMethod() .AllowAnyHeader() .AllowCredentials() #endif ); +#if DEBUG + app.UseHangfireDashboard("/hangfire"); +#else app.UseHangfireDashboard("/hangfire", new DashboardOptions { Authorization = new[] { new HangfireDashboardAuthorizationFilter() } }); +#endif RecurringJob.AddOrUpdate( "agenda-sync-daily",