diff --git a/Wordle.Api/Wordle.Api/Identity/IdentitySeed.cs b/Wordle.Api/Wordle.Api/Identity/IdentitySeed.cs new file mode 100644 index 00000000..c334eb7f --- /dev/null +++ b/Wordle.Api/Wordle.Api/Identity/IdentitySeed.cs @@ -0,0 +1,47 @@ +using Microsoft.AspNetCore.Identity; +using System.Data; +using System.Security.Claims; +using System; +using Wordle.Api.Models; + +namespace Wordle.Api.Identity; +public static class IdentitySeed +{ + public static async Task SeedAsync(UserManager userManager, RoleManager roleManager, WordleDbContext db) + { + // Seed Roles + await SeedRolesAsync(roleManager); + + // Seed Admin User + await SeedAdminUserAsync(userManager); + } + + private static async Task SeedRolesAsync(RoleManager roleManager) + { + // Seed Roles + if (!await roleManager.RoleExistsAsync(Roles.Admin)) + { + await roleManager.CreateAsync(new IdentityRole(Roles.Admin)); + } + } + + private static async Task SeedAdminUserAsync(UserManager userManager) + { + // Seed Admin User + if (await userManager.FindByEmailAsync("Admin@intellitect.com") == null) + { + AppUser user = new AppUser + { + UserName = "Admin@intellitect.com", + Email = "Admin@intellitect.com" + }; + + IdentityResult result = userManager.CreateAsync(user, "P@ssw0rd123").Result; + + if (result.Succeeded) + { + await userManager.AddToRoleAsync(user, Roles.Admin); + } + } + } +} \ No newline at end of file diff --git a/Wordle.Api/Wordle.Api/Identity/JwtConfiguration.cs b/Wordle.Api/Wordle.Api/Identity/JwtConfiguration.cs new file mode 100644 index 00000000..17597442 --- /dev/null +++ b/Wordle.Api/Wordle.Api/Identity/JwtConfiguration.cs @@ -0,0 +1,8 @@ +namespace Wordle.Api.Identity; +public class JwtConfiguration +{ + public required string Secret { get; set; } + public required string Issuer { get; set; } + public required string Audience { get; set; } + public int ExpirationInMinutes { get; set; } = 1440; +} diff --git a/Wordle.Api/Wordle.Api/Identity/Roles.cs b/Wordle.Api/Wordle.Api/Identity/Roles.cs new file mode 100644 index 00000000..6ca22e9f --- /dev/null +++ b/Wordle.Api/Wordle.Api/Identity/Roles.cs @@ -0,0 +1,5 @@ +namespace Wordle.Api.Identity; +public static class Roles +{ + public const string Admin = "Admin"; +} \ No newline at end of file diff --git a/Wordle.Api/Wordle.Api/Migrations/20240516225055_IdentityStuff.Designer.cs b/Wordle.Api/Wordle.Api/Migrations/20240516225055_IdentityStuff.Designer.cs new file mode 100644 index 00000000..8fccc3e8 --- /dev/null +++ b/Wordle.Api/Wordle.Api/Migrations/20240516225055_IdentityStuff.Designer.cs @@ -0,0 +1,398 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Wordle.Api.Models; + +#nullable disable + +namespace Wordle.Api.Migrations +{ + [DbContext(typeof(WordleDbContext))] + [Migration("20240516225055_IdentityStuff")] + partial class IdentityStuff + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.4") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => + { + b.Property("Id") + .HasColumnType("nvarchar(450)"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex") + .HasFilter("[NormalizedName] IS NOT NULL"); + + b.ToTable("AspNetRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("nvarchar(max)"); + + b.Property("ClaimValue") + .HasColumnType("nvarchar(max)"); + + b.Property("RoleId") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("nvarchar(max)"); + + b.Property("ClaimValue") + .HasColumnType("nvarchar(max)"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("nvarchar(450)"); + + b.Property("ProviderKey") + .HasColumnType("nvarchar(450)"); + + b.Property("ProviderDisplayName") + .HasColumnType("nvarchar(max)"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("nvarchar(450)"); + + b.Property("RoleId") + .HasColumnType("nvarchar(450)"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("nvarchar(450)"); + + b.Property("LoginProvider") + .HasColumnType("nvarchar(450)"); + + b.Property("Name") + .HasColumnType("nvarchar(450)"); + + b.Property("Value") + .HasColumnType("nvarchar(max)"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens", (string)null); + }); + + modelBuilder.Entity("Wordle.Api.Models.AppUser", b => + { + b.Property("Id") + .HasColumnType("nvarchar(450)"); + + b.Property("AccessFailedCount") + .HasColumnType("int"); + + b.Property("AverageAttempts") + .HasColumnType("float"); + + b.Property("AverageSecondsPerGame") + .HasColumnType("int"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("nvarchar(max)"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("EmailConfirmed") + .HasColumnType("bit"); + + b.Property("GameCount") + .HasColumnType("int"); + + b.Property("LockoutEnabled") + .HasColumnType("bit"); + + b.Property("LockoutEnd") + .HasColumnType("datetimeoffset"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("PasswordHash") + .HasColumnType("nvarchar(max)"); + + b.Property("PhoneNumber") + .HasColumnType("nvarchar(max)"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("bit"); + + b.Property("SecurityStamp") + .HasColumnType("nvarchar(max)"); + + b.Property("TwoFactorEnabled") + .HasColumnType("bit"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex") + .HasFilter("[NormalizedUserName] IS NOT NULL"); + + b.ToTable("AspNetUsers", (string)null); + }); + + modelBuilder.Entity("Wordle.Api.Models.Game", b => + { + b.Property("GameId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("GameId")); + + b.Property("Attempts") + .HasColumnType("int"); + + b.Property("DateAttempted") + .HasColumnType("datetime2"); + + b.Property("IsWin") + .HasColumnType("bit"); + + b.Property("WordId") + .HasColumnType("int"); + + b.Property("WordOfTheDayId") + .HasColumnType("int"); + + b.HasKey("GameId"); + + b.HasIndex("WordId"); + + b.HasIndex("WordOfTheDayId"); + + b.ToTable("Games"); + }); + + modelBuilder.Entity("Wordle.Api.Models.Word", b => + { + b.Property("WordId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("WordId")); + + b.Property("Text") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("WordId"); + + b.ToTable("Words"); + }); + + modelBuilder.Entity("Wordle.Api.Models.WordOfTheDay", b => + { + b.Property("WordOfTheDayId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("WordOfTheDayId")); + + b.Property("Date") + .HasColumnType("date"); + + b.Property("WordId") + .HasColumnType("int"); + + b.HasKey("WordOfTheDayId"); + + b.HasIndex("WordId"); + + b.ToTable("WordOfTheDay"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("Wordle.Api.Models.AppUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("Wordle.Api.Models.AppUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Wordle.Api.Models.AppUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("Wordle.Api.Models.AppUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Wordle.Api.Models.Game", b => + { + b.HasOne("Wordle.Api.Models.Word", "Word") + .WithMany("Games") + .HasForeignKey("WordId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Wordle.Api.Models.WordOfTheDay", "WordOfTheDay") + .WithMany("Games") + .HasForeignKey("WordOfTheDayId"); + + b.Navigation("Word"); + + b.Navigation("WordOfTheDay"); + }); + + modelBuilder.Entity("Wordle.Api.Models.WordOfTheDay", b => + { + b.HasOne("Wordle.Api.Models.Word", "Word") + .WithMany("WordsOfTheDays") + .HasForeignKey("WordId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Word"); + }); + + modelBuilder.Entity("Wordle.Api.Models.Word", b => + { + b.Navigation("Games"); + + b.Navigation("WordsOfTheDays"); + }); + + modelBuilder.Entity("Wordle.Api.Models.WordOfTheDay", b => + { + b.Navigation("Games"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Wordle.Api/Wordle.Api/Migrations/20240516225055_IdentityStuff.cs b/Wordle.Api/Wordle.Api/Migrations/20240516225055_IdentityStuff.cs new file mode 100644 index 00000000..e683b849 --- /dev/null +++ b/Wordle.Api/Wordle.Api/Migrations/20240516225055_IdentityStuff.cs @@ -0,0 +1,227 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Wordle.Api.Migrations +{ + /// + public partial class IdentityStuff : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "AspNetRoles", + columns: table => new + { + Id = table.Column(type: "nvarchar(450)", nullable: false), + Name = table.Column(type: "nvarchar(256)", maxLength: 256, nullable: true), + NormalizedName = table.Column(type: "nvarchar(256)", maxLength: 256, nullable: true), + ConcurrencyStamp = table.Column(type: "nvarchar(max)", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetRoles", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "AspNetUsers", + columns: table => new + { + Id = table.Column(type: "nvarchar(450)", nullable: false), + GameCount = table.Column(type: "int", nullable: false), + AverageAttempts = table.Column(type: "float", nullable: false), + AverageSecondsPerGame = table.Column(type: "int", nullable: false), + UserName = table.Column(type: "nvarchar(256)", maxLength: 256, nullable: true), + NormalizedUserName = table.Column(type: "nvarchar(256)", maxLength: 256, nullable: true), + Email = table.Column(type: "nvarchar(256)", maxLength: 256, nullable: true), + NormalizedEmail = table.Column(type: "nvarchar(256)", maxLength: 256, nullable: true), + EmailConfirmed = table.Column(type: "bit", nullable: false), + PasswordHash = table.Column(type: "nvarchar(max)", nullable: true), + SecurityStamp = table.Column(type: "nvarchar(max)", nullable: true), + ConcurrencyStamp = table.Column(type: "nvarchar(max)", nullable: true), + PhoneNumber = table.Column(type: "nvarchar(max)", nullable: true), + PhoneNumberConfirmed = table.Column(type: "bit", nullable: false), + TwoFactorEnabled = table.Column(type: "bit", nullable: false), + LockoutEnd = table.Column(type: "datetimeoffset", nullable: true), + LockoutEnabled = table.Column(type: "bit", nullable: false), + AccessFailedCount = table.Column(type: "int", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetUsers", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "AspNetRoleClaims", + columns: table => new + { + Id = table.Column(type: "int", nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + RoleId = table.Column(type: "nvarchar(450)", nullable: false), + ClaimType = table.Column(type: "nvarchar(max)", nullable: true), + ClaimValue = table.Column(type: "nvarchar(max)", 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(type: "int", nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + UserId = table.Column(type: "nvarchar(450)", nullable: false), + ClaimType = table.Column(type: "nvarchar(max)", nullable: true), + ClaimValue = table.Column(type: "nvarchar(max)", 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(type: "nvarchar(450)", nullable: false), + ProviderKey = table.Column(type: "nvarchar(450)", nullable: false), + ProviderDisplayName = table.Column(type: "nvarchar(max)", nullable: true), + UserId = table.Column(type: "nvarchar(450)", 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(type: "nvarchar(450)", nullable: false), + RoleId = table.Column(type: "nvarchar(450)", 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(type: "nvarchar(450)", nullable: false), + LoginProvider = table.Column(type: "nvarchar(450)", nullable: false), + Name = table.Column(type: "nvarchar(450)", nullable: false), + Value = table.Column(type: "nvarchar(max)", 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.CreateIndex( + name: "IX_AspNetRoleClaims_RoleId", + table: "AspNetRoleClaims", + column: "RoleId"); + + migrationBuilder.CreateIndex( + name: "RoleNameIndex", + table: "AspNetRoles", + column: "NormalizedName", + unique: true, + filter: "[NormalizedName] IS NOT NULL"); + + 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, + filter: "[NormalizedUserName] IS NOT NULL"); + } + + /// + 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: "AspNetRoles"); + + migrationBuilder.DropTable( + name: "AspNetUsers"); + } + } +} diff --git a/Wordle.Api/Wordle.Api/Migrations/WordleDbContextModelSnapshot.cs b/Wordle.Api/Wordle.Api/Migrations/WordleDbContextModelSnapshot.cs index d9dcec01..d87a2006 100644 --- a/Wordle.Api/Wordle.Api/Migrations/WordleDbContextModelSnapshot.cs +++ b/Wordle.Api/Wordle.Api/Migrations/WordleDbContextModelSnapshot.cs @@ -22,6 +22,213 @@ protected override void BuildModel(ModelBuilder modelBuilder) SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => + { + b.Property("Id") + .HasColumnType("nvarchar(450)"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex") + .HasFilter("[NormalizedName] IS NOT NULL"); + + b.ToTable("AspNetRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("nvarchar(max)"); + + b.Property("ClaimValue") + .HasColumnType("nvarchar(max)"); + + b.Property("RoleId") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("nvarchar(max)"); + + b.Property("ClaimValue") + .HasColumnType("nvarchar(max)"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("nvarchar(450)"); + + b.Property("ProviderKey") + .HasColumnType("nvarchar(450)"); + + b.Property("ProviderDisplayName") + .HasColumnType("nvarchar(max)"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("nvarchar(450)"); + + b.Property("RoleId") + .HasColumnType("nvarchar(450)"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("nvarchar(450)"); + + b.Property("LoginProvider") + .HasColumnType("nvarchar(450)"); + + b.Property("Name") + .HasColumnType("nvarchar(450)"); + + b.Property("Value") + .HasColumnType("nvarchar(max)"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens", (string)null); + }); + + modelBuilder.Entity("Wordle.Api.Models.AppUser", b => + { + b.Property("Id") + .HasColumnType("nvarchar(450)"); + + b.Property("AccessFailedCount") + .HasColumnType("int"); + + b.Property("AverageAttempts") + .HasColumnType("float"); + + b.Property("AverageSecondsPerGame") + .HasColumnType("int"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("nvarchar(max)"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("EmailConfirmed") + .HasColumnType("bit"); + + b.Property("GameCount") + .HasColumnType("int"); + + b.Property("LockoutEnabled") + .HasColumnType("bit"); + + b.Property("LockoutEnd") + .HasColumnType("datetimeoffset"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("PasswordHash") + .HasColumnType("nvarchar(max)"); + + b.Property("PhoneNumber") + .HasColumnType("nvarchar(max)"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("bit"); + + b.Property("SecurityStamp") + .HasColumnType("nvarchar(max)"); + + b.Property("TwoFactorEnabled") + .HasColumnType("bit"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex") + .HasFilter("[NormalizedUserName] IS NOT NULL"); + + b.ToTable("AspNetUsers", (string)null); + }); + modelBuilder.Entity("Wordle.Api.Models.Game", b => { b.Property("GameId") @@ -92,6 +299,57 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("WordOfTheDay"); }); + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("Wordle.Api.Models.AppUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("Wordle.Api.Models.AppUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Wordle.Api.Models.AppUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("Wordle.Api.Models.AppUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + modelBuilder.Entity("Wordle.Api.Models.Game", b => { b.HasOne("Wordle.Api.Models.Word", "Word") diff --git a/Wordle.Api/Wordle.Api/Models/AppUser.cs b/Wordle.Api/Wordle.Api/Models/AppUser.cs new file mode 100644 index 00000000..74fde1f1 --- /dev/null +++ b/Wordle.Api/Wordle.Api/Models/AppUser.cs @@ -0,0 +1,13 @@ +using Microsoft.AspNetCore.Identity; + +namespace Wordle.Api.Models +{ + public class AppUser : IdentityUser + { + public int GameCount { get; set; } + + public double AverageAttempts { get; set; } + + public int AverageSecondsPerGame { get; set; } + } +} diff --git a/Wordle.Api/Wordle.Api/Models/WordleDbContext.cs b/Wordle.Api/Wordle.Api/Models/WordleDbContext.cs index 8351ece9..b7116b5d 100644 --- a/Wordle.Api/Wordle.Api/Models/WordleDbContext.cs +++ b/Wordle.Api/Wordle.Api/Models/WordleDbContext.cs @@ -1,9 +1,10 @@ -using Microsoft.EntityFrameworkCore; +using Microsoft.AspNetCore.Identity.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore; namespace Wordle.Api.Models { - public class WordleDbContext:DbContext + public class WordleDbContext : IdentityDbContext { public DbSet WordsOfTheDays { get; set; } public DbSet Games { get; set; } @@ -13,5 +14,11 @@ public WordleDbContext(DbContextOptions options) : base(options) { } + + // Make sure the identity db ocntext on model creating is called + protected override void OnModelCreating(ModelBuilder builder) + { + base.OnModelCreating(builder); + } } } diff --git a/Wordle.Api/Wordle.Api/Program.cs b/Wordle.Api/Wordle.Api/Program.cs index def036a0..12e1efdc 100644 --- a/Wordle.Api/Wordle.Api/Program.cs +++ b/Wordle.Api/Wordle.Api/Program.cs @@ -1,7 +1,11 @@ +using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Identity; using Microsoft.EntityFrameworkCore; +using Microsoft.IdentityModel.Tokens; using System; +using System.Text; using Wordle.Api; +using Wordle.Api.Identity; using Wordle.Api.Models; using Wordle.Api.Services; @@ -33,6 +37,31 @@ builder.Services.AddScoped(); builder.Services.AddScoped(); +// Identity Services +builder.Services.AddIdentityCore(options => options.SignIn.RequireConfirmedAccount = false) + .AddRoles() + .AddEntityFrameworkStores(); // Tell identity where to sstore things + +// JWT Token Setup +JwtConfiguration jwtConfig = builder.Configuration + .GetSection("Jwt").Get() ?? throw new InvalidOperationException("JWT config not specified"); +builder.Services.AddSingleton(jwtConfig); + +builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) + .AddJwtBearer(options => + options.TokenValidationParameters = new TokenValidationParameters + { + ValidateIssuer = true, + ValidateAudience = true, + ValidateLifetime = true, + ValidateIssuerSigningKey = true, + ValidIssuer = jwtConfig.Issuer, + ValidAudience = jwtConfig.Audience, + + IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtConfig.Secret)) + } + ); + var app = builder.Build(); using (var scope = app.Services.CreateScope()) @@ -40,6 +69,10 @@ var db = scope.ServiceProvider.GetRequiredService(); db.Database.Migrate(); await Seeder.Seed(db); + await IdentitySeed.SeedAsync( + scope.ServiceProvider.GetRequiredService>(), + scope.ServiceProvider.GetRequiredService>(), + db); } // Configure the HTTP request pipeline. diff --git a/Wordle.Api/Wordle.Api/Wordle.Api.csproj b/Wordle.Api/Wordle.Api/Wordle.Api.csproj index 9a7d25da..5355197d 100644 --- a/Wordle.Api/Wordle.Api/Wordle.Api.csproj +++ b/Wordle.Api/Wordle.Api/Wordle.Api.csproj @@ -4,9 +4,12 @@ net8.0 enable enable + 9bb8bd8f-d074-4bf0-b5d9-d353207e1467 + + all diff --git a/Wordle.Api/Wordle.Api/appsettings.json b/Wordle.Api/Wordle.Api/appsettings.json index e73eedc3..02d17649 100644 --- a/Wordle.Api/Wordle.Api/appsettings.json +++ b/Wordle.Api/Wordle.Api/appsettings.json @@ -8,5 +8,11 @@ "Microsoft.AspNetCore": "Warning" } }, + "Jwt": { + "Secret": "EWU-CSCD379-2024-Super-Duper-Secret-JWT-Token", + "Issuer": "wordleapi.azurewebsite.net", + "Audience": "Wordle.Web", + "ExpirationInMinutes": 60 + }, "AllowedHosts": "*" }