Шифрование, GIF, хранилище, админка
This commit is contained in:
+81
@@ -0,0 +1,81 @@
|
||||
using System.Text.Json;
|
||||
using Knot.Shared.Infrastructure.Persistence;
|
||||
using Knot.Shared.Infrastructure.Persistence.Entities;
|
||||
using Knot.Shared.Kernel.Configuration;
|
||||
using Knot.Shared.Kernel.Security;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace Knot.Shared.Infrastructure.Configuration;
|
||||
|
||||
public class SettingsService : ISettingsService
|
||||
{
|
||||
private readonly IServiceProvider _serviceProvider;
|
||||
private readonly IEncryptionService _encryptionService;
|
||||
private SystemSettingsDto _current;
|
||||
|
||||
public SettingsService(IServiceProvider serviceProvider, IEncryptionService encryptionService)
|
||||
{
|
||||
_serviceProvider = serviceProvider;
|
||||
_encryptionService = encryptionService;
|
||||
_current = new SystemSettingsDto(); // Default
|
||||
}
|
||||
|
||||
public SystemSettingsDto Current => _current;
|
||||
|
||||
public async Task<SystemSettingsDto> GetSettingsAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
using var scope = _serviceProvider.CreateScope();
|
||||
var db = scope.ServiceProvider.GetRequiredService<SystemDbContext>();
|
||||
|
||||
var setting = await db.Settings.FirstOrDefaultAsync(s => s.Key == "Global", cancellationToken);
|
||||
if (setting == null)
|
||||
{
|
||||
return new SystemSettingsDto();
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var json = _encryptionService.DecryptMessage(setting.EncryptedValue);
|
||||
var dto = JsonSerializer.Deserialize<SystemSettingsDto>(json);
|
||||
if (dto != null)
|
||||
{
|
||||
_current = dto;
|
||||
return dto;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// If decryption fails or JSON is invalid, return default
|
||||
}
|
||||
|
||||
return new SystemSettingsDto();
|
||||
}
|
||||
|
||||
public async Task UpdateSettingsAsync(SystemSettingsDto settings, CancellationToken cancellationToken = default)
|
||||
{
|
||||
using var scope = _serviceProvider.CreateScope();
|
||||
var db = scope.ServiceProvider.GetRequiredService<SystemDbContext>();
|
||||
|
||||
var json = JsonSerializer.Serialize(settings);
|
||||
var encrypted = _encryptionService.EncryptMessage(json);
|
||||
|
||||
var setting = await db.Settings.FirstOrDefaultAsync(s => s.Key == "Global", cancellationToken);
|
||||
if (setting == null)
|
||||
{
|
||||
db.Settings.Add(new SystemSetting { Key = "Global", EncryptedValue = encrypted });
|
||||
}
|
||||
else
|
||||
{
|
||||
setting.EncryptedValue = encrypted;
|
||||
}
|
||||
|
||||
await db.SaveChangesAsync(cancellationToken);
|
||||
_current = settings; // Update in-memory reference
|
||||
}
|
||||
|
||||
public void Initialize()
|
||||
{
|
||||
_current = GetSettingsAsync().GetAwaiter().GetResult();
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,11 @@ using Microsoft.Extensions.Configuration;
|
||||
using Knot.Shared.Kernel;
|
||||
using Knot.Shared.Kernel.Security;
|
||||
using Knot.Shared.Kernel.Storage;
|
||||
using Knot.Shared.Infrastructure.Persistence;
|
||||
using Knot.Shared.Infrastructure.Configuration;
|
||||
using Knot.Shared.Infrastructure.Statistics;
|
||||
using Knot.Shared.Kernel.Configuration;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Minio;
|
||||
using System;
|
||||
|
||||
@@ -40,6 +45,20 @@ public static class DependencyInjection
|
||||
return new S3FileStorageService(minioClient, encService, s3Bucket);
|
||||
});
|
||||
|
||||
// Настройка System Database
|
||||
var connectionString = configuration.GetConnectionString("DefaultConnection")
|
||||
?? configuration["DATABASE_URL"]
|
||||
?? throw new ArgumentNullException("Database connection string not found.");
|
||||
|
||||
services.AddDbContext<SystemDbContext>(options =>
|
||||
options.UseNpgsql(connectionString));
|
||||
|
||||
services.AddMemoryCache();
|
||||
services.AddSingleton<ISettingsService, SettingsService>();
|
||||
services.AddScoped<Knot.Shared.Kernel.Services.IStatisticsService, StatisticsService>();
|
||||
|
||||
services.AddHostedService<StatisticsWorker>();
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
||||
|
||||
+5
-1
@@ -12,8 +12,12 @@
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="10.0.4" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.4">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Minio" Version="7.0.0" />
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="10.0.0" />
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="10.0.1" />
|
||||
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.16.0" />
|
||||
<PackageReference Include="Microsoft.IdentityModel.Tokens" Version="8.16.0" />
|
||||
</ItemGroup>
|
||||
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
using System.Net.Http.Headers;
|
||||
using System.Text;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
|
||||
namespace Knot.Shared.Infrastructure.Middleware;
|
||||
|
||||
public class AdminAuthMiddleware
|
||||
{
|
||||
private readonly RequestDelegate _next;
|
||||
|
||||
public AdminAuthMiddleware(RequestDelegate next)
|
||||
{
|
||||
_next = next;
|
||||
}
|
||||
|
||||
public async Task InvokeAsync(HttpContext context, IConfiguration configuration)
|
||||
{
|
||||
if (context.Request.Path.StartsWithSegments("/api/admin"))
|
||||
{
|
||||
var expectedUser = configuration["KNOT_ADMIN_USER"];
|
||||
var expectedPass = configuration["KNOT_ADMIN_PASSWORD"];
|
||||
|
||||
if (string.IsNullOrEmpty(expectedUser) || string.IsNullOrEmpty(expectedPass))
|
||||
{
|
||||
context.Response.StatusCode = 500;
|
||||
await context.Response.WriteAsync("Admin credentials not configured on server.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!context.Request.Headers.ContainsKey("Authorization"))
|
||||
{
|
||||
context.Response.Headers.Append("WWW-Authenticate", "Basic realm=\"Admin Area\"");
|
||||
context.Response.StatusCode = 401;
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var authHeader = AuthenticationHeaderValue.Parse(context.Request.Headers["Authorization"]);
|
||||
var credentialBytes = Convert.FromBase64String(authHeader.Parameter ?? string.Empty);
|
||||
var credentials = Encoding.UTF8.GetString(credentialBytes).Split(':', 2);
|
||||
|
||||
var username = credentials[0];
|
||||
var password = credentials[1];
|
||||
|
||||
if (username != expectedUser || password != expectedPass)
|
||||
{
|
||||
context.Response.StatusCode = 403;
|
||||
return;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
context.Response.StatusCode = 401;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
await _next(context);
|
||||
}
|
||||
}
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Knot.Shared.Infrastructure.Persistence;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Knot.Shared.Infrastructure.Migrations
|
||||
{
|
||||
[DbContext(typeof(SystemDbContext))]
|
||||
[Migration("20260315204509_InitialSystem")]
|
||||
partial class InitialSystem
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasDefaultSchema("system")
|
||||
.HasAnnotation("ProductVersion", "10.0.4")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("Knot.Shared.Infrastructure.Persistence.Entities.DailyStat", b =>
|
||||
{
|
||||
b.Property<DateTime>("Date")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<long>("ActiveUsers")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<long>("TotalFilesSize")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<long>("TotalMessages")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.HasKey("Date");
|
||||
|
||||
b.ToTable("DailyStats", "system");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Knot.Shared.Infrastructure.Persistence.Entities.SystemSetting", b =>
|
||||
{
|
||||
b.Property<string>("Key")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("EncryptedValue")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("Key");
|
||||
|
||||
b.ToTable("Settings", "system");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
+58
@@ -0,0 +1,58 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Knot.Shared.Infrastructure.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class InitialSystem : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.EnsureSchema(
|
||||
name: "system");
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "DailyStats",
|
||||
schema: "system",
|
||||
columns: table => new
|
||||
{
|
||||
Date = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
|
||||
TotalMessages = table.Column<long>(type: "bigint", nullable: false),
|
||||
TotalFilesSize = table.Column<long>(type: "bigint", nullable: false),
|
||||
ActiveUsers = table.Column<long>(type: "bigint", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_DailyStats", x => x.Date);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Settings",
|
||||
schema: "system",
|
||||
columns: table => new
|
||||
{
|
||||
Key = table.Column<string>(type: "text", nullable: false),
|
||||
EncryptedValue = table.Column<string>(type: "text", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_Settings", x => x.Key);
|
||||
});
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "DailyStats",
|
||||
schema: "system");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "Settings",
|
||||
schema: "system");
|
||||
}
|
||||
}
|
||||
}
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Knot.Shared.Infrastructure.Persistence;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Knot.Shared.Infrastructure.Migrations
|
||||
{
|
||||
[DbContext(typeof(SystemDbContext))]
|
||||
partial class SystemDbContextModelSnapshot : ModelSnapshot
|
||||
{
|
||||
protected override void BuildModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasDefaultSchema("system")
|
||||
.HasAnnotation("ProductVersion", "10.0.4")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("Knot.Shared.Infrastructure.Persistence.Entities.DailyStat", b =>
|
||||
{
|
||||
b.Property<DateTime>("Date")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<long>("ActiveUsers")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<long>("TotalFilesSize")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<long>("TotalMessages")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.HasKey("Date");
|
||||
|
||||
b.ToTable("DailyStats", "system");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Knot.Shared.Infrastructure.Persistence.Entities.SystemSetting", b =>
|
||||
{
|
||||
b.Property<string>("Key")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("EncryptedValue")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("Key");
|
||||
|
||||
b.ToTable("Settings", "system");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
using System;
|
||||
|
||||
namespace Knot.Shared.Infrastructure.Persistence.Entities;
|
||||
|
||||
public class DailyStat
|
||||
{
|
||||
public DateTime Date { get; set; }
|
||||
public long TotalMessages { get; set; }
|
||||
public long TotalFilesSize { get; set; }
|
||||
public long ActiveUsers { get; set; }
|
||||
}
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
using System;
|
||||
|
||||
namespace Knot.Shared.Infrastructure.Persistence.Entities;
|
||||
|
||||
public class SystemSetting
|
||||
{
|
||||
public string Key { get; set; } = string.Empty;
|
||||
public string EncryptedValue { get; set; } = string.Empty;
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
using Knot.Shared.Infrastructure.Persistence.Entities;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Knot.Shared.Infrastructure.Persistence;
|
||||
|
||||
public class SystemDbContext : DbContext
|
||||
{
|
||||
public SystemDbContext(DbContextOptions<SystemDbContext> options) : base(options) { }
|
||||
|
||||
public DbSet<SystemSetting> Settings => Set<SystemSetting>();
|
||||
public DbSet<DailyStat> DailyStats => Set<DailyStat>();
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||
{
|
||||
modelBuilder.HasDefaultSchema("system");
|
||||
|
||||
modelBuilder.Entity<SystemSetting>(builder =>
|
||||
{
|
||||
builder.ToTable("Settings");
|
||||
builder.HasKey(s => s.Key);
|
||||
});
|
||||
|
||||
modelBuilder.Entity<DailyStat>(builder =>
|
||||
{
|
||||
builder.ToTable("DailyStats");
|
||||
builder.HasKey(s => s.Date);
|
||||
});
|
||||
}
|
||||
}
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Design;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using System.IO;
|
||||
|
||||
namespace Knot.Shared.Infrastructure.Persistence;
|
||||
|
||||
public class SystemDbContextFactory : IDesignTimeDbContextFactory<SystemDbContext>
|
||||
{
|
||||
public SystemDbContext CreateDbContext(string[] args)
|
||||
{
|
||||
var basePath = Path.Combine(Directory.GetCurrentDirectory(), "..", "Host");
|
||||
if (!Directory.Exists(basePath))
|
||||
{
|
||||
basePath = Directory.GetCurrentDirectory(); // fallback
|
||||
}
|
||||
|
||||
var configuration = new ConfigurationBuilder()
|
||||
.SetBasePath(basePath)
|
||||
.AddJsonFile("appsettings.json", optional: true)
|
||||
.AddEnvironmentVariables()
|
||||
.Build();
|
||||
|
||||
var builder = new DbContextOptionsBuilder<SystemDbContext>();
|
||||
var connectionString = configuration.GetConnectionString("DefaultConnection")
|
||||
?? configuration["DATABASE_URL"]
|
||||
?? "Host=localhost;Port=5432;Database=knot_db;Username=knot;Password=knot_pass";
|
||||
|
||||
builder.UseNpgsql(connectionString);
|
||||
|
||||
return new SystemDbContext(builder.Options);
|
||||
}
|
||||
}
|
||||
+109
@@ -0,0 +1,109 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Knot.Shared.Kernel.Services;
|
||||
using Knot.Shared.Infrastructure.Persistence;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
|
||||
namespace Knot.Shared.Infrastructure.Statistics;
|
||||
|
||||
public class StatisticsService : IStatisticsService
|
||||
{
|
||||
private readonly SystemDbContext _db;
|
||||
private readonly IMemoryCache _cache;
|
||||
|
||||
private const string CacheKey = "dashboard_stats";
|
||||
|
||||
public StatisticsService(SystemDbContext db, IMemoryCache cache)
|
||||
{
|
||||
_db = db;
|
||||
_cache = cache;
|
||||
}
|
||||
|
||||
public async Task<DashboardStatsDto> GetDashboardStatsAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
// 10 second caching to allow fast UI updates while still preventing DB overload
|
||||
if (_cache.TryGetValue(CacheKey, out DashboardStatsDto cached))
|
||||
{
|
||||
return cached!;
|
||||
}
|
||||
|
||||
var latestStat = await _db.DailyStats
|
||||
.OrderByDescending(s => s.Date)
|
||||
.FirstOrDefaultAsync(cancellationToken);
|
||||
|
||||
var history = await _db.DailyStats
|
||||
.OrderByDescending(s => s.Date)
|
||||
.Take(30)
|
||||
.Select(s => new ActivityStatDto
|
||||
{
|
||||
Date = s.Date,
|
||||
Messages = s.TotalMessages,
|
||||
FilesSize = s.TotalFilesSize
|
||||
})
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
var totalUsers = await _db.Database.GetDbConnection().CreateCommand().QueryTotalUsersAsync(); // Faked for simplicity without direct Identity reference
|
||||
|
||||
long totalDiskSpace = 5L * 1024 * 1024 * 1024 * 1024; // Fallback
|
||||
long freeSpace = 0;
|
||||
try
|
||||
{
|
||||
var drive = new System.IO.DriveInfo("/");
|
||||
if(drive.IsReady)
|
||||
{
|
||||
totalDiskSpace = drive.TotalSize;
|
||||
freeSpace = drive.AvailableFreeSpace;
|
||||
}
|
||||
else
|
||||
{
|
||||
drive = new System.IO.DriveInfo(System.IO.Directory.GetCurrentDirectory());
|
||||
totalDiskSpace = drive.TotalSize;
|
||||
freeSpace = drive.AvailableFreeSpace;
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
|
||||
var usedSpace = latestStat?.TotalFilesSize ?? 0;
|
||||
|
||||
long onlineUsersCount = _cache.TryGetValue("Global_OnlineUsersCount", out int count) ? count : 0;
|
||||
long offlineUsersCount = totalUsers - onlineUsersCount;
|
||||
if(offlineUsersCount < 0) offlineUsersCount = 0;
|
||||
|
||||
var stats = new DashboardStatsDto
|
||||
{
|
||||
StorageUsedBytes = usedSpace,
|
||||
StorageLimitBytes = freeSpace > 0 ? usedSpace + freeSpace : totalDiskSpace,
|
||||
OnlineUsers = onlineUsersCount,
|
||||
OfflineUsers = offlineUsersCount,
|
||||
TotalUsers = totalUsers,
|
||||
ActivityTimeline = history
|
||||
};
|
||||
|
||||
_cache.Set(CacheKey, stats, TimeSpan.FromSeconds(10));
|
||||
|
||||
return stats;
|
||||
}
|
||||
}
|
||||
|
||||
// Temporary internal extensions for cross-module Db fetching during demonstration
|
||||
internal static class SqlExtensions
|
||||
{
|
||||
public static async Task<long> QueryTotalUsersAsync(this System.Data.IDbCommand cmd)
|
||||
{
|
||||
try
|
||||
{
|
||||
cmd.CommandText = "SELECT COUNT(*) FROM identity.\"Users\"";
|
||||
if (cmd.Connection?.State != System.Data.ConnectionState.Open)
|
||||
await ((System.Data.Common.DbConnection)cmd.Connection!).OpenAsync();
|
||||
var result = await ((System.Data.Common.DbCommand)cmd).ExecuteScalarAsync();
|
||||
return Convert.ToInt64(result);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return 0; // Fallback
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Knot.Shared.Infrastructure.Persistence;
|
||||
using Knot.Shared.Infrastructure.Persistence.Entities;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
|
||||
namespace Knot.Shared.Infrastructure.Statistics;
|
||||
|
||||
public class StatisticsWorker : BackgroundService
|
||||
{
|
||||
private readonly IServiceProvider _serviceProvider;
|
||||
|
||||
public StatisticsWorker(IServiceProvider serviceProvider)
|
||||
{
|
||||
_serviceProvider = serviceProvider;
|
||||
}
|
||||
|
||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||
{
|
||||
while (!stoppingToken.IsCancellationRequested)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var scope = _serviceProvider.CreateScope();
|
||||
var db = scope.ServiceProvider.GetRequiredService<SystemDbContext>();
|
||||
|
||||
// Simplified gathering for daily stats
|
||||
var today = DateTime.UtcNow.Date;
|
||||
var stat = await db.DailyStats.FirstOrDefaultAsync(s => s.Date == today, stoppingToken);
|
||||
|
||||
if (stat == null)
|
||||
{
|
||||
stat = new DailyStat { Date = today };
|
||||
db.DailyStats.Add(stat);
|
||||
}
|
||||
|
||||
var conn = db.Database.GetDbConnection();
|
||||
var cmd = conn.CreateCommand();
|
||||
|
||||
stat.ActiveUsers = await cmd.QueryTotalUsersAsync();
|
||||
|
||||
long dbSize = 0;
|
||||
long filesSize = 0;
|
||||
try
|
||||
{
|
||||
// 1. Database size itself
|
||||
cmd.CommandText = "SELECT pg_database_size(current_database());";
|
||||
var dbSizeResult = await cmd.ExecuteScalarAsync();
|
||||
dbSize = dbSizeResult != DBNull.Value ? Convert.ToInt64(dbSizeResult) : 0;
|
||||
|
||||
// 2. Sum of all uploaded files (which live in MinIO, but we track size in MessageMedia)
|
||||
cmd.CommandText = "SELECT SUM(\"Size\") FROM chats.\"MessageMedia\";";
|
||||
var mediaSizeResult = await cmd.ExecuteScalarAsync();
|
||||
filesSize = mediaSizeResult != DBNull.Value ? Convert.ToInt64(mediaSizeResult) : 0;
|
||||
}
|
||||
catch { }
|
||||
|
||||
stat.TotalFilesSize = dbSize + filesSize; // Database size + MinIO files size
|
||||
|
||||
await db.SaveChangesAsync(stoppingToken);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Background service swallows errors to not crash the app
|
||||
}
|
||||
|
||||
await Task.Delay(TimeSpan.FromSeconds(10), stoppingToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Knot.Shared.Kernel.Configuration;
|
||||
|
||||
public interface ISettingsService
|
||||
{
|
||||
Task<SystemSettingsDto> GetSettingsAsync(CancellationToken cancellationToken = default);
|
||||
Task UpdateSettingsAsync(SystemSettingsDto settings, CancellationToken cancellationToken = default);
|
||||
SystemSettingsDto Current { get; }
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
namespace Knot.Shared.Kernel.Configuration;
|
||||
|
||||
public class SystemSettingsDto
|
||||
{
|
||||
// Storage Limits
|
||||
public double MaxStorageQuotaTb { get; set; } = 5.0;
|
||||
public int MaxGroupMembers { get; set; } = 500;
|
||||
public int MaxFileSizeMb { get; set; } = 100;
|
||||
|
||||
// Calls
|
||||
public bool EnableCalls { get; set; } = true;
|
||||
public string TurnHost { get; set; } = string.Empty;
|
||||
public int TurnPort { get; set; } = 3478;
|
||||
public string TurnUser { get; set; } = string.Empty;
|
||||
public string TurnSecret { get; set; } = string.Empty;
|
||||
|
||||
// Klipy
|
||||
public bool EnableKlipy { get; set; } = true;
|
||||
public string KlipyApiKey { get; set; } = string.Empty;
|
||||
public string KlipyCustomerId { get; set; } = string.Empty;
|
||||
|
||||
// Confederation
|
||||
public bool EnableConfederation { get; set; } = false;
|
||||
public List<string> AllowedDomains { get; set; } = new();
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Knot.Shared.Kernel.Services;
|
||||
|
||||
public interface IStatisticsService
|
||||
{
|
||||
Task<DashboardStatsDto> GetDashboardStatsAsync(CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
public class DashboardStatsDto
|
||||
{
|
||||
public long StorageUsedBytes { get; set; }
|
||||
public long StorageLimitBytes { get; set; }
|
||||
public long OnlineUsers { get; set; }
|
||||
public long OfflineUsers { get; set; }
|
||||
public long TotalUsers { get; set; }
|
||||
public List<ActivityStatDto> ActivityTimeline { get; set; } = new();
|
||||
public List<TopUserDto> TopUsersByMessages { get; set; } = new();
|
||||
public List<TopUserDto> TopUsersByStorage { get; set; } = new();
|
||||
}
|
||||
|
||||
public class ActivityStatDto
|
||||
{
|
||||
public DateTime Date { get; set; }
|
||||
public long Messages { get; set; }
|
||||
public long FilesSize { get; set; }
|
||||
}
|
||||
|
||||
public class TopUserDto
|
||||
{
|
||||
public Guid UserId { get; set; }
|
||||
public string Username { get; set; } = string.Empty;
|
||||
public long Value { get; set; } // messages count or bytes
|
||||
}
|
||||
Reference in New Issue
Block a user