Harden notifications: bump MailKit, bound webhook body, share truncation, skip soft-deleted members
This commit is contained in:
@@ -183,4 +183,29 @@ public class LineNotificationServiceTests
|
|||||||
Assert.Equal(1, result.FailedCount);
|
Assert.Equal(1, result.FailedCount);
|
||||||
Assert.Equal(1, await db.NotificationLogs.CountAsync(l => l.Status == NotificationStatuses.Failed));
|
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")]
|
[HttpPost("webhook")]
|
||||||
|
[RequestSizeLimit(262_144)]
|
||||||
public async Task<IActionResult> Webhook(CancellationToken ct)
|
public async Task<IActionResult> Webhook(CancellationToken ct)
|
||||||
{
|
{
|
||||||
using var reader = new StreamReader(Request.Body, Encoding.UTF8);
|
using var reader = new StreamReader(Request.Body, Encoding.UTF8);
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
Provides DevExpress.Drawing.v24.1.Skia.dll; without it RichEditDocumentServer
|
Provides DevExpress.Drawing.v24.1.Skia.dll; without it RichEditDocumentServer
|
||||||
throws DllNotFoundException at runtime on Linux (Windows falls back to GDI+). -->
|
throws DllNotFoundException at runtime on Linux (Windows falls back to GDI+). -->
|
||||||
<PackageReference Include="DevExpress.Drawing.Skia" Version="24.1.3" />
|
<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.Authentication.JwtBearer" Version="8.0.11" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="8.0.11" />
|
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="8.0.11" />
|
||||||
|
|
||||||
|
|||||||
@@ -12,8 +12,6 @@ namespace ROLAC.API.Services.Notifications;
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class EmailService : IEmailService
|
public sealed class EmailService : IEmailService
|
||||||
{
|
{
|
||||||
private const int BodyLogMaxLength = 8000;
|
|
||||||
|
|
||||||
private readonly AppDbContext _db;
|
private readonly AppDbContext _db;
|
||||||
private readonly ISmtpDispatcher _dispatcher;
|
private readonly ISmtpDispatcher _dispatcher;
|
||||||
private readonly CurrentUserAccessor _currentUser;
|
private readonly CurrentUserAccessor _currentUser;
|
||||||
@@ -44,7 +42,7 @@ public sealed class EmailService : IEmailService
|
|||||||
TargetExternalId = recipient.Address,
|
TargetExternalId = recipient.Address,
|
||||||
Subject = message.Subject,
|
Subject = message.Subject,
|
||||||
MemberId = recipient.MemberId,
|
MemberId = recipient.MemberId,
|
||||||
Body = Truncate(message.HtmlBody),
|
Body = NotificationLogText.Truncate(message.HtmlBody),
|
||||||
SentByUserId = sentBy,
|
SentByUserId = sentBy,
|
||||||
SentAt = DateTime.UtcNow,
|
SentAt = DateTime.UtcNow,
|
||||||
};
|
};
|
||||||
@@ -97,6 +95,4 @@ public sealed class EmailService : IEmailService
|
|||||||
return resolved;
|
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 failures = new List<NotificationFailure>();
|
||||||
var sentCount = 0;
|
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
|
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);
|
.ToListAsync(ct);
|
||||||
foreach (var binding in bindings)
|
foreach (var binding in bindings)
|
||||||
{
|
{
|
||||||
@@ -146,7 +150,7 @@ public sealed class LineNotificationService : ILineNotificationService
|
|||||||
TargetExternalId = externalId,
|
TargetExternalId = externalId,
|
||||||
MemberId = memberId,
|
MemberId = memberId,
|
||||||
MessagingGroupId = groupId,
|
MessagingGroupId = groupId,
|
||||||
Body = body,
|
Body = NotificationLogText.Truncate(body),
|
||||||
Status = result.Success ? NotificationStatuses.Sent : NotificationStatuses.Failed,
|
Status = result.Success ? NotificationStatuses.Sent : NotificationStatuses.Failed,
|
||||||
Error = result.Error,
|
Error = result.Error,
|
||||||
SentByUserId = sentBy,
|
SentByUserId = sentBy,
|
||||||
|
|||||||
@@ -47,3 +47,13 @@ public sealed record EmailMessage(
|
|||||||
string HtmlBody,
|
string HtmlBody,
|
||||||
IReadOnlyList<EmailAttachment>? Attachments = null,
|
IReadOnlyList<EmailAttachment>? Attachments = null,
|
||||||
string? SentByUserId = 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]";
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user