Files
EchoHub/src/EchoHub.Server/Data/EchoHubDbContext.cs
T
HueByte e05b420ce9 feat: Implement end-to-end encryption for channels
- Added EncryptionSalt and WrappedRoomKey properties to Channel model.
- Introduced RoomCrypto class for client-side encryption and decryption.
- Updated ChannelService to handle encrypted channels, including creation and rekeying.
- Modified ChannelsController to expose crypto metadata and rekey functionality.
- Enhanced IrcCommandHandler to block joining encrypted channels over IRC.
- Updated database schema with migration for new encryption fields.
- Refactored file validation and image processing services to accommodate encrypted channels.
- Added unit tests for RoomCrypto functionality and updated existing tests for channel services.
2026-07-16 03:50:23 +02:00

116 lines
4.7 KiB
C#

using EchoHub.Core.Models;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
namespace EchoHub.Server.Data;
public class EchoHubDbContext : DbContext
{
public EchoHubDbContext(DbContextOptions<EchoHubDbContext> options) : base(options) { }
public DbSet<User> Users => Set<User>();
public DbSet<Channel> Channels => Set<Channel>();
public DbSet<Message> Messages => Set<Message>();
public DbSet<RefreshToken> RefreshTokens => Set<RefreshToken>();
public DbSet<ChannelMembership> ChannelMemberships => Set<ChannelMembership>();
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<User>(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<int>();
});
modelBuilder.Entity<Channel>(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<Message>(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<ChannelMembership>(entity =>
{
entity.HasKey(cm => new { cm.UserId, cm.ChannelId });
entity.HasIndex(cm => cm.UserId);
entity.HasIndex(cm => cm.ChannelId);
entity.HasOne<Channel>()
.WithMany()
.HasForeignKey(cm => cm.ChannelId)
.OnDelete(DeleteBehavior.Cascade);
entity.HasOne<User>()
.WithMany()
.HasForeignKey(cm => cm.UserId)
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity<RefreshToken>(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<DateTimeOffset, long>(
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);
}
}
}
}
}