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")
|
||||
?? throw new InvalidOperationException("JWT secret is not configured.");
|
||||
_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)
|
||||
@@ -37,7 +40,7 @@ public class JwtUtils : IJwtUtils
|
||||
Subject = new ClaimsIdentity(new[] { new Claim("address", publicAddress) }),
|
||||
Expires = DateTime.UtcNow.AddDays(15),
|
||||
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(
|
||||
new SymmetricSecurityKey(key),
|
||||
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.Configurations;
|
||||
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Diagnostics.HealthChecks;
|
||||
using Microsoft.AspNetCore.Http.Features;
|
||||
using Microsoft.AspNetCore.Server.Kestrel.Core;
|
||||
@@ -223,7 +224,8 @@ if (jwtSecret.Length < 32)
|
||||
|
||||
// Get issuer and audience configuration
|
||||
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)
|
||||
var enableIssuerValidation = builder.Configuration.GetValue<bool>("Jwt:ValidateIssuer",
|
||||
@@ -247,11 +249,11 @@ builder.Services
|
||||
o.TokenValidationParameters = new TokenValidationParameters
|
||||
{
|
||||
ValidIssuer = validIssuer,
|
||||
ValidAudience = validAudience,
|
||||
ValidAudiences = validAudiences.Length > 0 ? validAudiences : null,
|
||||
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtSecret)),
|
||||
ValidateIssuerSigningKey = true,
|
||||
ValidateIssuer = enableIssuerValidation && !string.IsNullOrWhiteSpace(validIssuer),
|
||||
ValidateAudience = enableAudienceValidation && !string.IsNullOrWhiteSpace(validAudience),
|
||||
ValidateAudience = enableAudienceValidation && validAudiences.Length > 0,
|
||||
ValidateLifetime = true, // Explicitly validate token expiration
|
||||
ClockSkew = clockSkew, // Configure clock skew tolerance
|
||||
RequireExpirationTime = true, // Ensure tokens have expiration
|
||||
@@ -275,6 +277,19 @@ builder.Services
|
||||
var logger = context.HttpContext.RequestServices
|
||||
.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)
|
||||
{
|
||||
context.Response.Headers["Token-Expired"] = "true";
|
||||
|
||||
@@ -44,14 +44,18 @@
|
||||
"Cors": {
|
||||
"AllowedOrigins": [
|
||||
"https://app.kaigen.ai",
|
||||
"https://api.kaigen.ai"
|
||||
"https://api.kaigen.ai",
|
||||
"https://web-ui.kai.managing.live"
|
||||
]
|
||||
},
|
||||
"Authentication": {
|
||||
"Schemes": {
|
||||
"Bearer": {
|
||||
"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.Handlers;
|
||||
using Managing.Application.Users;
|
||||
using Managing.Application.Whitelist;
|
||||
using Managing.Application.Workers;
|
||||
using Managing.Domain.Trades;
|
||||
using Managing.Infrastructure.Databases;
|
||||
@@ -408,6 +409,7 @@ public static class ApiBootstrap
|
||||
services.AddTransient<IWeb3ProxyService, Web3ProxyService>();
|
||||
services.AddTransient<IWebhookService, WebhookService>();
|
||||
services.AddTransient<IKaigenService, KaigenService>();
|
||||
services.AddTransient<IWhitelistService, WhitelistService>();
|
||||
|
||||
services.AddSingleton<IMessengerService, MessengerService>();
|
||||
services.AddSingleton<IDiscordService, DiscordService>();
|
||||
@@ -449,6 +451,7 @@ public static class ApiBootstrap
|
||||
services.AddTransient<IBotRepository, PostgreSqlBotRepository>();
|
||||
services.AddTransient<IWorkerRepository, PostgreSqlWorkerRepository>();
|
||||
services.AddTransient<ISynthRepository, PostgreSqlSynthRepository>();
|
||||
services.AddTransient<IWhitelistRepository, PostgreSqlWhitelistRepository>();
|
||||
|
||||
// InfluxDb Repositories - Use Singleton for proper connection management in Orleans grains
|
||||
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");
|
||||
});
|
||||
|
||||
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")
|
||||
|
||||
@@ -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<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 =>
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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