267 lines
9.1 KiB
C#
267 lines
9.1 KiB
C#
using System.Text.RegularExpressions;
|
|
using Managing.Application.Abstractions.Grains;
|
|
using Managing.Application.Abstractions.Repositories;
|
|
using Managing.Application.Abstractions.Services;
|
|
using Managing.Common;
|
|
using Managing.Domain.Accounts;
|
|
using Managing.Domain.Users;
|
|
using Microsoft.Extensions.Configuration;
|
|
using Microsoft.Extensions.Logging;
|
|
|
|
namespace Managing.Application.Users;
|
|
|
|
public class UserService : IUserService
|
|
{
|
|
private readonly IEvmManager _evmManager;
|
|
private readonly IUserRepository _userRepository;
|
|
private readonly IAccountService _accountService;
|
|
private readonly ILogger<UserService> _logger;
|
|
private readonly ICacheService _cacheService;
|
|
private readonly IGrainFactory _grainFactory;
|
|
private readonly string[] _authorizedAddresses;
|
|
|
|
public UserService(
|
|
IEvmManager evmManager,
|
|
IUserRepository userRepository,
|
|
IAccountService accountService,
|
|
ILogger<UserService> logger,
|
|
ICacheService cacheService,
|
|
IGrainFactory grainFactory,
|
|
IConfiguration configuration)
|
|
{
|
|
_evmManager = evmManager;
|
|
_userRepository = userRepository;
|
|
_accountService = accountService;
|
|
_logger = logger;
|
|
_cacheService = cacheService;
|
|
_grainFactory = grainFactory;
|
|
|
|
var authorizedAddressesString = configuration["AUTHORIZED_ADDRESSES"] ?? string.Empty;
|
|
_authorizedAddresses = string.IsNullOrEmpty(authorizedAddressesString)
|
|
? Array.Empty<string>()
|
|
: authorizedAddressesString.Split(';', StringSplitOptions.RemoveEmptyEntries);
|
|
}
|
|
|
|
public async Task<User> Authenticate(string name, string address, string message, string signature)
|
|
{
|
|
var recoveredAddress = _evmManager.VerifySignature(signature, message);
|
|
|
|
// Verify message
|
|
if (!message.Equals("KaigenTeamXCowchain"))
|
|
{
|
|
_logger.LogWarning($"Message {message} not starting with KaigenTeamXCowchain");
|
|
throw new Exception(
|
|
$"Message not good : {message} - Address : {address} - User : {name} - Signature : {signature}");
|
|
}
|
|
|
|
if (!_authorizedAddresses.Any(a => string.Equals(a, recoveredAddress, StringComparison.OrdinalIgnoreCase)))
|
|
{
|
|
_logger.LogWarning($"Address {recoveredAddress} not authorized");
|
|
throw new Exception("Address not authorized");
|
|
}
|
|
|
|
if (recoveredAddress == null || !recoveredAddress.Equals(address))
|
|
{
|
|
_logger.LogWarning($"Address {recoveredAddress} not corresponding");
|
|
throw new Exception("Address not corresponding");
|
|
}
|
|
|
|
// Check if account exist
|
|
var user = await _userRepository.GetUserByNameAsync(name);
|
|
|
|
if (user != null)
|
|
{
|
|
if (!user.Name.Equals(name))
|
|
throw new Exception("Name not corresponding");
|
|
|
|
// User and account found
|
|
user.Accounts = _accountService.GetAccountsByUser(user).ToList();
|
|
|
|
// Check if recoverred address owns the account
|
|
var account = user.Accounts.FirstOrDefault(a => a.Key.Equals(recoveredAddress));
|
|
|
|
if (account == null)
|
|
{
|
|
throw new Exception("Account not found");
|
|
}
|
|
|
|
return user;
|
|
}
|
|
else
|
|
{
|
|
// First login - create user first
|
|
user = new User
|
|
{
|
|
Name = name
|
|
};
|
|
|
|
// Save the user first
|
|
await _userRepository.SaveOrUpdateUserAsync(user);
|
|
|
|
// Create embedded account to authenticate user after user is saved
|
|
var account = await _accountService.CreateAccount(user, new Account
|
|
{
|
|
Name = $"{name}-embedded",
|
|
Key = recoveredAddress,
|
|
Exchange = Enums.TradingExchanges.Evm,
|
|
Type = Enums.AccountType.Privy
|
|
});
|
|
|
|
user.Accounts = new List<Account>()
|
|
{
|
|
account
|
|
};
|
|
}
|
|
|
|
return user;
|
|
}
|
|
|
|
public async Task<User> GetUserByAddressAsync(string address, bool useCache = true)
|
|
{
|
|
var cacheKey = $"user-by-address-{address}";
|
|
|
|
// Check cache first if caching is enabled
|
|
if (useCache)
|
|
{
|
|
var cachedUser = _cacheService.GetValue<User>(cacheKey);
|
|
if (cachedUser != null)
|
|
{
|
|
return cachedUser;
|
|
}
|
|
}
|
|
|
|
// Fetch from database (either cache miss or cache disabled)
|
|
var account = await _accountService.GetAccountByKey(address, true, false);
|
|
var user = await _userRepository.GetUserByNameAsync(account.User.Name);
|
|
|
|
// Use proper async version to avoid DbContext concurrency issues
|
|
user.Accounts = (await _accountService.GetAccountsByUserAsync(user)).ToList();
|
|
|
|
// Save to cache for 10 minutes if caching is enabled (JWT middleware calls this on every request)
|
|
if (useCache)
|
|
{
|
|
_cacheService.SaveValue(cacheKey, user, TimeSpan.FromMinutes(10));
|
|
}
|
|
|
|
return user;
|
|
}
|
|
|
|
public async Task<User> UpdateAgentName(User user, string agentName)
|
|
{
|
|
// Check if agent name is already used
|
|
var existingUser = await _userRepository.GetUserByAgentNameAsync(agentName);
|
|
if (existingUser != null)
|
|
{
|
|
throw new Exception($"Agent name already used by {existingUser.Name}");
|
|
}
|
|
else
|
|
{
|
|
user = await GetUserByName(user.Name);
|
|
if (!string.IsNullOrEmpty(user.AgentName) && user.AgentName.Equals(agentName))
|
|
return user;
|
|
|
|
// Update the agent name on the provided user object
|
|
user.AgentName = agentName;
|
|
await _userRepository.SaveOrUpdateUserAsync(user);
|
|
|
|
// Initialize the AgentGrain for this user
|
|
try
|
|
{
|
|
var agentGrain = _grainFactory.GetGrain<IAgentGrain>(user.Id);
|
|
await agentGrain.InitializeAsync(user.Id, agentName);
|
|
_logger.LogInformation("AgentGrain initialized for user {UserId} with agent name {AgentName}", user.Id,
|
|
agentName);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "Failed to initialize AgentGrain for user {UserId} with agent name {AgentName}",
|
|
user.Id, agentName);
|
|
// Don't throw here to avoid breaking the user update process
|
|
}
|
|
|
|
return user;
|
|
}
|
|
}
|
|
|
|
public async Task<User> UpdateAvatarUrl(User user, string avatarUrl)
|
|
{
|
|
// Validate URL format and image extension
|
|
if (!Uri.TryCreate(avatarUrl, UriKind.Absolute, out Uri? uriResult) ||
|
|
(uriResult.Scheme != Uri.UriSchemeHttp && uriResult.Scheme != Uri.UriSchemeHttps))
|
|
{
|
|
throw new Exception("Invalid URL format");
|
|
}
|
|
|
|
// Check for valid image extension
|
|
string pattern = @"\.(jpeg|jpg|png)$";
|
|
if (!Regex.IsMatch(avatarUrl, pattern, RegexOptions.IgnoreCase))
|
|
{
|
|
throw new Exception("URL must point to a JPEG or PNG image");
|
|
}
|
|
|
|
user = await GetUserByName(user.Name);
|
|
if (!string.IsNullOrEmpty(user.AvatarUrl) && user.AvatarUrl.Equals(avatarUrl))
|
|
return user;
|
|
|
|
// Update the avatar URL on the provided user object
|
|
user.AvatarUrl = avatarUrl;
|
|
await _userRepository.SaveOrUpdateUserAsync(user);
|
|
return user;
|
|
}
|
|
|
|
public async Task<User> UpdateTelegramChannel(User user, string telegramChannel)
|
|
{
|
|
// Validate Telegram channel format (numeric channel ID only)
|
|
if (!string.IsNullOrEmpty(telegramChannel))
|
|
{
|
|
string pattern = @"^[0-9]{5,15}$";
|
|
if (!Regex.IsMatch(telegramChannel, pattern))
|
|
{
|
|
throw new Exception("Invalid Telegram channel format. Must be numeric channel ID (5-15 digits).");
|
|
}
|
|
}
|
|
|
|
user = await GetUserByName(user.Name);
|
|
if (!string.IsNullOrEmpty(user.TelegramChannel) && user.TelegramChannel.Equals(telegramChannel))
|
|
return user;
|
|
|
|
// Update the telegram channel on the provided user object
|
|
user.TelegramChannel = telegramChannel;
|
|
await _userRepository.SaveOrUpdateUserAsync(user);
|
|
return user;
|
|
}
|
|
|
|
public async Task<User> GetUserByName(string name)
|
|
{
|
|
return await _userRepository.GetUserByNameAsync(name);
|
|
}
|
|
|
|
public async Task<User> GetUserByAgentName(string agentName)
|
|
{
|
|
var user = await _userRepository.GetUserByAgentNameAsync(agentName);
|
|
if (user == null)
|
|
{
|
|
throw new Exception($"User with agent name {agentName} not found");
|
|
}
|
|
|
|
return user;
|
|
}
|
|
|
|
public async Task<IEnumerable<User>> GetAllUsersAsync()
|
|
{
|
|
return await _userRepository.GetAllUsersAsync();
|
|
}
|
|
|
|
public async Task<User> GetUserByIdAsync(int userId)
|
|
{
|
|
var allUsers = await _userRepository.GetAllUsersAsync();
|
|
var user = allUsers.FirstOrDefault(u => u.Id == userId);
|
|
|
|
if (user == null)
|
|
{
|
|
throw new Exception($"User with ID {userId} not found");
|
|
}
|
|
|
|
return user;
|
|
}
|
|
} |