Configure push notifications for internal-only access

- Changed VAPID subject from public URL to mailto format
- Updated docker-compose.yml to use mailto:admin@littleshop.local
- Removed dependency on thebankofdebbie.giize.com public domain
- All push notifications now work through VPN (admin.dark.side) only
- Added update-push-internal.sh helper script for deployment
- Improved security by keeping all admin traffic internal

Push notifications will continue working normally through FCM,
but all configuration and management stays on the internal network.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-09-30 21:15:42 +01:00
parent 021cfc4edc
commit 5e90b86d8c
183 changed files with 522322 additions and 6190 deletions

View File

@@ -1,145 +1,145 @@
@model IEnumerable<LittleShop.DTOs.ProductDto>
@{
ViewData["Title"] = "Products";
}
<div class="row mb-4">
<div class="col">
<h1><i class="fas fa-box"></i> Products</h1>
</div>
<div class="col-auto">
<div class="btn-group">
<a href="@Url.Action("Blazor")" class="btn btn-success">
<i class="fas fa-rocket"></i> <span class="d-none d-sm-inline">New</span> Blazor UI
</a>
<a href="@Url.Action("Create")" class="btn btn-primary">
<i class="fas fa-plus"></i> <span class="d-none d-sm-inline">Add Product</span>
</a>
<a href="@Url.Action("Import")" class="btn btn-outline-success">
<i class="fas fa-upload"></i> <span class="d-none d-sm-inline">Import</span>
</a>
<a href="@Url.Action("Export")" class="btn btn-outline-info">
<i class="fas fa-download"></i> <span class="d-none d-sm-inline">Export</span>
</a>
</div>
</div>
</div>
<div class="card">
<div class="card-body">
@if (Model.Any())
{
<div class="table-responsive">
<table class="table table-striped">
<thead>
<tr>
<th>Image</th>
<th>Name</th>
<th>Category</th>
<th>Price</th>
<th>Variations</th>
<th>Stock</th>
<th>Weight</th>
<th>Status</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
@foreach (var product in Model)
{
<tr>
<td>
@if (product.Photos.Any())
{
<img src="@product.Photos.First().FilePath" alt="@product.Photos.First().AltText" class="img-thumbnail" style="width: 50px; height: 50px; object-fit: cover;">
}
else
{
<div class="bg-light d-flex align-items-center justify-content-center" style="width: 50px; height: 50px;">
<i class="fas fa-image text-muted"></i>
</div>
}
</td>
<td>
<strong>@product.Name</strong>
<br><small class="text-muted">@product.Description.Substring(0, Math.Min(50, product.Description.Length))@(product.Description.Length > 50 ? "..." : "")</small>
</td>
<td>
<span class="badge bg-secondary">@product.CategoryName</span>
</td>
<td>
<strong>£@product.Price</strong>
</td>
<td>
@if (product.MultiBuys.Any())
{
<span class="badge bg-info">@product.MultiBuys.Count() multi-buys</span>
}
@if (product.Variants.Any())
{
<span class="badge bg-success">@product.Variants.Count() variants</span>
}
@if (!product.MultiBuys.Any() && !product.Variants.Any())
{
<span class="text-muted">None</span>
}
</td>
<td>
@if (product.StockQuantity > 0)
{
<span class="badge bg-success">@product.StockQuantity in stock</span>
}
else
{
<span class="badge bg-warning text-dark">Out of stock</span>
}
</td>
<td>
@product.Weight @product.WeightUnit.ToString().ToLower()
</td>
<td>
@if (product.IsActive)
{
<span class="badge bg-success">Active</span>
}
else
{
<span class="badge bg-danger">Inactive</span>
}
</td>
<td>
<div class="btn-group btn-group-sm">
<a href="@Url.Action("Edit", new { id = product.Id })" class="btn btn-outline-primary" title="Edit Product">
<i class="fas fa-edit"></i>
</a>
<a href="@Url.Action("Variations", new { id = product.Id })" class="btn btn-outline-info" title="Manage Multi-Buys">
<i class="fas fa-tags"></i>
</a>
<a href="@Url.Action("Variants", new { id = product.Id })" class="btn btn-outline-success" title="Manage Variants">
<i class="fas fa-palette"></i>
</a>
<form method="post" action="@Url.Action("Delete", new { id = product.Id })" class="d-inline"
onsubmit="return confirm('Are you sure you want to delete this product?')">
@Html.AntiForgeryToken()
<button type="submit" class="btn btn-outline-danger" title="Delete Product">
<i class="fas fa-trash"></i>
</button>
</form>
</div>
</td>
</tr>
}
</tbody>
</table>
</div>
}
else
{
<div class="text-center py-4">
<i class="fas fa-box fa-3x text-muted mb-3"></i>
<p class="text-muted">No products found. <a href="@Url.Action("Create")">Create your first product</a>.</p>
</div>
}
</div>
@model IEnumerable<LittleShop.DTOs.ProductDto>
@{
ViewData["Title"] = "Products";
}
<div class="row mb-4">
<div class="col">
<h1><i class="fas fa-box"></i> Products</h1>
</div>
<div class="col-auto">
<div class="btn-group">
<a href="@Url.Action("Blazor")" class="btn btn-success">
<i class="fas fa-rocket"></i> <span class="d-none d-sm-inline">New</span> Blazor UI
</a>
<a href="@Url.Action("Create")" class="btn btn-primary">
<i class="fas fa-plus"></i> <span class="d-none d-sm-inline">Add Product</span>
</a>
<a href="@Url.Action("Import")" class="btn btn-outline-success">
<i class="fas fa-upload"></i> <span class="d-none d-sm-inline">Import</span>
</a>
<a href="@Url.Action("Export")" class="btn btn-outline-info">
<i class="fas fa-download"></i> <span class="d-none d-sm-inline">Export</span>
</a>
</div>
</div>
</div>
<div class="card">
<div class="card-body">
@if (Model.Any())
{
<div class="table-responsive">
<table class="table table-striped">
<thead>
<tr>
<th>Image</th>
<th>Name</th>
<th>Category</th>
<th>Price</th>
<th>Variations</th>
<th>Stock</th>
<th>Weight</th>
<th>Status</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
@foreach (var product in Model)
{
<tr>
<td>
@if (product.Photos.Any())
{
<img src="@product.Photos.First().FilePath" alt="@product.Photos.First().AltText" class="img-thumbnail" style="width: 50px; height: 50px; object-fit: cover;">
}
else
{
<div class="bg-light d-flex align-items-center justify-content-center" style="width: 50px; height: 50px;">
<i class="fas fa-image text-muted"></i>
</div>
}
</td>
<td>
<strong>@product.Name</strong>
<br><small class="text-muted">@product.Description.Substring(0, Math.Min(50, product.Description.Length))@(product.Description.Length > 50 ? "..." : "")</small>
</td>
<td>
<span class="badge bg-secondary">@product.CategoryName</span>
</td>
<td>
<strong>£@product.Price</strong>
</td>
<td>
@if (product.MultiBuys.Any())
{
<span class="badge bg-info">@product.MultiBuys.Count() multi-buys</span>
}
@if (product.Variants.Any())
{
<span class="badge bg-success">@product.Variants.Count() variants</span>
}
@if (!product.MultiBuys.Any() && !product.Variants.Any())
{
<span class="text-muted">None</span>
}
</td>
<td>
@if (product.StockQuantity > 0)
{
<span class="badge bg-success">@product.StockQuantity in stock</span>
}
else
{
<span class="badge bg-warning text-dark">Out of stock</span>
}
</td>
<td>
@product.Weight @product.WeightUnit.ToString().ToLower()
</td>
<td>
@if (product.IsActive)
{
<span class="badge bg-success">Active</span>
}
else
{
<span class="badge bg-danger">Inactive</span>
}
</td>
<td>
<div class="btn-group btn-group-sm">
<a href="@Url.Action("Edit", new { id = product.Id })" class="btn btn-outline-primary" title="Edit Product">
<i class="fas fa-edit"></i>
</a>
<a href="@Url.Action("Variations", new { id = product.Id })" class="btn btn-outline-info" title="Manage Multi-Buys">
<i class="fas fa-tags"></i>
</a>
<a href="@Url.Action("Variants", new { id = product.Id })" class="btn btn-outline-success" title="Manage Variants">
<i class="fas fa-palette"></i>
</a>
<form method="post" action="@Url.Action("Delete", new { id = product.Id })" class="d-inline"
onsubmit="return confirm('Are you sure you want to delete this product?')">
@Html.AntiForgeryToken()
<button type="submit" class="btn btn-outline-danger" title="Delete Product">
<i class="fas fa-trash"></i>
</button>
</form>
</div>
</td>
</tr>
}
</tbody>
</table>
</div>
}
else
{
<div class="text-center py-4">
<i class="fas fa-box fa-3x text-muted mb-3"></i>
<p class="text-muted">No products found. <a href="@Url.Action("Create")">Create your first product</a>.</p>
</div>
}
</div>
</div>