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!
This commit is contained in:
Julien Leicher 2020-12-11 17:59:35 +01:00
parent 9c75758921
commit 3cd5133f66
44 changed files with 1984 additions and 114 deletions

View File

@ -10,15 +10,17 @@ namespace HN.Application
public class AddLinkCommandHandler : IRequestHandler<AddLinkCommand, Guid>
{
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<Guid> Handle(AddLinkCommand request, CancellationToken cancellationToken)
{
var link = Link.FromUrl(request.Url);
var link = Link.FromUrl(_executingUserProvider.GetCurrentUserId(), request.Url);
await this._repository.AddAsync(link);

View File

@ -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<Guid> 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);

View File

@ -18,11 +18,13 @@ namespace HN.Application
public Task<LinkDto> 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)

View File

@ -0,0 +1,12 @@
using System;
namespace HN.Application
{
/// <summary>
/// Permet de récupérer l'utilisateur courant effectuant une commande.
/// </summary>
public interface IExecutingUserProvider
{
Guid GetCurrentUserId();
}
}

View File

@ -1,3 +1,4 @@
using System.Linq;
using HN.Domain;
using Microsoft.EntityFrameworkCore;
@ -10,5 +11,6 @@ namespace HN.Application
{
DbSet<Link> Links { get; }
DbSet<Comment> Comments { get; }
IQueryable<IUser> Users { get; }
}
}

10
Application/IUser.cs Normal file
View File

@ -0,0 +1,10 @@
using System;
namespace HN.Application
{
public interface IUser
{
Guid Id { get; }
string UserName { get; }
}
}

View File

@ -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; }

View File

@ -46,10 +46,12 @@ namespace HN.Application
public Task<LinkDto[]> 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)

View File

@ -8,23 +8,26 @@ namespace HN.Application
public sealed class VoteForCommentCommandHandler : IRequestHandler<VoteForCommentCommand>
{
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<Unit> 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;
}

View File

@ -8,23 +8,26 @@ namespace HN.Application
public sealed class VoteForLinkCommandHandler : IRequestHandler<VoteForLinkCommand>
{
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<Unit> 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;
}

View File

@ -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();
}
}
}

View File

@ -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<User> _userManager;
private readonly SignInManager<User> _signInManager;
public AccountsController(UserManager<User> userManager, SignInManager<User> signInManager)
{
_userManager = userManager;
_signInManager = signInManager;
}
[AllowAnonymous]
public IActionResult Register()
{
return View();
}
[HttpPost]
[ValidateAntiForgeryToken]
[AllowAnonymous]
public async Task<IActionResult> 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<IActionResult> 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<IActionResult> Logout()
{
await _signInManager.SignOutAsync();
SetFlash("Successfuly disconnected!");
return RedirectToAction(nameof(Login));
}
}
}

View File

@ -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<HomeController> _logger;
public HomeController(ILogger<HomeController> logger)
{
private readonly ILogger<HomeController> _logger;
public HomeController(ILogger<HomeController> 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 });
}
}
}

View File

@ -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<IActionResult> Index()
{
return View(await _bus.Send(new ListLinksQuery()));
}
[HttpGet("{controller}/{id:guid}")]
[AllowAnonymous]
public async Task<IActionResult> 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<IActionResult> 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<IActionResult> Create(AddLinkCommand command)
{
if (!ModelState.IsValid)

View File

@ -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();
}
}
}
}

View File

@ -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<User> _userManager;
public HttpExecutingUserProvider(IHttpContextAccessor httpContextAccessor, UserManager<User> 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;
}
}
}

View File

@ -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; }
}
}

View File

@ -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; }
}
}

View File

@ -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<IHNContext, HNDbContext>();
services.AddScoped<ILinkRepository, LinkRepository>();
services.AddScoped<ICommentRepository, CommentRepository>();
services.AddScoped<IExecutingUserProvider, HttpExecutingUserProvider>();
services.AddMediatR(typeof(HN.Application.IHNContext));
// Permet d'avoir des routes en lowercase
services.Configure<RouteOptions>(options =>
{
options.LowercaseUrls = true;
options.LowercaseQueryStrings = true;
});
services.AddControllersWithViews();
// Pour permettre l'authentification
services.AddIdentity<User, Role>(o =>
{
o.Password.RequiredLength = o.Password.RequiredUniqueChars = 0;
o.Password.RequireDigit = o.Password.RequireLowercase = o.Password.RequireNonAlphanumeric = o.Password.RequireUppercase = false;
})
.AddEntityFrameworkStores<HNDbContext>();
// 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<CookieAuthenticationOptions>(IdentityConstants.ApplicationScheme, o =>
{
o.LoginPath = "/accounts/login";
o.LogoutPath = "/accounts/logout";
});
services.AddControllersWithViews(o =>
{
o.Filters.Add<CustomExceptionFilter>();
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 =>

View File

@ -0,0 +1,12 @@
using System;
namespace Website
{
public sealed class UserNotConnected : Exception
{
public UserNotConnected() : base("User not connected!")
{
}
}
}

View File

@ -0,0 +1,13 @@
@model LoginViewModel
<form asp-action="Login" method="post">
<label asp-for="@Model.Username"></label>
<input asp-for="@Model.Username" />
<span asp-validation-for="@Model.Username"></span>
<label asp-for="@Model.Password"></label>
<input type="password" asp-for="@Model.Password" />
<span asp-validation-for="@Model.Password"></span>
<button type="submit">Sign in</button>
</form>

View File

@ -0,0 +1,17 @@
@model RegisterViewModel
<form asp-action="Register" method="post">
<label asp-for="@Model.Username"></label>
<input asp-for="@Model.Username" />
<span asp-validation-for="@Model.Username"></span>
<label asp-for="@Model.Password"></label>
<input type="password" asp-for="@Model.Password" />
<span asp-validation-for="@Model.Password"></span>
<label asp-for="@Model.PasswordConfirm"></label>
<input type="password" asp-for="@Model.PasswordConfirm" />
<span asp-validation-for="@Model.PasswordConfirm"></span>
<button type="submit">Register</button>
</form>

View File

@ -0,0 +1,2 @@
<a asp-action="Register" asp-controller="Accounts">Register</a>
<a asp-action="Login" asp-controller="Accounts">Sign in</a>

View File

@ -0,0 +1,4 @@
<p>Connected as @User.Identity.Name</p>
<form asp-action="Logout" asp-controller="Accounts" method="post">
<button type="submit">Sign out</button>
</form>

View File

@ -2,11 +2,15 @@
<div>
<h2>Add a comment</h2>
<form asp-action="Create" asp-controller="Comments" method="post">
<input type="hidden" asp-for="@Model.LinkId" />
<textarea asp-for="@Model.Content"></textarea>
<span asp-validation-for="@Model.Content"></span>
@if(User.Identity.IsAuthenticated) {
<form asp-action="Create" asp-controller="Comments" method="post">
<input type="hidden" asp-for="@Model.LinkId" />
<textarea asp-for="@Model.Content"></textarea>
<span asp-validation-for="@Model.Content"></span>
<button type="submit">Post a comment</button>
</form>
<button type="submit">Post a comment</button>
</form>
} else {
<p>Only logged in users can comment.</p>
}
</div>

View File

@ -4,8 +4,11 @@
<div>
👍: @Model.UpVotes / 👎: @Model.DownVotes
</div>
<form asp-action="Vote" asp-controller="Comments" asp-route-id="@Model.Id" method="post">
<input type="hidden" name="redirectTo" value="@Context.Request.Path" />
<input type="submit" name="type" value="up" />
<input type="submit" name="type" value="down" />
</form>
@if (User.Identity.IsAuthenticated) {
<form asp-action="Vote" asp-controller="Comments" asp-route-id="@Model.Id" method="post">
<input type="hidden" name="redirectTo" value="@Context.Request.Path" />
<input type="submit" name="type" value="up" />
<input type="submit" name="type" value="down" />
</form>
}

View File

@ -28,6 +28,8 @@
</li>
</ul>
</div>
<vc:login />
</div>
</nav>
</header>

View File

@ -1,9 +1,11 @@
@model HN.Application.LinkDto
<a asp-action="Show" asp-controller="Links" asp-route-id="@Model.Id">@Model.Url - created at @Model.CreatedAt.ToLocalTime() (👍: @Model.UpVotes / 👎: @Model.DownVotes)</a>
<a asp-action="Show" asp-controller="Links" asp-route-id="@Model.Id">@Model.Url - created at @Model.CreatedAt.ToLocalTime() by @Model.CreatedByName (👍: @Model.UpVotes / 👎: @Model.DownVotes)</a>
<form asp-controller="Links" asp-action="Vote" asp-route-id="@Model.Id" asp-route-url="@Model.Url" method="post">
<input type="hidden" name="redirectTo" value="@Context.Request.Path" />
<input type="submit" name="type" value="up" />
<input type="submit" name="type" value="down" />
</form>
@if(User.Identity.IsAuthenticated) {
<form asp-controller="Links" asp-action="Vote" asp-route-id="@Model.Id" asp-route-url="@Model.Url" method="post">
<input type="hidden" name="redirectTo" value="@Context.Request.Path" />
<input type="submit" name="type" value="up" />
<input type="submit" name="type" value="down" />
</form>
}

View File

@ -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<Vote> _votes;
public IReadOnlyList<Vote> 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<Vote>();
}
public void Upvote()
{
_votes.Add(new Vote(VoteType.Up));
}
public void Downvote()
{
_votes.Add(new Vote(VoteType.Down));
}
}
}

View File

@ -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<Vote> _votes;
public IReadOnlyList<Vote> 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<Vote>();
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);
}
}
}

43
Domain/Votable.cs Normal file
View File

@ -0,0 +1,43 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace HN.Domain
{
/// <summary>
/// Représente une entité sur laquelle on peut voter.
/// </summary>
public abstract class Votable
{
private List<Vote> _votes;
public IReadOnlyList<Vote> Votes => _votes;
protected Votable()
{
_votes = new List<Vote>();
}
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);
}
}
}

View File

@ -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;
}
/// <summary>
/// Change le type d'un vote
/// </summary>
/// <param name="type"></param>
public void HasType(VoteType type)
{
Type = type;
CreatedAt = DateTime.UtcNow;

View File

@ -17,11 +17,14 @@ namespace HN.Infrastructure
builder.Property(o => o.Content).IsRequired();
builder.Property(o => o.CreatedAt).IsRequired();
builder.HasOne<User>().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<User>().WithMany().HasForeignKey(nameof(Vote.CreatedBy)).IsRequired();
vote.Property(o => o.Type).IsRequired();
vote.Property(o => o.CreatedAt).IsRequired();
});

View File

@ -14,11 +14,14 @@ namespace HN.Infrastructure.EntityTypes
builder.Property(o => o.CreatedAt).IsRequired();
builder.HasIndex(o => o.Url).IsUnique();
builder.HasOne<User>().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<User>().WithMany().HasForeignKey(nameof(Vote.CreatedBy)).IsRequired();
vote.Property(o => o.Type).IsRequired();
vote.Property(o => o.CreatedAt).IsRequired();
});

View File

@ -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<User, Role, Guid>, IHNContext
{
private readonly ILoggerFactory _loggerFactory;
public DbSet<Link> Links { get; set; }
public DbSet<Comment> Comments { get; set; }
IQueryable<IUser> 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)
{

View File

@ -5,6 +5,7 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="5.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="5.0.0">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>

View File

@ -0,0 +1,364 @@
// <auto-generated />
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<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<string>("Content")
.IsRequired()
.HasColumnType("TEXT");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT");
b.Property<Guid>("LinkId")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("LinkId");
b.ToTable("comments");
});
modelBuilder.Entity("HN.Domain.Link", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT");
b.Property<string>("Url")
.IsRequired()
.HasMaxLength(500)
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("Url")
.IsUnique();
b.ToTable("links");
});
modelBuilder.Entity("HN.Infrastructure.Role", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken()
.HasColumnType("TEXT");
b.Property<string>("Name")
.HasMaxLength(256)
.HasColumnType("TEXT");
b.Property<string>("NormalizedName")
.HasMaxLength(256)
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("NormalizedName")
.IsUnique()
.HasDatabaseName("RoleNameIndex");
b.ToTable("AspNetRoles");
});
modelBuilder.Entity("HN.Infrastructure.User", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<int>("AccessFailedCount")
.HasColumnType("INTEGER");
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken()
.HasColumnType("TEXT");
b.Property<string>("Email")
.HasMaxLength(256)
.HasColumnType("TEXT");
b.Property<bool>("EmailConfirmed")
.HasColumnType("INTEGER");
b.Property<bool>("LockoutEnabled")
.HasColumnType("INTEGER");
b.Property<DateTimeOffset?>("LockoutEnd")
.HasColumnType("TEXT");
b.Property<string>("NormalizedEmail")
.HasMaxLength(256)
.HasColumnType("TEXT");
b.Property<string>("NormalizedUserName")
.HasMaxLength(256)
.HasColumnType("TEXT");
b.Property<string>("PasswordHash")
.HasColumnType("TEXT");
b.Property<string>("PhoneNumber")
.HasColumnType("TEXT");
b.Property<bool>("PhoneNumberConfirmed")
.HasColumnType("INTEGER");
b.Property<string>("SecurityStamp")
.HasColumnType("TEXT");
b.Property<bool>("TwoFactorEnabled")
.HasColumnType("INTEGER");
b.Property<string>("UserName")
.HasMaxLength(256)
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("NormalizedEmail")
.HasDatabaseName("EmailIndex");
b.HasIndex("NormalizedUserName")
.IsUnique()
.HasDatabaseName("UserNameIndex");
b.ToTable("AspNetUsers");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<System.Guid>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("ClaimType")
.HasColumnType("TEXT");
b.Property<string>("ClaimValue")
.HasColumnType("TEXT");
b.Property<Guid>("RoleId")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("RoleId");
b.ToTable("AspNetRoleClaims");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<System.Guid>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("ClaimType")
.HasColumnType("TEXT");
b.Property<string>("ClaimValue")
.HasColumnType("TEXT");
b.Property<Guid>("UserId")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("AspNetUserClaims");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<System.Guid>", b =>
{
b.Property<string>("LoginProvider")
.HasColumnType("TEXT");
b.Property<string>("ProviderKey")
.HasColumnType("TEXT");
b.Property<string>("ProviderDisplayName")
.HasColumnType("TEXT");
b.Property<Guid>("UserId")
.HasColumnType("TEXT");
b.HasKey("LoginProvider", "ProviderKey");
b.HasIndex("UserId");
b.ToTable("AspNetUserLogins");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<System.Guid>", b =>
{
b.Property<Guid>("UserId")
.HasColumnType("TEXT");
b.Property<Guid>("RoleId")
.HasColumnType("TEXT");
b.HasKey("UserId", "RoleId");
b.HasIndex("RoleId");
b.ToTable("AspNetUserRoles");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<System.Guid>", b =>
{
b.Property<Guid>("UserId")
.HasColumnType("TEXT");
b.Property<string>("LoginProvider")
.HasColumnType("TEXT");
b.Property<string>("Name")
.HasColumnType("TEXT");
b.Property<string>("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<Guid>("CommentId")
.HasColumnType("TEXT");
b1.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT");
b1.Property<int>("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<Guid>("LinkId")
.HasColumnType("TEXT");
b1.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT");
b1.Property<int>("Type")
.HasColumnType("INTEGER");
b1.HasKey("LinkId");
b1.ToTable("link_votes");
b1.WithOwner()
.HasForeignKey("LinkId");
});
b.Navigation("Votes");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<System.Guid>", b =>
{
b.HasOne("HN.Infrastructure.Role", null)
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<System.Guid>", b =>
{
b.HasOne("HN.Infrastructure.User", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<System.Guid>", b =>
{
b.HasOne("HN.Infrastructure.User", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<System.Guid>", 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<System.Guid>", b =>
{
b.HasOne("HN.Infrastructure.User", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
#pragma warning restore 612, 618
}
}
}

View File

@ -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<Guid>(type: "TEXT", nullable: false),
Name = table.Column<string>(type: "TEXT", maxLength: 256, nullable: true),
NormalizedName = table.Column<string>(type: "TEXT", maxLength: 256, nullable: true),
ConcurrencyStamp = table.Column<string>(type: "TEXT", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_AspNetRoles", x => x.Id);
});
migrationBuilder.CreateTable(
name: "AspNetUsers",
columns: table => new
{
Id = table.Column<Guid>(type: "TEXT", nullable: false),
UserName = table.Column<string>(type: "TEXT", maxLength: 256, nullable: true),
NormalizedUserName = table.Column<string>(type: "TEXT", maxLength: 256, nullable: true),
Email = table.Column<string>(type: "TEXT", maxLength: 256, nullable: true),
NormalizedEmail = table.Column<string>(type: "TEXT", maxLength: 256, nullable: true),
EmailConfirmed = table.Column<bool>(type: "INTEGER", nullable: false),
PasswordHash = table.Column<string>(type: "TEXT", nullable: true),
SecurityStamp = table.Column<string>(type: "TEXT", nullable: true),
ConcurrencyStamp = table.Column<string>(type: "TEXT", nullable: true),
PhoneNumber = table.Column<string>(type: "TEXT", nullable: true),
PhoneNumberConfirmed = table.Column<bool>(type: "INTEGER", nullable: false),
TwoFactorEnabled = table.Column<bool>(type: "INTEGER", nullable: false),
LockoutEnd = table.Column<DateTimeOffset>(type: "TEXT", nullable: true),
LockoutEnabled = table.Column<bool>(type: "INTEGER", nullable: false),
AccessFailedCount = table.Column<int>(type: "INTEGER", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_AspNetUsers", x => x.Id);
});
migrationBuilder.CreateTable(
name: "AspNetRoleClaims",
columns: table => new
{
Id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
RoleId = table.Column<Guid>(type: "TEXT", nullable: false),
ClaimType = table.Column<string>(type: "TEXT", nullable: true),
ClaimValue = table.Column<string>(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<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
UserId = table.Column<Guid>(type: "TEXT", nullable: false),
ClaimType = table.Column<string>(type: "TEXT", nullable: true),
ClaimValue = table.Column<string>(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<string>(type: "TEXT", nullable: false),
ProviderKey = table.Column<string>(type: "TEXT", nullable: false),
ProviderDisplayName = table.Column<string>(type: "TEXT", nullable: true),
UserId = table.Column<Guid>(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<Guid>(type: "TEXT", nullable: false),
RoleId = table.Column<Guid>(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<Guid>(type: "TEXT", nullable: false),
LoginProvider = table.Column<string>(type: "TEXT", nullable: false),
Name = table.Column<string>(type: "TEXT", nullable: false),
Value = table.Column<string>(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");
}
}
}

View File

@ -0,0 +1,408 @@
// <auto-generated />
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<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<string>("Content")
.IsRequired()
.HasColumnType("TEXT");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT");
b.Property<Guid>("CreatedBy")
.HasColumnType("TEXT");
b.Property<Guid>("LinkId")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("CreatedBy");
b.HasIndex("LinkId");
b.ToTable("comments");
});
modelBuilder.Entity("HN.Domain.Link", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT");
b.Property<Guid>("CreatedBy")
.HasColumnType("TEXT");
b.Property<string>("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<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken()
.HasColumnType("TEXT");
b.Property<string>("Name")
.HasMaxLength(256)
.HasColumnType("TEXT");
b.Property<string>("NormalizedName")
.HasMaxLength(256)
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("NormalizedName")
.IsUnique()
.HasDatabaseName("RoleNameIndex");
b.ToTable("AspNetRoles");
});
modelBuilder.Entity("HN.Infrastructure.User", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<int>("AccessFailedCount")
.HasColumnType("INTEGER");
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken()
.HasColumnType("TEXT");
b.Property<string>("Email")
.HasMaxLength(256)
.HasColumnType("TEXT");
b.Property<bool>("EmailConfirmed")
.HasColumnType("INTEGER");
b.Property<bool>("LockoutEnabled")
.HasColumnType("INTEGER");
b.Property<DateTimeOffset?>("LockoutEnd")
.HasColumnType("TEXT");
b.Property<string>("NormalizedEmail")
.HasMaxLength(256)
.HasColumnType("TEXT");
b.Property<string>("NormalizedUserName")
.HasMaxLength(256)
.HasColumnType("TEXT");
b.Property<string>("PasswordHash")
.HasColumnType("TEXT");
b.Property<string>("PhoneNumber")
.HasColumnType("TEXT");
b.Property<bool>("PhoneNumberConfirmed")
.HasColumnType("INTEGER");
b.Property<string>("SecurityStamp")
.HasColumnType("TEXT");
b.Property<bool>("TwoFactorEnabled")
.HasColumnType("INTEGER");
b.Property<string>("UserName")
.HasMaxLength(256)
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("NormalizedEmail")
.HasDatabaseName("EmailIndex");
b.HasIndex("NormalizedUserName")
.IsUnique()
.HasDatabaseName("UserNameIndex");
b.ToTable("AspNetUsers");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<System.Guid>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("ClaimType")
.HasColumnType("TEXT");
b.Property<string>("ClaimValue")
.HasColumnType("TEXT");
b.Property<Guid>("RoleId")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("RoleId");
b.ToTable("AspNetRoleClaims");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<System.Guid>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("ClaimType")
.HasColumnType("TEXT");
b.Property<string>("ClaimValue")
.HasColumnType("TEXT");
b.Property<Guid>("UserId")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("AspNetUserClaims");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<System.Guid>", b =>
{
b.Property<string>("LoginProvider")
.HasColumnType("TEXT");
b.Property<string>("ProviderKey")
.HasColumnType("TEXT");
b.Property<string>("ProviderDisplayName")
.HasColumnType("TEXT");
b.Property<Guid>("UserId")
.HasColumnType("TEXT");
b.HasKey("LoginProvider", "ProviderKey");
b.HasIndex("UserId");
b.ToTable("AspNetUserLogins");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<System.Guid>", b =>
{
b.Property<Guid>("UserId")
.HasColumnType("TEXT");
b.Property<Guid>("RoleId")
.HasColumnType("TEXT");
b.HasKey("UserId", "RoleId");
b.HasIndex("RoleId");
b.ToTable("AspNetUserRoles");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<System.Guid>", b =>
{
b.Property<Guid>("UserId")
.HasColumnType("TEXT");
b.Property<string>("LoginProvider")
.HasColumnType("TEXT");
b.Property<string>("Name")
.HasColumnType("TEXT");
b.Property<string>("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<Guid>("CommentId")
.HasColumnType("TEXT");
b1.Property<Guid>("CreatedBy")
.HasColumnType("TEXT");
b1.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT");
b1.Property<int>("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<Guid>("LinkId")
.HasColumnType("TEXT");
b1.Property<Guid>("CreatedBy")
.HasColumnType("TEXT");
b1.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT");
b1.Property<int>("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<System.Guid>", b =>
{
b.HasOne("HN.Infrastructure.Role", null)
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<System.Guid>", b =>
{
b.HasOne("HN.Infrastructure.User", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<System.Guid>", b =>
{
b.HasOne("HN.Infrastructure.User", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<System.Guid>", 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<System.Guid>", b =>
{
b.HasOne("HN.Infrastructure.User", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
#pragma warning restore 612, 618
}
}
}

View File

@ -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<Guid>(
name: "CreatedBy",
table: "links",
type: "TEXT",
nullable: false,
defaultValue: new Guid("00000000-0000-0000-0000-000000000000"));
migrationBuilder.AddColumn<Guid>(
name: "CreatedBy",
table: "link_votes",
type: "TEXT",
nullable: false,
defaultValue: new Guid("00000000-0000-0000-0000-000000000000"));
migrationBuilder.AddColumn<Guid>(
name: "CreatedBy",
table: "comments",
type: "TEXT",
nullable: false,
defaultValue: new Guid("00000000-0000-0000-0000-000000000000"));
migrationBuilder.AddColumn<Guid>(
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");
}
}
}

View File

@ -29,11 +29,16 @@ namespace Infrastructure.Migrations
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT");
b.Property<Guid>("CreatedBy")
.HasColumnType("TEXT");
b.Property<Guid>("LinkId")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("CreatedBy");
b.HasIndex("LinkId");
b.ToTable("comments");
@ -48,6 +53,9 @@ namespace Infrastructure.Migrations
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT");
b.Property<Guid>("CreatedBy")
.HasColumnType("TEXT");
b.Property<string>("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<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken()
.HasColumnType("TEXT");
b.Property<string>("Name")
.HasMaxLength(256)
.HasColumnType("TEXT");
b.Property<string>("NormalizedName")
.HasMaxLength(256)
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("NormalizedName")
.IsUnique()
.HasDatabaseName("RoleNameIndex");
b.ToTable("AspNetRoles");
});
modelBuilder.Entity("HN.Infrastructure.User", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<int>("AccessFailedCount")
.HasColumnType("INTEGER");
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken()
.HasColumnType("TEXT");
b.Property<string>("Email")
.HasMaxLength(256)
.HasColumnType("TEXT");
b.Property<bool>("EmailConfirmed")
.HasColumnType("INTEGER");
b.Property<bool>("LockoutEnabled")
.HasColumnType("INTEGER");
b.Property<DateTimeOffset?>("LockoutEnd")
.HasColumnType("TEXT");
b.Property<string>("NormalizedEmail")
.HasMaxLength(256)
.HasColumnType("TEXT");
b.Property<string>("NormalizedUserName")
.HasMaxLength(256)
.HasColumnType("TEXT");
b.Property<string>("PasswordHash")
.HasColumnType("TEXT");
b.Property<string>("PhoneNumber")
.HasColumnType("TEXT");
b.Property<bool>("PhoneNumberConfirmed")
.HasColumnType("INTEGER");
b.Property<string>("SecurityStamp")
.HasColumnType("TEXT");
b.Property<bool>("TwoFactorEnabled")
.HasColumnType("INTEGER");
b.Property<string>("UserName")
.HasMaxLength(256)
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("NormalizedEmail")
.HasDatabaseName("EmailIndex");
b.HasIndex("NormalizedUserName")
.IsUnique()
.HasDatabaseName("UserNameIndex");
b.ToTable("AspNetUsers");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<System.Guid>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("ClaimType")
.HasColumnType("TEXT");
b.Property<string>("ClaimValue")
.HasColumnType("TEXT");
b.Property<Guid>("RoleId")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("RoleId");
b.ToTable("AspNetRoleClaims");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<System.Guid>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("ClaimType")
.HasColumnType("TEXT");
b.Property<string>("ClaimValue")
.HasColumnType("TEXT");
b.Property<Guid>("UserId")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("AspNetUserClaims");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<System.Guid>", b =>
{
b.Property<string>("LoginProvider")
.HasColumnType("TEXT");
b.Property<string>("ProviderKey")
.HasColumnType("TEXT");
b.Property<string>("ProviderDisplayName")
.HasColumnType("TEXT");
b.Property<Guid>("UserId")
.HasColumnType("TEXT");
b.HasKey("LoginProvider", "ProviderKey");
b.HasIndex("UserId");
b.ToTable("AspNetUserLogins");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<System.Guid>", b =>
{
b.Property<Guid>("UserId")
.HasColumnType("TEXT");
b.Property<Guid>("RoleId")
.HasColumnType("TEXT");
b.HasKey("UserId", "RoleId");
b.HasIndex("RoleId");
b.ToTable("AspNetUserRoles");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<System.Guid>", b =>
{
b.Property<Guid>("UserId")
.HasColumnType("TEXT");
b.Property<string>("LoginProvider")
.HasColumnType("TEXT");
b.Property<string>("Name")
.HasColumnType("TEXT");
b.Property<string>("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<Guid>("CommentId")
.HasColumnType("TEXT");
b1.Property<Guid>("CreatedBy")
.HasColumnType("TEXT");
b1.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT");
b1.Property<int>("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<Guid>("LinkId")
.HasColumnType("TEXT");
b1.Property<Guid>("CreatedBy")
.HasColumnType("TEXT");
b1.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT");
b1.Property<int>("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<System.Guid>", b =>
{
b.HasOne("HN.Infrastructure.Role", null)
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<System.Guid>", b =>
{
b.HasOne("HN.Infrastructure.User", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<System.Guid>", b =>
{
b.HasOne("HN.Infrastructure.User", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<System.Guid>", 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<System.Guid>", b =>
{
b.HasOne("HN.Infrastructure.User", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
#pragma warning restore 612, 618
}
}

10
Infrastructure/Role.cs Normal file
View File

@ -0,0 +1,10 @@
using System;
using Microsoft.AspNetCore.Identity;
namespace HN.Infrastructure
{
public sealed class Role : IdentityRole<Guid>
{
}
}

14
Infrastructure/User.cs Normal file
View File

@ -0,0 +1,14 @@
using System;
using HN.Application;
using Microsoft.AspNetCore.Identity;
namespace HN.Infrastructure
{
public sealed class User : IdentityUser<Guid>, IUser
{
public User(string userName) : base(userName)
{
}
}
}

View File

@ -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<User>` et un `SignInManager<User>` 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`.