Harden notifications: bump MailKit, bound webhook body, share truncation, skip soft-deleted members

This commit is contained in:
Chris Chen
2026-06-23 19:29:23 -07:00
parent fd71f5a107
commit 5a915ebdd1
6 changed files with 44 additions and 8 deletions
@@ -183,4 +183,29 @@ public class LineNotificationServiceTests
Assert.Equal(1, result.FailedCount);
Assert.Equal(1, await db.NotificationLogs.CountAsync(l => l.Status == NotificationStatuses.Failed));
}
[Fact]
public async Task SendLineAsync_SkipsSoftDeletedMembers()
{
using var db = BuildDb();
var memberId = await SeedMemberAsync(db);
db.MemberChannelBindings.Add(new MemberChannelBinding
{
MemberId = memberId, Channel = "line", ExternalId = "U-DEL", BoundAt = DateTime.UtcNow,
});
await db.SaveChangesAsync();
// Soft-delete the member.
var member = await db.Members.FirstAsync(m => m.Id == memberId);
member.IsDeleted = true;
await db.SaveChangesAsync();
var channel = new FakeMessageChannel();
var service = new LineNotificationService(db, channel);
var result = await service.SendLineAsync("notice", new[] { memberId }, Array.Empty<int>(), "admin-1");
Assert.Equal(0, result.SentCount);
Assert.Empty(channel.UserPushes);
}
}
@@ -33,6 +33,7 @@ public sealed class LineWebhookController : ControllerBase
}
[HttpPost("webhook")]
[RequestSizeLimit(262_144)]
public async Task<IActionResult> Webhook(CancellationToken ct)
{
using var reader = new StreamReader(Request.Body, Encoding.UTF8);
+1 -1
View File
@@ -12,7 +12,7 @@
Provides DevExpress.Drawing.v24.1.Skia.dll; without it RichEditDocumentServer
throws DllNotFoundException at runtime on Linux (Windows falls back to GDI+). -->
<PackageReference Include="DevExpress.Drawing.Skia" Version="24.1.3" />
<PackageReference Include="MailKit" Version="4.8.0" />
<PackageReference Include="MailKit" Version="4.17.0" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.11" />
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="8.0.11" />
@@ -12,8 +12,6 @@ namespace ROLAC.API.Services.Notifications;
/// </summary>
public sealed class EmailService : IEmailService
{
private const int BodyLogMaxLength = 8000;
private readonly AppDbContext _db;
private readonly ISmtpDispatcher _dispatcher;
private readonly CurrentUserAccessor _currentUser;
@@ -44,7 +42,7 @@ public sealed class EmailService : IEmailService
TargetExternalId = recipient.Address,
Subject = message.Subject,
MemberId = recipient.MemberId,
Body = Truncate(message.HtmlBody),
Body = NotificationLogText.Truncate(message.HtmlBody),
SentByUserId = sentBy,
SentAt = DateTime.UtcNow,
};
@@ -97,6 +95,4 @@ public sealed class EmailService : IEmailService
return resolved;
}
private static string Truncate(string body) =>
body.Length <= BodyLogMaxLength ? body : body[..BodyLogMaxLength] + "…[truncated]";
}
@@ -31,8 +31,12 @@ public sealed class LineNotificationService : ILineNotificationService
var failures = new List<NotificationFailure>();
var sentCount = 0;
var liveMemberIds = await _db.Members
.Where(m => memberIds.Contains(m.Id))
.Select(m => m.Id)
.ToListAsync(ct);
var bindings = await _db.MemberChannelBindings
.Where(b => b.Channel == Channel && memberIds.Contains(b.MemberId))
.Where(b => b.Channel == Channel && liveMemberIds.Contains(b.MemberId))
.ToListAsync(ct);
foreach (var binding in bindings)
{
@@ -146,7 +150,7 @@ public sealed class LineNotificationService : ILineNotificationService
TargetExternalId = externalId,
MemberId = memberId,
MessagingGroupId = groupId,
Body = body,
Body = NotificationLogText.Truncate(body),
Status = result.Success ? NotificationStatuses.Sent : NotificationStatuses.Failed,
Error = result.Error,
SentByUserId = sentBy,
@@ -47,3 +47,13 @@ public sealed record EmailMessage(
string HtmlBody,
IReadOnlyList<EmailAttachment>? Attachments = null,
string? SentByUserId = null);
/// <summary>Helpers for building NotificationLog rows consistently across channels.</summary>
public static class NotificationLogText
{
public const int BodyMaxLength = 8000;
/// <summary>Caps a body string so an oversized message can't bloat the log table.</summary>
public static string Truncate(string body) =>
body.Length <= BodyMaxLength ? body : body[..BodyMaxLength] + "…[truncated]";
}