Add whitelisting service + update the jwt valid audience
This commit is contained in:
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; }
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user