Add whitelisting service + update the jwt valid audience
This commit is contained in:
@@ -24,7 +24,10 @@ public class JwtUtils : IJwtUtils
|
|||||||
_secret = config.GetValue<string>("Jwt:Secret")
|
_secret = config.GetValue<string>("Jwt:Secret")
|
||||||
?? throw new InvalidOperationException("JWT secret is not configured.");
|
?? throw new InvalidOperationException("JWT secret is not configured.");
|
||||||
_issuer = config.GetValue<string>("Authentication:Schemes:Bearer:ValidIssuer");
|
_issuer = config.GetValue<string>("Authentication:Schemes:Bearer:ValidIssuer");
|
||||||
_audience = config.GetValue<string>("Authentication:Schemes:Bearer:ValidAudiences");
|
// Get first audience from array (tokens are generated with a single audience)
|
||||||
|
var audiences = config.GetSection("Authentication:Schemes:Bearer:ValidAudiences")
|
||||||
|
.Get<string[]>() ?? Array.Empty<string>();
|
||||||
|
_audience = audiences.Length > 0 ? audiences[0] : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public string GenerateJwtToken(User user, string publicAddress)
|
public string GenerateJwtToken(User user, string publicAddress)
|
||||||
@@ -37,7 +40,7 @@ public class JwtUtils : IJwtUtils
|
|||||||
Subject = new ClaimsIdentity(new[] { new Claim("address", publicAddress) }),
|
Subject = new ClaimsIdentity(new[] { new Claim("address", publicAddress) }),
|
||||||
Expires = DateTime.UtcNow.AddDays(15),
|
Expires = DateTime.UtcNow.AddDays(15),
|
||||||
Issuer = _issuer, // Include issuer if configured
|
Issuer = _issuer, // Include issuer if configured
|
||||||
Audience = _audience, // Include audience if configured
|
Audience = _audience, // Include audience if configured (uses first from array)
|
||||||
SigningCredentials = new SigningCredentials(
|
SigningCredentials = new SigningCredentials(
|
||||||
new SymmetricSecurityKey(key),
|
new SymmetricSecurityKey(key),
|
||||||
SecurityAlgorithms.HmacSha256Signature)
|
SecurityAlgorithms.HmacSha256Signature)
|
||||||
|
|||||||
231
src/Managing.Api/Controllers/WhitelistController.cs
Normal file
231
src/Managing.Api/Controllers/WhitelistController.cs
Normal file
@@ -0,0 +1,231 @@
|
|||||||
|
using Managing.Api.Models;
|
||||||
|
using Managing.Application.Abstractions.Services;
|
||||||
|
using Managing.Application.Shared;
|
||||||
|
using Managing.Domain.Whitelist;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
|
namespace Managing.Api.Controllers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Controller for managing whitelist accounts (Privy wallets).
|
||||||
|
/// Requires admin authorization for all operations.
|
||||||
|
/// </summary>
|
||||||
|
[ApiController]
|
||||||
|
[Authorize]
|
||||||
|
[Route("api/[controller]")]
|
||||||
|
public class WhitelistController : BaseController
|
||||||
|
{
|
||||||
|
private readonly IWhitelistService _whitelistService;
|
||||||
|
private readonly IAdminConfigurationService _adminService;
|
||||||
|
private readonly ILogger<WhitelistController> _logger;
|
||||||
|
|
||||||
|
public WhitelistController(
|
||||||
|
IUserService userService,
|
||||||
|
IWhitelistService whitelistService,
|
||||||
|
IAdminConfigurationService adminService,
|
||||||
|
ILogger<WhitelistController> logger)
|
||||||
|
: base(userService)
|
||||||
|
{
|
||||||
|
_whitelistService = whitelistService;
|
||||||
|
_adminService = adminService;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets a paginated list of whitelist accounts with optional search filters.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="pageNumber">Page number (1-based)</param>
|
||||||
|
/// <param name="pageSize">Page size (1-100)</param>
|
||||||
|
/// <param name="searchExternalEthereumAccount">Optional search term for external Ethereum account</param>
|
||||||
|
/// <param name="searchTwitterAccount">Optional search term for Twitter account</param>
|
||||||
|
/// <returns>Paginated list of whitelist accounts</returns>
|
||||||
|
[HttpGet]
|
||||||
|
public async Task<ActionResult<PaginatedWhitelistAccountsResponse>> GetWhitelistAccounts(
|
||||||
|
[FromQuery] int pageNumber = 1,
|
||||||
|
[FromQuery] int pageSize = 20,
|
||||||
|
[FromQuery] string? searchExternalEthereumAccount = null,
|
||||||
|
[FromQuery] string? searchTwitterAccount = null)
|
||||||
|
{
|
||||||
|
var user = await GetUser();
|
||||||
|
|
||||||
|
if (!_adminService.IsUserAdmin(user.Name))
|
||||||
|
{
|
||||||
|
_logger.LogWarning("User {UserName} attempted to list whitelist accounts without admin privileges", user.Name);
|
||||||
|
return Forbid("Only admin users can list whitelist accounts");
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var (accounts, totalCount) = await _whitelistService.GetPaginatedAsync(
|
||||||
|
pageNumber,
|
||||||
|
pageSize,
|
||||||
|
searchExternalEthereumAccount,
|
||||||
|
searchTwitterAccount);
|
||||||
|
|
||||||
|
return Ok(new PaginatedWhitelistAccountsResponse
|
||||||
|
{
|
||||||
|
Accounts = accounts,
|
||||||
|
TotalCount = totalCount,
|
||||||
|
PageNumber = pageNumber,
|
||||||
|
PageSize = pageSize,
|
||||||
|
TotalPages = (int)Math.Ceiling(totalCount / (double)pageSize)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error listing whitelist accounts for admin user {UserName}", user.Name);
|
||||||
|
return StatusCode(500, new { error = ex.Message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sets the IsWhitelisted status to true for a selected account.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="id">The ID of the account to whitelist</param>
|
||||||
|
/// <returns>The updated account</returns>
|
||||||
|
[HttpPost("{id}/whitelist")]
|
||||||
|
public async Task<ActionResult<WhitelistAccount>> SetWhitelisted(int id)
|
||||||
|
{
|
||||||
|
var user = await GetUser();
|
||||||
|
|
||||||
|
if (!_adminService.IsUserAdmin(user.Name))
|
||||||
|
{
|
||||||
|
_logger.LogWarning("User {UserName} attempted to set whitelisted status without admin privileges", user.Name);
|
||||||
|
return Forbid("Only admin users can set whitelisted status");
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var updatedCount = await _whitelistService.SetIsWhitelistedAsync(new[] { id }, true);
|
||||||
|
|
||||||
|
if (updatedCount == 0)
|
||||||
|
{
|
||||||
|
return NotFound(new { error = $"Whitelist account with Id {id} not found" });
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.LogInformation("Admin user {UserName} set IsWhitelisted=true for account {Id}",
|
||||||
|
user.Name, id);
|
||||||
|
|
||||||
|
// Fetch the updated account to return it
|
||||||
|
var updatedAccount = await _whitelistService.GetByIdAsync(id);
|
||||||
|
|
||||||
|
if (updatedAccount == null)
|
||||||
|
{
|
||||||
|
return NotFound(new { error = $"Whitelist account with Id {id} not found after update" });
|
||||||
|
}
|
||||||
|
|
||||||
|
return Ok(updatedAccount);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error setting whitelisted status for admin user {UserName}", user.Name);
|
||||||
|
return StatusCode(500, new { error = ex.Message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Receives Privy webhook for wallet creation events.
|
||||||
|
/// This endpoint should be called by Privy when a wallet is created.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="webhook">The Privy webhook payload</param>
|
||||||
|
/// <returns>The created or updated whitelist account</returns>
|
||||||
|
[HttpPost("webhook")]
|
||||||
|
[AllowAnonymous] // Webhook endpoint - consider adding webhook signature verification
|
||||||
|
public async Task<ActionResult<WhitelistAccount>> ProcessPrivyWebhook(
|
||||||
|
[FromBody] PrivyWebhookDto webhook)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Validate webhook type
|
||||||
|
if (webhook.Type != "user.wallet_created")
|
||||||
|
{
|
||||||
|
_logger.LogWarning("Received webhook with unexpected type: {Type}", webhook.Type);
|
||||||
|
return BadRequest(new { error = $"Unexpected webhook type: {webhook.Type}" });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate required fields
|
||||||
|
if (string.IsNullOrWhiteSpace(webhook.User?.Id))
|
||||||
|
{
|
||||||
|
return BadRequest(new { error = "User ID is required" });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(webhook.Wallet?.Address))
|
||||||
|
{
|
||||||
|
return BadRequest(new { error = "Wallet address is required" });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate linked accounts
|
||||||
|
if (webhook.User.LinkedAccounts == null || !webhook.User.LinkedAccounts.Any())
|
||||||
|
{
|
||||||
|
return BadRequest(new { error = "At least one linked account is required" });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (webhook.User.LinkedAccounts.Count > 1)
|
||||||
|
{
|
||||||
|
_logger.LogWarning("Webhook received with {Count} linked accounts, expected 1. Using first account.",
|
||||||
|
webhook.User.LinkedAccounts.Count);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the first linked account (as per requirement)
|
||||||
|
var linkedAccount = webhook.User.LinkedAccounts.First();
|
||||||
|
|
||||||
|
// Determine ExternalEthereumAccount or TwitterAccount based on linked account type
|
||||||
|
string? externalEthereumAccount = null;
|
||||||
|
string? twitterAccount = null;
|
||||||
|
|
||||||
|
var accountType = linkedAccount.Type?.ToLowerInvariant();
|
||||||
|
switch (accountType)
|
||||||
|
{
|
||||||
|
case "ethereum":
|
||||||
|
case "wallet":
|
||||||
|
case "evm":
|
||||||
|
externalEthereumAccount = linkedAccount.Address;
|
||||||
|
break;
|
||||||
|
case "twitter":
|
||||||
|
case "twitter_oauth":
|
||||||
|
twitterAccount = linkedAccount.Address;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
_logger.LogWarning("Unknown linked account type: {Type}, address: {Address}",
|
||||||
|
linkedAccount.Type, linkedAccount.Address);
|
||||||
|
// Could be email or other types - we'll just log it
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process the webhook
|
||||||
|
var whitelistAccount = await _whitelistService.ProcessPrivyWebhookAsync(
|
||||||
|
webhook.User.Id,
|
||||||
|
webhook.User.CreatedAt,
|
||||||
|
webhook.Wallet.Address,
|
||||||
|
externalEthereumAccount,
|
||||||
|
twitterAccount);
|
||||||
|
|
||||||
|
_logger.LogInformation("Privy webhook processed successfully - AccountId: {Id}, PrivyId: {PrivyId}",
|
||||||
|
whitelistAccount.Id, whitelistAccount.PrivyId);
|
||||||
|
|
||||||
|
return Ok(whitelistAccount);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error processing Privy webhook");
|
||||||
|
SentrySdk.CaptureException(ex);
|
||||||
|
return StatusCode(500, new { error = ex.Message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Response model for paginated whitelist accounts.
|
||||||
|
/// </summary>
|
||||||
|
public class PaginatedWhitelistAccountsResponse
|
||||||
|
{
|
||||||
|
public IEnumerable<WhitelistAccount> Accounts { get; set; } = new List<WhitelistAccount>();
|
||||||
|
public int TotalCount { get; set; }
|
||||||
|
public int PageNumber { get; set; }
|
||||||
|
public int PageSize { get; set; }
|
||||||
|
public int TotalPages { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
79
src/Managing.Api/Models/PrivyWebhookDto.cs
Normal file
79
src/Managing.Api/Models/PrivyWebhookDto.cs
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace Managing.Api.Models;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Privy webhook payload for wallet creation events.
|
||||||
|
/// </summary>
|
||||||
|
public class PrivyWebhookDto
|
||||||
|
{
|
||||||
|
[JsonPropertyName("type")]
|
||||||
|
public string Type { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
[JsonPropertyName("user")]
|
||||||
|
public PrivyUserDto User { get; set; } = new();
|
||||||
|
|
||||||
|
[JsonPropertyName("wallet")]
|
||||||
|
public PrivyWalletDto Wallet { get; set; } = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Privy user information from webhook.
|
||||||
|
/// </summary>
|
||||||
|
public class PrivyUserDto
|
||||||
|
{
|
||||||
|
[JsonPropertyName("created_at")]
|
||||||
|
public long CreatedAt { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("has_accepted_terms")]
|
||||||
|
public bool HasAcceptedTerms { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("id")]
|
||||||
|
public string Id { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
[JsonPropertyName("is_guest")]
|
||||||
|
public bool IsGuest { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("linked_accounts")]
|
||||||
|
public List<PrivyLinkedAccountDto> LinkedAccounts { get; set; } = new();
|
||||||
|
|
||||||
|
[JsonPropertyName("mfa_methods")]
|
||||||
|
public List<object> MfaMethods { get; set; } = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Privy linked account information from webhook.
|
||||||
|
/// </summary>
|
||||||
|
public class PrivyLinkedAccountDto
|
||||||
|
{
|
||||||
|
[JsonPropertyName("address")]
|
||||||
|
public string Address { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
[JsonPropertyName("first_verified_at")]
|
||||||
|
public long? FirstVerifiedAt { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("latest_verified_at")]
|
||||||
|
public long? LatestVerifiedAt { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("type")]
|
||||||
|
public string Type { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
[JsonPropertyName("verified_at")]
|
||||||
|
public long? VerifiedAt { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Privy wallet information from webhook.
|
||||||
|
/// </summary>
|
||||||
|
public class PrivyWalletDto
|
||||||
|
{
|
||||||
|
[JsonPropertyName("type")]
|
||||||
|
public string Type { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
[JsonPropertyName("address")]
|
||||||
|
public string Address { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
[JsonPropertyName("chain_type")]
|
||||||
|
public string ChainType { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
@@ -14,6 +14,7 @@ using Managing.Infrastructure.Databases.InfluxDb.Models;
|
|||||||
using Managing.Infrastructure.Databases.PostgreSql;
|
using Managing.Infrastructure.Databases.PostgreSql;
|
||||||
using Managing.Infrastructure.Databases.PostgreSql.Configurations;
|
using Managing.Infrastructure.Databases.PostgreSql.Configurations;
|
||||||
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Diagnostics.HealthChecks;
|
using Microsoft.AspNetCore.Diagnostics.HealthChecks;
|
||||||
using Microsoft.AspNetCore.Http.Features;
|
using Microsoft.AspNetCore.Http.Features;
|
||||||
using Microsoft.AspNetCore.Server.Kestrel.Core;
|
using Microsoft.AspNetCore.Server.Kestrel.Core;
|
||||||
@@ -223,7 +224,8 @@ if (jwtSecret.Length < 32)
|
|||||||
|
|
||||||
// Get issuer and audience configuration
|
// Get issuer and audience configuration
|
||||||
var validIssuer = builder.Configuration["Authentication:Schemes:Bearer:ValidIssuer"];
|
var validIssuer = builder.Configuration["Authentication:Schemes:Bearer:ValidIssuer"];
|
||||||
var validAudience = builder.Configuration["Authentication:Schemes:Bearer:ValidAudiences"];
|
var validAudiences = builder.Configuration.GetSection("Authentication:Schemes:Bearer:ValidAudiences")
|
||||||
|
.Get<string[]>() ?? Array.Empty<string>();
|
||||||
|
|
||||||
// Determine if validation should be enabled (enable in production, allow override via config)
|
// Determine if validation should be enabled (enable in production, allow override via config)
|
||||||
var enableIssuerValidation = builder.Configuration.GetValue<bool>("Jwt:ValidateIssuer",
|
var enableIssuerValidation = builder.Configuration.GetValue<bool>("Jwt:ValidateIssuer",
|
||||||
@@ -247,11 +249,11 @@ builder.Services
|
|||||||
o.TokenValidationParameters = new TokenValidationParameters
|
o.TokenValidationParameters = new TokenValidationParameters
|
||||||
{
|
{
|
||||||
ValidIssuer = validIssuer,
|
ValidIssuer = validIssuer,
|
||||||
ValidAudience = validAudience,
|
ValidAudiences = validAudiences.Length > 0 ? validAudiences : null,
|
||||||
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtSecret)),
|
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtSecret)),
|
||||||
ValidateIssuerSigningKey = true,
|
ValidateIssuerSigningKey = true,
|
||||||
ValidateIssuer = enableIssuerValidation && !string.IsNullOrWhiteSpace(validIssuer),
|
ValidateIssuer = enableIssuerValidation && !string.IsNullOrWhiteSpace(validIssuer),
|
||||||
ValidateAudience = enableAudienceValidation && !string.IsNullOrWhiteSpace(validAudience),
|
ValidateAudience = enableAudienceValidation && validAudiences.Length > 0,
|
||||||
ValidateLifetime = true, // Explicitly validate token expiration
|
ValidateLifetime = true, // Explicitly validate token expiration
|
||||||
ClockSkew = clockSkew, // Configure clock skew tolerance
|
ClockSkew = clockSkew, // Configure clock skew tolerance
|
||||||
RequireExpirationTime = true, // Ensure tokens have expiration
|
RequireExpirationTime = true, // Ensure tokens have expiration
|
||||||
@@ -275,6 +277,19 @@ builder.Services
|
|||||||
var logger = context.HttpContext.RequestServices
|
var logger = context.HttpContext.RequestServices
|
||||||
.GetService<ILogger<Program>>();
|
.GetService<ILogger<Program>>();
|
||||||
|
|
||||||
|
// Check if the endpoint allows anonymous access
|
||||||
|
var endpoint = context.HttpContext.GetEndpoint();
|
||||||
|
var allowAnonymous = endpoint?.Metadata.GetMetadata<IAllowAnonymous>() != null;
|
||||||
|
|
||||||
|
// For anonymous endpoints with malformed tokens, skip authentication instead of failing
|
||||||
|
if (allowAnonymous && context.Exception is SecurityTokenMalformedException)
|
||||||
|
{
|
||||||
|
logger?.LogDebug("Skipping malformed token validation for anonymous endpoint: {Path}",
|
||||||
|
context.Request.Path);
|
||||||
|
context.NoResult(); // Skip authentication, don't fail
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
if (context.Exception is SecurityTokenExpiredException)
|
if (context.Exception is SecurityTokenExpiredException)
|
||||||
{
|
{
|
||||||
context.Response.Headers["Token-Expired"] = "true";
|
context.Response.Headers["Token-Expired"] = "true";
|
||||||
|
|||||||
@@ -44,14 +44,18 @@
|
|||||||
"Cors": {
|
"Cors": {
|
||||||
"AllowedOrigins": [
|
"AllowedOrigins": [
|
||||||
"https://app.kaigen.ai",
|
"https://app.kaigen.ai",
|
||||||
"https://api.kaigen.ai"
|
"https://api.kaigen.ai",
|
||||||
|
"https://web-ui.kai.managing.live"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"Authentication": {
|
"Authentication": {
|
||||||
"Schemes": {
|
"Schemes": {
|
||||||
"Bearer": {
|
"Bearer": {
|
||||||
"ValidIssuer": "https://api.kaigen.ai",
|
"ValidIssuer": "https://api.kaigen.ai",
|
||||||
"ValidAudiences": "https://app.kaigen.ai"
|
"ValidAudiences": [
|
||||||
|
"https://app.kaigen.ai",
|
||||||
|
"https://web-ui.kai.managing.live"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
using Managing.Domain.Whitelist;
|
||||||
|
|
||||||
|
namespace Managing.Application.Abstractions.Repositories;
|
||||||
|
|
||||||
|
public interface IWhitelistRepository
|
||||||
|
{
|
||||||
|
Task<(IEnumerable<WhitelistAccount> Accounts, int TotalCount)> GetPaginatedAsync(
|
||||||
|
int pageNumber,
|
||||||
|
int pageSize,
|
||||||
|
string? searchExternalEthereumAccount = null,
|
||||||
|
string? searchTwitterAccount = null);
|
||||||
|
|
||||||
|
Task<int> SetIsWhitelistedAsync(IEnumerable<int> accountIds, bool isWhitelisted);
|
||||||
|
Task<WhitelistAccount?> GetByIdAsync(int id);
|
||||||
|
Task<WhitelistAccount?> GetByPrivyIdAsync(string privyId);
|
||||||
|
Task<WhitelistAccount?> GetByEmbeddedWalletAsync(string embeddedWallet);
|
||||||
|
Task<WhitelistAccount> CreateOrUpdateAsync(WhitelistAccount whitelistAccount);
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
using Managing.Domain.Whitelist;
|
||||||
|
|
||||||
|
namespace Managing.Application.Abstractions.Services;
|
||||||
|
|
||||||
|
public interface IWhitelistService
|
||||||
|
{
|
||||||
|
Task<(IEnumerable<WhitelistAccount> Accounts, int TotalCount)> GetPaginatedAsync(
|
||||||
|
int pageNumber,
|
||||||
|
int pageSize,
|
||||||
|
string? searchExternalEthereumAccount = null,
|
||||||
|
string? searchTwitterAccount = null);
|
||||||
|
|
||||||
|
Task<int> SetIsWhitelistedAsync(IEnumerable<int> accountIds, bool isWhitelisted);
|
||||||
|
Task<WhitelistAccount?> GetByIdAsync(int id);
|
||||||
|
Task<WhitelistAccount> ProcessPrivyWebhookAsync(string privyUserId, long privyCreatedAt, string walletAddress, string? externalEthereumAccount, string? twitterAccount);
|
||||||
|
}
|
||||||
|
|
||||||
111
src/Managing.Application/Whitelist/WhitelistService.cs
Normal file
111
src/Managing.Application/Whitelist/WhitelistService.cs
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
using Managing.Application.Abstractions.Repositories;
|
||||||
|
using Managing.Application.Abstractions.Services;
|
||||||
|
using Managing.Domain.Whitelist;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace Managing.Application.Whitelist;
|
||||||
|
|
||||||
|
public class WhitelistService : IWhitelistService
|
||||||
|
{
|
||||||
|
private readonly IWhitelistRepository _whitelistRepository;
|
||||||
|
private readonly ILogger<WhitelistService> _logger;
|
||||||
|
|
||||||
|
public WhitelistService(
|
||||||
|
IWhitelistRepository whitelistRepository,
|
||||||
|
ILogger<WhitelistService> logger)
|
||||||
|
{
|
||||||
|
_whitelistRepository = whitelistRepository;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<(IEnumerable<WhitelistAccount> Accounts, int TotalCount)> GetPaginatedAsync(
|
||||||
|
int pageNumber,
|
||||||
|
int pageSize,
|
||||||
|
string? searchExternalEthereumAccount = null,
|
||||||
|
string? searchTwitterAccount = null)
|
||||||
|
{
|
||||||
|
if (pageNumber < 1)
|
||||||
|
{
|
||||||
|
throw new ArgumentException("Page number must be greater than 0", nameof(pageNumber));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pageSize < 1 || pageSize > 100)
|
||||||
|
{
|
||||||
|
throw new ArgumentException("Page size must be between 1 and 100", nameof(pageSize));
|
||||||
|
}
|
||||||
|
|
||||||
|
return await _whitelistRepository.GetPaginatedAsync(
|
||||||
|
pageNumber,
|
||||||
|
pageSize,
|
||||||
|
searchExternalEthereumAccount,
|
||||||
|
searchTwitterAccount);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<int> SetIsWhitelistedAsync(IEnumerable<int> accountIds, bool isWhitelisted)
|
||||||
|
{
|
||||||
|
var idsList = accountIds?.ToList() ?? new List<int>();
|
||||||
|
|
||||||
|
if (!idsList.Any())
|
||||||
|
{
|
||||||
|
throw new ArgumentException("At least one account ID must be provided", nameof(accountIds));
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.LogInformation("Setting IsWhitelisted to {IsWhitelisted} for {Count} account(s)",
|
||||||
|
isWhitelisted, idsList.Count);
|
||||||
|
|
||||||
|
var updatedCount = await _whitelistRepository.SetIsWhitelistedAsync(idsList, isWhitelisted);
|
||||||
|
|
||||||
|
_logger.LogInformation("Successfully updated {Count} account(s) to IsWhitelisted = {IsWhitelisted}",
|
||||||
|
updatedCount, isWhitelisted);
|
||||||
|
|
||||||
|
return updatedCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<WhitelistAccount?> GetByIdAsync(int id)
|
||||||
|
{
|
||||||
|
return await _whitelistRepository.GetByIdAsync(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<WhitelistAccount> ProcessPrivyWebhookAsync(
|
||||||
|
string privyUserId,
|
||||||
|
long privyCreatedAt,
|
||||||
|
string walletAddress,
|
||||||
|
string? externalEthereumAccount,
|
||||||
|
string? twitterAccount)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Processing Privy webhook - PrivyId: {PrivyId}, Wallet: {Wallet}, ExternalEthereum: {ExternalEthereum}, Twitter: {Twitter}",
|
||||||
|
privyUserId, walletAddress, externalEthereumAccount ?? "null", twitterAccount ?? "null");
|
||||||
|
|
||||||
|
// Convert Unix timestamp to DateTime
|
||||||
|
var privyCreationDate = DateTimeOffset.FromUnixTimeSeconds(privyCreatedAt).DateTime;
|
||||||
|
|
||||||
|
// Check if account already exists
|
||||||
|
var existing = await _whitelistRepository.GetByPrivyIdAsync(privyUserId) ??
|
||||||
|
await _whitelistRepository.GetByEmbeddedWalletAsync(walletAddress);
|
||||||
|
|
||||||
|
var whitelistAccount = new WhitelistAccount
|
||||||
|
{
|
||||||
|
PrivyId = privyUserId,
|
||||||
|
PrivyCreationDate = privyCreationDate,
|
||||||
|
EmbeddedWallet = walletAddress,
|
||||||
|
ExternalEthereumAccount = externalEthereumAccount,
|
||||||
|
TwitterAccount = twitterAccount,
|
||||||
|
IsWhitelisted = false, // Default to false, admin will set to true later
|
||||||
|
CreatedAt = existing?.CreatedAt ?? DateTime.UtcNow
|
||||||
|
};
|
||||||
|
|
||||||
|
// If existing, preserve the ID
|
||||||
|
if (existing != null)
|
||||||
|
{
|
||||||
|
whitelistAccount.Id = existing.Id;
|
||||||
|
}
|
||||||
|
|
||||||
|
var result = await _whitelistRepository.CreateOrUpdateAsync(whitelistAccount);
|
||||||
|
|
||||||
|
_logger.LogInformation("Privy webhook processed successfully - AccountId: {Id}, PrivyId: {PrivyId}",
|
||||||
|
result.Id, result.PrivyId);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -23,6 +23,7 @@ using Managing.Application.Trading;
|
|||||||
using Managing.Application.Trading.Commands;
|
using Managing.Application.Trading.Commands;
|
||||||
using Managing.Application.Trading.Handlers;
|
using Managing.Application.Trading.Handlers;
|
||||||
using Managing.Application.Users;
|
using Managing.Application.Users;
|
||||||
|
using Managing.Application.Whitelist;
|
||||||
using Managing.Application.Workers;
|
using Managing.Application.Workers;
|
||||||
using Managing.Domain.Trades;
|
using Managing.Domain.Trades;
|
||||||
using Managing.Infrastructure.Databases;
|
using Managing.Infrastructure.Databases;
|
||||||
@@ -408,6 +409,7 @@ public static class ApiBootstrap
|
|||||||
services.AddTransient<IWeb3ProxyService, Web3ProxyService>();
|
services.AddTransient<IWeb3ProxyService, Web3ProxyService>();
|
||||||
services.AddTransient<IWebhookService, WebhookService>();
|
services.AddTransient<IWebhookService, WebhookService>();
|
||||||
services.AddTransient<IKaigenService, KaigenService>();
|
services.AddTransient<IKaigenService, KaigenService>();
|
||||||
|
services.AddTransient<IWhitelistService, WhitelistService>();
|
||||||
|
|
||||||
services.AddSingleton<IMessengerService, MessengerService>();
|
services.AddSingleton<IMessengerService, MessengerService>();
|
||||||
services.AddSingleton<IDiscordService, DiscordService>();
|
services.AddSingleton<IDiscordService, DiscordService>();
|
||||||
@@ -449,6 +451,7 @@ public static class ApiBootstrap
|
|||||||
services.AddTransient<IBotRepository, PostgreSqlBotRepository>();
|
services.AddTransient<IBotRepository, PostgreSqlBotRepository>();
|
||||||
services.AddTransient<IWorkerRepository, PostgreSqlWorkerRepository>();
|
services.AddTransient<IWorkerRepository, PostgreSqlWorkerRepository>();
|
||||||
services.AddTransient<ISynthRepository, PostgreSqlSynthRepository>();
|
services.AddTransient<ISynthRepository, PostgreSqlSynthRepository>();
|
||||||
|
services.AddTransient<IWhitelistRepository, PostgreSqlWhitelistRepository>();
|
||||||
|
|
||||||
// InfluxDb Repositories - Use Singleton for proper connection management in Orleans grains
|
// InfluxDb Repositories - Use Singleton for proper connection management in Orleans grains
|
||||||
services.AddSingleton<IInfluxDbRepository, InfluxDbRepository>();
|
services.AddSingleton<IInfluxDbRepository, InfluxDbRepository>();
|
||||||
|
|||||||
15
src/Managing.Domain/Whitelist/WhitelistAccount.cs
Normal file
15
src/Managing.Domain/Whitelist/WhitelistAccount.cs
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
namespace Managing.Domain.Whitelist;
|
||||||
|
|
||||||
|
public class WhitelistAccount
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
public required string PrivyId { get; set; }
|
||||||
|
public DateTime PrivyCreationDate { get; set; }
|
||||||
|
public required string EmbeddedWallet { get; set; }
|
||||||
|
public string? ExternalEthereumAccount { get; set; }
|
||||||
|
public string? TwitterAccount { get; set; }
|
||||||
|
public bool IsWhitelisted { get; set; } = false;
|
||||||
|
public DateTime CreatedAt { get; set; }
|
||||||
|
public DateTime? UpdatedAt { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
1602
src/Managing.Infrastructure.Database/Migrations/20251107122139_AddWhitelistAccountsTable.Designer.cs
generated
Normal file
1602
src/Managing.Infrastructure.Database/Migrations/20251107122139_AddWhitelistAccountsTable.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1325,6 +1325,63 @@ namespace Managing.Infrastructure.Databases.Migrations
|
|||||||
b.ToTable("Users");
|
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 =>
|
modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.WorkerEntity", b =>
|
||||||
{
|
{
|
||||||
b.Property<int>("Id")
|
b.Property<int>("Id")
|
||||||
|
|||||||
@@ -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; }
|
||||||
|
}
|
||||||
|
|
||||||
@@ -55,6 +55,7 @@ public class ManagingDbContext : DbContext
|
|||||||
|
|
||||||
public DbSet<SynthMinersLeaderboardEntity> SynthMinersLeaderboards { get; set; }
|
public DbSet<SynthMinersLeaderboardEntity> SynthMinersLeaderboards { get; set; }
|
||||||
public DbSet<SynthPredictionEntity> SynthPredictions { get; set; }
|
public DbSet<SynthPredictionEntity> SynthPredictions { get; set; }
|
||||||
|
public DbSet<WhitelistAccountEntity> WhitelistAccounts { get; set; }
|
||||||
|
|
||||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||||
{
|
{
|
||||||
@@ -573,6 +574,29 @@ public class ManagingDbContext : DbContext
|
|||||||
entity.HasIndex(e => e.CacheKey).IsUnique();
|
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
|
// Configure AgentSummary entity
|
||||||
modelBuilder.Entity<AgentSummaryEntity>(entity =>
|
modelBuilder.Entity<AgentSummaryEntity>(entity =>
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ using Managing.Domain.Statistics;
|
|||||||
using Managing.Domain.Strategies;
|
using Managing.Domain.Strategies;
|
||||||
using Managing.Domain.Trades;
|
using Managing.Domain.Trades;
|
||||||
using Managing.Domain.Users;
|
using Managing.Domain.Users;
|
||||||
|
using Managing.Domain.Whitelist;
|
||||||
using Managing.Domain.Workers;
|
using Managing.Domain.Workers;
|
||||||
using Managing.Infrastructure.Databases.PostgreSql.Entities;
|
using Managing.Infrastructure.Databases.PostgreSql.Entities;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
@@ -992,6 +993,51 @@ public static class PostgreSqlMappers
|
|||||||
|
|
||||||
#endregion
|
#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)
|
private static int? ExtractBundleIndex(string name)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(name)) return null;
|
if (string.IsNullOrWhiteSpace(name)) return null;
|
||||||
|
|||||||
@@ -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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Reference in New Issue
Block a user