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;
///
/// 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.
///
[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 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 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 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 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);
}
}