From a452f4af04f510ffb1b3ce787cfa0ee7fc12ac4f Mon Sep 17 00:00:00 2001 From: Thomas Fransolet Date: Fri, 13 Mar 2026 17:45:01 +0100 Subject: [PATCH] Add migration, need to check if migration with apikey correct.. ! + add roles and security (role and apikey) - need to be tested + visitevent stats + ai etc + all need to be tested ! --- ManagerService/Controllers/AiController.cs | 66 + .../Controllers/ApiKeyController.cs | 68 + .../ApplicationInstanceController.cs | 11 +- .../Controllers/ConfigurationController.cs | 18 +- .../Controllers/DeviceController.cs | 4 +- .../Controllers/InstanceController.cs | 36 +- .../Controllers/ResourceController.cs | 4 +- .../Controllers/SectionAgendaController.cs | 4 +- .../Controllers/SectionController.cs | 24 +- .../Controllers/SectionEventController.cs | 4 +- .../Controllers/SectionMapController.cs | 6 +- .../Controllers/SectionQuizController.cs | 10 +- ManagerService/Controllers/StatsController.cs | 274 ++++ ManagerService/Controllers/UserController.cs | 97 +- ManagerService/DTOs/AiChatDTO.cs | 42 + ManagerService/DTOs/ApplicationInstanceDTO.cs | 4 + ManagerService/DTOs/InstanceDTO.cs | 2 + ManagerService/DTOs/StatsSummaryDTO.cs | 66 + ManagerService/DTOs/TokenDTO.cs | 2 + ManagerService/DTOs/UserDetailDTO.cs | 6 +- ManagerService/DTOs/VisitEventDTO.cs | 18 + ManagerService/Data/ApiKey.cs | 33 + ManagerService/Data/ApiKeyAppType.cs | 4 + ManagerService/Data/ApplicationInstance.cs | 8 +- ManagerService/Data/Instance.cs | 4 + ManagerService/Data/MyInfoMateDbContext.cs | 6 + ManagerService/Data/User.cs | 3 + ManagerService/Data/UserRole.cs | 10 + ManagerService/Data/VisitEvent.cs | 60 + ManagerService/ManagerService.csproj | 2 + .../20260312130921_AddAIAssistant.Designer.cs | 1248 +++++++++++++++ .../20260312130921_AddAIAssistant.cs | 40 + .../20260312154609_AddVisitEvent.Designer.cs | 1294 ++++++++++++++++ .../20260312154609_AddVisitEvent.cs | 53 + .../20260313144641_AddUserRole.Designer.cs | 1346 +++++++++++++++++ .../Migrations/20260313144641_AddUserRole.cs | 63 + .../20260313144657_AddApiKeys.Designer.cs | 1346 +++++++++++++++++ .../Migrations/20260313144657_AddApiKeys.cs | 22 + .../MyInfoMateDbContextModelSnapshot.cs | 104 ++ ManagerService/Security.cs | 60 +- .../Security/ApiKeyAuthenticationHandler.cs | 57 + .../Services/ApiKeyDatabaseService.cs | 125 ++ ManagerService/Services/AssistantService.cs | 165 ++ ManagerService/Services/TokensService.cs | 41 +- ManagerService/Startup.cs | 44 +- ManagerService/appsettings.json | 5 +- 46 files changed, 6753 insertions(+), 156 deletions(-) create mode 100644 ManagerService/Controllers/AiController.cs create mode 100644 ManagerService/Controllers/ApiKeyController.cs create mode 100644 ManagerService/Controllers/StatsController.cs create mode 100644 ManagerService/DTOs/AiChatDTO.cs create mode 100644 ManagerService/DTOs/StatsSummaryDTO.cs create mode 100644 ManagerService/DTOs/VisitEventDTO.cs create mode 100644 ManagerService/Data/ApiKey.cs create mode 100644 ManagerService/Data/ApiKeyAppType.cs create mode 100644 ManagerService/Data/UserRole.cs create mode 100644 ManagerService/Data/VisitEvent.cs create mode 100644 ManagerService/Migrations/20260312130921_AddAIAssistant.Designer.cs create mode 100644 ManagerService/Migrations/20260312130921_AddAIAssistant.cs create mode 100644 ManagerService/Migrations/20260312154609_AddVisitEvent.Designer.cs create mode 100644 ManagerService/Migrations/20260312154609_AddVisitEvent.cs create mode 100644 ManagerService/Migrations/20260313144641_AddUserRole.Designer.cs create mode 100644 ManagerService/Migrations/20260313144641_AddUserRole.cs create mode 100644 ManagerService/Migrations/20260313144657_AddApiKeys.Designer.cs create mode 100644 ManagerService/Migrations/20260313144657_AddApiKeys.cs create mode 100644 ManagerService/Security/ApiKeyAuthenticationHandler.cs create mode 100644 ManagerService/Services/ApiKeyDatabaseService.cs create mode 100644 ManagerService/Services/AssistantService.cs diff --git a/ManagerService/Controllers/AiController.cs b/ManagerService/Controllers/AiController.cs new file mode 100644 index 0000000..9f071b3 --- /dev/null +++ b/ManagerService/Controllers/AiController.cs @@ -0,0 +1,66 @@ +using ManagerService.Data; +using ManagerService.DTOs; +using ManagerService.Services; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; +using NSwag.Annotations; +using System; +using System.Linq; +using System.Threading.Tasks; + +namespace ManagerService.Controllers +{ + [Authorize(Policy = ManagerService.Service.Security.Policies.Viewer)] + [ApiController, Route("api/[controller]")] + [OpenApiTag("AI", Description = "Assistant IA")] + public class AiController : ControllerBase + { + private readonly AssistantService _assistantService; + private readonly MyInfoMateDbContext _context; + private readonly ILogger _logger; + + public AiController( + AssistantService assistantService, + MyInfoMateDbContext context, + ILogger logger) + { + _assistantService = assistantService; + _context = context; + _logger = logger; + } + + /// + /// Envoie un message à l'assistant IA, scopé à l'instance et optionnellement à une configuration + /// + [HttpPost("chat")] + [ProducesResponseType(typeof(AiChatResponse), 200)] + [ProducesResponseType(403)] + [ProducesResponseType(typeof(string), 500)] + public async Task Chat([FromBody] AiChatRequest request) + { + try + { + // Vérifie que l'instance a activé la fonctionnalité assistant + var instance = _context.Instances.FirstOrDefault(i => i.Id == request.InstanceId); + if (instance == null || !instance.IsAssistant) + return Forbid(); + + // Vérifie que l'app concernée a activé l'assistant + var appInstance = _context.ApplicationInstances + .FirstOrDefault(ai => ai.InstanceId == request.InstanceId && ai.AppType == request.AppType); + + if (appInstance == null || !appInstance.IsAssistant) + return Forbid(); + + var result = await _assistantService.ChatAsync(request); + return Ok(result); + } + catch (Exception ex) + { + _logger.LogError(ex, "Erreur lors de l'appel à l'assistant IA"); + return new ObjectResult("Une erreur est survenue") { StatusCode = 500 }; + } + } + } +} diff --git a/ManagerService/Controllers/ApiKeyController.cs b/ManagerService/Controllers/ApiKeyController.cs new file mode 100644 index 0000000..031c45c --- /dev/null +++ b/ManagerService/Controllers/ApiKeyController.cs @@ -0,0 +1,68 @@ +using ManagerService.Data; +using ManagerService.Services; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using NSwag.Annotations; +using System; +using System.Threading.Tasks; + +namespace ManagerService.Controllers +{ + [Authorize(Policy = ManagerService.Service.Security.Policies.InstanceAdmin)] + [ApiController, Route("api/[controller]")] + [OpenApiTag("ApiKey", Description = "API Key management for mobile apps")] + public class ApiKeyController : ControllerBase + { + private readonly ApiKeyDatabaseService _apiKeyService; + + public ApiKeyController(ApiKeyDatabaseService apiKeyService) + { + _apiKeyService = apiKeyService; + } + + /// List API keys for the caller's instance + [HttpGet] + public async Task GetApiKeys() + { + var instanceId = User.FindFirst(ManagerService.Service.Security.ClaimTypes.InstanceId)?.Value; + if (string.IsNullOrEmpty(instanceId)) + return Forbid(); + + var keys = await _apiKeyService.GetByInstanceAsync(instanceId); + return Ok(keys); + } + + /// Create a new API key (plain key returned once) + [HttpPost] + public async Task CreateApiKey([FromBody] CreateApiKeyRequest request) + { + if (request == null || string.IsNullOrEmpty(request.Name)) + return BadRequest("Name is required"); + + var instanceId = User.FindFirst(ManagerService.Service.Security.ClaimTypes.InstanceId)?.Value; + if (string.IsNullOrEmpty(instanceId)) + return Forbid(); + + var plainKey = await _apiKeyService.CreateAsync(instanceId, request.Name, request.AppType); + return Ok(new { key = plainKey }); + } + + /// Revoke an API key + [HttpDelete("{id}")] + public async Task RevokeApiKey(string id) + { + var instanceId = User.FindFirst(ManagerService.Service.Security.ClaimTypes.InstanceId)?.Value; + if (string.IsNullOrEmpty(instanceId)) + return Forbid(); + + var success = await _apiKeyService.RevokeAsync(id, instanceId); + return success ? NoContent() : NotFound(); + } + } + + public class CreateApiKeyRequest + { + public string Name { get; set; } + public ApiKeyAppType AppType { get; set; } + } +} diff --git a/ManagerService/Controllers/ApplicationInstanceController.cs b/ManagerService/Controllers/ApplicationInstanceController.cs index 0bbcacc..c1ddf1c 100644 --- a/ManagerService/Controllers/ApplicationInstanceController.cs +++ b/ManagerService/Controllers/ApplicationInstanceController.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using Manager.Services; @@ -14,7 +14,7 @@ using NSwag.Annotations; namespace ManagerService.Controllers { - [Authorize] // TODO Add ROLES (Roles = "Admin") + [Authorize(Policy = ManagerService.Service.Security.Policies.ContentEditor)] [ApiController, Route("api/[controller]")] [OpenApiTag("ApplicationInstance", Description = "Application instance management")] public class ApplicationInstanceController : ControllerBase @@ -49,8 +49,13 @@ namespace ManagerService.Controllers try { List applicationInstances = _myInfoMateDbContext.ApplicationInstances.Include(s => s.SectionEvent).Where(ai => ai.InstanceId == instanceId).ToList(); + var instance = _myInfoMateDbContext.Instances.FirstOrDefault(i => i.Id == instanceId); - return new OkObjectResult(applicationInstances.Select(ai => ai.ToDTO(_myInfoMateDbContext)).OrderBy(c => c.appType)); + return new OkObjectResult(applicationInstances.Select(ai => { + var dto = ai.ToDTO(_myInfoMateDbContext); + dto.isStatistic = instance?.IsStatistic ?? false; + return dto; + }).OrderBy(c => c.appType)); } catch (Exception ex) { diff --git a/ManagerService/Controllers/ConfigurationController.cs b/ManagerService/Controllers/ConfigurationController.cs index 438bfe1..bd17c24 100644 --- a/ManagerService/Controllers/ConfigurationController.cs +++ b/ManagerService/Controllers/ConfigurationController.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Net.Http; @@ -22,7 +22,7 @@ using NSwag.Annotations; namespace ManagerService.Controllers { - [Authorize] // TODO Add ROLES (Roles = "Admin") + [Authorize(Policy = ManagerService.Service.Security.Policies.ContentEditor)] [ApiController, Route("api/[controller]")] [OpenApiTag("Configuration", Description = "Configuration management")] public class ConfigurationController : ControllerBase @@ -49,7 +49,7 @@ namespace ManagerService.Controllers /// Get a list of all configuration (summary) /// /// id instance - [AllowAnonymous] + [Authorize(Policy = ManagerService.Service.Security.Policies.AppReadAccess)] [ProducesResponseType(typeof(List), 200)] [ProducesResponseType(typeof(string), 500)] [HttpGet] @@ -80,7 +80,7 @@ namespace ManagerService.Controllers /// Get Confuguration list by instanceId' pincode /// /// Code pin - [AllowAnonymous] + [Authorize(Policy = ManagerService.Service.Security.Policies.AppReadAccess)] [ProducesResponseType(typeof(List), 200)] [ProducesResponseType(typeof(string), 404)] [ProducesResponseType(typeof(string), 500)] @@ -114,7 +114,7 @@ namespace ManagerService.Controllers /// Get a specific display configuration /// /// id configuration - [AllowAnonymous] + [Authorize(Policy = ManagerService.Service.Security.Policies.AppReadAccess)] [ProducesResponseType(typeof(ConfigurationDTO), 200)] [ProducesResponseType(typeof(string), 404)] [ProducesResponseType(typeof(string), 500)] @@ -177,12 +177,12 @@ namespace ManagerService.Controllers } else { - Console.WriteLine("Aucune ville trouvée."); + Console.WriteLine("Aucune ville trouv�e."); } } catch (HttpRequestException e) { - Console.WriteLine($"Une erreur s'est produite lors de la requête HTTP : {e.Message}"); + Console.WriteLine($"Une erreur s'est produite lors de la requ�te HTTP : {e.Message}"); } } } @@ -191,7 +191,7 @@ namespace ManagerService.Controllers } } catch (Exception e) { - Console.WriteLine($"Une erreur s'est produite lors de la mise à jour des sections de type météo : {e.Message}"); + Console.WriteLine($"Une erreur s'est produite lors de la mise � jour des sections de type m�t�o : {e.Message}"); } return new OkObjectResult(configuration.ToDTO(sectionIds)); @@ -380,7 +380,7 @@ namespace ManagerService.Controllers /// /// Id of configuration to export /// Language to export - [AllowAnonymous] + [Authorize(Policy = ManagerService.Service.Security.Policies.AppReadAccess)] [ProducesResponseType(typeof(FileContentResult), 200)] [ProducesResponseType(typeof(string), 400)] [ProducesResponseType(typeof(string), 404)] diff --git a/ManagerService/Controllers/DeviceController.cs b/ManagerService/Controllers/DeviceController.cs index 1f9bd41..1411dfc 100644 --- a/ManagerService/Controllers/DeviceController.cs +++ b/ManagerService/Controllers/DeviceController.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using Manager.Services; @@ -15,7 +15,7 @@ using NSwag.Annotations; namespace ManagerService.Controllers { - [Authorize] // TODO Add ROLES (Roles = "Admin") + [Authorize(Policy = ManagerService.Service.Security.Policies.InstanceAdmin)] [ApiController, Route("api/[controller]")] [OpenApiTag("Device", Description = "Device management")] public class DeviceController : ControllerBase diff --git a/ManagerService/Controllers/InstanceController.cs b/ManagerService/Controllers/InstanceController.cs index 1f0ae2f..94f629f 100644 --- a/ManagerService/Controllers/InstanceController.cs +++ b/ManagerService/Controllers/InstanceController.cs @@ -1,6 +1,7 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; +using System.Threading.Tasks; using Manager.Services; using ManagerService.Data; using ManagerService.DTOs; @@ -13,7 +14,7 @@ using NSwag.Annotations; namespace ManagerService.Controllers { - [Authorize] // TODO Add ROLES (Roles = "Admin") + [Authorize(Policy = ManagerService.Service.Security.Policies.SuperAdmin)] [ApiController, Route("api/[controller]")] [OpenApiTag("Instance", Description = "Instance management")] public class InstanceController : ControllerBase @@ -24,15 +25,17 @@ namespace ManagerService.Controllers private UserDatabaseService _userService; private readonly ILogger _logger; private readonly ProfileLogic _profileLogic; + private readonly ApiKeyDatabaseService _apiKeyService; IHexIdGeneratorService idService = new HexIdGeneratorService(); - public InstanceController(ILogger logger, InstanceDatabaseService instanceService, UserDatabaseService userService, ProfileLogic profileLogic, MyInfoMateDbContext myInfoMateDbContext) + public InstanceController(ILogger logger, InstanceDatabaseService instanceService, UserDatabaseService userService, ProfileLogic profileLogic, MyInfoMateDbContext myInfoMateDbContext, ApiKeyDatabaseService apiKeyService) { _logger = logger; _instanceService = instanceService; _userService = userService; _profileLogic = profileLogic; _myInfoMateDbContext = myInfoMateDbContext; + _apiKeyService = apiKeyService; } /// @@ -223,6 +226,33 @@ namespace ManagerService.Controllers } + /// + /// Bootstrap: get (or create) an API key for a Flutter app by PIN code + /// + /// Instance PIN code + /// App type (VisitApp, TabletApp, Other) + [AllowAnonymous] + [ProducesResponseType(typeof(object), 200)] + [ProducesResponseType(typeof(string), 404)] + [ProducesResponseType(typeof(string), 500)] + [HttpGet("app-key")] + public async Task GetAppKeyByPin([FromQuery] string pinCode, [FromQuery] ApiKeyAppType appType) + { + try + { + var instance = _myInfoMateDbContext.Instances.FirstOrDefault(i => i.PinCode == pinCode); + if (instance == null) + return new NotFoundObjectResult("Instance not found"); + + var key = await _apiKeyService.GetOrCreateByPinAsync(instance.Id, appType); + return new OkObjectResult(new { key, instanceId = instance.Id }); + } + catch (Exception ex) + { + return new ObjectResult(ex.Message) { StatusCode = 500 }; + } + } + /// /// Delete an instance /// diff --git a/ManagerService/Controllers/ResourceController.cs b/ManagerService/Controllers/ResourceController.cs index c483bcd..01151b8 100644 --- a/ManagerService/Controllers/ResourceController.cs +++ b/ManagerService/Controllers/ResourceController.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Drawing; using System.IO; @@ -19,7 +19,7 @@ using NSwag.Annotations; namespace ManagerService.Controllers { - [Authorize] // TODO Add ROLES (Roles = "Admin") + [Authorize(Policy = ManagerService.Service.Security.Policies.ContentEditor)] [ApiController, Route("api/[controller]")] [OpenApiTag("Resource", Description = "Resource management")] public class ResourceController : ControllerBase diff --git a/ManagerService/Controllers/SectionAgendaController.cs b/ManagerService/Controllers/SectionAgendaController.cs index 4ceb010..96b6ec1 100644 --- a/ManagerService/Controllers/SectionAgendaController.cs +++ b/ManagerService/Controllers/SectionAgendaController.cs @@ -1,4 +1,4 @@ -using ManagerService.Data; +using ManagerService.Data; using ManagerService.Data.SubSection; using ManagerService.Services; using Microsoft.AspNetCore.Authorization; @@ -15,7 +15,7 @@ using ManagerService.Helpers; namespace ManagerService.Controllers { - [Authorize] // TODO Add ROLES (Roles = "Admin") + [Authorize(Policy = ManagerService.Service.Security.Policies.ContentEditor)] [ApiController, Route("api/[controller]")] [OpenApiTag("Section agenda", Description = "Section agenda management")] public class SectionAgendaController : ControllerBase diff --git a/ManagerService/Controllers/SectionController.cs b/ManagerService/Controllers/SectionController.cs index eb41066..622aee6 100644 --- a/ManagerService/Controllers/SectionController.cs +++ b/ManagerService/Controllers/SectionController.cs @@ -1,4 +1,4 @@ -using Manager.DTOs; +using Manager.DTOs; using Manager.Helpers; using Manager.Interfaces.Models; using Manager.Services; @@ -26,7 +26,7 @@ using static ManagerService.Data.SubSection.SectionEvent; namespace ManagerService.Controllers { - [Authorize] // TODO Add ROLES (Roles = "Admin") + [Authorize(Policy = ManagerService.Service.Security.Policies.ContentEditor)] [ApiController, Route("api/[controller]")] [OpenApiTag("Section", Description = "Section management")] public class SectionController : ControllerBase @@ -367,12 +367,12 @@ namespace ManagerService.Controllers } else { - Console.WriteLine("Aucune ville trouvée."); + Console.WriteLine("Aucune ville trouvée."); } } catch (HttpRequestException e) { - Console.WriteLine($"Une erreur s'est produite lors de la requête HTTP : {e.Message}"); + Console.WriteLine($"Une erreur s'est produite lors de la requête HTTP : {e.Message}"); } } } @@ -382,7 +382,7 @@ namespace ManagerService.Controllers } catch (Exception e) { - Console.WriteLine($"Une erreur s'est produite lors de la mise à jour des sections de type météo : {e.Message}"); + Console.WriteLine($"Une erreur s'est produite lors de la mise à jour des sections de type météo : {e.Message}"); } return new OkObjectResult(sectionsToReturn); @@ -684,13 +684,13 @@ namespace ManagerService.Controllers // UPDATE OTHER ORDER var sections = _myInfoMateDbContext.Sections.Where(s => s.ConfigurationId == newSection.configurationId && !s.IsSubSection).OrderBy(s => s.Order).ToList(); - // Retirer la question déplacée + // Retirer la question déplacée sections.RemoveAll(q => q.Id == section.Id); - // Insérer à la première position (déjà en 0-based) + // Insérer à la première position (déjà en 0-based) sections.Insert(0, section); - // Réassigner les ordres en 0-based + // Réassigner les ordres en 0-based for (int i = 0; i < sections.Count; i++) { sections[i].Order = i; @@ -787,7 +787,7 @@ namespace ManagerService.Controllers if (jsonElement.ValueKind == JsonValueKind.Null) throw new ArgumentNullException("Section param is null"); - // Désérialisation de jsonElement en SectionDTO + // Désérialisation de jsonElement en SectionDTO sectionDTO = JsonConvert.DeserializeObject(jsonElement.ToString()); } else @@ -808,15 +808,15 @@ namespace ManagerService.Controllers // If subsection, check if order changed var subSections = _myInfoMateDbContext.Sections.Where(s => s.ParentId == existingSection.ParentId).OrderBy(s => s.Order).ToList(); - // Retirer la sous section déplacée + // Retirer la sous section déplacée subSections.RemoveAll(q => q.Id == existingSection.Id); - // Insérer à la nouvelle position (déjà en 0-based) + // Insérer à la nouvelle position (déjà en 0-based) int newIndex = sectionDTO.order.Value; newIndex = Math.Clamp(newIndex, 0, subSections.Count); subSections.Insert(newIndex, existingSection); - // Réassigner les ordres en 0-based + // Réassigner les ordres en 0-based for (int i = 0; i < subSections.Count; i++) { subSections[i].Order = i; diff --git a/ManagerService/Controllers/SectionEventController.cs b/ManagerService/Controllers/SectionEventController.cs index 041eab3..d56afab 100644 --- a/ManagerService/Controllers/SectionEventController.cs +++ b/ManagerService/Controllers/SectionEventController.cs @@ -1,4 +1,4 @@ -using ManagerService.Data; +using ManagerService.Data; using ManagerService.Data.SubSection; using ManagerService.DTOs; using ManagerService.Helpers; @@ -16,7 +16,7 @@ using static ManagerService.Data.SubSection.SectionEvent; namespace ManagerService.Controllers { - [Authorize] // TODO Add ROLES (Roles = "Admin") + [Authorize(Policy = ManagerService.Service.Security.Policies.ContentEditor)] [ApiController, Route("api/[controller]")] [OpenApiTag("Section event", Description = "Section event management")] public class SectionEventController : ControllerBase diff --git a/ManagerService/Controllers/SectionMapController.cs b/ManagerService/Controllers/SectionMapController.cs index c2e927a..bb7b5bf 100644 --- a/ManagerService/Controllers/SectionMapController.cs +++ b/ManagerService/Controllers/SectionMapController.cs @@ -1,4 +1,4 @@ -using Manager.DTOs; +using Manager.DTOs; using ManagerService.Data; using ManagerService.Data.SubSection; using ManagerService.DTOs; @@ -16,7 +16,7 @@ using System.Linq; namespace ManagerService.Controllers { - [Authorize] // TODO Add ROLES (Roles = "Admin") + [Authorize(Policy = ManagerService.Service.Security.Policies.ContentEditor)] [ApiController, Route("api/[controller]")] [OpenApiTag("Section map", Description = "Section map management")] public class SectionMapController : ControllerBase @@ -658,7 +658,7 @@ namespace ManagerService.Controllers existingGuidedStep.ImageUrl = guidedStepDTO.imageUrl; existingGuidedStep.TriggerGeoPointId = guidedStepDTO.triggerGeoPointId; existingGuidedStep.IsHiddenInitially = guidedStepDTO.isHiddenInitially; - existingGuidedStep.QuizQuestions = guidedStepDTO.quizQuestions; // à convertir si besoin ? + existingGuidedStep.QuizQuestions = guidedStepDTO.quizQuestions; // à convertir si besoin ? existingGuidedStep.IsStepTimer = guidedStepDTO.isStepTimer; existingGuidedStep.IsStepLocked = guidedStepDTO.isStepLocked; existingGuidedStep.TimerSeconds = guidedStepDTO.timerSeconds; diff --git a/ManagerService/Controllers/SectionQuizController.cs b/ManagerService/Controllers/SectionQuizController.cs index 3ad22cf..b2d29f1 100644 --- a/ManagerService/Controllers/SectionQuizController.cs +++ b/ManagerService/Controllers/SectionQuizController.cs @@ -1,4 +1,4 @@ -using Manager.DTOs; +using Manager.DTOs; using Manager.Helpers; using ManagerService.Data; using ManagerService.Data.SubSection; @@ -21,7 +21,7 @@ using System.Text.Json; namespace ManagerService.Controllers { - [Authorize] // TODO Add ROLES (Roles = "Admin") + [Authorize(Policy = ManagerService.Service.Security.Policies.ContentEditor)] [ApiController, Route("api/[controller]")] [OpenApiTag("Section quiz", Description = "Section quiz management")] public class SectionQuizController : ControllerBase @@ -173,15 +173,15 @@ namespace ManagerService.Controllers var questions = existingSection.QuizQuestions.OrderBy(q => q.Order).ToList(); - // Retirer la question déplacée + // Retirer la question déplacée questions.RemoveAll(q => q.Id == existingQuestion.Id); - // Insérer à la nouvelle position (déjà en 0-based) + // Insérer à la nouvelle position (déjà en 0-based) int newIndex = questionDTO.order.Value; newIndex = Math.Clamp(newIndex, 0, questions.Count); questions.Insert(newIndex, existingQuestion); - // Réassigner les ordres en 0-based + // Réassigner les ordres en 0-based for (int i = 0; i < questions.Count; i++) { questions[i].Order = i; diff --git a/ManagerService/Controllers/StatsController.cs b/ManagerService/Controllers/StatsController.cs new file mode 100644 index 0000000..c1eb734 --- /dev/null +++ b/ManagerService/Controllers/StatsController.cs @@ -0,0 +1,274 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.Json; +using ManagerService.Data; +using ManagerService.DTOs; +using ManagerService.Helpers; +using ManagerService.Services; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using NSwag.Annotations; + +namespace ManagerService.Controllers +{ + [ApiController, Route("api/[controller]")] + [OpenApiTag("Stats", Description = "Visit statistics tracking and summary")] + public class StatsController : ControllerBase + { + private readonly MyInfoMateDbContext _db; + IHexIdGeneratorService _idService = new HexIdGeneratorService(); + + public StatsController(MyInfoMateDbContext db) + { + _db = db; + } + + /// Track a single visit event (anonymous) + [AllowAnonymous] + [HttpPost("event")] + [ProducesResponseType(204)] + [ProducesResponseType(typeof(string), 400)] + public IActionResult TrackEvent([FromBody] VisitEventDTO dto) + { + try + { + if (dto == null || string.IsNullOrEmpty(dto.instanceId)) + return BadRequest("instanceId is required"); + + if (!Enum.TryParse(dto.eventType, ignoreCase: true, out var eventType)) + return BadRequest($"Unknown eventType: {dto.eventType}"); + + if (!Enum.TryParse(dto.appType, ignoreCase: true, out var appType)) + appType = AppType.Mobile; + + var visitEvent = new VisitEvent + { + Id = _idService.GenerateHexId(), + InstanceId = dto.instanceId, + ConfigurationId = dto.configurationId, + SectionId = dto.sectionId, + SessionId = dto.sessionId ?? "unknown", + EventType = eventType, + AppType = appType, + Language = dto.language, + DurationSeconds = dto.durationSeconds, + Metadata = dto.metadata, + Timestamp = dto.timestamp?.ToUniversalTime() ?? DateTime.UtcNow + }; + + _db.VisitEvents.Add(visitEvent); + _db.SaveChanges(); + + return NoContent(); + } + catch (Exception ex) + { + return StatusCode(500, ex.Message); + } + } + + /// Get aggregated statistics for an instance + [Authorize(Policy = ManagerService.Service.Security.Policies.Viewer)] + [HttpGet("summary")] + [ProducesResponseType(typeof(StatsSummaryDTO), 200)] + [ProducesResponseType(typeof(string), 400)] + public IActionResult GetSummary([FromQuery] string instanceId, [FromQuery] DateTime? from, [FromQuery] DateTime? to, [FromQuery] string? appType) + { + try + { + if (string.IsNullOrEmpty(instanceId)) + return BadRequest("instanceId is required"); + + var fromDate = (from ?? DateTime.UtcNow.AddDays(-30)).ToUniversalTime(); + var toDate = (to ?? DateTime.UtcNow).ToUniversalTime(); + + var eventsQuery = _db.VisitEvents + .Where(e => e.InstanceId == instanceId && e.Timestamp >= fromDate && e.Timestamp <= toDate); + + if (!string.IsNullOrEmpty(appType) && Enum.TryParse(appType, ignoreCase: true, out var appTypeFilter)) + eventsQuery = eventsQuery.Where(e => e.AppType == appTypeFilter); + + var events = eventsQuery.ToList(); + + var summary = new StatsSummaryDTO(); + + // Sessions + summary.TotalSessions = events.Select(e => e.SessionId).Distinct().Count(); + + // Avg visit duration (from SectionLeave events with duration) + var leaveEvents = events.Where(e => e.EventType == VisitEventType.SectionLeave && e.DurationSeconds.HasValue).ToList(); + if (leaveEvents.Any()) + { + var sessionsWithDuration = leaveEvents + .GroupBy(e => e.SessionId) + .Select(g => g.Sum(e => e.DurationSeconds!.Value)); + summary.AvgVisitDurationSeconds = (int)sessionsWithDuration.Average(); + } + + // Top sections + var sectionViews = events + .Where(e => e.EventType == VisitEventType.SectionView && e.SectionId != null) + .GroupBy(e => e.SectionId!) + .Select(g => + { + var leaveDurations = leaveEvents + .Where(l => l.SectionId == g.Key) + .Select(l => l.DurationSeconds!.Value) + .ToList(); + return new SectionStatDTO + { + SectionId = g.Key, + SectionTitle = g.Key, // apps can enrich with section titles client-side + Views = g.Count(), + AvgDurationSeconds = leaveDurations.Any() ? (int)leaveDurations.Average() : 0 + }; + }) + .OrderByDescending(s => s.Views) + .Take(10) + .ToList(); + summary.TopSections = sectionViews; + + // Visits by day + summary.VisitsByDay = events + .Where(e => e.EventType == VisitEventType.SectionView) + .GroupBy(e => e.Timestamp.Date.ToString("yyyy-MM-dd")) + .Select(g => new DayStatDTO + { + Date = g.Key, + Total = g.Count(), + Mobile = g.Count(e => e.AppType == AppType.Mobile), + Tablet = g.Count(e => e.AppType == AppType.Tablet) + }) + .OrderBy(d => d.Date) + .ToList(); + + // Language distribution + summary.LanguageDistribution = events + .Where(e => e.Language != null) + .GroupBy(e => e.Language!) + .ToDictionary(g => g.Key, g => g.Count()); + + // AppType distribution + summary.AppTypeDistribution = events + .GroupBy(e => e.AppType.ToString()) + .ToDictionary(g => g.Key, g => g.Count()); + + // Top POIs + var poiEvents = events.Where(e => e.EventType == VisitEventType.MapPoiTap && e.Metadata != null).ToList(); + var poiGroups = new Dictionary(); + foreach (var ev in poiEvents) + { + try + { + var meta = JsonSerializer.Deserialize(ev.Metadata!); + if (meta.TryGetProperty("geoPointId", out var idEl) && meta.TryGetProperty("geoPointTitle", out var titleEl)) + { + int id = idEl.GetInt32(); + string title = titleEl.GetString() ?? ""; + string sectionId = ev.SectionId ?? ""; + if (poiGroups.TryGetValue(id, out var existing)) + poiGroups[id] = (existing.title, existing.taps + 1, existing.sectionId); + else + poiGroups[id] = (title, 1, sectionId); + } + } + catch { /* skip malformed metadata */ } + } + summary.TopPois = poiGroups + .Select(kv => new PoiStatDTO { GeoPointId = kv.Key, Title = kv.Value.title, Taps = kv.Value.taps, SectionId = kv.Value.sectionId }) + .OrderByDescending(p => p.Taps) + .Take(10) + .ToList(); + + // Top agenda events + var agendaEvents = events.Where(e => e.EventType == VisitEventType.AgendaEventTap && e.Metadata != null).ToList(); + var agendaGroups = new Dictionary(); + foreach (var ev in agendaEvents) + { + try + { + var meta = JsonSerializer.Deserialize(ev.Metadata!); + if (meta.TryGetProperty("eventId", out var idEl) && meta.TryGetProperty("eventTitle", out var titleEl)) + { + string id = idEl.GetString() ?? ""; + string title = titleEl.GetString() ?? ""; + if (agendaGroups.TryGetValue(id, out var existing)) + agendaGroups[id] = (existing.title, existing.taps + 1); + else + agendaGroups[id] = (title, 1); + } + } + catch { /* skip */ } + } + summary.TopAgendaEvents = agendaGroups + .Select(kv => new AgendaEventStatDTO { EventId = kv.Key, EventTitle = kv.Value.title, Taps = kv.Value.taps }) + .OrderByDescending(a => a.Taps) + .Take(10) + .ToList(); + + // Quiz stats + var quizEvents = events.Where(e => e.EventType == VisitEventType.QuizComplete && e.SectionId != null && e.Metadata != null).ToList(); + summary.QuizStats = quizEvents + .GroupBy(e => e.SectionId!) + .Select(g => + { + var scores = g.Select(e => + { + try + { + var meta = JsonSerializer.Deserialize(e.Metadata!); + double score = meta.TryGetProperty("score", out var s) ? s.GetDouble() : 0; + int total = meta.TryGetProperty("totalQuestions", out var t) ? t.GetInt32() : 1; + return (score, total); + } + catch { return (score: 0.0, total: 1); } + }).ToList(); + return new QuizStatDTO + { + SectionId = g.Key, + SectionTitle = g.Key, + Completions = g.Count(), + AvgScore = scores.Any() ? scores.Average(s => s.score) : 0, + TotalQuestions = scores.Any() ? (int)scores.Average(s => s.total) : 0 + }; + }) + .ToList(); + + // Game stats + var gameEvents = events.Where(e => e.EventType == VisitEventType.GameComplete && e.Metadata != null).ToList(); + var gameGroups = new Dictionary durations)>(); + foreach (var ev in gameEvents) + { + try + { + var meta = JsonSerializer.Deserialize(ev.Metadata!); + if (meta.TryGetProperty("gameType", out var typeEl)) + { + string gameType = typeEl.GetString() ?? "Unknown"; + int duration = meta.TryGetProperty("durationSeconds", out var dEl) ? dEl.GetInt32() : 0; + if (!gameGroups.ContainsKey(gameType)) + gameGroups[gameType] = (0, new List()); + gameGroups[gameType] = (gameGroups[gameType].completions + 1, gameGroups[gameType].durations.Append(duration).ToList()); + } + } + catch { /* skip */ } + } + summary.GameStats = gameGroups + .Select(kv => new GameStatDTO + { + GameType = kv.Key, + Completions = kv.Value.completions, + AvgDurationSeconds = kv.Value.durations.Any() ? (int)kv.Value.durations.Average() : 0 + }) + .ToList(); + + return Ok(summary); + } + catch (Exception ex) + { + return StatusCode(500, ex.Message); + } + } + } +} diff --git a/ManagerService/Controllers/UserController.cs b/ManagerService/Controllers/UserController.cs index 9bbb032..488ad3f 100644 --- a/ManagerService/Controllers/UserController.cs +++ b/ManagerService/Controllers/UserController.cs @@ -1,11 +1,12 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; +using System.Security.Claims; using Manager.Services; using ManagerService.Data; using ManagerService.DTOs; using ManagerService.Helpers; -using ManagerService.Service.Services; +using ManagerService.Service; using ManagerService.Services; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; @@ -14,42 +15,55 @@ using NSwag.Annotations; namespace ManagerService.Controllers { - [Authorize] // TODO Add ROLES (Roles = "Admin") + [Authorize(Policy = ManagerService.Service.Security.Policies.InstanceAdmin)] [ApiController, Route("api/[controller]")] [OpenApiTag("User", Description = "User management")] public class UserController : ControllerBase { private UserDatabaseService _userService; - private TokensService _tokenService; private readonly ILogger _logger; private readonly ProfileLogic _profileLogic; - private readonly MyInfoMateDbContext _myInfoMateDbContext; IHexIdGeneratorService idService = new HexIdGeneratorService(); - public UserController(ILogger logger, UserDatabaseService userService, TokensService tokenService, ProfileLogic profileLogic, MyInfoMateDbContext myInfoMateDbContext) + public UserController(ILogger logger, UserDatabaseService userService, ProfileLogic profileLogic, MyInfoMateDbContext myInfoMateDbContext) { _logger = logger; _userService = userService; - _tokenService = tokenService; _profileLogic = profileLogic; _myInfoMateDbContext = myInfoMateDbContext; } + private string? GetCallerInstanceId() => + User.FindFirst(ManagerService.Service.Security.ClaimTypes.InstanceId)?.Value; + + private bool IsSuperAdmin() => + User.HasClaim(ManagerService.Service.Security.ClaimTypes.Permission, ManagerService.Service.Security.Permissions.SuperAdmin); + + private UserRole GetCallerRole() + { + if (User.HasClaim(ManagerService.Service.Security.ClaimTypes.Permission, ManagerService.Service.Security.Permissions.SuperAdmin)) return UserRole.SuperAdmin; + if (User.HasClaim(ManagerService.Service.Security.ClaimTypes.Permission, ManagerService.Service.Security.Permissions.InstanceAdmin)) return UserRole.InstanceAdmin; + if (User.HasClaim(ManagerService.Service.Security.ClaimTypes.Permission, ManagerService.Service.Security.Permissions.ContentEditor)) return UserRole.ContentEditor; + return UserRole.Viewer; + } + /// - /// Get a list of user + /// Get a list of user /// - [ProducesResponseType(typeof(List), 200)] + [ProducesResponseType(typeof(List), 200)] [ProducesResponseType(typeof(string), 500)] [HttpGet] public ObjectResult Get() { try { - //List users = _userService.GetAll(); - List users= _myInfoMateDbContext.Users.ToList(); + var query = _myInfoMateDbContext.Users.AsQueryable(); - return new OkObjectResult(users); + if (!IsSuperAdmin()) + query = query.Where(u => u.InstanceId == GetCallerInstanceId()); + + return new OkObjectResult(query.ToList().Select(u => u.ToDTO())); } catch (Exception ex) { @@ -57,11 +71,10 @@ namespace ManagerService.Controllers } } - /// - /// Get a specific user + /// Get a specific user /// - /// id user + /// id user [ProducesResponseType(typeof(UserDetailDTO), 200)] [ProducesResponseType(typeof(string), 404)] [ProducesResponseType(typeof(string), 500)] @@ -71,7 +84,6 @@ namespace ManagerService.Controllers try { User user = _myInfoMateDbContext.Users.FirstOrDefault(i => i.Id == id); - //OldUser user = _userService.GetById(id); if (user == null) throw new KeyNotFoundException("This user was not found"); @@ -91,8 +103,7 @@ namespace ManagerService.Controllers /// /// Create an user /// - /// New user info - //[AllowAnonymous] + /// New user info [ProducesResponseType(typeof(UserDetailDTO), 200)] [ProducesResponseType(typeof(string), 400)] [ProducesResponseType(typeof(string), 409)] @@ -108,24 +119,27 @@ namespace ManagerService.Controllers if (newUserDTO.instanceId == null) throw new ArgumentNullException("InstanceId is null"); + if (newUserDTO.password == null) + throw new ArgumentNullException("Password is null"); + + var requestedRole = newUserDTO.role ?? UserRole.ContentEditor; + if (requestedRole < GetCallerRole()) + throw new UnauthorizedAccessException("Cannot assign a role higher than your own"); + User newUser = new User(); newUser.InstanceId = newUserDTO.instanceId; newUser.Email = newUserDTO.email; newUser.FirstName = newUserDTO.firstName; newUser.LastName = newUserDTO.lastName; - newUser.Token = _tokenService.GenerateToken(newUser.Email).ToString(); + newUser.Role = requestedRole; + newUser.Token = Guid.NewGuid().ToString(); newUser.DateCreation = DateTime.Now.ToUniversalTime(); newUser.Id = idService.GenerateHexId(); - List users= _myInfoMateDbContext.Users.ToList(); - //List users = _userService.GetAll(); - - if (users.Select(u => u.Email).Contains(newUser.Email)) + if (_myInfoMateDbContext.Users.Any(u => u.Email == newUser.Email)) throw new InvalidOperationException("This Email is already used"); - newUser.Password = _profileLogic.HashPassword(newUser.Password); - - //OldUser userCreated = _userService.Create(newUser); + newUser.Password = _profileLogic.HashPassword(newUserDTO.password); _myInfoMateDbContext.Add(newUser); _myInfoMateDbContext.SaveChanges(); @@ -136,6 +150,10 @@ namespace ManagerService.Controllers { return new BadRequestObjectResult(ex.Message) {}; } + catch (UnauthorizedAccessException ex) + { + return new ObjectResult(ex.Message) { StatusCode = 403 }; + } catch (InvalidOperationException ex) { return new ConflictObjectResult(ex.Message) {}; @@ -146,11 +164,10 @@ namespace ManagerService.Controllers } } - /// /// Update an user /// - /// User to update + /// User to update [ProducesResponseType(typeof(UserDetailDTO), 200)] [ProducesResponseType(typeof(string), 400)] [ProducesResponseType(typeof(string), 404)] @@ -163,16 +180,20 @@ namespace ManagerService.Controllers if (updatedUser == null) throw new ArgumentNullException("User param is null"); - User user = _myInfoMateDbContext.Users.FirstOrDefault(u => u.Id == updatedUser.id); - //OldUser user = _userService.GetById(updatedUser.Id); + User user = _myInfoMateDbContext.Users.FirstOrDefault(u => u.Id == updatedUser.id); if (user == null) throw new KeyNotFoundException("User does not exist"); - //OldUser userModified = _userService.Update(updatedUser.Id, updatedUser); user.FirstName = updatedUser.firstName; user.LastName = updatedUser.lastName; - // TODO other field ? + + if (updatedUser.role.HasValue) + { + if (updatedUser.role.Value < GetCallerRole()) + throw new UnauthorizedAccessException("Cannot assign a role higher than your own"); + user.Role = updatedUser.role.Value; + } _myInfoMateDbContext.SaveChanges(); @@ -182,6 +203,10 @@ namespace ManagerService.Controllers { return new BadRequestObjectResult(ex.Message) {}; } + catch (UnauthorizedAccessException ex) + { + return new ObjectResult(ex.Message) { StatusCode = 403 }; + } catch (KeyNotFoundException ex) { return new NotFoundObjectResult(ex.Message) {}; @@ -192,11 +217,10 @@ namespace ManagerService.Controllers } } - /// /// Delete an user /// - /// Id of user to delete + /// Id of user to delete [ProducesResponseType(typeof(string), 202)] [ProducesResponseType(typeof(string), 400)] [ProducesResponseType(typeof(string), 404)] @@ -209,18 +233,15 @@ namespace ManagerService.Controllers if (id == null) throw new ArgumentNullException("User param is null"); - User user = _myInfoMateDbContext.Users.FirstOrDefault(u => u.Id == id); - //OldUser user = _userService.GetById(id); + User user = _myInfoMateDbContext.Users.FirstOrDefault(u => u.Id == id); if (user == null) throw new KeyNotFoundException("User does not exist"); - //_userService.Remove(id); _myInfoMateDbContext.Remove(user); _myInfoMateDbContext.SaveChanges(); return new ObjectResult("The user has been deleted") { StatusCode = 202 }; - } catch (ArgumentNullException ex) { diff --git a/ManagerService/DTOs/AiChatDTO.cs b/ManagerService/DTOs/AiChatDTO.cs new file mode 100644 index 0000000..140f15d --- /dev/null +++ b/ManagerService/DTOs/AiChatDTO.cs @@ -0,0 +1,42 @@ +using ManagerService.Data; +using System.Collections.Generic; + +namespace ManagerService.DTOs +{ + public class AiChatRequest + { + public string Message { get; set; } + public string InstanceId { get; set; } + public AppType AppType { get; set; } + public string ConfigurationId { get; set; } // null = scope instance, fourni = scope configuration + public string Language { get; set; } + public List History { get; set; } = new(); + } + + public class AiChatMessage + { + public string Role { get; set; } // "user" | "assistant" + public string Content { get; set; } + } + + public class AiCardDTO + { + public string Title { get; set; } + public string Subtitle { get; set; } + public string? Icon { get; set; } + } + + public class NavigationActionDTO + { + public string SectionId { get; set; } + public string SectionTitle { get; set; } + public string SectionType { get; set; } + } + + public class AiChatResponse + { + public string Reply { get; set; } + public List? Cards { get; set; } + public NavigationActionDTO? Navigation { get; set; } + } +} diff --git a/ManagerService/DTOs/ApplicationInstanceDTO.cs b/ManagerService/DTOs/ApplicationInstanceDTO.cs index 99b022c..8ce4737 100644 --- a/ManagerService/DTOs/ApplicationInstanceDTO.cs +++ b/ManagerService/DTOs/ApplicationInstanceDTO.cs @@ -33,5 +33,9 @@ namespace ManagerService.DTOs public string sectionEventId { get; set; } public SectionEventDTO? sectionEventDTO { get; set; } + + public bool isAssistant { get; set; } + + public bool isStatistic { get; set; } } } diff --git a/ManagerService/DTOs/InstanceDTO.cs b/ManagerService/DTOs/InstanceDTO.cs index 7b4f885..f5d949c 100644 --- a/ManagerService/DTOs/InstanceDTO.cs +++ b/ManagerService/DTOs/InstanceDTO.cs @@ -16,6 +16,8 @@ namespace ManagerService.DTOs public bool isWeb { get; set; } public bool isVR { get; set; } + public bool isAssistant { get; set; } + public List applicationInstanceDTOs { get; set; } } } diff --git a/ManagerService/DTOs/StatsSummaryDTO.cs b/ManagerService/DTOs/StatsSummaryDTO.cs new file mode 100644 index 0000000..18a2fff --- /dev/null +++ b/ManagerService/DTOs/StatsSummaryDTO.cs @@ -0,0 +1,66 @@ +using System; +using System.Collections.Generic; + +namespace ManagerService.DTOs +{ + public class StatsSummaryDTO + { + public int TotalSessions { get; set; } + public int AvgVisitDurationSeconds { get; set; } + public List TopSections { get; set; } = new(); + public List VisitsByDay { get; set; } = new(); + public Dictionary LanguageDistribution { get; set; } = new(); + public Dictionary AppTypeDistribution { get; set; } = new(); + public List TopPois { get; set; } = new(); + public List TopAgendaEvents { get; set; } = new(); + public List QuizStats { get; set; } = new(); + public List GameStats { get; set; } = new(); + } + + public class SectionStatDTO + { + public string SectionId { get; set; } + public string SectionTitle { get; set; } + public int Views { get; set; } + public int AvgDurationSeconds { get; set; } + } + + public class DayStatDTO + { + public string Date { get; set; } // "2026-03-12" + public int Total { get; set; } + public int Mobile { get; set; } + public int Tablet { get; set; } + } + + public class PoiStatDTO + { + public int GeoPointId { get; set; } + public string Title { get; set; } + public int Taps { get; set; } + public string SectionId { get; set; } + } + + public class AgendaEventStatDTO + { + public string EventId { get; set; } + public string EventTitle { get; set; } + public int Taps { get; set; } + } + + public class QuizStatDTO + { + public string SectionId { get; set; } + public string SectionTitle { get; set; } + public double AvgScore { get; set; } + public int TotalQuestions { get; set; } + public int Completions { get; set; } + } + + public class GameStatDTO + { + public string GameType { get; set; } + public int Completions { get; set; } + public int AvgDurationSeconds { get; set; } + } +} diff --git a/ManagerService/DTOs/TokenDTO.cs b/ManagerService/DTOs/TokenDTO.cs index 7b0cd0b..0060f20 100644 --- a/ManagerService/DTOs/TokenDTO.cs +++ b/ManagerService/DTOs/TokenDTO.cs @@ -1,4 +1,5 @@ using System; +using ManagerService.Data; namespace ManagerService.DTOs { @@ -12,5 +13,6 @@ namespace ManagerService.DTOs public DateTimeOffset expiration { get; set; } public string instanceId { get; set; } public string pinCode { get; set; } + public UserRole role { get; set; } } } diff --git a/ManagerService/DTOs/UserDetailDTO.cs b/ManagerService/DTOs/UserDetailDTO.cs index aab7a8d..6820b84 100644 --- a/ManagerService/DTOs/UserDetailDTO.cs +++ b/ManagerService/DTOs/UserDetailDTO.cs @@ -1,4 +1,6 @@ -namespace ManagerService.DTOs +using ManagerService.Data; + +namespace ManagerService.DTOs { public class UserDetailDTO { @@ -7,5 +9,7 @@ public string firstName { get; set; } public string lastName { get; set; } public string instanceId { get; set; } + public UserRole? role { get; set; } + public string? password { get; set; } } } diff --git a/ManagerService/DTOs/VisitEventDTO.cs b/ManagerService/DTOs/VisitEventDTO.cs new file mode 100644 index 0000000..6df7ae0 --- /dev/null +++ b/ManagerService/DTOs/VisitEventDTO.cs @@ -0,0 +1,18 @@ +using System; + +namespace ManagerService.DTOs +{ + public class VisitEventDTO + { + public string instanceId { get; set; } + public string? configurationId { get; set; } + public string? sectionId { get; set; } + public string sessionId { get; set; } + public string eventType { get; set; } // VisitEventType as string + public string appType { get; set; } // "Mobile" / "Tablet" + public string? language { get; set; } + public int? durationSeconds { get; set; } + public string? metadata { get; set; } // JSON string + public DateTime? timestamp { get; set; } + } +} diff --git a/ManagerService/Data/ApiKey.cs b/ManagerService/Data/ApiKey.cs new file mode 100644 index 0000000..20951b2 --- /dev/null +++ b/ManagerService/Data/ApiKey.cs @@ -0,0 +1,33 @@ +using System; +using System.ComponentModel.DataAnnotations; + +namespace ManagerService.Data +{ + public class ApiKey + { + [Key] + public string Id { get; set; } + + /// Plain text key — only for PIN-bootstrapped keys (re-retrievable) + public string? Key { get; set; } + + /// SHA-256 hash — for manually created keys (returned plain text only once) + public string? KeyHash { get; set; } + + [Required] + public string Name { get; set; } + + [Required] + public string InstanceId { get; set; } + + public Instance Instance { get; set; } + + public ApiKeyAppType AppType { get; set; } + + public bool IsActive { get; set; } = true; + + public DateTime DateCreation { get; set; } + + public DateTime? DateExpiration { get; set; } + } +} diff --git a/ManagerService/Data/ApiKeyAppType.cs b/ManagerService/Data/ApiKeyAppType.cs new file mode 100644 index 0000000..460badb --- /dev/null +++ b/ManagerService/Data/ApiKeyAppType.cs @@ -0,0 +1,4 @@ +namespace ManagerService.Data +{ + public enum ApiKeyAppType { VisitApp, TabletApp, Other } +} diff --git a/ManagerService/Data/ApplicationInstance.cs b/ManagerService/Data/ApplicationInstance.cs index 8f1052c..a16a507 100644 --- a/ManagerService/Data/ApplicationInstance.cs +++ b/ManagerService/Data/ApplicationInstance.cs @@ -44,7 +44,9 @@ namespace ManagerService.Data public string? SectionEventId { get; set; } // Specific Mobile et web(?) [ForeignKey("SectionEventId")] - public SectionEvent? SectionEvent { get; set; } // => To Display in large a event with countdown (in mobile app). + public SectionEvent? SectionEvent { get; set; } // => To Display in large a event with countdown (in mobile app). + + public bool IsAssistant { get; set; } = false; public ApplicationInstanceDTO ToDTO(MyInfoMateDbContext myInfoMateDbContext) @@ -72,7 +74,8 @@ namespace ManagerService.Data layoutMainPage = LayoutMainPage, languages = Languages, sectionEventId = SectionEventId, - sectionEventDTO = sectionEventDTO + sectionEventDTO = sectionEventDTO, + isAssistant = IsAssistant }; } @@ -90,6 +93,7 @@ namespace ManagerService.Data Languages = dto.languages; Configurations = dto.configurations; SectionEventId = dto.sectionEventId; + IsAssistant = dto.isAssistant; return this; } diff --git a/ManagerService/Data/Instance.cs b/ManagerService/Data/Instance.cs index ffd8a5f..cdb36e3 100644 --- a/ManagerService/Data/Instance.cs +++ b/ManagerService/Data/Instance.cs @@ -33,6 +33,8 @@ namespace ManagerService.Data public bool IsVR { get; set; } + public bool IsAssistant { get; set; } + public InstanceDTO ToDTO(List applicationInstanceDTOs) { @@ -48,6 +50,7 @@ namespace ManagerService.Data isTablet = IsTablet, isWeb = IsWeb, isVR = IsVR, + isAssistant = IsAssistant, applicationInstanceDTOs = applicationInstanceDTOs }; } @@ -63,6 +66,7 @@ namespace ManagerService.Data IsTablet = instanceDTO.isTablet; IsWeb = instanceDTO.isWeb; IsVR = instanceDTO.isVR; + IsAssistant = instanceDTO.isAssistant; return this; } diff --git a/ManagerService/Data/MyInfoMateDbContext.cs b/ManagerService/Data/MyInfoMateDbContext.cs index ba05c1f..7633f52 100644 --- a/ManagerService/Data/MyInfoMateDbContext.cs +++ b/ManagerService/Data/MyInfoMateDbContext.cs @@ -39,6 +39,12 @@ namespace ManagerService.Data // Agenda public DbSet EventAgendas { get; set; } + // Statistics + public DbSet VisitEvents { get; set; } + + // API Keys + public DbSet ApiKeys { get; set; } + protected override void OnModelCreating(ModelBuilder modelBuilder) { diff --git a/ManagerService/Data/User.cs b/ManagerService/Data/User.cs index 0c90d47..60e3c5f 100644 --- a/ManagerService/Data/User.cs +++ b/ManagerService/Data/User.cs @@ -47,6 +47,8 @@ namespace ManagerService.Data [Required] public string InstanceId { get; set; } + public UserRole Role { get; set; } = UserRole.ContentEditor; + public UserDetailDTO ToDTO() { return new UserDetailDTO() @@ -56,6 +58,7 @@ namespace ManagerService.Data firstName = FirstName, lastName = LastName, instanceId = InstanceId, + role = Role, }; } diff --git a/ManagerService/Data/UserRole.cs b/ManagerService/Data/UserRole.cs new file mode 100644 index 0000000..432a2aa --- /dev/null +++ b/ManagerService/Data/UserRole.cs @@ -0,0 +1,10 @@ +namespace ManagerService.Data +{ + public enum UserRole + { + SuperAdmin = 0, + InstanceAdmin = 1, + ContentEditor = 2, + Viewer = 3 + } +} diff --git a/ManagerService/Data/VisitEvent.cs b/ManagerService/Data/VisitEvent.cs new file mode 100644 index 0000000..ed3bd14 --- /dev/null +++ b/ManagerService/Data/VisitEvent.cs @@ -0,0 +1,60 @@ +using System; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using Microsoft.EntityFrameworkCore; + +namespace ManagerService.Data +{ + [Index(nameof(InstanceId))] + [Index(nameof(Timestamp))] + public class VisitEvent + { + [Key] + public string Id { get; set; } + + [Required] + public string InstanceId { get; set; } + + public string? ConfigurationId { get; set; } + + public string? SectionId { get; set; } + + [Required] + public string SessionId { get; set; } + + public VisitEventType EventType { get; set; } + + public AppType AppType { get; set; } + + public string? Language { get; set; } + + public int? DurationSeconds { get; set; } + + /// + /// JSON string for sub-event data. + /// MapPoiTap: {"geoPointId":42,"geoPointTitle":"Fontaine","categoryId":3} + /// QrScan: {"valid":true,"sectionId":"abc"} + /// QuizComplete: {"score":7,"totalQuestions":10} + /// GameComplete: {"gameType":"Puzzle","durationSeconds":120} + /// AgendaEventTap: {"eventId":"xyz","eventTitle":"Atelier","eventDate":"2026-04-01"} + /// MenuItemTap: {"targetSectionId":"abc","menuItemTitle":"Carte"} + /// AssistantMessage: {"configurationId":"abc"} + /// + public string? Metadata { get; set; } + + public DateTime Timestamp { get; set; } = DateTime.UtcNow; + } + + public enum VisitEventType + { + SectionView, + SectionLeave, + MapPoiTap, + QrScan, + QuizComplete, + GameComplete, + AgendaEventTap, + MenuItemTap, + AssistantMessage + } +} diff --git a/ManagerService/ManagerService.csproj b/ManagerService/ManagerService.csproj index f97838b..2a39eca 100644 --- a/ManagerService/ManagerService.csproj +++ b/ManagerService/ManagerService.csproj @@ -12,6 +12,8 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive + + diff --git a/ManagerService/Migrations/20260312130921_AddAIAssistant.Designer.cs b/ManagerService/Migrations/20260312130921_AddAIAssistant.Designer.cs new file mode 100644 index 0000000..3d79c6e --- /dev/null +++ b/ManagerService/Migrations/20260312130921_AddAIAssistant.Designer.cs @@ -0,0 +1,1248 @@ +// +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("20260312130921_AddAIAssistant")] + partial class AddAIAssistant + { + /// + 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.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.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("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("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("DateCreation") + .HasColumnType("timestamp with time zone"); + + 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.HasKey("Id"); + + b.ToTable("Instances"); + }); + + 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("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("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("Website") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ResourceId"); + + b.HasIndex("SectionAgendaId"); + + b.HasIndex("SectionEventId"); + + 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>("Type") + .HasColumnType("jsonb"); + + b.HasKey("Id"); + + b.HasIndex("IconResourceId"); + + b.HasIndex("ProgrammeBlockId"); + + 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.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("Token") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Users"); + }); + + 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("EndDate") + .HasColumnType("timestamp with time zone"); + + b.Property>("ParcoursIds") + .HasColumnType("jsonb"); + + b.Property("StartDate") + .HasColumnType("timestamp with time zone"); + + 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>("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.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.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.Navigation("Resource"); + + b.Navigation("SectionAgenda"); + + b.Navigation("SectionEvent"); + }); + + 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.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.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("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/20260312130921_AddAIAssistant.cs b/ManagerService/Migrations/20260312130921_AddAIAssistant.cs new file mode 100644 index 0000000..9d9a966 --- /dev/null +++ b/ManagerService/Migrations/20260312130921_AddAIAssistant.cs @@ -0,0 +1,40 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace ManagerService.Migrations +{ + /// + public partial class AddAIAssistant : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "IsAssistant", + table: "Instances", + type: "boolean", + nullable: false, + defaultValue: false); + + migrationBuilder.AddColumn( + name: "IsAssistant", + table: "ApplicationInstances", + type: "boolean", + nullable: false, + defaultValue: false); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "IsAssistant", + table: "Instances"); + + migrationBuilder.DropColumn( + name: "IsAssistant", + table: "ApplicationInstances"); + } + } +} diff --git a/ManagerService/Migrations/20260312154609_AddVisitEvent.Designer.cs b/ManagerService/Migrations/20260312154609_AddVisitEvent.Designer.cs new file mode 100644 index 0000000..af75353 --- /dev/null +++ b/ManagerService/Migrations/20260312154609_AddVisitEvent.Designer.cs @@ -0,0 +1,1294 @@ +// +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("20260312154609_AddVisitEvent")] + partial class AddVisitEvent + { + /// + 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.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.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("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("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("DateCreation") + .HasColumnType("timestamp with time zone"); + + 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.HasKey("Id"); + + b.ToTable("Instances"); + }); + + 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("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("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("Website") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ResourceId"); + + b.HasIndex("SectionAgendaId"); + + b.HasIndex("SectionEventId"); + + 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>("Type") + .HasColumnType("jsonb"); + + b.HasKey("Id"); + + b.HasIndex("IconResourceId"); + + b.HasIndex("ProgrammeBlockId"); + + 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.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("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("EndDate") + .HasColumnType("timestamp with time zone"); + + b.Property>("ParcoursIds") + .HasColumnType("jsonb"); + + b.Property("StartDate") + .HasColumnType("timestamp with time zone"); + + 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>("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.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.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.Navigation("Resource"); + + b.Navigation("SectionAgenda"); + + b.Navigation("SectionEvent"); + }); + + 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.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.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("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/20260312154609_AddVisitEvent.cs b/ManagerService/Migrations/20260312154609_AddVisitEvent.cs new file mode 100644 index 0000000..e8050df --- /dev/null +++ b/ManagerService/Migrations/20260312154609_AddVisitEvent.cs @@ -0,0 +1,53 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace ManagerService.Migrations +{ + /// + public partial class AddVisitEvent : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "VisitEvents", + columns: table => new + { + Id = table.Column(type: "text", nullable: false), + InstanceId = table.Column(type: "text", nullable: false), + ConfigurationId = table.Column(type: "text", nullable: true), + SectionId = table.Column(type: "text", nullable: true), + SessionId = table.Column(type: "text", nullable: false), + EventType = table.Column(type: "integer", nullable: false), + AppType = table.Column(type: "integer", nullable: false), + Language = table.Column(type: "text", nullable: true), + DurationSeconds = table.Column(type: "integer", nullable: true), + Metadata = table.Column(type: "text", nullable: true), + Timestamp = table.Column(type: "timestamp with time zone", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_VisitEvents", x => x.Id); + }); + + migrationBuilder.CreateIndex( + name: "IX_VisitEvents_InstanceId", + table: "VisitEvents", + column: "InstanceId"); + + migrationBuilder.CreateIndex( + name: "IX_VisitEvents_Timestamp", + table: "VisitEvents", + column: "Timestamp"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "VisitEvents"); + } + } +} diff --git a/ManagerService/Migrations/20260313144641_AddUserRole.Designer.cs b/ManagerService/Migrations/20260313144641_AddUserRole.Designer.cs new file mode 100644 index 0000000..2221ec0 --- /dev/null +++ b/ManagerService/Migrations/20260313144641_AddUserRole.Designer.cs @@ -0,0 +1,1346 @@ +// +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("20260313144641_AddUserRole")] + partial class AddUserRole + { + /// + 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.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("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("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("DateCreation") + .HasColumnType("timestamp with time zone"); + + 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.HasKey("Id"); + + b.ToTable("Instances"); + }); + + 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("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("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("Website") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ResourceId"); + + b.HasIndex("SectionAgendaId"); + + b.HasIndex("SectionEventId"); + + 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>("Type") + .HasColumnType("jsonb"); + + b.HasKey("Id"); + + b.HasIndex("IconResourceId"); + + b.HasIndex("ProgrammeBlockId"); + + 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.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("EndDate") + .HasColumnType("timestamp with time zone"); + + b.Property>("ParcoursIds") + .HasColumnType("jsonb"); + + b.Property("StartDate") + .HasColumnType("timestamp with time zone"); + + 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>("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.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.Navigation("Resource"); + + b.Navigation("SectionAgenda"); + + b.Navigation("SectionEvent"); + }); + + 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.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.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("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/20260313144641_AddUserRole.cs b/ManagerService/Migrations/20260313144641_AddUserRole.cs new file mode 100644 index 0000000..75221e4 --- /dev/null +++ b/ManagerService/Migrations/20260313144641_AddUserRole.cs @@ -0,0 +1,63 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace ManagerService.Migrations +{ + /// + public partial class AddUserRole : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "Role", + table: "Users", + type: "integer", + nullable: false, + defaultValue: 0); + + migrationBuilder.CreateTable( + name: "ApiKeys", + columns: table => new + { + Id = table.Column(type: "text", nullable: false), + Key = table.Column(type: "text", nullable: true), + KeyHash = table.Column(type: "text", nullable: true), + Name = table.Column(type: "text", nullable: false), + InstanceId = table.Column(type: "text", nullable: false), + AppType = table.Column(type: "integer", nullable: false), + IsActive = table.Column(type: "boolean", nullable: false), + DateCreation = table.Column(type: "timestamp with time zone", nullable: false), + DateExpiration = table.Column(type: "timestamp with time zone", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_ApiKeys", x => x.Id); + table.ForeignKey( + name: "FK_ApiKeys_Instances_InstanceId", + column: x => x.InstanceId, + principalTable: "Instances", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_ApiKeys_InstanceId", + table: "ApiKeys", + column: "InstanceId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "ApiKeys"); + + migrationBuilder.DropColumn( + name: "Role", + table: "Users"); + } + } +} diff --git a/ManagerService/Migrations/20260313144657_AddApiKeys.Designer.cs b/ManagerService/Migrations/20260313144657_AddApiKeys.Designer.cs new file mode 100644 index 0000000..69f369b --- /dev/null +++ b/ManagerService/Migrations/20260313144657_AddApiKeys.Designer.cs @@ -0,0 +1,1346 @@ +// +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("20260313144657_AddApiKeys")] + partial class AddApiKeys + { + /// + 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.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("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("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("DateCreation") + .HasColumnType("timestamp with time zone"); + + 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.HasKey("Id"); + + b.ToTable("Instances"); + }); + + 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("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("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("Website") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ResourceId"); + + b.HasIndex("SectionAgendaId"); + + b.HasIndex("SectionEventId"); + + 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>("Type") + .HasColumnType("jsonb"); + + b.HasKey("Id"); + + b.HasIndex("IconResourceId"); + + b.HasIndex("ProgrammeBlockId"); + + 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.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("EndDate") + .HasColumnType("timestamp with time zone"); + + b.Property>("ParcoursIds") + .HasColumnType("jsonb"); + + b.Property("StartDate") + .HasColumnType("timestamp with time zone"); + + 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>("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.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.Navigation("Resource"); + + b.Navigation("SectionAgenda"); + + b.Navigation("SectionEvent"); + }); + + 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.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.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("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/20260313144657_AddApiKeys.cs b/ManagerService/Migrations/20260313144657_AddApiKeys.cs new file mode 100644 index 0000000..51917bc --- /dev/null +++ b/ManagerService/Migrations/20260313144657_AddApiKeys.cs @@ -0,0 +1,22 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace ManagerService.Migrations +{ + /// + public partial class AddApiKeys : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + + } + } +} diff --git a/ManagerService/Migrations/MyInfoMateDbContextModelSnapshot.cs b/ManagerService/Migrations/MyInfoMateDbContextModelSnapshot.cs index 2d47c0c..2102798 100644 --- a/ManagerService/Migrations/MyInfoMateDbContextModelSnapshot.cs +++ b/ManagerService/Migrations/MyInfoMateDbContextModelSnapshot.cs @@ -27,6 +27,44 @@ namespace ManagerService.Migrations 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") @@ -105,6 +143,9 @@ namespace ManagerService.Migrations .IsRequired() .HasColumnType("text"); + b.Property("IsAssistant") + .HasColumnType("boolean"); + b.PrimitiveCollection>("Languages") .HasColumnType("text[]"); @@ -258,6 +299,9 @@ namespace ManagerService.Migrations b.Property("DateCreation") .HasColumnType("timestamp with time zone"); + b.Property("IsAssistant") + .HasColumnType("boolean"); + b.Property("IsMobile") .HasColumnType("boolean"); @@ -778,6 +822,9 @@ namespace ManagerService.Migrations .IsRequired() .HasColumnType("text"); + b.Property("Role") + .HasColumnType("integer"); + b.Property("Token") .IsRequired() .HasColumnType("text"); @@ -787,6 +834,52 @@ namespace ManagerService.Migrations 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"); @@ -998,6 +1091,17 @@ namespace ManagerService.Migrations 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") diff --git a/ManagerService/Security.cs b/ManagerService/Security.cs index ba798c7..df4439a 100644 --- a/ManagerService/Security.cs +++ b/ManagerService/Security.cs @@ -1,8 +1,6 @@ using Manager.Interfaces.Models; -using System; +using ManagerService.Data; using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; namespace ManagerService.Service { @@ -10,51 +8,45 @@ namespace ManagerService.Service { public const string Scope = "Manager-api"; - /// - /// Permissions - /// - private class Permissions + public static class Permissions { - /// - /// Admin access - /// - public const string Admin = "Manager.admin"; + public const string SuperAdmin = "Manager.superadmin"; + public const string InstanceAdmin = "Manager.instanceadmin"; + public const string ContentEditor = "Manager.contenteditor"; + public const string Viewer = "Manager.viewer"; + public const string AppRead = "Manager.appread"; } - /// - /// Custom claims types - /// - public class ClaimTypes + public static class Policies { - public const string Permission = "Permission"; + public const string SuperAdmin = "Manager.SuperAdministration"; + public const string InstanceAdmin = "Manager.Administration"; + public const string ContentEditor = "Manager.Content"; + public const string Viewer = "Manager.ReadOnly"; + public const string AppReadAccess = "Manager.AppReadAccess"; } - /// - /// Permissions for each type of profile - /// - public static readonly Dictionary ProfilesConfiguration = new Dictionary() + public static readonly Dictionary RolePermissions = new() { - // An admin has access to everything - //{ typeof(AdminProfile), new[] { Permissions.Admin} }, + [UserRole.SuperAdmin] = new[] { Permissions.SuperAdmin, Permissions.InstanceAdmin, Permissions.ContentEditor, Permissions.Viewer }, + [UserRole.InstanceAdmin] = new[] { Permissions.InstanceAdmin, Permissions.ContentEditor, Permissions.Viewer }, + [UserRole.ContentEditor] = new[] { Permissions.ContentEditor, Permissions.Viewer }, + [UserRole.Viewer] = new[] { Permissions.Viewer }, }; - /// - /// Policies names - /// - public class Policies + public static class ClaimTypes { - /// - /// Administration - /// - public const string Admin = "Manager.Administration"; + public const string Permission = "Permission"; + public const string InstanceId = "InstanceId"; + public const string AppType = "AppType"; } - /// - /// Policies - /// public static readonly Policy[] PoliciesConfiguration = new[] { - new Policy() { Name = Policies.Admin, Claims = new[] { Permissions.Admin} } + new Policy { Name = Policies.SuperAdmin, Claims = new[] { Permissions.SuperAdmin } }, + new Policy { Name = Policies.InstanceAdmin, Claims = new[] { Permissions.InstanceAdmin } }, + new Policy { Name = Policies.ContentEditor, Claims = new[] { Permissions.ContentEditor } }, + new Policy { Name = Policies.Viewer, Claims = new[] { Permissions.Viewer } }, }; } } \ No newline at end of file diff --git a/ManagerService/Security/ApiKeyAuthenticationHandler.cs b/ManagerService/Security/ApiKeyAuthenticationHandler.cs new file mode 100644 index 0000000..b1c5c68 --- /dev/null +++ b/ManagerService/Security/ApiKeyAuthenticationHandler.cs @@ -0,0 +1,57 @@ +using ManagerService.Data; +using ManagerService.Service; +using ManagerService.Service.Services; +using Microsoft.AspNetCore.Authentication; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using System.Security.Claims; +using System.Text.Encodings.Web; +using System.Threading.Tasks; + +namespace ManagerService.Security +{ + public class ApiKeyAuthenticationHandler : AuthenticationHandler + { + private const string HeaderName = "X-Api-Key"; + private readonly MyInfoMateDbContext _db; + + public ApiKeyAuthenticationHandler( + IOptionsMonitor options, + ILoggerFactory logger, + UrlEncoder encoder, + MyInfoMateDbContext db) + : base(options, logger, encoder) + { + _db = db; + } + + protected override async Task HandleAuthenticateAsync() + { + if (!Request.Headers.TryGetValue(HeaderName, out var keyValue)) + return AuthenticateResult.NoResult(); + + var value = keyValue.ToString(); + var keyHash = TokensService.GenerateSHA256String(value); + + var apiKey = await _db.ApiKeys.FirstOrDefaultAsync(k => + k.IsActive && + (k.DateExpiration == null || k.DateExpiration > System.DateTime.UtcNow) && + (k.Key == value || k.KeyHash == keyHash)); + + if (apiKey == null) + return AuthenticateResult.Fail("Invalid API Key"); + + 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.Permission, Service.Security.Permissions.AppRead), + new Claim(Service.Security.ClaimTypes.Permission, Service.Security.Permissions.Viewer), + }; + + var principal = new ClaimsPrincipal(new ClaimsIdentity(claims, Scheme.Name)); + return AuthenticateResult.Success(new AuthenticationTicket(principal, Scheme.Name)); + } + } +} diff --git a/ManagerService/Services/ApiKeyDatabaseService.cs b/ManagerService/Services/ApiKeyDatabaseService.cs new file mode 100644 index 0000000..cf14efc --- /dev/null +++ b/ManagerService/Services/ApiKeyDatabaseService.cs @@ -0,0 +1,125 @@ +using ManagerService.Data; +using ManagerService.Helpers; +using ManagerService.Service.Services; +using Microsoft.EntityFrameworkCore; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Security.Cryptography; +using System.Threading.Tasks; + +namespace ManagerService.Services +{ + public class ApiKeyDatabaseService + { + private readonly MyInfoMateDbContext _db; + private readonly IHexIdGeneratorService _idService = new HexIdGeneratorService(); + + public ApiKeyDatabaseService(MyInfoMateDbContext db) + { + _db = db; + } + + /// + /// Creates a new API key with a hashed secret (returned once in plain text). + /// + public async Task CreateAsync(string instanceId, string name, ApiKeyAppType appType) + { + var plainKey = "ak_" + Convert.ToBase64String(RandomNumberGenerator.GetBytes(32)) + .Replace("+", "-").Replace("/", "_").TrimEnd('='); + + var keyHash = TokensService.GenerateSHA256String(plainKey); + + var apiKey = new ApiKey + { + Id = _idService.GenerateHexId(), + Name = name, + InstanceId = instanceId, + AppType = appType, + Key = null, + KeyHash = keyHash, + IsActive = true, + DateCreation = DateTime.UtcNow, + }; + + _db.ApiKeys.Add(apiKey); + await _db.SaveChangesAsync(); + + return plainKey; + } + + /// + /// Returns (or creates) a persistent plain-text key for a given instance + appType (PIN flow). + /// + public async Task GetOrCreateByPinAsync(string instanceId, ApiKeyAppType appType) + { + var existing = await _db.ApiKeys.FirstOrDefaultAsync(k => + k.InstanceId == instanceId && + k.AppType == appType && + k.IsActive && + k.Key != null); + + if (existing != null) + return existing.Key!; + + var plainKey = "ak_" + Convert.ToBase64String(RandomNumberGenerator.GetBytes(32)) + .Replace("+", "-").Replace("/", "_").TrimEnd('='); + + var apiKey = new ApiKey + { + Id = _idService.GenerateHexId(), + Name = $"Auto-{appType}-{instanceId}", + InstanceId = instanceId, + AppType = appType, + Key = plainKey, + KeyHash = null, + IsActive = true, + DateCreation = DateTime.UtcNow, + }; + + _db.ApiKeys.Add(apiKey); + await _db.SaveChangesAsync(); + + return plainKey; + } + + /// Returns all API keys for an instance (without secret values). + public async Task> GetByInstanceAsync(string instanceId) + { + return await _db.ApiKeys + .Where(k => k.InstanceId == instanceId) + .Select(k => new ApiKeyDTO + { + Id = k.Id, + Name = k.Name, + AppType = k.AppType, + IsActive = k.IsActive, + DateCreation = k.DateCreation, + DateExpiration = k.DateExpiration, + }) + .ToListAsync(); + } + + /// Revokes (deactivates) an API key, verifying ownership. + public async Task RevokeAsync(string id, string callerInstanceId) + { + var key = await _db.ApiKeys.FindAsync(id); + if (key == null || key.InstanceId != callerInstanceId) + return false; + + key.IsActive = false; + await _db.SaveChangesAsync(); + return true; + } + } + + public class ApiKeyDTO + { + public string Id { get; set; } + public string Name { get; set; } + public ApiKeyAppType AppType { get; set; } + public bool IsActive { get; set; } + public DateTime DateCreation { get; set; } + public DateTime? DateExpiration { get; set; } + } +} diff --git a/ManagerService/Services/AssistantService.cs b/ManagerService/Services/AssistantService.cs new file mode 100644 index 0000000..9080ae3 --- /dev/null +++ b/ManagerService/Services/AssistantService.cs @@ -0,0 +1,165 @@ +using ManagerService.Data; +using ManagerService.DTOs; +using Microsoft.Extensions.AI; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace ManagerService.Services +{ + public class AssistantService + { + private readonly IChatClient _chatClient; + private readonly MyInfoMateDbContext _context; + + private const int MaxHistoryMessages = 10; + + public AssistantService(IChatClient chatClient, MyInfoMateDbContext context) + { + _chatClient = chatClient; + _context = context; + } + + public async Task ChatAsync(AiChatRequest request) + { + var messages = new List(); + ChatOptions options; + + if (request.ConfigurationId != null) + { + // Scope configuration : l'IA connaît uniquement les sections de cette configuration + var sections = _context.Sections.Where(s => s.ConfigurationId == request.ConfigurationId).ToList(); + var config = _context.Configurations.FirstOrDefault(c => c.Id == request.ConfigurationId); + + var sectionsSummary = string.Join("\n", sections.Select(s => + { + var title = s.Title?.FirstOrDefault(t => t.language == request.Language)?.value + ?? s.Title?.FirstOrDefault()?.value + ?? s.Label; + return $"- id:{s.Id} | type:{s.Type} | titre:\"{title}\""; + })); + + messages.Add(new ChatMessage(ChatRole.System, $""" + Tu es l'assistant de visite de "{config?.Label ?? "cette application"}". + Tu réponds en {request.Language}. Tu es chaleureux, concis et utile. + + Voici les sections disponibles dans cette application : + {sectionsSummary} + + Pour obtenir le détail d'une section spécifique, utilise l'outil GetSectionDetail. + Pour afficher une liste d'éléments (événements, activités...) de façon visuelle, appelle show_cards avec les titres et sous-titres. + Pour proposer à l'utilisateur d'aller directement dans une section, appelle navigate_to_section. + Accompagne toujours tes outils d'un texte explicatif. + Ne parle jamais de sections qui ne sont pas dans cette liste. + """)); + + foreach (var h in request.History.TakeLast(MaxHistoryMessages)) + messages.Add(new ChatMessage(h.Role == "user" ? ChatRole.User : ChatRole.Assistant, h.Content)); + + messages.Add(new ChatMessage(ChatRole.User, request.Message)); + + NavigationActionDTO? navigation = null; + List? cards = null; + + // Outil scopé : vérifie que la section appartient bien à cette configuration + var scopedSections = sections; + var tools = new List + { + AIFunctionFactory.Create( + (string sectionId) => + { + var section = scopedSections.FirstOrDefault(s => s.Id == sectionId); + if (section == null) return "Section non trouvée dans cette application."; + + var title = section.Title?.FirstOrDefault(t => t.language == request.Language)?.value + ?? section.Title?.FirstOrDefault()?.value + ?? section.Label; + var desc = section.Description?.FirstOrDefault(t => t.language == request.Language)?.value + ?? section.Description?.FirstOrDefault()?.value + ?? ""; + return $"Titre: {title}\nDescription: {desc}\nType: {section.Type}"; + }, + "GetSectionDetail", + "Récupère le titre et la description complète d'une section à partir de son id" + ), + AIFunctionFactory.Create( + (string sectionId, string sectionTitle, string sectionType) => + { + navigation = new NavigationActionDTO { SectionId = sectionId, SectionTitle = sectionTitle, SectionType = sectionType }; + return Task.FromResult("Navigation proposée à l'utilisateur."); + }, + "navigate_to_section", + "Propose navigation to a specific section when the user wants to go there or see it." + ), + AIFunctionFactory.Create( + (string[] titles, string[] subtitles, string[]? icons) => + { + cards = titles.Select((t, i) => new AiCardDTO + { + Title = t, + Subtitle = i < subtitles.Length ? subtitles[i] : "", + Icon = icons != null && i < icons.Length ? icons[i] : null + }).ToList(); + return Task.FromResult("Cartes affichées."); + }, + "show_cards", + "Display structured info cards in the chat. Use for lists of items (events, activities...) that benefit from visual presentation." + ) + }; + + options = new ChatOptions { Tools = tools }; + var response = await _chatClient.GetResponseAsync(messages, options); + return new AiChatResponse { Reply = response.Text ?? "", Cards = cards, Navigation = navigation }; + } + else + { + // Scope instance : l'IA connaît les configurations disponibles + var configurations = _context.Configurations.Where(c => c.InstanceId == request.InstanceId).ToList(); + + var configsSummary = string.Join("\n", configurations.Select(c => + { + var title = c.Title?.FirstOrDefault(t => t.language == request.Language)?.value + ?? c.Title?.FirstOrDefault()?.value + ?? c.Label; + return $"- id:{c.Id} | titre:\"{title}\""; + })); + + messages.Add(new ChatMessage(ChatRole.System, $""" + Tu es l'assistant de visite. + Tu réponds en {request.Language}. Tu es chaleureux, concis et utile. + + Voici les expériences de visite disponibles : + {configsSummary} + + Aide le visiteur à trouver ce qui l'intéresse parmi ces expériences. + Pour orienter le visiteur vers une visite spécifique, appelle navigate_to_configuration. + Accompagne toujours tes outils d'un texte explicatif. + """)); + + foreach (var h in request.History.TakeLast(MaxHistoryMessages)) + messages.Add(new ChatMessage(h.Role == "user" ? ChatRole.User : ChatRole.Assistant, h.Content)); + + messages.Add(new ChatMessage(ChatRole.User, request.Message)); + + NavigationActionDTO? navigation = null; + + var tools = new List + { + AIFunctionFactory.Create( + (string configurationId, string configurationTitle) => + { + navigation = new NavigationActionDTO { SectionId = configurationId, SectionTitle = configurationTitle, SectionType = "Configuration" }; + return Task.FromResult("Navigation vers la visite proposée."); + }, + "navigate_to_configuration", + "Propose navigation to a specific visit/configuration when the user wants to start or explore it." + ) + }; + + options = new ChatOptions { Tools = tools }; + var response = await _chatClient.GetResponseAsync(messages, options); + return new AiChatResponse { Reply = response.Text ?? "", Navigation = navigation }; + } + } + } +} diff --git a/ManagerService/Services/TokensService.cs b/ManagerService/Services/TokensService.cs index 85a11a8..bb131f4 100644 --- a/ManagerService/Services/TokensService.cs +++ b/ManagerService/Services/TokensService.cs @@ -57,14 +57,18 @@ namespace ManagerService.Service.Services { try { - var claims = new List(); var expiration = DateTime.UtcNow.AddMinutes(_tokenSettings.AccessTokenExpiration); _profileLogic.TestPassword(user.Email, user.Password, password); - claims.Add(new Claim(ClaimTypes.Email, user.Email)); - - // TODO: add refresh token support + var claims = new List + { + new(ClaimTypes.Email, user.Email), + new(ClaimTypes.Name, $"{user.FirstName} {user.LastName}"), + new(Security.ClaimTypes.InstanceId, user.InstanceId), + }; + foreach (var perm in Security.RolePermissions[user.Role]) + claims.Add(new Claim(Security.ClaimTypes.Permission, perm)); var tokenHandler = new JwtSecurityTokenHandler(); var tokenDescriptor = new SecurityTokenDescriptor() @@ -75,8 +79,6 @@ namespace ManagerService.Service.Services }; var token = tokenHandler.CreateToken(tokenDescriptor); - //var instance = _instanceService.GetById(user.InstanceId); - var instance = _myInfoMateDbContext.Instances.Find(user.InstanceId); return new TokenDTO() @@ -87,7 +89,8 @@ namespace ManagerService.Service.Services token_type = "Bearer", scope = Security.Scope, instanceId = user.InstanceId, - pinCode = instance.PinCode + pinCode = instance.PinCode, + role = user.Role }; } catch (UnauthorizedAccessException ex) @@ -102,30 +105,6 @@ namespace ManagerService.Service.Services } } - public object GenerateToken(string username) - { - var secretKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_tokenSettings.Secret)); // Put the secret in a file or something - - var claims = new Claim[] { - new Claim(ClaimTypes.Name, username), - new Claim(JwtRegisteredClaimNames.Email, "john.doe@blinkingcaret.com"), - new Claim(ClaimTypes.Role, "Admin") - }; - - var token = new JwtSecurityToken( - issuer: "Manager App", - audience: "Manager client", - claims: claims, - notBefore: DateTime.Now, - expires: DateTime.Now.AddDays(28), - signingCredentials: new SigningCredentials(secretKey, SecurityAlgorithms.HmacSha256) - ); - - string jwtToken = new JwtSecurityTokenHandler().WriteToken(token); - - return jwtToken; - } - public static string GenerateSHA256String(string inputString) { SHA256 sha256 = SHA256Managed.Create(); diff --git a/ManagerService/Startup.cs b/ManagerService/Startup.cs index f1b41a2..c2d1420 100644 --- a/ManagerService/Startup.cs +++ b/ManagerService/Startup.cs @@ -1,3 +1,6 @@ +using Microsoft.Extensions.AI; +using OpenAI; +using System.ClientModel; using Manager.Framework.Models; using Manager.Helpers; using Manager.Interfaces; @@ -6,8 +9,10 @@ using Manager.Services; using ManagerService.Data; using ManagerService.Extensions; using ManagerService.Helpers; +using ManagerService.Security; using ManagerService.Service; using ManagerService.Service.Services; +using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Diagnostics; @@ -36,6 +41,7 @@ using System.Linq; using System.Text; using System.Text.Json.Serialization; using System.Threading.Tasks; +using ManagerService.Services; namespace ManagerService { @@ -105,16 +111,21 @@ namespace ManagerService services.Configure(tokensConfiguration); - foreach (var policy in Security.PoliciesConfiguration) + foreach (var policy in ManagerService.Service.Security.PoliciesConfiguration) services.AddAuthorization(options => { options.AddPolicy(policy.Name, policyAdmin => { foreach (var claim in policy.Claims) - policyAdmin.RequireClaim(Security.ClaimTypes.Permission, claim); + policyAdmin.RequireClaim(ManagerService.Service.Security.ClaimTypes.Permission, claim); }); }); + services.AddAuthorization(options => + options.AddPolicy(ManagerService.Service.Security.Policies.AppReadAccess, policy => + policy.AddAuthenticationSchemes(JwtBearerDefaults.AuthenticationScheme, "ApiKey") + .RequireAuthenticatedUser())); + services .AddAuthentication(x => { @@ -135,7 +146,8 @@ namespace ManagerService RequireExpirationTime = false, ValidateLifetime = true }; - }); + }) + .AddScheme("ApiKey", _ => { }); #if RELEASE //services.AddMqttClientHostedService(); @@ -151,6 +163,18 @@ namespace ManagerService services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); + + // Assistant IA — choisir un provider (package NuGet : Microsoft.Extensions.AI.OpenAI) : + // OpenAI : new OpenAIClient(new ApiKeyCredential(apiKey)).AsChatClient("gpt-4o-mini") + // Gemini : endpoint OpenAI-compatible (ci-dessous) + // Anthropic : package Anthropic.SDK → new AnthropicClient(apiKey).Messages.AsChatClient("claude-haiku-4-5-20251001") + services.AddSingleton(_ => + new OpenAIClient( + new ApiKeyCredential(Configuration["AI:ApiKey"]!), + new OpenAIClientOptions { Endpoint = new Uri("https://generativelanguage.googleapis.com/v1beta/openai/") } + ).AsChatClient("gemini-2.5-flash-lite")); + services.AddScoped(); var connectionString = Configuration.GetConnectionString("PostgresConnection"); @@ -162,7 +186,7 @@ namespace ManagerService services.AddDbContext(options => options.UseNpgsql(dataSource, o => o.UseNetTopologySuite()) - .EnableSensitiveDataLogging() // montre les valeurs des paramètres + .EnableSensitiveDataLogging() // montre les valeurs des param�tres .LogTo(Console.WriteLine, LogLevel.Information) ); } @@ -191,7 +215,7 @@ namespace ManagerService app.UseCors( #if DEBUG options => options - .SetIsOriginAllowed(origin => string.IsNullOrEmpty(origin) || origin == "http://localhost:64402") + .SetIsOriginAllowed(origin => string.IsNullOrEmpty(origin) || origin == "http://localhost:50479") .AllowAnyMethod() .AllowAnyHeader() .AllowCredentials() @@ -232,7 +256,7 @@ namespace ManagerService { Scopes = new Dictionary { - { Security.Scope, "Manager WebAPI" } + { ManagerService.Service.Security.Scope, "Manager WebAPI" } }, TokenUrl = "/api/authentication/Token", AuthorizationUrl = "/authentication/Token", @@ -241,6 +265,14 @@ namespace ManagerService }); config.OperationProcessors.Add(new AspNetCoreOperationSecurityScopeProcessor("bearer")); + config.AddSecurity("apikey", Enumerable.Empty(), new OpenApiSecurityScheme + { + Type = OpenApiSecuritySchemeType.ApiKey, + Name = "X-Api-Key", + In = OpenApiSecurityApiKeyLocation.Header, + Description = "API Key for mobile apps" + }); + config.PostProcess = document => { document.Info.Title = "Manager Service"; diff --git a/ManagerService/appsettings.json b/ManagerService/appsettings.json index 9082fa7..720ad8c 100644 --- a/ManagerService/appsettings.json +++ b/ManagerService/appsettings.json @@ -29,6 +29,9 @@ "TokenExpiryInHours": 2 }, "SupportedLanguages": [ "FR", "NL", "EN", "DE", "IT", "ES", "PL", "CN", "AR", "UK" ], - "OpenWeatherApiKey": "d489973b4c09ddc5fb56bd7b9270bbef" + "OpenWeatherApiKey": "d489973b4c09ddc5fb56bd7b9270bbef", + "AI": { + "ApiKey": "AIzaSyC7lJ8w1eQL4aZGNFLMabig5ul6yn66nug" + } //"Urls": "http://[::]:80" }