turf-tasker/auth.txt
Blake Ridgway b24beb3154 feat: Integrate ASP.NET Core Identity for user authentication
This commit integrates ASP.NET Core Identity into the application to enable user registration, login, and management. This lays the groundwork for securing data per user.

**Key Changes:**

*   **DbContext Configuration:**
    *   Modified `ApplicationDbContext.cs` to inherit from `IdentityDbContext<IdentityUser>`.
    *   Removed an unnecessary `using` statement from `ApplicationDbContext.cs`.

*   **Program.cs Setup:**
    *   Configured `AddDefaultIdentity<IdentityUser>` with `AddEntityFrameworkStores<ApplicationDbContext>()` to register Identity services.
    *   Ensured correct ordering of `UseAuthentication()` and `UseAuthorization()` middleware.
    *   Added `app.MapRazorPages()` to enable the Identity UI pages.
    *   Verified core package versions in `turf_tasker.csproj` for consistency across EF Core and Identity components (`8.0.6`).

*   **Identity UI:**
    *   Scaffolded ASP.NET Core Identity pages (Login, Register, Manage, etc.) to provide the user interface for authentication.
    *   Added a `_LoginPartial.cshtml` partial view to the `Views/Shared` folder.
    *   Rendered `_LoginPartial` in `Views/Shared/_Layout.cshtml` to display login/register/logout links in the navigation bar.

*   **Migrations:**
    *   Created and applied a new migration (`AddIdentitySchema`) to create the necessary ASP.NET Core Identity database tables (e.g., `AspNetUsers`, `AspNetRoles`).
2025-06-21 18:10:25 -05:00

417 lines
15 KiB
Text

diff --git a/Data/ApplicationDbContext.cs b/Data/ApplicationDbContext.cs
index e919f1a..0f29688 100644
--- a/Data/ApplicationDbContext.cs
+++ b/Data/ApplicationDbContext.cs
@@ -1,12 +1,20 @@
+using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
+using Microsoft.AspNetCore.Identity;
+using Microsoft.AspNetCore.Identity.UI;
using Microsoft.EntityFrameworkCore;
using turf_tasker.Models;
namespace turf_tasker.Data;
-public class ApplicationDbContext : DbContext
+public class ApplicationDbContext : IdentityDbContext<IdentityUser>
{
+ private readonly DbContextOptions<ApplicationDbContext> _options;
+
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
- : base(options) { }
+ : base(options)
+ {
+ _options = options;
+ }
public DbSet<LawnCareEvent> LawnCareEvents { get; set; }
diff --git a/Migrations/ApplicationDbContextModelSnapshot.cs b/Migrations/ApplicationDbContextModelSnapshot.cs
index 320c9f9..c57d1dc 100644
--- a/Migrations/ApplicationDbContextModelSnapshot.cs
+++ b/Migrations/ApplicationDbContextModelSnapshot.cs
@@ -15,7 +15,203 @@ namespace turf_tasker.Migrations
protected override void BuildModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
- modelBuilder.HasAnnotation("ProductVersion", "9.0.6");
+ modelBuilder.HasAnnotation("ProductVersion", "8.0.6");
+
+ 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.IdentityUser", b =>
+ {
+ b.Property<string>("Id")
+ .HasColumnType("TEXT");
+
+ b.Property<int>("AccessFailedCount")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("ConcurrencyStamp")
+ .IsConcurrencyToken()
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Email")
+ .HasMaxLength(256)
+ .HasColumnType("TEXT");
+
+ b.Property<bool>("EmailConfirmed")
+ .HasColumnType("INTEGER");
+
+ 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("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")
+ .HasMaxLength(128)
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ProviderKey")
+ .HasMaxLength(128)
+ .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")
+ .HasMaxLength(128)
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Name")
+ .HasMaxLength(128)
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Value")
+ .HasColumnType("TEXT");
+
+ b.HasKey("UserId", "LoginProvider", "Name");
+
+ b.ToTable("AspNetUserTokens", (string)null);
+ });
modelBuilder.Entity("turf_tasker.Models.LawnCareEvent", b =>
{
@@ -77,6 +273,57 @@ namespace turf_tasker.Migrations
b.ToTable("LawnCareTips");
});
+
+ 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("Microsoft.AspNetCore.Identity.IdentityUser", null)
+ .WithMany()
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
+ {
+ b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", 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("Microsoft.AspNetCore.Identity.IdentityUser", null)
+ .WithMany()
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
+ {
+ b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null)
+ .WithMany()
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
#pragma warning restore 612, 618
}
}
diff --git a/Program.cs b/Program.cs
index dcf9de2..701e4e2 100644
--- a/Program.cs
+++ b/Program.cs
@@ -1,20 +1,22 @@
using Microsoft.EntityFrameworkCore;
using turf_tasker.Data;
+using Microsoft.AspNetCore.Identity;
var builder = WebApplication.CreateBuilder(args);
-// Add services to the container.
builder.Services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlite(
builder.Configuration.GetConnectionString("DefaultConnection")
)
);
+builder.Services.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = true)
+ .AddEntityFrameworkStores<ApplicationDbContext>();
+
builder.Services.AddControllersWithViews();
var app = builder.Build();
-// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Home/Error");
@@ -26,6 +28,7 @@ app.UseStaticFiles();
app.UseRouting();
+app.UseAuthentication();
app.UseAuthorization();
app.MapControllerRoute(
@@ -33,4 +36,6 @@ app.MapControllerRoute(
pattern: "{controller=Home}/{action=Index}/{id?}"
);
+app.MapRazorPages();
+
app.Run();
\ No newline at end of file
diff --git a/Views/Shared/_Layout.cshtml b/Views/Shared/_Layout.cshtml
index c27a958..f67aa24 100644
--- a/Views/Shared/_Layout.cshtml
+++ b/Views/Shared/_Layout.cshtml
@@ -33,6 +33,7 @@
</div>
</div>
</nav>
+ <partial name="_LoginPartial" />
</header>
<div class="container">
<main role="main" class="pb-3">
diff --git a/Views/Shared/_LoginPartial.cshtml b/Views/Shared/_LoginPartial.cshtml
index e69de29..b838e04 100644
--- a/Views/Shared/_LoginPartial.cshtml
+++ b/Views/Shared/_LoginPartial.cshtml
@@ -0,0 +1,26 @@
+@using Microsoft.AspNetCore.Identity
+@inject SignInManager<IdentityUser> SignInManager
+@inject UserManager<IdentityUser> UserManager
+
+<ul class="navbar-nav">
+ @if (SignInManager.IsSignedIn(User))
+ {
+ <li class="nav-item">
+ <a id="manage" class="nav-link text-dark" asp-area="Identity" asp-page="/Account/Manage/Index" title="Manage">Hello @UserManager.GetUserName(User)!</a>
+ </li>
+ <li class="nav-item">
+ <form id="logoutForm" class="form-inline" asp-area="Identity" asp-page="/Account/Logout" asp-route-returnUrl="@Url.Action("Index", "Home", new { area = "" })">
+ <button id="logout" type="submit" class="nav-link btn btn-link text-dark">Logout</button>
+ </form>
+ </li>
+ }
+ else
+ {
+ <li class="nav-item">
+ <a class="nav-link text-dark" id="register" asp-area="Identity" asp-page="/Account/Register">Register</a>
+ </li>
+ <li class="nav-item">
+ <a class="nav-link text-dark" id="login" asp-area="Identity" asp-page="/Account/Login">Login</a>
+ </li>
+ }
+</ul>
\ No newline at end of file
diff --git a/turf_tasker.csproj b/turf_tasker.csproj
index dbd9ba1..2247d16 100644
--- a/turf_tasker.csproj
+++ b/turf_tasker.csproj
@@ -7,13 +7,22 @@
</PropertyGroup>
<ItemGroup>
- <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.0.6" />
- <PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="8.0.17" />
- <PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="9.0.6">
- <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
- <PrivateAssets>all</PrivateAssets>
- </PackageReference>
- <PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="8.0.7" />
+ <!-- Identity Packages -->
+ <PackageReference Include="Microsoft.AspNetCore.Identity.UI" Version="8.0.6" />
+ <PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="8.0.6" />
+
+ <!-- Explicitly define ALL core EF Core packages to force version alignment -->
+ <PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.6" />
+ <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="8.0.6" />
+ <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.6" />
+ <PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="8.0.6" />
+ <PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="8.0.6">
+ <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
+ <PrivateAssets>all</PrivateAssets>
+ </PackageReference>
+
+ <!-- Scaffolding Package -->
+ <PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="8.0.7" />
</ItemGroup>
</Project>