From 1456f170eb1982e44aad9a6cd2694b257e873e9d Mon Sep 17 00:00:00 2001
From: nns <278048682+nurdotnet@users.noreply.github.com>
Date: Wed, 6 May 2026 12:35:48 +0500
Subject: [PATCH] =?UTF-8?q?feat(platform):=20PlatformSettings=20entity=20+?=
=?UTF-8?q?=20=D0=BC=D0=B8=D0=B3=D1=80=D0=B0=D1=86=D0=B8=D1=8F=20(singleto?=
=?UTF-8?q?n=20SMTP-=D0=BA=D0=BE=D0=BD=D1=84=D0=B8=D0=B3)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Платформенные настройки: один row, не tenant-scoped, видны и меняются
только Супер-администратором. Хранят SMTP-креды для исходящей почты
(forgot-password, нотификации). IMAP к этому отношения не имеет — IMAP
для чтения входящей, нам нужен SMTP.
Поля:
- SmtpHost, SmtpPort
- SmtpUseSsl (implicit TLS / 465) и SmtpStartTls (587, по дефолту true)
- SmtpUsername, SmtpPasswordEncrypted (хранится зашифрованным через
DataProtection API; в API-ответах не выходит, только has-password флаг)
- FromEmail, FromName
Миграция Phase5b_PlatformSettings создаёт таблицу public.platform_settings.
Конфиг EF в AppDbContext: HasMaxLength для строк.
---
.../Platform/PlatformSettings.cs | 36 +++++++++++++++++
.../Persistence/AppDbContext.cs | 10 +++++
...20260506100000_Phase5b_PlatformSettings.cs | 39 +++++++++++++++++++
3 files changed, 85 insertions(+)
create mode 100644 src/food-market.domain/Platform/PlatformSettings.cs
create mode 100644 src/food-market.infrastructure/Persistence/Migrations/20260506100000_Phase5b_PlatformSettings.cs
diff --git a/src/food-market.domain/Platform/PlatformSettings.cs b/src/food-market.domain/Platform/PlatformSettings.cs
new file mode 100644
index 0000000..966180f
--- /dev/null
+++ b/src/food-market.domain/Platform/PlatformSettings.cs
@@ -0,0 +1,36 @@
+using foodmarket.Domain.Common;
+
+namespace foodmarket.Domain.Platform;
+
+/// Платформенные настройки (singleton, single-row). Хранят SMTP-креды
+/// для отправки писем (forgot-password, инвайты, нотификации). Не tenant-scoped —
+/// общий конфиг для всей платформы, видны и меняются только Супер-администратором.
+///
+/// SmtpPassword хранится зашифрованным через DataProtection API
+/// (`IDataProtectionProvider.CreateProtector("foodmarket.smtp")`); снаружи
+/// (контроллер) — никогда не возвращается в открытом виде, только has-password флаг.
+public class PlatformSettings : Entity
+{
+ /// SMTP-сервер для отправки исходящей почты (НЕ IMAP — IMAP это
+ /// для чтения входящей).
+ public string? SmtpHost { get; set; }
+ public int? SmtpPort { get; set; }
+
+ /// Implicit TLS (SMTPS, обычно порт 465). Взаимоисключающий
+ /// со SmtpStartTls (587).
+ public bool SmtpUseSsl { get; set; }
+
+ /// STARTTLS upgrade (обычно порт 587). По дефолту true в большинстве
+ /// современных провайдеров (Gmail/Yandex/Mailgun).
+ public bool SmtpStartTls { get; set; } = true;
+
+ public string? SmtpUsername { get; set; }
+
+ /// Зашифрованный SmtpPassword (base64 через DataProtection).
+ /// Никогда не отдаётся в API-ответах. Установка только через PUT с
+ /// явно переданным new-password полем.
+ public string? SmtpPasswordEncrypted { get; set; }
+
+ public string? FromEmail { get; set; }
+ public string? FromName { get; set; }
+}
diff --git a/src/food-market.infrastructure/Persistence/AppDbContext.cs b/src/food-market.infrastructure/Persistence/AppDbContext.cs
index 1397a10..cd8cb2e 100644
--- a/src/food-market.infrastructure/Persistence/AppDbContext.cs
+++ b/src/food-market.infrastructure/Persistence/AppDbContext.cs
@@ -51,6 +51,7 @@ public AppDbContext(DbContextOptions options, ITenantContext tenan
public DbSet EmployeeRetailPointAssignments => Set();
public DbSet SuperAdminAuditLogs => Set();
public DbSet SystemSettings => Set();
+ public DbSet PlatformSettings => Set();
protected override void OnModelCreating(ModelBuilder builder)
{
@@ -86,6 +87,15 @@ protected override void OnModelCreating(ModelBuilder builder)
b.ToTable("system_settings");
});
+ builder.Entity(b =>
+ {
+ b.ToTable("platform_settings");
+ b.Property(x => x.SmtpHost).HasMaxLength(200);
+ b.Property(x => x.SmtpUsername).HasMaxLength(200);
+ b.Property(x => x.FromEmail).HasMaxLength(200);
+ b.Property(x => x.FromName).HasMaxLength(200);
+ });
+
builder.Entity(b =>
{
b.ToTable("super_admin_audit_log");
diff --git a/src/food-market.infrastructure/Persistence/Migrations/20260506100000_Phase5b_PlatformSettings.cs b/src/food-market.infrastructure/Persistence/Migrations/20260506100000_Phase5b_PlatformSettings.cs
new file mode 100644
index 0000000..a360594
--- /dev/null
+++ b/src/food-market.infrastructure/Persistence/Migrations/20260506100000_Phase5b_PlatformSettings.cs
@@ -0,0 +1,39 @@
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace foodmarket.Infrastructure.Persistence.Migrations
+{
+ /// Платформенные настройки (singleton). SMTP-креды для отправки
+ /// писем (forgot-password, нотификации). Управляется SuperAdmin'ом через
+ /// /super-admin/platform-settings. Видна только им.
+ public partial class Phase5b_PlatformSettings : Migration
+ {
+ protected override void Up(MigrationBuilder b)
+ {
+ b.CreateTable(
+ name: "platform_settings",
+ schema: "public",
+ columns: table => new
+ {
+ Id = table.Column(type: "uuid", nullable: false),
+ SmtpHost = table.Column(type: "character varying(200)", maxLength: 200, nullable: true),
+ SmtpPort = table.Column(type: "integer", nullable: true),
+ SmtpUseSsl = table.Column(type: "boolean", nullable: false, defaultValue: false),
+ SmtpStartTls = table.Column(type: "boolean", nullable: false, defaultValue: true),
+ SmtpUsername = table.Column(type: "character varying(200)", maxLength: 200, nullable: true),
+ SmtpPasswordEncrypted = table.Column(type: "text", nullable: true),
+ FromEmail = table.Column(type: "character varying(200)", maxLength: 200, nullable: true),
+ FromName = table.Column(type: "character varying(200)", maxLength: 200, nullable: true),
+ CreatedAt = table.Column(type: "timestamp with time zone", nullable: false),
+ UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true),
+ },
+ constraints: table => table.PrimaryKey("PK_platform_settings", x => x.Id));
+ }
+
+ protected override void Down(MigrationBuilder b)
+ {
+ b.DropTable(name: "platform_settings", schema: "public");
+ }
+ }
+}