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");
+ }
+ }
+}