752 lines
29 KiB
C#
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();
|
|
}
|
|
}
|
|
}
|
|
} |