From 14c928b43520f05c880a7a60e9de0961fdab680b Mon Sep 17 00:00:00 2001 From: YuukanOO Date: Fri, 11 Dec 2020 15:23:16 +0100 Subject: [PATCH 1/5] signup and in are ok now! --- Apps/Website/Components/LoginViewComponent.cs | 17 + .../Website/Controllers/AccountsController.cs | 95 +++++ Apps/Website/Models/LoginViewModel.cs | 13 + Apps/Website/Models/RegisterViewModel.cs | 17 + Apps/Website/Startup.cs | 19 + Apps/Website/Views/Accounts/Login.cshtml | 13 + Apps/Website/Views/Accounts/Register.cshtml | 17 + .../Shared/Components/Login/Default.cshtml | 2 + .../Shared/Components/Login/LoggedIn.cshtml | 4 + Apps/Website/Views/Shared/_Layout.cshtml | 2 + Infrastructure/HNDbContext.cs | 12 +- Infrastructure/Infrastructure.csproj | 1 + .../20201211113924_AddNetIdentity.Designer.cs | 364 ++++++++++++++++++ .../20201211113924_AddNetIdentity.cs | 217 +++++++++++ .../Migrations/HNDbContextModelSnapshot.cs | 242 ++++++++++++ Infrastructure/Role.cs | 10 + Infrastructure/User.cs | 13 + README.md | 13 + 18 files changed, 1068 insertions(+), 3 deletions(-) create mode 100644 Apps/Website/Components/LoginViewComponent.cs create mode 100644 Apps/Website/Controllers/AccountsController.cs create mode 100644 Apps/Website/Models/LoginViewModel.cs create mode 100644 Apps/Website/Models/RegisterViewModel.cs create mode 100644 Apps/Website/Views/Accounts/Login.cshtml create mode 100644 Apps/Website/Views/Accounts/Register.cshtml create mode 100644 Apps/Website/Views/Shared/Components/Login/Default.cshtml create mode 100644 Apps/Website/Views/Shared/Components/Login/LoggedIn.cshtml create mode 100644 Infrastructure/Migrations/20201211113924_AddNetIdentity.Designer.cs create mode 100644 Infrastructure/Migrations/20201211113924_AddNetIdentity.cs create mode 100644 Infrastructure/Role.cs create mode 100644 Infrastructure/User.cs 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`. -- 2.39.5 From fb886c5c3369c8debab9820f07612f5ccb90b528 Mon Sep 17 00:00:00 2001 From: YuukanOO Date: Fri, 11 Dec 2020 16:00:29 +0100 Subject: [PATCH 2/5] add user relation in link, comment and vote --- Application/AddLink/AddLinkCommandHandler.cs | 8 +- .../CommentLink/CommentLinkCommandHandler.cs | 6 +- Application/IExecutingUserProvider.cs | 12 + .../VoteForCommentCommandHandler.cs | 9 +- .../VoteForLink/VoteForLinkCommandHandler.cs | 9 +- Apps/Website/HttpExecutingUserProvider.cs | 25 ++ Apps/Website/Startup.cs | 1 + Domain/Comment.cs | 28 +- Domain/Link.cs | 34 +- Domain/Votable.cs | 43 ++ Domain/Vote.cs | 14 +- .../EntityTypes/CommentEntityType.cs | 5 +- Infrastructure/EntityTypes/LinkEntityType.cs | 5 +- .../20201211144029_AddCreatedBy.Designer.cs | 408 ++++++++++++++++++ .../Migrations/20201211144029_AddCreatedBy.cs | 178 ++++++++ .../Migrations/HNDbContextModelSnapshot.cs | 48 ++- 16 files changed, 774 insertions(+), 59 deletions(-) create mode 100644 Application/IExecutingUserProvider.cs create mode 100644 Apps/Website/HttpExecutingUserProvider.cs create mode 100644 Domain/Votable.cs create mode 100644 Infrastructure/Migrations/20201211144029_AddCreatedBy.Designer.cs create mode 100644 Infrastructure/Migrations/20201211144029_AddCreatedBy.cs diff --git a/Application/AddLink/AddLinkCommandHandler.cs b/Application/AddLink/AddLinkCommandHandler.cs index f0f3a31..5028da3 100644 --- a/Application/AddLink/AddLinkCommandHandler.cs +++ b/Application/AddLink/AddLinkCommandHandler.cs @@ -10,15 +10,17 @@ namespace HN.Application public class AddLinkCommandHandler : IRequestHandler { private readonly ILinkRepository _repository; + private readonly IExecutingUserProvider _executingUserProvider; - public AddLinkCommandHandler(ILinkRepository repository) + public AddLinkCommandHandler(ILinkRepository repository, IExecutingUserProvider executingUserProvider) { - this._repository = repository; + _repository = repository; + _executingUserProvider = executingUserProvider; } public async Task Handle(AddLinkCommand request, CancellationToken cancellationToken) { - var link = Link.FromUrl(request.Url); + var link = Link.FromUrl(_executingUserProvider.GetCurrentUserId(), request.Url); await this._repository.AddAsync(link); diff --git a/Application/CommentLink/CommentLinkCommandHandler.cs b/Application/CommentLink/CommentLinkCommandHandler.cs index fb455b5..6b3fe08 100644 --- a/Application/CommentLink/CommentLinkCommandHandler.cs +++ b/Application/CommentLink/CommentLinkCommandHandler.cs @@ -10,17 +10,19 @@ namespace HN.Application { private readonly ILinkRepository _linkRepository; private readonly ICommentRepository _commentRepository; + private readonly IExecutingUserProvider _executingUserProvider; - public CommentLinkCommandHandler(ILinkRepository linkRepository, ICommentRepository commentRepository) + public CommentLinkCommandHandler(ILinkRepository linkRepository, ICommentRepository commentRepository, IExecutingUserProvider executingUserProvider) { _linkRepository = linkRepository; _commentRepository = commentRepository; + _executingUserProvider = executingUserProvider; } public async Task Handle(CommentLinkCommand request, CancellationToken cancellationToken) { var link = await _linkRepository.GetByIdAsync(request.LinkId); - var comment = link.AddComment(request.Content); + var comment = link.AddComment(_executingUserProvider.GetCurrentUserId(), request.Content); await _commentRepository.AddAsync(comment); diff --git a/Application/IExecutingUserProvider.cs b/Application/IExecutingUserProvider.cs new file mode 100644 index 0000000..4c3cd48 --- /dev/null +++ b/Application/IExecutingUserProvider.cs @@ -0,0 +1,12 @@ +using System; + +namespace HN.Application +{ + /// + /// Permet de récupérer l'utilisateur courant effectuant une commande. + /// + public interface IExecutingUserProvider + { + Guid GetCurrentUserId(); + } +} \ No newline at end of file diff --git a/Application/VoteForComment/VoteForCommentCommandHandler.cs b/Application/VoteForComment/VoteForCommentCommandHandler.cs index 153ce76..804bc31 100644 --- a/Application/VoteForComment/VoteForCommentCommandHandler.cs +++ b/Application/VoteForComment/VoteForCommentCommandHandler.cs @@ -8,23 +8,26 @@ namespace HN.Application public sealed class VoteForCommentCommandHandler : IRequestHandler { private readonly ICommentRepository _commentRepository; + private readonly IExecutingUserProvider _executingUserProvider; - public VoteForCommentCommandHandler(ICommentRepository commentRepository) + public VoteForCommentCommandHandler(ICommentRepository commentRepository, IExecutingUserProvider executingUserProvider) { _commentRepository = commentRepository; + _executingUserProvider = executingUserProvider; } public async Task Handle(VoteForCommentCommand request, CancellationToken cancellationToken) { var comment = await _commentRepository.GetByIdAsync(request.CommentId); + var userId = _executingUserProvider.GetCurrentUserId(); switch (request.Type) { case VoteType.Up: - comment.Upvote(); + comment.Upvote(userId); break; case VoteType.Down: - comment.Downvote(); + comment.Downvote(userId); break; } diff --git a/Application/VoteForLink/VoteForLinkCommandHandler.cs b/Application/VoteForLink/VoteForLinkCommandHandler.cs index 5c1965c..263d861 100644 --- a/Application/VoteForLink/VoteForLinkCommandHandler.cs +++ b/Application/VoteForLink/VoteForLinkCommandHandler.cs @@ -8,23 +8,26 @@ namespace HN.Application public sealed class VoteForLinkCommandHandler : IRequestHandler { private readonly ILinkRepository _linkRepository; + private readonly IExecutingUserProvider _executingUserProvider; - public VoteForLinkCommandHandler(ILinkRepository linkRepository) + public VoteForLinkCommandHandler(ILinkRepository linkRepository, IExecutingUserProvider executingUserProvider) { _linkRepository = linkRepository; + _executingUserProvider = executingUserProvider; } public async Task Handle(VoteForLinkCommand request, CancellationToken cancellationToken) { var link = await _linkRepository.GetByIdAsync(request.LinkId); + var userId = _executingUserProvider.GetCurrentUserId(); switch (request.Type) { case VoteType.Up: - link.Upvote(); + link.Upvote(userId); break; case VoteType.Down: - link.Downvote(); + link.Downvote(userId); break; } diff --git a/Apps/Website/HttpExecutingUserProvider.cs b/Apps/Website/HttpExecutingUserProvider.cs new file mode 100644 index 0000000..7e8fc64 --- /dev/null +++ b/Apps/Website/HttpExecutingUserProvider.cs @@ -0,0 +1,25 @@ +using System; +using HN.Application; +using HN.Infrastructure; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Identity; + +namespace Website +{ + public sealed class HttpExecutingUserProvider : IExecutingUserProvider + { + private readonly IHttpContextAccessor _httpContextAccessor; + private readonly UserManager _userManager; + + public HttpExecutingUserProvider(IHttpContextAccessor httpContextAccessor, UserManager userManager) + { + _httpContextAccessor = httpContextAccessor; + _userManager = userManager; + } + + public Guid GetCurrentUserId() + { + return Guid.Parse(_userManager.GetUserId(_httpContextAccessor.HttpContext.User)); + } + } +} \ No newline at end of file diff --git a/Apps/Website/Startup.cs b/Apps/Website/Startup.cs index bab0ba6..3e6f979 100644 --- a/Apps/Website/Startup.cs +++ b/Apps/Website/Startup.cs @@ -30,6 +30,7 @@ namespace Website services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); services.AddMediatR(typeof(HN.Application.IHNContext)); // Permet d'avoir des routes en lowercase diff --git a/Domain/Comment.cs b/Domain/Comment.cs index 91c688e..2530d4a 100644 --- a/Domain/Comment.cs +++ b/Domain/Comment.cs @@ -1,34 +1,22 @@ using System; -using System.Collections.Generic; namespace HN.Domain { - public sealed class Comment + public sealed class Comment : Votable { - public Guid Id { get; private set; } - public Guid LinkId { get; private set; } - public string Content { get; private set; } - public DateTime CreatedAt { get; private set; } - private List _votes; - public IReadOnlyList Votes => _votes; + public Guid Id { get; } + public Guid LinkId { get; } + public string Content { get; } + public Guid CreatedBy { get; } + public DateTime CreatedAt { get; } - internal Comment(Guid linkId, string content) + internal Comment(Guid linkId, Guid createdBy, string content) : base() { Id = Guid.NewGuid(); LinkId = linkId; + CreatedBy = createdBy; Content = content; CreatedAt = DateTime.UtcNow; - _votes = new List(); - } - - public void Upvote() - { - _votes.Add(new Vote(VoteType.Up)); - } - - public void Downvote() - { - _votes.Add(new Vote(VoteType.Down)); } } } \ No newline at end of file diff --git a/Domain/Link.cs b/Domain/Link.cs index 363d111..8b36aee 100644 --- a/Domain/Link.cs +++ b/Domain/Link.cs @@ -1,42 +1,30 @@ using System; -using System.Collections.Generic; namespace HN.Domain { - public sealed class Link + public sealed class Link : Votable { public Guid Id { get; } public string Url { get; } public DateTime CreatedAt { get; } - private List _votes; - public IReadOnlyList Votes => _votes; + public Guid CreatedBy { get; } - private Link(string url) + private Link(Guid createdBy, string url) : base() { - this.Id = Guid.NewGuid(); - this.CreatedAt = DateTime.UtcNow; - this.Url = url; - this._votes = new List(); + Id = Guid.NewGuid(); + CreatedBy = createdBy; + CreatedAt = DateTime.UtcNow; + Url = url; } - public static Link FromUrl(string url) + public static Link FromUrl(Guid posterId, string url) { - return new Link(url); + return new Link(posterId, url); } - public void Upvote() + public Comment AddComment(Guid userId, string content) { - _votes.Add(new Vote(VoteType.Up)); - } - - public void Downvote() - { - _votes.Add(new Vote(VoteType.Down)); - } - - public Comment AddComment(string content) - { - return new Comment(Id, content); + return new Comment(Id, userId, content); } } } diff --git a/Domain/Votable.cs b/Domain/Votable.cs new file mode 100644 index 0000000..074e360 --- /dev/null +++ b/Domain/Votable.cs @@ -0,0 +1,43 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace HN.Domain +{ + /// + /// Représente une entité sur laquelle on peut voter. + /// + public abstract class Votable + { + private List _votes; + public IReadOnlyList Votes => _votes; + + protected Votable() + { + _votes = new List(); + } + + public void Upvote(Guid userId) + { + UpsertUserVote(userId, VoteType.Up); + } + + public void Downvote(Guid userId) + { + UpsertUserVote(userId, VoteType.Down); + } + + private void UpsertUserVote(Guid userId, VoteType type) + { + var vote = _votes.SingleOrDefault(v => v.CreatedBy == userId); + + if (vote == null) + { + _votes.Add(new Vote(userId, type)); + return; + } + + vote.HasType(type); + } + } +} \ No newline at end of file diff --git a/Domain/Vote.cs b/Domain/Vote.cs index ea28898..dc74f69 100644 --- a/Domain/Vote.cs +++ b/Domain/Vote.cs @@ -5,9 +5,21 @@ namespace HN.Domain public sealed class Vote { public VoteType Type { get; private set; } + public Guid CreatedBy { get; } public DateTime CreatedAt { get; private set; } - internal Vote(VoteType type) + internal Vote(Guid createdBy, VoteType type) + { + CreatedBy = createdBy; + Type = type; + CreatedAt = DateTime.UtcNow; + } + + /// + /// Change le type d'un vote + /// + /// + public void HasType(VoteType type) { Type = type; CreatedAt = DateTime.UtcNow; diff --git a/Infrastructure/EntityTypes/CommentEntityType.cs b/Infrastructure/EntityTypes/CommentEntityType.cs index b6b4a66..dffdd31 100644 --- a/Infrastructure/EntityTypes/CommentEntityType.cs +++ b/Infrastructure/EntityTypes/CommentEntityType.cs @@ -17,11 +17,14 @@ namespace HN.Infrastructure builder.Property(o => o.Content).IsRequired(); builder.Property(o => o.CreatedAt).IsRequired(); + builder.HasOne().WithMany().HasForeignKey(nameof(Comment.CreatedBy)).IsRequired(); + builder.OwnsMany(o => o.Votes, vote => { vote.ToTable("comment_votes"); vote.WithOwner().HasForeignKey("CommentId"); - vote.HasKey("CommentId"); + vote.HasKey("CommentId", nameof(Comment.CreatedBy)); + vote.HasOne().WithMany().HasForeignKey(nameof(Vote.CreatedBy)).IsRequired(); vote.Property(o => o.Type).IsRequired(); vote.Property(o => o.CreatedAt).IsRequired(); }); diff --git a/Infrastructure/EntityTypes/LinkEntityType.cs b/Infrastructure/EntityTypes/LinkEntityType.cs index ad688cc..5fdaa51 100644 --- a/Infrastructure/EntityTypes/LinkEntityType.cs +++ b/Infrastructure/EntityTypes/LinkEntityType.cs @@ -14,11 +14,14 @@ namespace HN.Infrastructure.EntityTypes builder.Property(o => o.CreatedAt).IsRequired(); builder.HasIndex(o => o.Url).IsUnique(); + builder.HasOne().WithMany().HasForeignKey(nameof(Link.CreatedBy)).IsRequired(); + builder.OwnsMany(o => o.Votes, vote => { vote.ToTable("link_votes"); vote.WithOwner().HasForeignKey("LinkId"); - vote.HasKey("LinkId"); + vote.HasKey("LinkId", nameof(Link.CreatedBy)); + vote.HasOne().WithMany().HasForeignKey(nameof(Vote.CreatedBy)).IsRequired(); vote.Property(o => o.Type).IsRequired(); vote.Property(o => o.CreatedAt).IsRequired(); }); diff --git a/Infrastructure/Migrations/20201211144029_AddCreatedBy.Designer.cs b/Infrastructure/Migrations/20201211144029_AddCreatedBy.Designer.cs new file mode 100644 index 0000000..6275b2c --- /dev/null +++ b/Infrastructure/Migrations/20201211144029_AddCreatedBy.Designer.cs @@ -0,0 +1,408 @@ +// +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("20201211144029_AddCreatedBy")] + partial class AddCreatedBy + { + 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("CreatedBy") + .HasColumnType("TEXT"); + + b.Property("LinkId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("CreatedBy"); + + 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("CreatedBy") + .HasColumnType("TEXT"); + + b.Property("Url") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("CreatedBy"); + + 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.Infrastructure.User", null) + .WithMany() + .HasForeignKey("CreatedBy") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + 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("CreatedBy") + .HasColumnType("TEXT"); + + b1.Property("CreatedAt") + .HasColumnType("TEXT"); + + b1.Property("Type") + .HasColumnType("INTEGER"); + + b1.HasKey("CommentId", "CreatedBy"); + + b1.HasIndex("CreatedBy"); + + b1.ToTable("comment_votes"); + + b1.WithOwner() + .HasForeignKey("CommentId"); + + b1.HasOne("HN.Infrastructure.User", null) + .WithMany() + .HasForeignKey("CreatedBy") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + b.Navigation("Votes"); + }); + + modelBuilder.Entity("HN.Domain.Link", b => + { + b.HasOne("HN.Infrastructure.User", null) + .WithMany() + .HasForeignKey("CreatedBy") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.OwnsMany("HN.Domain.Vote", "Votes", b1 => + { + b1.Property("LinkId") + .HasColumnType("TEXT"); + + b1.Property("CreatedBy") + .HasColumnType("TEXT"); + + b1.Property("CreatedAt") + .HasColumnType("TEXT"); + + b1.Property("Type") + .HasColumnType("INTEGER"); + + b1.HasKey("LinkId", "CreatedBy"); + + b1.HasIndex("CreatedBy"); + + b1.ToTable("link_votes"); + + b1.HasOne("HN.Infrastructure.User", null) + .WithMany() + .HasForeignKey("CreatedBy") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + 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/20201211144029_AddCreatedBy.cs b/Infrastructure/Migrations/20201211144029_AddCreatedBy.cs new file mode 100644 index 0000000..97834e3 --- /dev/null +++ b/Infrastructure/Migrations/20201211144029_AddCreatedBy.cs @@ -0,0 +1,178 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +namespace Infrastructure.Migrations +{ + public partial class AddCreatedBy : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropPrimaryKey( + name: "PK_link_votes", + table: "link_votes"); + + migrationBuilder.DropPrimaryKey( + name: "PK_comment_votes", + table: "comment_votes"); + + migrationBuilder.AddColumn( + name: "CreatedBy", + table: "links", + type: "TEXT", + nullable: false, + defaultValue: new Guid("00000000-0000-0000-0000-000000000000")); + + migrationBuilder.AddColumn( + name: "CreatedBy", + table: "link_votes", + type: "TEXT", + nullable: false, + defaultValue: new Guid("00000000-0000-0000-0000-000000000000")); + + migrationBuilder.AddColumn( + name: "CreatedBy", + table: "comments", + type: "TEXT", + nullable: false, + defaultValue: new Guid("00000000-0000-0000-0000-000000000000")); + + migrationBuilder.AddColumn( + name: "CreatedBy", + table: "comment_votes", + type: "TEXT", + nullable: false, + defaultValue: new Guid("00000000-0000-0000-0000-000000000000")); + + migrationBuilder.AddPrimaryKey( + name: "PK_link_votes", + table: "link_votes", + columns: new[] { "LinkId", "CreatedBy" }); + + migrationBuilder.AddPrimaryKey( + name: "PK_comment_votes", + table: "comment_votes", + columns: new[] { "CommentId", "CreatedBy" }); + + migrationBuilder.CreateIndex( + name: "IX_links_CreatedBy", + table: "links", + column: "CreatedBy"); + + migrationBuilder.CreateIndex( + name: "IX_link_votes_CreatedBy", + table: "link_votes", + column: "CreatedBy"); + + migrationBuilder.CreateIndex( + name: "IX_comments_CreatedBy", + table: "comments", + column: "CreatedBy"); + + migrationBuilder.CreateIndex( + name: "IX_comment_votes_CreatedBy", + table: "comment_votes", + column: "CreatedBy"); + + migrationBuilder.AddForeignKey( + name: "FK_comment_votes_AspNetUsers_CreatedBy", + table: "comment_votes", + column: "CreatedBy", + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + + migrationBuilder.AddForeignKey( + name: "FK_comments_AspNetUsers_CreatedBy", + table: "comments", + column: "CreatedBy", + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + + migrationBuilder.AddForeignKey( + name: "FK_link_votes_AspNetUsers_CreatedBy", + table: "link_votes", + column: "CreatedBy", + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + + migrationBuilder.AddForeignKey( + name: "FK_links_AspNetUsers_CreatedBy", + table: "links", + column: "CreatedBy", + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_comment_votes_AspNetUsers_CreatedBy", + table: "comment_votes"); + + migrationBuilder.DropForeignKey( + name: "FK_comments_AspNetUsers_CreatedBy", + table: "comments"); + + migrationBuilder.DropForeignKey( + name: "FK_link_votes_AspNetUsers_CreatedBy", + table: "link_votes"); + + migrationBuilder.DropForeignKey( + name: "FK_links_AspNetUsers_CreatedBy", + table: "links"); + + migrationBuilder.DropIndex( + name: "IX_links_CreatedBy", + table: "links"); + + migrationBuilder.DropPrimaryKey( + name: "PK_link_votes", + table: "link_votes"); + + migrationBuilder.DropIndex( + name: "IX_link_votes_CreatedBy", + table: "link_votes"); + + migrationBuilder.DropIndex( + name: "IX_comments_CreatedBy", + table: "comments"); + + migrationBuilder.DropPrimaryKey( + name: "PK_comment_votes", + table: "comment_votes"); + + migrationBuilder.DropIndex( + name: "IX_comment_votes_CreatedBy", + table: "comment_votes"); + + migrationBuilder.DropColumn( + name: "CreatedBy", + table: "links"); + + migrationBuilder.DropColumn( + name: "CreatedBy", + table: "link_votes"); + + migrationBuilder.DropColumn( + name: "CreatedBy", + table: "comments"); + + migrationBuilder.DropColumn( + name: "CreatedBy", + table: "comment_votes"); + + migrationBuilder.AddPrimaryKey( + name: "PK_link_votes", + table: "link_votes", + column: "LinkId"); + + migrationBuilder.AddPrimaryKey( + name: "PK_comment_votes", + table: "comment_votes", + column: "CommentId"); + } + } +} diff --git a/Infrastructure/Migrations/HNDbContextModelSnapshot.cs b/Infrastructure/Migrations/HNDbContextModelSnapshot.cs index c2c1b4e..0ce4826 100644 --- a/Infrastructure/Migrations/HNDbContextModelSnapshot.cs +++ b/Infrastructure/Migrations/HNDbContextModelSnapshot.cs @@ -29,11 +29,16 @@ namespace Infrastructure.Migrations b.Property("CreatedAt") .HasColumnType("TEXT"); + b.Property("CreatedBy") + .HasColumnType("TEXT"); + b.Property("LinkId") .HasColumnType("TEXT"); b.HasKey("Id"); + b.HasIndex("CreatedBy"); + b.HasIndex("LinkId"); b.ToTable("comments"); @@ -48,6 +53,9 @@ namespace Infrastructure.Migrations b.Property("CreatedAt") .HasColumnType("TEXT"); + b.Property("CreatedBy") + .HasColumnType("TEXT"); + b.Property("Url") .IsRequired() .HasMaxLength(500) @@ -55,6 +63,8 @@ namespace Infrastructure.Migrations b.HasKey("Id"); + b.HasIndex("CreatedBy"); + b.HasIndex("Url") .IsUnique(); @@ -254,6 +264,12 @@ namespace Infrastructure.Migrations modelBuilder.Entity("HN.Domain.Comment", b => { + b.HasOne("HN.Infrastructure.User", null) + .WithMany() + .HasForeignKey("CreatedBy") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + b.HasOne("HN.Domain.Link", null) .WithMany() .HasForeignKey("LinkId") @@ -265,18 +281,29 @@ namespace Infrastructure.Migrations b1.Property("CommentId") .HasColumnType("TEXT"); + b1.Property("CreatedBy") + .HasColumnType("TEXT"); + b1.Property("CreatedAt") .HasColumnType("TEXT"); b1.Property("Type") .HasColumnType("INTEGER"); - b1.HasKey("CommentId"); + b1.HasKey("CommentId", "CreatedBy"); + + b1.HasIndex("CreatedBy"); b1.ToTable("comment_votes"); b1.WithOwner() .HasForeignKey("CommentId"); + + b1.HasOne("HN.Infrastructure.User", null) + .WithMany() + .HasForeignKey("CreatedBy") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); }); b.Navigation("Votes"); @@ -284,21 +311,38 @@ namespace Infrastructure.Migrations modelBuilder.Entity("HN.Domain.Link", b => { + b.HasOne("HN.Infrastructure.User", null) + .WithMany() + .HasForeignKey("CreatedBy") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + b.OwnsMany("HN.Domain.Vote", "Votes", b1 => { b1.Property("LinkId") .HasColumnType("TEXT"); + b1.Property("CreatedBy") + .HasColumnType("TEXT"); + b1.Property("CreatedAt") .HasColumnType("TEXT"); b1.Property("Type") .HasColumnType("INTEGER"); - b1.HasKey("LinkId"); + b1.HasKey("LinkId", "CreatedBy"); + + b1.HasIndex("CreatedBy"); b1.ToTable("link_votes"); + b1.HasOne("HN.Infrastructure.User", null) + .WithMany() + .HasForeignKey("CreatedBy") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + b1.WithOwner() .HasForeignKey("LinkId"); }); -- 2.39.5 From e00ba99050f37a4ab5b1d780053b95cd591700e6 Mon Sep 17 00:00:00 2001 From: YuukanOO Date: Fri, 11 Dec 2020 16:17:44 +0100 Subject: [PATCH 3/5] add user in get requests --- Application/GetLink/GetLinkQueryHandler.cs | 2 ++ Application/IHNContext.cs | 2 ++ Application/IUser.cs | 10 ++++++++++ Application/ListLinks/LinkDTO.cs | 1 + Application/ListLinks/ListLinksQueryHandler.cs | 2 ++ Apps/Website/Views/Shared/_CommentForm.cshtml | 18 +++++++++++------- Apps/Website/Views/Shared/_CommentItem.cshtml | 13 ++++++++----- Apps/Website/Views/Shared/_LinkItem.cshtml | 14 ++++++++------ Infrastructure/HNDbContext.cs | 3 +++ Infrastructure/User.cs | 3 ++- 10 files changed, 49 insertions(+), 19 deletions(-) create mode 100644 Application/IUser.cs diff --git a/Application/GetLink/GetLinkQueryHandler.cs b/Application/GetLink/GetLinkQueryHandler.cs index 65dc04e..b3e8695 100644 --- a/Application/GetLink/GetLinkQueryHandler.cs +++ b/Application/GetLink/GetLinkQueryHandler.cs @@ -18,11 +18,13 @@ namespace HN.Application public Task Handle(GetLinkQuery request, CancellationToken cancellationToken) { var result = from link in _context.Links + join user in _context.Users on link.CreatedBy equals user.Id where link.Id == request.Id select new LinkDto { Id = link.Id, Url = link.Url, + CreatedByName = user.UserName, CreatedAt = link.CreatedAt, UpVotes = link.Votes.Count(v => v.Type == VoteType.Up), DownVotes = link.Votes.Count(v => v.Type == VoteType.Down) diff --git a/Application/IHNContext.cs b/Application/IHNContext.cs index 1facc5e..375186a 100644 --- a/Application/IHNContext.cs +++ b/Application/IHNContext.cs @@ -1,3 +1,4 @@ +using System.Linq; using HN.Domain; using Microsoft.EntityFrameworkCore; @@ -10,5 +11,6 @@ namespace HN.Application { DbSet Links { get; } DbSet Comments { get; } + IQueryable Users { get; } } } diff --git a/Application/IUser.cs b/Application/IUser.cs new file mode 100644 index 0000000..2d260cf --- /dev/null +++ b/Application/IUser.cs @@ -0,0 +1,10 @@ +using System; + +namespace HN.Application +{ + public interface IUser + { + Guid Id { get; } + string UserName { get; } + } +} \ No newline at end of file diff --git a/Application/ListLinks/LinkDTO.cs b/Application/ListLinks/LinkDTO.cs index fa3bf7c..7cab81a 100644 --- a/Application/ListLinks/LinkDTO.cs +++ b/Application/ListLinks/LinkDTO.cs @@ -7,6 +7,7 @@ namespace HN.Application public Guid Id { get; set; } public string Url { get; set; } public DateTime CreatedAt { get; set; } + public string CreatedByName { get; set; } public int UpVotes { get; set; } public int DownVotes { get; set; } diff --git a/Application/ListLinks/ListLinksQueryHandler.cs b/Application/ListLinks/ListLinksQueryHandler.cs index 8851c34..8c1464c 100644 --- a/Application/ListLinks/ListLinksQueryHandler.cs +++ b/Application/ListLinks/ListLinksQueryHandler.cs @@ -46,10 +46,12 @@ namespace HN.Application public Task Handle(ListLinksQuery request, CancellationToken cancellationToken) { var links = from link in _context.Links + join user in _context.Users on link.CreatedBy equals user.Id select new LinkDto { Id = link.Id, Url = link.Url, + CreatedByName = user.UserName, CreatedAt = link.CreatedAt, UpVotes = link.Votes.Count(v => v.Type == VoteType.Up), DownVotes = link.Votes.Count(v => v.Type == VoteType.Down) diff --git a/Apps/Website/Views/Shared/_CommentForm.cshtml b/Apps/Website/Views/Shared/_CommentForm.cshtml index 57fd06c..5d88d12 100644 --- a/Apps/Website/Views/Shared/_CommentForm.cshtml +++ b/Apps/Website/Views/Shared/_CommentForm.cshtml @@ -2,11 +2,15 @@

Add a comment

-
- - - - - -
+ @if(User.Identity.IsAuthenticated) { +
+ + + + + +
+ } else { +

Only logged in users can comment.

+ }
\ No newline at end of file diff --git a/Apps/Website/Views/Shared/_CommentItem.cshtml b/Apps/Website/Views/Shared/_CommentItem.cshtml index 18aedb4..4667c92 100644 --- a/Apps/Website/Views/Shared/_CommentItem.cshtml +++ b/Apps/Website/Views/Shared/_CommentItem.cshtml @@ -4,8 +4,11 @@
👍: @Model.UpVotes / 👎: @Model.DownVotes
-
- - - -
\ No newline at end of file + +@if (User.Identity.IsAuthenticated) { +
+ + + +
+} \ No newline at end of file diff --git a/Apps/Website/Views/Shared/_LinkItem.cshtml b/Apps/Website/Views/Shared/_LinkItem.cshtml index cf16e40..6e480ea 100644 --- a/Apps/Website/Views/Shared/_LinkItem.cshtml +++ b/Apps/Website/Views/Shared/_LinkItem.cshtml @@ -1,9 +1,11 @@ @model HN.Application.LinkDto -@Model.Url - created at @Model.CreatedAt.ToLocalTime() (👍: @Model.UpVotes / 👎: @Model.DownVotes) +@Model.Url - created at @Model.CreatedAt.ToLocalTime() by @Model.CreatedByName (👍: @Model.UpVotes / 👎: @Model.DownVotes) -
- - - -
\ No newline at end of file +@if(User.Identity.IsAuthenticated) { +
+ + + +
+} \ No newline at end of file diff --git a/Infrastructure/HNDbContext.cs b/Infrastructure/HNDbContext.cs index e4e387d..ba600f1 100644 --- a/Infrastructure/HNDbContext.cs +++ b/Infrastructure/HNDbContext.cs @@ -1,4 +1,5 @@ using System; +using System.Linq; using HN.Application; using HN.Domain; using Microsoft.AspNetCore.Identity.EntityFrameworkCore; @@ -13,6 +14,8 @@ namespace HN.Infrastructure public DbSet Links { get; set; } public DbSet Comments { get; set; } + IQueryable IHNContext.Users => Users; + public HNDbContext() { diff --git a/Infrastructure/User.cs b/Infrastructure/User.cs index 8645123..fb961fa 100644 --- a/Infrastructure/User.cs +++ b/Infrastructure/User.cs @@ -1,9 +1,10 @@ using System; +using HN.Application; using Microsoft.AspNetCore.Identity; namespace HN.Infrastructure { - public sealed class User : IdentityUser + public sealed class User : IdentityUser, IUser { public User(string userName) : base(userName) { -- 2.39.5 From f4c564748aeef949f09985288fea09000c977418 Mon Sep 17 00:00:00 2001 From: YuukanOO Date: Fri, 11 Dec 2020 17:43:46 +0100 Subject: [PATCH 4/5] default to needing authentication and apply anonymous to some actions --- .../Website/Controllers/AccountsController.cs | 5 ++ Apps/Website/Controllers/HomeController.cs | 54 +++++++++---------- Apps/Website/Controllers/LinksController.cs | 3 ++ Apps/Website/Startup.cs | 6 ++- 4 files changed, 39 insertions(+), 29 deletions(-) diff --git a/Apps/Website/Controllers/AccountsController.cs b/Apps/Website/Controllers/AccountsController.cs index 15b69e0..c62e13e 100644 --- a/Apps/Website/Controllers/AccountsController.cs +++ b/Apps/Website/Controllers/AccountsController.cs @@ -1,6 +1,7 @@ using System.Linq; using System.Threading.Tasks; using HN.Infrastructure; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; using Website.Models; @@ -18,6 +19,7 @@ namespace Website.Controllers _signInManager = signInManager; } + [AllowAnonymous] public IActionResult Register() { return View(); @@ -25,6 +27,7 @@ namespace Website.Controllers [HttpPost] [ValidateAntiForgeryToken] + [AllowAnonymous] public async Task Register(RegisterViewModel command) { if (!ModelState.IsValid) @@ -46,6 +49,7 @@ namespace Website.Controllers return RedirectToAction(nameof(Login)); } + [AllowAnonymous] public IActionResult Login() { return View(); @@ -53,6 +57,7 @@ namespace Website.Controllers [HttpPost] [ValidateAntiForgeryToken] + [AllowAnonymous] public async Task Login(LoginViewModel command) { if (!ModelState.IsValid) diff --git a/Apps/Website/Controllers/HomeController.cs b/Apps/Website/Controllers/HomeController.cs index 75c56b8..d24ca8f 100644 --- a/Apps/Website/Controllers/HomeController.cs +++ b/Apps/Website/Controllers/HomeController.cs @@ -1,37 +1,35 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Linq; -using System.Threading.Tasks; +using System.Diagnostics; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; using Website.Models; namespace Website.Controllers { - public class HomeController : Controller + [AllowAnonymous] + public class HomeController : Controller + { + private readonly ILogger _logger; + + public HomeController(ILogger logger) { - private readonly ILogger _logger; - - public HomeController(ILogger logger) - { - _logger = logger; - } - - public IActionResult Index() - { - return View(); - } - - public IActionResult Privacy() - { - return View(); - } - - [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)] - public IActionResult Error() - { - return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier }); - } + _logger = logger; } + + public IActionResult Index() + { + return View(); + } + + public IActionResult Privacy() + { + return View(); + } + + [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)] + public IActionResult Error() + { + return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier }); + } + } } diff --git a/Apps/Website/Controllers/LinksController.cs b/Apps/Website/Controllers/LinksController.cs index 09e2bb7..95589cc 100644 --- a/Apps/Website/Controllers/LinksController.cs +++ b/Apps/Website/Controllers/LinksController.cs @@ -5,6 +5,7 @@ using System.Threading.Tasks; using System; using HN.Domain; using Website.Models; +using Microsoft.AspNetCore.Authorization; namespace Website.Controllers { @@ -18,12 +19,14 @@ namespace Website.Controllers } [HttpGet] + [AllowAnonymous] public async Task Index() { return View(await _bus.Send(new ListLinksQuery())); } [HttpGet("{controller}/{id:guid}")] + [AllowAnonymous] public async Task Show(Guid id) { var link = await _bus.Send(new GetLinkQuery(id)); diff --git a/Apps/Website/Startup.cs b/Apps/Website/Startup.cs index 3e6f979..393d96c 100644 --- a/Apps/Website/Startup.cs +++ b/Apps/Website/Startup.cs @@ -6,6 +6,7 @@ using Microsoft.AspNetCore.Authentication.Cookies; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc.Authorization; using Microsoft.AspNetCore.Routing; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; @@ -55,7 +56,10 @@ namespace Website o.LogoutPath = "/accounts/logout"; }); - services.AddControllersWithViews(); + services.AddControllersWithViews(o => + { + o.Filters.Add(new AuthorizeFilter()); // Nécessite l'authentification par défaut + }); } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. -- 2.39.5 From 51d66eafcaff686e4f4565e530211c537fa6460d Mon Sep 17 00:00:00 2001 From: YuukanOO Date: Fri, 11 Dec 2020 17:59:03 +0100 Subject: [PATCH 5/5] add exception filter when user not connected --- Apps/Website/Controllers/LinksController.cs | 11 ++++++----- Apps/Website/CustomExceptionFilter.cs | 16 ++++++++++++++++ Apps/Website/HttpExecutingUserProvider.cs | 9 ++++++++- Apps/Website/Startup.cs | 14 ++++++++++++++ Apps/Website/UserNotConnected.cs | 12 ++++++++++++ 5 files changed, 56 insertions(+), 6 deletions(-) create mode 100644 Apps/Website/CustomExceptionFilter.cs create mode 100644 Apps/Website/UserNotConnected.cs diff --git a/Apps/Website/Controllers/LinksController.cs b/Apps/Website/Controllers/LinksController.cs index 95589cc..1bdd404 100644 --- a/Apps/Website/Controllers/LinksController.cs +++ b/Apps/Website/Controllers/LinksController.cs @@ -34,11 +34,6 @@ namespace Website.Controllers return View(new ShowLinkViewModel(link, new CommentLinkCommand(id), comments)); } - public IActionResult Create() - { - return View(new AddLinkCommand()); - } - [HttpPost("{controller}/{id:guid}/vote")] [ValidateAntiForgeryToken] public async Task Vote(Guid id, string url, VoteType type, string redirectTo) @@ -49,8 +44,14 @@ namespace Website.Controllers return Redirect(redirectTo); } + public IActionResult Create() + { + return View(new AddLinkCommand()); + } + [HttpPost] [ValidateAntiForgeryToken] + public async Task Create(AddLinkCommand command) { if (!ModelState.IsValid) diff --git a/Apps/Website/CustomExceptionFilter.cs b/Apps/Website/CustomExceptionFilter.cs new file mode 100644 index 0000000..e433321 --- /dev/null +++ b/Apps/Website/CustomExceptionFilter.cs @@ -0,0 +1,16 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Filters; + +namespace Website +{ + public sealed class CustomExceptionFilter : IExceptionFilter + { + public void OnException(ExceptionContext context) + { + if (context.Exception is UserNotConnected) + { + context.Result = new UnauthorizedResult(); + } + } + } +} \ No newline at end of file diff --git a/Apps/Website/HttpExecutingUserProvider.cs b/Apps/Website/HttpExecutingUserProvider.cs index 7e8fc64..e42e9ce 100644 --- a/Apps/Website/HttpExecutingUserProvider.cs +++ b/Apps/Website/HttpExecutingUserProvider.cs @@ -19,7 +19,14 @@ namespace Website public Guid GetCurrentUserId() { - return Guid.Parse(_userManager.GetUserId(_httpContextAccessor.HttpContext.User)); + var uid = _userManager.GetUserId(_httpContextAccessor.HttpContext.User); + + if (!Guid.TryParse(uid, out Guid result)) + { + throw new UserNotConnected(); + } + + return result; } } } \ No newline at end of file diff --git a/Apps/Website/Startup.cs b/Apps/Website/Startup.cs index 393d96c..fe73284 100644 --- a/Apps/Website/Startup.cs +++ b/Apps/Website/Startup.cs @@ -58,6 +58,7 @@ namespace Website services.AddControllersWithViews(o => { + o.Filters.Add(); o.Filters.Add(new AuthorizeFilter()); // Nécessite l'authentification par défaut }); } @@ -82,6 +83,19 @@ namespace Website app.UseRouting(); + // Permet de rediriger selon les codes d'erreurs retournés, notamment par notre CustomExceptionFilter + app.UseStatusCodePages(context => + { + var request = context.HttpContext.Request; + var response = context.HttpContext.Response; + if (response.StatusCode == (int)System.Net.HttpStatusCode.Unauthorized) + { + response.Redirect("/accounts/login"); + } + + return System.Threading.Tasks.Task.CompletedTask; + }); + app.UseAuthentication(); app.UseAuthorization(); diff --git a/Apps/Website/UserNotConnected.cs b/Apps/Website/UserNotConnected.cs new file mode 100644 index 0000000..5f2b57f --- /dev/null +++ b/Apps/Website/UserNotConnected.cs @@ -0,0 +1,12 @@ +using System; + +namespace Website +{ + public sealed class UserNotConnected : Exception + { + public UserNotConnected() : base("User not connected!") + { + + } + } +} \ No newline at end of file -- 2.39.5