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