- Install Node.js 18.x in Docker build stage - Add MongoDB DataProtection for Swarm compatibility - Enables shared authentication keys across multiple replicas 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
442 lines
16 KiB
C#
442 lines
16 KiB
C#
using MongoDB.Driver;
|
|
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)
|
|
{
|
|
try
|
|
{
|
|
var qrHistory = new QRCodeHistory
|
|
{
|
|
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
|
|
};
|
|
|
|
await _context.QRCodeHistory.InsertOneAsync(qrHistory);
|
|
|
|
// Update user's QR history IDs if logged in
|
|
if (!string.IsNullOrEmpty(userId))
|
|
{
|
|
var update = Builders<User>.Update
|
|
.Push(u => u.QRHistoryIds, qrHistory.Id);
|
|
await _context.Users.UpdateOneAsync(u => u.Id == userId, update);
|
|
}
|
|
}
|
|
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)
|
|
{
|
|
var update = Builders<User>.Update
|
|
.Set(u => u.IsPremium, true)
|
|
.Set(u => u.StripeSubscriptionId, stripeSubscriptionId)
|
|
.Set(u => u.PremiumExpiresAt, expiryDate)
|
|
.Unset(u => u.PremiumCancelledAt);
|
|
|
|
await _context.Users.UpdateOneAsync(u => u.Id == userId, update);
|
|
_logger.LogInformation($"Activated premium for user {userId}");
|
|
}
|
|
|
|
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);
|
|
}
|
|
}
|
|
} |