Rebuild on Angular 22 + ASP.NET Core 10, containerised

The 2018 stack (Angular 5.2 / CLI 1.7, PHP, MySQL) had not been touched since
July 2018. Rebuilt rather than upgraded in place: the frontend was 17 major
versions behind, and of ~16,700 lines of PHP only ~150 were application logic —
the rest was four near-identical vendored copies of php-crud-api plus
class.upload.php.

Backend — ASP.NET Core 10, EF Core, SQLite
  * ASP.NET Core Identity (PBKDF2) + JWT bearer auth
  * Clean REST API replacing php-crud-api's filter[]/transform query syntax
  * Box art uploads re-encoded to WebP via SkiaSharp
  * Imports the 105 games recovered from the 2018 dump on first run

Frontend — Angular 22, zoneless, signals, Material 22
  * Standalone components, lazy routes, functional guards and interceptor
  * Vitest replaces Karma/Jasmine; fonts and icons bundled, no CDN calls
  * No provideAnimations: @angular/animations is deprecated in v22 and
    Material no longer depends on it (pinned by a test)

Docker
  * Multi-stage builds for both services, non-root at runtime
  * nginx serves the SPA and reverse-proxies the API, so everything is
    same-origin; one volume holds the database, uploads and DP keys

Security issues in the old code, not carried across:
  * Two endpoints exposed unauthenticated CRUD over every table
  * The client chose whose rows to read (filter[]=userId,eq,N); ownership now
    comes from the JWT subject server-side
  * Login was hardcoded to a single username
  * crypt() with one global salt, silently truncating passwords to 8 chars
  * JWT secret was the literal string "testing", tokens never expired
  * Token travelled in the query string rather than a header
  * Uploads were anonymous with the path built from the client filename
  * Access-Control-Allow-Origin: *

The live MySQL password committed in 2018 remains in git history and must be
rotated independently of this change.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-03 18:46:33 -04:00
co-authored by Claude Opus 5
parent 10757575c2
commit 2a7d90b2d5
165 changed files with 14908 additions and 31942 deletions
+136
View File
@@ -0,0 +1,136 @@
using System.Text.Json;
using LudosData.Api.Domain;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
namespace LudosData.Api.Data;
public class SeedOptions
{
public const string SectionName = "Seed";
/// <summary>When false, migrations still run but no user or games are created.</summary>
public bool Enabled { get; set; } = true;
public string UserName { get; set; } = string.Empty;
public string Email { get; set; } = string.Empty;
public string Password { get; set; } = string.Empty;
}
/// <summary>
/// Applies migrations and, on a genuinely empty database, creates the initial user
/// and imports the 105 games recovered from the 2018 MySQL dump.
///
/// The dump predates the multi-user work, so it has no users table and no per-game
/// owner: every imported game is assigned to the seed user.
/// </summary>
public static class DbSeeder
{
public static async Task MigrateAndSeedAsync(IServiceProvider services, CancellationToken ct = default)
{
using var scope = services.CreateScope();
var sp = scope.ServiceProvider;
var logger = sp.GetRequiredService<ILoggerFactory>().CreateLogger("DbSeeder");
var db = sp.GetRequiredService<LudosDbContext>();
await db.Database.MigrateAsync(ct);
var options = sp.GetRequiredService<Microsoft.Extensions.Options.IOptions<SeedOptions>>().Value;
if (!options.Enabled)
{
logger.LogInformation("Seeding disabled; skipping.");
return;
}
if (await db.Users.AnyAsync(ct))
{
logger.LogInformation("Database already has users; skipping seed.");
return;
}
if (string.IsNullOrWhiteSpace(options.UserName) || string.IsNullOrWhiteSpace(options.Password))
{
logger.LogWarning(
"Seeding is enabled but Seed:UserName / Seed:Password are not set, so no initial user was " +
"created and the {Count} games from the 2018 dump were not imported. Set SEED__USERNAME, " +
"SEED__EMAIL and SEED__PASSWORD and restart, or register a user and import manually.",
await CountSeedGamesAsync(ct));
return;
}
var userManager = sp.GetRequiredService<UserManager<AppUser>>();
var user = new AppUser
{
UserName = options.UserName,
Email = string.IsNullOrWhiteSpace(options.Email) ? $"{options.UserName}@localhost" : options.Email,
EmailConfirmed = true,
};
var created = await userManager.CreateAsync(user, options.Password);
if (!created.Succeeded)
{
var errors = string.Join("; ", created.Errors.Select(e => e.Description));
logger.LogError("Could not create the seed user: {Errors}", errors);
return;
}
var games = await LoadSeedGamesAsync(ct);
foreach (var game in games)
{
game.OwnerId = user.Id;
db.Games.Add(game);
}
await db.SaveChangesAsync(ct);
logger.LogInformation(
"Seeded user {UserName} with {Count} games from the 2018 dump.", user.UserName, games.Count);
}
private static async Task<List<Game>> LoadSeedGamesAsync(CancellationToken ct)
{
var path = Path.Combine(AppContext.BaseDirectory, "Data", "Seed", "games.json");
if (!File.Exists(path))
{
return [];
}
await using var stream = File.OpenRead(path);
var records = await JsonSerializer.DeserializeAsync<List<SeedGame>>(
stream, new JsonSerializerOptions(JsonSerializerDefaults.Web), ct) ?? [];
return records.Select(r => new Game
{
Title = r.Title,
System = r.System,
Genre = r.Genre,
Year = r.Year,
Developer = r.Developer,
Publisher = r.Publisher,
Art = r.Art,
Description = r.Description,
Own = r.Own,
Dumped = r.Dumped,
Played = r.Played,
Finished = r.Finished,
}).ToList();
}
private static async Task<int> CountSeedGamesAsync(CancellationToken ct) =>
(await LoadSeedGamesAsync(ct)).Count;
private sealed record SeedGame(
string Title,
string? System,
string? Genre,
string? Year,
string? Developer,
string? Publisher,
string? Art,
string? Description,
bool Own,
bool Dumped,
bool Played,
bool Finished);
}
@@ -0,0 +1,60 @@
using LudosData.Api.Domain;
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
namespace LudosData.Api.Data;
public class LudosDbContext(DbContextOptions<LudosDbContext> options)
: IdentityDbContext<AppUser>(options)
{
public DbSet<Game> Games => Set<Game>();
protected override void OnModelCreating(ModelBuilder builder)
{
base.OnModelCreating(builder);
builder.Entity<Game>(game =>
{
game.HasOne(g => g.Owner)
.WithMany(u => u.Games)
.HasForeignKey(g => g.OwnerId)
.OnDelete(DeleteBehavior.Cascade);
// Every list query filters by owner first, then narrows or sorts on
// these columns, so they lead the composite indexes.
game.HasIndex(g => new { g.OwnerId, g.Title });
game.HasIndex(g => new { g.OwnerId, g.System });
game.HasIndex(g => new { g.OwnerId, g.Genre });
});
}
public override int SaveChanges()
{
StampTimestamps();
return base.SaveChanges();
}
public override Task<int> SaveChangesAsync(CancellationToken cancellationToken = default)
{
StampTimestamps();
return base.SaveChangesAsync(cancellationToken);
}
private void StampTimestamps()
{
var now = DateTimeOffset.UtcNow;
foreach (var entry in ChangeTracker.Entries<Game>())
{
if (entry.State == EntityState.Added)
{
entry.Entity.CreatedAt = now;
entry.Entity.UpdatedAt = now;
}
else if (entry.State == EntityState.Modified)
{
entry.Entity.UpdatedAt = now;
}
}
}
}
@@ -0,0 +1,367 @@
// <auto-generated />
using System;
using LudosData.Api.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
#nullable disable
namespace LudosData.Api.Data.Migrations
{
[DbContext(typeof(LudosDbContext))]
[Migration("20260803220157_InitialCreate")]
partial class InitialCreate
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "10.0.10");
modelBuilder.Entity("LudosData.Api.Domain.AppUser", b =>
{
b.Property<string>("Id")
.HasColumnType("TEXT");
b.Property<int>("AccessFailedCount")
.HasColumnType("INTEGER");
b.Property<string>("Art")
.HasColumnType("TEXT");
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken()
.HasColumnType("TEXT");
b.Property<DateTimeOffset>("CreatedAt")
.HasColumnType("TEXT");
b.Property<string>("Email")
.HasMaxLength(256)
.HasColumnType("TEXT");
b.Property<bool>("EmailConfirmed")
.HasColumnType("INTEGER");
b.Property<string>("FirstName")
.HasColumnType("TEXT");
b.Property<string>("LastName")
.HasColumnType("TEXT");
b.Property<bool>("LockoutEnabled")
.HasColumnType("INTEGER");
b.Property<DateTimeOffset?>("LockoutEnd")
.HasColumnType("TEXT");
b.Property<string>("NormalizedEmail")
.HasMaxLength(256)
.HasColumnType("TEXT");
b.Property<string>("NormalizedUserName")
.HasMaxLength(256)
.HasColumnType("TEXT");
b.Property<string>("PasswordHash")
.HasColumnType("TEXT");
b.Property<string>("PhoneNumber")
.HasColumnType("TEXT");
b.Property<bool>("PhoneNumberConfirmed")
.HasColumnType("INTEGER");
b.Property<string>("SecurityStamp")
.HasColumnType("TEXT");
b.Property<bool>("TwoFactorEnabled")
.HasColumnType("INTEGER");
b.Property<string>("UserName")
.HasMaxLength(256)
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("NormalizedEmail")
.HasDatabaseName("EmailIndex");
b.HasIndex("NormalizedUserName")
.IsUnique()
.HasDatabaseName("UserNameIndex");
b.ToTable("AspNetUsers", (string)null);
});
modelBuilder.Entity("LudosData.Api.Domain.Game", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("Art")
.HasMaxLength(200)
.HasColumnType("TEXT");
b.Property<DateTimeOffset>("CreatedAt")
.HasColumnType("TEXT");
b.Property<string>("Description")
.HasColumnType("TEXT");
b.Property<string>("Developer")
.HasMaxLength(100)
.HasColumnType("TEXT");
b.Property<bool>("Dumped")
.HasColumnType("INTEGER");
b.Property<bool>("Finished")
.HasColumnType("INTEGER");
b.Property<string>("Genre")
.HasMaxLength(50)
.HasColumnType("TEXT");
b.Property<bool>("Own")
.HasColumnType("INTEGER");
b.Property<string>("OwnerId")
.IsRequired()
.HasColumnType("TEXT");
b.Property<bool>("Played")
.HasColumnType("INTEGER");
b.Property<string>("Publisher")
.HasMaxLength(100)
.HasColumnType("TEXT");
b.Property<string>("System")
.HasMaxLength(50)
.HasColumnType("TEXT");
b.Property<string>("Title")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("TEXT");
b.Property<DateTimeOffset>("UpdatedAt")
.HasColumnType("TEXT");
b.Property<string>("Year")
.HasMaxLength(50)
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("OwnerId", "Genre");
b.HasIndex("OwnerId", "System");
b.HasIndex("OwnerId", "Title");
b.ToTable("Games");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b =>
{
b.Property<string>("Id")
.HasColumnType("TEXT");
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken()
.HasColumnType("TEXT");
b.Property<string>("Name")
.HasMaxLength(256)
.HasColumnType("TEXT");
b.Property<string>("NormalizedName")
.HasMaxLength(256)
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("NormalizedName")
.IsUnique()
.HasDatabaseName("RoleNameIndex");
b.ToTable("AspNetRoles", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("ClaimType")
.HasColumnType("TEXT");
b.Property<string>("ClaimValue")
.HasColumnType("TEXT");
b.Property<string>("RoleId")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("RoleId");
b.ToTable("AspNetRoleClaims", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("ClaimType")
.HasColumnType("TEXT");
b.Property<string>("ClaimValue")
.HasColumnType("TEXT");
b.Property<string>("UserId")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("AspNetUserClaims", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
{
b.Property<string>("LoginProvider")
.HasColumnType("TEXT");
b.Property<string>("ProviderKey")
.HasColumnType("TEXT");
b.Property<string>("ProviderDisplayName")
.HasColumnType("TEXT");
b.Property<string>("UserId")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("LoginProvider", "ProviderKey");
b.HasIndex("UserId");
b.ToTable("AspNetUserLogins", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
{
b.Property<string>("UserId")
.HasColumnType("TEXT");
b.Property<string>("RoleId")
.HasColumnType("TEXT");
b.HasKey("UserId", "RoleId");
b.HasIndex("RoleId");
b.ToTable("AspNetUserRoles", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
{
b.Property<string>("UserId")
.HasColumnType("TEXT");
b.Property<string>("LoginProvider")
.HasColumnType("TEXT");
b.Property<string>("Name")
.HasColumnType("TEXT");
b.Property<string>("Value")
.HasColumnType("TEXT");
b.HasKey("UserId", "LoginProvider", "Name");
b.ToTable("AspNetUserTokens", (string)null);
});
modelBuilder.Entity("LudosData.Api.Domain.Game", b =>
{
b.HasOne("LudosData.Api.Domain.AppUser", "Owner")
.WithMany("Games")
.HasForeignKey("OwnerId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Owner");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
{
b.HasOne("LudosData.Api.Domain.AppUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
{
b.HasOne("LudosData.Api.Domain.AppUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("LudosData.Api.Domain.AppUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
{
b.HasOne("LudosData.Api.Domain.AppUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("LudosData.Api.Domain.AppUser", b =>
{
b.Navigation("Games");
});
#pragma warning restore 612, 618
}
}
}
@@ -0,0 +1,277 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace LudosData.Api.Data.Migrations
{
/// <inheritdoc />
public partial class InitialCreate : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "AspNetRoles",
columns: table => new
{
Id = table.Column<string>(type: "TEXT", nullable: false),
Name = table.Column<string>(type: "TEXT", maxLength: 256, nullable: true),
NormalizedName = table.Column<string>(type: "TEXT", maxLength: 256, nullable: true),
ConcurrencyStamp = table.Column<string>(type: "TEXT", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_AspNetRoles", x => x.Id);
});
migrationBuilder.CreateTable(
name: "AspNetUsers",
columns: table => new
{
Id = table.Column<string>(type: "TEXT", nullable: false),
FirstName = table.Column<string>(type: "TEXT", nullable: true),
LastName = table.Column<string>(type: "TEXT", nullable: true),
Art = table.Column<string>(type: "TEXT", nullable: true),
CreatedAt = table.Column<DateTimeOffset>(type: "TEXT", nullable: false),
UserName = table.Column<string>(type: "TEXT", maxLength: 256, nullable: true),
NormalizedUserName = table.Column<string>(type: "TEXT", maxLength: 256, nullable: true),
Email = table.Column<string>(type: "TEXT", maxLength: 256, nullable: true),
NormalizedEmail = table.Column<string>(type: "TEXT", maxLength: 256, nullable: true),
EmailConfirmed = table.Column<bool>(type: "INTEGER", nullable: false),
PasswordHash = table.Column<string>(type: "TEXT", nullable: true),
SecurityStamp = table.Column<string>(type: "TEXT", nullable: true),
ConcurrencyStamp = table.Column<string>(type: "TEXT", nullable: true),
PhoneNumber = table.Column<string>(type: "TEXT", nullable: true),
PhoneNumberConfirmed = table.Column<bool>(type: "INTEGER", nullable: false),
TwoFactorEnabled = table.Column<bool>(type: "INTEGER", nullable: false),
LockoutEnd = table.Column<DateTimeOffset>(type: "TEXT", nullable: true),
LockoutEnabled = table.Column<bool>(type: "INTEGER", nullable: false),
AccessFailedCount = table.Column<int>(type: "INTEGER", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_AspNetUsers", x => x.Id);
});
migrationBuilder.CreateTable(
name: "AspNetRoleClaims",
columns: table => new
{
Id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
RoleId = table.Column<string>(type: "TEXT", nullable: false),
ClaimType = table.Column<string>(type: "TEXT", nullable: true),
ClaimValue = table.Column<string>(type: "TEXT", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_AspNetRoleClaims", x => x.Id);
table.ForeignKey(
name: "FK_AspNetRoleClaims_AspNetRoles_RoleId",
column: x => x.RoleId,
principalTable: "AspNetRoles",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "AspNetUserClaims",
columns: table => new
{
Id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
UserId = table.Column<string>(type: "TEXT", nullable: false),
ClaimType = table.Column<string>(type: "TEXT", nullable: true),
ClaimValue = table.Column<string>(type: "TEXT", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_AspNetUserClaims", x => x.Id);
table.ForeignKey(
name: "FK_AspNetUserClaims_AspNetUsers_UserId",
column: x => x.UserId,
principalTable: "AspNetUsers",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "AspNetUserLogins",
columns: table => new
{
LoginProvider = table.Column<string>(type: "TEXT", nullable: false),
ProviderKey = table.Column<string>(type: "TEXT", nullable: false),
ProviderDisplayName = table.Column<string>(type: "TEXT", nullable: true),
UserId = table.Column<string>(type: "TEXT", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_AspNetUserLogins", x => new { x.LoginProvider, x.ProviderKey });
table.ForeignKey(
name: "FK_AspNetUserLogins_AspNetUsers_UserId",
column: x => x.UserId,
principalTable: "AspNetUsers",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "AspNetUserRoles",
columns: table => new
{
UserId = table.Column<string>(type: "TEXT", nullable: false),
RoleId = table.Column<string>(type: "TEXT", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_AspNetUserRoles", x => new { x.UserId, x.RoleId });
table.ForeignKey(
name: "FK_AspNetUserRoles_AspNetRoles_RoleId",
column: x => x.RoleId,
principalTable: "AspNetRoles",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_AspNetUserRoles_AspNetUsers_UserId",
column: x => x.UserId,
principalTable: "AspNetUsers",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "AspNetUserTokens",
columns: table => new
{
UserId = table.Column<string>(type: "TEXT", nullable: false),
LoginProvider = table.Column<string>(type: "TEXT", nullable: false),
Name = table.Column<string>(type: "TEXT", nullable: false),
Value = table.Column<string>(type: "TEXT", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_AspNetUserTokens", x => new { x.UserId, x.LoginProvider, x.Name });
table.ForeignKey(
name: "FK_AspNetUserTokens_AspNetUsers_UserId",
column: x => x.UserId,
principalTable: "AspNetUsers",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "Games",
columns: table => new
{
Id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
Title = table.Column<string>(type: "TEXT", maxLength: 200, nullable: false),
System = table.Column<string>(type: "TEXT", maxLength: 50, nullable: true),
Genre = table.Column<string>(type: "TEXT", maxLength: 50, nullable: true),
Year = table.Column<string>(type: "TEXT", maxLength: 50, nullable: true),
Developer = table.Column<string>(type: "TEXT", maxLength: 100, nullable: true),
Publisher = table.Column<string>(type: "TEXT", maxLength: 100, nullable: true),
Art = table.Column<string>(type: "TEXT", maxLength: 200, nullable: true),
Description = table.Column<string>(type: "TEXT", nullable: true),
Own = table.Column<bool>(type: "INTEGER", nullable: false),
Dumped = table.Column<bool>(type: "INTEGER", nullable: false),
Played = table.Column<bool>(type: "INTEGER", nullable: false),
Finished = table.Column<bool>(type: "INTEGER", nullable: false),
OwnerId = table.Column<string>(type: "TEXT", nullable: false),
CreatedAt = table.Column<DateTimeOffset>(type: "TEXT", nullable: false),
UpdatedAt = table.Column<DateTimeOffset>(type: "TEXT", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Games", x => x.Id);
table.ForeignKey(
name: "FK_Games_AspNetUsers_OwnerId",
column: x => x.OwnerId,
principalTable: "AspNetUsers",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_AspNetRoleClaims_RoleId",
table: "AspNetRoleClaims",
column: "RoleId");
migrationBuilder.CreateIndex(
name: "RoleNameIndex",
table: "AspNetRoles",
column: "NormalizedName",
unique: true);
migrationBuilder.CreateIndex(
name: "IX_AspNetUserClaims_UserId",
table: "AspNetUserClaims",
column: "UserId");
migrationBuilder.CreateIndex(
name: "IX_AspNetUserLogins_UserId",
table: "AspNetUserLogins",
column: "UserId");
migrationBuilder.CreateIndex(
name: "IX_AspNetUserRoles_RoleId",
table: "AspNetUserRoles",
column: "RoleId");
migrationBuilder.CreateIndex(
name: "EmailIndex",
table: "AspNetUsers",
column: "NormalizedEmail");
migrationBuilder.CreateIndex(
name: "UserNameIndex",
table: "AspNetUsers",
column: "NormalizedUserName",
unique: true);
migrationBuilder.CreateIndex(
name: "IX_Games_OwnerId_Genre",
table: "Games",
columns: new[] { "OwnerId", "Genre" });
migrationBuilder.CreateIndex(
name: "IX_Games_OwnerId_System",
table: "Games",
columns: new[] { "OwnerId", "System" });
migrationBuilder.CreateIndex(
name: "IX_Games_OwnerId_Title",
table: "Games",
columns: new[] { "OwnerId", "Title" });
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "AspNetRoleClaims");
migrationBuilder.DropTable(
name: "AspNetUserClaims");
migrationBuilder.DropTable(
name: "AspNetUserLogins");
migrationBuilder.DropTable(
name: "AspNetUserRoles");
migrationBuilder.DropTable(
name: "AspNetUserTokens");
migrationBuilder.DropTable(
name: "Games");
migrationBuilder.DropTable(
name: "AspNetRoles");
migrationBuilder.DropTable(
name: "AspNetUsers");
}
}
}
@@ -0,0 +1,364 @@
// <auto-generated />
using System;
using LudosData.Api.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
#nullable disable
namespace LudosData.Api.Data.Migrations
{
[DbContext(typeof(LudosDbContext))]
partial class LudosDbContextModelSnapshot : ModelSnapshot
{
protected override void BuildModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "10.0.10");
modelBuilder.Entity("LudosData.Api.Domain.AppUser", b =>
{
b.Property<string>("Id")
.HasColumnType("TEXT");
b.Property<int>("AccessFailedCount")
.HasColumnType("INTEGER");
b.Property<string>("Art")
.HasColumnType("TEXT");
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken()
.HasColumnType("TEXT");
b.Property<DateTimeOffset>("CreatedAt")
.HasColumnType("TEXT");
b.Property<string>("Email")
.HasMaxLength(256)
.HasColumnType("TEXT");
b.Property<bool>("EmailConfirmed")
.HasColumnType("INTEGER");
b.Property<string>("FirstName")
.HasColumnType("TEXT");
b.Property<string>("LastName")
.HasColumnType("TEXT");
b.Property<bool>("LockoutEnabled")
.HasColumnType("INTEGER");
b.Property<DateTimeOffset?>("LockoutEnd")
.HasColumnType("TEXT");
b.Property<string>("NormalizedEmail")
.HasMaxLength(256)
.HasColumnType("TEXT");
b.Property<string>("NormalizedUserName")
.HasMaxLength(256)
.HasColumnType("TEXT");
b.Property<string>("PasswordHash")
.HasColumnType("TEXT");
b.Property<string>("PhoneNumber")
.HasColumnType("TEXT");
b.Property<bool>("PhoneNumberConfirmed")
.HasColumnType("INTEGER");
b.Property<string>("SecurityStamp")
.HasColumnType("TEXT");
b.Property<bool>("TwoFactorEnabled")
.HasColumnType("INTEGER");
b.Property<string>("UserName")
.HasMaxLength(256)
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("NormalizedEmail")
.HasDatabaseName("EmailIndex");
b.HasIndex("NormalizedUserName")
.IsUnique()
.HasDatabaseName("UserNameIndex");
b.ToTable("AspNetUsers", (string)null);
});
modelBuilder.Entity("LudosData.Api.Domain.Game", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("Art")
.HasMaxLength(200)
.HasColumnType("TEXT");
b.Property<DateTimeOffset>("CreatedAt")
.HasColumnType("TEXT");
b.Property<string>("Description")
.HasColumnType("TEXT");
b.Property<string>("Developer")
.HasMaxLength(100)
.HasColumnType("TEXT");
b.Property<bool>("Dumped")
.HasColumnType("INTEGER");
b.Property<bool>("Finished")
.HasColumnType("INTEGER");
b.Property<string>("Genre")
.HasMaxLength(50)
.HasColumnType("TEXT");
b.Property<bool>("Own")
.HasColumnType("INTEGER");
b.Property<string>("OwnerId")
.IsRequired()
.HasColumnType("TEXT");
b.Property<bool>("Played")
.HasColumnType("INTEGER");
b.Property<string>("Publisher")
.HasMaxLength(100)
.HasColumnType("TEXT");
b.Property<string>("System")
.HasMaxLength(50)
.HasColumnType("TEXT");
b.Property<string>("Title")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("TEXT");
b.Property<DateTimeOffset>("UpdatedAt")
.HasColumnType("TEXT");
b.Property<string>("Year")
.HasMaxLength(50)
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("OwnerId", "Genre");
b.HasIndex("OwnerId", "System");
b.HasIndex("OwnerId", "Title");
b.ToTable("Games");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b =>
{
b.Property<string>("Id")
.HasColumnType("TEXT");
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken()
.HasColumnType("TEXT");
b.Property<string>("Name")
.HasMaxLength(256)
.HasColumnType("TEXT");
b.Property<string>("NormalizedName")
.HasMaxLength(256)
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("NormalizedName")
.IsUnique()
.HasDatabaseName("RoleNameIndex");
b.ToTable("AspNetRoles", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("ClaimType")
.HasColumnType("TEXT");
b.Property<string>("ClaimValue")
.HasColumnType("TEXT");
b.Property<string>("RoleId")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("RoleId");
b.ToTable("AspNetRoleClaims", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("ClaimType")
.HasColumnType("TEXT");
b.Property<string>("ClaimValue")
.HasColumnType("TEXT");
b.Property<string>("UserId")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("AspNetUserClaims", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
{
b.Property<string>("LoginProvider")
.HasColumnType("TEXT");
b.Property<string>("ProviderKey")
.HasColumnType("TEXT");
b.Property<string>("ProviderDisplayName")
.HasColumnType("TEXT");
b.Property<string>("UserId")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("LoginProvider", "ProviderKey");
b.HasIndex("UserId");
b.ToTable("AspNetUserLogins", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
{
b.Property<string>("UserId")
.HasColumnType("TEXT");
b.Property<string>("RoleId")
.HasColumnType("TEXT");
b.HasKey("UserId", "RoleId");
b.HasIndex("RoleId");
b.ToTable("AspNetUserRoles", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
{
b.Property<string>("UserId")
.HasColumnType("TEXT");
b.Property<string>("LoginProvider")
.HasColumnType("TEXT");
b.Property<string>("Name")
.HasColumnType("TEXT");
b.Property<string>("Value")
.HasColumnType("TEXT");
b.HasKey("UserId", "LoginProvider", "Name");
b.ToTable("AspNetUserTokens", (string)null);
});
modelBuilder.Entity("LudosData.Api.Domain.Game", b =>
{
b.HasOne("LudosData.Api.Domain.AppUser", "Owner")
.WithMany("Games")
.HasForeignKey("OwnerId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Owner");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
{
b.HasOne("LudosData.Api.Domain.AppUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
{
b.HasOne("LudosData.Api.Domain.AppUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("LudosData.Api.Domain.AppUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
{
b.HasOne("LudosData.Api.Domain.AppUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("LudosData.Api.Domain.AppUser", b =>
{
b.Navigation("Games");
});
#pragma warning restore 612, 618
}
}
}
File diff suppressed because it is too large Load Diff