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 !
This commit is contained in:
parent
bde1666b42
commit
a452f4af04
66
ManagerService/Controllers/AiController.cs
Normal file
66
ManagerService/Controllers/AiController.cs
Normal file
@ -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<AiController> _logger;
|
||||
|
||||
public AiController(
|
||||
AssistantService assistantService,
|
||||
MyInfoMateDbContext context,
|
||||
ILogger<AiController> logger)
|
||||
{
|
||||
_assistantService = assistantService;
|
||||
_context = context;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Envoie un message à l'assistant IA, scopé à l'instance et optionnellement à une configuration
|
||||
/// </summary>
|
||||
[HttpPost("chat")]
|
||||
[ProducesResponseType(typeof(AiChatResponse), 200)]
|
||||
[ProducesResponseType(403)]
|
||||
[ProducesResponseType(typeof(string), 500)]
|
||||
public async Task<IActionResult> 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 };
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
68
ManagerService/Controllers/ApiKeyController.cs
Normal file
68
ManagerService/Controllers/ApiKeyController.cs
Normal file
@ -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;
|
||||
}
|
||||
|
||||
/// <summary>List API keys for the caller's instance</summary>
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> 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);
|
||||
}
|
||||
|
||||
/// <summary>Create a new API key (plain key returned once)</summary>
|
||||
[HttpPost]
|
||||
public async Task<IActionResult> 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 });
|
||||
}
|
||||
|
||||
/// <summary>Revoke an API key</summary>
|
||||
[HttpDelete("{id}")]
|
||||
public async Task<IActionResult> 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; }
|
||||
}
|
||||
}
|
||||
@ -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<ApplicationInstance> 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)
|
||||
{
|
||||
|
||||
@ -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)
|
||||
/// </summary>
|
||||
/// <param name="id">id instance</param>
|
||||
[AllowAnonymous]
|
||||
[Authorize(Policy = ManagerService.Service.Security.Policies.AppReadAccess)]
|
||||
[ProducesResponseType(typeof(List<ConfigurationDTO>), 200)]
|
||||
[ProducesResponseType(typeof(string), 500)]
|
||||
[HttpGet]
|
||||
@ -80,7 +80,7 @@ namespace ManagerService.Controllers
|
||||
/// Get Confuguration list by instanceId' pincode
|
||||
/// </summary>
|
||||
/// <param name="pinCode">Code pin</param>
|
||||
[AllowAnonymous]
|
||||
[Authorize(Policy = ManagerService.Service.Security.Policies.AppReadAccess)]
|
||||
[ProducesResponseType(typeof(List<ConfigurationDTO>), 200)]
|
||||
[ProducesResponseType(typeof(string), 404)]
|
||||
[ProducesResponseType(typeof(string), 500)]
|
||||
@ -114,7 +114,7 @@ namespace ManagerService.Controllers
|
||||
/// Get a specific display configuration
|
||||
/// </summary>
|
||||
/// <param name="id">id configuration</param>
|
||||
[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<EFBFBD>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<EFBFBD>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 <EFBFBD> jour des sections de type m<>t<EFBFBD>o : {e.Message}");
|
||||
}
|
||||
|
||||
return new OkObjectResult(configuration.ToDTO(sectionIds));
|
||||
@ -380,7 +380,7 @@ namespace ManagerService.Controllers
|
||||
/// </summary>
|
||||
/// <param name="id">Id of configuration to export</param>
|
||||
/// <param name="language">Language to export</param>
|
||||
[AllowAnonymous]
|
||||
[Authorize(Policy = ManagerService.Service.Security.Policies.AppReadAccess)]
|
||||
[ProducesResponseType(typeof(FileContentResult), 200)]
|
||||
[ProducesResponseType(typeof(string), 400)]
|
||||
[ProducesResponseType(typeof(string), 404)]
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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<InstanceController> _logger;
|
||||
private readonly ProfileLogic _profileLogic;
|
||||
private readonly ApiKeyDatabaseService _apiKeyService;
|
||||
IHexIdGeneratorService idService = new HexIdGeneratorService();
|
||||
|
||||
public InstanceController(ILogger<InstanceController> logger, InstanceDatabaseService instanceService, UserDatabaseService userService, ProfileLogic profileLogic, MyInfoMateDbContext myInfoMateDbContext)
|
||||
public InstanceController(ILogger<InstanceController> logger, InstanceDatabaseService instanceService, UserDatabaseService userService, ProfileLogic profileLogic, MyInfoMateDbContext myInfoMateDbContext, ApiKeyDatabaseService apiKeyService)
|
||||
{
|
||||
_logger = logger;
|
||||
_instanceService = instanceService;
|
||||
_userService = userService;
|
||||
_profileLogic = profileLogic;
|
||||
_myInfoMateDbContext = myInfoMateDbContext;
|
||||
_apiKeyService = apiKeyService;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -223,6 +226,33 @@ namespace ManagerService.Controllers
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Bootstrap: get (or create) an API key for a Flutter app by PIN code
|
||||
/// </summary>
|
||||
/// <param name="pinCode">Instance PIN code</param>
|
||||
/// <param name="appType">App type (VisitApp, TabletApp, Other)</param>
|
||||
[AllowAnonymous]
|
||||
[ProducesResponseType(typeof(object), 200)]
|
||||
[ProducesResponseType(typeof(string), 404)]
|
||||
[ProducesResponseType(typeof(string), 500)]
|
||||
[HttpGet("app-key")]
|
||||
public async Task<ObjectResult> 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 };
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Delete an instance
|
||||
/// </summary>
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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<SectionDTO>(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;
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
274
ManagerService/Controllers/StatsController.cs
Normal file
274
ManagerService/Controllers/StatsController.cs
Normal file
@ -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;
|
||||
}
|
||||
|
||||
/// <summary>Track a single visit event (anonymous)</summary>
|
||||
[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<VisitEventType>(dto.eventType, ignoreCase: true, out var eventType))
|
||||
return BadRequest($"Unknown eventType: {dto.eventType}");
|
||||
|
||||
if (!Enum.TryParse<AppType>(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);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Get aggregated statistics for an instance</summary>
|
||||
[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>(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<int, (string title, int taps, string sectionId)>();
|
||||
foreach (var ev in poiEvents)
|
||||
{
|
||||
try
|
||||
{
|
||||
var meta = JsonSerializer.Deserialize<JsonElement>(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<string, (string title, int taps)>();
|
||||
foreach (var ev in agendaEvents)
|
||||
{
|
||||
try
|
||||
{
|
||||
var meta = JsonSerializer.Deserialize<JsonElement>(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<JsonElement>(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<string, (int completions, List<int> durations)>();
|
||||
foreach (var ev in gameEvents)
|
||||
{
|
||||
try
|
||||
{
|
||||
var meta = JsonSerializer.Deserialize<JsonElement>(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<int>());
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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<UserController> _logger;
|
||||
private readonly ProfileLogic _profileLogic;
|
||||
|
||||
private readonly MyInfoMateDbContext _myInfoMateDbContext;
|
||||
IHexIdGeneratorService idService = new HexIdGeneratorService();
|
||||
|
||||
public UserController(ILogger<UserController> logger, UserDatabaseService userService, TokensService tokenService, ProfileLogic profileLogic, MyInfoMateDbContext myInfoMateDbContext)
|
||||
public UserController(ILogger<UserController> 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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get a list of user
|
||||
/// </summary>
|
||||
[ProducesResponseType(typeof(List<User>), 200)]
|
||||
[ProducesResponseType(typeof(List<UserDetailDTO>), 200)]
|
||||
[ProducesResponseType(typeof(string), 500)]
|
||||
[HttpGet]
|
||||
public ObjectResult Get()
|
||||
{
|
||||
try
|
||||
{
|
||||
//List<OldUser> users = _userService.GetAll();
|
||||
List<User> 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,7 +71,6 @@ namespace ManagerService.Controllers
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Get a specific user
|
||||
/// </summary>
|
||||
@ -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
|
||||
/// <summary>
|
||||
/// Create an user
|
||||
/// </summary>
|
||||
/// <param name="newUser">New user info</param>
|
||||
//[AllowAnonymous]
|
||||
/// <param name="newUserDTO">New user info</param>
|
||||
[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<User> users= _myInfoMateDbContext.Users.ToList();
|
||||
//List<OldUser> 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,7 +164,6 @@ namespace ManagerService.Controllers
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Update an user
|
||||
/// </summary>
|
||||
@ -164,15 +181,19 @@ namespace ManagerService.Controllers
|
||||
throw new ArgumentNullException("User param is null");
|
||||
|
||||
User user = _myInfoMateDbContext.Users.FirstOrDefault(u => u.Id == updatedUser.id);
|
||||
//OldUser user = _userService.GetById(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,7 +217,6 @@ namespace ManagerService.Controllers
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Delete an user
|
||||
/// </summary>
|
||||
@ -210,17 +234,14 @@ namespace ManagerService.Controllers
|
||||
throw new ArgumentNullException("User param is null");
|
||||
|
||||
User user = _myInfoMateDbContext.Users.FirstOrDefault(u => u.Id == id);
|
||||
//OldUser user = _userService.GetById(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)
|
||||
{
|
||||
|
||||
42
ManagerService/DTOs/AiChatDTO.cs
Normal file
42
ManagerService/DTOs/AiChatDTO.cs
Normal file
@ -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<AiChatMessage> 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<AiCardDTO>? Cards { get; set; }
|
||||
public NavigationActionDTO? Navigation { get; set; }
|
||||
}
|
||||
}
|
||||
@ -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; }
|
||||
}
|
||||
}
|
||||
|
||||
@ -16,6 +16,8 @@ namespace ManagerService.DTOs
|
||||
public bool isWeb { get; set; }
|
||||
public bool isVR { get; set; }
|
||||
|
||||
public bool isAssistant { get; set; }
|
||||
|
||||
public List<ApplicationInstanceDTO> applicationInstanceDTOs { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
66
ManagerService/DTOs/StatsSummaryDTO.cs
Normal file
66
ManagerService/DTOs/StatsSummaryDTO.cs
Normal file
@ -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<SectionStatDTO> TopSections { get; set; } = new();
|
||||
public List<DayStatDTO> VisitsByDay { get; set; } = new();
|
||||
public Dictionary<string, int> LanguageDistribution { get; set; } = new();
|
||||
public Dictionary<string, int> AppTypeDistribution { get; set; } = new();
|
||||
public List<PoiStatDTO> TopPois { get; set; } = new();
|
||||
public List<AgendaEventStatDTO> TopAgendaEvents { get; set; } = new();
|
||||
public List<QuizStatDTO> QuizStats { get; set; } = new();
|
||||
public List<GameStatDTO> 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; }
|
||||
}
|
||||
}
|
||||
@ -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; }
|
||||
}
|
||||
}
|
||||
|
||||
@ -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; }
|
||||
}
|
||||
}
|
||||
|
||||
18
ManagerService/DTOs/VisitEventDTO.cs
Normal file
18
ManagerService/DTOs/VisitEventDTO.cs
Normal file
@ -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; }
|
||||
}
|
||||
}
|
||||
33
ManagerService/Data/ApiKey.cs
Normal file
33
ManagerService/Data/ApiKey.cs
Normal file
@ -0,0 +1,33 @@
|
||||
using System;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace ManagerService.Data
|
||||
{
|
||||
public class ApiKey
|
||||
{
|
||||
[Key]
|
||||
public string Id { get; set; }
|
||||
|
||||
/// <summary>Plain text key — only for PIN-bootstrapped keys (re-retrievable)</summary>
|
||||
public string? Key { get; set; }
|
||||
|
||||
/// <summary>SHA-256 hash — for manually created keys (returned plain text only once)</summary>
|
||||
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; }
|
||||
}
|
||||
}
|
||||
4
ManagerService/Data/ApiKeyAppType.cs
Normal file
4
ManagerService/Data/ApiKeyAppType.cs
Normal file
@ -0,0 +1,4 @@
|
||||
namespace ManagerService.Data
|
||||
{
|
||||
public enum ApiKeyAppType { VisitApp, TabletApp, Other }
|
||||
}
|
||||
@ -46,6 +46,8 @@ namespace ManagerService.Data
|
||||
[ForeignKey("SectionEventId")]
|
||||
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;
|
||||
}
|
||||
|
||||
@ -33,6 +33,8 @@ namespace ManagerService.Data
|
||||
|
||||
public bool IsVR { get; set; }
|
||||
|
||||
public bool IsAssistant { get; set; }
|
||||
|
||||
|
||||
public InstanceDTO ToDTO(List<ApplicationInstanceDTO> 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;
|
||||
}
|
||||
|
||||
|
||||
@ -39,6 +39,12 @@ namespace ManagerService.Data
|
||||
// Agenda
|
||||
public DbSet<EventAgenda> EventAgendas { get; set; }
|
||||
|
||||
// Statistics
|
||||
public DbSet<VisitEvent> VisitEvents { get; set; }
|
||||
|
||||
// API Keys
|
||||
public DbSet<ApiKey> ApiKeys { get; set; }
|
||||
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||
{
|
||||
|
||||
@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
10
ManagerService/Data/UserRole.cs
Normal file
10
ManagerService/Data/UserRole.cs
Normal file
@ -0,0 +1,10 @@
|
||||
namespace ManagerService.Data
|
||||
{
|
||||
public enum UserRole
|
||||
{
|
||||
SuperAdmin = 0,
|
||||
InstanceAdmin = 1,
|
||||
ContentEditor = 2,
|
||||
Viewer = 3
|
||||
}
|
||||
}
|
||||
60
ManagerService/Data/VisitEvent.cs
Normal file
60
ManagerService/Data/VisitEvent.cs
Normal file
@ -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; }
|
||||
|
||||
/// <summary>
|
||||
/// 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"}
|
||||
/// </summary>
|
||||
public string? Metadata { get; set; }
|
||||
|
||||
public DateTime Timestamp { get; set; } = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
public enum VisitEventType
|
||||
{
|
||||
SectionView,
|
||||
SectionLeave,
|
||||
MapPoiTap,
|
||||
QrScan,
|
||||
QuizComplete,
|
||||
GameComplete,
|
||||
AgendaEventTap,
|
||||
MenuItemTap,
|
||||
AssistantMessage
|
||||
}
|
||||
}
|
||||
@ -12,6 +12,8 @@
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.Extensions.AI" Version="9.3.0-preview.1.25161.3" />
|
||||
<PackageReference Include="Microsoft.Extensions.AI.OpenAI" Version="9.3.0-preview.1.25161.3" />
|
||||
<PackageReference Include="Microsoft.IdentityModel.Tokens" Version="7.2.0" />
|
||||
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.14.0" />
|
||||
<PackageReference Include="MongoDB.Driver" Version="2.19.0" />
|
||||
|
||||
1248
ManagerService/Migrations/20260312130921_AddAIAssistant.Designer.cs
generated
Normal file
1248
ManagerService/Migrations/20260312130921_AddAIAssistant.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
40
ManagerService/Migrations/20260312130921_AddAIAssistant.cs
Normal file
40
ManagerService/Migrations/20260312130921_AddAIAssistant.cs
Normal file
@ -0,0 +1,40 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace ManagerService.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddAIAssistant : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<bool>(
|
||||
name: "IsAssistant",
|
||||
table: "Instances",
|
||||
type: "boolean",
|
||||
nullable: false,
|
||||
defaultValue: false);
|
||||
|
||||
migrationBuilder.AddColumn<bool>(
|
||||
name: "IsAssistant",
|
||||
table: "ApplicationInstances",
|
||||
type: "boolean",
|
||||
nullable: false,
|
||||
defaultValue: false);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "IsAssistant",
|
||||
table: "Instances");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "IsAssistant",
|
||||
table: "ApplicationInstances");
|
||||
}
|
||||
}
|
||||
}
|
||||
1294
ManagerService/Migrations/20260312154609_AddVisitEvent.Designer.cs
generated
Normal file
1294
ManagerService/Migrations/20260312154609_AddVisitEvent.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
53
ManagerService/Migrations/20260312154609_AddVisitEvent.cs
Normal file
53
ManagerService/Migrations/20260312154609_AddVisitEvent.cs
Normal file
@ -0,0 +1,53 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace ManagerService.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddVisitEvent : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "VisitEvents",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<string>(type: "text", nullable: false),
|
||||
InstanceId = table.Column<string>(type: "text", nullable: false),
|
||||
ConfigurationId = table.Column<string>(type: "text", nullable: true),
|
||||
SectionId = table.Column<string>(type: "text", nullable: true),
|
||||
SessionId = table.Column<string>(type: "text", nullable: false),
|
||||
EventType = table.Column<int>(type: "integer", nullable: false),
|
||||
AppType = table.Column<int>(type: "integer", nullable: false),
|
||||
Language = table.Column<string>(type: "text", nullable: true),
|
||||
DurationSeconds = table.Column<int>(type: "integer", nullable: true),
|
||||
Metadata = table.Column<string>(type: "text", nullable: true),
|
||||
Timestamp = table.Column<DateTime>(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");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "VisitEvents");
|
||||
}
|
||||
}
|
||||
}
|
||||
1346
ManagerService/Migrations/20260313144641_AddUserRole.Designer.cs
generated
Normal file
1346
ManagerService/Migrations/20260313144641_AddUserRole.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
63
ManagerService/Migrations/20260313144641_AddUserRole.cs
Normal file
63
ManagerService/Migrations/20260313144641_AddUserRole.cs
Normal file
@ -0,0 +1,63 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace ManagerService.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddUserRole : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "Role",
|
||||
table: "Users",
|
||||
type: "integer",
|
||||
nullable: false,
|
||||
defaultValue: 0);
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "ApiKeys",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<string>(type: "text", nullable: false),
|
||||
Key = table.Column<string>(type: "text", nullable: true),
|
||||
KeyHash = table.Column<string>(type: "text", nullable: true),
|
||||
Name = table.Column<string>(type: "text", nullable: false),
|
||||
InstanceId = table.Column<string>(type: "text", nullable: false),
|
||||
AppType = table.Column<int>(type: "integer", nullable: false),
|
||||
IsActive = table.Column<bool>(type: "boolean", nullable: false),
|
||||
DateCreation = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
|
||||
DateExpiration = table.Column<DateTime>(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");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "ApiKeys");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "Role",
|
||||
table: "Users");
|
||||
}
|
||||
}
|
||||
}
|
||||
1346
ManagerService/Migrations/20260313144657_AddApiKeys.Designer.cs
generated
Normal file
1346
ManagerService/Migrations/20260313144657_AddApiKeys.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
22
ManagerService/Migrations/20260313144657_AddApiKeys.cs
Normal file
22
ManagerService/Migrations/20260313144657_AddApiKeys.cs
Normal file
@ -0,0 +1,22 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace ManagerService.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddApiKeys : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -27,6 +27,44 @@ namespace ManagerService.Migrations
|
||||
NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "postgis");
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("ManagerService.Data.ApiKey", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<int>("AppType")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<DateTime>("DateCreation")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<DateTime?>("DateExpiration")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("InstanceId")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<bool>("IsActive")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("Key")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("KeyHash")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("InstanceId");
|
||||
|
||||
b.ToTable("ApiKeys");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ManagerService.Data.AppConfigurationLink", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
@ -105,6 +143,9 @@ namespace ManagerService.Migrations
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<bool>("IsAssistant")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.PrimitiveCollection<List<string>>("Languages")
|
||||
.HasColumnType("text[]");
|
||||
|
||||
@ -258,6 +299,9 @@ namespace ManagerService.Migrations
|
||||
b.Property<DateTime>("DateCreation")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<bool>("IsAssistant")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<bool>("IsMobile")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
@ -778,6 +822,9 @@ namespace ManagerService.Migrations
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<int>("Role")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("Token")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
@ -787,6 +834,52 @@ namespace ManagerService.Migrations
|
||||
b.ToTable("Users");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ManagerService.Data.VisitEvent", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<int>("AppType")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("ConfigurationId")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<int?>("DurationSeconds")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("EventType")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("InstanceId")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Language")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Metadata")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("SectionId")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("SessionId")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<DateTime>("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")
|
||||
|
||||
@ -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";
|
||||
|
||||
/// <summary>
|
||||
/// Permissions
|
||||
/// </summary>
|
||||
private class Permissions
|
||||
public static class Permissions
|
||||
{
|
||||
/// <summary>
|
||||
/// Admin access
|
||||
/// </summary>
|
||||
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";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Custom claims types
|
||||
/// </summary>
|
||||
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";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Permissions for each type of profile
|
||||
/// </summary>
|
||||
public static readonly Dictionary<System.Type, string[]> ProfilesConfiguration = new Dictionary<System.Type, string[]>()
|
||||
public static readonly Dictionary<UserRole, string[]> 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 },
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Policies names
|
||||
/// </summary>
|
||||
public class Policies
|
||||
public static class ClaimTypes
|
||||
{
|
||||
/// <summary>
|
||||
/// Administration
|
||||
/// </summary>
|
||||
public const string Admin = "Manager.Administration";
|
||||
public const string Permission = "Permission";
|
||||
public const string InstanceId = "InstanceId";
|
||||
public const string AppType = "AppType";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Policies
|
||||
/// </summary>
|
||||
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 } },
|
||||
};
|
||||
}
|
||||
}
|
||||
57
ManagerService/Security/ApiKeyAuthenticationHandler.cs
Normal file
57
ManagerService/Security/ApiKeyAuthenticationHandler.cs
Normal file
@ -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<AuthenticationSchemeOptions>
|
||||
{
|
||||
private const string HeaderName = "X-Api-Key";
|
||||
private readonly MyInfoMateDbContext _db;
|
||||
|
||||
public ApiKeyAuthenticationHandler(
|
||||
IOptionsMonitor<AuthenticationSchemeOptions> options,
|
||||
ILoggerFactory logger,
|
||||
UrlEncoder encoder,
|
||||
MyInfoMateDbContext db)
|
||||
: base(options, logger, encoder)
|
||||
{
|
||||
_db = db;
|
||||
}
|
||||
|
||||
protected override async Task<AuthenticateResult> 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
125
ManagerService/Services/ApiKeyDatabaseService.cs
Normal file
125
ManagerService/Services/ApiKeyDatabaseService.cs
Normal file
@ -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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new API key with a hashed secret (returned once in plain text).
|
||||
/// </summary>
|
||||
public async Task<string> 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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns (or creates) a persistent plain-text key for a given instance + appType (PIN flow).
|
||||
/// </summary>
|
||||
public async Task<string> 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;
|
||||
}
|
||||
|
||||
/// <summary>Returns all API keys for an instance (without secret values).</summary>
|
||||
public async Task<List<ApiKeyDTO>> 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();
|
||||
}
|
||||
|
||||
/// <summary>Revokes (deactivates) an API key, verifying ownership.</summary>
|
||||
public async Task<bool> 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; }
|
||||
}
|
||||
}
|
||||
165
ManagerService/Services/AssistantService.cs
Normal file
165
ManagerService/Services/AssistantService.cs
Normal file
@ -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<AiChatResponse> ChatAsync(AiChatRequest request)
|
||||
{
|
||||
var messages = new List<ChatMessage>();
|
||||
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<AiCardDTO>? cards = null;
|
||||
|
||||
// Outil scopé : vérifie que la section appartient bien à cette configuration
|
||||
var scopedSections = sections;
|
||||
var tools = new List<AITool>
|
||||
{
|
||||
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<AITool>
|
||||
{
|
||||
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 };
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -57,14 +57,18 @@ namespace ManagerService.Service.Services
|
||||
{
|
||||
try
|
||||
{
|
||||
var claims = new List<System.Security.Claims.Claim>();
|
||||
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<System.Security.Claims.Claim>
|
||||
{
|
||||
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();
|
||||
|
||||
@ -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<TokensSettings>(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<AuthenticationSchemeOptions, ApiKeyAuthenticationHandler>("ApiKey", _ => { });
|
||||
|
||||
#if RELEASE
|
||||
//services.AddMqttClientHostedService();
|
||||
@ -151,6 +163,18 @@ namespace ManagerService
|
||||
services.AddScoped<ResourceDatabaseService>();
|
||||
services.AddScoped<DeviceDatabaseService>();
|
||||
services.AddScoped<InstanceDatabaseService>();
|
||||
services.AddScoped<ApiKeyDatabaseService>();
|
||||
|
||||
// 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<IChatClient>(_ =>
|
||||
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<AssistantService>();
|
||||
|
||||
|
||||
var connectionString = Configuration.GetConnectionString("PostgresConnection");
|
||||
@ -162,7 +186,7 @@ namespace ManagerService
|
||||
|
||||
services.AddDbContext<MyInfoMateDbContext>(options =>
|
||||
options.UseNpgsql(dataSource, o => o.UseNetTopologySuite())
|
||||
.EnableSensitiveDataLogging() // montre les valeurs des paramètres
|
||||
.EnableSensitiveDataLogging() // montre les valeurs des param<EFBFBD>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<string, string>
|
||||
{
|
||||
{ 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<string>(), 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";
|
||||
|
||||
@ -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"
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user