@@ -15,7 +15,8 @@ public class ChurchProfileService : IChurchProfileService
|
||||
var p = await GetOrCreateAsync();
|
||||
return new ChurchProfileDto
|
||||
{
|
||||
Id = p.Id, Name = p.Name, Address = p.Address, City = p.City, State = p.State,
|
||||
Id = p.Id, Name = p.Name, NameZh = p.NameZh, Phone = p.Phone, Email = p.Email,
|
||||
Website = p.Website, Address = p.Address, City = p.City, State = p.State,
|
||||
ZipCode = p.ZipCode, BankName = p.BankName, BankAccountNumber = p.BankAccountNumber,
|
||||
BankRoutingNumber = p.BankRoutingNumber, NextCheckNumber = p.NextCheckNumber,
|
||||
};
|
||||
@@ -24,7 +25,8 @@ public class ChurchProfileService : IChurchProfileService
|
||||
public async Task UpdateAsync(UpdateChurchProfileRequest r)
|
||||
{
|
||||
var p = await GetOrCreateAsync();
|
||||
p.Name = r.Name; p.Address = r.Address; p.City = r.City; p.State = r.State;
|
||||
p.Name = r.Name; p.NameZh = r.NameZh; p.Phone = r.Phone; p.Email = r.Email;
|
||||
p.Website = r.Website; p.Address = r.Address; p.City = r.City; p.State = r.State;
|
||||
p.ZipCode = r.ZipCode; p.BankName = r.BankName; p.BankAccountNumber = r.BankAccountNumber;
|
||||
p.BankRoutingNumber = r.BankRoutingNumber; p.NextCheckNumber = r.NextCheckNumber;
|
||||
await _db.SaveChangesAsync();
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
using ROLAC.API.DTOs.Settings;
|
||||
|
||||
namespace ROLAC.API.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Reads and writes the singleton SiteSetting and NotificationSetting rows. Notification secrets
|
||||
/// are masked on read and treated as write-only on update (blank = keep). After a notification
|
||||
/// update the runtime cache is reloaded so changes apply without an API restart.
|
||||
/// </summary>
|
||||
public interface ISettingsService
|
||||
{
|
||||
Task<SiteSettingDto> GetSiteAsync();
|
||||
Task UpdateSiteAsync(UpdateSiteSettingRequest request);
|
||||
|
||||
Task<NotificationSettingDto> GetNotificationAsync();
|
||||
Task UpdateNotificationAsync(UpdateNotificationSettingRequest request);
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
using System.Net.Http.Headers;
|
||||
using System.Net.Http.Json;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace ROLAC.API.Services.Notifications;
|
||||
|
||||
@@ -11,12 +10,12 @@ public sealed class LineMessageChannel : IMessageChannel
|
||||
private const string ReplyUrl = "https://api.line.me/v2/bot/message/reply";
|
||||
|
||||
private readonly HttpClient _http;
|
||||
private readonly LineOptions _options;
|
||||
private readonly INotificationSettingsService _settings;
|
||||
|
||||
public LineMessageChannel(HttpClient http, IOptions<LineOptions> options)
|
||||
public LineMessageChannel(HttpClient http, INotificationSettingsService settings)
|
||||
{
|
||||
_http = http;
|
||||
_options = options.Value;
|
||||
_settings = settings;
|
||||
}
|
||||
|
||||
public Task<MessageSendResult> PushToUserAsync(string externalId, string text, CancellationToken ct = default)
|
||||
@@ -36,7 +35,8 @@ public sealed class LineMessageChannel : IMessageChannel
|
||||
{
|
||||
Content = JsonContent.Create(payload),
|
||||
};
|
||||
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", _options.ChannelAccessToken);
|
||||
request.Headers.Authorization =
|
||||
new AuthenticationHeaderValue("Bearer", _settings.GetLine().ChannelAccessToken);
|
||||
|
||||
using var response = await _http.SendAsync(request, ct);
|
||||
if (response.IsSuccessStatusCode) return new MessageSendResult(true, null);
|
||||
|
||||
@@ -1,21 +1,22 @@
|
||||
using MailKit.Net.Smtp;
|
||||
using MailKit.Security;
|
||||
using Microsoft.Extensions.Options;
|
||||
using MimeKit;
|
||||
|
||||
namespace ROLAC.API.Services.Notifications;
|
||||
|
||||
/// <summary>Sends a single email via MailKit using the configured SMTP server.</summary>
|
||||
/// <summary>Sends a single email via MailKit using the current (DB-backed) SMTP settings.</summary>
|
||||
public sealed class MailKitSmtpDispatcher : ISmtpDispatcher
|
||||
{
|
||||
private readonly SmtpOptions _options;
|
||||
private readonly INotificationSettingsService _settings;
|
||||
|
||||
public MailKitSmtpDispatcher(IOptions<SmtpOptions> options) => _options = options.Value;
|
||||
public MailKitSmtpDispatcher(INotificationSettingsService settings) => _settings = settings;
|
||||
|
||||
public async Task SendAsync(OutboundEmail email, CancellationToken ct = default)
|
||||
{
|
||||
var options = _settings.GetSmtp();
|
||||
|
||||
var message = new MimeMessage();
|
||||
message.From.Add(new MailboxAddress(_options.FromName, _options.FromAddress));
|
||||
message.From.Add(new MailboxAddress(options.FromName, options.FromAddress));
|
||||
message.To.Add(MailboxAddress.Parse(email.ToAddress));
|
||||
message.Subject = email.Subject;
|
||||
|
||||
@@ -28,10 +29,10 @@ public sealed class MailKitSmtpDispatcher : ISmtpDispatcher
|
||||
message.Body = builder.ToMessageBody();
|
||||
|
||||
using var client = new SmtpClient();
|
||||
var socketOptions = _options.UseSsl ? SecureSocketOptions.StartTls : SecureSocketOptions.Auto;
|
||||
await client.ConnectAsync(_options.Host, _options.Port, socketOptions, ct);
|
||||
if (!string.IsNullOrEmpty(_options.User))
|
||||
await client.AuthenticateAsync(_options.User, _options.Password, ct);
|
||||
var socketOptions = options.UseSsl ? SecureSocketOptions.StartTls : SecureSocketOptions.Auto;
|
||||
await client.ConnectAsync(options.Host, options.Port, socketOptions, ct);
|
||||
if (!string.IsNullOrEmpty(options.User))
|
||||
await client.AuthenticateAsync(options.User, options.Password, ct);
|
||||
await client.SendAsync(message, ct);
|
||||
await client.DisconnectAsync(true, ct);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Options;
|
||||
using ROLAC.API.Data;
|
||||
|
||||
namespace ROLAC.API.Services.Notifications;
|
||||
|
||||
/// <summary>
|
||||
/// Supplies the current SMTP/Line settings from the <c>NotificationSetting</c> singleton row,
|
||||
/// caching a snapshot in memory so send paths don't hit the DB on every message. Registered as a
|
||||
/// singleton; the Settings UI calls <see cref="Reload"/> after an edit so changes take effect
|
||||
/// without restarting the API. Falls back to the "Smtp"/"Line" appsettings sections if the row
|
||||
/// has not been seeded yet.
|
||||
/// </summary>
|
||||
public interface INotificationSettingsService
|
||||
{
|
||||
SmtpOptions GetSmtp();
|
||||
LineOptions GetLine();
|
||||
void Reload();
|
||||
}
|
||||
|
||||
public sealed class NotificationSettingsService : INotificationSettingsService
|
||||
{
|
||||
private readonly IServiceScopeFactory _scopeFactory;
|
||||
private readonly IOptions<SmtpOptions> _smtpFallback;
|
||||
private readonly IOptions<LineOptions> _lineFallback;
|
||||
private readonly object _gate = new();
|
||||
|
||||
private SmtpOptions? _smtp;
|
||||
private LineOptions? _line;
|
||||
|
||||
public NotificationSettingsService(
|
||||
IServiceScopeFactory scopeFactory,
|
||||
IOptions<SmtpOptions> smtpFallback,
|
||||
IOptions<LineOptions> lineFallback)
|
||||
{
|
||||
_scopeFactory = scopeFactory;
|
||||
_smtpFallback = smtpFallback;
|
||||
_lineFallback = lineFallback;
|
||||
}
|
||||
|
||||
public SmtpOptions GetSmtp()
|
||||
{
|
||||
EnsureLoaded();
|
||||
return _smtp!;
|
||||
}
|
||||
|
||||
public LineOptions GetLine()
|
||||
{
|
||||
EnsureLoaded();
|
||||
return _line!;
|
||||
}
|
||||
|
||||
public void Reload()
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
_smtp = null;
|
||||
_line = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void EnsureLoaded()
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
if (_smtp is not null && _line is not null)
|
||||
return;
|
||||
|
||||
using var scope = _scopeFactory.CreateScope();
|
||||
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
||||
var row = db.NotificationSettings.AsNoTracking().OrderBy(s => s.Id).FirstOrDefault();
|
||||
|
||||
if (row is null)
|
||||
{
|
||||
// Not seeded yet — use the appsettings values so sends still work.
|
||||
_smtp = _smtpFallback.Value;
|
||||
_line = _lineFallback.Value;
|
||||
return;
|
||||
}
|
||||
|
||||
_smtp = new SmtpOptions
|
||||
{
|
||||
Host = row.SmtpHost,
|
||||
Port = row.SmtpPort,
|
||||
UseSsl = row.SmtpUseSsl,
|
||||
User = row.SmtpUser,
|
||||
Password = row.SmtpPassword,
|
||||
FromAddress = row.FromAddress,
|
||||
FromName = row.FromName,
|
||||
};
|
||||
_line = new LineOptions
|
||||
{
|
||||
ChannelAccessToken = row.LineChannelAccessToken,
|
||||
ChannelSecret = row.LineChannelSecret,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using ROLAC.API.Data;
|
||||
using ROLAC.API.DTOs.Settings;
|
||||
using ROLAC.API.Entities;
|
||||
using ROLAC.API.Services.Notifications;
|
||||
|
||||
namespace ROLAC.API.Services;
|
||||
|
||||
public class SettingsService : ISettingsService
|
||||
{
|
||||
private readonly AppDbContext _db;
|
||||
private readonly INotificationSettingsService _notificationSettings;
|
||||
|
||||
public SettingsService(AppDbContext db, INotificationSettingsService notificationSettings)
|
||||
{
|
||||
_db = db;
|
||||
_notificationSettings = notificationSettings;
|
||||
}
|
||||
|
||||
public async Task<SiteSettingDto> GetSiteAsync()
|
||||
{
|
||||
var s = await GetOrCreateSiteAsync();
|
||||
return new SiteSettingDto
|
||||
{
|
||||
SiteTitle = s.SiteTitle,
|
||||
SiteTitleZh = s.SiteTitleZh,
|
||||
DefaultLanguage = s.DefaultLanguage,
|
||||
TimeZone = s.TimeZone,
|
||||
DateFormat = s.DateFormat,
|
||||
Currency = s.Currency,
|
||||
};
|
||||
}
|
||||
|
||||
public async Task UpdateSiteAsync(UpdateSiteSettingRequest r)
|
||||
{
|
||||
var s = await GetOrCreateSiteAsync();
|
||||
s.SiteTitle = r.SiteTitle;
|
||||
s.SiteTitleZh = r.SiteTitleZh;
|
||||
s.DefaultLanguage = r.DefaultLanguage;
|
||||
s.TimeZone = r.TimeZone;
|
||||
s.DateFormat = r.DateFormat;
|
||||
s.Currency = r.Currency;
|
||||
await _db.SaveChangesAsync();
|
||||
}
|
||||
|
||||
public async Task<NotificationSettingDto> GetNotificationAsync()
|
||||
{
|
||||
var n = await GetOrCreateNotificationAsync();
|
||||
return new NotificationSettingDto
|
||||
{
|
||||
EnableEmail = n.EnableEmail,
|
||||
SmtpHost = n.SmtpHost,
|
||||
SmtpPort = n.SmtpPort,
|
||||
SmtpUseSsl = n.SmtpUseSsl,
|
||||
SmtpUser = n.SmtpUser,
|
||||
FromAddress = n.FromAddress,
|
||||
FromName = n.FromName,
|
||||
HasSmtpPassword = !string.IsNullOrEmpty(n.SmtpPassword),
|
||||
EnableLine = n.EnableLine,
|
||||
HasLineChannelAccessToken = !string.IsNullOrEmpty(n.LineChannelAccessToken),
|
||||
HasLineChannelSecret = !string.IsNullOrEmpty(n.LineChannelSecret),
|
||||
// WebhookUrl is filled by the controller (needs the request host).
|
||||
};
|
||||
}
|
||||
|
||||
public async Task UpdateNotificationAsync(UpdateNotificationSettingRequest r)
|
||||
{
|
||||
var n = await GetOrCreateNotificationAsync();
|
||||
n.EnableEmail = r.EnableEmail;
|
||||
n.SmtpHost = r.SmtpHost;
|
||||
n.SmtpPort = r.SmtpPort;
|
||||
n.SmtpUseSsl = r.SmtpUseSsl;
|
||||
n.SmtpUser = r.SmtpUser;
|
||||
n.FromAddress = r.FromAddress ?? "";
|
||||
n.FromName = r.FromName ?? "";
|
||||
n.EnableLine = r.EnableLine;
|
||||
|
||||
// Secrets are write-only: a blank value means "keep what's stored".
|
||||
if (!string.IsNullOrWhiteSpace(r.SmtpPassword))
|
||||
n.SmtpPassword = r.SmtpPassword;
|
||||
if (!string.IsNullOrWhiteSpace(r.LineChannelAccessToken))
|
||||
n.LineChannelAccessToken = r.LineChannelAccessToken;
|
||||
if (!string.IsNullOrWhiteSpace(r.LineChannelSecret))
|
||||
n.LineChannelSecret = r.LineChannelSecret;
|
||||
|
||||
await _db.SaveChangesAsync();
|
||||
|
||||
// Drop the cached snapshot so the new values are used on the next send — no restart needed.
|
||||
_notificationSettings.Reload();
|
||||
}
|
||||
|
||||
private async Task<SiteSetting> GetOrCreateSiteAsync()
|
||||
{
|
||||
var s = await _db.SiteSettings.OrderBy(x => x.Id).FirstOrDefaultAsync();
|
||||
if (s is null)
|
||||
{
|
||||
s = new SiteSetting { SiteTitle = "Church" };
|
||||
_db.SiteSettings.Add(s);
|
||||
await _db.SaveChangesAsync();
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
private async Task<NotificationSetting> GetOrCreateNotificationAsync()
|
||||
{
|
||||
var n = await _db.NotificationSettings.OrderBy(x => x.Id).FirstOrDefaultAsync();
|
||||
if (n is null)
|
||||
{
|
||||
n = new NotificationSetting();
|
||||
_db.NotificationSettings.Add(n);
|
||||
await _db.SaveChangesAsync();
|
||||
}
|
||||
return n;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user