BTCPay-infrastructure-recovery
This commit is contained in:
parent
b4cee007c4
commit
be4d797c6c
92
BTCPAY_SETUP.md
Normal file
92
BTCPAY_SETUP.md
Normal 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
175
DEPLOY_BTCPAY_SERVER.md
Normal 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`.
|
||||||
@ -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();
|
||||||
|
|||||||
@ -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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
82
LittleShop/Areas/Admin/Views/Users/Edit.cshtml
Normal file
82
LittleShop/Areas/Admin/Views/Users/Edit.cshtml
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
@model LittleShop.DTOs.UpdateUserDto
|
||||||
|
|
||||||
|
@{
|
||||||
|
ViewData["Title"] = "Edit User";
|
||||||
|
var userId = ViewData["UserId"] as Guid?;
|
||||||
|
}
|
||||||
|
|
||||||
|
<div class="row mb-4">
|
||||||
|
<div class="col">
|
||||||
|
<h1><i class="fas fa-user-edit"></i> Edit User</h1>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-8">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body">
|
||||||
|
<form method="post" asp-area="Admin" asp-controller="Users" asp-action="Edit" asp-route-id="@userId">
|
||||||
|
@Html.AntiForgeryToken()
|
||||||
|
|
||||||
|
@if (ViewData.ModelState[""] != null && ViewData.ModelState[""].Errors.Count > 0)
|
||||||
|
{
|
||||||
|
<div class="alert alert-danger" role="alert">
|
||||||
|
@foreach (var error in ViewData.ModelState[""].Errors)
|
||||||
|
{
|
||||||
|
<div>@error.ErrorMessage</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="Username" class="form-label">Username</label>
|
||||||
|
<input name="Username" id="Username" class="form-control" value="@Model?.Username" required />
|
||||||
|
<div class="form-text">Must be unique across all users</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="Password" class="form-label">New Password</label>
|
||||||
|
<input name="Password" id="Password" type="password" class="form-control" />
|
||||||
|
<div class="form-text">Leave blank to keep current password. Minimum 3 characters if changing.</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3 form-check">
|
||||||
|
<input name="IsActive" id="IsActive" type="checkbox" class="form-check-input" value="true" @(Model?.IsActive == true ? "checked" : "") />
|
||||||
|
<input name="IsActive" type="hidden" value="false" />
|
||||||
|
<label for="IsActive" class="form-check-label">
|
||||||
|
User is active (can log in)
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-flex justify-content-between">
|
||||||
|
<a href="@Url.Action("Index")" class="btn btn-secondary">
|
||||||
|
<i class="fas fa-arrow-left"></i> Back to Users
|
||||||
|
</a>
|
||||||
|
<button type="submit" class="btn btn-primary">
|
||||||
|
<i class="fas fa-save"></i> Update User
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5><i class="fas fa-info-circle"></i> Edit Information</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<ul class="list-unstyled">
|
||||||
|
<li><strong>Username:</strong> Can be changed if unique</li>
|
||||||
|
<li><strong>Password:</strong> Optional - leave blank to keep current</li>
|
||||||
|
<li><strong>Status:</strong> Inactive users cannot log in</li>
|
||||||
|
</ul>
|
||||||
|
<div class="alert alert-warning mt-3">
|
||||||
|
<i class="fas fa-exclamation-triangle"></i>
|
||||||
|
<strong>Warning:</strong> Deactivating your own account will lock you out.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@ -15,6 +15,22 @@
|
|||||||
</div>
|
</div>
|
||||||
</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())
|
||||||
|
|||||||
180
LittleShop/Controllers/BTCPayWebhookController.cs
Normal file
180
LittleShop/Controllers/BTCPayWebhookController.cs
Normal file
@ -0,0 +1,180 @@
|
|||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using System.Security.Cryptography;
|
||||||
|
using System.Text;
|
||||||
|
using System.Text.Json;
|
||||||
|
using LittleShop.DTOs;
|
||||||
|
using LittleShop.Services;
|
||||||
|
using LittleShop.Enums;
|
||||||
|
|
||||||
|
namespace LittleShop.Controllers;
|
||||||
|
|
||||||
|
[ApiController]
|
||||||
|
[Route("api/btcpay")]
|
||||||
|
public class BTCPayWebhookController : ControllerBase
|
||||||
|
{
|
||||||
|
private readonly ICryptoPaymentService _cryptoPaymentService;
|
||||||
|
private readonly IBTCPayServerService _btcPayService;
|
||||||
|
private readonly IConfiguration _configuration;
|
||||||
|
private readonly ILogger<BTCPayWebhookController> _logger;
|
||||||
|
|
||||||
|
public BTCPayWebhookController(
|
||||||
|
ICryptoPaymentService cryptoPaymentService,
|
||||||
|
IBTCPayServerService btcPayService,
|
||||||
|
IConfiguration configuration,
|
||||||
|
ILogger<BTCPayWebhookController> logger)
|
||||||
|
{
|
||||||
|
_cryptoPaymentService = cryptoPaymentService;
|
||||||
|
_btcPayService = btcPayService;
|
||||||
|
_configuration = configuration;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("webhook")]
|
||||||
|
public async Task<IActionResult> ProcessWebhook()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Read the raw request body
|
||||||
|
using var reader = new StreamReader(Request.Body);
|
||||||
|
var requestBody = await reader.ReadToEndAsync();
|
||||||
|
|
||||||
|
// Get webhook signature from headers
|
||||||
|
var signature = Request.Headers["BTCPAY-SIG"].FirstOrDefault();
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(signature))
|
||||||
|
{
|
||||||
|
_logger.LogWarning("Webhook received without signature");
|
||||||
|
return BadRequest("Missing webhook signature");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate webhook signature
|
||||||
|
var webhookSecret = _configuration["BTCPayServer:WebhookSecret"];
|
||||||
|
if (string.IsNullOrEmpty(webhookSecret))
|
||||||
|
{
|
||||||
|
_logger.LogError("BTCPay webhook secret not configured");
|
||||||
|
return StatusCode(500, "Webhook validation not configured");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ValidateWebhookSignature(requestBody, signature, webhookSecret))
|
||||||
|
{
|
||||||
|
_logger.LogWarning("Invalid webhook signature");
|
||||||
|
return BadRequest("Invalid webhook signature");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse webhook data
|
||||||
|
var webhookData = JsonSerializer.Deserialize<BTCPayWebhookDto>(requestBody, new JsonSerializerOptions
|
||||||
|
{
|
||||||
|
PropertyNameCaseInsensitive = true
|
||||||
|
});
|
||||||
|
|
||||||
|
if (webhookData == null)
|
||||||
|
{
|
||||||
|
_logger.LogWarning("Unable to parse webhook data");
|
||||||
|
return BadRequest("Invalid webhook data");
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.LogInformation("Processing BTCPay webhook: Type={Type}, InvoiceId={InvoiceId}, StoreId={StoreId}",
|
||||||
|
webhookData.Type, webhookData.InvoiceId, webhookData.StoreId);
|
||||||
|
|
||||||
|
// Process the webhook based on event type
|
||||||
|
var success = await ProcessWebhookEvent(webhookData);
|
||||||
|
|
||||||
|
if (!success)
|
||||||
|
{
|
||||||
|
return BadRequest("Failed to process webhook");
|
||||||
|
}
|
||||||
|
|
||||||
|
return Ok();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error processing BTCPay webhook");
|
||||||
|
return StatusCode(500, "Internal server error");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool ValidateWebhookSignature(string payload, string signature, string secret)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// BTCPay Server uses HMAC-SHA256 with format "sha256=<hex>"
|
||||||
|
if (!signature.StartsWith("sha256="))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var expectedHash = signature.Substring(7); // Remove "sha256=" prefix
|
||||||
|
var secretBytes = Encoding.UTF8.GetBytes(secret);
|
||||||
|
var payloadBytes = Encoding.UTF8.GetBytes(payload);
|
||||||
|
|
||||||
|
using var hmac = new HMACSHA256(secretBytes);
|
||||||
|
var computedHash = hmac.ComputeHash(payloadBytes);
|
||||||
|
var computedHashHex = Convert.ToHexString(computedHash).ToLowerInvariant();
|
||||||
|
|
||||||
|
return expectedHash.Equals(computedHashHex, StringComparison.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error validating webhook signature");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<bool> ProcessWebhookEvent(BTCPayWebhookDto webhookData)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Map BTCPay webhook event types to our payment status
|
||||||
|
var paymentStatus = MapWebhookEventToPaymentStatus(webhookData.Type);
|
||||||
|
|
||||||
|
if (!paymentStatus.HasValue)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Ignoring webhook event type: {Type}", webhookData.Type);
|
||||||
|
return true; // Not an error, just not a status we care about
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract payment details
|
||||||
|
var amount = webhookData.Payment?.PaymentMethodPaid ?? 0;
|
||||||
|
var transactionHash = webhookData.Payment?.TransactionData?.TransactionHash;
|
||||||
|
|
||||||
|
// Process the payment update
|
||||||
|
var success = await _cryptoPaymentService.ProcessPaymentWebhookAsync(
|
||||||
|
webhookData.InvoiceId,
|
||||||
|
paymentStatus.Value,
|
||||||
|
amount,
|
||||||
|
transactionHash);
|
||||||
|
|
||||||
|
if (success)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Successfully processed webhook for invoice {InvoiceId} with status {Status}",
|
||||||
|
webhookData.InvoiceId, paymentStatus.Value);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_logger.LogWarning("Failed to process webhook for invoice {InvoiceId}", webhookData.InvoiceId);
|
||||||
|
}
|
||||||
|
|
||||||
|
return success;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error processing webhook event for invoice {InvoiceId}", webhookData.InvoiceId);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static PaymentStatus? MapWebhookEventToPaymentStatus(string eventType)
|
||||||
|
{
|
||||||
|
return eventType switch
|
||||||
|
{
|
||||||
|
"InvoiceCreated" => PaymentStatus.Pending,
|
||||||
|
"InvoiceReceivedPayment" => PaymentStatus.Processing,
|
||||||
|
"InvoicePaymentSettled" => PaymentStatus.Completed,
|
||||||
|
"InvoiceProcessing" => PaymentStatus.Processing,
|
||||||
|
"InvoiceExpired" => PaymentStatus.Expired,
|
||||||
|
"InvoiceSettled" => PaymentStatus.Completed,
|
||||||
|
"InvoiceInvalid" => PaymentStatus.Failed,
|
||||||
|
_ => null // Unknown event type
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -44,9 +44,15 @@ public class PushNotificationController : ControllerBase
|
|||||||
try
|
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 });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
94
LittleShop/DTOs/BTCPayWebhookDto.cs
Normal file
94
LittleShop/DTOs/BTCPayWebhookDto.cs
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace LittleShop.DTOs;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// DTO for BTCPay Server webhook events
|
||||||
|
/// Based on BTCPay Server webhook documentation
|
||||||
|
/// </summary>
|
||||||
|
public class BTCPayWebhookDto
|
||||||
|
{
|
||||||
|
[JsonPropertyName("deliveryId")]
|
||||||
|
public string DeliveryId { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
[JsonPropertyName("webhookId")]
|
||||||
|
public string WebhookId { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
[JsonPropertyName("originalDeliveryId")]
|
||||||
|
public string? OriginalDeliveryId { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("isRedelivery")]
|
||||||
|
public bool IsRedelivery { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("type")]
|
||||||
|
public string Type { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
[JsonPropertyName("timestamp")]
|
||||||
|
public long Timestamp { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("storeId")]
|
||||||
|
public string StoreId { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
[JsonPropertyName("invoiceId")]
|
||||||
|
public string InvoiceId { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
[JsonPropertyName("afterExpiration")]
|
||||||
|
public bool? AfterExpiration { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("manuallyMarked")]
|
||||||
|
public bool? ManuallyMarked { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("overPaid")]
|
||||||
|
public bool? OverPaid { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("partiallyPaid")]
|
||||||
|
public bool? PartiallyPaid { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("payment")]
|
||||||
|
public BTCPayWebhookPayment? Payment { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class BTCPayWebhookPayment
|
||||||
|
{
|
||||||
|
[JsonPropertyName("id")]
|
||||||
|
public string Id { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
[JsonPropertyName("receivedDate")]
|
||||||
|
public long ReceivedDate { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("value")]
|
||||||
|
public decimal Value { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("fee")]
|
||||||
|
public decimal? Fee { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("status")]
|
||||||
|
public string Status { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
[JsonPropertyName("destination")]
|
||||||
|
public string? Destination { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("paymentMethod")]
|
||||||
|
public string PaymentMethod { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
[JsonPropertyName("paymentMethodPaid")]
|
||||||
|
public decimal PaymentMethodPaid { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("transactionData")]
|
||||||
|
public BTCPayWebhookTransactionData? TransactionData { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class BTCPayWebhookTransactionData
|
||||||
|
{
|
||||||
|
[JsonPropertyName("transactionHash")]
|
||||||
|
public string? TransactionHash { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("blockHash")]
|
||||||
|
public string? BlockHash { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("blockHeight")]
|
||||||
|
public int? BlockHeight { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("confirmations")]
|
||||||
|
public int? Confirmations { get; set; }
|
||||||
|
}
|
||||||
@ -103,6 +103,21 @@ public class AuthService : IAuthService
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<UserDto?> GetUserByUsernameAsync(string username)
|
||||||
|
{
|
||||||
|
var user = await _context.Users
|
||||||
|
.FirstOrDefaultAsync(u => u.Username == username && u.IsActive);
|
||||||
|
if (user == null) return null;
|
||||||
|
|
||||||
|
return new UserDto
|
||||||
|
{
|
||||||
|
Id = user.Id,
|
||||||
|
Username = user.Username,
|
||||||
|
CreatedAt = user.CreatedAt,
|
||||||
|
IsActive = user.IsActive
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<IEnumerable<UserDto>> GetAllUsersAsync()
|
public async Task<IEnumerable<UserDto>> GetAllUsersAsync()
|
||||||
{
|
{
|
||||||
return await _context.Users
|
return await _context.Users
|
||||||
|
|||||||
@ -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!");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@ -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);
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
@ -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
111
btcpay-minimal-compose.yml
Normal 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
|
||||||
35
btcpay-minimal-working.yml
Normal file
35
btcpay-minimal-working.yml
Normal 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
134
btcpay-server-compose.yml
Normal 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
115
btcpay-simple-compose.yml
Normal 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
45
btcpay-simple-testnet.yml
Normal 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:
|
||||||
84
btcpay-working-compose.yml
Normal file
84
btcpay-working-compose.yml
Normal 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
21
btcpay.env
Normal 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
|
||||||
Loading…
Reference in New Issue
Block a user