food-market/src/food-market.api/Controllers/SuperAdmin/PlatformSettingsController.cs
nns 03bdf4d4f1 rebrand: письма и серверные строки ALL MARKET
- EmailTemplates (invite/low-stock/weekly-summary): шапка с текстовым
  вордмарком «all market» (бренд-цвета, всегда рендерится — без внешних
  картинок и привязки к домену) + подпись «ALL MARKET — система учёта торговли».
- Имя отправителя по умолчанию, темы/тела системных писем, экспорт-README,
  дневная сводка, тест SMTP → ALL MARKET.
- Цвета писем и домены не менялись.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 17:34:38 +05:00

163 lines
6.7 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using System.Security.Claims;
using foodmarket.Application.Common.Email;
using foodmarket.Domain.Organizations;
using foodmarket.Domain.Platform;
using foodmarket.Infrastructure.Persistence;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.DataProtection;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
namespace foodmarket.Api.Controllers.SuperAdmin;
/// <summary>SuperAdmin: единая платформенная настройка SMTP. GET отдаёт всё
/// КРОМЕ пароля (только has-password флаг). PUT принимает все поля + опционально
/// новый пароль. Пароль шифруется через DataProtection (purpose="foodmarket.smtp")
/// перед записью в БД. Все мутации пишутся в SuperAdminAuditLog с reason.</summary>
[ApiController]
[Authorize(Roles = "SuperAdmin")]
[Route("api/super-admin/platform-settings")]
public class PlatformSettingsController : ControllerBase
{
private readonly AppDbContext _db;
private readonly IDataProtectionProvider _dpProvider;
private readonly IEmailSender _email;
public PlatformSettingsController(AppDbContext db, IDataProtectionProvider dpProvider, IEmailSender email)
{
_db = db; _dpProvider = dpProvider; _email = email;
}
public record PlatformSettingsDto(
string? SmtpHost, int? SmtpPort,
bool SmtpUseSsl, bool SmtpStartTls,
string? SmtpUsername, bool HasSmtpPassword,
string? FromEmail, string? FromName,
DateTime? UpdatedAt);
public record PlatformSettingsInput(
string Reason,
string? SmtpHost, int? SmtpPort,
bool SmtpUseSsl, bool SmtpStartTls,
string? SmtpUsername,
// Если null/пусто — пароль не меняется. Если задан — шифруется и
// записывается. Чтобы СНЯТЬ пароль (отправлять без auth), используем
// спец-значение "__clear__".
string? NewSmtpPassword,
string? FromEmail, string? FromName);
public record TestSendInput(string ToEmail, string Subject, string Body);
[HttpGet]
public async Task<ActionResult<PlatformSettingsDto>> Get(CancellationToken ct)
{
var s = await GetOrCreateAsync(ct);
return new PlatformSettingsDto(
s.SmtpHost, s.SmtpPort,
s.SmtpUseSsl, s.SmtpStartTls,
s.SmtpUsername, !string.IsNullOrEmpty(s.SmtpPasswordEncrypted),
s.FromEmail, s.FromName,
s.UpdatedAt);
}
[HttpPut]
public async Task<IActionResult> Update([FromBody] PlatformSettingsInput input, CancellationToken ct)
{
if (string.IsNullOrWhiteSpace(input.Reason) || input.Reason.Trim().Length < 10)
return BadRequest(new { error = "Причина изменения обязательна (≥ 10 символов) — она пишется в журнал." });
var s = await GetOrCreateAsync(ct);
var prev = new
{
s.SmtpHost, s.SmtpPort, s.SmtpUseSsl, s.SmtpStartTls,
s.SmtpUsername, s.FromEmail, s.FromName,
HasPassword = !string.IsNullOrEmpty(s.SmtpPasswordEncrypted),
};
s.SmtpHost = input.SmtpHost?.Trim();
s.SmtpPort = input.SmtpPort;
s.SmtpUseSsl = input.SmtpUseSsl;
s.SmtpStartTls = input.SmtpStartTls;
s.SmtpUsername = input.SmtpUsername?.Trim();
s.FromEmail = input.FromEmail?.Trim();
s.FromName = input.FromName?.Trim();
s.UpdatedAt = DateTime.UtcNow;
if (input.NewSmtpPassword == "__clear__")
{
s.SmtpPasswordEncrypted = null;
}
else if (!string.IsNullOrEmpty(input.NewSmtpPassword))
{
var protector = _dpProvider.CreateProtector("foodmarket.smtp");
s.SmtpPasswordEncrypted = protector.Protect(input.NewSmtpPassword);
}
await _db.SaveChangesAsync(ct);
await LogAsync("PlatformSettingsUpdate",
$"Обновлены SMTP-настройки платформы (host={s.SmtpHost} from={s.FromEmail})",
input.Reason,
System.Text.Json.JsonSerializer.Serialize(new { prev, next = new {
s.SmtpHost, s.SmtpPort, s.SmtpUseSsl, s.SmtpStartTls,
s.SmtpUsername, s.FromEmail, s.FromName,
HasPassword = !string.IsNullOrEmpty(s.SmtpPasswordEncrypted),
} }), ct);
return NoContent();
}
[HttpPost("test-send")]
public async Task<IActionResult> TestSend([FromBody] TestSendInput input, CancellationToken ct)
{
if (string.IsNullOrWhiteSpace(input.ToEmail))
return BadRequest(new { error = "Адрес получателя обязателен." });
try
{
await _email.SendAsync(input.ToEmail.Trim(),
string.IsNullOrWhiteSpace(input.Subject) ? "ALL MARKET — тестовое сообщение" : input.Subject,
string.IsNullOrWhiteSpace(input.Body) ? "Тестовое письмо от ALL MARKET." : input.Body,
ct);
return Ok(new { ok = true, sentTo = input.ToEmail });
}
catch (EmailNotConfiguredException ex)
{
return BadRequest(new { ok = false, error = ex.Message });
}
catch (Exception ex)
{
// Полный текст ошибки SMTP пишем в response — это SuperAdmin-only,
// diagnostic-info ему нужна.
return StatusCode(StatusCodes.Status500InternalServerError, new { ok = false, error = ex.Message });
}
}
private async Task<PlatformSettings> GetOrCreateAsync(CancellationToken ct)
{
var s = await _db.PlatformSettings.FirstOrDefaultAsync(ct);
if (s is null)
{
s = new PlatformSettings();
_db.PlatformSettings.Add(s);
await _db.SaveChangesAsync(ct);
}
return s;
}
private async Task LogAsync(string actionType, string description, string? reason, string changesJson, CancellationToken ct)
{
var userIdRaw = User.FindFirstValue(ClaimTypes.NameIdentifier) ?? User.FindFirstValue("sub");
Guid.TryParse(userIdRaw, out var uid);
_db.SuperAdminAuditLogs.Add(new SuperAdminAuditLog
{
SuperAdminUserId = uid,
ActionType = actionType,
OrganizationId = null,
Description = description, Reason = reason,
ChangesJson = changesJson,
IpAddress = HttpContext?.Connection?.RemoteIpAddress?.ToString() ?? "",
});
await _db.SaveChangesAsync(ct);
}
}