@using ZB.MOM.WW.ScadaBridge.CentralUI.Services @using ZB.MOM.WW.ScadaBridge.Commons.Messages.Management @inject IServiceProvider MutationServices @page "/notifications/sms" @using ZB.MOM.WW.ScadaBridge.Security @using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Repositories @using SmsConfigurationEntity = ZB.MOM.WW.ScadaBridge.Commons.Entities.Notifications.SmsConfiguration @attribute [Authorize(Policy = AuthorizationPolicies.RequireAdmin)] @inject INotificationRepository NotificationRepository @inject NavigationManager NavigationManager

SMS Configuration

@if (_loading) { } else if (_errorMessage != null) {
@_errorMessage
} else { @if (_smsConfigs.Count == 0 && !_showForm) {

No SMS configuration set.

} else { @foreach (var sms in _smsConfigs) {
@sms.AccountSid @if (_editingSms?.Id != sms.Id || !_showForm) { }
Account SID
@sms.AccountSid
From Number
@sms.FromNumber
Messaging Service SID
@(string.IsNullOrWhiteSpace(sms.MessagingServiceSid) ? "(not set)" : sms.MessagingServiceSid)
API Base URL
@* An override base URL is an operator-supplied URL with no break opportunities — clip it so it cannot widen the card. *@
@(string.IsNullOrWhiteSpace(sms.ApiBaseUrl) ? "(provider default)" : sms.ApiBaseUrl)
Auth Token
@(string.IsNullOrWhiteSpace(sms.AuthToken) ? "(not set)" : "(stored)")
Connection Timeout
@sms.ConnectionTimeoutSeconds s
Max Retries
@sms.MaxRetries
Retry Delay
@((int)sms.RetryDelay.TotalSeconds) s
} @if (_showForm) {
@(_editingSms != null ? "Edit SMS Configuration" : "Add SMS Configuration")
Provide a From Number or a Messaging Service SID (at least one is required).
Optional — used instead of the From number when set.
Treat as sensitive — visible to admins only. @if (_editingSms != null) { Leave blank to keep the existing token. }
@if (_formError != null) {
@_formError
}
} else if (_smsConfigs.Count == 0) { } } }
@code { private bool _loading = true; private string? _errorMessage; private List _smsConfigs = new(); private bool _showForm; private SmsConfigurationEntity? _editingSms; private string _accountSid = string.Empty; private string _fromNumber = string.Empty; private string? _messagingServiceSid; private string? _apiBaseUrl; private string? _authToken; private int _connectionTimeoutSeconds = 30; private int _maxRetries = 10; private int _retryDelaySeconds = 60; private string? _formError; private ToastNotification _toast = default!; protected override async Task OnInitializedAsync() { await LoadAsync(); } private async Task LoadAsync() { _loading = true; _errorMessage = null; try { _smsConfigs = (await NotificationRepository.GetAllSmsConfigurationsAsync()).ToList(); } catch (Exception ex) { _errorMessage = ex.Message; } _loading = false; } private void ShowAddForm() { _editingSms = null; _accountSid = string.Empty; _fromNumber = string.Empty; _messagingServiceSid = null; _apiBaseUrl = null; _authToken = null; _connectionTimeoutSeconds = 30; _maxRetries = 10; _retryDelaySeconds = 60; _formError = null; _showForm = true; } private void StartEdit(SmsConfigurationEntity sms) { _editingSms = sms; _accountSid = sms.AccountSid; _fromNumber = sms.FromNumber ?? string.Empty; _messagingServiceSid = sms.MessagingServiceSid; _apiBaseUrl = sms.ApiBaseUrl; // Never pre-fill the stored secret; blank means "keep existing". _authToken = null; _connectionTimeoutSeconds = sms.ConnectionTimeoutSeconds; _maxRetries = sms.MaxRetries; _retryDelaySeconds = (int)sms.RetryDelay.TotalSeconds; _formError = null; _showForm = true; } private void CancelForm() { _showForm = false; _formError = null; } private async Task Save() { _formError = null; // A valid Twilio config needs an Account SID and at least one sender identity: // a From number OR a Messaging Service SID (the latter is used instead of From). if (string.IsNullOrWhiteSpace(_accountSid) || (string.IsNullOrWhiteSpace(_fromNumber) && string.IsNullOrWhiteSpace(_messagingServiceSid))) { _formError = "Account SID and either a From Number or a Messaging Service SID are required."; return; } // The AccountSid is interpolated un-escaped into the Twilio request URI, so reject // any malformed SID client-side too (the ManagementActor enforces the same guard // server-side; keep the message identical). if (!System.Text.RegularExpressions.Regex.IsMatch(_accountSid.Trim(), "^AC[0-9a-fA-F]{32}$")) { _formError = "Account SID must match Twilio's format: 'AC' followed by 32 hex characters."; return; } var typedAuthToken = string.IsNullOrWhiteSpace(_authToken) ? null : _authToken.Trim(); try { if (_editingSms is null && typedAuthToken is null) { _formError = "Auth Token is required."; return; } var from = string.IsNullOrWhiteSpace(_fromNumber) ? null : _fromNumber.Trim(); var messaging = string.IsNullOrWhiteSpace(_messagingServiceSid) ? null : _messagingServiceSid.Trim(); await MutationServices.ExecuteConfigurationAsync(_editingSms is not null ? new UpdateSmsConfigCommand(_editingSms.Id, _accountSid.Trim(), from, messaging, _apiBaseUrl?.Trim() ?? "", typedAuthToken, _connectionTimeoutSeconds, _maxRetries, TimeSpan.FromSeconds(_retryDelaySeconds)) : new CreateSmsConfigCommand(_accountSid.Trim(), from, messaging, _apiBaseUrl?.Trim(), typedAuthToken, _connectionTimeoutSeconds, _maxRetries, TimeSpan.FromSeconds(_retryDelaySeconds))); _showForm = false; _toast.ShowSuccess("SMS configuration saved."); await LoadAsync(); } catch (Exception ex) { _formError = ex.Message; } } }