manager-service/ManagerService/Data/MyInfoMateDbContext.cs

394 lines
16 KiB
C#

using Manager.DTOs;
using ManagerService.Data.SubSection;
using ManagerService.DTOs;
using Microsoft.AspNetCore.Http;
using Microsoft.EntityFrameworkCore;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using static ManagerService.Data.SubSection.SectionEvent;
namespace ManagerService.Data
{
public class MyInfoMateDbContext : DbContext
{
private readonly IHttpContextAccessor _httpContextAccessor;
public MyInfoMateDbContext(DbContextOptions<MyInfoMateDbContext> options, IHttpContextAccessor httpContextAccessor)
: base(options)
{
_httpContextAccessor = httpContextAccessor;
}
public DbSet<Instance> Instances { get; set; }
public DbSet<SubscriptionPlan> SubscriptionPlans { get; set; }
public DbSet<Configuration> Configurations { get; set; }
public DbSet<Section> Sections { get; set; }
public DbSet<Device> Devices { get; set; }
public DbSet<Resource> Resources { get; set; }
public DbSet<User> Users { get; set; }
public DbSet<ApplicationInstance> ApplicationInstances { get; set; }
public DbSet<AppConfigurationLink> AppConfigurationLinks { get; set; }
// MAP
public DbSet<GeoPoint> GeoPoints { get; set; }
// QUIZ
public DbSet<QuizQuestion> QuizQuestions { get; set; }
public DbSet<GuidedPath> GuidedPaths { get; set; }
public DbSet<GuidedStep> GuidedSteps { get; set; }
// Events
public DbSet<ProgrammeBlock> ProgrammeBlocks { get; set; }
public DbSet<MapAnnotation> MapAnnotations { get; set; }
// Agenda
public DbSet<EventAgenda> EventAgendas { get; set; }
// Statistics
public DbSet<VisitEvent> VisitEvents { get; set; }
// API Keys
public DbSet<ApiKey> ApiKeys { get; set; }
// Push Notifications
public DbSet<PushNotification> PushNotifications { get; set; }
// Audit
public DbSet<AuditLog> AuditLogs { get; set; }
public override async Task<int> SaveChangesAsync(CancellationToken cancellationToken = default)
{
var auditEntries = BuildAuditEntries();
var result = await base.SaveChangesAsync(cancellationToken);
if (auditEntries.Any())
await base.SaveChangesAsync(cancellationToken);
return result;
}
private static readonly HashSet<Type> AuditedTypes = new()
{
typeof(Section), typeof(Resource), typeof(Configuration),
typeof(Device), typeof(User), typeof(Instance)
};
private List<AuditLog> BuildAuditEntries()
{
var userId = _httpContextAccessor.HttpContext?.User?.FindFirstValue(ClaimTypes.NameIdentifier);
var entries = new List<AuditLog>();
foreach (var entry in ChangeTracker.Entries()
.Where(e => AuditedTypes.Contains(e.Entity.GetType())
&& e.State is EntityState.Added or EntityState.Modified or EntityState.Deleted))
{
var action = entry.State switch
{
EntityState.Added => "Create",
EntityState.Modified => "Update",
EntityState.Deleted => "Delete",
_ => null
};
var entityId = entry.Properties
.FirstOrDefault(p => p.Metadata.IsPrimaryKey())?.CurrentValue?.ToString();
var instanceId = entry.Properties
.FirstOrDefault(p => p.Metadata.Name == "InstanceId")?.CurrentValue?.ToString();
string? oldValues = null;
string? newValues = null;
if (entry.State == EntityState.Modified)
{
oldValues = JsonSerializer.Serialize(
entry.Properties.Where(p => p.IsModified)
.ToDictionary(p => p.Metadata.Name, p => p.OriginalValue));
newValues = JsonSerializer.Serialize(
entry.Properties.Where(p => p.IsModified)
.ToDictionary(p => p.Metadata.Name, p => p.CurrentValue));
}
else if (entry.State == EntityState.Added)
{
newValues = JsonSerializer.Serialize(
entry.Properties.ToDictionary(p => p.Metadata.Name, p => p.CurrentValue));
}
var log = new AuditLog
{
EntityType = entry.Entity.GetType().Name,
EntityId = entityId ?? "",
Action = action!,
UserId = userId,
InstanceId = instanceId,
OldValues = oldValues,
NewValues = newValues
};
AuditLogs.Add(log);
entries.Add(log);
}
return entries;
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
var options = new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true
};
base.OnModelCreating(modelBuilder);
// Les types suivants sont utilisés uniquement comme valeurs JSONB, pas comme entités DB.
// Le provider InMemory les découvre par convention et échoue à la validation
// si aucune clé primaire n'est définie. On les exclut explicitement.
modelBuilder.Ignore<ContentDTO>();
modelBuilder.Ignore<ResponseDTO>();
modelBuilder.Ignore<CategorieDTO>();
modelBuilder.Ignore<TranslationDTO>();
modelBuilder.Ignore<TranslationAndResourceDTO>();
modelBuilder.Ignore<OrderedTranslationAndResourceDTO>();
modelBuilder.Ignore<EventAddress>();
modelBuilder.Ignore<Translation>();
modelBuilder.Ignore<TranslationAndResource>();
modelBuilder.Ignore<OrderedTranslationAndResource>();
modelBuilder.Entity<Configuration>()
.Property(s => s.Title)
.HasColumnType("jsonb")
.HasConversion(
v => JsonSerializer.Serialize(v, options),
v => JsonSerializer.Deserialize<List<TranslationDTO>>(v, options));
modelBuilder.Entity<Section>()
.Property<string>("Discriminator")
.HasMaxLength(50);
modelBuilder.Entity<Section>()
.HasDiscriminator<string>("Discriminator")
.HasValue<Section>("Base")
.HasValue<SectionAgenda>("Agenda")
.HasValue<SectionArticle>("Article")
.HasValue<SectionEvent>("Event")
.HasValue<SectionMap>("Map")
.HasValue<SectionMenu>("Menu")
.HasValue<SectionPdf>("PDF")
.HasValue<SectionGame>("Game")
.HasValue<SectionQuiz>("Quiz")
.HasValue<SectionSlider>("Slider")
.HasValue<SectionVideo>("Video")
.HasValue<SectionWeather>("Weather")
.HasValue<SectionWeb>("Web");
/*modelBuilder.Entity<GeoPoint>(entity =>
{
entity.Property(e => e.Geometry).HasColumnType("geometry");
});
modelBuilder.Entity<GuidedStep>(entity =>
{
entity.Property(e => e.Geometry).HasColumnType("geometry");
});*/
modelBuilder.Entity<Section>()
.Property(s => s.Title)
.HasColumnType("jsonb")
.HasConversion(
v => JsonSerializer.Serialize(v, options),
v => JsonSerializer.Deserialize<List<TranslationDTO>>(v, options));
modelBuilder.Entity<Section>()
.Property(s => s.Description)
.HasColumnType("jsonb")
.HasConversion(
v => JsonSerializer.Serialize(v, options),
v => JsonSerializer.Deserialize<List<TranslationDTO>>(v, options));
modelBuilder.Entity<GeoPoint>()
.Property(s => s.Title)
.HasColumnType("jsonb")
.HasConversion(
v => JsonSerializer.Serialize(v, options),
v => JsonSerializer.Deserialize<List<TranslationDTO>>(v, options));
modelBuilder.Entity<GeoPoint>()
.Property(s => s.Description)
.HasColumnType("jsonb")
.HasConversion(
v => JsonSerializer.Serialize(v, options),
v => JsonSerializer.Deserialize<List<TranslationDTO>>(v, options));
modelBuilder.Entity<GeoPoint>()
.Property(s => s.Contents)
.HasColumnType("jsonb")
.HasConversion(
v => JsonSerializer.Serialize(v, options),
v => JsonSerializer.Deserialize<List<ContentDTO>>(v, options));
modelBuilder.Entity<GeoPoint>()
.Property(s => s.Schedules)
.HasColumnType("jsonb")
.HasConversion(
v => JsonSerializer.Serialize(v, options),
v => JsonSerializer.Deserialize<List<TranslationDTO>>(v, options));
modelBuilder.Entity<GeoPoint>()
.Property(s => s.Prices)
.HasColumnType("jsonb")
.HasConversion(
v => JsonSerializer.Serialize(v, options),
v => JsonSerializer.Deserialize<List<TranslationDTO>>(v, options));
modelBuilder.Entity<GeoPoint>()
.Property(s => s.Phone)
.HasColumnType("jsonb")
.HasConversion(
v => JsonSerializer.Serialize(v, options),
v => JsonSerializer.Deserialize<List<TranslationDTO>>(v, options));
modelBuilder.Entity<GeoPoint>()
.Property(s => s.Email)
.HasColumnType("jsonb")
.HasConversion(
v => JsonSerializer.Serialize(v, options),
v => JsonSerializer.Deserialize<List<TranslationDTO>>(v, options));
modelBuilder.Entity<GeoPoint>()
.Property(s => s.Site)
.HasColumnType("jsonb")
.HasConversion(
v => JsonSerializer.Serialize(v, options),
v => JsonSerializer.Deserialize<List<TranslationDTO>>(v, options));
// Configurations JSON pour GuidedPath
modelBuilder.Entity<GuidedPath>()
.Property(gp => gp.Title)
.HasColumnType("jsonb")
.HasConversion(
v => JsonSerializer.Serialize(v, options),
v => JsonSerializer.Deserialize<List<TranslationDTO>>(v, options));
modelBuilder.Entity<GuidedPath>()
.Property(gp => gp.Description)
.HasColumnType("jsonb")
.HasConversion(
v => JsonSerializer.Serialize(v, options),
v => JsonSerializer.Deserialize<List<TranslationDTO>>(v, options));
// Configurations JSON pour GuidedStep
modelBuilder.Entity<GuidedStep>()
.Property(gs => gs.Title)
.HasColumnType("jsonb")
.HasConversion(
v => JsonSerializer.Serialize(v, options),
v => JsonSerializer.Deserialize<List<TranslationDTO>>(v, options));
modelBuilder.Entity<GuidedStep>()
.Property(gs => gs.Description)
.HasColumnType("jsonb")
.HasConversion(
v => JsonSerializer.Serialize(v, options),
v => JsonSerializer.Deserialize<List<TranslationDTO>>(v, options));
modelBuilder.Entity<GuidedStep>()
.Property(gp => gp.TimerExpiredMessage)
.HasColumnType("jsonb")
.HasConversion(
v => JsonSerializer.Serialize(v, options),
v => JsonSerializer.Deserialize<List<TranslationDTO>>(v, options));
modelBuilder.Entity<EventAgenda>()
.Property(s => s.Label)
.HasColumnType("jsonb")
.HasConversion(
v => JsonSerializer.Serialize(v, options),
v => JsonSerializer.Deserialize<List<TranslationDTO>>(v, options));
modelBuilder.Entity<EventAgenda>()
.Property(s => s.Description)
.HasColumnType("jsonb")
.HasConversion(
v => JsonSerializer.Serialize(v, options),
v => JsonSerializer.Deserialize<List<TranslationDTO>>(v, options));
modelBuilder.Entity<EventAgenda>()
.Property(s => s.Address)
.HasColumnType("jsonb")
.HasConversion(
v => JsonSerializer.Serialize(v, options),
v => JsonSerializer.Deserialize<EventAddress>(v, options));
// SectionEvent: link to base SectionMap
modelBuilder.Entity<SectionEvent>()
.HasOne(se => se.BaseMap)
.WithMany()
.HasForeignKey(se => se.BaseSectionMapId)
.IsRequired(false)
.OnDelete(DeleteBehavior.SetNull);
// MapAnnotation: global event-level annotations linked directly to SectionEvent
modelBuilder.Entity<MapAnnotation>()
.HasOne<SectionEvent>()
.WithMany(se => se.GlobalMapAnnotations)
.HasForeignKey(ma => ma.SectionEventId)
.IsRequired(false)
.OnDelete(DeleteBehavior.Cascade);
// Ces colonnes ont un defaultValue dans la migration, ce qui amène EF Core à les traiter
// comme ValueGeneratedOnAdd (read-only après insert). On force ValueGeneratedNever.
modelBuilder.Entity<Instance>()
.Property(i => i.StorageQuotaBytes).ValueGeneratedNever();
modelBuilder.Entity<Instance>()
.Property(i => i.AiRequestsPerMonth).ValueGeneratedNever();
modelBuilder.Entity<Instance>()
.Property(i => i.HasStats).ValueGeneratedNever();
modelBuilder.Entity<Instance>()
.Property(i => i.StatsHistoryDays).ValueGeneratedNever();
modelBuilder.Entity<Instance>()
.Property(i => i.HasAdvancedStats).ValueGeneratedNever();
// Seed : plans d'abonnement par défaut
modelBuilder.Entity<SubscriptionPlan>().HasData(
new SubscriptionPlan
{
Id = "plan-starter",
Name = "Starter",
StorageQuotaBytes = 1L * 1024 * 1024 * 1024, // 1 GB
AiRequestsPerMonth = 0,
HasStats = false,
StatsHistoryDays = 30,
HasAdvancedStats = false,
},
new SubscriptionPlan
{
Id = "plan-standard",
Name = "Standard",
StorageQuotaBytes = 10L * 1024 * 1024 * 1024, // 10 GB
AiRequestsPerMonth = 500,
HasStats = true,
StatsHistoryDays = 30,
HasAdvancedStats = false,
},
new SubscriptionPlan
{
Id = "plan-premium",
Name = "Premium",
StorageQuotaBytes = 50L * 1024 * 1024 * 1024, // 50 GB
AiRequestsPerMonth = 2000,
HasStats = true,
StatsHistoryDays = 0,
HasAdvancedStats = true,
}
);
}
}
}