From 641313f97aad43ff5c161085c4769e0c47c2f7c3 Mon Sep 17 00:00:00 2001 From: Oliver Booth Date: Sat, 12 Aug 2023 14:24:27 +0100 Subject: [PATCH] refactor: remove Author schema Introducing new User which serves both as author model and credential model --- OliverBooth/Areas/Blog/Pages/Article.cshtml | 6 +- .../Areas/Blog/Pages/Article.cshtml.cs | 9 +- .../Areas/Blog/Pages/RawArticle.cshtml.cs | 9 +- OliverBooth/Controllers/BlogApiController.cs | 11 ++- OliverBooth/Data/Blog/Author.cs | 75 ----------------- .../Blog/Configuration/AuthorConfiguration.cs | 21 ----- OliverBooth/Data/BlogContext.cs | 14 ++-- OliverBooth/Middleware/RssMiddleware.cs | 8 +- OliverBooth/Program.cs | 1 + OliverBooth/Services/BlogService.cs | 40 +-------- OliverBooth/Services/BlogUserService.cs | 83 +++++++++++++++++++ 11 files changed, 120 insertions(+), 157 deletions(-) delete mode 100644 OliverBooth/Data/Blog/Author.cs delete mode 100644 OliverBooth/Data/Blog/Configuration/AuthorConfiguration.cs create mode 100644 OliverBooth/Services/BlogUserService.cs diff --git a/OliverBooth/Areas/Blog/Pages/Article.cshtml b/OliverBooth/Areas/Blog/Pages/Article.cshtml index 0679a2b..b693710 100644 --- a/OliverBooth/Areas/Blog/Pages/Article.cshtml +++ b/OliverBooth/Areas/Blog/Pages/Article.cshtml @@ -12,7 +12,7 @@ @{ ViewData["Title"] = post.Title; - Author author = Model.Author; + User author = Model.Author; DateTimeOffset published = post.Published; } @@ -27,8 +27,8 @@

@post.Title

- @author.Name - @author.Name • + @author.DisplayName + @author.DisplayName • Published @published.Humanize() diff --git a/OliverBooth/Areas/Blog/Pages/Article.cshtml.cs b/OliverBooth/Areas/Blog/Pages/Article.cshtml.cs index b5c016b..f1e433b 100644 --- a/OliverBooth/Areas/Blog/Pages/Article.cshtml.cs +++ b/OliverBooth/Areas/Blog/Pages/Article.cshtml.cs @@ -12,21 +12,24 @@ namespace OliverBooth.Areas.Blog.Pages; public class Article : PageModel { private readonly BlogService _blogService; + private readonly BlogUserService _blogUserService; ///

/// Initializes a new instance of the class. /// /// The . - public Article(BlogService blogService) + /// The . + public Article(BlogService blogService, BlogUserService blogUserService) { _blogService = blogService; + _blogUserService = blogUserService; } /// /// Gets the author of the post. /// /// The author of the post. - public Author Author { get; private set; } = null!; + public User Author { get; private set; } = null!; /// /// Gets a value indicating whether the post is a legacy WordPress post. @@ -51,7 +54,7 @@ public class Article : PageModel } Post = post; - Author = _blogService.TryGetAuthor(post, out Author? author) ? author : null!; + Author = _blogUserService.TryGetUser(post.AuthorId, out User? author) ? author : null!; return Page(); } } diff --git a/OliverBooth/Areas/Blog/Pages/RawArticle.cshtml.cs b/OliverBooth/Areas/Blog/Pages/RawArticle.cshtml.cs index 9fe6bc7..89dcbb9 100644 --- a/OliverBooth/Areas/Blog/Pages/RawArticle.cshtml.cs +++ b/OliverBooth/Areas/Blog/Pages/RawArticle.cshtml.cs @@ -13,14 +13,17 @@ namespace OliverBooth.Areas.Blog.Pages; public class RawArticle : PageModel { private readonly BlogService _blogService; + private readonly BlogUserService _blogUserService; /// /// Initializes a new instance of the class. /// /// The . - public RawArticle(BlogService blogService) + /// The . + public RawArticle(BlogService blogService, BlogUserService blogUserService) { _blogService = blogService; + _blogUserService = blogUserService; } public IActionResult OnGet(int year, int month, int day, string slug) @@ -34,8 +37,8 @@ public class RawArticle : PageModel using Utf8ValueStringBuilder builder = ZString.CreateUtf8StringBuilder(); builder.AppendLine("# " + post.Title); - if (_blogService.TryGetAuthor(post, out Author? author)) - builder.AppendLine($"Author: {author.Name}"); + if (_blogUserService.TryGetUser(post.AuthorId, out User? author)) + builder.AppendLine($"Author: {author.DisplayName}"); builder.AppendLine($"Published: {post.Published:R}"); if (post.Updated.HasValue) diff --git a/OliverBooth/Controllers/BlogApiController.cs b/OliverBooth/Controllers/BlogApiController.cs index 8e7e921..800eab6 100644 --- a/OliverBooth/Controllers/BlogApiController.cs +++ b/OliverBooth/Controllers/BlogApiController.cs @@ -16,14 +16,17 @@ namespace OliverBooth.Controllers; public sealed class BlogApiController : ControllerBase { private readonly BlogService _blogService; + private readonly BlogUserService _blogUserService; /// /// Initializes a new instance of the class. /// /// The . - public BlogApiController(BlogService blogService) + /// The . + public BlogApiController(BlogService blogService, BlogUserService blogUserService) { _blogService = blogService; + _blogUserService = blogUserService; } [Route("count")] @@ -67,12 +70,12 @@ public sealed class BlogApiController : ControllerBase public IActionResult GetAuthor(Guid id) { if (!ValidateReferer()) return NotFound(); - if (!_blogService.TryGetAuthor(id, out Author? author)) return NotFound(); + if (!_blogUserService.TryGetUser(id, out User? author)) return NotFound(); return Ok(new { id = author.Id, - name = author.Name, + name = author.DisplayName, avatarHash = author.AvatarHash, }); } @@ -80,6 +83,6 @@ public sealed class BlogApiController : ControllerBase private bool ValidateReferer() { var referer = Request.Headers["Referer"].ToString(); - return referer.StartsWith(Url.PageLink("/index",values: new{area="blog"})!); + return referer.StartsWith(Url.PageLink("/index", values: new { area = "blog" })!); } } diff --git a/OliverBooth/Data/Blog/Author.cs b/OliverBooth/Data/Blog/Author.cs deleted file mode 100644 index 318c1ce..0000000 --- a/OliverBooth/Data/Blog/Author.cs +++ /dev/null @@ -1,75 +0,0 @@ -using System.ComponentModel.DataAnnotations.Schema; -using System.Security.Cryptography; -using System.Text; - -namespace OliverBooth.Data.Blog; - -/// -/// Represents an author of a blog post. -/// -public sealed class Author : IEquatable -{ - [NotMapped] - public string AvatarHash - { - get - { - if (EmailAddress is null) - { - return string.Empty; - } - - using var md5 = MD5.Create(); - ReadOnlySpan span = EmailAddress.AsSpan(); - int byteCount = Encoding.UTF8.GetByteCount(span); - Span bytes = stackalloc byte[byteCount]; - Encoding.UTF8.GetBytes(span, bytes); - - Span hash = stackalloc byte[16]; - md5.TryComputeHash(bytes, hash, out _); - - var builder = new StringBuilder(); - foreach (byte b in hash) - { - builder.Append(b.ToString("x2")); - } - - return builder.ToString(); - } - } - - /// - /// Gets or sets the email address of the author. - /// - /// The email address. - public string? EmailAddress { get; set; } - - /// - /// Gets the ID of the author. - /// - /// The ID. - public Guid Id { get; private set; } - - /// - /// Gets or sets the name of the author. - /// - /// The name. - public string Name { get; set; } = string.Empty; - - public bool Equals(Author? other) - { - if (ReferenceEquals(null, other)) return false; - if (ReferenceEquals(this, other)) return true; - return Id == other.Id; - } - - public override bool Equals(object? obj) - { - return ReferenceEquals(this, obj) || obj is Author other && Equals(other); - } - - public override int GetHashCode() - { - return Id.GetHashCode(); - } -} diff --git a/OliverBooth/Data/Blog/Configuration/AuthorConfiguration.cs b/OliverBooth/Data/Blog/Configuration/AuthorConfiguration.cs deleted file mode 100644 index 70bb47c..0000000 --- a/OliverBooth/Data/Blog/Configuration/AuthorConfiguration.cs +++ /dev/null @@ -1,21 +0,0 @@ -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Metadata.Builders; - -namespace OliverBooth.Data.Blog.Configuration; - -/// -/// Represents the configuration for the entity. -/// -internal sealed class AuthorConfiguration : IEntityTypeConfiguration -{ - /// - public void Configure(EntityTypeBuilder builder) - { - builder.ToTable("Author"); - builder.HasKey(e => e.Id); - - builder.Property(e => e.Id); - builder.Property(e => e.Name).HasMaxLength(100).IsRequired(); - builder.Property(e => e.EmailAddress).HasMaxLength(255).IsRequired(false); - } -} diff --git a/OliverBooth/Data/BlogContext.cs b/OliverBooth/Data/BlogContext.cs index c282b55..f5af0e0 100644 --- a/OliverBooth/Data/BlogContext.cs +++ b/OliverBooth/Data/BlogContext.cs @@ -20,18 +20,18 @@ public sealed class BlogContext : DbContext _configuration = configuration; } - /// - /// Gets the set of authors. - /// - /// The set of authors. - public DbSet Authors { get; internal set; } = null!; - /// /// Gets the set of blog posts. /// /// The set of blog posts. public DbSet BlogPosts { get; internal set; } = null!; + /// + /// Gets the set of users. + /// + /// The set of users. + public DbSet Users { get; internal set; } = null!; + /// protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { @@ -43,7 +43,7 @@ public sealed class BlogContext : DbContext /// protected override void OnModelCreating(ModelBuilder modelBuilder) { - modelBuilder.ApplyConfiguration(new AuthorConfiguration()); modelBuilder.ApplyConfiguration(new BlogPostConfiguration()); + modelBuilder.ApplyConfiguration(new UserConfiguration()); } } diff --git a/OliverBooth/Middleware/RssMiddleware.cs b/OliverBooth/Middleware/RssMiddleware.cs index d21792f..9e6ac6b 100644 --- a/OliverBooth/Middleware/RssMiddleware.cs +++ b/OliverBooth/Middleware/RssMiddleware.cs @@ -12,6 +12,7 @@ namespace OliverBooth.Middleware; internal sealed class RssMiddleware { private readonly BlogService _blogService; + private readonly BlogUserService _userService; private readonly ConfigurationService _configurationService; /// @@ -19,12 +20,15 @@ internal sealed class RssMiddleware /// /// The request delegate. /// The blog service. + /// The user service. /// The configuration service. public RssMiddleware(RequestDelegate _, BlogService blogService, + BlogUserService userService, ConfigurationService configurationService) { _blogService = blogService; + _userService = userService; _configurationService = configurationService; } @@ -42,14 +46,14 @@ internal sealed class RssMiddleware string excerpt = _blogService.GetExcerpt(blogPost, out _); var description = $"{excerpt}

Read more...

"; - _blogService.TryGetAuthor(blogPost, out Author? author); + _userService.TryGetUser(blogPost.AuthorId, out User? author); var item = new BlogItem { Title = blogPost.Title, Link = url, Comments = $"{url}#disqus_thread", - Creator = author?.Name ?? string.Empty, + Creator = author?.DisplayName ?? string.Empty, PubDate = blogPost.Published.ToString("R"), Guid = $"{baseUrl}?pid={blogPost.Id}", Description = description diff --git a/OliverBooth/Program.cs b/OliverBooth/Program.cs index 53f30b4..4f79459 100644 --- a/OliverBooth/Program.cs +++ b/OliverBooth/Program.cs @@ -18,6 +18,7 @@ builder.Logging.AddNLog(); builder.Services.AddHostedSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); +builder.Services.AddSingleton(); builder.Services.AddSingleton(provider => new MarkdownPipelineBuilder() .Use() diff --git a/OliverBooth/Services/BlogService.cs b/OliverBooth/Services/BlogService.cs index 969ff7c..1c2e71c 100644 --- a/OliverBooth/Services/BlogService.cs +++ b/OliverBooth/Services/BlogService.cs @@ -64,44 +64,6 @@ public sealed class BlogService return RenderContent(result).Trim(); } - /// - /// Attempts to find the author by ID. - /// - /// The ID of the author. - /// - /// When this method returns, contains the associated with the specified ID, if the author - /// is found; otherwise, . - /// - /// if the author is found; otherwise, . - /// is . - public bool TryGetAuthor(Guid id, [NotNullWhen(true)] out Author? author) - { - using BlogContext context = _dbContextFactory.CreateDbContext(); - author = context.Authors.FirstOrDefault(a => a.Id == id); - - return author is not null; - } - - /// - /// Attempts to find the author of a blog post. - /// - /// The blog post. - /// - /// When this method returns, contains the associated with the specified blog post, if the - /// author is found; otherwise, . - /// - /// if the author is found; otherwise, . - /// is . - public bool TryGetAuthor(BlogPost post, [NotNullWhen(true)] out Author? author) - { - if (post is null) throw new ArgumentNullException(nameof(post)); - - using BlogContext context = _dbContextFactory.CreateDbContext(); - author = context.Authors.FirstOrDefault(a => a.Id == post.AuthorId); - - return author is not null; - } - /// /// Attempts to find a blog post by its publication date and slug. /// @@ -158,7 +120,7 @@ public sealed class BlogService post = context.BlogPosts.FirstOrDefault(p => p.WordPressId == postId); return post is not null; } - + private string RenderContent(string content) { content = content.Replace("", string.Empty); diff --git a/OliverBooth/Services/BlogUserService.cs b/OliverBooth/Services/BlogUserService.cs new file mode 100644 index 0000000..ea724ee --- /dev/null +++ b/OliverBooth/Services/BlogUserService.cs @@ -0,0 +1,83 @@ +using System.Diagnostics.CodeAnalysis; +using Microsoft.EntityFrameworkCore; +using OliverBooth.Data; +using OliverBooth.Data.Blog; + +namespace OliverBooth.Services; + +/// +/// Represents a service for managing blog users. +/// +public sealed class BlogUserService +{ + private readonly IDbContextFactory _dbContextFactory; + + /// + /// Initializes a new instance of the class. + /// + /// The database context factory. + public BlogUserService(IDbContextFactory dbContextFactory) + { + _dbContextFactory = dbContextFactory; + } + + /// + /// Attempts to authenticate the user with the specified email address and password. + /// + /// The email address. + /// The password. + /// + /// When this method returns, contains the user with the specified email address and password, if the user + /// exists; otherwise, . + /// + /// + /// if the authentication was successful; otherwise, . + /// + public bool TryAuthenticateUser(string? emailAddress, string? password, [NotNullWhen(true)] out User? user) + { + if (string.IsNullOrWhiteSpace(emailAddress) || string.IsNullOrWhiteSpace(password)) + { + user = null; + return false; + } + + using BlogContext context = _dbContextFactory.CreateDbContext(); + user = context.Users.FirstOrDefault(u => u.EmailAddress == emailAddress); + if (user is null) + { + return false; + } + + string hashedPassword = BC.HashPassword(password, user.Salt); + return hashedPassword == user.Password; + } + + /// + /// Attempts to retrieve the user with the specified user ID. + /// + /// The user ID. + /// + /// When this method returns, contains the user with the specified user ID, if the user exists; otherwise, + /// . + /// + /// if the user exists; otherwise, . + public bool TryGetUser(Guid userId, [NotNullWhen(true)] out User? user) + { + using BlogContext context = _dbContextFactory.CreateDbContext(); + user = context.Users.FirstOrDefault(u => u.Id == userId); + return user is not null; + } + + /// + /// Returns a value indicating whether the specified user requires a password reset. + /// + /// The user. + /// + /// if the specified user requires a password reset; otherwise, + /// . + /// + public bool UserRequiresPasswordReset(User user) + { + return string.IsNullOrEmpty(user.Password) || string.IsNullOrEmpty(user.Salt); + } +}