QrRapido/Services/UserService.cs
Ricardo Carneiro 7a0c12f8d2
Some checks failed
Deploy QR Rapido / test (push) Failing after 17s
Deploy QR Rapido / build-and-push (push) Has been skipped
Deploy QR Rapido / deploy-staging (push) Has been skipped
Deploy QR Rapido / deploy-production (push) Has been skipped
feat: api separada do front-end e area do desenvolvedor.
2026-03-08 12:40:51 -03:00

752 lines
29 KiB
C#

using MongoDB.Driver;
using MongoDB.Bson;
using QRRapidoApp.Data;
using QRRapidoApp.Models;
using QRRapidoApp.Models.ViewModels;
using System.Text.Json;
namespace QRRapidoApp.Services
{
public class UserService : IUserService
{
private readonly MongoDbContext _context;
private readonly IConfiguration _config;
private readonly ILogger<UserService> _logger;
public UserService(MongoDbContext context, IConfiguration config, ILogger<UserService> logger)
{
_context = context;
_config = config;
_logger = logger;
}
public async Task<User?> GetUserAsync(string userId)
{
try
{
if (_context.Users == null) return null; // Development mode without MongoDB
User? userData = null;
if (!String.IsNullOrEmpty(userId))
{
userData = await _context.Users.Find(u => u.Id == userId).FirstOrDefaultAsync();
}
return userData ?? new User();
}
catch (Exception ex)
{
_logger.LogError(ex, $"Error getting user {userId}: {ex.Message}");
return null;
}
}
public async Task<User?> GetUserByEmailAsync(string email)
{
try
{
if (_context.Users == null) return null; // Development mode without MongoDB
return await _context.Users.Find(u => u.Email == email).FirstOrDefaultAsync();
}
catch (Exception ex)
{
_logger.LogError(ex, $"Error getting user by email {email}: {ex.Message}");
return null;
}
}
public async Task<User?> GetUserByProviderAsync(string provider, string providerId)
{
try
{
return await _context.Users
.Find(u => u.Provider == provider && u.ProviderId == providerId)
.FirstOrDefaultAsync();
}
catch (Exception ex)
{
_logger.LogError(ex, $"Error getting user by provider {provider}:{providerId}: {ex.Message}");
return null;
}
}
public async Task<User> CreateUserAsync(string email, string name, string provider, string providerId)
{
var user = new User
{
Email = email,
Name = name,
Provider = provider,
ProviderId = providerId,
CreatedAt = DateTime.UtcNow,
LastLoginAt = DateTime.UtcNow,
PreferredLanguage = "pt-BR",
DailyQRCount = 0,
LastQRDate = DateTime.UtcNow.Date,
TotalQRGenerated = 0
};
await _context.Users.InsertOneAsync(user);
_logger.LogInformation($"Created new user: {email} via {provider}");
return user;
}
public async Task UpdateLastLoginAsync(string userId)
{
try
{
var update = Builders<User>.Update
.Set(u => u.LastLoginAt, DateTime.UtcNow);
await _context.Users.UpdateOneAsync(u => u.Id == userId, update);
}
catch (Exception ex)
{
_logger.LogError(ex, $"Error updating last login for user {userId}: {ex.Message}");
}
}
public async Task<bool> UpdateUserAsync(User user)
{
try
{
var result = await _context.Users.ReplaceOneAsync(u => u.Id == user.Id, user);
return result.ModifiedCount > 0;
}
catch (Exception ex)
{
_logger.LogError(ex, $"Error updating user {user.Id}: {ex.Message}");
return false;
}
}
public async Task<int> GetDailyQRCountAsync(string? userId)
{
if (string.IsNullOrEmpty(userId))
return 0; // Anonymous users tracked separately
try
{
var user = await GetUserAsync(userId);
if (user == null) return 0;
// Reset count if it's a new day
if (user.LastQRDate.Date < DateTime.UtcNow.Date)
{
user.DailyQRCount = 0;
user.LastQRDate = DateTime.UtcNow.Date;
await UpdateUserAsync(user);
}
return user.DailyQRCount;
}
catch (Exception ex)
{
_logger.LogError(ex, $"Error getting daily QR count for user {userId}: {ex.Message}");
return 0;
}
}
public async Task<int> IncrementDailyQRCountAsync(string userId)
{
try
{
var user = await GetUserAsync(userId);
if (user == null) return 0;
// Reset count if it's a new day
if (user.LastQRDate.Date < DateTime.UtcNow.Date)
{
user.DailyQRCount = 1;
user.LastQRDate = DateTime.UtcNow.Date;
}
else
{
user.DailyQRCount++;
}
user.TotalQRGenerated++;
await UpdateUserAsync(user);
// Premium and logged users have unlimited QR codes
return int.MaxValue;
}
catch (Exception ex)
{
_logger.LogError(ex, $"Error incrementing daily QR count for user {userId}: {ex.Message}");
return 0;
}
}
public async Task<int> GetRemainingQRCountAsync(string userId)
{
try
{
var user = await GetUserAsync(userId);
if (user == null) return 0;
// Premium users have unlimited
if (user.IsPremium) return int.MaxValue;
// Logged users (non-premium) have unlimited
return int.MaxValue;
}
catch (Exception ex)
{
_logger.LogError(ex, $"Error getting remaining QR count for user {userId}: {ex.Message}");
return 0;
}
}
public async Task<bool> CanGenerateQRAsync(string? userId, bool isPremium)
{
// Premium users have unlimited QR codes
if (isPremium) return true;
// Logged users (non-premium) have unlimited QR codes
if (!string.IsNullOrEmpty(userId)) return true;
// Anonymous users have 3 QR codes per day
var dailyCount = await GetDailyQRCountAsync(userId);
var limit = 3;
return dailyCount < limit;
}
public async Task SaveQRToHistoryAsync(string? userId, QRGenerationResult qrResult, int costInCredits = 0)
{
try
{
var qrHistory = new QRCodeHistory
{
Id = string.IsNullOrEmpty(qrResult.QRId) ? Guid.NewGuid().ToString() : qrResult.QRId,
UserId = userId,
Type = qrResult.RequestSettings?.Type ?? "unknown",
Content = qrResult.RequestSettings?.Content ?? "",
QRCodeBase64 = qrResult.QRCodeBase64,
CustomizationSettings = JsonSerializer.Serialize(qrResult.RequestSettings),
CreatedAt = DateTime.UtcNow,
Language = qrResult.RequestSettings?.Language ?? "pt-BR",
Size = qrResult.RequestSettings?.Size ?? 300,
GenerationTimeMs = qrResult.GenerationTimeMs,
FromCache = qrResult.FromCache,
IsActive = true,
LastAccessedAt = DateTime.UtcNow,
TrackingId = qrResult.TrackingId,
IsDynamic = !string.IsNullOrEmpty(qrResult.TrackingId),
CostInCredits = costInCredits
};
await _context.QRCodeHistory.InsertOneAsync(qrHistory);
}
catch (Exception ex)
{
_logger.LogError(ex, $"Error saving QR to history: {ex.Message}");
}
}
public async Task<List<QRCodeHistory>> GetUserQRHistoryAsync(string userId, int limit = 50)
{
try
{
return await _context.QRCodeHistory
.Find(q => q.UserId == userId && q.IsActive)
.SortByDescending(q => q.CreatedAt)
.Limit(limit)
.ToListAsync();
}
catch (Exception ex)
{
_logger.LogError(ex, $"Error getting QR history for user {userId}: {ex.Message}");
return new List<QRCodeHistory>();
}
}
public async Task<QRCodeHistory?> GetQRDataAsync(string qrId)
{
try
{
return await _context.QRCodeHistory
.Find(q => q.Id == qrId && q.IsActive)
.FirstOrDefaultAsync();
}
catch (Exception ex)
{
_logger.LogError(ex, $"Error getting QR data {qrId}: {ex.Message}");
return null;
}
}
public async Task<bool> DeleteQRFromHistoryAsync(string userId, string qrId)
{
try
{
// First verify that the QR code belongs to the user
var qrCode = await _context.QRCodeHistory
.Find(q => q.Id == qrId && q.UserId == userId && q.IsActive)
.FirstOrDefaultAsync();
if (qrCode == null)
{
_logger.LogWarning($"QR code not found or doesn't belong to user - QRId: {qrId}, UserId: {userId}");
return false;
}
// Soft delete: mark as inactive instead of permanently deleting
var update = Builders<QRCodeHistory>.Update.Set(q => q.IsActive, false);
var result = await _context.QRCodeHistory.UpdateOneAsync(
q => q.Id == qrId && q.UserId == userId,
update);
return result.ModifiedCount > 0;
}
catch (Exception ex)
{
_logger.LogError(ex, $"Error deleting QR from history - QRId: {qrId}, UserId: {userId}: {ex.Message}");
return false;
}
}
public async Task<int> GetQRCountThisMonthAsync(string userId)
{
try
{
var startOfMonth = new DateTime(DateTime.UtcNow.Year, DateTime.UtcNow.Month, 1);
var endOfMonth = startOfMonth.AddMonths(1);
var count = await _context.QRCodeHistory
.CountDocumentsAsync(q => q.UserId == userId &&
q.CreatedAt >= startOfMonth &&
q.CreatedAt < endOfMonth);
return (int)count;
}
catch (Exception ex)
{
_logger.LogError(ex, $"Error getting monthly QR count for user {userId}: {ex.Message}");
return 0;
}
}
// MÉTODO REMOVIDO: ExtendAdFreeTimeAsync - não é mais necessário
public async Task<string> GetUserEmailAsync(string userId)
{
var user = await GetUserAsync(userId);
return user?.Email ?? string.Empty;
}
public async Task MarkPremiumCancelledAsync(string userId, DateTime cancelledAt)
{
try
{
if (_context.Users == null) return; // Development mode without MongoDB
var update = Builders<User>.Update
.Set(u => u.IsPremium, false)
.Set(u => u.PremiumCancelledAt, cancelledAt)
.Set(u => u.PremiumExpiresAt, null);
await _context.Users.UpdateOneAsync(u => u.Id == userId, update);
_logger.LogInformation($"Marked premium as cancelled for user {userId} at {cancelledAt}");
}
catch (Exception ex)
{
_logger.LogError(ex, $"Error marking premium cancelled for user {userId}: {ex.Message}");
}
}
public async Task<List<User>> GetUsersForHistoryCleanupAsync(DateTime cutoffDate)
{
try
{
if (_context.Users == null) return new List<User>(); // Development mode without MongoDB
return await _context.Users
.Find(u => u.PremiumCancelledAt != null &&
u.PremiumCancelledAt < cutoffDate &&
u.QRHistoryIds.Count > 0)
.ToListAsync();
}
catch (Exception ex)
{
_logger.LogError(ex, $"Error getting users for history cleanup: {ex.Message}");
return new List<User>();
}
}
public async Task DeleteUserHistoryAsync(string userId)
{
try
{
if (_context.Users == null || _context.QRCodeHistory == null) return; // Development mode without MongoDB
var user = await GetUserAsync(userId);
if (user?.QRHistoryIds?.Any() == true)
{
// Remover histórico de QR codes
await _context.QRCodeHistory.DeleteManyAsync(qr => user.QRHistoryIds.Contains(qr.Id));
// Limpar lista de histórico do usuário
var update = Builders<User>.Update.Set(u => u.QRHistoryIds, new List<string>());
await _context.Users.UpdateOneAsync(u => u.Id == userId, update);
_logger.LogInformation($"Deleted history for user {userId}");
}
}
catch (Exception ex)
{
_logger.LogError(ex, $"Error deleting history for user {userId}: {ex.Message}");
}
}
public async Task ActivatePremiumStatus(string userId, string stripeSubscriptionId, DateTime expiryDate)
{
// Verifica se é uma nova assinatura (não renovação)
var user = await GetUserAsync(userId);
var isNewSubscription = user?.StripeSubscriptionId != stripeSubscriptionId;
var updateBuilder = Builders<User>.Update
.Set(u => u.IsPremium, true)
.Set(u => u.StripeSubscriptionId, stripeSubscriptionId)
.Set(u => u.PremiumExpiresAt, expiryDate)
.Unset(u => u.PremiumCancelledAt);
// Se é nova assinatura, atualiza a data de início (para CDC 7 dias)
if (isNewSubscription)
{
updateBuilder = updateBuilder.Set(u => u.SubscriptionStartedAt, DateTime.UtcNow);
}
await _context.Users.UpdateOneAsync(u => u.Id == userId, updateBuilder);
_logger.LogInformation($"Activated premium for user {userId} (new subscription: {isNewSubscription})");
}
public async Task DeactivatePremiumStatus(string stripeSubscriptionId)
{
var update = Builders<User>.Update
.Set(u => u.IsPremium, false)
.Set(u => u.PremiumCancelledAt, DateTime.UtcNow);
await _context.Users.UpdateOneAsync(u => u.StripeSubscriptionId == stripeSubscriptionId, update);
_logger.LogInformation($"Deactivated premium for subscription {stripeSubscriptionId}");
}
public async Task<User?> GetUserByStripeCustomerIdAsync(string customerId)
{
return await _context.Users.Find(u => u.StripeCustomerId == customerId).FirstOrDefaultAsync();
}
public async Task UpdateUserStripeCustomerIdAsync(string userId, string stripeCustomerId)
{
var update = Builders<User>.Update.Set(u => u.StripeCustomerId, stripeCustomerId);
await _context.Users.UpdateOneAsync(u => u.Id == userId, update);
}
/// <summary>
/// Get QR code by tracking ID (for analytics)
/// </summary>
public async Task<QRCodeHistory?> GetQRByTrackingIdAsync(string trackingId)
{
try
{
return await _context.QRCodeHistory
.Find(q => q.TrackingId == trackingId && q.IsActive)
.FirstOrDefaultAsync();
}
catch (Exception ex)
{
_logger.LogError(ex, "Error getting QR by tracking ID {TrackingId}: {ErrorMessage}", trackingId, ex.Message);
return null;
}
}
/// <summary>
/// Increment scan count for QR code analytics
/// </summary>
public async Task IncrementQRScanCountAsync(string trackingId)
{
try
{
var filter = Builders<QRCodeHistory>.Filter.Eq(q => q.TrackingId, trackingId);
var update = Builders<QRCodeHistory>.Update
.Inc(q => q.ScanCount, 1)
.Set(q => q.LastAccessedAt, DateTime.UtcNow);
var result = await _context.QRCodeHistory.UpdateOneAsync(filter, update);
if (result.ModifiedCount > 0)
{
_logger.LogDebug("QR scan count incremented - TrackingId: {TrackingId}", trackingId);
}
else
{
_logger.LogWarning("Failed to increment scan count - TrackingId not found: {TrackingId}", trackingId);
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Error incrementing scan count for {TrackingId}: {ErrorMessage}", trackingId, ex.Message);
}
}
public async Task<bool> DeductCreditAsync(string userId)
{
try
{
var update = Builders<User>.Update.Inc(u => u.Credits, -1);
var result = await _context.Users.UpdateOneAsync(u => u.Id == userId && u.Credits > 0, update);
return result.ModifiedCount > 0;
}
catch (Exception ex)
{
_logger.LogError(ex, $"Error deducting credit for user {userId}");
return false;
}
}
public async Task<bool> AddCreditsAsync(string userId, int amount)
{
try
{
var update = Builders<User>.Update.Inc(u => u.Credits, amount);
var result = await _context.Users.UpdateOneAsync(u => u.Id == userId, update);
return result.ModifiedCount > 0;
}
catch (Exception ex)
{
_logger.LogError(ex, $"Error adding credits for user {userId}");
return false;
}
}
public async Task<bool> IncrementFreeUsageAsync(string userId)
{
try
{
// Limite de 5 QRs gratuitos vitalícios/iniciais
var user = await GetUserAsync(userId);
if (user == null || user.FreeQRsUsed >= 5) return false;
var update = Builders<User>.Update.Inc(u => u.FreeQRsUsed, 1);
await _context.Users.UpdateOneAsync(u => u.Id == userId, update);
return true;
}
catch (Exception ex)
{
_logger.LogError(ex, $"Error incrementing free usage for user {userId}");
return false;
}
}
public async Task<QRCodeHistory?> FindDuplicateQRAsync(string userId, string contentHash)
{
try
{
// Verifica se o hash existe na lista do usuário (rápido)
var user = await GetUserAsync(userId);
if (user == null || user.HistoryHashes == null || !user.HistoryHashes.Contains(contentHash))
{
return null;
}
// Se existe, busca o objeto completo no histórico
return await _context.QRCodeHistory
.Find(q => q.UserId == userId && q.ContentHash == contentHash && q.IsActive)
.SortByDescending(q => q.CreatedAt)
.FirstOrDefaultAsync();
}
catch (Exception ex)
{
_logger.LogError(ex, $"Error finding duplicate QR for user {userId}");
return null;
}
}
public async Task<bool> CheckAnonymousLimitAsync(string ipAddress, string deviceId)
{
try
{
// Definição do limite: 1 por dia
var limit = 1;
var today = DateTime.UtcNow.Date;
var tomorrow = today.AddDays(1);
// Busca QRs gerados hoje por este IP OU DeviceId
var count = await _context.QRCodeHistory
.CountDocumentsAsync(q =>
q.UserId == null && // Apenas anônimos
q.CreatedAt >= today &&
q.CreatedAt < tomorrow &&
(q.IpAddress == ipAddress || q.DeviceId == deviceId)
);
return count < limit;
}
catch (Exception ex)
{
_logger.LogError(ex, $"Error checking anonymous limit for IP {ipAddress}");
// Em caso de erro no banco (timeout, etc), bloqueia por segurança ou libera?
// Vamos liberar para não prejudicar UX em falha técnica momentânea,
// mas logamos o erro.
return true;
}
}
public async Task RegisterAnonymousUsageAsync(string ipAddress, string deviceId, string qrId)
{
try
{
var update = Builders<QRCodeHistory>.Update
.Set(q => q.IpAddress, ipAddress)
.Set(q => q.DeviceId, deviceId);
await _context.QRCodeHistory.UpdateOneAsync(q => q.Id == qrId, update);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error registering anonymous usage");
}
}
public async Task<(string rawKey, string prefix)> GenerateApiKeyAsync(string userId, string keyName = "Default")
{
var rawKey = $"qr_{Guid.NewGuid():N}{Guid.NewGuid():N}";
var prefix = rawKey.Substring(0, 8);
var hash = ComputeSha256Hash(rawKey);
var keyConfig = new ApiKeyConfig
{
KeyHash = hash,
Prefix = prefix,
Name = keyName,
CreatedAt = DateTime.UtcNow,
IsActive = true
};
var update = Builders<User>.Update.Push(u => u.ApiKeys, keyConfig);
await _context.Users.UpdateOneAsync(u => u.Id == userId, update);
return (rawKey, prefix);
}
public async Task<bool> RevokeApiKeyAsync(string userId, string prefix)
{
var filter = Builders<User>.Filter.Eq(u => u.Id, userId);
var update = Builders<User>.Update.Set("apiKeys.$[key].isActive", false);
var arrayFilters = new List<ArrayFilterDefinition> {
new BsonDocumentArrayFilterDefinition<BsonDocument>(new BsonDocument("key.prefix", prefix))
};
var result = await _context.Users.UpdateOneAsync(filter, update, new UpdateOptions { ArrayFilters = arrayFilters });
return result.ModifiedCount > 0;
}
public async Task<User?> GetUserByApiKeyAsync(string rawKey)
{
var hash = ComputeSha256Hash(rawKey);
var user = await _context.Users.Find(u => u.ApiKeys.Any(k => k.KeyHash == hash && k.IsActive)).FirstOrDefaultAsync();
if (user != null)
{
// Update last used timestamp (fire and forget)
var update = Builders<User>.Update.Set("apiKeys.$[key].lastUsedAt", DateTime.UtcNow);
var arrayFilters = new List<ArrayFilterDefinition> {
new BsonDocumentArrayFilterDefinition<BsonDocument>(new BsonDocument("key.keyHash", hash))
};
_ = _context.Users.UpdateOneAsync(u => u.Id == user.Id, update, new UpdateOptions { ArrayFilters = arrayFilters });
}
return user;
}
public async Task ActivateApiSubscriptionAsync(
string userId,
string stripeSubscriptionId,
ApiPlanTier tier,
DateTime periodEnd,
string stripeCustomerId)
{
try
{
var update = Builders<User>.Update
.Set(u => u.ApiSubscription.Tier, tier)
.Set(u => u.ApiSubscription.Status, "active")
.Set(u => u.ApiSubscription.StripeSubscriptionId, stripeSubscriptionId)
.Set(u => u.ApiSubscription.StripeCustomerId, stripeCustomerId)
.Set(u => u.ApiSubscription.CurrentPeriodEnd, periodEnd)
.Set(u => u.ApiSubscription.ActivatedAt, DateTime.UtcNow)
.Set(u => u.ApiSubscription.CanceledAt, (DateTime?)null);
await _context.Users.UpdateOneAsync(u => u.Id == userId, update);
_logger.LogInformation("API subscription activated: user={UserId} tier={Tier}", userId, tier);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error activating API subscription for user {UserId}", userId);
}
}
public async Task<User?> GetUserByApiSubscriptionIdAsync(string stripeSubscriptionId)
{
try
{
return await _context.Users
.Find(u => u.ApiSubscription.StripeSubscriptionId == stripeSubscriptionId)
.FirstOrDefaultAsync();
}
catch (Exception ex)
{
_logger.LogError(ex, "Error finding user by API subscription {SubId}", stripeSubscriptionId);
return null;
}
}
public async Task UpdateApiSubscriptionStatusAsync(
string stripeSubscriptionId,
string status,
ApiPlanTier? newTier = null,
DateTime? periodEnd = null)
{
try
{
var updateDef = Builders<User>.Update
.Set(u => u.ApiSubscription.Status, status);
if (newTier.HasValue)
updateDef = updateDef.Set(u => u.ApiSubscription.Tier, newTier.Value);
if (periodEnd.HasValue)
updateDef = updateDef.Set(u => u.ApiSubscription.CurrentPeriodEnd, periodEnd.Value);
if (status == "canceled")
updateDef = updateDef.Set(u => u.ApiSubscription.CanceledAt, DateTime.UtcNow);
await _context.Users.UpdateOneAsync(
u => u.ApiSubscription.StripeSubscriptionId == stripeSubscriptionId,
updateDef);
_logger.LogInformation("API subscription {SubId} status → {Status}", stripeSubscriptionId, status);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error updating API subscription status {SubId}", stripeSubscriptionId);
}
}
private string ComputeSha256Hash(string rawData)
{
using (var sha256Hash = System.Security.Cryptography.SHA256.Create())
{
byte[] bytes = sha256Hash.ComputeHash(System.Text.Encoding.UTF8.GetBytes(rawData));
var builder = new System.Text.StringBuilder();
for (int i = 0; i < bytes.Length; i++)
builder.Append(bytes[i].ToString("x2"));
return builder.ToString();
}
}
}
}