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.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);
+ }
+}