Add whitelisting service + update the jwt valid audience

This commit is contained in:
2025-11-07 19:38:33 +07:00
parent 5578d272fa
commit 21110cd771
17 changed files with 2575 additions and 7 deletions

View File

@@ -0,0 +1,70 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace Managing.Infrastructure.Databases.Migrations
{
/// <inheritdoc />
public partial class AddWhitelistAccountsTable : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "WhitelistAccounts",
columns: table => new
{
Id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
PrivyId = table.Column<string>(type: "character varying(255)", maxLength: 255, nullable: false),
PrivyCreationDate = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
EmbeddedWallet = table.Column<string>(type: "character varying(42)", maxLength: 42, nullable: false),
ExternalEthereumAccount = table.Column<string>(type: "character varying(42)", maxLength: 42, nullable: true),
TwitterAccount = table.Column<string>(type: "character varying(255)", maxLength: 255, nullable: true),
IsWhitelisted = table.Column<bool>(type: "boolean", nullable: false, defaultValue: false),
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
UpdatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_WhitelistAccounts", x => x.Id);
});
migrationBuilder.CreateIndex(
name: "IX_WhitelistAccounts_CreatedAt",
table: "WhitelistAccounts",
column: "CreatedAt");
migrationBuilder.CreateIndex(
name: "IX_WhitelistAccounts_EmbeddedWallet",
table: "WhitelistAccounts",
column: "EmbeddedWallet",
unique: true);
migrationBuilder.CreateIndex(
name: "IX_WhitelistAccounts_ExternalEthereumAccount",
table: "WhitelistAccounts",
column: "ExternalEthereumAccount");
migrationBuilder.CreateIndex(
name: "IX_WhitelistAccounts_PrivyId",
table: "WhitelistAccounts",
column: "PrivyId",
unique: true);
migrationBuilder.CreateIndex(
name: "IX_WhitelistAccounts_TwitterAccount",
table: "WhitelistAccounts",
column: "TwitterAccount");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "WhitelistAccounts");
}
}
}

View File

@@ -1325,6 +1325,63 @@ namespace Managing.Infrastructure.Databases.Migrations
b.ToTable("Users");
});
modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.WhitelistAccountEntity", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("EmbeddedWallet")
.IsRequired()
.HasMaxLength(42)
.HasColumnType("character varying(42)");
b.Property<string>("ExternalEthereumAccount")
.HasMaxLength(42)
.HasColumnType("character varying(42)");
b.Property<bool>("IsWhitelisted")
.ValueGeneratedOnAdd()
.HasColumnType("boolean")
.HasDefaultValue(false);
b.Property<DateTime>("PrivyCreationDate")
.HasColumnType("timestamp with time zone");
b.Property<string>("PrivyId")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("character varying(255)");
b.Property<string>("TwitterAccount")
.HasMaxLength(255)
.HasColumnType("character varying(255)");
b.Property<DateTime?>("UpdatedAt")
.HasColumnType("timestamp with time zone");
b.HasKey("Id");
b.HasIndex("CreatedAt");
b.HasIndex("EmbeddedWallet")
.IsUnique();
b.HasIndex("ExternalEthereumAccount");
b.HasIndex("PrivyId")
.IsUnique();
b.HasIndex("TwitterAccount");
b.ToTable("WhitelistAccounts");
});
modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.WorkerEntity", b =>
{
b.Property<int>("Id")

View File

@@ -0,0 +1,35 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace Managing.Infrastructure.Databases.PostgreSql.Entities;
[Table("WhitelistAccounts")]
public class WhitelistAccountEntity
{
[Key] public int Id { get; set; }
[Required]
[MaxLength(255)]
public required string PrivyId { get; set; }
[Required]
public DateTime PrivyCreationDate { get; set; }
[Required]
[MaxLength(42)]
public required string EmbeddedWallet { get; set; }
[MaxLength(42)]
public string? ExternalEthereumAccount { get; set; }
[MaxLength(255)]
public string? TwitterAccount { get; set; }
[Required]
public bool IsWhitelisted { get; set; } = false;
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
public DateTime? UpdatedAt { get; set; }
}

View File

@@ -55,6 +55,7 @@ public class ManagingDbContext : DbContext
public DbSet<SynthMinersLeaderboardEntity> SynthMinersLeaderboards { get; set; }
public DbSet<SynthPredictionEntity> SynthPredictions { get; set; }
public DbSet<WhitelistAccountEntity> WhitelistAccounts { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
@@ -573,6 +574,29 @@ public class ManagingDbContext : DbContext
entity.HasIndex(e => e.CacheKey).IsUnique();
});
// Configure WhitelistAccount entity
modelBuilder.Entity<WhitelistAccountEntity>(entity =>
{
entity.HasKey(e => e.Id);
entity.Property(e => e.PrivyId).IsRequired().HasMaxLength(255);
entity.Property(e => e.PrivyCreationDate).IsRequired();
entity.Property(e => e.EmbeddedWallet).IsRequired().HasMaxLength(42);
entity.Property(e => e.ExternalEthereumAccount).HasMaxLength(42);
entity.Property(e => e.TwitterAccount).HasMaxLength(255);
entity.Property(e => e.IsWhitelisted)
.IsRequired()
.HasDefaultValue(false);
entity.Property(e => e.CreatedAt).IsRequired();
entity.Property(e => e.UpdatedAt);
// Create indexes for search performance
entity.HasIndex(e => e.PrivyId).IsUnique();
entity.HasIndex(e => e.EmbeddedWallet).IsUnique();
entity.HasIndex(e => e.ExternalEthereumAccount);
entity.HasIndex(e => e.TwitterAccount);
entity.HasIndex(e => e.CreatedAt);
});
// Configure AgentSummary entity
modelBuilder.Entity<AgentSummaryEntity>(entity =>
{

View File

@@ -10,6 +10,7 @@ using Managing.Domain.Statistics;
using Managing.Domain.Strategies;
using Managing.Domain.Trades;
using Managing.Domain.Users;
using Managing.Domain.Whitelist;
using Managing.Domain.Workers;
using Managing.Infrastructure.Databases.PostgreSql.Entities;
using Newtonsoft.Json;
@@ -992,6 +993,51 @@ public static class PostgreSqlMappers
#endregion
#region WhitelistAccount Mappings
public static WhitelistAccount Map(WhitelistAccountEntity entity)
{
if (entity == null) return null;
return new WhitelistAccount
{
Id = entity.Id,
PrivyId = entity.PrivyId,
PrivyCreationDate = entity.PrivyCreationDate,
EmbeddedWallet = entity.EmbeddedWallet,
ExternalEthereumAccount = entity.ExternalEthereumAccount,
TwitterAccount = entity.TwitterAccount,
IsWhitelisted = entity.IsWhitelisted,
CreatedAt = entity.CreatedAt,
UpdatedAt = entity.UpdatedAt
};
}
public static WhitelistAccountEntity Map(WhitelistAccount whitelistAccount)
{
if (whitelistAccount == null) return null;
return new WhitelistAccountEntity
{
Id = whitelistAccount.Id,
PrivyId = whitelistAccount.PrivyId,
PrivyCreationDate = whitelistAccount.PrivyCreationDate,
EmbeddedWallet = whitelistAccount.EmbeddedWallet,
ExternalEthereumAccount = whitelistAccount.ExternalEthereumAccount,
TwitterAccount = whitelistAccount.TwitterAccount,
IsWhitelisted = whitelistAccount.IsWhitelisted,
CreatedAt = whitelistAccount.CreatedAt,
UpdatedAt = whitelistAccount.UpdatedAt
};
}
public static IEnumerable<WhitelistAccount> Map(IEnumerable<WhitelistAccountEntity> entities)
{
return entities?.Select(Map) ?? Enumerable.Empty<WhitelistAccount>();
}
#endregion
private static int? ExtractBundleIndex(string name)
{
if (string.IsNullOrWhiteSpace(name)) return null;

View File

@@ -0,0 +1,237 @@
using Managing.Application.Abstractions.Repositories;
using Managing.Domain.Whitelist;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
namespace Managing.Infrastructure.Databases.PostgreSql;
public class PostgreSqlWhitelistRepository : BaseRepositoryWithLogging, IWhitelistRepository
{
public PostgreSqlWhitelistRepository(
ManagingDbContext context,
ILogger<SqlQueryLogger> logger,
SentrySqlMonitoringService sentryMonitoringService)
: base(context, logger, sentryMonitoringService)
{
}
public async Task<(IEnumerable<WhitelistAccount> Accounts, int TotalCount)> GetPaginatedAsync(
int pageNumber,
int pageSize,
string? searchExternalEthereumAccount = null,
string? searchTwitterAccount = null)
{
return await ExecuteWithLoggingAsync(async () =>
{
try
{
await PostgreSqlConnectionHelper.EnsureConnectionOpenAsync(_context);
var query = _context.WhitelistAccounts.AsNoTracking().AsQueryable();
// Apply search filters
if (!string.IsNullOrWhiteSpace(searchExternalEthereumAccount))
{
var searchTerm = searchExternalEthereumAccount.Trim().ToLower();
query = query.Where(e =>
e.ExternalEthereumAccount != null &&
e.ExternalEthereumAccount.ToLower().Contains(searchTerm));
}
if (!string.IsNullOrWhiteSpace(searchTwitterAccount))
{
var searchTerm = searchTwitterAccount.Trim().ToLower();
query = query.Where(e =>
e.TwitterAccount != null &&
e.TwitterAccount.ToLower().Contains(searchTerm));
}
// Get total count before pagination
var totalCount = await query.CountAsync().ConfigureAwait(false);
// Apply pagination and ordering
var entities = await query
.OrderByDescending(e => e.CreatedAt)
.Skip((pageNumber - 1) * pageSize)
.Take(pageSize)
.ToListAsync()
.ConfigureAwait(false);
var accounts = entities.Select(PostgreSqlMappers.Map);
return (accounts, totalCount);
}
finally
{
await PostgreSqlConnectionHelper.SafeCloseConnectionAsync(_context);
}
}, nameof(GetPaginatedAsync),
("pageNumber", pageNumber.ToString()),
("pageSize", pageSize.ToString()));
}
public async Task<int> SetIsWhitelistedAsync(IEnumerable<int> accountIds, bool isWhitelisted)
{
return await ExecuteWithLoggingAsync(async () =>
{
try
{
await PostgreSqlConnectionHelper.EnsureConnectionOpenAsync(_context);
var idsList = accountIds.ToList();
if (!idsList.Any())
{
return 0;
}
var entities = await _context.WhitelistAccounts
.AsTracking()
.Where(e => idsList.Contains(e.Id))
.ToListAsync()
.ConfigureAwait(false);
foreach (var entity in entities)
{
entity.IsWhitelisted = isWhitelisted;
entity.UpdatedAt = DateTime.UtcNow;
}
var updatedCount = await _context.SaveChangesAsync().ConfigureAwait(false);
return updatedCount;
}
finally
{
await PostgreSqlConnectionHelper.SafeCloseConnectionAsync(_context);
}
}, nameof(SetIsWhitelistedAsync),
("accountIds", string.Join(",", accountIds)),
("isWhitelisted", isWhitelisted.ToString()));
}
public async Task<WhitelistAccount?> GetByIdAsync(int id)
{
return await ExecuteWithLoggingAsync(async () =>
{
try
{
await PostgreSqlConnectionHelper.EnsureConnectionOpenAsync(_context);
var entity = await _context.WhitelistAccounts
.AsNoTracking()
.FirstOrDefaultAsync(e => e.Id == id)
.ConfigureAwait(false);
return entity != null ? PostgreSqlMappers.Map(entity) : null;
}
finally
{
await PostgreSqlConnectionHelper.SafeCloseConnectionAsync(_context);
}
}, nameof(GetByIdAsync), ("id", id.ToString()));
}
public async Task<WhitelistAccount?> GetByPrivyIdAsync(string privyId)
{
return await ExecuteWithLoggingAsync(async () =>
{
try
{
await PostgreSqlConnectionHelper.EnsureConnectionOpenAsync(_context);
var entity = await _context.WhitelistAccounts
.AsNoTracking()
.FirstOrDefaultAsync(e => e.PrivyId == privyId)
.ConfigureAwait(false);
return entity != null ? PostgreSqlMappers.Map(entity) : null;
}
finally
{
await PostgreSqlConnectionHelper.SafeCloseConnectionAsync(_context);
}
}, nameof(GetByPrivyIdAsync), ("privyId", privyId));
}
public async Task<WhitelistAccount?> GetByEmbeddedWalletAsync(string embeddedWallet)
{
return await ExecuteWithLoggingAsync(async () =>
{
try
{
await PostgreSqlConnectionHelper.EnsureConnectionOpenAsync(_context);
var entity = await _context.WhitelistAccounts
.AsNoTracking()
.FirstOrDefaultAsync(e => e.EmbeddedWallet.ToLower() == embeddedWallet.ToLower())
.ConfigureAwait(false);
return entity != null ? PostgreSqlMappers.Map(entity) : null;
}
finally
{
await PostgreSqlConnectionHelper.SafeCloseConnectionAsync(_context);
}
}, nameof(GetByEmbeddedWalletAsync), ("embeddedWallet", embeddedWallet));
}
public async Task<WhitelistAccount> CreateOrUpdateAsync(WhitelistAccount whitelistAccount)
{
return await ExecuteWithLoggingAsync(async () =>
{
try
{
await PostgreSqlConnectionHelper.EnsureConnectionOpenAsync(_context);
// Check if account exists by PrivyId or EmbeddedWallet
var existing = await _context.WhitelistAccounts
.AsTracking()
.FirstOrDefaultAsync(e =>
e.PrivyId == whitelistAccount.PrivyId ||
e.EmbeddedWallet.ToLower() == whitelistAccount.EmbeddedWallet.ToLower())
.ConfigureAwait(false);
if (existing != null)
{
// Update existing account
existing.PrivyId = whitelistAccount.PrivyId;
existing.PrivyCreationDate = whitelistAccount.PrivyCreationDate;
existing.EmbeddedWallet = whitelistAccount.EmbeddedWallet;
// Only update if new value is provided
if (!string.IsNullOrWhiteSpace(whitelistAccount.ExternalEthereumAccount))
{
existing.ExternalEthereumAccount = whitelistAccount.ExternalEthereumAccount;
}
if (!string.IsNullOrWhiteSpace(whitelistAccount.TwitterAccount))
{
existing.TwitterAccount = whitelistAccount.TwitterAccount;
}
existing.UpdatedAt = DateTime.UtcNow;
await _context.SaveChangesAsync().ConfigureAwait(false);
return PostgreSqlMappers.Map(existing);
}
else
{
// Create new account
var entity = PostgreSqlMappers.Map(whitelistAccount);
entity.CreatedAt = DateTime.UtcNow;
_context.WhitelistAccounts.Add(entity);
await _context.SaveChangesAsync().ConfigureAwait(false);
return PostgreSqlMappers.Map(entity);
}
}
finally
{
await PostgreSqlConnectionHelper.SafeCloseConnectionAsync(_context);
}
}, nameof(CreateOrUpdateAsync), ("privyId", whitelistAccount.PrivyId));
}
}