feat: Add customer management, payments, and push notifications with security enhancements
Some checks failed
Build and Deploy LittleShop / Build TeleBot Docker Image (push) Failing after 11s
Build and Deploy LittleShop / Build LittleShop Docker Image (push) Failing after 15s
Build and Deploy LittleShop / Deploy to Production VPS (Manual Only) (push) Has been skipped
Build and Deploy LittleShop / Deploy to Pre-Production (CT109) (push) Has been skipped
Some checks failed
Build and Deploy LittleShop / Build TeleBot Docker Image (push) Failing after 11s
Build and Deploy LittleShop / Build LittleShop Docker Image (push) Failing after 15s
Build and Deploy LittleShop / Deploy to Production VPS (Manual Only) (push) Has been skipped
Build and Deploy LittleShop / Deploy to Pre-Production (CT109) (push) Has been skipped
Major Feature Additions: - Customer management: Full CRUD with data export and privacy compliance - Payment management: Centralized payment tracking and administration - Push notification subscriptions: Manage and track web push subscriptions Security Enhancements: - IP whitelist middleware for administrative endpoints - Data retention service with configurable policies - Enhanced push notification security documentation - Security fixes progress tracking (2025-11-14) UI/UX Improvements: - Enhanced navigation with improved mobile responsiveness - Updated admin dashboard with order status counts - Improved product CRUD forms - New customer and payment management interfaces Backend Improvements: - Extended customer service with data export capabilities - Enhanced order service with status count queries - Improved crypto payment service with better error handling - Updated validators and configuration Documentation: - DEPLOYMENT_NGINX_GUIDE.md: Nginx deployment instructions - IP_STORAGE_ANALYSIS.md: IP storage security analysis - PUSH_NOTIFICATION_SECURITY.md: Push notification security guide - UI_UX_IMPROVEMENT_PLAN.md: Planned UI/UX enhancements - UI_UX_IMPROVEMENTS_COMPLETED.md: Completed improvements Cleanup: - Removed temporary database WAL files - Removed stale commit message file 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -28,14 +28,12 @@ public class AccountController : Controller
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
// [ValidateAntiForgeryToken] // Temporarily disabled for HTTPS proxy issue
|
||||
[ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> Login(string Username, string Password)
|
||||
{
|
||||
// Make parameters case-insensitive for form compatibility
|
||||
var username = Username?.ToLowerInvariant();
|
||||
var password = Password;
|
||||
|
||||
Console.WriteLine($"Received Username: '{username}', Password: '{password}'");
|
||||
|
||||
if (string.IsNullOrEmpty(username) || string.IsNullOrEmpty(password))
|
||||
{
|
||||
|
||||
@@ -74,16 +74,6 @@ public class BotsController : Controller
|
||||
[ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> Wizard(BotWizardDto dto)
|
||||
{
|
||||
Console.WriteLine("=== BOT WIZARD DEBUG ===");
|
||||
Console.WriteLine($"Received: BotName='{dto?.BotName}', BotUsername='{dto?.BotUsername}', PersonalityName='{dto?.PersonalityName}'");
|
||||
Console.WriteLine($"ModelState.IsValid: {ModelState.IsValid}");
|
||||
Console.WriteLine("Raw form data:");
|
||||
foreach (var key in Request.Form.Keys)
|
||||
{
|
||||
Console.WriteLine($" {key} = '{Request.Form[key]}'");
|
||||
}
|
||||
Console.WriteLine("========================");
|
||||
|
||||
_logger.LogInformation("Wizard POST received - BotName: '{BotName}', BotUsername: '{BotUsername}'", dto.BotName, dto.BotUsername);
|
||||
|
||||
if (!ModelState.IsValid)
|
||||
@@ -102,7 +92,6 @@ public class BotsController : Controller
|
||||
var personalities = new[] { "Alan", "Dave", "Sarah", "Mike", "Emma", "Tom" };
|
||||
var random = new Random();
|
||||
dto.PersonalityName = personalities[random.Next(personalities.Length)];
|
||||
Console.WriteLine($"Auto-assigned personality: {dto.PersonalityName}");
|
||||
}
|
||||
|
||||
// Generate BotFather commands
|
||||
|
||||
@@ -31,15 +31,8 @@ public class CategoriesController : Controller
|
||||
[ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> Create(CreateCategoryDto model)
|
||||
{
|
||||
Console.WriteLine($"Received Category: Name='{model?.Name}', Description='{model?.Description}'");
|
||||
Console.WriteLine($"ModelState.IsValid: {ModelState.IsValid}");
|
||||
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
foreach (var error in ModelState)
|
||||
{
|
||||
Console.WriteLine($"ModelState Error - Key: {error.Key}, Errors: {string.Join(", ", error.Value.Errors.Select(e => e.ErrorMessage))}");
|
||||
}
|
||||
return View(model);
|
||||
}
|
||||
|
||||
@@ -70,18 +63,8 @@ public class CategoriesController : Controller
|
||||
[ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> Edit(Guid id, UpdateCategoryDto model)
|
||||
{
|
||||
Console.WriteLine($"Edit POST - CategoryId: {id}");
|
||||
Console.WriteLine($"Edit POST - Name: '{model?.Name}'");
|
||||
Console.WriteLine($"Edit POST - Description: '{model?.Description}'");
|
||||
Console.WriteLine($"Edit POST - IsActive: {model?.IsActive} (HasValue: {model?.IsActive.HasValue})");
|
||||
Console.WriteLine($"ModelState.IsValid: {ModelState.IsValid}");
|
||||
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
foreach (var error in ModelState)
|
||||
{
|
||||
Console.WriteLine($"ModelState Error - Key: {error.Key}, Errors: {string.Join(", ", error.Value.Errors.Select(e => e.ErrorMessage))}");
|
||||
}
|
||||
ViewData["CategoryId"] = id;
|
||||
return View(model);
|
||||
}
|
||||
|
||||
339
LittleShop/Areas/Admin/Controllers/CustomersController.cs
Normal file
339
LittleShop/Areas/Admin/Controllers/CustomersController.cs
Normal file
@@ -0,0 +1,339 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using LittleShop.Services;
|
||||
using LittleShop.DTOs;
|
||||
|
||||
namespace LittleShop.Areas.Admin.Controllers;
|
||||
|
||||
[Area("Admin")]
|
||||
[Authorize(AuthenticationSchemes = "Cookies", Roles = "Admin")]
|
||||
public class CustomersController : Controller
|
||||
{
|
||||
private readonly ICustomerService _customerService;
|
||||
private readonly IOrderService _orderService;
|
||||
private readonly ILogger<CustomersController> _logger;
|
||||
|
||||
public CustomersController(
|
||||
ICustomerService customerService,
|
||||
IOrderService orderService,
|
||||
ILogger<CustomersController> logger)
|
||||
{
|
||||
_customerService = customerService;
|
||||
_orderService = orderService;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task<IActionResult> Index(string searchTerm = "")
|
||||
{
|
||||
try
|
||||
{
|
||||
IEnumerable<CustomerDto> customers;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(searchTerm))
|
||||
{
|
||||
customers = await _customerService.SearchCustomersAsync(searchTerm);
|
||||
ViewData["SearchTerm"] = searchTerm;
|
||||
}
|
||||
else
|
||||
{
|
||||
customers = await _customerService.GetAllCustomersAsync();
|
||||
}
|
||||
|
||||
return View(customers.OrderByDescending(c => c.CreatedAt));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error loading customers index");
|
||||
TempData["ErrorMessage"] = "Error loading customers";
|
||||
return View(new List<CustomerDto>());
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<IActionResult> Details(Guid id)
|
||||
{
|
||||
try
|
||||
{
|
||||
var customer = await _customerService.GetCustomerByIdAsync(id);
|
||||
if (customer == null)
|
||||
{
|
||||
TempData["ErrorMessage"] = "Customer not found";
|
||||
return RedirectToAction(nameof(Index));
|
||||
}
|
||||
|
||||
// Get customer's order history
|
||||
var allOrders = await _orderService.GetAllOrdersAsync();
|
||||
var customerOrders = allOrders
|
||||
.Where(o => o.CustomerId == id)
|
||||
.OrderByDescending(o => o.CreatedAt)
|
||||
.ToList();
|
||||
|
||||
ViewData["CustomerOrders"] = customerOrders;
|
||||
|
||||
return View(customer);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error loading customer {CustomerId}", id);
|
||||
TempData["ErrorMessage"] = "Error loading customer details";
|
||||
return RedirectToAction(nameof(Index));
|
||||
}
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> Block(Guid id, string reason)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(reason))
|
||||
{
|
||||
TempData["ErrorMessage"] = "Block reason is required";
|
||||
return RedirectToAction(nameof(Details), new { id });
|
||||
}
|
||||
|
||||
var success = await _customerService.BlockCustomerAsync(id, reason);
|
||||
if (success)
|
||||
{
|
||||
_logger.LogWarning("Customer {CustomerId} blocked by admin. Reason: {Reason}", id, reason);
|
||||
TempData["SuccessMessage"] = "Customer blocked successfully";
|
||||
}
|
||||
else
|
||||
{
|
||||
TempData["ErrorMessage"] = "Failed to block customer";
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error blocking customer {CustomerId}", id);
|
||||
TempData["ErrorMessage"] = "Error blocking customer";
|
||||
}
|
||||
|
||||
return RedirectToAction(nameof(Details), new { id });
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> Unblock(Guid id)
|
||||
{
|
||||
try
|
||||
{
|
||||
var success = await _customerService.UnblockCustomerAsync(id);
|
||||
if (success)
|
||||
{
|
||||
_logger.LogInformation("Customer {CustomerId} unblocked by admin", id);
|
||||
TempData["SuccessMessage"] = "Customer unblocked successfully";
|
||||
}
|
||||
else
|
||||
{
|
||||
TempData["ErrorMessage"] = "Failed to unblock customer";
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error unblocking customer {CustomerId}", id);
|
||||
TempData["ErrorMessage"] = "Error unblocking customer";
|
||||
}
|
||||
|
||||
return RedirectToAction(nameof(Details), new { id });
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> RefreshRiskScore(Guid id)
|
||||
{
|
||||
try
|
||||
{
|
||||
await _customerService.UpdateCustomerMetricsAsync(id);
|
||||
TempData["SuccessMessage"] = "Risk score recalculated successfully";
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error refreshing risk score for customer {CustomerId}", id);
|
||||
TempData["ErrorMessage"] = "Error refreshing risk score";
|
||||
}
|
||||
|
||||
return RedirectToAction(nameof(Details), new { id });
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> Delete(Guid id)
|
||||
{
|
||||
try
|
||||
{
|
||||
var success = await _customerService.DeleteCustomerAsync(id);
|
||||
if (success)
|
||||
{
|
||||
_logger.LogWarning("Customer {CustomerId} deleted by admin", id);
|
||||
TempData["SuccessMessage"] = "Customer deleted successfully (soft delete - data retained)";
|
||||
return RedirectToAction(nameof(Index));
|
||||
}
|
||||
else
|
||||
{
|
||||
TempData["ErrorMessage"] = "Failed to delete customer";
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error deleting customer {CustomerId}", id);
|
||||
TempData["ErrorMessage"] = "Error deleting customer";
|
||||
}
|
||||
|
||||
return RedirectToAction(nameof(Details), new { id });
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Export customer data as JSON (GDPR "Right to Data Portability")
|
||||
/// </summary>
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> ExportJson(Guid id)
|
||||
{
|
||||
try
|
||||
{
|
||||
var exportData = await _customerService.GetCustomerDataForExportAsync(id);
|
||||
if (exportData == null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
var fileName = $"customer-data-{exportData.TelegramUsername ?? exportData.TelegramUserId.ToString()}-{DateTime.UtcNow:yyyyMMdd-HHmmss}.json";
|
||||
var jsonOptions = new System.Text.Json.JsonSerializerOptions
|
||||
{
|
||||
WriteIndented = true,
|
||||
PropertyNamingPolicy = System.Text.Json.JsonNamingPolicy.CamelCase
|
||||
};
|
||||
var jsonData = System.Text.Json.JsonSerializer.Serialize(exportData, jsonOptions);
|
||||
|
||||
_logger.LogInformation("Customer {CustomerId} data exported as JSON by admin", id);
|
||||
return File(System.Text.Encoding.UTF8.GetBytes(jsonData), "application/json", fileName);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error exporting customer {CustomerId} data as JSON", id);
|
||||
TempData["ErrorMessage"] = "Error generating JSON export";
|
||||
return RedirectToAction(nameof(Details), new { id });
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Export customer data as CSV (GDPR "Right to Data Portability")
|
||||
/// </summary>
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> ExportCsv(Guid id)
|
||||
{
|
||||
try
|
||||
{
|
||||
var exportData = await _customerService.GetCustomerDataForExportAsync(id);
|
||||
if (exportData == null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
var fileName = $"customer-data-{exportData.TelegramUsername ?? exportData.TelegramUserId.ToString()}-{DateTime.UtcNow:yyyyMMdd-HHmmss}.csv";
|
||||
var csvData = GenerateCsvExport(exportData);
|
||||
|
||||
_logger.LogInformation("Customer {CustomerId} data exported as CSV by admin", id);
|
||||
return File(System.Text.Encoding.UTF8.GetBytes(csvData), "text/csv", fileName);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error exporting customer {CustomerId} data as CSV", id);
|
||||
TempData["ErrorMessage"] = "Error generating CSV export";
|
||||
return RedirectToAction(nameof(Details), new { id });
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Helper method to generate CSV export from customer data
|
||||
/// </summary>
|
||||
private string GenerateCsvExport(LittleShop.DTOs.CustomerDataExportDto exportData)
|
||||
{
|
||||
var csv = new System.Text.StringBuilder();
|
||||
|
||||
// Customer Profile Section
|
||||
csv.AppendLine("CUSTOMER PROFILE");
|
||||
csv.AppendLine("Field,Value");
|
||||
csv.AppendLine($"Customer ID,{exportData.CustomerId}");
|
||||
csv.AppendLine($"Telegram User ID,{exportData.TelegramUserId}");
|
||||
csv.AppendLine($"Telegram Username,\"{EscapeCsv(exportData.TelegramUsername)}\"");
|
||||
csv.AppendLine($"Telegram Display Name,\"{EscapeCsv(exportData.TelegramDisplayName)}\"");
|
||||
csv.AppendLine($"First Name,\"{EscapeCsv(exportData.TelegramFirstName)}\"");
|
||||
csv.AppendLine($"Last Name,\"{EscapeCsv(exportData.TelegramLastName)}\"");
|
||||
csv.AppendLine($"Email,\"{EscapeCsv(exportData.Email)}\"");
|
||||
csv.AppendLine($"Phone,\"{EscapeCsv(exportData.PhoneNumber)}\"");
|
||||
csv.AppendLine($"Allow Marketing,{exportData.AllowMarketing}");
|
||||
csv.AppendLine($"Allow Order Updates,{exportData.AllowOrderUpdates}");
|
||||
csv.AppendLine($"Language,{exportData.Language}");
|
||||
csv.AppendLine($"Timezone,{exportData.Timezone}");
|
||||
csv.AppendLine($"Total Orders,{exportData.TotalOrders}");
|
||||
csv.AppendLine($"Total Spent,{exportData.TotalSpent:F2}");
|
||||
csv.AppendLine($"Average Order Value,{exportData.AverageOrderValue:F2}");
|
||||
csv.AppendLine($"Is Blocked,{exportData.IsBlocked}");
|
||||
csv.AppendLine($"Block Reason,\"{EscapeCsv(exportData.BlockReason)}\"");
|
||||
csv.AppendLine($"Risk Score,{exportData.RiskScore}");
|
||||
csv.AppendLine($"Created At,{exportData.CreatedAt:yyyy-MM-dd HH:mm:ss}");
|
||||
csv.AppendLine($"Updated At,{exportData.UpdatedAt:yyyy-MM-dd HH:mm:ss}");
|
||||
csv.AppendLine();
|
||||
|
||||
// Orders Section
|
||||
csv.AppendLine("ORDERS");
|
||||
csv.AppendLine("Order ID,Status,Total Amount,Currency,Order Date,Shipping Name,Shipping Address,City,Post Code,Country,Tracking Number");
|
||||
foreach (var order in exportData.Orders)
|
||||
{
|
||||
csv.AppendLine($"{order.OrderId},{order.Status},{order.TotalAmount:F2},{order.Currency},{order.OrderDate:yyyy-MM-dd HH:mm:ss}," +
|
||||
$"\"{EscapeCsv(order.ShippingName)}\",\"{EscapeCsv(order.ShippingAddress)}\",\"{EscapeCsv(order.ShippingCity)}\"," +
|
||||
$"\"{order.ShippingPostCode}\",\"{order.ShippingCountry}\",\"{EscapeCsv(order.TrackingNumber)}\"");
|
||||
|
||||
// Order Items sub-section
|
||||
if (order.Items.Any())
|
||||
{
|
||||
csv.AppendLine(" Order Items:");
|
||||
csv.AppendLine(" Product Name,Variant,Quantity,Unit Price,Total Price");
|
||||
foreach (var item in order.Items)
|
||||
{
|
||||
csv.AppendLine($" \"{EscapeCsv(item.ProductName)}\",\"{EscapeCsv(item.VariantName)}\"," +
|
||||
$"{item.Quantity},{item.UnitPrice:F2},{item.TotalPrice:F2}");
|
||||
}
|
||||
}
|
||||
}
|
||||
csv.AppendLine();
|
||||
|
||||
// Messages Section
|
||||
if (exportData.Messages.Any())
|
||||
{
|
||||
csv.AppendLine("MESSAGES");
|
||||
csv.AppendLine("Sent At,Type,Content,Was Read,Read At");
|
||||
foreach (var message in exportData.Messages)
|
||||
{
|
||||
csv.AppendLine($"{message.SentAt:yyyy-MM-dd HH:mm:ss},{message.MessageType}," +
|
||||
$"\"{EscapeCsv(message.Content)}\",{message.WasRead}," +
|
||||
$"{(message.ReadAt.HasValue ? message.ReadAt.Value.ToString("yyyy-MM-dd HH:mm:ss") : "")}");
|
||||
}
|
||||
csv.AppendLine();
|
||||
}
|
||||
|
||||
// Reviews Section
|
||||
if (exportData.Reviews.Any())
|
||||
{
|
||||
csv.AppendLine("REVIEWS");
|
||||
csv.AppendLine("Product Name,Rating,Comment,Created At,Is Approved,Is Verified Purchase");
|
||||
foreach (var review in exportData.Reviews)
|
||||
{
|
||||
csv.AppendLine($"\"{EscapeCsv(review.ProductName)}\",{review.Rating}," +
|
||||
$"\"{EscapeCsv(review.Comment)}\",{review.CreatedAt:yyyy-MM-dd HH:mm:ss}," +
|
||||
$"{review.IsApproved},{review.IsVerifiedPurchase}");
|
||||
}
|
||||
}
|
||||
|
||||
return csv.ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Escape CSV values containing quotes, commas, or newlines
|
||||
/// </summary>
|
||||
private string EscapeCsv(string? value)
|
||||
{
|
||||
if (string.IsNullOrEmpty(value)) return string.Empty;
|
||||
return value.Replace("\"", "\"\""); // Escape quotes by doubling them
|
||||
}
|
||||
}
|
||||
@@ -56,12 +56,13 @@ public class OrdersController : Controller
|
||||
break;
|
||||
}
|
||||
|
||||
// Get workflow counts for tab badges
|
||||
ViewData["PendingCount"] = (await _orderService.GetOrdersByStatusAsync(LittleShop.Enums.OrderStatus.PendingPayment)).Count();
|
||||
ViewData["AcceptCount"] = (await _orderService.GetOrdersRequiringActionAsync()).Count();
|
||||
ViewData["PackingCount"] = (await _orderService.GetOrdersForPackingAsync()).Count();
|
||||
ViewData["DispatchedCount"] = (await _orderService.GetOrdersByStatusAsync(LittleShop.Enums.OrderStatus.Dispatched)).Count();
|
||||
ViewData["OnHoldCount"] = (await _orderService.GetOrdersOnHoldAsync()).Count();
|
||||
// Get workflow counts for tab badges (single optimized query)
|
||||
var statusCounts = await _orderService.GetOrderStatusCountsAsync();
|
||||
ViewData["PendingCount"] = statusCounts.PendingPaymentCount;
|
||||
ViewData["AcceptCount"] = statusCounts.RequiringActionCount;
|
||||
ViewData["PackingCount"] = statusCounts.ForPackingCount;
|
||||
ViewData["DispatchedCount"] = statusCounts.DispatchedCount;
|
||||
ViewData["OnHoldCount"] = statusCounts.OnHoldCount;
|
||||
|
||||
return View();
|
||||
}
|
||||
@@ -137,15 +138,6 @@ public class OrdersController : Controller
|
||||
{
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
// Log validation errors for debugging
|
||||
foreach (var error in ModelState)
|
||||
{
|
||||
if (error.Value?.Errors.Count > 0)
|
||||
{
|
||||
Console.WriteLine($"Validation error for {error.Key}: {string.Join(", ", error.Value.Errors.Select(e => e.ErrorMessage))}");
|
||||
}
|
||||
}
|
||||
|
||||
// Return to details page with error
|
||||
var order = await _orderService.GetOrderByIdAsync(id);
|
||||
if (order == null)
|
||||
|
||||
53
LittleShop/Areas/Admin/Controllers/PaymentsController.cs
Normal file
53
LittleShop/Areas/Admin/Controllers/PaymentsController.cs
Normal file
@@ -0,0 +1,53 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using LittleShop.Services;
|
||||
using LittleShop.Enums;
|
||||
|
||||
namespace LittleShop.Areas.Admin.Controllers;
|
||||
|
||||
[Area("Admin")]
|
||||
[Authorize(AuthenticationSchemes = "Cookies", Roles = "Admin")]
|
||||
public class PaymentsController : Controller
|
||||
{
|
||||
private readonly ICryptoPaymentService _paymentService;
|
||||
private readonly IOrderService _orderService;
|
||||
private readonly ILogger<PaymentsController> _logger;
|
||||
|
||||
public PaymentsController(
|
||||
ICryptoPaymentService paymentService,
|
||||
IOrderService orderService,
|
||||
ILogger<PaymentsController> _logger)
|
||||
{
|
||||
_paymentService = paymentService;
|
||||
_orderService = orderService;
|
||||
this._logger = _logger;
|
||||
}
|
||||
|
||||
//GET: Admin/Payments
|
||||
public async Task<IActionResult> Index(string? status = null)
|
||||
{
|
||||
try
|
||||
{
|
||||
var payments = await _paymentService.GetAllPaymentsAsync();
|
||||
|
||||
// Filter by status if provided
|
||||
if (!string.IsNullOrEmpty(status) && Enum.TryParse<PaymentStatus>(status, out var paymentStatus))
|
||||
{
|
||||
payments = payments.Where(p => p.Status == paymentStatus);
|
||||
ViewData["CurrentStatus"] = status;
|
||||
}
|
||||
|
||||
// Get orders for payment-order linking
|
||||
var allOrders = await _orderService.GetAllOrdersAsync();
|
||||
ViewData["Orders"] = allOrders.ToDictionary(o => o.Id, o => o);
|
||||
|
||||
return View(payments.OrderByDescending(p => p.CreatedAt));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error retrieving payments list");
|
||||
TempData["ErrorMessage"] = "Failed to load payments. Please try again.";
|
||||
return View(Enumerable.Empty<LittleShop.DTOs.CryptoPaymentDto>());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -14,13 +14,17 @@ public class ProductsController : Controller
|
||||
private readonly ICategoryService _categoryService;
|
||||
private readonly IProductImportService _importService;
|
||||
private readonly IVariantCollectionService _variantCollectionService;
|
||||
private readonly IReviewService _reviewService;
|
||||
private readonly ILogger<ProductsController> _logger;
|
||||
|
||||
public ProductsController(IProductService productService, ICategoryService categoryService, IProductImportService importService, IVariantCollectionService variantCollectionService)
|
||||
public ProductsController(IProductService productService, ICategoryService categoryService, IProductImportService importService, IVariantCollectionService variantCollectionService, IReviewService reviewService, ILogger<ProductsController> logger)
|
||||
{
|
||||
_productService = productService;
|
||||
_categoryService = categoryService;
|
||||
_importService = importService;
|
||||
_variantCollectionService = variantCollectionService;
|
||||
_reviewService = reviewService;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task<IActionResult> Index()
|
||||
@@ -49,24 +53,11 @@ public class ProductsController : Controller
|
||||
[ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> Create(CreateProductDto model)
|
||||
{
|
||||
Console.WriteLine($"Received Product: Name='{model?.Name}', Description='{model?.Description}', Price={model?.Price}, Stock={model?.StockQuantity}");
|
||||
Console.WriteLine($"CategoryId: {model?.CategoryId}");
|
||||
Console.WriteLine($"Weight: {model?.Weight}, WeightUnit: {model?.WeightUnit}");
|
||||
Console.WriteLine($"ModelState.IsValid: {ModelState.IsValid}");
|
||||
|
||||
// Remove Description validation errors since it's optional
|
||||
ModelState.Remove("Description");
|
||||
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
Console.WriteLine("Validation errors:");
|
||||
foreach (var error in ModelState)
|
||||
{
|
||||
if (error.Value?.Errors.Count > 0)
|
||||
{
|
||||
Console.WriteLine($" {error.Key}: {string.Join(", ", error.Value.Errors.Select(e => e.ErrorMessage))}");
|
||||
}
|
||||
}
|
||||
|
||||
var categories = await _categoryService.GetAllCategoriesAsync();
|
||||
ViewData["Categories"] = categories.Where(c => c.IsActive);
|
||||
@@ -114,32 +105,9 @@ public class ProductsController : Controller
|
||||
product.VariantsJson = System.Text.Json.JsonSerializer.Serialize(variantsForJson);
|
||||
}
|
||||
|
||||
// TODO: Add ReviewService injection and retrieve actual reviews
|
||||
// For now, providing mock review data for demonstration
|
||||
ViewData["ProductReviews"] = new[]
|
||||
{
|
||||
new {
|
||||
Rating = 5,
|
||||
CustomerName = "John D.",
|
||||
Comment = "Excellent quality! Exceeded my expectations.",
|
||||
CreatedAt = DateTime.Now.AddDays(-7),
|
||||
OrderReference = "ORD-123456"
|
||||
},
|
||||
new {
|
||||
Rating = 4,
|
||||
CustomerName = "Sarah M.",
|
||||
Comment = "Very good product, fast delivery.",
|
||||
CreatedAt = DateTime.Now.AddDays(-14),
|
||||
OrderReference = "ORD-789012"
|
||||
},
|
||||
new {
|
||||
Rating = 5,
|
||||
CustomerName = (string?)null, // Anonymous
|
||||
Comment = "Love it! Will order again.",
|
||||
CreatedAt = DateTime.Now.AddDays(-21),
|
||||
OrderReference = "ORD-345678"
|
||||
}
|
||||
};
|
||||
// Load actual product reviews from ReviewService
|
||||
var productReviews = await _reviewService.GetReviewsByProductAsync(id, approvedOnly: true);
|
||||
ViewData["ProductReviews"] = productReviews;
|
||||
|
||||
var model = new UpdateProductDto
|
||||
{
|
||||
@@ -325,16 +293,29 @@ public class ProductsController : Controller
|
||||
|
||||
[HttpPost]
|
||||
[ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> DeleteAllSalesData()
|
||||
public async Task<IActionResult> DeleteAllSalesData(string confirmText)
|
||||
{
|
||||
// Require explicit typed confirmation
|
||||
if (confirmText != "DELETE ALL SALES DATA")
|
||||
{
|
||||
TempData["ErrorMessage"] = "❌ Confirmation text did not match. Operation cancelled for safety.";
|
||||
return RedirectToAction(nameof(Index));
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var deletedCount = await _importService.DeleteAllOrdersAndSalesDataAsync();
|
||||
|
||||
// Log this critical action
|
||||
_logger.LogWarning("CRITICAL: User {UserId} deleted ALL sales data ({Count} records)",
|
||||
User.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value, deletedCount);
|
||||
|
||||
TempData["SuccessMessage"] = $"✅ Successfully deleted {deletedCount} sales records (orders, payments, customers, messages)";
|
||||
return RedirectToAction(nameof(Index));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to delete all sales data");
|
||||
TempData["ErrorMessage"] = $"Failed to delete sales data: {ex.Message}";
|
||||
return RedirectToAction(nameof(Index));
|
||||
}
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using LittleShop.Services;
|
||||
|
||||
namespace LittleShop.Areas.Admin.Controllers;
|
||||
|
||||
[Area("Admin")]
|
||||
[Authorize(AuthenticationSchemes = "Cookies", Roles = "Admin")]
|
||||
public class PushSubscriptionsController : Controller
|
||||
{
|
||||
private readonly IPushNotificationService _pushService;
|
||||
private readonly ILogger<PushSubscriptionsController> _logger;
|
||||
|
||||
public PushSubscriptionsController(
|
||||
IPushNotificationService pushService,
|
||||
ILogger<PushSubscriptionsController> logger)
|
||||
{
|
||||
_pushService = pushService;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
// GET: Admin/PushSubscriptions
|
||||
public async Task<IActionResult> Index()
|
||||
{
|
||||
try
|
||||
{
|
||||
var subscriptions = await _pushService.GetActiveSubscriptionsAsync();
|
||||
return View(subscriptions.OrderByDescending(s => s.SubscribedAt));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error retrieving push subscriptions list");
|
||||
TempData["ErrorMessage"] = "Failed to load push subscriptions. Please try again.";
|
||||
return View(new List<LittleShop.Models.PushSubscription>());
|
||||
}
|
||||
}
|
||||
|
||||
// POST: Admin/PushSubscriptions/Delete/{id}
|
||||
[HttpPost]
|
||||
[ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> Delete(int id)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Find subscription by ID and delete via UnsubscribeAsync (which uses endpoint)
|
||||
var subscriptions = await _pushService.GetActiveSubscriptionsAsync();
|
||||
var subscription = subscriptions.FirstOrDefault(s => s.Id == id);
|
||||
|
||||
if (subscription == null)
|
||||
{
|
||||
TempData["ErrorMessage"] = "Push subscription not found.";
|
||||
return RedirectToAction(nameof(Index));
|
||||
}
|
||||
|
||||
var success = await _pushService.UnsubscribeAsync(subscription.Endpoint);
|
||||
|
||||
if (success)
|
||||
{
|
||||
_logger.LogInformation("Deleted push subscription {Id} (Endpoint: {Endpoint})", id, subscription.Endpoint);
|
||||
TempData["SuccessMessage"] = "Push subscription deleted successfully.";
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogWarning("Failed to delete push subscription {Id}", id);
|
||||
TempData["ErrorMessage"] = "Failed to delete push subscription.";
|
||||
}
|
||||
|
||||
return RedirectToAction(nameof(Index));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error deleting push subscription {Id}", id);
|
||||
TempData["ErrorMessage"] = $"Error deleting push subscription: {ex.Message}";
|
||||
return RedirectToAction(nameof(Index));
|
||||
}
|
||||
}
|
||||
|
||||
// POST: Admin/PushSubscriptions/CleanupExpired
|
||||
[HttpPost]
|
||||
[ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> CleanupExpired()
|
||||
{
|
||||
try
|
||||
{
|
||||
var deletedCount = await _pushService.CleanupExpiredSubscriptionsAsync();
|
||||
|
||||
_logger.LogInformation("Cleaned up {Count} expired push subscriptions", deletedCount);
|
||||
|
||||
if (deletedCount > 0)
|
||||
{
|
||||
TempData["SuccessMessage"] = $"Successfully cleaned up {deletedCount} expired subscription(s).";
|
||||
}
|
||||
else
|
||||
{
|
||||
TempData["InfoMessage"] = "No expired subscriptions found to clean up.";
|
||||
}
|
||||
|
||||
return RedirectToAction(nameof(Index));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error cleaning up expired push subscriptions");
|
||||
TempData["ErrorMessage"] = $"Error during cleanup: {ex.Message}";
|
||||
return RedirectToAction(nameof(Index));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -55,6 +55,7 @@ public class ReviewsController : Controller
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> Approve(Guid id)
|
||||
{
|
||||
try
|
||||
@@ -86,6 +87,7 @@ public class ReviewsController : Controller
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> Delete(Guid id)
|
||||
{
|
||||
try
|
||||
|
||||
@@ -86,9 +86,9 @@ public class UsersController : Controller
|
||||
}
|
||||
|
||||
// Validate password if provided
|
||||
if (!string.IsNullOrEmpty(model.Password) && model.Password.Length < 3)
|
||||
if (!string.IsNullOrEmpty(model.Password) && model.Password.Length < 8)
|
||||
{
|
||||
ModelState.AddModelError("Password", "Password must be at least 3 characters if changing");
|
||||
ModelState.AddModelError("Password", "Password must be at least 8 characters if changing");
|
||||
}
|
||||
|
||||
if (!ModelState.IsValid)
|
||||
|
||||
Reference in New Issue
Block a user