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 _logger; private readonly ICacheService _cacheService; private readonly IGrainFactory _grainFactory; private readonly string[] _authorizedAddresses; public UserService( IEvmManager evmManager, IUserRepository userRepository, IAccountService accountService, ILogger 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() : authorizedAddressesString.Split(';', StringSplitOptions.RemoveEmptyEntries); } public async Task 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 }; } return user; } public async Task 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(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 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(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 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 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 GetUserByName(string name) { return await _userRepository.GetUserByNameAsync(name); } public async Task 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> GetAllUsersAsync() { return await _userRepository.GetAllUsersAsync(); } public async Task 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; } }