From 3cd5133f66a0219c98e66c1cfb3fb1f960a0dbe5 Mon Sep 17 00:00:00 2001 From: Julien Leicher Date: Fri, 11 Dec 2020 17:59:35 +0100 Subject: [PATCH] add-aspnet-identity (#26) add exception filter when user not connected default to needing authentication and apply anonymous to some actions add user in get requests add user relation in link, comment and vote signup and in are ok now! --- Application/AddLink/AddLinkCommandHandler.cs | 8 +- .../CommentLink/CommentLinkCommandHandler.cs | 6 +- Application/GetLink/GetLinkQueryHandler.cs | 2 + Application/IExecutingUserProvider.cs | 12 + Application/IHNContext.cs | 2 + Application/IUser.cs | 10 + Application/ListLinks/LinkDTO.cs | 1 + .../ListLinks/ListLinksQueryHandler.cs | 2 + .../VoteForCommentCommandHandler.cs | 9 +- .../VoteForLink/VoteForLinkCommandHandler.cs | 9 +- Apps/Website/Components/LoginViewComponent.cs | 17 + .../Website/Controllers/AccountsController.cs | 100 +++++ Apps/Website/Controllers/HomeController.cs | 54 ++- Apps/Website/Controllers/LinksController.cs | 14 +- Apps/Website/CustomExceptionFilter.cs | 16 + Apps/Website/HttpExecutingUserProvider.cs | 32 ++ Apps/Website/Models/LoginViewModel.cs | 13 + Apps/Website/Models/RegisterViewModel.cs | 17 + Apps/Website/Startup.cs | 40 +- Apps/Website/UserNotConnected.cs | 12 + 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/_CommentForm.cshtml | 18 +- Apps/Website/Views/Shared/_CommentItem.cshtml | 13 +- Apps/Website/Views/Shared/_Layout.cshtml | 2 + Apps/Website/Views/Shared/_LinkItem.cshtml | 14 +- 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 +- Infrastructure/HNDbContext.cs | 15 +- Infrastructure/Infrastructure.csproj | 1 + .../20201211113924_AddNetIdentity.Designer.cs | 364 ++++++++++++++++ .../20201211113924_AddNetIdentity.cs | 217 ++++++++++ .../20201211144029_AddCreatedBy.Designer.cs | 408 ++++++++++++++++++ .../Migrations/20201211144029_AddCreatedBy.cs | 178 ++++++++ .../Migrations/HNDbContextModelSnapshot.cs | 290 ++++++++++++- Infrastructure/Role.cs | 10 + Infrastructure/User.cs | 14 + README.md | 13 + 44 files changed, 1984 insertions(+), 114 deletions(-) create mode 100644 Application/IExecutingUserProvider.cs create mode 100644 Application/IUser.cs create mode 100644 Apps/Website/Components/LoginViewComponent.cs create mode 100644 Apps/Website/Controllers/AccountsController.cs create mode 100644 Apps/Website/CustomExceptionFilter.cs create mode 100644 Apps/Website/HttpExecutingUserProvider.cs create mode 100644 Apps/Website/Models/LoginViewModel.cs create mode 100644 Apps/Website/Models/RegisterViewModel.cs create mode 100644 Apps/Website/UserNotConnected.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 Domain/Votable.cs create mode 100644 Infrastructure/Migrations/20201211113924_AddNetIdentity.Designer.cs create mode 100644 Infrastructure/Migrations/20201211113924_AddNetIdentity.cs create mode 100644 Infrastructure/Migrations/20201211144029_AddCreatedBy.Designer.cs create mode 100644 Infrastructure/Migrations/20201211144029_AddCreatedBy.cs create mode 100644 Infrastructure/Role.cs create mode 100644 Infrastructure/User.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/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/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/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/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/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..c62e13e --- /dev/null +++ b/Apps/Website/Controllers/AccountsController.cs @@ -0,0 +1,100 @@ +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; + +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; + } + + [AllowAnonymous] + public IActionResult Register() + { + return View(); + } + + [HttpPost] + [ValidateAntiForgeryToken] + [AllowAnonymous] + 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)); + } + + [AllowAnonymous] + public IActionResult Login() + { + return View(); + } + + [HttpPost] + [ValidateAntiForgeryToken] + [AllowAnonymous] + 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/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..1bdd404 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)); @@ -31,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) @@ -46,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 new file mode 100644 index 0000000..e42e9ce --- /dev/null +++ b/Apps/Website/HttpExecutingUserProvider.cs @@ -0,0 +1,32 @@ +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() + { + 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/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..fe73284 100644 --- a/Apps/Website/Startup.cs +++ b/Apps/Website/Startup.cs @@ -2,8 +2,11 @@ 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.Mvc.Authorization; using Microsoft.AspNetCore.Routing; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; @@ -28,15 +31,36 @@ namespace Website services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); services.AddMediatR(typeof(HN.Application.IHNContext)); + // Permet d'avoir des routes en lowercase services.Configure(options => { options.LowercaseUrls = true; options.LowercaseQueryStrings = true; }); - services.AddControllersWithViews(); + // 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(o => + { + o.Filters.Add(); + 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. @@ -59,6 +83,20 @@ 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(); app.UseEndpoints(endpoints => 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 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/_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/_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/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/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/HNDbContext.cs b/Infrastructure/HNDbContext.cs index 6236ca5..ba600f1 100644 --- a/Infrastructure/HNDbContext.cs +++ b/Infrastructure/HNDbContext.cs @@ -1,16 +1,21 @@ -using HN.Application; +using System; +using System.Linq; +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; } public DbSet Comments { get; set; } + IQueryable IHNContext.Users => Users; + public HNDbContext() { @@ -21,7 +26,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/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 200017a..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,14 +63,213 @@ namespace Infrastructure.Migrations 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") @@ -74,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"); @@ -93,27 +311,95 @@ 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"); }); 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..fb961fa --- /dev/null +++ b/Infrastructure/User.cs @@ -0,0 +1,14 @@ +using System; +using HN.Application; +using Microsoft.AspNetCore.Identity; + +namespace HN.Infrastructure +{ + public sealed class User : IdentityUser, IUser + { + 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`.