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,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; }
}