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`.