Compare commits
4 Commits
9d0e16abc1
...
641313f97a
Author | SHA1 | Date | |
---|---|---|---|
641313f97a | |||
47b648f327 | |||
6f7fa67135 | |||
034bd66b29 |
@ -12,7 +12,7 @@
|
|||||||
|
|
||||||
@{
|
@{
|
||||||
ViewData["Title"] = post.Title;
|
ViewData["Title"] = post.Title;
|
||||||
Author author = Model.Author;
|
User author = Model.Author;
|
||||||
DateTimeOffset published = post.Published;
|
DateTimeOffset published = post.Published;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -27,8 +27,8 @@
|
|||||||
|
|
||||||
<h1>@post.Title</h1>
|
<h1>@post.Title</h1>
|
||||||
<p class="text-muted">
|
<p class="text-muted">
|
||||||
<img class="blog-author-icon" src="https://gravatar.com/avatar/@author.AvatarHash?s=28" alt="@author.Name">
|
<img class="blog-author-icon" src="https://gravatar.com/avatar/@author.AvatarHash?s=28" alt="@author.DisplayName">
|
||||||
@author.Name •
|
@author.DisplayName •
|
||||||
|
|
||||||
<abbr data-bs-toggle="tooltip" data-bs-title="@published.ToString("dddd, d MMMM yyyy HH:mm")">
|
<abbr data-bs-toggle="tooltip" data-bs-title="@published.ToString("dddd, d MMMM yyyy HH:mm")">
|
||||||
Published @published.Humanize()
|
Published @published.Humanize()
|
||||||
|
@ -12,21 +12,24 @@ namespace OliverBooth.Areas.Blog.Pages;
|
|||||||
public class Article : PageModel
|
public class Article : PageModel
|
||||||
{
|
{
|
||||||
private readonly BlogService _blogService;
|
private readonly BlogService _blogService;
|
||||||
|
private readonly BlogUserService _blogUserService;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="Article" /> class.
|
/// Initializes a new instance of the <see cref="Article" /> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="blogService">The <see cref="BlogService" />.</param>
|
/// <param name="blogService">The <see cref="BlogService" />.</param>
|
||||||
public Article(BlogService blogService)
|
/// <param name="blogUserService">The <see cref="BlogUserService" />.</param>
|
||||||
|
public Article(BlogService blogService, BlogUserService blogUserService)
|
||||||
{
|
{
|
||||||
_blogService = blogService;
|
_blogService = blogService;
|
||||||
|
_blogUserService = blogUserService;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the author of the post.
|
/// Gets the author of the post.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <value>The author of the post.</value>
|
/// <value>The author of the post.</value>
|
||||||
public Author Author { get; private set; } = null!;
|
public User Author { get; private set; } = null!;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets a value indicating whether the post is a legacy WordPress post.
|
/// Gets a value indicating whether the post is a legacy WordPress post.
|
||||||
@ -51,7 +54,7 @@ public class Article : PageModel
|
|||||||
}
|
}
|
||||||
|
|
||||||
Post = post;
|
Post = post;
|
||||||
Author = _blogService.TryGetAuthor(post, out Author? author) ? author : null!;
|
Author = _blogUserService.TryGetUser(post.AuthorId, out User? author) ? author : null!;
|
||||||
return Page();
|
return Page();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -13,14 +13,17 @@ namespace OliverBooth.Areas.Blog.Pages;
|
|||||||
public class RawArticle : PageModel
|
public class RawArticle : PageModel
|
||||||
{
|
{
|
||||||
private readonly BlogService _blogService;
|
private readonly BlogService _blogService;
|
||||||
|
private readonly BlogUserService _blogUserService;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="RawArticle" /> class.
|
/// Initializes a new instance of the <see cref="RawArticle" /> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="blogService">The <see cref="BlogService" />.</param>
|
/// <param name="blogService">The <see cref="BlogService" />.</param>
|
||||||
public RawArticle(BlogService blogService)
|
/// <param name="blogUserService">The <see cref="BlogUserService" />.</param>
|
||||||
|
public RawArticle(BlogService blogService, BlogUserService blogUserService)
|
||||||
{
|
{
|
||||||
_blogService = blogService;
|
_blogService = blogService;
|
||||||
|
_blogUserService = blogUserService;
|
||||||
}
|
}
|
||||||
|
|
||||||
public IActionResult OnGet(int year, int month, int day, string slug)
|
public IActionResult OnGet(int year, int month, int day, string slug)
|
||||||
@ -34,8 +37,8 @@ public class RawArticle : PageModel
|
|||||||
|
|
||||||
using Utf8ValueStringBuilder builder = ZString.CreateUtf8StringBuilder();
|
using Utf8ValueStringBuilder builder = ZString.CreateUtf8StringBuilder();
|
||||||
builder.AppendLine("# " + post.Title);
|
builder.AppendLine("# " + post.Title);
|
||||||
if (_blogService.TryGetAuthor(post, out Author? author))
|
if (_blogUserService.TryGetUser(post.AuthorId, out User? author))
|
||||||
builder.AppendLine($"Author: {author.Name}");
|
builder.AppendLine($"Author: {author.DisplayName}");
|
||||||
|
|
||||||
builder.AppendLine($"Published: {post.Published:R}");
|
builder.AppendLine($"Published: {post.Published:R}");
|
||||||
if (post.Updated.HasValue)
|
if (post.Updated.HasValue)
|
||||||
|
@ -16,14 +16,17 @@ namespace OliverBooth.Controllers;
|
|||||||
public sealed class BlogApiController : ControllerBase
|
public sealed class BlogApiController : ControllerBase
|
||||||
{
|
{
|
||||||
private readonly BlogService _blogService;
|
private readonly BlogService _blogService;
|
||||||
|
private readonly BlogUserService _blogUserService;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="BlogApiController" /> class.
|
/// Initializes a new instance of the <see cref="BlogApiController" /> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="blogService">The <see cref="BlogService" />.</param>
|
/// <param name="blogService">The <see cref="BlogService" />.</param>
|
||||||
public BlogApiController(BlogService blogService)
|
/// <param name="blogUserService">The <see cref="BlogUserService" />.</param>
|
||||||
|
public BlogApiController(BlogService blogService, BlogUserService blogUserService)
|
||||||
{
|
{
|
||||||
_blogService = blogService;
|
_blogService = blogService;
|
||||||
|
_blogUserService = blogUserService;
|
||||||
}
|
}
|
||||||
|
|
||||||
[Route("count")]
|
[Route("count")]
|
||||||
@ -67,12 +70,12 @@ public sealed class BlogApiController : ControllerBase
|
|||||||
public IActionResult GetAuthor(Guid id)
|
public IActionResult GetAuthor(Guid id)
|
||||||
{
|
{
|
||||||
if (!ValidateReferer()) return NotFound();
|
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
|
return Ok(new
|
||||||
{
|
{
|
||||||
id = author.Id,
|
id = author.Id,
|
||||||
name = author.Name,
|
name = author.DisplayName,
|
||||||
avatarHash = author.AvatarHash,
|
avatarHash = author.AvatarHash,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -1,75 +0,0 @@
|
|||||||
using System.ComponentModel.DataAnnotations.Schema;
|
|
||||||
using System.Security.Cryptography;
|
|
||||||
using System.Text;
|
|
||||||
|
|
||||||
namespace OliverBooth.Data.Blog;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Represents an author of a blog post.
|
|
||||||
/// </summary>
|
|
||||||
public sealed class Author : IEquatable<Author>
|
|
||||||
{
|
|
||||||
[NotMapped]
|
|
||||||
public string AvatarHash
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
if (EmailAddress is null)
|
|
||||||
{
|
|
||||||
return string.Empty;
|
|
||||||
}
|
|
||||||
|
|
||||||
using var md5 = MD5.Create();
|
|
||||||
ReadOnlySpan<char> span = EmailAddress.AsSpan();
|
|
||||||
int byteCount = Encoding.UTF8.GetByteCount(span);
|
|
||||||
Span<byte> bytes = stackalloc byte[byteCount];
|
|
||||||
Encoding.UTF8.GetBytes(span, bytes);
|
|
||||||
|
|
||||||
Span<byte> 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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the email address of the author.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>The email address.</value>
|
|
||||||
public string? EmailAddress { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the ID of the author.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>The ID.</value>
|
|
||||||
public Guid Id { get; private set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the name of the author.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>The name.</value>
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,21 +0,0 @@
|
|||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
|
||||||
|
|
||||||
namespace OliverBooth.Data.Blog.Configuration;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Represents the configuration for the <see cref="Author" /> entity.
|
|
||||||
/// </summary>
|
|
||||||
internal sealed class AuthorConfiguration : IEntityTypeConfiguration<Author>
|
|
||||||
{
|
|
||||||
/// <inheritdoc />
|
|
||||||
public void Configure(EntityTypeBuilder<Author> 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);
|
|
||||||
}
|
|
||||||
}
|
|
@ -20,18 +20,18 @@ public sealed class BlogContext : DbContext
|
|||||||
_configuration = configuration;
|
_configuration = configuration;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the set of authors.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>The set of authors.</value>
|
|
||||||
public DbSet<Author> Authors { get; internal set; } = null!;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the set of blog posts.
|
/// Gets the set of blog posts.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <value>The set of blog posts.</value>
|
/// <value>The set of blog posts.</value>
|
||||||
public DbSet<BlogPost> BlogPosts { get; internal set; } = null!;
|
public DbSet<BlogPost> BlogPosts { get; internal set; } = null!;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the set of users.
|
||||||
|
/// </summary>
|
||||||
|
/// <value>The set of users.</value>
|
||||||
|
public DbSet<User> Users { get; internal set; } = null!;
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
|
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
|
||||||
{
|
{
|
||||||
@ -43,7 +43,7 @@ public sealed class BlogContext : DbContext
|
|||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||||
{
|
{
|
||||||
modelBuilder.ApplyConfiguration(new AuthorConfiguration());
|
|
||||||
modelBuilder.ApplyConfiguration(new BlogPostConfiguration());
|
modelBuilder.ApplyConfiguration(new BlogPostConfiguration());
|
||||||
|
modelBuilder.ApplyConfiguration(new UserConfiguration());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using SmartFormat.Core.Extensions;
|
using SmartFormat.Core.Extensions;
|
||||||
|
|
||||||
namespace OliverBooth;
|
namespace OliverBooth.Formatting;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Represents a SmartFormat formatter that formats a date.
|
/// Represents a SmartFormat formatter that formats a date.
|
38
OliverBooth/Formatting/MarkdownFormatter.cs
Normal file
38
OliverBooth/Formatting/MarkdownFormatter.cs
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
using Markdig;
|
||||||
|
using SmartFormat.Core.Extensions;
|
||||||
|
|
||||||
|
namespace OliverBooth.Formatting;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a SmartFormat formatter that formats markdown.
|
||||||
|
/// </summary>
|
||||||
|
internal sealed class MarkdownFormatter : IFormatter
|
||||||
|
{
|
||||||
|
private readonly IServiceProvider _serviceProvider;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="MarkdownFormatter" /> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="serviceProvider">The <see cref="IServiceProvider" />.</param>
|
||||||
|
public MarkdownFormatter(IServiceProvider serviceProvider)
|
||||||
|
{
|
||||||
|
_serviceProvider = serviceProvider;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public bool CanAutoDetect { get; set; } = true;
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public string Name { get; set; } = "markdown";
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public bool TryEvaluateFormat(IFormattingInfo formattingInfo)
|
||||||
|
{
|
||||||
|
if (formattingInfo.CurrentValue is not string value)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
var pipeline = _serviceProvider.GetService<MarkdownPipeline>();
|
||||||
|
formattingInfo.Write(Markdig.Markdown.ToHtml(value, pipeline));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
@ -12,6 +12,7 @@ namespace OliverBooth.Middleware;
|
|||||||
internal sealed class RssMiddleware
|
internal sealed class RssMiddleware
|
||||||
{
|
{
|
||||||
private readonly BlogService _blogService;
|
private readonly BlogService _blogService;
|
||||||
|
private readonly BlogUserService _userService;
|
||||||
private readonly ConfigurationService _configurationService;
|
private readonly ConfigurationService _configurationService;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -19,12 +20,15 @@ internal sealed class RssMiddleware
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="_">The request delegate.</param>
|
/// <param name="_">The request delegate.</param>
|
||||||
/// <param name="blogService">The blog service.</param>
|
/// <param name="blogService">The blog service.</param>
|
||||||
|
/// <param name="userService">The user service.</param>
|
||||||
/// <param name="configurationService">The configuration service.</param>
|
/// <param name="configurationService">The configuration service.</param>
|
||||||
public RssMiddleware(RequestDelegate _,
|
public RssMiddleware(RequestDelegate _,
|
||||||
BlogService blogService,
|
BlogService blogService,
|
||||||
|
BlogUserService userService,
|
||||||
ConfigurationService configurationService)
|
ConfigurationService configurationService)
|
||||||
{
|
{
|
||||||
_blogService = blogService;
|
_blogService = blogService;
|
||||||
|
_userService = userService;
|
||||||
_configurationService = configurationService;
|
_configurationService = configurationService;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -42,14 +46,14 @@ internal sealed class RssMiddleware
|
|||||||
string excerpt = _blogService.GetExcerpt(blogPost, out _);
|
string excerpt = _blogService.GetExcerpt(blogPost, out _);
|
||||||
var description = $"{excerpt}<p><a href=\"{url}\">Read more...</a></p>";
|
var description = $"{excerpt}<p><a href=\"{url}\">Read more...</a></p>";
|
||||||
|
|
||||||
_blogService.TryGetAuthor(blogPost, out Author? author);
|
_userService.TryGetUser(blogPost.AuthorId, out User? author);
|
||||||
|
|
||||||
var item = new BlogItem
|
var item = new BlogItem
|
||||||
{
|
{
|
||||||
Title = blogPost.Title,
|
Title = blogPost.Title,
|
||||||
Link = url,
|
Link = url,
|
||||||
Comments = $"{url}#disqus_thread",
|
Comments = $"{url}#disqus_thread",
|
||||||
Creator = author?.Name ?? string.Empty,
|
Creator = author?.DisplayName ?? string.Empty,
|
||||||
PubDate = blogPost.Published.ToString("R"),
|
PubDate = blogPost.Published.ToString("R"),
|
||||||
Guid = $"{baseUrl}?pid={blogPost.Id}",
|
Guid = $"{baseUrl}?pid={blogPost.Id}",
|
||||||
Description = description
|
Description = description
|
||||||
|
@ -4,7 +4,6 @@ using Markdig;
|
|||||||
using NLog;
|
using NLog;
|
||||||
using NLog.Extensions.Logging;
|
using NLog.Extensions.Logging;
|
||||||
using OliverBooth.Data;
|
using OliverBooth.Data;
|
||||||
using OliverBooth.Markdown;
|
|
||||||
using OliverBooth.Markdown.Template;
|
using OliverBooth.Markdown.Template;
|
||||||
using OliverBooth.Markdown.Timestamp;
|
using OliverBooth.Markdown.Timestamp;
|
||||||
using OliverBooth.Middleware;
|
using OliverBooth.Middleware;
|
||||||
@ -19,6 +18,7 @@ builder.Logging.AddNLog();
|
|||||||
builder.Services.AddHostedSingleton<LoggingService>();
|
builder.Services.AddHostedSingleton<LoggingService>();
|
||||||
builder.Services.AddSingleton<ConfigurationService>();
|
builder.Services.AddSingleton<ConfigurationService>();
|
||||||
builder.Services.AddSingleton<TemplateService>();
|
builder.Services.AddSingleton<TemplateService>();
|
||||||
|
builder.Services.AddSingleton<BlogUserService>();
|
||||||
|
|
||||||
builder.Services.AddSingleton(provider => new MarkdownPipelineBuilder()
|
builder.Services.AddSingleton(provider => new MarkdownPipelineBuilder()
|
||||||
.Use<TimestampExtension>()
|
.Use<TimestampExtension>()
|
||||||
|
@ -64,44 +64,6 @@ public sealed class BlogService
|
|||||||
return RenderContent(result).Trim();
|
return RenderContent(result).Trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Attempts to find the author by ID.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="id">The ID of the author.</param>
|
|
||||||
/// <param name="author">
|
|
||||||
/// When this method returns, contains the <see cref="Author" /> associated with the specified ID, if the author
|
|
||||||
/// is found; otherwise, <see langword="null" />.
|
|
||||||
/// </param>
|
|
||||||
/// <returns><see langword="true" /> if the author is found; otherwise, <see langword="false" />.</returns>
|
|
||||||
/// <exception cref="ArgumentNullException"><paramref name="post" /> is <see langword="null" />.</exception>
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Attempts to find the author of a blog post.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="post">The blog post.</param>
|
|
||||||
/// <param name="author">
|
|
||||||
/// When this method returns, contains the <see cref="Author" /> associated with the specified blog post, if the
|
|
||||||
/// author is found; otherwise, <see langword="null" />.
|
|
||||||
/// </param>
|
|
||||||
/// <returns><see langword="true" /> if the author is found; otherwise, <see langword="false" />.</returns>
|
|
||||||
/// <exception cref="ArgumentNullException"><paramref name="post" /> is <see langword="null" />.</exception>
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Attempts to find a blog post by its publication date and slug.
|
/// Attempts to find a blog post by its publication date and slug.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
83
OliverBooth/Services/BlogUserService.cs
Normal file
83
OliverBooth/Services/BlogUserService.cs
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using OliverBooth.Data;
|
||||||
|
using OliverBooth.Data.Blog;
|
||||||
|
|
||||||
|
namespace OliverBooth.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a service for managing blog users.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class BlogUserService
|
||||||
|
{
|
||||||
|
private readonly IDbContextFactory<BlogContext> _dbContextFactory;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="BlogUserService" /> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="dbContextFactory">The database context factory.</param>
|
||||||
|
public BlogUserService(IDbContextFactory<BlogContext> dbContextFactory)
|
||||||
|
{
|
||||||
|
_dbContextFactory = dbContextFactory;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Attempts to authenticate the user with the specified email address and password.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="emailAddress">The email address.</param>
|
||||||
|
/// <param name="password">The password.</param>
|
||||||
|
/// <param name="user">
|
||||||
|
/// When this method returns, contains the user with the specified email address and password, if the user
|
||||||
|
/// exists; otherwise, <see langword="null" />.
|
||||||
|
/// </param>
|
||||||
|
/// <returns>
|
||||||
|
/// <see langword="true" /> if the authentication was successful; otherwise, <see langword="false" />.
|
||||||
|
/// </returns>
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Attempts to retrieve the user with the specified user ID.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="userId">The user ID.</param>
|
||||||
|
/// <param name="user">
|
||||||
|
/// When this method returns, contains the user with the specified user ID, if the user exists; otherwise,
|
||||||
|
/// <see langword="null" />.
|
||||||
|
/// </param>
|
||||||
|
/// <returns><see langword="true" /> if the user exists; otherwise, <see langword="false" />.</returns>
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns a value indicating whether the specified user requires a password reset.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="user">The user.</param>
|
||||||
|
/// <returns>
|
||||||
|
/// <see langword="true" /> if the specified user requires a password reset; otherwise,
|
||||||
|
/// <see langword="false" />.
|
||||||
|
/// </returns>
|
||||||
|
public bool UserRequiresPasswordReset(User user)
|
||||||
|
{
|
||||||
|
return string.IsNullOrEmpty(user.Password) || string.IsNullOrEmpty(user.Salt);
|
||||||
|
}
|
||||||
|
}
|
@ -1,7 +1,10 @@
|
|||||||
|
using System.Buffers.Binary;
|
||||||
|
using Markdig;
|
||||||
|
using Markdig.Syntax;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using OliverBooth.Data;
|
using OliverBooth.Data;
|
||||||
using OliverBooth.Data.Web;
|
using OliverBooth.Data.Web;
|
||||||
using OliverBooth.Markdown;
|
using OliverBooth.Formatting;
|
||||||
using OliverBooth.Markdown.Template;
|
using OliverBooth.Markdown.Template;
|
||||||
using SmartFormat;
|
using SmartFormat;
|
||||||
using SmartFormat.Extensions;
|
using SmartFormat.Extensions;
|
||||||
@ -13,20 +16,25 @@ namespace OliverBooth.Services;
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class TemplateService
|
public sealed class TemplateService
|
||||||
{
|
{
|
||||||
|
private static readonly Random Random = new();
|
||||||
|
private readonly IServiceProvider _serviceProvider;
|
||||||
private readonly IDbContextFactory<WebContext> _webContextFactory;
|
private readonly IDbContextFactory<WebContext> _webContextFactory;
|
||||||
private readonly SmartFormatter _formatter;
|
private readonly SmartFormatter _formatter;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="TemplateService" /> class.
|
/// Initializes a new instance of the <see cref="TemplateService" /> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="serviceProvider">The <see cref="IServiceProvider" />.</param>
|
||||||
/// <param name="webContextFactory">The <see cref="WebContext" /> factory.</param>
|
/// <param name="webContextFactory">The <see cref="WebContext" /> factory.</param>
|
||||||
public TemplateService(IDbContextFactory<WebContext> webContextFactory)
|
public TemplateService(IServiceProvider serviceProvider, IDbContextFactory<WebContext> webContextFactory)
|
||||||
{
|
{
|
||||||
_formatter = Smart.CreateDefaultSmartFormat();
|
_formatter = Smart.CreateDefaultSmartFormat();
|
||||||
_formatter.AddExtensions(new DefaultSource());
|
_formatter.AddExtensions(new DefaultSource());
|
||||||
_formatter.AddExtensions(new ReflectionSource());
|
_formatter.AddExtensions(new ReflectionSource());
|
||||||
_formatter.AddExtensions(new DateFormatter());
|
_formatter.AddExtensions(new DateFormatter());
|
||||||
|
_formatter.AddExtensions(new MarkdownFormatter(serviceProvider));
|
||||||
|
|
||||||
|
_serviceProvider = serviceProvider;
|
||||||
_webContextFactory = webContextFactory;
|
_webContextFactory = webContextFactory;
|
||||||
Current = this;
|
Current = this;
|
||||||
}
|
}
|
||||||
@ -44,6 +52,7 @@ public sealed class TemplateService
|
|||||||
public string RenderTemplate(TemplateInline templateInline)
|
public string RenderTemplate(TemplateInline templateInline)
|
||||||
{
|
{
|
||||||
if (templateInline is null) throw new ArgumentNullException(nameof(templateInline));
|
if (templateInline is null) throw new ArgumentNullException(nameof(templateInline));
|
||||||
|
|
||||||
using WebContext webContext = _webContextFactory.CreateDbContext();
|
using WebContext webContext = _webContextFactory.CreateDbContext();
|
||||||
ArticleTemplate? template = webContext.ArticleTemplates.Find(templateInline.Name);
|
ArticleTemplate? template = webContext.ArticleTemplates.Find(templateInline.Name);
|
||||||
if (template is null)
|
if (template is null)
|
||||||
@ -51,16 +60,21 @@ public sealed class TemplateService
|
|||||||
return $"{{{{{templateInline.Name}}}}}";
|
return $"{{{{{templateInline.Name}}}}}";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Span<byte> randomBytes = stackalloc byte[20];
|
||||||
|
Random.NextBytes(randomBytes);
|
||||||
|
|
||||||
var formatted = new
|
var formatted = new
|
||||||
{
|
{
|
||||||
templateInline.ArgumentList,
|
templateInline.ArgumentList,
|
||||||
templateInline.ArgumentString,
|
templateInline.ArgumentString,
|
||||||
templateInline.Params,
|
templateInline.Params,
|
||||||
|
RandomInt = BinaryPrimitives.ReadInt32LittleEndian(randomBytes[..4]),
|
||||||
|
RandomGuid = new Guid(randomBytes[4..]).ToString("N"),
|
||||||
};
|
};
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
return Markdig.Markdown.ToHtml(_formatter.Format(template.FormatString, formatted));
|
return _formatter.Format(template.FormatString, formatted);
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
|
Loading…
Reference in New Issue
Block a user