229 lines
8.4 KiB
C#
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;
|
|
}
|
|
}
|
|
|
|
}
|