using MemberCenter.Application.Abstractions; using MemberCenter.Application.Models.Admin; using MemberCenter.Domain.Entities; using MemberCenter.Infrastructure.Persistence; using Microsoft.EntityFrameworkCore; using System.Net; using System.Net.Mail; namespace MemberCenter.Infrastructure.Services; public sealed class SecuritySettingsService : ISecuritySettingsService { private const string AccessTokenKey = "token_access_minutes"; private const string RefreshTokenKey = "token_refresh_days"; private const string PublicBaseUrlKey = "public_base_url"; private const string SmtpRelayHostKey = "smtp_relay_host"; private const string SmtpRelayPortKey = "smtp_relay_port"; private const string SmtpUseTlsKey = "smtp_use_tls"; private const string SmtpUseSslKey = "smtp_use_ssl"; private const string SmtpTimeoutSecondsKey = "smtp_timeout_seconds"; private const string SmtpUsernameKey = "smtp_username"; private const string SmtpPasswordKey = "smtp_password"; private const string SenderNameKey = "smtp_sender_name"; private const string SenderEmailKey = "smtp_sender_email"; private readonly MemberCenterDbContext _dbContext; private readonly IAuditLogWriter _auditLogWriter; public SecuritySettingsService( MemberCenterDbContext dbContext, IAuditLogWriter auditLogWriter) { _dbContext = dbContext; _auditLogWriter = auditLogWriter; } public async Task GetAsync() { var access = await GetFlagAsync(AccessTokenKey, 60); var refresh = await GetFlagAsync(RefreshTokenKey, 30); var smtp = await GetSmtpSettingsAsync(); return new SecuritySettingsDto( access, refresh, smtp.PublicBaseUrl, smtp.RelayHost, smtp.RelayPort, smtp.UseTls, smtp.UseSsl, smtp.TimeoutSeconds, smtp.Username, string.Empty, smtp.HasPassword, smtp.SenderName, smtp.SenderEmail, string.Empty); } public async Task SaveAsync(SecuritySettingsDto settings, Guid? actorUserId = null) { if (settings.SmtpUseTls && settings.SmtpUseSsl) { throw new InvalidOperationException("SMTP TLS and SSL cannot both be enabled."); } await SetFlagAsync(AccessTokenKey, settings.AccessTokenMinutes.ToString()); await SetFlagAsync(RefreshTokenKey, settings.RefreshTokenDays.ToString()); await SetFlagAsync(PublicBaseUrlKey, settings.PublicBaseUrl.Trim()); await SetFlagAsync(SmtpRelayHostKey, settings.SmtpRelayHost.Trim()); await SetFlagAsync(SmtpRelayPortKey, settings.SmtpRelayPort.ToString()); await SetFlagAsync(SmtpUseTlsKey, settings.SmtpUseTls.ToString()); await SetFlagAsync(SmtpUseSslKey, settings.SmtpUseSsl.ToString()); await SetFlagAsync(SmtpTimeoutSecondsKey, settings.SmtpTimeoutSeconds.ToString()); await SetFlagAsync(SmtpUsernameKey, settings.SmtpUsername.Trim()); if (!string.IsNullOrWhiteSpace(settings.SmtpPassword)) { await SetFlagAsync(SmtpPasswordKey, settings.SmtpPassword); } await SetFlagAsync(SenderNameKey, settings.SenderName.Trim()); await SetFlagAsync(SenderEmailKey, settings.SenderEmail.Trim()); await _auditLogWriter.WriteAsync("user", actorUserId, "system.security_settings_updated", new { settings.AccessTokenMinutes, settings.RefreshTokenDays, settings.PublicBaseUrl, settings.SmtpRelayHost, settings.SmtpRelayPort, settings.SmtpUseTls, settings.SmtpUseSsl, settings.SmtpTimeoutSeconds, settings.SmtpUsername, settings.SenderName, settings.SenderEmail }); await _dbContext.SaveChangesAsync(); } public async Task SendTestEmailAsync(string toEmail, Guid? actorUserId = null) { var smtp = await GetSmtpSettingsAsync(); ValidateSmtpSettings(smtp); using var message = new MailMessage { Subject = "[SMTP Test] Member Center SMTP 設定測試", Body = "這是一封測試信,代表 SMTP 設定可正常寄送。", IsBodyHtml = false, From = new MailAddress(smtp.SenderEmail, smtp.SenderName) }; message.To.Add(new MailAddress(toEmail)); message.AlternateViews.Add(AlternateView.CreateAlternateViewFromString("

這是一封測試信,代表 SMTP 設定可正常寄送。

", null, "text/html")); using var client = new SmtpClient(smtp.RelayHost, smtp.RelayPort) { EnableSsl = smtp.UseSsl || smtp.UseTls, DeliveryMethod = SmtpDeliveryMethod.Network, Timeout = Math.Max(1000, smtp.TimeoutSeconds * 1000) }; if (!string.IsNullOrWhiteSpace(smtp.Username)) { client.Credentials = new NetworkCredential(smtp.Username, smtp.Password); } await client.SendMailAsync(message); await _auditLogWriter.WriteAsync("user", actorUserId, "system.security_test_email_sent", new { to_email = toEmail, sender_email = smtp.SenderEmail }); return 1; } private async Task GetSmtpSettingsAsync() { var relayHost = await GetFlagAsync(SmtpRelayHostKey, string.Empty); var publicBaseUrl = await GetFlagAsync(PublicBaseUrlKey, string.Empty); var relayPort = await GetFlagAsync(SmtpRelayPortKey, 587); var useTls = await GetFlagAsync(SmtpUseTlsKey, true); var useSsl = await GetFlagAsync(SmtpUseSslKey, false); var timeoutSeconds = await GetFlagAsync(SmtpTimeoutSecondsKey, 15); var username = await GetFlagAsync(SmtpUsernameKey, string.Empty); var password = await GetFlagAsync(SmtpPasswordKey, string.Empty); var senderName = await GetFlagAsync(SenderNameKey, "Member Center"); var senderEmail = await GetFlagAsync(SenderEmailKey, string.Empty); return new SmtpSettingsDto( publicBaseUrl, relayHost, relayPort, useTls, useSsl, timeoutSeconds, username, password, !string.IsNullOrWhiteSpace(password), senderName, senderEmail); } private static void ValidateSmtpSettings(SmtpSettingsDto smtp) { if (string.IsNullOrWhiteSpace(smtp.RelayHost)) { throw new InvalidOperationException("SMTP relay host is empty. Please save SMTP settings first."); } if (smtp.UseTls && smtp.UseSsl) { throw new InvalidOperationException("SMTP TLS and SSL cannot both be enabled."); } if (string.IsNullOrWhiteSpace(smtp.SenderEmail)) { throw new InvalidOperationException("Sender email is empty. Please save sender settings first."); } } private async Task GetFlagAsync(string key, int defaultValue) { var flag = await _dbContext.SystemFlags.FirstOrDefaultAsync(f => f.Key == key); if (flag is null) { return defaultValue; } return int.TryParse(flag.Value, out var value) ? value : defaultValue; } private async Task GetFlagAsync(string key, bool defaultValue) { var flag = await _dbContext.SystemFlags.FirstOrDefaultAsync(f => f.Key == key); if (flag is null) { return defaultValue; } return bool.TryParse(flag.Value, out var value) ? value : defaultValue; } private async Task GetFlagAsync(string key, string defaultValue) { var flag = await _dbContext.SystemFlags.FirstOrDefaultAsync(f => f.Key == key); return flag?.Value ?? defaultValue; } private async Task SetFlagAsync(string key, string value) { var flag = await _dbContext.SystemFlags.FirstOrDefaultAsync(f => f.Key == key); if (flag is null) { _dbContext.SystemFlags.Add(new SystemFlag { Id = Guid.NewGuid(), Key = key, Value = value, UpdatedAt = DateTimeOffset.UtcNow }); } else { flag.Value = value; flag.UpdatedAt = DateTimeOffset.UtcNow; } } }