Some checks failed
Build and Deploy LittleShop / Build TeleBot Docker Image (push) Failing after 11s
Build and Deploy LittleShop / Build LittleShop Docker Image (push) Failing after 15s
Build and Deploy LittleShop / Deploy to Production VPS (Manual Only) (push) Has been skipped
Build and Deploy LittleShop / Deploy to Pre-Production (CT109) (push) Has been skipped
Major Feature Additions: - Customer management: Full CRUD with data export and privacy compliance - Payment management: Centralized payment tracking and administration - Push notification subscriptions: Manage and track web push subscriptions Security Enhancements: - IP whitelist middleware for administrative endpoints - Data retention service with configurable policies - Enhanced push notification security documentation - Security fixes progress tracking (2025-11-14) UI/UX Improvements: - Enhanced navigation with improved mobile responsiveness - Updated admin dashboard with order status counts - Improved product CRUD forms - New customer and payment management interfaces Backend Improvements: - Extended customer service with data export capabilities - Enhanced order service with status count queries - Improved crypto payment service with better error handling - Updated validators and configuration Documentation: - DEPLOYMENT_NGINX_GUIDE.md: Nginx deployment instructions - IP_STORAGE_ANALYSIS.md: IP storage security analysis - PUSH_NOTIFICATION_SECURITY.md: Push notification security guide - UI_UX_IMPROVEMENT_PLAN.md: Planned UI/UX enhancements - UI_UX_IMPROVEMENTS_COMPLETED.md: Completed improvements Cleanup: - Removed temporary database WAL files - Removed stale commit message file 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
261 lines
13 KiB
Plaintext
261 lines
13 KiB
Plaintext
@model IEnumerable<LittleShop.Models.PushSubscription>
|
|
@{
|
|
ViewData["Title"] = "Push Subscriptions";
|
|
}
|
|
|
|
<div class="row mb-3">
|
|
<div class="col">
|
|
<nav aria-label="breadcrumb">
|
|
<ol class="breadcrumb">
|
|
<li class="breadcrumb-item"><a href="@Url.Action("Index", "Dashboard")">Dashboard</a></li>
|
|
<li class="breadcrumb-item active">Push Subscriptions</li>
|
|
</ol>
|
|
</nav>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="row mb-3">
|
|
<div class="col-md-8">
|
|
<h1><i class="fas fa-bell"></i> Push Subscriptions</h1>
|
|
<p class="text-muted mb-0">Manage browser push notification subscriptions for admins and customers</p>
|
|
</div>
|
|
<div class="col-md-4 text-end">
|
|
<form method="post" action="@Url.Action("CleanupExpired")" class="d-inline">
|
|
@Html.AntiForgeryToken()
|
|
<button type="submit" class="btn btn-warning"
|
|
onclick="return confirm('Remove all inactive and expired subscriptions?')">
|
|
<i class="fas fa-broom"></i> Cleanup Expired
|
|
</button>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Statistics Cards -->
|
|
<div class="row mb-4">
|
|
<div class="col-md-3">
|
|
<div class="card border-primary">
|
|
<div class="card-body">
|
|
<h6 class="text-muted mb-2">Total Subscriptions</h6>
|
|
<h3 class="mb-0">@Model.Count()</h3>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-3">
|
|
<div class="card border-success">
|
|
<div class="card-body">
|
|
<h6 class="text-muted mb-2">Active</h6>
|
|
<h3 class="mb-0 text-success">@Model.Count(s => s.IsActive)</h3>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-3">
|
|
<div class="card border-info">
|
|
<div class="card-body">
|
|
<h6 class="text-muted mb-2">Admin Users</h6>
|
|
<h3 class="mb-0">@Model.Count(s => s.UserId.HasValue)</h3>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-3">
|
|
<div class="card border-warning">
|
|
<div class="card-body">
|
|
<h6 class="text-muted mb-2">Customers</h6>
|
|
<h3 class="mb-0">@Model.Count(s => s.CustomerId.HasValue)</h3>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Subscriptions Table -->
|
|
@if (Model.Any())
|
|
{
|
|
<div class="card">
|
|
<div class="card-header bg-light">
|
|
<h5 class="mb-0"><i class="fas fa-list"></i> Subscription List</h5>
|
|
</div>
|
|
<div class="card-body p-0">
|
|
<div class="table-responsive">
|
|
<table class="table table-hover mb-0">
|
|
<thead class="table-light">
|
|
<tr>
|
|
<th style="width: 5%;">ID</th>
|
|
<th style="width: 15%;">Type</th>
|
|
<th style="width: 20%;">Endpoint</th>
|
|
<th style="width: 15%;">Subscribed</th>
|
|
<th style="width: 15%;">Last Used</th>
|
|
<th style="width: 15%;">Browser/Device</th>
|
|
<th style="width: 8%;">Status</th>
|
|
<th class="text-center" style="width: 7%;">Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
@foreach (var subscription in Model)
|
|
{
|
|
var daysInactive = subscription.LastUsedAt.HasValue
|
|
? (DateTime.UtcNow - subscription.LastUsedAt.Value).Days
|
|
: (DateTime.UtcNow - subscription.SubscribedAt).Days;
|
|
|
|
var statusClass = subscription.IsActive
|
|
? (daysInactive > 90 ? "warning" : "success")
|
|
: "danger";
|
|
|
|
<tr>
|
|
<td><code>@subscription.Id</code></td>
|
|
<td>
|
|
@if (subscription.UserId.HasValue)
|
|
{
|
|
<span class="badge bg-info">
|
|
<i class="fas fa-user-shield"></i> Admin User
|
|
</span>
|
|
@if (subscription.User != null)
|
|
{
|
|
<br><small class="text-muted">@subscription.User.Username</small>
|
|
}
|
|
}
|
|
else if (subscription.CustomerId.HasValue)
|
|
{
|
|
<span class="badge bg-warning">
|
|
<i class="fas fa-user"></i> Customer
|
|
</span>
|
|
@if (subscription.Customer != null)
|
|
{
|
|
<br><small class="text-muted">@subscription.Customer.TelegramDisplayName</small>
|
|
}
|
|
}
|
|
else
|
|
{
|
|
<span class="badge bg-secondary">
|
|
<i class="fas fa-question"></i> Unknown
|
|
</span>
|
|
}
|
|
</td>
|
|
<td>
|
|
<small class="font-monospace text-break"
|
|
data-bs-toggle="tooltip"
|
|
title="@subscription.Endpoint">
|
|
@(subscription.Endpoint.Length > 40 ? subscription.Endpoint.Substring(0, 40) + "..." : subscription.Endpoint)
|
|
</small>
|
|
@if (!string.IsNullOrEmpty(subscription.IpAddress))
|
|
{
|
|
<br><span class="badge bg-secondary"><i class="fas fa-network-wired"></i> @subscription.IpAddress</span>
|
|
}
|
|
</td>
|
|
<td>
|
|
@subscription.SubscribedAt.ToString("MMM dd, yyyy")
|
|
<br><small class="text-muted">@subscription.SubscribedAt.ToString("HH:mm")</small>
|
|
</td>
|
|
<td>
|
|
@if (subscription.LastUsedAt.HasValue)
|
|
{
|
|
@subscription.LastUsedAt.Value.ToString("MMM dd, yyyy")
|
|
<br><small class="text-muted">@subscription.LastUsedAt.Value.ToString("HH:mm")</small>
|
|
|
|
@if (daysInactive > 0)
|
|
{
|
|
<br><span class="badge bg-@(daysInactive > 90 ? "danger" : daysInactive > 30 ? "warning" : "info")">
|
|
@daysInactive days ago
|
|
</span>
|
|
}
|
|
}
|
|
else
|
|
{
|
|
<span class="text-muted">Never</span>
|
|
}
|
|
</td>
|
|
<td>
|
|
@if (!string.IsNullOrEmpty(subscription.UserAgent))
|
|
{
|
|
var ua = subscription.UserAgent;
|
|
var browser = ua.Contains("Chrome") ? "Chrome" :
|
|
ua.Contains("Firefox") ? "Firefox" :
|
|
ua.Contains("Safari") ? "Safari" :
|
|
ua.Contains("Edge") ? "Edge" : "Unknown";
|
|
var os = ua.Contains("Windows") ? "Windows" :
|
|
ua.Contains("Mac") ? "macOS" :
|
|
ua.Contains("Linux") ? "Linux" :
|
|
ua.Contains("Android") ? "Android" :
|
|
ua.Contains("iOS") ? "iOS" : "Unknown";
|
|
|
|
<span class="badge bg-secondary">@browser</span>
|
|
<span class="badge bg-dark">@os</span>
|
|
<br><small class="text-muted"
|
|
data-bs-toggle="tooltip"
|
|
title="@subscription.UserAgent">
|
|
@(ua.Length > 30 ? ua.Substring(0, 30) + "..." : ua)
|
|
</small>
|
|
}
|
|
else
|
|
{
|
|
<span class="text-muted">Not available</span>
|
|
}
|
|
</td>
|
|
<td class="text-center">
|
|
<span class="badge bg-@statusClass">
|
|
@if (subscription.IsActive)
|
|
{
|
|
<i class="fas fa-check-circle"></i> <text>Active</text>
|
|
}
|
|
else
|
|
{
|
|
<i class="fas fa-times-circle"></i> <text>Inactive</text>
|
|
}
|
|
</span>
|
|
</td>
|
|
<td class="text-center">
|
|
<form method="post" action="@Url.Action("Delete", new { id = subscription.Id })" class="d-inline">
|
|
@Html.AntiForgeryToken()
|
|
<button type="submit"
|
|
class="btn btn-sm btn-danger"
|
|
onclick="return confirm('Are you sure you want to delete this push subscription?')"
|
|
title="Delete Subscription">
|
|
<i class="fas fa-trash"></i>
|
|
</button>
|
|
</form>
|
|
</td>
|
|
</tr>
|
|
}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
}
|
|
else
|
|
{
|
|
<div class="alert alert-info">
|
|
<i class="fas fa-info-circle"></i>
|
|
<strong>No push subscriptions yet</strong>
|
|
<p class="mb-0">Push notification subscriptions will appear here when users enable browser notifications.</p>
|
|
</div>
|
|
}
|
|
|
|
<!-- Information Card -->
|
|
<div class="row mt-4">
|
|
<div class="col">
|
|
<div class="card border-info">
|
|
<div class="card-header bg-info text-white">
|
|
<h6 class="mb-0"><i class="fas fa-info-circle"></i> About Push Subscriptions</h6>
|
|
</div>
|
|
<div class="card-body">
|
|
<ul class="mb-0">
|
|
<li><strong>Active Status:</strong> Subscriptions marked as active can receive push notifications</li>
|
|
<li><strong>IP Address Storage:</strong> IP addresses are stored for security and duplicate detection purposes</li>
|
|
<li><strong>Cleanup:</strong> Expired subscriptions (inactive for >90 days) can be removed using the cleanup button</li>
|
|
<li><strong>User Agent:</strong> Browser and device information helps identify subscription sources</li>
|
|
<li><strong>Privacy:</strong> Subscription data contains encryption keys required for Web Push API</li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
@section Scripts {
|
|
<script>
|
|
// Initialize Bootstrap tooltips
|
|
var tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'))
|
|
var tooltipList = tooltipTriggerList.map(function (tooltipTriggerEl) {
|
|
return new bootstrap.Tooltip(tooltipTriggerEl)
|
|
});
|
|
</script>
|
|
}
|