@@ -1,6 +1,7 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using ROLAC.API.Authorization;
|
||||
using ROLAC.API.Data;
|
||||
using ROLAC.API.DTOs.Notifications;
|
||||
using ROLAC.API.Services.Logging;
|
||||
@@ -20,15 +21,17 @@ public sealed class NotificationsController : ControllerBase
|
||||
{
|
||||
private readonly IEmailService _email;
|
||||
private readonly ILineNotificationService _line;
|
||||
private readonly IWebPushService _webPush;
|
||||
private readonly AppDbContext _db;
|
||||
private readonly CurrentUserAccessor _currentUser;
|
||||
|
||||
public NotificationsController(
|
||||
IEmailService email, ILineNotificationService line,
|
||||
IEmailService email, ILineNotificationService line, IWebPushService webPush,
|
||||
AppDbContext db, CurrentUserAccessor currentUser)
|
||||
{
|
||||
_email = email;
|
||||
_line = line;
|
||||
_webPush = webPush;
|
||||
_db = db;
|
||||
_currentUser = currentUser;
|
||||
}
|
||||
@@ -92,4 +95,14 @@ public sealed class NotificationsController : ControllerBase
|
||||
HtmlBody: request.HtmlBody,
|
||||
Attachments: null,
|
||||
SentByUserId: _currentUser.UserIdOrSystem), ct));
|
||||
|
||||
// Manual one-to-(few) Web Push send. Guarded by Settings:Write (sending is an admin capability);
|
||||
// the Member Management "send push" test action calls this for a single member.
|
||||
[HttpPost("send-webpush")]
|
||||
[HasPermission(Modules.Settings, PermissionActions.Write)]
|
||||
public async Task<IActionResult> SendWebPush([FromBody] SendWebPushRequest request, CancellationToken ct)
|
||||
=> Ok(await _webPush.SendToMembersAsync(
|
||||
request.MemberIds ?? [],
|
||||
new WebPushPayload(request.Title, request.Body, request.Url),
|
||||
_currentUser.UserIdOrSystem, ct));
|
||||
}
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using ROLAC.API.Data;
|
||||
using ROLAC.API.DTOs.Push;
|
||||
using ROLAC.API.Entities.Notifications;
|
||||
using ROLAC.API.Services.Logging;
|
||||
using ROLAC.API.Services.Notifications;
|
||||
|
||||
namespace ROLAC.API.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// Self-service Web Push subscription management for the logged-in member: hand out the VAPID public
|
||||
/// key, store the browser's subscription, and remove it on opt-out. Subscriptions are keyed to the
|
||||
/// caller's linked Member, so an admin-only account (no MemberId) cannot subscribe.
|
||||
/// </summary>
|
||||
[ApiController]
|
||||
[Route("api/push")]
|
||||
[Authorize]
|
||||
public sealed class PushSubscriptionsController : ControllerBase
|
||||
{
|
||||
private readonly AppDbContext _db;
|
||||
private readonly INotificationSettingsService _settings;
|
||||
private readonly IWebPushService _webPush;
|
||||
private readonly CurrentUserAccessor _currentUser;
|
||||
|
||||
public PushSubscriptionsController(
|
||||
AppDbContext db, INotificationSettingsService settings,
|
||||
IWebPushService webPush, CurrentUserAccessor currentUser)
|
||||
{
|
||||
_db = db;
|
||||
_settings = settings;
|
||||
_webPush = webPush;
|
||||
_currentUser = currentUser;
|
||||
}
|
||||
|
||||
[HttpGet("vapid-public-key")]
|
||||
public IActionResult VapidPublicKey()
|
||||
=> Ok(new { publicKey = _settings.GetWebPush().PublicKey });
|
||||
|
||||
[HttpPost("subscriptions")]
|
||||
public async Task<IActionResult> Subscribe([FromBody] PushSubscriptionRequest request, CancellationToken ct)
|
||||
{
|
||||
var memberId = await CurrentMemberIdAsync(ct);
|
||||
if (memberId is null)
|
||||
return Conflict(new { message = "This account is not linked to a member, so it cannot receive push notifications." });
|
||||
|
||||
var existing = await _db.WebPushSubscriptions
|
||||
.FirstOrDefaultAsync(subscription => subscription.Endpoint == request.Endpoint, ct);
|
||||
|
||||
if (existing is null)
|
||||
{
|
||||
_db.WebPushSubscriptions.Add(new WebPushSubscription
|
||||
{
|
||||
MemberId = memberId.Value,
|
||||
Endpoint = request.Endpoint,
|
||||
P256dh = request.Keys.P256dh,
|
||||
Auth = request.Keys.Auth,
|
||||
UserAgent = request.UserAgent,
|
||||
CreatedAt = DateTime.UtcNow,
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
// Same endpoint re-subscribed (e.g. keys rotated, or now a different member on this browser).
|
||||
existing.MemberId = memberId.Value;
|
||||
existing.P256dh = request.Keys.P256dh;
|
||||
existing.Auth = request.Keys.Auth;
|
||||
existing.UserAgent = request.UserAgent;
|
||||
}
|
||||
|
||||
await _db.SaveChangesAsync(ct);
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
[HttpDelete("subscriptions")]
|
||||
public async Task<IActionResult> Unsubscribe([FromBody] PushUnsubscribeRequest request, CancellationToken ct)
|
||||
{
|
||||
var subscription = await _db.WebPushSubscriptions
|
||||
.FirstOrDefaultAsync(s => s.Endpoint == request.Endpoint, ct);
|
||||
if (subscription is not null)
|
||||
{
|
||||
_db.WebPushSubscriptions.Remove(subscription);
|
||||
await _db.SaveChangesAsync(ct);
|
||||
}
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
// Sends a canned push to the caller's own devices so they can confirm notifications work.
|
||||
[HttpPost("test")]
|
||||
public async Task<IActionResult> SendTestToSelf(CancellationToken ct)
|
||||
{
|
||||
var memberId = await CurrentMemberIdAsync(ct);
|
||||
if (memberId is null)
|
||||
return Conflict(new { message = "This account is not linked to a member, so it cannot receive push notifications." });
|
||||
|
||||
var result = await _webPush.SendToMembersAsync(
|
||||
new[] { memberId.Value },
|
||||
new WebPushPayload("River of Life", "通知測試成功!This is a test notification.", "/user-portal/account"),
|
||||
_currentUser.UserIdOrSystem, ct);
|
||||
return Ok(result);
|
||||
}
|
||||
|
||||
private async Task<int?> CurrentMemberIdAsync(CancellationToken ct)
|
||||
{
|
||||
var userId = _currentUser.UserId;
|
||||
if (string.IsNullOrEmpty(userId)) return null;
|
||||
return await _db.Users.Where(user => user.Id == userId).Select(user => user.MemberId).FirstOrDefaultAsync(ct);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user