diff --git a/Apps/Website/Components/LoginViewComponent.cs b/Apps/Website/Components/LoginViewComponent.cs new file mode 100644 index 0000000..285ff75 --- /dev/null +++ b/Apps/Website/Components/LoginViewComponent.cs @@ -0,0 +1,17 @@ +using Microsoft.AspNetCore.Mvc; + +namespace Website.Components +{ + public sealed class LoginViewComponent : ViewComponent + { + public IViewComponentResult Invoke() + { + if (User.Identity.IsAuthenticated) + { + return View("LoggedIn"); + } + + return View(); + } + } +} \ No newline at end of file diff --git a/Apps/Website/Controllers/AccountsController.cs b/Apps/Website/Controllers/AccountsController.cs new file mode 100644 index 0000000..15b69e0 --- /dev/null +++ b/Apps/Website/Controllers/AccountsController.cs @@ -0,0 +1,95 @@ +using System.Linq; +using System.Threading.Tasks; +using HN.Infrastructure; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; +using Website.Models; + +namespace Website.Controllers +{ + public sealed class AccountsController : BaseController + { + private readonly UserManager _userManager; + private readonly SignInManager _signInManager; + + public AccountsController(UserManager userManager, SignInManager signInManager) + { + _userManager = userManager; + _signInManager = signInManager; + } + + public IActionResult Register() + { + return View(); + } + + [HttpPost] + [ValidateAntiForgeryToken] + public async Task Register(RegisterViewModel command) + { + if (!ModelState.IsValid) + { + return View(command); + } + + var user = new User(command.Username); + var result = await _userManager.CreateAsync(user, command.Password); + + if (!result.Succeeded) + { + ModelState.AddModelError(nameof(RegisterViewModel.Username), string.Join(", ", result.Errors.Select(e => e.Description))); + return View(command); + } + + SetFlash("Account created, you can now sign in!"); + + return RedirectToAction(nameof(Login)); + } + + public IActionResult Login() + { + return View(); + } + + [HttpPost] + [ValidateAntiForgeryToken] + public async Task Login(LoginViewModel command) + { + if (!ModelState.IsValid) + { + return View(); + } + + var user = await _userManager.FindByNameAsync(command.Username); + + if (user == null) + { + ModelState.AddModelError(nameof(LoginViewModel.Username), "Could not verify user identity"); + return View(); + } + + var result = await _signInManager.PasswordSignInAsync(user, command.Password, true, false); + + if (!result.Succeeded) + { + ModelState.AddModelError(nameof(LoginViewModel.Username), "Could not verify user identity"); + return View(); + } + + SetFlash("Successfuly connected!"); + + return RedirectToAction(nameof(LinksController.Index), "Links"); + } + + [HttpPost] + [ValidateAntiForgeryToken] + public async Task Logout() + { + await _signInManager.SignOutAsync(); + + SetFlash("Successfuly disconnected!"); + + return RedirectToAction(nameof(Login)); + } + } +} \ No newline at end of file diff --git a/Apps/Website/Models/LoginViewModel.cs b/Apps/Website/Models/LoginViewModel.cs new file mode 100644 index 0000000..e094e55 --- /dev/null +++ b/Apps/Website/Models/LoginViewModel.cs @@ -0,0 +1,13 @@ +using System.ComponentModel.DataAnnotations; + +namespace Website.Models +{ + public sealed class LoginViewModel + { + [Required] + public string Username { get; set; } + + [Required] + public string Password { get; set; } + } +} \ No newline at end of file diff --git a/Apps/Website/Models/RegisterViewModel.cs b/Apps/Website/Models/RegisterViewModel.cs new file mode 100644 index 0000000..ae41de5 --- /dev/null +++ b/Apps/Website/Models/RegisterViewModel.cs @@ -0,0 +1,17 @@ +using System.ComponentModel.DataAnnotations; + +namespace Website.Models +{ + public sealed class RegisterViewModel + { + [Required] + public string Username { get; set; } + + [Required] + public string Password { get; set; } + + [Required] + [Compare(nameof(Password))] + public string PasswordConfirm { get; set; } + } +} \ No newline at end of file diff --git a/Apps/Website/Startup.cs b/Apps/Website/Startup.cs index d02c726..bab0ba6 100644 --- a/Apps/Website/Startup.cs +++ b/Apps/Website/Startup.cs @@ -2,8 +2,10 @@ using HN.Application; using HN.Domain; using HN.Infrastructure; using MediatR; +using Microsoft.AspNetCore.Authentication.Cookies; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Routing; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; @@ -30,12 +32,28 @@ namespace Website services.AddScoped(); services.AddMediatR(typeof(HN.Application.IHNContext)); + // Permet d'avoir des routes en lowercase services.Configure(options => { options.LowercaseUrls = true; options.LowercaseQueryStrings = true; }); + // Pour permettre l'authentification + services.AddIdentity(o => + { + o.Password.RequiredLength = o.Password.RequiredUniqueChars = 0; + o.Password.RequireDigit = o.Password.RequireLowercase = o.Password.RequireNonAlphanumeric = o.Password.RequireUppercase = false; + }) + .AddEntityFrameworkStores(); + + // Permet de reconfigurer certaines parties préconfigurées par Identity https://github.com/dotnet/aspnetcore/blob/3ea1fc7aac9d43152908d5d45ae811f3df7ca399/src/Identity/Core/src/IdentityServiceCollectionExtensions.cs#L51 + services.PostConfigure(IdentityConstants.ApplicationScheme, o => + { + o.LoginPath = "/accounts/login"; + o.LogoutPath = "/accounts/logout"; + }); + services.AddControllersWithViews(); } @@ -59,6 +77,7 @@ namespace Website app.UseRouting(); + app.UseAuthentication(); app.UseAuthorization(); app.UseEndpoints(endpoints => diff --git a/Apps/Website/Views/Accounts/Login.cshtml b/Apps/Website/Views/Accounts/Login.cshtml new file mode 100644 index 0000000..3644db5 --- /dev/null +++ b/Apps/Website/Views/Accounts/Login.cshtml @@ -0,0 +1,13 @@ +@model LoginViewModel + +
+ + + + + + + + + +
diff --git a/Apps/Website/Views/Accounts/Register.cshtml b/Apps/Website/Views/Accounts/Register.cshtml new file mode 100644 index 0000000..efc99ba --- /dev/null +++ b/Apps/Website/Views/Accounts/Register.cshtml @@ -0,0 +1,17 @@ +@model RegisterViewModel + +
+ + + + + + + + + + + + + +
\ No newline at end of file diff --git a/Apps/Website/Views/Shared/Components/Login/Default.cshtml b/Apps/Website/Views/Shared/Components/Login/Default.cshtml new file mode 100644 index 0000000..b871a71 --- /dev/null +++ b/Apps/Website/Views/Shared/Components/Login/Default.cshtml @@ -0,0 +1,2 @@ +Register +Sign in \ No newline at end of file diff --git a/Apps/Website/Views/Shared/Components/Login/LoggedIn.cshtml b/Apps/Website/Views/Shared/Components/Login/LoggedIn.cshtml new file mode 100644 index 0000000..3a3e0d5 --- /dev/null +++ b/Apps/Website/Views/Shared/Components/Login/LoggedIn.cshtml @@ -0,0 +1,4 @@ +

Connected as @User.Identity.Name

+
+ +
\ No newline at end of file diff --git a/Apps/Website/Views/Shared/_Layout.cshtml b/Apps/Website/Views/Shared/_Layout.cshtml index bfe128b..1c6cae2 100644 --- a/Apps/Website/Views/Shared/_Layout.cshtml +++ b/Apps/Website/Views/Shared/_Layout.cshtml @@ -28,6 +28,8 @@ + + diff --git a/Infrastructure/HNDbContext.cs b/Infrastructure/HNDbContext.cs index 6236ca5..e4e387d 100644 --- a/Infrastructure/HNDbContext.cs +++ b/Infrastructure/HNDbContext.cs @@ -1,11 +1,13 @@ -using HN.Application; +using System; +using HN.Application; using HN.Domain; +using Microsoft.AspNetCore.Identity.EntityFrameworkCore; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; namespace HN.Infrastructure { - public sealed class HNDbContext : DbContext, IHNContext + public sealed class HNDbContext : IdentityDbContext, IHNContext { private readonly ILoggerFactory _loggerFactory; public DbSet Links { get; set; } @@ -21,7 +23,11 @@ namespace HN.Infrastructure _loggerFactory = loggerFactory; } - protected override void OnModelCreating(ModelBuilder modelBuilder) => modelBuilder.ApplyConfigurationsFromAssembly(this.GetType().Assembly); + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + modelBuilder.ApplyConfigurationsFromAssembly(this.GetType().Assembly); + } protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { diff --git a/Infrastructure/Infrastructure.csproj b/Infrastructure/Infrastructure.csproj index 5856d17..5c7acfe 100644 --- a/Infrastructure/Infrastructure.csproj +++ b/Infrastructure/Infrastructure.csproj @@ -5,6 +5,7 @@ + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/Infrastructure/Migrations/20201211113924_AddNetIdentity.Designer.cs b/Infrastructure/Migrations/20201211113924_AddNetIdentity.Designer.cs new file mode 100644 index 0000000..5274992 --- /dev/null +++ b/Infrastructure/Migrations/20201211113924_AddNetIdentity.Designer.cs @@ -0,0 +1,364 @@ +// +using System; +using HN.Infrastructure; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +namespace Infrastructure.Migrations +{ + [DbContext(typeof(HNDbContext))] + [Migration("20201211113924_AddNetIdentity")] + partial class AddNetIdentity + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "5.0.0"); + + modelBuilder.Entity("HN.Domain.Comment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Content") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("LinkId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("LinkId"); + + b.ToTable("comments"); + }); + + modelBuilder.Entity("HN.Domain.Link", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("Url") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Url") + .IsUnique(); + + b.ToTable("links"); + }); + + modelBuilder.Entity("HN.Infrastructure.Role", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("TEXT"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex"); + + b.ToTable("AspNetRoles"); + }); + + modelBuilder.Entity("HN.Infrastructure.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("AccessFailedCount") + .HasColumnType("INTEGER"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("TEXT"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("EmailConfirmed") + .HasColumnType("INTEGER"); + + b.Property("LockoutEnabled") + .HasColumnType("INTEGER"); + + b.Property("LockoutEnd") + .HasColumnType("TEXT"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("PasswordHash") + .HasColumnType("TEXT"); + + b.Property("PhoneNumber") + .HasColumnType("TEXT"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("INTEGER"); + + b.Property("SecurityStamp") + .HasColumnType("TEXT"); + + b.Property("TwoFactorEnabled") + .HasColumnType("INTEGER"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex"); + + b.ToTable("AspNetUsers"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClaimType") + .HasColumnType("TEXT"); + + b.Property("ClaimValue") + .HasColumnType("TEXT"); + + b.Property("RoleId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClaimType") + .HasColumnType("TEXT"); + + b.Property("ClaimValue") + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("TEXT"); + + b.Property("ProviderKey") + .HasColumnType("TEXT"); + + b.Property("ProviderDisplayName") + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("RoleId") + .HasColumnType("TEXT"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("LoginProvider") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Value") + .HasColumnType("TEXT"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens"); + }); + + modelBuilder.Entity("HN.Domain.Comment", b => + { + b.HasOne("HN.Domain.Link", null) + .WithMany() + .HasForeignKey("LinkId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.OwnsMany("HN.Domain.Vote", "Votes", b1 => + { + b1.Property("CommentId") + .HasColumnType("TEXT"); + + b1.Property("CreatedAt") + .HasColumnType("TEXT"); + + b1.Property("Type") + .HasColumnType("INTEGER"); + + b1.HasKey("CommentId"); + + b1.ToTable("comment_votes"); + + b1.WithOwner() + .HasForeignKey("CommentId"); + }); + + b.Navigation("Votes"); + }); + + modelBuilder.Entity("HN.Domain.Link", b => + { + b.OwnsMany("HN.Domain.Vote", "Votes", b1 => + { + b1.Property("LinkId") + .HasColumnType("TEXT"); + + b1.Property("CreatedAt") + .HasColumnType("TEXT"); + + b1.Property("Type") + .HasColumnType("INTEGER"); + + b1.HasKey("LinkId"); + + b1.ToTable("link_votes"); + + b1.WithOwner() + .HasForeignKey("LinkId"); + }); + + b.Navigation("Votes"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("HN.Infrastructure.Role", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("HN.Infrastructure.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("HN.Infrastructure.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("HN.Infrastructure.Role", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("HN.Infrastructure.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("HN.Infrastructure.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Infrastructure/Migrations/20201211113924_AddNetIdentity.cs b/Infrastructure/Migrations/20201211113924_AddNetIdentity.cs new file mode 100644 index 0000000..12b9053 --- /dev/null +++ b/Infrastructure/Migrations/20201211113924_AddNetIdentity.cs @@ -0,0 +1,217 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +namespace Infrastructure.Migrations +{ + public partial class AddNetIdentity : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "AspNetRoles", + columns: table => new + { + Id = table.Column(type: "TEXT", nullable: false), + Name = table.Column(type: "TEXT", maxLength: 256, nullable: true), + NormalizedName = table.Column(type: "TEXT", maxLength: 256, nullable: true), + ConcurrencyStamp = table.Column(type: "TEXT", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetRoles", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "AspNetUsers", + columns: table => new + { + Id = table.Column(type: "TEXT", nullable: false), + UserName = table.Column(type: "TEXT", maxLength: 256, nullable: true), + NormalizedUserName = table.Column(type: "TEXT", maxLength: 256, nullable: true), + Email = table.Column(type: "TEXT", maxLength: 256, nullable: true), + NormalizedEmail = table.Column(type: "TEXT", maxLength: 256, nullable: true), + EmailConfirmed = table.Column(type: "INTEGER", nullable: false), + PasswordHash = table.Column(type: "TEXT", nullable: true), + SecurityStamp = table.Column(type: "TEXT", nullable: true), + ConcurrencyStamp = table.Column(type: "TEXT", nullable: true), + PhoneNumber = table.Column(type: "TEXT", nullable: true), + PhoneNumberConfirmed = table.Column(type: "INTEGER", nullable: false), + TwoFactorEnabled = table.Column(type: "INTEGER", nullable: false), + LockoutEnd = table.Column(type: "TEXT", nullable: true), + LockoutEnabled = table.Column(type: "INTEGER", nullable: false), + AccessFailedCount = table.Column(type: "INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetUsers", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "AspNetRoleClaims", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + RoleId = table.Column(type: "TEXT", nullable: false), + ClaimType = table.Column(type: "TEXT", nullable: true), + ClaimValue = table.Column(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(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + UserId = table.Column(type: "TEXT", nullable: false), + ClaimType = table.Column(type: "TEXT", nullable: true), + ClaimValue = table.Column(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(type: "TEXT", nullable: false), + ProviderKey = table.Column(type: "TEXT", nullable: false), + ProviderDisplayName = table.Column(type: "TEXT", nullable: true), + UserId = table.Column(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(type: "TEXT", nullable: false), + RoleId = table.Column(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(type: "TEXT", nullable: false), + LoginProvider = table.Column(type: "TEXT", nullable: false), + Name = table.Column(type: "TEXT", nullable: false), + Value = table.Column(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.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); + } + + 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/Infrastructure/Migrations/HNDbContextModelSnapshot.cs b/Infrastructure/Migrations/HNDbContextModelSnapshot.cs index 200017a..c2c1b4e 100644 --- a/Infrastructure/Migrations/HNDbContextModelSnapshot.cs +++ b/Infrastructure/Migrations/HNDbContextModelSnapshot.cs @@ -61,6 +61,197 @@ namespace Infrastructure.Migrations b.ToTable("links"); }); + modelBuilder.Entity("HN.Infrastructure.Role", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("TEXT"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex"); + + b.ToTable("AspNetRoles"); + }); + + modelBuilder.Entity("HN.Infrastructure.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("AccessFailedCount") + .HasColumnType("INTEGER"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("TEXT"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("EmailConfirmed") + .HasColumnType("INTEGER"); + + b.Property("LockoutEnabled") + .HasColumnType("INTEGER"); + + b.Property("LockoutEnd") + .HasColumnType("TEXT"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("PasswordHash") + .HasColumnType("TEXT"); + + b.Property("PhoneNumber") + .HasColumnType("TEXT"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("INTEGER"); + + b.Property("SecurityStamp") + .HasColumnType("TEXT"); + + b.Property("TwoFactorEnabled") + .HasColumnType("INTEGER"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex"); + + b.ToTable("AspNetUsers"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClaimType") + .HasColumnType("TEXT"); + + b.Property("ClaimValue") + .HasColumnType("TEXT"); + + b.Property("RoleId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClaimType") + .HasColumnType("TEXT"); + + b.Property("ClaimValue") + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("TEXT"); + + b.Property("ProviderKey") + .HasColumnType("TEXT"); + + b.Property("ProviderDisplayName") + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("RoleId") + .HasColumnType("TEXT"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("LoginProvider") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Value") + .HasColumnType("TEXT"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens"); + }); + modelBuilder.Entity("HN.Domain.Comment", b => { b.HasOne("HN.Domain.Link", null) @@ -114,6 +305,57 @@ namespace Infrastructure.Migrations b.Navigation("Votes"); }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("HN.Infrastructure.Role", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("HN.Infrastructure.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("HN.Infrastructure.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("HN.Infrastructure.Role", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("HN.Infrastructure.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("HN.Infrastructure.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); #pragma warning restore 612, 618 } } diff --git a/Infrastructure/Role.cs b/Infrastructure/Role.cs new file mode 100644 index 0000000..82e6405 --- /dev/null +++ b/Infrastructure/Role.cs @@ -0,0 +1,10 @@ +using System; +using Microsoft.AspNetCore.Identity; + +namespace HN.Infrastructure +{ + public sealed class Role : IdentityRole + { + + } +} \ No newline at end of file diff --git a/Infrastructure/User.cs b/Infrastructure/User.cs new file mode 100644 index 0000000..8645123 --- /dev/null +++ b/Infrastructure/User.cs @@ -0,0 +1,13 @@ +using System; +using Microsoft.AspNetCore.Identity; + +namespace HN.Infrastructure +{ + public sealed class User : IdentityUser + { + public User(string userName) : base(userName) + { + + } + } +} \ No newline at end of file diff --git a/README.md b/README.md index d2bdf16..9e91a79 100644 --- a/README.md +++ b/README.md @@ -87,6 +87,19 @@ Proche de ce qui est fait en Blazor mais sans la partie interactivité. Possibil #### Tag Helpers +### Authentification avec ASP.Net Identity Core + +```console +$ cd Infrastructure +$ dotnet add package Microsoft.AspNetCore.Identity.EntityFrameworkCore +``` + +On fait hériter notre `HNDbContext` de `IdentityDbContext`. On peut créer des types customs pour nos User et Role de manière à utiliser des Guid et rester cohérent. + +Côté web, on s'assure d'avoir bien ajouter `AddIdentity` avec les options qui nous intéressent. + +Grâce à ça, nous aurons à notre disposition un `UserManager` et un `SignInManager` nous permettant de réaliser les opérations d'authentification. Bien penser au `UseAuthentication` avant le `UseAuthorization` afin que l'authentification puisse avoir lieu. + ## Démarche On crée un fichier solution avec `dotnet new sln`. On pourra alimenter ce fichier sln avec la commande `dotnet sln add DirProjet`.