111 lines
4.2 KiB
C#
111 lines
4.2 KiB
C#
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);
|
|
}
|
|
}
|