229 lines
8.4 KiB
C#

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<SecuritySettingsDto> 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<int> 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("<p>這是一封測試信,代表 SMTP 設定可正常寄送。</p>", 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<SmtpSettingsDto> 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<int> 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<bool> 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<string> 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;
}
}
}