BTCPay-infrastructure-recovery

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

92
BTCPAY_SETUP.md Normal file
View File

@ -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.)

175
DEPLOY_BTCPAY_SERVER.md Normal file
View File

@ -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`.

View File

@ -38,13 +38,21 @@ public class AccountController : Controller
return View(); 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)
{
// Get the actual user from database to get correct ID
var user = await _authService.GetUserByUsernameAsync(username);
if (user != null)
{ {
var claims = new List<Claim> var claims = new List<Claim>
{ {
new(ClaimTypes.Name, "admin"), new(ClaimTypes.Name, user.Username),
new(ClaimTypes.NameIdentifier, Guid.NewGuid().ToString()), new(ClaimTypes.NameIdentifier, user.Id.ToString()), // Use real database ID
new(ClaimTypes.Role, "Admin") new(ClaimTypes.Role, "Admin") // All users in admin system are admins
}; };
var identity = new ClaimsIdentity(claims, "Cookies"); var identity = new ClaimsIdentity(claims, "Cookies");
@ -53,6 +61,7 @@ public class AccountController : Controller
await HttpContext.SignInAsync("Cookies", principal); await HttpContext.SignInAsync("Cookies", principal);
return RedirectToAction("Index", "Dashboard"); return RedirectToAction("Index", "Dashboard");
} }
}
ModelState.AddModelError("", "Invalid username or password"); ModelState.AddModelError("", "Invalid username or password");
return View(); return View();

View File

@ -29,6 +29,8 @@ public class UsersController : Controller
[HttpPost] [HttpPost]
public async Task<IActionResult> Create(CreateUserDto model) public async Task<IActionResult> Create(CreateUserDto model)
{
try
{ {
if (!ModelState.IsValid) if (!ModelState.IsValid)
{ {
@ -38,12 +40,19 @@ public class UsersController : Controller
var user = await _authService.CreateUserAsync(model); var user = await _authService.CreateUserAsync(model);
if (user == null) if (user == null)
{ {
ModelState.AddModelError("", "User with this username already exists"); ModelState.AddModelError("Username", "User with this username already exists");
return View(model); return View(model);
} }
TempData["SuccessMessage"] = $"User '{user.Username}' created successfully";
return RedirectToAction(nameof(Index)); return RedirectToAction(nameof(Index));
} }
catch (Exception ex)
{
ModelState.AddModelError("", "An error occurred while creating the user: " + ex.Message);
return View(model);
}
}
public async Task<IActionResult> Edit(Guid id) public async Task<IActionResult> Edit(Guid id)
{ {
@ -66,6 +75,20 @@ public class UsersController : Controller
[HttpPost] [HttpPost]
public async Task<IActionResult> Edit(Guid id, UpdateUserDto model) public async Task<IActionResult> Edit(Guid id, UpdateUserDto model)
{ {
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) if (!ModelState.IsValid)
{ {
ViewData["UserId"] = id; ViewData["UserId"] = id;
@ -74,17 +97,67 @@ public class UsersController : Controller
var success = await _authService.UpdateUserAsync(id, model); var success = await _authService.UpdateUserAsync(id, model);
if (!success) if (!success)
{
// Check if it's because of duplicate username
var existingUser = await _authService.GetUserByIdAsync(id);
if (existingUser == null)
{ {
return NotFound(); 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)); return RedirectToAction(nameof(Index));
} }
catch (Exception ex)
{
ModelState.AddModelError("", "An error occurred while updating the user: " + ex.Message);
ViewData["UserId"] = id;
return View(model);
}
}
[HttpPost] [HttpPost]
public async Task<IActionResult> Delete(Guid id) public async Task<IActionResult> Delete(Guid id)
{ {
await _authService.DeleteUserAsync(id); 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)); return RedirectToAction(nameof(Index));
} }
} }
}

View File

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

View File

@ -15,6 +15,22 @@
</div> </div>
</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">
<div class="card-body"> <div class="card-body">
@if (Model.Any()) @if (Model.Any())

View File

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

View File

@ -44,9 +44,15 @@ public class PushNotificationController : ControllerBase
try try
{ {
var userIdClaim = User.FindFirst(ClaimTypes.NameIdentifier)?.Value; 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)) 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(); var userAgent = Request.Headers.UserAgent.ToString();
@ -65,6 +71,8 @@ public class PushNotificationController : ControllerBase
} }
catch (Exception ex) catch (Exception ex)
{ {
var logger = HttpContext.RequestServices.GetRequiredService<ILogger<PushNotificationController>>();
logger.LogError(ex, "Push subscription error");
return StatusCode(500, new { error = ex.Message }); return StatusCode(500, new { error = ex.Message });
} }
} }

View File

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

View File

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

View File

@ -23,11 +23,33 @@ public class DataSeederService : IDataSeederService
public async Task SeedSampleDataAsync() public async Task SeedSampleDataAsync()
{ {
// Check if we already have data await SeedProductionDataAsync();
var hasCategories = await _context.Categories.AnyAsync(); }
if (hasCategories)
private async Task SeedProductionDataAsync()
{ {
_logger.LogInformation("Sample data already exists, skipping seed"); _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())
{
_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; return;
} }
@ -69,75 +91,161 @@ public class DataSeederService : IDataSeederService
await _context.SaveChangesAsync(); await _context.SaveChangesAsync();
_logger.LogInformation("Created {Count} categories", categories.Count); _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> var products = new List<Product>
{ {
// ELECTRONICS - High-margin, popular items
new Product new Product
{ {
Id = Guid.NewGuid(), Id = Guid.NewGuid(),
Name = "Wireless Headphones", Name = "Wireless Noise-Cancelling Headphones",
Description = "High-quality Bluetooth headphones with noise cancellation", 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 = 89.99m, Price = 149.99m,
Weight = 250, Weight = 280,
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,
WeightUnit = ProductWeightUnit.Grams, WeightUnit = ProductWeightUnit.Grams,
StockQuantity = 25, 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, IsActive = true,
CreatedAt = DateTime.UtcNow, CreatedAt = DateTime.UtcNow,
UpdatedAt = DateTime.UtcNow UpdatedAt = DateTime.UtcNow
@ -488,4 +596,44 @@ public class DataSeederService : IDataSeederService
_logger.LogInformation("Sample data seeding completed successfully!"); _logger.LogInformation("Sample data seeding completed successfully!");
} }
private async Task UpdateProductionStockAsync()
{
_logger.LogInformation("Updating production product stock levels...");
var productStockUpdates = new Dictionary<string, int>
{
["Wireless Noise-Cancelling Headphones"] = 25,
["Fast Wireless Charging Stand"] = 50,
["Ultra-Slim Power Bank 20,000mAh"] = 35,
["Premium Phone Case with MagSafe"] = 75,
["Premium Cotton T-Shirt"] = 100,
["Classic Denim Jeans"] = 60,
["Cozy Knit Sweater"] = 40,
["The Complete Guide to Cryptocurrency"] = 30,
["Modern Web Development Handbook"] = 25,
["Mindfulness and Productivity Journal"] = 45
};
foreach (var update in productStockUpdates)
{
var product = await _context.Products
.FirstOrDefaultAsync(p => p.Name == update.Key);
if (product != null)
{
var oldStock = product.StockQuantity;
product.StockQuantity = update.Value;
product.UpdatedAt = DateTime.UtcNow;
_logger.LogInformation("Updated stock for {Product} from {OldStock} to {NewStock}", product.Name, oldStock, update.Value);
}
else
{
_logger.LogWarning("Product not found: {ProductName}", update.Key);
}
}
await _context.SaveChangesAsync();
_logger.LogInformation("Production stock update completed!");
}
} }

View File

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

View File

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

View File

@ -40,6 +40,14 @@ public class PushNotificationService : IPushNotificationService
{ {
try 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 // Check if subscription already exists
var existingSubscription = await _context.PushSubscriptions var existingSubscription = await _context.PushSubscriptions
.FirstOrDefaultAsync(ps => ps.Endpoint == subscriptionDto.Endpoint && ps.UserId == userId); .FirstOrDefaultAsync(ps => ps.Endpoint == subscriptionDto.Endpoint && ps.UserId == userId);
@ -53,6 +61,7 @@ public class PushNotificationService : IPushNotificationService
existingSubscription.IsActive = true; existingSubscription.IsActive = true;
existingSubscription.UserAgent = userAgent; existingSubscription.UserAgent = userAgent;
existingSubscription.IpAddress = ipAddress; existingSubscription.IpAddress = ipAddress;
Log.Information("Updated existing push subscription for user {UserId}", userId);
} }
else else
{ {
@ -71,10 +80,11 @@ public class PushNotificationService : IPushNotificationService
}; };
_context.PushSubscriptions.Add(subscription); _context.PushSubscriptions.Add(subscription);
Log.Information("Created new push subscription for user {UserId}", userId);
} }
await _context.SaveChangesAsync(); 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; return true;
} }
catch (Exception ex) catch (Exception ex)

View File

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

111
btcpay-minimal-compose.yml Normal file
View File

@ -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

View File

@ -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:

134
btcpay-server-compose.yml Normal file
View File

@ -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

115
btcpay-simple-compose.yml Normal file
View File

@ -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

45
btcpay-simple-testnet.yml Normal file
View File

@ -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:

View File

@ -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:

21
btcpay.env Normal file
View File

@ -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