WebPush-and-photo-upload-fixes
This commit is contained in:
355
LittleShop/Services/PushNotificationService.cs
Normal file
355
LittleShop/Services/PushNotificationService.cs
Normal file
@@ -0,0 +1,355 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Serilog;
|
||||
using WebPush;
|
||||
using System.Text.Json;
|
||||
using LittleShop.Data;
|
||||
using LittleShop.DTOs;
|
||||
using LittleShop.Models;
|
||||
|
||||
namespace LittleShop.Services;
|
||||
|
||||
public class PushNotificationService : IPushNotificationService
|
||||
{
|
||||
private readonly LittleShopContext _context;
|
||||
private readonly IConfiguration _configuration;
|
||||
private readonly WebPushClient _webPushClient;
|
||||
private readonly VapidDetails _vapidDetails;
|
||||
|
||||
public PushNotificationService(LittleShopContext context, IConfiguration configuration)
|
||||
{
|
||||
_context = context;
|
||||
_configuration = configuration;
|
||||
|
||||
// Initialize VAPID details
|
||||
_vapidDetails = new VapidDetails(
|
||||
subject: _configuration["WebPush:Subject"] ?? "mailto:admin@littleshop.local",
|
||||
publicKey: _configuration["WebPush:VapidPublicKey"] ?? throw new InvalidOperationException("WebPush:VapidPublicKey not configured"),
|
||||
privateKey: _configuration["WebPush:VapidPrivateKey"] ?? throw new InvalidOperationException("WebPush:VapidPrivateKey not configured")
|
||||
);
|
||||
|
||||
_webPushClient = new WebPushClient();
|
||||
}
|
||||
|
||||
public string GetVapidPublicKey()
|
||||
{
|
||||
return _vapidDetails.PublicKey;
|
||||
}
|
||||
|
||||
public async Task<bool> SubscribeUserAsync(Guid userId, PushSubscriptionDto subscriptionDto, string? userAgent = null, string? ipAddress = null)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Check if subscription already exists
|
||||
var existingSubscription = await _context.PushSubscriptions
|
||||
.FirstOrDefaultAsync(ps => ps.Endpoint == subscriptionDto.Endpoint && ps.UserId == userId);
|
||||
|
||||
if (existingSubscription != null)
|
||||
{
|
||||
// Update existing subscription
|
||||
existingSubscription.P256DH = subscriptionDto.P256DH;
|
||||
existingSubscription.Auth = subscriptionDto.Auth;
|
||||
existingSubscription.LastUsedAt = DateTime.UtcNow;
|
||||
existingSubscription.IsActive = true;
|
||||
existingSubscription.UserAgent = userAgent;
|
||||
existingSubscription.IpAddress = ipAddress;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Create new subscription
|
||||
var subscription = new Models.PushSubscription
|
||||
{
|
||||
UserId = userId,
|
||||
Endpoint = subscriptionDto.Endpoint,
|
||||
P256DH = subscriptionDto.P256DH,
|
||||
Auth = subscriptionDto.Auth,
|
||||
SubscribedAt = DateTime.UtcNow,
|
||||
LastUsedAt = DateTime.UtcNow,
|
||||
IsActive = true,
|
||||
UserAgent = userAgent,
|
||||
IpAddress = ipAddress
|
||||
};
|
||||
|
||||
_context.PushSubscriptions.Add(subscription);
|
||||
}
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
Log.Information("Push subscription created/updated for user {UserId}", userId);
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error(ex, "Failed to subscribe user {UserId} to push notifications", userId);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<bool> SubscribeCustomerAsync(Guid customerId, PushSubscriptionDto subscriptionDto, string? userAgent = null, string? ipAddress = null)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Check if subscription already exists
|
||||
var existingSubscription = await _context.PushSubscriptions
|
||||
.FirstOrDefaultAsync(ps => ps.Endpoint == subscriptionDto.Endpoint && ps.CustomerId == customerId);
|
||||
|
||||
if (existingSubscription != null)
|
||||
{
|
||||
// Update existing subscription
|
||||
existingSubscription.P256DH = subscriptionDto.P256DH;
|
||||
existingSubscription.Auth = subscriptionDto.Auth;
|
||||
existingSubscription.LastUsedAt = DateTime.UtcNow;
|
||||
existingSubscription.IsActive = true;
|
||||
existingSubscription.UserAgent = userAgent;
|
||||
existingSubscription.IpAddress = ipAddress;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Create new subscription
|
||||
var subscription = new Models.PushSubscription
|
||||
{
|
||||
CustomerId = customerId,
|
||||
Endpoint = subscriptionDto.Endpoint,
|
||||
P256DH = subscriptionDto.P256DH,
|
||||
Auth = subscriptionDto.Auth,
|
||||
SubscribedAt = DateTime.UtcNow,
|
||||
LastUsedAt = DateTime.UtcNow,
|
||||
IsActive = true,
|
||||
UserAgent = userAgent,
|
||||
IpAddress = ipAddress
|
||||
};
|
||||
|
||||
_context.PushSubscriptions.Add(subscription);
|
||||
}
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
Log.Information("Push subscription created/updated for customer {CustomerId}", customerId);
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error(ex, "Failed to subscribe customer {CustomerId} to push notifications", customerId);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<bool> UnsubscribeAsync(string endpoint)
|
||||
{
|
||||
try
|
||||
{
|
||||
var subscription = await _context.PushSubscriptions
|
||||
.FirstOrDefaultAsync(ps => ps.Endpoint == endpoint);
|
||||
|
||||
if (subscription != null)
|
||||
{
|
||||
subscription.IsActive = false;
|
||||
await _context.SaveChangesAsync();
|
||||
Log.Information("Push subscription unsubscribed for endpoint {Endpoint}", endpoint);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error(ex, "Failed to unsubscribe endpoint {Endpoint}", endpoint);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<bool> SendNotificationToUserAsync(Guid userId, PushNotificationDto notification)
|
||||
{
|
||||
var subscriptions = await _context.PushSubscriptions
|
||||
.Where(ps => ps.UserId == userId && ps.IsActive)
|
||||
.ToListAsync();
|
||||
|
||||
return await SendNotificationToSubscriptions(subscriptions, notification);
|
||||
}
|
||||
|
||||
public async Task<bool> SendNotificationToCustomerAsync(Guid customerId, PushNotificationDto notification)
|
||||
{
|
||||
var subscriptions = await _context.PushSubscriptions
|
||||
.Where(ps => ps.CustomerId == customerId && ps.IsActive)
|
||||
.ToListAsync();
|
||||
|
||||
return await SendNotificationToSubscriptions(subscriptions, notification);
|
||||
}
|
||||
|
||||
public async Task<bool> SendNotificationToAllUsersAsync(PushNotificationDto notification)
|
||||
{
|
||||
var subscriptions = await _context.PushSubscriptions
|
||||
.Where(ps => ps.UserId != null && ps.IsActive)
|
||||
.ToListAsync();
|
||||
|
||||
return await SendNotificationToSubscriptions(subscriptions, notification);
|
||||
}
|
||||
|
||||
public async Task<bool> SendNotificationToAllCustomersAsync(PushNotificationDto notification)
|
||||
{
|
||||
var subscriptions = await _context.PushSubscriptions
|
||||
.Where(ps => ps.CustomerId != null && ps.IsActive)
|
||||
.ToListAsync();
|
||||
|
||||
return await SendNotificationToSubscriptions(subscriptions, notification);
|
||||
}
|
||||
|
||||
public async Task<bool> SendOrderNotificationAsync(Guid orderId, string title, string body)
|
||||
{
|
||||
try
|
||||
{
|
||||
var order = await _context.Orders
|
||||
.Include(o => o.Customer)
|
||||
.FirstOrDefaultAsync(o => o.Id == orderId);
|
||||
|
||||
if (order == null)
|
||||
return false;
|
||||
|
||||
var notification = new PushNotificationDto
|
||||
{
|
||||
Title = title,
|
||||
Body = body,
|
||||
Icon = "/icons/icon-192x192.png",
|
||||
Badge = "/icons/icon-72x72.png",
|
||||
Url = $"/Admin/Orders/Details/{orderId}",
|
||||
Data = new { orderId = orderId, type = "order" }
|
||||
};
|
||||
|
||||
// Send to all admin users
|
||||
await SendNotificationToAllUsersAsync(notification);
|
||||
|
||||
// Send to customer if they have push subscription
|
||||
if (order.CustomerId.HasValue)
|
||||
{
|
||||
var customerNotification = new PushNotificationDto
|
||||
{
|
||||
Title = title,
|
||||
Body = body,
|
||||
Icon = "/icons/icon-192x192.png",
|
||||
Badge = "/icons/icon-72x72.png",
|
||||
Data = new { orderId = orderId, type = "order" }
|
||||
};
|
||||
await SendNotificationToCustomerAsync(order.CustomerId.Value, customerNotification);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error(ex, "Failed to send order notification for order {OrderId}", orderId);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<bool> SendTestNotificationAsync(string? userId = null, string title = "Test Notification", string body = "This is a test notification from LittleShop")
|
||||
{
|
||||
var notification = new PushNotificationDto
|
||||
{
|
||||
Title = title,
|
||||
Body = body,
|
||||
Icon = "/icons/icon-192x192.png",
|
||||
Badge = "/icons/icon-72x72.png",
|
||||
Data = new { type = "test", timestamp = DateTime.UtcNow }
|
||||
};
|
||||
|
||||
if (!string.IsNullOrEmpty(userId) && Guid.TryParse(userId, out Guid userIdGuid))
|
||||
{
|
||||
return await SendNotificationToUserAsync(userIdGuid, notification);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Send to all admin users
|
||||
return await SendNotificationToAllUsersAsync(notification);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<bool> SendNotificationToSubscriptions(List<Models.PushSubscription> subscriptions, PushNotificationDto notification)
|
||||
{
|
||||
if (!subscriptions.Any())
|
||||
return false;
|
||||
|
||||
int successCount = 0;
|
||||
var failedSubscriptions = new List<Models.PushSubscription>();
|
||||
|
||||
var payload = JsonSerializer.Serialize(new
|
||||
{
|
||||
title = notification.Title,
|
||||
body = notification.Body,
|
||||
icon = notification.Icon ?? "/icons/icon-192x192.png",
|
||||
badge = notification.Badge ?? "/icons/icon-72x72.png",
|
||||
url = notification.Url,
|
||||
data = notification.Data
|
||||
});
|
||||
|
||||
foreach (var subscription in subscriptions)
|
||||
{
|
||||
try
|
||||
{
|
||||
var pushSubscription = new WebPush.PushSubscription(
|
||||
endpoint: subscription.Endpoint,
|
||||
p256dh: subscription.P256DH,
|
||||
auth: subscription.Auth
|
||||
);
|
||||
|
||||
await _webPushClient.SendNotificationAsync(pushSubscription, payload, _vapidDetails);
|
||||
|
||||
// Update last used time
|
||||
subscription.LastUsedAt = DateTime.UtcNow;
|
||||
successCount++;
|
||||
|
||||
Log.Information("Push notification sent successfully to endpoint {Endpoint}", subscription.Endpoint);
|
||||
}
|
||||
catch (WebPushException ex) when (ex.StatusCode == System.Net.HttpStatusCode.Gone)
|
||||
{
|
||||
// Subscription is no longer valid, mark as inactive
|
||||
subscription.IsActive = false;
|
||||
failedSubscriptions.Add(subscription);
|
||||
Log.Warning("Push subscription expired for endpoint {Endpoint}", subscription.Endpoint);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
failedSubscriptions.Add(subscription);
|
||||
Log.Error(ex, "Failed to send push notification to endpoint {Endpoint}", subscription.Endpoint);
|
||||
}
|
||||
}
|
||||
|
||||
// Save changes to update last used times and inactive subscriptions
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
Log.Information("Push notifications sent: {SuccessCount} successful, {FailedCount} failed",
|
||||
successCount, failedSubscriptions.Count);
|
||||
|
||||
return successCount > 0;
|
||||
}
|
||||
|
||||
public async Task<int> CleanupExpiredSubscriptionsAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
var thirtyDaysAgo = DateTime.UtcNow.AddDays(-30);
|
||||
|
||||
var expiredSubscriptions = await _context.PushSubscriptions
|
||||
.Where(ps => ps.LastUsedAt < thirtyDaysAgo || !ps.IsActive)
|
||||
.ToListAsync();
|
||||
|
||||
_context.PushSubscriptions.RemoveRange(expiredSubscriptions);
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
Log.Information("Cleaned up {Count} expired push subscriptions", expiredSubscriptions.Count);
|
||||
return expiredSubscriptions.Count;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error(ex, "Failed to cleanup expired push subscriptions");
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<List<Models.PushSubscription>> GetActiveSubscriptionsAsync()
|
||||
{
|
||||
return await _context.PushSubscriptions
|
||||
.Where(ps => ps.IsActive)
|
||||
.Include(ps => ps.User)
|
||||
.Include(ps => ps.Customer)
|
||||
.OrderByDescending(ps => ps.SubscribedAt)
|
||||
.ToListAsync();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user