support PWA notification.
ci-cd-vm / ci-cd (push) Failing after 1m34s

This commit is contained in:
Chris Chen
2026-06-29 22:20:15 -07:00
parent 45d910b554
commit b9210f2501
32 changed files with 1054 additions and 12 deletions
@@ -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);
}
}