BTCPay-infrastructure-recovery
This commit is contained in:
@@ -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");
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
82
LittleShop/Areas/Admin/Views/Users/Edit.cshtml
Normal file
82
LittleShop/Areas/Admin/Views/Users/Edit.cshtml
Normal 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>
|
||||
@@ -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())
|
||||
|
||||
180
LittleShop/Controllers/BTCPayWebhookController.cs
Normal file
180
LittleShop/Controllers/BTCPayWebhookController.cs
Normal 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
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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 });
|
||||
}
|
||||
}
|
||||
|
||||
94
LittleShop/DTOs/BTCPayWebhookDto.cs
Normal file
94
LittleShop/DTOs/BTCPayWebhookDto.cs
Normal 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; }
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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!");
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -10,8 +10,8 @@
|
||||
},
|
||||
"BTCPayServer": {
|
||||
"BaseUrl": "https://pay.silverlabs.uk",
|
||||
"ApiKey": "885a65ead85b87d5a10095b6cb6ad87866988cc2",
|
||||
"StoreId": "51kbAYszqX2gEK2E9EYwqbixcDmsafuBXukx7v1PrZUD",
|
||||
"ApiKey": "994589c8b514531f867dd24c83a02b6381a5f4a2",
|
||||
"StoreId": "AoxXjM9NJT6P9C1MErkaawXaSchz8sFPYdQ9FyhmQz33",
|
||||
"WebhookSecret": ""
|
||||
},
|
||||
"RoyalMail": {
|
||||
|
||||
Reference in New Issue
Block a user