From be4d797c6c7e50d6243d937d29660ba603007937 Mon Sep 17 00:00:00 2001 From: sysadmin Date: Thu, 4 Sep 2025 21:28:47 +0100 Subject: [PATCH] BTCPay-infrastructure-recovery --- BTCPAY_SETUP.md | 92 ++++++ DEPLOY_BTCPAY_SERVER.md | 175 +++++++++++ .../Admin/Controllers/AccountController.cs | 29 +- .../Admin/Controllers/UsersController.cs | 115 +++++-- .../Areas/Admin/Views/Users/Edit.cshtml | 82 +++++ .../Areas/Admin/Views/Users/Index.cshtml | 16 + .../Controllers/BTCPayWebhookController.cs | 180 +++++++++++ .../Controllers/PushNotificationController.cs | 10 +- LittleShop/DTOs/BTCPayWebhookDto.cs | 94 ++++++ LittleShop/Services/AuthService.cs | 15 + LittleShop/Services/DataSeederService.cs | 280 +++++++++++++----- LittleShop/Services/IAuthService.cs | 1 + LittleShop/Services/ProductService.cs | 3 + .../Services/PushNotificationService.cs | 12 +- LittleShop/appsettings.json | 4 +- btcpay-minimal-compose.yml | 111 +++++++ btcpay-minimal-working.yml | 35 +++ btcpay-server-compose.yml | 134 +++++++++ btcpay-simple-compose.yml | 115 +++++++ btcpay-simple-testnet.yml | 45 +++ btcpay-working-compose.yml | 84 ++++++ btcpay.env | 21 ++ 22 files changed, 1552 insertions(+), 101 deletions(-) create mode 100644 BTCPAY_SETUP.md create mode 100644 DEPLOY_BTCPAY_SERVER.md create mode 100644 LittleShop/Areas/Admin/Views/Users/Edit.cshtml create mode 100644 LittleShop/Controllers/BTCPayWebhookController.cs create mode 100644 LittleShop/DTOs/BTCPayWebhookDto.cs create mode 100644 btcpay-minimal-compose.yml create mode 100644 btcpay-minimal-working.yml create mode 100644 btcpay-server-compose.yml create mode 100644 btcpay-simple-compose.yml create mode 100644 btcpay-simple-testnet.yml create mode 100644 btcpay-working-compose.yml create mode 100644 btcpay.env diff --git a/BTCPAY_SETUP.md b/BTCPAY_SETUP.md new file mode 100644 index 0000000..661416c --- /dev/null +++ b/BTCPAY_SETUP.md @@ -0,0 +1,92 @@ +# BTCPay Server Integration Setup + +## Current Status +✅ BTCPay Server deployed at: https://pay.silverlabs.uk +✅ Admin account created: jamie@Silverlabs.uk +✅ Store created in BTCPay Server +✅ LittleShop BaseUrl updated to: https://pay.silverlabs.uk + +## Required Configuration Steps + +### 1. Get Store ID +1. Login to https://pay.silverlabs.uk +2. Go to **Stores** → **Settings** → **General** +3. Copy the Store ID (usually found in URL or displayed on settings page) + +### 2. Generate API Key +1. Go to **Account** → **Manage Account** → **API Keys** +2. Click **Generate Key** +3. Label: `LittleShop Integration` +4. Required Permissions: + - `btcpay.store.canviewstores` + - `btcpay.store.canmodifyinvoices` + - `btcpay.store.cancreateinvoice` +5. Copy the generated API key + +### 3. Configure Webhook +1. Go to **Stores** → **Settings** → **Webhooks** +2. Click **Create Webhook** +3. Webhook URL: `https://your-littleshop-domain.com/api/btcpay/webhook` +4. Events to enable: + - Invoice payment settled + - Invoice expired + - Invoice invalid +5. Copy the webhook secret + +### 4. Configure Bitcoin Wallet +1. Go to **Stores** → **Settings** → **Bitcoin** +2. Click **Set up a wallet** +3. For development: Choose **Use the hot wallet** +4. Generate new wallet or import existing +5. Complete wallet setup + +## LittleShop Configuration Template + +Once you have the values above, update your appsettings.json: + +```json +{ + "BTCPayServer": { + "BaseUrl": "https://pay.silverlabs.uk", + "ApiKey": "YOUR_API_KEY_HERE", + "StoreId": "YOUR_STORE_ID_HERE", + "WebhookSecret": "YOUR_WEBHOOK_SECRET_HERE" + } +} +``` + +## Supported Cryptocurrencies + +The LittleShop integration supports: +- BTC (Bitcoin) +- XMR (Monero) - requires additional setup +- USDT (Tether) +- LTC (Litecoin) +- ETH (Ethereum) +- ZEC (Zcash) +- DASH (Dash) +- DOGE (Dogecoin) + +Note: Only Bitcoin is enabled by default. Additional cryptocurrencies require: +1. Enabling them in BTCPay Server store settings +2. Configuring wallets for each currency + +## Testing the Integration + +After configuration: +1. Start your LittleShop application +2. Create a test order +3. Select cryptocurrency payment +4. Verify invoice creation in BTCPay Server +5. Test payment flow and webhook notifications + +## Webhook Endpoint + +The webhook endpoint is already implemented in LittleShop: +- URL: `/api/btcpay/webhook` +- Controller: `BTCPayWebhookController.ProcessWebhook` +- Features: + - HMAC-SHA256 signature validation + - Proper BTCPay Server webhook format handling + - Payment status mapping and processing +- Handles: All BTCPay Server webhook events (invoice created, payment settled, expired, etc.) \ No newline at end of file diff --git a/DEPLOY_BTCPAY_SERVER.md b/DEPLOY_BTCPAY_SERVER.md new file mode 100644 index 0000000..45877f2 --- /dev/null +++ b/DEPLOY_BTCPAY_SERVER.md @@ -0,0 +1,175 @@ +# BTCPay Server Deployment Instructions + +## Infrastructure Status +- **Target Server**: portainer-01 (10.0.0.51) +- **Domain**: https://pay.silverlabs.uk +- **HAProxy Router**: VyOS (10.0.0.1) + +## Prerequisites +1. Access to portainer-01 server (10.0.0.51) with sysadmin/Phenom12# credentials +2. Access to VyOS router (10.0.0.1) for HAProxy configuration +3. Docker and Docker Compose installed on portainer-01 + +## Step 1: Deploy BTCPay Server to Portainer + +### Option A: Via Portainer Web UI +1. Access Portainer at https://10.0.0.51:9443 +2. Login with admin credentials (may need to reset if infrastructure was reset) +3. Navigate to "Stacks" → "Add Stack" +4. Name: `btcpay-server` +5. Copy the contents of `btcpay-server-compose.yml` into the web editor +6. Upload the environment file `btcpay.env` or add environment variables manually +7. Deploy the stack + +### Option B: Via SSH/Command Line (if SSH access is available) +```bash +# Copy deployment files to server +scp btcpay-server-compose.yml sysadmin@10.0.0.51:/tmp/ +scp btcpay.env sysadmin@10.0.0.51:/tmp/ + +# SSH to server +ssh sysadmin@10.0.0.51 + +# Create deployment directory +sudo mkdir -p /opt/btcpay +sudo cp /tmp/btcpay-server-compose.yml /opt/btcpay/docker-compose.yml +sudo cp /tmp/btcpay.env /opt/btcpay/.env + +# Deploy BTCPay Server +cd /opt/btcpay +sudo docker-compose up -d +``` + +### Option C: Via Docker API (if accessible) +```bash +# Copy files and use docker-compose remotely +export DOCKER_HOST=tcp://10.0.0.51:2376 +docker-compose -f btcpay-server-compose.yml --env-file btcpay.env up -d +``` + +## Step 2: Configure HAProxy on VyOS Router + +SSH to VyOS router (10.0.0.1) and configure routing: + +```bash +ssh sysadmin@10.0.0.1 + +# Enter configuration mode +configure + +# Configure backend for BTCPay Server +set load-balancing reverse-proxy service btcpay-backend backend btcpay-server address 10.0.0.51 +set load-balancing reverse-proxy service btcpay-backend backend btcpay-server port 49392 +set load-balancing reverse-proxy service btcpay-backend backend btcpay-server check + +# Configure frontend rule for pay.silverlabs.uk +set load-balancing reverse-proxy service btcpay-frontend bind 0.0.0.0 port 443 +set load-balancing reverse-proxy service btcpay-frontend rule pay-silverlabs domain-name pay.silverlabs.uk +set load-balancing reverse-proxy service btcpay-frontend rule pay-silverlabs set backend btcpay-backend +set load-balancing reverse-proxy service btcpay-frontend ssl certificate selfsigned + +# Also configure HTTP redirect to HTTPS +set load-balancing reverse-proxy service btcpay-frontend-http bind 0.0.0.0 port 80 +set load-balancing reverse-proxy service btcpay-frontend-http rule pay-silverlabs-redirect domain-name pay.silverlabs.uk +set load-balancing reverse-proxy service btcpay-frontend-http rule pay-silverlabs-redirect redirect location https://pay.silverlabs.uk + +# Commit and save +commit +save +``` + +## Step 3: Verify Deployment + +1. **Check container status**: + ```bash + ssh sysadmin@10.0.0.51 + sudo docker ps | grep btcpay + ``` + +2. **Check logs**: + ```bash + sudo docker logs btcpayserver + sudo docker logs btcpay-postgres + sudo docker logs btcpay-nbxplorer + ``` + +3. **Test local access**: + ```bash + curl -k http://10.0.0.51:49392/api/v1/health + ``` + +4. **Test domain access**: + ```bash + curl -k https://pay.silverlabs.uk/api/v1/health + ``` + +## Step 4: Complete BTCPay Server Setup + +1. Access https://pay.silverlabs.uk +2. Create admin account (suggest using jamie@silverlabs.uk as before) +3. Complete initial setup wizard: + - Set up Bitcoin wallet (hot wallet for development) + - Configure store settings + - Generate API keys for LittleShop integration +4. Configure webhooks pointing to LittleShop instance + +## Step 5: Update LittleShop Configuration + +Update LittleShop's `appsettings.json`: +```json +{ + "BTCPayServer": { + "BaseUrl": "https://pay.silverlabs.uk", + "ApiKey": "GENERATED_API_KEY_FROM_BTCPAY", + "StoreId": "STORE_ID_FROM_BTCPAY", + "WebhookSecret": "WEBHOOK_SECRET_FROM_BTCPAY" + } +} +``` + +## Troubleshooting + +### If Portainer access is needed: +- Reset Portainer admin password via Docker: + ```bash + sudo docker exec -it portainer /portainer --admin-password='$2y$10$HASH_OF_NEW_PASSWORD' + ``` + +### If deployment fails: +- Check Docker logs: `sudo docker logs btcpayserver` +- Verify network connectivity between containers +- Check if ports are already in use: `sudo netstat -tulpn | grep 49392` + +### If domain routing doesn't work: +- Verify HAProxy configuration: `show configuration` in VyOS +- Check if SSL certificate is valid +- Test direct IP access first: http://10.0.0.51:49392 + +## Security Considerations + +- Change all default passwords in btcpay.env +- Use proper SSL certificates (Let's Encrypt recommended) +- Ensure Bitcoin node is properly secured +- Monitor logs for any suspicious activity +- Regular backups of BTCPay data and Bitcoin blockchain data + +## Post-Deployment + +After successful deployment: +1. Test payment flow end-to-end +2. Configure additional cryptocurrencies if needed +3. Set up monitoring and alerting +4. Schedule regular backups +5. Update DNS records if necessary + +## Container Services Overview + +| Service | Port | Purpose | +|---------|------|---------| +| btcpayserver | 49392 | Main BTCPay Server application | +| postgres | 5432 | Database for BTCPay data | +| nbxplorer | 32838 | Bitcoin blockchain explorer | +| bitcoind | 8332/8333 | Bitcoin node (RPC/P2P) | +| tor | 9050 | Tor proxy for privacy | + +All services are connected via Docker network `btcpaynetwork`. \ No newline at end of file diff --git a/LittleShop/Areas/Admin/Controllers/AccountController.cs b/LittleShop/Areas/Admin/Controllers/AccountController.cs index 1636c1d..7c1fe01 100644 --- a/LittleShop/Areas/Admin/Controllers/AccountController.cs +++ b/LittleShop/Areas/Admin/Controllers/AccountController.cs @@ -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 + // 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 + { + 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"); diff --git a/LittleShop/Areas/Admin/Controllers/UsersController.cs b/LittleShop/Areas/Admin/Controllers/UsersController.cs index 0b27a73..a388bb8 100644 --- a/LittleShop/Areas/Admin/Controllers/UsersController.cs +++ b/LittleShop/Areas/Admin/Controllers/UsersController.cs @@ -30,19 +30,28 @@ public class UsersController : Controller [HttpPost] public async Task 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 Edit(Guid id) @@ -66,25 +75,89 @@ public class UsersController : Controller [HttpPost] public async Task 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 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)); + } } } \ No newline at end of file diff --git a/LittleShop/Areas/Admin/Views/Users/Edit.cshtml b/LittleShop/Areas/Admin/Views/Users/Edit.cshtml new file mode 100644 index 0000000..11481c0 --- /dev/null +++ b/LittleShop/Areas/Admin/Views/Users/Edit.cshtml @@ -0,0 +1,82 @@ +@model LittleShop.DTOs.UpdateUserDto + +@{ + ViewData["Title"] = "Edit User"; + var userId = ViewData["UserId"] as Guid?; +} + +
+
+

Edit User

+
+
+ +
+
+
+
+
+ @Html.AntiForgeryToken() + + @if (ViewData.ModelState[""] != null && ViewData.ModelState[""].Errors.Count > 0) + { + + } + +
+ + +
Must be unique across all users
+
+ +
+ + +
Leave blank to keep current password. Minimum 3 characters if changing.
+
+ +
+ + + +
+ +
+ + Back to Users + + +
+
+
+
+
+ +
+
+
+
Edit Information
+
+
+
    +
  • Username: Can be changed if unique
  • +
  • Password: Optional - leave blank to keep current
  • +
  • Status: Inactive users cannot log in
  • +
+
+ + Warning: Deactivating your own account will lock you out. +
+
+
+
+
\ No newline at end of file diff --git a/LittleShop/Areas/Admin/Views/Users/Index.cshtml b/LittleShop/Areas/Admin/Views/Users/Index.cshtml index d89a3a9..ac9a167 100644 --- a/LittleShop/Areas/Admin/Views/Users/Index.cshtml +++ b/LittleShop/Areas/Admin/Views/Users/Index.cshtml @@ -15,6 +15,22 @@ +@if (TempData["SuccessMessage"] != null) +{ + +} + +@if (TempData["ErrorMessage"] != null) +{ + +} +
@if (Model.Any()) diff --git a/LittleShop/Controllers/BTCPayWebhookController.cs b/LittleShop/Controllers/BTCPayWebhookController.cs new file mode 100644 index 0000000..89708d1 --- /dev/null +++ b/LittleShop/Controllers/BTCPayWebhookController.cs @@ -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 _logger; + + public BTCPayWebhookController( + ICryptoPaymentService cryptoPaymentService, + IBTCPayServerService btcPayService, + IConfiguration configuration, + ILogger logger) + { + _cryptoPaymentService = cryptoPaymentService; + _btcPayService = btcPayService; + _configuration = configuration; + _logger = logger; + } + + [HttpPost("webhook")] + public async Task 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(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=" + 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 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 + }; + } +} \ No newline at end of file diff --git a/LittleShop/Controllers/PushNotificationController.cs b/LittleShop/Controllers/PushNotificationController.cs index 9db7f08..7485abd 100644 --- a/LittleShop/Controllers/PushNotificationController.cs +++ b/LittleShop/Controllers/PushNotificationController.cs @@ -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>(); + 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>(); + logger.LogError(ex, "Push subscription error"); return StatusCode(500, new { error = ex.Message }); } } diff --git a/LittleShop/DTOs/BTCPayWebhookDto.cs b/LittleShop/DTOs/BTCPayWebhookDto.cs new file mode 100644 index 0000000..94875bf --- /dev/null +++ b/LittleShop/DTOs/BTCPayWebhookDto.cs @@ -0,0 +1,94 @@ +using System.Text.Json.Serialization; + +namespace LittleShop.DTOs; + +/// +/// DTO for BTCPay Server webhook events +/// Based on BTCPay Server webhook documentation +/// +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; } +} \ No newline at end of file diff --git a/LittleShop/Services/AuthService.cs b/LittleShop/Services/AuthService.cs index 1d5d2f2..230035f 100644 --- a/LittleShop/Services/AuthService.cs +++ b/LittleShop/Services/AuthService.cs @@ -103,6 +103,21 @@ public class AuthService : IAuthService }; } + public async Task 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> GetAllUsersAsync() { return await _context.Users diff --git a/LittleShop/Services/DataSeederService.cs b/LittleShop/Services/DataSeederService.cs index 6fd2951..c5c763e 100644 --- a/LittleShop/Services/DataSeederService.cs +++ b/LittleShop/Services/DataSeederService.cs @@ -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 { + // 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 + { + ["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!"); + } } \ No newline at end of file diff --git a/LittleShop/Services/IAuthService.cs b/LittleShop/Services/IAuthService.cs index 6916b15..5515bf9 100644 --- a/LittleShop/Services/IAuthService.cs +++ b/LittleShop/Services/IAuthService.cs @@ -8,6 +8,7 @@ public interface IAuthService Task SeedDefaultUserAsync(); Task CreateUserAsync(CreateUserDto createUserDto); Task GetUserByIdAsync(Guid id); + Task GetUserByUsernameAsync(string username); Task> GetAllUsersAsync(); Task DeleteUserAsync(Guid id); Task UpdateUserAsync(Guid id, UpdateUserDto updateUserDto); diff --git a/LittleShop/Services/ProductService.cs b/LittleShop/Services/ProductService.cs index 5d137ed..c2196ff 100644 --- a/LittleShop/Services/ProductService.cs +++ b/LittleShop/Services/ProductService.cs @@ -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, diff --git a/LittleShop/Services/PushNotificationService.cs b/LittleShop/Services/PushNotificationService.cs index d0a970a..2bb2a3b 100644 --- a/LittleShop/Services/PushNotificationService.cs +++ b/LittleShop/Services/PushNotificationService.cs @@ -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) diff --git a/LittleShop/appsettings.json b/LittleShop/appsettings.json index 105d448..fffd9a4 100644 --- a/LittleShop/appsettings.json +++ b/LittleShop/appsettings.json @@ -10,8 +10,8 @@ }, "BTCPayServer": { "BaseUrl": "https://pay.silverlabs.uk", - "ApiKey": "885a65ead85b87d5a10095b6cb6ad87866988cc2", - "StoreId": "51kbAYszqX2gEK2E9EYwqbixcDmsafuBXukx7v1PrZUD", + "ApiKey": "994589c8b514531f867dd24c83a02b6381a5f4a2", + "StoreId": "AoxXjM9NJT6P9C1MErkaawXaSchz8sFPYdQ9FyhmQz33", "WebhookSecret": "" }, "RoyalMail": { diff --git a/btcpay-minimal-compose.yml b/btcpay-minimal-compose.yml new file mode 100644 index 0000000..ed53e2b --- /dev/null +++ b/btcpay-minimal-compose.yml @@ -0,0 +1,111 @@ +services: + btcpayserver: + image: btcpayserver/btcpayserver:2.2.0 + container_name: btcpayserver + restart: unless-stopped + ports: + - "49392:49392" # BTCPay Server HTTP port + environment: + # Database + - POSTGRES_HOST=postgres + - POSTGRES_PORT=5432 + - POSTGRES_USER=postgres + - POSTGRES_PASSWORD=${POSTGRES_PASSWORD:-BTCPay2024SecurePassword123!} + - POSTGRES_DB=btcpayserver + + # BTCPay Server Configuration + - BTCPAY_HOST=pay.silverlabs.uk + - BTCPAY_PROTOCOL=https + - BTCPAY_BIND=0.0.0.0:49392 + + # Network settings - Start with testnet for easier setup + - BTCPAY_NETWORK=testnet + - BTCPAY_CHAINS=btc + - BTCPAY_BTCEXPLORERURL=http://nbxplorer:32838 + + # Other settings + - BTCPAY_ROOTPATH=/ + - BTCPAY_DEBUGLOG=btcpay.log + - BTCPAY_LOGS_LEVEL=info + + volumes: + - btcpay_datadir:/datadir + - btcpay_logs:/var/log/btcpayserver + networks: + - btcpaynetwork + depends_on: + - postgres + - nbxplorer + + postgres: + image: postgres:13 + container_name: btcpay-postgres + restart: unless-stopped + environment: + - POSTGRES_USER=postgres + - POSTGRES_PASSWORD=${POSTGRES_PASSWORD:-BTCPay2024SecurePassword123!} + - POSTGRES_DB=btcpayserver + volumes: + - postgres_data:/var/lib/postgresql/data + networks: + - btcpaynetwork + + nbxplorer: + image: nicolasdorier/nbxplorer:2.5.30 + container_name: btcpay-nbxplorer + restart: unless-stopped + ports: + - "32838:32838" + environment: + - NBXPLORER_NETWORK=testnet + - NBXPLORER_CHAINS=btc + - NBXPLORER_BTCRPCURL=http://bitcoind:18332 + - NBXPLORER_BTCRPCUSER=bitcoinrpc + - NBXPLORER_BTCRPCPASSWORD=${BTC_RPC_PASSWORD:-BitcoinRPC2024SecurePassword456!} + - NBXPLORER_BIND=0.0.0.0:32838 + - NBXPLORER_VERBOSE=1 + - NBXPLORER_NOAUTH=1 + volumes: + - nbxplorer_data:/datadir + networks: + - btcpaynetwork + depends_on: + - bitcoind + + bitcoind: + image: btcpayserver/bitcoin:26.0 + container_name: btcpay-bitcoind + restart: unless-stopped + command: > + bitcoind + -testnet + -server=1 + -rpcuser=bitcoinrpc + -rpcpassword=${BTC_RPC_PASSWORD:-BitcoinRPC2024SecurePassword456!} + -rpcbind=0.0.0.0:18332 + -rpcallowip=0.0.0.0/0 + -txindex=1 + -prune=0 + volumes: + - bitcoin_data:/home/bitcoin/.bitcoin + networks: + - btcpaynetwork + ports: + - "18333:18333" # Bitcoin Testnet P2P + - "18332:18332" # Bitcoin Testnet RPC + +volumes: + btcpay_datadir: + driver: local + btcpay_logs: + driver: local + postgres_data: + driver: local + nbxplorer_data: + driver: local + bitcoin_data: + driver: local + +networks: + btcpaynetwork: + driver: bridge \ No newline at end of file diff --git a/btcpay-minimal-working.yml b/btcpay-minimal-working.yml new file mode 100644 index 0000000..6dd9645 --- /dev/null +++ b/btcpay-minimal-working.yml @@ -0,0 +1,35 @@ +services: + btcpayserver: + image: btcpayserver/btcpayserver:2.2.0 + container_name: btcpayserver + restart: unless-stopped + ports: + - "49392:49392" + environment: + - BTCPAY_HOST=pay.silverlabs.uk + - BTCPAY_BIND=0.0.0.0:49392 + - BTCPAY_NETWORK=testnet + - BTCPAY_CHAINS=btc + - BTCPAY_ROOTPATH=/ + - BTCPAY_BTCEXPLORERURL=http://dummy:1234 + - BTCPAY_BTCEXPLORERNOAUTH=1 + - BTCPAY_POSTGRES=User ID=btcpay;Host=postgres;Port=5432;Database=btcpayserver;Password=btcpay + volumes: + - btcpay_datadir:/datadir + depends_on: + - postgres + + postgres: + image: postgres:13 + container_name: btcpay-postgres + restart: unless-stopped + environment: + - POSTGRES_USER=btcpay + - POSTGRES_PASSWORD=btcpay + - POSTGRES_DB=btcpayserver + volumes: + - postgres_data:/var/lib/postgresql/data + +volumes: + btcpay_datadir: + postgres_data: \ No newline at end of file diff --git a/btcpay-server-compose.yml b/btcpay-server-compose.yml new file mode 100644 index 0000000..cf56258 --- /dev/null +++ b/btcpay-server-compose.yml @@ -0,0 +1,134 @@ +services: + btcpayserver: + image: btcpayserver/btcpayserver:1.13.8 + container_name: btcpayserver + restart: unless-stopped + ports: + - "49392:49392" # BTCPay Server HTTP port + environment: + # Database + - POSTGRES_HOST=postgres + - POSTGRES_PORT=5432 + - POSTGRES_USER=postgres + - POSTGRES_PASSWORD=${POSTGRES_PASSWORD:-SomeRandomPasswordForDatabase} + - POSTGRES_DB=btcpayserver + + # BTCPay Server Configuration + - BTCPAY_HOST=pay.silverlabs.uk + - BTCPAY_PROTOCOL=https + - BTCPAY_BIND=0.0.0.0:49392 + - BTCPAY_SOCKSENDPOINT=tor:9050 + - BTCPAY_TORRCFILE=/datadir/Tor/torrc + - BTCPAY_TORSERVICES=btcpayserver:49392 + + # Network settings + - BTCPAY_NETWORK=mainnet + - BTCPAY_CHAINS=btc + - BTCPAY_BTCEXPLORERURL=http://nbxplorer:32838 + - BTCPAY_BTCLIGHTNING=type=lnd-rest;server=https://lnd:8080/;macaroonfilepath=/datadir/admin.macaroon;certfilepath=/datadir/tls.cert + + # Other settings + - BTCPAY_ROOTPATH=/ + - BTCPAY_DEBUGLOG=btcpay.log + - BTCPAY_LOGS_FILE=/datadir/logs/btcpay.log + - BTCPAY_LOGS_LEVEL=info + + volumes: + - btcpay_datadir:/datadir + - btcpay_logs:/var/log/btcpayserver + networks: + - btcpaynetwork + depends_on: + - postgres + - nbxplorer + labels: + - "traefik.enable=true" + - "traefik.http.routers.btcpay.rule=Host(`pay.silverlabs.uk`)" + - "traefik.http.routers.btcpay.tls=true" + - "traefik.http.routers.btcpay.tls.certresolver=letsencrypt" + - "traefik.http.services.btcpay.loadbalancer.server.port=49392" + + postgres: + image: postgres:13 + container_name: btcpay-postgres + restart: unless-stopped + environment: + - POSTGRES_USER=postgres + - POSTGRES_PASSWORD=${POSTGRES_PASSWORD:-SomeRandomPasswordForDatabase} + - POSTGRES_DB=btcpayserver + volumes: + - postgres_data:/var/lib/postgresql/data + networks: + - btcpaynetwork + + nbxplorer: + image: nicolasdorier/nbxplorer:2.5.0 + container_name: btcpay-nbxplorer + restart: unless-stopped + ports: + - "32838:32838" + environment: + - NBXPLORER_NETWORK=mainnet + - NBXPLORER_CHAINS=btc + - NBXPLORER_BTCRPCURL=http://bitcoind:8332 + - NBXPLORER_BTCRPCUSER=bitcoinrpc + - NBXPLORER_BTCRPCPASSWORD=${BTC_RPC_PASSWORD:-SomeRandomBitcoinPassword} + - NBXPLORER_BIND=0.0.0.0:32838 + - NBXPLORER_VERBOSE=1 + - NBXPLORER_NOAUTH=1 + volumes: + - nbxplorer_data:/datadir + networks: + - btcpaynetwork + depends_on: + - bitcoind + + bitcoind: + image: ruimarinho/bitcoin-core:25 + container_name: btcpay-bitcoind + restart: unless-stopped + command: > + bitcoind + -server=1 + -rpcuser=bitcoinrpc + -rpcpassword=${BTC_RPC_PASSWORD:-SomeRandomBitcoinPassword} + -rpcbind=0.0.0.0:8332 + -rpcallowip=0.0.0.0/0 + -txindex=1 + -prune=0 + volumes: + - bitcoin_data:/home/bitcoin/.bitcoin + networks: + - btcpaynetwork + ports: + - "8333:8333" # Bitcoin P2P + - "8332:8332" # Bitcoin RPC (internal only) + + tor: + image: btcpayserver/tor:latest + container_name: btcpay-tor + restart: unless-stopped + environment: + - TOR_EXTRA_ARGS= + volumes: + - tor_data:/datadir + networks: + - btcpaynetwork + +volumes: + btcpay_datadir: + driver: local + btcpay_logs: + driver: local + postgres_data: + driver: local + nbxplorer_data: + driver: local + bitcoin_data: + driver: local + tor_data: + driver: local + +networks: + btcpaynetwork: + driver: bridge \ No newline at end of file diff --git a/btcpay-simple-compose.yml b/btcpay-simple-compose.yml new file mode 100644 index 0000000..b019c35 --- /dev/null +++ b/btcpay-simple-compose.yml @@ -0,0 +1,115 @@ +services: + btcpayserver: + image: btcpayserver/btcpayserver:2.2.0 + container_name: btcpayserver + restart: unless-stopped + ports: + - "49392:49392" # BTCPay Server HTTP port + environment: + # Database + - POSTGRES_HOST=postgres + - POSTGRES_PORT=5432 + - POSTGRES_USER=postgres + - POSTGRES_PASSWORD=${POSTGRES_PASSWORD:-BTCPay2024SecurePassword123!} + - POSTGRES_DB=btcpayserver + + # BTCPay Server Configuration + - BTCPAY_HOST=pay.silverlabs.uk + - BTCPAY_PROTOCOL=https + - BTCPAY_BIND=0.0.0.0:49392 + + # Network settings + - BTCPAY_NETWORK=mainnet + - BTCPAY_CHAINS=btc + - BTCPAY_BTCEXPLORERURL=http://nbxplorer:32838 + + # Other settings + - BTCPAY_ROOTPATH=/ + - BTCPAY_DEBUGLOG=btcpay.log + - BTCPAY_LOGS_LEVEL=info + + volumes: + - btcpay_datadir:/datadir + - btcpay_logs:/var/log/btcpayserver + networks: + - btcpaynetwork + depends_on: + - postgres + - nbxplorer + + postgres: + image: postgres:13 + container_name: btcpay-postgres + restart: unless-stopped + environment: + - POSTGRES_USER=postgres + - POSTGRES_PASSWORD=${POSTGRES_PASSWORD:-BTCPay2024SecurePassword123!} + - POSTGRES_DB=btcpayserver + volumes: + - postgres_data:/var/lib/postgresql/data + networks: + - btcpaynetwork + + nbxplorer: + image: nicolasdorier/nbxplorer:2.5.30 + container_name: btcpay-nbxplorer + restart: unless-stopped + ports: + - "32838:32838" + environment: + - NBXPLORER_NETWORK=mainnet + - NBXPLORER_CHAINS=btc + - NBXPLORER_BTCRPCURL=http://bitcoind:8332 + - NBXPLORER_BTCRPCUSER=bitcoinrpc + - NBXPLORER_BTCRPCPASSWORD=${BTC_RPC_PASSWORD:-BitcoinRPC2024SecurePassword456!} + - NBXPLORER_BIND=0.0.0.0:32838 + - NBXPLORER_VERBOSE=1 + - NBXPLORER_NOAUTH=1 + - POSTGRES_HOST=postgres + - POSTGRES_PORT=5432 + - POSTGRES_USER=postgres + - POSTGRES_PASSWORD=${POSTGRES_PASSWORD:-BTCPay2024SecurePassword123!} + - POSTGRES_DB=nbxplorer + volumes: + - nbxplorer_data:/datadir + networks: + - btcpaynetwork + depends_on: + - bitcoind + + bitcoind: + image: btcpayserver/bitcoin:26.0 + container_name: btcpay-bitcoind + restart: unless-stopped + command: > + bitcoind + -server=1 + -rpcuser=bitcoinrpc + -rpcpassword=${BTC_RPC_PASSWORD:-BitcoinRPC2024SecurePassword456!} + -rpcbind=0.0.0.0:8332 + -rpcallowip=0.0.0.0/0 + -txindex=1 + -prune=0 + volumes: + - bitcoin_data:/home/bitcoin/.bitcoin + networks: + - btcpaynetwork + ports: + - "8333:8333" # Bitcoin P2P + - "8332:8332" # Bitcoin RPC (internal only) + +volumes: + btcpay_datadir: + driver: local + btcpay_logs: + driver: local + postgres_data: + driver: local + nbxplorer_data: + driver: local + bitcoin_data: + driver: local + +networks: + btcpaynetwork: + driver: bridge \ No newline at end of file diff --git a/btcpay-simple-testnet.yml b/btcpay-simple-testnet.yml new file mode 100644 index 0000000..18a1265 --- /dev/null +++ b/btcpay-simple-testnet.yml @@ -0,0 +1,45 @@ +services: + btcpayserver: + image: btcpayserver/btcpayserver:2.2.0 + container_name: btcpayserver + restart: unless-stopped + ports: + - "49392:49392" + environment: + - BTCPAY_HOST=pay.silverlabs.uk + - BTCPAY_PROTOCOL=https + - BTCPAY_BIND=0.0.0.0:49392 + - BTCPAY_NETWORK=testnet + - BTCPAY_CHAINS=btc + - BTCPAY_ROOTPATH=/ + - POSTGRES_HOST=postgres + - POSTGRES_PORT=5432 + - POSTGRES_USER=postgres + - POSTGRES_PASSWORD=BTCPay2024SecurePassword123! + - POSTGRES_DB=btcpayserver + volumes: + - btcpay_datadir:/datadir + networks: + - btcpaynetwork + depends_on: + - postgres + + postgres: + image: postgres:13 + container_name: btcpay-postgres + restart: unless-stopped + environment: + - POSTGRES_USER=postgres + - POSTGRES_PASSWORD=BTCPay2024SecurePassword123! + - POSTGRES_DB=btcpayserver + volumes: + - postgres_data:/var/lib/postgresql/data + networks: + - btcpaynetwork + +volumes: + btcpay_datadir: + postgres_data: + +networks: + btcpaynetwork: \ No newline at end of file diff --git a/btcpay-working-compose.yml b/btcpay-working-compose.yml new file mode 100644 index 0000000..175dc24 --- /dev/null +++ b/btcpay-working-compose.yml @@ -0,0 +1,84 @@ +services: + btcpayserver: + image: btcpayserver/btcpayserver:2.2.0 + container_name: btcpayserver + restart: unless-stopped + ports: + - "49392:49392" + environment: + - POSTGRES_HOST=postgres + - POSTGRES_PORT=5432 + - POSTGRES_USER=postgres + - POSTGRES_PASSWORD=BTCPay2024SecurePassword123! + - POSTGRES_DB=btcpayserver + - BTCPAY_HOST=pay.silverlabs.uk + - BTCPAY_PROTOCOL=https + - BTCPAY_BIND=0.0.0.0:49392 + - BTCPAY_NETWORK=testnet + - BTCPAY_CHAINS=btc + - BTCPAY_BTCEXPLORERURL=http://nbxplorer:32838 + - BTCPAY_ROOTPATH=/ + volumes: + - btcpay_datadir:/datadir + networks: + - btcpaynetwork + depends_on: + - postgres + - nbxplorer + + postgres: + image: postgres:13 + container_name: btcpay-postgres + restart: unless-stopped + environment: + - POSTGRES_USER=postgres + - POSTGRES_PASSWORD=BTCPay2024SecurePassword123! + - POSTGRES_DB=btcpayserver + volumes: + - postgres_data:/var/lib/postgresql/data + networks: + - btcpaynetwork + + nbxplorer: + image: nicolasdorier/nbxplorer:2.5.30 + container_name: btcpay-nbxplorer + restart: unless-stopped + ports: + - "32838:32838" + environment: + - NBXPLORER_NETWORK=testnet + - NBXPLORER_CHAINS=btc + - NBXPLORER_BTCRPCURL=http://bitcoind:18332 + - NBXPLORER_BTCRPCUSER=bitcoinrpc + - NBXPLORER_BTCRPCPASSWORD=BitcoinRPC2024SecurePassword456! + - NBXPLORER_BIND=0.0.0.0:32838 + - NBXPLORER_NOAUTH=1 + volumes: + - nbxplorer_data:/datadir + networks: + - btcpaynetwork + depends_on: + - bitcoind + + bitcoind: + image: btcpayserver/bitcoin:26.0 + container_name: btcpay-bitcoind + restart: unless-stopped + environment: + - BITCOIN_NETWORK=testnet + volumes: + - bitcoin_data:/data + networks: + - btcpaynetwork + ports: + - "18333:18333" + - "18332:18332" + +volumes: + btcpay_datadir: + postgres_data: + nbxplorer_data: + bitcoin_data: + +networks: + btcpaynetwork: \ No newline at end of file diff --git a/btcpay.env b/btcpay.env new file mode 100644 index 0000000..04f2abb --- /dev/null +++ b/btcpay.env @@ -0,0 +1,21 @@ +# BTCPay Server Environment Configuration +# Generated for deployment to portainer-01 (10.0.0.51) + +# Database Configuration +POSTGRES_PASSWORD=BTCPay2024SecurePassword123! + +# Bitcoin RPC Configuration +BTC_RPC_PASSWORD=BitcoinRPC2024SecurePassword456! +BTC_RPC_AUTH=bitcoinrpc:28b2e126c32fe5f3e5cd8e43cddb98b98b33c9dd$$d3f4e8f21aa0c7ab24ed9a6d64c6803616c36fe6e57c5b2c00e7b8b6b4e8d8f1 + +# BTCPay Server Configuration +BTCPAY_ROOTPATH=/ +BTCPAY_HOST=pay.silverlabs.uk +BTCPAY_PROTOCOL=https + +# Logging +BTCPAY_DEBUGLOG=btcpay.log +BTCPAY_LOGS_LEVEL=info + +# Network (use mainnet for production, testnet for testing) +BTCPAY_NETWORK=mainnet \ No newline at end of file