feat(domain): Organization.IsArchived/AccountOwner + SuperAdminAuditLog + migration
Базовый domain-каркас для SuperAdmin console (Phase 1): Organization: - IsArchived bool + ArchivedAt DateTime? — архивная орга не видна юзерам, но данные сохраняются. Удалить навсегда можно только из архива >30 дней (логика в API на следующем коммите). - AccountOwnerUserId Guid? — главный владелец, не путать с админами per-org. SuperAdmin может сменить через action c reason в audit-log. - HasIndex(IsArchived) для быстрой фильтрации. SuperAdminAuditLog (новая таблица super_admin_audit_log): - Не tenant-scoped — лог общий по всей системе. - ActionType (CreateOrg/EditOrg/ArchiveOrg/RestoreOrg/DeleteOrg/ ChangeOwner/EditEntity), OrganizationId, EntityType+EntityId, Description, Reason, ChangesJson (jsonb), IpAddress. - Индексы: CreatedAt, (SuperAdminUserId, CreatedAt), (OrganizationId, CreatedAt) — типовые запросы фильтра. Migration Phase4_SuperAdminConsole добавляет 3 колонки в organizations + создаёт super_admin_audit_log с тремя композитными индексами. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
bf6880fe0f
commit
e93634fad4
|
|
@ -13,6 +13,17 @@ public class Organization : Entity
|
|||
public string? Email { get; set; }
|
||||
public bool IsActive { get; set; } = true;
|
||||
|
||||
/// <summary>Архивирована ли организация. Архивные не видны пользователям
|
||||
/// но данные сохраняются. Из архива можно восстановить или (после 30 дней)
|
||||
/// удалить навсегда — этим управляет SuperAdmin.</summary>
|
||||
public bool IsArchived { get; set; }
|
||||
public DateTime? ArchivedAt { get; set; }
|
||||
|
||||
/// <summary>Account owner (главный владелец, не путать с админами роли).
|
||||
/// Это конкретный AppUser, который считается «хозяином» организации;
|
||||
/// SuperAdmin может сменить через отдельный action c reason в audit-log.</summary>
|
||||
public Guid? AccountOwnerUserId { get; set; }
|
||||
|
||||
/// <summary>Персональный API-токен MoySklad. Храним per-organization чтобы
|
||||
/// пользователю не нужно было вводить его каждый раз при импорте.</summary>
|
||||
public string? MoySkladToken { get; set; }
|
||||
|
|
|
|||
20
src/food-market.domain/Organizations/SuperAdminAuditLog.cs
Normal file
20
src/food-market.domain/Organizations/SuperAdminAuditLog.cs
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
using foodmarket.Domain.Common;
|
||||
|
||||
namespace foodmarket.Domain.Organizations;
|
||||
|
||||
/// <summary>Журнал действий SuperAdmin'а: создание/правка/архивирование
|
||||
/// организаций, смена аккаунт-владельца, правки в режиме «войти как».
|
||||
/// Не tenant-scoped — лог общий для всей системы.</summary>
|
||||
public class SuperAdminAuditLog : Entity
|
||||
{
|
||||
public Guid SuperAdminUserId { get; set; }
|
||||
public string ActionType { get; set; } = "";
|
||||
public Guid? OrganizationId { get; set; }
|
||||
public string? EntityType { get; set; }
|
||||
public Guid? EntityId { get; set; }
|
||||
public string? Description { get; set; }
|
||||
public string? Reason { get; set; }
|
||||
/// <summary>JSON с diff'ом before/after или другим payload'ом действия.</summary>
|
||||
public string ChangesJson { get; set; } = "{}";
|
||||
public string IpAddress { get; set; } = "";
|
||||
}
|
||||
|
|
@ -49,6 +49,7 @@ public AppDbContext(DbContextOptions<AppDbContext> options, ITenantContext tenan
|
|||
public DbSet<EmployeeRole> EmployeeRoles => Set<EmployeeRole>();
|
||||
public DbSet<Employee> Employees => Set<Employee>();
|
||||
public DbSet<EmployeeRetailPointAssignment> EmployeeRetailPointAssignments => Set<EmployeeRetailPointAssignment>();
|
||||
public DbSet<SuperAdminAuditLog> SuperAdminAuditLogs => Set<SuperAdminAuditLog>();
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder builder)
|
||||
{
|
||||
|
|
@ -76,6 +77,21 @@ protected override void OnModelCreating(ModelBuilder builder)
|
|||
b.Property(o => o.MoySkladToken).HasMaxLength(200);
|
||||
b.HasOne(o => o.DefaultCurrency).WithMany().HasForeignKey(o => o.DefaultCurrencyId).OnDelete(DeleteBehavior.Restrict);
|
||||
b.HasIndex(o => o.Name);
|
||||
b.HasIndex(o => o.IsArchived);
|
||||
});
|
||||
|
||||
builder.Entity<SuperAdminAuditLog>(b =>
|
||||
{
|
||||
b.ToTable("super_admin_audit_log");
|
||||
b.Property(x => x.ActionType).HasMaxLength(50).IsRequired();
|
||||
b.Property(x => x.EntityType).HasMaxLength(100);
|
||||
b.Property(x => x.Description).HasMaxLength(500);
|
||||
b.Property(x => x.Reason).HasMaxLength(1000);
|
||||
b.Property(x => x.ChangesJson).HasColumnType("jsonb");
|
||||
b.Property(x => x.IpAddress).HasMaxLength(45);
|
||||
b.HasIndex(x => x.CreatedAt);
|
||||
b.HasIndex(x => new { x.SuperAdminUserId, x.CreatedAt });
|
||||
b.HasIndex(x => new { x.OrganizationId, x.CreatedAt });
|
||||
});
|
||||
|
||||
builder.ConfigureCatalog();
|
||||
|
|
|
|||
2048
src/food-market.infrastructure/Persistence/Migrations/20260427040000_Phase4_SuperAdminConsole.Designer.cs
generated
Normal file
2048
src/food-market.infrastructure/Persistence/Migrations/20260427040000_Phase4_SuperAdminConsole.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,71 @@
|
|||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace foodmarket.Infrastructure.Persistence.Migrations
|
||||
{
|
||||
/// <summary>SuperAdmin console: organizations.IsArchived/ArchivedAt/
|
||||
/// AccountOwnerUserId + новая таблица super_admin_audit_log для журнала
|
||||
/// действий супер-админа (создание/правка/архивирование орг,
|
||||
/// смена аккаунт-владельца, правки в режиме «войти как»).</summary>
|
||||
public partial class Phase4_SuperAdminConsole : Migration
|
||||
{
|
||||
protected override void Up(MigrationBuilder b)
|
||||
{
|
||||
b.AddColumn<bool>(
|
||||
name: "IsArchived", schema: "public", table: "organizations",
|
||||
type: "boolean", nullable: false, defaultValue: false);
|
||||
b.AddColumn<System.DateTime>(
|
||||
name: "ArchivedAt", schema: "public", table: "organizations",
|
||||
type: "timestamp with time zone", nullable: true);
|
||||
b.AddColumn<System.Guid>(
|
||||
name: "AccountOwnerUserId", schema: "public", table: "organizations",
|
||||
type: "uuid", nullable: true);
|
||||
b.CreateIndex(
|
||||
name: "IX_organizations_IsArchived",
|
||||
schema: "public",
|
||||
table: "organizations",
|
||||
column: "IsArchived");
|
||||
|
||||
b.CreateTable(
|
||||
name: "super_admin_audit_log",
|
||||
schema: "public",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<System.Guid>(type: "uuid", nullable: false),
|
||||
SuperAdminUserId = table.Column<System.Guid>(type: "uuid", nullable: false),
|
||||
ActionType = table.Column<string>(type: "character varying(50)", maxLength: 50, nullable: false),
|
||||
OrganizationId = table.Column<System.Guid>(type: "uuid", nullable: true),
|
||||
EntityType = table.Column<string>(type: "character varying(100)", maxLength: 100, nullable: true),
|
||||
EntityId = table.Column<System.Guid>(type: "uuid", nullable: true),
|
||||
Description = table.Column<string>(type: "character varying(500)", maxLength: 500, nullable: true),
|
||||
Reason = table.Column<string>(type: "character varying(1000)", maxLength: 1000, nullable: true),
|
||||
ChangesJson = table.Column<string>(type: "jsonb", nullable: false),
|
||||
IpAddress = table.Column<string>(type: "character varying(45)", maxLength: 45, nullable: false),
|
||||
CreatedAt = table.Column<System.DateTime>(type: "timestamp with time zone", nullable: false),
|
||||
UpdatedAt = table.Column<System.DateTime>(type: "timestamp with time zone", nullable: true),
|
||||
},
|
||||
constraints: table => table.PrimaryKey("PK_super_admin_audit_log", x => x.Id));
|
||||
b.CreateIndex(
|
||||
name: "IX_super_admin_audit_log_CreatedAt",
|
||||
schema: "public", table: "super_admin_audit_log", column: "CreatedAt");
|
||||
b.CreateIndex(
|
||||
name: "IX_super_admin_audit_log_SuperAdminUserId_CreatedAt",
|
||||
schema: "public", table: "super_admin_audit_log",
|
||||
columns: new[] { "SuperAdminUserId", "CreatedAt" });
|
||||
b.CreateIndex(
|
||||
name: "IX_super_admin_audit_log_OrganizationId_CreatedAt",
|
||||
schema: "public", table: "super_admin_audit_log",
|
||||
columns: new[] { "OrganizationId", "CreatedAt" });
|
||||
}
|
||||
|
||||
protected override void Down(MigrationBuilder b)
|
||||
{
|
||||
b.DropTable(name: "super_admin_audit_log", schema: "public");
|
||||
b.DropIndex(name: "IX_organizations_IsArchived", schema: "public", table: "organizations");
|
||||
b.DropColumn(name: "AccountOwnerUserId", schema: "public", table: "organizations");
|
||||
b.DropColumn(name: "ArchivedAt", schema: "public", table: "organizations");
|
||||
b.DropColumn(name: "IsArchived", schema: "public", table: "organizations");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1086,6 +1086,15 @@ protected override void BuildModel(ModelBuilder modelBuilder)
|
|||
b.Property<bool>("IsActive")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<bool>("IsArchived")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<DateTime?>("ArchivedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<Guid?>("AccountOwnerUserId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("MoySkladToken")
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("character varying(200)");
|
||||
|
|
@ -1131,6 +1140,8 @@ protected override void BuildModel(ModelBuilder modelBuilder)
|
|||
|
||||
b.HasIndex("Name");
|
||||
|
||||
b.HasIndex("IsArchived");
|
||||
|
||||
b.ToTable("organizations", "public");
|
||||
});
|
||||
|
||||
|
|
@ -1889,6 +1900,27 @@ protected override void BuildModel(ModelBuilder modelBuilder)
|
|||
b.Navigation("Lines");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("foodmarket.Domain.Organizations.SuperAdminAuditLog", b =>
|
||||
{
|
||||
b.Property<Guid>("Id").ValueGeneratedOnAdd().HasColumnType("uuid");
|
||||
b.Property<Guid>("SuperAdminUserId").HasColumnType("uuid");
|
||||
b.Property<string>("ActionType").IsRequired().HasMaxLength(50).HasColumnType("character varying(50)");
|
||||
b.Property<Guid?>("OrganizationId").HasColumnType("uuid");
|
||||
b.Property<string>("EntityType").HasMaxLength(100).HasColumnType("character varying(100)");
|
||||
b.Property<Guid?>("EntityId").HasColumnType("uuid");
|
||||
b.Property<string>("Description").HasMaxLength(500).HasColumnType("character varying(500)");
|
||||
b.Property<string>("Reason").HasMaxLength(1000).HasColumnType("character varying(1000)");
|
||||
b.Property<string>("ChangesJson").HasColumnType("jsonb");
|
||||
b.Property<string>("IpAddress").HasMaxLength(45).HasColumnType("character varying(45)");
|
||||
b.Property<DateTime>("CreatedAt").HasColumnType("timestamp with time zone");
|
||||
b.Property<DateTime?>("UpdatedAt").HasColumnType("timestamp with time zone");
|
||||
b.HasKey("Id");
|
||||
b.HasIndex("CreatedAt");
|
||||
b.HasIndex("SuperAdminUserId", "CreatedAt");
|
||||
b.HasIndex("OrganizationId", "CreatedAt");
|
||||
b.ToTable("super_admin_audit_log", "public");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("foodmarket.Domain.Organizations.EmployeeRole", b =>
|
||||
{
|
||||
b.Property<Guid>("Id").ValueGeneratedOnAdd().HasColumnType("uuid");
|
||||
|
|
|
|||
Loading…
Reference in a new issue