Add whitelisting and admin
This commit is contained in:
@@ -34,6 +34,7 @@
|
|||||||
"RequestsChannelId": 1018589494968078356,
|
"RequestsChannelId": 1018589494968078356,
|
||||||
"ButtonExpirationMinutes": 2
|
"ButtonExpirationMinutes": 2
|
||||||
},
|
},
|
||||||
|
"EnableSwagger": true,
|
||||||
"RunOrleansGrains": true,
|
"RunOrleansGrains": true,
|
||||||
"AllowedHosts": "*",
|
"AllowedHosts": "*",
|
||||||
"KAIGEN_SECRET_KEY": "KaigenXCowchain",
|
"KAIGEN_SECRET_KEY": "KaigenXCowchain",
|
||||||
|
|||||||
@@ -13,5 +13,6 @@ public interface IWhitelistService
|
|||||||
Task<int> SetIsWhitelistedAsync(IEnumerable<int> accountIds, bool isWhitelisted);
|
Task<int> SetIsWhitelistedAsync(IEnumerable<int> accountIds, bool isWhitelisted);
|
||||||
Task<WhitelistAccount?> GetByIdAsync(int id);
|
Task<WhitelistAccount?> GetByIdAsync(int id);
|
||||||
Task<WhitelistAccount> ProcessPrivyWebhookAsync(string privyUserId, long privyCreatedAt, string walletAddress, string? externalEthereumAccount, string? twitterAccount);
|
Task<WhitelistAccount> ProcessPrivyWebhookAsync(string privyUserId, long privyCreatedAt, string walletAddress, string? externalEthereumAccount, string? twitterAccount);
|
||||||
|
Task<bool> IsEmbeddedWalletWhitelistedAsync(string embeddedWallet);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
|
using Managing.Application.Abstractions.Repositories;
|
||||||
using Microsoft.Extensions.Configuration;
|
using Microsoft.Extensions.Configuration;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace Managing.Application.Shared;
|
namespace Managing.Application.Shared;
|
||||||
@@ -13,11 +15,16 @@ public class AdminConfigurationService : IAdminConfigurationService
|
|||||||
{
|
{
|
||||||
private readonly IConfiguration _configuration;
|
private readonly IConfiguration _configuration;
|
||||||
private readonly ILogger<AdminConfigurationService> _logger;
|
private readonly ILogger<AdminConfigurationService> _logger;
|
||||||
|
private readonly IServiceScopeFactory _serviceScopeFactory;
|
||||||
|
|
||||||
public AdminConfigurationService(IConfiguration configuration, ILogger<AdminConfigurationService> logger)
|
public AdminConfigurationService(
|
||||||
|
IConfiguration configuration,
|
||||||
|
ILogger<AdminConfigurationService> logger,
|
||||||
|
IServiceScopeFactory serviceScopeFactory)
|
||||||
{
|
{
|
||||||
_configuration = configuration;
|
_configuration = configuration;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
|
_serviceScopeFactory = serviceScopeFactory;
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool IsUserAdmin(string userName)
|
public bool IsUserAdmin(string userName)
|
||||||
@@ -27,15 +34,37 @@ public class AdminConfigurationService : IAdminConfigurationService
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// First check configuration (for backward compatibility)
|
||||||
var adminUserNames = GetAdminUserNames();
|
var adminUserNames = GetAdminUserNames();
|
||||||
var isAdmin = adminUserNames.Contains(userName, StringComparer.OrdinalIgnoreCase);
|
var isAdminFromConfig = adminUserNames.Contains(userName, StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
if (isAdmin)
|
if (isAdminFromConfig)
|
||||||
{
|
{
|
||||||
_logger.LogInformation("User {UserName} has admin privileges", userName);
|
_logger.LogInformation("User {UserName} has admin privileges from configuration", userName);
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
return isAdmin;
|
// If not in config, check database User.IsAdmin flag
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var scope = _serviceScopeFactory.CreateScope();
|
||||||
|
var userRepository = scope.ServiceProvider.GetRequiredService<IUserRepository>();
|
||||||
|
|
||||||
|
var user = userRepository.GetUserByNameAsync(userName).GetAwaiter().GetResult();
|
||||||
|
|
||||||
|
if (user != null && user.IsAdmin)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("User {UserName} has admin privileges from database", userName);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(ex, "Error checking admin status for user {UserName} from database", userName);
|
||||||
|
// If database check fails, fall back to config-only result
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<string> GetAdminUserNames()
|
public List<string> GetAdminUserNames()
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ public class UserService : IUserService
|
|||||||
private readonly ILogger<UserService> _logger;
|
private readonly ILogger<UserService> _logger;
|
||||||
private readonly ICacheService _cacheService;
|
private readonly ICacheService _cacheService;
|
||||||
private readonly IGrainFactory _grainFactory;
|
private readonly IGrainFactory _grainFactory;
|
||||||
|
private readonly IWhitelistService _whitelistService;
|
||||||
private readonly string[] _authorizedAddresses;
|
private readonly string[] _authorizedAddresses;
|
||||||
|
|
||||||
public UserService(
|
public UserService(
|
||||||
@@ -27,6 +28,7 @@ public class UserService : IUserService
|
|||||||
ILogger<UserService> logger,
|
ILogger<UserService> logger,
|
||||||
ICacheService cacheService,
|
ICacheService cacheService,
|
||||||
IGrainFactory grainFactory,
|
IGrainFactory grainFactory,
|
||||||
|
IWhitelistService whitelistService,
|
||||||
IConfiguration configuration)
|
IConfiguration configuration)
|
||||||
{
|
{
|
||||||
_evmManager = evmManager;
|
_evmManager = evmManager;
|
||||||
@@ -35,6 +37,7 @@ public class UserService : IUserService
|
|||||||
_logger = logger;
|
_logger = logger;
|
||||||
_cacheService = cacheService;
|
_cacheService = cacheService;
|
||||||
_grainFactory = grainFactory;
|
_grainFactory = grainFactory;
|
||||||
|
_whitelistService = whitelistService;
|
||||||
|
|
||||||
var authorizedAddressesString = configuration["AUTHORIZED_ADDRESSES"] ?? string.Empty;
|
var authorizedAddressesString = configuration["AUTHORIZED_ADDRESSES"] ?? string.Empty;
|
||||||
_authorizedAddresses = string.IsNullOrEmpty(authorizedAddressesString)
|
_authorizedAddresses = string.IsNullOrEmpty(authorizedAddressesString)
|
||||||
@@ -54,10 +57,20 @@ public class UserService : IUserService
|
|||||||
$"Message not good : {message} - Address : {address} - User : {name} - Signature : {signature}");
|
$"Message not good : {message} - Address : {address} - User : {name} - Signature : {signature}");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!_authorizedAddresses.Any(a => string.Equals(a, recoveredAddress, StringComparison.OrdinalIgnoreCase)))
|
// Check if address is whitelisted first (skip authorized addresses check if whitelisted)
|
||||||
|
var isWhitelisted = await _whitelistService.IsEmbeddedWalletWhitelistedAsync(recoveredAddress);
|
||||||
|
|
||||||
|
if (!isWhitelisted)
|
||||||
{
|
{
|
||||||
|
// Only check authorized addresses if not whitelisted
|
||||||
|
var isInAuthorizedAddresses = _authorizedAddresses.Any(a => string.Equals(a, recoveredAddress, StringComparison.OrdinalIgnoreCase));
|
||||||
|
|
||||||
|
if (!isInAuthorizedAddresses)
|
||||||
|
{
|
||||||
|
_logger.LogWarning("Address {Address} is not authorized and not whitelisted", recoveredAddress);
|
||||||
throw new Exception($"Address {recoveredAddress} not authorized. Please wait for team approval.");
|
throw new Exception($"Address {recoveredAddress} not authorized. Please wait for team approval.");
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
if (recoveredAddress == null || !recoveredAddress.Equals(address))
|
if (recoveredAddress == null || !recoveredAddress.Equals(address))
|
||||||
|
|||||||
@@ -66,6 +66,18 @@ public class WhitelistService : IWhitelistService
|
|||||||
return await _whitelistRepository.GetByIdAsync(id);
|
return await _whitelistRepository.GetByIdAsync(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<bool> IsEmbeddedWalletWhitelistedAsync(string embeddedWallet)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(embeddedWallet))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var account = await _whitelistRepository.GetByEmbeddedWalletAsync(embeddedWallet);
|
||||||
|
|
||||||
|
return account?.IsWhitelisted ?? false;
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<WhitelistAccount> ProcessPrivyWebhookAsync(
|
public async Task<WhitelistAccount> ProcessPrivyWebhookAsync(
|
||||||
string privyUserId,
|
string privyUserId,
|
||||||
long privyCreatedAt,
|
long privyCreatedAt,
|
||||||
@@ -76,8 +88,8 @@ public class WhitelistService : IWhitelistService
|
|||||||
_logger.LogInformation("Processing Privy webhook - PrivyId: {PrivyId}, Wallet: {Wallet}, ExternalEthereum: {ExternalEthereum}, Twitter: {Twitter}",
|
_logger.LogInformation("Processing Privy webhook - PrivyId: {PrivyId}, Wallet: {Wallet}, ExternalEthereum: {ExternalEthereum}, Twitter: {Twitter}",
|
||||||
privyUserId, walletAddress, externalEthereumAccount ?? "null", twitterAccount ?? "null");
|
privyUserId, walletAddress, externalEthereumAccount ?? "null", twitterAccount ?? "null");
|
||||||
|
|
||||||
// Convert Unix timestamp to DateTime
|
// Convert Unix timestamp to UTC DateTime (PostgreSQL requires UTC)
|
||||||
var privyCreationDate = DateTimeOffset.FromUnixTimeSeconds(privyCreatedAt).DateTime;
|
var privyCreationDate = DateTimeOffset.FromUnixTimeSeconds(privyCreatedAt).UtcDateTime;
|
||||||
|
|
||||||
// Check if account already exists
|
// Check if account already exists
|
||||||
var existing = await _whitelistRepository.GetByPrivyIdAsync(privyUserId) ??
|
var existing = await _whitelistRepository.GetByPrivyIdAsync(privyUserId) ??
|
||||||
|
|||||||
@@ -17,4 +17,6 @@ public class User
|
|||||||
[Id(4)] public string AvatarUrl { get; set; } = string.Empty;
|
[Id(4)] public string AvatarUrl { get; set; } = string.Empty;
|
||||||
|
|
||||||
[Id(5)] public string TelegramChannel { get; set; } = string.Empty;
|
[Id(5)] public string TelegramChannel { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
[Id(6)] public bool IsAdmin { get; set; } = false;
|
||||||
}
|
}
|
||||||
1605
src/Managing.Infrastructure.Database/Migrations/20251107163433_AddIsAdminToUsers.Designer.cs
generated
Normal file
1605
src/Managing.Infrastructure.Database/Migrations/20251107163433_AddIsAdminToUsers.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,29 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Managing.Infrastructure.Databases.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class AddIsAdminToUsers : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AddColumn<bool>(
|
||||||
|
name: "IsAdmin",
|
||||||
|
table: "Users",
|
||||||
|
type: "boolean",
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "IsAdmin",
|
||||||
|
table: "Users");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1306,6 +1306,9 @@ namespace Managing.Infrastructure.Databases.Migrations
|
|||||||
.HasMaxLength(500)
|
.HasMaxLength(500)
|
||||||
.HasColumnType("character varying(500)");
|
.HasColumnType("character varying(500)");
|
||||||
|
|
||||||
|
b.Property<bool>("IsAdmin")
|
||||||
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
b.Property<string>("Name")
|
b.Property<string>("Name")
|
||||||
.IsRequired()
|
.IsRequired()
|
||||||
.HasMaxLength(255)
|
.HasMaxLength(255)
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ public class UserEntity
|
|||||||
[MaxLength(255)] public string? AgentName { get; set; }
|
[MaxLength(255)] public string? AgentName { get; set; }
|
||||||
public string? AvatarUrl { get; set; }
|
public string? AvatarUrl { get; set; }
|
||||||
public string? TelegramChannel { get; set; }
|
public string? TelegramChannel { get; set; }
|
||||||
|
public bool IsAdmin { get; set; } = false;
|
||||||
|
|
||||||
// Navigation properties
|
// Navigation properties
|
||||||
public virtual ICollection<AccountEntity> Accounts { get; set; } = new List<AccountEntity>();
|
public virtual ICollection<AccountEntity> Accounts { get; set; } = new List<AccountEntity>();
|
||||||
|
|||||||
@@ -131,6 +131,7 @@ public static class PostgreSqlMappers
|
|||||||
AvatarUrl = entity.AvatarUrl,
|
AvatarUrl = entity.AvatarUrl,
|
||||||
TelegramChannel = entity.TelegramChannel,
|
TelegramChannel = entity.TelegramChannel,
|
||||||
Id = entity.Id, // Assuming Id is the primary key for UserEntity
|
Id = entity.Id, // Assuming Id is the primary key for UserEntity
|
||||||
|
IsAdmin = entity.IsAdmin,
|
||||||
Accounts = entity.Accounts?.Select(MapAccountWithoutUser).ToList() ?? new List<Account>()
|
Accounts = entity.Accounts?.Select(MapAccountWithoutUser).ToList() ?? new List<Account>()
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -163,7 +164,8 @@ public static class PostgreSqlMappers
|
|||||||
Name = user.Name,
|
Name = user.Name,
|
||||||
AgentName = user.AgentName,
|
AgentName = user.AgentName,
|
||||||
AvatarUrl = user.AvatarUrl,
|
AvatarUrl = user.AvatarUrl,
|
||||||
TelegramChannel = user.TelegramChannel
|
TelegramChannel = user.TelegramChannel,
|
||||||
|
IsAdmin = user.IsAdmin
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -201,6 +201,7 @@ public class PostgreSqlUserRepository : BaseRepositoryWithLogging, IUserReposito
|
|||||||
existingUser.AgentName = user.AgentName;
|
existingUser.AgentName = user.AgentName;
|
||||||
existingUser.AvatarUrl = user.AvatarUrl;
|
existingUser.AvatarUrl = user.AvatarUrl;
|
||||||
existingUser.TelegramChannel = user.TelegramChannel;
|
existingUser.TelegramChannel = user.TelegramChannel;
|
||||||
|
existingUser.IsAdmin = user.IsAdmin;
|
||||||
|
|
||||||
_context.Users.Update(existingUser);
|
_context.Users.Update(existingUser);
|
||||||
|
|
||||||
|
|||||||
@@ -3879,6 +3879,142 @@ export class UserClient extends AuthorizedApiBase {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class WhitelistClient extends AuthorizedApiBase {
|
||||||
|
private http: { fetch(url: RequestInfo, init?: RequestInit): Promise<Response> };
|
||||||
|
private baseUrl: string;
|
||||||
|
protected jsonParseReviver: ((key: string, value: any) => any) | undefined = undefined;
|
||||||
|
|
||||||
|
constructor(configuration: IConfig, baseUrl?: string, http?: { fetch(url: RequestInfo, init?: RequestInit): Promise<Response> }) {
|
||||||
|
super(configuration);
|
||||||
|
this.http = http ? http : window as any;
|
||||||
|
this.baseUrl = baseUrl ?? "http://localhost:5000";
|
||||||
|
}
|
||||||
|
|
||||||
|
whitelist_GetWhitelistAccounts(pageNumber: number | undefined, pageSize: number | undefined, searchExternalEthereumAccount: string | null | undefined, searchTwitterAccount: string | null | undefined): Promise<PaginatedWhitelistAccountsResponse> {
|
||||||
|
let url_ = this.baseUrl + "/api/Whitelist?";
|
||||||
|
if (pageNumber === null)
|
||||||
|
throw new Error("The parameter 'pageNumber' cannot be null.");
|
||||||
|
else if (pageNumber !== undefined)
|
||||||
|
url_ += "pageNumber=" + encodeURIComponent("" + pageNumber) + "&";
|
||||||
|
if (pageSize === null)
|
||||||
|
throw new Error("The parameter 'pageSize' cannot be null.");
|
||||||
|
else if (pageSize !== undefined)
|
||||||
|
url_ += "pageSize=" + encodeURIComponent("" + pageSize) + "&";
|
||||||
|
if (searchExternalEthereumAccount !== undefined && searchExternalEthereumAccount !== null)
|
||||||
|
url_ += "searchExternalEthereumAccount=" + encodeURIComponent("" + searchExternalEthereumAccount) + "&";
|
||||||
|
if (searchTwitterAccount !== undefined && searchTwitterAccount !== null)
|
||||||
|
url_ += "searchTwitterAccount=" + encodeURIComponent("" + searchTwitterAccount) + "&";
|
||||||
|
url_ = url_.replace(/[?&]$/, "");
|
||||||
|
|
||||||
|
let options_: RequestInit = {
|
||||||
|
method: "GET",
|
||||||
|
headers: {
|
||||||
|
"Accept": "application/json"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return this.transformOptions(options_).then(transformedOptions_ => {
|
||||||
|
return this.http.fetch(url_, transformedOptions_);
|
||||||
|
}).then((_response: Response) => {
|
||||||
|
return this.processWhitelist_GetWhitelistAccounts(_response);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
protected processWhitelist_GetWhitelistAccounts(response: Response): Promise<PaginatedWhitelistAccountsResponse> {
|
||||||
|
const status = response.status;
|
||||||
|
let _headers: any = {}; if (response.headers && response.headers.forEach) { response.headers.forEach((v: any, k: any) => _headers[k] = v); };
|
||||||
|
if (status === 200) {
|
||||||
|
return response.text().then((_responseText) => {
|
||||||
|
let result200: any = null;
|
||||||
|
result200 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver) as PaginatedWhitelistAccountsResponse;
|
||||||
|
return result200;
|
||||||
|
});
|
||||||
|
} else if (status !== 200 && status !== 204) {
|
||||||
|
return response.text().then((_responseText) => {
|
||||||
|
return throwException("An unexpected server error occurred.", status, _responseText, _headers);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return Promise.resolve<PaginatedWhitelistAccountsResponse>(null as any);
|
||||||
|
}
|
||||||
|
|
||||||
|
whitelist_SetWhitelisted(id: number): Promise<WhitelistAccount> {
|
||||||
|
let url_ = this.baseUrl + "/api/Whitelist/{id}/whitelist";
|
||||||
|
if (id === undefined || id === null)
|
||||||
|
throw new Error("The parameter 'id' must be defined.");
|
||||||
|
url_ = url_.replace("{id}", encodeURIComponent("" + id));
|
||||||
|
url_ = url_.replace(/[?&]$/, "");
|
||||||
|
|
||||||
|
let options_: RequestInit = {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Accept": "application/json"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return this.transformOptions(options_).then(transformedOptions_ => {
|
||||||
|
return this.http.fetch(url_, transformedOptions_);
|
||||||
|
}).then((_response: Response) => {
|
||||||
|
return this.processWhitelist_SetWhitelisted(_response);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
protected processWhitelist_SetWhitelisted(response: Response): Promise<WhitelistAccount> {
|
||||||
|
const status = response.status;
|
||||||
|
let _headers: any = {}; if (response.headers && response.headers.forEach) { response.headers.forEach((v: any, k: any) => _headers[k] = v); };
|
||||||
|
if (status === 200) {
|
||||||
|
return response.text().then((_responseText) => {
|
||||||
|
let result200: any = null;
|
||||||
|
result200 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver) as WhitelistAccount;
|
||||||
|
return result200;
|
||||||
|
});
|
||||||
|
} else if (status !== 200 && status !== 204) {
|
||||||
|
return response.text().then((_responseText) => {
|
||||||
|
return throwException("An unexpected server error occurred.", status, _responseText, _headers);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return Promise.resolve<WhitelistAccount>(null as any);
|
||||||
|
}
|
||||||
|
|
||||||
|
whitelist_ProcessPrivyWebhook(webhook: PrivyWebhookDto): Promise<WhitelistAccount> {
|
||||||
|
let url_ = this.baseUrl + "/api/Whitelist/webhook";
|
||||||
|
url_ = url_.replace(/[?&]$/, "");
|
||||||
|
|
||||||
|
const content_ = JSON.stringify(webhook);
|
||||||
|
|
||||||
|
let options_: RequestInit = {
|
||||||
|
body: content_,
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Accept": "application/json"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return this.transformOptions(options_).then(transformedOptions_ => {
|
||||||
|
return this.http.fetch(url_, transformedOptions_);
|
||||||
|
}).then((_response: Response) => {
|
||||||
|
return this.processWhitelist_ProcessPrivyWebhook(_response);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
protected processWhitelist_ProcessPrivyWebhook(response: Response): Promise<WhitelistAccount> {
|
||||||
|
const status = response.status;
|
||||||
|
let _headers: any = {}; if (response.headers && response.headers.forEach) { response.headers.forEach((v: any, k: any) => _headers[k] = v); };
|
||||||
|
if (status === 200) {
|
||||||
|
return response.text().then((_responseText) => {
|
||||||
|
let result200: any = null;
|
||||||
|
result200 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver) as WhitelistAccount;
|
||||||
|
return result200;
|
||||||
|
});
|
||||||
|
} else if (status !== 200 && status !== 204) {
|
||||||
|
return response.text().then((_responseText) => {
|
||||||
|
return throwException("An unexpected server error occurred.", status, _responseText, _headers);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return Promise.resolve<WhitelistAccount>(null as any);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export interface Account {
|
export interface Account {
|
||||||
id?: number;
|
id?: number;
|
||||||
name: string;
|
name: string;
|
||||||
@@ -5117,6 +5253,55 @@ export interface LoginRequest {
|
|||||||
message: string;
|
message: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface PaginatedWhitelistAccountsResponse {
|
||||||
|
accounts?: WhitelistAccount[] | null;
|
||||||
|
totalCount?: number;
|
||||||
|
pageNumber?: number;
|
||||||
|
pageSize?: number;
|
||||||
|
totalPages?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WhitelistAccount {
|
||||||
|
id?: number;
|
||||||
|
privyId?: string | null;
|
||||||
|
privyCreationDate?: Date;
|
||||||
|
embeddedWallet?: string | null;
|
||||||
|
externalEthereumAccount?: string | null;
|
||||||
|
twitterAccount?: string | null;
|
||||||
|
isWhitelisted?: boolean;
|
||||||
|
createdAt?: Date;
|
||||||
|
updatedAt?: Date | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PrivyWebhookDto {
|
||||||
|
type?: string | null;
|
||||||
|
user?: PrivyUserDto | null;
|
||||||
|
wallet?: PrivyWalletDto | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PrivyUserDto {
|
||||||
|
created_at?: number;
|
||||||
|
has_accepted_terms?: boolean;
|
||||||
|
id?: string | null;
|
||||||
|
is_guest?: boolean;
|
||||||
|
linked_accounts?: PrivyLinkedAccountDto[] | null;
|
||||||
|
mfa_methods?: any[] | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PrivyLinkedAccountDto {
|
||||||
|
address?: string | null;
|
||||||
|
first_verified_at?: number | null;
|
||||||
|
latest_verified_at?: number | null;
|
||||||
|
type?: string | null;
|
||||||
|
verified_at?: number | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PrivyWalletDto {
|
||||||
|
type?: string | null;
|
||||||
|
address?: string | null;
|
||||||
|
chain_type?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
export interface FileResponse {
|
export interface FileResponse {
|
||||||
data: Blob;
|
data: Blob;
|
||||||
status: number;
|
status: number;
|
||||||
|
|||||||
@@ -1248,6 +1248,55 @@ export interface LoginRequest {
|
|||||||
message: string;
|
message: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface PaginatedWhitelistAccountsResponse {
|
||||||
|
accounts?: WhitelistAccount[] | null;
|
||||||
|
totalCount?: number;
|
||||||
|
pageNumber?: number;
|
||||||
|
pageSize?: number;
|
||||||
|
totalPages?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WhitelistAccount {
|
||||||
|
id?: number;
|
||||||
|
privyId?: string | null;
|
||||||
|
privyCreationDate?: Date;
|
||||||
|
embeddedWallet?: string | null;
|
||||||
|
externalEthereumAccount?: string | null;
|
||||||
|
twitterAccount?: string | null;
|
||||||
|
isWhitelisted?: boolean;
|
||||||
|
createdAt?: Date;
|
||||||
|
updatedAt?: Date | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PrivyWebhookDto {
|
||||||
|
type?: string | null;
|
||||||
|
user?: PrivyUserDto | null;
|
||||||
|
wallet?: PrivyWalletDto | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PrivyUserDto {
|
||||||
|
created_at?: number;
|
||||||
|
has_accepted_terms?: boolean;
|
||||||
|
id?: string | null;
|
||||||
|
is_guest?: boolean;
|
||||||
|
linked_accounts?: PrivyLinkedAccountDto[] | null;
|
||||||
|
mfa_methods?: any[] | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PrivyLinkedAccountDto {
|
||||||
|
address?: string | null;
|
||||||
|
first_verified_at?: number | null;
|
||||||
|
latest_verified_at?: number | null;
|
||||||
|
type?: string | null;
|
||||||
|
verified_at?: number | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PrivyWalletDto {
|
||||||
|
type?: string | null;
|
||||||
|
address?: string | null;
|
||||||
|
chain_type?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
export interface FileResponse {
|
export interface FileResponse {
|
||||||
data: Blob;
|
data: Blob;
|
||||||
status: number;
|
status: number;
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import DefaultConfig from './defaultConfig/defaultConfig'
|
|||||||
import UserInfoSettings from './UserInfoSettings'
|
import UserInfoSettings from './UserInfoSettings'
|
||||||
import AccountFee from './accountFee/accountFee'
|
import AccountFee from './accountFee/accountFee'
|
||||||
import SqlMonitoring from './sqlmonitoring/sqlMonitoring'
|
import SqlMonitoring from './sqlmonitoring/sqlMonitoring'
|
||||||
|
import WhitelistSettings from './whitelist/whitelistSettings'
|
||||||
|
|
||||||
type TabsType = {
|
type TabsType = {
|
||||||
label: string
|
label: string
|
||||||
@@ -59,6 +60,11 @@ const tabs: TabsType = [
|
|||||||
index: 8,
|
index: 8,
|
||||||
label: 'SQL Monitoring',
|
label: 'SQL Monitoring',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
Component: WhitelistSettings,
|
||||||
|
index: 9,
|
||||||
|
label: 'Whitelist',
|
||||||
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
const Settings: React.FC = () => {
|
const Settings: React.FC = () => {
|
||||||
|
|||||||
@@ -0,0 +1,115 @@
|
|||||||
|
import {useState} from 'react'
|
||||||
|
import {useQuery} from '@tanstack/react-query'
|
||||||
|
|
||||||
|
import useApiUrlStore from '../../../app/store/apiStore'
|
||||||
|
import {WhitelistClient} from '../../../generated/ManagingApi'
|
||||||
|
|
||||||
|
import WhitelistTable from './whitelistTable'
|
||||||
|
|
||||||
|
const WhitelistSettings: React.FC = () => {
|
||||||
|
const { apiUrl } = useApiUrlStore()
|
||||||
|
const [pageNumber, setPageNumber] = useState(1)
|
||||||
|
const [pageSize, setPageSize] = useState(20)
|
||||||
|
const [searchExternalEthereumAccount, setSearchExternalEthereumAccount] = useState<string>('')
|
||||||
|
const [searchTwitterAccount, setSearchTwitterAccount] = useState<string>('')
|
||||||
|
|
||||||
|
const whitelistClient = new WhitelistClient({}, apiUrl)
|
||||||
|
|
||||||
|
const {
|
||||||
|
data: whitelistData,
|
||||||
|
isLoading,
|
||||||
|
error,
|
||||||
|
refetch
|
||||||
|
} = useQuery({
|
||||||
|
queryKey: ['whitelistAccounts', pageNumber, pageSize, searchExternalEthereumAccount, searchTwitterAccount],
|
||||||
|
queryFn: async () => {
|
||||||
|
return await whitelistClient.whitelist_GetWhitelistAccounts(
|
||||||
|
pageNumber,
|
||||||
|
pageSize,
|
||||||
|
searchExternalEthereumAccount || null,
|
||||||
|
searchTwitterAccount || null
|
||||||
|
)
|
||||||
|
},
|
||||||
|
staleTime: 30000,
|
||||||
|
gcTime: 5 * 60 * 1000,
|
||||||
|
})
|
||||||
|
|
||||||
|
const accounts = whitelistData?.accounts || []
|
||||||
|
const totalCount = whitelistData?.totalCount || 0
|
||||||
|
const totalPages = whitelistData?.totalPages || 0
|
||||||
|
|
||||||
|
const handlePageChange = (newPage: number) => {
|
||||||
|
setPageNumber(newPage)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSearchExternalEthereum = (value: string) => {
|
||||||
|
setSearchExternalEthereumAccount(value)
|
||||||
|
setPageNumber(1) // Reset to first page when searching
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSearchTwitter = (value: string) => {
|
||||||
|
setSearchTwitterAccount(value)
|
||||||
|
setPageNumber(1) // Reset to first page when searching
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleWhitelistSuccess = () => {
|
||||||
|
refetch()
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="container mx-auto">
|
||||||
|
<div className="mb-4">
|
||||||
|
<h2 className="text-2xl font-bold mb-4">Whitelist Accounts</h2>
|
||||||
|
|
||||||
|
{/* Search Filters */}
|
||||||
|
<div className="flex gap-4 mb-4">
|
||||||
|
<div className="form-control w-full max-w-xs">
|
||||||
|
<label className="label">
|
||||||
|
<span className="label-text">Search by Ethereum Account</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Enter Ethereum address..."
|
||||||
|
className="input input-bordered w-full"
|
||||||
|
value={searchExternalEthereumAccount}
|
||||||
|
onChange={(e) => handleSearchExternalEthereum(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="form-control w-full max-w-xs">
|
||||||
|
<label className="label">
|
||||||
|
<span className="label-text">Search by Twitter Account</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Enter Twitter handle..."
|
||||||
|
className="input input-bordered w-full"
|
||||||
|
value={searchTwitterAccount}
|
||||||
|
onChange={(e) => handleSearchTwitter(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<WhitelistTable
|
||||||
|
list={accounts}
|
||||||
|
isFetching={isLoading}
|
||||||
|
totalCount={totalCount}
|
||||||
|
currentPage={pageNumber}
|
||||||
|
totalPages={totalPages}
|
||||||
|
onPageChange={handlePageChange}
|
||||||
|
onWhitelistSuccess={handleWhitelistSuccess}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<div className="alert alert-error mt-4">
|
||||||
|
<span>Failed to load whitelist accounts. Please try again.</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default WhitelistSettings
|
||||||
|
|
||||||
@@ -0,0 +1,216 @@
|
|||||||
|
import React, {useMemo} from 'react'
|
||||||
|
import {type WhitelistAccount, WhitelistClient} from '../../../generated/ManagingApi'
|
||||||
|
import useApiUrlStore from '../../../app/store/apiStore'
|
||||||
|
import {Table, Toast} from '../../../components/mollecules'
|
||||||
|
|
||||||
|
interface IWhitelistList {
|
||||||
|
list: WhitelistAccount[]
|
||||||
|
isFetching: boolean
|
||||||
|
totalCount: number
|
||||||
|
currentPage: number
|
||||||
|
totalPages: number
|
||||||
|
onPageChange: (page: number) => void
|
||||||
|
onWhitelistSuccess: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const WhitelistTable: React.FC<IWhitelistList> = ({
|
||||||
|
list,
|
||||||
|
isFetching,
|
||||||
|
totalCount,
|
||||||
|
currentPage,
|
||||||
|
totalPages,
|
||||||
|
onPageChange,
|
||||||
|
onWhitelistSuccess
|
||||||
|
}) => {
|
||||||
|
const { apiUrl } = useApiUrlStore()
|
||||||
|
const whitelistClient = new WhitelistClient({}, apiUrl)
|
||||||
|
|
||||||
|
async function handleSetWhitelisted(id: number) {
|
||||||
|
const t = new Toast('Updating whitelist status...')
|
||||||
|
|
||||||
|
try {
|
||||||
|
await whitelistClient.whitelist_SetWhitelisted(id)
|
||||||
|
t.update('success', 'Account whitelisted successfully')
|
||||||
|
onWhitelistSuccess()
|
||||||
|
} catch (err: any) {
|
||||||
|
const errorMessage = err?.response || err?.message || 'Unknown error'
|
||||||
|
t.update('error', `Error: ${errorMessage}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const columns = useMemo(() => [
|
||||||
|
{
|
||||||
|
Header: 'Is Whitelisted',
|
||||||
|
accessor: 'isWhitelisted',
|
||||||
|
width: 120,
|
||||||
|
Cell: ({ value }: any) => (
|
||||||
|
<div className={`badge badge-lg ${value ? 'badge-success' : 'badge-warning'}`}>
|
||||||
|
{value ? 'Yes' : 'No'}
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Header: 'Actions',
|
||||||
|
accessor: 'actions',
|
||||||
|
width: 150,
|
||||||
|
Cell: ({ row }: any) => {
|
||||||
|
const account = row.original as WhitelistAccount
|
||||||
|
return (
|
||||||
|
<div className="flex gap-2">
|
||||||
|
{!account.isWhitelisted && (
|
||||||
|
<button
|
||||||
|
className="btn btn-sm btn-primary"
|
||||||
|
onClick={() => handleSetWhitelisted(account.id!)}
|
||||||
|
disabled={isFetching}
|
||||||
|
>
|
||||||
|
Whitelist
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
{account.isWhitelisted && (
|
||||||
|
<span className="badge badge-success badge-lg">Whitelisted</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Header: 'Privy ID',
|
||||||
|
accessor: 'privyId',
|
||||||
|
width: 200,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Header: 'Embedded Wallet',
|
||||||
|
accessor: 'embeddedWallet',
|
||||||
|
width: 200,
|
||||||
|
Cell: ({ value }: any) => (
|
||||||
|
<span className="font-mono text-xs">{value || '-'}</span>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Header: 'External Ethereum Account',
|
||||||
|
accessor: 'externalEthereumAccount',
|
||||||
|
width: 200,
|
||||||
|
Cell: ({ value }: any) => (
|
||||||
|
<span className="font-mono text-xs">{value || '-'}</span>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Header: 'Twitter Account',
|
||||||
|
accessor: 'twitterAccount',
|
||||||
|
width: 150,
|
||||||
|
Cell: ({ value }: any) => (
|
||||||
|
<span>{value || '-'}</span>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
], [isFetching])
|
||||||
|
|
||||||
|
const tableData = useMemo(() => {
|
||||||
|
return list.map((account) => ({
|
||||||
|
id: account.id,
|
||||||
|
privyId: account.privyId || '-',
|
||||||
|
embeddedWallet: account.embeddedWallet || '-',
|
||||||
|
externalEthereumAccount: account.externalEthereumAccount || '-',
|
||||||
|
twitterAccount: account.twitterAccount || '-',
|
||||||
|
isWhitelisted: account.isWhitelisted || false,
|
||||||
|
createdAt: account.createdAt,
|
||||||
|
actions: account,
|
||||||
|
}))
|
||||||
|
}, [list])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="mb-4">
|
||||||
|
<p className="text-sm text-gray-500">
|
||||||
|
Total accounts: {totalCount} | Page {currentPage} of {totalPages}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{isFetching && (
|
||||||
|
<div className="flex justify-center my-4">
|
||||||
|
<span className="loading loading-spinner loading-lg"></span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!isFetching && list.length === 0 && (
|
||||||
|
<div className="alert alert-info">
|
||||||
|
<span>No whitelist accounts found.</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!isFetching && list.length > 0 && (
|
||||||
|
<>
|
||||||
|
<Table
|
||||||
|
columns={columns}
|
||||||
|
data={tableData}
|
||||||
|
showPagination={false}
|
||||||
|
hiddenColumns={[]}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Manual Pagination */}
|
||||||
|
{totalPages > 1 && (
|
||||||
|
<div className="flex justify-center items-center gap-2 mt-4">
|
||||||
|
<button
|
||||||
|
className="btn btn-sm"
|
||||||
|
onClick={() => onPageChange(1)}
|
||||||
|
disabled={currentPage === 1}
|
||||||
|
>
|
||||||
|
{'<<'}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="btn btn-sm"
|
||||||
|
onClick={() => onPageChange(currentPage - 1)}
|
||||||
|
disabled={currentPage === 1}
|
||||||
|
>
|
||||||
|
{'<'}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* Page numbers */}
|
||||||
|
<div className="flex gap-1">
|
||||||
|
{Array.from({ length: Math.min(5, totalPages) }, (_, i) => {
|
||||||
|
let pageNum
|
||||||
|
if (totalPages <= 5) {
|
||||||
|
pageNum = i + 1
|
||||||
|
} else if (currentPage <= 3) {
|
||||||
|
pageNum = i + 1
|
||||||
|
} else if (currentPage >= totalPages - 2) {
|
||||||
|
pageNum = totalPages - 4 + i
|
||||||
|
} else {
|
||||||
|
pageNum = currentPage - 2 + i
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={pageNum}
|
||||||
|
className={`btn btn-sm ${currentPage === pageNum ? 'btn-primary' : ''}`}
|
||||||
|
onClick={() => onPageChange(pageNum)}
|
||||||
|
>
|
||||||
|
{pageNum}
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
className="btn btn-sm"
|
||||||
|
onClick={() => onPageChange(currentPage + 1)}
|
||||||
|
disabled={currentPage >= totalPages}
|
||||||
|
>
|
||||||
|
{'>'}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="btn btn-sm"
|
||||||
|
onClick={() => onPageChange(totalPages)}
|
||||||
|
disabled={currentPage >= totalPages}
|
||||||
|
>
|
||||||
|
{'>>'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default WhitelistTable
|
||||||
|
|
||||||
Reference in New Issue
Block a user