BTCPay-infrastructure-recovery

This commit is contained in:
sysadmin
2025-09-04 21:28:47 +01:00
parent b4cee007c4
commit be4d797c6c
22 changed files with 1552 additions and 101 deletions

View File

@@ -38,20 +38,29 @@ public class AccountController : Controller
return View();
}
if (username == "admin" && password == "admin")
// Use AuthService to validate against database users
var loginDto = new LoginDto { Username = username, Password = password };
var authResponse = await _authService.LoginAsync(loginDto);
if (authResponse != null)
{
var claims = new List<Claim>
// Get the actual user from database to get correct ID
var user = await _authService.GetUserByUsernameAsync(username);
if (user != null)
{
new(ClaimTypes.Name, "admin"),
new(ClaimTypes.NameIdentifier, Guid.NewGuid().ToString()),
new(ClaimTypes.Role, "Admin")
};
var claims = new List<Claim>
{
new(ClaimTypes.Name, user.Username),
new(ClaimTypes.NameIdentifier, user.Id.ToString()), // Use real database ID
new(ClaimTypes.Role, "Admin") // All users in admin system are admins
};
var identity = new ClaimsIdentity(claims, "Cookies");
var principal = new ClaimsPrincipal(identity);
var identity = new ClaimsIdentity(claims, "Cookies");
var principal = new ClaimsPrincipal(identity);
await HttpContext.SignInAsync("Cookies", principal);
return RedirectToAction("Index", "Dashboard");
await HttpContext.SignInAsync("Cookies", principal);
return RedirectToAction("Index", "Dashboard");
}
}
ModelState.AddModelError("", "Invalid username or password");

View File

@@ -30,19 +30,28 @@ public class UsersController : Controller
[HttpPost]
public async Task<IActionResult> Create(CreateUserDto model)
{
if (!ModelState.IsValid)
try
{
if (!ModelState.IsValid)
{
return View(model);
}
var user = await _authService.CreateUserAsync(model);
if (user == null)
{
ModelState.AddModelError("Username", "User with this username already exists");
return View(model);
}
TempData["SuccessMessage"] = $"User '{user.Username}' created successfully";
return RedirectToAction(nameof(Index));
}
catch (Exception ex)
{
ModelState.AddModelError("", "An error occurred while creating the user: " + ex.Message);
return View(model);
}
var user = await _authService.CreateUserAsync(model);
if (user == null)
{
ModelState.AddModelError("", "User with this username already exists");
return View(model);
}
return RedirectToAction(nameof(Index));
}
public async Task<IActionResult> Edit(Guid id)
@@ -66,25 +75,89 @@ public class UsersController : Controller
[HttpPost]
public async Task<IActionResult> Edit(Guid id, UpdateUserDto model)
{
if (!ModelState.IsValid)
try
{
// Additional validation for required username
if (string.IsNullOrWhiteSpace(model.Username))
{
ModelState.AddModelError("Username", "Username is required");
}
// Validate password if provided
if (!string.IsNullOrEmpty(model.Password) && model.Password.Length < 3)
{
ModelState.AddModelError("Password", "Password must be at least 3 characters if changing");
}
if (!ModelState.IsValid)
{
ViewData["UserId"] = id;
return View(model);
}
var success = await _authService.UpdateUserAsync(id, model);
if (!success)
{
// Check if it's because of duplicate username
var existingUser = await _authService.GetUserByIdAsync(id);
if (existingUser == null)
{
return NotFound();
}
ModelState.AddModelError("Username", "Username is already taken by another user");
ViewData["UserId"] = id;
return View(model);
}
TempData["SuccessMessage"] = "User updated successfully";
return RedirectToAction(nameof(Index));
}
catch (Exception ex)
{
ModelState.AddModelError("", "An error occurred while updating the user: " + ex.Message);
ViewData["UserId"] = id;
return View(model);
}
var success = await _authService.UpdateUserAsync(id, model);
if (!success)
{
return NotFound();
}
return RedirectToAction(nameof(Index));
}
[HttpPost]
public async Task<IActionResult> Delete(Guid id)
{
await _authService.DeleteUserAsync(id);
return RedirectToAction(nameof(Index));
try
{
// Prevent admin user from deleting themselves
var currentUserIdClaim = User.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value;
if (Guid.TryParse(currentUserIdClaim, out Guid currentUserId) && currentUserId == id)
{
TempData["ErrorMessage"] = "You cannot delete your own account";
return RedirectToAction(nameof(Index));
}
// Get user info for confirmation message
var user = await _authService.GetUserByIdAsync(id);
if (user == null)
{
TempData["ErrorMessage"] = "User not found";
return RedirectToAction(nameof(Index));
}
var success = await _authService.DeleteUserAsync(id);
if (success)
{
TempData["SuccessMessage"] = $"User '{user.Username}' has been deactivated";
}
else
{
TempData["ErrorMessage"] = "Failed to delete user";
}
return RedirectToAction(nameof(Index));
}
catch (Exception ex)
{
TempData["ErrorMessage"] = "An error occurred while deleting the user: " + ex.Message;
return RedirectToAction(nameof(Index));
}
}
}

View File

@@ -0,0 +1,82 @@
@model LittleShop.DTOs.UpdateUserDto
@{
ViewData["Title"] = "Edit User";
var userId = ViewData["UserId"] as Guid?;
}
<div class="row mb-4">
<div class="col">
<h1><i class="fas fa-user-edit"></i> Edit User</h1>
</div>
</div>
<div class="row">
<div class="col-md-8">
<div class="card">
<div class="card-body">
<form method="post" asp-area="Admin" asp-controller="Users" asp-action="Edit" asp-route-id="@userId">
@Html.AntiForgeryToken()
@if (ViewData.ModelState[""] != null && ViewData.ModelState[""].Errors.Count > 0)
{
<div class="alert alert-danger" role="alert">
@foreach (var error in ViewData.ModelState[""].Errors)
{
<div>@error.ErrorMessage</div>
}
</div>
}
<div class="mb-3">
<label for="Username" class="form-label">Username</label>
<input name="Username" id="Username" class="form-control" value="@Model?.Username" required />
<div class="form-text">Must be unique across all users</div>
</div>
<div class="mb-3">
<label for="Password" class="form-label">New Password</label>
<input name="Password" id="Password" type="password" class="form-control" />
<div class="form-text">Leave blank to keep current password. Minimum 3 characters if changing.</div>
</div>
<div class="mb-3 form-check">
<input name="IsActive" id="IsActive" type="checkbox" class="form-check-input" value="true" @(Model?.IsActive == true ? "checked" : "") />
<input name="IsActive" type="hidden" value="false" />
<label for="IsActive" class="form-check-label">
User is active (can log in)
</label>
</div>
<div class="d-flex justify-content-between">
<a href="@Url.Action("Index")" class="btn btn-secondary">
<i class="fas fa-arrow-left"></i> Back to Users
</a>
<button type="submit" class="btn btn-primary">
<i class="fas fa-save"></i> Update User
</button>
</div>
</form>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card">
<div class="card-header">
<h5><i class="fas fa-info-circle"></i> Edit Information</h5>
</div>
<div class="card-body">
<ul class="list-unstyled">
<li><strong>Username:</strong> Can be changed if unique</li>
<li><strong>Password:</strong> Optional - leave blank to keep current</li>
<li><strong>Status:</strong> Inactive users cannot log in</li>
</ul>
<div class="alert alert-warning mt-3">
<i class="fas fa-exclamation-triangle"></i>
<strong>Warning:</strong> Deactivating your own account will lock you out.
</div>
</div>
</div>
</div>
</div>

View File

@@ -15,6 +15,22 @@
</div>
</div>
@if (TempData["SuccessMessage"] != null)
{
<div class="alert alert-success alert-dismissible fade show" role="alert">
<i class="fas fa-check-circle"></i> @TempData["SuccessMessage"]
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
}
@if (TempData["ErrorMessage"] != null)
{
<div class="alert alert-danger alert-dismissible fade show" role="alert">
<i class="fas fa-exclamation-circle"></i> @TempData["ErrorMessage"]
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
}
<div class="card">
<div class="card-body">
@if (Model.Any())

View File

@@ -0,0 +1,180 @@
using Microsoft.AspNetCore.Mvc;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using LittleShop.DTOs;
using LittleShop.Services;
using LittleShop.Enums;
namespace LittleShop.Controllers;
[ApiController]
[Route("api/btcpay")]
public class BTCPayWebhookController : ControllerBase
{
private readonly ICryptoPaymentService _cryptoPaymentService;
private readonly IBTCPayServerService _btcPayService;
private readonly IConfiguration _configuration;
private readonly ILogger<BTCPayWebhookController> _logger;
public BTCPayWebhookController(
ICryptoPaymentService cryptoPaymentService,
IBTCPayServerService btcPayService,
IConfiguration configuration,
ILogger<BTCPayWebhookController> logger)
{
_cryptoPaymentService = cryptoPaymentService;
_btcPayService = btcPayService;
_configuration = configuration;
_logger = logger;
}
[HttpPost("webhook")]
public async Task<IActionResult> ProcessWebhook()
{
try
{
// Read the raw request body
using var reader = new StreamReader(Request.Body);
var requestBody = await reader.ReadToEndAsync();
// Get webhook signature from headers
var signature = Request.Headers["BTCPAY-SIG"].FirstOrDefault();
if (string.IsNullOrEmpty(signature))
{
_logger.LogWarning("Webhook received without signature");
return BadRequest("Missing webhook signature");
}
// Validate webhook signature
var webhookSecret = _configuration["BTCPayServer:WebhookSecret"];
if (string.IsNullOrEmpty(webhookSecret))
{
_logger.LogError("BTCPay webhook secret not configured");
return StatusCode(500, "Webhook validation not configured");
}
if (!ValidateWebhookSignature(requestBody, signature, webhookSecret))
{
_logger.LogWarning("Invalid webhook signature");
return BadRequest("Invalid webhook signature");
}
// Parse webhook data
var webhookData = JsonSerializer.Deserialize<BTCPayWebhookDto>(requestBody, new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true
});
if (webhookData == null)
{
_logger.LogWarning("Unable to parse webhook data");
return BadRequest("Invalid webhook data");
}
_logger.LogInformation("Processing BTCPay webhook: Type={Type}, InvoiceId={InvoiceId}, StoreId={StoreId}",
webhookData.Type, webhookData.InvoiceId, webhookData.StoreId);
// Process the webhook based on event type
var success = await ProcessWebhookEvent(webhookData);
if (!success)
{
return BadRequest("Failed to process webhook");
}
return Ok();
}
catch (Exception ex)
{
_logger.LogError(ex, "Error processing BTCPay webhook");
return StatusCode(500, "Internal server error");
}
}
private bool ValidateWebhookSignature(string payload, string signature, string secret)
{
try
{
// BTCPay Server uses HMAC-SHA256 with format "sha256=<hex>"
if (!signature.StartsWith("sha256="))
{
return false;
}
var expectedHash = signature.Substring(7); // Remove "sha256=" prefix
var secretBytes = Encoding.UTF8.GetBytes(secret);
var payloadBytes = Encoding.UTF8.GetBytes(payload);
using var hmac = new HMACSHA256(secretBytes);
var computedHash = hmac.ComputeHash(payloadBytes);
var computedHashHex = Convert.ToHexString(computedHash).ToLowerInvariant();
return expectedHash.Equals(computedHashHex, StringComparison.OrdinalIgnoreCase);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error validating webhook signature");
return false;
}
}
private async Task<bool> ProcessWebhookEvent(BTCPayWebhookDto webhookData)
{
try
{
// Map BTCPay webhook event types to our payment status
var paymentStatus = MapWebhookEventToPaymentStatus(webhookData.Type);
if (!paymentStatus.HasValue)
{
_logger.LogInformation("Ignoring webhook event type: {Type}", webhookData.Type);
return true; // Not an error, just not a status we care about
}
// Extract payment details
var amount = webhookData.Payment?.PaymentMethodPaid ?? 0;
var transactionHash = webhookData.Payment?.TransactionData?.TransactionHash;
// Process the payment update
var success = await _cryptoPaymentService.ProcessPaymentWebhookAsync(
webhookData.InvoiceId,
paymentStatus.Value,
amount,
transactionHash);
if (success)
{
_logger.LogInformation("Successfully processed webhook for invoice {InvoiceId} with status {Status}",
webhookData.InvoiceId, paymentStatus.Value);
}
else
{
_logger.LogWarning("Failed to process webhook for invoice {InvoiceId}", webhookData.InvoiceId);
}
return success;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error processing webhook event for invoice {InvoiceId}", webhookData.InvoiceId);
return false;
}
}
private static PaymentStatus? MapWebhookEventToPaymentStatus(string eventType)
{
return eventType switch
{
"InvoiceCreated" => PaymentStatus.Pending,
"InvoiceReceivedPayment" => PaymentStatus.Processing,
"InvoicePaymentSettled" => PaymentStatus.Completed,
"InvoiceProcessing" => PaymentStatus.Processing,
"InvoiceExpired" => PaymentStatus.Expired,
"InvoiceSettled" => PaymentStatus.Completed,
"InvoiceInvalid" => PaymentStatus.Failed,
_ => null // Unknown event type
};
}
}

View File

@@ -44,9 +44,15 @@ public class PushNotificationController : ControllerBase
try
{
var userIdClaim = User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
var username = User.FindFirst(ClaimTypes.Name)?.Value ?? User.Identity?.Name;
// Debug logging
var logger = HttpContext.RequestServices.GetRequiredService<ILogger<PushNotificationController>>();
logger.LogInformation("Push subscription attempt - UserIdClaim: {UserIdClaim}, Username: {Username}", userIdClaim, username);
if (string.IsNullOrEmpty(userIdClaim) || !Guid.TryParse(userIdClaim, out Guid userId))
{
return Unauthorized("Invalid user ID");
return Unauthorized(new { error = "Invalid user ID", userIdClaim, username });
}
var userAgent = Request.Headers.UserAgent.ToString();
@@ -65,6 +71,8 @@ public class PushNotificationController : ControllerBase
}
catch (Exception ex)
{
var logger = HttpContext.RequestServices.GetRequiredService<ILogger<PushNotificationController>>();
logger.LogError(ex, "Push subscription error");
return StatusCode(500, new { error = ex.Message });
}
}

View File

@@ -0,0 +1,94 @@
using System.Text.Json.Serialization;
namespace LittleShop.DTOs;
/// <summary>
/// DTO for BTCPay Server webhook events
/// Based on BTCPay Server webhook documentation
/// </summary>
public class BTCPayWebhookDto
{
[JsonPropertyName("deliveryId")]
public string DeliveryId { get; set; } = string.Empty;
[JsonPropertyName("webhookId")]
public string WebhookId { get; set; } = string.Empty;
[JsonPropertyName("originalDeliveryId")]
public string? OriginalDeliveryId { get; set; }
[JsonPropertyName("isRedelivery")]
public bool IsRedelivery { get; set; }
[JsonPropertyName("type")]
public string Type { get; set; } = string.Empty;
[JsonPropertyName("timestamp")]
public long Timestamp { get; set; }
[JsonPropertyName("storeId")]
public string StoreId { get; set; } = string.Empty;
[JsonPropertyName("invoiceId")]
public string InvoiceId { get; set; } = string.Empty;
[JsonPropertyName("afterExpiration")]
public bool? AfterExpiration { get; set; }
[JsonPropertyName("manuallyMarked")]
public bool? ManuallyMarked { get; set; }
[JsonPropertyName("overPaid")]
public bool? OverPaid { get; set; }
[JsonPropertyName("partiallyPaid")]
public bool? PartiallyPaid { get; set; }
[JsonPropertyName("payment")]
public BTCPayWebhookPayment? Payment { get; set; }
}
public class BTCPayWebhookPayment
{
[JsonPropertyName("id")]
public string Id { get; set; } = string.Empty;
[JsonPropertyName("receivedDate")]
public long ReceivedDate { get; set; }
[JsonPropertyName("value")]
public decimal Value { get; set; }
[JsonPropertyName("fee")]
public decimal? Fee { get; set; }
[JsonPropertyName("status")]
public string Status { get; set; } = string.Empty;
[JsonPropertyName("destination")]
public string? Destination { get; set; }
[JsonPropertyName("paymentMethod")]
public string PaymentMethod { get; set; } = string.Empty;
[JsonPropertyName("paymentMethodPaid")]
public decimal PaymentMethodPaid { get; set; }
[JsonPropertyName("transactionData")]
public BTCPayWebhookTransactionData? TransactionData { get; set; }
}
public class BTCPayWebhookTransactionData
{
[JsonPropertyName("transactionHash")]
public string? TransactionHash { get; set; }
[JsonPropertyName("blockHash")]
public string? BlockHash { get; set; }
[JsonPropertyName("blockHeight")]
public int? BlockHeight { get; set; }
[JsonPropertyName("confirmations")]
public int? Confirmations { get; set; }
}

View File

@@ -103,6 +103,21 @@ public class AuthService : IAuthService
};
}
public async Task<UserDto?> GetUserByUsernameAsync(string username)
{
var user = await _context.Users
.FirstOrDefaultAsync(u => u.Username == username && u.IsActive);
if (user == null) return null;
return new UserDto
{
Id = user.Id,
Username = user.Username,
CreatedAt = user.CreatedAt,
IsActive = user.IsActive
};
}
public async Task<IEnumerable<UserDto>> GetAllUsersAsync()
{
return await _context.Users

View File

@@ -23,11 +23,33 @@ public class DataSeederService : IDataSeederService
public async Task SeedSampleDataAsync()
{
// Check if we already have data
var hasCategories = await _context.Categories.AnyAsync();
if (hasCategories)
await SeedProductionDataAsync();
}
private async Task SeedProductionDataAsync()
{
_logger.LogInformation("Setting up production-ready catalog...");
// Clean up existing test products first (excluding valid products that just need stock update)
var testProducts = await _context.Products
.Where(p => p.Name.Contains("JAMES") || p.Name.Contains("dsasada") || p.Name.Contains("asdsads"))
.ToListAsync();
if (testProducts.Any())
{
_logger.LogInformation("Sample data already exists, skipping seed");
_context.Products.RemoveRange(testProducts);
await _context.SaveChangesAsync();
_logger.LogInformation("Removed {Count} test products", testProducts.Count);
}
// Check if we need to create production catalog or update stock
var hasProductionProducts = await _context.Products
.AnyAsync(p => p.Name.Contains("Wireless Noise-Cancelling Headphones"));
if (hasProductionProducts)
{
// Update stock for existing production products
await UpdateProductionStockAsync();
return;
}
@@ -69,75 +91,161 @@ public class DataSeederService : IDataSeederService
await _context.SaveChangesAsync();
_logger.LogInformation("Created {Count} categories", categories.Count);
// Create Products
// Ensure we have categories before creating products
var electronicsCategory = await _context.Categories.FirstOrDefaultAsync(c => c.Name == "Electronics");
var clothingCategory = await _context.Categories.FirstOrDefaultAsync(c => c.Name == "Clothing");
var booksCategory = await _context.Categories.FirstOrDefaultAsync(c => c.Name == "Books");
if (electronicsCategory == null || clothingCategory == null || booksCategory == null)
{
_logger.LogWarning("Categories not found, creating them first");
// Categories would be created by the original seeder logic above
}
// Create Production-Ready Products with proper stock
var products = new List<Product>
{
// ELECTRONICS - High-margin, popular items
new Product
{
Id = Guid.NewGuid(),
Name = "Wireless Headphones",
Description = "High-quality Bluetooth headphones with noise cancellation",
Price = 89.99m,
Weight = 250,
WeightUnit = ProductWeightUnit.Grams,
StockQuantity = 10,
CategoryId = categories[0].Id,
IsActive = true,
CreatedAt = DateTime.UtcNow,
UpdatedAt = DateTime.UtcNow
},
new Product
{
Id = Guid.NewGuid(),
Name = "Smartphone Case",
Description = "Durable protective case for latest smartphones",
Price = 19.99m,
Weight = 50,
WeightUnit = ProductWeightUnit.Grams,
StockQuantity = 10,
CategoryId = categories[0].Id,
IsActive = true,
CreatedAt = DateTime.UtcNow,
UpdatedAt = DateTime.UtcNow
},
new Product
{
Id = Guid.NewGuid(),
Name = "T-Shirt",
Description = "100% cotton comfortable t-shirt",
Price = 24.99m,
Weight = 200,
WeightUnit = ProductWeightUnit.Grams,
StockQuantity = 15,
CategoryId = categories[1].Id,
IsActive = true,
CreatedAt = DateTime.UtcNow,
UpdatedAt = DateTime.UtcNow
},
new Product
{
Id = Guid.NewGuid(),
Name = "Jeans",
Description = "Classic denim jeans",
Price = 59.99m,
Weight = 500,
WeightUnit = ProductWeightUnit.Grams,
StockQuantity = 15,
CategoryId = categories[1].Id,
IsActive = true,
CreatedAt = DateTime.UtcNow,
UpdatedAt = DateTime.UtcNow
},
new Product
{
Id = Guid.NewGuid(),
Name = "Programming Book",
Description = "Learn programming with practical examples",
Price = 34.99m,
Weight = 800,
Name = "Wireless Noise-Cancelling Headphones",
Description = "Premium Bluetooth 5.0 headphones with active noise cancellation, 30-hour battery life, and crystal-clear audio. Perfect for music, calls, and travel. Includes carrying case and charging cable.",
Price = 149.99m,
Weight = 280,
WeightUnit = ProductWeightUnit.Grams,
StockQuantity = 25,
CategoryId = categories[2].Id,
CategoryId = electronicsCategory?.Id ?? categories[0].Id,
IsActive = true,
CreatedAt = DateTime.UtcNow,
UpdatedAt = DateTime.UtcNow
},
new Product
{
Id = Guid.NewGuid(),
Name = "Fast Wireless Charging Stand",
Description = "15W fast wireless charger compatible with iPhone, Samsung, and Qi-enabled devices. Anti-slip base, LED indicator, includes AC adapter. Charge through most phone cases up to 5mm thick.",
Price = 34.99m,
Weight = 180,
WeightUnit = ProductWeightUnit.Grams,
StockQuantity = 50,
CategoryId = electronicsCategory?.Id ?? categories[0].Id,
IsActive = true,
CreatedAt = DateTime.UtcNow,
UpdatedAt = DateTime.UtcNow
},
new Product
{
Id = Guid.NewGuid(),
Name = "Ultra-Slim Power Bank 20,000mAh",
Description = "High-capacity portable charger with dual USB-A and USB-C ports. Fast charging technology, digital display shows remaining power. Charges iPhone 13 up to 4 times, includes USB-C cable.",
Price = 59.99m,
Weight = 450,
WeightUnit = ProductWeightUnit.Grams,
StockQuantity = 35,
CategoryId = electronicsCategory?.Id ?? categories[0].Id,
IsActive = true,
CreatedAt = DateTime.UtcNow,
UpdatedAt = DateTime.UtcNow
},
new Product
{
Id = Guid.NewGuid(),
Name = "Premium Phone Case with MagSafe",
Description = "Military-grade protection with built-in MagSafe compatibility. Drop-tested to 12 feet, raised camera and screen edges, clear back shows your phone's design. Compatible with iPhone 14/15 series.",
Price = 29.99m,
Weight = 65,
WeightUnit = ProductWeightUnit.Grams,
StockQuantity = 75,
CategoryId = electronicsCategory?.Id ?? categories[0].Id,
IsActive = true,
CreatedAt = DateTime.UtcNow,
UpdatedAt = DateTime.UtcNow
},
// CLOTHING - Essential wardrobe items
new Product
{
Id = Guid.NewGuid(),
Name = "Premium Cotton T-Shirt",
Description = "100% organic cotton, pre-shrunk, tagless design. Soft, breathable fabric in classic fit. Available in multiple colors. Perfect for casual wear or layering. Machine washable, retains shape after washing.",
Price = 24.99m,
Weight = 180,
WeightUnit = ProductWeightUnit.Grams,
StockQuantity = 100,
CategoryId = clothingCategory?.Id ?? categories[1].Id,
IsActive = true,
CreatedAt = DateTime.UtcNow,
UpdatedAt = DateTime.UtcNow
},
new Product
{
Id = Guid.NewGuid(),
Name = "Classic Denim Jeans",
Description = "Premium denim with perfect stretch for comfort. Classic 5-pocket styling, reinforced stress points, fade-resistant color. Available in multiple washes and sizes. Timeless style that works with everything.",
Price = 79.99m,
Weight = 650,
WeightUnit = ProductWeightUnit.Grams,
StockQuantity = 60,
CategoryId = clothingCategory?.Id ?? categories[1].Id,
IsActive = true,
CreatedAt = DateTime.UtcNow,
UpdatedAt = DateTime.UtcNow
},
new Product
{
Id = Guid.NewGuid(),
Name = "Cozy Knit Sweater",
Description = "Soft merino wool blend, lightweight yet warm. Crew neck design, ribbed cuffs and hem. Perfect for layering or wearing alone. Hand-washable, pill-resistant fabric maintains shape and softness.",
Price = 89.99m,
Weight = 320,
WeightUnit = ProductWeightUnit.Grams,
StockQuantity = 40,
CategoryId = clothingCategory?.Id ?? categories[1].Id,
IsActive = true,
CreatedAt = DateTime.UtcNow,
UpdatedAt = DateTime.UtcNow
},
// BOOKS - Knowledge and entertainment
new Product
{
Id = Guid.NewGuid(),
Name = "The Complete Guide to Cryptocurrency",
Description = "Comprehensive guide to understanding Bitcoin, Ethereum, DeFi, and blockchain technology. Written for beginners and enthusiasts. 400+ pages with real-world examples, investment strategies, and security tips.",
Price = 39.99m,
Weight = 580,
WeightUnit = ProductWeightUnit.Grams,
StockQuantity = 30,
CategoryId = booksCategory?.Id ?? categories[2].Id,
IsActive = true,
CreatedAt = DateTime.UtcNow,
UpdatedAt = DateTime.UtcNow
},
new Product
{
Id = Guid.NewGuid(),
Name = "Modern Web Development Handbook",
Description = "Learn React, Node.js, and modern JavaScript. Hands-on projects, best practices, and deployment strategies. Includes access to online code repository and video tutorials. Perfect for career advancement.",
Price = 49.99m,
Weight = 720,
WeightUnit = ProductWeightUnit.Grams,
StockQuantity = 25,
CategoryId = booksCategory?.Id ?? categories[2].Id,
IsActive = true,
CreatedAt = DateTime.UtcNow,
UpdatedAt = DateTime.UtcNow
},
new Product
{
Id = Guid.NewGuid(),
Name = "Mindfulness and Productivity Journal",
Description = "Daily planner with mindfulness exercises and productivity techniques. 6-month undated format, premium paper, goal-setting frameworks. Improve focus, reduce stress, achieve work-life balance.",
Price = 27.99m,
Weight = 380,
WeightUnit = ProductWeightUnit.Grams,
StockQuantity = 45,
CategoryId = booksCategory?.Id ?? categories[2].Id,
IsActive = true,
CreatedAt = DateTime.UtcNow,
UpdatedAt = DateTime.UtcNow
@@ -488,4 +596,44 @@ public class DataSeederService : IDataSeederService
_logger.LogInformation("Sample data seeding completed successfully!");
}
private async Task UpdateProductionStockAsync()
{
_logger.LogInformation("Updating production product stock levels...");
var productStockUpdates = new Dictionary<string, int>
{
["Wireless Noise-Cancelling Headphones"] = 25,
["Fast Wireless Charging Stand"] = 50,
["Ultra-Slim Power Bank 20,000mAh"] = 35,
["Premium Phone Case with MagSafe"] = 75,
["Premium Cotton T-Shirt"] = 100,
["Classic Denim Jeans"] = 60,
["Cozy Knit Sweater"] = 40,
["The Complete Guide to Cryptocurrency"] = 30,
["Modern Web Development Handbook"] = 25,
["Mindfulness and Productivity Journal"] = 45
};
foreach (var update in productStockUpdates)
{
var product = await _context.Products
.FirstOrDefaultAsync(p => p.Name == update.Key);
if (product != null)
{
var oldStock = product.StockQuantity;
product.StockQuantity = update.Value;
product.UpdatedAt = DateTime.UtcNow;
_logger.LogInformation("Updated stock for {Product} from {OldStock} to {NewStock}", product.Name, oldStock, update.Value);
}
else
{
_logger.LogWarning("Product not found: {ProductName}", update.Key);
}
}
await _context.SaveChangesAsync();
_logger.LogInformation("Production stock update completed!");
}
}

View File

@@ -8,6 +8,7 @@ public interface IAuthService
Task<bool> SeedDefaultUserAsync();
Task<UserDto?> CreateUserAsync(CreateUserDto createUserDto);
Task<UserDto?> GetUserByIdAsync(Guid id);
Task<UserDto?> GetUserByUsernameAsync(string username);
Task<IEnumerable<UserDto>> GetAllUsersAsync();
Task<bool> DeleteUserAsync(Guid id);
Task<bool> UpdateUserAsync(Guid id, UpdateUserDto updateUserDto);

View File

@@ -30,6 +30,7 @@ public class ProductService : IProductService
Price = p.Price,
Weight = p.Weight,
WeightUnit = p.WeightUnit,
StockQuantity = p.StockQuantity,
CategoryId = p.CategoryId,
CategoryName = p.Category.Name,
CreatedAt = p.CreatedAt,
@@ -61,6 +62,7 @@ public class ProductService : IProductService
Price = p.Price,
Weight = p.Weight,
WeightUnit = p.WeightUnit,
StockQuantity = p.StockQuantity,
CategoryId = p.CategoryId,
CategoryName = p.Category.Name,
CreatedAt = p.CreatedAt,
@@ -309,6 +311,7 @@ public class ProductService : IProductService
Price = p.Price,
Weight = p.Weight,
WeightUnit = p.WeightUnit,
StockQuantity = p.StockQuantity,
CategoryId = p.CategoryId,
CategoryName = p.Category.Name,
CreatedAt = p.CreatedAt,

View File

@@ -40,6 +40,14 @@ public class PushNotificationService : IPushNotificationService
{
try
{
// Check if the user actually exists in the database
var userExists = await _context.Users.AnyAsync(u => u.Id == userId);
if (!userExists)
{
Log.Warning("Attempted to subscribe non-existent user {UserId} to push notifications", userId);
return false;
}
// Check if subscription already exists
var existingSubscription = await _context.PushSubscriptions
.FirstOrDefaultAsync(ps => ps.Endpoint == subscriptionDto.Endpoint && ps.UserId == userId);
@@ -53,6 +61,7 @@ public class PushNotificationService : IPushNotificationService
existingSubscription.IsActive = true;
existingSubscription.UserAgent = userAgent;
existingSubscription.IpAddress = ipAddress;
Log.Information("Updated existing push subscription for user {UserId}", userId);
}
else
{
@@ -71,10 +80,11 @@ public class PushNotificationService : IPushNotificationService
};
_context.PushSubscriptions.Add(subscription);
Log.Information("Created new push subscription for user {UserId}", userId);
}
await _context.SaveChangesAsync();
Log.Information("Push subscription created/updated for user {UserId}", userId);
Log.Information("Push subscription saved successfully for user {UserId}", userId);
return true;
}
catch (Exception ex)

View File

@@ -10,8 +10,8 @@
},
"BTCPayServer": {
"BaseUrl": "https://pay.silverlabs.uk",
"ApiKey": "885a65ead85b87d5a10095b6cb6ad87866988cc2",
"StoreId": "51kbAYszqX2gEK2E9EYwqbixcDmsafuBXukx7v1PrZUD",
"ApiKey": "994589c8b514531f867dd24c83a02b6381a5f4a2",
"StoreId": "AoxXjM9NJT6P9C1MErkaawXaSchz8sFPYdQ9FyhmQz33",
"WebhookSecret": ""
},
"RoyalMail": {