using EchoHub.Core.Models; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; namespace EchoHub.Server.Data; public class EchoHubDbContext : DbContext { public EchoHubDbContext(DbContextOptions options) : base(options) { } public DbSet Users => Set(); public DbSet Channels => Set(); public DbSet Messages => Set(); public DbSet RefreshTokens => Set(); public DbSet ChannelMemberships => Set(); protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { if (!optionsBuilder.IsConfigured) { var dbPath = Path.Combine(AppContext.BaseDirectory, "echohub.db"); optionsBuilder.UseSqlite($"Data Source={dbPath}"); } } protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity(entity => { entity.HasKey(u => u.Id); entity.HasIndex(u => u.Username).IsUnique(); entity.Property(u => u.Username).IsRequired().HasMaxLength(50); entity.Property(u => u.PasswordHash).IsRequired(); entity.Property(u => u.DisplayName).HasMaxLength(100); entity.Property(u => u.Bio).HasMaxLength(500); entity.Property(u => u.NicknameColor).HasMaxLength(7); entity.Property(u => u.AvatarAscii).HasMaxLength(10000); entity.Property(u => u.StatusMessage).HasMaxLength(100); entity.Property(u => u.Role).HasConversion(); }); modelBuilder.Entity(entity => { entity.HasKey(c => c.Id); entity.HasIndex(c => c.Name).IsUnique(); entity.Property(c => c.Name).IsRequired().HasMaxLength(100); entity.Property(c => c.Topic).HasMaxLength(500); entity.Property(c => c.PasswordHash).HasMaxLength(100); entity.Property(c => c.EncryptionSalt).HasMaxLength(64); entity.Property(c => c.WrappedRoomKey).HasMaxLength(200); entity.HasMany(c => c.Messages) .WithOne(m => m.Channel) .HasForeignKey(m => m.ChannelId) .OnDelete(DeleteBehavior.Cascade); }); modelBuilder.Entity(entity => { entity.HasKey(m => m.Id); entity.HasIndex(m => m.SentAt); entity.Property(m => m.Content).IsRequired().HasMaxLength(16000); // Increased for encrypted content (Base64 overhead) entity.Property(m => m.SenderUsername).IsRequired().HasMaxLength(50); entity.Property(m => m.AttachmentUrl).HasMaxLength(500); entity.Property(m => m.AttachmentFileName).HasMaxLength(255); entity.Property(m => m.EmbedJson).HasMaxLength(32000); // Increased for encrypted embed JSON }); modelBuilder.Entity(entity => { entity.HasKey(cm => new { cm.UserId, cm.ChannelId }); entity.HasIndex(cm => cm.UserId); entity.HasIndex(cm => cm.ChannelId); entity.HasOne() .WithMany() .HasForeignKey(cm => cm.ChannelId) .OnDelete(DeleteBehavior.Cascade); entity.HasOne() .WithMany() .HasForeignKey(cm => cm.UserId) .OnDelete(DeleteBehavior.Cascade); }); modelBuilder.Entity(entity => { entity.HasKey(r => r.Id); entity.HasIndex(r => r.TokenHash); entity.HasIndex(r => r.UserId); entity.Property(r => r.TokenHash).IsRequired().HasMaxLength(128); entity.HasOne(r => r.User) .WithMany() .HasForeignKey(r => r.UserId) .OnDelete(DeleteBehavior.Cascade); }); // SQLite does not support DateTimeOffset in ORDER BY clauses. // Convert all DateTimeOffset properties to Unix milliseconds (long) for storage. var dateTimeOffsetConverter = new ValueConverter( v => v.ToUnixTimeMilliseconds(), v => DateTimeOffset.FromUnixTimeMilliseconds(v)); foreach (var entityType in modelBuilder.Model.GetEntityTypes()) { foreach (var property in entityType.GetProperties()) { if (property.ClrType == typeof(DateTimeOffset) || property.ClrType == typeof(DateTimeOffset?)) { property.SetValueConverter(dateTimeOffsetConverter); } } } } }