Add transfer tracking and secure admin access
This commit is contained in:
@@ -23,6 +23,9 @@ public class AppDbContext(DbContextOptions<AppDbContext> options) : DbContext(op
|
||||
public DbSet<PushMessage> PushMessages => Set<PushMessage>();
|
||||
public DbSet<PushDelivery> PushDeliveries => Set<PushDelivery>();
|
||||
public DbSet<BudgetNotificationReceipt> BudgetNotificationReceipts => Set<BudgetNotificationReceipt>();
|
||||
public DbSet<AdminUser> AdminUsers => Set<AdminUser>();
|
||||
public DbSet<AdminSession> AdminSessions => Set<AdminSession>();
|
||||
public DbSet<AdminAuditLog> AdminAuditLogs => Set<AdminAuditLog>();
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder b)
|
||||
{
|
||||
@@ -54,8 +57,16 @@ public class AppDbContext(DbContextOptions<AppDbContext> options) : DbContext(op
|
||||
e.HasIndex(x => new { x.LedgerId, x.OccurredAt });
|
||||
e.HasIndex(x => new { x.UserId, x.IsDeleted });
|
||||
e.Property(x => x.ClientRequestId).HasMaxLength(64);
|
||||
e.HasIndex(x => new { x.UserId, x.ClientRequestId }).IsUnique();
|
||||
e.HasQueryFilter(x => !x.IsDeleted); // 软删除全局过滤
|
||||
e.HasIndex(x => new { x.UserId, x.ClientRequestId }).IsUnique();
|
||||
e.Property(x => x.Counterparty).HasMaxLength(100);
|
||||
e.Property(x => x.Provider).HasMaxLength(24);
|
||||
e.Property(x => x.ProviderTransactionId).HasMaxLength(128);
|
||||
e.Property(x => x.RecognitionOccurrenceId).HasMaxLength(64);
|
||||
e.Property(x => x.EvidenceFingerprint).HasMaxLength(64);
|
||||
e.Property(x => x.RecognitionConfidence).HasMaxLength(24);
|
||||
e.HasIndex(x => new { x.UserId, x.Provider, x.ProviderTransactionId }).IsUnique();
|
||||
e.HasIndex(x => new { x.UserId, x.RecognitionOccurrenceId }).IsUnique();
|
||||
e.HasQueryFilter(x => !x.IsDeleted); // 软删除全局过滤
|
||||
});
|
||||
|
||||
b.Entity<Category>(e => e.Property(x => x.ColorKey).HasMaxLength(32));
|
||||
@@ -142,6 +153,40 @@ public class AppDbContext(DbContextOptions<AppDbContext> options) : DbContext(op
|
||||
.HasForeignKey(x => x.BudgetId).OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
b.Entity<AdminUser>(e =>
|
||||
{
|
||||
e.Property(x => x.Username).HasMaxLength(64);
|
||||
e.Property(x => x.PasswordHash).HasMaxLength(128);
|
||||
e.Property(x => x.Role).HasMaxLength(24);
|
||||
e.HasIndex(x => x.Username).IsUnique();
|
||||
e.HasIndex(x => new { x.IsActive, x.Role });
|
||||
});
|
||||
|
||||
b.Entity<AdminSession>(e =>
|
||||
{
|
||||
e.Property(x => x.TokenHash).HasMaxLength(64);
|
||||
e.Property(x => x.CsrfTokenHash).HasMaxLength(64);
|
||||
e.Property(x => x.IpAddress).HasMaxLength(64);
|
||||
e.Property(x => x.UserAgent).HasMaxLength(300);
|
||||
e.HasIndex(x => x.TokenHash).IsUnique();
|
||||
e.HasIndex(x => new { x.AdminUserId, x.RevokedAt, x.ExpiresAt });
|
||||
e.HasOne(x => x.AdminUser).WithMany(x => x.Sessions)
|
||||
.HasForeignKey(x => x.AdminUserId).OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
b.Entity<AdminAuditLog>(e =>
|
||||
{
|
||||
e.Property(x => x.Username).HasMaxLength(64);
|
||||
e.Property(x => x.Action).HasMaxLength(80);
|
||||
e.Property(x => x.Resource).HasMaxLength(120);
|
||||
e.Property(x => x.HttpMethod).HasMaxLength(12);
|
||||
e.Property(x => x.Path).HasMaxLength(300);
|
||||
e.Property(x => x.Detail).HasMaxLength(1000);
|
||||
e.Property(x => x.IpAddress).HasMaxLength(64);
|
||||
e.HasIndex(x => x.CreatedAt);
|
||||
e.HasIndex(x => new { x.AdminUserId, x.CreatedAt });
|
||||
});
|
||||
|
||||
// MySQL DATETIME has no timezone metadata. Preserve stored UTC wall-clock
|
||||
// values and restore DateTimeKind.Utc whenever EF materializes them.
|
||||
var utcDateTimeConverter = new ValueConverter<DateTime, DateTime>(
|
||||
|
||||
+1255
File diff suppressed because it is too large
Load Diff
+255
@@ -0,0 +1,255 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Metadata;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace MiaoJiZhang.Infrastructure.Persistence.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class TransferAndAdminSecurity : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "Counterparty",
|
||||
table: "Transactions",
|
||||
type: "varchar(100)",
|
||||
maxLength: 100,
|
||||
nullable: true)
|
||||
.Annotation("MySql:CharSet", "utf8mb4");
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "EvidenceFingerprint",
|
||||
table: "Transactions",
|
||||
type: "varchar(64)",
|
||||
maxLength: 64,
|
||||
nullable: true)
|
||||
.Annotation("MySql:CharSet", "utf8mb4");
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "Provider",
|
||||
table: "Transactions",
|
||||
type: "varchar(24)",
|
||||
maxLength: 24,
|
||||
nullable: true)
|
||||
.Annotation("MySql:CharSet", "utf8mb4");
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "ProviderTransactionId",
|
||||
table: "Transactions",
|
||||
type: "varchar(128)",
|
||||
maxLength: 128,
|
||||
nullable: true)
|
||||
.Annotation("MySql:CharSet", "utf8mb4");
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "RecognitionConfidence",
|
||||
table: "Transactions",
|
||||
type: "varchar(24)",
|
||||
maxLength: 24,
|
||||
nullable: true)
|
||||
.Annotation("MySql:CharSet", "utf8mb4");
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "RecognitionOccurrenceId",
|
||||
table: "Transactions",
|
||||
type: "varchar(64)",
|
||||
maxLength: 64,
|
||||
nullable: true)
|
||||
.Annotation("MySql:CharSet", "utf8mb4");
|
||||
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "TransferDirection",
|
||||
table: "Transactions",
|
||||
type: "int",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "AdminAuditLogs",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<long>(type: "bigint", nullable: false)
|
||||
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
|
||||
AdminUserId = table.Column<long>(type: "bigint", nullable: true),
|
||||
Username = table.Column<string>(type: "varchar(64)", maxLength: 64, nullable: true)
|
||||
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||
Action = table.Column<string>(type: "varchar(80)", maxLength: 80, nullable: false)
|
||||
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||
Resource = table.Column<string>(type: "varchar(120)", maxLength: 120, nullable: false)
|
||||
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||
HttpMethod = table.Column<string>(type: "varchar(12)", maxLength: 12, nullable: false)
|
||||
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||
Path = table.Column<string>(type: "varchar(300)", maxLength: 300, nullable: false)
|
||||
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||
StatusCode = table.Column<int>(type: "int", nullable: false),
|
||||
Success = table.Column<bool>(type: "tinyint(1)", nullable: false),
|
||||
Detail = table.Column<string>(type: "varchar(1000)", maxLength: 1000, nullable: true)
|
||||
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||
IpAddress = table.Column<string>(type: "varchar(64)", maxLength: 64, nullable: true)
|
||||
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||
CreatedAt = table.Column<DateTime>(type: "datetime(6)", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_AdminAuditLogs", x => x.Id);
|
||||
})
|
||||
.Annotation("MySql:CharSet", "utf8mb4");
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "AdminUsers",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<long>(type: "bigint", nullable: false)
|
||||
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
|
||||
Username = table.Column<string>(type: "varchar(64)", maxLength: 64, nullable: false)
|
||||
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||
PasswordHash = table.Column<string>(type: "varchar(128)", maxLength: 128, nullable: false)
|
||||
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||
Role = table.Column<string>(type: "varchar(24)", maxLength: 24, nullable: false)
|
||||
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||
IsActive = table.Column<bool>(type: "tinyint(1)", nullable: false),
|
||||
MustChangePassword = table.Column<bool>(type: "tinyint(1)", nullable: false),
|
||||
AuthVersion = table.Column<int>(type: "int", nullable: false),
|
||||
FailedLoginCount = table.Column<int>(type: "int", nullable: false),
|
||||
LockedUntil = table.Column<DateTime>(type: "datetime(6)", nullable: true),
|
||||
LastLoginAt = table.Column<DateTime>(type: "datetime(6)", nullable: true),
|
||||
CreatedAt = table.Column<DateTime>(type: "datetime(6)", nullable: false),
|
||||
UpdatedAt = table.Column<DateTime>(type: "datetime(6)", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_AdminUsers", x => x.Id);
|
||||
})
|
||||
.Annotation("MySql:CharSet", "utf8mb4");
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "AdminSessions",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<long>(type: "bigint", nullable: false)
|
||||
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
|
||||
AdminUserId = table.Column<long>(type: "bigint", nullable: false),
|
||||
TokenHash = table.Column<string>(type: "varchar(64)", maxLength: 64, nullable: false)
|
||||
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||
CsrfTokenHash = table.Column<string>(type: "varchar(64)", maxLength: 64, nullable: false)
|
||||
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||
AuthVersion = table.Column<int>(type: "int", nullable: false),
|
||||
ExpiresAt = table.Column<DateTime>(type: "datetime(6)", nullable: false),
|
||||
AbsoluteExpiresAt = table.Column<DateTime>(type: "datetime(6)", nullable: false),
|
||||
RevokedAt = table.Column<DateTime>(type: "datetime(6)", nullable: true),
|
||||
LastSeenAt = table.Column<DateTime>(type: "datetime(6)", nullable: false),
|
||||
IpAddress = table.Column<string>(type: "varchar(64)", maxLength: 64, nullable: true)
|
||||
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||
UserAgent = table.Column<string>(type: "varchar(300)", maxLength: 300, nullable: true)
|
||||
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||
CreatedAt = table.Column<DateTime>(type: "datetime(6)", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_AdminSessions", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_AdminSessions_AdminUsers_AdminUserId",
|
||||
column: x => x.AdminUserId,
|
||||
principalTable: "AdminUsers",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
})
|
||||
.Annotation("MySql:CharSet", "utf8mb4");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Transactions_UserId_Provider_ProviderTransactionId",
|
||||
table: "Transactions",
|
||||
columns: new[] { "UserId", "Provider", "ProviderTransactionId" },
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Transactions_UserId_RecognitionOccurrenceId",
|
||||
table: "Transactions",
|
||||
columns: new[] { "UserId", "RecognitionOccurrenceId" },
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_AdminAuditLogs_AdminUserId_CreatedAt",
|
||||
table: "AdminAuditLogs",
|
||||
columns: new[] { "AdminUserId", "CreatedAt" });
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_AdminAuditLogs_CreatedAt",
|
||||
table: "AdminAuditLogs",
|
||||
column: "CreatedAt");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_AdminSessions_AdminUserId_RevokedAt_ExpiresAt",
|
||||
table: "AdminSessions",
|
||||
columns: new[] { "AdminUserId", "RevokedAt", "ExpiresAt" });
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_AdminSessions_TokenHash",
|
||||
table: "AdminSessions",
|
||||
column: "TokenHash",
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_AdminUsers_IsActive_Role",
|
||||
table: "AdminUsers",
|
||||
columns: new[] { "IsActive", "Role" });
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_AdminUsers_Username",
|
||||
table: "AdminUsers",
|
||||
column: "Username",
|
||||
unique: true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "AdminAuditLogs");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "AdminSessions");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "AdminUsers");
|
||||
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_Transactions_UserId_Provider_ProviderTransactionId",
|
||||
table: "Transactions");
|
||||
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_Transactions_UserId_RecognitionOccurrenceId",
|
||||
table: "Transactions");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "Counterparty",
|
||||
table: "Transactions");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "EvidenceFingerprint",
|
||||
table: "Transactions");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "Provider",
|
||||
table: "Transactions");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "ProviderTransactionId",
|
||||
table: "Transactions");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "RecognitionConfidence",
|
||||
table: "Transactions");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "RecognitionOccurrenceId",
|
||||
table: "Transactions");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "TransferDirection",
|
||||
table: "Transactions");
|
||||
}
|
||||
}
|
||||
}
|
||||
+1252
-1028
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user