cd8e4872f6
Findings from the per-module code review of the SMS feature (code-reviews/):
- ManagementService (High): UpdateSmsConfig + UpdateSmtpConfig were Designer-gated
while both /notifications/{sms,smtp} pages enforce RequireAdmin — a Designer
blocked in the UI could still rotate a production credential via CLI. Moved both
to the Administrator arm so the actor gate matches the UI.
- ManagementService (Medium): UpdateSmsConfig treated --auth-token "" as a value,
silently clearing the stored Twilio token. Guard on IsNullOrWhiteSpace so empty ==
omitted (SMTP Credentials keeps its null-only guard — empty is valid for no-auth).
- CentralUI (Medium): NotificationLists recipient badge rendered Name <Email>
unconditionally, showing "Name <>" for SMS lists. Now type-aware (phone for SMS).
- ConfigurationDatabase (Medium): AddSmsNotifications.Down() backfilled NULL emails
to '' — silent data loss for SMS-only recipients. Added a pre-drop guard that
refuses rollback while such rows exist.
- NotificationOutbox (Low): SMS body truncation could split a surrogate pair at the
cap boundary; back off one code unit to stay well-formed.
- Commons (Low): NotificationRecipient public ctor name-guard now matches the
ForEmail factory (IsNullOrWhiteSpace). Documented SmsConfiguration.MaxRetries/
RetryDelay as RESERVED (dispatcher reuses the shared SMTP-derived retry policy).
93 lines
3.7 KiB
C#
93 lines
3.7 KiB
C#
namespace ZB.MOM.WW.ScadaBridge.Commons.Entities.Notifications;
|
|
|
|
public class NotificationRecipient
|
|
{
|
|
/// <summary>Gets or sets the database primary key.</summary>
|
|
public int Id { get; set; }
|
|
/// <summary>Gets or sets the id of the parent notification list.</summary>
|
|
public int NotificationListId { get; set; }
|
|
/// <summary>Gets or sets the display name of the recipient.</summary>
|
|
public string Name { get; set; }
|
|
/// <summary>Gets or sets the recipient's email address, or null for non-email recipients.</summary>
|
|
public string? EmailAddress { get; set; }
|
|
/// <summary>Gets or sets the recipient's phone number (E.164), or null for non-SMS recipients.</summary>
|
|
public string? PhoneNumber { get; set; }
|
|
|
|
/// <summary>
|
|
/// Initializes a new <see cref="NotificationRecipient"/> with the required fields (email path).
|
|
/// </summary>
|
|
/// <param name="name">Display name of the recipient.</param>
|
|
/// <param name="emailAddress">Email address of the recipient.</param>
|
|
public NotificationRecipient(string name, string emailAddress)
|
|
{
|
|
// Match the ForEmail factory's guard so the invariant ("a recipient always
|
|
// has a non-blank display name") holds regardless of construction path. EF
|
|
// materializes via the private parameterless ctor + property injection — an
|
|
// SMS-only recipient has a null EmailAddress — so this ctor is only reached
|
|
// by code that genuinely intends the email path.
|
|
if (string.IsNullOrWhiteSpace(name))
|
|
{
|
|
throw new ArgumentException("Name must not be empty.", nameof(name));
|
|
}
|
|
|
|
Name = name;
|
|
EmailAddress = emailAddress ?? throw new ArgumentNullException(nameof(emailAddress));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates an email recipient with the given name and email address; the phone number is left null.
|
|
/// </summary>
|
|
/// <param name="name">Display name of the recipient.</param>
|
|
/// <param name="emailAddress">Email address of the recipient.</param>
|
|
/// <returns>A new email <see cref="NotificationRecipient"/>.</returns>
|
|
public static NotificationRecipient ForEmail(string name, string emailAddress)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(name))
|
|
{
|
|
throw new ArgumentException("Name must not be empty.", nameof(name));
|
|
}
|
|
|
|
if (emailAddress is null)
|
|
{
|
|
throw new ArgumentNullException(nameof(emailAddress));
|
|
}
|
|
|
|
return new NotificationRecipient(name, emailAddress);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates an SMS recipient with the given name and phone number; the email address is left null.
|
|
/// </summary>
|
|
/// <param name="name">Display name of the recipient.</param>
|
|
/// <param name="phoneNumber">Phone number (E.164) of the recipient.</param>
|
|
/// <returns>A new SMS <see cref="NotificationRecipient"/>.</returns>
|
|
public static NotificationRecipient ForSms(string name, string phoneNumber)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(name))
|
|
{
|
|
throw new ArgumentException("Name must not be empty.", nameof(name));
|
|
}
|
|
|
|
if (phoneNumber is null)
|
|
{
|
|
throw new ArgumentNullException(nameof(phoneNumber));
|
|
}
|
|
|
|
return new NotificationRecipient
|
|
{
|
|
Name = name,
|
|
PhoneNumber = phoneNumber
|
|
};
|
|
}
|
|
|
|
/// <summary>
|
|
/// Private parameterless constructor that backs the <see cref="ForSms"/> factory path,
|
|
/// where the contact field is assigned via property setters rather than constructor
|
|
/// parameters — without exposing a half-initialized public constructor to callers.
|
|
/// </summary>
|
|
private NotificationRecipient()
|
|
{
|
|
Name = string.Empty;
|
|
}
|
|
}
|